diff --git a/seq/src/model/forth.rs b/seq/src/model/forth.rs index 7291d52..b268bf8 100644 --- a/seq/src/model/forth.rs +++ b/seq/src/model/forth.rs @@ -24,7 +24,7 @@ impl StepContext { pub type Variables = Arc>>; pub type Rng = Arc>; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum Value { Int(i64), Float(f64), @@ -1628,14 +1628,29 @@ fn compile_if(tokens: &[Token]) -> Result<(Vec, Vec, usize), String> { Ok((then_ops, else_ops, then_pos + 1)) } +pub type Stack = Arc>>; + pub struct Forth { + stack: Stack, vars: Variables, rng: Rng, } impl Forth { pub fn new(vars: Variables, rng: Rng) -> Self { - Self { vars, rng } + Self { + stack: Arc::new(Mutex::new(Vec::new())), + vars, + rng, + } + } + + pub fn stack(&self) -> Vec { + self.stack.lock().unwrap().clone() + } + + pub fn clear_stack(&self) { + self.stack.lock().unwrap().clear(); } pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result, String> { @@ -1649,7 +1664,7 @@ impl Forth { } fn execute(&self, ops: &[Op], ctx: &StepContext) -> Result, String> { - let mut stack: Vec = Vec::new(); + let mut stack = self.stack.lock().unwrap(); let mut outputs: Vec = Vec::new(); let mut time_stack: Vec = vec![TimeContext { start: 0.0, diff --git a/seq/src/model/forth_tests/arithmetic.rs b/seq/src/model/forth_tests/arithmetic.rs new file mode 100644 index 0000000..a1b122c --- /dev/null +++ b/seq/src/model/forth_tests/arithmetic.rs @@ -0,0 +1,152 @@ +use super::harness::*; + +#[test] +fn add_integers() { + expect_int("2 3 +", 5); +} + +#[test] +fn add_floats() { + expect_int("2.5 3.5 +", 6); +} + +#[test] +fn add_mixed() { + expect_float("2 3.5 +", 5.5); +} + +#[test] +fn sub() { + expect_int("10 3 -", 7); +} + +#[test] +fn sub_negative() { + expect_int("3 10 -", -7); +} + +#[test] +fn mul() { + expect_int("4 5 *", 20); +} + +#[test] +fn mul_floats() { + expect_int("2.5 4 *", 10); +} + +#[test] +fn div() { + expect_int("10 2 /", 5); +} + +#[test] +fn div_float_result() { + expect_float("7 2 /", 3.5); +} + +#[test] +fn modulo() { + expect_int("7 3 mod", 1); +} + +#[test] +fn modulo_exact() { + expect_int("9 3 mod", 0); +} + +#[test] +fn neg_int() { + expect_int("5 neg", -5); +} + +#[test] +fn neg_float() { + expect_float("3.5 neg", -3.5); +} + +#[test] +fn neg_double() { + expect_int("-5 neg", 5); +} + +#[test] +fn abs_positive() { + expect_int("5 abs", 5); +} + +#[test] +fn abs_negative() { + expect_int("-5 abs", 5); +} + +#[test] +fn abs_float() { + expect_float("-3.5 abs", 3.5); +} + +#[test] +fn floor() { + expect_int("3.7 floor", 3); +} + +#[test] +fn floor_negative() { + expect_int("-3.2 floor", -4); +} + +#[test] +fn ceil() { + expect_int("3.2 ceil", 4); +} + +#[test] +fn ceil_negative() { + expect_int("-3.7 ceil", -3); +} + +#[test] +fn round_down() { + expect_int("3.4 round", 3); +} + +#[test] +fn round_up() { + expect_int("3.6 round", 4); +} + +#[test] +fn round_half() { + expect_int("3.5 round", 4); +} + +#[test] +fn min() { + expect_int("3 5 min", 3); +} + +#[test] +fn min_reverse() { + expect_int("5 3 min", 3); +} + +#[test] +fn max() { + expect_int("3 5 max", 5); +} + +#[test] +fn max_reverse() { + expect_int("5 3 max", 5); +} + +#[test] +fn chain() { + // (2 + 3) * 4 - 1 = 19 + expect_int("2 3 + 4 * 1 -", 19); +} + +#[test] +fn underflow() { + expect_error("1 +", "stack underflow"); +} diff --git a/seq/src/model/forth_tests/comparison.rs b/seq/src/model/forth_tests/comparison.rs new file mode 100644 index 0000000..5926493 --- /dev/null +++ b/seq/src/model/forth_tests/comparison.rs @@ -0,0 +1,136 @@ +use super::harness::*; + +#[test] +fn eq_true() { + expect_int("3 3 =", 1); +} + +#[test] +fn eq_false() { + expect_int("3 4 =", 0); +} + +#[test] +fn eq_mixed_types() { + expect_int("3.0 3 =", 1); +} + +#[test] +fn ne_true() { + expect_int("3 4 <>", 1); +} + +#[test] +fn ne_false() { + expect_int("3 3 <>", 0); +} + +#[test] +fn lt_true() { + expect_int("2 3 <", 1); +} + +#[test] +fn lt_equal() { + expect_int("3 3 <", 0); +} + +#[test] +fn lt_false() { + expect_int("4 3 <", 0); +} + +#[test] +fn gt_true() { + expect_int("4 3 >", 1); +} + +#[test] +fn gt_equal() { + expect_int("3 3 >", 0); +} + +#[test] +fn gt_false() { + expect_int("2 3 >", 0); +} + +#[test] +fn le_less() { + expect_int("2 3 <=", 1); +} + +#[test] +fn le_equal() { + expect_int("3 3 <=", 1); +} + +#[test] +fn le_greater() { + expect_int("4 3 <=", 0); +} + +#[test] +fn ge_greater() { + expect_int("4 3 >=", 1); +} + +#[test] +fn ge_equal() { + expect_int("3 3 >=", 1); +} + +#[test] +fn ge_less() { + expect_int("2 3 >=", 0); +} + +#[test] +fn and_tt() { + expect_int("1 1 and", 1); +} + +#[test] +fn and_tf() { + expect_int("1 0 and", 0); +} + +#[test] +fn and_ff() { + expect_int("0 0 and", 0); +} + +#[test] +fn or_tt() { + expect_int("1 1 or", 1); +} + +#[test] +fn or_tf() { + expect_int("1 0 or", 1); +} + +#[test] +fn or_ff() { + expect_int("0 0 or", 0); +} + +#[test] +fn not_true() { + expect_int("1 not", 0); +} + +#[test] +fn not_false() { + expect_int("0 not", 1); +} + +#[test] +fn truthy_nonzero() { + expect_int("5 not", 0); +} + +#[test] +fn truthy_negative() { + expect_int("-1 not", 0); +} diff --git a/seq/src/model/forth_tests/context.rs b/seq/src/model/forth_tests/context.rs new file mode 100644 index 0000000..2ade530 --- /dev/null +++ b/seq/src/model/forth_tests/context.rs @@ -0,0 +1,71 @@ +use super::harness::*; + +#[test] +fn step() { + let ctx = ctx_with(|c| c.step = 7); + let f = run_ctx("step", &ctx); + assert_eq!(stack_int(&f), 7); +} + +#[test] +fn beat() { + let ctx = ctx_with(|c| c.beat = 4.5); + let f = run_ctx("beat", &ctx); + assert!((stack_float(&f) - 4.5).abs() < 1e-9); +} + +#[test] +fn bank() { + let ctx = ctx_with(|c| c.bank = 2); + let f = run_ctx("bank", &ctx); + assert_eq!(stack_int(&f), 2); +} + +#[test] +fn pattern() { + let ctx = ctx_with(|c| c.pattern = 3); + let f = run_ctx("pattern", &ctx); + assert_eq!(stack_int(&f), 3); +} + +#[test] +fn tempo() { + let ctx = ctx_with(|c| c.tempo = 140.0); + let f = run_ctx("tempo", &ctx); + assert!((stack_float(&f) - 140.0).abs() < 1e-9); +} + +#[test] +fn phase() { + let ctx = ctx_with(|c| c.phase = 0.25); + let f = run_ctx("phase", &ctx); + assert!((stack_float(&f) - 0.25).abs() < 1e-9); +} + +#[test] +fn slot() { + let ctx = ctx_with(|c| c.slot = 5); + let f = run_ctx("slot", &ctx); + assert_eq!(stack_int(&f), 5); +} + +#[test] +fn runs() { + let ctx = ctx_with(|c| c.runs = 10); + let f = run_ctx("runs", &ctx); + assert_eq!(stack_int(&f), 10); +} + +#[test] +fn stepdur() { + // stepdur = 60.0 / tempo / 4.0 / speed = 60 / 120 / 4 / 1 = 0.125 + let f = run("stepdur"); + assert!((stack_float(&f) - 0.125).abs() < 1e-9); +} + +#[test] +fn context_in_computation() { + let ctx = ctx_with(|c| c.step = 3); + let f = run_ctx("60 step +", &ctx); + assert_eq!(stack_int(&f), 63); +} diff --git a/seq/src/model/forth_tests/control_flow.rs b/seq/src/model/forth_tests/control_flow.rs new file mode 100644 index 0000000..8650d2a --- /dev/null +++ b/seq/src/model/forth_tests/control_flow.rs @@ -0,0 +1,64 @@ +use super::harness::*; + +#[test] +fn if_then_true() { + expect_int("1 if 42 then", 42); +} + +#[test] +fn if_then_false() { + let f = run("0 if 42 then"); + assert!(f.stack().is_empty()); +} + +#[test] +fn if_then_with_base() { + expect_int("100 0 if 50 + then", 100); +} + +#[test] +fn if_else_true() { + expect_int("1 if 42 else 99 then", 42); +} + +#[test] +fn if_else_false() { + expect_int("0 if 42 else 99 then", 99); +} + +#[test] +fn nested_tt() { + expect_int("1 if 1 if 100 else 200 then else 300 then", 100); +} + +#[test] +fn nested_tf() { + expect_int("1 if 0 if 100 else 200 then else 300 then", 200); +} + +#[test] +fn nested_f() { + expect_int("0 if 1 if 100 else 200 then else 300 then", 300); +} + +#[test] +fn if_with_computation() { + expect_int("3 2 > if 42 else 99 then", 42); +} + +#[test] +fn missing_then() { + expect_error("1 if 42", "missing 'then'"); +} + +#[test] +fn deeply_nested() { + expect_int("1 if 1 if 1 if 42 then then then", 42); +} + +#[test] +fn chained_conditionals() { + // First if leaves nothing, second if runs + let f = run("0 if 1 then 1 if 42 then"); + assert_eq!(stack_int(&f), 42); +} diff --git a/seq/src/model/forth_tests/errors.rs b/seq/src/model/forth_tests/errors.rs new file mode 100644 index 0000000..0edae38 --- /dev/null +++ b/seq/src/model/forth_tests/errors.rs @@ -0,0 +1,106 @@ +use super::harness::*; + +#[test] +fn empty_script() { + expect_error("", "empty script"); +} + +#[test] +fn whitespace_only() { + expect_error(" \n\t ", "empty script"); +} + +#[test] +fn unknown_word() { + expect_error("foobar", "unknown word"); +} + +#[test] +fn string_not_number() { + expect_error(r#""hello" neg"#, "expected number"); +} + +#[test] +fn get_expects_string() { + expect_error("42 get", "expected string"); +} + +#[test] +fn set_expects_string() { + expect_error("1 2 set", "expected string"); +} + +#[test] +fn comment_ignored() { + expect_int("1 (this is a comment) 2 +", 3); +} + +#[test] +fn multiline_comment() { + expect_int("1 (multi\nline\ncomment) 2 +", 3); +} + +#[test] +fn negative_literal() { + expect_int("-5", -5); +} + +#[test] +fn float_literal() { + let f = run("3.14159"); + let val = stack_float(&f); + assert!((val - 3.14159).abs() < 1e-9); +} + +#[test] +fn string_with_spaces() { + let f = run(r#""hello world" "x" set "x" get"#); + match stack_top(&f) { + crate::model::forth::Value::Str(s) => assert_eq!(s, "hello world"), + other => panic!("expected string, got {:?}", other), + } +} + +#[test] +fn list_count() { + // [ 1 2 3 ] => stack: 1 2 3 3 (items + count on top) + let f = run("[ 1 2 3 ]"); + assert_eq!(stack_int(&f), 3); +} + +#[test] +fn list_empty() { + expect_int("[ ]", 0); +} + +#[test] +fn list_preserves_values() { + // [ 10 20 ] => stack: 10 20 2 + // drop => 10 20 + // + => 30 + expect_int("[ 10 20 ] drop +", 30); +} + +#[test] +fn conditional_based_on_step() { + let ctx0 = ctx_with(|c| c.step = 0); + let ctx1 = ctx_with(|c| c.step = 1); + + let f0 = run_ctx("step 2 mod 0 = if 100 else 200 then", &ctx0); + let f1 = run_ctx("step 2 mod 0 = if 100 else 200 then", &ctx1); + + assert_eq!(stack_int(&f0), 100); + assert_eq!(stack_int(&f1), 200); +} + +#[test] +fn accumulator() { + let f = forth(); + let ctx = default_ctx(); + f.evaluate(r#"0 "acc" set"#, &ctx).unwrap(); + for _ in 0..5 { + f.clear_stack(); + f.evaluate(r#""acc" get 1 + dup "acc" set"#, &ctx).unwrap(); + } + assert_eq!(stack_int(&f), 5); +} diff --git a/seq/src/model/forth_tests/harness.rs b/seq/src/model/forth_tests/harness.rs new file mode 100644 index 0000000..0e64318 --- /dev/null +++ b/seq/src/model/forth_tests/harness.rs @@ -0,0 +1,136 @@ +use crate::model::forth::{Forth, Rng, StepContext, Value, Variables}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +pub fn default_ctx() -> StepContext { + StepContext { + step: 0, + beat: 0.0, + bank: 0, + pattern: 0, + tempo: 120.0, + phase: 0.0, + slot: 0, + runs: 0, + speed: 1.0, + } +} + +pub fn ctx_with(f: impl FnOnce(&mut StepContext)) -> StepContext { + let mut ctx = default_ctx(); + f(&mut ctx); + ctx +} + +pub fn new_vars() -> Variables { + Arc::new(Mutex::new(HashMap::new())) +} + +pub fn seeded_rng(seed: u64) -> Rng { + Arc::new(Mutex::new(StdRng::seed_from_u64(seed))) +} + +pub fn forth() -> Forth { + Forth::new(new_vars(), seeded_rng(42)) +} + +pub fn forth_seeded(seed: u64) -> Forth { + Forth::new(new_vars(), seeded_rng(seed)) +} + +pub fn run(script: &str) -> Forth { + let f = forth(); + f.evaluate(script, &default_ctx()).unwrap(); + f +} + +pub fn run_ctx(script: &str, ctx: &StepContext) -> Forth { + let f = forth(); + f.evaluate(script, ctx).unwrap(); + f +} + +pub fn stack_top(f: &Forth) -> Value { + f.stack().pop().expect("stack empty") +} + +pub fn stack_int(f: &Forth) -> i64 { + match stack_top(f) { + Value::Int(i) => i, + other => panic!("expected Int, got {:?}", other), + } +} + +pub fn stack_float(f: &Forth) -> f64 { + match stack_top(f) { + Value::Float(x) => x, + Value::Int(i) => i as f64, + other => panic!("expected number, got {:?}", other), + } +} + +pub fn expect_stack(script: &str, expected: &[Value]) { + let f = run(script); + let stack = f.stack(); + assert_eq!(stack, expected, "script: {}", script); +} + +pub fn expect_int(script: &str, expected: i64) { + expect_stack(script, &[Value::Int(expected)]); +} + +pub fn expect_float(script: &str, expected: f64) { + let f = run(script); + let stack = f.stack(); + assert_eq!(stack.len(), 1, "expected single value on stack"); + let val = stack_float(&f); + assert!( + (val - expected).abs() < 1e-9, + "expected {}, got {}", + expected, + val + ); +} + +pub fn expect_floats_close(script: &str, expected: f64, epsilon: f64) { + let f = run(script); + let val = stack_float(&f); + assert!( + (val - expected).abs() < epsilon, + "expected ~{}, got {}", + expected, + val + ); +} + +pub fn expect_error(script: &str, expected_substr: &str) { + let f = forth(); + let result = f.evaluate(script, &default_ctx()); + assert!(result.is_err(), "expected error for '{}'", script); + let err = result.unwrap_err(); + assert!( + err.contains(expected_substr), + "error '{}' does not contain '{}'", + err, + expected_substr + ); +} + +pub fn expect_outputs(script: &str, count: usize) -> Vec { + let f = forth(); + let outputs = f.evaluate(script, &default_ctx()).unwrap(); + assert_eq!(outputs.len(), count, "expected {} outputs", count); + outputs +} + +pub fn expect_output_contains(script: &str, substr: &str) { + let outputs = expect_outputs(script, 1); + assert!( + outputs[0].contains(substr), + "output '{}' does not contain '{}'", + outputs[0], + substr + ); +} diff --git a/seq/src/model/forth_tests/mod.rs b/seq/src/model/forth_tests/mod.rs new file mode 100644 index 0000000..5534465 --- /dev/null +++ b/seq/src/model/forth_tests/mod.rs @@ -0,0 +1,10 @@ +mod arithmetic; +mod comparison; +mod context; +mod control_flow; +mod errors; +mod harness; +mod randomness; +mod sound; +mod stack; +mod variables; diff --git a/seq/src/model/forth_tests/randomness.rs b/seq/src/model/forth_tests/randomness.rs new file mode 100644 index 0000000..011d037 --- /dev/null +++ b/seq/src/model/forth_tests/randomness.rs @@ -0,0 +1,114 @@ +use super::harness::*; + +#[test] +fn rand_in_range() { + let f = forth_seeded(12345); + f.evaluate("0 10 rand", &default_ctx()).unwrap(); + let val = stack_float(&f); + assert!(val >= 0.0 && val < 10.0, "rand {} not in [0, 10)", val); +} + +#[test] +fn rand_deterministic() { + let f1 = forth_seeded(99); + let f2 = forth_seeded(99); + f1.evaluate("0 100 rand", &default_ctx()).unwrap(); + f2.evaluate("0 100 rand", &default_ctx()).unwrap(); + assert_eq!(f1.stack(), f2.stack()); +} + +#[test] +fn rrand_inclusive() { + let f = forth_seeded(42); + for _ in 0..20 { + f.clear_stack(); + f.evaluate("1 3 rrand", &default_ctx()).unwrap(); + let val = stack_int(&f); + assert!(val >= 1 && val <= 3, "rrand {} not in [1, 3]", val); + } +} + +#[test] +fn seed_resets() { + let f1 = forth_seeded(1); + f1.evaluate("42 seed 0 100 rand", &default_ctx()).unwrap(); + let f2 = forth_seeded(999); + f2.evaluate("42 seed 0 100 rand", &default_ctx()).unwrap(); + assert_eq!(f1.stack(), f2.stack()); +} + +#[test] +fn coin_binary() { + let f = forth_seeded(42); + f.evaluate("coin", &default_ctx()).unwrap(); + let val = stack_int(&f); + assert!(val == 0 || val == 1); +} + +#[test] +fn chance_zero() { + expect_int("0.0 chance", 0); +} + +#[test] +fn chance_one() { + expect_int("1.0 chance", 1); +} + +#[test] +fn choose_from_list() { + let f = forth_seeded(42); + f.evaluate("10 20 30 3 choose", &default_ctx()).unwrap(); + let val = stack_int(&f); + assert!(val == 10 || val == 20 || val == 30); +} + +#[test] +fn choose_underflow() { + expect_error("1 2 5 choose", "stack underflow"); +} + +#[test] +fn cycle_deterministic() { + for runs in 0..6 { + let ctx = ctx_with(|c| c.runs = runs); + let f = run_ctx("10 20 30 3 cycle", &ctx); + let expected = [10, 20, 30][runs % 3]; + assert_eq!(stack_int(&f), expected, "cycle at runs={}", runs); + } +} + +#[test] +fn cycle_zero_count() { + expect_error("1 2 3 0 cycle", "cycle count must be > 0"); +} + +#[test] +fn mtof_a4() { + expect_float("69 mtof", 440.0); +} + +#[test] +fn mtof_octave() { + expect_float("81 mtof", 880.0); +} + +#[test] +fn mtof_c4() { + expect_floats_close("60 mtof", 261.6255653, 0.001); +} + +#[test] +fn ftom_440() { + expect_float("440 ftom", 69.0); +} + +#[test] +fn ftom_880() { + expect_float("880 ftom", 81.0); +} + +#[test] +fn mtof_ftom_roundtrip() { + expect_float("60 mtof ftom", 60.0); +} diff --git a/seq/src/model/forth_tests/sound.rs b/seq/src/model/forth_tests/sound.rs new file mode 100644 index 0000000..f6beaf8 --- /dev/null +++ b/seq/src/model/forth_tests/sound.rs @@ -0,0 +1,101 @@ +use super::harness::*; + +#[test] +fn basic_emit() { + let outputs = expect_outputs(r#""kick" sound emit"#, 1); + assert!(outputs[0].contains("sound/kick")); +} + +#[test] +fn alias_s() { + let outputs = expect_outputs(r#""snare" s emit"#, 1); + assert!(outputs[0].contains("sound/snare")); +} + +#[test] +fn with_params() { + let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain emit"#, 1); + assert!(outputs[0].contains("sound/kick")); + assert!(outputs[0].contains("freq/440")); + assert!(outputs[0].contains("gain/0.5")); +} + +#[test] +fn auto_dur() { + let outputs = expect_outputs(r#""kick" s emit"#, 1); + assert!(outputs[0].contains("dur/")); +} + +#[test] +fn auto_delaytime() { + let outputs = expect_outputs(r#""kick" s emit"#, 1); + assert!(outputs[0].contains("delaytime/")); +} + +#[test] +fn emit_no_sound() { + expect_error("emit", "no sound set"); +} + +#[test] +fn implicit_emit() { + let outputs = expect_outputs(r#""kick" s 440 freq"#, 1); + assert!(outputs[0].contains("sound/kick")); + assert!(outputs[0].contains("freq/440")); +} + +#[test] +fn multiple_emits() { + let outputs = expect_outputs(r#""kick" s emit "snare" s emit"#, 2); + assert!(outputs[0].contains("sound/kick")); + assert!(outputs[1].contains("sound/snare")); +} + +#[test] +fn subdivide_each() { + let outputs = expect_outputs(r#""kick" s 4 div each"#, 4); +} + +#[test] +fn window_pop() { + let outputs = expect_outputs( + r#"0.0 0.5 window "kick" s emit pop 0.5 1.0 window "snare" s emit"#, + 2, + ); + assert!(outputs[0].contains("sound/kick")); + assert!(outputs[1].contains("sound/snare")); +} + +#[test] +fn pop_root_fails() { + expect_error("pop", "cannot pop root time context"); +} + +#[test] +fn subdivide_zero() { + expect_error(r#""kick" s 0 div each"#, "subdivide count must be > 0"); +} + +#[test] +fn each_without_div() { + expect_error(r#""kick" s each"#, "each requires subdivide first"); +} + +#[test] +fn envelope_params() { + let outputs = expect_outputs( + r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release emit"#, + 1, + ); + assert!(outputs[0].contains("attack/0.01")); + assert!(outputs[0].contains("decay/0.1")); + assert!(outputs[0].contains("sustain/0.7")); + assert!(outputs[0].contains("release/0.3")); +} + +#[test] +fn filter_params() { + let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq emit"#, 1); + assert!(outputs[0].contains("lpf/2000")); + assert!(outputs[0].contains("lpq/0.5")); +} diff --git a/seq/src/model/forth_tests/stack.rs b/seq/src/model/forth_tests/stack.rs new file mode 100644 index 0000000..0c2a134 --- /dev/null +++ b/seq/src/model/forth_tests/stack.rs @@ -0,0 +1,90 @@ +use super::harness::*; +use crate::model::forth::Value::Int; + +#[test] +fn dup() { + expect_stack("3 dup", &[Int(3), Int(3)]); +} + +#[test] +fn dup_underflow() { + expect_error("dup", "stack underflow"); +} + +#[test] +fn drop() { + expect_stack("1 2 drop", &[Int(1)]); +} + +#[test] +fn drop_underflow() { + expect_error("drop", "stack underflow"); +} + +#[test] +fn swap() { + expect_stack("1 2 swap", &[Int(2), Int(1)]); +} + +#[test] +fn swap_underflow() { + expect_error("1 swap", "stack underflow"); +} + +#[test] +fn over() { + expect_stack("1 2 over", &[Int(1), Int(2), Int(1)]); +} + +#[test] +fn over_underflow() { + expect_error("1 over", "stack underflow"); +} + +#[test] +fn rot() { + expect_stack("1 2 3 rot", &[Int(2), Int(3), Int(1)]); +} + +#[test] +fn rot_underflow() { + expect_error("1 2 rot", "stack underflow"); +} + +#[test] +fn nip() { + expect_stack("1 2 nip", &[Int(2)]); +} + +#[test] +fn nip_underflow() { + expect_error("1 nip", "stack underflow"); +} + +#[test] +fn tuck() { + expect_stack("1 2 tuck", &[Int(2), Int(1), Int(2)]); +} + +#[test] +fn tuck_underflow() { + expect_error("1 tuck", "stack underflow"); +} + +#[test] +fn stack_persists() { + let f = forth(); + let ctx = default_ctx(); + f.evaluate("1 2 3", &ctx).unwrap(); + assert_eq!(f.stack(), vec![Int(1), Int(2), Int(3)]); + f.evaluate("4 5", &ctx).unwrap(); + assert_eq!(f.stack(), vec![Int(1), Int(2), Int(3), Int(4), Int(5)]); +} + +#[test] +fn clear_stack() { + let f = forth(); + f.evaluate("1 2 3", &default_ctx()).unwrap(); + f.clear_stack(); + assert!(f.stack().is_empty()); +} diff --git a/seq/src/model/forth_tests/variables.rs b/seq/src/model/forth_tests/variables.rs new file mode 100644 index 0000000..3c26f1e --- /dev/null +++ b/seq/src/model/forth_tests/variables.rs @@ -0,0 +1,39 @@ +use super::harness::*; + +#[test] +fn set_get() { + expect_int(r#"42 "x" set "x" get"#, 42); +} + +#[test] +fn get_nonexistent() { + expect_int(r#""novar" get"#, 0); +} + +#[test] +fn persistence_across_evals() { + let f = forth(); + let ctx = default_ctx(); + f.evaluate(r#"10 "counter" set"#, &ctx).unwrap(); + f.clear_stack(); + f.evaluate(r#""counter" get 1 +"#, &ctx).unwrap(); + assert_eq!(stack_int(&f), 11); +} + +#[test] +fn overwrite() { + expect_int(r#"1 "x" set 99 "x" set "x" get"#, 99); +} + +#[test] +fn multiple_vars() { + let f = run(r#"10 "a" set 20 "b" set "a" get "b" get +"#); + assert_eq!(stack_int(&f), 30); +} + +#[test] +fn float_var() { + let f = run(r#"3.14 "pi" set "pi" get"#); + let val = stack_float(&f); + assert!((val - 3.14).abs() < 1e-9); +} diff --git a/seq/src/model/mod.rs b/seq/src/model/mod.rs index bc10df8..83452c5 100644 --- a/seq/src/model/mod.rs +++ b/seq/src/model/mod.rs @@ -1,5 +1,7 @@ mod file; pub mod forth; +#[cfg(test)] +mod forth_tests; mod project; mod script;