#![cfg_attr(feature = "no_std", no_std)]
#[cfg(feature = "no_std")]
extern crate alloc;
#[cfg(feature = "no_std")]
use alloc::{string::String, vec::Vec};
pub mod error;
pub mod error_messages;
pub mod value;
pub mod lexer;
pub mod parser;
pub mod math;
pub mod memory;
pub mod naming;
pub mod ops;
pub mod precheck;
pub mod builtins;
pub mod host;
pub mod methods;
pub mod suggest;
pub mod check;
#[cfg(feature = "bop-std")]
pub mod stdlib;
mod evaluator;
pub use error::BopError;
pub use error::BopWarning;
pub use parser::{Stmt, count_instructions};
pub use value::Value;
pub use evaluator::pattern_matches;
pub use evaluator::resolve_type_in;
pub use evaluator::TypeResolveFn;
pub use evaluator::ReplSession;
#[derive(Debug, Clone)]
pub struct BopLimits {
pub max_steps: u64,
pub max_memory: usize,
}
impl BopLimits {
pub fn standard() -> Self {
Self {
max_steps: 10_000,
max_memory: 10 * 1024 * 1024, }
}
pub fn demo() -> Self {
Self {
max_steps: 1_000,
max_memory: 1024 * 1024, }
}
}
impl Default for BopLimits {
fn default() -> Self {
Self::standard()
}
}
pub trait BopHost {
fn call(&mut self, name: &str, args: &[Value], line: u32) -> Option<Result<Value, BopError>>;
fn on_print(&mut self, message: &str) {
let _ = message;
}
fn function_hint(&self) -> &str {
""
}
fn on_tick(&mut self) -> Result<(), BopError> {
Ok(())
}
fn resolve_module(&mut self, name: &str) -> Option<Result<String, BopError>> {
let _ = name;
None
}
}
pub fn run<H: BopHost>(source: &str, host: &mut H, limits: &BopLimits) -> Result<(), BopError> {
let tokens = lexer::lex(source)?;
let stmts = parser::parse(tokens)?;
let eval = evaluator::Evaluator::new(host, limits.clone());
eval.run(&stmts)
}
pub fn parse(source: &str) -> Result<Vec<Stmt>, BopError> {
let tokens = lexer::lex(source)?;
parser::parse(tokens)
}
pub fn parse_with_warnings(
source: &str,
) -> Result<(Vec<Stmt>, Vec<error::BopWarning>), BopError> {
let stmts = parse(source)?;
let warnings = check::check_program(&stmts);
Ok((stmts, warnings))
}
pub fn parse_with_warnings_and_resolver<R>(
source: &str,
mut resolver: R,
) -> Result<(Vec<Stmt>, Vec<error::BopWarning>), BopError>
where
R: FnMut(&str) -> Option<Result<String, BopError>>,
{
let stmts = parse(source)?;
let warnings = check::check_program_with_resolver(&stmts, &mut resolver);
Ok((stmts, warnings))
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct TestHost {
prints: RefCell<Vec<String>>,
}
impl TestHost {
fn new() -> Self {
Self {
prints: RefCell::new(Vec::new()),
}
}
fn last_print(&self) -> String {
self.prints.borrow().last().cloned().expect("no print output")
}
}
impl BopHost for TestHost {
fn call(&mut self, _name: &str, _args: &[Value], _line: u32) -> Option<Result<Value, BopError>> {
None
}
fn on_print(&mut self, message: &str) {
self.prints.borrow_mut().push(message.to_string());
}
}
fn test_limits() -> BopLimits {
BopLimits::standard()
}
fn say(code: &str) -> String {
let mut host = TestHost::new();
run(code, &mut host, &test_limits()).unwrap();
host.last_print()
}
fn run_err(code: &str) -> String {
let mut host = TestHost::new();
run(code, &mut host, &test_limits()).unwrap_err().message
}
fn parse_err(code: &str) -> String {
parse(code).unwrap_err().message
}
fn run_err_with_limits(code: &str, limits: BopLimits) -> String {
let mut host = TestHost::new();
run(code, &mut host, &limits).unwrap_err().message
}
fn tight_limits() -> BopLimits {
BopLimits {
max_steps: 500,
max_memory: 64 * 1024,
}
}
#[test]
fn add_numbers() {
assert_eq!(say("print(1 + 2)"), "3");
}
#[test]
fn subtract() {
assert_eq!(say("print(10 - 3)"), "7");
}
#[test]
fn multiply() {
assert_eq!(say("print(4 * 5)"), "20");
}
#[test]
fn divide_float() {
assert_eq!(say("print(7 / 2)"), "3.5");
}
#[test]
fn divide_whole() {
assert_eq!(say("print(6 / 2)"), "3");
}
#[test]
fn modulo() {
assert_eq!(say("print(10 % 3)"), "1");
}
#[test]
fn precedence() {
assert_eq!(say("print(2 + 3 * 4)"), "14");
}
#[test]
fn parentheses() {
assert_eq!(say("print((2 + 3) * 4)"), "20");
}
#[test]
fn unary_neg() {
assert_eq!(say("print(-5)"), "-5");
}
#[test]
fn unary_not() {
assert_eq!(say("print(!true)"), "false");
}
#[test]
fn string_concat() {
assert_eq!(say(r#"print("hello" + " " + "world")"#), "hello world");
}
#[test]
fn string_repeat() {
assert_eq!(say(r#"print("ab" * 3)"#), "ababab");
}
#[test]
fn string_interpolation() {
assert_eq!(
say(r#"let name = "bop"
print("hi {name}!")"#),
"hi bop!"
);
}
#[test]
fn string_auto_coerce_in_add() {
assert_eq!(say(r#"print("val=" + 42)"#), "val=42");
}
#[test]
fn equality() {
assert_eq!(say("print(1 == 1)"), "true");
assert_eq!(say("print(1 == 2)"), "false");
assert_eq!(say("print(1 != 2)"), "true");
}
#[test]
fn ordering() {
assert_eq!(say("print(3 < 5)"), "true");
assert_eq!(say("print(5 <= 5)"), "true");
assert_eq!(say("print(6 > 5)"), "true");
assert_eq!(say("print(5 >= 6)"), "false");
}
#[test]
fn logical_and_or() {
assert_eq!(say("print(true && false)"), "false");
assert_eq!(say("print(true || false)"), "true");
}
#[test]
fn short_circuit_and() {
assert_eq!(say("print(false && x)"), "false");
}
#[test]
fn short_circuit_or() {
assert_eq!(say("print(true || x)"), "true");
}
#[test]
fn let_and_use() {
assert_eq!(say("let x = 10\nprint(x)"), "10");
}
#[test]
fn assign() {
assert_eq!(say("let x = 1\nx = 5\nprint(x)"), "5");
}
#[test]
fn compound_assign() {
assert_eq!(say("let x = 10\nx += 5\nprint(x)"), "15");
assert_eq!(say("let x = 10\nx -= 3\nprint(x)"), "7");
assert_eq!(say("let x = 4\nx *= 3\nprint(x)"), "12");
assert_eq!(say("let x = 10\nx /= 4\nprint(x)"), "2.5");
assert_eq!(say("let x = 10\nx %= 3\nprint(x)"), "1");
}
#[test]
fn undefined_variable_error() {
assert!(run_err("print(nope)").contains("not found"));
}
#[test]
fn assign_undeclared_error() {
assert!(run_err("x = 5").contains("doesn't exist"));
}
#[test]
fn if_true_branch() {
assert_eq!(say("if true { print(\"yes\") } else { print(\"no\") }"), "yes");
}
#[test]
fn if_false_branch() {
assert_eq!(say("if false { print(\"yes\") } else { print(\"no\") }"), "no");
}
#[test]
fn if_else_if() {
assert_eq!(
say(r#"let x = 2
if x == 1 { print("one") } else if x == 2 { print("two") } else { print("other") }"#),
"two"
);
}
#[test]
fn if_expression() {
assert_eq!(say("let x = if true { 1 } else { 2 }\nprint(x)"), "1");
}
#[test]
fn while_loop() {
assert_eq!(say("let i = 0\nwhile i < 5 { i += 1 }\nprint(i)"), "5");
}
#[test]
fn while_break() {
assert_eq!(
say("let i = 0\nwhile true { i += 1\nif i == 3 { break } }\nprint(i)"),
"3"
);
}
#[test]
fn while_continue() {
assert_eq!(
say(r#"let sum = 0
let i = 0
while i < 10 {
i += 1
if i % 2 == 0 { continue }
sum += i
}
print(sum)"#),
"25"
);
}
#[test]
fn for_over_array() {
assert_eq!(
say(r#"let sum = 0
for x in [10, 20, 30] { sum += x }
print(sum)"#),
"60"
);
}
#[test]
fn for_over_range() {
assert_eq!(
say("let sum = 0\nfor i in range(5) { sum += i }\nprint(sum)"),
"10"
);
}
#[test]
fn for_over_string() {
assert_eq!(
say(r#"let out = ""
for ch in "abc" { out += ch + "-" }
print(out)"#),
"a-b-c-"
);
}
#[test]
fn for_with_break() {
assert_eq!(
say("let last = 0\nfor i in range(100) { if i == 3 { break }\nlast = i }\nprint(last)"),
"2"
);
}
#[test]
fn repeat_loop() {
assert_eq!(say("let n = 0\nrepeat 4 { n += 1 }\nprint(n)"), "4");
}
#[test]
fn repeat_zero() {
assert_eq!(say("let n = 99\nrepeat 0 { n = 0 }\nprint(n)"), "99");
}
#[test]
fn fn_basic() {
assert_eq!(say("fn double(x) { return x * 2 }\nprint(double(5))"), "10");
}
#[test]
fn fn_implicit_return_none() {
assert_eq!(
say(r#"fn noop() { let x = 1 }
print(noop().type())"#),
"none"
);
}
#[test]
fn fn_multiple_params() {
assert_eq!(say("fn add(a, b) { return a + b }\nprint(add(3, 7))"), "10");
}
#[test]
fn fn_scope_isolation() {
assert!(
run_err(
r#"let secret = 42
fn peek() { return secret }
peek()"#
)
.contains("not found")
);
}
#[test]
fn fn_wrong_arg_count() {
assert!(run_err("fn f(a, b) { return a }\nf(1)").contains("expects 2"));
}
#[test]
fn fn_recursion() {
assert_eq!(
say(r#"fn fib(n) {
if n <= 1 { return n }
return fib(n - 1) + fib(n - 2)
}
print(fib(10))"#),
"55"
);
}
#[test]
fn lambda_basic() {
assert_eq!(
say(r#"let double = fn(x) { return x * 2 }
print(double(5))"#),
"10"
);
}
#[test]
fn lambda_captures_value() {
assert_eq!(
say(r#"let n = 5
let add_n = fn(x) { return x + n }
print(add_n(3))"#),
"8"
);
}
#[test]
fn lambda_captures_are_snapshot() {
assert_eq!(
say(r#"let n = 5
let add_n = fn(x) { return x + n }
n = 100
print(add_n(3))"#),
"8"
);
}
#[test]
fn lambda_returned_from_fn() {
assert_eq!(
say(r#"fn make_adder(n) { return fn(x) { return x + n } }
let add5 = make_adder(5)
let add10 = make_adder(10)
print(add5(3))
print(add10(3))"#),
"13"
);
}
#[test]
fn named_fn_is_first_class_value() {
assert_eq!(
say(r#"fn double(x) { return x * 2 }
let f = double
print(f(7))"#),
"14"
);
}
#[test]
fn fn_stored_in_array_and_called_via_index() {
assert_eq!(
say(r#"fn add(x, y) { return x + y }
fn mul(x, y) { return x * y }
let ops = [add, mul]
print(ops[0](2, 3))
print(ops[1](2, 3))"#),
"6"
);
}
#[test]
fn higher_order_apply() {
assert_eq!(
say(r#"fn apply(f, x) { return f(x) }
fn square(n) { return n * n }
print(apply(square, 4))
print(apply(fn(n) { return n + 1 }, 4))"#),
"5"
);
}
#[test]
fn lambda_self_reference_via_named_fn() {
assert_eq!(
say(r#"fn countdown(n) {
if n <= 0 { return "done" }
return countdown(n - 1)
}
print(countdown(3))"#),
"done"
);
}
#[test]
fn type_of_fn_is_fn() {
assert_eq!(say("fn f() { }\nprint(f.type())"), "fn");
assert_eq!(say("let g = fn() { }\nprint(g.type())"), "fn");
}
#[test]
fn calling_non_callable_value_errors() {
assert!(run_err("let x = 5\nx(1)").contains("not a function"));
}
#[test]
fn lambda_captures_nested_scope() {
assert_eq!(
say(r#"let a = 1
if true {
let b = 2
let f = fn() { return a + b }
print(f())
}"#),
"3"
);
}
#[test]
fn iife() {
assert_eq!(say("print((fn(x) { return x * 3 })(4))"), "12");
}
#[test]
fn array_literal_and_index() {
assert_eq!(say("let a = [10, 20, 30]\nprint(a[1])"), "20");
}
#[test]
fn array_negative_index() {
assert_eq!(say("let a = [10, 20, 30]\nprint(a[-1])"), "30");
}
#[test]
fn array_assign_index() {
assert_eq!(say("let a = [1, 2, 3]\na[1] = 99\nprint(a[1])"), "99");
}
#[test]
fn array_push_pop() {
assert_eq!(
say(r#"let a = [1, 2]
a.push(3)
print(a.len())"#),
"3"
);
assert_eq!(
say(r#"let a = [1, 2, 3]
let last = a.pop()
print(last)"#),
"3"
);
}
#[test]
fn array_has() {
assert_eq!(say("print([1, 2, 3].has(2))"), "true");
assert_eq!(say("print([1, 2, 3].has(9))"), "false");
}
#[test]
fn array_index_of() {
assert_eq!(say("print([10, 20, 30].index_of(20))"), "1");
assert_eq!(say("print([10, 20, 30].index_of(99))"), "-1");
}
#[test]
fn array_slice() {
assert_eq!(say("print([1, 2, 3, 4, 5].slice(1, 4))"), "[2, 3, 4]");
}
#[test]
fn array_join() {
assert_eq!(say(r#"print([1, 2, 3].join("-"))"#), "1-2-3");
}
#[test]
fn array_sort() {
assert_eq!(say("let a = [3, 1, 2]\na.sort()\nprint(a)"), "[1, 2, 3]");
}
#[test]
fn array_reverse() {
assert_eq!(say("let a = [1, 2, 3]\na.reverse()\nprint(a)"), "[3, 2, 1]");
}
#[test]
fn array_insert_remove() {
assert_eq!(
say(r#"let a = [1, 3]
a.insert(1, 2)
print(a)"#),
"[1, 2, 3]"
);
assert_eq!(
say(r#"let a = [1, 2, 3]
let removed = a.remove(1)
print(removed)"#),
"2"
);
}
#[test]
fn array_concat() {
assert_eq!(say("print([1, 2] + [3, 4])"), "[1, 2, 3, 4]");
}
#[test]
fn array_out_of_bounds() {
assert!(run_err("let a = [1]\nprint(a[5])").contains("out of bounds"));
}
#[test]
fn string_len() {
assert_eq!(say(r#"print("hello".len())"#), "5");
}
#[test]
fn string_contains() {
assert_eq!(say(r#"print("abcdef".contains("cd"))"#), "true");
assert_eq!(say(r#"print("abcdef".contains("zz"))"#), "false");
}
#[test]
fn string_starts_ends_with() {
assert_eq!(say(r#"print("hello".starts_with("he"))"#), "true");
assert_eq!(say(r#"print("hello".ends_with("lo"))"#), "true");
}
#[test]
fn string_split() {
assert_eq!(say(r#"print("a,b,c".split(","))"#), r#"["a", "b", "c"]"#);
}
#[test]
fn string_replace() {
assert_eq!(
say(r#"print("hello world".replace("world", "bop"))"#),
"hello bop"
);
}
#[test]
fn string_upper_lower_trim() {
assert_eq!(say(r#"print("Hello".upper())"#), "HELLO");
assert_eq!(say(r#"print("Hello".lower())"#), "hello");
assert_eq!(say(r#"print(" hi ".trim())"#), "hi");
}
#[test]
fn string_slice() {
assert_eq!(say(r#"print("hello".slice(1, 4))"#), "ell");
}
#[test]
fn string_index_of() {
assert_eq!(say(r#"print("hello".index_of("ll"))"#), "2");
assert_eq!(say(r#"print("hello".index_of("zz"))"#), "-1");
}
#[test]
fn string_index_char() {
assert_eq!(say(r#"print("abc"[1])"#), "b");
}
#[test]
fn dict_literal_and_access() {
assert_eq!(
say(r#"let d = {"name": "bop", "hp": 100}
print(d["name"])"#),
"bop"
);
}
#[test]
fn dict_assign_key() {
assert_eq!(
say(r#"let d = {"a": 1}
d["b"] = 2
print(d["b"])"#),
"2"
);
}
#[test]
fn dict_methods() {
assert_eq!(
say(r#"let d = {"x": 1, "y": 2}
print(d.len())"#),
"2"
);
assert_eq!(say(r#"print({"a": 1, "b": 2}.has("a"))"#), "true");
assert_eq!(say(r#"print({"a": 1, "b": 2}.has("z"))"#), "false");
}
#[test]
fn dict_keys_values() {
assert_eq!(say(r#"print({"a": 1, "b": 2}.keys())"#), r#"["a", "b"]"#);
assert_eq!(say(r#"print({"a": 1, "b": 2}.values())"#), "[1, 2]");
}
#[test]
fn builtin_range_1arg() {
assert_eq!(say("print(range(5))"), "[0, 1, 2, 3, 4]");
}
#[test]
fn builtin_range_2args() {
assert_eq!(say("print(range(2, 5))"), "[2, 3, 4]");
}
#[test]
fn builtin_range_3args() {
assert_eq!(say("print(range(0, 10, 3))"), "[0, 3, 6, 9]");
}
#[test]
fn builtin_range_reverse() {
assert_eq!(say("print(range(5, 0))"), "[5, 4, 3, 2, 1]");
}
#[test]
fn builtin_str() {
assert_eq!(say(r#"print(42.to_str())"#), "42");
assert_eq!(say(r#"print(true.to_str())"#), "true");
}
#[test]
fn builtin_int() {
assert_eq!(say("print(3.7.to_int())"), "3");
assert_eq!(say("print((-2.9).to_int())"), "-2");
}
#[test]
fn builtin_type() {
assert_eq!(say("print(42.type())"), "int");
assert_eq!(say("print(42.0.type())"), "number");
assert_eq!(say(r#"print("hi".type())"#), "string");
assert_eq!(say("print(true.type())"), "bool");
assert_eq!(say("print(none.type())"), "none");
assert_eq!(say("print([].type())"), "array");
}
#[test]
fn builtin_abs_min_max() {
assert_eq!(say("print((-5).abs())"), "5");
assert_eq!(say("print(3.min(7))"), "3");
assert_eq!(say("print(3.max(7))"), "7");
}
#[test]
fn builtin_len() {
assert_eq!(say(r#"print("hello".len())"#), "5");
assert_eq!(say("print([1, 2, 3].len())"), "3");
}
#[test]
fn builtin_inspect() {
assert_eq!(say(r#"print("hi".inspect())"#), r#""hi""#);
assert_eq!(say("print(42.inspect())"), "42");
}
#[test]
fn builtin_print_multi_args() {
let mut host = TestHost::new();
run(r#"print("a", "b", "c")"#, &mut host, &test_limits()).unwrap();
assert_eq!(host.prints.borrow().as_slice(), &["a b c"]);
}
#[test]
fn builtin_rand_deterministic() {
let a = say("print(rand(100))");
let b = say("print(rand(100))");
assert_eq!(a, b);
}
#[test]
fn error_division_by_zero() {
assert!(run_err("print(1 / 0)").contains("Division by zero"));
}
#[test]
fn error_type_mismatch_subtract() {
let msg = run_err(r#"print("a" - 1)"#);
assert!(msg.contains("Can't use `-`"));
}
#[test]
fn error_unknown_function() {
assert!(run_err("nope()").contains("not found"));
}
#[test]
fn error_infinite_loop_protection() {
let msg = run_err("while true { }");
assert!(msg.contains("too many steps"));
}
#[test]
fn error_break_outside_loop() {
assert!(run_err("break").contains("outside of a loop"));
}
#[test]
fn error_continue_outside_loop() {
assert!(run_err("continue").contains("outside of a loop"));
}
#[test]
fn parse_error_missing_rparen() {
assert!(parse_err("print(1").contains("Expected `)`"));
}
#[test]
fn parse_error_missing_rbrace() {
assert!(parse_err("if true {").contains("Expected `}`"));
}
#[test]
fn empty_program() {
let mut host = TestHost::new();
run("", &mut host, &test_limits()).unwrap();
assert!(host.prints.borrow().is_empty());
}
#[test]
fn trailing_comma_in_array() {
assert_eq!(say("print([1, 2, 3,])"), "[1, 2, 3]");
}
#[test]
fn trailing_comma_in_dict() {
assert_eq!(say(r#"print({"a": 1,}.len())"#), "1");
}
#[test]
fn none_value() {
assert_eq!(say("print(none)"), "none");
assert_eq!(say("print(none == none)"), "true");
}
#[test]
fn equality_across_types() {
assert_eq!(say("print(1 == true)"), "false");
assert_eq!(say(r#"print(0 == "")"#), "false");
assert_eq!(say("print(none == false)"), "false");
}
#[test]
fn dict_equality() {
assert_eq!(say(r#"print({"a": 1, "b": 2} == {"b": 2, "a": 1})"#), "true");
assert_eq!(say(r#"print({"a": 1} == {"a": 2})"#), "false");
assert_eq!(say(r#"print({"a": 1} == {"b": 1})"#), "false");
assert_eq!(say(r#"print({"a": 1} == {"a": 1, "b": 2})"#), "false");
assert_eq!(say(r#"print({"a": {"x": 1}} == {"a": {"x": 1}})"#), "true");
}
#[test]
fn nested_array_access() {
assert_eq!(say("let m = [[1, 2], [3, 4]]\nprint(m[1][0])"), "3");
}
#[test]
fn method_chain() {
assert_eq!(say(r#"print(" HELLO ".trim().lower())"#), "hello");
}
#[test]
fn parse_error_carries_column_info() {
let err = parse_err_full("let 42");
assert_eq!(err.line, Some(1), "err: {:?}", err);
assert_eq!(err.column, Some(5), "err: {:?}", err);
assert!(err.message.contains("Expected a name"));
}
#[test]
fn parse_error_renders_with_snippet_and_carat() {
let src = "let 42";
let err = parse_err_full(src);
let rendered = err.render(src);
assert!(rendered.contains("--> line 1:5"), "rendered:\n{}", rendered);
assert!(rendered.contains("let 42"));
assert!(rendered.contains(" ^"), "rendered:\n{}", rendered);
}
#[test]
fn parse_error_on_line_2_points_at_line_2() {
let src = "let x = 1\nlet = 2";
let err = parse_err_full(src);
assert_eq!(err.line, Some(2), "err: {:?}", err);
let rendered = err.render(src);
assert!(
rendered.contains("let = 2"),
"rendered:\n{}",
rendered
);
}
#[test]
fn runtime_error_renders_without_column() {
let src = "let x = 1 / 0";
let err = run_err_full(src);
assert_eq!(err.line, Some(1));
assert!(err.column.is_none());
let rendered = err.render(src);
assert!(rendered.contains("--> line 1"));
assert!(rendered.contains("let x = 1 / 0"));
assert!(!rendered.contains("^"), "rendered:\n{}", rendered);
}
fn parse_err_full(code: &str) -> BopError {
parse(code).unwrap_err()
}
fn run_err_full(code: &str) -> BopError {
let mut host = TestHost::new();
run(code, &mut host, &test_limits()).unwrap_err()
}
#[test]
fn typo_variable_suggests_closest_local() {
let err = run_err_full(
r#"let length = 5
print(lenght)"#,
);
assert!(err.message.contains("not found"), "err: {:?}", err);
assert_eq!(
err.friendly_hint.as_deref(),
Some("Did you mean `length`?")
);
}
#[test]
fn typo_variable_falls_back_when_nothing_close() {
let err = run_err_full("print(xylophone_constant)");
assert_eq!(
err.friendly_hint.as_deref(),
Some("Did you forget to create it with `let`?")
);
}
#[test]
fn typo_function_suggests_user_fn() {
let err = run_err_full(
r#"fn greet(name) { print("hi " + name) }
gret("world")"#,
);
assert!(err.message.contains("not found"));
assert_eq!(
err.friendly_hint.as_deref(),
Some("Did you mean `greet`?")
);
}
#[test]
fn typo_builtin_suggests_core_name() {
let err = run_err_full("rang(5)");
assert_eq!(
err.friendly_hint.as_deref(),
Some("Did you mean `range`?")
);
}
#[test]
fn typo_struct_field_at_access_suggests_declared() {
let err = run_err_full(
r#"struct Point { x, y }
let p = Point { x: 1, y: 2 }
print(p.z)"#,
);
assert!(err.message.contains("has no field `z`"));
assert_eq!(
err.friendly_hint.as_deref(),
Some("Did you mean `x`?")
);
}
#[test]
fn typo_struct_field_at_construction_suggests_declared() {
let err = run_err_full(
r#"struct Point { x, y }
let p = Point { x: 1, ya: 2 }"#,
);
assert!(err.message.contains("has no field `ya`"));
assert_eq!(
err.friendly_hint.as_deref(),
Some("Did you mean `y`?")
);
}
#[test]
fn typo_enum_variant_suggests_declared() {
let err = run_err_full(
r#"enum Shape { Circle(r), Rectangle { w, h } }
let s = Shape::Circel(5)"#,
);
assert!(err.message.contains("has no variant `Circel`"));
assert_eq!(
err.friendly_hint.as_deref(),
Some("Did you mean `Circle`?")
);
}
#[test]
fn typo_hint_renders_in_source_snippet() {
let src = r#"let length = 5
print(lenght)"#;
let err = run_err_full(src);
let rendered = err.render(src);
assert!(
rendered.contains("hint: Did you mean `length`?"),
"rendered:\n{}",
rendered
);
}
#[test]
fn comments_in_code() {
assert_eq!(
say(r#"// this is a comment
let x = 42 // inline comment
print(x)"#),
"42"
);
}
fn count(code: &str) -> u32 {
let stmts = parse(code).unwrap();
count_instructions(&stmts)
}
#[test]
fn count_simple_calls() {
assert_eq!(count("print(1)"), 1);
assert_eq!(count("print(1); print(2); print(3)"), 3);
}
#[test]
fn count_repeat() {
assert_eq!(count("repeat 7 { print(1) }"), 2);
}
#[test]
fn count_if() {
assert_eq!(count("if true { print(1) }"), 2);
assert_eq!(count("if true { print(1) } else { print(2) }"), 3);
}
#[test]
fn count_while() {
assert_eq!(count("while true { print(1) }"), 2);
}
#[test]
fn count_fn_skips_body() {
assert_eq!(count("fn go() { print(1); print(2); print(3) }\ngo()"), 2);
}
#[test]
fn count_format_independent() {
let one_line = count("repeat 7 { print(1) }");
let multi_line = count("repeat 7 {\n print(1)\n}");
assert_eq!(one_line, multi_line);
assert_eq!(one_line, 2);
}
#[test]
fn count_nested() {
assert_eq!(count("repeat 7 { if true { print(1) } }"), 3);
}
#[test]
fn count_empty_program() {
assert_eq!(count(""), 0);
}
#[test]
fn if_block_scope() {
assert!(
run_err(
r#"if true { let inner = 1 }
print(inner)"#
)
.contains("not found")
);
}
#[test]
fn for_loop_var_scoped() {
assert!(
run_err(
r#"for item in [1, 2] { let x = item }
print(item)"#
)
.contains("not found")
);
}
#[test]
fn fizzbuzz() {
assert_eq!(
say(r#"let result = []
for i in range(1, 16) {
if i % 15 == 0 {
result.push("FizzBuzz")
} else if i % 3 == 0 {
result.push("Fizz")
} else if i % 5 == 0 {
result.push("Buzz")
} else {
result.push(i.to_str())
}
}
print(result.join(", "))"#),
"1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz"
);
}
#[test]
fn nested_function_calls() {
assert_eq!(
say(r#"fn square(n) { return n * n }
fn sum_squares(a, b) { return square(a) + square(b) }
print(sum_squares(3, 4))"#),
"25"
);
}
#[test]
fn array_manipulation_program() {
assert_eq!(
say(r#"let data = [5, 2, 8, 1, 9, 3]
data.sort()
let top3 = data.slice(3, 6)
print(top3.join(", "))"#),
"5, 8, 9"
);
}
#[test]
fn truthy_values() {
assert_eq!(say("print(if 1 { \"yes\" } else { \"no\" })"), "yes");
assert_eq!(say(r#"print(if "x" { "yes" } else { "no" })"#), "yes");
assert_eq!(say("print(if [1] { \"yes\" } else { \"no\" })"), "yes");
}
#[test]
fn falsy_values() {
assert_eq!(say("print(if 0 { \"yes\" } else { \"no\" })"), "no");
assert_eq!(say("print(if false { \"yes\" } else { \"no\" })"), "no");
assert_eq!(say("print(if none { \"yes\" } else { \"no\" })"), "no");
assert_eq!(say(r#"print(if "" { "yes" } else { "no" })"#), "no");
}
#[test]
fn display_whole_number_as_int() {
assert_eq!(say("print(5.0)"), "5");
}
#[test]
fn display_float_with_decimals() {
assert_eq!(say("print(3.14)"), "3.14");
}
#[test]
fn safety_infinite_loop_halts() {
let msg = run_err_with_limits("while true { }", tight_limits());
assert!(msg.contains("too many steps"), "got: {}", msg);
}
#[test]
fn safety_memory_bomb_string_doubling() {
let msg = run_err_with_limits(
r#"let s = "aaaaaaaaaa"
repeat 100 { s = s + s }"#,
tight_limits(),
);
assert!(msg.contains("Memory limit"), "got: {}", msg);
}
#[test]
fn safety_memory_bomb_array_growth() {
let msg = run_err_with_limits(
r#"let arr = []
repeat 500 {
arr.push("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
}"#,
tight_limits(),
);
assert!(
msg.contains("Memory limit") || msg.contains("too many steps"),
"got: {}", msg
);
}
#[test]
fn safety_deep_recursion_halts() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| run_err_with_limits("fn f() { f() }\nf()", tight_limits()))
.expect("spawn recursion test thread");
let msg = handle.join().expect("recursion test thread panicked");
assert!(
msg.contains("nested function calls") || msg.contains("recursion"),
"got: {}", msg
);
}
#[test]
fn safety_deep_parse_nesting() {
let code = "(".repeat(200) + "1" + &")".repeat(200);
let msg = parse(&code).unwrap_err().message;
assert!(msg.contains("nested too deeply"), "got: {}", msg);
}
#[test]
fn safety_string_repeat_bomb() {
let msg = run_err_with_limits(r#"let s = "x" * 999999"#, tight_limits());
assert!(msg.contains("Memory limit"), "got: {}", msg);
}
#[test]
fn safety_string_concat_bomb() {
let msg = run_err_with_limits(
r#"let s = "x" * 1000
repeat 100 { s = s + s }"#,
tight_limits(),
);
assert!(msg.contains("Memory limit"), "got: {}", msg);
}
#[test]
fn safety_array_concat_bomb() {
let msg = run_err_with_limits(
r#"let a = range(100)
repeat 50 { a = a + a }"#,
tight_limits(),
);
assert!(
msg.contains("Memory limit") || msg.contains("too many steps"),
"got: {}", msg
);
}
#[test]
fn safety_for_in_large_string() {
let msg = run_err_with_limits(
r#"let s = "x" * 10000
for c in s { }"#,
tight_limits(),
);
assert!(
msg.contains("too many steps") || msg.contains("Memory limit"),
"got: {}", msg
);
}
#[test]
fn safety_demo_limits_step_bound() {
let msg = run_err_with_limits(
"let i = 0\nwhile true { i = i + 1 }",
BopLimits::demo(),
);
assert!(msg.contains("too many steps"), "got: {}", msg);
}
#[test]
fn safety_demo_limits_memory_bound() {
let msg = run_err_with_limits(
r#"let s = "x" * 1100000
print(s)"#,
BopLimits::demo(),
);
assert!(msg.contains("Memory limit"), "got: {}", msg);
}
#[test]
fn safety_nested_loop_step_bound() {
let msg = run_err_with_limits("repeat 100 { repeat 100 { let x = 1 } }", tight_limits());
assert!(msg.contains("too many steps"), "got: {}", msg);
}
#[test]
fn safety_string_split_bomb() {
let msg = run_err_with_limits(
r#"let s = "abababababab" * 2000
let parts = s.split("a")
let x = 1"#,
tight_limits(),
);
assert!(
msg.contains("Memory limit") || msg.contains("too many steps"),
"got: {}", msg
);
}
#[test]
fn safety_join_bomb() {
let msg = run_err_with_limits(
r#"let a = []
repeat 400 { a.push("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }
let s = a.join("")
let x = 1"#,
tight_limits(),
);
assert!(
msg.contains("Memory limit") || msg.contains("too many steps"),
"got: {}", msg
);
}
#[test]
fn safety_range_hard_cap() {
let msg = run_err_with_limits(
r#"let a = range(100000)
let x = 1"#,
tight_limits(),
);
assert!(
msg.contains("Memory limit") || msg.contains("too many steps"),
"got: {}", msg
);
}
#[test]
fn safety_array_method_doubling() {
let msg = run_err_with_limits(
r#"let a = []
repeat 400 { a.push("aaaaaaaaaaaaaaaaaaaaaa") }
a.reverse()
let x = 1"#,
tight_limits(),
);
assert!(
msg.contains("Memory limit") || msg.contains("too many steps"),
"got: {}", msg
);
}
#[test]
fn safety_preflight_catches() {
let limits = BopLimits {
max_steps: 500,
max_memory: 32 * 1024,
};
let msg = run_err_with_limits(r#"let s = "x" * 40000"#, limits);
assert!(msg.contains("Memory limit"), "got: {}", msg);
}
#[test]
fn safety_bounded_overshoot() {
let limits = BopLimits {
max_steps: 500,
max_memory: 64 * 1024,
};
let mut host = TestHost::new();
let result = run(
r#"let s = "abababab" * 1000
let parts = s.split("a")"#,
&mut host,
&limits,
);
assert!(result.is_ok(), "Expected success (bounded overshoot), got error");
}
#[test]
fn safety_dict_growth_tracked() {
let msg = run_err_with_limits(
r#"let d = {}
repeat 400 {
d[d.len().to_str()] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
let x = 1"#,
tight_limits(),
);
assert!(
msg.contains("Memory limit") || msg.contains("too many steps"),
"got: {}", msg
);
}
struct CustomHost {
prints: Vec<String>,
}
impl BopHost for CustomHost {
fn call(&mut self, name: &str, args: &[Value], line: u32) -> Option<Result<Value, BopError>> {
match name {
"greet" => {
if args.len() != 1 {
return Some(Err(BopError {
line: Some(line),
column: None,
message: "greet() needs 1 argument".into(),
friendly_hint: None,
is_fatal: false,
is_try_return: false,
}));
}
Some(Ok(Value::new_str(format!("Hello, {}!", args[0]))))
}
_ => None,
}
}
fn on_print(&mut self, message: &str) {
self.prints.push(message.to_string());
}
fn function_hint(&self) -> &str {
"Available: greet(name)"
}
}
#[test]
fn host_custom_builtin() {
let mut host = CustomHost { prints: vec![] };
run(r#"print(greet("world"))"#, &mut host, &BopLimits::standard()).unwrap();
assert_eq!(host.prints, vec!["Hello, world!"]);
}
#[test]
fn host_function_hint() {
let mut host = CustomHost { prints: vec![] };
let err = run("unknown()", &mut host, &BopLimits::standard()).unwrap_err();
assert!(err.message.contains("not found"));
}
#[test]
fn match_literal_arms() {
assert_eq!(
say(r#"let x = 2
let out = match x {
1 => "one",
2 => "two",
3 => "three",
_ => "other",
}
print(out)"#),
"two"
);
}
#[test]
fn match_falls_through_to_wildcard() {
assert_eq!(
say(r#"let x = 42
print(match x {
1 => "one",
_ => "other",
})"#),
"other"
);
}
#[test]
fn match_no_arm_errors() {
let err = run_err(r#"let x = 5
match x { 1 => "a", 2 => "b" }"#);
assert!(err.contains("No match arm matched"), "got: {}", err);
}
#[test]
fn match_binding_captures_scrutinee() {
assert_eq!(
say(r#"print(match 42 {
x => x + 1,
})"#),
"43"
);
}
#[test]
fn match_guard_accepts() {
assert_eq!(
say(r#"print(match 7 {
n if n > 10 => "big",
n if n > 0 => "small",
_ => "zero or less",
})"#),
"small"
);
}
#[test]
fn match_guard_rejects_continues() {
assert_eq!(
say(r#"print(match 5 {
n if n < 0 => "neg",
n if n > 100 => "huge",
_ => "mid",
})"#),
"mid"
);
}
#[test]
fn match_or_pattern() {
assert_eq!(
say(r#"let x = 3
print(match x {
1 | 2 | 3 => "small",
_ => "other",
})"#),
"small"
);
}
#[test]
fn match_enum_unit_variant() {
assert_eq!(
say(r#"enum E { A, B, C }
print(match E::B {
E::A => "a",
E::B => "b",
E::C => "c",
})"#),
"b"
);
}
#[test]
fn match_enum_tuple_binds() {
assert_eq!(
say(r#"enum Shape { Circle(r), Square(s), Empty }
let s = Shape::Circle(5)
print(match s {
Shape::Circle(r) => r * 2,
Shape::Square(s) => s * s,
Shape::Empty => 0,
})"#),
"10"
);
}
#[test]
fn match_enum_struct_variant_binds() {
assert_eq!(
say(r#"enum Shape { Rect { w, h }, Empty }
let r = Shape::Rect { w: 4, h: 3 }
print(match r {
Shape::Rect { w, h } => w * h,
Shape::Empty => 0,
})"#),
"12"
);
}
#[test]
fn match_struct_destructure() {
assert_eq!(
say(r#"struct Point { x, y }
let p = Point { x: 7, y: 3 }
print(match p {
Point { x, y } => x + y,
})"#),
"10"
);
}
#[test]
fn match_struct_partial_with_rest() {
assert_eq!(
say(r#"struct Triple { a, b, c }
let t = Triple { a: 1, b: 2, c: 3 }
print(match t {
Triple { b, .. } => b,
})"#),
"2"
);
}
#[test]
fn match_nested_pattern() {
assert_eq!(
say(r#"enum FileError { NotFound(path), Permission(path), Other }
enum Result { Ok(value), Err(error) }
let r = Result::Err(FileError::NotFound("/etc/passwd"))
print(match r {
Result::Ok(v) => v,
Result::Err(FileError::NotFound(p)) => p,
Result::Err(FileError::Permission(p)) => p,
Result::Err(FileError::Other) => "other",
})"#),
"/etc/passwd"
);
}
#[test]
fn match_array_exact() {
assert_eq!(
say(r#"let a = [1, 2, 3]
print(match a {
[] => "empty",
[x] => "one",
[x, y] => "two",
[x, y, z] => x + y + z,
_ => "long",
})"#),
"6"
);
}
#[test]
fn match_array_with_rest() {
assert_eq!(
say(r#"let a = [10, 20, 30, 40, 50]
print(match a {
[head, ..rest] => rest,
_ => [],
})"#),
"[20, 30, 40, 50]"
);
}
#[test]
fn match_array_with_ignored_rest() {
assert_eq!(
say(r#"let a = [10, 20, 30]
print(match a {
[first, ..] => first,
_ => 0,
})"#),
"10"
);
}
#[test]
fn match_binding_scope_limited_to_arm() {
assert!(
run_err(r#"let v = 5
match v { x => print(x) }
print(x)"#)
.contains("not found")
);
}
#[test]
fn match_negative_literal() {
assert_eq!(
say(r#"print(match -3 {
-3 => "neg three",
_ => "other",
})"#),
"neg three"
);
}
#[test]
fn match_string_literal() {
assert_eq!(
say(r#"let s = "hello"
print(match s {
"hi" => 1,
"hello" => 2,
_ => 0,
})"#),
"2"
);
}
#[test]
fn match_bool_none() {
assert_eq!(
say(r#"print(match true {
true => "t",
false => "f",
})"#),
"t"
);
assert_eq!(
say(r#"print(match none {
none => "n",
_ => "other",
})"#),
"n"
);
}
#[test]
fn try_unwraps_ok_variant() {
assert_eq!(
say(r#"enum Result { Ok(v), Err(e) }
fn doit() {
let v = try Result::Ok(42)
return v
}
print(doit())"#),
"42"
);
}
#[test]
fn try_propagates_err_variant() {
assert_eq!(
say(r#"enum Result { Ok(v), Err(e) }
fn doit() {
let v = try Result::Err("boom")
return Result::Ok(v)
}
let r = doit()
print(match r {
Result::Ok(v) => v,
Result::Err(e) => e,
})"#),
"boom"
);
}
#[test]
fn try_sentinel_uses_flag_not_message_string() {
let mut host = TestHost::new();
let err = run(
"print(1 / 0)",
&mut host,
&test_limits(),
)
.unwrap_err();
assert_eq!(err.message, "Division by zero");
assert!(!err.is_try_return, "got: {:?}", err);
assert!(!err.is_fatal, "got: {:?}", err);
}
#[test]
fn try_chains_through_nested_calls() {
assert_eq!(
say(r#"enum Result { Ok(v), Err(e) }
fn leaf() { return Result::Err("leaf-err") }
fn middle() {
let v = try leaf()
return Result::Ok(v + 1)
}
fn top() {
let v = try middle()
return Result::Ok(v * 2)
}
print(match top() {
Result::Ok(v) => v,
Result::Err(e) => e,
})"#),
"leaf-err"
);
}
#[test]
fn user_result_with_unit_ok_coexists_with_builtin() {
assert_eq!(
say(r#"enum Result { Ok, Err(e) }
fn doit() {
let v = try Result::Ok
return v.type()
}
print(doit())"#),
"none"
);
}
#[test]
fn try_inside_lambda_returns_from_lambda_only() {
assert_eq!(
say(r#"enum Result { Ok(v), Err(e) }
let f = fn() {
let v = try Result::Err("inner")
return Result::Ok(v)
}
let r = f()
print("after lambda")
print(match r {
Result::Ok(_) => "ok",
Result::Err(e) => e,
})"#),
"inner"
);
}
#[test]
fn try_at_top_level_on_err_value_errors() {
let msg = run_err(
r#"enum Result { Ok(v), Err(e) }
let r = try Result::Err("boom")"#,
);
assert!(msg.contains("top-level"), "got: {}", msg);
}
#[test]
fn try_on_non_result_errors() {
let msg = run_err(
r#"fn doit() {
let v = try 42
return v
}
doit()"#,
);
assert!(msg.contains("Result-shaped"), "got: {}", msg);
}
#[test]
fn try_ok_tuple_wrong_arity_errors() {
let msg = run_err(
r#"enum Result { Ok(a, b), Err(e) }
fn doit() {
let v = try Result::Ok(1, 2)
return v
}
doit()"#,
);
assert!(
msg.contains("Ok variant must carry exactly one"),
"got: {}",
msg
);
}
#[test]
fn try_in_for_loop_short_circuits() {
assert_eq!(
say(r#"enum Result { Ok(v), Err(e) }
fn lookup(i) {
if i == 2 { return Result::Err("stop") }
return Result::Ok(i * 10)
}
fn sum_until_err() {
let total = 0
for i in range(5) {
let v = try lookup(i)
total = total + v
}
return Result::Ok(total)
}
print(match sum_until_err() {
Result::Ok(v) => v,
Result::Err(e) => e,
})"#),
"stop"
);
}
#[test]
fn try_threaded_through_nested_fn_composition() {
assert_eq!(
say(r#"enum Result { Ok(v), Err(e) }
fn compute(input) {
if input < 0 { return Result::Err("negative") }
return Result::Ok(input * 2)
}
fn with_try(x) {
let doubled = try compute(x)
return Result::Ok(doubled + 1)
}
print(match with_try(5) { Result::Ok(v) => v, Result::Err(_) => -1 })
print(match with_try(-1) { Result::Ok(_) => "ok", Result::Err(e) => e })"#),
"negative"
);
}
#[test]
fn try_call_wraps_successful_return_in_ok() {
assert_eq!(
say(r#"let r = try_call(fn() { return 42 })
print(match r {
Result::Ok(v) => v,
Result::Err(_) => -1,
})"#),
"42"
);
}
#[test]
fn try_call_wraps_non_fatal_error_in_err() {
assert_eq!(
say(r#"let r = try_call(fn() { return 1 / 0 })
print(match r {
Result::Ok(_) => "ok",
Result::Err(e) => e.message,
})"#),
"Division by zero"
);
}
#[test]
fn try_call_runtime_error_carries_line_number() {
assert_eq!(
say(r#"let r = try_call(fn() {
let x = 1
return x / 0
})
print(match r {
Result::Ok(_) => -1,
Result::Err(e) => e.line,
})"#),
"3"
);
}
#[test]
fn try_call_step_limit_error_is_fatal_and_bypasses_wrap() {
let tight = BopLimits {
max_steps: 200,
max_memory: 1 << 20,
};
let mut host = TestHost::new();
let err = run(
r#"let r = try_call(fn() {
while true { }
})
print("should never run")"#,
&mut host,
&tight,
)
.unwrap_err();
assert!(err.is_fatal, "expected fatal: {}", err.message);
assert!(
err.message.contains("too many steps"),
"got: {}",
err.message
);
assert!(host.prints.borrow().is_empty());
}
#[test]
fn try_call_plays_with_try_operator_to_chain_errors() {
assert_eq!(
say(r#"fn risky(x) {
let arr = [1, 2]
return arr[x] // out-of-bounds when x > 1
}
let r = try_call(fn() { return risky(5) })
print(match r {
Result::Ok(_) => "ok",
Result::Err(e) => e.message,
})"#),
"Index 5 is out of bounds (array has 2 items)"
);
}
#[test]
fn try_call_errors_on_wrong_arg_count() {
let msg = run_err("try_call()");
assert!(
msg.contains("try_call` expects 1"),
"got: {}",
msg
);
}
#[test]
fn try_call_errors_on_non_function_arg() {
let msg = run_err("try_call(42)");
assert!(
msg.contains("try_call` expects a function"),
"got: {}",
msg
);
}
#[test]
fn try_call_result_ok_is_matchable_even_without_declared_type() {
assert_eq!(
say(r#"let r = try_call(fn() { return "yay" })
print(match r {
Result::Ok(v) => v + "!",
Result::Err(_) => "bad",
})"#),
"yay!"
);
}
#[test]
fn try_call_nested_outer_sees_ok_of_inner_err() {
assert_eq!(
say(r#"let r = try_call(fn() {
let inner = try_call(fn() { return 1 / 0 })
return inner
})
print(match r {
Result::Ok(Result::Err(e)) => e.message,
Result::Ok(Result::Ok(_)) => "inner ok?",
Result::Err(_) => "outer caught",
})"#),
"Division by zero"
);
}
#[test]
fn int_literal_produces_int_value() {
assert_eq!(say("print(42.type())"), "int");
assert_eq!(say("print((-3).type())"), "int");
assert_eq!(say("print(0.type())"), "int");
}
#[test]
fn float_literal_produces_number_value() {
assert_eq!(say("print(42.0.type())"), "number");
assert_eq!(say("print(3.14.type())"), "number");
assert_eq!(say("print((-0.5).type())"), "number");
}
#[test]
fn int_int_arithmetic_stays_int() {
assert_eq!(say("print((1 + 2).type())"), "int");
assert_eq!(say("print(1 + 2)"), "3");
assert_eq!(say("print(10 - 4)"), "6");
assert_eq!(say("print(3 * 4)"), "12");
assert_eq!(say("print(10 % 3)"), "1");
}
#[test]
fn division_slash_always_returns_number() {
assert_eq!(say("print((10 / 3).type())"), "number");
assert_eq!(say("print(10 / 4)"), "2.5");
assert_eq!(say("print((10 / 5).type())"), "number");
}
#[test]
fn int_division_via_int_of_quotient() {
assert_eq!(say("print((10 / 3).to_int().type())"), "int");
assert_eq!(say("print((10 / 3).to_int())"), "3");
assert_eq!(say("print((-7 / 2).to_int())"), "-3");
assert_eq!(say("print((10 / -3).to_int())"), "-3");
}
#[test]
fn int_number_mixed_widens_to_number() {
assert_eq!(say("print((1 + 2.0).type())"), "number");
assert_eq!(say("print(1 + 2.0)"), "3");
assert_eq!(say("print(3 * 0.5)"), "1.5");
assert_eq!(say("print((2.0 - 1).type())"), "number");
}
#[test]
fn int_comparison_uses_exact_integer_ordering() {
assert_eq!(say("print(10 < 20)"), "true");
assert_eq!(say("print(10 == 10)"), "true");
assert_eq!(say("print(1 == 1.0)"), "true");
assert_eq!(say("print(2 > 1.5)"), "true");
}
#[test]
fn division_by_zero_errors() {
let msg = run_err("print(10 / 0)");
assert!(msg.contains("Division by zero"), "got: {}", msg);
}
#[test]
fn int_overflow_on_add_errors() {
let msg = run_err("print(9223372036854775807 + 1)");
assert!(msg.contains("Integer overflow"), "got: {}", msg);
}
#[test]
fn int_overflow_on_neg_of_i64_min_errors() {
let msg = run_err(
"let x = -9223372036854775807 - 1\nprint(-x)",
);
assert!(msg.contains("overflow"), "got: {}", msg);
}
#[test]
fn int_builtin_converts_to_int() {
assert_eq!(say("print(3.7.to_int())"), "3");
assert_eq!(say("print(3.7.to_int().type())"), "int");
assert_eq!(say(r#"print("42".to_int())"#), "42");
assert_eq!(say(r#"print("42".to_int().type())"#), "int");
assert_eq!(say(r#"print("3.7".to_int())"#), "3");
}
#[test]
fn float_builtin_converts_to_number() {
assert_eq!(say("print(42.to_float())"), "42");
assert_eq!(say("print(42.to_float().type())"), "number");
assert_eq!(say(r#"print("3.14".to_float())"#), "3.14");
}
#[test]
fn len_returns_int() {
assert_eq!(say(r#"print("hi".len().type())"#), "int");
assert_eq!(say("print([1, 2, 3].len().type())"), "int");
}
#[test]
fn range_produces_int_elements() {
assert_eq!(say("print((range(3)[0]).type())"), "int");
}
#[test]
fn array_index_accepts_int_and_float() {
assert_eq!(say("let a = [10, 20]\nprint(a[0])"), "10");
assert_eq!(say("let a = [10, 20]\nprint(a[0.0])"), "10");
}
#[test]
fn int_match_literal_pattern() {
assert_eq!(
say(r#"let x = 2
print(match x {
1 => "one",
2 => "two",
_ => "other",
})"#),
"two"
);
}
#[test]
fn repeat_accepts_int() {
assert_eq!(
say(r#"let n = 0
repeat 5 { n = n + 1 }
print(n)"#),
"5"
);
}
#[test]
fn int_overflow_literal_parse_errors() {
let msg = parse_err("let x = 99999999999999999999");
assert!(msg.contains("out of range"), "got: {}", msg);
}
struct ModuleHost {
prints: RefCell<Vec<String>>,
modules: std::collections::HashMap<String, String>,
resolve_counts: RefCell<std::collections::HashMap<String, u32>>,
}
impl ModuleHost {
fn new(modules: &[(&str, &str)]) -> Self {
let mut map = std::collections::HashMap::new();
for (name, source) in modules {
map.insert((*name).to_string(), (*source).to_string());
}
Self {
prints: RefCell::new(Vec::new()),
modules: map,
resolve_counts: RefCell::new(std::collections::HashMap::new()),
}
}
fn prints(&self) -> Vec<String> {
self.prints.borrow().clone()
}
fn resolve_count(&self, name: &str) -> u32 {
*self
.resolve_counts
.borrow()
.get(name)
.unwrap_or(&0)
}
}
impl BopHost for ModuleHost {
fn call(
&mut self,
_name: &str,
_args: &[Value],
_line: u32,
) -> Option<Result<Value, BopError>> {
None
}
fn on_print(&mut self, message: &str) {
self.prints.borrow_mut().push(message.to_string());
}
fn resolve_module(&mut self, name: &str) -> Option<Result<String, BopError>> {
*self
.resolve_counts
.borrow_mut()
.entry(name.to_string())
.or_insert(0) += 1;
self.modules.get(name).cloned().map(Ok)
}
}
#[test]
fn import_brings_let_binding_into_scope() {
let mut host = ModuleHost::new(&[("math", "let pi = 3")]);
run(
r#"use math
print(pi)"#,
&mut host,
&BopLimits::standard(),
)
.unwrap();
assert_eq!(host.prints(), vec!["3"]);
}
#[test]
fn import_brings_fn_into_scope() {
let mut host = ModuleHost::new(&[(
"math",
r#"fn square(n) { return n * n }
let pi = 3"#,
)]);
run(
r#"use math
print(square(5))
print(pi)"#,
&mut host,
&BopLimits::standard(),
)
.unwrap();
assert_eq!(host.prints(), vec!["25", "3"]);
}
#[test]
fn import_dotted_path_passes_through_to_host() {
let mut host = ModuleHost::new(&[("std.math", "let e = 2")]);
run(
r#"use std.math
print(e)"#,
&mut host,
&BopLimits::standard(),
)
.unwrap();
assert_eq!(host.prints(), vec!["2"]);
assert_eq!(host.resolve_count("std.math"), 1);
}
#[test]
fn import_module_not_found_errors() {
let mut host = ModuleHost::new(&[]);
let err = run("use nope", &mut host, &BopLimits::standard())
.unwrap_err();
assert!(
err.message.contains("Module `nope` not found"),
"got: {}",
err.message
);
}
#[test]
fn import_cache_resolves_once() {
let mut host = ModuleHost::new(&[("m", "let x = 1")]);
run(
r#"use m
use m
print(x)"#,
&mut host,
&BopLimits::standard(),
)
.unwrap();
assert_eq!(host.prints(), vec!["1"]);
assert_eq!(host.resolve_count("m"), 1);
}
#[test]
fn import_module_can_import_other_modules() {
let mut host = ModuleHost::new(&[
("a", "use b\nlet doubled_pi = pi + pi"),
("b", "let pi = 3"),
]);
run(
r#"use a
print(doubled_pi)"#,
&mut host,
&BopLimits::standard(),
)
.unwrap();
assert_eq!(host.prints(), vec!["6"]);
}
#[test]
fn import_circular_detected() {
let mut host = ModuleHost::new(&[
("a", "use b\nlet x = 1"),
("b", "use a\nlet y = 2"),
]);
let err = run("use a", &mut host, &BopLimits::standard())
.unwrap_err();
assert!(
err.message.contains("Circular import"),
"got: {}",
err.message
);
}
#[test]
fn glob_use_shadowing_is_a_warning_first_wins() {
let mut host = ModuleHost::new(&[("m", "let x = 99")]);
run(
r#"let x = 1
use m
print(x)"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["1".to_string()]);
}
#[test]
fn use_selective_form_pulls_only_listed_names() {
let mut host = ModuleHost::new(&[("m", "let a = 1\nlet b = 2\nlet c = 3")]);
run(
r#"use m.{a, c}
print(a)
print(c)"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["1".to_string(), "3".to_string()]);
}
#[test]
fn use_selective_unknown_name_errors() {
let mut host = ModuleHost::new(&[("m", "let a = 1")]);
let err = run(
r#"use m.{b}"#,
&mut host,
&BopLimits::standard(),
)
.unwrap_err();
assert!(
err.message.contains("isn't exported"),
"got: {}",
err.message
);
}
#[test]
fn use_alias_binds_module_value() {
let mut host = ModuleHost::new(&[(
"m",
"let pi = 3\nfn double(n) { return n + n }",
)]);
run(
r#"use m as m
print(m.pi)
print(m.double(7))"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["3".to_string(), "14".to_string()]);
}
#[test]
fn use_alias_selective_form() {
let mut host = ModuleHost::new(&[("m", "let a = 1\nlet b = 2\nlet c = 3")]);
run(
r#"use m.{a, c} as m
print(m.a)
print(m.c)"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["1".to_string(), "3".to_string()]);
}
#[test]
fn use_alias_rejects_missing_module_field() {
let mut host = ModuleHost::new(&[("m", "let a = 1")]);
let err = run(
r#"use m as m
print(m.b)"#,
&mut host,
&BopLimits::standard(),
)
.unwrap_err();
assert!(
err.message.contains("isn't exported"),
"got: {}",
err.message
);
}
#[test]
fn glob_skips_underscore_prefixed_exports() {
let mut host = ModuleHost::new(&[("m", "let public = 1\nlet _private = 2")]);
let err = run(
r#"use m
print(_private)"#,
&mut host,
&BopLimits::standard(),
)
.unwrap_err();
assert!(
err.message.contains("_private"),
"expected `_private not found`, got: {}",
err.message
);
}
#[test]
fn selective_form_can_reach_underscore_prefixed_names() {
let mut host = ModuleHost::new(&[("m", "let _private = 42")]);
run(
r#"use m.{_private}
print(_private)"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["42".to_string()]);
}
#[test]
fn alias_exposes_underscore_prefixed_names() {
let mut host = ModuleHost::new(&[("m", "let _private = 7")]);
run(
r#"use m as m
print(m._private)"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["7".to_string()]);
}
#[test]
fn alias_namespaced_struct_literal() {
let mut host = ModuleHost::new(&[(
"g",
"struct Entity { id, hp }\nfn spawn(id) { return Entity { id: id, hp: 100 } }",
)]);
run(
r#"use g as g
let e = g.Entity { id: 1, hp: 50 }
print(e.id)
print(e.hp)"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["1".to_string(), "50".to_string()]);
}
#[test]
fn alias_namespaced_variant_ctor() {
let mut host = ModuleHost::new(&[(
"r",
"enum Result { Ok(v), Err(e) }",
)]);
run(
r#"use r as r
let v = r.Result::Ok(42)
match v {
r.Result::Ok(n) => print(n),
r.Result::Err(_) => print("err"),
}"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["42".to_string()]);
}
#[test]
fn alias_module_value_is_a_module_type() {
let mut host = ModuleHost::new(&[("m", "let x = 1")]);
run(
r#"use m as mm
print(mm.type())"#,
&mut host,
&BopLimits::standard(),
)
.expect("run ok");
assert_eq!(host.prints(), vec!["module".to_string()]);
}
#[test]
fn struct_decl_and_construct() {
assert_eq!(
say(r#"struct Point { x, y }
let p = Point { x: 3, y: 4 }
print(p.x)
print(p.y)"#),
"4"
);
}
#[test]
fn struct_display_shows_type_name_and_fields() {
assert_eq!(
say(r#"struct Point { x, y }
let p = Point { x: 3, y: 4 }
print(p)"#),
"Point { x: 3, y: 4 }"
);
}
#[test]
fn struct_fields_respect_declaration_order() {
assert_eq!(
say(r#"struct Point { x, y }
let p = Point { y: 4, x: 3 }
print(p)"#),
"Point { x: 3, y: 4 }"
);
}
#[test]
fn struct_equality_is_structural() {
assert_eq!(
say(r#"struct Point { x, y }
let a = Point { x: 1, y: 2 }
let b = Point { x: 1, y: 2 }
print(a == b)"#),
"true"
);
assert_eq!(
say(r#"struct Point { x, y }
let a = Point { x: 1, y: 2 }
let b = Point { x: 1, y: 3 }
print(a == b)"#),
"false"
);
}
#[test]
fn struct_different_types_never_equal() {
assert_eq!(
say(r#"struct A { x }
struct B { x }
let a = A { x: 1 }
let b = B { x: 1 }
print(a == b)"#),
"false"
);
}
#[test]
fn struct_type_name_is_struct() {
assert_eq!(
say(r#"struct Foo { a }
print(Foo { a: 1 }.type())"#),
"struct"
);
}
#[test]
fn struct_missing_field_errors() {
let err = run_err(r#"struct Point { x, y }
let p = Point { x: 1 }"#);
assert!(err.contains("Missing field"), "got: {}", err);
}
#[test]
fn struct_extra_field_errors() {
let err = run_err(r#"struct Point { x, y }
let p = Point { x: 1, y: 2, z: 3 }"#);
assert!(err.contains("has no field"), "got: {}", err);
}
#[test]
fn struct_duplicate_field_errors() {
let err = run_err(r#"struct Point { x, y }
let p = Point { x: 1, x: 2, y: 3 }"#);
assert!(err.contains("specified twice"), "got: {}", err);
}
#[test]
fn struct_undeclared_type_errors() {
let err = run_err(r#"let p = Nope { x: 1 }"#);
assert!(err.contains("not declared"), "got: {}", err);
}
#[test]
fn struct_field_access_missing_errors() {
let err = run_err(r#"struct Point { x, y }
let p = Point { x: 1, y: 2 }
print(p.z)"#);
assert!(err.contains("no field"), "got: {}", err);
}
#[test]
fn struct_field_access_on_non_struct_errors() {
let err = run_err("let x = 42\nprint(x.value)");
assert!(err.contains("Can't read field"), "got: {}", err);
}
#[test]
fn struct_duplicate_decl_errors() {
let err = run_err(r#"struct Foo { x }
struct Foo { y }"#);
assert!(err.contains("already declared"), "got: {}", err);
}
#[test]
fn struct_nested() {
assert_eq!(
say(r#"struct Inner { v }
struct Outer { name, inner }
let o = Outer { name: "nest", inner: Inner { v: 42 } }
print(o.inner.v)"#),
"42"
);
}
#[test]
fn struct_in_array_and_iteration() {
assert_eq!(
say(r#"struct Item { name, qty }
let cart = [Item { name: "apple", qty: 3 }, Item { name: "banana", qty: 2 }]
let total = 0
for i in cart { total += i.qty }
print(total)"#),
"5"
);
}
#[test]
fn struct_literal_disallowed_in_if_condition_parses() {
let err = run_err("if Foo { print(\"hi\") }");
assert!(err.contains("not found"), "got: {}", err);
}
#[test]
fn struct_literal_disallowed_in_for_iterable() {
assert_eq!(
say("let arr = [1, 2, 3]\nlet sum = 0\nfor x in arr { sum += x }\nprint(sum)"),
"6"
);
}
#[test]
fn struct_literal_ok_in_let_rhs() {
assert_eq!(
say(r#"struct P { x }
let p = P { x: 7 }
print(p.x)"#),
"7"
);
}
#[test]
fn struct_field_assign_basic() {
assert_eq!(
say(r#"struct Point { x, y }
let p = Point { x: 1, y: 2 }
p.x = 99
print(p.x)
print(p.y)"#),
"2"
);
}
#[test]
fn struct_field_compound_assign() {
assert_eq!(
say(r#"struct Counter { n }
let c = Counter { n: 10 }
c.n += 5
c.n *= 2
print(c.n)"#),
"30"
);
}
#[test]
fn struct_field_assign_unknown_field_errors() {
let err = run_err(r#"struct P { x }
let p = P { x: 1 }
p.y = 99"#);
assert!(err.contains("no field"), "got: {}", err);
}
#[test]
fn struct_field_assign_on_non_struct_errors() {
let err = run_err(r#"let x = 5
x.field = 1"#);
assert!(err.contains("Can't assign to field"), "got: {}", err);
}
#[test]
fn struct_field_assign_chain_via_intermediate_var() {
assert_eq!(
say(r#"struct Inner { v }
struct Outer { inner }
let o = Outer { inner: Inner { v: 1 } }
let i = o.inner
i.v = 99
o.inner = i
print(o.inner.v)"#),
"99"
);
}
#[test]
fn enum_unit_variant_basic() {
assert_eq!(
say(r#"enum Shape { Empty, Circle(r), Square(s) }
let s = Shape::Empty
print(s)"#),
"Shape::Empty"
);
}
#[test]
fn enum_tuple_variant() {
assert_eq!(
say(r#"enum Shape { Empty, Circle(r), Pair(x, y) }
let p = Shape::Pair(3, 4)
print(p)"#),
"Shape::Pair(3, 4)"
);
}
#[test]
fn enum_struct_variant() {
assert_eq!(
say(r#"enum Shape { Rectangle { width, height }, Empty }
let r = Shape::Rectangle { width: 4, height: 3 }
print(r)
print(r.width)
print(r.height)"#),
"3"
);
}
#[test]
fn enum_equality_same_variant() {
assert_eq!(
say(r#"enum E { A, B(x) }
print(E::A == E::A)
print(E::B(1) == E::B(1))
print(E::B(1) == E::B(2))
print(E::A == E::B(1))"#),
"false"
);
}
#[test]
fn enum_different_types_not_equal() {
assert_eq!(
say(r#"enum A { X }
enum B { X }
print(A::X == B::X)"#),
"false"
);
}
#[test]
fn enum_variant_mismatch_unit_given_args() {
let err = run_err(r#"enum E { A }
let x = E::A(1)"#);
assert!(err.contains("no payload"), "got: {}", err);
}
#[test]
fn enum_variant_mismatch_tuple_arity() {
let err = run_err(r#"enum E { P(x, y) }
let p = E::P(1)"#);
assert!(err.contains("expects 2 argument"), "got: {}", err);
}
#[test]
fn enum_variant_mismatch_struct_missing_field() {
let err = run_err(r#"enum E { R { w, h } }
let r = E::R { w: 1 }"#);
assert!(err.contains("Missing field"), "got: {}", err);
}
#[test]
fn enum_variant_mismatch_struct_extra_field() {
let err = run_err(r#"enum E { R { w, h } }
let r = E::R { w: 1, h: 2, extra: 3 }"#);
assert!(err.contains("no field"), "got: {}", err);
}
#[test]
fn enum_undeclared_variant_errors() {
let err = run_err(r#"enum E { A }
let x = E::Z"#);
assert!(err.contains("no variant"), "got: {}", err);
}
#[test]
fn enum_undeclared_type_errors() {
let err = run_err("let x = Nope::V");
assert!(err.contains("not declared"), "got: {}", err);
}
#[test]
fn enum_struct_variant_field_access() {
assert_eq!(
say(r#"enum Shape { Rect { w, h }, Empty }
let r = Shape::Rect { w: 10, h: 3 }
print(r.w * r.h)"#),
"30"
);
}
#[test]
fn enum_used_in_if_condition() {
assert_eq!(
say(r#"enum E { V }
if E::V == E::V {
print("yes")
} else {
print("no")
}"#),
"yes"
);
}
#[test]
fn enum_type_name_is_enum() {
assert_eq!(
say(r#"enum E { V }
print((E::V).type())"#),
"enum"
);
}
#[test]
fn enum_in_array_of_values() {
assert_eq!(
say(r#"enum Color { Red, Green, Blue }
let palette = [Color::Red, Color::Green, Color::Blue]
print(palette)"#),
"[Color::Red, Color::Green, Color::Blue]"
);
}
#[test]
fn enum_duplicate_decl_errors() {
let err = run_err(r#"enum E { A }
enum E { B }"#);
assert!(err.contains("already declared"), "got: {}", err);
}
#[test]
fn method_on_struct_basic() {
assert_eq!(
say(r#"struct Point { x, y }
fn Point.sum(self) { return self.x + self.y }
let p = Point { x: 3, y: 4 }
print(p.sum())"#),
"7"
);
}
#[test]
fn method_with_extra_args() {
assert_eq!(
say(r#"struct Counter { n }
fn Counter.add(self, delta) { return Counter { n: self.n + delta } }
let c = Counter { n: 10 }
let c2 = c.add(5)
print(c2.n)
print(c.n)"#),
"10"
);
}
#[test]
fn method_does_not_mutate_receiver() {
assert_eq!(
say(r#"struct Counter { n }
fn Counter.bump(self) { self.n = self.n + 1 }
let c = Counter { n: 5 }
c.bump()
print(c.n)"#),
"5"
);
}
#[test]
fn method_on_enum_dispatches_on_type() {
assert_eq!(
say(r#"enum Shape { Circle(r), Rect { w, h }, Empty }
fn Shape.name(self) { return "shape" }
print(Shape::Circle(3).name())
print(Shape::Rect { w: 4, h: 3 }.name())
print(Shape::Empty.name())"#),
"shape"
);
}
#[test]
fn method_overrides_builtin() {
assert_eq!(
say(r#"struct Wrapper { data }
fn Wrapper.len(self) { return 99 }
let w = Wrapper { data: [1, 2, 3] }
print(w.len())"#),
"99"
);
}
#[test]
fn method_unknown_on_struct_errors() {
let err = run_err(r#"struct P { x }
let p = P { x: 1 }
p.nope()"#);
assert!(err.contains(".nope()"), "got: {}", err);
}
#[test]
fn method_wrong_arg_count_errors() {
let err = run_err(r#"struct P { x }
fn P.set(self, v) { return P { x: v } }
let p = P { x: 1 }
p.set(1, 2)"#);
assert!(err.contains("expects"), "got: {}", err);
}
#[test]
fn method_chain_user_defined() {
assert_eq!(
say(r#"struct Adder { n }
fn Adder.then(self, m) { return Adder { n: self.n + m } }
let result = Adder { n: 1 }.then(2).then(3).then(4)
print(result.n)"#),
"10"
);
}
#[test]
fn method_self_is_clone() {
assert_eq!(
say(r#"struct P { x }
fn P.identity(self) { return self }
let a = P { x: 7 }
let b = a.identity()
print(a == b)
print(b.x)"#),
"7"
);
}
#[test]
fn method_on_enum_reads_payload_field() {
assert_eq!(
say(r#"enum Shape { Circle(r), Rect { w, h } }
fn Shape.label(self, prefix) {
return prefix + "-shape"
}
let c = Shape::Circle(5)
print(c.label("small"))"#),
"small-shape"
);
}
#[test]
fn enum_duplicate_variant_errors() {
let err = run_err(r#"enum E { A, A }"#);
assert!(err.contains("duplicate variant"), "got: {}", err);
}
#[test]
fn struct_empty() {
assert_eq!(
say(r#"struct Unit { }
let u = Unit { }
print(u)"#),
"Unit {}"
);
}
#[test]
fn import_module_does_not_see_importer_scope() {
let mut host = ModuleHost::new(&[("m", "fn leak() { return outer }")]);
let err = run(
r#"let outer = 42
use m
print(leak())"#,
&mut host,
&BopLimits::standard(),
)
.unwrap_err();
assert!(
err.message.contains("outer"),
"expected 'outer' not-found error, got: {}",
err.message
);
}
#[test]
fn const_declares_an_immutable_binding() {
assert_eq!(say("const PI = 3\nprint(PI)"), "3");
}
#[test]
fn const_can_reference_another_const() {
assert_eq!(
say("const PI = 3\nconst DIAMETER = PI * 2\nprint(DIAMETER)"),
"6"
);
}
#[test]
fn const_reassignment_is_refused_at_parse_time() {
let err = run_err("const PI = 3\nPI = 4");
assert!(
err.contains("can't reassign") && err.contains("constant"),
"expected const-reassignment error, got: {err}"
);
}
#[test]
fn let_name_must_start_lowercase() {
let err = run_err("let Foo = 1");
assert!(err.to_lowercase().contains("value"), "got: {err}");
}
#[test]
fn let_with_all_caps_suggests_const() {
let err = run_err("let MAX = 1");
assert!(
err.contains("const"),
"expected hint to suggest `const`, got: {err}"
);
}
#[test]
fn struct_name_must_start_uppercase() {
let err = run_err("struct entity { id }");
assert!(err.to_lowercase().contains("type"), "got: {err}");
}
#[test]
fn enum_variants_start_uppercase() {
let err = run_err("enum Event { spawn, damage }");
assert!(err.to_lowercase().contains("type"), "got: {err}");
}
#[test]
fn enum_with_all_caps_variants_is_allowed() {
assert_eq!(
say("enum Dir { N, E, S, W }\nlet d = Dir::E\nprint(\"ok\")"),
"ok"
);
}
#[test]
fn fn_name_must_start_lowercase() {
let err = run_err("fn Greet() { return 1 }");
assert!(err.to_lowercase().contains("value"), "got: {err}");
}
#[test]
fn fn_params_must_start_lowercase() {
let err = run_err("fn greet(Name) { return Name }");
assert!(err.to_lowercase().contains("value"), "got: {err}");
}
#[test]
fn for_loop_var_must_start_lowercase() {
let err = run_err("for I in range(3) { print(I) }");
assert!(err.to_lowercase().contains("value"), "got: {err}");
}
#[test]
fn const_name_must_be_all_caps() {
let err = run_err("const Pi = 3");
assert!(
err.to_lowercase().contains("constant"),
"got: {err}"
);
}
#[test]
fn underscore_prefix_is_allowed_for_all_buckets() {
assert_eq!(
say(r#"
let _hidden = 1
const _DEBUG = true
struct _Internal { _counter }
let s = _Internal { _counter: _hidden }
print(s._counter)
"#),
"1"
);
}
#[test]
fn two_modules_can_declare_same_type_name_with_different_shapes() {
let mut host = crate::host::StringModuleHost::new([
("paint", "enum Color { Red, Blue }"),
("other", "enum Color { Red, Green, Yellow }"),
]);
run(
r#"use paint as p
use other as o
let a = p.Color::Red
let b = o.Color::Red
print(a == b)
print(a == a)
print(a)
print(b)"#,
&mut host,
&BopLimits::standard(),
)
.unwrap();
assert_eq!(host.output(), "false\ntrue\nColor::Red\nColor::Red");
}
#[test]
fn namespaced_pattern_matches_only_same_module_value() {
let mut host = crate::host::StringModuleHost::new([
("paint", "enum Color { Red, Blue }"),
("other", "enum Color { Red, Green, Yellow }"),
]);
run(
r#"use paint as p
use other as o
fn label(c) {
return match c {
p.Color::Red => "paint red",
o.Color::Red => "other red",
_ => "other",
}
}
print(label(p.Color::Red))
print(label(o.Color::Red))
print(label(o.Color::Green))"#,
&mut host,
&BopLimits::standard(),
)
.unwrap();
assert_eq!(
host.output(),
"paint red\nother red\nother"
);
}
#[test]
fn string_module_host_runs_use_end_to_end() {
let mut host = crate::host::StringModuleHost::new([
("greetings", "fn hello(name) { print(\"hi \" + name) }"),
]);
run(
"use greetings\nhello(\"bop\")",
&mut host,
&BopLimits::standard(),
)
.unwrap();
assert_eq!(host.output(), "hi bop");
}
#[test]
fn runtime_error_carries_column_for_undefined_ident() {
let err = run_err_full("let x = 1\nprint(undefined)");
assert_eq!(err.line, Some(2), "line");
assert!(
err.column.is_some(),
"expected runtime error to carry column info, got None"
);
}
#[test]
fn runtime_error_column_renders_with_carat() {
let src = "let x = 1\nprint(undefined)";
let err = run_err_full(src);
let rendered = err.render(src);
assert!(
rendered.contains("--> line 2:"),
"rendered should include line+col header, got:\n{}",
rendered
);
assert!(
rendered.contains("^"),
"rendered should draw a carat, got:\n{}",
rendered
);
}
#[test]
fn resolve_from_map_returns_none_for_unknown_modules() {
let resolver = crate::host::resolve_from_map([("m", "let x = 1")]);
assert!(resolver("m").is_some());
assert!(resolver("other").is_none());
}
fn repl_eval(
session: &mut ReplSession,
src: &str,
host: &mut TestHost,
) -> Result<Option<Value>, BopError> {
session.eval(src, host, &test_limits())
}
#[test]
fn session_let_binding_survives_between_evals() {
let mut session = ReplSession::new();
let mut host = TestHost::new();
repl_eval(&mut session, "let x = 5", &mut host).unwrap();
repl_eval(&mut session, "print(x)", &mut host).unwrap();
assert_eq!(host.last_print(), "5");
}
#[test]
fn session_mutated_let_reflects_in_next_eval() {
let mut session = ReplSession::new();
let mut host = TestHost::new();
repl_eval(&mut session, "let counter = 0", &mut host).unwrap();
repl_eval(&mut session, "counter = counter + 1", &mut host).unwrap();
repl_eval(&mut session, "counter = counter + 1", &mut host).unwrap();
repl_eval(&mut session, "print(counter)", &mut host).unwrap();
assert_eq!(host.last_print(), "2");
}
#[test]
fn session_fn_declared_on_one_eval_callable_next() {
let mut session = ReplSession::new();
let mut host = TestHost::new();
repl_eval(
&mut session,
"fn double(x) { return x + x }",
&mut host,
)
.unwrap();
repl_eval(&mut session, "print(double(21))", &mut host).unwrap();
assert_eq!(host.last_print(), "42");
}
#[test]
fn session_struct_and_method_survive() {
let mut session = ReplSession::new();
let mut host = TestHost::new();
repl_eval(
&mut session,
"struct Point { x, y }\nfn Point.sum(self) { return self.x + self.y }",
&mut host,
)
.unwrap();
repl_eval(
&mut session,
"let p = Point { x: 3, y: 4 }\nprint(p.sum())",
&mut host,
)
.unwrap();
assert_eq!(host.last_print(), "7");
}
#[test]
fn session_bare_expression_returns_value() {
let mut session = ReplSession::new();
let mut host = TestHost::new();
assert!(repl_eval(&mut session, "let x = 5", &mut host)
.unwrap()
.is_none());
let v = repl_eval(&mut session, "x + 1", &mut host).unwrap();
match v {
Some(Value::Int(n)) => assert_eq!(n, 6),
other => panic!("expected Int(6), got: {:?}", other),
}
}
#[test]
fn session_errors_preserve_earlier_effects() {
let mut session = ReplSession::new();
let mut host = TestHost::new();
let err = repl_eval(
&mut session,
"let kept = 1\nlet bad = undefined\nlet skipped = 3",
&mut host,
);
assert!(err.is_err(), "expected runtime error");
assert!(session.get("kept").is_some());
assert!(session.get("skipped").is_none());
}
#[test]
fn session_normalises_scope_depth_after_block_error() {
let mut session = ReplSession::new();
let mut host = TestHost::new();
let _ = repl_eval(
&mut session,
"if true {\n let y = undefined\n}",
&mut host,
);
repl_eval(&mut session, "let after = 7", &mut host).unwrap();
let v = repl_eval(&mut session, "after", &mut host).unwrap();
match v {
Some(Value::Int(n)) => assert_eq!(n, 7),
other => panic!("expected Int(7), got: {:?}", other),
}
}
#[test]
fn session_binding_names_surfaces_lets_and_fns() {
let mut session = ReplSession::new();
let mut host = TestHost::new();
repl_eval(&mut session, "let alpha = 1", &mut host).unwrap();
repl_eval(&mut session, "fn beta() { return 2 }", &mut host).unwrap();
let names = session.binding_names();
assert!(names.contains(&"alpha".to_string()));
assert!(names.contains(&"beta".to_string()));
}
#[test]
fn session_use_carries_imports_across_evals() {
struct ModHost {
prints: std::cell::RefCell<Vec<String>>,
}
impl BopHost for ModHost {
fn call(
&mut self,
_: &str,
_: &[Value],
_: u32,
) -> Option<Result<Value, BopError>> {
None
}
fn on_print(&mut self, message: &str) {
self.prints.borrow_mut().push(message.to_string());
}
fn resolve_module(&mut self, name: &str) -> Option<Result<String, BopError>> {
match name {
"m" => Some(Ok("fn greet() { return \"hi\" }".to_string())),
_ => None,
}
}
}
let mut host = ModHost {
prints: std::cell::RefCell::new(Vec::new()),
};
let mut session = ReplSession::new();
session.eval("use m", &mut host, &test_limits()).unwrap();
session
.eval("print(greet())", &mut host, &test_limits())
.unwrap();
let prints = host.prints.borrow();
assert_eq!(prints.last().map(|s| s.as_str()), Some("hi"));
}
}