AST reference
Every AST kind, every field. This page exists so a tool author never has to open ast.ts. It is the authoritative shape reference for the tree that parse returns and that resolve and expand transform.
The model
Nodes are discriminated unions keyed on kind. Every node extends HasLoc ({ loc: Loc }). There are three top-level
unions — Block, Inline, and DataValue — plus
the helpers Param, AccessPath, and Condition. AstNode is the umbrella union of all of them.
Top level
Document—children: Block[]. The rootparsereturns. (After expansion the root is a distinctExpandedDocument— see Resolve & expand.)
The Block union
Block = Paragraph | Comment | NodeUse | NodeDef | DataDef | ReferenceDirective | IfStatement | EachStatement | ScriptBlockParagraph
children: Inline[] → <p>. A run of prose.
Comment
text: string · inline: boolean. A ~ comment.
Dropped by renderers unless WIT_DEBUG_COMMENTS=1.
NodeUse — the central node
name: string— the node name.access?: string[]— segments of a dotted use (@a.b.c→['b','c']after the head).params: Param[]— the arguments (seeParam).paramsSource—'parens' | 'none': parameters came from a glued(…), or there are none.argsRaw?: string— the raw text inside(…), set whenparamsSourceis'parens'. The parser splits it without knowing the definition; the expander re-splits it against the definition's declared parameters, so a value runs to the next declaredkey:.body—(Block | Inline)[] | null.null= closeless (no body);[]= empty body.inline: boolean— inline in prose vs. its own block.closeStyle—'named' | 'parens' | 'bare': closed byname@orname); ended by its(…)with no body; or a bare name with no params and no body.raw?: boolean—@@name; body is one verbatimText.frozen?: boolean—@@@name; verbatim and no{{ }}interpolation.
NodeDef
name: string— the name authors invoke with@name.captures: string[]— every::name::parameter the body uses, header or not.header?: HeaderParam[]— the declared header of#cite(author, literal url), in order:{ name: string; literal: boolean }[]. Positional values bind in this order; without a header a use passes params by name. Aliteralparameter's value is used as written, never read as Wit.shape—'block' | 'single-line' | 'value-block'.body—(Block | Inline | Record | Collection)[].additive: boolean—truefor a+#namepartial.
A single-line definition whose whole value is a literal collapses: its body becomes [Record] or [Collection] even though those kinds are not in the Block or Inline unions. That collapse case is the one time body holds a DataValue.
DataDef
name: string · value: DataValue · loadedFrom?: string. A #name: { … } !! data definition. @load nodes become DataDef nodes during loadExternalData; loadedFrom is
the alias, so several files loading the same alias make one definition, not a
duplicate.
ReferenceDirective
path: string. A reference ./other.wit line.
IfStatement
cond: Condition · then: Block[] · else?: Block[] · inline?: boolean. Evaluated to one branch by expand. A
statement written inside a line of prose is inline: each branch is
one paragraph, spliced into the line.
EachStatement
collection: AccessPath · itemName: string · body: Block[] · inline?: boolean. Unrolled by expand; each item exposes @x.index, @x.count and @x.last.
ScriptBlock
content: string (the JS text) · inline: boolean.
The Inline union
Inline = Text | Italic | Bold | Interpolation | BodySlot | NodeUse | ScriptCall | ScriptBlock | Comment | IfStatement | EachStatementText—value: string.Italic—children: Inline[]→<em>.Bold—children: Inline[]→<strong>.Interpolation—name: string. A::name::parameter in a definition body.BodySlot— no fields. The::body::slot in a definition body.ScriptCall—fnName: string·args: string[]. Args are raw source tokens — a quoted arg keeps its quotes (see Scripting).
The DataValue union
DataValue = StringValue | NumberValue | BooleanValue | NullValue | Record | CollectionStringValue—value: string·literal?: boolean.literalis set for a quoted value ("Came, H.") and for loaded data: the text is used as written, never read as markup.NumberValue—value: number·raw?: string, the number as written (0.050,3.10) for rendering.BooleanValue—value: boolean.NullValue— no fields.Record—fields: { key: string; value: DataValue }[]. An ordered array, not a JS object.Collection—items: DataValue[].
Helpers
Param—name: string | null(null= positional) ·value: string·typedValue?: DataValue.typedValueis shape-probed: it is populated when the captured text looks like[...],{...}, or a scalar literal — after expansion a reference value such as@table(rows: @sales)carries the referenced collection here.valuealways exists;typedValueis additive.AccessPath—segments: string[]·capture?: boolean, true for a::name::parameter used as a condition subject ((if ::met::)). It has nokindfield.Condition— eitherExistenceCondition { path: AccessPath }orComparisonCondition { left: AccessPath; op: 'is' | 'equals'; right: DataValue }. The parser emits only'is';'equals'remains in the type but(if @x equals v)raisesE_MALFORMED_STATEMENT.Loc—{ file, line, col, offset, length }·HasLoc—{ loc: Loc }.
Worked shapes
Each below is a one-line source and the trimmed JSON it parses to, lifted from the fixture that verifies the shape.
DataDef + Record
#author: { name: Mara Finch } !!
{ "kind": "dataDef", "name": "author",
"value": { "kind": "record",
"fields": [ { "key": "name",
"value": { "kind": "stringValue", "value": "Mara Finch" } } ] } }Fixture 09-records.
Collection
#tags: [ a, b ] !!
{ "kind": "dataDef", "name": "tags",
"value": { "kind": "collection", "items": [
{ "kind": "stringValue", "value": "a" },
{ "kind": "stringValue", "value": "b" } ] } }Fixture 10-collections.
EachStatement
(each @team as p) (li @p.name li) (end)
{ "kind": "eachStatement", "itemName": "p",
"collection": { "segments": ["team"] },
"body": [ { "kind": "nodeUse", "name": "li", … } ] }Fixtures 13-iteration and 12-conditionals (for IfStatement).
Header and positional use
#cite(author, year) ::author:: (::year::) cite# As @cite(Weil, 1952) argued.
As Weil (1952) argued.
{ "kind": "nodeDef", "name": "cite", "captures": ["author", "year"],
"header": [ { "name": "author", "literal": false },
{ "name": "year", "literal": false } ], "shape": "block", … }
{ "kind": "nodeUse", "name": "cite", "paramsSource": "parens",
"argsRaw": "Weil, 1952",
"params": [ { "name": null, "value": "Weil" },
{ "name": null, "value": "1952",
"typedValue": { "kind": "numberValue", "value": 1952, "raw": "1952" } } ],
"body": null, "inline": true, … }The parser leaves positional values unnamed (name: null); the
expander names them in header order. body: null because cite has no ::body::, so the use needs no close.
After expansion no cite node remains — its body is inlined.
Edges
NodeUse.body === null(closeless) is distinct from[](empty body). Branch on both.Param.name === nullmarks a positional parameter.Record.fieldsorder is preserved; do not reorder it.typedValueis additive —param.valueis always the raw string,typedValuethe structured view when one applies.- A
NodeDef.bodymay hold aRecordorCollection(the collapse case) even though those are notBlockorInline.
Common mistakes
- Treating
Recordas a JS object — it is an ordered{ key, value }array. - Treating
body: nullandbody: []as the same. - Forgetting
CommentandNodeUselive in both theBlockandInlineunions. - Reaching for
contenton a normal node — onlyScriptBlockhas it.
See also
Parsing → the AST · Resolve & expand (what is gone post-expand) · Build your own renderer.