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, options?: ParseOptions) → 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 (20 codes) with a .code and a .loc.
Wrap it.
ParseOptions = { closeless?: Iterable<string> } — names defined
without ::body:: in other files of the reference graph,
so their uses in this file need no close. The parser already knows the
definitions in its own file and the void names br, hr, img. Get the set from scanCloseless(rootPath, source) in @witlang/runtime; a single-file document never needs 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) — or the same use written @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 use that fills its own line is inline: false —
being on its own line is what makes it a block. closeStyle is 'named' because a closing tag ended it — aside@ or aside); the AST does not record which. 'parens' means the use ended at its (…) with no body; 'bare' means a name alone, such as @hr or a bare reference.
Edge — a parse error
A use that opens a block and never closes throws:
parse('@aside\noops');
// WitError { code: 'E_UNCLOSED_NODE', loc: { line: 1, col: 1, … } }Inside a sentence the same use is closeless instead: Note @aside here. parses to an empty inline aside followed by the text here. — no error.
NodeUse — the node you branch on most
NodeUse is 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 how they were written (see below).body—(Block | Inline)[] | null.nullis closeless (no body);[]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 and argsRaw
There is one parameter syntax, so paramsSource has two values:
'parens'—@img(src: a.png, alt: A lamp).'none'— no parameters.
With parens, argsRaw holds the text inside them verbatim
("src: a.png, alt: A lamp") and params holds a first
split. The parser does not know the definition yet, so that split guesses where
each value ends; the expander re-splits argsRaw against the
definition's declared parameters, so a value runs to the next declared key: and may hold commas and colons. Read params for
core nodes, whose parameters are known up front; for your own definitions,
the expanded tree has already applied them.
#cite(author, year) ::author:: (::year::) cite# @cite(Weil, 1952)
Weil (1952)
Positional values parse with name: null: params: [ { name: null, value: "Weil" }, { name: null, value: "1952", typedValue: … } ].
The header on the definition, header: [ { name: "author", literal: false }, … ],
names them at expansion.
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 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 indentToken streams
lex(source, file)— the raw lexer output. The lexer recognises markers anywhere, so an email's@or a lone!!arrive as structure.parserTokens(source, file, closeless?)— what the parser actually walks: the stray-token pass has turned every marker no open construct can use back into atextRunof its exact source text.
lex('Mail a@b.org now!!', 'x').map(t => t.kind);
// ['textRun', 'nodeClose', 'textRun', 'bangBang', 'eof']
parserTokens('Mail a@b.org now!!', 'x').map(t => t.kind);
// ['textRun', 'eof']wit tokens -e '…' and wit tokens --parser -e '…' print the
same two streams. closelessNames(tokens) returns the closeless names
a token stream defines.
Side-door parsers
Three exports parse a fragment without a whole document. They exist because the expander re-parses parameter text — turning a value 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 use on its own line is a block. - Reading
paramsof a use of your own definition and expecting the final binding. The expander re-splitsargsRaw; use the expanded tree. - 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.