Files
Cagire/crates/forth/src/vm.rs
2026-02-22 13:28:03 +01:00

1669 lines
64 KiB
Rust

//! Stack-based Forth interpreter with audio command generation.
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::{Rng as RngTrait, SeedableRng};
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use super::compiler::compile_script;
use super::ops::Op;
use super::types::{
CmdRegister, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, Stack, StepContext,
Value, Variables, VariablesMap,
};
pub struct Forth {
stack: Stack,
vars: Variables,
dict: Dictionary,
rng: Rng,
}
impl Forth {
pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self {
Self {
stack: Mutex::new(Vec::new()),
vars,
dict,
rng,
}
}
pub fn stack(&self) -> Vec<Value> {
self.stack.lock().clone()
}
pub fn clear_stack(&self) {
self.stack.lock().clear();
}
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
self.apply_var_writes(var_writes);
Ok(outputs)
}
pub fn evaluate_with_trace(
&self,
script: &str,
ctx: &StepContext,
trace: &mut ExecutionTrace,
) -> Result<Vec<String>, String> {
let (outputs, var_writes) = self.evaluate_impl(script, ctx, Some(trace))?;
self.apply_var_writes(var_writes);
Ok(outputs)
}
pub fn evaluate_raw(
&self,
script: &str,
ctx: &StepContext,
trace: &mut ExecutionTrace,
) -> Result<(Vec<String>, HashMap<String, Value>), String> {
self.evaluate_impl(script, ctx, Some(trace))
}
fn apply_var_writes(&self, writes: HashMap<String, Value>) {
if writes.is_empty() {
return;
}
let mut new_vars = (*self.vars.load_full()).clone();
for (k, v) in writes {
new_vars.insert(k, v);
}
self.vars.store(Arc::new(new_vars));
}
fn evaluate_impl(
&self,
script: &str,
ctx: &StepContext,
trace: Option<&mut ExecutionTrace>,
) -> Result<(Vec<String>, HashMap<String, Value>), String> {
if script.trim().is_empty() {
return Err("empty script".into());
}
let ops = compile_script(script, &self.dict)?;
self.execute(&ops, ctx, trace)
}
fn execute(
&self,
ops: &[Op],
ctx: &StepContext,
trace: Option<&mut ExecutionTrace>,
) -> Result<(Vec<String>, HashMap<String, Value>), String> {
let mut stack = self.stack.lock();
let mut outputs: Vec<String> = Vec::with_capacity(8);
let mut cmd = CmdRegister::new();
let vars_snapshot = self.vars.load_full();
let mut var_writes: HashMap<String, Value> = HashMap::new();
self.execute_ops(
ops,
ctx,
&mut stack,
&mut outputs,
&mut cmd,
trace,
&vars_snapshot,
&mut var_writes,
)?;
Ok((outputs, var_writes))
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::only_used_in_recursion)]
fn execute_ops(
&self,
ops: &[Op],
ctx: &StepContext,
stack: &mut Vec<Value>,
outputs: &mut Vec<String>,
cmd: &mut CmdRegister,
trace: Option<&mut ExecutionTrace>,
vars_snapshot: &VariablesMap,
var_writes: &mut HashMap<String, Value>,
) -> Result<(), String> {
let mut pc = 0;
let trace_cell = std::cell::RefCell::new(trace);
let var_writes_cell = std::cell::RefCell::new(Some(var_writes));
let run_quotation = |quot: Value,
stack: &mut Vec<Value>,
outputs: &mut Vec<String>,
cmd: &mut CmdRegister|
-> Result<(), String> {
match quot {
Value::Quotation(quot_ops, body_span) => {
if let Some(span) = body_span {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.executed_spans.push(span);
}
}
let mut trace_opt = trace_cell.borrow_mut().take();
let mut var_writes_guard = var_writes_cell.borrow_mut();
let vw = var_writes_guard.as_mut().expect("var_writes taken");
self.execute_ops(
&quot_ops,
ctx,
stack,
outputs,
cmd,
trace_opt.as_deref_mut(),
vars_snapshot,
vw,
)?;
drop(var_writes_guard);
*trace_cell.borrow_mut() = trace_opt;
Ok(())
}
_ => Err("expected quotation".into()),
}
};
let select_and_run = |selected: Value,
stack: &mut Vec<Value>,
outputs: &mut Vec<String>,
cmd: &mut CmdRegister|
-> Result<(), String> {
if let Some(span) = selected.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if matches!(selected, Value::Quotation(..)) {
run_quotation(selected, stack, outputs, cmd)
} else {
stack.push(selected);
Ok(())
}
};
let drain_select_run = |count: usize,
idx: usize,
stack: &mut Vec<Value>,
outputs: &mut Vec<String>,
cmd: &mut CmdRegister|
-> Result<(), String> {
ensure(stack, count)?;
let start = stack.len() - count;
let selected = stack[start + idx].clone();
stack.truncate(start);
select_and_run(selected, stack, outputs, cmd)
};
let compute_poly_count = |cmd: &CmdRegister| -> usize {
let sound_len = match cmd.sound() {
Some(Value::CycleList(items)) => items.len(),
_ => 1,
};
let param_max = cmd
.params()
.iter()
.map(|(_, v)| match v {
Value::CycleList(items) => items.len(),
_ => 1,
})
.max()
.unwrap_or(1);
sound_len.max(param_max)
};
let has_arp_list = |cmd: &CmdRegister| -> bool {
matches!(cmd.sound(), Some(Value::ArpList(_)))
|| cmd.params().iter().any(|(_, v)| matches!(v, Value::ArpList(_)))
};
let compute_arp_count = |cmd: &CmdRegister| -> usize {
let sound_len = match cmd.sound() {
Some(Value::ArpList(items)) => items.len(),
_ => 0,
};
let param_max = cmd
.params()
.iter()
.map(|(_, v)| match v {
Value::ArpList(items) => items.len(),
_ => 0,
})
.max()
.unwrap_or(0);
sound_len.max(param_max).max(1)
};
let emit_with_cycling = |cmd: &CmdRegister,
emit_idx: usize,
delta_secs: f64,
outputs: &mut Vec<String>|
-> Result<Option<Value>, String> {
let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?;
let resolved_sound_val = sound_opt.map(|sv| resolve_cycling(sv, emit_idx));
let sound_str = match &resolved_sound_val {
Some(v) => Some(v.as_str()?.to_string()),
None => None,
};
let resolved_params: Vec<(&str, String)> = params
.iter()
.map(|(k, v)| {
let resolved = resolve_cycling(v, emit_idx);
if let Value::CycleList(_) | Value::ArpList(_) = v {
if let Some(span) = resolved.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
}
(*k, resolved.to_param_string())
})
.collect();
emit_output(
sound_str.as_deref(),
&resolved_params,
ctx.step_duration(),
delta_secs,
outputs,
);
Ok(resolved_sound_val.map(|v| v.into_owned()))
};
while pc < ops.len() {
match &ops[pc] {
Op::PushInt(n, span) => stack.push(Value::Int(*n, *span)),
Op::PushFloat(f, span) => stack.push(Value::Float(*f, *span)),
Op::PushStr(s, span) => stack.push(Value::Str(s.clone(), *span)),
Op::Dup => {
ensure(stack, 1)?;
let v = stack.last().unwrap().clone();
stack.push(v);
}
Op::Dupn => {
let n = pop_int(stack)?;
let v = pop(stack)?;
for _ in 0..n {
stack.push(v.clone());
}
}
Op::Drop => {
pop(stack)?;
}
Op::Swap => {
ensure(stack, 2)?;
let len = stack.len();
stack.swap(len - 1, len - 2);
}
Op::Over => {
ensure(stack, 2)?;
let v = stack[stack.len() - 2].clone();
stack.push(v);
}
Op::Rot => {
ensure(stack, 3)?;
let v = stack.remove(stack.len() - 3);
stack.push(v);
}
Op::Nip => {
ensure(stack, 2)?;
stack.remove(stack.len() - 2);
}
Op::Tuck => {
ensure(stack, 2)?;
let len = stack.len();
let v = stack[len - 1].clone();
stack.insert(len - 2, v);
}
Op::Dup2 => {
ensure(stack, 2)?;
let len = stack.len();
let a = stack[len - 2].clone();
let b = stack[len - 1].clone();
stack.push(a);
stack.push(b);
}
Op::Drop2 => {
ensure(stack, 2)?;
stack.pop();
stack.pop();
}
Op::Swap2 => {
ensure(stack, 4)?;
let len = stack.len();
stack.swap(len - 4, len - 2);
stack.swap(len - 3, len - 1);
}
Op::Over2 => {
ensure(stack, 4)?;
let len = stack.len();
let a = stack[len - 4].clone();
let b = stack[len - 3].clone();
stack.push(a);
stack.push(b);
}
Op::Rev => {
let count = pop_int(stack)? as usize;
ensure(stack, count)?;
let start = stack.len() - count;
stack[start..].reverse();
}
Op::Shuffle => {
let count = pop_int(stack)? as usize;
ensure(stack, count)?;
let start = stack.len() - count;
let slice = &mut stack[start..];
let mut rng = self.rng.lock();
for i in (1..slice.len()).rev() {
let j = rng.gen_range(0..=i);
slice.swap(i, j);
}
}
Op::Sort => {
let count = pop_int(stack)? as usize;
ensure(stack, count)?;
let start = stack.len() - count;
stack[start..].sort_by(|a, b| {
a.as_float()
.unwrap_or(0.0)
.partial_cmp(&b.as_float().unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
}
Op::RSort => {
let count = pop_int(stack)? as usize;
ensure(stack, count)?;
let start = stack.len() - count;
stack[start..].sort_by(|a, b| {
b.as_float()
.unwrap_or(0.0)
.partial_cmp(&a.as_float().unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
}
Op::Sum => {
let count = pop_int(stack)? as usize;
ensure(stack, count)?;
let start = stack.len() - count;
let total: f64 = stack
.drain(start..)
.map(|v| v.as_float().unwrap_or(0.0))
.sum();
stack.push(float_to_value(total));
}
Op::Prod => {
let count = pop_int(stack)? as usize;
ensure(stack, count)?;
let start = stack.len() - count;
let product: f64 = stack
.drain(start..)
.map(|v| v.as_float().unwrap_or(1.0))
.product();
stack.push(float_to_value(product));
}
Op::Add => binary_op(stack, |a, b| a + b)?,
Op::Sub => binary_op(stack, |a, b| a - b)?,
Op::Mul => binary_op(stack, |a, b| a * b)?,
Op::Div => {
let b = pop(stack)?;
let a = pop(stack)?;
if b.as_float().map_or(true, |v| v == 0.0) {
return Err("division by zero".into());
}
stack.push(lift_binary(a, b, |x, y| x / y)?);
}
Op::Mod => {
let b = pop(stack)?;
let a = pop(stack)?;
if b.as_float().map_or(true, |v| v == 0.0) {
return Err("modulo by zero".into());
}
let result = lift_binary(a, b, |x, y| (x as i64 % y as i64) as f64)?;
stack.push(result);
}
Op::Neg => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| -x)?);
}
Op::Abs => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.abs())?);
}
Op::Floor => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.floor())?);
}
Op::Ceil => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.ceil())?);
}
Op::Round => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.round())?);
}
Op::Min => binary_op(stack, |a, b| a.min(b))?,
Op::Max => binary_op(stack, |a, b| a.max(b))?,
Op::Pow => binary_op(stack, |a, b| a.powf(b))?,
Op::Sqrt => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.sqrt())?);
}
Op::Sin => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.sin())?);
}
Op::Cos => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.cos())?);
}
Op::Log => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.ln())?);
}
Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?,
Op::Ne => cmp_op(stack, |a, b| (a - b).abs() >= f64::EPSILON)?,
Op::Lt => cmp_op(stack, |a, b| a < b)?,
Op::Gt => cmp_op(stack, |a, b| a > b)?,
Op::Le => cmp_op(stack, |a, b| a <= b)?,
Op::Ge => cmp_op(stack, |a, b| a >= b)?,
Op::And => {
let b = pop_bool(stack)?;
let a = pop_bool(stack)?;
stack.push(Value::Int(if a && b { 1 } else { 0 }, None));
}
Op::Or => {
let b = pop_bool(stack)?;
let a = pop_bool(stack)?;
stack.push(Value::Int(if a || b { 1 } else { 0 }, None));
}
Op::Not => {
let v = pop_bool(stack)?;
stack.push(Value::Int(if v { 0 } else { 1 }, None));
}
Op::Xor => {
let b = pop_bool(stack)?;
let a = pop_bool(stack)?;
stack.push(Value::Int(if a ^ b { 1 } else { 0 }, None));
}
Op::Nand => {
let b = pop_bool(stack)?;
let a = pop_bool(stack)?;
stack.push(Value::Int(if !(a && b) { 1 } else { 0 }, None));
}
Op::Nor => {
let b = pop_bool(stack)?;
let a = pop_bool(stack)?;
stack.push(Value::Int(if !(a || b) { 1 } else { 0 }, None));
}
Op::BranchIfZero(offset, then_span, else_span) => {
let v = pop(stack)?;
if !v.is_truthy() {
if let Some(span) = else_span {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.executed_spans.push(*span);
}
}
pc += offset;
} else if let Some(span) = then_span {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.executed_spans.push(*span);
}
}
}
Op::Branch(offset) => {
pc += offset;
}
Op::NewCmd => {
ensure(stack, 1)?;
let values = std::mem::take(stack);
let val = if values.len() == 1 {
values.into_iter().next().unwrap()
} else {
Value::CycleList(Arc::from(values))
};
cmd.set_sound(val);
}
Op::SetParam(param) => {
ensure(stack, 1)?;
let values = std::mem::take(stack);
let val = if values.len() == 1 {
values.into_iter().next().unwrap()
} else {
Value::CycleList(Arc::from(values))
};
cmd.set_param(param, val);
}
Op::Emit => {
if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd);
let explicit_deltas = !cmd.deltas().is_empty();
let delta_list: Vec<Value> = if explicit_deltas {
cmd.deltas().to_vec()
} else {
Vec::new()
};
let count = if explicit_deltas {
arp_count.max(delta_list.len())
} else {
arp_count
};
for i in 0..count {
let delta_secs = if explicit_deltas {
let dv = &delta_list[i % delta_list.len()];
let frac = dv.as_float()?;
if let Some(span) = dv.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
ctx.nudge_secs + frac * ctx.step_duration()
} else {
ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration()
};
if let Some(sound_val) =
emit_with_cycling(cmd, i, delta_secs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
}
}
} else {
let poly_count = compute_poly_count(cmd);
let deltas = if cmd.deltas().is_empty() {
vec![Value::Float(0.0, None)]
} else {
cmd.deltas().to_vec()
};
for poly_idx in 0..poly_count {
for delta_val in deltas.iter() {
let delta_frac = delta_val.as_float()?;
let delta_secs =
ctx.nudge_secs + delta_frac * ctx.step_duration();
if let Some(span) = delta_val.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if let Some(sound_val) =
emit_with_cycling(cmd, poly_idx, delta_secs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) =
trace_cell.borrow_mut().as_mut()
{
trace.selected_spans.push(span);
}
}
}
}
}
}
}
Op::Get => {
let name = pop(stack)?;
let name = name.as_str()?;
let vw = var_writes_cell.borrow();
let vw_ref = vw.as_ref().expect("var_writes taken");
let val = vw_ref
.get(name)
.or_else(|| vars_snapshot.get(name))
.cloned()
.unwrap_or(Value::Int(0, None));
drop(vw);
stack.push(val);
}
Op::Set => {
let name = pop(stack)?;
let name = name.as_str()?.to_string();
let val = pop(stack)?;
var_writes_cell
.borrow_mut()
.as_mut()
.expect("var_writes taken")
.insert(name, val);
}
Op::SetKeep => {
let name = pop(stack)?;
let name = name.as_str()?.to_string();
let val = stack.last().ok_or("Stack underflow")?.clone();
var_writes_cell
.borrow_mut()
.as_mut()
.expect("var_writes taken")
.insert(name, val);
}
Op::GetContext(name) => {
let val = match *name {
"step" => Value::Int(ctx.step as i64, None),
"beat" => Value::Float(ctx.beat, None),
"bank" => Value::Int(ctx.bank as i64, None),
"pattern" => Value::Int(ctx.pattern as i64, None),
"tempo" => Value::Float(ctx.tempo, None),
"phase" => Value::Float(ctx.phase, None),
"slot" => Value::Int(ctx.slot as i64, None),
"runs" => Value::Int(ctx.runs as i64, None),
"iter" => Value::Int(ctx.iter as i64, None),
"speed" => Value::Float(ctx.speed, None),
"stepdur" => Value::Float(ctx.step_duration(), None),
"fill" => Value::Int(if ctx.fill { 1 } else { 0 }, None),
"mx" => Value::Float(ctx.mouse_x, None),
"my" => Value::Float(ctx.mouse_y, None),
"mdown" => Value::Float(ctx.mouse_down, None),
_ => Value::Int(0, None),
};
stack.push(val);
}
Op::Rand(word_span) => {
let b = pop(stack)?;
let a = pop(stack)?;
match (&a, &b) {
(Value::Int(a_i, _), Value::Int(b_i, _)) => {
let (lo, hi) = if a_i <= b_i {
(*a_i, *b_i)
} else {
(*b_i, *a_i)
};
let val = self.rng.lock().gen_range(lo..=hi);
record_resolved(&trace_cell, *word_span, ResolvedValue::Int(val));
stack.push(Value::Int(val, None));
}
_ => {
let a_f = a.as_float()?;
let b_f = b.as_float()?;
let (lo, hi) = if a_f <= b_f { (a_f, b_f) } else { (b_f, a_f) };
let val = if (hi - lo).abs() < f64::EPSILON {
lo
} else {
self.rng.lock().gen_range(lo..hi)
};
record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
stack.push(Value::Float(val, None));
}
}
}
Op::ExpRand(word_span) => {
let hi = pop_float(stack)?;
let lo = pop_float(stack)?;
if lo <= 0.0 || hi <= 0.0 {
return Err("exprand requires positive values".into());
}
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let u: f64 = self.rng.lock().gen();
let val = lo * (hi / lo).powf(u);
record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
stack.push(Value::Float(val, None));
}
Op::LogRand(word_span) => {
let hi = pop_float(stack)?;
let lo = pop_float(stack)?;
if lo <= 0.0 || hi <= 0.0 {
return Err("logrand requires positive values".into());
}
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let u: f64 = self.rng.lock().gen();
let val = hi * (lo / hi).powf(u);
record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
stack.push(Value::Float(val, None));
}
Op::Seed => {
let s = pop_int(stack)?;
*self.rng.lock() = StdRng::seed_from_u64(s as u64);
}
Op::Cycle(word_span) | Op::PCycle(word_span) => {
let count = pop_int(stack)? as usize;
if count == 0 {
return Err("cycle count must be > 0".into());
}
let idx = match &ops[pc] {
Op::Cycle(_) => ctx.runs,
_ => ctx.iter,
} % count;
if let Some(span) = word_span {
if stack.len() >= count {
let start = stack.len() - count;
let selected = &stack[start + idx];
record_resolved_from_value(&trace_cell, Some(*span), selected);
}
}
drain_select_run(count, idx, stack, outputs, cmd)?;
}
Op::Choose(word_span) => {
let count = pop_int(stack)? as usize;
if count == 0 {
return Err("choose count must be > 0".into());
}
let idx = self.rng.lock().gen_range(0..count);
if let Some(span) = word_span {
if stack.len() >= count {
let start = stack.len() - count;
let selected = &stack[start + idx];
record_resolved_from_value(&trace_cell, Some(*span), selected);
}
}
drain_select_run(count, idx, stack, outputs, cmd)?;
}
Op::Bounce(word_span) => {
let count = pop_int(stack)? as usize;
if count == 0 {
return Err("bounce count must be > 0".into());
}
let idx = if count == 1 {
0
} else {
let period = 2 * (count - 1);
let raw = ctx.runs % period;
if raw < count { raw } else { period - raw }
};
if let Some(span) = word_span {
if stack.len() >= count {
let start = stack.len() - count;
let selected = &stack[start + idx];
record_resolved_from_value(&trace_cell, Some(*span), selected);
}
}
drain_select_run(count, idx, stack, outputs, cmd)?;
}
Op::WChoose(word_span) => {
let count = pop_int(stack)? as usize;
if count == 0 {
return Err("wchoose count must be > 0".into());
}
let pairs_needed = count * 2;
ensure(stack, pairs_needed)?;
let start = stack.len() - pairs_needed;
let mut values = Vec::with_capacity(count);
let mut weights = Vec::with_capacity(count);
for i in 0..count {
let val = stack[start + i * 2].clone();
let w = stack[start + i * 2 + 1].as_float()?;
if w < 0.0 {
return Err("wchoose: negative weight".into());
}
values.push(val);
weights.push(w);
}
stack.truncate(start);
let total: f64 = weights.iter().sum();
if total <= 0.0 {
return Err("wchoose: total weight must be > 0".into());
}
let threshold: f64 = self.rng.lock().gen::<f64>() * total;
let mut cumulative = 0.0;
let mut selected_idx = count - 1;
for (i, &w) in weights.iter().enumerate() {
cumulative += w;
if threshold < cumulative {
selected_idx = i;
break;
}
}
let selected = values.swap_remove(selected_idx);
record_resolved_from_value(&trace_cell, *word_span, &selected);
select_and_run(selected, stack, outputs, cmd)?;
}
Op::ChanceExec(word_span) | Op::ProbExec(word_span) => {
let threshold = pop_float(stack)?;
let quot = pop(stack)?;
let val: f64 = self.rng.lock().gen();
let limit = match &ops[pc] {
Op::ChanceExec(_) => threshold,
_ => threshold / 100.0,
};
let fired = val < limit;
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(fired));
if fired {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::Coin(word_span) => {
let val: f64 = self.rng.lock().gen();
let result = val < 0.5;
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
stack.push(Value::Int(if result { 1 } else { 0 }, None));
}
Op::Every(word_span) => {
let n = pop_int(stack)?;
let quot = pop(stack)?;
if n <= 0 {
return Err("every count must be > 0".into());
}
let result = ctx.iter as i64 % n == 0;
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
if result {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::Bjork(word_span) | Op::PBjork(word_span) => {
let n = pop_int(stack)?;
let k = pop_int(stack)?;
let quot = pop(stack)?;
if n <= 0 || k < 0 {
return Err("bjork: n must be > 0, k must be >= 0".into());
}
let counter = match &ops[pc] {
Op::Bjork(_) => ctx.runs,
_ => ctx.iter,
};
let pos = counter % n as usize;
let hit = k >= n || euclidean_hit(k as usize, n as usize, pos);
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(hit));
if hit {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::Quotation(quote_ops, body_span) => {
stack.push(Value::Quotation(quote_ops.clone(), *body_span));
}
Op::When | Op::Unless => {
let cond = pop(stack)?;
let quot = pop(stack)?;
let should_run = match &ops[pc] {
Op::When => cond.is_truthy(),
_ => !cond.is_truthy(),
};
if should_run {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::IfElse => {
let cond = pop(stack)?;
let false_quot = pop(stack)?;
let true_quot = pop(stack)?;
let quot = if cond.is_truthy() {
true_quot
} else {
false_quot
};
run_quotation(quot, stack, outputs, cmd)?;
}
Op::Pick => {
let idx_i = pop_int(stack)?;
if idx_i < 0 {
return Err(format!("pick index must be >= 0, got {idx_i}"));
}
let idx = idx_i as usize;
let mut quots: Vec<Value> = Vec::new();
while let Some(val) = stack.pop() {
match &val {
Value::Quotation(_, _) => quots.push(val),
_ => {
stack.push(val);
break;
}
}
}
quots.reverse();
if idx >= quots.len() {
return Err(format!(
"pick index {} out of range (have {} quotations)",
idx,
quots.len()
));
}
run_quotation(quots.swap_remove(idx), stack, outputs, cmd)?;
}
Op::Mtof => {
let note = pop_float(stack)?;
let freq = 440.0 * 2.0_f64.powf((note - 69.0) / 12.0);
stack.push(Value::Float(freq, None));
}
Op::Ftom => {
let freq = pop_float(stack)?;
let note = 69.0 + 12.0 * (freq / 440.0).log2();
stack.push(Value::Float(note, None));
}
Op::Degree(pattern) => {
if pattern.is_empty() {
return Err("empty scale pattern".into());
}
let val = pop(stack)?;
let len = pattern.len() as i64;
let result = lift_unary_int(val, |degree| {
let octave_offset = degree.div_euclid(len);
let idx = degree.rem_euclid(len) as usize;
60 + octave_offset * 12 + pattern[idx]
})?;
stack.push(result);
}
Op::Chord(intervals) => {
let root = pop_int(stack)?;
for &interval in *intervals {
stack.push(Value::Int(root + interval, None));
}
}
Op::Oct => {
let shift = pop(stack)?;
let note = pop(stack)?;
let result = lift_binary(note, shift, |n, s| n + s * 12.0)?;
stack.push(result);
}
Op::SetTempo => {
let tempo = pop_float(stack)?;
let clamped = tempo.clamp(20.0, 300.0);
var_writes_cell
.borrow_mut()
.as_mut()
.expect("var_writes taken")
.insert("__tempo__".to_string(), Value::Float(clamped, None));
}
Op::SetSpeed => {
let speed = pop_float(stack)?;
let clamped = speed.clamp(0.125, 8.0);
var_writes_cell
.borrow_mut()
.as_mut()
.expect("var_writes taken")
.insert(ctx.speed_key.to_string(), Value::Float(clamped, None));
}
Op::Loop => {
let beats = pop_float(stack)?;
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
return Err("tempo and speed must be non-zero".into());
}
let dur = beats * 60.0 / ctx.tempo / ctx.speed;
cmd.set_param("fit", Value::Float(dur, None));
cmd.set_param("dur", Value::Float(dur, None));
}
Op::At => {
ensure(stack, 1)?;
let deltas = std::mem::take(stack);
cmd.set_deltas(deltas);
}
Op::Arp => {
ensure(stack, 1)?;
let values = std::mem::take(stack);
stack.push(Value::ArpList(Arc::from(values)));
}
Op::Adsr => {
let r = pop(stack)?;
let s = pop(stack)?;
let d = pop(stack)?;
let a = pop(stack)?;
cmd.set_param("attack", a);
cmd.set_param("decay", d);
cmd.set_param("sustain", s);
cmd.set_param("release", r);
}
Op::Ad => {
let d = pop(stack)?;
let a = pop(stack)?;
cmd.set_param("attack", a);
cmd.set_param("decay", d);
cmd.set_param("sustain", Value::Int(0, None));
}
Op::Apply => {
let quot = pop(stack)?;
run_quotation(quot, stack, outputs, cmd)?;
}
Op::Ramp => {
let curve = pop_float(stack)?;
let freq = pop_float(stack)?;
let phase = (freq * ctx.beat).fract();
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
let val = phase.powf(curve);
stack.push(Value::Float(val, None));
}
Op::Triangle => {
let freq = pop_float(stack)?;
let phase = (freq * ctx.beat).fract();
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
let val = 1.0 - (2.0 * phase - 1.0).abs();
stack.push(Value::Float(val, None));
}
Op::Range => {
let max = pop_float(stack)?;
let min = pop_float(stack)?;
let val = pop_float(stack)?;
stack.push(Value::Float(min + val * (max - min), None));
}
Op::Perlin => {
let freq = pop_float(stack)?;
let val = perlin_noise_1d(freq * ctx.beat);
stack.push(Value::Float(val, None));
}
Op::ClearCmd => {
cmd.clear();
}
Op::IntRange => {
let end = pop_int(stack)?;
let start = pop_int(stack)?;
if start <= end {
for i in start..=end {
stack.push(Value::Int(i, None));
}
} else {
for i in (end..=start).rev() {
stack.push(Value::Int(i, None));
}
}
}
Op::StepRange => {
let step = pop_float(stack)?;
let end = pop_float(stack)?;
let start = pop_float(stack)?;
if step == 0.0 {
return Err("step cannot be zero".into());
}
let ascending = step > 0.0;
let mut val = start;
loop {
let done = if ascending { val > end } else { val < end };
if done {
break;
}
stack.push(float_to_value(val));
val += step;
}
}
Op::Generate => {
let count = pop_int(stack)?;
let quot = pop(stack)?;
if count < 0 {
return Err("gen count must be >= 0".into());
}
let mut results = Vec::with_capacity(count as usize);
for _ in 0..count {
run_quotation(quot.clone(), stack, outputs, cmd)?;
results.push(stack.pop().ok_or("gen: quotation must produce a value")?);
}
for val in results {
stack.push(val);
}
}
Op::Times => {
let quot = pop(stack)?;
let count = pop_int(stack)?;
if count < 0 {
return Err("times count must be >= 0".into());
}
for i in 0..count {
var_writes_cell
.borrow_mut()
.as_mut()
.expect("var_writes taken")
.insert("i".to_string(), Value::Int(i, None));
run_quotation(quot.clone(), stack, outputs, cmd)?;
}
}
Op::GeomRange => {
let count = pop_int(stack)?;
let ratio = pop_float(stack)?;
let start = pop_float(stack)?;
if count < 0 {
return Err("geom.. count must be >= 0".into());
}
let mut val = start;
for _ in 0..count {
stack.push(float_to_value(val));
val *= ratio;
}
}
Op::Euclid => {
let n = pop_int(stack)?;
let k = pop_int(stack)?;
if k < 0 || n < 0 {
return Err("euclid: k and n must be >= 0".into());
}
for idx in euclidean_rhythm(k as usize, n as usize, 0) {
stack.push(Value::Int(idx, None));
}
}
Op::EuclidRot => {
let r = pop_int(stack)?;
let n = pop_int(stack)?;
let k = pop_int(stack)?;
if k < 0 || n < 0 || r < 0 {
return Err("euclidrot: k, n, and r must be >= 0".into());
}
for idx in euclidean_rhythm(k as usize, n as usize, r as usize) {
stack.push(Value::Int(idx, None));
}
}
Op::ModLfo(shape) => {
let period = pop_float(stack)? * ctx.step_duration();
let max = pop_float(stack)?;
let min = pop_float(stack)?;
let suffix = match shape { 1 => "t", 2 => "w", 3 => "q", _ => "" };
let s = format!("{min}~{max}:{period}{suffix}");
stack.push(Value::Str(s.into(), None));
}
Op::ModSlide(curve) => {
let dur = pop_float(stack)? * ctx.step_duration();
let end = pop_float(stack)?;
let start = pop_float(stack)?;
let suffix = match curve { 1 => "e", 2 => "s", _ => "" };
let s = format!("{start}>{end}:{dur}{suffix}");
stack.push(Value::Str(s.into(), None));
}
Op::ModRnd(dist) => {
let period = pop_float(stack)? * ctx.step_duration();
let max = pop_float(stack)?;
let min = pop_float(stack)?;
let suffix = match dist { 1 => "s", 2 => "d", _ => "" };
let s = format!("{min}?{max}:{period}{suffix}");
stack.push(Value::Str(s.into(), None));
}
Op::ModEnv => {
ensure(stack, 1)?;
let values = std::mem::take(stack);
let mut floats = Vec::with_capacity(values.len());
for v in &values {
floats.push(v.as_float()?);
}
if floats.len() < 3 || (floats.len() - 1) % 2 != 0 {
return Err("env expects: start target1 dur1 [target2 dur2 ...]".into());
}
let step_dur = ctx.step_duration();
use std::fmt::Write;
let mut s = String::new();
let _ = write!(&mut s, "{}", floats[0]);
for pair in floats[1..].chunks(2) {
let _ = write!(&mut s, ">{}:{}", pair[0], pair[1] * step_dur);
}
stack.push(Value::Str(s.into(), None));
}
// MIDI operations
Op::MidiEmit => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
// Build schedule: (emit_idx, delta_secs) — same logic as Op::Emit
let schedule: Vec<(usize, f64)> = if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd);
let explicit = !cmd.deltas().is_empty();
let delta_list = cmd.deltas();
let count = if explicit {
arp_count.max(delta_list.len())
} else {
arp_count
};
(0..count)
.map(|i| {
let delta_secs = if explicit {
let frac = delta_list[i % delta_list.len()]
.as_float()
.unwrap_or(0.0);
ctx.nudge_secs + frac * ctx.step_duration()
} else {
ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration()
};
(i, delta_secs)
})
.collect()
} else {
let poly_count = compute_poly_count(cmd);
let deltas: Vec<f64> = if cmd.deltas().is_empty() {
vec![0.0]
} else {
cmd.deltas()
.iter()
.filter_map(|v| v.as_float().ok())
.collect()
};
let mut sched = Vec::with_capacity(poly_count * deltas.len());
for poly_idx in 0..poly_count {
for &frac in &deltas {
sched.push((
poly_idx,
ctx.nudge_secs + frac * ctx.step_duration(),
));
}
}
sched
};
for (emit_idx, delta_secs) in schedule {
let get_int = |name: &str| -> Option<i64> {
params
.iter()
.rev()
.find(|(k, _)| *k == name)
.and_then(|(_, v)| {
resolve_cycling(v, emit_idx).as_int().ok()
})
};
let get_float = |name: &str| -> Option<f64> {
params
.iter()
.rev()
.find(|(k, _)| *k == name)
.and_then(|(_, v)| {
resolve_cycling(v, emit_idx).as_float().ok()
})
};
let chan = get_int("chan")
.map(|c| (c.clamp(1, 16) - 1) as u8)
.unwrap_or(0);
let dev =
get_int("dev").map(|d| d.clamp(0, 3) as u8).unwrap_or(0);
let delta_suffix = if delta_secs > 0.0 {
format!("/delta/{delta_secs}")
} else {
String::new()
};
if let (Some(cc), Some(val)) = (get_int("ccnum"), get_int("ccout")) {
let cc = cc.clamp(0, 127) as u8;
let val = val.clamp(0, 127) as u8;
outputs.push(format!(
"/midi/cc/{cc}/{val}/chan/{chan}/dev/{dev}{delta_suffix}"
));
} else if let Some(bend) = get_float("bend") {
let bend_clamped = bend.clamp(-1.0, 1.0);
let bend_14bit = ((bend_clamped + 1.0) * 8191.5) as u16;
outputs.push(format!(
"/midi/bend/{bend_14bit}/chan/{chan}/dev/{dev}{delta_suffix}"
));
} else if let Some(pressure) = get_int("pressure") {
let pressure = pressure.clamp(0, 127) as u8;
outputs.push(format!(
"/midi/pressure/{pressure}/chan/{chan}/dev/{dev}{delta_suffix}"
));
} else if let Some(program) = get_int("program") {
let program = program.clamp(0, 127) as u8;
outputs.push(format!(
"/midi/program/{program}/chan/{chan}/dev/{dev}{delta_suffix}"
));
} else {
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
let velocity =
get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
let dur = get_float("dur").unwrap_or(1.0);
let dur_secs = dur * ctx.step_duration();
outputs.push(format!(
"/midi/note/{note}/vel/{velocity}/chan/{chan}/dur/{dur_secs}/dev/{dev}{delta_suffix}"
));
}
}
}
Op::MidiClock => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = extract_dev_param(params);
outputs.push(format!("/midi/clock/dev/{dev}"));
}
Op::MidiStart => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = extract_dev_param(params);
outputs.push(format!("/midi/start/dev/{dev}"));
}
Op::MidiStop => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = extract_dev_param(params);
outputs.push(format!("/midi/stop/dev/{dev}"));
}
Op::MidiContinue => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = extract_dev_param(params);
outputs.push(format!("/midi/continue/dev/{dev}"));
}
Op::GetMidiCC => {
let chan = pop_int(stack)?;
let cc = pop_int(stack)?;
let cc_clamped = (cc.clamp(0, 127)) as usize;
let chan_clamped = (chan.clamp(1, 16) - 1) as usize;
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
let dev = extract_dev_param(params) as usize;
let val = ctx
.cc_access
.as_ref()
.map(|cc| cc.get_cc(dev, chan_clamped, cc_clamped))
.unwrap_or(0);
stack.push(Value::Int(val as i64, None));
}
Op::Forget => {
let name = pop(stack)?;
self.dict.lock().remove(name.as_str()?);
}
}
pc += 1;
}
Ok(())
}
}
fn record_resolved(
trace_cell: &std::cell::RefCell<Option<&mut ExecutionTrace>>,
span: Option<SourceSpan>,
value: ResolvedValue,
) {
if let Some(span) = span {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.resolved.push((span, value));
}
}
}
fn record_resolved_from_value(
trace_cell: &std::cell::RefCell<Option<&mut ExecutionTrace>>,
span: Option<SourceSpan>,
value: &Value,
) {
if let Some(span) = span {
let resolved = match value {
Value::Int(i, _) => ResolvedValue::Int(*i),
Value::Float(f, _) => ResolvedValue::Float(*f),
Value::Str(s, _) => ResolvedValue::Str(s.clone()),
_ => return,
};
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.resolved.push((span, resolved));
}
}
}
fn extract_dev_param(params: &[(&str, Value)]) -> u8 {
params
.iter()
.rev()
.find(|(k, _)| *k == "dev")
.and_then(|(_, v)| v.as_int().ok())
.map(|d| d.clamp(0, 3) as u8)
.unwrap_or(0)
}
fn is_tempo_scaled_param(name: &str) -> bool {
matches!(
name,
"attack"
| "decay"
| "release"
| "lpa"
| "lpd"
| "lpr"
| "hpa"
| "hpd"
| "hpr"
| "bpa"
| "bpd"
| "bpr"
| "patt"
| "pdec"
| "prel"
| "fma"
| "fmd"
| "fmr"
| "glide"
| "chorusdelay"
| "duration"
)
}
fn emit_output(
sound: Option<&str>,
params: &[(&str, String)],
step_duration: f64,
nudge_secs: f64,
outputs: &mut Vec<String>,
) {
use std::fmt::Write;
let mut out = String::with_capacity(128);
out.push('/');
let has_dur = params.iter().any(|(k, _)| *k == "dur");
let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime");
if let Some(s) = sound {
let _ = write!(&mut out, "sound/{s}");
}
for (i, (k, v)) in params.iter().enumerate() {
if !out.ends_with('/') {
out.push('/');
}
if is_tempo_scaled_param(k) {
if let Ok(val) = v.parse::<f64>() {
let _ = write!(&mut out, "{k}/{}", val * step_duration);
continue;
}
}
if Some(i) == delaytime_idx && sound.is_some() {
let ratio: f64 = v.parse().unwrap_or(1.0);
let _ = write!(&mut out, "{k}/{}", ratio * step_duration);
} else {
let _ = write!(&mut out, "{k}/{v}");
}
}
if nudge_secs > 0.0 {
if !out.ends_with('/') {
out.push('/');
}
let _ = write!(&mut out, "delta/{nudge_secs}");
}
if !has_dur {
if !out.ends_with('/') {
out.push('/');
}
let _ = write!(&mut out, "dur/{}", step_duration * 4.0);
}
if sound.is_some() && delaytime_idx.is_none() {
if !out.ends_with('/') {
out.push('/');
}
let _ = write!(&mut out, "delaytime/{step_duration}");
}
outputs.push(out);
}
fn euclidean_hit(k: usize, n: usize, pos: usize) -> bool {
if k == 0 {
return false;
}
((pos + 1) * k) / n != (pos * k) / n
}
fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec<i64> {
if k == 0 || n == 0 {
return Vec::new();
}
if k >= n {
return (0..n as i64).collect();
}
let mut groups: Vec<Vec<bool>> = (0..k).map(|_| vec![true]).collect();
groups.extend((0..(n - k)).map(|_| vec![false]));
while groups.len() > 1 {
let ones_count = groups.iter().filter(|g| g[0]).count();
let zeros_count = groups.len() - ones_count;
if zeros_count == 0 || ones_count == 0 {
break;
}
let min_count = ones_count.min(zeros_count);
let mut new_groups = Vec::with_capacity(groups.len() - min_count);
let (mut ones, mut zeros): (Vec<_>, Vec<_>) =
groups.into_iter().partition(|g| g[0]);
for _ in 0..min_count {
let mut one = ones.pop().unwrap();
one.extend(zeros.pop().unwrap());
new_groups.push(one);
}
new_groups.extend(ones);
new_groups.extend(zeros);
groups = new_groups;
}
let pattern: Vec<bool> = groups.into_iter().flatten().collect();
let rotated = if rotation > 0 && !pattern.is_empty() {
let r = rotation % pattern.len();
pattern.iter().cycle().skip(r).take(pattern.len()).copied().collect()
} else {
pattern
};
rotated
.into_iter()
.enumerate()
.filter_map(|(i, hit)| if hit { Some(i as i64) } else { None })
.collect()
}
fn perlin_grad(hash_input: i64) -> f64 {
let mut h = (hash_input as u64)
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
h ^= h >> 33;
h = h.wrapping_mul(0xff51afd7ed558ccd);
h ^= h >> 33;
// Convert to float in [-1, 1] range for varied gradients
(h as i64 as f64) / (i64::MAX as f64)
}
fn perlin_noise_1d(x: f64) -> f64 {
let x0 = x.floor() as i64;
let t = x - x0 as f64;
let s = t * t * (3.0 - 2.0 * t);
let d0 = perlin_grad(x0) * t;
let d1 = perlin_grad(x0 + 1) * (t - 1.0);
(d0 + s * (d1 - d0)) * 0.5 + 0.5
}
fn pop(stack: &mut Vec<Value>) -> Result<Value, String> {
stack.pop().ok_or_else(|| "stack underflow".to_string())
}
fn pop_int(stack: &mut Vec<Value>) -> Result<i64, String> {
pop(stack)?.as_int()
}
fn pop_float(stack: &mut Vec<Value>) -> Result<f64, String> {
pop(stack)?.as_float()
}
fn pop_bool(stack: &mut Vec<Value>) -> Result<bool, String> {
Ok(pop(stack)?.is_truthy())
}
fn ensure(stack: &[Value], n: usize) -> Result<(), String> {
if stack.len() < n {
return Err("stack underflow".into());
}
Ok(())
}
fn float_to_value(result: f64) -> Value {
if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
Value::Int(result as i64, None)
} else {
Value::Float(result, None)
}
}
fn lift_unary<F>(val: Value, f: F) -> Result<Value, String>
where
F: Fn(f64) -> f64,
{
Ok(float_to_value(f(val.as_float()?)))
}
fn lift_unary_int<F>(val: Value, f: F) -> Result<Value, String>
where
F: Fn(i64) -> i64,
{
Ok(Value::Int(f(val.as_int()?), None))
}
fn lift_binary<F>(a: Value, b: Value, f: F) -> Result<Value, String>
where
F: Fn(f64, f64) -> f64,
{
Ok(float_to_value(f(a.as_float()?, b.as_float()?)))
}
fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
where
F: Fn(f64, f64) -> f64 + Copy,
{
let b = pop(stack)?;
let a = pop(stack)?;
stack.push(lift_binary(a, b, f)?);
Ok(())
}
fn cmp_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
where
F: Fn(f64, f64) -> bool,
{
let b = pop(stack)?;
let a = pop(stack)?;
let result = if f(a.as_float()?, b.as_float()?) {
1
} else {
0
};
stack.push(Value::Int(result, None));
Ok(())
}
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> {
match val {
Value::CycleList(items) | Value::ArpList(items) if !items.is_empty() => {
Cow::Owned(items[emit_idx % items.len()].clone())
}
other => Cow::Borrowed(other),
}
}