Skip to content

AST

The AST that comes out of parser.parse() is a flat array of nodes that reference each other by integer index. Reading and walking it is fast, predictable, and explicit. There are no boxed structs, no virtual dispatch, no surprise allocations. Every operation is a tagged-union switch and a slice index away.

The same tree, when exposed through the yuku-parser npm package, becomes ESTree-compatible output matching Oxc:

  • JavaScript / JSX: fully conformant with ESTree, identical to Acorn.
  • TypeScript: conforms to TS-ESTree used by @typescript-eslint.

Comments can be attached to the AST nodes they belong to, exposed as a flat offset-indexed array, or both. See Comments.

On top of the base specs, the AST also carries Stage 3 decorators, import defer, import source, and a hashbang field on program. These extensions are present in Oxc as well.

The AST is not a graph of heap-allocated structs. Every node lives in a single flat array (Tree.nodes), and child references are indices into that array. Variable-length child lists live in a second flat array (Tree.extras), and string content lives in a string pool. Three arrays, one arena.

Tree
nodes NodeList flat array of all nodes (data + span, struct-of-arrays)
extras []NodeIndex variable-length child lists (IndexRange points here)
strings StringPool all string content (source refs + interned extras)

NodeList is a MultiArrayList(Node), so data and span are stored in two separate parallel arrays. Code that only reads spans, or only reads data, touches one array.

All memory is owned by a single ArenaAllocator. tree.deinit() frees the entire tree at once.

Tree is the root container returned by parser.parse(). The fields you read directly:

FieldTypeDescription
rootNodeIndexIndex of the root node (a program)
diagnosticsArrayList(Diagnostic)Parse errors, warnings, hints
source[]const u8Original source text
source_typeSourceType.script or .module
langLang.js, .ts, .jsx, .tsx, or .dts

Everything else is reached through methods:

tree.data(idx) // NodeData for the node at idx
tree.span(idx) // Span (source byte range) for the node at idx
tree.extra(range) // []const NodeIndex for an IndexRange
tree.string(handle) // []const u8 for a String handle
tree.commentsOf(idx) // []const AttachedComment attached to the node at idx
tree.lineColOf(pos) // zero-indexed { line, col } for a byte offset
tree.isTs() // language is .ts, .tsx, or .dts
tree.isJsx() // language is .jsx or .tsx
tree.isModule() // source_type is .module
tree.hasErrors() // any diagnostic with severity .error

Four small types carry every reference inside the AST.

pub const NodeIndex = enum(u32) { null = std.math.maxInt(u32), _ };

Every node is identified by its position in Tree.nodes. Optional child slots use .null to signal absence.

// if_statement.alternate is .null when there is no else branch
if (node.alternate != .null) {
const else_data = tree.data(node.alternate);
}
pub const IndexRange = struct { start: u32, len: u32 };

Variable-length children are stored as a contiguous slice in Tree.extras. An IndexRange is a (start, len) window into that array. Resolve it with tree.extra(range):

const children = tree.extra(node.body); // []const NodeIndex
for (children) |child| {
const child_data = tree.data(child);
}

IndexRange.empty is the zero-length range.

pub const String = struct { start: u32, end: u32 };

String is a lightweight handle to text. It points into one of two backing stores:

  • Source slice (zero-copy): most identifiers and string literals parsed from input. The bytes live inside tree.source directly.
  • Pool entry: interned strings such as escaped identifiers and names produced by transforms. These live in the string pool’s extra buffer.

tree.string(handle) resolves both transparently and always returns []const u8:

const name = tree.string(node.name);
pub const Span = struct { start: u32, end: u32 };

Byte offsets into the source text. start is inclusive, end is exclusive.

const span = tree.span(idx);
const text = tree.source[span.start..span.end];

NodeData is a tagged union with one variant per node type. The variant tags are snake_case (binary_expression, if_statement, ts_type_alias_declaration, …). tree.data(idx) returns one. switch on the tag and unpack:

switch (tree.data(idx)) {
.binary_expression => |expr| {
// expr.left and expr.right are NodeIndex (recurse with data)
// expr.operator is a BinaryOperator enum
const left = tree.data(expr.left);
},
.variable_declaration => |decl| {
// decl.kind is VariableKind (.var, .let, .const, .using, .await_using)
// decl.declarators is IndexRange (read with extra)
for (tree.extra(decl.declarators)) |d| { /* ... */ }
},
.identifier_reference => |id| {
// id.name is a String (resolve with string)
const text = tree.string(id.name);
},
else => {},
}

