Wit

Resolve & expand

Everything between "I parsed a file" and "I can render it" happens in two runtime passes. resolve is binding — it answers "what does @x mean?", tying every use to the definition or data it names and merging partials and cross-file references. expand is evaluation — it produces the final tree: inlining bodies, substituting interpolations and body slots, running conditionals and iteration, and running scripts. A renderer author must know exactly what survives.

resolve

resolve(doc: Document, options?: ResolveOptions) → ResolvedDocument.

ResolveOptions has five fields, all optional — and notably no readFile:

  • rootPath?: string — the absolute path of the document. Required if the document has any reference directive, or resolve throws E_MISSING_REFERENCE_FILE up front.
  • fileReader?: (absPath: string) => string — synchronous. How referenced files are read; defaults to a real filesystem reader. Supply your own for a virtual FS or in-memory documents.
  • onMissingReference?: (path: string) => null | void — called instead of throwing when a referenced file is missing. Return null to skip it, or throw to restore strict behaviour. For in-progress documents.
  • closeless?: ReadonlySet<string> — the graph's closeless names, from scanCloseless, used when parsing each referenced file. Pass the same set you gave parse.
  • prepare?: (doc: Document) => Document — applied to each referenced file right after it is parsed. The CLI passes (d) => loadExternalData(d, loader) so a chapter's @load lines load too.

scanCloseless(rootPath: string, rootSource: string, reader?: FileReader) → Set<string> walks the reference graph at token level (no full parse) and returns every name defined without ::body::, plus br, hr, img. Unreadable or unparseable files are skipped; the real parse reports them.

What resolve does

Three walks. First, it collects every NodeDef and DataDef into name-keyed maps — a duplicate non-additive name throws E_DUPLICATE_DEFINITION; additive +#x partials accumulate. Second, it merges referenced files' definitions and data in. Third, it walks every NodeUse and records a binding.

ResolvedDocument

  • children — the immutable parser AST (unchanged).
  • definitions: Map<string, NodeDef> · dataDefs: Map<string, DataDef>.
  • bindings: Map<NodeUse, Binding> — the side table tying uses to what they mean.
  • partials: Map<string, NodeDef[]> — a working table: entries are folded into definitions as a MergedNodeDef (with entries and partialSources) and the map is cleared, so it is empty on the returned document.
  • references: Set<string> · resolvedFiles: Map<string, ResolvedDocument> — the cross-file graph, resolving every referenced file exactly once.
  • unresolvedAt: Loc[] — reserved; always empty today.

Binding rules

  • A @@name raw node records no binding — it is an opaque carrier and its body is verbatim.
  • An iteration item name in scope ((each … as X)) resolves at expand time, so no binding is recorded. A definition's own parameters are pushed into scope the same way, so @person.name and (each @people as p) inside #card(person, people) don't fail resolution.
  • Reserved core vocab and @node without an access path pass through with no binding.
  • A bare @x resolves to NodeDef x (falling back to DataDef x); a dotted @x.y resolves to DataDef x first (falling back to NodeDef).
  • The first unresolved use throws E_UNRESOLVED_REFERENCE at its loc.

expand

expand(resolved: ResolvedDocument) → ExpandedDocument. It walks the resolved tree and, in order: binds each use's parameters — re-splitting argsRaw against the definition's header (or, without one, its captures) and naming positional values in header order; inlines the definition body; substitutes Interpolation (::name::) and BodySlot (::body::); evaluates each IfStatement to one branch; unrolls each EachStatement; resolves ref-valued params onto param.typedValue; then builds the lh bridge and runs the scripts.

ExpandedDocument = { kind: 'expanded-document', children: Block[], loc }.

What is gone after expand

nodeDef, dataDef, reference, interpolation, bodySlot, and the evaluated if / each (their chosen blocks are spliced in). A survivor of any of those signals an expansion failure — a renderer may treat it as an error or warning. Recursive definitions throw E_EXPANSION_DEPTH_LIMIT at depth 256. Parameter binding throws E_POSITIONAL_WITHOUT_HEADER, E_TOO_MANY_ARGUMENTS, E_UNKNOWN_PARAM, E_MISSING_PARAM or, for a parameter missing its colon, E_REMOVED_SYNTAX.

