Skip to main content

bock_interp/
builtins.rs

1//! Built-in method dispatch registry for the Bock interpreter.
2//!
3//! Provides a `(TypeTag, method_name) → Rust function` dispatch table with a
4//! registration API. Phase 6 packages (P6.1–P6.6) can extend the registry
5//! without modifying this module.
6
7use std::collections::HashMap;
8
9use futures::future::BoxFuture;
10
11use crate::error::RuntimeError;
12use crate::value::{BockString, OrdF64, Value};
13
14// ─── TypeTag ──────────────────────────────────────────────────────────────────
15
16/// Identifies the runtime type of a [`Value`] for method dispatch.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum TypeTag {
19    Int,
20    Float,
21    Bool,
22    String,
23    Char,
24    Void,
25    List,
26    Map,
27    Set,
28    Tuple,
29    Record,
30    Enum,
31    Function,
32    Optional,
33    Result,
34    Range,
35    Iterator,
36    StringBuilder,
37    Future,
38    Duration,
39    Instant,
40    Channel,
41}
42
43impl TypeTag {
44    /// Determine the [`TypeTag`] for a given [`Value`].
45    #[must_use]
46    pub fn of(value: &Value) -> Self {
47        match value {
48            Value::Int(_) => TypeTag::Int,
49            Value::Float(_) => TypeTag::Float,
50            Value::Bool(_) => TypeTag::Bool,
51            Value::String(_) => TypeTag::String,
52            Value::Char(_) => TypeTag::Char,
53            Value::Void => TypeTag::Void,
54            Value::List(_) => TypeTag::List,
55            Value::Map(_) => TypeTag::Map,
56            Value::Set(_) => TypeTag::Set,
57            Value::Tuple(_) => TypeTag::Tuple,
58            Value::Record(_) => TypeTag::Record,
59            Value::Enum(_) => TypeTag::Enum,
60            Value::Function(_) => TypeTag::Function,
61            Value::Optional(_) => TypeTag::Optional,
62            Value::Result(_) => TypeTag::Result,
63            Value::Range { .. } => TypeTag::Range,
64            Value::Iterator(_) => TypeTag::Iterator,
65            Value::StringBuilder(_) => TypeTag::StringBuilder,
66            Value::Future(_) => TypeTag::Future,
67            Value::Duration(_) => TypeTag::Duration,
68            Value::Instant(_) => TypeTag::Instant,
69            Value::Channel(_) => TypeTag::Channel,
70        }
71    }
72
73    /// Human-readable name for error messages.
74    #[must_use]
75    pub fn name(self) -> &'static str {
76        match self {
77            TypeTag::Int => "Int",
78            TypeTag::Float => "Float",
79            TypeTag::Bool => "Bool",
80            TypeTag::String => "String",
81            TypeTag::Char => "Char",
82            TypeTag::Void => "Void",
83            TypeTag::List => "List",
84            TypeTag::Map => "Map",
85            TypeTag::Set => "Set",
86            TypeTag::Tuple => "Tuple",
87            TypeTag::Record => "Record",
88            TypeTag::Enum => "Enum",
89            TypeTag::Function => "Function",
90            TypeTag::Optional => "Optional",
91            TypeTag::Result => "Result",
92            TypeTag::Range => "Range",
93            TypeTag::Iterator => "Iterator",
94            TypeTag::StringBuilder => "StringBuilder",
95            TypeTag::Future => "Future",
96            TypeTag::Duration => "Duration",
97            TypeTag::Instant => "Instant",
98            TypeTag::Channel => "Channel",
99        }
100    }
101}
102
103impl std::fmt::Display for TypeTag {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.write_str(self.name())
106    }
107}
108
109// ─── CallbackInvoker ──────────────────────────────────────────────────────────
110
111/// Trait for invoking Bock closures from within built-in functions.
112///
113/// Higher-order builtins (e.g., `List.map`, `Optional.flat_map`) receive a
114/// `&mut dyn CallbackInvoker` so they can call user-supplied closures without
115/// needing direct access to the interpreter.
116///
117/// Returns a boxed future so the trait stays object-safe while still letting
118/// the interpreter drive recursive async evaluation.
119pub trait CallbackInvoker: Send {
120    /// Invoke `callable` (a `Value::Function`) with the given arguments.
121    fn invoke<'a>(
122        &'a mut self,
123        callable: &'a Value,
124        args: &'a [Value],
125    ) -> BoxFuture<'a, Result<Value, RuntimeError>>;
126}
127
128/// A no-op invoker that always returns an error.
129///
130/// Used in tests and contexts where callback invocation is not available.
131pub struct NoOpInvoker;
132
133impl CallbackInvoker for NoOpInvoker {
134    fn invoke<'a>(
135        &'a mut self,
136        _callable: &'a Value,
137        _args: &'a [Value],
138    ) -> BoxFuture<'a, Result<Value, RuntimeError>> {
139        Box::pin(async {
140            Err(RuntimeError::TypeError(
141                "callback invocation not available in this context".to_string(),
142            ))
143        })
144    }
145}
146
147// ─── BuiltinFn ────────────────────────────────────────────────────────────────
148
149/// Signature for a built-in method.
150///
151/// The first element of `args` is the receiver (the value the method is called on).
152/// Remaining elements are the method arguments.
153pub type BuiltinFn = fn(&[Value]) -> Result<Value, RuntimeError>;
154
155/// Signature for a higher-order built-in method that needs to invoke callbacks.
156///
157/// Like [`BuiltinFn`], the first element of `args` is the receiver.
158/// The `invoker` parameter allows calling Bock closures passed as arguments.
159///
160/// Returns a boxed future so the builtin can `.await` callback invocations.
161pub type HigherOrderBuiltinFn = for<'a> fn(
162    &'a [Value],
163    &'a mut dyn CallbackInvoker,
164) -> BoxFuture<'a, Result<Value, RuntimeError>>;
165
166// ─── BuiltinRegistry ─────────────────────────────────────────────────────────
167
168/// A dispatch table mapping `(TypeTag, method_name)` pairs to built-in
169/// implementations.
170///
171/// Also supports global built-in functions (e.g., `print`, `println`, `debug`).
172/// Higher-order methods (those needing callback invocation) are stored separately.
173#[derive(Clone)]
174pub struct BuiltinRegistry {
175    /// Method dispatch: `(TypeTag, name) → fn`.
176    methods: HashMap<(TypeTag, String), BuiltinFn>,
177    /// Higher-order method dispatch: `(TypeTag, name) → fn` (needs callback invoker).
178    ho_methods: HashMap<(TypeTag, String), HigherOrderBuiltinFn>,
179    /// Global built-in functions: `name → fn`.
180    globals: HashMap<String, BuiltinFn>,
181}
182
183impl Default for BuiltinRegistry {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189impl BuiltinRegistry {
190    /// Create an empty registry.
191    #[must_use]
192    pub fn new() -> Self {
193        Self {
194            methods: HashMap::new(),
195            ho_methods: HashMap::new(),
196            globals: HashMap::new(),
197        }
198    }
199
200    /// Register a method for a specific type.
201    pub fn register(&mut self, type_tag: TypeTag, name: &str, func: BuiltinFn) {
202        self.methods.insert((type_tag, name.to_string()), func);
203    }
204
205    /// Register a higher-order method that needs callback invocation.
206    pub fn register_ho(&mut self, type_tag: TypeTag, name: &str, func: HigherOrderBuiltinFn) {
207        self.ho_methods.insert((type_tag, name.to_string()), func);
208    }
209
210    /// Register a global built-in function.
211    pub fn register_global(&mut self, name: &str, func: BuiltinFn) {
212        self.globals.insert(name.to_string(), func);
213    }
214
215    /// Look up and call a method on a value.
216    ///
217    /// `receiver` is the value the method is called on, `args` are the
218    /// additional arguments. Returns `None` if no method is registered,
219    /// allowing the interpreter to fall back to other dispatch mechanisms.
220    ///
221    /// Note: this does NOT check higher-order methods. Use [`get_ho_method`]
222    /// for those, since they require a [`CallbackInvoker`].
223    pub fn call(
224        &self,
225        type_tag: TypeTag,
226        name: &str,
227        args: &[Value],
228    ) -> Option<Result<Value, RuntimeError>> {
229        self.methods
230            .get(&(type_tag, name.to_string()))
231            .map(|func| func(args))
232    }
233
234    /// Look up and call a global built-in function.
235    ///
236    /// Returns `None` if no global with that name is registered.
237    pub fn call_global(&self, name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
238        self.globals.get(name).map(|func| func(args))
239    }
240
241    /// Look up a higher-order method. Returns the function pointer if found.
242    ///
243    /// The caller is responsible for invoking the returned function with
244    /// a `&mut dyn CallbackInvoker`.
245    #[must_use]
246    pub fn get_ho_method(&self, type_tag: TypeTag, name: &str) -> Option<HigherOrderBuiltinFn> {
247        self.ho_methods.get(&(type_tag, name.to_string())).copied()
248    }
249
250    /// Check whether a method is registered for the given type (simple or higher-order).
251    #[must_use]
252    pub fn has_method(&self, type_tag: TypeTag, name: &str) -> bool {
253        let key = (type_tag, name.to_string());
254        self.methods.contains_key(&key) || self.ho_methods.contains_key(&key)
255    }
256
257    /// Check whether a global function is registered.
258    #[must_use]
259    pub fn has_global(&self, name: &str) -> bool {
260        self.globals.contains_key(name)
261    }
262
263    /// Iterate all registered `(receiver type, method name)` pairs for both
264    /// ordinary and higher-order methods. Intended for introspection tools
265    /// (vocab emitter, documentation generators).
266    pub fn method_keys(&self) -> impl Iterator<Item = (TypeTag, &str)> {
267        self.methods
268            .keys()
269            .chain(self.ho_methods.keys())
270            .map(|(t, n)| (*t, n.as_str()))
271    }
272
273    /// Iterate all registered global function names.
274    pub fn global_names(&self) -> impl Iterator<Item = &str> {
275        self.globals.keys().map(String::as_str)
276    }
277
278    /// Register test assertion built-ins (`expect` global and assertion methods).
279    ///
280    /// Call this when running in test mode so that `expect(x).to_equal(y)` and
281    /// related assertion methods are available.
282    pub fn register_test_builtins(&mut self) {
283        self.register_global("expect", builtin_expect);
284        self.register(TypeTag::Record, "to_equal", expect_to_equal);
285        self.register(TypeTag::Record, "to_be_ok", expect_to_be_ok);
286        self.register(TypeTag::Record, "to_be_err", expect_to_be_err);
287        self.register(TypeTag::Record, "to_be_some", expect_to_be_some);
288        self.register(TypeTag::Record, "to_be_none", expect_to_be_none);
289        self.register(TypeTag::Record, "to_throw", expect_to_throw);
290        self.register(TypeTag::Record, "to_be_true", expect_to_be_true);
291        self.register(TypeTag::Record, "to_be_false", expect_to_be_false);
292    }
293
294    /// Register the minimal bootstrap set of built-in methods and globals.
295    ///
296    /// This provides just enough to make the interpreter functional:
297    /// - Globals: `print`, `println`, `debug`
298    /// - String: `len`, `to_string`
299    /// - List: `len`, `get`, `push`
300    /// - Map: `get`, `set`, `len`
301    pub fn register_defaults(&mut self) {
302        // ── Global functions ──────────────────────────────────────────────
303        self.register_global("print", builtin_print);
304        self.register_global("println", builtin_println);
305        self.register_global("debug", builtin_debug);
306        self.register_global("assert", builtin_assert);
307        self.register_global("todo", builtin_todo);
308        self.register_global("unreachable", builtin_unreachable);
309
310        // ── String methods ────────────────────────────────────────────────
311        self.register(TypeTag::String, "len", string_len);
312        self.register(TypeTag::String, "to_string", string_to_string);
313
314        // ── List methods ──────────────────────────────────────────────────
315        self.register(TypeTag::List, "len", list_len);
316        self.register(TypeTag::List, "get", list_get);
317        self.register(TypeTag::List, "push", list_push);
318
319        // ── Map methods ───────────────────────────────────────────────────
320        self.register(TypeTag::Map, "len", map_len);
321        self.register(TypeTag::Map, "get", map_get);
322        self.register(TypeTag::Map, "set", map_set);
323
324        // ── Primitive conversion methods ─────────────────────────────────
325        self.register(TypeTag::Int, "to_float", int_to_float);
326        self.register(TypeTag::Float, "to_int", float_to_int);
327        self.register(TypeTag::Bool, "to_int", bool_to_int);
328        self.register(TypeTag::Char, "to_int", char_to_int);
329
330        // ── Universal ─────────────────────────────────────────────────────
331        // to_string for all types (registered per type for non-String)
332        self.register(TypeTag::Int, "to_string", universal_to_string);
333        self.register(TypeTag::Float, "to_string", universal_to_string);
334        self.register(TypeTag::Bool, "to_string", universal_to_string);
335        self.register(TypeTag::Char, "to_string", universal_to_string);
336        self.register(TypeTag::Void, "to_string", universal_to_string);
337        self.register(TypeTag::List, "to_string", universal_to_string);
338        self.register(TypeTag::Map, "to_string", universal_to_string);
339        self.register(TypeTag::Set, "to_string", universal_to_string);
340        self.register(TypeTag::Tuple, "to_string", universal_to_string);
341        self.register(TypeTag::Record, "to_string", universal_to_string);
342        self.register(TypeTag::Enum, "to_string", universal_to_string);
343        self.register(TypeTag::Function, "to_string", universal_to_string);
344        self.register(TypeTag::Optional, "to_string", universal_to_string);
345        self.register(TypeTag::Result, "to_string", universal_to_string);
346        self.register(TypeTag::Range, "to_string", universal_to_string);
347        self.register(TypeTag::Iterator, "to_string", universal_to_string);
348        self.register(TypeTag::StringBuilder, "to_string", universal_to_string);
349        self.register(TypeTag::Duration, "to_string", universal_to_string);
350        self.register(TypeTag::Instant, "to_string", universal_to_string);
351        self.register(TypeTag::Channel, "to_string", universal_to_string);
352    }
353}
354
355// ─── Global built-in functions ────────────────────────────────────────────────
356
357/// `print(args...)` — print values separated by spaces, no trailing newline.
358fn builtin_print(args: &[Value]) -> Result<Value, RuntimeError> {
359    let parts: Vec<String> = args.iter().map(|v| v.to_string()).collect();
360    print!("{}", parts.join(" "));
361    Ok(Value::Void)
362}
363
364/// `println(args...)` — print values separated by spaces, with trailing newline.
365fn builtin_println(args: &[Value]) -> Result<Value, RuntimeError> {
366    let parts: Vec<String> = args.iter().map(|v| v.to_string()).collect();
367    println!("{}", parts.join(" "));
368    Ok(Value::Void)
369}
370
371/// `debug(value)` — print a debug representation of a value.
372fn builtin_debug(args: &[Value]) -> Result<Value, RuntimeError> {
373    for arg in args {
374        println!("{arg:?}");
375    }
376    Ok(Value::Void)
377}
378
379/// `assert(condition, message?)` — panic if condition is false.
380fn builtin_assert(args: &[Value]) -> Result<Value, RuntimeError> {
381    let condition = match args.first() {
382        Some(Value::Bool(b)) => *b,
383        Some(other) => {
384            return Err(RuntimeError::TypeError(format!(
385                "assert expects Bool, got {other}"
386            )))
387        }
388        None => {
389            return Err(RuntimeError::ArityMismatch {
390                expected: 1,
391                got: 0,
392            })
393        }
394    };
395    if condition {
396        Ok(Value::Void)
397    } else {
398        let msg = match args.get(1) {
399            Some(Value::String(s)) => format!("assertion failed: {}", s.as_str()),
400            Some(other) => format!("assertion failed: {other}"),
401            None => "assertion failed".to_string(),
402        };
403        Err(RuntimeError::AssertionFailed(msg))
404    }
405}
406
407/// `todo(message?)` — always panic with "not yet implemented".
408fn builtin_todo(args: &[Value]) -> Result<Value, RuntimeError> {
409    let msg = match args.first() {
410        Some(Value::String(s)) => format!("not yet implemented: {}", s.as_str()),
411        Some(other) => format!("not yet implemented: {other}"),
412        None => "not yet implemented".to_string(),
413    };
414    Err(RuntimeError::NotImplemented(msg))
415}
416
417/// `unreachable(message?)` — always panic with "entered unreachable code".
418fn builtin_unreachable(_args: &[Value]) -> Result<Value, RuntimeError> {
419    Err(RuntimeError::Unreachable)
420}
421
422// ─── Universal methods ────────────────────────────────────────────────────────
423
424/// `value.to_string()` — convert any value to its string representation.
425fn universal_to_string(args: &[Value]) -> Result<Value, RuntimeError> {
426    let receiver = args
427        .first()
428        .ok_or_else(|| RuntimeError::TypeError("to_string requires a receiver".to_string()))?;
429    Ok(Value::String(BockString::new(receiver.to_string())))
430}
431
432// ─── Primitive conversion methods ────────────────────────────────────────────
433
434/// `int.to_float()` — convert Int to Float.
435fn int_to_float(args: &[Value]) -> Result<Value, RuntimeError> {
436    match args.first() {
437        Some(Value::Int(n)) => Ok(Value::Float(OrdF64(*n as f64))),
438        _ => Err(RuntimeError::TypeError(
439            "Int.to_float called on non-Int".to_string(),
440        )),
441    }
442}
443
444/// `float.to_int()` — truncate Float to Int.
445fn float_to_int(args: &[Value]) -> Result<Value, RuntimeError> {
446    match args.first() {
447        Some(Value::Float(f)) => {
448            if f.0.is_nan() || f.0.is_infinite() {
449                Err(RuntimeError::TypeError(
450                    "cannot convert NaN or Infinity to Int".to_string(),
451                ))
452            } else {
453                Ok(Value::Int(f.0 as i64))
454            }
455        }
456        _ => Err(RuntimeError::TypeError(
457            "Float.to_int called on non-Float".to_string(),
458        )),
459    }
460}
461
462/// `bool.to_int()` — convert Bool to Int (true=1, false=0).
463fn bool_to_int(args: &[Value]) -> Result<Value, RuntimeError> {
464    match args.first() {
465        Some(Value::Bool(b)) => Ok(Value::Int(if *b { 1 } else { 0 })),
466        _ => Err(RuntimeError::TypeError(
467            "Bool.to_int called on non-Bool".to_string(),
468        )),
469    }
470}
471
472/// `char.to_int()` — convert Char to its Unicode codepoint.
473fn char_to_int(args: &[Value]) -> Result<Value, RuntimeError> {
474    match args.first() {
475        Some(Value::Char(c)) => Ok(Value::Int(*c as i64)),
476        _ => Err(RuntimeError::TypeError(
477            "Char.to_int called on non-Char".to_string(),
478        )),
479    }
480}
481
482// ─── String methods ───────────────────────────────────────────────────────────
483
484/// `string.len()` — number of characters (not bytes).
485fn string_len(args: &[Value]) -> Result<Value, RuntimeError> {
486    let receiver = args
487        .first()
488        .ok_or_else(|| RuntimeError::TypeError("String.len requires a receiver".to_string()))?;
489    match receiver {
490        Value::String(s) => Ok(Value::Int(s.as_str().chars().count() as i64)),
491        _ => Err(RuntimeError::TypeError(
492            "String.len called on non-String".to_string(),
493        )),
494    }
495}
496
497/// `string.to_string()` — identity for strings.
498fn string_to_string(args: &[Value]) -> Result<Value, RuntimeError> {
499    let receiver = args.first().ok_or_else(|| {
500        RuntimeError::TypeError("String.to_string requires a receiver".to_string())
501    })?;
502    Ok(receiver.clone())
503}
504
505// ─── List methods ─────────────────────────────────────────────────────────────
506
507/// `list.len()` — number of elements.
508fn list_len(args: &[Value]) -> Result<Value, RuntimeError> {
509    match args.first() {
510        Some(Value::List(items)) => Ok(Value::Int(items.len() as i64)),
511        _ => Err(RuntimeError::TypeError(
512            "List.len called on non-List".to_string(),
513        )),
514    }
515}
516
517/// `list.get(index)` — get element at index, returning `Optional`.
518fn list_get(args: &[Value]) -> Result<Value, RuntimeError> {
519    let items = match args.first() {
520        Some(Value::List(items)) => items,
521        _ => {
522            return Err(RuntimeError::TypeError(
523                "List.get called on non-List".to_string(),
524            ))
525        }
526    };
527    let idx = args.get(1).ok_or(RuntimeError::ArityMismatch {
528        expected: 1,
529        got: 0,
530    })?;
531    match idx {
532        Value::Int(i) => {
533            let i = *i;
534            if i < 0 || i as usize >= items.len() {
535                Ok(Value::Optional(None))
536            } else {
537                Ok(Value::Optional(Some(Box::new(items[i as usize].clone()))))
538            }
539        }
540        _ => Err(RuntimeError::TypeError(
541            "List.get expects an Int index".to_string(),
542        )),
543    }
544}
545
546/// `list.push(value)` — return a new list with the value appended.
547fn list_push(args: &[Value]) -> Result<Value, RuntimeError> {
548    let items = match args.first() {
549        Some(Value::List(items)) => items,
550        _ => {
551            return Err(RuntimeError::TypeError(
552                "List.push called on non-List".to_string(),
553            ))
554        }
555    };
556    let val = args.get(1).ok_or(RuntimeError::ArityMismatch {
557        expected: 1,
558        got: 0,
559    })?;
560    let mut new_list = items.clone();
561    new_list.push(val.clone());
562    Ok(Value::List(new_list))
563}
564
565// ─── Map methods ──────────────────────────────────────────────────────────────
566
567/// `map.len()` — number of entries.
568fn map_len(args: &[Value]) -> Result<Value, RuntimeError> {
569    match args.first() {
570        Some(Value::Map(map)) => Ok(Value::Int(map.len() as i64)),
571        _ => Err(RuntimeError::TypeError(
572            "Map.len called on non-Map".to_string(),
573        )),
574    }
575}
576
577/// `map.get(key)` — look up a key, returning `Optional`.
578fn map_get(args: &[Value]) -> Result<Value, RuntimeError> {
579    let map = match args.first() {
580        Some(Value::Map(map)) => map,
581        _ => {
582            return Err(RuntimeError::TypeError(
583                "Map.get called on non-Map".to_string(),
584            ))
585        }
586    };
587    let key = args.get(1).ok_or(RuntimeError::ArityMismatch {
588        expected: 1,
589        got: 0,
590    })?;
591    Ok(Value::Optional(map.get(key).cloned().map(Box::new)))
592}
593
594/// `map.set(key, value)` — return a new map with the key-value pair added/updated.
595fn map_set(args: &[Value]) -> Result<Value, RuntimeError> {
596    let map = match args.first() {
597        Some(Value::Map(map)) => map,
598        _ => {
599            return Err(RuntimeError::TypeError(
600                "Map.set called on non-Map".to_string(),
601            ))
602        }
603    };
604    if args.len() < 3 {
605        return Err(RuntimeError::ArityMismatch {
606            expected: 2,
607            got: args.len() - 1,
608        });
609    }
610    let key = args[1].clone();
611    let val = args[2].clone();
612    let mut new_map = map.clone();
613    new_map.insert(key, val);
614    Ok(Value::Map(new_map))
615}
616
617// ─── Test assertion built-ins ─────────────────────────────────────────────
618
619use crate::value::RecordValue;
620use std::collections::BTreeMap;
621
622/// `expect(value)` — create an `Expectation` record wrapping the given value.
623fn builtin_expect(args: &[Value]) -> Result<Value, RuntimeError> {
624    let actual = args.first().cloned().unwrap_or(Value::Void);
625    let mut fields = BTreeMap::new();
626    fields.insert("actual".to_string(), actual);
627    Ok(Value::Record(RecordValue {
628        type_name: "Expectation".to_string(),
629        fields,
630    }))
631}
632
633/// Extract the `actual` value from an Expectation record.
634fn get_expectation_actual(args: &[Value]) -> Result<Value, RuntimeError> {
635    match args.first() {
636        Some(Value::Record(r)) if r.type_name == "Expectation" => r
637            .fields
638            .get("actual")
639            .cloned()
640            .ok_or_else(|| RuntimeError::TypeError("malformed Expectation".to_string())),
641        _ => Err(RuntimeError::TypeError(
642            "assertion method called on non-Expectation value".to_string(),
643        )),
644    }
645}
646
647/// `expect(x).to_equal(y)` — assert `x == y`.
648fn expect_to_equal(args: &[Value]) -> Result<Value, RuntimeError> {
649    let actual = get_expectation_actual(args)?;
650    let expected = args.get(1).ok_or(RuntimeError::ArityMismatch {
651        expected: 1,
652        got: 0,
653    })?;
654    if actual != *expected {
655        return Err(RuntimeError::AssertionFailed(format!(
656            "expected {expected}, got {actual}"
657        )));
658    }
659    Ok(Value::Void)
660}
661
662/// `expect(x).to_be_ok()` — assert `x` is `Ok(...)`.
663fn expect_to_be_ok(args: &[Value]) -> Result<Value, RuntimeError> {
664    let actual = get_expectation_actual(args)?;
665    match &actual {
666        Value::Result(Ok(_)) => Ok(Value::Void),
667        _ => Err(RuntimeError::AssertionFailed(format!(
668            "expected Ok(...), got {actual}"
669        ))),
670    }
671}
672
673/// `expect(x).to_be_err()` — assert `x` is `Err(...)`.
674fn expect_to_be_err(args: &[Value]) -> Result<Value, RuntimeError> {
675    let actual = get_expectation_actual(args)?;
676    match &actual {
677        Value::Result(Err(_)) => Ok(Value::Void),
678        _ => Err(RuntimeError::AssertionFailed(format!(
679            "expected Err(...), got {actual}"
680        ))),
681    }
682}
683
684/// `expect(x).to_be_some()` — assert `x` is `Some(...)`.
685fn expect_to_be_some(args: &[Value]) -> Result<Value, RuntimeError> {
686    let actual = get_expectation_actual(args)?;
687    match &actual {
688        Value::Optional(Some(_)) => Ok(Value::Void),
689        _ => Err(RuntimeError::AssertionFailed(format!(
690            "expected Some(...), got {actual}"
691        ))),
692    }
693}
694
695/// `expect(x).to_be_none()` — assert `x` is `None`.
696fn expect_to_be_none(args: &[Value]) -> Result<Value, RuntimeError> {
697    let actual = get_expectation_actual(args)?;
698    match &actual {
699        Value::Optional(None) => Ok(Value::Void),
700        _ => Err(RuntimeError::AssertionFailed(format!(
701            "expected None, got {actual}"
702        ))),
703    }
704}
705
706/// `expect(x).to_throw()` — assert `x` is an error value (Err variant).
707///
708/// Alias-like behavior for `to_be_err` but semantically about "throwing".
709fn expect_to_throw(args: &[Value]) -> Result<Value, RuntimeError> {
710    expect_to_be_err(args)
711}
712
713/// `expect(x).to_be_true()` — assert `x` is `true`.
714fn expect_to_be_true(args: &[Value]) -> Result<Value, RuntimeError> {
715    let actual = get_expectation_actual(args)?;
716    if actual != Value::Bool(true) {
717        return Err(RuntimeError::AssertionFailed(format!(
718            "expected true, got {actual}"
719        )));
720    }
721    Ok(Value::Void)
722}
723
724/// `expect(x).to_be_false()` — assert `x` is `false`.
725fn expect_to_be_false(args: &[Value]) -> Result<Value, RuntimeError> {
726    let actual = get_expectation_actual(args)?;
727    if actual != Value::Bool(false) {
728        return Err(RuntimeError::AssertionFailed(format!(
729            "expected false, got {actual}"
730        )));
731    }
732    Ok(Value::Void)
733}
734
735// ─── Tests ────────────────────────────────────────────────────────────────────
736
737#[cfg(test)]
738mod tests {
739    use std::collections::BTreeMap;
740
741    use super::*;
742
743    fn make_registry() -> BuiltinRegistry {
744        let mut reg = BuiltinRegistry::new();
745        reg.register_defaults();
746        reg
747    }
748
749    // ── TypeTag ──────────────────────────────────────────────────────────
750
751    #[test]
752    fn type_tag_of_all_variants() {
753        assert_eq!(TypeTag::of(&Value::Int(0)), TypeTag::Int);
754        assert_eq!(TypeTag::of(&Value::Float(0.0.into())), TypeTag::Float);
755        assert_eq!(TypeTag::of(&Value::Bool(true)), TypeTag::Bool);
756        assert_eq!(
757            TypeTag::of(&Value::String(BockString::new("x"))),
758            TypeTag::String
759        );
760        assert_eq!(TypeTag::of(&Value::Char('x')), TypeTag::Char);
761        assert_eq!(TypeTag::of(&Value::Void), TypeTag::Void);
762        assert_eq!(TypeTag::of(&Value::List(vec![])), TypeTag::List);
763        assert_eq!(TypeTag::of(&Value::Map(BTreeMap::new())), TypeTag::Map);
764    }
765
766    #[test]
767    fn type_tag_display() {
768        assert_eq!(TypeTag::Int.to_string(), "Int");
769        assert_eq!(TypeTag::String.to_string(), "String");
770        assert_eq!(TypeTag::List.to_string(), "List");
771    }
772
773    // ── Global functions ─────────────────────────────────────────────────
774
775    #[test]
776    fn println_returns_void() {
777        let reg = make_registry();
778        let result = reg
779            .call_global("println", &[Value::String(BockString::new("hello"))])
780            .unwrap();
781        assert_eq!(result.unwrap(), Value::Void);
782    }
783
784    #[test]
785    fn print_returns_void() {
786        let reg = make_registry();
787        let result = reg.call_global("print", &[Value::Int(42)]).unwrap();
788        assert_eq!(result.unwrap(), Value::Void);
789    }
790
791    #[test]
792    fn debug_returns_void() {
793        let reg = make_registry();
794        let result = reg.call_global("debug", &[Value::Bool(true)]).unwrap();
795        assert_eq!(result.unwrap(), Value::Void);
796    }
797
798    #[test]
799    fn unknown_global_returns_none() {
800        let reg = make_registry();
801        assert!(reg.call_global("nonexistent", &[]).is_none());
802    }
803
804    // ── String methods ───────────────────────────────────────────────────
805
806    #[test]
807    fn string_len_counts_chars() {
808        let reg = make_registry();
809        let recv = Value::String(BockString::new("héllo"));
810        let result = reg.call(TypeTag::String, "len", &[recv]).unwrap().unwrap();
811        assert_eq!(result, Value::Int(5));
812    }
813
814    #[test]
815    fn string_to_string_identity() {
816        let reg = make_registry();
817        let recv = Value::String(BockString::new("test"));
818        let result = reg
819            .call(TypeTag::String, "to_string", &[recv.clone()])
820            .unwrap()
821            .unwrap();
822        assert_eq!(result, recv);
823    }
824
825    // ── List methods ─────────────────────────────────────────────────────
826
827    #[test]
828    fn list_len_works() {
829        let reg = make_registry();
830        let recv = Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
831        let result = reg.call(TypeTag::List, "len", &[recv]).unwrap().unwrap();
832        assert_eq!(result, Value::Int(3));
833    }
834
835    #[test]
836    fn list_get_valid_index() {
837        let reg = make_registry();
838        let recv = Value::List(vec![Value::Int(10), Value::Int(20)]);
839        let result = reg
840            .call(TypeTag::List, "get", &[recv, Value::Int(1)])
841            .unwrap()
842            .unwrap();
843        assert_eq!(result, Value::Optional(Some(Box::new(Value::Int(20)))));
844    }
845
846    #[test]
847    fn list_get_out_of_bounds() {
848        let reg = make_registry();
849        let recv = Value::List(vec![Value::Int(10)]);
850        let result = reg
851            .call(TypeTag::List, "get", &[recv, Value::Int(5)])
852            .unwrap()
853            .unwrap();
854        assert_eq!(result, Value::Optional(None));
855    }
856
857    #[test]
858    fn list_push_appends() {
859        let reg = make_registry();
860        let recv = Value::List(vec![Value::Int(1)]);
861        let result = reg
862            .call(TypeTag::List, "push", &[recv, Value::Int(2)])
863            .unwrap()
864            .unwrap();
865        assert_eq!(result, Value::List(vec![Value::Int(1), Value::Int(2)]));
866    }
867
868    // ── Map methods ──────────────────────────────────────────────────────
869
870    #[test]
871    fn map_len_works() {
872        let reg = make_registry();
873        let mut m = BTreeMap::new();
874        m.insert(Value::Int(1), Value::Bool(true));
875        let recv = Value::Map(m);
876        let result = reg.call(TypeTag::Map, "len", &[recv]).unwrap().unwrap();
877        assert_eq!(result, Value::Int(1));
878    }
879
880    #[test]
881    fn map_get_existing_key() {
882        let reg = make_registry();
883        let mut m = BTreeMap::new();
884        m.insert(Value::String(BockString::new("a")), Value::Int(42));
885        let recv = Value::Map(m);
886        let result = reg
887            .call(
888                TypeTag::Map,
889                "get",
890                &[recv, Value::String(BockString::new("a"))],
891            )
892            .unwrap()
893            .unwrap();
894        assert_eq!(result, Value::Optional(Some(Box::new(Value::Int(42)))));
895    }
896
897    #[test]
898    fn map_get_missing_key() {
899        let reg = make_registry();
900        let recv = Value::Map(BTreeMap::new());
901        let result = reg
902            .call(
903                TypeTag::Map,
904                "get",
905                &[recv, Value::String(BockString::new("missing"))],
906            )
907            .unwrap()
908            .unwrap();
909        assert_eq!(result, Value::Optional(None));
910    }
911
912    #[test]
913    fn map_set_inserts() {
914        let reg = make_registry();
915        let recv = Value::Map(BTreeMap::new());
916        let result = reg
917            .call(
918                TypeTag::Map,
919                "set",
920                &[recv, Value::Int(1), Value::Bool(true)],
921            )
922            .unwrap()
923            .unwrap();
924        let mut expected = BTreeMap::new();
925        expected.insert(Value::Int(1), Value::Bool(true));
926        assert_eq!(result, Value::Map(expected));
927    }
928
929    // ── Unknown method produces clear error ──────────────────────────────
930
931    #[test]
932    fn unknown_method_returns_none() {
933        let reg = make_registry();
934        let recv = Value::Int(42);
935        assert!(reg.call(TypeTag::Int, "nonexistent", &[recv]).is_none());
936    }
937
938    // ── Registration API extensibility ───────────────────────────────────
939
940    #[test]
941    fn external_registration_works() {
942        let mut reg = make_registry();
943        fn custom_method(args: &[Value]) -> Result<Value, RuntimeError> {
944            Ok(args.first().cloned().unwrap_or(Value::Void))
945        }
946        reg.register(TypeTag::Int, "custom", custom_method);
947        let result = reg
948            .call(TypeTag::Int, "custom", &[Value::Int(99)])
949            .unwrap()
950            .unwrap();
951        assert_eq!(result, Value::Int(99));
952    }
953
954    // ── Universal to_string ──────────────────────────────────────────────
955
956    #[test]
957    fn int_to_string() {
958        let reg = make_registry();
959        let result = reg
960            .call(TypeTag::Int, "to_string", &[Value::Int(42)])
961            .unwrap()
962            .unwrap();
963        assert_eq!(result, Value::String(BockString::new("42")));
964    }
965
966    #[test]
967    fn bool_to_string() {
968        let reg = make_registry();
969        let result = reg
970            .call(TypeTag::Bool, "to_string", &[Value::Bool(true)])
971            .unwrap()
972            .unwrap();
973        assert_eq!(result, Value::String(BockString::new("true")));
974    }
975
976    // ── assert / todo / unreachable ─────────────────────────────────────
977
978    #[test]
979    fn assert_true_passes() {
980        let reg = make_registry();
981        let result = reg.call_global("assert", &[Value::Bool(true)]).unwrap();
982        assert_eq!(result.unwrap(), Value::Void);
983    }
984
985    #[test]
986    fn assert_false_errors() {
987        let reg = make_registry();
988        let result = reg.call_global("assert", &[Value::Bool(false)]).unwrap();
989        assert!(result.is_err());
990    }
991
992    #[test]
993    fn assert_false_with_message() {
994        let reg = make_registry();
995        let result = reg
996            .call_global(
997                "assert",
998                &[Value::Bool(false), Value::String(BockString::new("bad"))],
999            )
1000            .unwrap();
1001        match result {
1002            Err(RuntimeError::AssertionFailed(msg)) => assert!(msg.contains("bad")),
1003            other => panic!("expected AssertionFailed, got {other:?}"),
1004        }
1005    }
1006
1007    #[test]
1008    fn todo_produces_runtime_error() {
1009        let reg = make_registry();
1010        let result = reg.call_global("todo", &[]).unwrap();
1011        match result {
1012            Err(RuntimeError::NotImplemented(msg)) => {
1013                assert!(msg.contains("not yet implemented"));
1014            }
1015            other => panic!("expected NotImplemented, got {other:?}"),
1016        }
1017    }
1018
1019    #[test]
1020    fn unreachable_produces_runtime_error() {
1021        let reg = make_registry();
1022        let result = reg.call_global("unreachable", &[]).unwrap();
1023        assert!(matches!(result, Err(RuntimeError::Unreachable)));
1024    }
1025}