use super::harness::*; #[allow(unused_imports)] use super::harness::{forth_with_counter, new_emission_counter}; use std::collections::HashMap; fn parse_params(output: &str) -> HashMap { let mut params = HashMap::new(); let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); let mut i = 0; while i + 1 < parts.len() { if let Ok(v) = parts[i + 1].parse::() { params.insert(parts[i].to_string(), v); } i += 2; } params } fn get_deltas(outputs: &[String]) -> Vec { outputs .iter() .map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0)) .collect() } fn get_durs(outputs: &[String]) -> Vec { outputs .iter() .map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0)) .collect() } fn get_sounds(outputs: &[String]) -> Vec { outputs .iter() .map(|o| { let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect(); if parts.len() >= 2 && parts[0] == "sound" { parts[1].to_string() } else { String::new() } }) .collect() } const EPSILON: f64 = 1e-9; fn approx_eq(a: f64, b: f64) -> bool { (a - b).abs() < EPSILON } // At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s // Root duration = 4 * stepdur = 0.5s #[test] fn stepdur_baseline() { let f = run("stepdur"); assert!(approx_eq(stack_float(&f), 0.125)); } #[test] fn single_emit() { let outputs = expect_outputs(r#""kick" s @"#, 1); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0"); } #[test] fn implicit_subdivision_2() { let outputs = expect_outputs(r#""kick" s @ @"#, 2); let deltas = get_deltas(&outputs); let step = 0.5 / 2.0; assert!(approx_eq(deltas[0], 0.0), "first slot at 0"); assert!(approx_eq(deltas[1], step), "second slot at {}, got {}", step, deltas[1]); } #[test] fn implicit_subdivision_4() { let outputs = expect_outputs(r#""kick" s @ @ @ @"#, 4); let deltas = get_deltas(&outputs); let step = 0.5 / 4.0; for (i, delta) in deltas.iter().enumerate() { let expected = step * i as f64; assert!( approx_eq(*delta, expected), "slot {}: expected {}, got {}", i, expected, delta ); } } #[test] fn implicit_subdivision_3() { let outputs = expect_outputs(r#""kick" s @ @ @"#, 3); let deltas = get_deltas(&outputs); let step = 0.5 / 3.0; assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], step), "got {}", deltas[1]); assert!(approx_eq(deltas[2], 2.0 * step), "got {}", deltas[2]); } #[test] fn silence_creates_gap() { let outputs = expect_outputs(r#""kick" s @ ~ @"#, 2); let deltas = get_deltas(&outputs); let step = 0.5 / 3.0; assert!(approx_eq(deltas[0], 0.0), "first at 0"); assert!( approx_eq(deltas[1], 2.0 * step), "third slot (after silence) at {}, got {}", 2.0 * step, deltas[1] ); } #[test] fn silence_at_start() { let outputs = expect_outputs(r#""kick" s ~ @"#, 1); let deltas = get_deltas(&outputs); let step = 0.5 / 2.0; assert!( approx_eq(deltas[0], step), "emit after silence at {}, got {}", step, deltas[0] ); } #[test] fn silence_only() { let outputs = expect_outputs(r#""kick" s ~"#, 0); assert!(outputs.is_empty(), "silence only should produce no output"); } #[test] fn sound_persists() { let outputs = expect_outputs(r#""kick" s @ @ "hat" s @ @"#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds[0], "kick"); assert_eq!(sounds[1], "kick"); assert_eq!(sounds[2], "hat"); assert_eq!(sounds[3], "hat"); } #[test] fn alternating_sounds() { let outputs = expect_outputs(r#""kick" s @ "snare" s @ "kick" s @ "snare" s @"#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]); } #[test] fn dur_matches_slot_duration() { let outputs = expect_outputs(r#""kick" s @ @ @ @"#, 4); let durs = get_durs(&outputs); let expected_dur = 0.5 / 4.0; for (i, dur) in durs.iter().enumerate() { assert!( approx_eq(*dur, expected_dur), "slot {} dur: expected {}, got {}", i, expected_dur, dur ); } } #[test] fn tempo_affects_subdivision() { let ctx = ctx_with(|c| c.tempo = 60.0); let f = forth(); let outputs = f.evaluate(r#""kick" s @ @"#, &ctx).unwrap(); let deltas = get_deltas(&outputs); // At 60 BPM: stepdur = 0.25, root dur = 1.0 let step = 1.0 / 2.0; assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], step), "got {}", deltas[1]); } #[test] fn speed_affects_subdivision() { let ctx = ctx_with(|c| c.speed = 2.0); let f = forth(); let outputs = f.evaluate(r#""kick" s @ @"#, &ctx).unwrap(); let deltas = get_deltas(&outputs); // At speed 2.0: stepdur = 0.0625, root dur = 0.25 let step = 0.25 / 2.0; assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], step), "got {}", deltas[1]); } #[test] fn cycle_picks_by_step() { for runs in 0..4 { let ctx = ctx_with(|c| c.runs = runs); let f = forth(); let outputs = f.evaluate(r#""kick" s < @ ~ >"#, &ctx).unwrap(); if runs % 2 == 0 { assert_eq!(outputs.len(), 1, "runs={}: @ should be picked", runs); } else { assert_eq!(outputs.len(), 0, "runs={}: ~ should be picked", runs); } } } #[test] fn pcycle_picks_by_pattern() { for iter in 0..4 { let ctx = ctx_with(|c| c.iter = iter); let f = forth(); let outputs = f.evaluate(r#""kick" s << @ ~ >>"#, &ctx).unwrap(); if iter % 2 == 0 { assert_eq!(outputs.len(), 1, "iter={}: @ should be picked", iter); } else { assert_eq!(outputs.len(), 0, "iter={}: ~ should be picked", iter); } } } #[test] fn cycle_with_sounds() { for runs in 0..3 { let ctx = ctx_with(|c| c.runs = runs); let f = forth(); let outputs = f.evaluate(r#"< { "kick" s @ } { "hat" s @ } { "snare" s @ } >"#, &ctx).unwrap(); assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs); let sounds = get_sounds(&outputs); let expected = ["kick", "hat", "snare"][runs % 3]; assert_eq!(sounds[0], expected, "runs={}: expected {}", runs, expected); } } #[test] fn dot_alias_for_emit() { let outputs = expect_outputs(r#""kick" s . . . ."#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]); } #[test] fn dot_with_silence() { let outputs = expect_outputs(r#""kick" s . ~ . ~"#, 2); let deltas = get_deltas(&outputs); let step = 0.5 / 4.0; assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 2.0 * step)); } #[test] fn internal_alternation_basic() { let outputs = expect_outputs(r#"| "kick" "snare" | s . . . ."#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]); } #[test] fn internal_alternation_three_sounds() { let outputs = expect_outputs(r#"| "kick" "snare" "hat" | s . . . . . ."#, 6); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "snare", "hat", "kick", "snare", "hat"]); } #[test] fn internal_alternation_single_item() { let outputs = expect_outputs(r#"| "kick" | s . . . ."#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]); } #[test] fn internal_alternation_with_params() { let outputs = expect_outputs(r#"| 0.5 0.9 | gain "kick" s . ."#, 2); fn parse_gain(output: &str) -> f64 { let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); for i in 0..parts.len() - 1 { if parts[i] == "gain" { return parts[i + 1].parse().unwrap_or(0.0); } } 0.0 } let gains: Vec = outputs.iter().map(|o| parse_gain(o)).collect(); assert!(approx_eq(gains[0], 0.5), "first gain should be 0.5, got {}", gains[0]); assert!(approx_eq(gains[1], 0.9), "second gain should be 0.9, got {}", gains[1]); } #[test] fn internal_alternation_empty_error() { let f = forth(); let result = f.evaluate(r#"| | . ."#, &default_ctx()); assert!(result.is_err(), "empty internal cycle should error"); } #[test] fn div_basic_subdivision() { let outputs = expect_outputs(r#"div "kick" s . "hat" s . end"#, 2); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "hat"]); assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.25), "second should be at 0.25, got {}", deltas[1]); } #[test] fn div_superposition() { let outputs = expect_outputs(r#"div "kick" s . end div "hat" s . end"#, 2); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); assert_eq!(sounds.len(), 2); // Both at delta 0 (superposed) assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.0)); } #[test] fn div_with_root_emit() { // kick at root level, hat in div - both should superpose at 0 // Note: div resolves first (when end is hit), root resolves at script end let outputs = expect_outputs(r#""kick" s . div "hat" s . end"#, 2); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); // Order is hat then kick because div resolves before root assert_eq!(sounds, vec!["hat", "kick"]); assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.0)); } #[test] fn div_nested() { // kick takes first slot in outer div, inner div takes second slot // Inner div resolves first, then outer div resolves let outputs = expect_outputs(r#"div "kick" s . div "hat" s . . end end"#, 3); let sounds = get_sounds(&outputs); let deltas = get_deltas(&outputs); // Inner div resolves first (hat, hat), then outer div (kick) assert_eq!(sounds[0], "hat"); assert_eq!(sounds[1], "hat"); assert_eq!(sounds[2], "kick"); // Inner div inherits parent's start (0) and duration (0.5), subdivides into 2 assert!(approx_eq(deltas[0], 0.0), "first hat at 0, got {}", deltas[0]); assert!(approx_eq(deltas[1], 0.25), "second hat at 0.25, got {}", deltas[1]); // Outer div has 2 slots: kick at 0, inner div at slot 1 (but inner resolved independently) assert!(approx_eq(deltas[2], 0.0), "kick at 0, got {}", deltas[2]); } #[test] fn div_with_silence() { let outputs = expect_outputs(r#"div "kick" s . ~ end"#, 1); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0)); } #[test] fn div_unmatched_end_error() { let f = forth(); let result = f.evaluate(r#""kick" s . end"#, &default_ctx()); assert!(result.is_err(), "unmatched end should error"); } #[test] fn alternator_with_scale() { let outputs = expect_outputs(r#""sine" s | 0 1 2 3 | mixolydian note . . . ."#, 4); fn parse_note(output: &str) -> i64 { let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); for i in 0..parts.len() - 1 { if parts[i] == "note" { return parts[i + 1].parse().unwrap_or(0); } } 0 } let notes: Vec = outputs.iter().map(|o| parse_note(o)).collect(); // mixolydian from C4: 0->60, 1->62, 2->64, 3->65 assert_eq!(notes, vec![60, 62, 64, 65]); } #[test] fn alternator_with_arithmetic() { let outputs = expect_outputs(r#""sine" s | 100 200 | 2 * freq . ."#, 2); fn parse_freq(output: &str) -> f64 { let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); for i in 0..parts.len() - 1 { if parts[i] == "freq" { return parts[i + 1].parse().unwrap_or(0.0); } } 0.0 } let freqs: Vec = outputs.iter().map(|o| parse_freq(o)).collect(); assert!(approx_eq(freqs[0], 200.0), "first freq: expected 200, got {}", freqs[0]); assert!(approx_eq(freqs[1], 400.0), "second freq: expected 400, got {}", freqs[1]); } #[test] fn stack_superposes_sounds() { let outputs = expect_outputs(r#"stack "kick" s . "hat" s . end"#, 2); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); assert_eq!(sounds.len(), 2); // Both at delta 0 (stacked/superposed) assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.0)); } #[test] fn stack_with_multiple_emits() { let outputs = expect_outputs(r#"stack "kick" s . . . . end"#, 4); let deltas = get_deltas(&outputs); // All 4 kicks at delta 0 for (i, delta) in deltas.iter().enumerate() { assert!(approx_eq(*delta, 0.0), "emit {} should be at 0, got {}", i, delta); } } #[test] fn stack_inside_div() { // div subdivides, stack inside superposes // stack doesn't claim a slot in parent div, so snare is also at 0 let outputs = expect_outputs(r#"div stack "kick" s . "hat" s . end "snare" s . end"#, 3); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); // stack resolves first (kick, hat at 0), then div resolves (snare at 0) // since stack doesn't consume a slot in the parent div assert_eq!(sounds[0], "kick"); assert_eq!(sounds[1], "hat"); assert_eq!(sounds[2], "snare"); assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.0)); assert!(approx_eq(deltas[2], 0.0), "snare at 0, got {}", deltas[2]); } #[test] fn div_then_stack_sequential() { // Nested div doesn't claim a slot in parent, only emit/silence do // So nested div and snare both resolve with parent's timing let outputs = expect_outputs(r#"div div "kick" s . "hat" s . end "snare" s . end"#, 3); let deltas = get_deltas(&outputs); let sounds = get_sounds(&outputs); // Inner div resolves first (kick at 0, hat at 0.25 of parent duration) // Outer div has 1 slot (snare's .), so snare at 0 assert_eq!(sounds[0], "kick"); assert_eq!(sounds[1], "hat"); assert_eq!(sounds[2], "snare"); assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], 0.25), "hat at 0.25, got {}", deltas[1]); assert!(approx_eq(deltas[2], 0.0), "snare at 0, got {}", deltas[2]); } #[test] fn emit_n_basic() { let outputs = expect_outputs(r#""kick" s 4 .!"#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]); } #[test] fn emit_n_with_alternator() { let outputs = expect_outputs(r#"| "kick" "snare" | s 4 .!"#, 4); let sounds = get_sounds(&outputs); assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]); } #[test] fn emit_n_zero() { let outputs = expect_outputs(r#""kick" s 0 .!"#, 0); assert!(outputs.is_empty()); } #[test] fn emit_n_negative_error() { let f = forth(); let result = f.evaluate(r#""kick" s -1 .!"#, &default_ctx()); assert!(result.is_err()); } #[test] fn persistent_counter_across_evaluations() { let counter = new_emission_counter(); let ctx = default_ctx(); // First evaluation: kick, snare, kick, snare let f1 = forth_with_counter(counter.clone()); let outputs1 = f1.evaluate(r#"| "kick" "snare" | s . ."#, &ctx).unwrap(); let sounds1 = get_sounds(&outputs1); assert_eq!(sounds1, vec!["kick", "snare"]); // Second evaluation: continues from where we left off let f2 = forth_with_counter(counter.clone()); let outputs2 = f2.evaluate(r#"| "kick" "snare" | s . ."#, &ctx).unwrap(); let sounds2 = get_sounds(&outputs2); assert_eq!(sounds2, vec!["kick", "snare"]); } #[test] fn persistent_counter_three_item_cycle() { let counter = new_emission_counter(); let ctx = default_ctx(); // First eval: kick, snare let f1 = forth_with_counter(counter.clone()); let outputs1 = f1.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap(); let sounds1 = get_sounds(&outputs1); assert_eq!(sounds1, vec!["kick", "snare"]); // Second eval: continues from hat (index 2) let f2 = forth_with_counter(counter.clone()); let outputs2 = f2.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap(); let sounds2 = get_sounds(&outputs2); assert_eq!(sounds2, vec!["hat", "kick"]); // Third eval: snare, hat let f3 = forth_with_counter(counter.clone()); let outputs3 = f3.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap(); let sounds3 = get_sounds(&outputs3); assert_eq!(sounds3, vec!["snare", "hat"]); } #[test] fn emit_n_with_persistent_counter() { let counter = new_emission_counter(); let ctx = default_ctx(); // First eval: 3 emits from a 4-item cycle let f1 = forth_with_counter(counter.clone()); let outputs1 = f1.evaluate(r#"| "a" "b" "c" "d" | s 3 .!"#, &ctx).unwrap(); let sounds1 = get_sounds(&outputs1); assert_eq!(sounds1, vec!["a", "b", "c"]); // Second eval: continues from d let f2 = forth_with_counter(counter.clone()); let outputs2 = f2.evaluate(r#"| "a" "b" "c" "d" | s 3 .!"#, &ctx).unwrap(); let sounds2 = get_sounds(&outputs2); assert_eq!(sounds2, vec!["d", "a", "b"]); }