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

42841
echo Normal file

File diff suppressed because it is too large Load Diff

42842
ok Normal file

File diff suppressed because it is too large Load Diff

42842
okok Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,10 +18,7 @@ use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use crossterm::event::{
self, Event, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
PushKeyboardEnhancementFlags,
};
use crossterm::event::{self, Event};
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
@@ -134,11 +131,6 @@ fn main() -> io::Result<()> {
enable_raw_mode()?;
io::stdout().execute(EnterAlternateScreen)?;
let _ = io::stdout().execute(PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::REPORT_EVENT_TYPES
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
));
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
@@ -217,7 +209,6 @@ fn main() -> io::Result<()> {
}
let _ = io::stdout().execute(PopKeyboardEnhancementFlags);
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;

View File

@@ -195,6 +195,7 @@ pub enum Op {
ListEndPCycle,
At,
Window,
Scale,
Pop,
Subdivide,
SetTempo,
@@ -205,6 +206,11 @@ pub enum Op {
Unless,
Adsr,
Ad,
Stack,
For,
LocalCycleEnd,
Echo,
Necho,
}
pub enum WordCompile {
@@ -448,19 +454,26 @@ pub const WORDS: &[Word] = &[
example: "\"kick\" s emit",
compile: Simple,
},
// Variables
Word {
name: "get",
stack: "(name -- val)",
desc: "Get variable value",
example: "\"x\" get",
name: "@",
stack: "(--)",
desc: "Alias for emit",
example: "\"kick\" s 0.5 at @ pop",
compile: Alias("emit"),
},
// Variables (prefix syntax: @name to fetch, !name to store)
Word {
name: "@<var>",
stack: "( -- val)",
desc: "Fetch variable value",
example: "@freq => 440",
compile: Simple,
},
Word {
name: "set",
stack: "(val name --)",
desc: "Set variable value",
example: "42 \"x\" set",
name: "!<var>",
stack: "(val --)",
desc: "Store value in variable",
example: "440 !freq",
compile: Simple,
},
// Randomness
@@ -682,22 +695,22 @@ pub const WORDS: &[Word] = &[
Word {
name: "at",
stack: "(pos --)",
desc: "Emit at position in window",
example: "0.5 at",
desc: "Position in time (push context)",
example: "\"kick\" s 0.5 at emit pop",
compile: Simple,
},
Word {
name: "@",
stack: "(pos --)",
desc: "Alias for at",
example: "\"kick\" s 0.5 @",
compile: Alias("at"),
name: "zoom",
stack: "(start end --)",
desc: "Zoom into time region",
example: "0.0 0.5 zoom",
compile: Simple,
},
Word {
name: "window",
stack: "(start end --)",
desc: "Create time window",
example: "0.0 0.5 window",
name: "scale!",
stack: "(factor --)",
desc: "Scale time context duration",
example: "2 scale!",
compile: Simple,
},
Word {
@@ -721,6 +734,41 @@ pub const WORDS: &[Word] = &[
example: "4 div each",
compile: Simple,
},
Word {
name: "stack",
stack: "(n --)",
desc: "Create n subdivisions at same time",
example: "3 stack",
compile: Simple,
},
Word {
name: "echo",
stack: "(n --)",
desc: "Create n subdivisions with halving durations (stutter)",
example: "3 echo",
compile: Simple,
},
Word {
name: "necho",
stack: "(n --)",
desc: "Create n subdivisions with doubling durations (swell)",
example: "3 necho",
compile: Simple,
},
Word {
name: "for",
stack: "(quot --)",
desc: "Execute quotation for each subdivision",
example: "{ emit } 3 div for",
compile: Simple,
},
Word {
name: "|",
stack: "(-- marker)",
desc: "Start local cycle list",
example: "| 60 62 64 |",
compile: Simple,
},
Word {
name: "tempo!",
stack: "(bpm --)",
@@ -1577,8 +1625,6 @@ fn simple_op(name: &str) -> Option<Op> {
"not" => Op::Not,
"sound" => Op::NewCmd,
"emit" => Op::Emit,
"get" => Op::Get,
"set" => Op::Set,
"rand" => Op::Rand,
"rrand" => Op::Rrand,
"seed" => Op::Seed,
@@ -1594,7 +1640,8 @@ fn simple_op(name: &str) -> Option<Op> {
"?" => Op::When,
"!?" => Op::Unless,
"at" => Op::At,
"window" => Op::Window,
"zoom" => Op::Window,
"scale!" => Op::Scale,
"pop" => Op::Pop,
"div" => Op::Subdivide,
"each" => Op::Each,
@@ -1605,6 +1652,10 @@ fn simple_op(name: &str) -> Option<Op> {
">>" => Op::ListEndPCycle,
"adsr" => Op::Adsr,
"ad" => Op::Ad,
"stack" => Op::Stack,
"for" => Op::For,
"echo" => Op::Echo,
"necho" => Op::Necho,
_ => return None,
})
}
@@ -1629,6 +1680,31 @@ fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
return true;
}
}
// @varname - fetch variable
if let Some(var_name) = name.strip_prefix('@') {
if !var_name.is_empty() {
ops.push(Op::PushStr(var_name.to_string(), None));
ops.push(Op::Get);
return true;
}
}
// !varname - store into variable
if let Some(var_name) = name.strip_prefix('!') {
if !var_name.is_empty() {
ops.push(Op::PushStr(var_name.to_string(), None));
ops.push(Op::Set);
return true;
}
}
// Internal ops not exposed in WORDS
if let Some(op) = simple_op(name) {
ops.push(op);
return true;
}
false
}
@@ -1637,6 +1713,7 @@ struct TimeContext {
start: f64,
duration: f64,
subdivisions: Option<Vec<(f64, f64)>>,
iteration_index: Option<usize>,
}
#[derive(Clone, Debug)]
@@ -1734,6 +1811,7 @@ fn tokenize(input: &str) -> Vec<Token> {
fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
let mut ops = Vec::new();
let mut i = 0;
let mut pipe_parity = false;
while i < tokens.len() {
match &tokens[i] {
@@ -1750,7 +1828,14 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
}
Token::Word(w, _) => {
let word = w.as_str();
if word == "if" {
if word == "|" {
if pipe_parity {
ops.push(Op::LocalCycleEnd);
} else {
ops.push(Op::ListStart);
}
pipe_parity = !pipe_parity;
} else if word == "if" {
let (then_ops, else_ops, consumed) = compile_if(&tokens[i + 1..])?;
i += consumed;
if else_ops.is_empty() {
@@ -1897,6 +1982,7 @@ impl Forth {
start: 0.0,
duration: ctx.step_duration(),
subdivisions: None,
iteration_index: None,
}];
let mut cmd = CmdRegister::default();
@@ -1910,16 +1996,6 @@ impl Forth {
trace,
)?;
if outputs.is_empty() {
if let Some((sound, params)) = cmd.take() {
let mut pairs = vec![("sound".into(), sound)];
pairs.extend(params);
pairs.push(("dur".into(), ctx.step_duration().to_string()));
pairs.push(("delaytime".into(), ctx.step_duration().to_string()));
outputs.push(format_cmd(&pairs));
}
}
Ok(outputs)
}
@@ -2078,14 +2154,13 @@ impl Forth {
pairs.push(("delta".into(), time_ctx.start.to_string()));
}
if !pairs.iter().any(|(k, _)| k == "dur") {
pairs.push(("dur".into(), ctx.step_duration().to_string()));
pairs.push(("dur".into(), time_ctx.duration.to_string()));
}
let stepdur = ctx.step_duration();
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
pairs[idx].1 = (ratio * stepdur).to_string();
pairs[idx].1 = (ratio * time_ctx.duration).to_string();
} else {
pairs.push(("delaytime".into(), stepdur.to_string()));
pairs.push(("delaytime".into(), time_ctx.duration.to_string()));
}
outputs.push(format_cmd(&pairs));
}
@@ -2206,7 +2281,9 @@ impl Forth {
if val < prob {
match quot {
Value::Quotation(quot_ops) => {
self.execute_ops(&quot_ops, ctx, stack, outputs, time_stack, cmd, None)?;
let mut trace_opt = trace_cell.borrow_mut().take();
self.execute_ops(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
@@ -2220,7 +2297,9 @@ impl Forth {
if val < pct / 100.0 {
match quot {
Value::Quotation(quot_ops) => {
self.execute_ops(&quot_ops, ctx, stack, outputs, time_stack, cmd, None)?;
let mut trace_opt = trace_cell.borrow_mut().take();
self.execute_ops(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
@@ -2251,7 +2330,9 @@ impl Forth {
if cond.is_truthy() {
match quot {
Value::Quotation(quot_ops) => {
self.execute_ops(&quot_ops, ctx, stack, outputs, time_stack, cmd, None)?;
let mut trace_opt = trace_cell.borrow_mut().take();
self.execute_ops(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
@@ -2264,7 +2345,9 @@ impl Forth {
if !cond.is_truthy() {
match quot {
Value::Quotation(quot_ops) => {
self.execute_ops(&quot_ops, ctx, stack, outputs, time_stack, cmd, None)?;
let mut trace_opt = trace_cell.borrow_mut().take();
self.execute_ops(&quot_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
*trace_cell.borrow_mut() = trace_opt;
}
_ => return Err("expected quotation".into()),
}
@@ -2285,25 +2368,14 @@ impl Forth {
Op::At => {
let pos = stack.pop().ok_or("stack underflow")?.as_float()?;
let (sound, mut params) = cmd.take().ok_or("no sound set")?;
let mut pairs = vec![("sound".into(), sound)];
pairs.append(&mut params);
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
let absolute_time = time_ctx.start + time_ctx.duration * pos;
if absolute_time > 0.0 {
pairs.push(("delta".into(), absolute_time.to_string()));
}
if !pairs.iter().any(|(k, _)| k == "dur") {
pairs.push(("dur".into(), ctx.step_duration().to_string()));
}
let stepdur = ctx.step_duration();
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
pairs[idx].1 = (ratio * stepdur).to_string();
} else {
pairs.push(("delaytime".into(), stepdur.to_string()));
}
outputs.push(format_cmd(&pairs));
let parent = time_stack.last().ok_or("time stack underflow")?;
let new_start = parent.start + parent.duration * pos;
time_stack.push(TimeContext {
start: new_start,
duration: parent.duration * (1.0 - pos),
subdivisions: None,
iteration_index: parent.iteration_index,
});
}
Op::Window => {
@@ -2316,6 +2388,18 @@ impl Forth {
start: new_start,
duration: new_duration,
subdivisions: None,
iteration_index: parent.iteration_index,
});
}
Op::Scale => {
let factor = stack.pop().ok_or("stack underflow")?.as_float()?;
let parent = time_stack.last().ok_or("time stack underflow")?;
time_stack.push(TimeContext {
start: parent.start,
duration: parent.duration * factor,
subdivisions: None,
iteration_index: parent.iteration_index,
});
}
@@ -2347,21 +2431,20 @@ impl Forth {
.subdivisions
.as_ref()
.ok_or("each requires subdivide first")?;
for (sub_start, _sub_dur) in subs {
for (sub_start, sub_dur) in subs {
let mut pairs = vec![("sound".into(), sound.clone())];
pairs.extend(params.iter().cloned());
if *sub_start > 0.0 {
pairs.push(("delta".into(), sub_start.to_string()));
}
if !pairs.iter().any(|(k, _)| k == "dur") {
pairs.push(("dur".into(), ctx.step_duration().to_string()));
pairs.push(("dur".into(), sub_dur.to_string()));
}
let stepdur = ctx.step_duration();
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
pairs[idx].1 = (ratio * stepdur).to_string();
pairs[idx].1 = (ratio * sub_dur).to_string();
} else {
pairs.push(("delaytime".into(), stepdur.to_string()));
pairs.push(("delaytime".into(), sub_dur.to_string()));
}
outputs.push(format_cmd(&pairs));
}
@@ -2459,6 +2542,121 @@ impl Forth {
cmd.set_param("decay".into(), d.to_param_string());
cmd.set_param("sustain".into(), "0".into());
}
Op::Stack => {
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if n == 0 {
return Err("stack count must be > 0".into());
}
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
let sub_duration = time_ctx.duration / n as f64;
let mut subs = Vec::with_capacity(n);
for _ in 0..n {
subs.push((time_ctx.start, sub_duration));
}
time_ctx.subdivisions = Some(subs);
}
Op::Echo => {
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if n == 0 {
return Err("echo count must be > 0".into());
}
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
// Geometric series: d1 * (2 - 2^(1-n)) = total
let d1 = time_ctx.duration / (2.0 - 2.0_f64.powi(1 - n as i32));
let mut subs = Vec::with_capacity(n);
for i in 0..n {
let dur = d1 / 2.0_f64.powi(i as i32);
let start = if i == 0 {
time_ctx.start
} else {
time_ctx.start + d1 * (2.0 - 2.0_f64.powi(1 - i as i32))
};
subs.push((start, dur));
}
time_ctx.subdivisions = Some(subs);
}
Op::Necho => {
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
if n == 0 {
return Err("necho count must be > 0".into());
}
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
// Reverse geometric: d1 + 2*d1 + 4*d1 + ... = d1 * (2^n - 1) = total
let d1 = time_ctx.duration / (2.0_f64.powi(n as i32) - 1.0);
let mut subs = Vec::with_capacity(n);
for i in 0..n {
let dur = d1 * 2.0_f64.powi(i as i32);
let start = if i == 0 {
time_ctx.start
} else {
// Sum of previous durations: d1 * (2^i - 1)
time_ctx.start + d1 * (2.0_f64.powi(i as i32) - 1.0)
};
subs.push((start, dur));
}
time_ctx.subdivisions = Some(subs);
}
Op::For => {
let quot = stack.pop().ok_or("stack underflow")?;
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
let subs = time_ctx
.subdivisions
.clone()
.ok_or("for requires subdivide first")?;
match quot {
Value::Quotation(quot_ops) => {
for (i, (sub_start, sub_dur)) in subs.iter().enumerate() {
time_stack.push(TimeContext {
start: *sub_start,
duration: *sub_dur,
subdivisions: None,
iteration_index: Some(i),
});
let mut trace_opt = trace_cell.borrow_mut().take();
self.execute_ops(
&quot_ops,
ctx,
stack,
outputs,
time_stack,
cmd,
trace_opt.as_deref_mut(),
)?;
*trace_cell.borrow_mut() = trace_opt;
time_stack.pop();
}
}
_ => return Err("expected quotation".into()),
}
}
Op::LocalCycleEnd => {
let mut values = Vec::new();
while let Some(v) = stack.pop() {
if v.is_marker() {
break;
}
values.push(v);
}
if values.is_empty() {
return Err("empty local cycle list".into());
}
values.reverse();
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
let idx = time_ctx.iteration_index.unwrap_or(0) % values.len();
let selected = values[idx].clone();
if let Some(span) = selected.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
stack.push(selected);
}
}
pc += 1;
}

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);
}