Basic — a definition and a use

source
#greeting
Hello from a def.
greeting#

@greeting
result

Hello from a def.

resolve reports definitions: ['greeting'] and bindings.size: 1; expand leaves children: ['paragraph'] (the definition inlined and gone); render → <p>Hello from a def.</p>.

Realistic — additive partials

source
@ul
(each @tags as t)
(li @t li)
(end)
ul@

+#tags: alpha !!
+#tags: beta !!
result
  • alpha
  • beta

The resolver collects every +#tags entry — from this file and every referenced file, each once, in the order met — into one MergedNodeDef in definitions; its entries are what the loop walks → <ul><li>alpha</li><li>beta</li></ul>. Fixture 08-additive-partials.

Realistic — iteration

source
#team: [
  { name: Ada, role: lead }
  { name: Béla, role: ic }
] !!

@ul
(each @team as p)
(li @p.name — @p.role li)
(end)
ul@
result
  • Ada — lead
  • Béla — ic

→ <ul><li>Ada — lead</li><li>Béla — ic</li></ul>. Fixture 13-iteration.

Realistic — conditional

source
#book: { status: draft } !!

(if @book.status is draft)
Draft — do not distribute.
(else)
Final.
(end)
result

Draft — do not distribute.

The draft branch renders; the else branch is dropped entirely. Fixture 12-conditionals.

Edge — cross-file references, in memory

Because fileReader is just a (absPath) => string, you can resolve a document whose reference points at a file that exists only in memory:

const files = {
  '/proj/shared.wit': '#brand: { name: Northlight } !!\n\n#sig\n— the Northlight team\nsig#',
  '/proj/main.wit':   'reference ./shared.wit\n\n@brand.name\n\n@sig\nand friends.',
};
const reader    = (abs) => files[abs];                  // synchronous lookup
const closeless = scanCloseless('/proj/main.wit', files['/proj/main.wit'], reader);
const doc       = parse(files['/proj/main.wit'], '/proj/main.wit', { closeless });
const resolved  = resolve(doc, { rootPath: '/proj/main.wit', fileReader: reader, closeless });
renderHtml(expand(resolved));
// → <article class="wit-doc"><p>Northlight</p><p>— the Northlight team and friends.</p></article>

Without closeless, parse cannot know that sig takes no body and throws E_UNCLOSED_NODE at the @sig line.

Every option / edge

  • onMissingReference returning null silently skips the missing file; throwing from it restores strict resolution.
  • rootPath absent with a reference present → E_MISSING_REFERENCE_FILE.
  • A circular reference → E_CIRCULAR_REFERENCE; resolvedFiles caches each file once, so a shared file parses only once.
  • A duplicate non-additive #name → E_DUPLICATE_DEFINITION; a partial whose shape disagrees with its base → E_PARTIAL_SHAPE_MISMATCH.
  • A parameter value carrying a typedValue (collection, record, or scalar) is pushed as an iteration frame, so with @card(person: @team.0) the body's @person.name and (each @person.tags as t) resolve.
  • E_NOT_ITERABLE when (each) targets a non-collection; E_AMBIGUOUS_RECORD_KEY when two keys of a record match the same loose key. A path to a missing field is not an error: it renders as a visible wit-unresolved span. Fixtures 11-data-access and 16-ambiguity.
  • An unknown @name (no definition, no data, not core vocab) → E_UNRESOLVED_REFERENCE at resolve, before render. Contrast a surviving unresolved access path, which the renderer shows as a visible span (see Rendering).

Common mistakes

  • Passing an async fileReader — silently wrong.
  • Handing a ResolvedDocument to a renderer; renderers expect an ExpandedDocument.
  • Expecting dataDefs to still be in the expanded tree — they are consumed.
  • Expecting bindings to cover inlined clones — expand re-resolves by name, by design, so the side table does not list them.

See also

AST reference · External data (runs before resolve) · Rendering · Scripting (the tail of expand) · Errors reference.