pub struct Interpreter<H> { /* private fields */ }Implementations§
Source§impl<H> Interpreter<H>where
H: 'static,
impl<H> Interpreter<H>where
H: 'static,
pub fn new() -> Interpreter<H>
Sourcepub fn fork(&self) -> Interpreter<H>
pub fn fork(&self) -> Interpreter<H>
Derive a child interpreter that shares this one’s already-evaluated globals instead of rebuilding them.
Building an interpreter is dominated by evaluating the Lisp standard
library, not by registering the Rust primitives: measured at 945µs per
install_full_stdlib_with, which is essentially the entire cost of
running one short program. Any embedder that needs many isolated
interpreters — a test runner giving each test a clean slate, a
supervisor giving each process private state — pays that once per child
for a result that is identical every time.
fork pays it once. The child gets:
- every global, readable. Frames are
Arc-shared, so the stdlib is not copied at all. - a private frame for its own
defines. They land above the shared frames, so the parent and every sibling are blind to them. - no write path to the shared frames. They sit below the child’s
write floor, so a
set!reaching for a parent binding raisesSetSealedinstead of mutating state another child can observe.
That last property is why this is a fork and not a clone: Env::set
takes &self and writes through the Arc, so a plain clone would
share globals in the mutable direction too, and one child could edit
another’s stdlib.
§What is deliberately still shared
The module registry, so a (require ...) resolved by one child is not
re-read by the next. Modules are immutable once loaded; this is a cache,
not shared program state.
§What a fork is not
It is not a snapshot. The shared frames stay live: a define the
parent runs after forking is visible to children already forked,
because they hold the same frame. Fork children, then stop defining in
the parent.
Sourcepub fn set_loader(&mut self, loader: Arc<dyn Loader>)
pub fn set_loader(&mut self, loader: Arc<dyn Loader>)
Replace the source loader. Required for (require ...) to do
anything useful — the default NoLoader rejects every require.
Sourcepub fn modules(&self) -> &ModuleRegistry
pub fn modules(&self) -> &ModuleRegistry
Borrow the module registry. Useful for tests + inspection.
Sourcepub fn register_fn<F>(
&mut self,
name: impl Into<Arc<str>>,
arity: Arity,
callable: F,
)where
F: NativeCallable<H>,
pub fn register_fn<F>(
&mut self,
name: impl Into<Arc<str>>,
arity: Arity,
callable: F,
)where
F: NativeCallable<H>,
Register a native Rust function, exposing it to Lisp code under
name. Re-registering the same name overwrites the prior entry
(last-write-wins) and leaves the global binding intact.
Sourcepub fn register_higher_order_fn<F>(
&mut self,
name: impl Into<Arc<str>>,
arity: Arity,
callable: F,
)where
F: HigherOrderCallable<H>,
pub fn register_higher_order_fn<F>(
&mut self,
name: impl Into<Arc<str>>,
arity: Arity,
callable: F,
)where
F: HigherOrderCallable<H>,
Register a higher-order Rust primitive — receives a Caller so it
can invoke Value::Closure / Value::NativeFn arguments back into
the eval loop. Used for map, filter, fold, apply,
for-each, etc. Same overwrite semantics as register_fn.
Sourcepub fn register_awaitable_fn<R, C>(
&mut self,
name: impl Into<Arc<str>>,
arity: Arity,
ready: R,
call: C,
)
pub fn register_awaitable_fn<R, C>( &mut self, name: impl Into<Arc<str>>, arity: Arity, ready: R, call: C, )
Register a primitive that may have to wait.
Two phases: ready(&[Value], &H) -> bool decides against an
immutable host, and call(&[Value], &mut H, Span) does the work only
once ready said yes. The immutable borrow in ready is the point —
see crate::ffi::AwaitableCallable: it makes consume-then-wait a
compile error rather than a documented hazard.
Under Vm::step/resume a not-ready call parks the process. Under
Vm::run, which has no scheduler, it is VmError::Deadlocked.
§Consume-then-wait does not typecheck
This is the reason the form is split, so it is asserted, not
described. The body below is the natural shape of a selective
receive — take a message, find it does not match, wait — which is
precisely the message-losing bug a one-phase parking contract cannot
prevent. ready holds &H, so it is rejected at compile time:
use tatara_lisp_eval::{ffi::Arity, Interpreter, Value};
#[derive(Default)]
struct Mail { queue: std::collections::VecDeque<i64> }
let mut interp: Interpreter<Mail> = Interpreter::new();
interp.register_awaitable_fn(
"selective-take",
Arity::Exact(0),
// E0596: cannot borrow `mail.queue` as mutable.
|_args: &[Value], mail: &Mail| mail.queue.pop_front().is_some(),
|_args: &[Value], _mail: &mut Mail, _span| Ok(Value::Nil),
);The permitted shape reads the queue and leaves it alone:
use tatara_lisp_eval::{ffi::Arity, Interpreter, Value};
#[derive(Default)]
struct Mail { queue: std::collections::VecDeque<i64> }
let mut interp: Interpreter<Mail> = Interpreter::new();
interp.register_awaitable_fn(
"take",
Arity::Exact(0),
|_args: &[Value], mail: &Mail| !mail.queue.is_empty(),
|_args: &[Value], mail: &mut Mail, _span| {
Ok(Value::Int(mail.queue.pop_front().expect("ready said yes")))
},
);Sourcepub fn set_macro_expansion_limit(&mut self, limit: usize)
pub fn set_macro_expansion_limit(&mut self, limit: usize)
Raise or lower the macro rewrite-chain ceiling.
There is no way to remove it. An unbounded expander does not fail the compilation, it aborts the process.
Sourcepub fn eval_spanned(
&mut self,
form: &Spanned,
host: &mut H,
) -> Result<Value, EvalError>
pub fn eval_spanned( &mut self, form: &Spanned, host: &mut H, ) -> Result<Value, EvalError>
Evaluate a single already-read spanned form in this interpreter’s
global environment. Macro expansion runs first if any macros are
registered. Bare eval_spanned does NOT register top-level
defmacro — eval_top_form is the entry point for that.
Sourcepub fn eval_program(
&mut self,
forms: &[Spanned],
host: &mut H,
) -> Result<Value, EvalError>
pub fn eval_program( &mut self, forms: &[Spanned], host: &mut H, ) -> Result<Value, EvalError>
Evaluate a slice of forms in order, returning the last result.
Top-level defmacro / defpoint-template / defcheck forms register
into the persistent expander and yield Value::Nil. All other forms
are fully expanded (recursively rewriting macro calls anywhere
in the form tree, with each macro body run through the live
evaluator at expansion time) before being evaluated. This is the
canonical entry point for running a tatara-lisp program — REPL,
embedded host, batch script.
Empty input returns Value::Nil.
Sourcepub fn eval_top_form(
&mut self,
form: &Spanned,
host: &mut H,
) -> Result<Value, EvalError>
pub fn eval_top_form( &mut self, form: &Spanned, host: &mut H, ) -> Result<Value, EvalError>
Evaluate one top-level form: register macros, handle module-
system forms (provide / require), expand, then eval.
Public so embedders that drive the read-eval loop themselves
(REPL, hot-reload watchers) can preserve top-level semantics
without re-implementing the registration handshake.
Sourcepub fn fully_expand(
&mut self,
form: &Spanned,
host: &mut H,
) -> Result<Spanned, EvalError>
pub fn fully_expand( &mut self, form: &Spanned, host: &mut H, ) -> Result<Spanned, EvalError>
Fully expand a form: walk the tree; whenever the head of a list is a registered macro, evaluate the macro body (a regular Lisp program) at expansion time, convert the resulting Value back to a Spanned tree, and recurse — the expansion may itself contain further macro calls.
This is the CL/Racket macro model: the macro body has full access to every primitive and library function, can compute over its argument source forms (which arrive as Lisp data structures — lists of symbols, etc.), and produces code as data.
Sourcepub fn expander(&self) -> &SpannedExpander
pub fn expander(&self) -> &SpannedExpander
Borrow the macro expander. Embedders may register macros directly (e.g. preloaded standard library) without reading them from source.
Sourcepub fn expander_mut(&mut self) -> &mut SpannedExpander
pub fn expander_mut(&mut self) -> &mut SpannedExpander
Mutable access to the expander — for preloading macros via
try_register_macro from a separately-read form list, or clearing
the registry.
Sourcepub fn lookup_global(&self, name: &str) -> Option<Value>
pub fn lookup_global(&self, name: &str) -> Option<Value>
Look up a symbol in the global env.
Sourcepub fn define_global(&mut self, name: impl Into<Arc<str>>, value: Value)
pub fn define_global(&mut self, name: impl Into<Arc<str>>, value: Value)
Bind a value in the global env.
Sourcepub fn globals_snapshot(&self) -> &Env
pub fn globals_snapshot(&self) -> &Env
Borrow the globals env. Used by the VM to snapshot at closure creation time.
Sourcepub fn resolve_head(&self, name: &str) -> Option<HeadBinding>
pub fn resolve_head(&self, name: &str) -> Option<HeadBinding>
What the head symbol of a form resolves to, in the order the evaluator actually consults.
Exposed because that order is not obvious and getting it wrong is silent. There are three arbiters, not one:
- a
SpecialForm— amatcharm, so it appears in no environment andglobals_snapshot().lookup()cannot see it; - a macro registered on the expander, which rewrites before evaluation;
- a binding in the environment.
A consumer checking “is this name already taken” by looking only at the
environment misses the first two entirely. That is not hypothetical:
blue lowered assert e to a name the stdlib bound as a macro, a
macro beat the primitive, and every assertion in its test suite silently
passed. Its gate then still could not see special forms, so not,
and, or, if and when all read as unbound.
Answering from a live interpreter rather than from a published list is deliberate: a list is a second copy of the truth and goes stale on the next installer that lands.
Sourcepub fn reserved_head_names(&self) -> BTreeSet<Arc<str>>
pub fn reserved_head_names(&self) -> BTreeSet<Arc<str>>
Every head symbol a form in this interpreter could resolve to.
The union across all three arbiters, read off this interpreter rather
than declared, so it is exactly as current as the interpreter is. Build
one with crate::install_full_stdlib_with and this is the full
reserved surface.
Sourcepub fn apply_external_value(
&mut self,
callee: &Value,
args: Vec<Value>,
host: &mut H,
call_span: Span,
) -> Result<Value, EvalError>
pub fn apply_external_value( &mut self, callee: &Value, args: Vec<Value>, host: &mut H, call_span: Span, ) -> Result<Value, EvalError>
External entry point: apply a callable Value (closure or
native fn) with args. Wraps the internal apply_external so
the VM can dispatch to the tree-walker for non-VM callables.
Sourcepub fn eval_program_vm(
&mut self,
forms: &[Spanned],
host: &mut H,
) -> Result<Value, EvalError>
pub fn eval_program_vm( &mut self, forms: &[Spanned], host: &mut H, ) -> Result<Value, EvalError>
Compile + execute a parsed program through the bytecode VM.
Top-level defmacro forms register into the persistent
expander (same as eval_program); every other form is
macro-expanded in place, then a fresh Chunk is compiled and
run. This is the opt-in fast path; eval_program remains the
authoritative tree-walker. Returns the value of the last form.
Sourcepub fn register_typed0<R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
pub fn register_typed0<R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
Register a 0-arity native fn with typed return value.
Sourcepub fn register_typed1<A, R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
pub fn register_typed1<A, R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
Register a 1-arity native fn with typed arg + return.
Sourcepub fn register_typed2<A, B, R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
pub fn register_typed2<A, B, R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
Register a 2-arity native fn with typed args + return.
Sourcepub fn register_typed3<A, B, C, R, F>(
&mut self,
name: impl Into<Arc<str>>,
f: F,
)
pub fn register_typed3<A, B, C, R, F>( &mut self, name: impl Into<Arc<str>>, f: F, )
Register a 3-arity native fn with typed args + return.
Sourcepub fn register_typed4<A, B, C, D, R, F>(
&mut self,
name: impl Into<Arc<str>>,
f: F,
)
pub fn register_typed4<A, B, C, D, R, F>( &mut self, name: impl Into<Arc<str>>, f: F, )
Register a 4-arity native fn with typed args + return.