The same snake_case tag names are used as visitor hook names: a method called enter_binary_expression on your visitor struct fires when the traverser enters that node kind.

A node’s children sit in two kinds of fields:

  • Single child: NodeIndex. Either a real index, or .null for an absent optional slot. Read with tree.data(field).
  • Variadic children: IndexRange. Resolve with tree.extra(range) to get []const NodeIndex, then iterate.
switch (tree.data(idx)) {
.function => |func| {
// single child
const body = tree.data(func.body);
// variadic children: function params -> formal_parameters -> items
const params = tree.data(func.params).formal_parameters;
for (tree.extra(params.items)) |param| { /* ... */ }
},
else => {},
}

For tree-wide walks, use the traverser instead of writing recursion by hand. It handles every node kind correctly without per-tag bookkeeping.

Eight methods on NodeData answer the categorical questions linters and analyzers ask most often. They collapse a family of tags into a single boolean.

MethodTrue for
isExpression()Any node that produces a value at runtime: literals, identifier references, operator expressions, member access, calls, function and class expression forms, JSX elements, and the TypeScript value-position wrappers.
isStatement()Any node valid at statement position: control flow, structural statements, declarations, imports, exports, and TypeScript top-level declarations. Function and class declaration forms are included, expression forms are not.
isLiteral()string_literal, numeric_literal, bigint_literal, boolean_literal, null_literal, regexp_literal, template_literal.
isCallable()function (any form) and arrow_function_expression. Excludes method_definition, which wraps a function in its value field.
isPattern()binding_identifier, array_pattern, object_pattern, assignment_pattern.
isDeclaration()variable_declaration, function and class declaration forms, import_declaration, export_named_declaration, export_default_declaration, export_all_declaration, ts_type_alias_declaration, ts_interface_declaration, ts_enum_declaration, ts_module_declaration, ts_global_declaration, ts_import_equals_declaration.
isIteration()for_statement, for_in_statement, for_of_statement, while_statement, do_while_statement. Useful for break and continue scope checks.
isTypeContext()Any node that roots a TypeScript type-only subtree: type annotations, type references, function and constructor types, conditional and mapped types, type literals, interface heritage, and the various TS signatures. The semantic traverser uses this to drive its inTypePosition() context flag.

For dual-purpose nodes (function and class) the predicates consult the type field internally, so isExpression() returns true only for the expression forms and isStatement() / isDeclaration() only for the declaration forms.

const data = tree.data(idx);
if (data.isExpression()) {
// any value-producing node
}
if (data.isCallable()) {
// function or arrow_function_expression
// The body, params, etc. are still type-specific,
// so switch on the tag to access them.
}

For anything narrower than these eight, switch directly:

switch (data) {
.arrow_function_expression => |arrow| { /* ... */ },
else => {},
}

Every entry in NodeData is a distinct node tag. The tag name is the exact name used in tree.data() switches and visitor hooks (enter_<tag>).

Optional child fields are noted with .null. Optional child lists are noted with .empty.


The root of every tree. There is always exactly one program node at tree.root.

pub const Program = struct {
source_type: SourceType, // .script or .module
body: IndexRange, // (any statement | directive)[]
hashbang: ?Hashbang, // non-null for #!/usr/bin/env node lines
};

directive nodes (such as "use strict";) appear at the start of the body. Imports and exports appear in source order alongside other statements.


