Tree-sitter Parsing

The treesitter module parses source code into concrete syntax trees with Tree-sitter through the go-tree-sitter bindings.

This page is an API reference with partial parsing recipes. Source strings and query patterns are application input, and node-level snippets assume a live tree from an earlier checked parse. Parsers, trees, queries, and cursors are owned resources: close every successfully created handle when its last dependent operation is complete.

The resulting syntax trees:

  • Represent the full structure of source code
  • Update incrementally as code changes
  • Are robust to syntax errors (partial parsing)
  • Support pattern-based queries using S-expressions

Loading

local treesitter = require("treesitter")
The `treesitter` module is optional and is present only in builds that include the `treesitter` build tag. Official Wippy binaries include it. Source builds can use `make build-wippy` or `go build -tags treesitter`; without the tag, `require("treesitter")` is unavailable.

Supported Languages

Language Aliases Root Node
Go go, golang source_file
JavaScript js, javascript program
TypeScript ts, typescript program
TSX tsx program
Python python, py module
Lua lua chunk
PHP php program
C# csharp, cs, c# compilation_unit
HTML html, html5 document
Markdown markdown, md document
SQL sql -
local langs = treesitter.supported_languages()
-- {go = true, javascript = true, python = true, ...}

Quick Start

Parse Code

local code = [[
func hello() {
    return "Hello!"
}
]]

local tree, err = treesitter.parse("go", code)
if err then
    return nil, err
end

local root, root_err = tree:root_node()
if root_err then
    tree:close()
    return nil, root_err
end
print(root:kind())        -- "source_file"
print(root:child_count()) -- number of top-level declarations
tree:close()

Query Syntax Tree

local code = [[
func hello() {}
func world() {}
]]

local tree, parse_err = treesitter.parse("go", code)
if parse_err then
    return nil, parse_err
end
local root, root_err = tree:root_node()
if root_err then
    tree:close()
    return nil, root_err
end

-- Find all function names
local query, query_err = treesitter.query("go", [[
    (function_declaration name: (identifier) @func_name)
]])
if query_err then
    tree:close()
    return nil, query_err
end

local captures, captures_err = query:captures(root, code)
if captures_err then
    query:close()
    tree:close()
    return nil, captures_err
end
for _, capture in ipairs(captures) do
    print(capture.name, capture.text)
end
-- "func_name"  "hello"
-- "func_name"  "world"
query:close()
tree:close()

Parsing

Simple Parse

Parse source code with a temporary internal parser.

local tree, err = treesitter.parse("go", code)
if err then
    return nil, err
end
-- Use the tree, then call tree:close().
Parameter Type Description
language string Language name or alias
code string Source code

Returns: Tree, error

Reusable Parser

Create a reusable parser for repeated parsing or incremental updates.

local parser, parser_err = treesitter.parser()
if parser_err then
    return nil, parser_err
end
local _, language_err = parser:set_language("go")
if language_err then
    parser:close()
    return nil, language_err
end

local tree1, first_err = parser:parse("package main")
if first_err then
    parser:close()
    return nil, first_err
end

-- Parse another source with the reusable parser. For an incremental update,
-- edit the old tree first as shown in the complete recipe below.
local tree2, second_err = parser:parse("package main\nfunc foo() {}")
if second_err then
    tree1:close()
    parser:close()
    return nil, second_err
end

tree2:close()
tree1:close()
parser:close()

Returns: Parser, error

Parser Methods

Method Description
set_language(lang) Set parser language, returns boolean, error
get_language() Get current language name
parse(code, old_tree?) Parse code, optionally with old tree for incremental parsing
set_timeout(duration) Set parse timeout (string like "1s" or nanoseconds)
set_ranges(ranges) Set byte ranges to parse
reset() Reset parser state
close() Release parser resources

Trees created by a reusable parser and the parser itself are independently owned; close each successful handle. Nodes borrow their tree's storage and must not be used after that tree is closed. Cursors borrow a tree as well, so close them before the tree. Queries own separate native resources and also require close(); close a query after its captures or matches are no longer needed. Explicit cleanup is deterministic, while the process resource store is only a fallback for handles left open at process teardown.

Syntax Trees

Get Root Node

local tree, err = treesitter.parse("go", "package main")
if err then
    return nil, err
end
local root, root_err = tree:root_node()
if root_err then
    tree:close()
    return nil, root_err
end

print(root:kind())  -- "source_file"
local source_text, text_err = root:text()
if text_err then
    tree:close()
    return nil, text_err
