Wit

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 root parse returns. (After expansion the root is a distinct ExpandedDocument — see Resolve & expand.)

The Block union

Block = Paragraph | Comment | NodeUse | NodeDef | DataDef | ReferenceDirective | IfStatement | EachStatement | ScriptBlock

Paragraph

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 (see Param).
  • paramsSource — 'parens' | 'none': parameters came from a glued (…), or there are none.
  • argsRaw?: string — the raw text inside (…), set when paramsSource is '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 declared key:.
  • body — (Block | Inline)[] | null. null = closeless (no body); [] = empty body.
  • inline: boolean — inline in prose vs. its own block.
  • closeStyle — 'named' | 'parens' | 'bare': closed by name@ or name); ended by its (…) with no body; or a bare name with no params and no body.
  • raw?: boolean — @@name; body is one verbatim Text.
  • 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. A literal parameter's value is used as written, never read as Wit.
  • shape — 'block' | 'single-line' | 'value-block'.
  • body — (Block | Inline | Record | Collection)[].
  • additive: boolean — true for a +#name partial.

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 | EachStatement
  • Text — 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 | Collection
  • StringValue — value: string · literal?: boolean. literal is 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. typedValue is 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. value always exists; typedValue is additive.
  • AccessPath — segments: string[] · capture?: boolean, true for a ::name:: parameter used as a condition subject ((if ::met::)). It has no kind field.
  • Condition — either ExistenceCondition { path: AccessPath } or ComparisonCondition { left: AccessPath; op: 'is' | 'equals'; right: DataValue }. The parser emits only 'is'; 'equals' remains in the type but (if @x equals v) raises E_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

source
#cite(author, year)
::author:: (::year::)
cite#

As @cite(Weil, 1952) argued.
result

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 === null marks a positional parameter.
  • Record.fields order is preserved; do not reorder it.
  • typedValue is additive — param.value is always the raw string, typedValue the structured view when one applies.
  • A NodeDef.body may hold a Record or Collection (the collapse case) even though those are not Block or Inline.

Common mistakes

  • Treating Record as a JS object — it is an ordered { key, value } array.
  • Treating body: null and body: [] as the same.
  • Forgetting Comment and NodeUse live in both the Block and Inline unions.
  • Reaching for content on a normal node — only ScriptBlock has it.

See also

Parsing → the AST · Resolve & expand (what is gone post-expand) · Build your own renderer.