TagSyntaxDescription
expression_statementexpr;An expression used as a statement.
block_statement{ ... }A braced block.
empty_statement;A standalone semicolon.
debugger_statementdebugger;A debugger breakpoint.
if_statementif (test) cons else altAn if / else branch.
switch_statementswitch (d) { cases }A switch with one or more case and default clauses.
switch_casecase x: ... / default: ...A single clause inside a switch.
for_statementfor (init; test; update) bodyA C-style for loop.
for_in_statementfor (x in y) bodyA for ... in loop iterating over enumerable property keys.
for_of_statementfor (x of y) bodyA for ... of or for await ... of loop iterating over an iterable.
while_statementwhile (test) bodyA while loop.
do_while_statementdo body while (test)A do ... while loop.
break_statementbreak; / break label;A break exiting the nearest loop, switch, or labeled statement.
continue_statementcontinue; / continue label;A continue jumping to the next iteration of the nearest or labeled loop.
labeled_statementlabel: stmtA statement prefixed with a label that break and continue can target.
return_statementreturn; / return expr;A return from the enclosing function.
throw_statementthrow expr;A throw raising an exception.
try_statementtry {} catch {} finally {}A try with optional catch and finally clauses.
catch_clausecatch (e) { body }The catch clause of a try, with an optional binding.
with_statementwith (obj) bodyA with block. Forbidden in strict mode.

TagSyntaxDescription
variable_declarationvar/let/const/using x = ...A var, let, const, using, or await using declaration.
variable_declaratorx = initA single binding inside a variable declaration.
directive"use strict";A directive prologue, only valid at the top of a function or module body.
functionfunction foo() {}Every function form (declaration, expression, ambient, body-less signature).
classclass Foo {}Both class declarations and class expressions.

function and class are dual-purpose nodes. The type field distinguishes the form:

// FunctionType
function_declaration // function foo() {}
function_expression // const x = function () {}
ts_declare_function // declare function foo(): void
// also: plain overload signatures
ts_empty_body_function_expression // body-less class methods (overloads,
// abstract, ambient)
// ClassType
class_declaration // class Foo {}
class_expression // const x = class {}

TagSyntaxDescription
binary_expressiona + b, a === b, a instanceof bA non-logical, non-assignment binary operation.
logical_expressiona && b, a || b, a ?? bA short-circuiting logical operation.
unary_expression!x, typeof x, void x, delete xA unary prefix operation.
update_expressionx++, ++x, x--A prefix or postfix increment or decrement.
assignment_expressionx = y, x += y, x ??= yAn assignment or compound assignment.
conditional_expressiontest ? a : bA ternary expression.
sequence_expressiona, b, cA comma-separated sequence of expressions.
parenthesized_expression(expr)An expression wrapped in parentheses, preserved in the tree.
member_expressionobj.prop, obj[x], obj.#privProperty access, in static, computed, or optional form.
call_expressionfn(args), fn?.()A function call, optionally with type arguments or optional invocation.
new_expressionnew Foo(args)A new constructor invocation.
chain_expressiona?.b, a?.()A wrapper that scopes optional-chain short-circuiting to a member or call chain.
tagged_template_expressiontag`hello`A template literal preceded by a tag function.
await_expressionawait exprAn await of a promise inside an async context.
yield_expressionyield expr, yield* exprA yield or delegating yield* inside a generator.
meta_propertyimport.meta, new.targetA meta property reference such as import.meta or new.target.
array_expression[a, , b, ...c]An array literal, including holes and spread elements.
object_expression{a: 1, b, ...c}An object literal, including spread elements.
object_propertykey: value, getters, setters, methodsA property entry inside an object literal.
spread_element...exprA spread element used in arrays, calls, and object literals.
import_expressionimport(src), import.source(src)A dynamic import() call or phased import.
this_expressionthisThe this keyword used as an expression.
supersuperThe super keyword used as an expression head.

TagSyntaxDescription
string_literal"hello", 'world'A string literal with escape sequences resolved.
numeric_literal42, 0xFF, 0o7, 0b1010A numeric literal in decimal, hex, octal, or binary.
bigint_literal42nA BigInt literal.
boolean_literaltrue, falseThe true or false keyword as a value.
null_literalnullThe null literal.
regexp_literal/pattern/flagsA regular expression literal.
template_literal`hello ${name}`A template literal with zero or more interpolations.
template_elementtext part between ${...}A static text span inside a template literal.

Five tags, all carrying a single name: String field. They are structurally identical but appear in different syntactic positions and resolve differently.

TagUsed for
identifier_referenceA name used as a value: x, console, Math
binding_identifierA name being declared: const x, function foo, import { x }
identifier_nameA bare name in non-expression position: object keys ({foo: 1}), member access right-hand side (obj.foo), import.meta
label_identifierA label name in break label, continue label, or label: stmt
private_identifierA private class member: #field (the # is not part of name)
const foo = bar.baz;
// ^^^ ^^^ ^^^
// | | identifier_name (property, never resolved)
// | identifier_reference (variable use, resolved by scope chain)
// binding_identifier (declaration, recorded as a symbol)

