tatara-lisp-eval 0.3.47

Runtime evaluator for tatara-lisp — embeddable Scheme-ish eval scoped to orchestration (job queues, rules, REPL). See docs/eval-design.md.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Runtime values.
//!
//! `Value` is distinct from `Sexp`: evaluation produces `Value`, while the
//! source AST is `Sexp` / `Spanned`. Values include runtime-only variants
//! (closures, native functions, opaque host-owned Foreign values) that
//! have no surface syntax.

use std::any::Any;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;

use tatara_lisp::{Sexp, Span, Spanned};

use crate::env::Env;
use crate::ffi::Arity;

/// An evaluated runtime value.
#[derive(Clone)]
pub enum Value {
    Nil,
    Bool(bool),
    Int(i64),
    Float(f64),
    Str(Arc<str>),
    Symbol(Arc<str>),
    Keyword(Arc<str>),
    List(Arc<Vec<Value>>),
    /// Persistent hash map keyed by a hashable subset of `Value`
    /// (`Bool`, `Int`, `Float`, `Str`, `Symbol`, `Keyword`, `Nil`).
    /// Inserting / removing yields a new Map (copy-on-write via `Arc`).
    Map(Arc<HashMap<MapKey, Value>>),
    Closure(Arc<Closure>),
    NativeFn(Arc<NativeFn>),
    /// A delayed (lazy) computation. First force triggers evaluation
    /// of the underlying thunk; subsequent forces return the cached
    /// result. Backed by `Mutex` so a Promise can be shared across
    /// references safely (single-threaded runtime, but the lock is
    /// trivial overhead and gives us zero-effort safety).
    Promise(Arc<std::sync::Mutex<PromiseState>>),
    /// A first-class structured error — Clojure ex-info shape:
    /// a tag (keyword/string), a message string, and a data plist.
    /// Constructed by `(error tag msg data)` / `(ex-info msg data)`.
    /// Raised by `(throw err)`. Caught by `(try ... (catch (e) ...))`.
    Error(Arc<ErrorObj>),
    /// Escape hatch: unevaluated source form carried as a value, e.g. after
    /// `(quote x)`. Preserves span info.
    Sexp(Sexp, Span),
    /// Opaque host-owned value. The embedder supplies these via FFI; native
    /// functions read them back via downcast. Used to expose typed Rust
    /// handles (job refs, client handles) to Lisp code.
    Foreign(Arc<dyn Any + Send + Sync>),
}

/// Structured error payload — tag + message + attached data. The data
/// is a list of (key, value) pairs preserving insertion order — a
/// plist-style alist. Keys are typically `Value::Keyword`s but any
/// equality-comparable Value works.
#[derive(Debug, Clone)]
pub struct ErrorObj {
    pub tag: Arc<str>,
    pub message: Arc<str>,
    pub data: Vec<(Value, Value)>,
}

/// Hashable subset of `Value` — every variant that has well-defined
/// equality and hashing semantics. Used as the key type for `Value::Map`.
///
/// `Float` keys are stored as raw bit patterns so two `NaN`s hash to the
/// same slot and equality is bit-exact. This trades IEEE-NaN-comparison
/// semantics for usability — keys round-trip correctly.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MapKey {
    Nil,
    Bool(bool),
    Int(i64),
    Float(u64),
    Str(Arc<str>),
    Symbol(Arc<str>),
    Keyword(Arc<str>),
}

impl MapKey {
    /// Try to convert a Value into a hashable map key. Returns None
    /// for non-hashable variants (List, Map, Closure, NativeFn,
    /// Error, Sexp, Foreign).
    pub fn from_value(v: &Value) -> Option<Self> {
        Some(match v {
            Value::Nil => Self::Nil,
            Value::Bool(b) => Self::Bool(*b),
            Value::Int(n) => Self::Int(*n),
            Value::Float(n) => Self::Float(n.to_bits()),
            Value::Str(s) => Self::Str(s.clone()),
            Value::Symbol(s) => Self::Symbol(s.clone()),
            Value::Keyword(s) => Self::Keyword(s.clone()),
            _ => return None,
        })
    }

    /// Convert back to a Value. The reverse direction is total — every
    /// MapKey variant has a corresponding Value variant.
    pub fn to_value(&self) -> Value {
        match self {
            Self::Nil => Value::Nil,
            Self::Bool(b) => Value::Bool(*b),
            Self::Int(n) => Value::Int(*n),
            Self::Float(b) => Value::Float(f64::from_bits(*b)),
            Self::Str(s) => Value::Str(s.clone()),
            Self::Symbol(s) => Value::Symbol(s.clone()),
            Self::Keyword(s) => Value::Keyword(s.clone()),
        }
    }
}

impl fmt::Display for MapKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.to_value())
    }
}

/// State of a `Value::Promise`. Created Pending wrapping a thunk
/// (always a unary closure of zero args); on first force, the thunk
/// runs and the result replaces the state with `Forced(value)`. All
/// subsequent forces return the cached value without re-evaluation.
pub enum PromiseState {
    Pending(Arc<Closure>),
    Forced(Value),
}