end
print(source_text)  -- "package main"
tree:close()

Tree Methods

Method Description
root_node() Get root node of tree
root_node_with_offset(bytes, point) Get root with offset applied
language() Get tree's language object
copy() Create deep copy of tree
walk() Create cursor for traversal
edit(edit_table) Apply incremental edit
changed_ranges(other_tree) Get ranges that changed
included_ranges() Get ranges included during parsing
dot_graph() Get DOT graph representation
close() Release tree resources

Incremental Editing

Apply an edit before reparsing changed source code:

local code = "func main() { x := 1 }"
local tree, parse_err = treesitter.parse("go", code)
if parse_err then
    return nil, parse_err
end

-- Mark edit: changed "1" to "100" at byte 19
local _, edit_err = tree:edit({
    start_byte = 19,
    old_end_byte = 20,
    new_end_byte = 22,
    start_row = 0,
    start_column = 19,
    old_end_row = 0,
    old_end_column = 20,
    new_end_row = 0,
    new_end_column = 22
})
if edit_err then
    tree:close()
    return nil, edit_err
end

-- Re-parse with edited tree (faster than full parse)
local parser, parser_err = treesitter.parser()
if parser_err then
    tree:close()
    return nil, parser_err
end
local _, language_err = parser:set_language("go")
if language_err then
    parser:close()
    tree:close()
    return nil, language_err
end
local new_tree, new_tree_err = parser:parse("func main() { x := 100 }", tree)
if new_tree_err then
    parser:close()
    tree:close()
    return nil, new_tree_err
end

new_tree:close()
parser:close()
tree:close()

Nodes

Nodes represent elements in the syntax tree. In the isolated snippets below, root, node, and func_decl are application-selected nodes borrowed from a still-open tree.

Node Types

local node = root:child(0)

-- Type information
print(node:kind())        -- "package_clause"
print(node:type())        -- same as kind()
print(node:is_named())    -- true for significant nodes
print(node:grammar_name()) -- grammar rule name
-- Children
local child = node:child(0)           -- by index (0-based)
local named = node:named_child(0)     -- named children only
local count = node:child_count()
local named_count = node:named_child_count()

-- Siblings
local next = node:next_sibling()
local prev = node:prev_sibling()
local next_named = node:next_named_sibling()
local prev_named = node:prev_named_sibling()

-- Parent
local parent = node:parent()

-- By field name
local name_node = func_decl:child_by_field_name("name")
local field = node:field_name_for_child(0)

Position Information

-- Byte offsets
local start = node:start_byte()
local end_ = node:end_byte()

-- Row/column positions (0-based)
local start_pt = node:start_point()  -- {row = 0, column = 0}
local end_pt = node:end_point()      -- {row = 0, column = 12}

-- Source text
local source_text, err = node:text()
if err then
    return nil, err
end

Error Detection

if root:has_error() then
    -- Tree contains syntax errors
end

if node:is_error() then
    -- This specific node is an error
end

if node:is_missing() then
    -- Parser inserted this to recover from error
end

if node:is_extra() then
    -- Node is an "extra" (e.g. a comment) not required by the grammar
end

Other node methods: descendant_count() and named_descendant_for_point_range(start_pt, end_pt).

S-Expression

local sexp = node:to_sexp()
-- "(source_file (package_clause (package_identifier)))"

Queries

Tree-sitter queries match syntax-tree patterns written as S-expressions.

Create Query

local query, err = treesitter.query("go", [[
    (function_declaration
        name: (identifier) @func_name
        parameters: (parameter_list) @params
    )
]])
if err then
    return nil, err
end
-- The owner calls query:close() after the final query operation.
Parameter Type Description
language string Language name
pattern string Query pattern in S-expression syntax

Returns: Query, error

Execute Query

-- Get all captures (flattened)
local captures, captures_err = query:captures(root, source_code)
if captures_err then
    query:close()
    tree:close()
    return nil, captures_err
end
for _, capture in ipairs(captures) do
    print(capture.name)   -- "func_name"
    print(capture.text)   -- actual text
    print(capture.index)  -- capture index
    -- capture.node is the Node object
end

-- Get matches (grouped by pattern)
local matches, matches_err = query:matches(root, source_code)
if matches_err then
    query:close()
    tree:close()
    return nil, matches_err