binding_identifier additionally carries decorators, an optional type annotation, and an optional ? flag when it appears in a parameter position.


TagSyntaxDescription
array_pattern[a, , b, ...rest]An array destructuring pattern.
object_pattern{a, b: c, ...rest}An object destructuring pattern.
binding_propertykey: value or shorthand keyA single property inside an object pattern.
assignment_patternx = defaultA binding pattern with a default value, used in destructuring and parameter lists.
binding_rest_element...restA ...rest element inside a binding pattern or parameter list.
formal_parameters(a, b = 1, ...rest)The parameter list of a function.
formal_parametera single parameter slotA single parameter slot wrapping a binding pattern.

pub const Function = struct {
type: FunctionType, // declaration, expression, or TS forms
id: NodeIndex, // binding_identifier (.null for anonymous)
generator: bool, // true for function*
async: bool, // true for async function
declare: bool, // true for declare function
params: NodeIndex, // formal_parameters
body: NodeIndex, // function_body (.null for TS overloads / abstract)
type_parameters: NodeIndex, // ts_type_parameter_declaration or .null
return_type: NodeIndex, // ts_type_annotation or .null
};
TagDescription
functionEvery named and anonymous function form, including ambient and body-less ones.
function_bodyThe braced body of a function.
arrow_function_expressionAn arrow function, with either an expression or a block body.

pub const Class = struct {
type: ClassType, // class_declaration or class_expression
decorators: IndexRange, // decorator[] (empty if none)
id: NodeIndex, // binding_identifier (.null for anonymous expressions)
super_class: NodeIndex, // any expression (.null if no extends clause)
body: NodeIndex, // class_body
type_parameters: NodeIndex, // ts_type_parameter_declaration or .null
super_type_arguments: NodeIndex, // ts_type_parameter_instantiation or .null
implements: IndexRange, // ts_class_implements[] (empty if none)
abstract: bool, // true for abstract class
declare: bool, // true for declare class
};
TagDescription
classBoth class declarations and class expressions.
class_bodyThe braced body of a class, holding its members.
method_definitionA method, getter, setter, or constructor inside a class.
property_definitionA class field or auto-accessor declaration.
static_blockA static { ... } initialization block inside a class.
decoratorA decorator (@expr) applied to a class or class member.
superThe super keyword used as an expression head.

TagSyntaxDescription
import_declarationimport x from 'y'A static import declaration, including side-effect and phased forms.
import_specifier{ imported as local }A named binding specifier in an import declaration.
import_default_specifierimport x from ...The default-binding specifier in an import declaration.
import_namespace_specifierimport * as x from ...A * as local namespace import specifier.
import_attribute{ type: "json" }A single attribute in a with { ... } clause on an import or export.
export_named_declarationexport { x }, export var xAn export { ... } or export <decl> declaration, with optional re-export.
export_default_declarationexport default exprAn export default declaration.
export_all_declarationexport * from 'y'An export * from "m" or export * as ns from "m" declaration.
export_specifier{ local as exported }A named binding specifier in an export declaration.

JSX nodes are only present in .jsx and .tsx trees.

TagSyntaxDescription
jsx_element<Foo>...</Foo>A JSX element, possibly self-closing.
jsx_opening_element<Foo ...>The opening tag of a JSX element.
jsx_closing_element</Foo>The closing tag of a JSX element.
jsx_fragment<>...</>A JSX fragment.
jsx_opening_fragment<>The opening <> of a JSX fragment.
jsx_closing_fragment</>The closing </> of a JSX fragment.
jsx_identifierFoo in JSX positionAn identifier used as a JSX tag or attribute name.
jsx_namespaced_namenamespace:nameA namespaced JSX name.
jsx_member_expressionFoo.Bar.BazA dotted JSX tag name.
jsx_attributefoo="bar" or foo={expr}A single JSX attribute, including boolean-only forms.
jsx_spread_attribute{...props}A spread attribute on a JSX element.
jsx_expression_container{expression}An { expression } slot inside JSX.
jsx_empty_expression{}The empty {} placeholder inside a JSX expression slot.
jsx_texttext content between tagsA span of raw text inside a JSX element or fragment.
jsx_spread_child{...children}A spread child inside a JSX element.

