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

View File

@@ -195,6 +195,7 @@ pub enum Op {
ListEndPCycle, ListEndPCycle,
At, At,
Window, Window,
Scale,
Pop, Pop,
Subdivide, Subdivide,
SetTempo, SetTempo,
@@ -205,6 +206,11 @@ pub enum Op {
Unless, Unless,
Adsr, Adsr,
Ad, Ad,
Stack,
For,
LocalCycleEnd,
Echo,
Necho,
} }
pub enum WordCompile { pub enum WordCompile {
@@ -448,19 +454,26 @@ pub const WORDS: &[Word] = &[
example: "\"kick\" s emit", example: "\"kick\" s emit",
compile: Simple, compile: Simple,
}, },
// Variables
Word { Word {
name: "get", name: "@",
stack: "(name -- val)", stack: "(--)",
desc: "Get variable value", desc: "Alias for emit",
example: "\"x\" get", 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, compile: Simple,
}, },
Word { Word {
name: "set", name: "!<var>",
stack: "(val name --)", stack: "(val --)",
desc: "Set variable value", desc: "Store value in variable",
example: "42 \"x\" set", example: "440 !freq",
compile: Simple, compile: Simple,
}, },
// Randomness // Randomness
@@ -682,22 +695,22 @@ pub const WORDS: &[Word] = &[
Word { Word {
name: "at", name: "at",
stack: "(pos --)", stack: "(pos --)",
desc: "Emit at position in window", desc: "Position in time (push context)",
example: "0.5 at", example: "\"kick\" s 0.5 at emit pop",
compile: Simple, compile: Simple,
}, },
Word { Word {
name: "@", name: "zoom",
stack: "(pos --)", stack: "(start end --)",
desc: "Alias for at", desc: "Zoom into time region",
example: "\"kick\" s 0.5 @", example: "0.0 0.5 zoom",
compile: Alias("at"), compile: Simple,
}, },
Word { Word {
name: "window", name: "scale!",
stack: "(start end --)", stack: "(factor --)",
desc: "Create time window", desc: "Scale time context duration",
example: "0.0 0.5 window", example: "2 scale!",
compile: Simple, compile: Simple,
}, },
Word { Word {
@@ -721,6 +734,41 @@ pub const WORDS: &[Word] = &[
example: "4 div each", example: "4 div each",
compile: Simple, 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 { Word {
name: "tempo!", name: "tempo!",
stack: "(bpm --)", stack: "(bpm --)",
@@ -1577,8 +1625,6 @@ fn simple_op(name: &str) -> Option<Op> {
"not" => Op::Not, "not" => Op::Not,
"sound" => Op::NewCmd, "sound" => Op::NewCmd,
"emit" => Op::Emit, "emit" => Op::Emit,
"get" => Op::Get,
"set" => Op::Set,
"rand" => Op::Rand, "rand" => Op::Rand,
"rrand" => Op::Rrand, "rrand" => Op::Rrand,
"seed" => Op::Seed, "seed" => Op::Seed,
@@ -1594,7 +1640,8 @@ fn simple_op(name: &str) -> Option<Op> {
"?" => Op::When, "?" => Op::When,
"!?" => Op::Unless, "!?" => Op::Unless,
"at" => Op::At, "at" => Op::At,
"window" => Op::Window, "zoom" => Op::Window,
"scale!" => Op::Scale,
"pop" => Op::Pop, "pop" => Op::Pop,
"div" => Op::Subdivide, "div" => Op::Subdivide,
"each" => Op::Each, "each" => Op::Each,
@@ -1605,6 +1652,10 @@ fn simple_op(name: &str) -> Option<Op> {
">>" => Op::ListEndPCycle, ">>" => Op::ListEndPCycle,
"adsr" => Op::Adsr, "adsr" => Op::Adsr,
"ad" => Op::Ad, "ad" => Op::Ad,
"stack" => Op::Stack,
"for" => Op::For,
"echo" => Op::Echo,
"necho" => Op::Necho,
_ => return None, _ => return None,
}) })
} }
@@ -1629,6 +1680,31 @@ fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
return true; 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 false
} }
@@ -1637,6 +1713,7 @@ struct TimeContext {
start: f64, start: f64,
duration: f64, duration: f64,
subdivisions: Option<Vec<(f64, f64)>>, subdivisions: Option<Vec<(f64, f64)>>,
iteration_index: Option<usize>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -1734,6 +1811,7 @@ fn tokenize(input: &str) -> Vec<Token> {
fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> { fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
let mut ops = Vec::new(); let mut ops = Vec::new();
let mut i = 0; let mut i = 0;
let mut pipe_parity = false;
while i < tokens.len() { while i < tokens.len() {
match &tokens[i] { match &tokens[i] {
@@ -1750,7 +1828,14 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
} }
Token::Word(w, _) => { Token::Word(w, _) => {
let word = w.as_str(); 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..])?; let (then_ops, else_ops, consumed) = compile_if(&tokens[i + 1..])?;
i += consumed; i += consumed;
if else_ops.is_empty() { if else_ops.is_empty() {
@@ -1897,6 +1982,7 @@ impl Forth {
start: 0.0, start: 0.0,
duration: ctx.step_duration(), duration: ctx.step_duration(),
subdivisions: None, subdivisions: None,
iteration_index: None,
}]; }];
let mut cmd = CmdRegister::default(); let mut cmd = CmdRegister::default();
@@ -1910,16 +1996,6 @@ impl Forth {
trace, 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) Ok(outputs)
} }
@@ -2078,14 +2154,13 @@ impl Forth {
pairs.push(("delta".into(), time_ctx.start.to_string())); pairs.push(("delta".into(), time_ctx.start.to_string()));
} }
if !pairs.iter().any(|(k, _)| k == "dur") { 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") { if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); 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 { } else {
pairs.push(("delaytime".into(), stepdur.to_string())); pairs.push(("delaytime".into(), time_ctx.duration.to_string()));
} }
outputs.push(format_cmd(&pairs)); outputs.push(format_cmd(&pairs));
} }
@@ -2206,7 +2281,9 @@ impl Forth {
if val < prob { if val < prob {
match quot { match quot {
Value::Quotation(quot_ops) => { 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()), _ => return Err("expected quotation".into()),
} }
@@ -2220,7 +2297,9 @@ impl Forth {
if val < pct / 100.0 { if val < pct / 100.0 {
match quot { match quot {
Value::Quotation(quot_ops) => { 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()), _ => return Err("expected quotation".into()),
} }
@@ -2251,7 +2330,9 @@ impl Forth {
if cond.is_truthy() { if cond.is_truthy() {
match quot { match quot {
Value::Quotation(quot_ops) => { 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()), _ => return Err("expected quotation".into()),
} }
@@ -2264,7 +2345,9 @@ impl Forth {
if !cond.is_truthy() { if !cond.is_truthy() {
match quot { match quot {
Value::Quotation(quot_ops) => { 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()), _ => return Err("expected quotation".into()),
} }
@@ -2285,25 +2368,14 @@ impl Forth {
Op::At => { Op::At => {
let pos = stack.pop().ok_or("stack underflow")?.as_float()?; let pos = stack.pop().ok_or("stack underflow")?.as_float()?;
let (sound, mut params) = cmd.take().ok_or("no sound set")?; let parent = time_stack.last().ok_or("time stack underflow")?;
let mut pairs = vec![("sound".into(), sound)]; let new_start = parent.start + parent.duration * pos;
pairs.append(&mut params); time_stack.push(TimeContext {
let time_ctx = time_stack.last().ok_or("time stack underflow")?; start: new_start,
let absolute_time = time_ctx.start + time_ctx.duration * pos; duration: parent.duration * (1.0 - pos),
if absolute_time > 0.0 { subdivisions: None,
pairs.push(("delta".into(), absolute_time.to_string())); iteration_index: parent.iteration_index,
} });
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));
} }
Op::Window => { Op::Window => {
@@ -2316,6 +2388,18 @@ impl Forth {
start: new_start, start: new_start,
duration: new_duration, duration: new_duration,
subdivisions: None, 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 .subdivisions
.as_ref() .as_ref()
.ok_or("each requires subdivide first")?; .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())]; let mut pairs = vec![("sound".into(), sound.clone())];
pairs.extend(params.iter().cloned()); pairs.extend(params.iter().cloned());
if *sub_start > 0.0 { if *sub_start > 0.0 {
pairs.push(("delta".into(), sub_start.to_string())); pairs.push(("delta".into(), sub_start.to_string()));
} }
if !pairs.iter().any(|(k, _)| k == "dur") { 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") { if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); 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 { } else {
pairs.push(("delaytime".into(), stepdur.to_string())); pairs.push(("delaytime".into(), sub_dur.to_string()));
} }
outputs.push(format_cmd(&pairs)); outputs.push(format_cmd(&pairs));
} }
@@ -2459,6 +2542,121 @@ impl Forth {
cmd.set_param("decay".into(), d.to_param_string()); cmd.set_param("decay".into(), d.to_param_string());
cmd.set_param("sustain".into(), "0".into()); 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; pc += 1;
} }

View File

@@ -33,3 +33,6 @@ mod variables;
#[path = "forth/quotations.rs"] #[path = "forth/quotations.rs"]
mod quotations; 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"); 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] #[test]
fn comment_ignored() { fn comment_ignored() {
expect_int("1 (this is a comment) 2 +", 3); expect_int("1 (this is a comment) 2 +", 3);
@@ -54,7 +44,7 @@ fn float_literal() {
#[test] #[test]
fn string_with_spaces() { 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) { match stack_top(&f) {
cagire::model::forth::Value::Str(s, _) => assert_eq!(s, "hello world"), cagire::model::forth::Value::Str(s, _) => assert_eq!(s, "hello world"),
other => panic!("expected string, got {:?}", other), other => panic!("expected string, got {:?}", other),
@@ -63,7 +53,6 @@ fn string_with_spaces() {
#[test] #[test]
fn list_count() { fn list_count() {
// [ 1 2 3 ] => stack: 1 2 3 3 (items + count on top)
let f = run("[ 1 2 3 ]"); let f = run("[ 1 2 3 ]");
assert_eq!(stack_int(&f), 3); assert_eq!(stack_int(&f), 3);
} }
@@ -75,9 +64,6 @@ fn list_empty() {
#[test] #[test]
fn list_preserves_values() { fn list_preserves_values() {
// [ 10 20 ] => stack: 10 20 2
// drop => 10 20
// + => 30
expect_int("[ 10 20 ] drop +", 30); expect_int("[ 10 20 ] drop +", 30);
} }
@@ -97,10 +83,10 @@ fn conditional_based_on_step() {
fn accumulator() { fn accumulator() {
let f = forth(); let f = forth();
let ctx = default_ctx(); let ctx = default_ctx();
f.evaluate(r#"0 "acc" set"#, &ctx).unwrap(); f.evaluate(r#"0 !acc"#, &ctx).unwrap();
for _ in 0..5 { for _ in 0..5 {
f.clear_stack(); 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); 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 let outputs = f
.evaluate(r#""kick" s { emit } 0 ?"#, &default_ctx()) .evaluate(r#""kick" s { emit } 0 ?"#, &default_ctx())
.unwrap(); .unwrap();
// Should have 1 output from implicit emit at end (since cmd is set but not emitted) // No output since emit was skipped and no implicit emit
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 0);
} }
#[test] #[test]

View File

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

View File

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

View File

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