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) → 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 &#39;@witlang/parser&#39;;

try {
  const doc = parse(source, &#39;report.wit&#39;);
} 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(&#39;The keeper trimmed the wick _slowly_.&#39;)

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(&#39;@aside oops&#39;);
// WitError { code: &#39;E_UNCLOSED_NODE&#39;, loc: { line: 1, col: 1, … } }

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).
  • accessstring[], 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 @@name and @@@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) · fixture 05-nodes-parens.
  • 'pipes'@x |k v| · fixture 06-parameters-pipes.
  • 'record'@x { k: v } · fixture 22-record-args.
  • 'form-fill' — a multi-line k: v body · fixture 23-form-fill.
  • 'mixed' — more than one of the above on one call · fixture 26-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 &#39;@witlang/parser&#39;;
const tidy = format(messySource, { indent: &#39;    &#39; }); // 4-space indent

Side-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.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 named node on its own line is a block.
  • 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.