Skip to main content

prima_core/
value.rs

1use std::collections::{HashMap, HashSet};
2
3use num_rational::BigRational;
4use num_traits::ToPrimitive;
5
6use crate::expr_pool::ExprId;
7use crate::number::{Number, Real};
8
9/// Indeterminate form (spec §6.2): mathematically undefined forms (0/0 etc.) that exist **only in the symbolic layer**;
10/// they can take part in later simplification; when collapse to the numeric layer fails they become `Undefined`.
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum IndeterminateForm {
13    ZeroOverZero,
14    InfOverInf,
15    ZeroTimesInf,
16    InfMinusInf,
17}
18
19/// Value type (spec §5): covers the value forms of each layer of the three-world architecture —
20/// the symbolic layer (`Expr`/`Symbol`/`Indeterminate`), the numeric layer (`Number`), and the host layer
21/// (`Bool`/`String`/`Error`, etc.). `Array` is a variable-length heterogeneous sequence (v2.1, spec §11.3);
22/// `Dict`/`Set` are variable host collections keyed/elemented by immutable hashable `ValueKey`s (spec §4.6/§11.6).
23/// `Result`/`Error` carry a structured `Error` as a message string (the structured enum from spec §16.1 is deferred to a later release).
24#[derive(Debug, Clone, PartialEq)]
25pub enum Value {
26    Nil,
27    Number(Number),
28    Bool(bool),
29    Char(char),
30    String(String),
31    Array(Vec<Value>),
32    Dict(HashMap<ValueKey, Value>),
33    Set(HashSet<ValueKey>),
34    Expr(ExprId),
35    Symbol(u32),
36    Indeterminate(IndeterminateForm),
37    Undefined,
38    Error(String),
39    Tuple(Vec<Value>),
40    Result(std::result::Result<Box<Value>, String>),
41    Class(u32), // class instance handle (spec §5); registry lives in prima-runtime
42    /// Compiled/JIT-ed function handle (spec §19.2/§19.4): a process-local id into the
43    /// `prima-runtime::jit` registry. Only produced by the `jit(...)` builtin. Like `Class`,
44    /// the id is process-local (no cross-process serialization) and lives for the process lifetime.
45    JitFunction(u32),
46    Option(Option<Box<Value>>), // Option<T>: Some(T) / None
47}
48
49/// Hashable key for `Dict`/`Set` (spec §11.6): a value-semantic, immutable subset of `Value` —
50/// numbers (canonicalized), strings, chars, bools, and symbol/expr handles.
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub enum ValueKey {
53    Int(i64),
54    BigInt(num_bigint::BigInt),
55    Rational(num_bigint::BigInt, num_bigint::BigInt), // reduced fraction; denominator positive
56    Float(u64),                                       // f64 bit pattern; NaN keys are rejected
57    Str(String),
58    Char(char),
59    Bool(bool),
60    Symbol(u32), // SymbolId.0
61    Expr(u32),   // ExprId inner value
62}
63
64impl ValueKey {
65    /// Convert a `Value` to a hashable key, or `None` if the value is not a valid key type
66    /// (complex numbers, arrays, dicts, sets, class instances, etc. → `None`; NaN → `None`).
67    pub fn from_value(v: &Value) -> Option<ValueKey> {
68        match v {
69            Value::Number(n) => number_to_key(n),
70            Value::String(s) => Some(ValueKey::Str(s.clone())),
71            Value::Char(c) => Some(ValueKey::Char(*c)),
72            Value::Bool(b) => Some(ValueKey::Bool(*b)),
73            Value::Symbol(s) => Some(ValueKey::Symbol(*s)),
74            Value::Expr(id) => Some(ValueKey::Expr(id.as_u32())),
75            _ => None,
76        }
77    }
78
79    /// Reconstruct the corresponding `Value` (numeric keys produce `Number`, `Symbol` produces
80    /// `Value::Symbol`, `Expr` produces `Value::Expr`).
81    pub fn to_value(&self) -> Value {
82        match self {
83            ValueKey::Int(i) => Value::Number(Number::from(*i)),
84            ValueKey::BigInt(b) => Value::Number(Number::Integer(b.clone())),
85            ValueKey::Rational(n, d) => {
86                Value::Number(Number::Rational(BigRational::new(n.clone(), d.clone())))
87            }
88            ValueKey::Float(bits) => Value::Number(Number::Real(Real::F64(f64::from_bits(*bits)))),
89            ValueKey::Str(s) => Value::String(s.clone()),
90            ValueKey::Char(c) => Value::Char(*c),
91            ValueKey::Bool(b) => Value::Bool(*b),
92            ValueKey::Symbol(s) => Value::Symbol(*s),
93            ValueKey::Expr(u) => Value::Expr(ExprId::from_u32(*u)),
94        }
95    }
96}
97
98/// Map a `Number` to a hashable key (spec §11.6): integers that fit `i64` → `Int` (else `BigInt`),
99/// rationals → reduced `Rational` with positive denominator, reals → `Float` bit pattern (NaN → `None`),
100/// complex → `None`. Fixed-width collapsed variants are keyed by value after normalizing to the
101/// exact/`Real` layer (spec §6.1).
102fn number_to_key(n: &Number) -> Option<ValueKey> {
103    if n.is_complex() {
104        return None;
105    }
106    match n {
107        Number::Integer(i) => match i.to_i64() {
108            Some(v) => Some(ValueKey::Int(v)),
109            None => Some(ValueKey::BigInt(i.clone())),
110        },
111        Number::Rational(r) => {
112            // Re-normalize so the key always holds a reduced fraction with a positive denominator (spec §6.1).
113            let r = BigRational::new(r.numer().clone(), r.denom().clone());
114            Some(ValueKey::Rational(r.numer().clone(), r.denom().clone()))
115        }
116        Number::Real(Real::F64(x)) => (!x.is_nan()).then_some(ValueKey::Float(x.to_bits())),
117        Number::Real(Real::F32(x)) => {
118            (!x.is_nan()).then_some(ValueKey::Float((*x as f64).to_bits()))
119        }
120        Number::BigFloat(f) => (!f.is_nan()).then_some(ValueKey::Float(f.to_bits())),
121        // Fixed-width collapsed integers normalize to the exact layer (spec §6.1).
122        other => match other.as_i64() {
123            Some(v) => Some(ValueKey::Int(v)),
124            None => other.as_bigint().map(ValueKey::BigInt),
125        },
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use num_bigint::BigInt;
133    use num_rational::BigRational;
134
135    use crate::expr_pool::ExprPool;
136
137    /// `from_value` → key, `to_value` → the original value (round-trip, spec §11.6).
138    fn assert_roundtrip(v: Value, key: ValueKey) {
139        let k = ValueKey::from_value(&v).unwrap_or_else(|| panic!("expected a key for {v:?}"));
140        assert_eq!(k, key);
141        assert_eq!(k.to_value(), v);
142    }
143
144    #[test]
145    fn int_key_roundtrip() {
146        assert_roundtrip(Value::Number(Number::from(1)), ValueKey::Int(1));
147        assert_roundtrip(Value::Number(Number::from(-7)), ValueKey::Int(-7));
148    }
149
150    #[test]
151    fn bigint_key_roundtrip() {
152        let big = BigInt::from(i64::MAX) + BigInt::from(1);
153        assert_roundtrip(
154            Value::Number(Number::Integer(big.clone())),
155            ValueKey::BigInt(big),
156        );
157    }
158
159    #[test]
160    fn rational_key_roundtrip() {
161        assert_roundtrip(
162            Value::Number(Number::Rational(BigRational::new(
163                BigInt::from(1),
164                BigInt::from(3),
165            ))),
166            ValueKey::Rational(BigInt::from(1), BigInt::from(3)),
167        );
168        // The key always holds a reduced fraction with a positive denominator (spec §6.1).
169        assert_roundtrip(
170            Value::Number(Number::Rational(BigRational::new(
171                BigInt::from(2),
172                BigInt::from(-3),
173            ))),
174            ValueKey::Rational(BigInt::from(-2), BigInt::from(3)),
175        );
176    }
177
178    #[test]
179    fn float_key_roundtrip() {
180        assert_roundtrip(
181            Value::Number(Number::from(2.5)),
182            ValueKey::Float(2.5f64.to_bits()),
183        );
184        // F32 promotes to F64 when keyed (spec §6.1 promotion); the key is the F64 bit pattern.
185        let v = Value::Number(Number::Real(Real::F32(1.5)));
186        assert_eq!(
187            ValueKey::from_value(&v),
188            Some(ValueKey::Float((1.5f32 as f64).to_bits()))
189        );
190        assert_eq!(
191            ValueKey::Float((1.5f32 as f64).to_bits()).to_value(),
192            Value::Number(Number::Real(Real::F64(1.5)))
193        );
194    }
195
196    #[test]
197    fn scalar_key_roundtrip() {
198        assert_roundtrip(
199            Value::String("hello".to_string()),
200            ValueKey::Str("hello".to_string()),
201        );
202        assert_roundtrip(Value::Char('x'), ValueKey::Char('x'));
203        assert_roundtrip(Value::Bool(true), ValueKey::Bool(true));
204    }
205
206    #[test]
207    fn symbol_and_expr_keys() {
208        assert_eq!(
209            ValueKey::from_value(&Value::Symbol(42)),
210            Some(ValueKey::Symbol(42))
211        );
212        assert_eq!(ValueKey::Symbol(42).to_value(), Value::Symbol(42));
213
214        let pool = ExprPool::new();
215        let id = pool.integer(3);
216        assert_eq!(
217            ValueKey::from_value(&Value::Expr(id)),
218            Some(ValueKey::Expr(id.as_u32()))
219        );
220        assert_eq!(ValueKey::Expr(id.as_u32()).to_value(), Value::Expr(id));
221    }
222
223    #[test]
224    fn unsupported_values_are_none() {
225        assert_eq!(
226            ValueKey::from_value(&Value::Number(Number::complex(1, 2))),
227            None
228        );
229        assert_eq!(
230            ValueKey::from_value(&Value::Array(vec![Value::Number(Number::from(1))])),
231            None
232        );
233        assert_eq!(ValueKey::from_value(&Value::Dict(HashMap::new())), None);
234        assert_eq!(ValueKey::from_value(&Value::Set(HashSet::new())), None);
235        assert_eq!(ValueKey::from_value(&Value::Undefined), None);
236        assert_eq!(ValueKey::from_value(&Value::Nil), None);
237    }
238
239    #[test]
240    fn nan_is_not_a_key() {
241        assert_eq!(
242            ValueKey::from_value(&Value::Number(Number::Real(Real::F64(f64::NAN)))),
243            None
244        );
245        assert_eq!(
246            ValueKey::from_value(&Value::Number(Number::Real(Real::F32(f32::NAN)))),
247            None
248        );
249    }
250
251    #[test]
252    fn to_value_reconstructs_values() {
253        assert_eq!(ValueKey::Int(5).to_value(), Value::Number(Number::from(5)));
254        assert_eq!(
255            ValueKey::BigInt(BigInt::from(1u64 << 40)).to_value(),
256            Value::Number(Number::Integer(BigInt::from(1u64 << 40)))
257        );
258        assert_eq!(
259            ValueKey::Float(2.5f64.to_bits()).to_value(),
260            Value::Number(Number::Real(Real::F64(2.5)))
261        );
262        assert_eq!(
263            ValueKey::Str("abc".to_string()).to_value(),
264            Value::String("abc".to_string())
265        );
266        assert_eq!(ValueKey::Char('z').to_value(), Value::Char('z'));
267        assert_eq!(ValueKey::Bool(false).to_value(), Value::Bool(false));
268    }
269
270    #[test]
271    fn dict_and_set_hold_hashable_keys() {
272        let mut m = HashMap::new();
273        m.insert(ValueKey::Str("a".into()), Value::Number(Number::from(1)));
274        let Value::Dict(d) = Value::Dict(m) else {
275            unreachable!()
276        };
277        assert_eq!(
278            d.get(&ValueKey::Str("a".into())),
279            Some(&Value::Number(Number::from(1)))
280        );
281
282        let mut s = HashSet::new();
283        s.insert(ValueKey::Int(1));
284        s.insert(ValueKey::Int(1));
285        s.insert(ValueKey::Float(2.5f64.to_bits()));
286        let Value::Set(s) = Value::Set(s) else {
287            unreachable!()
288        };
289        assert_eq!(s.len(), 2);
290        assert!(s.contains(&ValueKey::Int(1)));
291        assert!(s.contains(&ValueKey::Float(2.5f64.to_bits())));
292    }
293}