WIP
This commit is contained in:
@@ -34,9 +34,6 @@ mod variables;
|
||||
#[path = "forth/quotations.rs"]
|
||||
mod quotations;
|
||||
|
||||
#[path = "forth/iteration.rs"]
|
||||
mod iteration;
|
||||
|
||||
#[path = "forth/notes.rs"]
|
||||
mod notes;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use cagire::forth::{Dictionary, Forth, Rng, StepContext, Value, Variables};
|
||||
use cagire::forth::{Dictionary, EmissionCounter, Forth, Rng, StepContext, Value, Variables};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -46,6 +46,14 @@ pub fn forth_seeded(seed: u64) -> Forth {
|
||||
Forth::new(new_vars(), new_dict(), seeded_rng(seed))
|
||||
}
|
||||
|
||||
pub fn new_emission_counter() -> EmissionCounter {
|
||||
Arc::new(Mutex::new(0))
|
||||
}
|
||||
|
||||
pub fn forth_with_counter(counter: EmissionCounter) -> Forth {
|
||||
Forth::new_with_counter(new_vars(), new_dict(), seeded_rng(42), counter)
|
||||
}
|
||||
|
||||
pub fn run(script: &str) -> Forth {
|
||||
let f = forth();
|
||||
f.evaluate(script, &default_ctx()).unwrap();
|
||||
|
||||
@@ -22,7 +22,7 @@ fn interval_stacking_builds_chord() {
|
||||
|
||||
#[test]
|
||||
fn interval_tritone() {
|
||||
expect_stack("c4 tritone", &ints(&[60, 66]));
|
||||
// "tritone" word is taken by the scale, use aug4 or dim5 for interval
|
||||
expect_stack("c4 aug4", &ints(&[60, 66]));
|
||||
expect_stack("c4 dim5", &ints(&[60, 66]));
|
||||
}
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
use super::harness::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn parse_params(output: &str) -> HashMap<String, f64> {
|
||||
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::<f64>() {
|
||||
params.insert(parts[i].to_string(), v);
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
fn get_deltas(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_durs(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_notes(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("note").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_sounds(outputs: &[String]) -> Vec<String> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| {
|
||||
let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect();
|
||||
for i in (0..parts.len()).step_by(2) {
|
||||
if parts[i] == "sound" && i + 1 < parts.len() {
|
||||
return parts[i + 1].to_string();
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
const EPSILON: f64 = 1e-9;
|
||||
|
||||
fn approx_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < EPSILON
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_creates_subdivisions_at_same_time() {
|
||||
let outputs = expect_outputs(r#""kick" s 3 stack each"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.0));
|
||||
assert!(approx_eq(deltas[2], 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_vs_div_timing() {
|
||||
let stack_outputs = expect_outputs(r#""kick" s 3 stack each"#, 3);
|
||||
let div_outputs = expect_outputs(r#""kick" s 3 div each"#, 3);
|
||||
|
||||
let stack_deltas = get_deltas(&stack_outputs);
|
||||
let div_deltas = get_deltas(&div_outputs);
|
||||
|
||||
for d in stack_deltas {
|
||||
assert!(approx_eq(d, 0.0), "stack should have all delta=0");
|
||||
}
|
||||
|
||||
assert!(approx_eq(div_deltas[0], 0.0));
|
||||
assert!(!approx_eq(div_deltas[1], 0.0), "div should spread in time");
|
||||
assert!(!approx_eq(div_deltas[2], 0.0), "div should spread in time");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_with_div_arpeggio() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 div for"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(!approx_eq(deltas[1], 0.0));
|
||||
assert!(!approx_eq(deltas[2], 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_with_stack_chord() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 stack for"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
for d in deltas {
|
||||
assert!(approx_eq(d, 0.0), "stack for should have all delta=0");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_with_for() {
|
||||
let outputs = expect_outputs(r#"{ | 60 62 64 | note "sine" s emit } 3 div for"#, 3);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 62.0));
|
||||
assert!(approx_eq(notes[2], 64.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_wraps_around() {
|
||||
let outputs = expect_outputs(r#"{ | 60 62 | note "sine" s emit } 4 div for"#, 4);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 62.0));
|
||||
assert!(approx_eq(notes[2], 60.0));
|
||||
assert!(approx_eq(notes[3], 62.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_local_cycles() {
|
||||
let outputs =
|
||||
expect_outputs(r#"{ | "bd" "sn" | s | 60 64 | note emit } 2 stack for"#, 2);
|
||||
let sounds = get_sounds(&outputs);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert_eq!(sounds[0], "bd");
|
||||
assert_eq!(sounds[1], "sn");
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_outside_for_defaults_to_first() {
|
||||
expect_int("| 60 62 64 |", 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polymetric_cycles() {
|
||||
let outputs = expect_outputs(
|
||||
r#"{ | 0 1 | n | "a" "b" "c" | s emit } 6 div for"#,
|
||||
6,
|
||||
);
|
||||
let sounds = get_sounds(&outputs);
|
||||
|
||||
assert_eq!(sounds[0], "a");
|
||||
assert_eq!(sounds[1], "b");
|
||||
assert_eq!(sounds[2], "c");
|
||||
assert_eq!(sounds[3], "a");
|
||||
assert_eq!(sounds[4], "b");
|
||||
assert_eq!(sounds[5], "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 stack each"#, "stack count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_requires_subdivide() {
|
||||
expect_error(r#"{ "kick" s emit } for"#, "for requires subdivide first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_requires_quotation() {
|
||||
expect_error(r#"42 3 div for"#, "expected quotation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_local_cycle() {
|
||||
expect_error("| |", "empty local cycle list");
|
||||
}
|
||||
|
||||
// Echo tests - stutter effect with halving durations
|
||||
|
||||
#[test]
|
||||
fn echo_creates_decaying_subdivisions() {
|
||||
// default dur = 0.5, echo 3
|
||||
// d1 + d1/2 + d1/4 = d1 * 1.75 = 0.5
|
||||
// d1 = 0.5 / 1.75
|
||||
let outputs = expect_outputs(r#""kick" s 3 echo each"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
let d1 = 0.5 / 1.75;
|
||||
let d2 = d1 / 2.0;
|
||||
let d3 = d1 / 4.0;
|
||||
|
||||
assert!(approx_eq(durs[0], d1), "first dur should be {}, got {}", d1, durs[0]);
|
||||
assert!(approx_eq(durs[1], d2), "second dur should be {}, got {}", d2, durs[1]);
|
||||
assert!(approx_eq(durs[2], d3), "third dur should be {}, got {}", d3, durs[2]);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0), "first delta should be 0");
|
||||
assert!(approx_eq(deltas[1], d1), "second delta should be {}, got {}", d1, deltas[1]);
|
||||
assert!(approx_eq(deltas[2], d1 + d2), "third delta should be {}, got {}", d1 + d2, deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_with_for() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 echo for"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
|
||||
// Each subsequent duration should be half the previous
|
||||
assert!(approx_eq(durs[1], durs[0] / 2.0), "second should be half of first");
|
||||
assert!(approx_eq(durs[2], durs[1] / 2.0), "third should be half of second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 echo each"#, "echo count must be > 0");
|
||||
}
|
||||
|
||||
// Necho tests - reverse echo (durations grow)
|
||||
|
||||
#[test]
|
||||
fn necho_creates_growing_subdivisions() {
|
||||
// default dur = 0.5, necho 3
|
||||
// d1 + 2*d1 + 4*d1 = d1 * 7 = 0.5
|
||||
// d1 = 0.5 / 7
|
||||
let outputs = expect_outputs(r#""kick" s 3 necho each"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
let d1 = 0.5 / 7.0;
|
||||
let d2 = d1 * 2.0;
|
||||
let d3 = d1 * 4.0;
|
||||
|
||||
assert!(approx_eq(durs[0], d1), "first dur should be {}, got {}", d1, durs[0]);
|
||||
assert!(approx_eq(durs[1], d2), "second dur should be {}, got {}", d2, durs[1]);
|
||||
assert!(approx_eq(durs[2], d3), "third dur should be {}, got {}", d3, durs[2]);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0), "first delta should be 0");
|
||||
assert!(approx_eq(deltas[1], d1), "second delta should be {}, got {}", d1, deltas[1]);
|
||||
assert!(approx_eq(deltas[2], d1 + d2), "third delta should be {}, got {}", d1 + d2, deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn necho_with_for() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 necho for"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
|
||||
// Each subsequent duration should be double the previous
|
||||
assert!(approx_eq(durs[1], durs[0] * 2.0), "second should be double first");
|
||||
assert!(approx_eq(durs[2], durs[1] * 2.0), "third should be double second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn necho_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 necho each"#, "necho count must be > 0");
|
||||
}
|
||||
@@ -122,13 +122,3 @@ fn multi_op_quotation_second() {
|
||||
assert_eq!(stack_int(&f), 13); // runs=1 picks {3 +}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_syntax_with_words() {
|
||||
// | word1 word2 | uses LocalCycleEnd which should auto-apply quotations
|
||||
// LocalCycleEnd uses time_ctx.iteration_index, which defaults to 0 outside for loops
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(": add3 3 + ; : add5 5 + ; 10 | add3 add5 |", &ctx).unwrap();
|
||||
// iteration_index defaults to 0, picks first word (add3)
|
||||
assert_eq!(stack_int(&f), 13);
|
||||
}
|
||||
|
||||
@@ -17,17 +17,6 @@ fn rand_deterministic() {
|
||||
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);
|
||||
|
||||
@@ -44,36 +44,6 @@ fn multiple_emits() {
|
||||
assert!(outputs[1].contains("sound/snare"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subdivide_each() {
|
||||
let _outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_pop() {
|
||||
let outputs = expect_outputs(
|
||||
r#"0.0 0.5 zoom "kick" s emit pop 0.5 1.0 zoom "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(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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<String, f64> {
|
||||
@@ -21,6 +23,27 @@ fn get_deltas(outputs: &[String]) -> Vec<f64> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_durs(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_sounds(outputs: &[String]) -> Vec<String> {
|
||||
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 {
|
||||
@@ -28,7 +51,7 @@ fn approx_eq(a: f64, b: f64) -> bool {
|
||||
}
|
||||
|
||||
// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s
|
||||
// Default duration = 4 * stepdur = 0.5s
|
||||
// Root duration = 4 * stepdur = 0.5s
|
||||
|
||||
#[test]
|
||||
fn stepdur_baseline() {
|
||||
@@ -37,80 +60,39 @@ fn stepdur_baseline() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_no_delta() {
|
||||
let outputs = expect_outputs(r#""kick" s emit"#, 1);
|
||||
fn single_emit() {
|
||||
let outputs = expect_outputs(r#""kick" s @"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.0),
|
||||
"emit at start should have delta 0"
|
||||
);
|
||||
assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_half() {
|
||||
// at 0.5 in root (0..0.5) => delta = 0.5 * 0.5 = 0.25
|
||||
let outputs = expect_outputs(r#""kick" s 0.5 at emit pop"#, 1);
|
||||
fn implicit_subdivision_2() {
|
||||
let outputs = expect_outputs(r#""kick" s @ @"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.25),
|
||||
"at 0.5 should be delta 0.25, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
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 at_quarter() {
|
||||
// at 0.25 in root (0..0.5) => delta = 0.25 * 0.5 = 0.125
|
||||
let outputs = expect_outputs(r#""kick" s 0.25 at emit pop"#, 1);
|
||||
fn implicit_subdivision_4() {
|
||||
let outputs = expect_outputs(r#""kick" s @ @ @ @"#, 4);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.125),
|
||||
"at 0.25 should be delta 0.125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_zero() {
|
||||
let outputs = expect_outputs(r#""kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "at 0.0 should be delta 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_2_each() {
|
||||
// 2 subdivisions: deltas at 0 and 0.25 (half of 0.5)
|
||||
let outputs = expect_outputs(r#""kick" s 2 div each"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "first subdivision at 0");
|
||||
assert!(
|
||||
approx_eq(deltas[1], 0.25),
|
||||
"second subdivision at 0.25, got {}",
|
||||
deltas[1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_4_each() {
|
||||
// 4 subdivisions: 0, 0.125, 0.25, 0.375
|
||||
let outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
|
||||
let deltas = get_deltas(&outputs);
|
||||
let expected = [0.0, 0.125, 0.25, 0.375];
|
||||
for (i, (got, exp)) in deltas.iter().zip(expected.iter()).enumerate() {
|
||||
let step = 0.5 / 4.0;
|
||||
for (i, delta) in deltas.iter().enumerate() {
|
||||
let expected = step * i as f64;
|
||||
assert!(
|
||||
approx_eq(*got, *exp),
|
||||
"subdivision {}: expected {}, got {}",
|
||||
i,
|
||||
exp,
|
||||
got
|
||||
approx_eq(*delta, expected),
|
||||
"slot {}: expected {}, got {}",
|
||||
i, expected, delta
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_3_each() {
|
||||
// 3 subdivisions: 0, 0.5/3, 2*0.5/3
|
||||
let outputs = expect_outputs(r#""kick" s 3 div each"#, 3);
|
||||
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));
|
||||
@@ -119,113 +101,433 @@ fn div_3_each() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_full() {
|
||||
// zoom 0.0 1.0 is the full duration, same as root
|
||||
let outputs = expect_outputs(r#"0.0 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.25), "full zoom at 0.5 = 0.25");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_first_half() {
|
||||
// zoom 0.0 0.5 restricts to first half (0..0.25)
|
||||
// at 0.5 within that = 0.125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
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[0], 0.125),
|
||||
"first-half zoom at 0.5 = 0.125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_second_half() {
|
||||
// zoom 0.5 1.0 restricts to second half (0.25..0.5)
|
||||
// at 0.0 within that = start of second half = 0.25
|
||||
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.25),
|
||||
"second-half zoom at 0.0 = 0.25, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_second_half_middle() {
|
||||
// zoom 0.5 1.0, at 0.5 within that = 0.75 of full duration = 0.375
|
||||
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.375), "got {}", deltas[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_zooms() {
|
||||
// zoom 0.0 0.5, then zoom 0.5 1.0 within that
|
||||
// outer: 0..0.25, inner: 0.5..1.0 of that = 0.125..0.25
|
||||
// at 0.0 in inner = 0.125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom 0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.125),
|
||||
"nested zoom at 0.0 = 0.125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_pop_sequence() {
|
||||
// First in zoom 0.0 0.5 at 0.0 -> delta 0
|
||||
// Pop at context and zoom, then in zoom 0.5 1.0 at 0.0 -> delta 0.25
|
||||
let outputs = expect_outputs(
|
||||
r#"0.0 0.5 zoom "kick" s 0.0 at emit pop pop 0.5 1.0 zoom "snare" s 0.0 at emit pop"#,
|
||||
2,
|
||||
);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "first zoom start");
|
||||
assert!(
|
||||
approx_eq(deltas[1], 0.25),
|
||||
"second zoom start, got {}",
|
||||
approx_eq(deltas[1], 2.0 * step),
|
||||
"third slot (after silence) at {}, got {}",
|
||||
2.0 * step,
|
||||
deltas[1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_in_zoom() {
|
||||
// zoom 0.0 0.5 (duration 0.25), then div 2 each
|
||||
// subdivisions at 0 and 0.125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 2 div each"#, 2);
|
||||
fn silence_at_start() {
|
||||
let outputs = expect_outputs(r#""kick" s ~ @"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.125), "got {}", deltas[1]);
|
||||
let step = 0.5 / 2.0;
|
||||
assert!(
|
||||
approx_eq(deltas[0], step),
|
||||
"emit after silence at {}, got {}",
|
||||
step,
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tempo_affects_stepdur() {
|
||||
// At 60 BPM: stepdur = 60/60/4/1 = 0.25
|
||||
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();
|
||||
f.evaluate("stepdur", &ctx).unwrap();
|
||||
assert!(approx_eq(stack_float(&f), 0.25));
|
||||
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_stepdur() {
|
||||
// At 120 BPM, speed 2.0: stepdur = 60/120/4/2 = 0.0625
|
||||
fn speed_affects_subdivision() {
|
||||
let ctx = ctx_with(|c| c.speed = 2.0);
|
||||
let f = forth();
|
||||
f.evaluate("stepdur", &ctx).unwrap();
|
||||
assert!(approx_eq(stack_float(&f), 0.0625));
|
||||
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 div_each_at_different_tempo() {
|
||||
// At 60 BPM: stepdur = 0.25, default dur = 1.0, so div 2 each => 0, 0.5
|
||||
let ctx = ctx_with(|c| c.tempo = 60.0);
|
||||
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<f64> = 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 outputs = f.evaluate(r#""kick" s 2 div each"#, &ctx).unwrap();
|
||||
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));
|
||||
assert!(approx_eq(deltas[1], 0.5), "got {}", deltas[1]);
|
||||
}
|
||||
|
||||
#[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<i64> = 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<f64> = 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"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user