Module interp

Module interp 

Source
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’s str_index_of and list_get return one, so they are not compile-time builtins. Where the operation is needed anyway the compile-time half is total and refuses rather than answering None — indexing (xs[i]) is that, and so is str_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, sin and cos would 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 macro receives 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 def or a macro, as names.