Skip to main content

tatara_lisp_eval/
ffi.rs

1//! FFI — register Rust functions as callable Lisp procedures.
2//!
3//! Two registration modes:
4//!
5//!   - **Raw** (`Interpreter::register_fn`): you receive `&[Value]` and
6//!     pick values out yourself. Most flexible, no marshalling overhead.
7//!     Appropriate for primitives that need to inspect arg kinds
8//!     directly or handle variadic arguments.
9//!
10//!   - **Typed** (`Interpreter::register_typed{0,1,2,3,4}`): you declare
11//!     Rust arg + return types; the runtime marshals `Value` ↔ Rust
12//!     types via the `FromValue` and `IntoValue` traits. Arity is
13//!     inferred from the Rust signature. This is the common-case API
14//!     for embedder code.
15//!
16//! Values that need to cross the FFI boundary unchanged (e.g., opaque
17//! host handles) can be wrapped in `Value::Foreign(Arc<dyn Any>)` and
18//! downcast in the native fn body.
19
20use std::sync::atomic::{AtomicUsize, Ordering};
21use std::sync::Arc;
22
23use tatara_lisp::Span;
24
25use crate::error::{EvalError, Result};
26use crate::value::Value;
27use crate::vm::Budget;
28
29/// How many arguments a registered function accepts.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum Arity {
32    Exact(usize),
33    AtLeast(usize),
34    Range(usize, usize),
35    Any,
36}
37
38impl Arity {
39    /// Check `got` against this arity; returns `Ok(())` or a reason string.
40    pub fn check(&self, got: usize) -> std::result::Result<(), String> {
41        match *self {
42            Self::Exact(n) if got == n => Ok(()),
43            Self::Exact(n) => Err(format!("expected exactly {n}, got {got}")),
44            Self::AtLeast(n) if got >= n => Ok(()),
45            Self::AtLeast(n) => Err(format!("expected at least {n}, got {got}")),
46            Self::Range(lo, hi) if got >= lo && got <= hi => Ok(()),
47            Self::Range(lo, hi) => Err(format!("expected {lo}..={hi}, got {got}")),
48            Self::Any => Ok(()),
49        }
50    }
51}
52
53/// A native Rust function the host has registered. Parameterized over the
54/// host context type `H` so the callable can read/write host state.
55///
56/// The simple flavor — no access to the function registry. Use this for
57/// primitives that operate purely on `Value` arguments. For higher-order
58/// primitives (`map`, `filter`, `fold`, ...) that need to invoke a
59/// callable `Value`, register via `Interpreter::register_higher_order_fn`
60/// instead — the host then receives a `Caller` it can use to call back
61/// into the eval loop.
62pub trait NativeCallable<H>: Send + Sync + 'static {
63    fn call(&self, args: &[Value], host: &mut H, call_span: Span) -> Result<Value>;
64}
65
66impl<H, F> NativeCallable<H> for F
67where
68    F: Fn(&[Value], &mut H, Span) -> Result<Value> + Send + Sync + 'static,
69{
70    fn call(&self, args: &[Value], host: &mut H, call_span: Span) -> Result<Value> {
71        (self)(args, host, call_span)
72    }
73}
74
75/// A native Rust primitive that receives its arguments **by value**.
76///
77/// ## Why a second shape rather than widening `NativeCallable`
78///
79/// [`NativeCallable`] hands out a borrowed `&[Value]`, and that one
80/// signature decides the runtime's whole memory story. A borrowed slice is
81/// a live second reference to every `Arc` payload it names, so
82/// `Arc::get_mut` / `Arc::try_unwrap` **cannot succeed inside a primitive**
83/// — not "usually fails", cannot. Every copy-on-write update is therefore
84/// an unconditional full copy, whether or not anyone else can still observe
85/// the old value. `hash-map-set` was the measured instance: an O(n)
86/// `HashMap` clone per insert at every size.
87///
88/// Taking `Vec<Value>` **moves** the arguments in. A primitive whose
89/// argument happens to be the only reference to its payload can mutate that
90/// payload in place; one whose argument is shared still copies. The
91/// difference is read off the refcount at run time, so nothing has to be
92/// proved statically and no program changes meaning.
93///
94/// ## This is a parallel path, deliberately
95///
96/// 243 `register_fn` call sites across this workspace keep compiling
97/// untouched. Reach for `register_owned_fn` when a primitive **builds a new
98/// heap value out of an argument's heap value** — that is the whole class
99/// that can profit. A primitive that only reads its arguments gains nothing
100/// and should stay on [`NativeCallable`], where the borrow documents that it
101/// consumes nothing.
102///
103/// ## An owned primitive must not park
104///
105/// [`Park`](crate::vm::run::Park)'s contract is that a parking primitive has
106/// consumed nothing, because the parked call is re-executed from the top. This
107/// signature consumes its arguments by taking them — so the contract is
108/// unsatisfiable here, not merely hard to honour. That is what lets the VM
109/// skip preserving an owned call's arguments for a retry (`Vm::do_call`), and
110/// skipping that copy is what makes the uniqueness visible at all. A parking
111/// primitive belongs on `register_awaitable_fn`, whose two phases exist for
112/// exactly this reason; one that calls [`crate::vm::Vm::park`] from here gets
113/// [`crate::vm::run::VmError::ParkContract`] rather than a silently corrupted
114/// retry.
115pub trait OwnedCallable<H>: Send + Sync + 'static {
116    fn call(&self, args: Vec<Value>, host: &mut H, call_span: Span) -> Result<Value>;
117}
118
119impl<H, F> OwnedCallable<H> for F
120where
121    F: Fn(Vec<Value>, &mut H, Span) -> Result<Value> + Send + Sync + 'static,
122{
123    fn call(&self, args: Vec<Value>, host: &mut H, call_span: Span) -> Result<Value> {
124        (self)(args, host, call_span)
125    }
126}
127
128/// A higher-order Rust primitive — receives a `Caller` so it can invoke
129/// `Value::Closure` / `Value::NativeFn` arguments back into the eval loop.
130/// Used by `map`, `filter`, `fold`, `for-each`, and friends.
131pub trait HigherOrderCallable<H>: Send + Sync + 'static {
132    fn call(
133        &self,
134        args: &[Value],
135        host: &mut H,
136        caller: &Caller<H>,
137        call_span: Span,
138    ) -> Result<Value>;
139}
140
141impl<H, F> HigherOrderCallable<H> for F
142where
143    F: Fn(&[Value], &mut H, &Caller<H>, Span) -> Result<Value> + Send + Sync + 'static,
144{
145    fn call(
146        &self,
147        args: &[Value],
148        host: &mut H,
149        caller: &Caller<H>,
150        call_span: Span,
151    ) -> Result<Value> {
152        (self)(args, host, caller, call_span)
153    }
154}
155
156/// Handle that a higher-order primitive uses to invoke a callable `Value`
157/// back into the eval loop. Holds borrows of the eval-time read-only
158/// state — the function registry and the macro expander. `apply_value`
159/// dispatches through whichever `Value` kind the callee is (`Closure`,
160/// `NativeFn`, `HigherOrderFn`).
161///
162/// Construction is private — `Caller` only ever appears via
163/// `HigherOrderCallable::call`, so primitives can only obtain one for the
164/// duration of the call they're servicing.
165pub struct Caller<'a, H> {
166    pub(crate) registry: &'a FnRegistry<H>,
167    pub(crate) expander: &'a tatara_lisp::SpannedExpander,
168}
169
170impl<'a, H: 'static> Caller<'a, H> {
171    /// Apply a callable `Value` to `args` against this caller's registry.
172    /// Mirrors the eval loop's `apply` precisely — closures get a fresh
173    /// frame; native fns dispatch through the registry; higher-order
174    /// fns receive a fresh `Caller` of their own.
175    pub fn apply_value(
176        &self,
177        callee: &Value,
178        args: Vec<Value>,
179        host: &mut H,
180        call_span: Span,
181    ) -> Result<Value> {
182        crate::eval::apply_external(callee, args, call_span, self.registry, self.expander, host)
183    }
184
185    /// Borrow the macro expander — primitives like `macroexpand-1`
186    /// look up registered macros through this handle.
187    pub fn expander(&self) -> &tatara_lisp::SpannedExpander {
188        self.expander
189    }
190
191    /// Convenience: call a unary callable with one arg. Errors with a
192    /// canonical message if the callee is not a procedure.
193    pub fn call1(&self, f: &Value, x: Value, host: &mut H, span: Span) -> Result<Value> {
194        self.apply_value(f, vec![x], host, span)
195    }
196
197    /// Convenience: call a binary callable with two args.
198    pub fn call2(&self, f: &Value, a: Value, b: Value, host: &mut H, span: Span) -> Result<Value> {
199        self.apply_value(f, vec![a, b], host, span)
200    }
201}
202
203/// One registered callable. Internal storage; primitives don't see this.
204/// `Arc` (not `Box`) so the apply path can clone the callable out of the
205/// registry borrow before invoking it — letting `apply()` hold `&mut
206/// Interpreter` while a higher-order primitive runs (which lets that
207/// primitive re-enter the dispatch path with the same Interpreter).
208/// A primitive that may have to wait, split so that **waiting cannot
209/// consume**.
210///
211/// ## Why two phases
212///
213/// A one-phase parking primitive — one that inspects the host, decides it
214/// cannot proceed, and returns [`crate::vm::Vm::park`] — carries a contract
215/// the VM cannot check: it must not have consumed anything, because the
216/// parked call is *re-executed from the top*. Take-then-park loses whatever
217/// was taken.
218///
219/// That contract is exactly the kind that holds until the first interesting
220/// case. A selective `receive` is the interesting case: it takes a message,
221/// finds it does not match the pattern, and must wait — and the natural
222/// implementation of that loses the message on every non-match.
223///
224/// Splitting the primitive in two removes the possibility rather than
225/// warning about it. [`AwaitableCallable::ready`] gets `&H` — an immutable
226/// borrow — so **it cannot mutate the host at all**; the compiler rejects
227/// the attempt. [`AwaitableCallable::call`] gets `&mut H` and is invoked
228/// only once `ready` has said yes, so it never has a reason to park.
229///
230/// Take-then-park is not discouraged here. It does not typecheck.
231pub trait AwaitableCallable<H>: Send + Sync + 'static {
232    /// May this call proceed? Answered against an **immutable** host.
233    ///
234    /// Return `false` to park the calling process. The VM restores the stack
235    /// and retries the call later.
236    fn ready(&self, args: &[Value], host: &H) -> bool;
237
238    /// Do the work. Called only when [`Self::ready`] returned `true`.
239    fn call(&self, args: &[Value], host: &mut H, call_span: Span) -> Result<Value>;
240}
241
242/// The ergonomic form: a pair of closures.
243pub struct Awaitable<R, C> {
244    pub ready: R,
245    pub call: C,
246}
247
248impl<H, R, C> AwaitableCallable<H> for Awaitable<R, C>
249where
250    R: Fn(&[Value], &H) -> bool + Send + Sync + 'static,
251    C: Fn(&[Value], &mut H, Span) -> Result<Value> + Send + Sync + 'static,
252{
253    fn ready(&self, args: &[Value], host: &H) -> bool {
254        (self.ready)(args, host)
255    }
256    fn call(&self, args: &[Value], host: &mut H, call_span: Span) -> Result<Value> {
257        (self.call)(args, host, call_span)
258    }
259}
260
261pub(crate) enum FnImpl<H> {
262    Native(Arc<dyn NativeCallable<H>>),
263    Owned(Arc<dyn OwnedCallable<H>>),
264    Higher(Arc<dyn HigherOrderCallable<H>>),
265    Awaitable(Arc<dyn AwaitableCallable<H>>),
266}
267
268impl<H> Clone for FnImpl<H> {
269    fn clone(&self) -> Self {
270        match self {
271            Self::Native(f) => Self::Native(Arc::clone(f)),
272            Self::Owned(f) => Self::Owned(Arc::clone(f)),
273            Self::Higher(f) => Self::Higher(Arc::clone(f)),
274            Self::Awaitable(f) => Self::Awaitable(Arc::clone(f)),
275        }
276    }
277}
278
279impl<H> FnImpl<H> {
280    /// May a call into this callable return the park sentinel
281    /// ([`crate::vm::Vm::park`])?
282    ///
283    /// The VM preserves a copy of a call's arguments so it can rebuild the
284    /// operand stack if the call parks and has to be retried. That copy is
285    /// also a live second reference to every argument for the whole call —
286    /// which is precisely what stops an owned primitive from ever seeing an
287    /// unaliased argument. So the VM asks this first and only pays when the
288    /// answer is yes.
289    ///
290    /// **Only `Owned` answers `false`, and the reason is the signature, not
291    /// the body.** [`Park`](crate::vm::run::Park)'s contract is that a parking
292    /// primitive *must have consumed nothing*, because the call is re-executed
293    /// from the top. An [`OwnedCallable`] receives `Vec<Value>` **by value**:
294    /// it has consumed its arguments before its first line runs. So a parking
295    /// owned primitive is not merely discouraged, it cannot satisfy the
296    /// contract, and skipping its retry copy costs nothing that was reachable.
297    ///
298    /// The three other shapes all answer `true`, `Native` included — an
299    /// earlier version of this reasoned from "no [`Caller`], so it cannot
300    /// reach the eval loop" and got `Native` wrong. [`crate::vm::Vm::park`] is
301    /// public, and `vm::run::tests::install_parking_prim` registers a parking
302    /// primitive through `register_fn` today. Reachability of the *eval loop*
303    /// was never the question; reachability of `Vm::park` is, and only the
304    /// owned signature settles it.
305    pub(crate) fn may_park(&self) -> bool {
306        match self {
307            Self::Owned(_) => false,
308            Self::Native(_) | Self::Higher(_) | Self::Awaitable(_) => true,
309        }
310    }
311}
312
313/// Registry of registered native functions for an `Interpreter<H>`.
314pub(crate) struct FnRegistry<H> {
315    entries: Vec<FnEntry<H>>,
316    /// The host's interrupt: once set, the next evaluation step returns
317    /// [`crate::EvalError::Halted`]. Shared by clones, since a fork is the
318    /// same running program. See `Interpreter::set_interrupt`.
319    pub(crate) halt: Arc<std::sync::atomic::AtomicBool>,
320    /// The tree-walker's execution budget and what this run has spent of it.
321    /// See [`Meter`].
322    pub(crate) meter: Meter,
323    /// The host's call observer, if any. Shared by clones: a fork is watched
324    /// by whoever watched its parent. See [`crate::observe`].
325    pub(crate) observer: Option<Arc<dyn crate::observe::CallObserver>>,
326    /// The frames the last error unwound through. Not shared by clones.
327    pub(crate) unwind: crate::observe::Unwind,
328}
329
330/// A [`Budget`] and the two counters that spend it.
331///
332/// **Not shared by clones.** A fork is a new incarnation and gets the whole
333/// budget again, which is what a test runner giving each test a clean slate
334/// and a supervisor restarting a child both want. The interrupt flag, by
335/// contrast, IS shared: it is the host stopping the program, not the program
336/// running out.
337///
338/// The counters are atomics only so the registry stays `Sync`; one
339/// interpreter runs on one thread, so `Relaxed` load-then-store is exact and
340/// costs a plain load and store.
341pub(crate) struct Meter {
342    pub(crate) budget: Budget,
343    depth: AtomicUsize,
344    spent: AtomicUsize,
345}
346
347impl Meter {
348    pub(crate) fn new(budget: Budget) -> Self {
349        Self {
350            budget,
351            depth: AtomicUsize::new(0),
352            spent: AtomicUsize::new(0),
353        }
354    }
355
356    pub(crate) fn spent(&self) -> usize {
357        self.spent.load(Ordering::Relaxed)
358    }
359
360    /// Enter one non-tail closure call. The guard leaves it on drop, on every
361    /// exit path, `?` included.
362    pub(crate) fn enter(&self, at: Span) -> crate::error::Result<DepthGuard<'_>> {
363        let depth = self.depth.load(Ordering::Relaxed) + 1;
364        if let Some(limit) = self.budget.max_depth {
365            if depth > limit {
366                return Err(crate::EvalError::BudgetExceeded {
367                    dimension: crate::error::BudgetDimension::Depth,
368                    limit,
369                    function: None,
370                    at,
371                });
372            }
373        }
374        self.depth.store(depth, Ordering::Relaxed);
375        Ok(DepthGuard(&self.depth))
376    }
377}
378
379/// Leaves the call [`Meter::enter`] entered.
380pub(crate) struct DepthGuard<'a>(&'a AtomicUsize);
381
382impl Drop for DepthGuard<'_> {
383    fn drop(&mut self) {
384        self.0
385            .store(self.0.load(Ordering::Relaxed) - 1, Ordering::Relaxed);
386    }
387}
388
389pub(crate) struct FnEntry<H> {
390    pub name: Arc<str>,
391    /// Kept for future registry introspection — arity checking at call
392    /// time uses the copy on `Value::NativeFn` for a quicker path.
393    #[allow(dead_code)]
394    pub arity: Arity,
395    pub callable: FnImpl<H>,
396}
397
398// Hand-written rather than derived: `#[derive(Clone)]` on a generic struct
399// adds a `H: Clone` bound, and `H` is the embedder's host type, which has no
400// reason to be cloneable. Nothing here actually holds an `H` — the callables
401// are `Arc<dyn …>` — so the bound would be a derive artefact that blocks every
402// real embedder from forking an interpreter.
403impl<H> Clone for FnEntry<H> {
404    fn clone(&self) -> Self {
405        Self {
406            name: Arc::clone(&self.name),
407            arity: self.arity,
408            callable: self.callable.clone(),
409        }
410    }
411}
412
413impl<H> Clone for FnRegistry<H> {
414    fn clone(&self) -> Self {
415        Self {
416            entries: self.entries.clone(),
417            halt: Arc::clone(&self.halt),
418            meter: Meter::new(self.meter.budget),
419            observer: self.observer.clone(),
420            unwind: crate::observe::Unwind::default(),
421        }
422    }
423}
424
425impl<H> Default for FnRegistry<H> {
426    fn default() -> Self {
427        Self {
428            entries: Vec::new(),
429            halt: Arc::new(std::sync::atomic::AtomicBool::new(false)),
430            meter: Meter::new(Budget::tree_walker_default()),
431            observer: None,
432            unwind: crate::observe::Unwind::default(),
433        }
434    }
435}
436
437impl<H> FnRegistry<H> {
438    /// One evaluation step: `Err(Halted)` once the host has interrupted this
439    /// program, `Err(BudgetExceeded)` once it has spent its fuel.
440    #[inline]
441    pub(crate) fn step(&self, at: Span) -> crate::error::Result<()> {
442        if self.halt.load(Ordering::Relaxed) {
443            return Err(crate::EvalError::Halted);
444        }
445        let spent = self.meter.spent.load(Ordering::Relaxed) + 1;
446        self.meter.spent.store(spent, Ordering::Relaxed);
447        match self.meter.budget.fuel {
448            Some(limit) if spent > limit => Err(crate::EvalError::BudgetExceeded {
449                dimension: crate::error::BudgetDimension::Fuel,
450                limit,
451                function: None,
452                at,
453            }),
454            _ => Ok(()),
455        }
456    }
457
458    pub(crate) fn new() -> Self {
459        Self::default()
460    }
461
462    pub(crate) fn insert(&mut self, entry: FnEntry<H>) {
463        // Shadow any earlier registration with the same name — last wins.
464        if let Some(slot) = self.entries.iter_mut().find(|e| e.name == entry.name) {
465            *slot = entry;
466        } else {
467            self.entries.push(entry);
468        }
469    }
470
471    pub(crate) fn lookup(&self, name: &str) -> Option<&FnEntry<H>> {
472        self.entries.iter().find(|e| &*e.name == name)
473    }
474}
475
476// ── Typed marshalling ──────────────────────────────────────────────────
477
478/// Convert from a Lisp `Value` into a Rust value. Implemented for the
479/// common primitive types and for `Value` itself (identity). Used by the
480/// `register_typed{N}` helpers to destructure args.
481pub trait FromValue: Sized {
482    fn from_value(v: &Value, at: Span) -> Result<Self>;
483}
484
485impl FromValue for Value {
486    fn from_value(v: &Value, _at: Span) -> Result<Self> {
487        Ok(v.clone())
488    }
489}
490
491impl FromValue for i64 {
492    fn from_value(v: &Value, at: Span) -> Result<Self> {
493        match v {
494            Value::Int(n) => Ok(*n),
495            other => Err(EvalError::type_mismatch("integer", other.type_name(), at)),
496        }
497    }
498}
499
500impl FromValue for f64 {
501    fn from_value(v: &Value, at: Span) -> Result<Self> {
502        match v {
503            Value::Int(n) => Ok(*n as f64),
504            Value::Float(n) => Ok(*n),
505            other => Err(EvalError::type_mismatch("number", other.type_name(), at)),
506        }
507    }
508}
509
510impl FromValue for bool {
511    fn from_value(v: &Value, at: Span) -> Result<Self> {
512        match v {
513            Value::Bool(b) => Ok(*b),
514            other => Err(EvalError::type_mismatch("bool", other.type_name(), at)),
515        }
516    }
517}
518
519impl FromValue for String {
520    fn from_value(v: &Value, at: Span) -> Result<Self> {
521        match v {
522            Value::Str(s) => Ok(s.to_string()),
523            other => Err(EvalError::type_mismatch("string", other.type_name(), at)),
524        }
525    }
526}
527
528impl FromValue for Arc<str> {
529    fn from_value(v: &Value, at: Span) -> Result<Self> {
530        match v {
531            Value::Str(s) => Ok(s.clone()),
532            Value::Symbol(s) => Ok(s.clone()),
533            Value::Keyword(s) => Ok(s.clone()),
534            other => Err(EvalError::type_mismatch(
535                "string/symbol",
536                other.type_name(),
537                at,
538            )),
539        }
540    }
541}
542
543impl FromValue for Vec<Value> {
544    fn from_value(v: &Value, at: Span) -> Result<Self> {
545        match v {
546            Value::Nil => Ok(Vec::new()),
547            Value::List(xs) => Ok(xs.to_vec()),
548            other => Err(EvalError::type_mismatch("list", other.type_name(), at)),
549        }
550    }
551}
552
553impl<T: FromValue> FromValue for Option<T> {
554    fn from_value(v: &Value, at: Span) -> Result<Self> {
555        match v {
556            Value::Nil => Ok(None),
557            other => T::from_value(other, at).map(Some),
558        }
559    }
560}
561
562/// Convert a Rust value into a `Value` for Lisp. Implemented for the
563/// primitive types. The blanket `From<T> for Value` impls cover most
564/// cases; this trait is the named interface used by typed-helper
565/// registration.
566pub trait IntoValue {
567    fn into_value(self) -> Value;
568}
569
570impl IntoValue for Value {
571    fn into_value(self) -> Value {
572        self
573    }
574}
575
576impl IntoValue for () {
577    fn into_value(self) -> Value {
578        Value::Nil
579    }
580}
581
582impl IntoValue for bool {
583    fn into_value(self) -> Value {
584        Value::Bool(self)
585    }
586}
587
588impl IntoValue for i64 {
589    fn into_value(self) -> Value {
590        Value::Int(self)
591    }
592}
593
594impl IntoValue for f64 {
595    fn into_value(self) -> Value {
596        Value::Float(self)
597    }
598}
599
600impl IntoValue for String {
601    fn into_value(self) -> Value {
602        Value::Str(Arc::from(self))
603    }
604}
605
606impl IntoValue for &str {
607    fn into_value(self) -> Value {
608        Value::Str(Arc::from(self))
609    }
610}
611
612impl IntoValue for Arc<str> {
613    fn into_value(self) -> Value {
614        Value::Str(self)
615    }
616}
617
618impl<T: IntoValue> IntoValue for Option<T> {
619    fn into_value(self) -> Value {
620        match self {
621            None => Value::Nil,
622            Some(x) => x.into_value(),
623        }
624    }
625}
626
627impl<T: IntoValue> IntoValue for Vec<T> {
628    fn into_value(self) -> Value {
629        Value::list(self.into_iter().map(IntoValue::into_value))
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn arity_check() {
639        assert!(Arity::Exact(2).check(2).is_ok());
640        assert!(Arity::Exact(2).check(3).is_err());
641        assert!(Arity::AtLeast(1).check(5).is_ok());
642        assert!(Arity::AtLeast(1).check(0).is_err());
643        assert!(Arity::Range(1, 3).check(2).is_ok());
644        assert!(Arity::Range(1, 3).check(4).is_err());
645        assert!(Arity::Any.check(0).is_ok());
646        assert!(Arity::Any.check(1000).is_ok());
647    }
648
649    #[test]
650    fn from_value_round_trips_primitives() {
651        let sp = Span::synthetic();
652        assert_eq!(i64::from_value(&Value::Int(42), sp).unwrap(), 42);
653        assert_eq!(f64::from_value(&Value::Float(1.5), sp).unwrap(), 1.5);
654        assert!(bool::from_value(&Value::Bool(true), sp).unwrap());
655        assert_eq!(
656            String::from_value(&Value::Str(Arc::from("hi")), sp).unwrap(),
657            "hi"
658        );
659    }
660
661    #[test]
662    fn from_value_int_to_float_coerces() {
663        let sp = Span::synthetic();
664        assert_eq!(f64::from_value(&Value::Int(3), sp).unwrap(), 3.0);
665    }
666
667    #[test]
668    fn from_value_option_nil_is_none() {
669        let sp = Span::synthetic();
670        assert_eq!(
671            <Option<i64> as FromValue>::from_value(&Value::Nil, sp).unwrap(),
672            None
673        );
674        assert_eq!(
675            <Option<i64> as FromValue>::from_value(&Value::Int(7), sp).unwrap(),
676            Some(7)
677        );
678    }
679
680    #[test]
681    fn from_value_type_mismatch_reports_expected_kind() {
682        let sp = Span::synthetic();
683        let err = i64::from_value(&Value::Str(Arc::from("x")), sp).unwrap_err();
684        assert!(matches!(
685            err,
686            EvalError::TypeMismatch {
687                expected: "integer",
688                ..
689            }
690        ));
691    }
692
693    #[test]
694    fn into_value_round_trips() {
695        assert!(matches!(42i64.into_value(), Value::Int(42)));
696        assert!(matches!(true.into_value(), Value::Bool(true)));
697        assert!(matches!(().into_value(), Value::Nil));
698        match String::from("hello").into_value() {
699            Value::Str(s) => assert_eq!(&*s, "hello"),
700            other => panic!("{other:?}"),
701        }
702    }
703
704    #[test]
705    fn into_value_vec_produces_list() {
706        let v: Vec<i64> = vec![1, 2, 3];
707        match v.into_value() {
708            Value::List(xs) => {
709                assert_eq!(xs.len(), 3);
710                assert!(matches!(&xs[0], Value::Int(1)));
711            }
712            other => panic!("{other:?}"),
713        }
714    }
715}