impl fmt::Debug for PromiseState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Pending(_) => f.write_str("Pending(…)"),
            Self::Forced(v) => write!(f, "Forced({v:?})"),
        }
    }
}

/// A user-defined closure produced by `(lambda …)` or `(define (f …) …)`.
pub struct Closure {
    pub params: Vec<Arc<str>>,
    /// Optional rest parameter — `(lambda (a b . rest) …)` or
    /// `(lambda (a b &rest rs) …)`.
    pub rest: Option<Arc<str>>,
    /// Body forms, preserved as `Spanned` so error locations inside the
    /// body remain accurate after construction.
    pub body: Vec<Spanned>,
    pub captured_env: Env,
    pub source: Span,
}

/// A host-registered Rust function exposed to Lisp code. The actual
/// callable lives in the `Interpreter<H>`'s `FnRegistry`, keyed by
/// `name` — this struct carries just the lookup key and arity so
/// `Value` remains non-generic over `H`.
#[derive(Clone, Debug)]
pub struct NativeFn {
    pub name: Arc<str>,
    pub arity: Arity,
}

// ── Convenience constructors ────────────────────────────────────────────

impl Value {
    pub fn symbol(s: impl Into<Arc<str>>) -> Self {
        Self::Symbol(s.into())
    }

    pub fn keyword(s: impl Into<Arc<str>>) -> Self {
        Self::Keyword(s.into())
    }

    pub fn string(s: impl Into<Arc<str>>) -> Self {
        Self::Str(s.into())
    }

    pub fn list<I: IntoIterator<Item = Value>>(xs: I) -> Self {
        Self::List(Arc::new(xs.into_iter().collect()))
    }

    pub fn is_truthy(&self) -> bool {
        !matches!(self, Self::Nil | Self::Bool(false))
    }

    /// Is this `Value` the **only** reference to its heap payload?
    ///
    /// The aliasing coordinate. `true` means no other `Value` anywhere can
    /// observe the payload, so mutating it in place is unobservable and a
    /// copy-on-write copy would be pure waste. `false` means somebody else
    /// holds it and the copy is mandatory.
    ///
    /// Two things it is not:
    ///
    /// - **Not a static promise.** It is a refcount reading at one instant,
    ///   which is why it is sound to act on: a primitive that reads `true`
    ///   holds the value by *value*, so nothing can acquire a second
    ///   reference behind its back.
    /// - **Not observable from the language.** No Lisp-visible behaviour
    ///   depends on it — the same call returns the same value either way. It
    ///   only decides whether an allocation happens.
    ///
    /// Variants with no heap payload (`Nil`, `Bool`, `Int`, `Float`) are
    /// trivially unique: there is nothing to share. `Sexp` answers `false`
    /// because it carries its tree by value and this query cannot see the
    /// sharing *inside* that tree — and a wrong `true` is the one answer that
    /// could license an update somebody else observes, so an unmeasured
    /// payload rounds down, never up.
    #[must_use]
    pub fn is_unique(&self) -> bool {
        fn solo<T: ?Sized>(a: &Arc<T>) -> bool {
            Arc::strong_count(a) == 1 && Arc::weak_count(a) == 0
        }
        match self {
            Self::Nil | Self::Bool(_) | Self::Int(_) | Self::Float(_) => true,
            Self::Str(s) | Self::Symbol(s) | Self::Keyword(s) => solo(s),
            Self::List(xs) => solo(xs),
            Self::Map(m) => solo(m),
            Self::Closure(c) => solo(c),
            Self::NativeFn(f) => solo(f),
            Self::Promise(p) => solo(p),
            Self::Error(e) => solo(e),
            Self::Sexp(..) => false,
            Self::Foreign(f) => solo(f),
        }
    }

    /// Short type name for error messages.
    pub fn type_name(&self) -> &'static str {
        match self {
            Self::Nil => "nil",
            Self::Bool(_) => "bool",
            Self::Int(_) => "int",
            Self::Float(_) => "float",
            Self::Str(_) => "string",
            Self::Symbol(_) => "symbol",
            Self::Keyword(_) => "keyword",
            Self::List(_) => "list",
            Self::Map(_) => "map",
            Self::Closure(_) => "closure",
            Self::NativeFn(_) => "native-fn",
            Self::Promise(_) => "promise",
            Self::Error(_) => "error",
            Self::Sexp(..) => "sexp",
            Self::Foreign(_) => "foreign",
        }
    }
}

// ── Debug / Display ─────────────────────────────────────────────────────

