All about temporal semantics

This commit is contained in:
2026-01-22 00:56:02 +01:00
parent 5e47e909d1
commit c73e25e207
12 changed files with 129102 additions and 145 deletions

View File

@@ -33,3 +33,6 @@ mod variables;
#[path = "forth/quotations.rs"]
mod quotations;
#[path = "forth/iteration.rs"]
mod iteration;

View File

@@ -20,16 +20,6 @@ fn string_not_number() {
expect_error(r#""hello" neg"#, "expected number");
}
#[test]
fn get_expects_string() {
expect_error("42 get", "expected string");
}
#[test]
fn set_expects_string() {
expect_error("1 2 set", "expected string");
}
#[test]
fn comment_ignored() {
expect_int("1 (this is a comment) 2 +", 3);
@@ -54,7 +44,7 @@ fn float_literal() {
#[test]
fn string_with_spaces() {
let f = run(r#""hello world" "x" set "x" get"#);
let f = run(r#""hello world" !x @x"#);
match stack_top(&f) {
cagire::model::forth::Value::Str(s, _) => assert_eq!(s, "hello world"),
other => panic!("expected string, got {:?}", other),
@@ -63,7 +53,6 @@ fn string_with_spaces() {
#[test]
fn list_count() {
// [ 1 2 3 ] => stack: 1 2 3 3 (items + count on top)
let f = run("[ 1 2 3 ]");
assert_eq!(stack_int(&f), 3);
}
@@ -75,9 +64,6 @@ fn list_empty() {
#[test]
fn list_preserves_values() {
// [ 10 20 ] => stack: 10 20 2
// drop => 10 20
// + => 30
expect_int("[ 10 20 ] drop +", 30);
}
@@ -97,10 +83,10 @@ fn conditional_based_on_step() {
fn accumulator() {
let f = forth();
let ctx = default_ctx();
f.evaluate(r#"0 "acc" set"#, &ctx).unwrap();
f.evaluate(r#"0 !acc"#, &ctx).unwrap();
for _ in 0..5 {
f.clear_stack();
f.evaluate(r#""acc" get 1 + dup "acc" set"#, &ctx).unwrap();
f.evaluate(r#"@acc 1 + dup !acc"#, &ctx).unwrap();
}
assert_eq!(stack_int(&f), 5);
}

256
tests/forth/iteration.rs Normal file
View File

@@ -0,0 +1,256 @@
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() {
// stepdur = 0.125, echo 3
// d1 + d1/2 + d1/4 = d1 * 1.75 = 0.125
// d1 = 0.125 / 1.75 = 0.0714285714...
let outputs = expect_outputs(r#""kick" s 3 echo each"#, 3);
let durs = get_durs(&outputs);
let deltas = get_deltas(&outputs);
let d1 = 0.125 / 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() {
// stepdur = 0.125, necho 3
// d1 + 2*d1 + 4*d1 = d1 * 7 = 0.125
// d1 = 0.125 / 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.125 / 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");
}

View File

@@ -83,8 +83,8 @@ fn quotation_skips_emit() {
let outputs = f
.evaluate(r#""kick" s { emit } 0 ?"#, &default_ctx())
.unwrap();
// Should have 1 output from implicit emit at end (since cmd is set but not emitted)
assert_eq!(outputs.len(), 1);
// No output since emit was skipped and no implicit emit
assert_eq!(outputs.len(), 0);
}
#[test]

View File

@@ -37,13 +37,6 @@ fn emit_no_sound() {
expect_error("emit", "no sound set");
}
#[test]
fn implicit_emit() {
let outputs = expect_outputs(r#""kick" s 440 freq"#, 1);
assert!(outputs[0].contains("sound/kick"));
assert!(outputs[0].contains("freq/440"));
}
#[test]
fn multiple_emits() {
let outputs = expect_outputs(r#""kick" s emit "snare" s emit"#, 2);
@@ -57,9 +50,9 @@ fn subdivide_each() {
}
#[test]
fn window_pop() {
fn zoom_pop() {
let outputs = expect_outputs(
r#"0.0 0.5 window "kick" s emit pop 0.5 1.0 window "snare" s emit"#,
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"));

View File

@@ -47,8 +47,8 @@ fn emit_no_delta() {
#[test]
fn at_half() {
// at 0.5 in root window (0..0.125) => delta = 0.5 * 0.125 = 0.0625
let outputs = expect_outputs(r#""kick" s 0.5 at"#, 1);
// at 0.5 in root zoom (0..0.125) => delta = 0.5 * 0.125 = 0.0625
let outputs = expect_outputs(r#""kick" s 0.5 at emit pop"#, 1);
let deltas = get_deltas(&outputs);
assert!(
approx_eq(deltas[0], 0.0625),
@@ -59,7 +59,7 @@ fn at_half() {
#[test]
fn at_quarter() {
let outputs = expect_outputs(r#""kick" s 0.25 at"#, 1);
let outputs = expect_outputs(r#""kick" s 0.25 at emit pop"#, 1);
let deltas = get_deltas(&outputs);
assert!(
approx_eq(deltas[0], 0.03125),
@@ -70,7 +70,7 @@ fn at_quarter() {
#[test]
fn at_zero() {
let outputs = expect_outputs(r#""kick" s 0.0 at"#, 1);
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");
}
@@ -117,83 +117,83 @@ fn div_3_each() {
}
#[test]
fn window_full() {
// window 0.0 1.0 is the full step, same as root
let outputs = expect_outputs(r#"0.0 1.0 window "kick" s 0.5 at"#, 1);
fn zoom_full() {
// zoom 0.0 1.0 is the full step, 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.0625), "full window at 0.5 = 0.0625");
assert!(approx_eq(deltas[0], 0.0625), "full zoom at 0.5 = 0.0625");
}
#[test]
fn window_first_half() {
// window 0.0 0.5 restricts to first half (0..0.0625)
fn zoom_first_half() {
// zoom 0.0 0.5 restricts to first half (0..0.0625)
// at 0.5 within that = 0.25 of full step = 0.03125
let outputs = expect_outputs(r#"0.0 0.5 window "kick" s 0.5 at"#, 1);
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 0.5 at emit pop"#, 1);
let deltas = get_deltas(&outputs);
assert!(
approx_eq(deltas[0], 0.03125),
"first-half window at 0.5 = 0.03125, got {}",
"first-half zoom at 0.5 = 0.03125, got {}",
deltas[0]
);
}
#[test]
fn window_second_half() {
// window 0.5 1.0 restricts to second half (0.0625..0.125)
fn zoom_second_half() {
// zoom 0.5 1.0 restricts to second half (0.0625..0.125)
// at 0.0 within that = start of second half = 0.0625
let outputs = expect_outputs(r#"0.5 1.0 window "kick" s 0.0 at"#, 1);
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.0625),
"second-half window at 0.0 = 0.0625, got {}",
"second-half zoom at 0.0 = 0.0625, got {}",
deltas[0]
);
}
#[test]
fn window_second_half_middle() {
// window 0.5 1.0, at 0.5 within that = 0.75 of full step = 0.09375
let outputs = expect_outputs(r#"0.5 1.0 window "kick" s 0.5 at"#, 1);
fn zoom_second_half_middle() {
// zoom 0.5 1.0, at 0.5 within that = 0.75 of full step = 0.09375
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.09375), "got {}", deltas[0]);
}
#[test]
fn nested_windows() {
// window 0.0 0.5, then window 0.5 1.0 within that
fn nested_zooms() {
// zoom 0.0 0.5, then zoom 0.5 1.0 within that
// outer: 0..0.0625, inner: 0.5..1.0 of that = 0.03125..0.0625
// at 0.0 in inner = 0.03125
let outputs = expect_outputs(r#"0.0 0.5 window 0.5 1.0 window "kick" s 0.0 at"#, 1);
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.03125),
"nested window at 0.0 = 0.03125, got {}",
"nested zoom at 0.0 = 0.03125, got {}",
deltas[0]
);
}
#[test]
fn window_pop_sequence() {
// First in window 0.0 0.5 at 0.0 -> delta 0
// Pop, then in window 0.5 1.0 at 0.0 -> delta 0.0625
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.0625
let outputs = expect_outputs(
r#"0.0 0.5 window "kick" s 0.0 at pop 0.5 1.0 window "snare" s 0.0 at"#,
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 window start");
assert!(approx_eq(deltas[0], 0.0), "first zoom start");
assert!(
approx_eq(deltas[1], 0.0625),
"second window start, got {}",
"second zoom start, got {}",
deltas[1]
);
}
#[test]
fn div_in_window() {
// window 0.0 0.5 (duration 0.0625), then div 2 each
fn div_in_zoom() {
// zoom 0.0 0.5 (duration 0.0625), then div 2 each
// subdivisions at 0 and 0.03125
let outputs = expect_outputs(r#"0.0 0.5 window "kick" s 2 div each"#, 2);
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 2 div each"#, 2);
let deltas = get_deltas(&outputs);
assert!(approx_eq(deltas[0], 0.0));
assert!(approx_eq(deltas[1], 0.03125), "got {}", deltas[1]);

View File

@@ -1,39 +1,44 @@
use super::harness::*;
#[test]
fn set_get() {
expect_int(r#"42 "x" set "x" get"#, 42);
fn fetch_store() {
expect_int(r#"42 !x @x"#, 42);
}
#[test]
fn get_nonexistent() {
expect_int(r#""novar" get"#, 0);
fn fetch_nonexistent() {
expect_int(r#"@novar"#, 0);
}
#[test]
fn persistence_across_evals() {
let f = forth();
let ctx = default_ctx();
f.evaluate(r#"10 "counter" set"#, &ctx).unwrap();
f.evaluate(r#"10 !counter"#, &ctx).unwrap();
f.clear_stack();
f.evaluate(r#""counter" get 1 +"#, &ctx).unwrap();
f.evaluate(r#"@counter 1 +"#, &ctx).unwrap();
assert_eq!(stack_int(&f), 11);
}
#[test]
fn overwrite() {
expect_int(r#"1 "x" set 99 "x" set "x" get"#, 99);
expect_int(r#"1 !x 99 !x @x"#, 99);
}
#[test]
fn multiple_vars() {
let f = run(r#"10 "a" set 20 "b" set "a" get "b" get +"#);
let f = run(r#"10 !a 20 !b @a @b +"#);
assert_eq!(stack_int(&f), 30);
}
#[test]
fn float_var() {
let f = run(r#"3.14 "pi" set "pi" get"#);
let f = run(r#"3.14 !pi @pi"#);
let val = stack_float(&f);
assert!((val - 3.14).abs() < 1e-9);
}
#[test]
fn increment_pattern() {
expect_int(r#"0 !n @n 1 + !n @n 1 + !n @n"#, 2);
}