Skip to main content

bop/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6#[cfg(not(feature = "std"))]
7use alloc::vec::Vec;
8
9pub mod error;
10pub mod value;
11pub mod lexer;
12pub mod parser;
13pub mod memory;
14pub mod precheck;
15
16mod evaluator;
17mod builtins;
18mod methods;
19
20pub use error::BopError;
21pub use parser::{Stmt, count_instructions};
22pub use value::Value;
23
24// ─── BopLimits ─────────────────────────────────────────────────────────────
25
26/// Resource limits enforced during execution.
27#[derive(Debug, Clone)]
28pub struct BopLimits {
29    /// Max interpreter ticks (loop iterations, statements, etc.)
30    pub max_steps: u64,
31    /// Max total tracked memory (bytes) for strings + arrays
32    pub max_memory: usize,
33}
34
35impl BopLimits {
36    pub fn standard() -> Self {
37        Self {
38            max_steps: 10_000,
39            max_memory: 10 * 1024 * 1024, // 10 MB
40        }
41    }
42
43    pub fn demo() -> Self {
44        Self {
45            max_steps: 1_000,
46            max_memory: 1024 * 1024, // 1 MB
47        }
48    }
49}
50
51impl Default for BopLimits {
52    fn default() -> Self {
53        Self::standard()
54    }
55}
56
57// ─── BopHost trait ─────────────────────────────────────────────────────────
58
59/// Extension point for embedders to add custom built-in functions.
60pub trait BopHost {
61    /// Called for unknown function names. Return `None` = not handled.
62    fn call(&mut self, name: &str, args: &[Value], line: u32) -> Option<Result<Value, BopError>>;
63
64    /// Called by `print()`. Default: writes to stdout (std only), panics (no-std).
65    fn on_print(&mut self, message: &str) {
66        #[cfg(feature = "std")]
67        {
68            println!("{}", message);
69        }
70        #[cfg(not(feature = "std"))]
71        {
72            let _ = message;
73            panic!("BopHost::on_print must be implemented in no-std environments");
74        }
75    }
76
77    /// Hint text for "function not found" errors.
78    fn function_hint(&self) -> &str {
79        ""
80    }
81
82    /// Called each tick. Return `Err` to halt execution.
83    fn on_tick(&mut self) -> Result<(), BopError> {
84        Ok(())
85    }
86}
87
88// ─── StdHost ───────────────────────────────────────────────────────────────
89
90/// Default host: no custom builtins, print to stdout.
91pub struct StdHost;
92
93impl BopHost for StdHost {
94    fn call(&mut self, _name: &str, _args: &[Value], _line: u32) -> Option<Result<Value, BopError>> {
95        None
96    }
97}
98
99// ─── Public API ────────────────────────────────────────────────────────────
100
101/// Run a Bop program with the given host and limits.
102pub fn run<H: BopHost>(source: &str, host: &mut H, limits: &BopLimits) -> Result<(), BopError> {
103    let tokens = lexer::lex(source)?;
104    let stmts = parser::parse(tokens)?;
105    let eval = evaluator::Evaluator::new(host, limits.clone());
106    eval.run(&stmts)
107}
108
109/// Parse Bop source into an AST (useful for instruction counting).
110pub fn parse(source: &str) -> Result<Vec<Stmt>, BopError> {
111    let tokens = lexer::lex(source)?;
112    parser::parse(tokens)
113}
114
115// ─── Tests ─────────────────────────────────────────────────────────────────
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use std::cell::RefCell;
121
122    // ─── Test host ─────────────────────────────────────────────────
123
124    struct TestHost {
125        prints: RefCell<Vec<String>>,
126    }
127
128    impl TestHost {
129        fn new() -> Self {
130            Self {
131                prints: RefCell::new(Vec::new()),
132            }
133        }
134
135        fn last_print(&self) -> String {
136            self.prints.borrow().last().cloned().expect("no print output")
137        }
138    }
139
140    impl BopHost for TestHost {
141        fn call(&mut self, _name: &str, _args: &[Value], _line: u32) -> Option<Result<Value, BopError>> {
142            None
143        }
144
145        fn on_print(&mut self, message: &str) {
146            self.prints.borrow_mut().push(message.to_string());
147        }
148    }
149
150    // ─── Test helpers ──────────────────────────────────────────────
151
152    fn test_limits() -> BopLimits {
153        BopLimits::standard()
154    }
155
156    /// Run code, return last print output
157    fn say(code: &str) -> String {
158        // Change say() -> print() in test code
159        let mut host = TestHost::new();
160        run(code, &mut host, &test_limits()).unwrap();
161        host.last_print()
162    }
163
164    /// Run code, expect runtime error, return message
165    fn run_err(code: &str) -> String {
166        let mut host = TestHost::new();
167        run(code, &mut host, &test_limits()).unwrap_err().message
168    }
169
170    /// Expect a lex or parse error, return message
171    fn parse_err(code: &str) -> String {
172        parse(code).unwrap_err().message
173    }
174
175    /// Run code with custom limits, expect a runtime error, return message
176    fn run_err_with_limits(code: &str, limits: BopLimits) -> String {
177        let mut host = TestHost::new();
178        run(code, &mut host, &limits).unwrap_err().message
179    }
180
181    /// Tight limits for safety tests
182    fn tight_limits() -> BopLimits {
183        BopLimits {
184            max_steps: 500,
185            max_memory: 64 * 1024,
186        }
187    }
188
189    // ─── Arithmetic ────────────────────────────────────────────────
190
191    #[test]
192    fn add_numbers() {
193        assert_eq!(say("print(1 + 2)"), "3");
194    }
195
196    #[test]
197    fn subtract() {
198        assert_eq!(say("print(10 - 3)"), "7");
199    }
200
201    #[test]
202    fn multiply() {
203        assert_eq!(say("print(4 * 5)"), "20");
204    }
205
206    #[test]
207    fn divide_float() {
208        assert_eq!(say("print(7 / 2)"), "3.5");
209    }
210
211    #[test]
212    fn divide_whole() {
213        assert_eq!(say("print(6 / 2)"), "3");
214    }
215
216    #[test]
217    fn modulo() {
218        assert_eq!(say("print(10 % 3)"), "1");
219    }
220
221    #[test]
222    fn precedence() {
223        assert_eq!(say("print(2 + 3 * 4)"), "14");
224    }
225
226    #[test]
227    fn parentheses() {
228        assert_eq!(say("print((2 + 3) * 4)"), "20");
229    }
230
231    #[test]
232    fn unary_neg() {
233        assert_eq!(say("print(-5)"), "-5");
234    }
235
236    #[test]
237    fn unary_not() {
238        assert_eq!(say("print(!true)"), "false");
239    }
240
241    // ─── Strings ───────────────────────────────────────────────────
242
243    #[test]
244    fn string_concat() {
245        assert_eq!(say(r#"print("hello" + " " + "world")"#), "hello world");
246    }
247
248    #[test]
249    fn string_repeat() {
250        assert_eq!(say(r#"print("ab" * 3)"#), "ababab");
251    }
252
253    #[test]
254    fn string_interpolation() {
255        assert_eq!(
256            say(r#"let name = "bop"
257print("hi {name}!")"#),
258            "hi bop!"
259        );
260    }
261
262    #[test]
263    fn string_auto_coerce_in_add() {
264        assert_eq!(say(r#"print("val=" + 42)"#), "val=42");
265    }
266
267    // ─── Comparisons & Logic ───────────────────────────────────────
268
269    #[test]
270    fn equality() {
271        assert_eq!(say("print(1 == 1)"), "true");
272        assert_eq!(say("print(1 == 2)"), "false");
273        assert_eq!(say("print(1 != 2)"), "true");
274    }
275
276    #[test]
277    fn ordering() {
278        assert_eq!(say("print(3 < 5)"), "true");
279        assert_eq!(say("print(5 <= 5)"), "true");
280        assert_eq!(say("print(6 > 5)"), "true");
281        assert_eq!(say("print(5 >= 6)"), "false");
282    }
283
284    #[test]
285    fn logical_and_or() {
286        assert_eq!(say("print(true && false)"), "false");
287        assert_eq!(say("print(true || false)"), "true");
288    }
289
290    #[test]
291    fn short_circuit_and() {
292        assert_eq!(say("print(false && x)"), "false");
293    }
294
295    #[test]
296    fn short_circuit_or() {
297        assert_eq!(say("print(true || x)"), "true");
298    }
299
300    // ─── Variables ─────────────────────────────────────────────────
301
302    #[test]
303    fn let_and_use() {
304        assert_eq!(say("let x = 10\nprint(x)"), "10");
305    }
306
307    #[test]
308    fn assign() {
309        assert_eq!(say("let x = 1\nx = 5\nprint(x)"), "5");
310    }
311
312    #[test]
313    fn compound_assign() {
314        assert_eq!(say("let x = 10\nx += 5\nprint(x)"), "15");
315        assert_eq!(say("let x = 10\nx -= 3\nprint(x)"), "7");
316        assert_eq!(say("let x = 4\nx *= 3\nprint(x)"), "12");
317        assert_eq!(say("let x = 10\nx /= 4\nprint(x)"), "2.5");
318        assert_eq!(say("let x = 10\nx %= 3\nprint(x)"), "1");
319    }
320
321    #[test]
322    fn undefined_variable_error() {
323        assert!(run_err("print(nope)").contains("not found"));
324    }
325
326    #[test]
327    fn assign_undeclared_error() {
328        assert!(run_err("x = 5").contains("doesn't exist"));
329    }
330
331    // ─── If / Else ─────────────────────────────────────────────────
332
333    #[test]
334    fn if_true_branch() {
335        assert_eq!(say("if true { print(\"yes\") } else { print(\"no\") }"), "yes");
336    }
337
338    #[test]
339    fn if_false_branch() {
340        assert_eq!(say("if false { print(\"yes\") } else { print(\"no\") }"), "no");
341    }
342
343    #[test]
344    fn if_else_if() {
345        assert_eq!(
346            say(r#"let x = 2
347if x == 1 { print("one") } else if x == 2 { print("two") } else { print("other") }"#),
348            "two"
349        );
350    }
351
352    #[test]
353    fn if_expression() {
354        assert_eq!(say("let x = if true { 1 } else { 2 }\nprint(x)"), "1");
355    }
356
357    // ─── While ─────────────────────────────────────────────────────
358
359    #[test]
360    fn while_loop() {
361        assert_eq!(say("let i = 0\nwhile i < 5 { i += 1 }\nprint(i)"), "5");
362    }
363
364    #[test]
365    fn while_break() {
366        assert_eq!(
367            say("let i = 0\nwhile true { i += 1\nif i == 3 { break } }\nprint(i)"),
368            "3"
369        );
370    }
371
372    #[test]
373    fn while_continue() {
374        assert_eq!(
375            say(r#"let sum = 0
376let i = 0
377while i < 10 {
378    i += 1
379    if i % 2 == 0 { continue }
380    sum += i
381}
382print(sum)"#),
383            "25"
384        );
385    }
386
387    // ─── For ───────────────────────────────────────────────────────
388
389    #[test]
390    fn for_over_array() {
391        assert_eq!(
392            say(r#"let sum = 0
393for x in [10, 20, 30] { sum += x }
394print(sum)"#),
395            "60"
396        );
397    }
398
399    #[test]
400    fn for_over_range() {
401        assert_eq!(
402            say("let sum = 0\nfor i in range(5) { sum += i }\nprint(sum)"),
403            "10"
404        );
405    }
406
407    #[test]
408    fn for_over_string() {
409        assert_eq!(
410            say(r#"let out = ""
411for ch in "abc" { out += ch + "-" }
412print(out)"#),
413            "a-b-c-"
414        );
415    }
416
417    #[test]
418    fn for_with_break() {
419        assert_eq!(
420            say("let last = 0\nfor i in range(100) { if i == 3 { break }\nlast = i }\nprint(last)"),
421            "2"
422        );
423    }
424
425    // ─── Repeat ────────────────────────────────────────────────────
426
427    #[test]
428    fn repeat_loop() {
429        assert_eq!(say("let n = 0\nrepeat 4 { n += 1 }\nprint(n)"), "4");
430    }
431
432    #[test]
433    fn repeat_zero() {
434        assert_eq!(say("let n = 99\nrepeat 0 { n = 0 }\nprint(n)"), "99");
435    }
436
437    // ─── Functions ─────────────────────────────────────────────────
438
439    #[test]
440    fn fn_basic() {
441        assert_eq!(say("fn double(x) { return x * 2 }\nprint(double(5))"), "10");
442    }
443
444    #[test]
445    fn fn_implicit_return_none() {
446        assert_eq!(
447            say(r#"fn noop() { let x = 1 }
448print(type(noop()))"#),
449            "none"
450        );
451    }
452
453    #[test]
454    fn fn_multiple_params() {
455        assert_eq!(say("fn add(a, b) { return a + b }\nprint(add(3, 7))"), "10");
456    }
457
458    #[test]
459    fn fn_scope_isolation() {
460        assert!(
461            run_err(
462                r#"let secret = 42
463fn peek() { return secret }
464peek()"#
465            )
466            .contains("not found")
467        );
468    }
469
470    #[test]
471    fn fn_wrong_arg_count() {
472        assert!(run_err("fn f(a, b) { return a }\nf(1)").contains("expects 2"));
473    }
474
475    #[test]
476    fn fn_recursion() {
477        assert_eq!(
478            say(r#"fn fib(n) {
479    if n <= 1 { return n }
480    return fib(n - 1) + fib(n - 2)
481}
482print(fib(10))"#),
483            "55"
484        );
485    }
486
487    // ─── Arrays ────────────────────────────────────────────────────
488
489    #[test]
490    fn array_literal_and_index() {
491        assert_eq!(say("let a = [10, 20, 30]\nprint(a[1])"), "20");
492    }
493
494    #[test]
495    fn array_negative_index() {
496        assert_eq!(say("let a = [10, 20, 30]\nprint(a[-1])"), "30");
497    }
498
499    #[test]
500    fn array_assign_index() {
501        assert_eq!(say("let a = [1, 2, 3]\na[1] = 99\nprint(a[1])"), "99");
502    }
503
504    #[test]
505    fn array_push_pop() {
506        assert_eq!(
507            say(r#"let a = [1, 2]
508a.push(3)
509print(a.len())"#),
510            "3"
511        );
512        assert_eq!(
513            say(r#"let a = [1, 2, 3]
514let last = a.pop()
515print(last)"#),
516            "3"
517        );
518    }
519
520    #[test]
521    fn array_has() {
522        assert_eq!(say("print([1, 2, 3].has(2))"), "true");
523        assert_eq!(say("print([1, 2, 3].has(9))"), "false");
524    }
525
526    #[test]
527    fn array_index_of() {
528        assert_eq!(say("print([10, 20, 30].index_of(20))"), "1");
529        assert_eq!(say("print([10, 20, 30].index_of(99))"), "-1");
530    }
531
532    #[test]
533    fn array_slice() {
534        assert_eq!(say("print([1, 2, 3, 4, 5].slice(1, 4))"), "[2, 3, 4]");
535    }
536
537    #[test]
538    fn array_join() {
539        assert_eq!(say(r#"print([1, 2, 3].join("-"))"#), "1-2-3");
540    }
541
542    #[test]
543    fn array_sort() {
544        assert_eq!(say("let a = [3, 1, 2]\na.sort()\nprint(a)"), "[1, 2, 3]");
545    }
546
547    #[test]
548    fn array_reverse() {
549        assert_eq!(say("let a = [1, 2, 3]\na.reverse()\nprint(a)"), "[3, 2, 1]");
550    }
551
552    #[test]
553    fn array_insert_remove() {
554        assert_eq!(
555            say(r#"let a = [1, 3]
556a.insert(1, 2)
557print(a)"#),
558            "[1, 2, 3]"
559        );
560        assert_eq!(
561            say(r#"let a = [1, 2, 3]
562let removed = a.remove(1)
563print(removed)"#),
564            "2"
565        );
566    }
567
568    #[test]
569    fn array_concat() {
570        assert_eq!(say("print([1, 2] + [3, 4])"), "[1, 2, 3, 4]");
571    }
572
573    #[test]
574    fn array_out_of_bounds() {
575        assert!(run_err("let a = [1]\nprint(a[5])").contains("out of bounds"));
576    }
577
578    // ─── Strings (methods) ─────────────────────────────────────────
579
580    #[test]
581    fn string_len() {
582        assert_eq!(say(r#"print("hello".len())"#), "5");
583    }
584
585    #[test]
586    fn string_contains() {
587        assert_eq!(say(r#"print("abcdef".contains("cd"))"#), "true");
588        assert_eq!(say(r#"print("abcdef".contains("zz"))"#), "false");
589    }
590
591    #[test]
592    fn string_starts_ends_with() {
593        assert_eq!(say(r#"print("hello".starts_with("he"))"#), "true");
594        assert_eq!(say(r#"print("hello".ends_with("lo"))"#), "true");
595    }
596
597    #[test]
598    fn string_split() {
599        assert_eq!(say(r#"print("a,b,c".split(","))"#), r#"["a", "b", "c"]"#);
600    }
601
602    #[test]
603    fn string_replace() {
604        assert_eq!(
605            say(r#"print("hello world".replace("world", "bop"))"#),
606            "hello bop"
607        );
608    }
609
610    #[test]
611    fn string_upper_lower_trim() {
612        assert_eq!(say(r#"print("Hello".upper())"#), "HELLO");
613        assert_eq!(say(r#"print("Hello".lower())"#), "hello");
614        assert_eq!(say(r#"print("  hi  ".trim())"#), "hi");
615    }
616
617    #[test]
618    fn string_slice() {
619        assert_eq!(say(r#"print("hello".slice(1, 4))"#), "ell");
620    }
621
622    #[test]
623    fn string_index_of() {
624        assert_eq!(say(r#"print("hello".index_of("ll"))"#), "2");
625        assert_eq!(say(r#"print("hello".index_of("zz"))"#), "-1");
626    }
627
628    #[test]
629    fn string_index_char() {
630        assert_eq!(say(r#"print("abc"[1])"#), "b");
631    }
632
633    // ─── Dicts ─────────────────────────────────────────────────────
634
635    #[test]
636    fn dict_literal_and_access() {
637        assert_eq!(
638            say(r#"let d = {"name": "bop", "hp": 100}
639print(d["name"])"#),
640            "bop"
641        );
642    }
643
644    #[test]
645    fn dict_assign_key() {
646        assert_eq!(
647            say(r#"let d = {"a": 1}
648d["b"] = 2
649print(d["b"])"#),
650            "2"
651        );
652    }
653
654    #[test]
655    fn dict_methods() {
656        assert_eq!(
657            say(r#"let d = {"x": 1, "y": 2}
658print(d.len())"#),
659            "2"
660        );
661        assert_eq!(say(r#"print({"a": 1, "b": 2}.has("a"))"#), "true");
662        assert_eq!(say(r#"print({"a": 1, "b": 2}.has("z"))"#), "false");
663    }
664
665    #[test]
666    fn dict_keys_values() {
667        assert_eq!(say(r#"print({"a": 1, "b": 2}.keys())"#), r#"["a", "b"]"#);
668        assert_eq!(say(r#"print({"a": 1, "b": 2}.values())"#), "[1, 2]");
669    }
670
671    // ─── Built-in functions ────────────────────────────────────────
672
673    #[test]
674    fn builtin_range_1arg() {
675        assert_eq!(say("print(range(5))"), "[0, 1, 2, 3, 4]");
676    }
677
678    #[test]
679    fn builtin_range_2args() {
680        assert_eq!(say("print(range(2, 5))"), "[2, 3, 4]");
681    }
682
683    #[test]
684    fn builtin_range_3args() {
685        assert_eq!(say("print(range(0, 10, 3))"), "[0, 3, 6, 9]");
686    }
687
688    #[test]
689    fn builtin_range_reverse() {
690        assert_eq!(say("print(range(5, 0))"), "[5, 4, 3, 2, 1]");
691    }
692
693    #[test]
694    fn builtin_str() {
695        assert_eq!(say(r#"print(str(42))"#), "42");
696        assert_eq!(say(r#"print(str(true))"#), "true");
697    }
698
699    #[test]
700    fn builtin_int() {
701        assert_eq!(say("print(int(3.7))"), "3");
702        assert_eq!(say("print(int(-2.9))"), "-2");
703    }
704
705    #[test]
706    fn builtin_type() {
707        assert_eq!(say("print(type(42))"), "number");
708        assert_eq!(say(r#"print(type("hi"))"#), "string");
709        assert_eq!(say("print(type(true))"), "bool");
710        assert_eq!(say("print(type(none))"), "none");
711        assert_eq!(say("print(type([]))"), "array");
712    }
713
714    #[test]
715    fn builtin_abs_min_max() {
716        assert_eq!(say("print(abs(-5))"), "5");
717        assert_eq!(say("print(min(3, 7))"), "3");
718        assert_eq!(say("print(max(3, 7))"), "7");
719    }
720
721    #[test]
722    fn builtin_len() {
723        assert_eq!(say(r#"print(len("hello"))"#), "5");
724        assert_eq!(say("print(len([1, 2, 3]))"), "3");
725    }
726
727    #[test]
728    fn builtin_inspect() {
729        assert_eq!(say(r#"print(inspect("hi"))"#), r#""hi""#);
730        assert_eq!(say("print(inspect(42))"), "42");
731    }
732
733    #[test]
734    fn builtin_print_multi_args() {
735        let mut host = TestHost::new();
736        run(r#"print("a", "b", "c")"#, &mut host, &test_limits()).unwrap();
737        assert_eq!(host.prints.borrow().as_slice(), &["a b c"]);
738    }
739
740    #[test]
741    fn builtin_rand_deterministic() {
742        let a = say("print(rand(100))");
743        let b = say("print(rand(100))");
744        assert_eq!(a, b);
745    }
746
747    // ─── Error cases ───────────────────────────────────────────────
748
749    #[test]
750    fn error_division_by_zero() {
751        assert!(run_err("print(1 / 0)").contains("Division by zero"));
752    }
753
754    #[test]
755    fn error_type_mismatch_subtract() {
756        let msg = run_err(r#"print("a" - 1)"#);
757        assert!(msg.contains("Can't use `-`"));
758    }
759
760    #[test]
761    fn error_unknown_function() {
762        assert!(run_err("nope()").contains("not found"));
763    }
764
765    #[test]
766    fn error_infinite_loop_protection() {
767        let msg = run_err("while true { }");
768        assert!(msg.contains("too many steps"));
769    }
770
771    #[test]
772    fn error_break_outside_loop() {
773        assert!(run_err("break").contains("outside of a loop"));
774    }
775
776    #[test]
777    fn error_continue_outside_loop() {
778        assert!(run_err("continue").contains("outside of a loop"));
779    }
780
781    // ─── Parse errors ──────────────────────────────────────────────
782
783    #[test]
784    fn parse_error_missing_rparen() {
785        assert!(parse_err("print(1").contains("Expected `)`"));
786    }
787
788    #[test]
789    fn parse_error_missing_rbrace() {
790        assert!(parse_err("if true {").contains("Expected `}`"));
791    }
792
793    // ─── Edge cases ────────────────────────────────────────────────
794
795    #[test]
796    fn empty_program() {
797        let mut host = TestHost::new();
798        run("", &mut host, &test_limits()).unwrap();
799        assert!(host.prints.borrow().is_empty());
800    }
801
802    #[test]
803    fn trailing_comma_in_array() {
804        assert_eq!(say("print([1, 2, 3,])"), "[1, 2, 3]");
805    }
806
807    #[test]
808    fn trailing_comma_in_dict() {
809        assert_eq!(say(r#"print({"a": 1,}.len())"#), "1");
810    }
811
812    #[test]
813    fn none_value() {
814        assert_eq!(say("print(none)"), "none");
815        assert_eq!(say("print(none == none)"), "true");
816    }
817
818    #[test]
819    fn equality_across_types() {
820        assert_eq!(say("print(1 == true)"), "false");
821        assert_eq!(say(r#"print(0 == "")"#), "false");
822        assert_eq!(say("print(none == false)"), "false");
823    }
824
825    #[test]
826    fn dict_equality() {
827        assert_eq!(say(r#"print({"a": 1, "b": 2} == {"b": 2, "a": 1})"#), "true");
828        assert_eq!(say(r#"print({"a": 1} == {"a": 2})"#), "false");
829        assert_eq!(say(r#"print({"a": 1} == {"b": 1})"#), "false");
830        assert_eq!(say(r#"print({"a": 1} == {"a": 1, "b": 2})"#), "false");
831        assert_eq!(say(r#"print({"a": {"x": 1}} == {"a": {"x": 1}})"#), "true");
832    }
833
834    #[test]
835    fn nested_array_access() {
836        assert_eq!(say("let m = [[1, 2], [3, 4]]\nprint(m[1][0])"), "3");
837    }
838
839    #[test]
840    fn method_chain() {
841        assert_eq!(say(r#"print("  HELLO  ".trim().lower())"#), "hello");
842    }
843
844    #[test]
845    fn comments_in_code() {
846        assert_eq!(
847            say(r#"// this is a comment
848let x = 42 // inline comment
849print(x)"#),
850            "42"
851        );
852    }
853
854    // ─── Instruction counting ─────────────────────────────────────
855
856    fn count(code: &str) -> u32 {
857        let stmts = parse(code).unwrap();
858        count_instructions(&stmts)
859    }
860
861    #[test]
862    fn count_simple_calls() {
863        assert_eq!(count("print(1)"), 1);
864        assert_eq!(count("print(1); print(2); print(3)"), 3);
865    }
866
867    #[test]
868    fn count_repeat() {
869        assert_eq!(count("repeat 7 { print(1) }"), 2);
870    }
871
872    #[test]
873    fn count_if() {
874        assert_eq!(count("if true { print(1) }"), 2);
875        assert_eq!(count("if true { print(1) } else { print(2) }"), 3);
876    }
877
878    #[test]
879    fn count_while() {
880        assert_eq!(count("while true { print(1) }"), 2);
881    }
882
883    #[test]
884    fn count_fn_skips_body() {
885        assert_eq!(count("fn go() { print(1); print(2); print(3) }\ngo()"), 2);
886    }
887
888    #[test]
889    fn count_format_independent() {
890        let one_line = count("repeat 7 { print(1) }");
891        let multi_line = count("repeat 7 {\n    print(1)\n}");
892        assert_eq!(one_line, multi_line);
893        assert_eq!(one_line, 2);
894    }
895
896    #[test]
897    fn count_nested() {
898        assert_eq!(count("repeat 7 { if true { print(1) } }"), 3);
899    }
900
901    #[test]
902    fn count_empty_program() {
903        assert_eq!(count(""), 0);
904    }
905
906    // ─── Scope / block isolation ───────────────────────────────────
907
908    #[test]
909    fn if_block_scope() {
910        assert!(
911            run_err(
912                r#"if true { let inner = 1 }
913print(inner)"#
914            )
915            .contains("not found")
916        );
917    }
918
919    #[test]
920    fn for_loop_var_scoped() {
921        assert!(
922            run_err(
923                r#"for item in [1, 2] { let x = item }
924print(item)"#
925            )
926            .contains("not found")
927        );
928    }
929
930    // ─── Complex programs ──────────────────────────────────────────
931
932    #[test]
933    fn fizzbuzz() {
934        assert_eq!(
935            say(r#"let result = []
936for i in range(1, 16) {
937    if i % 15 == 0 {
938        result.push("FizzBuzz")
939    } else if i % 3 == 0 {
940        result.push("Fizz")
941    } else if i % 5 == 0 {
942        result.push("Buzz")
943    } else {
944        result.push(str(i))
945    }
946}
947print(result.join(", "))"#),
948            "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz"
949        );
950    }
951
952    #[test]
953    fn nested_function_calls() {
954        assert_eq!(
955            say(r#"fn square(n) { return n * n }
956fn sum_squares(a, b) { return square(a) + square(b) }
957print(sum_squares(3, 4))"#),
958            "25"
959        );
960    }
961
962    #[test]
963    fn array_manipulation_program() {
964        assert_eq!(
965            say(r#"let data = [5, 2, 8, 1, 9, 3]
966data.sort()
967let top3 = data.slice(3, 6)
968print(top3.join(", "))"#),
969            "5, 8, 9"
970        );
971    }
972
973    // ─── Truthiness ────────────────────────────────────────────────
974
975    #[test]
976    fn truthy_values() {
977        assert_eq!(say("print(if 1 { \"yes\" } else { \"no\" })"), "yes");
978        assert_eq!(say(r#"print(if "x" { "yes" } else { "no" })"#), "yes");
979        assert_eq!(say("print(if [1] { \"yes\" } else { \"no\" })"), "yes");
980    }
981
982    #[test]
983    fn falsy_values() {
984        assert_eq!(say("print(if 0 { \"yes\" } else { \"no\" })"), "no");
985        assert_eq!(say("print(if false { \"yes\" } else { \"no\" })"), "no");
986        assert_eq!(say("print(if none { \"yes\" } else { \"no\" })"), "no");
987        assert_eq!(say(r#"print(if "" { "yes" } else { "no" })"#), "no");
988    }
989
990    // ─── Number display ────────────────────────────────────────────
991
992    #[test]
993    fn display_whole_number_as_int() {
994        assert_eq!(say("print(5.0)"), "5");
995    }
996
997    #[test]
998    fn display_float_with_decimals() {
999        assert_eq!(say("print(3.14)"), "3.14");
1000    }
1001
1002    // ─── Safety / resource-limit tests ──────────────────────────────
1003
1004    #[test]
1005    fn safety_infinite_loop_halts() {
1006        let msg = run_err_with_limits("while true { }", tight_limits());
1007        assert!(msg.contains("too many steps"), "got: {}", msg);
1008    }
1009
1010    #[test]
1011    fn safety_memory_bomb_string_doubling() {
1012        let msg = run_err_with_limits(
1013            r#"let s = "aaaaaaaaaa"
1014repeat 100 { s = s + s }"#,
1015            tight_limits(),
1016        );
1017        assert!(msg.contains("Memory limit"), "got: {}", msg);
1018    }
1019
1020    #[test]
1021    fn safety_memory_bomb_array_growth() {
1022        let msg = run_err_with_limits(
1023            r#"let arr = []
1024repeat 500 {
1025    arr.push("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
1026}"#,
1027            tight_limits(),
1028        );
1029        assert!(
1030            msg.contains("Memory limit") || msg.contains("too many steps"),
1031            "got: {}", msg
1032        );
1033    }
1034
1035    #[test]
1036    fn safety_deep_recursion_halts() {
1037        let msg = run_err_with_limits("fn f() { f() }\nf()", tight_limits());
1038        assert!(
1039            msg.contains("nested function calls") || msg.contains("recursion"),
1040            "got: {}", msg
1041        );
1042    }
1043
1044    #[test]
1045    fn safety_deep_parse_nesting() {
1046        let code = "(".repeat(200) + "1" + &")".repeat(200);
1047        let msg = parse(&code).unwrap_err().message;
1048        assert!(msg.contains("nested too deeply"), "got: {}", msg);
1049    }
1050
1051    #[test]
1052    fn safety_string_repeat_bomb() {
1053        let msg = run_err_with_limits(r#"let s = "x" * 999999"#, tight_limits());
1054        assert!(msg.contains("Memory limit"), "got: {}", msg);
1055    }
1056
1057    #[test]
1058    fn safety_string_concat_bomb() {
1059        let msg = run_err_with_limits(
1060            r#"let s = "x" * 1000
1061repeat 100 { s = s + s }"#,
1062            tight_limits(),
1063        );
1064        assert!(msg.contains("Memory limit"), "got: {}", msg);
1065    }
1066
1067    #[test]
1068    fn safety_array_concat_bomb() {
1069        let msg = run_err_with_limits(
1070            r#"let a = range(100)
1071repeat 50 { a = a + a }"#,
1072            tight_limits(),
1073        );
1074        assert!(
1075            msg.contains("Memory limit") || msg.contains("too many steps"),
1076            "got: {}", msg
1077        );
1078    }
1079
1080    #[test]
1081    fn safety_for_in_large_string() {
1082        let msg = run_err_with_limits(
1083            r#"let s = "x" * 10000
1084for c in s { }"#,
1085            tight_limits(),
1086        );
1087        assert!(
1088            msg.contains("too many steps") || msg.contains("Memory limit"),
1089            "got: {}", msg
1090        );
1091    }
1092
1093    #[test]
1094    fn safety_demo_limits_step_bound() {
1095        let msg = run_err_with_limits(
1096            "let i = 0\nwhile true { i = i + 1 }",
1097            BopLimits::demo(),
1098        );
1099        assert!(msg.contains("too many steps"), "got: {}", msg);
1100    }
1101
1102    #[test]
1103    fn safety_demo_limits_memory_bound() {
1104        let msg = run_err_with_limits(
1105            r#"let s = "x" * 1100000
1106print(s)"#,
1107            BopLimits::demo(),
1108        );
1109        assert!(msg.contains("Memory limit"), "got: {}", msg);
1110    }
1111
1112    #[test]
1113    fn safety_nested_loop_step_bound() {
1114        let msg = run_err_with_limits("repeat 100 { repeat 100 { let x = 1 } }", tight_limits());
1115        assert!(msg.contains("too many steps"), "got: {}", msg);
1116    }
1117
1118    #[test]
1119    fn safety_string_split_bomb() {
1120        let msg = run_err_with_limits(
1121            r#"let s = "abababababab" * 2000
1122let parts = s.split("a")
1123let x = 1"#,
1124            tight_limits(),
1125        );
1126        assert!(
1127            msg.contains("Memory limit") || msg.contains("too many steps"),
1128            "got: {}", msg
1129        );
1130    }
1131
1132    #[test]
1133    fn safety_join_bomb() {
1134        let msg = run_err_with_limits(
1135            r#"let a = []
1136repeat 400 { a.push("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }
1137let s = a.join("")
1138let x = 1"#,
1139            tight_limits(),
1140        );
1141        assert!(
1142            msg.contains("Memory limit") || msg.contains("too many steps"),
1143            "got: {}", msg
1144        );
1145    }
1146
1147    #[test]
1148    fn safety_range_hard_cap() {
1149        let msg = run_err_with_limits(
1150            r#"let a = range(100000)
1151let x = 1"#,
1152            tight_limits(),
1153        );
1154        assert!(
1155            msg.contains("Memory limit") || msg.contains("too many steps"),
1156            "got: {}", msg
1157        );
1158    }
1159
1160    #[test]
1161    fn safety_array_method_doubling() {
1162        let msg = run_err_with_limits(
1163            r#"let a = []
1164repeat 400 { a.push("aaaaaaaaaaaaaaaaaaaaaa") }
1165a.reverse()
1166let x = 1"#,
1167            tight_limits(),
1168        );
1169        assert!(
1170            msg.contains("Memory limit") || msg.contains("too many steps"),
1171            "got: {}", msg
1172        );
1173    }
1174
1175    #[test]
1176    fn safety_preflight_catches() {
1177        let limits = BopLimits {
1178            max_steps: 500,
1179            max_memory: 32 * 1024,
1180        };
1181        let msg = run_err_with_limits(r#"let s = "x" * 40000"#, limits);
1182        assert!(msg.contains("Memory limit"), "got: {}", msg);
1183    }
1184
1185    #[test]
1186    fn safety_bounded_overshoot() {
1187        let limits = BopLimits {
1188            max_steps: 500,
1189            max_memory: 64 * 1024,
1190        };
1191        let mut host = TestHost::new();
1192        let result = run(
1193            r#"let s = "abababab" * 1000
1194let parts = s.split("a")"#,
1195            &mut host,
1196            &limits,
1197        );
1198        assert!(result.is_ok(), "Expected success (bounded overshoot), got error");
1199    }
1200
1201    #[test]
1202    fn safety_dict_growth_tracked() {
1203        let msg = run_err_with_limits(
1204            r#"let d = {}
1205repeat 400 {
1206    d[str(d.len())] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1207}
1208let x = 1"#,
1209            tight_limits(),
1210        );
1211        assert!(
1212            msg.contains("Memory limit") || msg.contains("too many steps"),
1213            "got: {}", msg
1214        );
1215    }
1216
1217    // ─── BopHost extension ─────────────────────────────────────────
1218
1219    struct CustomHost {
1220        prints: Vec<String>,
1221    }
1222
1223    impl BopHost for CustomHost {
1224        fn call(&mut self, name: &str, args: &[Value], line: u32) -> Option<Result<Value, BopError>> {
1225            match name {
1226                "greet" => {
1227                    if args.len() != 1 {
1228                        return Some(Err(BopError {
1229                            line: Some(line),
1230                            column: None,
1231                            message: "greet() needs 1 argument".into(),
1232                            friendly_hint: None,
1233                        }));
1234                    }
1235                    Some(Ok(Value::new_str(format!("Hello, {}!", args[0]))))
1236                }
1237                _ => None,
1238            }
1239        }
1240
1241        fn on_print(&mut self, message: &str) {
1242            self.prints.push(message.to_string());
1243        }
1244
1245        fn function_hint(&self) -> &str {
1246            "Available: greet(name)"
1247        }
1248    }
1249
1250    #[test]
1251    fn host_custom_builtin() {
1252        let mut host = CustomHost { prints: vec![] };
1253        run(r#"print(greet("world"))"#, &mut host, &BopLimits::standard()).unwrap();
1254        assert_eq!(host.prints, vec!["Hello, world!"]);
1255    }
1256
1257    #[test]
1258    fn host_function_hint() {
1259        let mut host = CustomHost { prints: vec![] };
1260        let err = run("unknown()", &mut host, &BopLimits::standard()).unwrap_err();
1261        assert!(err.message.contains("not found"));
1262    }
1263}