Skip to content

AST

Yuku’s internal AST is a flat, arena-allocated structure optimized for sequential access. When serialized to JSON or exposed through Node.js bindings, it is converted to an ESTree-compatible format matching Oxc:

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

Extensions beyond the base specs: Stage 3 decorators, import defer, import source, and a hashbang field on Program.

This page covers the internal Zig AST, its memory model, core types, and the complete node reference.

The AST is not a graph of heap-allocated structs. All nodes live in a single flat array (Tree.nodes), and children reference each other by integer index. This gives linear memory layout and predictable cache behavior during traversal.

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

NodeList is a MultiArrayList(Node), meaning data and span are stored in separate parallel arrays. Reading only spans, or only data, stays within a single array.

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

Tree is the root container returned by parser.parse().

FieldTypeDescription
programNodeIndexRoot node (always a program)
nodesNodeListAll AST nodes
extraArrayList(NodeIndex)Variable-length child index lists
diagnosticsArrayList(Diagnostic)Parse errors, warnings, hints
comments[]const CommentAll comments found in source
source[]const u8Original source text
source_typeSourceType.script or .module
langLang.js, .ts, .jsx, .tsx, or .dts

Key read methods:

tree.getData(index) // NodeData for a node
tree.getSpan(index) // Span (source byte range) for a node
tree.getExtra(range) // []const NodeIndex for an IndexRange
tree.getString(handle) // []const u8 from a String handle
tree.hasErrors() // true if any diagnostic has severity .error
tree.isTs() // true for .ts, .tsx, .dts
tree.isJsx() // true for .jsx, .tsx
tree.isModule() // true for source_type .module

Four types appear in nearly every node definition. Understanding them once makes the entire node reference readable.

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

Every node is identified by its position in Tree.nodes. Optional child fields use .null to indicate absence:

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

Nodes with a variable number of children store them as a contiguous slice in Tree.extra. An IndexRange is a (start, len) window into that array.

const children = tree.getExtra(node.body); // []const NodeIndex
for (children) |child_index| {
const child_data = tree.getData(child_index);
}

IndexRange.empty ({ .start = 0, .len = 0 }) represents an empty list.

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

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

  • Source slice (zero-copy): most identifiers and string literals parsed from source. The bytes live inside the original source slice .
  • Pool entry: programmatically added strings (tree.addString()), transformed names, or escaped identifiers. These live in the string pool’s extra buffer.

tree.getString(handle) resolves both cases transparently:

const name = tree.getString(node.name); // always returns []const u8
pub const Span = struct { start: u32, end: u32 };

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

const span = tree.getSpan(index);
const source_text = tree.source[span.start..span.end];

NodeData is a tagged union with a variant for every node type. tree.getData(index) returns one, and you switch on the tag to determine the type and unpack its fields:

const data = tree.getData(index);
switch (data) {
.binary_expression => |expr| {
// expr.left and expr.right are NodeIndex (recurse with getData)
// expr.operator is a BinaryOperator enum
const left_data = tree.getData(expr.left);
},
.variable_declaration => |decl| {
// decl.kind is VariableKind (.var, .let, .const, .using, .await_using)
// decl.declarators is IndexRange (read with getExtra)
const declarators = tree.getExtra(decl.declarators);
},
.identifier_reference => |id| {
// id.name is a String (resolve with getString)
const name = tree.getString(id.name);
},
else => {},
}

Every field of ast.NodeData is a distinct node type. The field name is the exact hook name used in visitor structs:

// NodeData field: binary_expression: BinaryExpression
// Visitor hook: enter_binary_expression / exit_binary_expression

The full NodeData union is what the traverser’s compile-time validation checks against. Any enter_* or exit_* method on your visitor must match a field name here exactly.

Optional child fields (.null) and optional child lists (IndexRange.empty) are noted where they apply.


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

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