A JSX tag name (the name field on jsx_opening_element, jsx_closing_element, and one form of jsx_attribute) is one of jsx_identifier, jsx_namespaced_name, or jsx_member_expression.

A JSX child (entries in the children list on jsx_element and jsx_fragment) is one of jsx_text, jsx_expression_container, jsx_spread_child, jsx_element, or jsx_fragment.


TypeScript nodes are present in .ts, .tsx, and .dts trees.

TagSyntaxDescription
ts_type_annotation: TA : T annotation wrapping an inner type. The span starts at the : token.

Each keyword is its own zero-field node.

TagSyntax
ts_any_keywordany
ts_unknown_keywordunknown
ts_never_keywordnever
ts_void_keywordvoid
ts_null_keywordnull
ts_undefined_keywordundefined
ts_string_keywordstring
ts_number_keywordnumber
ts_bigint_keywordbigint
ts_boolean_keywordboolean
ts_symbol_keywordsymbol
ts_object_keywordobject
ts_intrinsic_keywordintrinsic
ts_this_typethis
TagSyntaxDescription
ts_type_referenceFoo, Promise<T>A reference to a named type, optionally with type arguments.
ts_qualified_nameA.B.CA left-associative dotted type name.
ts_type_querytypeof console.logThe typeof type operator applied to a value reference.
ts_import_typeimport("m").Foo<T>A reference to a type imported from a module path, written in type position.
TagSyntaxDescription
ts_type_parameterT, T extends U = VA single type parameter introduced by a generic declaration.
ts_type_parameter_declaration<T, U>The <...> parameter list introduced by a generic declaration.
ts_type_parameter_instantiation<number, string>The <...> argument list applied at a call site, reference, or instantiation.
TagSyntaxDescription
ts_literal_type"hello", 42, true, -1A literal value used in type position.
ts_template_literal_type`Hello, ${N}!`A template literal in type position with one or more interpolations.
TagSyntaxDescription
ts_array_typeT[]A postfix array type.
ts_indexed_access_typeT[K]An indexed access type that looks up a property type.
ts_tuple_type[T, U?, ...V[]]A fixed-length tuple type with positional, optional, rest, or named entries.
ts_named_tuple_memberlabel: T, label?: TA labeled element inside a tuple type.
ts_optional_typeT? (in tuple slot)An optional element inside a tuple type.
ts_rest_type...T (in tuple slot)A rest element inside a tuple type.
TagSyntaxDescription
ts_union_typeA | B | CA union of two or more types.
ts_intersection_typeA & B & CAn intersection of two or more types.
ts_conditional_typeT extends U ? X : YA conditional type selecting between two branches.
ts_infer_typeinfer R, infer R extends stringAn infer placeholder inside a conditional type’s extends branch.
TagSyntaxDescription
ts_type_operatorkeyof T, unique symbol, readonly T[]A keyof, unique, or readonly prefix on an inner type.
ts_parenthesized_type(T)A parenthesized type used for grouping or precedence.
TagSyntaxDescription
ts_function_type(x: T) => UA callable signature in type position.
ts_constructor_typenew (x: T) => U, abstract new ...A constructor signature in type position, optionally abstract.
ts_type_predicatex is T, asserts x is T, asserts xA type predicate that narrows a parameter or this in control-flow analysis.
TagSyntaxDescription
ts_type_literal{ x: T; y: U }An anonymous object type holding a list of signatures.
ts_mapped_type{ [K in T]: V }A mapped type that projects every key in a union to a new property type.
TagSyntaxDescription
ts_jsdoc_nullable_type?T or T?A JSDoc-style nullable type marker.
ts_jsdoc_non_nullable_type!T or T!A JSDoc-style non-nullable type marker.
ts_jsdoc_unknown_type? (in Foo<?>)A JSDoc-style unknown type, valid only in a type argument slot.

These appear inside ts_type_literal.members and ts_interface_body.body.

