README

#syntax package

AST node types for the Starlark language. Import connect0459/starlark/syntax to work with parsed source trees: inspect nodes, walk the tree, or build ASTs for testing.

Obtain a File by calling @eval.parse_file; use the node types and walker functions to analyse or transform it.

#Key types and functions

SymbolDescription
FileTop-level parsed file (path + statement list)
ExprExpression node (17 variants)
StmtStatement node (12 variants)
Param / Arg / CompClauseParameter, call argument, comprehension clause
LiteralVal / BinaryOp / UnaryOp / AugOpLeaf enums
NodeVisitor wrapper covering all node kinds
walk_file / walk_stmt / walk_exprDepth-first tree traversal
stmt_pos / expr_posSource position of any node

#Quick start

Building a File directly for tests (bypassing the parser):

///|
test {
let pos = @errors.Position::new("<test>", 1, 1)
let stmt = @syntax.Stmt::SPass(pos)
let file = @syntax.File::new("<test>", [stmt])
assert_eq(file.path(), "<test>")
assert_eq(file.stmts().length(), 1)
}

Collecting identifiers from an expression node:

///|
test {
let pos = @errors.Position::new("<e>", 1, 1)
let lhs = @syntax.Expr::EIdent("x", pos)
let rhs = @syntax.Expr::EIdent("y", pos)
let expr = @syntax.Expr::EBinary(lhs, @syntax.BinaryOp::OpAdd, rhs, pos)
let names : Array[String] = []
@syntax.walk_expr(expr, fn(node) {
match node {
Some(@syntax.Node::NExpr(@syntax.Expr::EIdent(name, _))) =>
names.push(name)
_ => ()
}
true
})
assert_true(names == ["x", "y"])
}

Parsing from source requires @eval.parse_file / @eval.parse_expr.

#Error-position anchoring policy

When the compiler or resolver emits a diagnostic for an AST node it must choose one token as the anchor position — the source location shown to the user. The rules below are applied across the compilation pipeline (syntax, internal/parser, internal/resolver, internal/compile, eval) and mirror the behaviour of the starlark-go reference implementation.

Error classAnchor tokenHow to obtain it
Assignment target is not assignable (e.g. f() = 2, (a or b) = 1)Leftmost token of the LHS expressionsyntax.start(lhs)
Augmented-assignment target is not assignableLeftmost token of the LHS expressionsyntax.start(lhs)
Dict key errors in literals and comprehensions (unhashable key, duplicate key)Colon token separating the key from its valuecolon_pos from EDict/EDictComp; see note below
def/lambda parameter is not an identifierScanner cursor (one position past the bad token)Parser::scanner_errorscanner.position()
Positional argument appears after *args, **kwargs, or a keyword argumentLeftmost token of the misplaced argument expressionsyntax.start(arg_expr)

#Position accessors

Four mechanisms cover all anchoring sites above:

  • syntax.start(e) — walks into EBinary, ECall, ESlice, EIndex, and EDot sub-expressions to reach the leftmost token of a compound expression. Use this whenever the error should point at the beginning of a possibly-compound expression (LHS of assignments, misplaced arguments).
  • expr_pos(e) — returns the position stored in the node's own slot (operator, opening bracket, or other delimiter). Use this when the error is about the operator or delimiter itself rather than an operand.
  • Parser::scanner_error(msg) — uses scanner.position(), which is one past the current token. Use this for unexpected-token parse errors that must match starlark-go's scanner cursor convention.
  • set_pos(colon) in internal/compile — dict-literal and dict-comprehension key errors use a different mechanism: the compiler calls set_pos(colon_pos) before emitting the dict-insertion opcode (SetDictUniq / SetDict), recording the colon as the error anchor. The error itself is raised at runtime by the eval package using that recorded position. Both EDict pairs and EDictComp store a colon_pos; compile uses it in both code paths.

#Adding a new anchoring site

  1. Identify which rule class the new error falls into (table above).
  2. Choose the matching mechanism (syntax.start, expr_pos, scanner_error, or set_pos in compile).
  3. Add a conformance test that checks the reported column against the expected source position so regressions are caught automatically.

#API reference

#Functions

