From 066b582e3688cd1aa02b541da099fa43f305e6f9 Mon Sep 17 00:00:00 2001 From: Mateo Rodriguez Date: Sat, 2 May 2026 16:58:49 -0400 Subject: [PATCH 1/2] fix bugs from clippy --- .gitignore | 3 ++- bytecode/src/context.rs | 4 ++-- bytecode/src/file.rs | 2 +- bytecode/src/function.rs | 29 ++++++++++++------------- bytecode/src/instruction.rs | 19 +++++++--------- bytecode/src/lib.rs | 1 + bytecode/src/variables/object.rs | 2 +- bytecode/src/variables/primitive.rs | 2 +- bytecode_dev_transpiler/src/lib.rs | 2 +- compiler/src/ast.rs | 2 +- compiler/src/ast/declaration.rs | 4 ++-- compiler/src/ast/dot_lookup.rs | 3 +-- compiler/src/ast/function_arguments.rs | 2 +- compiler/src/ast/function_parameters.rs | 4 ++-- compiler/src/ast/list.rs | 2 +- compiler/src/ast/reassignment.rs | 2 +- compiler/src/ast/type.rs | 4 ++-- 17 files changed, 42 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index a45c7bb..78e64f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ **/target .VSCodeCounter -.idea \ No newline at end of file +.idea +.DS_Store \ No newline at end of file diff --git a/bytecode/src/context.rs b/bytecode/src/context.rs index 6f13448..929f529 100644 --- a/bytecode/src/context.rs +++ b/bytecode/src/context.rs @@ -81,7 +81,7 @@ pub struct Ctx<'a> { /// be modified via the [`Ctx::signal`] and [`Ctx::clear_signal`] methods. exit_state: Box, /// The arguments to the function. - args: Cow<'a, Vec>, + args: Cow<'a, [Primitive]>, /// A separate variable mapping for closures and objects. If `None`, this function is neither. /// Otherwise, contains a reference shared amongst all instances of the callback to point to /// shared/global data. @@ -100,7 +100,7 @@ impl<'a> Ctx<'a> { pub fn new( function: &'a Function, call_stack: Rc>, - args: Cow<'a, Vec>, + args: Cow<'a, [Primitive]>, callback_state: Option, ) -> Self { Self { diff --git a/bytecode/src/file.rs b/bytecode/src/file.rs index 263738e..21d118e 100644 --- a/bytecode/src/file.rs +++ b/bytecode/src/file.rs @@ -142,7 +142,7 @@ impl MScriptFile { pub fn run_function( &self, name: &String, - args: Cow>, + args: Cow<[Primitive]>, current_frame: Rc>, callback_state: Option, jump_callback: &mut impl Fn(&JumpRequest) -> Result, diff --git a/bytecode/src/function.rs b/bytecode/src/function.rs index 9ab9a01..249479d 100644 --- a/bytecode/src/function.rs +++ b/bytecode/src/function.rs @@ -798,7 +798,7 @@ impl BuiltInFunction { let result: Primitive = match this { Primitive::Int(i32) => Primitive::Float(f64::from(*i32).powf(*power)), Primitive::BigInt(i128) => { - Primitive::Float(f64::from(*i128 as i32).powf(*power)) + Primitive::Float((*i128 as f64).powf(*power)) } Primitive::Byte(u8) => Primitive::Float((*u8 as f64).powf(*power)), Primitive::Float(f64) => Primitive::Float(f64.powf(*power)), @@ -814,7 +814,7 @@ impl BuiltInFunction { let result: Primitive = match this { Primitive::Int(i32) => Primitive::Float(f64::from(*i32).sqrt()), - Primitive::BigInt(i128) => Primitive::Float(f64::from(*i128 as i32).sqrt()), + Primitive::BigInt(i128) => Primitive::Float((*i128 as f64).sqrt()), Primitive::Byte(u8) => Primitive::Float((*u8 as f64).sqrt()), Primitive::Float(f64) => Primitive::Float(f64.sqrt()), bad => unreachable!("{bad}"), @@ -891,10 +891,7 @@ impl BuiltInFunction { let result: Primitive = match this { Primitive::Int(i32) => Primitive::Float((*i32).into()), - Primitive::BigInt(i128) => Primitive::Float(f64::from( - i32::try_from(*i128) - .with_context(|| format!("`{i128}` cannot be made into a float"))?, - )), + Primitive::BigInt(i128) => Primitive::Float(*i128 as f64), Primitive::Byte(u8) => Primitive::Float(*u8 as f64), Primitive::Float(f64) => Primitive::Float(*f64), bad => unreachable!("{bad}"), @@ -1348,14 +1345,14 @@ impl Function { /// # Arguments /// * `args` - The arguments to this function, provided by the caller. /// * `current_frame` - A shared reference to the current stack trace. The caller - /// **SHOULD NOT** push a stack frame for a bytecode function - /// before calling it; this method will handle that. + /// **SHOULD NOT** push a stack frame for a bytecode function + /// before calling it; this method will handle that. /// * `callback_state` - A shared reference to the [`VariableMapping`] that this - /// function can access. This argument is used for callbacks - /// and closures exclusively. Normal variables should be - /// added to `current_frame`. + /// function can access. This argument is used for callbacks + /// and closures exclusively. Normal variables should be + /// added to `current_frame`. /// * `jump_callback` - A callback that defines how this function's jumps are handled. - /// The implementation is up to the caller. + /// The implementation is up to the caller. /// /// # Errors /// This function can error if an instruction raises an error during execution. @@ -1364,7 +1361,7 @@ impl Function { /// This function will `panic!` if the instruction byte falls outside of (0..[`INSTRUCTION_COUNT`][crate::instruction_constants::INSTRUCTION_COUNT]) pub(crate) fn run( &self, - args: Cow>, + args: Cow<[Primitive]>, current_frame: Rc>, callback_state: Option, jump_callback: &mut impl Fn(&JumpRequest) -> Result, @@ -1414,7 +1411,9 @@ impl Function { .checked_add_signed(offset) .with_context(|| format!("numeric overflow ({instruction_ptr} + {offset})"))?; #[cfg(not(feature = "debug"))] - let new_val = (instruction_ptr as isize + offset) as usize; + let new_val = instruction_ptr + .checked_add_signed(offset) + .with_context(|| format!("numeric overflow ({instruction_ptr} + {offset})"))?; if new_val >= instruction_len { bail!("goto position index {new_val} is too big, instruction length is {instruction_len}."); @@ -1537,7 +1536,7 @@ impl<'a> Functions { pub(crate) fn run_function( &self, name: &String, - args: Cow>, + args: Cow<[Primitive]>, current_frame: Rc>, callback_state: Option, jump_callback: &mut impl Fn(&JumpRequest) -> Result, diff --git a/bytecode/src/instruction.rs b/bytecode/src/instruction.rs index 7521bbd..f26cccc 100644 --- a/bytecode/src/instruction.rs +++ b/bytecode/src/instruction.rs @@ -5,18 +5,17 @@ use super::function::InstructionExitState; use super::stack::{Stack, VariableMapping}; use super::variables::{ObjectBuilder, Primitive}; use anyhow::{bail, Context, Result}; -use once_cell::sync::Lazy; use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashSet; use std::fmt::Debug; use std::io::{stdin, stdout, Write}; use std::rc::Rc; -use std::sync::Mutex; -/// This variable allows instructions to register objects on the fly. -static mut OBJECT_BUILDER: Lazy> = - Lazy::new(|| Mutex::new(ObjectBuilder::new())); +// This variable allows instructions to register objects on the fly. +thread_local! { + static OBJECT_BUILDER: RefCell = RefCell::new(ObjectBuilder::new()); +} /// Used for type declarations in lookup tables. pub type InstructionSignature = fn(&mut Ctx, &[String]) -> Result<()>; @@ -565,9 +564,7 @@ pub mod implementations { let function = ctx.owner(); let name = function.name(); - let obj = unsafe { - let mut lock = OBJECT_BUILDER.lock().unwrap(); - + let obj = OBJECT_BUILDER.with_borrow_mut(|lock| -> Result<_> { if !lock.has_class_been_registered(name) { let location = &function.location(); let object_path = format!("{}#{name}$", location.upgrade().unwrap().path()); @@ -593,10 +590,10 @@ pub mod implementations { lock.register_class(name.to_owned(), mapping); } - lock.name(name.to_owned()) + Ok(lock.name(name.to_owned()) .object_variables(object_variables) - .build() - }; + .build()) + })?; ctx.push(object!(obj)); diff --git a/bytecode/src/lib.rs b/bytecode/src/lib.rs index 2eabf50..77845e1 100644 --- a/bytecode/src/lib.rs +++ b/bytecode/src/lib.rs @@ -1,6 +1,7 @@ //! This is the API for all-things-bytecode. #![allow(dead_code)] +#![allow(non_local_definitions)] pub(crate) mod context; pub(crate) mod file; diff --git a/bytecode/src/variables/object.rs b/bytecode/src/variables/object.rs index e3b812d..9bd2218 100644 --- a/bytecode/src/variables/object.rs +++ b/bytecode/src/variables/object.rs @@ -134,7 +134,7 @@ impl Debug for Object { result } else if let Some(ref name) = self.name { - write!(f, "", name) + write!(f, "") } else { write!(f, "...") } diff --git a/bytecode/src/variables/primitive.rs b/bytecode/src/variables/primitive.rs index 56c3ddc..9454f3c 100644 --- a/bytecode/src/variables/primitive.rs +++ b/bytecode/src/variables/primitive.rs @@ -693,7 +693,7 @@ impl Primitive { write!(f, "]") } Module(module) => { - write!(f, "module {:#?}", module) + write!(f, "module {module:#?}") // if cfg!(feature = "debug") { // write!(f, "module {:#?}", module) diff --git a/bytecode_dev_transpiler/src/lib.rs b/bytecode_dev_transpiler/src/lib.rs index c295c7e..4930b7d 100644 --- a/bytecode_dev_transpiler/src/lib.rs +++ b/bytecode_dev_transpiler/src/lib.rs @@ -16,7 +16,7 @@ impl Instruction { fn repr(&self) -> String { let mut args = String::new(); - if self.arguments.len() >= 1 { + if !self.arguments.is_empty() { for arg in &self.arguments[..] { args.push(' '); if arg.contains(' ') { diff --git a/compiler/src/ast.rs b/compiler/src/ast.rs index 156cff4..5bf8d54 100644 --- a/compiler/src/ast.rs +++ b/compiler/src/ast.rs @@ -255,7 +255,7 @@ impl CompiledItem { Self::Instruction { id, arguments } => { let mut args = String::new(); - if arguments.len() >= 1 { + if !arguments.is_empty() { for arg in &arguments[..] { args.push(' '); let replaced = arg.replace('"', "\\\""); diff --git a/compiler/src/ast/declaration.rs b/compiler/src/ast/declaration.rs index 02acc59..0b43400 100644 --- a/compiler/src/ast/declaration.rs +++ b/compiler/src/ast/declaration.rs @@ -22,7 +22,7 @@ pub(crate) enum Declaration { ReturnStatement(ReturnStatement), IfStatement(IfStatement), WhileLoop(WhileLoop), - NumberLoop(NumberLoop), + NumberLoop(Box), Continue(Continue), Break(Break), Assertion(Assertion), @@ -114,7 +114,7 @@ impl Parser { Declaration::Continue(Self::continue_statement(declaration).to_err_vec()?) } Rule::break_statement => Declaration::Break(Self::break_statement(input).to_err_vec()?), - Rule::number_loop => Declaration::NumberLoop(Self::number_loop(declaration)?), + Rule::number_loop => Declaration::NumberLoop(Box::new(Self::number_loop(declaration)?)), Rule::reassignment => Declaration::Reassignment(Self::reassignment(declaration)?), Rule::assertion => Declaration::Assertion(Self::assertion(declaration)?), Rule::class => Declaration::Class(Self::class(declaration)?), diff --git a/compiler/src/ast/dot_lookup.rs b/compiler/src/ast/dot_lookup.rs index 985ae4b..016fe77 100644 --- a/compiler/src/ast/dot_lookup.rs +++ b/compiler/src/ast/dot_lookup.rs @@ -173,8 +173,7 @@ impl Parser { ident_span, &source_name, format!( - "this field has the type `{}`, which is not callable", - type_of_property + "this field has the type `{type_of_property}`, which is not callable" ), )]); }; diff --git a/compiler/src/ast/function_arguments.rs b/compiler/src/ast/function_arguments.rs index 0fff5cc..7f8d747 100644 --- a/compiler/src/ast/function_arguments.rs +++ b/compiler/src/ast/function_arguments.rs @@ -37,7 +37,7 @@ impl Parser { let mut result_len: usize = 0; let mut errors = vec![]; - let expected_types: Cow>> = expected_parameters.to_types(); + let expected_types: Cow<[Cow]> = expected_parameters.to_types(); let mut child_span = input.as_span(); diff --git a/compiler/src/ast/function_parameters.rs b/compiler/src/ast/function_parameters.rs index 660073d..ed7fe64 100644 --- a/compiler/src/ast/function_parameters.rs +++ b/compiler/src/ast/function_parameters.rs @@ -29,7 +29,7 @@ impl FunctionParameters { } } - pub fn to_types(&self) -> Cow<'_, Vec>> { + pub fn to_types(&self) -> Cow<'_, [Cow<'static, TypeLayout>]> { match self { FunctionParameters::Named(names) => { Cow::Owned(names.iter().map(|x| x.ty().unwrap().clone()).collect()) @@ -43,7 +43,7 @@ impl Display for FunctionParameters { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut buf: String = String::new(); - let types: Cow>> = self.to_types(); + let types: Cow<[Cow]> = self.to_types(); let mut iter = types.iter(); let Some(first) = iter.next() else { diff --git a/compiler/src/ast/list.rs b/compiler/src/ast/list.rs index f21e3d1..30e8e23 100644 --- a/compiler/src/ast/list.rs +++ b/compiler/src/ast/list.rs @@ -174,7 +174,7 @@ impl ListType { pub fn upper_bound(&self) -> ListBound { match self { Self::Open { .. } => ListBound::Infinite, - Self::Mixed(types) => ListBound::Numeric(types.len() - 1), + Self::Mixed(types) => ListBound::Numeric(types.len().saturating_sub(1)), } } diff --git a/compiler/src/ast/reassignment.rs b/compiler/src/ast/reassignment.rs index e757720..5036867 100644 --- a/compiler/src/ast/reassignment.rs +++ b/compiler/src/ast/reassignment.rs @@ -242,7 +242,7 @@ impl Parser { let path = children.next().unwrap(); let value = children.next().unwrap(); - let path_span = value.as_span(); + let path_span = path.as_span(); let (path, is_const) = ReassignmentPath::parse(path)?; diff --git a/compiler/src/ast/type.rs b/compiler/src/ast/type.rs index b3e9288..93187dd 100644 --- a/compiler/src/ast/type.rs +++ b/compiler/src/ast/type.rs @@ -597,7 +597,7 @@ impl Display for TypeLayout { Self::Optional(None) => write!(f, "nil"), Self::Void => write!(f, "void"), Self::Generic(generic) => write!(f, "{generic}"), - Self::Module(module) if cfg!(test) => write!(f, "", module), + Self::Module(module) if cfg!(test) => write!(f, ""), Self::Module(module) => write!(f, "", module.name.as_os_str()), Self::Map(map) => write!(f, "map[{}, {}]", map.key_type(), map.value_type()), } @@ -1677,7 +1677,7 @@ impl TypeLayout { usize::MAX ), ValToUsize::NotConstexpr => match me { - TypeLayout::List(ListType::Open(ty)) => return Ok(Cow::Borrowed(ty.as_ref())), + TypeLayout::List(ListType::Open(ty)) => Ok(Cow::Borrowed(ty.as_ref())), TypeLayout::List(ListType::Mixed(ty)) => bail!("Indexing into a mixed type list ({:?}) requires that the index be evaluable at compile time", ty.iter().map(|x| x.to_string()).collect::>()), _ => todo!() }, From 00c8448e44197c9c22f83c1024c8255daa458beb Mon Sep 17 00:00:00 2001 From: Mateo Rodriguez Date: Sat, 2 May 2026 17:11:25 -0400 Subject: [PATCH 2/2] add more tests --- compiler/src/tests/builtins.rs | 93 ++++++++++++++++++++++++++ compiler/src/tests/functions.rs | 105 ++++++++++++++++++++++++++++++ compiler/src/tests/if.rs | 88 ++++++++++++++++++++++--- compiler/src/tests/maps.rs | 100 ++++++++++++++++++++++++++++ compiler/src/tests/number_loop.rs | 88 +++++++++++++++++++++++++ compiler/src/tests/while.rs | 73 +++++++++++++++++++++ 6 files changed, 537 insertions(+), 10 deletions(-) diff --git a/compiler/src/tests/builtins.rs b/compiler/src/tests/builtins.rs index 4bc9fee..2f14af5 100644 --- a/compiler/src/tests/builtins.rs +++ b/compiler/src/tests/builtins.rs @@ -233,3 +233,96 @@ fn number_properties() { ) .unwrap(); } + +#[test] +fn string_chars() { + eval( + r#" + chars = "hello".chars() + assert typeof chars == "[str...]" + assert chars.len() == 5 + assert chars[0] == "h" + assert chars[4] == "o" + + reconstructed = "" + from 0 to chars.len(), i { + reconstructed += chars[i] + } + assert reconstructed == "hello" + + assert "".chars().len() == 0 + assert "x".chars() == ["x"] + "#, + ) + .unwrap(); +} + +#[test] +fn number_to_str() { + eval( + r#" + assert 42.to_str() == "42" + assert (-7).to_str() == "-7" + assert 0.to_str() == "0" + assert 3.14.to_str() == "3.14" + + n = 255 + assert n.to_str() == "255" + "#, + ) + .unwrap(); +} + +#[test] +fn array_to_str() { + eval( + r#" + assert [1, 2, 3].to_str() == "[1, 2, 3]" + assert (["a", "b"]).to_str() == "[\"a\", \"b\"]" + empty: [int...] = [] + assert empty.to_str() == "[]" + "#, + ) + .unwrap(); +} + +#[test] +fn array_filter_map_chain() { + eval( + r#" + words: [str...] = ["banana", "apple", "cherry"] + lengths = words.map(fn(w: str) -> int { return w.len() }) + + assert lengths == [6, 5, 6] + + short_words = words.filter(fn(w: str) -> bool { return w.len() <= 5 }) + assert short_words == ["apple"] + assert typeof short_words == "[str...]" + "#, + ) + .unwrap(); +} + +#[test] +fn count_chars_with_chars_method() { + eval( + r#" + count_vowels = fn(s: str) -> int { + vowels = "aeiouAEIOU" + count = 0 + chars = s.chars() + from 0 to chars.len(), i { + if vowels.contains(chars[i]) { + count += 1 + } + } + return count + } + + assert count_vowels("hello") == 2 + assert count_vowels("rhythm") == 0 + assert count_vowels("aeiou") == 5 + "#, + ) + .unwrap(); +} diff --git a/compiler/src/tests/functions.rs b/compiler/src/tests/functions.rs index 066b6e4..db07fb4 100644 --- a/compiler/src/tests/functions.rs +++ b/compiler/src/tests/functions.rs @@ -1,5 +1,110 @@ use crate::eval; +#[test] +fn higher_order_callback() { + eval( + r#" + apply = fn(f: fn(int) -> int, x: int) -> int { + return f(x) + } + + double = fn(x: int) -> int { + return x * 2 + } + + triple = fn(x: int) -> int { + return x * 3 + } + + assert apply(double, 5) == 10 + assert apply(triple, 5) == 15 + assert apply(fn(x: int) -> int { return x * x }, 4) == 16 + "#, + ) + .unwrap() +} + +#[test] +fn function_factory() { + eval( + r#" + make_adder = fn(n: int) -> (fn(int) -> int) { + return fn(x: int) -> int { + return x + n + } + } + + add5 = make_adder(5) + add10 = make_adder(10) + + assert add5(3) == 8 + assert add10(3) == 13 + assert add5(add10(1)) == 16 + "#, + ) + .unwrap() +} + +#[test] +fn immediately_invoked() { + eval( + r#" + result = fn(x: int, y: int) -> int { + return x + y + }(10, 20) + + assert result == 30 + "#, + ) + .unwrap() +} + +#[test] +fn multi_capture_closure() { + eval( + r#" + base = 100 + multiplier = 3 + offset = -10 + + transform = fn(x: int) -> int { + return base + x * multiplier + offset + } + + assert transform(0) == 90 + assert transform(5) == 105 + + multiplier = 2 + + assert transform(5) == 100 + "#, + ) + .unwrap() +} + +#[test] +fn function_stored_in_array() { + eval( + r#" + ops: [fn(int) -> int...] = [ + fn(x: int) -> int { return x + 1 }, + fn(x: int) -> int { return x * 2 }, + fn(x: int) -> int { return x * x }, + ] + + result = 3 + + from 0 to ops.len(), i { + result = (ops[i])(result) + } + + # (3 + 1) = 4, (4 * 2) = 8, (8 * 8) = 64 + assert result == 64 + "#, + ) + .unwrap() +} + #[test] fn make_function() { eval( diff --git a/compiler/src/tests/if.rs b/compiler/src/tests/if.rs index d3f51db..a5f6b66 100644 --- a/compiler/src/tests/if.rs +++ b/compiler/src/tests/if.rs @@ -84,19 +84,19 @@ fn if_else_inside_numeric_loop() { r#" fizz_buzz = fn(input: int) -> str? { result: str? = nil - + if input % 3 == 0 { result = "Fizz" } - + if input % 5 == 0 { preappend = get result or "" result = preappend + "Buzz" } - - return result + + return result } - + result_buffer = "" from 1 through 15, i { @@ -120,19 +120,19 @@ fn if_else_inside_while_loop() { r#" fizz_buzz = fn(input: int) -> str? { result: str? = nil - + if input % 3 == 0 { result = "Fizz" } - + if input % 5 == 0 { preappend = get result or "" result = preappend + "Buzz" } - - return result + + return result } - + result_buffer = "" i = 1 @@ -153,3 +153,71 @@ fn if_else_inside_while_loop() { ) .unwrap(); } + +#[test] +fn deeply_nested_conditions() { + eval( + r#" + classify = fn(n: int) -> str { + if n < 0 { + if n < -100 { + return "very negative" + } else if n < -10 { + return "negative" + } else { + return "slightly negative" + } + } else if n == 0 { + return "zero" + } else { + if n > 100 { + return "very positive" + } else if n > 10 { + return "positive" + } else { + return "slightly positive" + } + } + } + + assert classify(-200) == "very negative" + assert classify(-50) == "negative" + assert classify(-5) == "slightly negative" + assert classify(0) == "zero" + assert classify(5) == "slightly positive" + assert classify(50) == "positive" + assert classify(200) == "very positive" + "#, + ) + .unwrap(); +} + +#[test] +fn if_with_side_effects_in_condition() { + eval( + r#" + calls = 0 + + expensive_check = fn() -> bool { + modify calls = calls + 1 + return calls >= 3 + } + + result = "" + + if expensive_check() { + result = "first" + } else if expensive_check() { + result = "second" + } else if expensive_check() { + result = "third" + } else { + result = "never" + } + + assert result == "third" + assert calls == 3 + "#, + ) + .unwrap(); +} diff --git a/compiler/src/tests/maps.rs b/compiler/src/tests/maps.rs index 4823d91..aacd84b 100644 --- a/compiler/src/tests/maps.rs +++ b/compiler/src/tests/maps.rs @@ -289,3 +289,103 @@ fn bad_map_type_1() { ) .unwrap() } + +#[test] +fn map_clear() { + eval( + r#" + m = map[str, int] { + "a": 1, + "b": 2, + "c": 3, + } + assert m.len() == 3 + m.clear() + assert m.len() == 0 + assert m["a"] == nil + assert !m.contains_key("b") + + m["x"] = 99 + assert m.len() == 1 + assert m["x"] == 99 + "#, + ) + .unwrap() +} + +#[test] +fn frequency_counter() { + eval( + r#" + words: [str...] = ["apple", "banana", "apple", "cherry", "banana", "apple"] + + freq = map[str, int] + + from 0 to words.len(), i { + word = words[i] + current = (freq[word]) or 0 + freq[word] = current + 1 + } + + assert freq["apple"] == 3 + assert freq["banana"] == 2 + assert freq["cherry"] == 1 + assert freq["grape"] == nil + "#, + ) + .unwrap() +} + +#[test] +fn map_with_class_values() { + eval( + r#" + class Score { + value: int + constructor(self, value: int) { + self.value = value + } + fn to_str(self) -> str { + return "Score(" + self.value + ")" + } + } + + scores = map[str, Score] { + "Alice": Score(95), + "Bob": Score(82), + "Carol": Score(91), + } + + assert scores.len() == 3 + assert (get scores["Alice"]).value == 95 + assert (get scores["Bob"]).to_str() == "Score(82)" + + scores["Dave"] = Score(78) + assert scores.len() == 4 + assert (get scores["Dave"]).value == 78 + "#, + ) + .unwrap() +} + +#[test] +fn map_optional_values() { + eval( + r#" + cache = map[int, str] + + lookup = fn(key: int) -> str? { + return cache[key] + } + + assert lookup(1) == nil + cache[1] = "one" + assert lookup(1) == "one" + + cache[2] = "two" + assert cache.len() == 2 + assert (get cache[1]) + " " + (get cache[2]) == "one two" + "#, + ) + .unwrap() +} diff --git a/compiler/src/tests/number_loop.rs b/compiler/src/tests/number_loop.rs index 7b3c929..b7f6122 100644 --- a/compiler/src/tests/number_loop.rs +++ b/compiler/src/tests/number_loop.rs @@ -1,5 +1,93 @@ use crate::eval; +#[test] +fn break_in_numeric_loop() { + eval( + r#" + first_div_by_7 = -1 + from 1 through 100, n { + if n % 7 == 0 { + first_div_by_7 = n + break + } + } + assert first_div_by_7 == 7 + "#, + ) + .unwrap(); +} + +#[test] +fn continue_in_numeric_loop() { + eval( + r#" + sum_of_odds = 0 + from 1 through 10, n { + if n % 2 == 0 { + continue + } + sum_of_odds += n + } + # 1 + 3 + 5 + 7 + 9 = 25 + assert sum_of_odds == 25 + "#, + ) + .unwrap(); +} + +#[test] +fn nested_loop() { + eval( + r#" + pairs: [str...] = [] + from 0 to 3, i { + from 0 to 3, j { + if i != j { + pairs.push(i + "," + j) + } + } + } + # (0,1),(0,2),(1,0),(1,2),(2,0),(2,1) = 6 pairs + assert pairs.len() == 6 + assert pairs[0] == "0,1" + assert pairs[5] == "2,1" + "#, + ) + .unwrap(); +} + +#[test] +fn prime_check() { + eval( + r#" + is_prime = fn(n: int) -> bool { + if n < 2 { + return false + } + + i = 0 + from 2 to n, i { + if n % i == 0 { + break + } + } + + return i == n + } + + assert !is_prime(0) + assert !is_prime(1) + assert is_prime(2) + assert is_prime(3) + assert !is_prime(4) + assert is_prime(5) + assert !is_prime(9) + assert is_prime(97) + "#, + ) + .unwrap(); +} + #[test] fn number_loop() { eval( diff --git a/compiler/src/tests/while.rs b/compiler/src/tests/while.rs index e5df770..6013530 100644 --- a/compiler/src/tests/while.rs +++ b/compiler/src/tests/while.rs @@ -1,5 +1,78 @@ use crate::eval; +#[test] +fn while_never_runs() { + eval( + r#" + count = 0 + while false { + count += 1 + } + assert count == 0 + "#, + ) + .unwrap(); +} + +#[test] +fn nested_while() { + eval( + r#" + result = 0 + i = 0 + while i < 3 { + j = 0 + while j < 4 { + j += 1 + if j == 3 { + break + } + result += 1 + } + i += 1 + } + # 3 outer iterations * 2 inner iterations each = 6 + assert result == 6 + "#, + ) + .unwrap(); +} + +#[test] +fn while_accumulate_string() { + eval( + r#" + result = "" + i = 0 + while i < 5 { + result += i + i += 1 + } + assert result == "01234" + "#, + ) + .unwrap(); +} + +#[test] +fn while_multiple_exits() { + eval( + r#" + found = -1 + i = 0 + while i < 20 { + i += 1 + if i % 3 == 0 && i % 5 == 0 { + found = i + break + } + } + assert found == 15 + "#, + ) + .unwrap(); +} + #[test] fn while_loop() { eval(