TagSyntaxDescription
ts_property_signaturekey: T, readonly key?: TA property declaration inside a type literal or interface body.
ts_method_signaturem(x: T): U, get x(): T, set x(v: T)A method, getter, or setter declaration inside a type literal or interface body.
ts_call_signature_declaration(x: T): UA bare call signature inside a type literal or interface body.
ts_construct_signature_declarationnew (x: T): UA bare construct signature inside a type literal or interface body.
ts_index_signature[k: K]: V, readonly [...]: VAn index signature inside a type literal, interface body, or class body.
TagSyntaxDescription
ts_type_alias_declarationtype Maybe<T> = T | nullA type alias declaration, optionally generic and optionally ambient.
ts_interface_declarationinterface Foo<T> extends Bar { ... }An interface declaration, optionally generic and optionally ambient.
ts_interface_body{ ... } of an interfaceThe body of an interface, holding its signature members.
ts_interface_heritageone entry of an extends clauseA single parent listed in an interface’s extends clause.
ts_class_implementsone entry of an implements clauseA single interface listed in a class’s implements clause.
ts_enum_declarationenum Color { ... }An enum declaration, optionally const and optionally ambient.
ts_enum_body{ ... } of an enumThe body of an enum, holding its members in source order.
ts_enum_memberA = 1 inside an enum bodyA single member of an enum body, with an optional initializer.
TagSyntaxDescription
ts_module_declarationnamespace Foo { ... }, module "x" {}A namespace or module declaration, optionally ambient.
ts_module_blockthe { ... } of a moduleThe body of a namespace, module, or declare global declaration.
ts_global_declarationdeclare global { ... }A declare global augmentation block.
TagSyntaxDescription
ts_parameter_propertyconstructor(public x: T) {}A constructor parameter that implicitly declares a class field.
ts_this_parameterfunction f(this: T)An explicit this parameter declaring the type of this in the function body.
TagSyntaxDescription
ts_as_expressionexpr as TA postfix as type assertion.
ts_satisfies_expressionexpr satisfies TA postfix satisfies constraint check.
ts_type_assertion<T>exprA prefix <T> type assertion. Forbidden in .tsx.
ts_non_null_expressionexpr!A postfix non-null assertion.
ts_instantiation_expressionexpr<T>A type instantiation expression without call parentheses.
TagSyntaxDescription
ts_export_assignmentexport = exprA CommonJS-style ambient export.
ts_namespace_export_declarationexport as namespace NameA UMD ambient namespace export.
ts_import_equals_declarationimport x = require("m"), import x = Foo.BarAn import = declaration binding to a require or entity name.
ts_external_module_referencerequire("m")The require("module") form on the right-hand side of import x = require(...).

The comments option controls collection. It has four modes:

  • .flat (default): every comment lands in a flat, source-ordered list, tree.comments, each carrying its source span. No per-node attachment.
  • .attached: each comment is attached to the AST node it sits next to, read with tree.commentsOf(idx).
  • .both: the flat list and per-node attachment.
  • .none: comments are skipped like whitespace.
var tree = try parser.parse(allocator, source, .{ .comments = .flat });
defer tree.deinit();
for (tree.comments) |c| {
// c.span is the whole comment, delimiters included
std.debug.print("{s} @ {d}..{d}\n", .{ tree.string(c.value), c.span.start, c.span.end });
}

Each entry is a Comment:

pub const Comment = struct {
type: Type, // .line or .block
value: String, // body without `//` or `/* */` delimiters
span: Span, // full comment span, delimiters included
};

.attached (and .both) bind each comment to its host node, read with tree.commentsOf(idx):

var tree = try parser.parse(allocator, source, .{ .comments = .attached });
defer tree.deinit();
for (tree.commentsOf(some_node_idx)) |c| {
std.debug.print("{s} {s} {s}\n", .{
@tagName(c.position),
@tagName(c.type),
tree.string(c.value),
});
}

Each is an AttachedComment:

pub const AttachedComment = struct {
type: Comment.Type, // .line or .block
position: Position, // .before, .after, or .inside (relative to host)
same_line: bool, // shares a source line with the host's adjacent edge
value: String, // body without `//` or `/* */` delimiters
};

position tells you where the comment sits relative to its host:

  • .before: leading the host node.
  • .after: trailing the host node.
  • .inside: interior to an otherwise empty host, like function f() { /* hi */ }.

same_line is true when the comment shares a source line with the host’s adjacent edge (host start for .before, host end for .after). For .inside it is always false.