builtins.rs registers all built-in macros as function pointers in a
HashMap. case_conversion.rs provides the Case enum, a word-splitting
iterator, and the convert_case function used by the case-transformation
builtins. source_utils.rs provides modify_source, the low-level routine
used by %here.
Design rationale
Function-pointer dispatch table
Built-ins are stored as fn(&mut Evaluator, &ASTNode) → EvalResult<String>
function pointers — not closures — because they receive the Evaluator by
&mut reference and so cannot capture it. The HashMap<String, BuiltinFn>
is populated once at construction time by default_builtins().
Checking the builtin map before the user-macro scope ensures that built-in
names (def, set, if, …) are reserved and cannot be shadowed.
Shared define_macro helper
%def, %rhaidef, and %pydef have identical argument-parsing logic (two or
more params: name, optional formal params, body). A DefMacroConfig struct
carries the variant-specific error messages and ScriptKind, and a shared
define_macro function uses it. This avoids triplicating 40 lines of
parameter-extraction code.
single_ident_param: strict identifier validation
Many built-ins require an argument to be a plain identifier (no spaces, no
commas, no =, no leading digit). single_ident_param enforces this.
Accepting =-style params here would silently interpret %def(foo, x=y, body)
as a named argument assignment rather than a parameter name, which would be
confusing.
Allowed identifier charset (enforced jointly by the lexer and this validator):
-
Start:
[A-Za-z_]— the lexer never emits anIdenttoken whose first byte is a digit or a special character. -
Continue:
[A-Za-z0-9_]— digits are allowed after the first character. -
single_ident_paramadditionally rejects names that start with an ASCII digit even if the text somehow arrived as anIdentnode, matching the "no leading digit" convention of most programming languages.
Practical consequence: foo, _bar, my_param2 are valid; foo-bar,
2fast, foo bar, and x=y are all rejected with InvalidUsage.
Inside macro argument parsing, a hyphen is not a delimiter, so foo-bar is
lexed as a single Text token "foo-bar" (the else branch in the args-state
loop consumes all bytes that are not whitespace, ,, ), =, or the special
char). single_ident_param then rejects it because the Param node’s only
non-space child has kind Text, not Ident — identifiers can only start
with [A-Za-z_] and the lexer never emits an Ident token for foo-bar.
(TokenKind::Special is emitted only for the doubled special char — e.g. %
is the escape sequence for a literal % — not for arbitrary punctuation.)
%here: the one side-effecting builtin
%here(macro_name, args…) calls builtin_eval to get the expansion, then
uses modify_source to patch the current source file in place — prepending the
special char before the %here token (to neutralise the call site) and
appending the expanded text after it, skipping the rest of the line. It then
sets early_exit so the evaluator stops cleanly.
The three safety properties and how they are guaranteed:
Single-fire per run. After modify_source writes the file, set_early_exit()
flips a flag that makes every subsequent evaluate() / evaluate_to() call
return immediately. Even if a file contains multiple %here calls, only the
first one encountered reaches builtin_here; all later ones are unreachable.
Source-position stability. The AST built before the call is now stale
(byte offsets no longer match the modified file), but early_exit ensures no
further evaluation touches it. The next tool run starts fresh with a new
Evaluator, re-parses the modified file, and sees the neutralised call site.
Idempotency. The prepend step inserts one extra special char, turning
%here(…) into %here(…). A double special char is lexed as literal
text (the escape sequence for a single %), so re-running on the already-patched
file produces no macro call and no second patch.
Case conversion: WordSplitter iterator
A custom WordSplitter iterator handles all common word boundaries: delimiter
characters (_, -, space), camelCase transitions (lowercase→uppercase),
acronyms (uppercase→uppercase→lowercase), and digit transitions
(letter→digit, digit→letter). All nine Case variants are implemented as
simple transformations over the collected word slices.
Built-in macro table
| Name | Behaviour |
|---|---|
|
Define a text-substitution macro. |
|
Define a Rhai-scripted macro. Body is evaluated as Rhai code at call time. |
|
Define a Python-scripted macro via monty. |
|
Set a variable in the current scope. |
|
Move a variable or macro from the current scope into the parent scope, freezing free variables. |
|
Evaluate the named file and splice its output inline. |
|
Evaluate the named file for its side effects (macro/variable definitions) only; output is discarded. |
|
If |
|
Return |
|
Look up |
|
Expand the macro and splice the result into the current source file (one-shot source patching). |
|
Upper- / lower-case the first character. |
|
Convert identifier case. |
|
Manage the persistent Rhai store. |
|
Manage the persistent Python store. |
|
Read an environment variable. Requires |
File structure
// <<@file weaveback-macro/src/evaluator/builtins.rs>>=
// <<builtins preamble>>
// <<builtins type and registry>>
// <<builtins def macro config>>
// <<builtins single ident param>>
// <<builtins define macro helper>>
// <<builtins def rhaidef pydef>>
// <<builtins include import>>
// <<builtins if equal>>
// <<builtins set export eval here>>
// <<builtins capitalize>>
// <<builtins convert case builtins>>
// <<builtins rhai store builtins>>
// <<builtins py store builtins>>
// <<builtins env>>
// @
// <<@file weaveback-macro/src/evaluator/case_conversion.rs>>=
// <<case conversion preamble>>
// <<case enum>>
// <<word splitter>>
// <<convert case functions>>
// <<capitalize helper>>
// @
// <<@file weaveback-macro/src/evaluator/source_utils.rs>>=
// <<source utils>>
// @
Builtins preamble
// <<builtins preamble>>=
// crates/weaveback-macro/src/evaluator/builtins.rs
use HashMap;
use HashSet;
use Arc;
use convert_case_str;
use Evaluator;
use ;
use ScriptKind;
use crate;
// @
BuiltinFn type and default registry
// <<builtins type and registry>>=
/// Type for a builtin macro function: (Evaluator, node) -> String
pub type BuiltinFn = fn ;
/// Return the default builtins
// @
DefMacroConfig — shared configuration for %def / %rhaidef / %pydef
// <<builtins def macro config>>=
// @
single_ident_param — strict identifier validator
Validates that a Param node contains exactly one Ident child (no spaces,
no =, no leading digit). Used for macro names, formal parameter names, and
variable names in %set / %export.
// <<builtins single ident param>>=
/// Helper: Checks that a Param node contains exactly one identifier child
// @
define_macro — shared helper for %def / %rhaidef / %pydef
Extracts (name, [p1, p2, …,] body) from the node’s parts, validates each
identifier, records the call site for tracing, and stores the
MacroDefinition.
// <<builtins define macro helper>>=
// @
%def, %rhaidef, %pydef
// <<builtins def rhaidef pydef>>=
// @
%include and %import
builtin_import discards the text output from process_include_file (returns
"") but the evaluation has already executed the file’s side effects (macro
and variable definitions).
// <<builtins include import>>=
// @
%if and %equal
%if treats any non-whitespace-only string as truthy. %equal returns the
first argument if both arguments are byte-identical, otherwise "".
// <<builtins if equal>>=
// @
%set, %export, %eval, %here
%eval re-dispatches at evaluation time: it evaluates the first argument to get
the macro name, then constructs a synthetic Macro AST node using the name
argument’s token as the origin (so error messages point to the right place).
%here is the one builtin with file I/O. It calls builtin_eval to expand
the macro, then calls modify_source with two insertions: a special-char
prefix before the %here call (neutralising it) and the expansion after it
(skipping the rest of the original line). Then it sets early_exit.
// <<builtins set export eval here>>=
// @
Capitalisation builtins
// <<builtins capitalize>>=
// @
Case-conversion builtins
// <<builtins convert case builtins>>=
// @
Rhai store builtins
// <<builtins rhai store builtins>>=
// @
Python store builtins
// <<builtins py store builtins>>=
// @
%env
// <<builtins env>>=
// @
Case conversion (case_conversion.rs)
Preamble
// <<case conversion preamble>>=
// crates/weaveback-macro/src/evaluator/case_conversion.rs
use FromStr;
// @
Case enum
Nine target case styles are supported. The FromStr impl accepts multiple
aliases (e.g. "snake", "snake_case") and is case-insensitive.
// <<case enum>>=
// @
WordSplitter iterator
WordSplitter splits an identifier string into word slices without allocating.
It recognises four boundary types:
-
Delimiter characters:
_,-, whitespace — consumed and not included in any word. -
camelCase transition: lowercase→uppercase (e.g.
hello|World). -
Acronym end: uppercase→uppercase→lowercase (
XML|Http). -
Digit transitions: letter→digit and digit→letter.
// <<word splitter>>=
// @
convert_case and convert_case_str
// <<convert case functions>>=
// @
capitalize helper
// <<capitalize helper>>=
// @
Source patching (source_utils.rs)
modify_source sorts the insertions by byte offset, copies unchanged ranges
verbatim, injects the new bytes at each position, and optionally skips to the
next newline (used by %here to overwrite the rest of the original call line).
// <<source utils>>=
// <[@file crates/weaveback-macro/src/evaluator/source_utils.rs]>=
// weaveback/crates/weaveback-macro/src/evaluator/source_utils.rs
use fs;
use ;
use Path;
/// Modify `source_file` by inserting text at byte offsets, optionally skipping to newline.
// @