end
for _, match in ipairs(matches) do
    print(match.id, match.pattern)
    for _, capture in ipairs(match.captures) do
        local captured_text, text_err = capture.node:text()
        if text_err then
            query:close()
            tree:close()
            return nil, text_err
        end
        print(capture.name, captured_text)
    end
end

query:close()
tree:close()

Passing userdata of the wrong type instead of a Tree-sitter Node returns nil, error; passing a primitive or table raises a Lua argument error before that check. root, source_code, and query here must come from a still-open tree and a successfully created query. The snippet uses the owning tree handle to close both resources before it returns.

Query Control

-- Limit query scope
query:set_byte_range(0, 1000)
query:set_point_range({row = 0, column = 0}, {row = 10, column = 0})

-- Limit matches
query:set_match_limit(100)
if query:did_exceed_match_limit() then
    -- More matches exist
end

-- Timeout (string duration or nanoseconds)
query:set_timeout("500ms")
query:set_timeout(1000000000)  -- 1 second in nanoseconds

-- Disable patterns/captures
query:disable_pattern(0)
query:disable_capture("func_name")

Query Inspection

local pattern_count = query:pattern_count()
local capture_count = query:capture_count()
local name = query:capture_name_for_id(0)
local id = query:capture_index_for_name("func_name")

Further inspection methods: string_count(), start_byte_for_pattern(i), end_byte_for_pattern(i), get_match_limit(), get_timeout(), is_pattern_rooted(i), is_pattern_non_local(i), is_pattern_guaranteed(i), capture_quantifier(pattern, capture), set_max_start_depth(n), get_property_predicates(i), get_property_settings(i), get_text_predicates(i).

Tree Cursor

A tree cursor traverses a tree without creating a node object at every step.

Basic Traversal

local cursor, err = tree:walk()
if err then
    return nil, err
end

-- Start at root
print(cursor:current_node():kind())  -- "source_file"
print(cursor:current_depth())        -- 0

-- Navigate
if cursor:goto_first_child() then
    print(cursor:current_node():kind())
    print(cursor:current_depth())  -- 1
end

if cursor:goto_next_sibling() then
    -- moved to next sibling
end

cursor:goto_parent()  -- back to parent

cursor:close()

Cursor Methods

Method Returns Description
current_node() Node Node at cursor position
current_depth() integer Depth (0 = root)
current_field_name() string? Field name if any
current_field_id() integer Field ID (0 if none)
current_descendant_index() integer Descendant index of current node
goto_parent() boolean Move to parent
goto_first_child() boolean Move to first child
goto_last_child() boolean Move to last child
goto_next_sibling() boolean Move to next sibling
goto_previous_sibling() boolean Move to previous sibling
goto_descendant(index) - Move to descendant by index
goto_first_child_for_byte(n) integer? Move to child containing byte
goto_first_child_for_point(pt) integer? Move to child containing point
reset(node) - Reset cursor to node
reset_to(cursor) - Reset cursor to another cursor's position
copy() Cursor Create copy of cursor
close() - Release resources

Language Metadata

local lang, err = treesitter.language("go")
if err then
    return nil, err
end

print(lang:version())           -- ABI version
print(lang:node_kind_count())   -- number of node types
print(lang:field_count())       -- number of fields
print(lang:parse_state_count()) -- number of parse states

-- Node kind lookup
local kind = lang:node_kind_for_id(1)
local id = lang:id_for_node_kind("identifier", true)
local is_named = lang:node_kind_is_named(1)

-- Field lookup
local field_name = lang:field_name_for_id(1)
local field_id = lang:field_id_for_name("name")

Errors

Condition Kind Retryable
Language not supported errors.INVALID no
Language has no binding errors.INVALID no
Invalid query pattern errors.INVALID no
Invalid positions errors.INVALID no
Parse failed errors.INTERNAL no
No execution context errors.INTERNAL no

Closing an already closed parser, tree, query, or cursor is safe. Calling any other method on a closed handle raises a Lua argument error.

See Error Handling for working with errors.

Query Syntax Reference

Tree-sitter queries use S-expression patterns:

; Match a node type
(identifier)

; Match with field names
(function_declaration name: (identifier))

; Capture with @name
(function_declaration name: (identifier) @func_name)

; Multiple patterns
[
  (function_declaration)
  (method_declaration)
] @declaration

; Wildcards
(_)           ; any node
(identifier)+ ; one or more
(identifier)* ; zero or more
(identifier)? ; optional

; Predicates
((identifier) @var
  (#match? @var "^_"))  ; regex match

See Tree-sitter Query Syntax for complete documentation.