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}