NodeJS SyntaxNotes
expression_statementexpr;Wraps any expression used as a statement
block_statement{ ... }body is a statement/directive list
empty_statement;No fields
debugger_statementdebugger;No fields
if_statementif (test) cons else altalternate is .null when no else branch
switch_statementswitch (d) { cases }cases is a list of switch_case nodes
switch_casecase x: ... / default: ...test is .null for the default case
for_statementfor (init; test; update) bodyinit, test, and update are all optional (.null)
for_in_statementfor (x in y) bodyleft is a declaration or assignment target
for_of_statementfor (x of y) bodyawait: bool for for await (...of...)
while_statementwhile (test) body
do_while_statementdo body while (test)
break_statementbreak; / break label;label is .null for unlabeled
continue_statementcontinue; / continue label;label is .null for unlabeled
labeled_statementlabel: stmt
return_statementreturn; / return expr;argument is .null for bare return
throw_statementthrow expr;
try_statementtry {} catch {} finally {}handler and finalizer are .null when absent
catch_clausecatch (e) { body }param is .null for catch {} without binding
with_statementwith (obj) body

NodeJS SyntaxNotes
variable_declarationvar/let/const/using x = ...kind is a VariableKind enum; declarators is a list of variable_declarator nodes
variable_declaratorx = initid is a binding pattern; init is .null for let x;
directive"use strict";Only appears at the start of a function or module body, before regular statements
functionfunction foo() {}Covers all function forms; check the type field
classclass Foo {}Covers both declarations and expressions; check the type field

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
ts_empty_body_function_expression // abstract methods, interface methods
// ClassType
class_declaration // class Foo {}
class_expression // const x = class {}

NodeJS SyntaxNotes
binary_expressiona + b, a === b, a instanceof boperator is a BinaryOperator enum
logical_expressiona && b, a || b, a ?? boperator is a LogicalOperator enum
unary_expression!x, typeof x, void x, delete xoperator is a UnaryOperator enum
update_expressionx++, ++x, x--operator is UpdateOperator; prefix: bool distinguishes pre/post
assignment_expressionx = y, x += y, x ??= yoperator is an AssignmentOperator enum
conditional_expressiontest ? a : b
sequence_expressiona, b, cexpressions is a node list
parenthesized_expression(expr)Wraps an expression to preserve explicit parentheses in the tree
member_expressionobj.prop, obj[x], obj.#privcomputed: bool for bracket access; optional: bool for ?.
call_expressionfn(args), fn?.()optional: bool for ?.()
new_expressionnew Foo(args)
chain_expressiona?.b, a?.()Wrapper around an optional chain; the inner node carries optional: true
tagged_template_expressiontag`hello`tag is the function; quasi is the template literal
await_expressionawait expr
yield_expressionyield expr, yield* exprdelegate: bool for yield*; argument may be .null
meta_propertyimport.meta, new.targetmeta and property are identifier_name nodes
array_expression[a, , b, ...c]elements may contain .null entries for holes
object_expression{a: 1, b, ...c}properties contains object_property and spread_element nodes
object_propertykey: value, getters, setters, methodskind (PropertyKind), method, shorthand, computed
spread_element...exprUsed in arrays, calls, and object literals
import_expressionimport(src), import.source(src)Dynamic import; phase may be .source, .defer, or null
this_expressionthisNo fields

NodeJS SyntaxNotes
string_literal"hello", 'world'value is the decoded content without quotes (escape sequences resolved). Raw source text is available via the span.
numeric_literal42, 0xFF, 0o7, 0b1010value is the parsed f64; kind distinguishes decimal / hex / octal / binary
bigint_literal42nvalue is the digits without the trailing n suffix
boolean_literaltrue, falsevalue: bool
null_literalnullNo fields
regexp_literal/pattern/flagspattern and flags are separate String handles
template_literal`hello ${name}`quasis (list of template_element) and expressions are interleaved; always quasis.len == expressions.len + 1
template_elementthe text parts between ${}cooked is the escape-decoded content (empty when is_cooked_undefined); tail: bool marks the last segment. Raw source text is available via the span.

Four distinct identifier node types all carry a single name: String field:

