Skip to main content

bop/
lib.rs

1#![cfg_attr(feature = "no_std", no_std)]
2
3#[cfg(feature = "no_std")]
4extern crate alloc;
5
6#[cfg(feature = "no_std")]
7use alloc::{string::String, vec::Vec};
8
9pub mod error;
10pub mod error_messages;
11pub mod value;
12pub mod lexer;
13pub mod parser;
14pub mod math;
15pub mod memory;
16pub mod naming;
17pub mod ops;
18pub mod precheck;
19pub mod builtins;
20pub mod host;
21pub mod methods;
22pub mod suggest;
23pub mod check;
24/// Bundled Bop standard library (`use std.math`, `std.json`,
25/// `std.collections`, `std.iter`, `std.string`, `std.result`,
26/// `std.test`). Feature-gated behind `bop-std` (on by default).
27///
28/// The module is named `stdlib` rather than `std` specifically
29/// to avoid shadowing Rust's own `::std` when a consumer does
30/// `use bop::*` (or when anything in the crate's own tests
31/// does `use super::*`). `bop::stdlib::resolve(...)` is the
32/// public entry point.
33#[cfg(feature = "bop-std")]
34pub mod stdlib;
35
36mod evaluator;
37
38pub use error::BopError;
39pub use error::BopWarning;
40pub use parser::{Stmt, count_instructions};
41pub use value::Value;
42
43/// The core pattern matcher. Re-exported so engines beyond the
44/// tree-walker (the bytecode VM, the AOT runtime) can apply the
45/// same structural rules without re-implementing them.
46pub use evaluator::pattern_matches;
47
48/// Shared scope walker for resolving source-level type
49/// references to the declaring module. Re-exported because VM
50/// and AOT both need to build per-frame type resolvers when
51/// calling [`pattern_matches`] — the identity comparison lives
52/// on the matcher side, but the scope lookup is identical
53/// across engines.
54pub use evaluator::resolve_type_in;
55
56/// Type alias for the resolver closure `pattern_matches` expects.
57pub use evaluator::TypeResolveFn;
58
59/// Persistent state for an interactive REPL session. Unlike
60/// the one-shot [`run`] / [`Evaluator::run`] pair, a
61/// `ReplSession` carries its scopes, fns, user types, method
62/// tables, and import cache across calls, so
63/// `session.eval("let x = 5")` followed by
64/// `session.eval("print(x)")` sees the same `x`.
65pub use evaluator::ReplSession;
66
67// ─── BopLimits ─────────────────────────────────────────────────────────────
68
69/// Resource limits enforced during execution.
70#[derive(Debug, Clone)]
71pub struct BopLimits {
72    /// Max interpreter ticks (loop iterations, statements, etc.)
73    pub max_steps: u64,
74    /// Max total tracked memory (bytes) for strings + arrays
75    pub max_memory: usize,
76}
77
78impl BopLimits {
79    pub fn standard() -> Self {
80        Self {
81            max_steps: 10_000,
82            max_memory: 10 * 1024 * 1024, // 10 MB
83        }
84    }
85
86    pub fn demo() -> Self {
87        Self {
88            max_steps: 1_000,
89            max_memory: 1024 * 1024, // 1 MB
90        }
91    }
92}
93
94impl Default for BopLimits {
95    fn default() -> Self {
96        Self::standard()
97    }
98}
99
100// ─── BopHost trait ─────────────────────────────────────────────────────────
101
102/// Extension point for embedders to add custom built-in functions.
103pub trait BopHost {
104    /// Called for unknown function names. Return `None` = not handled.
105    fn call(&mut self, name: &str, args: &[Value], line: u32) -> Option<Result<Value, BopError>>;
106
107    /// Called by `print()`.
108    fn on_print(&mut self, message: &str) {
109        let _ = message;
110    }
111
112    /// Hint text for "function not found" errors.
113    fn function_hint(&self) -> &str {
114        ""
115    }
116
117    /// Called each tick. Return `Err` to halt execution.
118    fn on_tick(&mut self) -> Result<(), BopError> {
119        Ok(())
120    }
121
122    /// Resolve an `use` target to Bop source.
123    ///
124    /// The core language doesn't know where modules live — a
125    /// filesystem embedder reads `.bop` files, a browser embedder
126    /// might fetch a URL, an embedded host might look up bundled
127    /// string assets. Returning:
128    ///
129    /// - `None` — "I don't handle this module path": the runtime
130    ///   raises a *module not found* error.
131    /// - `Some(Ok(source))` — the module's source text, to be
132    ///   parsed and executed by the engine.
133    /// - `Some(Err(e))` — the resolver itself failed (I/O error,
134    ///   bad path, …); the engine propagates the error as-is.
135    ///
136    /// The default impl returns `None`, so by default a program
137    /// that imports anything halts with *module not found*.
138    fn resolve_module(&mut self, name: &str) -> Option<Result<String, BopError>> {
139        let _ = name;
140        None
141    }
142}
143
144// ─── Public API ────────────────────────────────────────────────────────────
145
146/// Run a Bop program with the given host and limits.
147pub fn run<H: BopHost>(source: &str, host: &mut H, limits: &BopLimits) -> Result<(), BopError> {
148    let tokens = lexer::lex(source)?;
149    let stmts = parser::parse(tokens)?;
150    let eval = evaluator::Evaluator::new(host, limits.clone());
151    eval.run(&stmts)
152}
153
154/// Parse Bop source into an AST (useful for instruction counting).
155pub fn parse(source: &str) -> Result<Vec<Stmt>, BopError> {
156    let tokens = lexer::lex(source)?;
157    parser::parse(tokens)
158}
159
160/// Parse Bop source and run the static check pass, returning
161/// both the AST and any non-fatal warnings (currently:
162/// match-exhaustiveness). Prefer this over [`parse`] in tools
163/// that surface diagnostics to users — the CLI uses it to
164/// print warnings before running the program.
165///
166/// Imported enums are opaque at this layer — see
167/// [`parse_with_warnings_and_resolver`] for a variant that
168/// walks `use` statements to pick up imported enum decls so
169/// exhaustiveness warnings fire on them too.
170pub fn parse_with_warnings(
171    source: &str,
172) -> Result<(Vec<Stmt>, Vec<error::BopWarning>), BopError> {
173    let stmts = parse(source)?;
174    let warnings = check::check_program(&stmts);
175    Ok((stmts, warnings))
176}
177
178/// Like [`parse_with_warnings`] but follows every top-level
179/// `use` statement via the supplied resolver so the
180/// exhaustiveness check can see imported enums. `resolver`
181/// has the same shape as [`BopHost::resolve_module`].
182///
183/// Returning `Some(Err(_))` or `None` from `resolver` is
184/// *not* propagated — the checker silently falls back to
185/// treating that module's enums as opaque, same as
186/// [`parse_with_warnings`]. Only a parse error of the root
187/// `source` is surfaced.
188pub fn parse_with_warnings_and_resolver<R>(
189    source: &str,
190    mut resolver: R,
191) -> Result<(Vec<Stmt>, Vec<error::BopWarning>), BopError>
192where
193    R: FnMut(&str) -> Option<Result<String, BopError>>,
194{
195    let stmts = parse(source)?;
196    let warnings = check::check_program_with_resolver(&stmts, &mut resolver);
197    Ok((stmts, warnings))
198}
199
200// ─── Tests ─────────────────────────────────────────────────────────────────
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::cell::RefCell;
206
207    // ─── Test host ─────────────────────────────────────────────────
208
209    struct TestHost {
210        prints: RefCell<Vec<String>>,
211    }
212
213    impl TestHost {
214        fn new() -> Self {
215            Self {
216                prints: RefCell::new(Vec::new()),
217            }
218        }
219
220        fn last_print(&self) -> String {
221            self.prints.borrow().last().cloned().expect("no print output")
222        }
223    }
224
225    impl BopHost for TestHost {
226        fn call(&mut self, _name: &str, _args: &[Value], _line: u32) -> Option<Result<Value, BopError>> {
227            None
228        }
229
230        fn on_print(&mut self, message: &str) {
231            self.prints.borrow_mut().push(message.to_string());
232        }
233    }
234
235    // ─── Test helpers ──────────────────────────────────────────────
236
237    fn test_limits() -> BopLimits {
238        BopLimits::standard()
239    }
240
241    /// Run code, return last print output
242    fn say(code: &str) -> String {
243        // Change say() -> print() in test code
244        let mut host = TestHost::new();
245        run(code, &mut host, &test_limits()).unwrap();
246        host.last_print()
247    }
248
249    /// Run code, expect runtime error, return message
250    fn run_err(code: &str) -> String {
251        let mut host = TestHost::new();
252        run(code, &mut host, &test_limits()).unwrap_err().message
253    }
254
255    /// Expect a lex or parse error, return message
256    fn parse_err(code: &str) -> String {
257        parse(code).unwrap_err().message
258    }
259
260    /// Run code with custom limits, expect a runtime error, return message
261    fn run_err_with_limits(code: &str, limits: BopLimits) -> String {
262        let mut host = TestHost::new();
263        run(code, &mut host, &limits).unwrap_err().message
264    }
265
266    /// Tight limits for safety tests
267    fn tight_limits() -> BopLimits {
268        BopLimits {
269            max_steps: 500,
270            max_memory: 64 * 1024,
271        }
272    }
273
274    // ─── Arithmetic ────────────────────────────────────────────────
275
276    #[test]
277    fn add_numbers() {
278        assert_eq!(say("print(1 + 2)"), "3");
279    }
280
281    #[test]
282    fn subtract() {
283        assert_eq!(say("print(10 - 3)"), "7");
284    }
285
286    #[test]
287    fn multiply() {
288        assert_eq!(say("print(4 * 5)"), "20");
289    }
290
291    #[test]
292    fn divide_float() {
293        assert_eq!(say("print(7 / 2)"), "3.5");
294    }
295
296    #[test]
297    fn divide_whole() {
298        assert_eq!(say("print(6 / 2)"), "3");
299    }
300
301    #[test]
302    fn modulo() {
303        assert_eq!(say("print(10 % 3)"), "1");
304    }
305
306    #[test]
307    fn precedence() {
308        assert_eq!(say("print(2 + 3 * 4)"), "14");
309    }
310
311    #[test]
312    fn parentheses() {
313        assert_eq!(say("print((2 + 3) * 4)"), "20");
314    }
315
316    #[test]
317    fn unary_neg() {
318        assert_eq!(say("print(-5)"), "-5");
319    }
320
321    #[test]
322    fn unary_not() {
323        assert_eq!(say("print(!true)"), "false");
324    }
325
326    // ─── Strings ───────────────────────────────────────────────────
327
328    #[test]
329    fn string_concat() {
330        assert_eq!(say(r#"print("hello" + " " + "world")"#), "hello world");
331    }
332
333    #[test]
334    fn string_repeat() {
335        assert_eq!(say(r#"print("ab" * 3)"#), "ababab");
336    }
337
338    #[test]
339    fn string_interpolation() {
340        assert_eq!(
341            say(r#"let name = "bop"
342print("hi {name}!")"#),
343            "hi bop!"
344        );
345    }
346
347    #[test]
348    fn string_auto_coerce_in_add() {
349        assert_eq!(say(r#"print("val=" + 42)"#), "val=42");
350    }
351
352    // ─── Comparisons & Logic ───────────────────────────────────────
353
354    #[test]
355    fn equality() {
356        assert_eq!(say("print(1 == 1)"), "true");
357        assert_eq!(say("print(1 == 2)"), "false");
358        assert_eq!(say("print(1 != 2)"), "true");
359    }
360
361    #[test]
362    fn ordering() {
363        assert_eq!(say("print(3 < 5)"), "true");
364        assert_eq!(say("print(5 <= 5)"), "true");
365        assert_eq!(say("print(6 > 5)"), "true");
366        assert_eq!(say("print(5 >= 6)"), "false");
367    }
368
369    #[test]
370    fn logical_and_or() {
371        assert_eq!(say("print(true && false)"), "false");
372        assert_eq!(say("print(true || false)"), "true");
373    }
374
375    #[test]
376    fn short_circuit_and() {
377        assert_eq!(say("print(false && x)"), "false");
378    }
379
380    #[test]
381    fn short_circuit_or() {
382        assert_eq!(say("print(true || x)"), "true");
383    }
384
385    // ─── Variables ─────────────────────────────────────────────────
386
387    #[test]
388    fn let_and_use() {
389        assert_eq!(say("let x = 10\nprint(x)"), "10");
390    }
391
392    #[test]
393    fn assign() {
394        assert_eq!(say("let x = 1\nx = 5\nprint(x)"), "5");
395    }
396
397    #[test]
398    fn compound_assign() {
399        assert_eq!(say("let x = 10\nx += 5\nprint(x)"), "15");
400        assert_eq!(say("let x = 10\nx -= 3\nprint(x)"), "7");
401        assert_eq!(say("let x = 4\nx *= 3\nprint(x)"), "12");
402        assert_eq!(say("let x = 10\nx /= 4\nprint(x)"), "2.5");
403        assert_eq!(say("let x = 10\nx %= 3\nprint(x)"), "1");
404    }
405
406    #[test]
407    fn undefined_variable_error() {
408        assert!(run_err("print(nope)").contains("not found"));
409    }
410
411    #[test]
412    fn assign_undeclared_error() {
413        assert!(run_err("x = 5").contains("doesn't exist"));
414    }
415
416    // ─── If / Else ─────────────────────────────────────────────────
417
418    #[test]
419    fn if_true_branch() {
420        assert_eq!(say("if true { print(\"yes\") } else { print(\"no\") }"), "yes");
421    }
422
423    #[test]
424    fn if_false_branch() {
425        assert_eq!(say("if false { print(\"yes\") } else { print(\"no\") }"), "no");
426    }
427
428    #[test]
429    fn if_else_if() {
430        assert_eq!(
431            say(r#"let x = 2
432if x == 1 { print("one") } else if x == 2 { print("two") } else { print("other") }"#),
433            "two"
434        );
435    }
436
437    #[test]
438    fn if_expression() {
439        assert_eq!(say("let x = if true { 1 } else { 2 }\nprint(x)"), "1");
440    }
441
442    // ─── While ─────────────────────────────────────────────────────
443
444    #[test]
445    fn while_loop() {
446        assert_eq!(say("let i = 0\nwhile i < 5 { i += 1 }\nprint(i)"), "5");
447    }
448
449    #[test]
450    fn while_break() {
451        assert_eq!(
452            say("let i = 0\nwhile true { i += 1\nif i == 3 { break } }\nprint(i)"),
453            "3"
454        );
455    }
456
457    #[test]
458    fn while_continue() {
459        assert_eq!(
460            say(r#"let sum = 0
461let i = 0
462while i < 10 {
463    i += 1
464    if i % 2 == 0 { continue }
465    sum += i
466}
467print(sum)"#),
468            "25"
469        );
470    }
471
472    // ─── For ───────────────────────────────────────────────────────
473
474    #[test]
475    fn for_over_array() {
476        assert_eq!(
477            say(r#"let sum = 0
478for x in [10, 20, 30] { sum += x }
479print(sum)"#),
480            "60"
481        );
482    }
483
484    #[test]
485    fn for_over_range() {
486        assert_eq!(
487            say("let sum = 0\nfor i in range(5) { sum += i }\nprint(sum)"),
488            "10"
489        );
490    }
491
492    #[test]
493    fn for_over_string() {
494        assert_eq!(
495            say(r#"let out = ""
496for ch in "abc" { out += ch + "-" }
497print(out)"#),
498            "a-b-c-"
499        );
500    }
501
502    #[test]
503    fn for_with_break() {
504        assert_eq!(
505            say("let last = 0\nfor i in range(100) { if i == 3 { break }\nlast = i }\nprint(last)"),
506            "2"
507        );
508    }
509
510    // ─── Repeat ────────────────────────────────────────────────────
511
512    #[test]
513    fn repeat_loop() {
514        assert_eq!(say("let n = 0\nrepeat 4 { n += 1 }\nprint(n)"), "4");
515    }
516
517    #[test]
518    fn repeat_zero() {
519        assert_eq!(say("let n = 99\nrepeat 0 { n = 0 }\nprint(n)"), "99");
520    }
521
522    // ─── Functions ─────────────────────────────────────────────────
523
524    #[test]
525    fn fn_basic() {
526        assert_eq!(say("fn double(x) { return x * 2 }\nprint(double(5))"), "10");
527    }
528
529    #[test]
530    fn fn_implicit_return_none() {
531        assert_eq!(
532            say(r#"fn noop() { let x = 1 }
533print(noop().type())"#),
534            "none"
535        );
536    }
537
538    #[test]
539    fn fn_multiple_params() {
540        assert_eq!(say("fn add(a, b) { return a + b }\nprint(add(3, 7))"), "10");
541    }
542
543    #[test]
544    fn fn_scope_isolation() {
545        assert!(
546            run_err(
547                r#"let secret = 42
548fn peek() { return secret }
549peek()"#
550            )
551            .contains("not found")
552        );
553    }
554
555    #[test]
556    fn fn_wrong_arg_count() {
557        assert!(run_err("fn f(a, b) { return a }\nf(1)").contains("expects 2"));
558    }
559
560    #[test]
561    fn fn_recursion() {
562        assert_eq!(
563            say(r#"fn fib(n) {
564    if n <= 1 { return n }
565    return fib(n - 1) + fib(n - 2)
566}
567print(fib(10))"#),
568            "55"
569        );
570    }
571
572    // ─── Closures / first-class functions ──────────────────────────
573
574    #[test]
575    fn lambda_basic() {
576        assert_eq!(
577            say(r#"let double = fn(x) { return x * 2 }
578print(double(5))"#),
579            "10"
580        );
581    }
582
583    #[test]
584    fn lambda_captures_value() {
585        assert_eq!(
586            say(r#"let n = 5
587let add_n = fn(x) { return x + n }
588print(add_n(3))"#),
589            "8"
590        );
591    }
592
593    #[test]
594    fn lambda_captures_are_snapshot() {
595        // Mutating `n` after the lambda is built should not
596        // affect the captured value — the snapshot semantics are
597        // deliberate.
598        assert_eq!(
599            say(r#"let n = 5
600let add_n = fn(x) { return x + n }
601n = 100
602print(add_n(3))"#),
603            "8"
604        );
605    }
606
607    #[test]
608    fn lambda_returned_from_fn() {
609        // Classic closure pattern: factory function returns a
610        // specialised closure. The captured `n` outlives the
611        // enclosing frame because it was cloned into the closure.
612        assert_eq!(
613            say(r#"fn make_adder(n) { return fn(x) { return x + n } }
614let add5 = make_adder(5)
615let add10 = make_adder(10)
616print(add5(3))
617print(add10(3))"#),
618            "13"
619        );
620    }
621
622    #[test]
623    fn named_fn_is_first_class_value() {
624        assert_eq!(
625            say(r#"fn double(x) { return x * 2 }
626let f = double
627print(f(7))"#),
628            "14"
629        );
630    }
631
632    #[test]
633    fn fn_stored_in_array_and_called_via_index() {
634        assert_eq!(
635            say(r#"fn add(x, y) { return x + y }
636fn mul(x, y) { return x * y }
637let ops = [add, mul]
638print(ops[0](2, 3))
639print(ops[1](2, 3))"#),
640            "6"
641        );
642    }
643
644    #[test]
645    fn higher_order_apply() {
646        // `apply` takes any callable, proving we call through a
647        // parameter, not a statically known name.
648        assert_eq!(
649            say(r#"fn apply(f, x) { return f(x) }
650fn square(n) { return n * n }
651print(apply(square, 4))
652print(apply(fn(n) { return n + 1 }, 4))"#),
653            "5"
654        );
655    }
656
657    #[test]
658    fn lambda_self_reference_via_named_fn() {
659        // Named-fn decls see themselves through `self.functions`,
660        // so recursion works. Let-bound lambdas are documented as
661        // not supporting self-reference — this test pins the
662        // working case.
663        assert_eq!(
664            say(r#"fn countdown(n) {
665    if n <= 0 { return "done" }
666    return countdown(n - 1)
667}
668print(countdown(3))"#),
669            "done"
670        );
671    }
672
673    #[test]
674    fn type_of_fn_is_fn() {
675        assert_eq!(say("fn f() { }\nprint(f.type())"), "fn");
676        assert_eq!(say("let g = fn() { }\nprint(g.type())"), "fn");
677    }
678
679    #[test]
680    fn calling_non_callable_value_errors() {
681        assert!(run_err("let x = 5\nx(1)").contains("not a function"));
682    }
683
684    #[test]
685    fn lambda_captures_nested_scope() {
686        // Closures snapshot the full lexical scope stack, so
687        // bindings from outer blocks are visible inside nested
688        // lambdas.
689        assert_eq!(
690            say(r#"let a = 1
691if true {
692    let b = 2
693    let f = fn() { return a + b }
694    print(f())
695}"#),
696            "3"
697        );
698    }
699
700    #[test]
701    fn iife() {
702        // Immediately-invoked lambda: `(fn() { ... })()`. Falls
703        // out of the parser for free once lambdas are expressions.
704        assert_eq!(say("print((fn(x) { return x * 3 })(4))"), "12");
705    }
706
707    // ─── Arrays ────────────────────────────────────────────────────
708
709    #[test]
710    fn array_literal_and_index() {
711        assert_eq!(say("let a = [10, 20, 30]\nprint(a[1])"), "20");
712    }
713
714    #[test]
715    fn array_negative_index() {
716        assert_eq!(say("let a = [10, 20, 30]\nprint(a[-1])"), "30");
717    }
718
719    #[test]
720    fn array_assign_index() {
721        assert_eq!(say("let a = [1, 2, 3]\na[1] = 99\nprint(a[1])"), "99");
722    }
723
724    #[test]
725    fn array_push_pop() {
726        assert_eq!(
727            say(r#"let a = [1, 2]
728a.push(3)
729print(a.len())"#),
730            "3"
731        );
732        assert_eq!(
733            say(r#"let a = [1, 2, 3]
734let last = a.pop()
735print(last)"#),
736            "3"
737        );
738    }
739
740    #[test]
741    fn array_has() {
742        assert_eq!(say("print([1, 2, 3].has(2))"), "true");
743        assert_eq!(say("print([1, 2, 3].has(9))"), "false");
744    }
745
746    #[test]
747    fn array_index_of() {
748        assert_eq!(say("print([10, 20, 30].index_of(20))"), "1");
749        assert_eq!(say("print([10, 20, 30].index_of(99))"), "-1");
750    }
751
752    #[test]
753    fn array_slice() {
754        assert_eq!(say("print([1, 2, 3, 4, 5].slice(1, 4))"), "[2, 3, 4]");
755    }
756
757    #[test]
758    fn array_join() {
759        assert_eq!(say(r#"print([1, 2, 3].join("-"))"#), "1-2-3");
760    }
761
762    #[test]
763    fn array_sort() {
764        assert_eq!(say("let a = [3, 1, 2]\na.sort()\nprint(a)"), "[1, 2, 3]");
765    }
766
767    #[test]
768    fn array_reverse() {
769        assert_eq!(say("let a = [1, 2, 3]\na.reverse()\nprint(a)"), "[3, 2, 1]");
770    }
771
772    #[test]
773    fn array_insert_remove() {
774        assert_eq!(
775            say(r#"let a = [1, 3]
776a.insert(1, 2)
777print(a)"#),
778            "[1, 2, 3]"
779        );
780        assert_eq!(
781            say(r#"let a = [1, 2, 3]
782let removed = a.remove(1)
783print(removed)"#),
784            "2"
785        );
786    }
787
788    #[test]
789    fn array_concat() {
790        assert_eq!(say("print([1, 2] + [3, 4])"), "[1, 2, 3, 4]");
791    }
792
793    #[test]
794    fn array_out_of_bounds() {
795        assert!(run_err("let a = [1]\nprint(a[5])").contains("out of bounds"));
796    }
797
798    // ─── Strings (methods) ─────────────────────────────────────────
799
800    #[test]
801    fn string_len() {
802        assert_eq!(say(r#"print("hello".len())"#), "5");
803    }
804
805    #[test]
806    fn string_contains() {
807        assert_eq!(say(r#"print("abcdef".contains("cd"))"#), "true");
808        assert_eq!(say(r#"print("abcdef".contains("zz"))"#), "false");
809    }
810
811    #[test]
812    fn string_starts_ends_with() {
813        assert_eq!(say(r#"print("hello".starts_with("he"))"#), "true");
814        assert_eq!(say(r#"print("hello".ends_with("lo"))"#), "true");
815    }
816
817    #[test]
818    fn string_split() {
819        assert_eq!(say(r#"print("a,b,c".split(","))"#), r#"["a", "b", "c"]"#);
820    }
821
822    #[test]
823    fn string_replace() {
824        assert_eq!(
825            say(r#"print("hello world".replace("world", "bop"))"#),
826            "hello bop"
827        );
828    }
829
830    #[test]
831    fn string_upper_lower_trim() {
832        assert_eq!(say(r#"print("Hello".upper())"#), "HELLO");
833        assert_eq!(say(r#"print("Hello".lower())"#), "hello");
834        assert_eq!(say(r#"print("  hi  ".trim())"#), "hi");
835    }
836
837    #[test]
838    fn string_slice() {
839        assert_eq!(say(r#"print("hello".slice(1, 4))"#), "ell");
840    }
841
842    #[test]
843    fn string_index_of() {
844        assert_eq!(say(r#"print("hello".index_of("ll"))"#), "2");
845        assert_eq!(say(r#"print("hello".index_of("zz"))"#), "-1");
846    }
847
848    #[test]
849    fn string_index_char() {
850        assert_eq!(say(r#"print("abc"[1])"#), "b");
851    }
852
853    // ─── Dicts ─────────────────────────────────────────────────────
854
855    #[test]
856    fn dict_literal_and_access() {
857        assert_eq!(
858            say(r#"let d = {"name": "bop", "hp": 100}
859print(d["name"])"#),
860            "bop"
861        );
862    }
863
864    #[test]
865    fn dict_assign_key() {
866        assert_eq!(
867            say(r#"let d = {"a": 1}
868d["b"] = 2
869print(d["b"])"#),
870            "2"
871        );
872    }
873
874    #[test]
875    fn dict_methods() {
876        assert_eq!(
877            say(r#"let d = {"x": 1, "y": 2}
878print(d.len())"#),
879            "2"
880        );
881        assert_eq!(say(r#"print({"a": 1, "b": 2}.has("a"))"#), "true");
882        assert_eq!(say(r#"print({"a": 1, "b": 2}.has("z"))"#), "false");
883    }
884
885    #[test]
886    fn dict_keys_values() {
887        assert_eq!(say(r#"print({"a": 1, "b": 2}.keys())"#), r#"["a", "b"]"#);
888        assert_eq!(say(r#"print({"a": 1, "b": 2}.values())"#), "[1, 2]");
889    }
890
891    // ─── Built-in functions ────────────────────────────────────────
892
893    #[test]
894    fn builtin_range_1arg() {
895        assert_eq!(say("print(range(5))"), "[0, 1, 2, 3, 4]");
896    }
897
898    #[test]
899    fn builtin_range_2args() {
900        assert_eq!(say("print(range(2, 5))"), "[2, 3, 4]");
901    }
902
903    #[test]
904    fn builtin_range_3args() {
905        assert_eq!(say("print(range(0, 10, 3))"), "[0, 3, 6, 9]");
906    }
907
908    #[test]
909    fn builtin_range_reverse() {
910        assert_eq!(say("print(range(5, 0))"), "[5, 4, 3, 2, 1]");
911    }
912
913    #[test]
914    fn builtin_str() {
915        assert_eq!(say(r#"print(42.to_str())"#), "42");
916        assert_eq!(say(r#"print(true.to_str())"#), "true");
917    }
918
919    #[test]
920    fn builtin_int() {
921        assert_eq!(say("print(3.7.to_int())"), "3");
922        assert_eq!(say("print((-2.9).to_int())"), "-2");
923    }
924
925    #[test]
926    fn builtin_type() {
927        // Phase 6 split numeric types: `42` is an int, `42.0`
928        // is a number.
929        assert_eq!(say("print(42.type())"), "int");
930        assert_eq!(say("print(42.0.type())"), "number");
931        assert_eq!(say(r#"print("hi".type())"#), "string");
932        assert_eq!(say("print(true.type())"), "bool");
933        assert_eq!(say("print(none.type())"), "none");
934        assert_eq!(say("print([].type())"), "array");
935    }
936
937    #[test]
938    fn builtin_abs_min_max() {
939        assert_eq!(say("print((-5).abs())"), "5");
940        assert_eq!(say("print(3.min(7))"), "3");
941        assert_eq!(say("print(3.max(7))"), "7");
942    }
943
944    #[test]
945    fn builtin_len() {
946        assert_eq!(say(r#"print("hello".len())"#), "5");
947        assert_eq!(say("print([1, 2, 3].len())"), "3");
948    }
949
950    #[test]
951    fn builtin_inspect() {
952        assert_eq!(say(r#"print("hi".inspect())"#), r#""hi""#);
953        assert_eq!(say("print(42.inspect())"), "42");
954    }
955
956    #[test]
957    fn builtin_print_multi_args() {
958        let mut host = TestHost::new();
959        run(r#"print("a", "b", "c")"#, &mut host, &test_limits()).unwrap();
960        assert_eq!(host.prints.borrow().as_slice(), &["a b c"]);
961    }
962
963    #[test]
964    fn builtin_rand_deterministic() {
965        let a = say("print(rand(100))");
966        let b = say("print(rand(100))");
967        assert_eq!(a, b);
968    }
969
970    // ─── Error cases ───────────────────────────────────────────────
971
972    #[test]
973    fn error_division_by_zero() {
974        assert!(run_err("print(1 / 0)").contains("Division by zero"));
975    }
976
977    #[test]
978    fn error_type_mismatch_subtract() {
979        let msg = run_err(r#"print("a" - 1)"#);
980        assert!(msg.contains("Can't use `-`"));
981    }
982
983    #[test]
984    fn error_unknown_function() {
985        assert!(run_err("nope()").contains("not found"));
986    }
987
988    #[test]
989    fn error_infinite_loop_protection() {
990        let msg = run_err("while true { }");
991        assert!(msg.contains("too many steps"));
992    }
993
994    #[test]
995    fn error_break_outside_loop() {
996        assert!(run_err("break").contains("outside of a loop"));
997    }
998
999    #[test]
1000    fn error_continue_outside_loop() {
1001        assert!(run_err("continue").contains("outside of a loop"));
1002    }
1003
1004    // ─── Parse errors ──────────────────────────────────────────────
1005
1006    #[test]
1007    fn parse_error_missing_rparen() {
1008        assert!(parse_err("print(1").contains("Expected `)`"));
1009    }
1010
1011    #[test]
1012    fn parse_error_missing_rbrace() {
1013        assert!(parse_err("if true {").contains("Expected `}`"));
1014    }
1015
1016    // ─── Edge cases ────────────────────────────────────────────────
1017
1018    #[test]
1019    fn empty_program() {
1020        let mut host = TestHost::new();
1021        run("", &mut host, &test_limits()).unwrap();
1022        assert!(host.prints.borrow().is_empty());
1023    }
1024
1025    #[test]
1026    fn trailing_comma_in_array() {
1027        assert_eq!(say("print([1, 2, 3,])"), "[1, 2, 3]");
1028    }
1029
1030    #[test]
1031    fn trailing_comma_in_dict() {
1032        assert_eq!(say(r#"print({"a": 1,}.len())"#), "1");
1033    }
1034
1035    #[test]
1036    fn none_value() {
1037        assert_eq!(say("print(none)"), "none");
1038        assert_eq!(say("print(none == none)"), "true");
1039    }
1040
1041    #[test]
1042    fn equality_across_types() {
1043        assert_eq!(say("print(1 == true)"), "false");
1044        assert_eq!(say(r#"print(0 == "")"#), "false");
1045        assert_eq!(say("print(none == false)"), "false");
1046    }
1047
1048    #[test]
1049    fn dict_equality() {
1050        assert_eq!(say(r#"print({"a": 1, "b": 2} == {"b": 2, "a": 1})"#), "true");
1051        assert_eq!(say(r#"print({"a": 1} == {"a": 2})"#), "false");
1052        assert_eq!(say(r#"print({"a": 1} == {"b": 1})"#), "false");
1053        assert_eq!(say(r#"print({"a": 1} == {"a": 1, "b": 2})"#), "false");
1054        assert_eq!(say(r#"print({"a": {"x": 1}} == {"a": {"x": 1}})"#), "true");
1055    }
1056
1057    #[test]
1058    fn nested_array_access() {
1059        assert_eq!(say("let m = [[1, 2], [3, 4]]\nprint(m[1][0])"), "3");
1060    }
1061
1062    #[test]
1063    fn method_chain() {
1064        assert_eq!(say(r#"print("  HELLO  ".trim().lower())"#), "hello");
1065    }
1066
1067    // ─── Error diagnostics (phase 8 polish) ──────────────────────
1068
1069    #[test]
1070    fn parse_error_carries_column_info() {
1071        // "let 42" is a parse error — the int `42` shows up
1072        // where a name was expected. Column should point at the
1073        // start of the `42` token (column 5).
1074        let err = parse_err_full("let 42");
1075        assert_eq!(err.line, Some(1), "err: {:?}", err);
1076        assert_eq!(err.column, Some(5), "err: {:?}", err);
1077        assert!(err.message.contains("Expected a name"));
1078    }
1079
1080    #[test]
1081    fn parse_error_renders_with_snippet_and_carat() {
1082        let src = "let 42";
1083        let err = parse_err_full(src);
1084        let rendered = err.render(src);
1085        assert!(rendered.contains("--> line 1:5"), "rendered:\n{}", rendered);
1086        assert!(rendered.contains("let 42"));
1087        // Four spaces for `let ` before the carat at col 5.
1088        assert!(rendered.contains("    ^"), "rendered:\n{}", rendered);
1089    }
1090
1091    #[test]
1092    fn parse_error_on_line_2_points_at_line_2() {
1093        let src = "let x = 1\nlet = 2";
1094        let err = parse_err_full(src);
1095        assert_eq!(err.line, Some(2), "err: {:?}", err);
1096        let rendered = err.render(src);
1097        assert!(
1098            rendered.contains("let = 2"),
1099            "rendered:\n{}",
1100            rendered
1101        );
1102    }
1103
1104    #[test]
1105    fn runtime_error_renders_without_column() {
1106        // Runtime errors don't currently carry column; the
1107        // renderer should still produce a readable snippet.
1108        let src = "let x = 1 / 0";
1109        let err = run_err_full(src);
1110        assert_eq!(err.line, Some(1));
1111        assert!(err.column.is_none());
1112        let rendered = err.render(src);
1113        assert!(rendered.contains("--> line 1"));
1114        assert!(rendered.contains("let x = 1 / 0"));
1115        // No carat line (column unknown).
1116        assert!(!rendered.contains("^"), "rendered:\n{}", rendered);
1117    }
1118
1119    /// Like `parse_err` but returns the full `BopError` so
1120    /// tests can inspect line / column fields directly.
1121    fn parse_err_full(code: &str) -> BopError {
1122        parse(code).unwrap_err()
1123    }
1124
1125    /// Like `run_err` but returns the full `BopError`.
1126    fn run_err_full(code: &str) -> BopError {
1127        let mut host = TestHost::new();
1128        run(code, &mut host, &test_limits()).unwrap_err()
1129    }
1130
1131    // ─── "Did you mean?" suggestions ─────────────────────────────
1132
1133    #[test]
1134    fn typo_variable_suggests_closest_local() {
1135        let err = run_err_full(
1136            r#"let length = 5
1137print(lenght)"#,
1138        );
1139        assert!(err.message.contains("not found"), "err: {:?}", err);
1140        assert_eq!(
1141            err.friendly_hint.as_deref(),
1142            Some("Did you mean `length`?")
1143        );
1144    }
1145
1146    #[test]
1147    fn typo_variable_falls_back_when_nothing_close() {
1148        // No similar name in scope → keeps the generic hint
1149        // rather than suggesting something wild.
1150        let err = run_err_full("print(xylophone_constant)");
1151        assert_eq!(
1152            err.friendly_hint.as_deref(),
1153            Some("Did you forget to create it with `let`?")
1154        );
1155    }
1156
1157    #[test]
1158    fn typo_function_suggests_user_fn() {
1159        let err = run_err_full(
1160            r#"fn greet(name) { print("hi " + name) }
1161gret("world")"#,
1162        );
1163        assert!(err.message.contains("not found"));
1164        assert_eq!(
1165            err.friendly_hint.as_deref(),
1166            Some("Did you mean `greet`?")
1167        );
1168    }
1169
1170    #[test]
1171    fn typo_builtin_suggests_core_name() {
1172        // `rang(5)` → core builtin `range`.
1173        let err = run_err_full("rang(5)");
1174        assert_eq!(
1175            err.friendly_hint.as_deref(),
1176            Some("Did you mean `range`?")
1177        );
1178    }
1179
1180    #[test]
1181    fn typo_struct_field_at_access_suggests_declared() {
1182        let err = run_err_full(
1183            r#"struct Point { x, y }
1184let p = Point { x: 1, y: 2 }
1185print(p.z)"#,
1186        );
1187        assert!(err.message.contains("has no field `z`"));
1188        // Both `x` and `y` are within 1 edit of `z`; the
1189        // candidate order in `s.fields()` is declaration order,
1190        // so `x` wins the tie.
1191        assert_eq!(
1192            err.friendly_hint.as_deref(),
1193            Some("Did you mean `x`?")
1194        );
1195    }
1196
1197    #[test]
1198    fn typo_struct_field_at_construction_suggests_declared() {
1199        let err = run_err_full(
1200            r#"struct Point { x, y }
1201let p = Point { x: 1, ya: 2 }"#,
1202        );
1203        assert!(err.message.contains("has no field `ya`"));
1204        assert_eq!(
1205            err.friendly_hint.as_deref(),
1206            Some("Did you mean `y`?")
1207        );
1208    }
1209
1210    #[test]
1211    fn typo_enum_variant_suggests_declared() {
1212        let err = run_err_full(
1213            r#"enum Shape { Circle(r), Rectangle { w, h } }
1214let s = Shape::Circel(5)"#,
1215        );
1216        assert!(err.message.contains("has no variant `Circel`"));
1217        assert_eq!(
1218            err.friendly_hint.as_deref(),
1219            Some("Did you mean `Circle`?")
1220        );
1221    }
1222
1223    #[test]
1224    fn typo_hint_renders_in_source_snippet() {
1225        let src = r#"let length = 5
1226print(lenght)"#;
1227        let err = run_err_full(src);
1228        let rendered = err.render(src);
1229        assert!(
1230            rendered.contains("hint: Did you mean `length`?"),
1231            "rendered:\n{}",
1232            rendered
1233        );
1234    }
1235
1236    #[test]
1237    fn comments_in_code() {
1238        // `//` is the line-comment marker. There's no
1239        // integer-division operator — `/` always returns a
1240        // `Number`, and users who want an integer cast through
1241        // `(a / b).to_int()` — which frees `//` for comments.
1242        assert_eq!(
1243            say(r#"// this is a comment
1244let x = 42 // inline comment
1245print(x)"#),
1246            "42"
1247        );
1248    }
1249
1250    // ─── Instruction counting ─────────────────────────────────────
1251
1252    fn count(code: &str) -> u32 {
1253        let stmts = parse(code).unwrap();
1254        count_instructions(&stmts)
1255    }
1256
1257    #[test]
1258    fn count_simple_calls() {
1259        assert_eq!(count("print(1)"), 1);
1260        assert_eq!(count("print(1); print(2); print(3)"), 3);
1261    }
1262
1263    #[test]
1264    fn count_repeat() {
1265        assert_eq!(count("repeat 7 { print(1) }"), 2);
1266    }
1267
1268    #[test]
1269    fn count_if() {
1270        assert_eq!(count("if true { print(1) }"), 2);
1271        assert_eq!(count("if true { print(1) } else { print(2) }"), 3);
1272    }
1273
1274    #[test]
1275    fn count_while() {
1276        assert_eq!(count("while true { print(1) }"), 2);
1277    }
1278
1279    #[test]
1280    fn count_fn_skips_body() {
1281        assert_eq!(count("fn go() { print(1); print(2); print(3) }\ngo()"), 2);
1282    }
1283
1284    #[test]
1285    fn count_format_independent() {
1286        let one_line = count("repeat 7 { print(1) }");
1287        let multi_line = count("repeat 7 {\n    print(1)\n}");
1288        assert_eq!(one_line, multi_line);
1289        assert_eq!(one_line, 2);
1290    }
1291
1292    #[test]
1293    fn count_nested() {
1294        assert_eq!(count("repeat 7 { if true { print(1) } }"), 3);
1295    }
1296
1297    #[test]
1298    fn count_empty_program() {
1299        assert_eq!(count(""), 0);
1300    }
1301
1302    // ─── Scope / block isolation ───────────────────────────────────
1303
1304    #[test]
1305    fn if_block_scope() {
1306        assert!(
1307            run_err(
1308                r#"if true { let inner = 1 }
1309print(inner)"#
1310            )
1311            .contains("not found")
1312        );
1313    }
1314
1315    #[test]
1316    fn for_loop_var_scoped() {
1317        assert!(
1318            run_err(
1319                r#"for item in [1, 2] { let x = item }
1320print(item)"#
1321            )
1322            .contains("not found")
1323        );
1324    }
1325
1326    // ─── Complex programs ──────────────────────────────────────────
1327
1328    #[test]
1329    fn fizzbuzz() {
1330        assert_eq!(
1331            say(r#"let result = []
1332for i in range(1, 16) {
1333    if i % 15 == 0 {
1334        result.push("FizzBuzz")
1335    } else if i % 3 == 0 {
1336        result.push("Fizz")
1337    } else if i % 5 == 0 {
1338        result.push("Buzz")
1339    } else {
1340        result.push(i.to_str())
1341    }
1342}
1343print(result.join(", "))"#),
1344            "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz"
1345        );
1346    }
1347
1348    #[test]
1349    fn nested_function_calls() {
1350        assert_eq!(
1351            say(r#"fn square(n) { return n * n }
1352fn sum_squares(a, b) { return square(a) + square(b) }
1353print(sum_squares(3, 4))"#),
1354            "25"
1355        );
1356    }
1357
1358    #[test]
1359    fn array_manipulation_program() {
1360        assert_eq!(
1361            say(r#"let data = [5, 2, 8, 1, 9, 3]
1362data.sort()
1363let top3 = data.slice(3, 6)
1364print(top3.join(", "))"#),
1365            "5, 8, 9"
1366        );
1367    }
1368
1369    // ─── Truthiness ────────────────────────────────────────────────
1370
1371    #[test]
1372    fn truthy_values() {
1373        assert_eq!(say("print(if 1 { \"yes\" } else { \"no\" })"), "yes");
1374        assert_eq!(say(r#"print(if "x" { "yes" } else { "no" })"#), "yes");
1375        assert_eq!(say("print(if [1] { \"yes\" } else { \"no\" })"), "yes");
1376    }
1377
1378    #[test]
1379    fn falsy_values() {
1380        assert_eq!(say("print(if 0 { \"yes\" } else { \"no\" })"), "no");
1381        assert_eq!(say("print(if false { \"yes\" } else { \"no\" })"), "no");
1382        assert_eq!(say("print(if none { \"yes\" } else { \"no\" })"), "no");
1383        assert_eq!(say(r#"print(if "" { "yes" } else { "no" })"#), "no");
1384    }
1385
1386    // ─── Number display ────────────────────────────────────────────
1387
1388    #[test]
1389    fn display_whole_number_as_int() {
1390        assert_eq!(say("print(5.0)"), "5");
1391    }
1392
1393    #[test]
1394    fn display_float_with_decimals() {
1395        assert_eq!(say("print(3.14)"), "3.14");
1396    }
1397
1398    // ─── Safety / resource-limit tests ──────────────────────────────
1399
1400    #[test]
1401    fn safety_infinite_loop_halts() {
1402        let msg = run_err_with_limits("while true { }", tight_limits());
1403        assert!(msg.contains("too many steps"), "got: {}", msg);
1404    }
1405
1406    #[test]
1407    fn safety_memory_bomb_string_doubling() {
1408        let msg = run_err_with_limits(
1409            r#"let s = "aaaaaaaaaa"
1410repeat 100 { s = s + s }"#,
1411            tight_limits(),
1412        );
1413        assert!(msg.contains("Memory limit"), "got: {}", msg);
1414    }
1415
1416    #[test]
1417    fn safety_memory_bomb_array_growth() {
1418        let msg = run_err_with_limits(
1419            r#"let arr = []
1420repeat 500 {
1421    arr.push("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
1422}"#,
1423            tight_limits(),
1424        );
1425        assert!(
1426            msg.contains("Memory limit") || msg.contains("too many steps"),
1427            "got: {}", msg
1428        );
1429    }
1430
1431    #[test]
1432    fn safety_deep_recursion_halts() {
1433        // Run the recursing program on a worker thread with a
1434        // generous stack budget. Debug builds grow each walker
1435        // frame enough that the default ~2 MiB stack can abort
1436        // (SIGABRT) before the engine's 64-frame call-depth cap
1437        // fires — which would look like a crash rather than the
1438        // clean "too many nested function calls" the sandbox
1439        // promises. 8 MiB comfortably fits `MAX_CALL_DEPTH`
1440        // walker frames with debug frame bloat, and release
1441        // builds never needed the headroom.
1442        let handle = std::thread::Builder::new()
1443            .stack_size(8 * 1024 * 1024)
1444            .spawn(|| run_err_with_limits("fn f() { f() }\nf()", tight_limits()))
1445            .expect("spawn recursion test thread");
1446        let msg = handle.join().expect("recursion test thread panicked");
1447        assert!(
1448            msg.contains("nested function calls") || msg.contains("recursion"),
1449            "got: {}", msg
1450        );
1451    }
1452
1453    #[test]
1454    fn safety_deep_parse_nesting() {
1455        let code = "(".repeat(200) + "1" + &")".repeat(200);
1456        let msg = parse(&code).unwrap_err().message;
1457        assert!(msg.contains("nested too deeply"), "got: {}", msg);
1458    }
1459
1460    #[test]
1461    fn safety_string_repeat_bomb() {
1462        let msg = run_err_with_limits(r#"let s = "x" * 999999"#, tight_limits());
1463        assert!(msg.contains("Memory limit"), "got: {}", msg);
1464    }
1465
1466    #[test]
1467    fn safety_string_concat_bomb() {
1468        let msg = run_err_with_limits(
1469            r#"let s = "x" * 1000
1470repeat 100 { s = s + s }"#,
1471            tight_limits(),
1472        );
1473        assert!(msg.contains("Memory limit"), "got: {}", msg);
1474    }
1475
1476    #[test]
1477    fn safety_array_concat_bomb() {
1478        let msg = run_err_with_limits(
1479            r#"let a = range(100)
1480repeat 50 { a = a + a }"#,
1481            tight_limits(),
1482        );
1483        assert!(
1484            msg.contains("Memory limit") || msg.contains("too many steps"),
1485            "got: {}", msg
1486        );
1487    }
1488
1489    #[test]
1490    fn safety_for_in_large_string() {
1491        let msg = run_err_with_limits(
1492            r#"let s = "x" * 10000
1493for c in s { }"#,
1494            tight_limits(),
1495        );
1496        assert!(
1497            msg.contains("too many steps") || msg.contains("Memory limit"),
1498            "got: {}", msg
1499        );
1500    }
1501
1502    #[test]
1503    fn safety_demo_limits_step_bound() {
1504        let msg = run_err_with_limits(
1505            "let i = 0\nwhile true { i = i + 1 }",
1506            BopLimits::demo(),
1507        );
1508        assert!(msg.contains("too many steps"), "got: {}", msg);
1509    }
1510
1511    #[test]
1512    fn safety_demo_limits_memory_bound() {
1513        let msg = run_err_with_limits(
1514            r#"let s = "x" * 1100000
1515print(s)"#,
1516            BopLimits::demo(),
1517        );
1518        assert!(msg.contains("Memory limit"), "got: {}", msg);
1519    }
1520
1521    #[test]
1522    fn safety_nested_loop_step_bound() {
1523        let msg = run_err_with_limits("repeat 100 { repeat 100 { let x = 1 } }", tight_limits());
1524        assert!(msg.contains("too many steps"), "got: {}", msg);
1525    }
1526
1527    #[test]
1528    fn safety_string_split_bomb() {
1529        let msg = run_err_with_limits(
1530            r#"let s = "abababababab" * 2000
1531let parts = s.split("a")
1532let x = 1"#,
1533            tight_limits(),
1534        );
1535        assert!(
1536            msg.contains("Memory limit") || msg.contains("too many steps"),
1537            "got: {}", msg
1538        );
1539    }
1540
1541    #[test]
1542    fn safety_join_bomb() {
1543        let msg = run_err_with_limits(
1544            r#"let a = []
1545repeat 400 { a.push("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }
1546let s = a.join("")
1547let x = 1"#,
1548            tight_limits(),
1549        );
1550        assert!(
1551            msg.contains("Memory limit") || msg.contains("too many steps"),
1552            "got: {}", msg
1553        );
1554    }
1555
1556    #[test]
1557    fn safety_range_hard_cap() {
1558        let msg = run_err_with_limits(
1559            r#"let a = range(100000)
1560let x = 1"#,
1561            tight_limits(),
1562        );
1563        assert!(
1564            msg.contains("Memory limit") || msg.contains("too many steps"),
1565            "got: {}", msg
1566        );
1567    }
1568
1569    #[test]
1570    fn safety_array_method_doubling() {
1571        let msg = run_err_with_limits(
1572            r#"let a = []
1573repeat 400 { a.push("aaaaaaaaaaaaaaaaaaaaaa") }
1574a.reverse()
1575let x = 1"#,
1576            tight_limits(),
1577        );
1578        assert!(
1579            msg.contains("Memory limit") || msg.contains("too many steps"),
1580            "got: {}", msg
1581        );
1582    }
1583
1584    #[test]
1585    fn safety_preflight_catches() {
1586        let limits = BopLimits {
1587            max_steps: 500,
1588            max_memory: 32 * 1024,
1589        };
1590        let msg = run_err_with_limits(r#"let s = "x" * 40000"#, limits);
1591        assert!(msg.contains("Memory limit"), "got: {}", msg);
1592    }
1593
1594    #[test]
1595    fn safety_bounded_overshoot() {
1596        let limits = BopLimits {
1597            max_steps: 500,
1598            max_memory: 64 * 1024,
1599        };
1600        let mut host = TestHost::new();
1601        let result = run(
1602            r#"let s = "abababab" * 1000
1603let parts = s.split("a")"#,
1604            &mut host,
1605            &limits,
1606        );
1607        assert!(result.is_ok(), "Expected success (bounded overshoot), got error");
1608    }
1609
1610    #[test]
1611    fn safety_dict_growth_tracked() {
1612        let msg = run_err_with_limits(
1613            r#"let d = {}
1614repeat 400 {
1615    d[d.len().to_str()] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1616}
1617let x = 1"#,
1618            tight_limits(),
1619        );
1620        assert!(
1621            msg.contains("Memory limit") || msg.contains("too many steps"),
1622            "got: {}", msg
1623        );
1624    }
1625
1626    // ─── BopHost extension ─────────────────────────────────────────
1627
1628    struct CustomHost {
1629        prints: Vec<String>,
1630    }
1631
1632    impl BopHost for CustomHost {
1633        fn call(&mut self, name: &str, args: &[Value], line: u32) -> Option<Result<Value, BopError>> {
1634            match name {
1635                "greet" => {
1636                    if args.len() != 1 {
1637                        return Some(Err(BopError {
1638                            line: Some(line),
1639                            column: None,
1640                            message: "greet() needs 1 argument".into(),
1641                            friendly_hint: None,
1642                            is_fatal: false,
1643                            is_try_return: false,
1644                        }));
1645                    }
1646                    Some(Ok(Value::new_str(format!("Hello, {}!", args[0]))))
1647                }
1648                _ => None,
1649            }
1650        }
1651
1652        fn on_print(&mut self, message: &str) {
1653            self.prints.push(message.to_string());
1654        }
1655
1656        fn function_hint(&self) -> &str {
1657            "Available: greet(name)"
1658        }
1659    }
1660
1661    #[test]
1662    fn host_custom_builtin() {
1663        let mut host = CustomHost { prints: vec![] };
1664        run(r#"print(greet("world"))"#, &mut host, &BopLimits::standard()).unwrap();
1665        assert_eq!(host.prints, vec!["Hello, world!"]);
1666    }
1667
1668    #[test]
1669    fn host_function_hint() {
1670        let mut host = CustomHost { prints: vec![] };
1671        let err = run("unknown()", &mut host, &BopLimits::standard()).unwrap_err();
1672        assert!(err.message.contains("not found"));
1673    }
1674
1675    // ─── Pattern matching ─────────────────────────────────────────
1676
1677    #[test]
1678    fn match_literal_arms() {
1679        assert_eq!(
1680            say(r#"let x = 2
1681let out = match x {
1682    1 => "one",
1683    2 => "two",
1684    3 => "three",
1685    _ => "other",
1686}
1687print(out)"#),
1688            "two"
1689        );
1690    }
1691
1692    #[test]
1693    fn match_falls_through_to_wildcard() {
1694        assert_eq!(
1695            say(r#"let x = 42
1696print(match x {
1697    1 => "one",
1698    _ => "other",
1699})"#),
1700            "other"
1701        );
1702    }
1703
1704    #[test]
1705    fn match_no_arm_errors() {
1706        let err = run_err(r#"let x = 5
1707match x { 1 => "a", 2 => "b" }"#);
1708        assert!(err.contains("No match arm matched"), "got: {}", err);
1709    }
1710
1711    #[test]
1712    fn match_binding_captures_scrutinee() {
1713        assert_eq!(
1714            say(r#"print(match 42 {
1715    x => x + 1,
1716})"#),
1717            "43"
1718        );
1719    }
1720
1721    #[test]
1722    fn match_guard_accepts() {
1723        assert_eq!(
1724            say(r#"print(match 7 {
1725    n if n > 10 => "big",
1726    n if n > 0 => "small",
1727    _ => "zero or less",
1728})"#),
1729            "small"
1730        );
1731    }
1732
1733    #[test]
1734    fn match_guard_rejects_continues() {
1735        assert_eq!(
1736            say(r#"print(match 5 {
1737    n if n < 0 => "neg",
1738    n if n > 100 => "huge",
1739    _ => "mid",
1740})"#),
1741            "mid"
1742        );
1743    }
1744
1745    #[test]
1746    fn match_or_pattern() {
1747        assert_eq!(
1748            say(r#"let x = 3
1749print(match x {
1750    1 | 2 | 3 => "small",
1751    _ => "other",
1752})"#),
1753            "small"
1754        );
1755    }
1756
1757    #[test]
1758    fn match_enum_unit_variant() {
1759        assert_eq!(
1760            say(r#"enum E { A, B, C }
1761print(match E::B {
1762    E::A => "a",
1763    E::B => "b",
1764    E::C => "c",
1765})"#),
1766            "b"
1767        );
1768    }
1769
1770    #[test]
1771    fn match_enum_tuple_binds() {
1772        assert_eq!(
1773            say(r#"enum Shape { Circle(r), Square(s), Empty }
1774let s = Shape::Circle(5)
1775print(match s {
1776    Shape::Circle(r) => r * 2,
1777    Shape::Square(s) => s * s,
1778    Shape::Empty => 0,
1779})"#),
1780            "10"
1781        );
1782    }
1783
1784    #[test]
1785    fn match_enum_struct_variant_binds() {
1786        assert_eq!(
1787            say(r#"enum Shape { Rect { w, h }, Empty }
1788let r = Shape::Rect { w: 4, h: 3 }
1789print(match r {
1790    Shape::Rect { w, h } => w * h,
1791    Shape::Empty => 0,
1792})"#),
1793            "12"
1794        );
1795    }
1796
1797    #[test]
1798    fn match_struct_destructure() {
1799        assert_eq!(
1800            say(r#"struct Point { x, y }
1801let p = Point { x: 7, y: 3 }
1802print(match p {
1803    Point { x, y } => x + y,
1804})"#),
1805            "10"
1806        );
1807    }
1808
1809    #[test]
1810    fn match_struct_partial_with_rest() {
1811        // `Point { x, .. }` matches regardless of the other
1812        // fields. The walker's match_struct_fields looks up by
1813        // name; `rest` relaxes the "mention every field" rule.
1814        assert_eq!(
1815            say(r#"struct Triple { a, b, c }
1816let t = Triple { a: 1, b: 2, c: 3 }
1817print(match t {
1818    Triple { b, .. } => b,
1819})"#),
1820            "2"
1821        );
1822    }
1823
1824    #[test]
1825    fn match_nested_pattern() {
1826        // Classic Rust-style: Err(FileError::NotFound(path)).
1827        assert_eq!(
1828            say(r#"enum FileError { NotFound(path), Permission(path), Other }
1829enum Result { Ok(value), Err(error) }
1830let r = Result::Err(FileError::NotFound("/etc/passwd"))
1831print(match r {
1832    Result::Ok(v) => v,
1833    Result::Err(FileError::NotFound(p)) => p,
1834    Result::Err(FileError::Permission(p)) => p,
1835    Result::Err(FileError::Other) => "other",
1836})"#),
1837            "/etc/passwd"
1838        );
1839    }
1840
1841    #[test]
1842    fn match_array_exact() {
1843        assert_eq!(
1844            say(r#"let a = [1, 2, 3]
1845print(match a {
1846    [] => "empty",
1847    [x] => "one",
1848    [x, y] => "two",
1849    [x, y, z] => x + y + z,
1850    _ => "long",
1851})"#),
1852            "6"
1853        );
1854    }
1855
1856    #[test]
1857    fn match_array_with_rest() {
1858        assert_eq!(
1859            say(r#"let a = [10, 20, 30, 40, 50]
1860print(match a {
1861    [head, ..rest] => rest,
1862    _ => [],
1863})"#),
1864            "[20, 30, 40, 50]"
1865        );
1866    }
1867
1868    #[test]
1869    fn match_array_with_ignored_rest() {
1870        assert_eq!(
1871            say(r#"let a = [10, 20, 30]
1872print(match a {
1873    [first, ..] => first,
1874    _ => 0,
1875})"#),
1876            "10"
1877        );
1878    }
1879
1880    #[test]
1881    fn match_binding_scope_limited_to_arm() {
1882        assert!(
1883            run_err(r#"let v = 5
1884match v { x => print(x) }
1885print(x)"#)
1886                .contains("not found")
1887        );
1888    }
1889
1890    #[test]
1891    fn match_negative_literal() {
1892        assert_eq!(
1893            say(r#"print(match -3 {
1894    -3 => "neg three",
1895    _ => "other",
1896})"#),
1897            "neg three"
1898        );
1899    }
1900
1901    #[test]
1902    fn match_string_literal() {
1903        assert_eq!(
1904            say(r#"let s = "hello"
1905print(match s {
1906    "hi" => 1,
1907    "hello" => 2,
1908    _ => 0,
1909})"#),
1910            "2"
1911        );
1912    }
1913
1914    #[test]
1915    fn match_bool_none() {
1916        assert_eq!(
1917            say(r#"print(match true {
1918    true => "t",
1919    false => "f",
1920})"#),
1921            "t"
1922        );
1923        assert_eq!(
1924            say(r#"print(match none {
1925    none => "n",
1926    _ => "other",
1927})"#),
1928            "n"
1929        );
1930    }
1931
1932    // ─── `try` operator ────────────────────────────────────────────
1933
1934    #[test]
1935    fn try_unwraps_ok_variant() {
1936        assert_eq!(
1937            say(r#"enum Result { Ok(v), Err(e) }
1938fn doit() {
1939    let v = try Result::Ok(42)
1940    return v
1941}
1942print(doit())"#),
1943            "42"
1944        );
1945    }
1946
1947    #[test]
1948    fn try_propagates_err_variant() {
1949        // `try` on Err inside a fn causes the fn to return the
1950        // same Err variant unchanged. The caller matches it out.
1951        assert_eq!(
1952            say(r#"enum Result { Ok(v), Err(e) }
1953fn doit() {
1954    let v = try Result::Err("boom")
1955    return Result::Ok(v)
1956}
1957let r = doit()
1958print(match r {
1959    Result::Ok(v) => v,
1960    Result::Err(e) => e,
1961})"#),
1962            "boom"
1963        );
1964    }
1965
1966    #[test]
1967    fn try_sentinel_uses_flag_not_message_string() {
1968        // Regression for the tech-debt-2 refactor: the walker
1969        // used to check whether a `BopError`'s `.message`
1970        // equalled the string `"__bop_try_return_signal__"` to
1971        // know whether an error was a `try`-unwinding sentinel.
1972        // Now it checks `is_try_return` — a dedicated flag that
1973        // user-authored errors can't accidentally set.
1974        //
1975        // A user program whose code happens to produce that
1976        // exact message should NOT trigger try-return unwind;
1977        // it should propagate as a normal runtime error.
1978        // We can't construct that specific message from Bop
1979        // source directly (none of our error sites spell it),
1980        // but we can verify the flag-based design by pinning
1981        // the BopError the walker produces: a real runtime
1982        // failure has `is_try_return: false`, while a `try`-
1983        // triggered unwind internally carries `is_try_return:
1984        // true` but is caught at the fn boundary and never
1985        // reaches the caller.
1986        let mut host = TestHost::new();
1987        let err = run(
1988            "print(1 / 0)",
1989            &mut host,
1990            &test_limits(),
1991        )
1992        .unwrap_err();
1993        assert_eq!(err.message, "Division by zero");
1994        // A real error must not be classified as a try-return
1995        // — otherwise it'd silently get swallowed by any
1996        // enclosing `try_call`.
1997        assert!(!err.is_try_return, "got: {:?}", err);
1998        // And the fatal flag is independent of the try flag.
1999        assert!(!err.is_fatal, "got: {:?}", err);
2000    }
2001
2002    #[test]
2003    fn try_chains_through_nested_calls() {
2004        // An Err at the deepest fn short-circuits back up through
2005        // the whole chain, skipping each caller's remaining work.
2006        assert_eq!(
2007            say(r#"enum Result { Ok(v), Err(e) }
2008fn leaf() { return Result::Err("leaf-err") }
2009fn middle() {
2010    let v = try leaf()
2011    return Result::Ok(v + 1)
2012}
2013fn top() {
2014    let v = try middle()
2015    return Result::Ok(v * 2)
2016}
2017print(match top() {
2018    Result::Ok(v) => v,
2019    Result::Err(e) => e,
2020})"#),
2021            "leaf-err"
2022        );
2023    }
2024
2025    #[test]
2026    fn user_result_with_unit_ok_coexists_with_builtin() {
2027        // Module-scoped types: the program's own `enum Result
2028        // { Ok, Err(e) }` lives under `<root>.Result`, distinct
2029        // from the `<builtin>.Result` that `try_call` returns.
2030        // `try Result::Ok` resolves to the user's root-level
2031        // Result and unwraps the Unit-Ok variant to `none`.
2032        assert_eq!(
2033            say(r#"enum Result { Ok, Err(e) }
2034fn doit() {
2035    let v = try Result::Ok
2036    return v.type()
2037}
2038print(doit())"#),
2039            "none"
2040        );
2041    }
2042
2043    #[test]
2044    fn try_inside_lambda_returns_from_lambda_only() {
2045        // The lambda's own fn-boundary catches the `try` unwind;
2046        // the caller keeps running.
2047        assert_eq!(
2048            say(r#"enum Result { Ok(v), Err(e) }
2049let f = fn() {
2050    let v = try Result::Err("inner")
2051    return Result::Ok(v)
2052}
2053let r = f()
2054print("after lambda")
2055print(match r {
2056    Result::Ok(_) => "ok",
2057    Result::Err(e) => e,
2058})"#),
2059            "inner"
2060        );
2061    }
2062
2063    #[test]
2064    fn try_at_top_level_on_err_value_errors() {
2065        let msg = run_err(
2066            r#"enum Result { Ok(v), Err(e) }
2067let r = try Result::Err("boom")"#,
2068        );
2069        assert!(msg.contains("top-level"), "got: {}", msg);
2070    }
2071
2072    #[test]
2073    fn try_on_non_result_errors() {
2074        let msg = run_err(
2075            r#"fn doit() {
2076    let v = try 42
2077    return v
2078}
2079doit()"#,
2080        );
2081        assert!(msg.contains("Result-shaped"), "got: {}", msg);
2082    }
2083
2084    #[test]
2085    fn try_ok_tuple_wrong_arity_errors() {
2086        // Module-scoped types: the program's own two-field
2087        // `Ok(a, b)` is a valid enum declaration (lives under
2088        // `<root>.Result`), but `try` still enforces the
2089        // "Ok must carry exactly one value" rule it uses to
2090        // unwrap successful results. The check fires at the
2091        // `try` site, not at the type declaration.
2092        let msg = run_err(
2093            r#"enum Result { Ok(a, b), Err(e) }
2094fn doit() {
2095    let v = try Result::Ok(1, 2)
2096    return v
2097}
2098doit()"#,
2099        );
2100        assert!(
2101            msg.contains("Ok variant must carry exactly one"),
2102            "got: {}",
2103            msg
2104        );
2105    }
2106
2107    #[test]
2108    fn try_in_for_loop_short_circuits() {
2109        // `try` on the first Err ends the loop and the fn.
2110        assert_eq!(
2111            say(r#"enum Result { Ok(v), Err(e) }
2112fn lookup(i) {
2113    if i == 2 { return Result::Err("stop") }
2114    return Result::Ok(i * 10)
2115}
2116fn sum_until_err() {
2117    let total = 0
2118    for i in range(5) {
2119        let v = try lookup(i)
2120        total = total + v
2121    }
2122    return Result::Ok(total)
2123}
2124print(match sum_until_err() {
2125    Result::Ok(v) => v,
2126    Result::Err(e) => e,
2127})"#),
2128            "stop"
2129        );
2130    }
2131
2132    #[test]
2133    fn try_threaded_through_nested_fn_composition() {
2134        // Mirrors the "try lowers to match" equivalence: a fn
2135        // using `try` delivers the same outcome as a hand-
2136        // written match+return using the same Err short-circuit.
2137        assert_eq!(
2138            say(r#"enum Result { Ok(v), Err(e) }
2139fn compute(input) {
2140    if input < 0 { return Result::Err("negative") }
2141    return Result::Ok(input * 2)
2142}
2143fn with_try(x) {
2144    let doubled = try compute(x)
2145    return Result::Ok(doubled + 1)
2146}
2147print(match with_try(5) { Result::Ok(v) => v, Result::Err(_) => -1 })
2148print(match with_try(-1) { Result::Ok(_) => "ok", Result::Err(e) => e })"#),
2149            "negative"
2150        );
2151    }
2152
2153    // ─── `try_call` builtin ────────────────────────────────────────
2154
2155    #[test]
2156    fn try_call_wraps_successful_return_in_ok() {
2157        // Plain successful call: `try_call(f)` yields
2158        // `Result::Ok(return_value)`. The program doesn't need
2159        // to declare `Result` — the value comes out pre-shaped
2160        // because `try_call` constructs it directly.
2161        assert_eq!(
2162            say(r#"let r = try_call(fn() { return 42 })
2163print(match r {
2164    Result::Ok(v) => v,
2165    Result::Err(_) => -1,
2166})"#),
2167            "42"
2168        );
2169    }
2170
2171    #[test]
2172    fn try_call_wraps_non_fatal_error_in_err() {
2173        // Division by zero is a non-fatal runtime error, so
2174        // `try_call` catches it and yields
2175        // `Result::Err(RuntimeError { message, line })`.
2176        assert_eq!(
2177            say(r#"let r = try_call(fn() { return 1 / 0 })
2178print(match r {
2179    Result::Ok(_) => "ok",
2180    Result::Err(e) => e.message,
2181})"#),
2182            "Division by zero"
2183        );
2184    }
2185
2186    #[test]
2187    fn try_call_runtime_error_carries_line_number() {
2188        // The RuntimeError struct exposes `line` so callers can
2189        // report where the failure happened.
2190        assert_eq!(
2191            say(r#"let r = try_call(fn() {
2192    let x = 1
2193    return x / 0
2194})
2195print(match r {
2196    Result::Ok(_) => -1,
2197    Result::Err(e) => e.line,
2198})"#),
2199            "3"
2200        );
2201    }
2202
2203    #[test]
2204    fn try_call_step_limit_error_is_fatal_and_bypasses_wrap() {
2205        // The step-limit error is fatal — `try_call` must NOT
2206        // swallow it or the sandbox invariant breaks. The
2207        // outer `run()` sees the fatal error unchanged.
2208        let tight = BopLimits {
2209            max_steps: 200,
2210            max_memory: 1 << 20,
2211        };
2212        let mut host = TestHost::new();
2213        let err = run(
2214            r#"let r = try_call(fn() {
2215    while true { }
2216})
2217print("should never run")"#,
2218            &mut host,
2219            &tight,
2220        )
2221        .unwrap_err();
2222        assert!(err.is_fatal, "expected fatal: {}", err.message);
2223        assert!(
2224            err.message.contains("too many steps"),
2225            "got: {}",
2226            err.message
2227        );
2228        // The post-try_call `print` never ran because the
2229        // error short-circuited the program.
2230        assert!(host.prints.borrow().is_empty());
2231    }
2232
2233    #[test]
2234    fn try_call_plays_with_try_operator_to_chain_errors() {
2235        // Classic "convert caught runtime error into a Result".
2236        // The fn uses `try` to short-circuit on Err; the outer
2237        // wraps the whole thing in try_call to catch anything
2238        // it didn't anticipate.
2239        assert_eq!(
2240            say(r#"fn risky(x) {
2241    let arr = [1, 2]
2242    return arr[x]  // out-of-bounds when x > 1
2243}
2244let r = try_call(fn() { return risky(5) })
2245print(match r {
2246    Result::Ok(_) => "ok",
2247    Result::Err(e) => e.message,
2248})"#),
2249            "Index 5 is out of bounds (array has 2 items)"
2250        );
2251    }
2252
2253    #[test]
2254    fn try_call_errors_on_wrong_arg_count() {
2255        let msg = run_err("try_call()");
2256        assert!(
2257            msg.contains("try_call` expects 1"),
2258            "got: {}",
2259            msg
2260        );
2261    }
2262
2263    #[test]
2264    fn try_call_errors_on_non_function_arg() {
2265        let msg = run_err("try_call(42)");
2266        assert!(
2267            msg.contains("try_call` expects a function"),
2268            "got: {}",
2269            msg
2270        );
2271    }
2272
2273    #[test]
2274    fn try_call_result_ok_is_matchable_even_without_declared_type() {
2275        // The returned `Result::Ok(...)` carries a type_name
2276        // of `"Result"` and variant_name of `"Ok"` — the
2277        // pattern matcher uses string comparison, so the user's
2278        // pattern matches regardless of whether they declared
2279        // their own `Result` enum. Same for `RuntimeError`.
2280        assert_eq!(
2281            say(r#"let r = try_call(fn() { return "yay" })
2282print(match r {
2283    Result::Ok(v) => v + "!",
2284    Result::Err(_) => "bad",
2285})"#),
2286            "yay!"
2287        );
2288    }
2289
2290    #[test]
2291    fn try_call_nested_outer_sees_ok_of_inner_err() {
2292        // Inner try_call catches its own error and returns
2293        // `Result::Err(...)`. Outer try_call sees a clean
2294        // return and wraps THAT in `Result::Ok(...)`.
2295        assert_eq!(
2296            say(r#"let r = try_call(fn() {
2297    let inner = try_call(fn() { return 1 / 0 })
2298    return inner
2299})
2300print(match r {
2301    Result::Ok(Result::Err(e)) => e.message,
2302    Result::Ok(Result::Ok(_)) => "inner ok?",
2303    Result::Err(_) => "outer caught",
2304})"#),
2305            "Division by zero"
2306        );
2307    }
2308
2309    // ─── Integer type (phase 6) ────────────────────────────────────
2310
2311    #[test]
2312    fn int_literal_produces_int_value() {
2313        assert_eq!(say("print(42.type())"), "int");
2314        assert_eq!(say("print((-3).type())"), "int");
2315        assert_eq!(say("print(0.type())"), "int");
2316    }
2317
2318    #[test]
2319    fn float_literal_produces_number_value() {
2320        assert_eq!(say("print(42.0.type())"), "number");
2321        assert_eq!(say("print(3.14.type())"), "number");
2322        assert_eq!(say("print((-0.5).type())"), "number");
2323    }
2324
2325    #[test]
2326    fn int_int_arithmetic_stays_int() {
2327        assert_eq!(say("print((1 + 2).type())"), "int");
2328        assert_eq!(say("print(1 + 2)"), "3");
2329        assert_eq!(say("print(10 - 4)"), "6");
2330        assert_eq!(say("print(3 * 4)"), "12");
2331        assert_eq!(say("print(10 % 3)"), "1");
2332    }
2333
2334    #[test]
2335    fn division_slash_always_returns_number() {
2336        // `/` always produces a `Number`, even for `Int / Int`.
2337        // Sidesteps the "1 / 2 == 0" surprise every C-family
2338        // language inflicts on beginners.
2339        assert_eq!(say("print((10 / 3).type())"), "number");
2340        assert_eq!(say("print(10 / 4)"), "2.5");
2341        assert_eq!(say("print((10 / 5).type())"), "number");
2342    }
2343
2344    #[test]
2345    fn int_division_via_int_of_quotient() {
2346        // There's no dedicated `//` operator — users who want
2347        // integer division coerce the float result back with
2348        // `(...).to_int()`. Truncates toward zero, matching the
2349        // behaviour of the removed `//` operator.
2350        assert_eq!(say("print((10 / 3).to_int().type())"), "int");
2351        assert_eq!(say("print((10 / 3).to_int())"), "3");
2352        assert_eq!(say("print((-7 / 2).to_int())"), "-3");
2353        assert_eq!(say("print((10 / -3).to_int())"), "-3");
2354    }
2355
2356    #[test]
2357    fn int_number_mixed_widens_to_number() {
2358        assert_eq!(say("print((1 + 2.0).type())"), "number");
2359        assert_eq!(say("print(1 + 2.0)"), "3");
2360        assert_eq!(say("print(3 * 0.5)"), "1.5");
2361        assert_eq!(say("print((2.0 - 1).type())"), "number");
2362    }
2363
2364    #[test]
2365    fn int_comparison_uses_exact_integer_ordering() {
2366        assert_eq!(say("print(10 < 20)"), "true");
2367        assert_eq!(say("print(10 == 10)"), "true");
2368        // Cross-type numeric equality: int == number when
2369        // numerically equal.
2370        assert_eq!(say("print(1 == 1.0)"), "true");
2371        assert_eq!(say("print(2 > 1.5)"), "true");
2372    }
2373
2374    #[test]
2375    fn division_by_zero_errors() {
2376        let msg = run_err("print(10 / 0)");
2377        assert!(msg.contains("Division by zero"), "got: {}", msg);
2378    }
2379
2380    #[test]
2381    fn int_overflow_on_add_errors() {
2382        // i64::MAX + 1 overflows. The message should mention
2383        // "overflow".
2384        let msg = run_err("print(9223372036854775807 + 1)");
2385        assert!(msg.contains("Integer overflow"), "got: {}", msg);
2386    }
2387
2388    #[test]
2389    fn int_overflow_on_neg_of_i64_min_errors() {
2390        // `i64::MIN` can't be written as a literal (its magnitude
2391        // exceeds `i64::MAX`), so we build it arithmetically and
2392        // then negate — which overflows.
2393        let msg = run_err(
2394            "let x = -9223372036854775807 - 1\nprint(-x)",
2395        );
2396        assert!(msg.contains("overflow"), "got: {}", msg);
2397    }
2398
2399    #[test]
2400    fn int_builtin_converts_to_int() {
2401        assert_eq!(say("print(3.7.to_int())"), "3");
2402        assert_eq!(say("print(3.7.to_int().type())"), "int");
2403        assert_eq!(say(r#"print("42".to_int())"#), "42");
2404        assert_eq!(say(r#"print("42".to_int().type())"#), "int");
2405        // Truncating a string that looks float-y still works.
2406        assert_eq!(say(r#"print("3.7".to_int())"#), "3");
2407    }
2408
2409    #[test]
2410    fn float_builtin_converts_to_number() {
2411        assert_eq!(say("print(42.to_float())"), "42");
2412        assert_eq!(say("print(42.to_float().type())"), "number");
2413        assert_eq!(say(r#"print("3.14".to_float())"#), "3.14");
2414    }
2415
2416    #[test]
2417    fn len_returns_int() {
2418        assert_eq!(say(r#"print("hi".len().type())"#), "int");
2419        assert_eq!(say("print([1, 2, 3].len().type())"), "int");
2420    }
2421
2422    #[test]
2423    fn range_produces_int_elements() {
2424        assert_eq!(say("print((range(3)[0]).type())"), "int");
2425    }
2426
2427    #[test]
2428    fn array_index_accepts_int_and_float() {
2429        // Both `arr[0]` (Int) and `arr[0.0]` (Number-via-cast)
2430        // should work — keeps legacy code with `0.0` running.
2431        assert_eq!(say("let a = [10, 20]\nprint(a[0])"), "10");
2432        assert_eq!(say("let a = [10, 20]\nprint(a[0.0])"), "10");
2433    }
2434
2435    #[test]
2436    fn int_match_literal_pattern() {
2437        assert_eq!(
2438            say(r#"let x = 2
2439print(match x {
2440    1 => "one",
2441    2 => "two",
2442    _ => "other",
2443})"#),
2444            "two"
2445        );
2446    }
2447
2448    #[test]
2449    fn repeat_accepts_int() {
2450        assert_eq!(
2451            say(r#"let n = 0
2452repeat 5 { n = n + 1 }
2453print(n)"#),
2454            "5"
2455        );
2456    }
2457
2458    #[test]
2459    fn int_overflow_literal_parse_errors() {
2460        // Integer literal that doesn't fit in i64 is a
2461        // lex-time error, not a silent downgrade to float.
2462        let msg = parse_err("let x = 99999999999999999999");
2463        assert!(msg.contains("out of range"), "got: {}", msg);
2464    }
2465
2466    // ─── Modules / use ──────────────────────────────────────────
2467
2468    /// Host that resolves modules from an in-memory map keyed by
2469    /// the dot-joined use path. Captures prints and tracks how
2470    /// many times each module was resolved so we can pin the
2471    /// caching behaviour.
2472    struct ModuleHost {
2473        prints: RefCell<Vec<String>>,
2474        modules: std::collections::HashMap<String, String>,
2475        resolve_counts: RefCell<std::collections::HashMap<String, u32>>,
2476    }
2477
2478    impl ModuleHost {
2479        fn new(modules: &[(&str, &str)]) -> Self {
2480            let mut map = std::collections::HashMap::new();
2481            for (name, source) in modules {
2482                map.insert((*name).to_string(), (*source).to_string());
2483            }
2484            Self {
2485                prints: RefCell::new(Vec::new()),
2486                modules: map,
2487                resolve_counts: RefCell::new(std::collections::HashMap::new()),
2488            }
2489        }
2490
2491        fn prints(&self) -> Vec<String> {
2492            self.prints.borrow().clone()
2493        }
2494
2495        fn resolve_count(&self, name: &str) -> u32 {
2496            *self
2497                .resolve_counts
2498                .borrow()
2499                .get(name)
2500                .unwrap_or(&0)
2501        }
2502    }
2503
2504    impl BopHost for ModuleHost {
2505        fn call(
2506            &mut self,
2507            _name: &str,
2508            _args: &[Value],
2509            _line: u32,
2510        ) -> Option<Result<Value, BopError>> {
2511            None
2512        }
2513
2514        fn on_print(&mut self, message: &str) {
2515            self.prints.borrow_mut().push(message.to_string());
2516        }
2517
2518        fn resolve_module(&mut self, name: &str) -> Option<Result<String, BopError>> {
2519            *self
2520                .resolve_counts
2521                .borrow_mut()
2522                .entry(name.to_string())
2523                .or_insert(0) += 1;
2524            self.modules.get(name).cloned().map(Ok)
2525        }
2526    }
2527
2528    #[test]
2529    fn import_brings_let_binding_into_scope() {
2530        let mut host = ModuleHost::new(&[("math", "let pi = 3")]);
2531        run(
2532            r#"use math
2533print(pi)"#,
2534            &mut host,
2535            &BopLimits::standard(),
2536        )
2537        .unwrap();
2538        assert_eq!(host.prints(), vec!["3"]);
2539    }
2540
2541    #[test]
2542    fn import_brings_fn_into_scope() {
2543        let mut host = ModuleHost::new(&[(
2544            "math",
2545            r#"fn square(n) { return n * n }
2546let pi = 3"#,
2547        )]);
2548        run(
2549            r#"use math
2550print(square(5))
2551print(pi)"#,
2552            &mut host,
2553            &BopLimits::standard(),
2554        )
2555        .unwrap();
2556        assert_eq!(host.prints(), vec!["25", "3"]);
2557    }
2558
2559    #[test]
2560    fn import_dotted_path_passes_through_to_host() {
2561        let mut host = ModuleHost::new(&[("std.math", "let e = 2")]);
2562        run(
2563            r#"use std.math
2564print(e)"#,
2565            &mut host,
2566            &BopLimits::standard(),
2567        )
2568        .unwrap();
2569        assert_eq!(host.prints(), vec!["2"]);
2570        // Exactly one resolve — `std.math` is the full key.
2571        assert_eq!(host.resolve_count("std.math"), 1);
2572    }
2573
2574    #[test]
2575    fn import_module_not_found_errors() {
2576        let mut host = ModuleHost::new(&[]);
2577        let err = run("use nope", &mut host, &BopLimits::standard())
2578            .unwrap_err();
2579        assert!(
2580            err.message.contains("Module `nope` not found"),
2581            "got: {}",
2582            err.message
2583        );
2584    }
2585
2586    #[test]
2587    fn import_cache_resolves_once() {
2588        // Two imports of the same module in the same run should
2589        // only hit the resolver once.
2590        let mut host = ModuleHost::new(&[("m", "let x = 1")]);
2591        run(
2592            r#"use m
2593use m
2594print(x)"#,
2595            &mut host,
2596            &BopLimits::standard(),
2597        )
2598        .unwrap();
2599        assert_eq!(host.prints(), vec!["1"]);
2600        assert_eq!(host.resolve_count("m"), 1);
2601    }
2602
2603    #[test]
2604    fn import_module_can_import_other_modules() {
2605        let mut host = ModuleHost::new(&[
2606            ("a", "use b\nlet doubled_pi = pi + pi"),
2607            ("b", "let pi = 3"),
2608        ]);
2609        run(
2610            r#"use a
2611print(doubled_pi)"#,
2612            &mut host,
2613            &BopLimits::standard(),
2614        )
2615        .unwrap();
2616        assert_eq!(host.prints(), vec!["6"]);
2617    }
2618
2619    #[test]
2620    fn import_circular_detected() {
2621        let mut host = ModuleHost::new(&[
2622            ("a", "use b\nlet x = 1"),
2623            ("b", "use a\nlet y = 2"),
2624        ]);
2625        let err = run("use a", &mut host, &BopLimits::standard())
2626            .unwrap_err();
2627        assert!(
2628            err.message.contains("Circular import"),
2629            "got: {}",
2630            err.message
2631        );
2632    }
2633
2634    #[test]
2635    fn glob_use_shadowing_is_a_warning_first_wins() {
2636        // Under the new semantics, glob imports emit a warning
2637        // (rather than an error) when a name they'd bring in is
2638        // already bound. The first definition wins and the
2639        // program continues. A user who genuinely wants the
2640        // imported value can use the selective or aliased form
2641        // to opt in explicitly.
2642        let mut host = ModuleHost::new(&[("m", "let x = 99")]);
2643        run(
2644            r#"let x = 1
2645use m
2646print(x)"#,
2647            &mut host,
2648            &BopLimits::standard(),
2649        )
2650        .expect("run ok");
2651        assert_eq!(host.prints(), vec!["1".to_string()]);
2652    }
2653
2654    #[test]
2655    fn use_selective_form_pulls_only_listed_names() {
2656        let mut host = ModuleHost::new(&[("m", "let a = 1\nlet b = 2\nlet c = 3")]);
2657        run(
2658            r#"use m.{a, c}
2659print(a)
2660print(c)"#,
2661            &mut host,
2662            &BopLimits::standard(),
2663        )
2664        .expect("run ok");
2665        assert_eq!(host.prints(), vec!["1".to_string(), "3".to_string()]);
2666    }
2667
2668    #[test]
2669    fn use_selective_unknown_name_errors() {
2670        let mut host = ModuleHost::new(&[("m", "let a = 1")]);
2671        let err = run(
2672            r#"use m.{b}"#,
2673            &mut host,
2674            &BopLimits::standard(),
2675        )
2676        .unwrap_err();
2677        assert!(
2678            err.message.contains("isn't exported"),
2679            "got: {}",
2680            err.message
2681        );
2682    }
2683
2684    #[test]
2685    fn use_alias_binds_module_value() {
2686        let mut host = ModuleHost::new(&[(
2687            "m",
2688            "let pi = 3\nfn double(n) { return n + n }",
2689        )]);
2690        run(
2691            r#"use m as m
2692print(m.pi)
2693print(m.double(7))"#,
2694            &mut host,
2695            &BopLimits::standard(),
2696        )
2697        .expect("run ok");
2698        assert_eq!(host.prints(), vec!["3".to_string(), "14".to_string()]);
2699    }
2700
2701    #[test]
2702    fn use_alias_selective_form() {
2703        let mut host = ModuleHost::new(&[("m", "let a = 1\nlet b = 2\nlet c = 3")]);
2704        run(
2705            r#"use m.{a, c} as m
2706print(m.a)
2707print(m.c)"#,
2708            &mut host,
2709            &BopLimits::standard(),
2710        )
2711        .expect("run ok");
2712        assert_eq!(host.prints(), vec!["1".to_string(), "3".to_string()]);
2713    }
2714
2715    #[test]
2716    fn use_alias_rejects_missing_module_field() {
2717        let mut host = ModuleHost::new(&[("m", "let a = 1")]);
2718        let err = run(
2719            r#"use m as m
2720print(m.b)"#,
2721            &mut host,
2722            &BopLimits::standard(),
2723        )
2724        .unwrap_err();
2725        assert!(
2726            err.message.contains("isn't exported"),
2727            "got: {}",
2728            err.message
2729        );
2730    }
2731
2732    #[test]
2733    fn glob_skips_underscore_prefixed_exports() {
2734        // Privacy convention: glob import skips `_foo`, so the
2735        // importer can't see it unless they selectively opt in.
2736        let mut host = ModuleHost::new(&[("m", "let public = 1\nlet _private = 2")]);
2737        let err = run(
2738            r#"use m
2739print(_private)"#,
2740            &mut host,
2741            &BopLimits::standard(),
2742        )
2743        .unwrap_err();
2744        // The private binding didn't land in scope — we get a
2745        // normal "variable not found" error.
2746        assert!(
2747            err.message.contains("_private"),
2748            "expected `_private not found`, got: {}",
2749            err.message
2750        );
2751    }
2752
2753    #[test]
2754    fn selective_form_can_reach_underscore_prefixed_names() {
2755        // Explicit listing overrides the privacy skip — the user
2756        // said they want `_private` and got it.
2757        let mut host = ModuleHost::new(&[("m", "let _private = 42")]);
2758        run(
2759            r#"use m.{_private}
2760print(_private)"#,
2761            &mut host,
2762            &BopLimits::standard(),
2763        )
2764        .expect("run ok");
2765        assert_eq!(host.prints(), vec!["42".to_string()]);
2766    }
2767
2768    #[test]
2769    fn alias_exposes_underscore_prefixed_names() {
2770        // Alias form keeps everything — privacy was about not
2771        // polluting the caller's scope with glob. When the user
2772        // says `as m`, they accept the whole module surface.
2773        let mut host = ModuleHost::new(&[("m", "let _private = 7")]);
2774        run(
2775            r#"use m as m
2776print(m._private)"#,
2777            &mut host,
2778            &BopLimits::standard(),
2779        )
2780        .expect("run ok");
2781        assert_eq!(host.prints(), vec!["7".to_string()]);
2782    }
2783
2784    #[test]
2785    fn alias_namespaced_struct_literal() {
2786        let mut host = ModuleHost::new(&[(
2787            "g",
2788            "struct Entity { id, hp }\nfn spawn(id) { return Entity { id: id, hp: 100 } }",
2789        )]);
2790        run(
2791            r#"use g as g
2792let e = g.Entity { id: 1, hp: 50 }
2793print(e.id)
2794print(e.hp)"#,
2795            &mut host,
2796            &BopLimits::standard(),
2797        )
2798        .expect("run ok");
2799        assert_eq!(host.prints(), vec!["1".to_string(), "50".to_string()]);
2800    }
2801
2802    #[test]
2803    fn alias_namespaced_variant_ctor() {
2804        let mut host = ModuleHost::new(&[(
2805            "r",
2806            "enum Result { Ok(v), Err(e) }",
2807        )]);
2808        run(
2809            r#"use r as r
2810let v = r.Result::Ok(42)
2811match v {
2812    r.Result::Ok(n) => print(n),
2813    r.Result::Err(_) => print("err"),
2814}"#,
2815            &mut host,
2816            &BopLimits::standard(),
2817        )
2818        .expect("run ok");
2819        assert_eq!(host.prints(), vec!["42".to_string()]);
2820    }
2821
2822    #[test]
2823    fn alias_module_value_is_a_module_type() {
2824        let mut host = ModuleHost::new(&[("m", "let x = 1")]);
2825        run(
2826            r#"use m as mm
2827print(mm.type())"#,
2828            &mut host,
2829            &BopLimits::standard(),
2830        )
2831        .expect("run ok");
2832        assert_eq!(host.prints(), vec!["module".to_string()]);
2833    }
2834
2835    // ─── Structs ──────────────────────────────────────────────────
2836
2837    #[test]
2838    fn struct_decl_and_construct() {
2839        assert_eq!(
2840            say(r#"struct Point { x, y }
2841let p = Point { x: 3, y: 4 }
2842print(p.x)
2843print(p.y)"#),
2844            "4"
2845        );
2846    }
2847
2848    #[test]
2849    fn struct_display_shows_type_name_and_fields() {
2850        assert_eq!(
2851            say(r#"struct Point { x, y }
2852let p = Point { x: 3, y: 4 }
2853print(p)"#),
2854            "Point { x: 3, y: 4 }"
2855        );
2856    }
2857
2858    #[test]
2859    fn struct_fields_respect_declaration_order() {
2860        // Fields specified out of declaration order should still
2861        // appear in declaration order in the value — stable
2862        // ordering matters for `print` / `inspect` / equality.
2863        assert_eq!(
2864            say(r#"struct Point { x, y }
2865let p = Point { y: 4, x: 3 }
2866print(p)"#),
2867            "Point { x: 3, y: 4 }"
2868        );
2869    }
2870
2871    #[test]
2872    fn struct_equality_is_structural() {
2873        assert_eq!(
2874            say(r#"struct Point { x, y }
2875let a = Point { x: 1, y: 2 }
2876let b = Point { x: 1, y: 2 }
2877print(a == b)"#),
2878            "true"
2879        );
2880        assert_eq!(
2881            say(r#"struct Point { x, y }
2882let a = Point { x: 1, y: 2 }
2883let b = Point { x: 1, y: 3 }
2884print(a == b)"#),
2885            "false"
2886        );
2887    }
2888
2889    #[test]
2890    fn struct_different_types_never_equal() {
2891        assert_eq!(
2892            say(r#"struct A { x }
2893struct B { x }
2894let a = A { x: 1 }
2895let b = B { x: 1 }
2896print(a == b)"#),
2897            "false"
2898        );
2899    }
2900
2901    #[test]
2902    fn struct_type_name_is_struct() {
2903        // `type()` returns a generic bucket; a per-type name
2904        // would require `display_type_name()` which isn't wired
2905        // to the builtin yet.
2906        assert_eq!(
2907            say(r#"struct Foo { a }
2908print(Foo { a: 1 }.type())"#),
2909            "struct"
2910        );
2911    }
2912
2913    #[test]
2914    fn struct_missing_field_errors() {
2915        let err = run_err(r#"struct Point { x, y }
2916let p = Point { x: 1 }"#);
2917        assert!(err.contains("Missing field"), "got: {}", err);
2918    }
2919
2920    #[test]
2921    fn struct_extra_field_errors() {
2922        let err = run_err(r#"struct Point { x, y }
2923let p = Point { x: 1, y: 2, z: 3 }"#);
2924        assert!(err.contains("has no field"), "got: {}", err);
2925    }
2926
2927    #[test]
2928    fn struct_duplicate_field_errors() {
2929        let err = run_err(r#"struct Point { x, y }
2930let p = Point { x: 1, x: 2, y: 3 }"#);
2931        assert!(err.contains("specified twice"), "got: {}", err);
2932    }
2933
2934    #[test]
2935    fn struct_undeclared_type_errors() {
2936        let err = run_err(r#"let p = Nope { x: 1 }"#);
2937        assert!(err.contains("not declared"), "got: {}", err);
2938    }
2939
2940    #[test]
2941    fn struct_field_access_missing_errors() {
2942        let err = run_err(r#"struct Point { x, y }
2943let p = Point { x: 1, y: 2 }
2944print(p.z)"#);
2945        assert!(err.contains("no field"), "got: {}", err);
2946    }
2947
2948    #[test]
2949    fn struct_field_access_on_non_struct_errors() {
2950        let err = run_err("let x = 42\nprint(x.value)");
2951        assert!(err.contains("Can't read field"), "got: {}", err);
2952    }
2953
2954    #[test]
2955    fn struct_duplicate_decl_errors() {
2956        let err = run_err(r#"struct Foo { x }
2957struct Foo { y }"#);
2958        assert!(err.contains("already declared"), "got: {}", err);
2959    }
2960
2961    #[test]
2962    fn struct_nested() {
2963        assert_eq!(
2964            say(r#"struct Inner { v }
2965struct Outer { name, inner }
2966let o = Outer { name: "nest", inner: Inner { v: 42 } }
2967print(o.inner.v)"#),
2968            "42"
2969        );
2970    }
2971
2972    #[test]
2973    fn struct_in_array_and_iteration() {
2974        assert_eq!(
2975            say(r#"struct Item { name, qty }
2976let cart = [Item { name: "apple", qty: 3 }, Item { name: "banana", qty: 2 }]
2977let total = 0
2978for i in cart { total += i.qty }
2979print(total)"#),
2980            "5"
2981        );
2982    }
2983
2984    #[test]
2985    fn struct_literal_disallowed_in_if_condition_parses() {
2986        // `if Foo { body }` should parse as `if Foo` with body
2987        // `{ body }`, not as `if (Foo { body })`. Reading a
2988        // bare `Foo` ident that isn't bound fails at runtime
2989        // with "not found" — confirming the struct-literal
2990        // restriction held at parse.
2991        let err = run_err("if Foo { print(\"hi\") }");
2992        assert!(err.contains("not found"), "got: {}", err);
2993    }
2994
2995    #[test]
2996    fn struct_literal_disallowed_in_for_iterable() {
2997        // `for x in arr { body }` — without the struct-literal
2998        // restriction, `arr { body }` would try to parse as a
2999        // struct literal where `arr` is the type name and `body`
3000        // is a field. The restriction ensures the `{` belongs to
3001        // the for body.
3002        assert_eq!(
3003            say("let arr = [1, 2, 3]\nlet sum = 0\nfor x in arr { sum += x }\nprint(sum)"),
3004            "6"
3005        );
3006    }
3007
3008    #[test]
3009    fn struct_literal_ok_in_let_rhs() {
3010        // In a let rhs, struct literals are allowed. This
3011        // confirms the context flag is re-enabled outside
3012        // control-flow conditions.
3013        assert_eq!(
3014            say(r#"struct P { x }
3015let p = P { x: 7 }
3016print(p.x)"#),
3017            "7"
3018        );
3019    }
3020
3021    #[test]
3022    fn struct_field_assign_basic() {
3023        assert_eq!(
3024            say(r#"struct Point { x, y }
3025let p = Point { x: 1, y: 2 }
3026p.x = 99
3027print(p.x)
3028print(p.y)"#),
3029            "2"
3030        );
3031    }
3032
3033    #[test]
3034    fn struct_field_compound_assign() {
3035        assert_eq!(
3036            say(r#"struct Counter { n }
3037let c = Counter { n: 10 }
3038c.n += 5
3039c.n *= 2
3040print(c.n)"#),
3041            "30"
3042        );
3043    }
3044
3045    #[test]
3046    fn struct_field_assign_unknown_field_errors() {
3047        let err = run_err(r#"struct P { x }
3048let p = P { x: 1 }
3049p.y = 99"#);
3050        assert!(err.contains("no field"), "got: {}", err);
3051    }
3052
3053    #[test]
3054    fn struct_field_assign_on_non_struct_errors() {
3055        let err = run_err(r#"let x = 5
3056x.field = 1"#);
3057        assert!(err.contains("Can't assign to field"), "got: {}", err);
3058    }
3059
3060    #[test]
3061    fn struct_field_assign_chain_via_intermediate_var() {
3062        // `outer.inner.v = 99` isn't supported yet (needs nested
3063        // writeback). Users can re-build through intermediate
3064        // vars instead.
3065        assert_eq!(
3066            say(r#"struct Inner { v }
3067struct Outer { inner }
3068let o = Outer { inner: Inner { v: 1 } }
3069let i = o.inner
3070i.v = 99
3071o.inner = i
3072print(o.inner.v)"#),
3073            "99"
3074        );
3075    }
3076
3077    // ─── Enums ────────────────────────────────────────────────────
3078
3079    #[test]
3080    fn enum_unit_variant_basic() {
3081        assert_eq!(
3082            say(r#"enum Shape { Empty, Circle(r), Square(s) }
3083let s = Shape::Empty
3084print(s)"#),
3085            "Shape::Empty"
3086        );
3087    }
3088
3089    #[test]
3090    fn enum_tuple_variant() {
3091        assert_eq!(
3092            say(r#"enum Shape { Empty, Circle(r), Pair(x, y) }
3093let p = Shape::Pair(3, 4)
3094print(p)"#),
3095            "Shape::Pair(3, 4)"
3096        );
3097    }
3098
3099    #[test]
3100    fn enum_struct_variant() {
3101        assert_eq!(
3102            say(r#"enum Shape { Rectangle { width, height }, Empty }
3103let r = Shape::Rectangle { width: 4, height: 3 }
3104print(r)
3105print(r.width)
3106print(r.height)"#),
3107            "3"
3108        );
3109    }
3110
3111    #[test]
3112    fn enum_equality_same_variant() {
3113        assert_eq!(
3114            say(r#"enum E { A, B(x) }
3115print(E::A == E::A)
3116print(E::B(1) == E::B(1))
3117print(E::B(1) == E::B(2))
3118print(E::A == E::B(1))"#),
3119            "false"
3120        );
3121    }
3122
3123    #[test]
3124    fn enum_different_types_not_equal() {
3125        assert_eq!(
3126            say(r#"enum A { X }
3127enum B { X }
3128print(A::X == B::X)"#),
3129            "false"
3130        );
3131    }
3132
3133    #[test]
3134    fn enum_variant_mismatch_unit_given_args() {
3135        let err = run_err(r#"enum E { A }
3136let x = E::A(1)"#);
3137        assert!(err.contains("no payload"), "got: {}", err);
3138    }
3139
3140    #[test]
3141    fn enum_variant_mismatch_tuple_arity() {
3142        let err = run_err(r#"enum E { P(x, y) }
3143let p = E::P(1)"#);
3144        assert!(err.contains("expects 2 argument"), "got: {}", err);
3145    }
3146
3147    #[test]
3148    fn enum_variant_mismatch_struct_missing_field() {
3149        let err = run_err(r#"enum E { R { w, h } }
3150let r = E::R { w: 1 }"#);
3151        assert!(err.contains("Missing field"), "got: {}", err);
3152    }
3153
3154    #[test]
3155    fn enum_variant_mismatch_struct_extra_field() {
3156        let err = run_err(r#"enum E { R { w, h } }
3157let r = E::R { w: 1, h: 2, extra: 3 }"#);
3158        assert!(err.contains("no field"), "got: {}", err);
3159    }
3160
3161    #[test]
3162    fn enum_undeclared_variant_errors() {
3163        let err = run_err(r#"enum E { A }
3164let x = E::Z"#);
3165        assert!(err.contains("no variant"), "got: {}", err);
3166    }
3167
3168    #[test]
3169    fn enum_undeclared_type_errors() {
3170        let err = run_err("let x = Nope::V");
3171        assert!(err.contains("not declared"), "got: {}", err);
3172    }
3173
3174    #[test]
3175    fn enum_struct_variant_field_access() {
3176        assert_eq!(
3177            say(r#"enum Shape { Rect { w, h }, Empty }
3178let r = Shape::Rect { w: 10, h: 3 }
3179print(r.w * r.h)"#),
3180            "30"
3181        );
3182    }
3183
3184    #[test]
3185    fn enum_used_in_if_condition() {
3186        // The struct-literal disambiguation flag also covers
3187        // enum struct-variants — `if Foo::V { body }` must
3188        // parse `V` as a unit variant and `{ body }` as the
3189        // if's block.
3190        assert_eq!(
3191            say(r#"enum E { V }
3192if E::V == E::V {
3193    print("yes")
3194} else {
3195    print("no")
3196}"#),
3197            "yes"
3198        );
3199    }
3200
3201    #[test]
3202    fn enum_type_name_is_enum() {
3203        assert_eq!(
3204            say(r#"enum E { V }
3205print((E::V).type())"#),
3206            "enum"
3207        );
3208    }
3209
3210    #[test]
3211    fn enum_in_array_of_values() {
3212        assert_eq!(
3213            say(r#"enum Color { Red, Green, Blue }
3214let palette = [Color::Red, Color::Green, Color::Blue]
3215print(palette)"#),
3216            "[Color::Red, Color::Green, Color::Blue]"
3217        );
3218    }
3219
3220    #[test]
3221    fn enum_duplicate_decl_errors() {
3222        let err = run_err(r#"enum E { A }
3223enum E { B }"#);
3224        assert!(err.contains("already declared"), "got: {}", err);
3225    }
3226
3227    // ─── User-defined methods on structs + enums ──────────────────
3228
3229    #[test]
3230    fn method_on_struct_basic() {
3231        assert_eq!(
3232            say(r#"struct Point { x, y }
3233fn Point.sum(self) { return self.x + self.y }
3234let p = Point { x: 3, y: 4 }
3235print(p.sum())"#),
3236            "7"
3237        );
3238    }
3239
3240    #[test]
3241    fn method_with_extra_args() {
3242        assert_eq!(
3243            say(r#"struct Counter { n }
3244fn Counter.add(self, delta) { return Counter { n: self.n + delta } }
3245let c = Counter { n: 10 }
3246let c2 = c.add(5)
3247print(c2.n)
3248print(c.n)"#),
3249            "10"
3250        );
3251    }
3252
3253    #[test]
3254    fn method_does_not_mutate_receiver() {
3255        // Mutating `self` inside a method doesn't propagate —
3256        // Bop passes self by value like any other parameter.
3257        // Users who want mutation rebind the result.
3258        assert_eq!(
3259            say(r#"struct Counter { n }
3260fn Counter.bump(self) { self.n = self.n + 1 }
3261let c = Counter { n: 5 }
3262c.bump()
3263print(c.n)"#),
3264            "5"
3265        );
3266    }
3267
3268    #[test]
3269    fn method_on_enum_dispatches_on_type() {
3270        assert_eq!(
3271            say(r#"enum Shape { Circle(r), Rect { w, h }, Empty }
3272fn Shape.name(self) { return "shape" }
3273print(Shape::Circle(3).name())
3274print(Shape::Rect { w: 4, h: 3 }.name())
3275print(Shape::Empty.name())"#),
3276            "shape"
3277        );
3278    }
3279
3280    #[test]
3281    fn method_overrides_builtin() {
3282        // A user-defined method of the same name as a built-in
3283        // wins — matches the walker-level dispatch rule.
3284        assert_eq!(
3285            say(r#"struct Wrapper { data }
3286fn Wrapper.len(self) { return 99 }
3287let w = Wrapper { data: [1, 2, 3] }
3288print(w.len())"#),
3289            "99"
3290        );
3291    }
3292
3293    #[test]
3294    fn method_unknown_on_struct_errors() {
3295        let err = run_err(r#"struct P { x }
3296let p = P { x: 1 }
3297p.nope()"#);
3298        assert!(err.contains(".nope()"), "got: {}", err);
3299    }
3300
3301    #[test]
3302    fn method_wrong_arg_count_errors() {
3303        let err = run_err(r#"struct P { x }
3304fn P.set(self, v) { return P { x: v } }
3305let p = P { x: 1 }
3306p.set(1, 2)"#);
3307        assert!(err.contains("expects"), "got: {}", err);
3308    }
3309
3310    #[test]
3311    fn method_chain_user_defined() {
3312        assert_eq!(
3313            say(r#"struct Adder { n }
3314fn Adder.then(self, m) { return Adder { n: self.n + m } }
3315let result = Adder { n: 1 }.then(2).then(3).then(4)
3316print(result.n)"#),
3317            "10"
3318        );
3319    }
3320
3321    #[test]
3322    fn method_self_is_clone() {
3323        // `self` in the method is independent from the caller's
3324        // binding, even if the method returns self: structural
3325        // equality still holds on the returned clone.
3326        assert_eq!(
3327            say(r#"struct P { x }
3328fn P.identity(self) { return self }
3329let a = P { x: 7 }
3330let b = a.identity()
3331print(a == b)
3332print(b.x)"#),
3333            "7"
3334        );
3335    }
3336
3337    #[test]
3338    fn method_on_enum_reads_payload_field() {
3339        assert_eq!(
3340            say(r#"enum Shape { Circle(r), Rect { w, h } }
3341fn Shape.label(self, prefix) {
3342    return prefix + "-shape"
3343}
3344let c = Shape::Circle(5)
3345print(c.label("small"))"#),
3346            "small-shape"
3347        );
3348    }
3349
3350    #[test]
3351    fn enum_duplicate_variant_errors() {
3352        let err = run_err(r#"enum E { A, A }"#);
3353        assert!(err.contains("duplicate variant"), "got: {}", err);
3354    }
3355
3356    #[test]
3357    fn struct_empty() {
3358        assert_eq!(
3359            say(r#"struct Unit { }
3360let u = Unit { }
3361print(u)"#),
3362            "Unit {}"
3363        );
3364    }
3365
3366    #[test]
3367    fn import_module_does_not_see_importer_scope() {
3368        // `outer` is defined in the importer's scope; the module
3369        // must not be able to reach it.
3370        let mut host = ModuleHost::new(&[("m", "fn leak() { return outer }")]);
3371        let err = run(
3372            r#"let outer = 42
3373use m
3374print(leak())"#,
3375            &mut host,
3376            &BopLimits::standard(),
3377        )
3378        .unwrap_err();
3379        assert!(
3380            err.message.contains("outer"),
3381            "expected 'outer' not-found error, got: {}",
3382            err.message
3383        );
3384    }
3385
3386    // ─── Const declarations + case conventions ─────────────────────
3387    //
3388    // The parser enforces three naming buckets — values are
3389    // lowercase-first, types start uppercase, constants are
3390    // all-uppercase. These tests cover the new `const` keyword
3391    // and each of the rule-violation error paths.
3392
3393    #[test]
3394    fn const_declares_an_immutable_binding() {
3395        assert_eq!(say("const PI = 3\nprint(PI)"), "3");
3396    }
3397
3398    #[test]
3399    fn const_can_reference_another_const() {
3400        assert_eq!(
3401            say("const PI = 3\nconst DIAMETER = PI * 2\nprint(DIAMETER)"),
3402            "6"
3403        );
3404    }
3405
3406    #[test]
3407    fn const_reassignment_is_refused_at_parse_time() {
3408        let err = run_err("const PI = 3\nPI = 4");
3409        assert!(
3410            err.contains("can't reassign") && err.contains("constant"),
3411            "expected const-reassignment error, got: {err}"
3412        );
3413    }
3414
3415    #[test]
3416    fn let_name_must_start_lowercase() {
3417        let err = run_err("let Foo = 1");
3418        assert!(err.to_lowercase().contains("value"), "got: {err}");
3419    }
3420
3421    #[test]
3422    fn let_with_all_caps_suggests_const() {
3423        let err = run_err("let MAX = 1");
3424        assert!(
3425            err.contains("const"),
3426            "expected hint to suggest `const`, got: {err}"
3427        );
3428    }
3429
3430    #[test]
3431    fn struct_name_must_start_uppercase() {
3432        let err = run_err("struct entity { id }");
3433        assert!(err.to_lowercase().contains("type"), "got: {err}");
3434    }
3435
3436    #[test]
3437    fn enum_variants_start_uppercase() {
3438        let err = run_err("enum Event { spawn, damage }");
3439        assert!(err.to_lowercase().contains("type"), "got: {err}");
3440    }
3441
3442    #[test]
3443    fn enum_with_all_caps_variants_is_allowed() {
3444        // `enum Dir { N, E, S, W }` — short-acronym variants are
3445        // type-shape and pass the type-name check.
3446        assert_eq!(
3447            say("enum Dir { N, E, S, W }\nlet d = Dir::E\nprint(\"ok\")"),
3448            "ok"
3449        );
3450    }
3451
3452    #[test]
3453    fn fn_name_must_start_lowercase() {
3454        let err = run_err("fn Greet() { return 1 }");
3455        assert!(err.to_lowercase().contains("value"), "got: {err}");
3456    }
3457
3458    #[test]
3459    fn fn_params_must_start_lowercase() {
3460        let err = run_err("fn greet(Name) { return Name }");
3461        assert!(err.to_lowercase().contains("value"), "got: {err}");
3462    }
3463
3464    #[test]
3465    fn for_loop_var_must_start_lowercase() {
3466        let err = run_err("for I in range(3) { print(I) }");
3467        assert!(err.to_lowercase().contains("value"), "got: {err}");
3468    }
3469
3470    #[test]
3471    fn const_name_must_be_all_caps() {
3472        let err = run_err("const Pi = 3");
3473        assert!(
3474            err.to_lowercase().contains("constant"),
3475            "got: {err}"
3476        );
3477    }
3478
3479    #[test]
3480    fn underscore_prefix_is_allowed_for_all_buckets() {
3481        // Private-by-convention — classification is unchanged.
3482        assert_eq!(
3483            say(r#"
3484                let _hidden = 1
3485                const _DEBUG = true
3486                struct _Internal { _counter }
3487                let s = _Internal { _counter: _hidden }
3488                print(s._counter)
3489            "#),
3490            "1"
3491        );
3492    }
3493
3494    // ─── host helpers ────────────────────────────────────────────────
3495
3496    // ─── module-qualified type identity ──────────────────────────────
3497
3498    #[test]
3499    fn two_modules_can_declare_same_type_name_with_different_shapes() {
3500        // Phase 2b — module-qualified types. Two modules each
3501        // declare `enum Color { ... }` with *different* variant
3502        // sets; both are usable through aliases, and values
3503        // constructed from one never compare equal to values
3504        // from the other even when the variant names match.
3505        let mut host = crate::host::StringModuleHost::new([
3506            ("paint", "enum Color { Red, Blue }"),
3507            ("other", "enum Color { Red, Green, Yellow }"),
3508        ]);
3509        run(
3510            r#"use paint as p
3511use other as o
3512let a = p.Color::Red
3513let b = o.Color::Red
3514print(a == b)
3515print(a == a)
3516print(a)
3517print(b)"#,
3518            &mut host,
3519            &BopLimits::standard(),
3520        )
3521        .unwrap();
3522        // a and b are both `Color::Red` but from *different*
3523        // modules, so `==` is false. `a == a` is true (same
3524        // identity). Display keeps the bare type name in both
3525        // cases so surface rendering stays readable even when
3526        // the underlying identities differ.
3527        assert_eq!(host.output(), "false\ntrue\nColor::Red\nColor::Red");
3528    }
3529
3530    #[test]
3531    fn namespaced_pattern_matches_only_same_module_value() {
3532        // Patterns resolve their type reference through the
3533        // alias, so `p.Color::Red` *only* matches a value
3534        // declared in the `paint` module — not the lookalike
3535        // value from `other`.
3536        let mut host = crate::host::StringModuleHost::new([
3537            ("paint", "enum Color { Red, Blue }"),
3538            ("other", "enum Color { Red, Green, Yellow }"),
3539        ]);
3540        run(
3541            r#"use paint as p
3542use other as o
3543fn label(c) {
3544    return match c {
3545        p.Color::Red => "paint red",
3546        o.Color::Red => "other red",
3547        _ => "other",
3548    }
3549}
3550print(label(p.Color::Red))
3551print(label(o.Color::Red))
3552print(label(o.Color::Green))"#,
3553            &mut host,
3554            &BopLimits::standard(),
3555        )
3556        .unwrap();
3557        assert_eq!(
3558            host.output(),
3559            "paint red\nother red\nother"
3560        );
3561    }
3562
3563    #[test]
3564    fn string_module_host_runs_use_end_to_end() {
3565        // End-to-end sanity on the embedder helper: the host
3566        // exposes modules through its in-memory map, captures
3567        // prints, and the runtime threads them together with no
3568        // extra wiring on the embedder side.
3569        let mut host = crate::host::StringModuleHost::new([
3570            ("greetings", "fn hello(name) { print(\"hi \" + name) }"),
3571        ]);
3572        run(
3573            "use greetings\nhello(\"bop\")",
3574            &mut host,
3575            &BopLimits::standard(),
3576        )
3577        .unwrap();
3578        assert_eq!(host.output(), "hi bop");
3579    }
3580
3581    // ─── column info on runtime errors ───────────────────────────────
3582
3583    #[test]
3584    fn runtime_error_carries_column_for_undefined_ident() {
3585        // Parse-time errors already carry column; runtime
3586        // errors historically did not. With column now
3587        // threaded through every `Expr` / `Stmt` at parse
3588        // time and surfaced via `error_at`, a runtime error
3589        // on a nested ident carries both line and column.
3590        let err = run_err_full("let x = 1\nprint(undefined)");
3591        assert_eq!(err.line, Some(2), "line");
3592        assert!(
3593            err.column.is_some(),
3594            "expected runtime error to carry column info, got None"
3595        );
3596    }
3597
3598    #[test]
3599    fn runtime_error_column_renders_with_carat() {
3600        // Smoke-test for the end-to-end UX: a rendered
3601        // runtime error shows `--> line:col` and a carat at
3602        // the offending column.
3603        let src = "let x = 1\nprint(undefined)";
3604        let err = run_err_full(src);
3605        let rendered = err.render(src);
3606        assert!(
3607            rendered.contains("--> line 2:"),
3608            "rendered should include line+col header, got:\n{}",
3609            rendered
3610        );
3611        assert!(
3612            rendered.contains("^"),
3613            "rendered should draw a carat, got:\n{}",
3614            rendered
3615        );
3616    }
3617
3618    #[test]
3619    fn resolve_from_map_returns_none_for_unknown_modules() {
3620        // The closure helper is a pure resolver — no print
3621        // handling, no default module. Unknown names return
3622        // `None` so the runtime falls through to its normal
3623        // "module not found" error.
3624        let resolver = crate::host::resolve_from_map([("m", "let x = 1")]);
3625        assert!(resolver("m").is_some());
3626        assert!(resolver("other").is_none());
3627    }
3628
3629    // ─── ReplSession ─────────────────────────────────────────────────
3630
3631    fn repl_eval(
3632        session: &mut ReplSession,
3633        src: &str,
3634        host: &mut TestHost,
3635    ) -> Result<Option<Value>, BopError> {
3636        session.eval(src, host, &test_limits())
3637    }
3638
3639    #[test]
3640    fn session_let_binding_survives_between_evals() {
3641        let mut session = ReplSession::new();
3642        let mut host = TestHost::new();
3643        repl_eval(&mut session, "let x = 5", &mut host).unwrap();
3644        repl_eval(&mut session, "print(x)", &mut host).unwrap();
3645        assert_eq!(host.last_print(), "5");
3646    }
3647
3648    #[test]
3649    fn session_mutated_let_reflects_in_next_eval() {
3650        let mut session = ReplSession::new();
3651        let mut host = TestHost::new();
3652        repl_eval(&mut session, "let counter = 0", &mut host).unwrap();
3653        repl_eval(&mut session, "counter = counter + 1", &mut host).unwrap();
3654        repl_eval(&mut session, "counter = counter + 1", &mut host).unwrap();
3655        repl_eval(&mut session, "print(counter)", &mut host).unwrap();
3656        assert_eq!(host.last_print(), "2");
3657    }
3658
3659    #[test]
3660    fn session_fn_declared_on_one_eval_callable_next() {
3661        let mut session = ReplSession::new();
3662        let mut host = TestHost::new();
3663        repl_eval(
3664            &mut session,
3665            "fn double(x) { return x + x }",
3666            &mut host,
3667        )
3668        .unwrap();
3669        repl_eval(&mut session, "print(double(21))", &mut host).unwrap();
3670        assert_eq!(host.last_print(), "42");
3671    }
3672
3673    #[test]
3674    fn session_struct_and_method_survive() {
3675        // The stickier path: user types live in
3676        // (module_path, type_name) registries and their
3677        // methods in a separate table. Both need to carry
3678        // across inputs.
3679        let mut session = ReplSession::new();
3680        let mut host = TestHost::new();
3681        repl_eval(
3682            &mut session,
3683            "struct Point { x, y }\nfn Point.sum(self) { return self.x + self.y }",
3684            &mut host,
3685        )
3686        .unwrap();
3687        repl_eval(
3688            &mut session,
3689            "let p = Point { x: 3, y: 4 }\nprint(p.sum())",
3690            &mut host,
3691        )
3692        .unwrap();
3693        assert_eq!(host.last_print(), "7");
3694    }
3695
3696    #[test]
3697    fn session_bare_expression_returns_value() {
3698        // The REPL's "echo the last expression" affordance:
3699        // `let` returns None, a bare expression returns
3700        // Some(Value). Drives the REPL echo behaviour.
3701        let mut session = ReplSession::new();
3702        let mut host = TestHost::new();
3703        assert!(repl_eval(&mut session, "let x = 5", &mut host)
3704            .unwrap()
3705            .is_none());
3706        let v = repl_eval(&mut session, "x + 1", &mut host).unwrap();
3707        match v {
3708            Some(Value::Int(n)) => assert_eq!(n, 6),
3709            other => panic!("expected Int(6), got: {:?}", other),
3710        }
3711    }
3712
3713    #[test]
3714    fn session_errors_preserve_earlier_effects() {
3715        // Partial-execution semantics: if a later stmt
3716        // errors, earlier bindings still stick. Matches what
3717        // a user expects from an interactive prompt.
3718        let mut session = ReplSession::new();
3719        let mut host = TestHost::new();
3720        let err = repl_eval(
3721            &mut session,
3722            "let kept = 1\nlet bad = undefined\nlet skipped = 3",
3723            &mut host,
3724        );
3725        assert!(err.is_err(), "expected runtime error");
3726        // `kept` made it; `skipped` did not.
3727        assert!(session.get("kept").is_some());
3728        assert!(session.get("skipped").is_none());
3729    }
3730
3731    #[test]
3732    fn session_normalises_scope_depth_after_block_error() {
3733        // A runtime error inside a nested block
3734        // (`if true { <error> }`) used to leave extra
3735        // scopes pushed. The session's writeback truncates
3736        // back to the root so the next `eval` starts clean.
3737        let mut session = ReplSession::new();
3738        let mut host = TestHost::new();
3739        let _ = repl_eval(
3740            &mut session,
3741            "if true {\n    let y = undefined\n}",
3742            &mut host,
3743        );
3744        // Next eval must not error on "scopes mysteriously
3745        // pre-pushed" — a plain let should work.
3746        repl_eval(&mut session, "let after = 7", &mut host).unwrap();
3747        let v = repl_eval(&mut session, "after", &mut host).unwrap();
3748        match v {
3749            Some(Value::Int(n)) => assert_eq!(n, 7),
3750            other => panic!("expected Int(7), got: {:?}", other),
3751        }
3752    }
3753
3754    #[test]
3755    fn session_binding_names_surfaces_lets_and_fns() {
3756        // `binding_names()` gives a REPL `:vars`-style
3757        // introspection hook. Covers both `let`-scope
3758        // bindings and `fn` declarations.
3759        let mut session = ReplSession::new();
3760        let mut host = TestHost::new();
3761        repl_eval(&mut session, "let alpha = 1", &mut host).unwrap();
3762        repl_eval(&mut session, "fn beta() { return 2 }", &mut host).unwrap();
3763        let names = session.binding_names();
3764        assert!(names.contains(&"alpha".to_string()));
3765        assert!(names.contains(&"beta".to_string()));
3766    }
3767
3768    #[test]
3769    fn session_use_carries_imports_across_evals() {
3770        // Custom host for this test because we need to
3771        // resolve a module. TestHost doesn't implement
3772        // `resolve_module` at the stdlib level, so we wire
3773        // a tiny one up inline.
3774        struct ModHost {
3775            prints: std::cell::RefCell<Vec<String>>,
3776        }
3777        impl BopHost for ModHost {
3778            fn call(
3779                &mut self,
3780                _: &str,
3781                _: &[Value],
3782                _: u32,
3783            ) -> Option<Result<Value, BopError>> {
3784                None
3785            }
3786            fn on_print(&mut self, message: &str) {
3787                self.prints.borrow_mut().push(message.to_string());
3788            }
3789            fn resolve_module(&mut self, name: &str) -> Option<Result<String, BopError>> {
3790                match name {
3791                    "m" => Some(Ok("fn greet() { return \"hi\" }".to_string())),
3792                    _ => None,
3793                }
3794            }
3795        }
3796        let mut host = ModHost {
3797            prints: std::cell::RefCell::new(Vec::new()),
3798        };
3799        let mut session = ReplSession::new();
3800        session.eval("use m", &mut host, &test_limits()).unwrap();
3801        session
3802            .eval("print(greet())", &mut host, &test_limits())
3803            .unwrap();
3804        let prints = host.prints.borrow();
3805        assert_eq!(prints.last().map(|s| s.as_str()), Some("hi"));
3806    }
3807}