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}