Skip to main content

soil_rt/
value.rs

1use std::any::Any;
2use std::cmp::Ordering;
3use std::fmt;
4use std::ops::Deref;
5use std::rc::Rc;
6
7use num_bigint::BigInt;
8
9use crate::descriptor::TypeId;
10use crate::error::{PanicKind, SoilError};
11
12/// A reference-counted, immutable heap allocation.
13///
14/// Non-atomic by decision: a runtime instance and every value it owns
15/// belong to a single thread. Cycles are unreachable through the public
16/// API today but would leak; the cycle-collection strategy is explicitly
17/// deferred (design §10).
18///
19/// Debug builds maintain a thread-local count of live allocations,
20/// exposed through [`debug_live_values`], which backs the leak-check
21/// exit criterion of plan 01.
22pub struct Ref<T: ?Sized>(Rc<T>);
23
24#[cfg(debug_assertions)]
25mod live {
26    use std::cell::Cell;
27
28    thread_local! {
29        static LIVE: Cell<usize> = const { Cell::new(0) };
30    }
31
32    pub fn inc() {
33        LIVE.with(|c| c.set(c.get() + 1));
34    }
35
36    pub fn dec() {
37        LIVE.with(|c| c.set(c.get() - 1));
38    }
39
40    pub fn count() -> usize {
41        LIVE.with(|c| c.get())
42    }
43}
44
45/// The number of live runtime allocations on this thread.
46///
47/// Meaningful in debug builds only; release builds compile the counter
48/// out and always return 0.
49pub fn debug_live_values() -> usize {
50    #[cfg(debug_assertions)]
51    {
52        live::count()
53    }
54    #[cfg(not(debug_assertions))]
55    {
56        0
57    }
58}
59
60impl<T> Ref<T> {
61    pub fn new(value: T) -> Self {
62        #[cfg(debug_assertions)]
63        live::inc();
64        Ref(Rc::new(value))
65    }
66}
67
68impl<T: ?Sized> Ref<T> {
69    /// The allocation's address — the identity used by the `opaque`
70    /// derivation strategy (design §3.7).
71    pub fn addr(&self) -> usize {
72        Rc::as_ptr(&self.0) as *const () as usize
73    }
74
75    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
76        Rc::ptr_eq(&a.0, &b.0)
77    }
78}
79
80impl From<&str> for Ref<str> {
81    fn from(s: &str) -> Self {
82        #[cfg(debug_assertions)]
83        live::inc();
84        Ref(Rc::from(s))
85    }
86}
87
88impl From<String> for Ref<str> {
89    fn from(s: String) -> Self {
90        #[cfg(debug_assertions)]
91        live::inc();
92        Ref(Rc::from(s.into_boxed_str()))
93    }
94}
95
96impl From<&[u8]> for Ref<[u8]> {
97    fn from(b: &[u8]) -> Self {
98        #[cfg(debug_assertions)]
99        live::inc();
100        Ref(Rc::from(b))
101    }
102}
103
104impl From<Vec<u8>> for Ref<[u8]> {
105    fn from(b: Vec<u8>) -> Self {
106        #[cfg(debug_assertions)]
107        live::inc();
108        Ref(Rc::from(b.into_boxed_slice()))
109    }
110}
111
112impl<T: ?Sized> Clone for Ref<T> {
113    fn clone(&self) -> Self {
114        Ref(self.0.clone())
115    }
116}
117
118impl<T: ?Sized> Drop for Ref<T> {
119    fn drop(&mut self) {
120        #[cfg(debug_assertions)]
121        if Rc::strong_count(&self.0) == 1 {
122            live::dec();
123        }
124    }
125}
126
127impl<T: ?Sized> Deref for Ref<T> {
128    type Target = T;
129
130    fn deref(&self) -> &T {
131        &self.0
132    }
133}
134
135impl<T: ?Sized + fmt::Debug> fmt::Debug for Ref<T> {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        self.0.fmt(f)
138    }
139}
140
141/// A Soil value.
142///
143/// Scalars are stored inline; everything else is behind a [`Ref`], so
144/// `clone` is at most a refcount bump. `Bool` is deliberately absent: it
145/// is the prelude sum `True | False` (design §3.12 / plan 01), which the
146/// registry pre-registers and the JSON layer special-cases.
147#[derive(Clone, Debug)]
148pub enum Value {
149    I64(i64),
150    U64(u64),
151    I32(i32),
152    U32(u32),
153    I16(i16),
154    U16(u16),
155    I8(i8),
156    U8(u8),
157    F64(f64),
158    BigInt(Ref<BigInt>),
159    Utf8(Ref<str>),
160    Bytes(Ref<[u8]>),
161    Unit,
162    Record(Ref<Record>),
163    Sum(Ref<SumVal>),
164    List(Ref<Vec<Value>>),
165    Map(Ref<MapVal>),
166    Closure(Ref<Closure>),
167    Opaque(Ref<OpaqueVal>),
168}
169
170/// A record value: its type plus field values in declaration order.
171/// Field names live in the type's descriptor, not in the value.
172#[derive(Debug)]
173pub struct Record {
174    pub type_id: TypeId,
175    pub fields: Box<[Value]>,
176}
177
178/// A sum value: its type, the variant's declaration index, and at most
179/// one payload (design §3.1). Variant names live in the descriptor.
180#[derive(Debug)]
181pub struct SumVal {
182    pub type_id: TypeId,
183    pub variant: u32,
184    pub payload: Option<Value>,
185}
186
187/// An opaque handle (FFI values, abstract types). Identity — for `eq`,
188/// `compare`, and `hash` under the `opaque` strategy — is the address of
189/// the containing allocation, so two handles are equal iff they are the
190/// same allocation.
191pub struct OpaqueVal {
192    pub type_id: TypeId,
193    pub payload: Box<dyn Any>,
194}
195
196impl fmt::Debug for OpaqueVal {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        write!(f, "<handle:{:?}>", self.type_id)
199    }
200}
201
202/// A Soil closure, as a boxed Rust callable (resolved decision, impl
203/// plan §1): plan-01 tests and builtins pass Rust closures, `soil0`'s
204/// interpreter captures AST + environment, compiled code later wraps an
205/// `extern "C"` function and its environment. One invocation path.
206pub type ClosureFn = Box<dyn Fn(&[Value]) -> Result<Value, SoilError>>;
207
208pub struct Closure {
209    f: ClosureFn,
210}
211
212impl Closure {
213    pub fn native(f: impl Fn(&[Value]) -> Result<Value, SoilError> + 'static) -> Self {
214        Closure { f: Box::new(f) }
215    }
216
217    pub fn call(&self, args: &[Value]) -> Result<Value, SoilError> {
218        (self.f)(args)
219    }
220}
221
222impl fmt::Debug for Closure {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        write!(f, "<closure>")
225    }
226}
227
228/// How a map orders its keys.
229///
230/// `Custom` carries a program-supplied comparator in OCaml's protocol
231/// (design §3.6): a closure taking `[k1, k2]` and returning an `I64`
232/// that is negative, zero, or positive. `Structural` is the derived
233/// total order (`ops::compare`) — the order used at the JSON boundary,
234/// where a decoded map has no program closure to carry.
235#[derive(Debug)]
236pub enum MapOrder {
237    Structural,
238    Custom(Ref<Closure>),
239}
240
241/// A Soil map: entries kept sorted by the carried order (a sorted vec
242/// by resolved decision; swap for a tree behind this API only if
243/// profiling demands it — plan 01).
244///
245/// Operations take the [`Runtime`](crate::Runtime) because the
246/// structural order consults the registry, and they are fallible
247/// because a custom comparator is a Soil closure. A comparator that is
248/// not a total order silently corrupts the sorted invariant; detecting
249/// that is the refinement checker's job, not the runtime's (impl plan
250/// §7). The order is structure, not content: derived `eq`/`compare`
251/// look only at the entries.
252#[derive(Debug)]
253pub struct MapVal {
254    order: MapOrder,
255    entries: Vec<(Value, Value)>,
256}
257
258impl MapVal {
259    pub fn new(order: MapOrder) -> Self {
260        MapVal {
261            order,
262            entries: Vec::new(),
263        }
264    }
265
266    pub fn order(&self) -> &MapOrder {
267        &self.order
268    }
269
270    /// Entries in comparator order — the order `show` and JSON emit.
271    pub fn entries(&self) -> &[(Value, Value)] {
272        &self.entries
273    }
274
275    pub fn len(&self) -> usize {
276        self.entries.len()
277    }
278
279    pub fn is_empty(&self) -> bool {
280        self.entries.is_empty()
281    }
282
283    fn compare_keys(
284        &self,
285        runtime: &crate::Runtime,
286        a: &Value,
287        b: &Value,
288    ) -> Result<Ordering, SoilError> {
289        match &self.order {
290            MapOrder::Structural => crate::ops::compare(runtime, a, b),
291            MapOrder::Custom(comparator) => {
292                let result = comparator.call(&[a.clone(), b.clone()])?;
293                match result {
294                    Value::I64(n) => Ok(n.cmp(&0)),
295                    other => Err(SoilError::new(
296                        PanicKind::TypeError,
297                        format!("map comparator returned {other:?}, expected I64"),
298                    )),
299                }
300            }
301        }
302    }
303
304    /// Binary search for `key`: `Ok(index)` if present, `Err(insertion
305    /// point)` if absent — or a comparator failure.
306    fn search(
307        &self,
308        runtime: &crate::Runtime,
309        key: &Value,
310    ) -> Result<Result<usize, usize>, SoilError> {
311        let mut lo = 0usize;
312        let mut hi = self.entries.len();
313        while lo < hi {
314            let mid = lo + (hi - lo) / 2;
315            match self.compare_keys(runtime, &self.entries[mid].0, key)? {
316                Ordering::Less => lo = mid + 1,
317                Ordering::Greater => hi = mid,
318                Ordering::Equal => return Ok(Ok(mid)),
319            }
320        }
321        Ok(Err(lo))
322    }
323
324    pub fn get(&self, runtime: &crate::Runtime, key: &Value) -> Result<Option<&Value>, SoilError> {
325        Ok(self.search(runtime, key)?.ok().map(|i| &self.entries[i].1))
326    }
327
328    /// Insert or replace. Mutation is a construction-time affair: a
329    /// `MapVal` already shared inside a `Value::Map` is immutable, and
330    /// Soil-level insertion clones the map (no mutation, design §3.13).
331    pub fn insert(
332        &mut self,
333        runtime: &crate::Runtime,
334        key: Value,
335        value: Value,
336    ) -> Result<(), SoilError> {
337        match self.search(runtime, &key)? {
338            Ok(i) => self.entries[i].1 = value,
339            Err(i) => self.entries.insert(i, (key, value)),
340        }
341        Ok(())
342    }
343
344    pub fn remove(
345        &mut self,
346        runtime: &crate::Runtime,
347        key: &Value,
348    ) -> Result<Option<Value>, SoilError> {
349        match self.search(runtime, key)? {
350            Ok(i) => Ok(Some(self.entries.remove(i).1)),
351            Err(_) => Ok(None),
352        }
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    fn i64_comparator() -> Ref<Closure> {
361        Ref::new(Closure::native(|args| match (&args[0], &args[1]) {
362            (Value::I64(a), Value::I64(b)) => Ok(Value::I64(match a.cmp(b) {
363                Ordering::Less => -1,
364                Ordering::Equal => 0,
365                Ordering::Greater => 1,
366            })),
367            _ => Err(SoilError::new(PanicKind::TypeError, "expected I64 keys")),
368        }))
369    }
370
371    #[test]
372    fn value_is_at_most_three_words() {
373        assert!(std::mem::size_of::<Value>() <= 24);
374    }
375
376    #[test]
377    fn refcounting_returns_to_zero() {
378        let before = debug_live_values();
379        {
380            let a = Ref::new(vec![Value::I64(1), Value::Unit]);
381            let b = a.clone();
382            assert!(Ref::ptr_eq(&a, &b));
383            assert_eq!(debug_live_values(), before + 1);
384            let s: Ref<str> = Ref::from("hello");
385            assert_eq!(&*s, "hello");
386            assert_eq!(debug_live_values(), before + 2);
387        }
388        assert_eq!(debug_live_values(), before);
389    }
390
391    #[test]
392    fn nested_values_release_on_drop() {
393        let before = debug_live_values();
394        {
395            let inner: Ref<str> = Ref::from("payload");
396            let list = Value::List(Ref::new(vec![Value::Utf8(inner)]));
397            let copy = list.clone();
398            drop(list);
399            assert_eq!(debug_live_values(), before + 2);
400            drop(copy);
401        }
402        assert_eq!(debug_live_values(), before);
403    }
404
405    #[test]
406    fn closure_invocation() {
407        let double = Closure::native(|args| match args {
408            [Value::I64(n)] => Ok(Value::I64(n * 2)),
409            _ => Err(SoilError::new(PanicKind::TypeError, "expected one I64")),
410        });
411        match double.call(&[Value::I64(21)]) {
412            Ok(Value::I64(42)) => {}
413            other => panic!("unexpected result: {other:?}"),
414        }
415        assert!(double.call(&[Value::Unit]).is_err());
416    }
417
418    #[test]
419    fn map_insert_get_remove_in_comparator_order() {
420        let rt = crate::Runtime::new();
421        let mut map = MapVal::new(MapOrder::Custom(i64_comparator()));
422        for k in [30i64, 10, 20, 10] {
423            map.insert(&rt, Value::I64(k), Value::I64(k * 100)).unwrap();
424        }
425        assert_eq!(map.len(), 3);
426        let keys: Vec<i64> = map
427            .entries()
428            .iter()
429            .map(|(k, _)| match k {
430                Value::I64(n) => *n,
431                _ => unreachable!(),
432            })
433            .collect();
434        assert_eq!(keys, vec![10, 20, 30]);
435
436        match map.get(&rt, &Value::I64(20)).unwrap() {
437            Some(Value::I64(2000)) => {}
438            other => panic!("unexpected: {other:?}"),
439        }
440        assert!(map.get(&rt, &Value::I64(99)).unwrap().is_none());
441
442        match map.remove(&rt, &Value::I64(10)).unwrap() {
443            Some(Value::I64(1000)) => {}
444            other => panic!("unexpected: {other:?}"),
445        }
446        assert_eq!(map.len(), 2);
447        assert!(map.remove(&rt, &Value::I64(10)).unwrap().is_none());
448    }
449
450    #[test]
451    fn map_structural_order_uses_derived_compare() {
452        let rt = crate::Runtime::new();
453        let mut map = MapVal::new(MapOrder::Structural);
454        for k in [30i64, 10, 20] {
455            map.insert(&rt, Value::I64(k), Value::Unit).unwrap();
456        }
457        let keys: Vec<i64> = map
458            .entries()
459            .iter()
460            .map(|(k, _)| match k {
461                Value::I64(n) => *n,
462                _ => unreachable!(),
463            })
464            .collect();
465        assert_eq!(keys, vec![10, 20, 30]);
466    }
467
468    #[test]
469    fn map_comparator_errors_propagate() {
470        let rt = crate::Runtime::new();
471        let mut map = MapVal::new(MapOrder::Custom(Ref::new(Closure::native(|_| {
472            Err(SoilError::new(PanicKind::TypeError, "always fails"))
473        }))));
474        assert!(map.insert(&rt, Value::I64(1), Value::Unit).is_ok());
475        assert!(map.insert(&rt, Value::I64(2), Value::Unit).is_err());
476    }
477
478    #[test]
479    fn opaque_identity_is_address() {
480        let a = Ref::new(OpaqueVal {
481            type_id: TypeId(0),
482            payload: Box::new(7u8),
483        });
484        let b = a.clone();
485        let c = Ref::new(OpaqueVal {
486            type_id: TypeId(0),
487            payload: Box::new(7u8),
488        });
489        assert_eq!(a.addr(), b.addr());
490        assert_ne!(a.addr(), c.addr());
491    }
492}