big commit

This commit is contained in:
2026-01-27 01:04:08 +01:00
parent 66933433d1
commit 5456c9414a
15 changed files with 821 additions and 222 deletions

View File

@@ -61,14 +61,14 @@ fn stepdur_baseline() {
#[test]
fn single_emit() {
let outputs = expect_outputs(r#""kick" s @"#, 1);
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 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");
@@ -77,7 +77,7 @@ fn implicit_subdivision_2() {
#[test]
fn implicit_subdivision_4() {
let outputs = expect_outputs(r#""kick" s @ @ @ @"#, 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() {
@@ -92,7 +92,7 @@ fn implicit_subdivision_4() {
#[test]
fn implicit_subdivision_3() {
let outputs = expect_outputs(r#""kick" s @ @ @"#, 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));
@@ -102,7 +102,7 @@ fn implicit_subdivision_3() {
#[test]
fn silence_creates_gap() {
let outputs = expect_outputs(r#""kick" s @ ~ @"#, 2);
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");
@@ -116,7 +116,7 @@ fn silence_creates_gap() {
#[test]
fn silence_at_start() {
let outputs = expect_outputs(r#""kick" s ~ @"#, 1);
let outputs = expect_outputs(r#""kick" s _ ."#, 1);
let deltas = get_deltas(&outputs);
let step = 0.5 / 2.0;
assert!(
@@ -129,13 +129,13 @@ fn silence_at_start() {
#[test]
fn silence_only() {
let outputs = expect_outputs(r#""kick" s ~"#, 0);
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 outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
let sounds = get_sounds(&outputs);
assert_eq!(sounds[0], "kick");
assert_eq!(sounds[1], "kick");
@@ -145,14 +145,14 @@ fn sound_persists() {
#[test]
fn alternating_sounds() {
let outputs = expect_outputs(r#""kick" s @ "snare" s @ "kick" s @ "snare" s @"#, 4);
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 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() {
@@ -168,7 +168,7 @@ fn dur_matches_slot_duration() {
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 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;
@@ -180,7 +180,7 @@ fn tempo_affects_subdivision() {
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 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;
@@ -193,11 +193,11 @@ 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();
let outputs = f.evaluate(r#""kick" s < . _ >"#, &ctx).unwrap();
if runs % 2 == 0 {
assert_eq!(outputs.len(), 1, "runs={}: @ should be picked", runs);
assert_eq!(outputs.len(), 1, "runs={}: . should be picked", runs);
} else {
assert_eq!(outputs.len(), 0, "runs={}: ~ should be picked", runs);
assert_eq!(outputs.len(), 0, "runs={}: _ should be picked", runs);
}
}
}
@@ -207,11 +207,11 @@ 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();
let outputs = f.evaluate(r#""kick" s << . _ >>"#, &ctx).unwrap();
if iter % 2 == 0 {
assert_eq!(outputs.len(), 1, "iter={}: @ should be picked", iter);
assert_eq!(outputs.len(), 1, "iter={}: . should be picked", iter);
} else {
assert_eq!(outputs.len(), 0, "iter={}: ~ should be picked", iter);
assert_eq!(outputs.len(), 0, "iter={}: _ should be picked", iter);
}
}
}
@@ -221,7 +221,7 @@ 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();
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];
@@ -238,7 +238,7 @@ fn dot_alias_for_emit() {
#[test]
fn dot_with_silence() {
let outputs = expect_outputs(r#""kick" s . ~ . ~"#, 2);
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));
@@ -292,7 +292,7 @@ fn internal_alternation_empty_error() {
#[test]
fn div_basic_subdivision() {
let outputs = expect_outputs(r#"div "kick" s . "hat" s . end"#, 2);
let outputs = expect_outputs(r#"div "kick" s . "hat" s . ~"#, 2);
let deltas = get_deltas(&outputs);
let sounds = get_sounds(&outputs);
assert_eq!(sounds, vec!["kick", "hat"]);
@@ -301,59 +301,58 @@ fn div_basic_subdivision() {
}
#[test]
fn div_superposition() {
let outputs = expect_outputs(r#"div "kick" s . end div "hat" s . end"#, 2);
fn div_sequential() {
// Two consecutive divs each claim a slot in root, so they're sequential
let outputs = expect_outputs(r#"div "kick" s . ~ div "hat" s . ~"#, 2);
let deltas = get_deltas(&outputs);
let sounds = get_sounds(&outputs);
assert_eq!(sounds.len(), 2);
// Both at delta 0 (superposed)
assert_eq!(sounds, vec!["kick", "hat"]);
assert!(approx_eq(deltas[0], 0.0));
assert!(approx_eq(deltas[1], 0.0));
assert!(approx_eq(deltas[1], 0.25), "second div at slot 1, got {}", deltas[1]);
}
#[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);
// kick claims slot 0 at root, div claims slot 1 at root
let outputs = expect_outputs(r#""kick" s . div "hat" s . ~"#, 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));
assert_eq!(sounds, vec!["kick", "hat"]);
assert!(approx_eq(deltas[0], 0.0), "kick at slot 0");
assert!(approx_eq(deltas[1], 0.25), "hat at slot 1, got {}", deltas[1]);
}
#[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);
// kick claims slot 0 in outer div, inner div claims slot 1
// Inner div's 2 hats subdivide its slot (0.25 duration) into 2 sub-slots
let outputs = expect_outputs(r#"div "kick" s . div "hat" s . . ~ ~"#, 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");
// Output order: kick (slot 0), then hats (slot 1 subdivided)
assert_eq!(sounds[0], "kick");
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]);
assert_eq!(sounds[2], "hat");
// Outer div has 2 slots of 0.25 each
// kick at slot 0 -> delta 0
// inner div at slot 1 -> starts at 0.25, subdivided into 2 -> hats at 0.25 and 0.375
assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]);
assert!(approx_eq(deltas[1], 0.25), "first hat at 0.25, got {}", deltas[1]);
assert!(approx_eq(deltas[2], 0.375), "second hat at 0.375, got {}", deltas[2]);
}
#[test]
fn div_with_silence() {
let outputs = expect_outputs(r#"div "kick" s . ~ end"#, 1);
let outputs = expect_outputs(r#"div "kick" s . _ ~"#, 1);
let deltas = get_deltas(&outputs);
assert!(approx_eq(deltas[0], 0.0));
}
#[test]
fn div_unmatched_end_error() {
fn unmatched_scope_terminator_error() {
let f = forth();
let result = f.evaluate(r#""kick" s . end"#, &default_ctx());
assert!(result.is_err(), "unmatched end should error");
let result = f.evaluate(r#""kick" s . ~"#, &default_ctx());
assert!(result.is_err(), "unmatched ~ should error");
}
#[test]
@@ -392,7 +391,7 @@ fn alternator_with_arithmetic() {
#[test]
fn stack_superposes_sounds() {
let outputs = expect_outputs(r#"stack "kick" s . "hat" s . end"#, 2);
let outputs = expect_outputs(r#"stack "kick" s . "hat" s . ~"#, 2);
let deltas = get_deltas(&outputs);
let sounds = get_sounds(&outputs);
assert_eq!(sounds.len(), 2);
@@ -403,7 +402,7 @@ fn stack_superposes_sounds() {
#[test]
fn stack_with_multiple_emits() {
let outputs = expect_outputs(r#"stack "kick" s . . . . end"#, 4);
let outputs = expect_outputs(r#"stack "kick" s . . . . ~"#, 4);
let deltas = get_deltas(&outputs);
// All 4 kicks at delta 0
for (i, delta) in deltas.iter().enumerate() {
@@ -415,7 +414,7 @@ fn stack_with_multiple_emits() {
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 outputs = expect_outputs(r#"div stack "kick" s . "hat" s . ~ "snare" s . ~"#, 3);
let deltas = get_deltas(&outputs);
let sounds = get_sounds(&outputs);
// stack resolves first (kick, hat at 0), then div resolves (snare at 0)
@@ -429,20 +428,21 @@ fn stack_inside_div() {
}
#[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);
fn div_nested_with_sibling() {
// Inner div claims slot 0, snare claims slot 1
// Inner div's kick/hat subdivide slot 0
let outputs = expect_outputs(r#"div div "kick" s . "hat" s . ~ "snare" s . ~"#, 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
// Outer div has 2 slots of 0.25 each
// Inner div at slot 0: kick at 0, hat at 0.125
// snare at slot 1: delta 0.25
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]);
assert!(approx_eq(deltas[0], 0.0), "kick at 0, got {}", deltas[0]);
assert!(approx_eq(deltas[1], 0.125), "hat at 0.125, got {}", deltas[1]);
assert!(approx_eq(deltas[2], 0.25), "snare at 0.25, got {}", deltas[2]);
}
#[test]