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, Rng, 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, } } #[allow(dead_code)] pub fn stack(&self) -> Vec { self.stack.lock().clone() } #[allow(dead_code)] pub fn clear_stack(&self) { self.stack.lock().clear(); } pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result, 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, 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, HashMap), String> { self.evaluate_impl(script, ctx, Some(trace)) } fn apply_var_writes(&self, writes: HashMap) { if writes.is_empty() { return; } let mut new_vars = (**self.vars.load()).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, HashMap), 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, HashMap), String> { let mut stack = self.stack.lock(); let mut outputs: Vec = Vec::with_capacity(8); let mut cmd = CmdRegister::new(); let vars_snapshot = self.vars.load(); let mut var_writes: HashMap = 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, outputs: &mut Vec, cmd: &mut CmdRegister, trace: Option<&mut ExecutionTrace>, vars_snapshot: &VariablesMap, var_writes: &mut HashMap, ) -> 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, outputs: &mut Vec, 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( "_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, outputs: &mut Vec, 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, outputs: &mut Vec, cmd: &mut CmdRegister| -> Result<(), String> { if stack.len() < count { return Err("stack underflow".into()); } 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 emit_with_cycling = |cmd: &CmdRegister, emit_idx: usize, delta_secs: f64, outputs: &mut Vec| -> Result, 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(_) = 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 => { let v = stack.last().ok_or("stack underflow")?.clone(); stack.push(v); } Op::Dupn => { let n = stack.pop().ok_or("stack underflow")?.as_int()?; let v = stack.pop().ok_or("stack underflow")?; for _ in 0..n { stack.push(v.clone()); } } Op::Drop => { stack.pop().ok_or("stack underflow")?; } Op::Swap => { let len = stack.len(); if len < 2 { return Err("stack underflow".into()); } stack.swap(len - 1, len - 2); } Op::Over => { let len = stack.len(); if len < 2 { return Err("stack underflow".into()); } let v = stack[len - 2].clone(); stack.push(v); } Op::Rot => { let len = stack.len(); if len < 3 { return Err("stack underflow".into()); } let v = stack.remove(len - 3); stack.push(v); } Op::Nip => { let len = stack.len(); if len < 2 { return Err("stack underflow".into()); } stack.remove(len - 2); } Op::Tuck => { let len = stack.len(); if len < 2 { return Err("stack underflow".into()); } let v = stack[len - 1].clone(); stack.insert(len - 2, v); } Op::Dup2 => { let len = stack.len(); if len < 2 { return Err("stack underflow".into()); } let a = stack[len - 2].clone(); let b = stack[len - 1].clone(); stack.push(a); stack.push(b); } Op::Drop2 => { let len = stack.len(); if len < 2 { return Err("stack underflow".into()); } stack.pop(); stack.pop(); } Op::Swap2 => { let len = stack.len(); if len < 4 { return Err("stack underflow".into()); } stack.swap(len - 4, len - 2); stack.swap(len - 3, len - 1); } Op::Over2 => { let len = stack.len(); if len < 4 { return Err("stack underflow".into()); } let a = stack[len - 4].clone(); let b = stack[len - 3].clone(); stack.push(a); stack.push(b); } Op::Rev => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count > stack.len() { return Err("stack underflow".into()); } let start = stack.len() - count; stack[start..].reverse(); } Op::Shuffle => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count > stack.len() { return Err("stack underflow".into()); } 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 = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count > stack.len() { return Err("stack underflow".into()); } 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 = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count > stack.len() { return Err("stack underflow".into()); } 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 = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count > stack.len() { return Err("stack underflow".into()); } 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 = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count > stack.len() { return Err("stack underflow".into()); } 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 = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; 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 = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; 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 = stack.pop().ok_or("stack underflow")?; stack.push(lift_unary(v, |x| -x)?); } Op::Abs => { let v = stack.pop().ok_or("stack underflow")?; stack.push(lift_unary(v, |x| x.abs())?); } Op::Floor => { let v = stack.pop().ok_or("stack underflow")?; stack.push(lift_unary(v, |x| x.floor())?); } Op::Ceil => { let v = stack.pop().ok_or("stack underflow")?; stack.push(lift_unary(v, |x| x.ceil())?); } Op::Round => { let v = stack.pop().ok_or("stack underflow")?; 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 = stack.pop().ok_or("stack underflow")?; stack.push(lift_unary(v, |x| x.sqrt())?); } Op::Sin => { let v = stack.pop().ok_or("stack underflow")?; stack.push(lift_unary(v, |x| x.sin())?); } Op::Cos => { let v = stack.pop().ok_or("stack underflow")?; stack.push(lift_unary(v, |x| x.cos())?); } Op::Log => { let v = stack.pop().ok_or("stack underflow")?; 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 = stack.pop().ok_or("stack underflow")?.is_truthy(); let a = stack.pop().ok_or("stack underflow")?.is_truthy(); stack.push(Value::Int(if a && b { 1 } else { 0 }, None)); } Op::Or => { let b = stack.pop().ok_or("stack underflow")?.is_truthy(); let a = stack.pop().ok_or("stack underflow")?.is_truthy(); stack.push(Value::Int(if a || b { 1 } else { 0 }, None)); } Op::Not => { let v = stack.pop().ok_or("stack underflow")?.is_truthy(); stack.push(Value::Int(if v { 0 } else { 1 }, None)); } Op::Xor => { let b = stack.pop().ok_or("stack underflow")?.is_truthy(); let a = stack.pop().ok_or("stack underflow")?.is_truthy(); stack.push(Value::Int(if a ^ b { 1 } else { 0 }, None)); } Op::Nand => { let b = stack.pop().ok_or("stack underflow")?.is_truthy(); let a = stack.pop().ok_or("stack underflow")?.is_truthy(); stack.push(Value::Int(if !(a && b) { 1 } else { 0 }, None)); } Op::Nor => { let b = stack.pop().ok_or("stack underflow")?.is_truthy(); let a = stack.pop().ok_or("stack underflow")?.is_truthy(); stack.push(Value::Int(if !(a || b) { 1 } else { 0 }, None)); } Op::BranchIfZero(offset, then_span, else_span) => { let v = stack.pop().ok_or("stack underflow")?; 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 => { if stack.is_empty() { return Err("stack underflow".into()); } 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) => { if stack.is_empty() { return Err("stack underflow".into()); } 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 => { 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 = stack.pop().ok_or("stack underflow")?; 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 = stack.pop().ok_or("stack underflow")?; let name = name.as_str()?.to_string(); let val = stack.pop().ok_or("stack underflow")?; 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), #[cfg(feature = "desktop")] "mx" => Value::Float(ctx.mouse_x, None), #[cfg(feature = "desktop")] "my" => Value::Float(ctx.mouse_y, None), #[cfg(feature = "desktop")] "mdown" => Value::Float(ctx.mouse_down, None), _ => Value::Int(0, None), }; stack.push(val); } Op::Rand => { let b = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; 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); 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) }; stack.push(Value::Float(val, None)); } } } Op::ExpRand => { let hi = stack.pop().ok_or("stack underflow")?.as_float()?; let lo = stack.pop().ok_or("stack underflow")?.as_float()?; 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); stack.push(Value::Float(val, None)); } Op::LogRand => { let hi = stack.pop().ok_or("stack underflow")?.as_float()?; let lo = stack.pop().ok_or("stack underflow")?.as_float()?; 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); stack.push(Value::Float(val, None)); } Op::Seed => { let s = stack.pop().ok_or("stack underflow")?.as_int()?; *self.rng.lock() = StdRng::seed_from_u64(s as u64); } Op::Cycle | Op::PCycle => { let count = stack.pop().ok_or("stack underflow")?.as_int()? 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; drain_select_run(count, idx, stack, outputs, cmd)?; } Op::Choose => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("choose count must be > 0".into()); } let idx = self.rng.lock().gen_range(0..count); drain_select_run(count, idx, stack, outputs, cmd)?; } Op::Bounce => { let count = stack.pop().ok_or("stack underflow")?.as_int()? 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 } }; drain_select_run(count, idx, stack, outputs, cmd)?; } Op::WChoose => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("wchoose count must be > 0".into()); } let pairs_needed = count * 2; if stack.len() < pairs_needed { return Err("stack underflow".into()); } 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::() * 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); select_and_run(selected, stack, outputs, cmd)?; } Op::ChanceExec | Op::ProbExec => { let threshold = stack.pop().ok_or("stack underflow")?.as_float()?; let quot = stack.pop().ok_or("stack underflow")?; let val: f64 = self.rng.lock().gen(); let limit = match &ops[pc] { Op::ChanceExec => threshold, _ => threshold / 100.0, }; if val < limit { run_quotation(quot, stack, outputs, cmd)?; } } Op::Coin => { let val: f64 = self.rng.lock().gen(); stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }, None)); } Op::Every => { let n = stack.pop().ok_or("stack underflow")?.as_int()?; if n <= 0 { return Err("every count must be > 0".into()); } let result = ctx.iter as i64 % n == 0; stack.push(Value::Int(if result { 1 } else { 0 }, None)); } Op::Quotation(quote_ops, body_span) => { stack.push(Value::Quotation(quote_ops.clone(), *body_span)); } Op::When | Op::Unless => { let cond = stack.pop().ok_or("stack underflow")?; let quot = stack.pop().ok_or("stack underflow")?; 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 = stack.pop().ok_or("stack underflow")?; let false_quot = stack.pop().ok_or("stack underflow")?; let true_quot = stack.pop().ok_or("stack underflow")?; let quot = if cond.is_truthy() { true_quot } else { false_quot }; run_quotation(quot, stack, outputs, cmd)?; } Op::Pick => { let idx_i = stack.pop().ok_or("stack underflow")?.as_int()?; 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 = 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 = stack.pop().ok_or("stack underflow")?.as_float()?; let freq = 440.0 * 2.0_f64.powf((note - 69.0) / 12.0); stack.push(Value::Float(freq, None)); } Op::Ftom => { let freq = stack.pop().ok_or("stack underflow")?.as_float()?; 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 = stack.pop().ok_or("stack underflow")?; 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 = stack.pop().ok_or("stack underflow")?.as_int()?; for &interval in *intervals { stack.push(Value::Int(root + interval, None)); } } Op::Oct => { let shift = stack.pop().ok_or("stack underflow")?; let note = stack.pop().ok_or("stack underflow")?; let result = lift_binary(note, shift, |n, s| n + s * 12.0)?; stack.push(result); } Op::SetTempo => { let tempo = stack.pop().ok_or("stack underflow")?.as_float()?; 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 = stack.pop().ok_or("stack underflow")?.as_float()?; 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::Chain => { let pattern = stack.pop().ok_or("stack underflow")?.as_int()? - 1; let bank = stack.pop().ok_or("stack underflow")?.as_int()? - 1; if bank < 0 || pattern < 0 { return Err("chain: bank and pattern must be >= 1".into()); } if bank as usize == ctx.bank && pattern as usize == ctx.pattern { // chaining to self is a no-op } else { use std::fmt::Write; let mut val = String::with_capacity(8); let _ = write!(&mut val, "{bank}:{pattern}"); var_writes_cell .borrow_mut() .as_mut() .expect("var_writes taken") .insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None)); } } Op::Loop => { let beats = stack.pop().ok_or("stack underflow")?.as_float()?; 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 => { if stack.is_empty() { return Err("stack underflow".into()); } let deltas = std::mem::take(stack); cmd.set_deltas(deltas); } Op::Adsr => { let r = stack.pop().ok_or("stack underflow")?; let s = stack.pop().ok_or("stack underflow")?; let d = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; cmd.set_param("attack", a); cmd.set_param("decay", d); cmd.set_param("sustain", s); cmd.set_param("release", r); } Op::Ad => { let d = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; cmd.set_param("attack", a); cmd.set_param("decay", d); cmd.set_param("sustain", Value::Int(0, None)); } Op::Apply => { let quot = stack.pop().ok_or("stack underflow")?; run_quotation(quot, stack, outputs, cmd)?; } Op::Ramp => { let curve = stack.pop().ok_or("stack underflow")?.as_float()?; let freq = stack.pop().ok_or("stack underflow")?.as_float()?; 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 = stack.pop().ok_or("stack underflow")?.as_float()?; 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 = stack.pop().ok_or("stack underflow")?.as_float()?; let min = stack.pop().ok_or("stack underflow")?.as_float()?; let val = stack.pop().ok_or("stack underflow")?.as_float()?; stack.push(Value::Float(min + val * (max - min), None)); } Op::Perlin => { let freq = stack.pop().ok_or("stack underflow")?.as_float()?; let val = perlin_noise_1d(freq * ctx.beat); stack.push(Value::Float(val, None)); } Op::ClearCmd => { cmd.clear(); } Op::IntRange => { let end = stack.pop().ok_or("stack underflow")?.as_int()?; let start = stack.pop().ok_or("stack underflow")?.as_int()?; 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 = stack.pop().ok_or("stack underflow")?.as_float()?; let end = stack.pop().ok_or("stack underflow")?.as_float()?; let start = stack.pop().ok_or("stack underflow")?.as_float()?; 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 = stack.pop().ok_or("stack underflow")?.as_int()?; let quot = stack.pop().ok_or("stack underflow")?; 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 = stack.pop().ok_or("stack underflow")?; let count = stack.pop().ok_or("stack underflow")?.as_int()?; 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 = stack.pop().ok_or("stack underflow")?.as_int()?; let ratio = stack.pop().ok_or("stack underflow")?.as_float()?; let start = stack.pop().ok_or("stack underflow")?.as_float()?; 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 = stack.pop().ok_or("stack underflow")?.as_int()?; let k = stack.pop().ok_or("stack underflow")?.as_int()?; 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 = stack.pop().ok_or("stack underflow")?.as_int()?; let n = stack.pop().ok_or("stack underflow")?.as_int()?; let k = stack.pop().ok_or("stack underflow")?.as_int()?; 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 = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration(); let max = stack.pop().ok_or("stack underflow")?.as_float()?; let min = stack.pop().ok_or("stack underflow")?.as_float()?; 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 = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration(); let end = stack.pop().ok_or("stack underflow")?.as_float()?; let start = stack.pop().ok_or("stack underflow")?.as_float()?; 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 = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration(); let max = stack.pop().ok_or("stack underflow")?.as_float()?; let min = stack.pop().ok_or("stack underflow")?.as_float()?; let suffix = match dist { 1 => "s", 2 => "d", _ => "" }; let s = format!("{min}?{max}:{period}{suffix}"); stack.push(Value::Str(s.into(), None)); } Op::ModEnv => { if stack.is_empty() { return Err("stack underflow".into()); } 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, &[])); let get_int = |name: &str| -> Option { params .iter() .rev() .find(|(k, _)| *k == name) .and_then(|(_, v)| v.as_int().ok()) }; let get_float = |name: &str| -> Option { params .iter() .rev() .find(|(k, _)| *k == name) .and_then(|(_, v)| v.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); 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}")); } 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}")); } 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}")); } 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}")); } 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}" )); } } 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 = stack.pop().ok_or("stack underflow")?.as_int()?; let cc = stack.pop().ok_or("stack underflow")?.as_int()?; 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 = stack.pop().ok_or("stack underflow")?.as_str()?.to_string(); self.dict.lock().remove(&name); } } pc += 1; } Ok(()) } } 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" | "verbdecay" | "verbpredelay" | "chorusdelay" | "duration" ) } fn emit_output( sound: Option<&str>, params: &[(&str, String)], step_duration: f64, nudge_secs: f64, outputs: &mut Vec, ) { 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::() { 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 sound.is_some() && !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_rhythm(k: usize, n: usize, rotation: usize) -> Vec { if k == 0 || n == 0 { return Vec::new(); } if k >= n { return (0..n as i64).collect(); } let mut groups: Vec> = (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 = 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 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(val: Value, f: F) -> Result where F: Fn(f64) -> f64, { Ok(float_to_value(f(val.as_float()?))) } fn lift_unary_int(val: Value, f: F) -> Result where F: Fn(i64) -> i64, { Ok(Value::Int(f(val.as_int()?), None)) } fn lift_binary(a: Value, b: Value, f: F) -> Result where F: Fn(f64, f64) -> f64, { Ok(float_to_value(f(a.as_float()?, b.as_float()?))) } fn binary_op(stack: &mut Vec, f: F) -> Result<(), String> where F: Fn(f64, f64) -> f64 + Copy, { let b = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; stack.push(lift_binary(a, b, f)?); Ok(()) } fn cmp_op(stack: &mut Vec, f: F) -> Result<(), String> where F: Fn(f64, f64) -> bool, { let b = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; 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) if !items.is_empty() => { Cow::Owned(items[emit_idx % items.len()].clone()) } other => Cow::Borrowed(other), } }