impl fmt::Debug for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Nil => f.write_str("Nil"),
            Self::Bool(b) => write!(f, "Bool({b})"),
            Self::Int(n) => write!(f, "Int({n})"),
            Self::Float(n) => write!(f, "Float({n})"),
            Self::Str(s) => write!(f, "Str({s:?})"),
            Self::Symbol(s) => write!(f, "Symbol({s})"),
            Self::Keyword(s) => write!(f, "Keyword(:{s})"),
            Self::List(xs) => f.debug_list().entries(xs.iter()).finish(),
            Self::Map(m) => write!(f, "Map({} entries)", m.len()),
            Self::Closure(_) => f.write_str("Closure(…)"),
            Self::NativeFn(n) => write!(f, "NativeFn({})", n.name),
            Self::Promise(_) => f.write_str("Promise(…)"),
            Self::Error(e) => write!(f, "Error({}: {})", e.tag, e.message),
            Self::Sexp(s, sp) => write!(f, "Sexp({s} @ {sp})"),
            Self::Foreign(_) => f.write_str("Foreign(…)"),
        }
    }
}

impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Nil => f.write_str("()"),
            Self::Bool(true) => f.write_str("#t"),
            Self::Bool(false) => f.write_str("#f"),
            Self::Int(n) => write!(f, "{n}"),
            Self::Float(n) => write!(f, "{n}"),
            Self::Str(s) => write!(f, "{s:?}"),
            Self::Symbol(s) => f.write_str(s),
            Self::Keyword(s) => write!(f, ":{s}"),
            Self::List(xs) => {
                f.write_str("(")?;
                for (i, v) in xs.iter().enumerate() {
                    if i > 0 {
                        f.write_str(" ")?;
                    }
                    write!(f, "{v}")?;
                }
                f.write_str(")")
            }
            Self::Map(m) => {
                // Render as `{k v k v ...}` — Clojure-style. Order is
                // not guaranteed (HashMap), so consumers that need
                // determinism should sort keys themselves.
                f.write_str("{")?;
                for (i, (k, v)) in m.iter().enumerate() {
                    if i > 0 {
                        f.write_str(", ")?;
                    }
                    write!(f, "{k} {v}")?;
                }
                f.write_str("}")
            }
            Self::Closure(c) => {
                write!(f, "#<closure")?;
                if !c.params.is_empty() {
                    write!(f, " ({}", c.params.join(" "))?;
                    if let Some(rest) = &c.rest {
                        write!(f, " . {rest}")?;
                    }
                    write!(f, ")")?;
                }
                write!(f, ">")
            }
            Self::NativeFn(n) => write!(f, "#<native {}>", n.name),
            Self::Promise(p) => {
                let state = p.lock().unwrap();
                match &*state {
                    PromiseState::Pending(_) => f.write_str("#<promise pending>"),
                    PromiseState::Forced(v) => write!(f, "#<promise {v}>"),
                }
            }
            Self::Error(e) => {
                write!(f, "#<error :{} {:?}", e.tag, e.message.as_ref())?;
                if !e.data.is_empty() {
                    f.write_str(" {")?;
                    for (i, (k, v)) in e.data.iter().enumerate() {
                        if i > 0 {
                            f.write_str(" ")?;
                        }
                        write!(f, "{k} {v}")?;
                    }
                    f.write_str("}")?;
                }
                f.write_str(">")
            }
            Self::Sexp(s, _) => write!(f, "'{s}"),
            Self::Foreign(_) => f.write_str("#<foreign>"),
        }
    }
}

// ── Rust <-> Value conversions (partial; filled in Phase 2.4) ──────────

impl From<bool> for Value {
    fn from(b: bool) -> Self {
        Self::Bool(b)
    }
}

impl From<i64> for Value {
    fn from(n: i64) -> Self {
        Self::Int(n)
    }
}

impl From<f64> for Value {
    fn from(n: f64) -> Self {
        Self::Float(n)
    }
}

impl From<String> for Value {
    fn from(s: String) -> Self {
        Self::Str(Arc::from(s))
    }
}

impl From<&str> for Value {
    fn from(s: &str) -> Self {
        Self::Str(Arc::from(s))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn truthiness() {
        assert!(Value::Bool(true).is_truthy());
        assert!(!Value::Bool(false).is_truthy());
        assert!(!Value::Nil.is_truthy());
        assert!(Value::Int(0).is_truthy(), "zero is truthy (Scheme-ish)");
        assert!(Value::list(std::iter::empty::<Value>()).is_truthy());
    }

    #[test]
    fn display_primitives() {
        assert_eq!(Value::Int(42).to_string(), "42");
        assert_eq!(Value::Bool(true).to_string(), "#t");
        assert_eq!(Value::Bool(false).to_string(), "#f");
        assert_eq!(Value::symbol("foo").to_string(), "foo");
        assert_eq!(Value::keyword("k").to_string(), ":k");
        assert_eq!(Value::Nil.to_string(), "()");
    }

    #[test]
    fn display_list() {
        let v = Value::list([Value::Int(1), Value::Int(2), Value::Int(3)]);
        assert_eq!(v.to_string(), "(1 2 3)");
    }

    #[test]
    fn type_names() {
        assert_eq!(Value::Int(0).type_name(), "int");
        assert_eq!(Value::Str(Arc::from("x")).type_name(), "string");
        assert_eq!(Value::Nil.type_name(), "nil");
    }
}