FunctionSignatureDescription
walk_file(File, (Node?) -> Bool) -> UnitDepth-first traversal of a file; visitor called with Some(node) on entry and None on exit after each parent's last child; return false to stop descending
walk_stmt(Stmt, (Node?) -> Bool) -> UnitDepth-first traversal of a statement; same Some/None entry/exit contract as walk_file
walk_expr(Expr, (Node?) -> Bool) -> UnitDepth-first traversal of an expression; same Some/None entry/exit contract as walk_file
stmt_pos(Stmt) -> @errors.PositionSource position of a statement node
expr_pos(Expr) -> @errors.PositionSource position of an expression node (node's own slot: operator, bracket, etc.)
start(Expr) -> @errors.PositionPosition of the leftmost token in an expression (walks into sub-expressions for compound LHS nodes)

#File

MethodSignatureDescription
File::new(String, Array[Stmt])-> FileConstruct from a path and statements
path()-> StringSource path
stmts()-> Array[Stmt]Top-level statements

#AST node enums

Every node carries a trailing @errors.Position.

///|
pub(all) enum Expr {
EIdent(String, @errors.Position)
ELiteral(LiteralVal, @errors.Position)
EUnary(UnaryOp, Expr, @errors.Position)
EBinary(Expr, BinaryOp, Expr, @errors.Position)
ECond(Expr, Expr, Expr, @errors.Position) // cond, then, else (surface: then if cond else else)
EIndex(Expr, Expr, @errors.Position) // a[i]
ESlice(Expr, Expr?, Expr?, Expr?, @errors.Position) // a[start:end:step]
EDot(Expr, String, @errors.Position) // x.attr
ECall(Expr, Array[Arg], @errors.Position) // f(args…)
EList(Array[Expr], @errors.Position)
ETuple(Array[Expr], @errors.Position)
EDict(Array[(Expr, Expr, @errors.Position)], @errors.Position) // (key, val, colon_pos)
ESet(Array[Expr], @errors.Position)
ELambda(Array[Param], Expr, @errors.Position)
EListComp(Expr, Array[CompClause], @errors.Position)
ESetComp(Expr, Array[CompClause], @errors.Position)
EDictComp(Expr, Expr, Array[CompClause], @errors.Position, @errors.Position) // (key, val, clauses, colon_pos, brace_pos)
}

///|
pub(all) enum Stmt {
SExpr(Expr)
SAssign(Expr, Expr, @errors.Position)
SAugAssign(Expr, AugOp, Expr, @errors.Position)
SIf(Expr, Array[Stmt], Array[Stmt], @errors.Position)
SFor(Expr, Expr, Array[Stmt], @errors.Position)
SWhile(Expr, Array[Stmt], @errors.Position)
SDef(String, Array[Param], Array[Stmt], @errors.Position)
SReturn(Expr?, @errors.Position)
SBreak(@errors.Position)
SContinue(@errors.Position)
SPass(@errors.Position)
SLoad(String, Array[(String, String, @errors.Position)], @errors.Position)
}

///|
pub(all) enum Param {
ParamIdent(String, @errors.Position) // x
ParamDefault(String, Expr, @errors.Position) // x=expr
ParamStarBare(@errors.Position) // *
ParamStarIdent(String, @errors.Position) // *args
ParamKwIdent(String, @errors.Position) // **kwargs
}

///|
pub(all) enum Arg {
ArgPos(Expr) // positional
ArgKw(String, Expr, @errors.Position) // name=expr
ArgStarArgs(Expr) // *args
ArgKwArgs(Expr) // **kwargs
}

///|
pub(all) enum CompClause {
ClauseFor(Expr, Expr, @errors.Position) // for target in iterable
ClauseIf(Expr, @errors.Position) // if guard
}

///|
pub(all) enum LiteralVal {
LitNone
LitBool(Bool)
LitInt(BigInt)
LitFloat(Double)
LitString(String)
LitBytes(Bytes)
}

///|
pub(all) enum BinaryOp {
OpAdd
OpSub
OpMul
OpDiv
OpFloorDiv
OpMod
OpBitAnd
OpBitOr
OpBitXor
OpLShift
OpRShift
OpEq
OpNe
OpLt
OpLe
OpGt
OpGe
OpIn
OpNotIn
OpAnd
OpOr
}

///|
pub(all) enum UnaryOp {
OpPlus
OpMinus
OpBitNot
OpNot
}

///|
pub(all) enum AugOp {
AugAdd
AugSub
AugMul
AugDiv
AugFloorDiv
AugMod
AugBitAnd
AugBitOr
AugBitXor
AugLShift
AugRShift
}

// Visitor wrapper passed to walk_*; one variant per node kind.

///|
pub(all) enum Node {
NFile(File)
NStmt(Stmt)
NExpr(Expr)
NParam(Param)
NArg(Arg)
NCompClause(CompClause)
}

#walk_file example

///|
test {
let pos = @errors.Position::new("<walk>", 1, 1)
let lhs = @syntax.Expr::EIdent("x", pos)
let rhs = @syntax.Expr::EIdent("y", pos)
let expr = @syntax.Expr::EBinary(lhs, @syntax.BinaryOp::OpAdd, rhs, pos)
let stmt = @syntax.Stmt::SExpr(expr)
let file = @syntax.File::new("<walk>", [stmt])
let idents : Array[String] = []
@syntax.walk_file(file, fn(node) {
match node {
Some(@syntax.Node::NExpr(@syntax.Expr::EIdent(name, _))) =>
idents.push(name)
_ => ()
}
true
})
assert_true(idents.contains("x"))
assert_true(idents.contains("y"))
}

#expr_pos example

///|
test {
let pos = @errors.Position::new("<expr>", 3, 5)
let expr = @syntax.Expr::EIdent("a", pos)
let got = @syntax.expr_pos(expr)
assert_eq(got.filename(), "<expr>")
assert_eq(got.line(), 3)
}

#
Arg

Represents a single argument at a call site.

Variants:

  • ArgPos : A positional argument expr.
  • ArgKw : A keyword argument name = expr.
  • ArgStarArgs : A *expr unpacked positional argument.
  • ArgKwArgs : A **expr unpacked keyword argument.

ArgKw, ArgStarArgs, and ArgKwArgs carry the source Position of the argument token.

#
AugOp

pub(all) enum AugOp {
AugAdd
AugSub
AugMul
AugDiv
AugFloorDiv
AugMod
AugBitAnd
AugBitOr
AugBitXor
AugLShift
AugRShift
}

Represents an augmented-assignment operator in the Starlark AST.

Each variant corresponds to one of the +=, -=, *=, /=, //=, %=, &=, |=, ^=, <<=, >>= forms.

#
BinaryOp

pub(all) enum BinaryOp {
OpAdd
OpSub
OpMul
OpDiv
OpFloorDiv
OpMod
OpBitAnd
OpBitOr
OpBitXor
OpLShift
OpRShift
OpEq
OpNe
OpLt
OpLe
OpGt
OpGe
OpIn
OpNotIn
OpAnd
OpOr
}

Represents a binary operator in the Starlark AST.

Covers arithmetic, bitwise, comparison, membership, and logical operators.

#
CompClause

Represents a single clause in a comprehension expression.

Variants:

  • ClauseFor : A for lhs in rhs iteration clause.
  • ClauseIf : A filtering if cond clause.

Each variant carries the source Position of the clause keyword.

#
Expr

Represents an expression node in the Starlark AST.

Every variant carries a source Position as its last field for error reporting. Variants:

  • EIdent : An identifier reference.
  • ELiteral : A literal value.
  • EUnary : A unary operation.
  • EBinary : A binary operation.
  • ECond : A conditional expression t if cond else f.
  • EIndex : A subscript expression obj[key].
  • ESlice : A slice expression obj[lo:hi:step]; absent bounds are None.
  • EDot : An attribute access obj.attr.
  • ECall : A function call fn(args...).
  • EList : A list display [...].
  • ETuple : A tuple display (...).
  • EDict : A dict display {k: v, ...}.
  • ESet : A set display {...}.
  • ELambda : A lambda expression lambda params: body.
  • EListComp : A list comprehension [expr for ...].
  • ESetComp : A set comprehension {expr for ...}.
  • EDictComp : A dict comprehension {k: v for ...}.

#
File

pub struct File {
// private fields
}

A parsed Starlark source file: its filename and top-level statement list.

#
File::new

fn File::new(path : String, stmts : Array[Stmt]) -> File

Constructs a File with the given source path and top-level statement list.

Parameters:

  • path : The source file path recorded in the AST.
  • stmts : The top-level statements parsed from the file.

Returns a new File AST node.

#
File::path

fn File::path(self : File) -> String

Returns the source file path.

Parameters:

  • self : The file node to inspect.

Returns the path string recorded in this File.

#
File::stmts

fn File::stmts(self : File) -> Array[Stmt]

Returns the top-level statement list.

Parameters:

  • self : The file node to inspect.

Returns the array of top-level Stmt nodes in this file.

#
LiteralVal

pub(all) enum LiteralVal {
LitInt(
BigInt
)
LitFloat(Double)
LitString(String)
LitBytes(Bytes)
}

Represents a literal value in the Starlark AST.

Variants:

  • LitInt : An integer literal.
  • LitFloat : A floating-point literal.
  • LitString : A string literal.
  • LitBytes : A bytes literal.

#
Node

pub(all) enum Node {
NFile(File)
NStmt(Stmt)
NExpr(Expr)
NParam(Param)
NArg(Arg)
NCompClause(CompClause)
}

A tagged union of every AST node kind, used as the argument to the visitor function passed to walk_file/walk_stmt/walk_expr.

#
Param

Represents a single parameter in a function definition.

Variants:

  • ParamIdent : A plain positional parameter name.
  • ParamDefault : A parameter with a default value name = expr.
  • ParamStarBare : A bare * separator (no name).
  • ParamStarIdent : A *name variadic positional parameter.
  • ParamKwIdent : A **name variadic keyword parameter.

Each variant carries the source Position of the parameter token.

#
Stmt

Represents a statement node in the Starlark AST.

Every variant (except SExpr) carries a source Position as its last field for error reporting. Variants:

  • SExpr : An expression used as a statement.
  • SAssign : A simple assignment lhs = rhs.
  • SAugAssign : An augmented assignment lhs op= rhs.
  • SIf : An if / else block; the else branch may be empty.
  • SFor : A for loop.
  • SWhile : A while loop.
  • SDef : A function definition def name(params): body.
  • SReturn : A return statement; the expression is optional.
  • SBreak : A break statement.
  • SContinue : A continue statement.
  • SPass : A pass statement.
  • SLoad : A load statement; each binding is (local, orig, pos).

#
UnaryOp

pub(all) enum UnaryOp {
OpPlus
OpMinus
OpBitNot
OpNot
}

Represents a unary operator in the Starlark AST.

Variants:

  • OpPlus : Unary +.
  • OpMinus : Unary -.
  • OpBitNot : Bitwise complement ~.
  • OpNot : Logical negation not.

#
expr_pos

Returns the source position of an expression node.

Parameters:

  • e : The expression node to inspect.

Returns the Position stored in the expression's position slot.

#
flatten_left_chain

fn[A] flatten_left_chain(expr : Expr, peel : (Expr) -> (Expr, A)?) -> (Expr, Array[A])

Iteratively peels a left-associative chain of Expr nodes, collecting a payload from each step until peel returns None.

Any walker that uses recursive descent over Expr nodes must handle the following left-recursive Expr variants iteratively (not recursively) to prevent stack overflow on deeply chained expressions:

  • EBinary — left-associative binary operations (arithmetic, comparison, in/not in, and, or). OpAnd/OpOr are encoded as EBinary with those operators, not as separate variants. Their short-circuit compilation interleaves label emission with the flatten loop, so compile_and/compile_or keep their own iterative loops rather than calling flatten_left_chain.
  • EIndex — chained subscript: a[i][j][k].
  • EDot — chained attribute access: a.b.c.d.

flatten_left_chain provides the shared primitive for walkers that collect payloads from each step. Its peel callback returns Some((left_child, payload)) when the current node matches the target kind, or None to stop.

Returns (leftmost, payloads) where payloads are in left-to-right (source-code) order.

#
start

Returns the position of the leftmost token in an expression.

Unlike expr_pos, which returns the position stored in the node's own slot (operator, bracket, etc.), start walks into sub-expressions to find the leftmost token of the whole expression. Used to anchor error messages at the beginning of a compound LHS (e.g. foobar in foobar() = 2). See the "Error-position anchoring policy" section in README.mbt.md for the full set of anchoring rules.

Parameters:

  • e : The expression node to inspect.

Returns the Position of the leftmost token.

#
stmt_pos

Returns the source position of a statement node.

Parameters:

  • s : The statement node to inspect.

Returns the Position stored in the statement's position slot.

#
walk_expr

fn walk_expr(expr : Expr, f : (Node?) -> Bool) -> Unit

Performs a depth-first walk of an expression and all its sub-expressions.

Parameters:

  • expr : The expression node to walk.
  • f : Visitor callback; receives Some(node) on entry and None on exit of each parent. Return false to skip a subtree.

#
walk_file

fn walk_file(file : File, f : (Node?) -> Bool) -> Unit

Performs a depth-first walk of a parsed Starlark file, calling f before descending into each node. If f returns false, the subtree is skipped. f(None) is called after the last child of every parent.

Parameters:

  • file : The parsed file whose nodes are visited.
  • f : Visitor callback; receives Some(node) on entry and None on exit of each parent. Return false to skip a subtree.

#
walk_stmt

fn walk_stmt(stmt : Stmt, f : (Node?) -> Bool) -> Unit

Performs a depth-first walk of a statement and all its sub-expressions.

Parameters:

  • stmt : The statement node to walk.
  • f : Visitor callback; receives Some(node) on entry and None on exit of each parent. Return false to skip a subtree.