Expand description
The macro interpreter: Beck, evaluated at compile time.
docs/02-syntax.md §2.4: “Macro bodies run at compile time
in the compiler’s own Beck interpreter, with a capability-restricted environment: pure
computation and reads of the declared module graph, no ambient filesystem or network.
Non-negotiable — build reproducibility and the ‘compile once, deploy many’ model depend on it,
and it closes a real supply-chain hole that Rust build.rs and npm postinstall leave open.”
This is that interpreter. A macro body is ordinary Beck — bindings, if, for, while,
lambdas, calls to the pure part of the prelude and to the module’s own defs — and quote:
is the one form whose value is syntax rather than a number or a string.
§Why it is a second interpreter rather than beck-eval
Untyped macros expand before the checker runs (docs/02
§2.4: “macro (untyped AST in, AST out, expands before type checking)”), so there is no
Core IR and no type for a macro body to be evaluated against — only [Node]. beck-eval
evaluates Core, and beck-core (which lowers to it) depends on this crate, so the dependency
could not run the other way even if the IR existed. The two are held together by a
differential rather than by sharing code: beck-cli/tests/macro_interp.rs computes the same
pure expressions at compile time and at run time and fails when the answers differ, which is
the same instrument docs/04 §4.8 points at
the backends.
Where an operation is somebody else’s table — case mapping, substring replacement — this calls
beck-prim, the crate the evaluator and a compiled program already call
(docs/93 §93.12). Agreement there is
not a property of two implementations being careful; there is one implementation.
§The sandbox
The environment is a whitelist: a name resolves to a local, to one of the module’s own
defs, or to one of the pure builtins in BUILTINS — and to nothing else. There is no name
for opening a file, reading the environment, starting a process or fetching a URL, because
nothing here defines one. The prelude’s effectful names are refused by name rather than left
to fall out of the whitelist (RESTRICTED), so that a macro reaching for now() is told what
it did wrong instead of being told the name does not exist.
That refusal is a claim, so it is a gate: beck-cli/tests/macro_sandbox.rs enumerates the
prelude and fails if an effectful primitive is missing from RESTRICTED or reachable from a
macro body.
§What it does not have
- Unions, and therefore no
Option: the prelude’sstr_index_ofandlist_getreturn one, so they are not compile-time builtins. Where the operation is needed anyway the compile-time half is total and refuses rather than answeringNone— indexing (xs[i]) is that, and so isstr_to_int, which a macro parsing a typed literal (§2.5) has no other way to do. match, for the same reason: its patterns are about variants.- The transcendentals.
sqrt,sinandcoswould make the compiler’s answer depend on the host’s libm, which is F9’s open question (docs/35§35.5) and not a thing to prejudge from here. - Types. A
typed macroreceives the AST with inferred types attached (§2.4) and needs the checker to have run; this interpreter is the untyped half.
Structs§
- FnDef
- A module-level
def, callable from a macro body. - Halt
- Evaluation stopped and a diagnostic has already been reported.
- Interp
- Lambda
- A lambda, with the frame it closed over.
Enums§
- Val
- A compile-time value.
Constants§
- BUILTINS
- Every name the compile-time environment defines.
- MAX_
STEPS - How many steps one module’s macro bodies may take, in total.
- RESTRICTED
- The prelude names a macro body may not call, with the effect atom each performs.
Functions§
- is_
builtin - param_
names - The parameter list of a
defor amacro, as names.