cljrs-eval 0.1.7

Tree-walking interpreter and macro expander for clojurust
Documentation

cljrs-eval

Tree-walking interpreter for clojurust. Evaluates Form AST nodes produced by cljrs-reader within a namespace-aware lexical environment.

Phase: 5–8 + defrecord/reify/built-in protocols + source-path management/require + dynamic variables (binding) — implemented.


Purpose

Implements a complete Clojure-compatible tree-walking evaluator including all special forms, macro expansion, tail-call optimization, sequential and associative destructuring, syntax-quote with gensyms, lazy sequences, and a full clojure.core bootstrap.


File layout

src/
  lib.rs          — module declarations, re-exports, standard_env(), standard_env_with_paths()
  error.rs        — EvalError enum, EvalResult<T> alias
  env.rs          — Frame, GlobalEnv (namespace registry + source paths + loaded set), Env (lexical scope), RequireSpec, RequireRefer
  eval.rs         — top-level eval dispatcher, form_to_value, inline tests
  dynamics.rs     — thread-local dynamic binding stack: BINDING_STACK, BindingGuard, push_frame/pop_frame, deref_var, set_thread_local, capture_current/install_frames, trace_current
  callback.rs     — thread-local eval context for Rust→Clojure callbacks; invoke(f, args) lets builtins call Clojure functions
  apply.rs        — eval_call, apply_value, call_cljrs_fn, ClosureThunk, handle_make_lazy_seq, handle_make_delay, handle_send, handle_vswap, handle_with_bindings, handle_alter_var_root, handle_vary_meta, type_tag_of, resolve_type_tag
  special.rs      — all special form handlers, SPECIAL_FORMS list; includes binding, extract_def_name, compile_meta_form
  loader.rs       — load_ns: resolves namespace names to files, evaluates them, applies alias/refer
  macros.rs       — macroexpand_1, macroexpand, value_to_form
  destructure.rs  — sequential + associative destructuring (bind_pattern, bind_sequential, bind_associative)
  syntax_quote.rs — syntax-quote expander with gensym counter
  builtins.rs     — native built-in functions + BOOTSTRAP_SOURCE Clojure string

Public API

/// Evaluate a Form in env and return the resulting Value.
pub fn eval(form: &Form, env: &mut Env) -> EvalResult;

/// Create a GlobalEnv pre-populated with clojure.core builtins
/// (native functions + bootstrap HOFs) and a user namespace
/// that refers everything from clojure.core.
pub fn standard_env() -> Arc<GlobalEnv>;

/// Like standard_env() but also configures the source search paths.
pub fn standard_env_with_paths(source_paths: Vec<PathBuf>) -> Arc<GlobalEnv>;

/// Load a namespace from source and wire alias/refer into current_ns.
pub fn load_ns(globals: Arc<GlobalEnv>, spec: &RequireSpec, current_ns: &str) -> Result<(), String>;

pub enum RequireRefer { None, All, Named(Vec<Arc<str>>) }
pub struct RequireSpec { pub ns: Arc<str>, pub alias: Option<Arc<str>>, pub refer: RequireRefer }

pub struct GlobalEnv { /* namespace registry + source_paths + loaded/loading sets */ }
impl GlobalEnv {
    pub fn new() -> Arc<Self>
    pub fn set_source_paths(&self, paths: Vec<PathBuf>)
    pub fn mark_loaded(&self, ns: &str)
    pub fn is_loaded(&self, ns: &str) -> bool
    pub fn resolve_alias(&self, current_ns: &str, alias: &str) -> Option<Arc<str>>
    pub fn get_or_create_ns(&self, name: &str) -> GcPtr<Namespace>
    pub fn intern(&self, ns: &str, name: Arc<str>, val: Value) -> GcPtr<Var>
    pub fn lookup_var(&self, ns: &str, name: &str) -> Option<GcPtr<Var>>
    pub fn lookup_in_ns(&self, ns: &str, name: &str) -> Option<Value>
    pub fn refer_all(&self, target_ns: &str, source_ns: &str)
    pub fn refer_named(&self, target_ns: &str, source_ns: &str, names: &[Arc<str>])
    pub fn add_alias(&self, current_ns: &str, alias: &str, full_ns: &str)
}

pub struct Env { /* frames + current_ns + globals */ }
impl Env {
    pub fn new(globals: Arc<GlobalEnv>, ns: &str) -> Self
    pub fn with_closure(globals: Arc<GlobalEnv>, ns: &str, f: &CljxFn) -> Self
    pub fn push_frame(&mut self)
    pub fn pop_frame(&mut self)
    pub fn bind(&mut self, name: Arc<str>, val: Value)
    pub fn lookup(&self, name: &str) -> Option<Value>
}

#[derive(Debug, thiserror::Error)]
pub enum EvalError {
    Runtime(String),
    UnboundSymbol(String),
    Arity { name: String, expected: String, got: usize },
    NotCallable(String),
    Thrown(Value),         // ex-info / throw
    Read(CljxError),       // from reader
    // internal only:
    Recur(Vec<Value>),     // tail-call signal
}

