534 lines
17 KiB
Rust
534 lines
17 KiB
Rust
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> {
|
|
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_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 {
|
|
(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<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 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 . ~"#, 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_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, vec!["kick", "hat"]);
|
|
assert!(approx_eq(deltas[0], 0.0));
|
|
assert!(approx_eq(deltas[1], 0.25), "second div at slot 1, got {}", deltas[1]);
|
|
}
|
|
|
|
#[test]
|
|
fn div_with_root_emit() {
|
|
// 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);
|
|
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 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);
|
|
// Output order: kick (slot 0), then hats (slot 1 subdivided)
|
|
assert_eq!(sounds[0], "kick");
|
|
assert_eq!(sounds[1], "hat");
|
|
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 . _ ~"#, 1);
|
|
let deltas = get_deltas(&outputs);
|
|
assert!(approx_eq(deltas[0], 0.0));
|
|
}
|
|
|
|
#[test]
|
|
fn unmatched_scope_terminator_error() {
|
|
let f = forth();
|
|
let result = f.evaluate(r#""kick" s . ~"#, &default_ctx());
|
|
assert!(result.is_err(), "unmatched ~ 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 . ~"#, 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 . . . . ~"#, 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 . ~ "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)
|
|
// 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_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);
|
|
// 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), "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]
|
|
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"]);
|
|
}
|