//! 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, }; /// Forth VM instance. Holds the stack, variables, dictionary, and RNG. pub struct Forth { stack: Stack, vars: Variables, dict: Dictionary, rng: Rng, global_params: Mutex>, } impl Forth { pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self { Self { stack: Mutex::new(Vec::new()), vars, dict, rng, global_params: Mutex::new(Vec::new()), } } pub fn stack(&self) -> Vec { self.stack.lock().clone() } pub fn clear_stack(&self) { self.stack.lock().clear(); } pub fn clear_global_params(&self) { self.global_params.lock().clear(); } /// Evaluate a Forth script and return audio command strings. 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) } /// Evaluate and collect an execution trace for UI highlighting. 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) } /// Evaluate and return both outputs and pending variable writes (without applying them). 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_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, 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_full(); let mut var_writes: HashMap = HashMap::new(); cmd.set_global(self.global_params.lock().clone()); self.execute_ops( ops, ctx, &mut stack, &mut outputs, &mut cmd, trace, &vars_snapshot, &mut var_writes, )?; *self.global_params.lock() = cmd.take_global(); 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 mut marks: Vec = Vec::new(); let trace_cell = std::cell::RefCell::new(trace); let var_writes_cell = std::cell::RefCell::new(Some(var_writes)); let read_key = |vwc: &std::cell::RefCell>>, vs: &VariablesMap| -> i64 { vwc.borrow() .as_ref() .and_then(|vw| vw.get("__key__")) .or_else(|| vs.get("__key__")) .and_then(|v| v.as_int().ok()) .unwrap_or(60) }; 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> { 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 .global_params() .iter() .chain(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.global_params().iter().chain(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, arp_idx: usize, poly_idx: usize, delta_secs: f64, outputs: &mut Vec| -> Result, String> { let has_sound = cmd.sound().is_some(); let has_params = !cmd.params().is_empty(); let has_global = !cmd.global_params().is_empty(); if !has_sound && !has_params && !has_global { return Err("nothing to emit".into()); } let resolved_sound_val = cmd.sound().map(|sv| resolve_value(sv, arp_idx, poly_idx)); let sound_str = match &resolved_sound_val { Some(v) => Some(v.as_str()?.to_string()), None => None, }; let resolved_params: Vec<(&str, String)> = cmd.global_params() .iter() .chain(cmd.params().iter()) .map(|(k, v)| { let resolved = resolve_value(v, arp_idx, poly_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().expect("stack non-empty after ensure").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::Print => { let val = pop(stack)?; let text = match &val { Value::Int(n, _) => n.to_string(), Value::Float(f, _) => format!("{f}"), Value::Str(s, _) => s.to_string(), _ => format!("{val:?}"), }; outputs.push(format!("print:{text}")); } 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 = drain_skip_quotations(stack); if values.is_empty() { return Err("expected sound name".into()); } 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 = drain_skip_quotations(stack); if values.is_empty() { return Err("expected parameter value".into()); } 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 poly_count = compute_poly_count(cmd); let explicit_deltas = !cmd.deltas().is_empty(); let delta_list: Vec = 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() }; for poly_i in 0..poly_count { if let Some(sound_val) = emit_with_cycling(cmd, i, poly_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, 0, 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) | Op::PBounce(word_span) => { let count = pop_int(stack)? as usize; if count == 0 { return Err("bounce count must be > 0".into()); } let counter = match &ops[pc] { Op::Bounce(_) => ctx.runs, _ => ctx.iter, }; let idx = if count == 1 { 0 } else { let period = 2 * (count - 1); let raw = counter % 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::() * 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::Except(word_span) => { let n = pop_int(stack)?; let quot = pop(stack)?; if n <= 0 { return Err("except 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::EveryOffset(word_span) => { let offset = pop_int(stack)?; 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 == offset.rem_euclid(n); record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result)); if result { run_quotation(quot, stack, outputs, cmd)?; } } Op::ExceptOffset(word_span) => { let offset = pop_int(stack)?; let n = pop_int(stack)?; let quot = pop(stack)?; if n <= 0 { return Err("except+ count must be > 0".into()); } let result = ctx.iter as i64 % n != offset.rem_euclid(n); 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 = 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()); } ensure(stack, 1)?; let len = pattern.len() as i64; let key = read_key(&var_writes_cell, vars_snapshot); let values = std::mem::take(stack); for val in values { let result = lift_unary_int(val, |degree| { let octave_offset = degree.div_euclid(len); let idx = degree.rem_euclid(len) as usize; key + 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::Transpose => { let n = pop_int(stack)?; for val in stack.iter_mut() { if let Value::Int(v, _) = val { *v += n; } } } Op::SetKey => { let key = pop_int(stack)?; var_writes_cell .borrow_mut() .as_mut() .expect("var_writes taken") .insert("__key__".to_string(), Value::Int(key, None)); } Op::Invert => { ensure(stack, 2)?; let start = stack.iter().rposition(|v| !matches!(v, Value::Int(..))).map_or(0, |i| i + 1); let bottom = stack[start].as_int()? + 12; stack.remove(start); stack.push(Value::Int(bottom, None)); } Op::DownInvert => { ensure(stack, 2)?; let top = pop_int(stack)? - 12; let start = stack.iter().rposition(|v| !matches!(v, Value::Int(..))).map_or(0, |i| i + 1); stack.insert(start, Value::Int(top, None)); } Op::VoiceDrop2 => { ensure(stack, 3)?; let len = stack.len(); let note = stack[len - 2].as_int()? - 12; stack.remove(len - 2); let start = stack.iter().rposition(|v| !matches!(v, Value::Int(..))).map_or(0, |i| i + 1); stack.insert(start, Value::Int(note, None)); } Op::VoiceDrop3 => { ensure(stack, 4)?; let len = stack.len(); let note = stack[len - 3].as_int()? - 12; stack.remove(len - 3); let start = stack.iter().rposition(|v| !matches!(v, Value::Int(..))).map_or(0, |i| i + 1); stack.insert(start, Value::Int(note, None)); } Op::DiatonicTriad(pattern) => { if pattern.is_empty() { return Err("empty scale pattern".into()); } let degree = pop_int(stack)?; let key = read_key(&var_writes_cell, vars_snapshot); let len = pattern.len() as i64; for offset in [0, 2, 4] { let d = degree + offset; let octave_offset = d.div_euclid(len); let idx = d.rem_euclid(len) as usize; stack.push(Value::Int(key + octave_offset * 12 + pattern[idx], None)); } } Op::DiatonicSeventh(pattern) => { if pattern.is_empty() { return Err("empty scale pattern".into()); } let degree = pop_int(stack)?; let key = read_key(&var_writes_cell, vars_snapshot); let len = pattern.len() as i64; for offset in [0, 2, 4, 6] { let d = degree + offset; let octave_offset = d.div_euclid(len); let idx = d.rem_euclid(len) as usize; stack.push(Value::Int(key + octave_offset * 12 + pattern[idx], 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::LinMap => { let out_hi = pop_float(stack)?; let out_lo = pop_float(stack)?; let in_hi = pop_float(stack)?; let in_lo = pop_float(stack)?; let val = pop_float(stack)?; let t = if (in_hi - in_lo).abs() < f64::EPSILON { 0.0 } else { (val - in_lo) / (in_hi - in_lo) }; stack.push(Value::Float(out_lo + t * (out_hi - out_lo), None)); } Op::ExpMap => { let hi = pop_float(stack)?; let lo = pop_float(stack)?; let val = pop_float(stack)?; if lo <= 0.0 || hi <= 0.0 { return Err("expmap requires positive bounds".into()); } stack.push(Value::Float(lo * (hi / lo).powf(val), 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::EmitAll => { // Retroactive: patch existing sound outputs with current params if !cmd.params().is_empty() { let step_duration = ctx.step_duration(); for output in outputs.iter_mut() { if output.starts_with("/sound/") { use std::fmt::Write; for (k, v) in cmd.params() { let val_str = v.to_param_string(); if !output.ends_with('/') { output.push('/'); } if is_tempo_scaled_param(k) { if let Ok(val) = val_str.parse::() { let _ = write!(output, "{k}/{}", val * step_duration); continue; } } let _ = write!(output, "{k}/{val_str}"); } } } } // Prospective: store for future emits cmd.commit_global(); } Op::ClearGlobal => { cmd.clear_global(); } 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: (arp_idx, poly_idx, delta_secs) let schedule: Vec<(usize, usize, f64)> = if has_arp_list(cmd) { let arp_count = compute_arp_count(cmd); let poly_count = compute_poly_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 }; let mut sched = Vec::with_capacity(count * poly_count); for i in 0..count { 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() }; for poly_i in 0..poly_count { sched.push((i, poly_i, delta_secs)); } } sched } else { let poly_count = compute_poly_count(cmd); let deltas: Vec = 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(( 0, poly_idx, ctx.nudge_secs + frac * ctx.step_duration(), )); } } sched }; for (arp_idx, poly_idx, delta_secs) in schedule { let get_int = |name: &str| -> Option { params .iter() .rev() .find(|(k, _)| *k == name) .and_then(|(_, v)| { resolve_value(v, arp_idx, poly_idx).as_int().ok() }) }; let get_float = |name: &str| -> Option { params .iter() .rev() .find(|(k, _)| *k == name) .and_then(|(_, v)| { resolve_value(v, arp_idx, poly_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::Mark => { marks.push(stack.len()); } Op::Count(span) => { let mark = marks.pop().ok_or("count without mark")?; stack.push(Value::Int((stack.len() - mark) as i64, *span)); } Op::Index(word_span) => { let idx = pop_int(stack)?; let count = pop_int(stack)? as usize; if count == 0 { return Err("index count must be > 0".into()); } let resolved_idx = ((idx % count as i64 + count as i64) % count as i64) as usize; if let Some(span) = word_span { if stack.len() >= count { let start = stack.len() - count; let selected = &stack[start + resolved_idx]; record_resolved_from_value(&trace_cell, Some(*span), selected); } } drain_select_run(count, resolved_idx, stack, outputs, cmd)?; } Op::Rec => { let name = pop(stack)?; outputs.push(format!("/doux/rec/sound/{}", name.as_str()?)); } Op::Overdub => { let name = pop(stack)?; outputs.push(format!("/doux/rec/sound/{}/overdub/1", name.as_str()?)); } Op::Orec => { let orbit = pop(stack)?.as_int()?; let name = pop(stack)?; outputs.push(format!("/doux/rec/sound/{}/orbit/{}", name.as_str()?, orbit)); } Op::Odub => { let orbit = pop(stack)?.as_int()?; let name = pop(stack)?; outputs.push(format!("/doux/rec/sound/{}/overdub/1/orbit/{}", name.as_str()?, orbit)); } Op::Forget => { let name = pop(stack)?; self.dict.lock().remove(name.as_str()?); } } pc += 1; } Ok(()) } } fn record_resolved( trace_cell: &std::cell::RefCell>, span: Option, 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>, span: Option, 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, ) { 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 !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 { 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().expect("ones sufficient for min_count"); one.extend(zeros.pop().expect("zeros sufficient for min_count")); 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 pop(stack: &mut Vec) -> Result { stack.pop().ok_or_else(|| "stack underflow".to_string()) } fn pop_int(stack: &mut Vec) -> Result { pop(stack)?.as_int() } fn pop_float(stack: &mut Vec) -> Result { pop(stack)?.as_float() } fn pop_bool(stack: &mut Vec) -> Result { Ok(pop(stack)?.is_truthy()) } /// Drain the stack, returning non-quotation values. /// Quotations are pushed back onto the stack (transparent). fn drain_skip_quotations(stack: &mut Vec) -> Vec { let values = std::mem::take(stack); let mut result = Vec::new(); for v in values { if matches!(v, Value::Quotation(..)) { stack.push(v); } else { result.push(v); } } result } 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(val: Value, f: F) -> Result where F: Fn(f64) -> f64 + Copy, { match val { Value::ArpList(items) => { let mapped: Result, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect(); Ok(Value::ArpList(Arc::from(mapped?))) } Value::CycleList(items) => { let mapped: Result, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect(); Ok(Value::CycleList(Arc::from(mapped?))) } v => Ok(float_to_value(f(v.as_float()?))), } } fn lift_unary_int(val: Value, f: F) -> Result where F: Fn(i64) -> i64 + Copy, { match val { Value::ArpList(items) => { let mapped: Result, _> = items.iter().map(|x| lift_unary_int(x.clone(), f)).collect(); Ok(Value::ArpList(Arc::from(mapped?))) } Value::CycleList(items) => { let mapped: Result, _> = items.iter().map(|x| lift_unary_int(x.clone(), f)).collect(); Ok(Value::CycleList(Arc::from(mapped?))) } v => Ok(Value::Int(f(v.as_int()?), None)), } } fn lift_binary(a: Value, b: Value, f: F) -> Result where F: Fn(f64, f64) -> f64 + Copy, { match (a, b) { (Value::ArpList(items), b) => { let mapped: Result, _> = items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect(); Ok(Value::ArpList(Arc::from(mapped?))) } (a, Value::ArpList(items)) => { let mapped: Result, _> = items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect(); Ok(Value::ArpList(Arc::from(mapped?))) } (Value::CycleList(items), b) => { let mapped: Result, _> = items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect(); Ok(Value::CycleList(Arc::from(mapped?))) } (a, Value::CycleList(items)) => { let mapped: Result, _> = items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect(); Ok(Value::CycleList(Arc::from(mapped?))) } (a, b) => 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 = pop(stack)?; let a = pop(stack)?; 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 = 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_value(val: &Value, arp_idx: usize, poly_idx: usize) -> Cow<'_, Value> { match val { Value::ArpList(items) if !items.is_empty() => { Cow::Owned(items[arp_idx % items.len()].clone()) } Value::CycleList(items) if !items.is_empty() => { Cow::Owned(items[poly_idx % items.len()].clone()) } other => Cow::Borrowed(other), } }