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

@@ -150,3 +150,53 @@ fn chain() {
fn underflow() {
expect_error("1 +", "stack underflow");
}
#[test]
fn pow_int() {
expect_int("2 3 pow", 8);
}
#[test]
fn pow_float() {
expect_float("2 0.5 pow", std::f64::consts::SQRT_2);
}
#[test]
fn sqrt() {
expect_int("16 sqrt", 4);
}
#[test]
fn sqrt_float() {
expect_float("2 sqrt", std::f64::consts::SQRT_2);
}
#[test]
fn sin_zero() {
expect_int("0 sin", 0);
}
#[test]
fn sin_pi_half() {
expect_float("3.14159265358979 2 / sin", 1.0);
}
#[test]
fn cos_zero() {
expect_int("0 cos", 1);
}
#[test]
fn cos_pi() {
expect_int("3.14159265358979 cos", -1);
}
#[test]
fn log_e() {
expect_float("2.718281828459045 log", 1.0);
}
#[test]
fn log_one() {
expect_int("1 log", 0);
}

View File

@@ -134,3 +134,83 @@ fn truthy_nonzero() {
fn truthy_negative() {
expect_int("-1 not", 0);
}
#[test]
fn xor_tt() {
expect_int("1 1 xor", 0);
}
#[test]
fn xor_tf() {
expect_int("1 0 xor", 1);
}
#[test]
fn xor_ft() {
expect_int("0 1 xor", 1);
}
#[test]
fn xor_ff() {
expect_int("0 0 xor", 0);
}
#[test]
fn nand_tt() {
expect_int("1 1 nand", 0);
}
#[test]
fn nand_tf() {
expect_int("1 0 nand", 1);
}
#[test]
fn nand_ff() {
expect_int("0 0 nand", 1);
}
#[test]
fn nor_tt() {
expect_int("1 1 nor", 0);
}
#[test]
fn nor_tf() {
expect_int("1 0 nor", 0);
}
#[test]
fn nor_ff() {
expect_int("0 0 nor", 1);
}
#[test]
fn ifelse_true() {
expect_int("{ 42 } { 99 } 1 ifelse", 42);
}
#[test]
fn ifelse_false() {
expect_int("{ 42 } { 99 } 0 ifelse", 99);
}
#[test]
fn pick_first() {
expect_int("{ 10 } { 20 } { 30 } 0 pick", 10);
}
#[test]
fn pick_second() {
expect_int("{ 10 } { 20 } { 30 } 1 pick", 20);
}
#[test]
fn pick_third() {
expect_int("{ 10 } { 20 } { 30 } 2 pick", 30);
}
#[test]
fn pick_preserves_stack() {
expect_int("5 { 10 } { 20 } 0 pick +", 15);
}

View File

@@ -22,7 +22,7 @@ fn redefine_word_overwrites() {
#[test]
fn word_with_param() {
let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" s loud emit", 1);
let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" s loud .", 1);
assert!(outputs[0].contains("gain/0.9"));
}
@@ -97,7 +97,7 @@ fn define_word_containing_quotation() {
#[test]
fn define_word_with_sound() {
let outputs = expect_outputs(": kick \"kick\" s emit ; kick", 1);
let outputs = expect_outputs(": kick \"kick\" s . ; kick", 1);
assert!(outputs[0].contains("sound/kick"));
}

View File

@@ -72,7 +72,7 @@ fn word_with_sound_params() {
let f = forth();
let ctx = ctx_with(|c| c.runs = 0);
let outputs = f.evaluate(
": myverb 0.5 verb ; \"sine\" s 440 freq < myverb > emit",
": myverb 0.5 verb ; \"sine\" s 440 freq < myverb > .",
&ctx
).unwrap();
assert_eq!(outputs.len(), 1);

View File

@@ -59,31 +59,31 @@ fn nested_quotations() {
#[test]
fn quotation_with_param() {
let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? emit"#, 1);
let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? ."#, 1);
assert!(outputs[0].contains("distort/2"));
}
#[test]
fn quotation_skips_param() {
let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? emit"#, 1);
let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? ."#, 1);
assert!(!outputs[0].contains("distort"));
}
#[test]
fn quotation_with_emit() {
// When true, emit should fire
let outputs = expect_outputs(r#""kick" s { emit } 1 ?"#, 1);
// When true, . should fire
let outputs = expect_outputs(r#""kick" s { . } 1 ?"#, 1);
assert!(outputs[0].contains("kick"));
}
#[test]
fn quotation_skips_emit() {
// When false, emit should not fire
// When false, . should not fire
let f = forth();
let outputs = f
.evaluate(r#""kick" s { emit } 0 ?"#, &default_ctx())
.evaluate(r#""kick" s { . } 0 ?"#, &default_ctx())
.unwrap();
// No output since emit was skipped and no implicit emit
// No output since . was skipped and no implicit emit
assert_eq!(outputs.len(), 0);
}
@@ -110,7 +110,7 @@ fn every_with_quotation_integration() {
let ctx = ctx_with(|c| c.iter = iter);
let f = forth();
let outputs = f
.evaluate(r#""kick" s { 2 distort } 2 every ? emit"#, &ctx)
.evaluate(r#""kick" s { 2 distort } 2 every ? ."#, &ctx)
.unwrap();
if iter % 2 == 0 {
assert!(
@@ -163,7 +163,7 @@ fn when_and_unless_complementary() {
let f = forth();
let outputs = f
.evaluate(
r#""kick" s { 2 distort } 2 every ? { 4 distort } 2 every !? emit"#,
r#""kick" s { 2 distort } 2 every ? { 4 distort } 2 every !? ."#,
&ctx,
)
.unwrap();

View File

@@ -130,64 +130,64 @@ fn ramp_with_range() {
}
#[test]
fn noise_deterministic() {
fn perlin_deterministic() {
let ctx = ctx_with(|c| c.beat = 2.7);
let f = forth();
f.evaluate("1.0 noise", &ctx).unwrap();
f.evaluate("1.0 perlin", &ctx).unwrap();
let val1 = stack_float(&f);
f.evaluate("1.0 noise", &ctx).unwrap();
f.evaluate("1.0 perlin", &ctx).unwrap();
let val2 = stack_float(&f);
assert!((val1 - val2).abs() < 1e-9, "noise should be deterministic");
assert!((val1 - val2).abs() < 1e-9, "perlin should be deterministic");
}
#[test]
fn noise_in_range() {
fn perlin_in_range() {
for i in 0..100 {
let ctx = ctx_with(|c| c.beat = i as f64 * 0.1);
let f = forth();
f.evaluate("1.0 noise", &ctx).unwrap();
f.evaluate("1.0 perlin", &ctx).unwrap();
let val = stack_float(&f);
assert!(val >= 0.0 && val <= 1.0, "noise out of range: {}", val);
assert!(val >= 0.0 && val <= 1.0, "perlin out of range: {}", val);
}
}
#[test]
fn noise_varies() {
fn perlin_varies() {
let ctx1 = ctx_with(|c| c.beat = 0.5);
let ctx2 = ctx_with(|c| c.beat = 1.5);
let f = forth();
f.evaluate("1.0 noise", &ctx1).unwrap();
f.evaluate("1.0 perlin", &ctx1).unwrap();
let val1 = stack_float(&f);
f.evaluate("1.0 noise", &ctx2).unwrap();
f.evaluate("1.0 perlin", &ctx2).unwrap();
let val2 = stack_float(&f);
assert!((val1 - val2).abs() > 1e-9, "noise should vary with beat");
assert!((val1 - val2).abs() > 1e-9, "perlin should vary with beat");
}
#[test]
fn noise_smooth() {
fn perlin_smooth() {
let f = forth();
let mut prev = 0.0;
for i in 0..100 {
let ctx = ctx_with(|c| c.beat = i as f64 * 0.01);
f.evaluate("1.0 noise", &ctx).unwrap();
f.evaluate("1.0 perlin", &ctx).unwrap();
let val = stack_float(&f);
if i > 0 {
assert!((val - prev).abs() < 0.2, "noise not smooth: jump {} at step {}", (val - prev).abs(), i);
assert!((val - prev).abs() < 0.2, "perlin not smooth: jump {} at step {}", (val - prev).abs(), i);
}
prev = val;
}
}
#[test]
fn noise_with_range() {
fn perlin_with_range() {
let ctx = ctx_with(|c| c.beat = 1.3);
let f = forth();
f.evaluate("1.0 noise 200.0 800.0 range", &ctx).unwrap();
f.evaluate("1.0 perlin 200.0 800.0 range", &ctx).unwrap();
let val = stack_float(&f);
assert!(val >= 200.0 && val <= 800.0, "noise+range out of bounds: {}", val);
assert!(val >= 200.0 && val <= 800.0, "perlin+range out of bounds: {}", val);
}
#[test]
fn noise_underflow() {
expect_error("noise", "stack underflow");
fn perlin_underflow() {
expect_error("perlin", "stack underflow");
}

View File

@@ -2,19 +2,19 @@ use super::harness::*;
#[test]
fn basic_emit() {
let outputs = expect_outputs(r#""kick" sound emit"#, 1);
let outputs = expect_outputs(r#""kick" sound ."#, 1);
assert!(outputs[0].contains("sound/kick"));
}
#[test]
fn alias_s() {
let outputs = expect_outputs(r#""snare" s emit"#, 1);
let outputs = expect_outputs(r#""snare" s ."#, 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);
let outputs = expect_outputs(r#""kick" s 440 freq 0.5 gain ."#, 1);
assert!(outputs[0].contains("sound/kick"));
assert!(outputs[0].contains("freq/440"));
assert!(outputs[0].contains("gain/0.5"));
@@ -22,24 +22,24 @@ fn with_params() {
#[test]
fn auto_dur() {
let outputs = expect_outputs(r#""kick" s emit"#, 1);
let outputs = expect_outputs(r#""kick" s ."#, 1);
assert!(outputs[0].contains("dur/"));
}
#[test]
fn auto_delaytime() {
let outputs = expect_outputs(r#""kick" s emit"#, 1);
let outputs = expect_outputs(r#""kick" s ."#, 1);
assert!(outputs[0].contains("delaytime/"));
}
#[test]
fn emit_no_sound() {
expect_error("emit", "no sound set");
expect_error(".", "no sound set");
}
#[test]
fn multiple_emits() {
let outputs = expect_outputs(r#""kick" s emit "snare" s emit"#, 2);
let outputs = expect_outputs(r#""kick" s . "snare" s ."#, 2);
assert!(outputs[0].contains("sound/kick"));
assert!(outputs[1].contains("sound/snare"));
}
@@ -47,7 +47,7 @@ fn multiple_emits() {
#[test]
fn envelope_params() {
let outputs = expect_outputs(
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release emit"#,
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
1,
);
assert!(outputs[0].contains("attack/0.01"));
@@ -58,14 +58,14 @@ fn envelope_params() {
#[test]
fn filter_params() {
let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq emit"#, 1);
let outputs = expect_outputs(r#""synth" s 2000 lpf 0.5 lpq ."#, 1);
assert!(outputs[0].contains("lpf/2000"));
assert!(outputs[0].contains("lpq/0.5"));
}
#[test]
fn adsr_sets_all_envelope_params() {
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr emit"#, 1);
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1);
assert!(outputs[0].contains("attack/0.01"));
assert!(outputs[0].contains("decay/0.1"));
assert!(outputs[0].contains("sustain/0.5"));
@@ -74,7 +74,7 @@ fn adsr_sets_all_envelope_params() {
#[test]
fn ad_sets_attack_decay_sustain_zero() {
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad emit"#, 1);
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1);
assert!(outputs[0].contains("attack/0.01"));
assert!(outputs[0].contains("decay/0.1"));
assert!(outputs[0].contains("sustain/0"));
@@ -82,7 +82,7 @@ fn ad_sets_attack_decay_sustain_zero() {
#[test]
fn bank_param() {
let outputs = expect_outputs(r#""loop" s "a" bank emit"#, 1);
let outputs = expect_outputs(r#""loop" s "a" bank ."#, 1);
assert!(outputs[0].contains("sound/loop"));
assert!(outputs[0].contains("bank/a"));
}

View File

@@ -35,11 +35,6 @@ fn dupn_underflow() {
expect_error("3 dupn", "stack underflow");
}
#[test]
fn bang_alias() {
expect_stack("c4 3 !", &[int(60), int(60), int(60)]);
}
#[test]
fn drop() {
expect_stack("1 2 drop", &[int(1)]);

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]