Skip to main content

Interpreter

Struct Interpreter 

Source
pub struct Interpreter<H> { /* private fields */ }

Implementations§

Source§

impl<H> Interpreter<H>
where H: 'static,

Source

pub fn new() -> Interpreter<H>

Source

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 raises SetSealed instead 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.

Source

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.

Source

pub fn modules(&self) -> &ModuleRegistry

Borrow the module registry. Useful for tests + inspection.

Source

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.

Source

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.

Source

pub fn register_awaitable_fn<R, C>( &mut self, name: impl Into<Arc<str>>, arity: Arity, ready: R, call: C, )
where R: Fn(&[Value], &H) -> bool + Send + Sync + 'static, C: Fn(&[Value], &mut H, Span) -> Result<Value, EvalError> + Send + Sync + 'static,

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")))
    },
);
Source

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.

Source

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 defmacroeval_top_form is the entry point for that.

Source

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.

Source

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.

Source

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.

Source

pub fn expander(&self) -> &SpannedExpander

Borrow the macro expander. Embedders may register macros directly (e.g. preloaded standard library) without reading them from source.

Source

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.

Source

pub fn lookup_global(&self, name: &str) -> Option<Value>

Look up a symbol in the global env.

Source

pub fn define_global(&mut self, name: impl Into<Arc<str>>, value: Value)

Bind a value in the global env.

Source

pub fn globals_snapshot(&self) -> &Env

Borrow the globals env. Used by the VM to snapshot at closure creation time.

Source

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:

  1. a SpecialForm — a match arm, so it appears in no environment and globals_snapshot().lookup() cannot see it;
  2. a macro registered on the expander, which rewrites before evaluation;
  3. 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.

Source

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.

Source

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.

Source

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.

Source

pub fn register_typed0<R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
where R: IntoValue + 'static, F: Fn(&mut H) -> Result<R, EvalError> + Send + Sync + 'static,

Register a 0-arity native fn with typed return value.

Source

pub fn register_typed1<A, R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
where A: FromValue + 'static, R: IntoValue + 'static, F: Fn(&mut H, A) -> Result<R, EvalError> + Send + Sync + 'static,

Register a 1-arity native fn with typed arg + return.

Source

pub fn register_typed2<A, B, R, F>(&mut self, name: impl Into<Arc<str>>, f: F)
where A: FromValue + 'static, B: FromValue + 'static, R: IntoValue + 'static, F: Fn(&mut H, A, B) -> Result<R, EvalError> + Send + Sync + 'static,

Register a 2-arity native fn with typed args + return.

Source

pub fn register_typed3<A, B, C, R, F>( &mut self, name: impl Into<Arc<str>>, f: F, )
where A: FromValue + 'static, B: FromValue + 'static, C: FromValue + 'static, R: IntoValue + 'static, F: Fn(&mut H, A, B, C) -> Result<R, EvalError> + Send + Sync + 'static,

Register a 3-arity native fn with typed args + return.

Source

pub fn register_typed4<A, B, C, D, R, F>( &mut self, name: impl Into<Arc<str>>, f: F, )
where A: FromValue + 'static, B: FromValue + 'static, C: FromValue + 'static, D: FromValue + 'static, R: IntoValue + 'static, F: Fn(&mut H, A, B, C, D) -> Result<R, EvalError> + Send + Sync + 'static,

Register a 4-arity native fn with typed args + return.

Trait Implementations§

Source§

impl<H> Default for Interpreter<H>
where H: 'static,

Source§

fn default() -> Interpreter<H>

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl<H> !RefUnwindSafe for Interpreter<H>

§

impl<H> !UnwindSafe for Interpreter<H>

§

impl<H> Freeze for Interpreter<H>

§

impl<H> Send for Interpreter<H>

§

impl<H> Sync for Interpreter<H>

§

impl<H> Unpin for Interpreter<H>

§

impl<H> UnsafeUnpin for Interpreter<H>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.