pub type EvalResult<T = Value> = Result<T, EvalError>;

Special forms

Form Notes
def / defn / defmacro / defonce Intern values in current namespace
fn / fn* Multi-arity; rest args; closure capture
let / let* Sequential binding with destructuring
letfn Mutually recursive local function bindings
loop / loop* + recur Tail-call trampoline
if / do / and / or Control flow
quote Return unevaluated form
var Return Value::Var for a namespace var
set! Set thread-local binding if inside binding; else mutate var's root
throw / try / catch / finally Exception handling
ns Switch current namespace; processes :require clauses; auto-refers clojure.core
require Load and wire namespaces; supports :as alias and :refer [...]/:refer :all
load-file Evaluate a .cljrs/.cljc file by absolute path
in-ns Switch to (or create) a namespace by quoted symbol
alias Add a namespace alias to the current namespace
. Stub — interop not yet implemented
defprotocol Define a protocol with named method signatures
extend-type Implement protocol methods for a type
extend-protocol Protocol-first sugar for extend-type
defmulti Define a multimethod with a dispatch function
defmethod Add an implementation for one dispatch value
future Evaluate body on a new thread; return Value::Future; conveys dynamic bindings
defrecord Define a named record type; generates ->Name/map->Name constructors; supports inline protocol impls
reify Create an anonymous protocol-implementing instance with a gensym'd type tag
binding Establish thread-local dynamic var bindings for body (RAII BindingGuard)

Built-in functions (clojure.core)

All built-ins have signature fn(&[Value]) -> ValueResult<Value>.

Arithmetic: + - * / mod rem quot inc dec max min abs

Comparison: = not= < > <= >= identical?

Predicates: nil? zero? pos? neg? not true? false? number? integer? float? string? keyword? symbol? fn? seq? map? vector? set? coll? boolean? char? var? atom? empty?

Collections: list vector hash-map hash-set conj assoc dissoc get get-in count seq first rest next cons nth last reverse concat keys vals contains? merge into empty vec set

Higher-order (bootstrap): map filter reduce keep remove mapcat take drop take-while drop-while comp partial identity constantly complement apply

Lazy sequences (bootstrap): lazy-seq (macro) range iterate repeat repeatedly cycle

Atoms: atom deref reset! swap! compare-and-set!

Volatiles: volatile! vreset! vswap! volatile?

Delays: delay (macro) force realized?

Promises: promise deliver

Futures: future (special form) future-done? future-cancelled? future-cancel

Agents: agent send send-off await agent-error restart-agent

I/O: print println prn pr pr-str str read-string slurp spit

Misc: gensym type hash name namespace ex-info ex-data ex-message ex-cause

Protocols: satisfies? extends?

Multimethods: prefer-method remove-method methods isa?

Records/reify: make-type-instance record? instance?

Dynamic vars: var-get var-set! bound? thread-bound? meta with-meta alter-var-root vary-meta with-bindings* (intercepted in eval_call)


Lazy sequences

Lazy sequences use a Thunk trait (defined in cljrs-value) and two new Value variants:

  • Value::LazySeq(GcPtr<LazySeq>) — a deferred sequence cell; forced at most once and cached.
  • Value::Cons(GcPtr<CljxCons>) — a cons cell with head: Value and tail: Value, used when cons is called with a lazy tail.

make-lazy-seq (special-cased in eval_call) wraps a zero-arg CljxFn in a ClosureThunk and returns a Value::LazySeq. The lazy-seq macro expands to (make-lazy-seq (fn [] ...)).


Destructuring

Both sequential and associative destructuring are supported in let, fn, and loop binding positions.

Sequential ([a b & rest :as whole]): positional bindings, rest, :as.

Associative ({:keys [a b] :strs [c] :syms [d] :as m :or {b 99}}): keyword, string, and symbol key extraction; entire-value alias; defaults.


Bootstrap HOFs

Higher-order functions that need to call back into the evaluator are defined in BOOTSTRAP_SOURCE — a Clojure string eval'd at startup in clojure.core: when when-not cond -> ->> reduce map filter remove keep mapcat mapv filterv take drop take-while drop-while some every? doseq dotimes for comp partial complement range iterate repeat repeatedly cycle update-in map-keys map-vals etc.


Dependencies

Crate Role
cljrs-types Span, CljxError
cljrs-gc GcPtr<T> smart pointer
cljrs-reader Form AST input
cljrs-value Value, CljxFn, Namespace, LazySeq, CljxCons, all collections
thiserror EvalError derive

Deferred to later phases

  • deftype — blocked by . interop (field access via (.field obj)); Phase 9
  • . interop, new — Phase 9
  • ref / STM (dosync, alter, commute, ensure) — deferred
  • locking macro — deferred
  • derive / full isa? hierarchy — deferred