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

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