Wit

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. null is 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 @@name and @@@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.

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

@cite(Weil, 1952)
result

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 indent

Token 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 a textRun of 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.content for a node body. The field is body; only ScriptBlock uses content (for its JS text).
  • Assuming @x on its own line is inline: true. It is false — a use on its own line is a block.
  • Reading params of a use of your own definition and expecting the final binding. The expander re-splits argsRaw; use the expanded tree.
  • Treating parse as total. It throws WitError; 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.