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(feature = "no_std")]
10use alloc::{boxed::Box, format, rc::Rc, string::{String, ToString}, vec::Vec};
11
12#[cfg(not(feature = "no_std"))]
13use std::rc::Rc;
14
15use core::cell::RefCell;
16
17use crate::memory::{bop_alloc, bop_dealloc};
18use crate::parser::Stmt;
19
20// ─── Tracked newtypes ──────────────────────────────────────────────────────
21//
22// Private inner fields prevent direct construction from outside this module.
23
24#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
25pub struct BopStr(String);
26
27#[derive(Debug)]
28pub struct BopArray(Vec<Value>);
29
30#[derive(Debug)]
31pub struct BopDict(Vec<(String, Value)>);
32
33/// A user-defined struct value. Carries the module it was
34/// declared in plus the bare type name, so two modules that
35/// happen to declare `struct Color { ... }` independently
36/// produce distinct values even when they share a name. The
37/// module path is `<root>` for the top-level program and
38/// `<builtin>` for engine-registered builtins like
39/// `RuntimeError`; for user modules it's the dot-joined `use`
40/// path (`"std.math"`, `"game.entity"`, …). Fields are stored
41/// in declaration order so iteration and `Display` stay stable.
42#[derive(Debug)]
43pub struct BopStruct {
44 module_path: String,
45 type_name: String,
46 fields: Vec<(String, Value)>,
47}
48
49/// A user-defined enum variant value — the concrete data side of
50/// Bop's sum types. Like [`BopStruct`], it's identified by the
51/// `(module_path, type_name)` pair, plus the selected variant's
52/// name and payload. Two enums declared in different modules with
53/// the same type name and even the same variants still compare
54/// as distinct types.
55#[derive(Debug)]
56pub struct BopEnumVariant {
57 module_path: String,
58 type_name: String,
59 variant: String,
60 payload: EnumPayload,
61}
62
63/// Module path used for engine-registered builtins (`Result`,
64/// `RuntimeError`). Surfaces wherever a struct / enum value
65/// needs to carry its declaring module; the engines all agree
66/// on this literal so patterns + equality line up across
67/// walker / VM / AOT.
68pub const BUILTIN_MODULE_PATH: &str = "<builtin>";
69
70/// Module path used for types declared directly in the program
71/// root (not in any imported module). Same literal across every
72/// engine.
73pub const ROOT_MODULE_PATH: &str = "<root>";
74
75/// Runtime payload attached to a `BopEnumVariant`. Mirrors the
76/// three variant shapes the parser recognises
77/// (`VariantKind::{Unit, Tuple, Struct}`).
78#[derive(Debug)]
79pub enum EnumPayload {
80 Unit,
81 Tuple(Vec<Value>),
82 Struct(Vec<(String, Value)>),
83}
84
85/// A Bop function value — the runtime representation of a closure
86/// or a reified `fn foo(...) { ... }` declaration. Shared by `Rc`
87/// so first-class usage (`let g = f; pass(f)`) is cheap.
88///
89/// The body is engine-opaque: the tree-walker produces an
90/// [`FnBody::Ast`] for direct interpretation; the bytecode VM
91/// produces an [`FnBody::Compiled`] carrying a pre-compiled body.
92/// Each engine only ever dispatches its own variant.
93pub struct BopFn {
94 pub params: Vec<String>,
95 /// Values captured from the enclosing scope at construction
96 /// time, cloned by value. Free variables in the body that
97 /// aren't parameters and aren't in this list fall through to
98 /// the outer module / global lookup at call time.
99 pub captures: Vec<(String, Value)>,
100 pub body: FnBody,
101 /// `Some(name)` when this `BopFn` is bound to its own name
102 /// for self-reference (the lowering of `fn foo(...) { ... }`).
103 /// Lambdas created from an `fn(...) { ... }` expression leave
104 /// this `None`.
105 pub self_name: Option<String>,
106}
107
108/// Engine-specific representation of a function body.
109///
110/// - The tree-walker creates `Ast` bodies and re-walks the AST on
111/// every call.
112/// - The bytecode VM creates `Compiled` bodies carrying a
113/// pre-compiled form (typically `Rc<bop_vm::Chunk>`). `Rc<dyn
114/// Any>` keeps `bop-lang` from taking a dep on any particular
115/// engine crate.
116///
117/// An engine that only understands one variant errors cleanly
118/// when handed the other, rather than silently misbehaving.
119pub enum FnBody {
120 Ast(Vec<Stmt>),
121 Compiled(Rc<dyn core::any::Any + 'static>),
122}
123
124impl core::fmt::Debug for BopFn {
125 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126 f.debug_struct("BopFn")
127 .field("params", &self.params)
128 .field("captures", &self.captures.len())
129 .field("body", &self.body)
130 .field("self_name", &self.self_name)
131 .finish()
132 }
133}
134
135impl core::fmt::Debug for FnBody {
136 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
137 match self {
138 FnBody::Ast(stmts) => write!(f, "Ast({} stmts)", stmts.len()),
139 FnBody::Compiled(_) => write!(f, "Compiled(<opaque>)"),
140 }
141 }
142}
143
144// ─── Value enum ────────────────────────────────────────────────────────────
145
146#[derive(Debug)]
147pub enum Value {
148 /// 64-bit signed integer. The go-to type for counts,
149 /// indices, and any arithmetic that wants exactness. Added
150 /// in phase 6; produced by integer literals (`42`), the
151 /// `int()` builtin, `len`, `range` elements, and the new
152 /// `//` integer-division operator.
153 Int(i64),
154 /// 64-bit IEEE-754 float. Produced by decimal literals
155 /// (`3.14`, `4.0`), the `float()` builtin, and by `/` on
156 /// any numeric pair (Python-style: `/` always floats).
157 Number(f64),
158 Str(BopStr),
159 Bool(bool),
160 None,
161 Array(BopArray),
162 Dict(BopDict),
163 Fn(Rc<BopFn>),
164 // `Struct` and `EnumVariant` live behind a `Box` so the
165 // `Value` enum stays compact (roughly the size of a `Vec`
166 // header) rather than ballooning to the size of the widest
167 // user-type variant. Keeps per-call stack frames small
168 // enough for deep recursion to halt on the call-depth
169 // counter before overflowing the native stack.
170 Struct(Box<BopStruct>),
171 EnumVariant(Box<BopEnumVariant>),
172 /// Namespace value produced by an aliased `use` statement
173 /// (`use std.math as m` binds `m` as a `Module`). Field
174 /// access dispatches to the module's exported `let` / `fn`
175 /// bindings; the runtime also consults the type list for
176 /// `m.Type { ... }` / `m.Type::Variant(...)` forms so those
177 /// namespaced constructors find the right declared type.
178 Module(Rc<BopModule>),
179 /// Lazy iterator. Cloning shares state (like `Value::Fn`) —
180 /// `let b = a; a.next(); b.next()` advances the same
181 /// underlying position, matching iterator semantics in
182 /// Python / Rust / JS. See [`BopIter`] for the built-in
183 /// variants; user-defined iterators are ordinary struct
184 /// values that happen to implement `.next()`.
185 Iter(Rc<RefCell<BopIter>>),
186}
187
188/// Built-in lazy iterator shapes. Each one holds a snapshot of
189/// the source sequence plus a cursor; advancing via [`Self::next`]
190/// yields items until exhausted. A user-defined iterator doesn't
191/// need to live here — it's just a struct with a `.next()`
192/// method, dispatched through the ordinary method path.
193#[derive(Debug)]
194pub enum BopIter {
195 /// Over a cloned-off array snapshot. Subsequent mutation of
196 /// the original array doesn't affect the iterator — matches
197 /// how most scripting languages present iteration.
198 Array { items: Vec<Value>, pos: usize },
199 /// Over a string's Unicode code points, one item per code
200 /// point. Each yielded value is a single-char string.
201 String { chars: Vec<char>, pos: usize },
202 /// Over a dict's keys, in declaration order. Same shape
203 /// `for k in dict` uses when the receiver is a plain dict.
204 Dict { keys: Vec<String>, pos: usize },
205}
206
207impl BopIter {
208 /// Advance by one and return the next item, or `None` when
209 /// the iterator is exhausted. Caller wraps the result in
210 /// `Iter::Next(v)` / `Iter::Done` for user code.
211 pub fn next(&mut self) -> Option<Value> {
212 match self {
213 BopIter::Array { items, pos } => {
214 if *pos < items.len() {
215 let v = items[*pos].clone();
216 *pos += 1;
217 Some(v)
218 } else {
219 None
220 }
221 }
222 BopIter::String { chars, pos } => {
223 if *pos < chars.len() {
224 let v = Value::new_str(chars[*pos].to_string());
225 *pos += 1;
226 Some(v)
227 } else {
228 None
229 }
230 }
231 BopIter::Dict { keys, pos } => {
232 if *pos < keys.len() {
233 let v = Value::new_str(keys[*pos].clone());
234 *pos += 1;
235 Some(v)
236 } else {
237 None
238 }
239 }
240 }
241 }
242}
243
244impl Drop for BopIter {
245 fn drop(&mut self) {
246 // Release the buffer tracked at construction time. The
247 // inner Values (for Array) free themselves through their
248 // own Drop; strings inside Dict keys do the same via
249 // `key_bytes` accounting below.
250 match self {
251 BopIter::Array { items, .. } => {
252 bop_dealloc(items.capacity() * core::mem::size_of::<Value>());
253 }
254 BopIter::String { chars, .. } => {
255 bop_dealloc(chars.capacity() * core::mem::size_of::<char>());
256 }
257 BopIter::Dict { keys, .. } => {
258 let key_bytes: usize = keys.iter().map(|k| k.capacity()).sum();
259 bop_dealloc(
260 keys.capacity() * core::mem::size_of::<String>() + key_bytes,
261 );
262 }
263 }
264 }
265}
266
267/// Exported surface of a module, as presented through an aliased
268/// `use` statement. `Rc<BopModule>` is what a `Value::Module`
269/// carries so cloning the Value stays cheap.
270#[derive(Debug)]
271pub struct BopModule {
272 /// The dotted path the module was loaded from ("std.math",
273 /// "game.entity", …). Useful for error messages.
274 pub path: String,
275 /// Exported `let` / `fn` / `const` bindings, in declaration
276 /// order. Accessed via `m.name` field reads.
277 pub bindings: Vec<(String, Value)>,
278 /// Names of struct / enum types the module declared.
279 /// Construction through the namespace (`m.Entity { ... }`)
280 /// verifies the type name appears in this list before
281 /// falling through to the engine's type registry.
282 pub types: Vec<String>,
283}
284
285// ─── Tracked constructors ──────────────────────────────────────────────────
286//
287// These call bop_alloc() to track the allocation but do NOT check the limit.
288// Enforcement happens at tick() via bop_memory_exceeded(). This means a single
289// operation can overshoot the limit before the next tick catches it. High-risk
290// operations (string repeat, string/array concat) use bop_would_exceed() as a
291// preflight check in the evaluator to avoid this.
292
293impl Value {
294 pub fn new_str(s: String) -> Self {
295 bop_alloc(s.capacity());
296 Value::Str(BopStr(s))
297 }
298
299 pub fn new_array(items: Vec<Value>) -> Self {
300 bop_alloc(items.capacity() * core::mem::size_of::<Value>());
301 Value::Array(BopArray(items))
302 }
303
304 pub fn new_dict(entries: Vec<(String, Value)>) -> Self {
305 let key_bytes: usize = entries.iter().map(|(k, _)| k.capacity()).sum();
306 bop_alloc(entries.capacity() * core::mem::size_of::<(String, Value)>() + key_bytes);
307 Value::Dict(BopDict(entries))
308 }
309
310 /// Build a user-defined struct value. `module_path` is the
311 /// module in which the type was declared (`<root>` at the
312 /// top level, `<builtin>` for engine-registered shapes like
313 /// `RuntimeError`, or the dot-joined `use` path for user
314 /// modules). Two structs are only the same type when both
315 /// the module path *and* the type name match — so a
316 /// `struct Color { ... }` declared in two separate modules
317 /// produces genuinely distinct values.
318 pub fn new_struct(
319 module_path: String,
320 type_name: String,
321 fields: Vec<(String, Value)>,
322 ) -> Self {
323 let key_bytes: usize = fields.iter().map(|(k, _)| k.capacity()).sum();
324 bop_alloc(
325 module_path.capacity()
326 + type_name.capacity()
327 + fields.capacity() * core::mem::size_of::<(String, Value)>()
328 + key_bytes,
329 );
330 Value::Struct(Box::new(BopStruct {
331 module_path,
332 type_name,
333 fields,
334 }))
335 }
336
337 /// Build a built-in iterator that yields each item of
338 /// `items` in order. Cloning the returned `Value::Iter`
339 /// shares the iteration cursor, so `let b = a; a.next()`
340 /// advances `b` too.
341 pub fn new_array_iter(items: Vec<Value>) -> Self {
342 bop_alloc(items.capacity() * core::mem::size_of::<Value>());
343 Value::Iter(Rc::new(RefCell::new(BopIter::Array { items, pos: 0 })))
344 }
345
346 /// Build a built-in iterator over a string's Unicode code
347 /// points.
348 pub fn new_string_iter(chars: Vec<char>) -> Self {
349 bop_alloc(chars.capacity() * core::mem::size_of::<char>());
350 Value::Iter(Rc::new(RefCell::new(BopIter::String { chars, pos: 0 })))
351 }
352
353 /// Build a built-in iterator over a dict's keys (declaration
354 /// order).
355 pub fn new_dict_iter(keys: Vec<String>) -> Self {
356 let key_bytes: usize = keys.iter().map(|k| k.capacity()).sum();
357 bop_alloc(keys.capacity() * core::mem::size_of::<String>() + key_bytes);
358 Value::Iter(Rc::new(RefCell::new(BopIter::Dict { keys, pos: 0 })))
359 }
360
361 pub fn new_enum_unit(module_path: String, type_name: String, variant: String) -> Self {
362 bop_alloc(module_path.capacity() + type_name.capacity() + variant.capacity());
363 Value::EnumVariant(Box::new(BopEnumVariant {
364 module_path,
365 type_name,
366 variant,
367 payload: EnumPayload::Unit,
368 }))
369 }
370
371 pub fn new_enum_tuple(
372 module_path: String,
373 type_name: String,
374 variant: String,
375 items: Vec<Value>,
376 ) -> Self {
377 bop_alloc(
378 module_path.capacity()
379 + type_name.capacity()
380 + variant.capacity()
381 + items.capacity() * core::mem::size_of::<Value>(),
382 );
383 Value::EnumVariant(Box::new(BopEnumVariant {
384 module_path,
385 type_name,
386 variant,
387 payload: EnumPayload::Tuple(items),
388 }))
389 }
390
391 pub fn new_enum_struct(
392 module_path: String,
393 type_name: String,
394 variant: String,
395 fields: Vec<(String, Value)>,
396 ) -> Self {
397 let key_bytes: usize = fields.iter().map(|(k, _)| k.capacity()).sum();
398 bop_alloc(
399 module_path.capacity()
400 + type_name.capacity()
401 + variant.capacity()
402 + fields.capacity() * core::mem::size_of::<(String, Value)>()
403 + key_bytes,
404 );
405 Value::EnumVariant(Box::new(BopEnumVariant {
406 module_path,
407 type_name,
408 variant,
409 payload: EnumPayload::Struct(fields),
410 }))
411 }
412
413 /// Build a tree-walker-ready closure value. The AST body moves
414 /// into a shared [`BopFn`] behind an `Rc`; subsequent clones
415 /// of the resulting `Value::Fn` just bump the refcount.
416 pub fn new_fn(
417 params: Vec<String>,
418 captures: Vec<(String, Value)>,
419 body: Vec<Stmt>,
420 self_name: Option<String>,
421 ) -> Self {
422 Value::Fn(Rc::new(BopFn {
423 params,
424 captures,
425 body: FnBody::Ast(body),
426 self_name,
427 }))
428 }
429
430 /// Build a closure value with an engine-opaque compiled body.
431 /// Used by the bytecode VM (and any future engine) to carry
432 /// its pre-compiled form inside a `Value::Fn` without
433 /// `bop-lang` depending on the engine crate.
434 pub fn new_compiled_fn(
435 params: Vec<String>,
436 captures: Vec<(String, Value)>,
437 body: Rc<dyn core::any::Any + 'static>,
438 self_name: Option<String>,
439 ) -> Self {
440 Value::Fn(Rc::new(BopFn {
441 params,
442 captures,
443 body: FnBody::Compiled(body),
444 self_name,
445 }))
446 }
447}
448
449// ─── Clone (tracks allocations) ────────────────────────────────────────────
450//
451// For Array and Dict, the inner .clone() recursively clones each element,
452// and each element's Clone impl calls bop_alloc for itself. We then ALSO
453// bop_alloc for the Vec buffer. This is correct — the buffer and the elements
454// are separate allocations that both need tracking.
455
456impl Clone for Value {
457 fn clone(&self) -> Self {
458 match self {
459 Value::Int(n) => Value::Int(*n),
460 Value::Number(n) => Value::Number(*n),
461 Value::Bool(b) => Value::Bool(*b),
462 Value::None => Value::None,
463 Value::Str(s) => {
464 let cloned = s.0.clone();
465 bop_alloc(cloned.capacity());
466 Value::Str(BopStr(cloned))
467 }
468 Value::Array(arr) => {
469 let cloned = arr.0.clone(); // each element's Clone tracks itself
470 bop_alloc(cloned.capacity() * core::mem::size_of::<Value>());
471 Value::Array(BopArray(cloned))
472 }
473 Value::Dict(d) => {
474 let cloned = d.0.clone(); // each Value's Clone tracks itself
475 let key_bytes: usize = cloned.iter().map(|(k, _)| k.capacity()).sum();
476 bop_alloc(cloned.capacity() * core::mem::size_of::<(String, Value)>() + key_bytes);
477 Value::Dict(BopDict(cloned))
478 }
479 Value::Struct(s) => {
480 let cloned_mp = s.module_path.clone();
481 let cloned_tn = s.type_name.clone();
482 let cloned_fields = s.fields.clone();
483 let key_bytes: usize =
484 cloned_fields.iter().map(|(k, _)| k.capacity()).sum();
485 bop_alloc(
486 cloned_mp.capacity()
487 + cloned_tn.capacity()
488 + cloned_fields.capacity()
489 * core::mem::size_of::<(String, Value)>()
490 + key_bytes,
491 );
492 Value::Struct(Box::new(BopStruct {
493 module_path: cloned_mp,
494 type_name: cloned_tn,
495 fields: cloned_fields,
496 }))
497 }
498 // Closures are reference-counted: cloning a Value::Fn
499 // is O(1) and doesn't duplicate the body or captures.
500 // Tracking the captures' memory happens once, at the
501 // moment the BopFn is constructed (by `new_fn`), via
502 // their own Value Clone/Drop hooks.
503 Value::Fn(f) => Value::Fn(Rc::clone(f)),
504 // Modules are reference-counted — same cheap clone as
505 // fns. The `bindings` and `types` vectors track their
506 // own memory when the `BopModule` is first built.
507 Value::Module(m) => Value::Module(Rc::clone(m)),
508 // Iterators are reference-counted and intentionally
509 // share their cursor — cloning `a = b` doesn't fork
510 // the iteration state, matching iterator semantics
511 // in Python / Rust / JS. The buffer was tracked once
512 // by the constructor and is dealloc'd by BopIter's
513 // Drop when the last clone goes away.
514 Value::Iter(it) => Value::Iter(Rc::clone(it)),
515 Value::EnumVariant(e) => {
516 let mp = e.module_path.clone();
517 let tn = e.type_name.clone();
518 let vn = e.variant.clone();
519 let base = mp.capacity() + tn.capacity() + vn.capacity();
520 let payload = match &e.payload {
521 EnumPayload::Unit => {
522 bop_alloc(base);
523 EnumPayload::Unit
524 }
525 EnumPayload::Tuple(items) => {
526 let cloned = items.clone();
527 bop_alloc(
528 base + cloned.capacity() * core::mem::size_of::<Value>(),
529 );
530 EnumPayload::Tuple(cloned)
531 }
532 EnumPayload::Struct(fields) => {
533 let cloned = fields.clone();
534 let key_bytes: usize =
535 cloned.iter().map(|(k, _)| k.capacity()).sum();
536 bop_alloc(
537 base
538 + cloned.capacity()
539 * core::mem::size_of::<(String, Value)>()
540 + key_bytes,
541 );
542 EnumPayload::Struct(cloned)
543 }
544 };
545 Value::EnumVariant(Box::new(BopEnumVariant {
546 module_path: mp,
547 type_name: tn,
548 variant: vn,
549 payload,
550 }))
551 }
552 }
553 }
554}
555
556// ─── Drop (tracks deallocations) ───────────────────────────────────────────
557
558impl Drop for Value {
559 fn drop(&mut self) {
560 match self {
561 Value::Str(s) => bop_dealloc(s.0.capacity()),
562 Value::Array(arr) => {
563 bop_dealloc(arr.0.capacity() * core::mem::size_of::<Value>());
564 }
565 Value::Dict(d) => {
566 let key_bytes: usize = d.0.iter().map(|(k, _)| k.capacity()).sum();
567 bop_dealloc(d.0.capacity() * core::mem::size_of::<(String, Value)>() + key_bytes);
568 }
569 Value::Struct(s) => {
570 let key_bytes: usize = s.fields.iter().map(|(k, _)| k.capacity()).sum();
571 bop_dealloc(
572 s.module_path.capacity()
573 + s.type_name.capacity()
574 + s.fields.capacity()
575 * core::mem::size_of::<(String, Value)>()
576 + key_bytes,
577 );
578 }
579 Value::EnumVariant(e) => {
580 let base = e.module_path.capacity()
581 + e.type_name.capacity()
582 + e.variant.capacity();
583 match &e.payload {
584 EnumPayload::Unit => bop_dealloc(base),
585 EnumPayload::Tuple(items) => bop_dealloc(
586 base + items.capacity() * core::mem::size_of::<Value>(),
587 ),
588 EnumPayload::Struct(fields) => {
589 let key_bytes: usize =
590 fields.iter().map(|(k, _)| k.capacity()).sum();
591 bop_dealloc(
592 base
593 + fields.capacity()
594 * core::mem::size_of::<(String, Value)>()
595 + key_bytes,
596 );
597 }
598 }
599 }
600 // Value::Fn drops by releasing its Rc. The inner
601 // captures' Drop impls fire only when the refcount
602 // reaches zero; no per-Value accounting here.
603 _ => {}
604 }
605 }
606}
607
608// ─── Display ───────────────────────────────────────────────────────────────
609
610impl core::fmt::Display for Value {
611 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
612 match self {
613 Value::Int(n) => write!(f, "{}", n),
614 Value::Number(n) => {
615 if *n == (*n as i64 as f64) && *n - *n == 0.0 {
616 write!(f, "{}", *n as i64)
617 } else {
618 write!(f, "{}", n)
619 }
620 }
621 Value::Str(s) => write!(f, "{}", s.0),
622 Value::Bool(b) => write!(f, "{}", b),
623 Value::None => write!(f, "none"),
624 Value::Array(items) => {
625 write!(f, "[")?;
626 for (i, item) in items.0.iter().enumerate() {
627 if i > 0 {
628 write!(f, ", ")?;
629 }
630 write!(f, "{}", item.inspect())?;
631 }
632 write!(f, "]")
633 }
634 Value::Dict(entries) => {
635 write!(f, "{{")?;
636 for (i, (k, v)) in entries.0.iter().enumerate() {
637 if i > 0 {
638 write!(f, ", ")?;
639 }
640 write!(f, "\"{}\": {}", k, v.inspect())?;
641 }
642 write!(f, "}}")
643 }
644 Value::Fn(func) => match &func.self_name {
645 Some(name) => write!(f, "<fn {}>", name),
646 None => write!(f, "<fn>"),
647 },
648 Value::Module(m) => write!(f, "<module {}>", m.path),
649 Value::Iter(it) => {
650 // Peek at the inner state for a useful Display —
651 // callers see `<iter array 0/3>` rather than a
652 // bare `<iter>`. If the RefCell is already
653 // borrowed (nested Display during a panic
654 // backtrace, say), fall back to the bare form.
655 match it.try_borrow() {
656 Ok(inner) => match &*inner {
657 BopIter::Array { items, pos } => {
658 write!(f, "<iter array {}/{}>", pos, items.len())
659 }
660 BopIter::String { chars, pos } => {
661 write!(f, "<iter string {}/{}>", pos, chars.len())
662 }
663 BopIter::Dict { keys, pos } => {
664 write!(f, "<iter dict {}/{}>", pos, keys.len())
665 }
666 },
667 Err(_) => write!(f, "<iter>"),
668 }
669 }
670 Value::Struct(s) => {
671 write!(f, "{} {{", s.type_name)?;
672 for (i, (k, v)) in s.fields.iter().enumerate() {
673 if i > 0 {
674 write!(f, ",")?;
675 }
676 write!(f, " {}: {}", k, v.inspect())?;
677 }
678 if !s.fields.is_empty() {
679 write!(f, " ")?;
680 }
681 write!(f, "}}")
682 }
683 Value::EnumVariant(e) => match &e.payload {
684 EnumPayload::Unit => write!(f, "{}::{}", e.type_name, e.variant),
685 EnumPayload::Tuple(items) => {
686 write!(f, "{}::{}(", e.type_name, e.variant)?;
687 for (i, v) in items.iter().enumerate() {
688 if i > 0 {
689 write!(f, ", ")?;
690 }
691 write!(f, "{}", v.inspect())?;
692 }
693 write!(f, ")")
694 }
695 EnumPayload::Struct(fields) => {
696 write!(f, "{}::{} {{", e.type_name, e.variant)?;
697 for (i, (k, v)) in fields.iter().enumerate() {
698 if i > 0 {
699 write!(f, ",")?;
700 }
701 write!(f, " {}: {}", k, v.inspect())?;
702 }
703 if !fields.is_empty() {
704 write!(f, " ")?;
705 }
706 write!(f, "}}")
707 }
708 },
709 }
710 }
711}
712
713impl core::fmt::Display for BopStr {
714 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
715 write!(f, "{}", self.0)
716 }
717}
718
719// ─── Value helpers ─────────────────────────────────────────────────────────
720
721impl Value {
722 pub fn inspect(&self) -> String {
723 match self {
724 Value::Str(s) => format!("\"{}\"", s.0),
725 other => format!("{}", other),
726 }
727 }
728
729 pub fn type_name(&self) -> &'static str {
730 match self {
731 Value::Int(_) => "int",
732 Value::Number(_) => "number",
733 Value::Str(_) => "string",
734 Value::Bool(_) => "bool",
735 Value::None => "none",
736 Value::Array(_) => "array",
737 Value::Dict(_) => "dict",
738 Value::Fn(_) => "fn",
739 // Generic bucket — the *specific* type name lives on
740 // the value itself (`struct_type_name()`). `type()`
741 // returns the Bop type name via the display path, so
742 // `type(Point { ... })` shows `"Point"`.
743 Value::Struct(_) => "struct",
744 Value::EnumVariant(_) => "enum",
745 Value::Module(_) => "module",
746 Value::Iter(_) => "iter",
747 }
748 }
749
750 /// The user-facing name for this value's type. For struct
751 /// values it's the declared type (`"Point"`); for enum
752 /// variants it's the enum's type name; for built-in
753 /// variants it matches [`Self::type_name`].
754 pub fn display_type_name(&self) -> String {
755 match self {
756 Value::Struct(s) => s.type_name.clone(),
757 Value::EnumVariant(e) => e.type_name.clone(),
758 other => other.type_name().to_string(),
759 }
760 }
761
762 pub fn is_truthy(&self) -> bool {
763 match self {
764 Value::Bool(b) => *b,
765 Value::None => false,
766 Value::Int(n) => *n != 0,
767 Value::Number(n) => *n != 0.0,
768 Value::Str(s) => !s.0.is_empty(),
769 Value::Array(a) => !a.0.is_empty(),
770 Value::Dict(d) => !d.0.is_empty(),
771 // A callable is always a "thing" — match other
772 // non-empty runtime objects.
773 Value::Fn(_) => true,
774 // Structs carry fielded data and are always truthy,
775 // even if they have no fields (the "unit struct"
776 // use case) — matching how classes / records behave
777 // in most scripting languages.
778 Value::Struct(_) => true,
779 // Enum variants represent a tagged choice; always
780 // truthy regardless of payload.
781 Value::EnumVariant(_) => true,
782 // A module is always a concrete thing — matches
783 // fn's behaviour.
784 Value::Module(_) => true,
785 // Iterators are always truthy, even when exhausted.
786 // Callers check `Iter::Done` via `.next()`, not via
787 // truthiness — matches how fns / modules behave.
788 Value::Iter(_) => true,
789 }
790 }
791}
792
793// ─── Deref for read access ─────────────────────────────────────────────────
794
795impl BopStr {
796 pub fn as_str(&self) -> &str {
797 &self.0
798 }
799}
800
801impl core::ops::Deref for BopStr {
802 type Target = str;
803 fn deref(&self) -> &str {
804 &self.0
805 }
806}
807
808impl core::ops::Deref for BopArray {
809 type Target = [Value];
810 fn deref(&self) -> &[Value] {
811 &self.0
812 }
813}
814
815impl core::ops::Deref for BopDict {
816 type Target = [(String, Value)];
817 fn deref(&self) -> &[(String, Value)] {
818 &self.0
819 }
820}
821
822// ─── Mutation methods ──────────────────────────────────────────────────────
823
824impl BopArray {
825 /// Take the inner Vec, leaving an empty array. Deallocates the buffer
826 /// from the memory tracker since it's leaving Value's control.
827 pub fn take(&mut self) -> Vec<Value> {
828 let taken = core::mem::take(&mut self.0);
829 bop_dealloc(taken.capacity() * core::mem::size_of::<Value>());
830 taken
831 }
832
833 /// Set a value at the given index. The old value at that index is dropped
834 /// (firing its Drop impl which calls bop_dealloc). No capacity change.
835 pub fn set(&mut self, index: usize, val: Value) {
836 self.0[index] = val;
837 }
838}
839
840impl BopStruct {
841 pub fn type_name(&self) -> &str {
842 &self.type_name
843 }
844
845 /// Module this struct type was declared in. Forms one half
846 /// of the type's identity — the other half is the bare
847 /// [`Self::type_name`].
848 pub fn module_path(&self) -> &str {
849 &self.module_path
850 }
851
852 pub fn fields(&self) -> &[(String, Value)] {
853 &self.fields
854 }
855
856 /// Look up a field by name. `None` if no such field.
857 pub fn field(&self, name: &str) -> Option<&Value> {
858 self.fields.iter().find(|(k, _)| k == name).map(|(_, v)| v)
859 }
860
861 /// Replace the value of an existing field. Returns `true` if
862 /// the field was present; `false` if the caller should raise
863 /// a "no such field" error. The old value is dropped (firing
864 /// its allocation tracking); no capacity change in the Vec.
865 pub fn set_field(&mut self, name: &str, value: Value) -> bool {
866 if let Some(entry) = self.fields.iter_mut().find(|(k, _)| k == name) {
867 entry.1 = value;
868 true
869 } else {
870 false
871 }
872 }
873}
874
875impl BopEnumVariant {
876 pub fn type_name(&self) -> &str {
877 &self.type_name
878 }
879
880 /// Module this enum type was declared in. Paired with
881 /// [`Self::type_name`] to form the type's identity.
882 pub fn module_path(&self) -> &str {
883 &self.module_path
884 }
885
886 pub fn variant(&self) -> &str {
887 &self.variant
888 }
889
890 pub fn payload(&self) -> &EnumPayload {
891 &self.payload
892 }
893
894 /// Field access for struct-variant payloads — mirrors
895 /// [`BopStruct::field`]. Returns `None` for unit / tuple
896 /// variants or when the field isn't in this variant's
897 /// payload.
898 pub fn field(&self, name: &str) -> Option<&Value> {
899 match &self.payload {
900 EnumPayload::Struct(fields) => {
901 fields.iter().find(|(k, _)| k == name).map(|(_, v)| v)
902 }
903 _ => None,
904 }
905 }
906}
907
908impl BopDict {
909 /// Set a key-value pair. If the key exists, replaces the value.
910 /// If new, tracks the key's allocation and any Vec capacity growth
911 /// from the push (Vec may reallocate to a larger buffer).
912 pub fn set_key(&mut self, key: &str, val: Value) {
913 if let Some(entry) = self.0.iter_mut().find(|(k, _)| k == key) {
914 entry.1 = val;
915 } else {
916 let old_cap = self.0.capacity();
917 let key = key.to_string();
918 bop_alloc(key.capacity());
919 self.0.push((key, val));
920 let new_cap = self.0.capacity();
921 if new_cap > old_cap {
922 bop_alloc((new_cap - old_cap) * core::mem::size_of::<(String, Value)>());
923 }
924 }
925 }
926}
927
928// ─── Equality ──────────────────────────────────────────────────────────────
929
930pub fn values_equal(a: &Value, b: &Value) -> bool {
931 match (a, b) {
932 (Value::Int(x), Value::Int(y)) => x == y,
933 (Value::Number(x), Value::Number(y)) => x == y,
934 // Cross-type numeric equality: `1 == 1.0` is true, same
935 // as Python / JS. Widens the Int through f64 for the
936 // comparison — lossy for magnitudes above 2^53, but
937 // that's the cost of the convenience. Stricter-typed
938 // code can call `int()` / `float()` explicitly first.
939 (Value::Int(x), Value::Number(y)) => (*x as f64) == *y,
940 (Value::Number(x), Value::Int(y)) => *x == (*y as f64),
941 (Value::Str(x), Value::Str(y)) => x == y,
942 (Value::Bool(x), Value::Bool(y)) => x == y,
943 (Value::None, Value::None) => true,
944 (Value::Array(x), Value::Array(y)) => {
945 x.len() == y.len() && x.iter().zip(y.iter()).all(|(a, b)| values_equal(a, b))
946 }
947 (Value::Dict(x), Value::Dict(y)) => {
948 x.len() == y.len()
949 && x.iter().all(|(k, v)| {
950 y.iter()
951 .find(|(k2, _)| k2 == k)
952 .is_some_and(|(_, v2)| values_equal(v, v2))
953 })
954 }
955 // Functions have identity-based equality: two references
956 // to the same `BopFn` compare equal; structurally identical
957 // closures constructed independently do not.
958 (Value::Fn(a), Value::Fn(b)) => Rc::ptr_eq(a, b),
959 // Structural equality for user structs: full type
960 // identity (module_path + type_name) AND every field
961 // equal in declaration order. Two structs with the same
962 // name declared in different modules deliberately compare
963 // as *not equal* — they're distinct types.
964 (Value::Struct(a), Value::Struct(b)) => {
965 a.module_path == b.module_path
966 && a.type_name == b.type_name
967 && a.fields.len() == b.fields.len()
968 && a.fields
969 .iter()
970 .zip(b.fields.iter())
971 .all(|((ka, va), (kb, vb))| ka == kb && values_equal(va, vb))
972 }
973 // Enum variants: same full type identity (module_path +
974 // type_name), same variant name, same payload shape +
975 // structural equality on payload items.
976 (Value::EnumVariant(a), Value::EnumVariant(b)) => {
977 a.module_path == b.module_path
978 && a.type_name == b.type_name
979 && a.variant == b.variant
980 && match (&a.payload, &b.payload) {
981 (EnumPayload::Unit, EnumPayload::Unit) => true,
982 (EnumPayload::Tuple(ax), EnumPayload::Tuple(bx)) => {
983 ax.len() == bx.len()
984 && ax
985 .iter()
986 .zip(bx.iter())
987 .all(|(x, y)| values_equal(x, y))
988 }
989 (EnumPayload::Struct(af), EnumPayload::Struct(bf)) => {
990 af.len() == bf.len()
991 && af
992 .iter()
993 .zip(bf.iter())
994 .all(|((ka, va), (kb, vb))| {
995 ka == kb && values_equal(va, vb)
996 })
997 }
998 _ => false,
999 }
1000 }
1001 _ => false,
1002 }
1003}