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#[derive(Debug, Clone)]
28pub struct BopLimits {
29 pub max_steps: u64,
31 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, }
41 }
42
43 pub fn demo() -> Self {
44 Self {
45 max_steps: 1_000,
46 max_memory: 1024 * 1024, }
48 }
49}
50
51impl Default for BopLimits {
52 fn default() -> Self {
53 Self::standard()
54 }
55}
56
57pub trait BopHost {
61 fn call(&mut self, name: &str, args: &[Value], line: u32) -> Option<Result<Value, BopError>>;
63
64 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 fn function_hint(&self) -> &str {
79 ""
80 }
81
82 fn on_tick(&mut self) -> Result<(), BopError> {
84 Ok(())
85 }
86}
87
88pub 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
99pub 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
109pub fn parse(source: &str) -> Result<Vec<Stmt>, BopError> {
111 let tokens = lexer::lex(source)?;
112 parser::parse(tokens)
113}
114
115#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::cell::RefCell;
121
122 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 fn test_limits() -> BopLimits {
153 BopLimits::standard()
154 }
155
156 fn say(code: &str) -> String {
158 let mut host = TestHost::new();
160 run(code, &mut host, &test_limits()).unwrap();
161 host.last_print()
162 }
163
164 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 fn parse_err(code: &str) -> String {
172 parse(code).unwrap_err().message
173 }
174
175 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 fn tight_limits() -> BopLimits {
183 BopLimits {
184 max_steps: 500,
185 max_memory: 64 * 1024,
186 }
187 }
188
189 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 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}