Parsing → the AST
parse() is the only way in. Every tool built on Wit — a linter, an
LSP, a codemod, a custom renderer — starts by calling it and branching on the
tree's kind discriminators. This page covers the signature, the
shape of what comes back, the side-door parsers for fragments, and the
formatter.
Signature
parse(source: string, file?: string) → Document. The file argument defaults to '<inline>' and only feeds loc.file on every node — it does not read the file for you; you pass
the text. parse is not total: it throws WitError (12 codes) with a .code and a .loc.
Wrap it.
import { parse, WitError } from '@witlang/parser';
try {
const doc = parse(source, 'report.wit');
} catch (err) {
if (err instanceof WitError) {
console.error(`${err.code} at ${err.loc.line}:${err.loc.col}`);
}
}The universal node shape
Every node is a tagged-union member: a kind string plus a loc. Loc is { file, line, col, offset, length } — line and col are 1-based; offset and length index
into the source string. There are three top-level unions — Block, Inline, and DataValue — plus helpers
(Param, AccessPath, Condition). The AST reference documents every field; this
page is the map.
Basic — prose parses to inline children
parse('The keeper trimmed the wick _slowly_.')
The document has one Paragraph whose children are three
inline nodes, each carrying its own loc (trimmed here):
{ "kind": "paragraph", "children": [
{ "kind": "text", "value": "The keeper trimmed the wick " },
{ "kind": "italic", "children": [ { "kind": "text", "value": "slowly" } ] },
{ "kind": "text", "value": "." }
] }Realistic — a node use
@aside good aside@ parses to a single NodeUse:
{ "kind": "nodeUse", "name": "aside", "params": [], "paramsSource": "none",
"body": [ { "kind": "paragraph",
"children": [ { "kind": "text", "value": " good " } ] } ],
"inline": false, "closeStyle": "named" }Two things to note. A named node on its own line is inline: false —
being on its own line, not being named, is what makes it a block. closeStyle is 'named' because it closed with aside@ rather than parentheses or a bare newline.
Edge — a parse error
@aside oops with no close throws:
parse('@aside oops'); // WitError { code: 'E_UNCLOSED_NODE', loc: { line: 1, col: 1, … } }
NodeUse — the node you branch on most
NodeUseis the center of gravity. Its fields, at a glance:
name— the node name ('aside','table', your definition's name).access—string[], present for a dotted use like@author.name; absent otherwise.params/paramsSource— the parameters and howthey were written (see below).body—(Block | Inline)[] | null.nullis self-closing;[]is an empty body. They are not the same — see the AST reference.inline— whether it sits inline in prose or stands as a block.closeStyle—'named','parens', or'bare'.raw/frozen— set by@@nameand@@@name(verbatim / no interpolation).
paramsSource — how the arguments were written
paramsSource records the surface syntax, which a formatter or
codemod needs even though params holds the same values:
'parens'—@x(k v)· fixture05-nodes-parens.'pipes'—@x |k v|· fixture06-parameters-pipes.'record'—@x { k: v }· fixture22-record-args.'form-fill'— a multi-linek: vbody · fixture23-form-fill.'mixed'— more than one of the above on one call · fixture26-all-param-forms.'none'— no parameters.
raw and frozen
@@name … name@@ sets raw: true: the body is a single
verbatim Text child, not re-parsed. @@@name also sets frozen: true: verbatim and no {{ }} interpolation. This is how @@style and @@@pre carry CSS or literal source untouched. Fixture 20-opaque-node and the
raw-node parser tests.
The formatter
format(source: string, opts?: FormatOptions) → string, where FormatOptions = { indent: string } (default two spaces). It is a structural re-indenter, not a pretty-printer: it rewrites only
leading whitespace and never reflows prose. Raw @@ bodies, <% %> scripts, and record / form-fill values are left verbatim.
It backs wit fmt and the VS Code formatter.
import { format } from '@witlang/parser';
const tidy = format(messySource, { indent: ' ' }); // 4-space indentSide-door parsers
Three exports parse a fragment without a whole document. They exist because the expander re-parses captured text — turning a captured string back into emphasis nodes at interpolation time — and a tool author can reuse them:
parseInlineFromText(source, file?='<interpolation>') → Inline[]— parse a snippet of prose (text, emphasis, inline nodes) with no enclosing paragraph.tryParseRecordFromText(src, base: Loc) → { record, endPos } | null— probe whether a string starts with a{ … }record.tryParseCollectionFromText(src, base: Loc) → { collection, endPos } | null— the same for a[ … ]collection.
The two try… functions return null rather than throwing
when the text is not a literal, which is what makes them safe to probe with.
wit parse
From the terminal, wit parse file.wit prints the whole AST as JSON —
the fastest way to see what a construct parses to before you write code against
it.
Common mistakes
- Reading
node.contentfor a node body. The field isbody; onlyScriptBlockusescontent(for its JS text). - Assuming
@xon its own line isinline: true. It isfalse— a named node on its own line is a block. - Treating
parseas total. It throwsWitError; a tool that does not catch it crashes on the first malformed input.
See also
AST reference(the field tables) · Resolve & expand · Errors reference · Scripting.