Skip to main content

bop/
value.rs

1//! Value type for the Bop interpreter.
2//!
3//! Heap-allocating variants use newtypes with private fields.
4//! The only way to construct them is through the tracked constructors
5//! (`Value::new_str`, `Value::new_array`, `Value::new_dict`), which
6//! call `bop_alloc`. This is enforced by the type system — code outside
7//! this module cannot access the private inner fields.
8
9#[cfg(not(feature = "std"))]
10use alloc::{format, string::{String, ToString}, vec::Vec};
11
12use crate::memory::{bop_alloc, bop_dealloc};
13
14// ─── Tracked newtypes ──────────────────────────────────────────────────────
15//
16// Private inner fields prevent direct construction from outside this module.
17
18#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
19pub struct BopStr(String);
20
21#[derive(Debug)]
22pub struct BopArray(Vec<Value>);
23
24#[derive(Debug)]
25pub struct BopDict(Vec<(String, Value)>);
26
27// ─── Value enum ────────────────────────────────────────────────────────────
28
29#[derive(Debug)]
30pub enum Value {
31    Number(f64),
32    Str(BopStr),
33    Bool(bool),
34    None,
35    Array(BopArray),
36    Dict(BopDict),
37}
38
39// ─── Tracked constructors ──────────────────────────────────────────────────
40//
41// These call bop_alloc() to track the allocation but do NOT check the limit.
42// Enforcement happens at tick() via bop_memory_exceeded(). This means a single
43// operation can overshoot the limit before the next tick catches it. High-risk
44// operations (string repeat, string/array concat) use bop_would_exceed() as a
45// preflight check in the evaluator to avoid this.
46
47impl Value {
48    pub fn new_str(s: String) -> Self {
49        bop_alloc(s.capacity());
50        Value::Str(BopStr(s))
51    }
52
53    pub fn new_array(items: Vec<Value>) -> Self {
54        bop_alloc(items.capacity() * core::mem::size_of::<Value>());
55        Value::Array(BopArray(items))
56    }
57
58    pub fn new_dict(entries: Vec<(String, Value)>) -> Self {
59        let key_bytes: usize = entries.iter().map(|(k, _)| k.capacity()).sum();
60        bop_alloc(entries.capacity() * core::mem::size_of::<(String, Value)>() + key_bytes);
61        Value::Dict(BopDict(entries))
62    }
63}
64
65// ─── Clone (tracks allocations) ────────────────────────────────────────────
66//
67// For Array and Dict, the inner .clone() recursively clones each element,
68// and each element's Clone impl calls bop_alloc for itself. We then ALSO
69// bop_alloc for the Vec buffer. This is correct — the buffer and the elements
70// are separate allocations that both need tracking.
71
72impl Clone for Value {
73    fn clone(&self) -> Self {
74        match self {
75            Value::Number(n) => Value::Number(*n),
76            Value::Bool(b) => Value::Bool(*b),
77            Value::None => Value::None,
78            Value::Str(s) => {
79                let cloned = s.0.clone();
80                bop_alloc(cloned.capacity());
81                Value::Str(BopStr(cloned))
82            }
83            Value::Array(arr) => {
84                let cloned = arr.0.clone(); // each element's Clone tracks itself
85                bop_alloc(cloned.capacity() * core::mem::size_of::<Value>());
86                Value::Array(BopArray(cloned))
87            }
88            Value::Dict(d) => {
89                let cloned = d.0.clone(); // each Value's Clone tracks itself
90                let key_bytes: usize = cloned.iter().map(|(k, _)| k.capacity()).sum();
91                bop_alloc(cloned.capacity() * core::mem::size_of::<(String, Value)>() + key_bytes);
92                Value::Dict(BopDict(cloned))
93            }
94        }
95    }
96}
97
98// ─── Drop (tracks deallocations) ───────────────────────────────────────────
99
100impl Drop for Value {
101    fn drop(&mut self) {
102        match self {
103            Value::Str(s) => bop_dealloc(s.0.capacity()),
104            Value::Array(arr) => {
105                bop_dealloc(arr.0.capacity() * core::mem::size_of::<Value>());
106            }
107            Value::Dict(d) => {
108                let key_bytes: usize = d.0.iter().map(|(k, _)| k.capacity()).sum();
109                bop_dealloc(d.0.capacity() * core::mem::size_of::<(String, Value)>() + key_bytes);
110            }
111            _ => {}
112        }
113    }
114}
115
116// ─── Display ───────────────────────────────────────────────────────────────
117
118impl core::fmt::Display for Value {
119    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
120        match self {
121            Value::Number(n) => {
122                if *n == (*n as i64 as f64) && *n - *n == 0.0 {
123                    write!(f, "{}", *n as i64)
124                } else {
125                    write!(f, "{}", n)
126                }
127            }
128            Value::Str(s) => write!(f, "{}", s.0),
129            Value::Bool(b) => write!(f, "{}", b),
130            Value::None => write!(f, "none"),
131            Value::Array(items) => {
132                write!(f, "[")?;
133                for (i, item) in items.0.iter().enumerate() {
134                    if i > 0 {
135                        write!(f, ", ")?;
136                    }
137                    write!(f, "{}", item.inspect())?;
138                }
139                write!(f, "]")
140            }
141            Value::Dict(entries) => {
142                write!(f, "{{")?;
143                for (i, (k, v)) in entries.0.iter().enumerate() {
144                    if i > 0 {
145                        write!(f, ", ")?;
146                    }
147                    write!(f, "\"{}\": {}", k, v.inspect())?;
148                }
149                write!(f, "}}")
150            }
151        }
152    }
153}
154
155impl core::fmt::Display for BopStr {
156    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
157        write!(f, "{}", self.0)
158    }
159}
160
161// ─── Value helpers ─────────────────────────────────────────────────────────
162
163impl Value {
164    pub fn inspect(&self) -> String {
165        match self {
166            Value::Str(s) => format!("\"{}\"", s.0),
167            other => format!("{}", other),
168        }
169    }
170
171    pub fn type_name(&self) -> &'static str {
172        match self {
173            Value::Number(_) => "number",
174            Value::Str(_) => "string",
175            Value::Bool(_) => "bool",
176            Value::None => "none",
177            Value::Array(_) => "array",
178            Value::Dict(_) => "dict",
179        }
180    }
181
182    pub fn is_truthy(&self) -> bool {
183        match self {
184            Value::Bool(b) => *b,
185            Value::None => false,
186            Value::Number(n) => *n != 0.0,
187            Value::Str(s) => !s.0.is_empty(),
188            Value::Array(a) => !a.0.is_empty(),
189            Value::Dict(d) => !d.0.is_empty(),
190        }
191    }
192}
193
194// ─── Deref for read access ─────────────────────────────────────────────────
195
196impl BopStr {
197    pub fn as_str(&self) -> &str {
198        &self.0
199    }
200}
201
202impl core::ops::Deref for BopStr {
203    type Target = str;
204    fn deref(&self) -> &str {
205        &self.0
206    }
207}
208
209impl core::ops::Deref for BopArray {
210    type Target = [Value];
211    fn deref(&self) -> &[Value] {
212        &self.0
213    }
214}
215
216impl core::ops::Deref for BopDict {
217    type Target = [(String, Value)];
218    fn deref(&self) -> &[(String, Value)] {
219        &self.0
220    }
221}
222
223// ─── Mutation methods ──────────────────────────────────────────────────────
224
225impl BopArray {
226    /// Take the inner Vec, leaving an empty array. Deallocates the buffer
227    /// from the memory tracker since it's leaving Value's control.
228    pub fn take(&mut self) -> Vec<Value> {
229        let taken = core::mem::take(&mut self.0);
230        bop_dealloc(taken.capacity() * core::mem::size_of::<Value>());
231        taken
232    }
233
234    /// Set a value at the given index. The old value at that index is dropped
235    /// (firing its Drop impl which calls bop_dealloc). No capacity change.
236    pub fn set(&mut self, index: usize, val: Value) {
237        self.0[index] = val;
238    }
239}
240
241impl BopDict {
242    /// Set a key-value pair. If the key exists, replaces the value.
243    /// If new, tracks the key's allocation and any Vec capacity growth
244    /// from the push (Vec may reallocate to a larger buffer).
245    pub fn set_key(&mut self, key: &str, val: Value) {
246        if let Some(entry) = self.0.iter_mut().find(|(k, _)| k == key) {
247            entry.1 = val;
248        } else {
249            let old_cap = self.0.capacity();
250            let key = key.to_string();
251            bop_alloc(key.capacity());
252            self.0.push((key, val));
253            let new_cap = self.0.capacity();
254            if new_cap > old_cap {
255                bop_alloc((new_cap - old_cap) * core::mem::size_of::<(String, Value)>());
256            }
257        }
258    }
259}
260
261// ─── Equality ──────────────────────────────────────────────────────────────
262
263pub fn values_equal(a: &Value, b: &Value) -> bool {
264    match (a, b) {
265        (Value::Number(x), Value::Number(y)) => x == y,
266        (Value::Str(x), Value::Str(y)) => x == y,
267        (Value::Bool(x), Value::Bool(y)) => x == y,
268        (Value::None, Value::None) => true,
269        (Value::Array(x), Value::Array(y)) => {
270            x.len() == y.len() && x.iter().zip(y.iter()).all(|(a, b)| values_equal(a, b))
271        }
272        (Value::Dict(x), Value::Dict(y)) => {
273            x.len() == y.len()
274                && x.iter().all(|(k, v)| {
275                    y.iter()
276                        .find(|(k2, _)| k2 == k)
277                        .is_some_and(|(_, v2)| values_equal(v, v2))
278                })
279        }
280        _ => false,
281    }
282}