Skip to main content

bop/
methods.rs

1#[cfg(feature = "no_std")]
2use alloc::{format, string::{String, ToString}, vec::Vec};
3
4use crate::builtins::{error, expect_number};
5use crate::error::BopError;
6use crate::value::{Value, values_equal};
7
8/// Returns (return_value, optional_mutated_object)
9pub fn array_method(
10    arr: &[Value],
11    method: &str,
12    args: &[Value],
13    line: u32,
14) -> Result<(Value, Option<Value>), BopError> {
15    match method {
16        "len" => Ok((Value::Int(arr.len() as i64), None)),
17        "push" => {
18            if args.len() != 1 {
19                return Err(error(line, ".push() needs exactly 1 argument"));
20            }
21            let mut new_arr = arr.to_vec();
22            new_arr.push(args[0].clone());
23            Ok((Value::None, Some(Value::new_array(new_arr))))
24        }
25        "pop" => {
26            let mut new_arr = arr.to_vec();
27            let popped = new_arr.pop().unwrap_or(Value::None);
28            Ok((popped, Some(Value::new_array(new_arr))))
29        }
30        "has" => {
31            if args.len() != 1 {
32                return Err(error(line, ".has() needs exactly 1 argument"));
33            }
34            let found = arr.iter().any(|v| values_equal(v, &args[0]));
35            Ok((Value::Bool(found), None))
36        }
37        "index_of" => {
38            if args.len() != 1 {
39                return Err(error(line, ".index_of() needs exactly 1 argument"));
40            }
41            let idx = arr.iter().position(|v| values_equal(v, &args[0]));
42            Ok((Value::Int(idx.map_or(-1, |i| i as i64)), None))
43        }
44        "insert" => {
45            if args.len() != 2 {
46                return Err(error(line, ".insert() needs 2 arguments: index and value"));
47            }
48            let i = expect_number("insert", &args[0], line)? as usize;
49            let mut new_arr = arr.to_vec();
50            if i > new_arr.len() {
51                return Err(error(line, format!("Insert index {} is out of bounds", i)));
52            }
53            new_arr.insert(i, args[1].clone());
54            Ok((Value::None, Some(Value::new_array(new_arr))))
55        }
56        "remove" => {
57            if args.len() != 1 {
58                return Err(error(line, ".remove() needs exactly 1 argument (index)"));
59            }
60            let i = expect_number("remove", &args[0], line)? as usize;
61            let mut new_arr = arr.to_vec();
62            if i >= new_arr.len() {
63                return Err(error(line, format!("Remove index {} is out of bounds", i)));
64            }
65            let removed = new_arr.remove(i);
66            Ok((removed, Some(Value::new_array(new_arr))))
67        }
68        "slice" => {
69            if args.len() != 2 {
70                return Err(error(line, ".slice() needs 2 arguments: start and end"));
71            }
72            let start = expect_number("slice", &args[0], line)? as usize;
73            let end = (expect_number("slice", &args[1], line)? as usize).min(arr.len());
74            let start = start.min(end);
75            let slice = arr[start..end].to_vec();
76            Ok((Value::new_array(slice), None))
77        }
78        "reverse" => {
79            let mut new_arr = arr.to_vec();
80            new_arr.reverse();
81            Ok((Value::None, Some(Value::new_array(new_arr))))
82        }
83        "sort" => {
84            let mut new_arr = arr.to_vec();
85            new_arr.sort_by(|a, b| match (a, b) {
86                (Value::Int(x), Value::Int(y)) => x.cmp(y),
87                (Value::Number(x), Value::Number(y)) => {
88                    x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal)
89                }
90                // Mixed numeric sort — widen through f64 so
91                // `[1, 2.5, 0]` sorts in the obvious numeric
92                // order.
93                (Value::Int(x), Value::Number(y)) => (*x as f64)
94                    .partial_cmp(y)
95                    .unwrap_or(core::cmp::Ordering::Equal),
96                (Value::Number(x), Value::Int(y)) => x
97                    .partial_cmp(&(*y as f64))
98                    .unwrap_or(core::cmp::Ordering::Equal),
99                (Value::Str(x), Value::Str(y)) => x.cmp(y),
100                _ => core::cmp::Ordering::Equal,
101            });
102            Ok((Value::None, Some(Value::new_array(new_arr))))
103        }
104        "join" => {
105            if args.len() != 1 {
106                return Err(error(line, ".join() needs exactly 1 argument (separator)"));
107            }
108            let sep = match &args[0] {
109                Value::Str(s) => s.as_str(),
110                _ => return Err(error(line, ".join() separator must be a string")),
111            };
112            let result = arr
113                .iter()
114                .map(|v| format!("{}", v))
115                .collect::<Vec<_>>()
116                .join(sep);
117            Ok((Value::new_str(result), None))
118        }
119        "iter" => {
120            crate::builtins::expect_args("iter", args, 0, line)?;
121            // Snapshot the items — subsequent mutation of the
122            // source array must not poke the iterator. Cheap
123            // because every inner Value::Clone is either a
124            // primitive copy or an Rc bump.
125            Ok((Value::new_array_iter(arr.to_vec()), None))
126        }
127        _ => Err(error(line, format!("Array doesn't have a .{}() method", method))),
128    }
129}
130
131pub fn string_method(
132    s: &str,
133    method: &str,
134    args: &[Value],
135    line: u32,
136) -> Result<(Value, Option<Value>), BopError> {
137    match method {
138        "len" => Ok((Value::Int(s.chars().count() as i64), None)),
139        "contains" => {
140            if args.len() != 1 {
141                return Err(error(line, ".contains() needs 1 argument"));
142            }
143            let substr = match &args[0] {
144                Value::Str(s) => s.as_str(),
145                _ => return Err(error(line, ".contains() needs a string argument")),
146            };
147            Ok((Value::Bool(s.contains(substr)), None))
148        }
149        "starts_with" => {
150            if args.len() != 1 {
151                return Err(error(line, ".starts_with() needs 1 argument"));
152            }
153            let prefix = match &args[0] {
154                Value::Str(s) => s.as_str(),
155                _ => return Err(error(line, ".starts_with() needs a string")),
156            };
157            Ok((Value::Bool(s.starts_with(prefix)), None))
158        }
159        "ends_with" => {
160            if args.len() != 1 {
161                return Err(error(line, ".ends_with() needs 1 argument"));
162            }
163            let suffix = match &args[0] {
164                Value::Str(s) => s.as_str(),
165                _ => return Err(error(line, ".ends_with() needs a string")),
166            };
167            Ok((Value::Bool(s.ends_with(suffix)), None))
168        }
169        "index_of" => {
170            if args.len() != 1 {
171                return Err(error(line, ".index_of() needs 1 argument"));
172            }
173            let substr = match &args[0] {
174                Value::Str(s) => s.as_str(),
175                _ => return Err(error(line, ".index_of() needs a string")),
176            };
177            let idx = s.find(substr).map_or(-1, |i| i as i64);
178            Ok((Value::Int(idx), None))
179        }
180        "split" => {
181            if args.len() != 1 {
182                return Err(error(line, ".split() needs 1 argument"));
183            }
184            let sep = match &args[0] {
185                Value::Str(s) => s.as_str(),
186                _ => return Err(error(line, ".split() needs a string")),
187            };
188            let parts: Vec<Value> = s
189                .split(sep)
190                .map(|p| Value::new_str(p.to_string()))
191                .collect();
192            Ok((Value::new_array(parts), None))
193        }
194        "replace" => {
195            if args.len() != 2 {
196                return Err(error(line, ".replace() needs 2 arguments: old and new"));
197            }
198            let old = match &args[0] {
199                Value::Str(s) => s.as_str(),
200                _ => return Err(error(line, ".replace() arguments must be strings")),
201            };
202            let new = match &args[1] {
203                Value::Str(s) => s.as_str(),
204                _ => return Err(error(line, ".replace() arguments must be strings")),
205            };
206            let result = s.replace(old, new);
207            Ok((Value::new_str(result), None))
208        }
209        "upper" => {
210            let result = s.to_uppercase();
211            Ok((Value::new_str(result), None))
212        }
213        "lower" => {
214            let result = s.to_lowercase();
215            Ok((Value::new_str(result), None))
216        }
217        "trim" => {
218            let result = s.trim().to_string();
219            Ok((Value::new_str(result), None))
220        }
221        "slice" => {
222            if args.len() != 2 {
223                return Err(error(line, ".slice() needs 2 arguments: start and end"));
224            }
225            let start = expect_number("slice", &args[0], line)? as usize;
226            let chars: Vec<char> = s.chars().collect();
227            let end = (expect_number("slice", &args[1], line)? as usize).min(chars.len());
228            let start = start.min(end);
229            let result: String = chars[start..end].iter().collect();
230            Ok((Value::new_str(result), None))
231        }
232        "to_int" => {
233            if !args.is_empty() {
234                return Err(error(line, ".to_int() takes no arguments"));
235            }
236            // Integer-first parse so `"42".to_int()` stays an
237            // `Int`. Fall back to float-then-truncate for
238            // decimal-shaped strings, matching the old
239            // `"3.7".to_int()` behaviour.
240            if let Ok(n) = s.parse::<i64>() {
241                return Ok((Value::Int(n), None));
242            }
243            let n: f64 = s.parse().map_err(|_| {
244                error(line, format!("Can't convert \"{}\" to a number", s))
245            })?;
246            Ok((Value::Int(n as i64), None))
247        }
248        "to_float" => {
249            if !args.is_empty() {
250                return Err(error(line, ".to_float() takes no arguments"));
251            }
252            let n: f64 = s.parse().map_err(|_| {
253                error(line, format!("Can't convert \"{}\" to a number", s))
254            })?;
255            Ok((Value::Number(n), None))
256        }
257        "iter" => {
258            crate::builtins::expect_args("iter", args, 0, line)?;
259            let chars: Vec<char> = s.chars().collect();
260            Ok((Value::new_string_iter(chars), None))
261        }
262        _ => Err(error(line, format!("String doesn't have a .{}() method", method))),
263    }
264}
265
266pub fn dict_method(
267    entries: &[(String, Value)],
268    method: &str,
269    args: &[Value],
270    line: u32,
271) -> Result<(Value, Option<Value>), BopError> {
272    match method {
273        "len" => Ok((Value::Int(entries.len() as i64), None)),
274        "keys" => {
275            let keys: Vec<Value> = entries
276                .iter()
277                .map(|(k, _)| Value::new_str(k.clone()))
278                .collect();
279            Ok((Value::new_array(keys), None))
280        }
281        "values" => {
282            let vals: Vec<Value> = entries.iter().map(|(_, v)| v.clone()).collect();
283            Ok((Value::new_array(vals), None))
284        }
285        "has" => {
286            if args.len() != 1 {
287                return Err(error(line, ".has() needs 1 argument"));
288            }
289            let key = match &args[0] {
290                Value::Str(s) => s.as_str(),
291                _ => return Err(error(line, ".has() needs a string key")),
292            };
293            Ok((Value::Bool(entries.iter().any(|(k, _)| k == key)), None))
294        }
295        "iter" => {
296            crate::builtins::expect_args("iter", args, 0, line)?;
297            // Matches `for k in dict` semantics: iterate keys
298            // in declaration order.
299            let keys: Vec<String> = entries.iter().map(|(k, _)| k.clone()).collect();
300            Ok((Value::new_dict_iter(keys), None))
301        }
302        _ => Err(error(line, format!("Dict doesn't have a .{}() method", method))),
303    }
304}
305
306/// Methods every value understands: introspection + stringification.
307/// Dispatched from `call_method` *before* the type-specific method
308/// tables so `x.type()`, `x.to_str()`, and `x.inspect()` work
309/// uniformly across every `Value` shape.
310///
311/// Returns `Ok(Some(result))` when the method name matched,
312/// `Ok(None)` when it didn't (so the caller falls through to
313/// the type-specific dispatcher). `Err` on arg-count mismatches.
314pub fn common_method(
315    receiver: &Value,
316    method: &str,
317    args: &[Value],
318    line: u32,
319) -> Result<Option<(Value, Option<Value>)>, BopError> {
320    match method {
321        "type" => {
322            crate::builtins::expect_args("type", args, 0, line)?;
323            Ok(Some((
324                Value::new_str(receiver.type_name().to_string()),
325                None,
326            )))
327        }
328        "to_str" => {
329            crate::builtins::expect_args("to_str", args, 0, line)?;
330            Ok(Some((
331                Value::new_str(format!("{}", receiver)),
332                None,
333            )))
334        }
335        "inspect" => {
336            crate::builtins::expect_args("inspect", args, 0, line)?;
337            Ok(Some((Value::new_str(receiver.inspect()), None)))
338        }
339        // `.is_none()` / `.is_some()` — dynamic-typing's answer
340        // to `Option`. Any variable can hold `none` (missing
341        // dict key, explicit `return none`, unset optional
342        // field), so the check works on every value rather than
343        // only on an Option-typed receiver. Equivalent to
344        // `x == none` / `x != none` but reads more intention-
345        // ally at the call site and composes with method chains.
346        "is_none" => {
347            crate::builtins::expect_args("is_none", args, 0, line)?;
348            Ok(Some((
349                Value::Bool(matches!(receiver, Value::None)),
350                None,
351            )))
352        }
353        "is_some" => {
354            crate::builtins::expect_args("is_some", args, 0, line)?;
355            Ok(Some((
356                Value::Bool(!matches!(receiver, Value::None)),
357                None,
358            )))
359        }
360        _ => Ok(None),
361    }
362}
363
364/// Method dispatch for `Int` and `Number` receivers. Covers the
365/// math operations that used to be global builtins (`abs`,
366/// `sqrt`, `sin`, …) plus numeric coercions (`to_int`,
367/// `to_float`). Returns `Err` on argument errors or unknown
368/// method names — no fall-through.
369pub fn numeric_method(
370    receiver: &Value,
371    method: &str,
372    args: &[Value],
373    line: u32,
374) -> Result<(Value, Option<Value>), BopError> {
375    use crate::builtins::{expect_args, finite_to_int_or_number};
376    match method {
377        // Preserves type: Int stays Int, Number stays Number.
378        "abs" => {
379            expect_args("abs", args, 0, line)?;
380            match receiver {
381                Value::Int(n) => n
382                    .checked_abs()
383                    .map(Value::Int)
384                    .map(|v| (v, None))
385                    .ok_or_else(|| error(line, "Integer overflow in `.abs()`")),
386                Value::Number(n) => Ok((Value::Number(n.abs()), None)),
387                _ => unreachable!("numeric_method called on non-numeric receiver"),
388            }
389        }
390        // Square / trig / exp / log: always return `Number`.
391        "sqrt" => unary_number(receiver, args, line, "sqrt", crate::math::sqrt),
392        "sin" => unary_number(receiver, args, line, "sin", crate::math::sin),
393        "cos" => unary_number(receiver, args, line, "cos", crate::math::cos),
394        "tan" => unary_number(receiver, args, line, "tan", crate::math::tan),
395        "exp" => unary_number(receiver, args, line, "exp", crate::math::exp),
396        "log" => unary_number(receiver, args, line, "log", crate::math::ln),
397        // Round-to-integer: return Int when the result fits i64,
398        // Number otherwise.
399        "floor" => unary_round(receiver, args, line, "floor", crate::math::floor),
400        "ceil" => unary_round(receiver, args, line, "ceil", crate::math::ceil),
401        "round" => unary_round(receiver, args, line, "round", crate::math::round),
402        "pow" => {
403            expect_args("pow", args, 1, line)?;
404            let base = to_f64_or_error(receiver, "pow", line)?;
405            let exp = to_f64_or_error(&args[0], "pow", line)?;
406            Ok((Value::Number(crate::math::powf(base, exp)), None))
407        }
408        // Binary pick-min / pick-max. Preserves type when both
409        // sides match; widens to Number on mixed operands —
410        // same rule the old `a.min(b)` / `a.max(b)` builtins
411        // used.
412        "min" => pair_pick(receiver, args, line, "min", true),
413        "max" => pair_pick(receiver, args, line, "max", false),
414        // Explicit numeric coercion. `int` ↔ `int` is a no-op,
415        // `number` → `int` truncates toward zero.
416        "to_int" => {
417            expect_args("to_int", args, 0, line)?;
418            match receiver {
419                Value::Int(n) => Ok((Value::Int(*n), None)),
420                Value::Number(n) => Ok((Value::Int(*n as i64), None)),
421                _ => unreachable!(),
422            }
423        }
424        "to_float" => {
425            expect_args("to_float", args, 0, line)?;
426            match receiver {
427                Value::Int(n) => Ok((Value::Number(*n as f64), None)),
428                Value::Number(n) => Ok((Value::Number(*n), None)),
429                _ => unreachable!(),
430            }
431        }
432        _ => {
433            let _ = finite_to_int_or_number;
434            Err(error(
435                line,
436                crate::error_messages::no_such_method(receiver.type_name(), method),
437            ))
438        }
439    }
440}
441
442/// Method dispatch for `Bool`. Only the numeric coercions
443/// (`true.to_int()` → `1`, etc.); `type` / `to_str` / `inspect`
444/// go through `common_method` before this is called.
445pub fn bool_method(
446    receiver: &Value,
447    method: &str,
448    args: &[Value],
449    line: u32,
450) -> Result<(Value, Option<Value>), BopError> {
451    use crate::builtins::expect_args;
452    let b = match receiver {
453        Value::Bool(b) => *b,
454        _ => unreachable!("bool_method called on non-bool receiver"),
455    };
456    match method {
457        "to_int" => {
458            expect_args("to_int", args, 0, line)?;
459            Ok((Value::Int(if b { 1 } else { 0 }), None))
460        }
461        "to_float" => {
462            expect_args("to_float", args, 0, line)?;
463            Ok((Value::Number(if b { 1.0 } else { 0.0 }), None))
464        }
465        _ => Err(error(
466            line,
467            crate::error_messages::no_such_method("bool", method),
468        )),
469    }
470}
471
472// ─── numeric_method helpers ────────────────────────────────────
473
474/// Coerce an `Int` / `Number` receiver to `f64`. Anything else
475/// is a programmer error — `numeric_method` only reaches this
476/// helper for numeric receivers.
477fn to_f64_or_error(v: &Value, method: &str, line: u32) -> Result<f64, BopError> {
478    match v {
479        Value::Int(n) => Ok(*n as f64),
480        Value::Number(n) => Ok(*n),
481        other => Err(error(
482            line,
483            format!(
484                "`.{}` expects a number, got {}",
485                method,
486                other.type_name()
487            ),
488        )),
489    }
490}
491
492/// Shared implementation for trig / exp / log — zero-arg
493/// methods that always return a `Number`.
494fn unary_number(
495    receiver: &Value,
496    args: &[Value],
497    line: u32,
498    method: &str,
499    op: fn(f64) -> f64,
500) -> Result<(Value, Option<Value>), BopError> {
501    crate::builtins::expect_args(method, args, 0, line)?;
502    let x = to_f64_or_error(receiver, method, line)?;
503    Ok((Value::Number(op(x)), None))
504}
505
506/// Shared implementation for `floor` / `ceil` / `round`. Return
507/// type mirrors the stdlib: `Int` when the rounded value fits
508/// in `i64`, `Number` otherwise.
509fn unary_round(
510    receiver: &Value,
511    args: &[Value],
512    line: u32,
513    method: &str,
514    op: fn(f64) -> f64,
515) -> Result<(Value, Option<Value>), BopError> {
516    use crate::builtins::{expect_args, finite_to_int_or_number};
517    expect_args(method, args, 0, line)?;
518    match receiver {
519        Value::Int(n) => Ok((Value::Int(*n), None)),
520        Value::Number(n) => Ok((finite_to_int_or_number(op(*n)), None)),
521        _ => unreachable!("unary_round called on non-numeric receiver"),
522    }
523}
524
525/// `.min(other)` / `.max(other)` — preserves numeric type
526/// when both operands match, widens to Number on mixed shape.
527fn pair_pick(
528    receiver: &Value,
529    args: &[Value],
530    line: u32,
531    method: &str,
532    pick_smaller: bool,
533) -> Result<(Value, Option<Value>), BopError> {
534    use crate::builtins::expect_args;
535    expect_args(method, args, 1, line)?;
536    match (receiver, &args[0]) {
537        (Value::Int(a), Value::Int(b)) => {
538            let pick = if pick_smaller { (*a).min(*b) } else { (*a).max(*b) };
539            Ok((Value::Int(pick), None))
540        }
541        (Value::Number(a), Value::Number(b)) => {
542            let pick = if pick_smaller { a.min(*b) } else { a.max(*b) };
543            Ok((Value::Number(pick), None))
544        }
545        (Value::Int(a), Value::Number(b)) => {
546            let af = *a as f64;
547            let pick = if pick_smaller { af.min(*b) } else { af.max(*b) };
548            Ok((Value::Number(pick), None))
549        }
550        (Value::Number(a), Value::Int(b)) => {
551            let bf = *b as f64;
552            let pick = if pick_smaller { a.min(bf) } else { a.max(bf) };
553            Ok((Value::Number(pick), None))
554        }
555        (_, other) => Err(error(
556            line,
557            format!(
558                "`.{}({})` expects a number, got {}",
559                method,
560                other.type_name(),
561                other.type_name()
562            ),
563        )),
564    }
565}
566
567pub fn is_mutating_method(method: &str) -> bool {
568    matches!(
569        method,
570        "push" | "pop" | "insert" | "remove" | "reverse" | "sort"
571    )
572}
573
574// ─── Result methods ────────────────────────────────────────────
575//
576// `Result` is a built-in enum (see `builtins::builtin_result_variants`
577// seeded into every engine's type table). Its combinators live
578// here so `r.is_ok()`, `r.unwrap()`, etc. are always available
579// without `use std.result`. Callable-taking variants (`map`,
580// `map_err`, `and_then`) need the evaluator's call primitive and
581// therefore live in each engine's MethodCall dispatch alongside
582// the user-method table, not here — see [`is_result_callable_method`].
583
584/// True when `receiver` is a `Value::EnumVariant` whose type
585/// identity is the built-in `Result`. Shared by each engine
586/// so a user enum named `Result` in some module can't
587/// accidentally steal the built-in method dispatch.
588pub fn is_builtin_result(receiver: &Value) -> bool {
589    match receiver {
590        Value::EnumVariant(e) => {
591            e.module_path() == crate::value::BUILTIN_MODULE_PATH && e.type_name() == "Result"
592        }
593        _ => false,
594    }
595}
596
597/// Identifies `map` / `map_err` / `and_then` as the
598/// callable-taking Result methods each engine handles inline.
599/// Returns `None` for anything else so the caller can fall
600/// through to [`result_method`] or the standard dispatcher.
601pub enum ResultCallableKind {
602    /// `r.map(f)` — `Ok(v)` becomes `Ok(f(v))`, `Err(e)` passes.
603    Map,
604    /// `r.map_err(f)` — `Err(e)` becomes `Err(f(e))`, `Ok(v)` passes.
605    MapErr,
606    /// `r.and_then(f)` — `Ok(v)` becomes `f(v)` (expected to
607    /// return a Result), `Err(e)` passes.
608    AndThen,
609}
610
611pub fn is_result_callable_method(method: &str) -> Option<ResultCallableKind> {
612    match method {
613        "map" => Some(ResultCallableKind::Map),
614        "map_err" => Some(ResultCallableKind::MapErr),
615        "and_then" => Some(ResultCallableKind::AndThen),
616        _ => None,
617    }
618}
619
620/// Pure Result method dispatch. Handles `is_ok`, `is_err`,
621/// `unwrap`, `expect`, `unwrap_or` — the combinators whose
622/// implementation doesn't need to invoke a user callable.
623///
624/// Returns `Ok(Some(value))` when the method name matched,
625/// `Ok(None)` when it didn't (so the caller falls through to
626/// the callable-taking Result methods, then to the standard
627/// dispatcher). Assumes `receiver` is already known to be a
628/// built-in Result — callers check [`is_builtin_result`] first.
629pub fn result_method(
630    receiver: &Value,
631    method: &str,
632    args: &[Value],
633    line: u32,
634) -> Result<Option<Value>, BopError> {
635    use crate::builtins::expect_args;
636    let e = match receiver {
637        Value::EnumVariant(e) => e,
638        _ => return Ok(None),
639    };
640    let ok_payload = || -> Option<Value> {
641        if e.variant() != "Ok" {
642            return None;
643        }
644        match e.payload() {
645            crate::value::EnumPayload::Tuple(items) if items.len() == 1 => {
646                Some(items[0].clone())
647            }
648            _ => None,
649        }
650    };
651    let err_payload = || -> Option<Value> {
652        if e.variant() != "Err" {
653            return None;
654        }
655        match e.payload() {
656            crate::value::EnumPayload::Tuple(items) if items.len() == 1 => {
657                Some(items[0].clone())
658            }
659            _ => None,
660        }
661    };
662    match method {
663        "is_ok" => {
664            expect_args("is_ok", args, 0, line)?;
665            Ok(Some(Value::Bool(e.variant() == "Ok")))
666        }
667        "is_err" => {
668            expect_args("is_err", args, 0, line)?;
669            Ok(Some(Value::Bool(e.variant() == "Err")))
670        }
671        "unwrap" => {
672            expect_args("unwrap", args, 0, line)?;
673            if let Some(v) = ok_payload() {
674                return Ok(Some(v));
675            }
676            // Err case — construct the same message std.result
677            // used to emit so migrated programs keep their
678            // crash-trace text stable.
679            let detail = match err_payload() {
680                Some(payload) => format!("unwrap on Err: {}", payload.inspect()),
681                None => String::from("unwrap on Err"),
682            };
683            Err(error(line, detail))
684        }
685        "expect" => {
686            expect_args("expect", args, 1, line)?;
687            if let Some(v) = ok_payload() {
688                return Ok(Some(v));
689            }
690            let message = match &args[0] {
691                Value::Str(s) => s.as_str().to_string(),
692                other => format!("{}", other),
693            };
694            Err(error(line, message))
695        }
696        "unwrap_or" => {
697            expect_args("unwrap_or", args, 1, line)?;
698            if let Some(v) = ok_payload() {
699                return Ok(Some(v));
700            }
701            Ok(Some(args[0].clone()))
702        }
703        _ => Ok(None),
704    }
705}
706
707/// Build a `Result::Ok(value)` with the builtin's module path so
708/// pattern matches against `Result::Ok(_)` fire regardless of
709/// which module the receiver came from. Mirror of the helper in
710/// `builtins` but specialised to the "wrap a value" case each
711/// engine's callable dispatch uses.
712pub fn make_result_ok(value: Value) -> Value {
713    Value::new_enum_tuple(
714        String::from(crate::value::BUILTIN_MODULE_PATH),
715        String::from("Result"),
716        String::from("Ok"),
717        alloc_vec_of(value),
718    )
719}
720
721/// Same as [`make_result_ok`] but for `Err`.
722pub fn make_result_err(value: Value) -> Value {
723    Value::new_enum_tuple(
724        String::from(crate::value::BUILTIN_MODULE_PATH),
725        String::from("Result"),
726        String::from("Err"),
727        alloc_vec_of(value),
728    )
729}
730
731fn alloc_vec_of(value: Value) -> Vec<Value> {
732    let mut v = Vec::with_capacity(1);
733    v.push(value);
734    v
735}
736
737// ─── Iterator methods ──────────────────────────────────────────
738//
739// `Value::Iter` receivers get two methods:
740//  - `.next()` advances the cursor and returns
741//    `Iter::Next(value)` or `Iter::Done` — the shape the `for`
742//    loop (and user code) pattern-matches on.
743//  - `.iter()` returns the receiver itself. Makes iterators
744//    idempotently iterable, matching Python's iterator protocol
745//    (an iterator's `__iter__` returns `self`), so `for x in
746//    arr.iter()` works without a special case.
747//
748// User-defined iterators are just ordinary struct values with
749// their own `.next()` method — they don't go through this
750// dispatcher. The `for` loop treats them uniformly via the
751// protocol: call `.iter()` to get an iterator, call `.next()`
752// until `Iter::Done`.
753
754pub fn iter_method(
755    receiver: &Value,
756    method: &str,
757    args: &[Value],
758    line: u32,
759) -> Result<(Value, Option<Value>), BopError> {
760    use crate::builtins::{expect_args, make_iter_done, make_iter_next};
761    let cell = match receiver {
762        Value::Iter(cell) => cell,
763        _ => unreachable!("iter_method called on non-iterator receiver"),
764    };
765    match method {
766        "next" => {
767            expect_args("next", args, 0, line)?;
768            let mut inner = cell.borrow_mut();
769            match inner.next() {
770                Some(v) => Ok((make_iter_next(v), None)),
771                None => Ok((make_iter_done(), None)),
772            }
773        }
774        "iter" => {
775            expect_args("iter", args, 0, line)?;
776            // Iterators are their own iterator — clone the Rc
777            // so callers share the cursor.
778            Ok((receiver.clone(), None))
779        }
780        _ => Err(error(
781            line,
782            crate::error_messages::no_such_method("iter", method),
783        )),
784    }
785}