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 anyreferencedirective, or resolve throwsE_MISSING_REFERENCE_FILEup 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. Returnnullto 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
@@nameraw 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 owncapturesare pushed into scope the same way, so@capand@cap.fieldinside a body don't fail resolution. - Reserved core vocab and
@nodewithout an access pathpass through with no binding. - A bare
@xresolves toNodeDef x(falling back toDataDef x); a dotted@x.yresolves toDataDef xfirst (falling back toNodeDef). - The first unresolved use throws
E_UNRESOLVED_REFERENCEat itsloc.
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
#greeting Hello from a def. greeting# @greeting
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
#tags @li alpha li@ tags# +#tags @li beta li@ tags# @ul @tags ul@
- 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
#team: [ { name - Ada, role - lead } { name - Béla, role - ic } ] @ul (each @team as p) @li @p.name — @p.role li@ (end) ul@
- Ada — lead
- Béla — ic
→ <ul><li>Ada — lead</li><li>Béla — ic</li></ul>. Fixture 13-iteration.
Realistic — conditional
#book: { status - draft } (if @book.status is draft) @p Draft — do not distribute. p@ (else) @p Final. p@ (end)
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 }',
'/proj/main.wit': 'reference ./shared.wit\n\n@brand.name',
};
const doc = parse(files['/proj/main.wit'], '/proj/main.wit');
const resolved = resolve(doc, {
rootPath: '/proj/main.wit',
fileReader: (abs) => files[abs], // synchronous lookup
});
renderHtml(expand(resolved)); // → <article class="wit-doc"><p>Northlight</p></article>Every option / edge
onMissingReferencereturningnullsilently skips the missing file; throwing from it restores strict resolution.rootPathabsent with areferencepresent →E_MISSING_REFERENCE_FILE.- A circular
reference→E_CIRCULAR_REFERENCE;resolvedFilescaches 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 captured param carrying a
typedValue(collection, record, or scalar) is pushed as an iteration frame, so@cap.fieldand(each @cap)resolve inside a definition body. E_NOT_ITERABLEwhen(each)targets a non-collection;E_MISSING_FIELD,E_MISSING_RECORD_FIELD,E_EXTRA_RECORD_FIELD,E_AMBIGUOUS_RECORD_KEY, andE_TYPE_MISMATCHfor record access mismatches. Fixtures11-data-accessand16-ambiguity.- An unknown
@name(no definition, no data, not core vocab) →E_UNRESOLVED_REFERENCEat resolve, before render. Contrast a surviving unresolved access path, which the renderer shows as a visible span (see Rendering).
Common mistakes
- Passing an
asyncfileReader— silently wrong. - Handing a
ResolvedDocumentto a renderer; renderers expect anExpandedDocument. - Expecting
dataDefsto still be in the expanded tree — they are consumed. - Expecting
bindingsto cover inlined clones —expandre-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.