core.rs defines the Evaluator struct — the central object that owns all
mutable state and implements macro expansion. It exposes two evaluation
paths (plain String and EvalOutput-sink) and handles macro call dispatch,
scope management, %include processing, and lexical closure (freeze).
Design rationale
Two evaluation paths sharing one scope
evaluate(node) → String and evaluate_to(node, &mut dyn EvalOutput) cover
the same AST node types but differ in how they emit text. Both paths push and
pop the same scope stack and execute the same parameter-binding logic. The
duplication in evaluate_macro_call / evaluate_macro_call_to is real but
bounded: the tracing path adds per-argument SpanRange threading that has no
counterpart in the plain path.
evaluate_to_with_context: threading MacroBody attribution
When evaluating a macro body on the tracing path, literal text tokens inside
the body need to be attributed to SpanKind::MacroBody rather than
SpanKind::Literal. A context_span parameter threads this annotation down
the recursive calls without changing the token’s own source position. Only
the kind field of the span is overridden.
Multi-line text tokens: splitting at newlines
A single Text token in a macro body may span multiple lines (the lexer groups
all literal bytes between two macro calls). Emitting it as one span would give
the tracer a single position for all those lines. Instead, evaluate_to
splits the text at \n boundaries and emits each segment with an adjusted
pos (byte offset within the token), so every line resolves to its true source
line.
freeze_macro_definition: poor-man’s closure
%export captures the exported macro’s body AST and walks it looking for Var
nodes that are not declared parameters. Their current values are copied into
frozen_args. On a later call to the exported macro, frozen_args are
installed first in the new scope — before parameter binding — so the macro
sees the same variable values it had at export time. This is a shallow
snapshot, not a full closure chain.
do_include cleanup on error
do_include inserts the resolved path into open_includes before recursing,
and removes it afterwards — even if the include raises an error — via a
dedicated result-processing closure. This prevents a failing include from
permanently blocking future includes of the same file (regression guard for
bug #6).
Evaluation dispatch overview
File structure
// <<@file weaveback-macro/src/evaluator/core.rs>>=
// <<core preamble>>
// <<evaluator struct>>
// <<evaluator new and accessors>>
// <<evaluator rhai store>>
// <<evaluator py store>>
// <<evaluator macro and var>>
// <<evaluator source and file>>
// <<evaluator plain evaluate>>
// <<evaluator node text>>
// <<evaluator extract name>>
// <<evaluator macro call plain>>
// <<evaluator export and freeze>>
// <<evaluator parse string and find file>>
// <<evaluator do include>>
// <<evaluator tracing helpers>>
// <<evaluator evaluate to>>
// <<evaluator macro call to>>
// @
Preamble
// <<core preamble>>=
// crates/weaveback-macro/src/evaluator/core.rs
use ;
use fs;
use ;
use Arc;
use ;
use ;
use MontyEvaluator;
use ;
use ;
use ;
use crate;
// @
Evaluator struct
// <<evaluator struct>>=
// @
Constructor and accessors
// <<evaluator new and accessors>>=
Rhai store
The Rhai store persists typed rhai::Dynamic values across %rhaidef calls.
Numeric strings are auto-promoted to i64 or f64 so that arithmetic works
inside scripts without explicit conversion. rhaistore_set_expr uses the Rhai
engine itself to evaluate a typed initialiser (e.g. [] for an array).
// <<evaluator rhai store>>=
/// Insert a value into the Rhai store.
/// Integers and floats are stored with their native Rhai type so that
/// arithmetic operators work inside scripts without explicit conversion.
/// Evaluate a Rhai expression and store the resulting Dynamic value.
/// Use this to initialise store entries with typed literals like `[]` or `#{}`.
/// Read a value from the Rhai store, converting it to String.
// @
Python store
// <<evaluator py store>>=
// @
Macro and variable delegation
These thin methods forward to EvaluatorState and also handle call-site
recording for the tracing maps.
// <<evaluator macro and var>>=
// @
Source and file management
// <<evaluator source and file>>=
// @
Plain evaluate path
The plain path returns a String. Comments are silently dropped. All other
node kinds recurse over their children.
// <<evaluator plain evaluate>>=
// @
node_text and extract_name_value
These helpers slice the raw source bytes using the token’s pos/length.
node_text strips the surrounding delimiters for Macro (%name(`→`name),
Var (%(name)`→`name), BlockOpen/BlockClose, and Special tokens.
extract_name_value returns the raw bytes for a plain Ident token (no
stripping needed).
// <<evaluator node text>>=
// @
// <<evaluator extract name>>=
// @
evaluate_macro_call — plain path
The plain path uses Python-style parameter binding (see
state.adoc design rationale for full rules). After binding,
evaluate(&mac.body) is called, then the result is passed through the script
engine if script_kind is Rhai or Python.
// <<evaluator macro call plain>>=
// @
%export and freeze_macro_definition
export copies one binding from the current (innermost) frame into the parent
frame. Both variable and macro bindings are supported. Macros are passed
through freeze_macro_definition first so they carry a snapshot of any free
variables from the exporting scope.
collect_freeze_vars walks the body AST, finds Var nodes that are not
declared parameters, and evaluates them in the current scope, capturing their
current values. The result is stored in frozen_args.
// <<evaluator export and freeze>>=
// @
parse_string, find_file, and do_include
parse_string reads (or re-uses) the source bytes from the SourceManager,
then chains lex_parse_content. If the path points to an existing file the
file is registered by canonical path (deduplication); otherwise the in-memory
bytes are registered directly (for string-only evaluations in tests).
find_file searches the configured include_paths in order, returning the
first match. Absolute paths are accepted as-is.
do_include tracks currently-open includes in open_includes to detect
cycles. The path is always removed on exit — whether the include succeeds or
fails — to prevent a failed include from permanently blocking re-includes.
// <<evaluator parse string and find file>>=
// @
// <<evaluator do include>>=
/// Return (and clear) the list of paths recorded during a discovery-mode run.
// @
Tracing helpers
These private helpers are used exclusively by evaluate_to and
evaluate_macro_call_to. span_of builds a SourceSpan from an AST node’s
token with SpanKind::Literal. evaluate_arg_to_traced evaluates one
argument into a PreciseTracingOutput to get both the value string and its
SpanRange list. tag_as_macro_arg re-tags those spans with
MacroArg { macro_name, param_name }.
// <<evaluator tracing helpers>>=
// ---- Tracked evaluation (EvalOutput) ------------------------------------
/// Build a `SourceSpan` from the token of an AST node, defaulting to Literal.
/// Evaluate `node` into a `(String, Vec<SpanRange>)` for argument threading.
/// Called only on the tracing path (`out.is_tracing() == true`).
/// Re-tag `raw_spans` to `MacroArg { macro_name, param_name }`.
/// If `raw_spans` is empty but `val` is non-empty, creates a single coarse span
/// from `param_node` so the tracer can still identify the parameter.
// @
evaluate_to — tracing path
evaluate_to and its internal evaluate_to_with_context mirror evaluate
but push text to an EvalOutput sink rather than accumulating a String.
The context_span parameter carries MacroBody attribution down the recursion:
literal text tokens in the body inherit the macro name from the context span
while keeping their own pos/length for exact line resolution.
Multi-line Text tokens are split at \n boundaries, each segment emitted
with its own adjusted pos within the original token, so every output line
maps to the correct source line.
// <<evaluator evaluate to>>=
/// Like `evaluate`, but writes to an `EvalOutput` sink so that span
/// information is available to the caller.
/// Internal evaluation method that accepts an optional `context_span` prefix.
/// This is used to thread `MacroBody` attribution down the evaluation tree.
// @
evaluate_macro_call_to — tracing path for macro calls
Built-in macro calls are delegated to the plain evaluate_macro_call.
If the result is non-empty it is pushed with SpanKind::Computed so the
tracer knows the output line and byte range even though the content was
produced programmatically. Builtins that return "" (%set, %def,
%include, …) produce no output, so the is_empty() guard is a no-op
for them.
User-defined macros go through the full parameter-binding and tracing
machinery. The body is evaluated with a MacroBody context span;
script-kind macros use the same note as in the plain path — they arrive
through the builtin map, so the ScriptKind::Rhai / ScriptKind::Python
branches here are currently unreachable.
// <<evaluator macro call to>>=
/// Like `evaluate_macro_call`, but writes to an `EvalOutput` sink.
}
// @