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 three fields — and notably noreadFile:

  • 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) => stringsynchronous. 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.

What resolve does

Three walks. First, it collects every NodeDef and DataDefinto 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[]> — accumulated additive definitions.
  • 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 capturesare pushed into scope the same way, so @cap and @cap.field inside a body don't fail resolution.
  • Reserved core vocab and @node without an access pathpass 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: inlines each bound NodeUse's definition body; substitutes Interpolation (::name::) and BodySlot (); 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.

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
#tags
@li alpha li@
tags#

+#tags
@li beta li@
tags#

@ul @tags ul@
result
  • alpha
  • beta

The base #tags and the +#tags partial fold into a single merged NodeDef<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)
@p Draft — do not distribute. p@
(else)
@p Final. p@
(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 = {
  &#39;/proj/shared.wit&#39;: &#39;#brand: { name - Northlight }&#39;,
  &#39;/proj/main.wit&#39;:   &#39;reference ./shared.wit\n\n@brand.name',
};
const doc      = parse(files[&#39;/proj/main.wit&#39;], &#39;/proj/main.wit&#39;);
const resolved = resolve(doc, {
  rootPath: &#39;/proj/main.wit&#39;,
  fileReader: (abs) => files[abs],   // synchronous lookup
});
renderHtml(expand(resolved)); // → <article class="wit-doc"><p>Northlight</p></article>

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 referenceE_CIRCULAR_REFERENCE; resolvedFiles caches each file once, so a shared file parses only once.
  • A duplicate non-additive #nameE_DUPLICATE_DEFINITION; a partial whose shape disagrees with its base → E_PARTIAL_SHAPE_MISMATCH.
  • A captured param carrying a typedValue (collection, record, or scalar) is pushed as an iteration frame, so @cap.field and (each @cap) resolve inside a definition body.
  • E_NOT_ITERABLE when (each) targets a non-collection; E_MISSING_FIELD, E_MISSING_RECORD_FIELD, E_EXTRA_RECORD_FIELD, E_AMBIGUOUS_RECORD_KEY, and E_TYPE_MISMATCH for record access mismatches. 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.