output.rs defines the EvalOutput trait and its three implementations,
the span types used to attribute output text back to source tokens, and the
MacroMapEntry record stored in the macro_map database table.
Design rationale
The EvalOutput trait: pluggable output sinks
The evaluator calls push_str(text, span) for every piece of tracked text and
push_untracked(text) for computed/script results. Decoupling the sink from
the evaluator allows three strategies with no runtime overhead in the common
case:
-
PlainOutput -
Ignores span arguments entirely.
push_stris#[inline]and compiles to a singleString::push_str. This is the default path and has zero overhead compared to the oldString-based accumulator. -
TracingOutput -
Records one
SourceSpanper output line (the first tracked span on each line wins). Used when building themacro_mapdatabase table. Allocations are proportional to line count rather than token count — much cheaper than per-push recording. -
PreciseTracingOutput -
Records one
SpanRangeper source-token transition. Provides exact byte attribution for every character in the output. Used by the MCPweaveback_apply_fixoracle to verify that a source edit produces the expected output.
SpanKind: why was this text produced?
Knowing where a piece of output came from is useful; knowing how it was
produced is essential for the apply-back tool. SpanKind distinguishes:
-
Literal— raw text from the document or a literal block. -
MacroBody— text from expanding a macro body. -
MacroArg— text substituted from a call-site argument. -
VarBinding— text from a%setvariable. -
Computed— script/builtin result with no direct source token.
TracingOutput first-wins-per-line semantics
For each output line, the first push_str call that carries a span sets the
line’s span. Subsequent tracked pushes on the same line are ignored (span
already set). Untracked pushes advance line counters without setting a span.
This models the most common query: "what source line corresponds to output line
N?" The first token on the line is the best answer.
Output sink types
File structure
// <<@file weaveback-macro/src/evaluator/output.rs>>=
// <<output span kind>>
// <<output source span>>
// <<output eval output trait>>
// <<output plain output>>
// <<output tracing output>>
// <<output macro map entry>>
// <<output tracing into macro map>>
// <<output span range>>
// <<output precise tracing output>>
// @
SpanKind — classification of how output was produced
// <<output span kind>>=
// crates/weaveback-macro/src/evaluator/output.rs
/// Indicates how a piece of output relates to the original source.
// @
SourceSpan — byte-offset reference into a source file
SourceSpan mirrors the fields of Token (src, pos, length) so that
no conversion is needed when creating a span from an AST node’s token.
Line and column numbers are derived on demand via LineIndex —
they are not cached here to keep the struct small.
// <<output source span>>=
/// Byte-offset span referencing the source token that produced a piece of output.
///
/// Fields mirror `Token.src`, `Token.pos`, `Token.length` — no conversion needed.
/// Line/col can be derived on demand via `LineIndex`.
// @
EvalOutput trait
push_str is the hot path — called for every literal text token and every
macro argument that expands to non-empty text. push_untracked is called for
built-in results and script outputs. finish consumes the accumulator and
returns the assembled string. is_tracing signals whether the caller should
invest the extra effort of per-argument span threading.
// <<output eval output trait>>=
/// Generic output sink for the evaluator.
///
/// The evaluator calls `push_str` for every piece of text it produces,
/// providing the `SourceSpan` of the token that generated it.
/// `push_untracked` is used for text whose origin cannot be attributed to
/// a single source span (e.g. Rhai/Python script results).
// @
PlainOutput — zero-overhead fast path
// <<output plain output>>=
/// Fast-path output accumulator — ignores span info, just collects text.
///
/// This is functionally identical to the existing `String`-based output in
/// `Evaluator::evaluate()`. Zero overhead: span arguments are discarded.
// @
TracingOutput — per-line source attribution
TracingOutput records one optional SourceSpan per completed output line.
When a \n byte is encountered inside a push_str call, advance_line
moves the current span into line_spans. If the text continues after the
\n on the same call (a multi-line literal), the span is propagated to the
next line so intermediate lines are not left unattributed.
// <<output tracing output>>=
/// Output accumulator that records one source span per output line.
///
/// For each completed output line the first tracked `push_str` span on that
/// line is stored. Untracked pushes (Rhai results, builtins) advance the line
/// counter but do not contribute a span.
///
/// This is much cheaper than recording per-push-call byte offsets: allocations
/// are proportional to line count rather than token count.
// @
|
Note
|
MacroMapEntry — database record
// <<output macro map entry>>=
/// A serialized entry stored in the `macro_map` database table.
/// It maps an output line (indirectly via the table key) to the original
/// `.md` source file that generated it.
// @
TracingOutput::into_macro_map_entries
Converts the per-line span records into MacroMapEntry values. The final
open line (if the output does not end with \n) is included via a chained
iterator. Lines with no tracked span are silently skipped.
// <<output tracing into macro map>>=
use crateSourceManager;
use crateLineIndex;
// @
SpanRange — a contiguous attributed byte range
SpanRange is used by PreciseTracingOutput and by TrackedValue.spans.
The start/end fields index into the output buffer (or variable value);
span gives the source token that produced those bytes.
// <<output span range>>=
/// A contiguous byte range in the output attributed to one source token.
/// Gaps (script/builtin results) are absent from the list.
// @
PreciseTracingOutput — exact per-byte attribution
PreciseTracingOutput coalesces consecutive pushes from the same token into a
single SpanRange (same src`+`pos`+`length). flush_current is called
when the token changes or on push_untracked. Gaps (untracked segments) are
simply absent from the ranges list — callers use span_at_byte with a
binary search to query the coverage.
// <<output precise tracing output>>=
/// Output accumulator with exact per-byte source attribution.
///
/// Records one `SpanRange` entry per source-token transition — far fewer
/// entries than bytes, and no granularity tradeoff.
///
/// Use `into_parts()` to obtain `(output_string, Vec<SpanRange>)`.
/// Use `span_at_byte` to query which span covers a given byte offset.
// @