NodeUsed 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;
// ^^^ ^^^ ^^^
// | | IdentifierName (property, never resolved)
// | IdentifierReference (variable use, resolved by scope chain)
// BindingIdentifier (declaration, recorded as a symbol)

:::


NodeJS SyntaxNotes
array_pattern[a, , b, ...rest]elements may include .null for holes; rest is .null if absent
object_pattern{a, b: c, ...rest}properties contains binding_property nodes; rest is .null if absent
binding_propertykey: value or shorthand keyshorthand: bool, computed: bool
assignment_patternx = defaultUsed for default values in destructuring and function parameters
binding_rest_element...restargument is the binding pattern the rest collects into
formal_parameters(a, b = 1, ...rest)items contains formal_parameter nodes; rest is .null if absent
formal_parametera single parameter slotpattern is the binding (may be any binding pattern, with optional default via assignment_pattern)

pub const Function = struct {
type: FunctionType, // declaration, expression, or TS forms
id: NodeIndex, // BindingIdentifier (.null for anonymous functions)
generator: bool, // true for function*
async: bool, // true for async function
params: NodeIndex, // FormalParameters
body: NodeIndex, // FunctionBody (.null for TS overloads/abstract methods)
};
NodeDescription
functionAll named and anonymous function forms. Use type, generator, and async to distinguish them.
function_bodyThe { ... } body of a function. Contains directives and statements in body: IndexRange.
arrow_function_expressionArrow functions. expression: bool is true when the body is an expression (not a block). async: bool for async arrows.

pub const Class = struct {
type: ClassType, // class_declaration or class_expression
decorators: IndexRange, // Decorator[] (empty if none)
id: NodeIndex, // BindingIdentifier (.null for anonymous expressions)
super_class: NodeIndex, // Expression (.null if no extends clause)
body: NodeIndex, // ClassBody
};
NodeDescription
classBoth class Foo {} and const x = class {}. Check type.
class_bodyThe { members } block. body contains method_definition, property_definition, and static_block nodes.
method_definitionA method, getter, setter, or constructor. kind is a MethodDefinitionKind enum (constructor, method, get, set). static: bool, computed: bool.
property_definitionA class field (x = 1). value is .null for fields without an initializer. accessor: bool for auto-accessors. static: bool, computed: bool.
static_blockstatic { ... }. body is a list of statements.
decorator@expr. expression is the decorator expression node.
superThe super keyword. No fields.

NodeJS SyntaxNotes
import_declarationimport x from 'y'specifiers is empty for side-effect-only imports; phase is .source or .defer for staged imports, null for regular
import_specifier{ imported as local }
import_default_specifierimport x from ...local is the binding
import_namespace_specifierimport * as x from ...local is the binding
import_attribute{ type: "json" }Import attributes / assertions
export_named_declarationexport { x }, export var xdeclaration is .null for specifier-only; source is .null for local (non-re-export)
export_default_declarationexport default exprdeclaration is an expression, function, or class
export_all_declarationexport * from 'y'exported is .null for export *; non-null for export * as name
export_specifier{ local as exported }

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

NodeJSX SyntaxNotes
jsx_element<Foo>...</Foo>closing_element is .null for self-closing tags
jsx_opening_element<Foo ...>self_closing: bool for <Foo />; name is a JSX name node
jsx_closing_element</Foo>
jsx_fragment<>...</>
jsx_opening_fragment<>No fields
jsx_closing_fragment</>No fields
jsx_identifierFoo in JSX positionname: String
jsx_namespaced_namenamespace:name
jsx_member_expressionFoo.Bar.Baz
jsx_attributefoo="bar" or foo={expr}value is .null for boolean attributes like disabled
jsx_spread_attribute{...props}argument is the spread expression
jsx_expression_container{expression}expression is a jsx_empty_expression node for {}
jsx_empty_expression{}No fields
jsx_textText content between tagsvalue: String is the text content
jsx_spread_child{...children}expression is the spread expression

NodeTS SyntaxNotes
ts_export_assignmentexport = exprCommonJS-style TypeScript module export
ts_namespace_export_declarationexport as namespace nameUMD global namespace declaration