Skip to main content

pepl_stdlib/
value.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::error::StdlibError;
6
7/// Runtime value in PEPL.
8///
9/// All PEPL values are immutable — operations that "modify" a value return a
10/// new value instead. [`BTreeMap`] is used for records to guarantee
11/// deterministic iteration order (a core PEPL invariant).
12///
13/// # Type names
14///
15/// [`Value::type_name`] returns the string used by `core.type_of()`:
16/// `"number"`, `"string"`, `"bool"`, `"nil"`, `"list"`, `"record"` (or the
17/// declared type name for named records/sum variants), `"color"`, `"result"`.
18#[derive(Debug, Clone)]
19pub enum Value {
20    /// 64-bit IEEE 754 floating-point number.
21    ///
22    /// NaN is prevented from entering state — operations that would produce
23    /// NaN trap instead.
24    Number(f64),
25
26    /// UTF-8 string.
27    String(String),
28
29    /// Boolean value.
30    Bool(bool),
31
32    /// The absence of a value.
33    Nil,
34
35    /// Ordered collection of values.
36    List(Vec<Value>),
37
38    /// Named fields with values. Uses [`BTreeMap`] for deterministic ordering.
39    ///
40    /// `type_name` is `Some("Todo")` for named record types (`type Todo = { ... }`),
41    /// `None` for anonymous inline records (`{ x: 1, y: 2 }`).
42    Record {
43        type_name: Option<String>,
44        fields: BTreeMap<String, Value>,
45    },
46
47    /// RGBA color value. Each component is in the range 0.0–1.0.
48    Color { r: f64, g: f64, b: f64, a: f64 },
49
50    /// Result type for fallible operations (`Ok` or `Err`).
51    Result(Box<ResultValue>),
52
53    /// Sum type variant (e.g., `Shape.Circle(5, 10)`).
54    ///
55    /// `type_name` is the declaring sum type (e.g., `"Shape"`).
56    /// `variant` is the variant name (e.g., `"Circle"`).
57    /// `fields` holds positional values — empty for unit variants like `Active`.
58    SumVariant {
59        type_name: String,
60        variant: String,
61        fields: Vec<Value>,
62    },
63
64    /// A callable function value for higher-order stdlib operations (map, filter, etc.).
65    ///
66    /// Wraps an `Arc<dyn Fn>` so it can be cloned and passed through `Vec<Value>`.
67    /// The evaluator creates these by wrapping PEPL lambdas/functions.
68    Function(StdlibFn),
69}
70
71/// A callable function value for higher-order stdlib operations.
72///
73/// Wraps an `Arc<dyn Fn>` so it can be cloned, and provides Debug/PartialEq
74/// implementations that the derive macros can't auto-generate for `dyn Fn`.
75#[derive(Clone)]
76pub struct StdlibFn(pub Arc<dyn Fn(Vec<Value>) -> Result<Value, StdlibError> + Send + Sync>);
77
78impl StdlibFn {
79    /// Create a new stdlib function from a closure.
80    pub fn new(
81        f: impl Fn(Vec<Value>) -> Result<Value, StdlibError> + Send + Sync + 'static,
82    ) -> Self {
83        Self(Arc::new(f))
84    }
85
86    /// Call the function with the given arguments.
87    pub fn call(&self, args: Vec<Value>) -> Result<Value, StdlibError> {
88        (self.0)(args)
89    }
90}
91
92impl fmt::Debug for StdlibFn {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "<function>")
95    }
96}
97
98impl PartialEq for StdlibFn {
99    fn eq(&self, other: &Self) -> bool {
100        // Function identity by Arc pointer equality
101        Arc::ptr_eq(&self.0, &other.0)
102    }
103}
104
105/// The two variants of a PEPL `Result` value.
106#[derive(Debug, Clone)]
107pub enum ResultValue {
108    Ok(Value),
109    Err(Value),
110}
111
112// ── Equality ──────────────────────────────────────────────────────────────────
113//
114// Structural equality per execution-semantics.md:
115//   - number:  IEEE 754 (NaN != NaN) — handled by f64 partial_eq
116//   - string:  byte-for-byte UTF-8
117//   - bool:    value equality
118//   - nil:     nil == nil
119//   - list:    same length + element-by-element
120//   - record:  recursive field-by-field
121//   - color:   RGBA value comparison
122//   - result:  same variant + same inner value
123//   - record:  structural (type_name ignored — type checker ensures compatibility)
124//   - sum:     nominal (type_name + variant + fields must all match)
125//   - Note: Functions/lambdas live in EvalValue (pepl-eval), not here
126
127impl PartialEq for Value {
128    fn eq(&self, other: &Self) -> bool {
129        match (self, other) {
130            (Value::Number(a), Value::Number(b)) => a == b, // IEEE 754: NaN != NaN
131            (Value::String(a), Value::String(b)) => a == b,
132            (Value::Bool(a), Value::Bool(b)) => a == b,
133            (Value::Nil, Value::Nil) => true,
134            (Value::List(a), Value::List(b)) => a == b,
135            // Structural equality for records — type_name is metadata, not identity
136            (Value::Record { fields: a, .. }, Value::Record { fields: b, .. }) => a == b,
137            (
138                Value::Color {
139                    r: r1,
140                    g: g1,
141                    b: b1,
142                    a: a1,
143                },
144                Value::Color {
145                    r: r2,
146                    g: g2,
147                    b: b2,
148                    a: a2,
149                },
150            ) => r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2,
151            (Value::Result(a), Value::Result(b)) => a == b,
152            // Nominal equality for sum variants — same type + variant + fields
153            (
154                Value::SumVariant {
155                    type_name: t1,
156                    variant: v1,
157                    fields: f1,
158                },
159                Value::SumVariant {
160                    type_name: t2,
161                    variant: v2,
162                    fields: f2,
163                },
164            ) => t1 == t2 && v1 == v2 && f1 == f2,
165            // Function identity by Arc pointer equality
166            (Value::Function(a), Value::Function(b)) => a == b,
167            _ => false, // different variants are never equal
168        }
169    }
170}
171
172impl PartialEq for ResultValue {
173    fn eq(&self, other: &Self) -> bool {
174        match (self, other) {
175            (ResultValue::Ok(a), ResultValue::Ok(b)) => a == b,
176            (ResultValue::Err(a), ResultValue::Err(b)) => a == b,
177            _ => false,
178        }
179    }
180}
181
182// ── Display ───────────────────────────────────────────────────────────────────
183//
184// Used by `core.log`, `convert.to_string`, and `string.from`.
185
186impl fmt::Display for Value {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            Value::Number(n) => {
190                // Print integers without decimal point
191                if n.fract() == 0.0 && n.is_finite() {
192                    write!(f, "{}", *n as i64)
193                } else {
194                    write!(f, "{n}")
195                }
196            }
197            Value::String(s) => write!(f, "{s}"),
198            Value::Bool(b) => write!(f, "{b}"),
199            Value::Nil => write!(f, "nil"),
200            Value::List(items) => {
201                write!(f, "[")?;
202                for (i, item) in items.iter().enumerate() {
203                    if i > 0 {
204                        write!(f, ", ")?;
205                    }
206                    // Strings inside lists/records get quoted
207                    match item {
208                        Value::String(s) => write!(f, "\"{s}\"")?,
209                        other => write!(f, "{other}")?,
210                    }
211                }
212                write!(f, "]")
213            }
214            Value::Record { type_name, fields } => {
215                if let Some(name) = type_name {
216                    write!(f, "{name}")?;
217                }
218                write!(f, "{{")?;
219                for (i, (key, val)) in fields.iter().enumerate() {
220                    if i > 0 {
221                        write!(f, ", ")?;
222                    }
223                    match val {
224                        Value::String(s) => write!(f, "{key}: \"{s}\"")?,
225                        other => write!(f, "{key}: {other}")?,
226                    }
227                }
228                write!(f, "}}")
229            }
230            Value::SumVariant {
231                variant, fields, ..
232            } => {
233                write!(f, "{variant}")?;
234                if !fields.is_empty() {
235                    write!(f, "(")?;
236                    for (i, val) in fields.iter().enumerate() {
237                        if i > 0 {
238                            write!(f, ", ")?;
239                        }
240                        write!(f, "{val}")?;
241                    }
242                    write!(f, ")")?;
243                }
244                Ok(())
245            }
246            Value::Color { r, g, b, a } => {
247                write!(f, "color({r}, {g}, {b}, {a})")
248            }
249            Value::Result(res) => match res.as_ref() {
250                ResultValue::Ok(v) => write!(f, "Ok({v})"),
251                ResultValue::Err(v) => write!(f, "Err({v})"),
252            },
253            Value::Function(_) => write!(f, "<function>"),
254        }
255    }
256}
257
258// ── Constructors & Helpers ────────────────────────────────────────────────────
259
260impl Value {
261    /// Returns the PEPL type name as used by `core.type_of()`.
262    pub fn type_name(&self) -> &str {
263        match self {
264            Value::Number(_) => "number",
265            Value::String(_) => "string",
266            Value::Bool(_) => "bool",
267            Value::Nil => "nil",
268            Value::List(_) => "list",
269            Value::Record {
270                type_name: Some(name),
271                ..
272            } => name.as_str(),
273            Value::Record {
274                type_name: None, ..
275            } => "record",
276            Value::Color { .. } => "color",
277            Value::Result(_) => "result",
278            Value::SumVariant { type_name, .. } => type_name.as_str(),
279            Value::Function(_) => "function",
280        }
281    }
282
283    /// Returns `true` if this value is truthy.
284    ///
285    /// Truthiness rules (per `convert.to_bool`):
286    /// - `false`, `nil`, `0`, `""` → falsy
287    /// - everything else → truthy
288    pub fn is_truthy(&self) -> bool {
289        match self {
290            Value::Bool(false) => false,
291            Value::Nil => false,
292            Value::Number(n) => *n != 0.0,
293            Value::String(s) => !s.is_empty(),
294            _ => true, // List, Record, Color, Result, SumVariant, Function are truthy
295        }
296    }
297
298    /// Convenience: wrap in `Ok` result.
299    pub fn ok(self) -> Value {
300        Value::Result(Box::new(ResultValue::Ok(self)))
301    }
302
303    /// Convenience: wrap in `Err` result.
304    pub fn err(self) -> Value {
305        Value::Result(Box::new(ResultValue::Err(self)))
306    }
307
308    /// Create an anonymous record (no type name).
309    pub fn record(fields: BTreeMap<String, Value>) -> Value {
310        Value::Record {
311            type_name: None,
312            fields,
313        }
314    }
315
316    /// Create a named record (e.g., `type Todo = { ... }`).
317    pub fn named_record(type_name: impl Into<String>, fields: BTreeMap<String, Value>) -> Value {
318        Value::Record {
319            type_name: Some(type_name.into()),
320            fields,
321        }
322    }
323
324    /// Create a unit sum variant (no payload fields).
325    pub fn unit_variant(type_name: impl Into<String>, variant: impl Into<String>) -> Value {
326        Value::SumVariant {
327            type_name: type_name.into(),
328            variant: variant.into(),
329            fields: vec![],
330        }
331    }
332
333    /// Create a sum variant with positional fields.
334    pub fn sum_variant(
335        type_name: impl Into<String>,
336        variant: impl Into<String>,
337        fields: Vec<Value>,
338    ) -> Value {
339        Value::SumVariant {
340            type_name: type_name.into(),
341            variant: variant.into(),
342            fields,
343        }
344    }
345
346    /// Try to extract a number, returning `None` if not a `Number`.
347    pub fn as_number(&self) -> Option<f64> {
348        match self {
349            Value::Number(n) => Some(*n),
350            _ => None,
351        }
352    }
353
354    /// Try to extract a string reference, returning `None` if not a `String`.
355    pub fn as_str(&self) -> Option<&str> {
356        match self {
357            Value::String(s) => Some(s),
358            _ => None,
359        }
360    }
361
362    /// Try to extract a bool, returning `None` if not a `Bool`.
363    pub fn as_bool(&self) -> Option<bool> {
364        match self {
365            Value::Bool(b) => Some(*b),
366            _ => None,
367        }
368    }
369
370    /// Try to extract a list reference, returning `None` if not a `List`.
371    pub fn as_list(&self) -> Option<&[Value]> {
372        match self {
373            Value::List(l) => Some(l),
374            _ => None,
375        }
376    }
377
378    /// Try to extract a record reference, returning `None` if not a `Record`.
379    pub fn as_record(&self) -> Option<&BTreeMap<String, Value>> {
380        match self {
381            Value::Record { fields, .. } => Some(fields),
382            _ => None,
383        }
384    }
385
386    /// Try to extract sum variant info: `(type_name, variant, fields)`.
387    pub fn as_variant(&self) -> Option<(&str, &str, &[Value])> {
388        match self {
389            Value::SumVariant {
390                type_name,
391                variant,
392                fields,
393            } => Some((type_name, variant, fields)),
394            _ => None,
395        }
396    }
397
398    /// Try to extract a function reference, returning `None` if not a `Function`.
399    pub fn as_function(&self) -> Option<&StdlibFn> {
400        match self {
401            Value::Function(f) => Some(f),
402            _ => None,
403        }
404    }
405
406    /// Returns the declared type name for named records and sum variants.
407    /// Returns `None` for anonymous records and all other value types.
408    pub fn declared_type_name(&self) -> Option<&str> {
409        match self {
410            Value::Record {
411                type_name: Some(name),
412                ..
413            } => Some(name),
414            Value::SumVariant { type_name, .. } => Some(type_name),
415            _ => None,
416        }
417    }
418}
419
420// ── From impls ────────────────────────────────────────────────────────────────
421
422impl From<f64> for Value {
423    fn from(n: f64) -> Self {
424        Value::Number(n)
425    }
426}
427
428impl From<i64> for Value {
429    fn from(n: i64) -> Self {
430        Value::Number(n as f64)
431    }
432}
433
434impl From<&str> for Value {
435    fn from(s: &str) -> Self {
436        Value::String(s.to_string())
437    }
438}
439
440impl From<String> for Value {
441    fn from(s: String) -> Self {
442        Value::String(s)
443    }
444}
445
446impl From<bool> for Value {
447    fn from(b: bool) -> Self {
448        Value::Bool(b)
449    }
450}
451
452impl From<BTreeMap<String, Value>> for Value {
453    fn from(fields: BTreeMap<String, Value>) -> Self {
454        Value::Record {
455            type_name: None,
456            fields,
457        }
458    }
459}