From 1433e07066ab5bd3c83ddf5cde6725b80c8149d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 23 Jan 2026 19:36:40 +0100 Subject: [PATCH] Break down forth implementation properly --- src/model/forth.rs | 2944 ----------------------------------- src/model/forth/compiler.rs | 282 ++++ src/model/forth/mod.rs | 11 + src/model/forth/ops.rs | 79 + src/model/forth/types.rs | 141 ++ src/model/forth/vm.rs | 920 +++++++++++ src/model/forth/words.rs | 1631 +++++++++++++++++++ tests/forth.rs | 3 + tests/forth/iteration.rs | 16 +- tests/forth/list_words.rs | 134 ++ tests/forth/temporal.rs | 72 +- 11 files changed, 3246 insertions(+), 2987 deletions(-) delete mode 100644 src/model/forth.rs create mode 100644 src/model/forth/compiler.rs create mode 100644 src/model/forth/mod.rs create mode 100644 src/model/forth/ops.rs create mode 100644 src/model/forth/types.rs create mode 100644 src/model/forth/vm.rs create mode 100644 src/model/forth/words.rs create mode 100644 tests/forth/list_words.rs diff --git a/src/model/forth.rs b/src/model/forth.rs deleted file mode 100644 index 7e04289..0000000 --- a/src/model/forth.rs +++ /dev/null @@ -1,2944 +0,0 @@ -use rand::rngs::StdRng; -use rand::{Rng as RngTrait, SeedableRng}; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct SourceSpan { - pub start: usize, - pub end: usize, -} - -#[derive(Clone, Debug, Default)] -pub struct ExecutionTrace { - pub executed_spans: Vec, - pub selected_spans: Vec, -} - -pub struct StepContext { - pub step: usize, - pub beat: f64, - pub pattern: usize, - pub tempo: f64, - pub phase: f64, - pub slot: usize, - pub runs: usize, - pub iter: usize, - pub speed: f64, - pub fill: bool, -} - -impl StepContext { - pub fn step_duration(&self) -> f64 { - 60.0 / self.tempo / 4.0 / self.speed - } -} - -pub type Variables = Arc>>; -pub type Dictionary = Arc>>>; -pub type Rng = Arc>; - -#[derive(Clone, Debug)] -pub enum Value { - Int(i64, Option), - Float(f64, Option), - Str(String, Option), - Marker, - Quotation(Vec, Option), -} - -impl PartialEq for Value { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Value::Int(a, _), Value::Int(b, _)) => a == b, - (Value::Float(a, _), Value::Float(b, _)) => a == b, - (Value::Str(a, _), Value::Str(b, _)) => a == b, - (Value::Marker, Value::Marker) => true, - (Value::Quotation(a, _), Value::Quotation(b, _)) => a == b, - _ => false, - } - } -} - -#[derive(Clone, Debug, Default)] -struct CmdRegister { - sound: Option, - params: Vec<(String, String)>, -} - -impl CmdRegister { - fn set_sound(&mut self, name: String) { - self.sound = Some(name); - } - - fn set_param(&mut self, key: String, value: String) { - self.params.push((key, value)); - } - - fn take(&mut self) -> Option<(String, Vec<(String, String)>)> { - let sound = self.sound.take()?; - let params = std::mem::take(&mut self.params); - Some((sound, params)) - } -} - -impl Value { - pub fn as_float(&self) -> Result { - match self { - Value::Float(f, _) => Ok(*f), - Value::Int(i, _) => Ok(*i as f64), - _ => Err("expected number".into()), - } - } - - fn as_int(&self) -> Result { - match self { - Value::Int(i, _) => Ok(*i), - Value::Float(f, _) => Ok(*f as i64), - _ => Err("expected number".into()), - } - } - - fn as_str(&self) -> Result<&str, String> { - match self { - Value::Str(s, _) => Ok(s), - _ => Err("expected string".into()), - } - } - - fn is_truthy(&self) -> bool { - match self { - Value::Int(i, _) => *i != 0, - Value::Float(f, _) => *f != 0.0, - Value::Str(s, _) => !s.is_empty(), - Value::Marker => false, - Value::Quotation(..) => true, - } - } - - fn is_marker(&self) -> bool { - matches!(self, Value::Marker) - } - - fn to_param_string(&self) -> String { - match self { - Value::Int(i, _) => i.to_string(), - Value::Float(f, _) => f.to_string(), - Value::Str(s, _) => s.clone(), - Value::Marker => String::new(), - Value::Quotation(..) => String::new(), - } - } - - fn span(&self) -> Option { - match self { - Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s, - Value::Marker | Value::Quotation(..) => None, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Op { - PushInt(i64, Option), - PushFloat(f64, Option), - PushStr(String, Option), - Dup, - Dupn, - Drop, - Swap, - Over, - Rot, - Nip, - Tuck, - Add, - Sub, - Mul, - Div, - Mod, - Neg, - Abs, - Floor, - Ceil, - Round, - Min, - Max, - Eq, - Ne, - Lt, - Gt, - Le, - Ge, - And, - Or, - Not, - BranchIfZero(usize, Option, Option), - Branch(usize), - NewCmd, - SetParam(String), - Emit, - Get, - Set, - GetContext(String), - Rand, - Rrand, - Seed, - Cycle, - Choose, - ChanceExec, - ProbExec, - Coin, - Mtof, - Ftom, - ListStart, - ListEnd, - ListEndCycle, - PCycle, - ListEndPCycle, - At, - Window, - Scale, - Pop, - Subdivide, - SetTempo, - Each, - Every, - Quotation(Vec, Option), - When, - Unless, - Adsr, - Ad, - Stack, - For, - LocalCycleEnd, - Echo, - Necho, - Apply, -} - -pub enum WordCompile { - Simple, - Context(&'static str), - Param, - Alias(&'static str), - Probability(f64), -} - -pub struct Word { - pub name: &'static str, - pub stack: &'static str, - pub desc: &'static str, - pub example: &'static str, - pub compile: WordCompile, -} - -use WordCompile::*; - -pub const WORDS: &[Word] = &[ - // Stack manipulation - Word { - name: "dup", - stack: "(a -- a a)", - desc: "Duplicate top of stack", - example: "3 dup => 3 3", - compile: Simple, - }, - Word { - name: "dupn", - stack: "(a n -- a a ... a)", - desc: "Duplicate a onto stack n times", - example: "2 4 dupn => 2 2 2 2", - compile: Simple, - }, - Word { - name: "!", - stack: "(a n -- a a ... a)", - desc: "Duplicate a onto stack n times (alias for dupn)", - example: "2 4 ! => 2 2 2 2", - compile: Alias("dupn"), - }, - Word { - name: "drop", - stack: "(a --)", - desc: "Remove top of stack", - example: "1 2 drop => 1", - compile: Simple, - }, - Word { - name: "swap", - stack: "(a b -- b a)", - desc: "Exchange top two items", - example: "1 2 swap => 2 1", - compile: Simple, - }, - Word { - name: "over", - stack: "(a b -- a b a)", - desc: "Copy second to top", - example: "1 2 over => 1 2 1", - compile: Simple, - }, - Word { - name: "rot", - stack: "(a b c -- b c a)", - desc: "Rotate top three", - example: "1 2 3 rot => 2 3 1", - compile: Simple, - }, - Word { - name: "nip", - stack: "(a b -- b)", - desc: "Remove second item", - example: "1 2 nip => 2", - compile: Simple, - }, - Word { - name: "tuck", - stack: "(a b -- b a b)", - desc: "Copy top under second", - example: "1 2 tuck => 2 1 2", - compile: Simple, - }, - // Arithmetic - Word { - name: "+", - stack: "(a b -- a+b)", - desc: "Add", - example: "2 3 + => 5", - compile: Simple, - }, - Word { - name: "-", - stack: "(a b -- a-b)", - desc: "Subtract", - example: "5 3 - => 2", - compile: Simple, - }, - Word { - name: "*", - stack: "(a b -- a*b)", - desc: "Multiply", - example: "3 4 * => 12", - compile: Simple, - }, - Word { - name: "/", - stack: "(a b -- a/b)", - desc: "Divide", - example: "10 2 / => 5", - compile: Simple, - }, - Word { - name: "mod", - stack: "(a b -- a%b)", - desc: "Modulo", - example: "7 3 mod => 1", - compile: Simple, - }, - Word { - name: "neg", - stack: "(a -- -a)", - desc: "Negate", - example: "5 neg => -5", - compile: Simple, - }, - Word { - name: "abs", - stack: "(a -- |a|)", - desc: "Absolute value", - example: "-5 abs => 5", - compile: Simple, - }, - Word { - name: "floor", - stack: "(f -- n)", - desc: "Round down to integer", - example: "3.7 floor => 3", - compile: Simple, - }, - Word { - name: "ceil", - stack: "(f -- n)", - desc: "Round up to integer", - example: "3.2 ceil => 4", - compile: Simple, - }, - Word { - name: "round", - stack: "(f -- n)", - desc: "Round to nearest integer", - example: "3.5 round => 4", - compile: Simple, - }, - Word { - name: "min", - stack: "(a b -- min)", - desc: "Minimum of two values", - example: "3 5 min => 3", - compile: Simple, - }, - Word { - name: "max", - stack: "(a b -- max)", - desc: "Maximum of two values", - example: "3 5 max => 5", - compile: Simple, - }, - // Comparison - Word { - name: "=", - stack: "(a b -- bool)", - desc: "Equal", - example: "3 3 = => 1", - compile: Simple, - }, - Word { - name: "<>", - stack: "(a b -- bool)", - desc: "Not equal", - example: "3 4 <> => 1", - compile: Simple, - }, - Word { - name: "lt", - stack: "(a b -- bool)", - desc: "Less than", - example: "2 3 lt => 1", - compile: Simple, - }, - Word { - name: "gt", - stack: "(a b -- bool)", - desc: "Greater than", - example: "3 2 gt => 1", - compile: Simple, - }, - Word { - name: "<=", - stack: "(a b -- bool)", - desc: "Less or equal", - example: "3 3 <= => 1", - compile: Simple, - }, - Word { - name: ">=", - stack: "(a b -- bool)", - desc: "Greater or equal", - example: "3 3 >= => 1", - compile: Simple, - }, - // Logic - Word { - name: "and", - stack: "(a b -- bool)", - desc: "Logical and", - example: "1 1 and => 1", - compile: Simple, - }, - Word { - name: "or", - stack: "(a b -- bool)", - desc: "Logical or", - example: "0 1 or => 1", - compile: Simple, - }, - Word { - name: "not", - stack: "(a -- bool)", - desc: "Logical not", - example: "0 not => 1", - compile: Simple, - }, - // Sound - Word { - name: "sound", - stack: "(name --)", - desc: "Begin sound command", - example: "\"kick\" sound", - compile: Simple, - }, - Word { - name: "s", - stack: "(name --)", - desc: "Alias for sound", - example: "\"kick\" s", - compile: Alias("sound"), - }, - Word { - name: "emit", - stack: "(--)", - desc: "Output current sound", - example: "\"kick\" s emit", - compile: Simple, - }, - Word { - name: "@", - stack: "(--)", - desc: "Alias for emit", - example: "\"kick\" s 0.5 at @ pop", - compile: Alias("emit"), - }, - // Variables (prefix syntax: @name to fetch, !name to store) - Word { - name: "@", - stack: "( -- val)", - desc: "Fetch variable value", - example: "@freq => 440", - compile: Simple, - }, - Word { - name: "!", - stack: "(val --)", - desc: "Store value in variable", - example: "440 !freq", - compile: Simple, - }, - // Randomness - Word { - name: "rand", - stack: "(min max -- f)", - desc: "Random float in range", - example: "0 1 rand => 0.42", - compile: Simple, - }, - Word { - name: "rrand", - stack: "(min max -- n)", - desc: "Random int in range", - example: "1 6 rrand => 4", - compile: Simple, - }, - Word { - name: "seed", - stack: "(n --)", - desc: "Set random seed", - example: "12345 seed", - compile: Simple, - }, - Word { - name: "coin", - stack: "(-- bool)", - desc: "50/50 random boolean", - example: "coin => 0 or 1", - compile: Simple, - }, - Word { - name: "chance", - stack: "(quot prob --)", - desc: "Execute quotation with probability (0.0-1.0)", - example: "{ 2 distort } 0.75 chance", - compile: Simple, - }, - Word { - name: "prob", - stack: "(quot pct --)", - desc: "Execute quotation with probability (0-100)", - example: "{ 2 distort } 75 prob", - compile: Simple, - }, - Word { - name: "choose", - stack: "(..n n -- val)", - desc: "Random pick from n items", - example: "1 2 3 3 choose", - compile: Simple, - }, - Word { - name: "cycle", - stack: "(..n n -- val)", - desc: "Cycle through n items by step", - example: "1 2 3 3 cycle", - compile: Simple, - }, - Word { - name: "pcycle", - stack: "(..n n -- val)", - desc: "Cycle through n items by pattern", - example: "1 2 3 3 pcycle", - compile: Simple, - }, - Word { - name: "every", - stack: "(n -- bool)", - desc: "True every nth iteration", - example: "4 every", - compile: Simple, - }, - // Probability shortcuts - Word { - name: "always", - stack: "(quot --)", - desc: "Always execute quotation", - example: "{ 2 distort } always", - compile: Probability(1.0), - }, - Word { - name: "never", - stack: "(quot --)", - desc: "Never execute quotation", - example: "{ 2 distort } never", - compile: Probability(0.0), - }, - Word { - name: "often", - stack: "(quot --)", - desc: "Execute quotation 75% of the time", - example: "{ 2 distort } often", - compile: Probability(0.75), - }, - Word { - name: "sometimes", - stack: "(quot --)", - desc: "Execute quotation 50% of the time", - example: "{ 2 distort } sometimes", - compile: Probability(0.5), - }, - Word { - name: "rarely", - stack: "(quot --)", - desc: "Execute quotation 25% of the time", - example: "{ 2 distort } rarely", - compile: Probability(0.25), - }, - Word { - name: "almostNever", - stack: "(quot --)", - desc: "Execute quotation 10% of the time", - example: "{ 2 distort } almostNever", - compile: Probability(0.1), - }, - Word { - name: "almostAlways", - stack: "(quot --)", - desc: "Execute quotation 90% of the time", - example: "{ 2 distort } almostAlways", - compile: Probability(0.9), - }, - // Context - Word { - name: "step", - stack: "(-- n)", - desc: "Current step index", - example: "step => 0", - compile: Context("step"), - }, - Word { - name: "beat", - stack: "(-- f)", - desc: "Current beat position", - example: "beat => 4.5", - compile: Context("beat"), - }, - Word { - name: "bank", - stack: "(str --)", - desc: "Set sample bank suffix", - example: "\"a\" bank", - compile: Param, - }, - Word { - name: "pattern", - stack: "(-- n)", - desc: "Current pattern index", - example: "pattern => 0", - compile: Context("pattern"), - }, - Word { - name: "tempo", - stack: "(-- f)", - desc: "Current BPM", - example: "tempo => 120.0", - compile: Context("tempo"), - }, - Word { - name: "phase", - stack: "(-- f)", - desc: "Phase in bar (0-1)", - example: "phase => 0.25", - compile: Context("phase"), - }, - Word { - name: "slot", - stack: "(-- n)", - desc: "Current slot number", - example: "slot => 0", - compile: Context("slot"), - }, - Word { - name: "runs", - stack: "(-- n)", - desc: "Times this step ran", - example: "runs => 3", - compile: Context("runs"), - }, - Word { - name: "iter", - stack: "(-- n)", - desc: "Pattern iteration count", - example: "iter => 2", - compile: Context("iter"), - }, - Word { - name: "stepdur", - stack: "(-- f)", - desc: "Step duration in seconds", - example: "stepdur => 0.125", - compile: Context("stepdur"), - }, - // Live keys - Word { - name: "fill", - stack: "(-- bool)", - desc: "True when fill is on (f key)", - example: "{ 4 div each } fill ?", - compile: Context("fill"), - }, - // Music - Word { - name: "mtof", - stack: "(midi -- hz)", - desc: "MIDI note to frequency", - example: "69 mtof => 440.0", - compile: Simple, - }, - Word { - name: "ftom", - stack: "(hz -- midi)", - desc: "Frequency to MIDI note", - example: "440 ftom => 69.0", - compile: Simple, - }, - // Time - Word { - name: "at", - stack: "(pos --)", - desc: "Position in time (push context)", - example: "\"kick\" s 0.5 at emit pop", - compile: Simple, - }, - Word { - name: "zoom", - stack: "(start end --)", - desc: "Zoom into time region", - example: "0.0 0.5 zoom", - compile: Simple, - }, - Word { - name: "scale!", - stack: "(factor --)", - desc: "Scale time context duration", - example: "2 scale!", - compile: Simple, - }, - Word { - name: "pop", - stack: "(--)", - desc: "Pop time context", - example: "pop", - compile: Simple, - }, - Word { - name: "div", - stack: "(n --)", - desc: "Subdivide time into n", - example: "4 div", - compile: Simple, - }, - Word { - name: "each", - stack: "(--)", - desc: "Emit at each subdivision", - example: "4 div each", - compile: Simple, - }, - Word { - name: "stack", - stack: "(n --)", - desc: "Create n subdivisions at same time", - example: "3 stack", - compile: Simple, - }, - Word { - name: "echo", - stack: "(n --)", - desc: "Create n subdivisions with halving durations (stutter)", - example: "3 echo", - compile: Simple, - }, - Word { - name: "necho", - stack: "(n --)", - desc: "Create n subdivisions with doubling durations (swell)", - example: "3 necho", - compile: Simple, - }, - Word { - name: "for", - stack: "(quot --)", - desc: "Execute quotation for each subdivision", - example: "{ emit } 3 div for", - compile: Simple, - }, - Word { - name: "|", - stack: "(-- marker)", - desc: "Start local cycle list", - example: "| 60 62 64 |", - compile: Simple, - }, - Word { - name: "tempo!", - stack: "(bpm --)", - desc: "Set global tempo", - example: "140 tempo!", - compile: Simple, - }, - // Lists - Word { - name: "[", - stack: "(-- marker)", - desc: "Start list", - example: "[ 1 2 3 ]", - compile: Simple, - }, - Word { - name: "]", - stack: "(marker..n -- n)", - desc: "End list, push count", - example: "[ 1 2 3 ] => 3", - compile: Simple, - }, - Word { - name: "<", - stack: "(-- marker)", - desc: "Start cycle list", - example: "< 1 2 3 >", - compile: Alias("["), - }, - Word { - name: ">", - stack: "(marker..n -- val)", - desc: "End cycle list, pick by step", - example: "< 1 2 3 > => cycles through 1, 2, 3", - compile: Simple, - }, - Word { - name: "<<", - stack: "(-- marker)", - desc: "Start pattern cycle list", - example: "<< 1 2 3 >>", - compile: Alias("["), - }, - Word { - name: ">>", - stack: "(marker..n -- val)", - desc: "End pattern cycle list, pick by pattern", - example: "<< 1 2 3 >> => cycles through 1, 2, 3 per pattern", - compile: Simple, - }, - // Quotations - Word { - name: "?", - stack: "(quot bool --)", - desc: "Execute quotation if true", - example: "{ 2 distort } 0.5 chance ?", - compile: Simple, - }, - Word { - name: "!?", - stack: "(quot bool --)", - desc: "Execute quotation if false", - example: "{ 1 distort } 0.5 chance !?", - compile: Simple, - }, - // Parameters (synthesis) - Word { - name: "time", - stack: "(f --)", - desc: "Set time offset", - example: "0.1 time", - compile: Param, - }, - Word { - name: "repeat", - stack: "(n --)", - desc: "Set repeat count", - example: "4 repeat", - compile: Param, - }, - Word { - name: "dur", - stack: "(f --)", - desc: "Set duration", - example: "0.5 dur", - compile: Param, - }, - Word { - name: "gate", - stack: "(f --)", - desc: "Set gate time", - example: "0.8 gate", - compile: Param, - }, - Word { - name: "freq", - stack: "(f --)", - desc: "Set frequency (Hz)", - example: "440 freq", - compile: Param, - }, - Word { - name: "detune", - stack: "(f --)", - desc: "Set detune amount", - example: "0.01 detune", - compile: Param, - }, - Word { - name: "speed", - stack: "(f --)", - desc: "Set playback speed", - example: "1.5 speed", - compile: Param, - }, - Word { - name: "glide", - stack: "(f --)", - desc: "Set glide/portamento", - example: "0.1 glide", - compile: Param, - }, - Word { - name: "pw", - stack: "(f --)", - desc: "Set pulse width", - example: "0.5 pw", - compile: Param, - }, - Word { - name: "spread", - stack: "(f --)", - desc: "Set stereo spread", - example: "0.5 spread", - compile: Param, - }, - Word { - name: "mult", - stack: "(f --)", - desc: "Set multiplier", - example: "2 mult", - compile: Param, - }, - Word { - name: "warp", - stack: "(f --)", - desc: "Set warp amount", - example: "0.5 warp", - compile: Param, - }, - Word { - name: "mirror", - stack: "(f --)", - desc: "Set mirror", - example: "1 mirror", - compile: Param, - }, - Word { - name: "harmonics", - stack: "(f --)", - desc: "Set harmonics", - example: "4 harmonics", - compile: Param, - }, - Word { - name: "timbre", - stack: "(f --)", - desc: "Set timbre", - example: "0.5 timbre", - compile: Param, - }, - Word { - name: "morph", - stack: "(f --)", - desc: "Set morph", - example: "0.5 morph", - compile: Param, - }, - Word { - name: "begin", - stack: "(f --)", - desc: "Set sample start (0-1)", - example: "0.25 begin", - compile: Param, - }, - Word { - name: "end", - stack: "(f --)", - desc: "Set sample end (0-1)", - example: "0.75 end", - compile: Param, - }, - Word { - name: "gain", - stack: "(f --)", - desc: "Set volume (0-1)", - example: "0.8 gain", - compile: Param, - }, - Word { - name: "postgain", - stack: "(f --)", - desc: "Set post gain", - example: "1.2 postgain", - compile: Param, - }, - Word { - name: "velocity", - stack: "(f --)", - desc: "Set velocity", - example: "100 velocity", - compile: Param, - }, - Word { - name: "pan", - stack: "(f --)", - desc: "Set pan (-1 to 1)", - example: "0.5 pan", - compile: Param, - }, - Word { - name: "attack", - stack: "(f --)", - desc: "Set attack time", - example: "0.01 attack", - compile: Param, - }, - Word { - name: "decay", - stack: "(f --)", - desc: "Set decay time", - example: "0.1 decay", - compile: Param, - }, - Word { - name: "sustain", - stack: "(f --)", - desc: "Set sustain level", - example: "0.5 sustain", - compile: Param, - }, - Word { - name: "release", - stack: "(f --)", - desc: "Set release time", - example: "0.3 release", - compile: Param, - }, - Word { - name: "adsr", - stack: "(a d s r --)", - desc: "Set attack, decay, sustain, release", - example: "0.01 0.1 0.5 0.3 adsr", - compile: Simple, - }, - Word { - name: "ad", - stack: "(a d --)", - desc: "Set attack, decay (sustain=0)", - example: "0.01 0.1 ad", - compile: Simple, - }, - Word { - name: "lpf", - stack: "(f --)", - desc: "Set lowpass frequency", - example: "2000 lpf", - compile: Param, - }, - Word { - name: "lpq", - stack: "(f --)", - desc: "Set lowpass resonance", - example: "0.5 lpq", - compile: Param, - }, - Word { - name: "lpe", - stack: "(f --)", - desc: "Set lowpass envelope", - example: "0.5 lpe", - compile: Param, - }, - Word { - name: "lpa", - stack: "(f --)", - desc: "Set lowpass attack", - example: "0.01 lpa", - compile: Param, - }, - Word { - name: "lpd", - stack: "(f --)", - desc: "Set lowpass decay", - example: "0.1 lpd", - compile: Param, - }, - Word { - name: "lps", - stack: "(f --)", - desc: "Set lowpass sustain", - example: "0.5 lps", - compile: Param, - }, - Word { - name: "lpr", - stack: "(f --)", - desc: "Set lowpass release", - example: "0.3 lpr", - compile: Param, - }, - Word { - name: "hpf", - stack: "(f --)", - desc: "Set highpass frequency", - example: "100 hpf", - compile: Param, - }, - Word { - name: "hpq", - stack: "(f --)", - desc: "Set highpass resonance", - example: "0.5 hpq", - compile: Param, - }, - Word { - name: "hpe", - stack: "(f --)", - desc: "Set highpass envelope", - example: "0.5 hpe", - compile: Param, - }, - Word { - name: "hpa", - stack: "(f --)", - desc: "Set highpass attack", - example: "0.01 hpa", - compile: Param, - }, - Word { - name: "hpd", - stack: "(f --)", - desc: "Set highpass decay", - example: "0.1 hpd", - compile: Param, - }, - Word { - name: "hps", - stack: "(f --)", - desc: "Set highpass sustain", - example: "0.5 hps", - compile: Param, - }, - Word { - name: "hpr", - stack: "(f --)", - desc: "Set highpass release", - example: "0.3 hpr", - compile: Param, - }, - Word { - name: "bpf", - stack: "(f --)", - desc: "Set bandpass frequency", - example: "1000 bpf", - compile: Param, - }, - Word { - name: "bpq", - stack: "(f --)", - desc: "Set bandpass resonance", - example: "0.5 bpq", - compile: Param, - }, - Word { - name: "bpe", - stack: "(f --)", - desc: "Set bandpass envelope", - example: "0.5 bpe", - compile: Param, - }, - Word { - name: "bpa", - stack: "(f --)", - desc: "Set bandpass attack", - example: "0.01 bpa", - compile: Param, - }, - Word { - name: "bpd", - stack: "(f --)", - desc: "Set bandpass decay", - example: "0.1 bpd", - compile: Param, - }, - Word { - name: "bps", - stack: "(f --)", - desc: "Set bandpass sustain", - example: "0.5 bps", - compile: Param, - }, - Word { - name: "bpr", - stack: "(f --)", - desc: "Set bandpass release", - example: "0.3 bpr", - compile: Param, - }, - Word { - name: "ftype", - stack: "(n --)", - desc: "Set filter type", - example: "1 ftype", - compile: Param, - }, - Word { - name: "penv", - stack: "(f --)", - desc: "Set pitch envelope", - example: "0.5 penv", - compile: Param, - }, - Word { - name: "patt", - stack: "(f --)", - desc: "Set pitch attack", - example: "0.01 patt", - compile: Param, - }, - Word { - name: "pdec", - stack: "(f --)", - desc: "Set pitch decay", - example: "0.1 pdec", - compile: Param, - }, - Word { - name: "psus", - stack: "(f --)", - desc: "Set pitch sustain", - example: "0 psus", - compile: Param, - }, - Word { - name: "prel", - stack: "(f --)", - desc: "Set pitch release", - example: "0.1 prel", - compile: Param, - }, - Word { - name: "vib", - stack: "(f --)", - desc: "Set vibrato rate", - example: "5 vib", - compile: Param, - }, - Word { - name: "vibmod", - stack: "(f --)", - desc: "Set vibrato depth", - example: "0.5 vibmod", - compile: Param, - }, - Word { - name: "vibshape", - stack: "(f --)", - desc: "Set vibrato shape", - example: "0 vibshape", - compile: Param, - }, - Word { - name: "fm", - stack: "(f --)", - desc: "Set FM frequency", - example: "200 fm", - compile: Param, - }, - Word { - name: "fmh", - stack: "(f --)", - desc: "Set FM harmonic ratio", - example: "2 fmh", - compile: Param, - }, - Word { - name: "fmshape", - stack: "(f --)", - desc: "Set FM shape", - example: "0 fmshape", - compile: Param, - }, - Word { - name: "fme", - stack: "(f --)", - desc: "Set FM envelope", - example: "0.5 fme", - compile: Param, - }, - Word { - name: "fma", - stack: "(f --)", - desc: "Set FM attack", - example: "0.01 fma", - compile: Param, - }, - Word { - name: "fmd", - stack: "(f --)", - desc: "Set FM decay", - example: "0.1 fmd", - compile: Param, - }, - Word { - name: "fms", - stack: "(f --)", - desc: "Set FM sustain", - example: "0.5 fms", - compile: Param, - }, - Word { - name: "fmr", - stack: "(f --)", - desc: "Set FM release", - example: "0.1 fmr", - compile: Param, - }, - Word { - name: "am", - stack: "(f --)", - desc: "Set AM frequency", - example: "10 am", - compile: Param, - }, - Word { - name: "amdepth", - stack: "(f --)", - desc: "Set AM depth", - example: "0.5 amdepth", - compile: Param, - }, - Word { - name: "amshape", - stack: "(f --)", - desc: "Set AM shape", - example: "0 amshape", - compile: Param, - }, - Word { - name: "rm", - stack: "(f --)", - desc: "Set RM frequency", - example: "100 rm", - compile: Param, - }, - Word { - name: "rmdepth", - stack: "(f --)", - desc: "Set RM depth", - example: "0.5 rmdepth", - compile: Param, - }, - Word { - name: "rmshape", - stack: "(f --)", - desc: "Set RM shape", - example: "0 rmshape", - compile: Param, - }, - Word { - name: "phaser", - stack: "(f --)", - desc: "Set phaser rate", - example: "1 phaser", - compile: Param, - }, - Word { - name: "phaserdepth", - stack: "(f --)", - desc: "Set phaser depth", - example: "0.5 phaserdepth", - compile: Param, - }, - Word { - name: "phasersweep", - stack: "(f --)", - desc: "Set phaser sweep", - example: "0.5 phasersweep", - compile: Param, - }, - Word { - name: "phasercenter", - stack: "(f --)", - desc: "Set phaser center", - example: "1000 phasercenter", - compile: Param, - }, - Word { - name: "flanger", - stack: "(f --)", - desc: "Set flanger rate", - example: "0.5 flanger", - compile: Param, - }, - Word { - name: "flangerdepth", - stack: "(f --)", - desc: "Set flanger depth", - example: "0.5 flangerdepth", - compile: Param, - }, - Word { - name: "flangerfeedback", - stack: "(f --)", - desc: "Set flanger feedback", - example: "0.5 flangerfeedback", - compile: Param, - }, - Word { - name: "chorus", - stack: "(f --)", - desc: "Set chorus rate", - example: "1 chorus", - compile: Param, - }, - Word { - name: "chorusdepth", - stack: "(f --)", - desc: "Set chorus depth", - example: "0.5 chorusdepth", - compile: Param, - }, - Word { - name: "chorusdelay", - stack: "(f --)", - desc: "Set chorus delay", - example: "0.02 chorusdelay", - compile: Param, - }, - Word { - name: "comb", - stack: "(f --)", - desc: "Set comb filter mix", - example: "0.5 comb", - compile: Param, - }, - Word { - name: "combfreq", - stack: "(f --)", - desc: "Set comb frequency", - example: "200 combfreq", - compile: Param, - }, - Word { - name: "combfeedback", - stack: "(f --)", - desc: "Set comb feedback", - example: "0.5 combfeedback", - compile: Param, - }, - Word { - name: "combdamp", - stack: "(f --)", - desc: "Set comb damping", - example: "0.5 combdamp", - compile: Param, - }, - Word { - name: "coarse", - stack: "(f --)", - desc: "Set coarse tune", - example: "12 coarse", - compile: Param, - }, - Word { - name: "crush", - stack: "(f --)", - desc: "Set bit crush", - example: "8 crush", - compile: Param, - }, - Word { - name: "fold", - stack: "(f --)", - desc: "Set wave fold", - example: "2 fold", - compile: Param, - }, - Word { - name: "wrap", - stack: "(f --)", - desc: "Set wave wrap", - example: "0.5 wrap", - compile: Param, - }, - Word { - name: "distort", - stack: "(f --)", - desc: "Set distortion", - example: "0.5 distort", - compile: Param, - }, - Word { - name: "distortvol", - stack: "(f --)", - desc: "Set distortion volume", - example: "0.8 distortvol", - compile: Param, - }, - Word { - name: "delay", - stack: "(f --)", - desc: "Set delay mix", - example: "0.3 delay", - compile: Param, - }, - Word { - name: "delaytime", - stack: "(f --)", - desc: "Set delay time", - example: "0.25 delaytime", - compile: Param, - }, - Word { - name: "delayfeedback", - stack: "(f --)", - desc: "Set delay feedback", - example: "0.5 delayfeedback", - compile: Param, - }, - Word { - name: "delaytype", - stack: "(n --)", - desc: "Set delay type", - example: "1 delaytype", - compile: Param, - }, - Word { - name: "verb", - stack: "(f --)", - desc: "Set reverb mix", - example: "0.3 verb", - compile: Param, - }, - Word { - name: "verbdecay", - stack: "(f --)", - desc: "Set reverb decay", - example: "2 verbdecay", - compile: Param, - }, - Word { - name: "verbdamp", - stack: "(f --)", - desc: "Set reverb damping", - example: "0.5 verbdamp", - compile: Param, - }, - Word { - name: "verbpredelay", - stack: "(f --)", - desc: "Set reverb predelay", - example: "0.02 verbpredelay", - compile: Param, - }, - Word { - name: "verbdiff", - stack: "(f --)", - desc: "Set reverb diffusion", - example: "0.7 verbdiff", - compile: Param, - }, - Word { - name: "voice", - stack: "(n --)", - desc: "Set voice number", - example: "1 voice", - compile: Param, - }, - Word { - name: "orbit", - stack: "(n --)", - desc: "Set orbit/bus", - example: "0 orbit", - compile: Param, - }, - Word { - name: "note", - stack: "(n --)", - desc: "Set MIDI note", - example: "60 note", - compile: Param, - }, - Word { - name: "size", - stack: "(f --)", - desc: "Set size", - example: "1 size", - compile: Param, - }, - Word { - name: "n", - stack: "(n --)", - desc: "Set sample number", - example: "0 n", - compile: Param, - }, - Word { - name: "cut", - stack: "(n --)", - desc: "Set cut group", - example: "1 cut", - compile: Param, - }, - Word { - name: "reset", - stack: "(n --)", - desc: "Reset parameter", - example: "1 reset", - compile: Param, - }, - // Quotation execution - Word { - name: "apply", - stack: "(quot --)", - desc: "Execute quotation unconditionally", - example: "{ 2 * } apply", - compile: Simple, - }, - // Word definitions - Word { - name: ":", - stack: "( -- )", - desc: "Begin word definition", - example: ": kick \"kick\" s emit ;", - compile: Simple, - }, - Word { - name: ";", - stack: "( -- )", - desc: "End word definition", - example: ": kick \"kick\" s emit ;", - compile: Simple, - }, -]; - -fn simple_op(name: &str) -> Option { - Some(match name { - "dup" => Op::Dup, - "dupn" => Op::Dupn, - "drop" => Op::Drop, - "swap" => Op::Swap, - "over" => Op::Over, - "rot" => Op::Rot, - "nip" => Op::Nip, - "tuck" => Op::Tuck, - "+" => Op::Add, - "-" => Op::Sub, - "*" => Op::Mul, - "/" => Op::Div, - "mod" => Op::Mod, - "neg" => Op::Neg, - "abs" => Op::Abs, - "floor" => Op::Floor, - "ceil" => Op::Ceil, - "round" => Op::Round, - "min" => Op::Min, - "max" => Op::Max, - "=" => Op::Eq, - "<>" => Op::Ne, - "lt" => Op::Lt, - "gt" => Op::Gt, - "<=" => Op::Le, - ">=" => Op::Ge, - "and" => Op::And, - "or" => Op::Or, - "not" => Op::Not, - "sound" => Op::NewCmd, - "emit" => Op::Emit, - "rand" => Op::Rand, - "rrand" => Op::Rrand, - "seed" => Op::Seed, - "cycle" => Op::Cycle, - "pcycle" => Op::PCycle, - "choose" => Op::Choose, - "every" => Op::Every, - "chance" => Op::ChanceExec, - "prob" => Op::ProbExec, - "coin" => Op::Coin, - "mtof" => Op::Mtof, - "ftom" => Op::Ftom, - "?" => Op::When, - "!?" => Op::Unless, - "at" => Op::At, - "zoom" => Op::Window, - "scale!" => Op::Scale, - "pop" => Op::Pop, - "div" => Op::Subdivide, - "each" => Op::Each, - "tempo!" => Op::SetTempo, - "[" => Op::ListStart, - "]" => Op::ListEnd, - ">" => Op::ListEndCycle, - ">>" => Op::ListEndPCycle, - "adsr" => Op::Adsr, - "ad" => Op::Ad, - "stack" => Op::Stack, - "for" => Op::For, - "echo" => Op::Echo, - "necho" => Op::Necho, - "apply" => Op::Apply, - _ => return None, - }) -} - -/// Parse note names like c4, c#4, cs4, eb4 into MIDI numbers. -/// C4 = 60 (middle C), A4 = 69 (440 Hz reference). -fn parse_note_name(name: &str) -> Option { - let name = name.to_lowercase(); - let bytes = name.as_bytes(); - - if bytes.len() < 2 { - return None; - } - - let base = match bytes[0] { - b'c' => 0, - b'd' => 2, - b'e' => 4, - b'f' => 5, - b'g' => 7, - b'a' => 9, - b'b' => 11, - _ => return None, - }; - - let (modifier, octave_start) = match bytes[1] { - b'#' | b's' => (1, 2), - b'b' if bytes.len() > 2 && bytes[2].is_ascii_digit() => (-1, 2), // flat: eb4, bb4 - b'0'..=b'9' => (0, 1), - _ => return None, - }; - - let octave_str = &name[octave_start..]; - let octave: i64 = octave_str.parse().ok()?; - - if !(-1..=9).contains(&octave) { - return None; - } - - // MIDI: C4 = 60, so C-1 = 0 - Some((octave + 1) * 12 + base + modifier) -} - -/// Parse interval names like m3, M3, P5 into semitone counts. -/// Supports simple intervals (1-8) and compound intervals (9-15). -fn parse_interval(name: &str) -> Option { - // Simple intervals: unison through octave - let simple = match name { - "P1" | "unison" => 0, - "m2" => 1, - "M2" => 2, - "m3" => 3, - "M3" => 4, - "P4" => 5, - "aug4" | "dim5" | "tritone" => 6, - "P5" => 7, - "m6" => 8, - "M6" => 9, - "m7" => 10, - "M7" => 11, - "P8" | "oct" => 12, - // Compound intervals (octave + simple) - "m9" => 13, - "M9" => 14, - "m10" => 15, - "M10" => 16, - "P11" => 17, - "aug11" => 18, - "P12" => 19, - "m13" => 20, - "M13" => 21, - "m14" => 22, - "M14" => 23, - "P15" => 24, - _ => return None, - }; - Some(simple) -} - -fn compile_word(name: &str, span: Option, ops: &mut Vec, dict: &Dictionary) -> bool { - for word in WORDS { - if word.name == name { - match &word.compile { - Simple => { - if let Some(op) = simple_op(name) { - ops.push(op); - } - } - Context(ctx) => ops.push(Op::GetContext((*ctx).into())), - Param => ops.push(Op::SetParam(name.into())), - Alias(target) => return compile_word(target, span, ops, dict), - Probability(p) => { - ops.push(Op::PushFloat(*p, None)); - ops.push(Op::ChanceExec); - } - } - return true; - } - } - - // @varname - fetch variable - if let Some(var_name) = name.strip_prefix('@') { - if !var_name.is_empty() { - ops.push(Op::PushStr(var_name.to_string(), span)); - ops.push(Op::Get); - return true; - } - } - - // !varname - store into variable - if let Some(var_name) = name.strip_prefix('!') { - if !var_name.is_empty() { - ops.push(Op::PushStr(var_name.to_string(), span)); - ops.push(Op::Set); - return true; - } - } - - // Note names: c4, c#4, cs4, eb4, etc. -> MIDI number - if let Some(midi) = parse_note_name(name) { - ops.push(Op::PushInt(midi, span)); - return true; - } - - // Intervals: m3, M3, P5, etc. -> dup top, add semitones (for chord building) - if let Some(semitones) = parse_interval(name) { - ops.push(Op::Dup); - ops.push(Op::PushInt(semitones, span)); - ops.push(Op::Add); - return true; - } - - // Internal ops not exposed in WORDS - if let Some(op) = simple_op(name) { - ops.push(op); - return true; - } - - // User-defined words from dictionary - if let Some(body) = dict.lock().unwrap().get(name) { - ops.extend(body.iter().cloned()); - return true; - } - - false -} - -#[derive(Clone, Debug)] -struct TimeContext { - start: f64, - duration: f64, - subdivisions: Option>, - iteration_index: Option, -} - -#[derive(Clone, Debug)] -enum Token { - Int(i64, SourceSpan), - Float(f64, SourceSpan), - Str(String, SourceSpan), - Word(String, SourceSpan), - QuoteStart(usize), - QuoteEnd(usize), -} - -fn tokenize(input: &str) -> Vec { - let mut tokens = Vec::new(); - let mut chars = input.char_indices().peekable(); - - while let Some(&(pos, c)) = chars.peek() { - if c.is_whitespace() { - chars.next(); - continue; - } - - if c == '"' { - let start = pos; - chars.next(); - let mut s = String::new(); - let mut end = start + 1; - while let Some(&(i, ch)) = chars.peek() { - end = i + ch.len_utf8(); - chars.next(); - if ch == '"' { - break; - } - s.push(ch); - } - tokens.push(Token::Str(s, SourceSpan { start, end })); - continue; - } - - if c == '(' { - while let Some(&(_, ch)) = chars.peek() { - chars.next(); - if ch == ')' { - break; - } - } - continue; - } - - if c == '{' { - chars.next(); - tokens.push(Token::QuoteStart(pos)); - continue; - } - - if c == '}' { - chars.next(); - tokens.push(Token::QuoteEnd(pos)); - continue; - } - - let start = pos; - let mut word = String::new(); - let mut end = start; - while let Some(&(i, ch)) = chars.peek() { - if ch.is_whitespace() || ch == '{' || ch == '}' { - break; - } - end = i + ch.len_utf8(); - word.push(ch); - chars.next(); - } - - let span = SourceSpan { start, end }; - if let Ok(i) = word.parse::() { - tokens.push(Token::Int(i, span)); - } else if let Ok(f) = word.parse::() { - tokens.push(Token::Float(f, span)); - } else { - tokens.push(Token::Word(word, span)); - } - } - - tokens -} - -fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { - let mut ops = Vec::new(); - let mut i = 0; - let mut pipe_parity = false; - - while i < tokens.len() { - match &tokens[i] { - Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))), - Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))), - Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))), - Token::QuoteStart(start_pos) => { - let (quote_ops, consumed, end_pos) = compile_quotation(&tokens[i + 1..], dict)?; - i += consumed; - let body_span = SourceSpan { start: *start_pos, end: end_pos + 1 }; - ops.push(Op::Quotation(quote_ops, Some(body_span))); - } - Token::QuoteEnd(_) => { - return Err("unexpected }".into()); - } - Token::Word(w, span) => { - let word = w.as_str(); - if word == ":" { - let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?; - i += consumed; - dict.lock().unwrap().insert(name, body); - } else if word == ";" { - return Err("unexpected ;".into()); - } else if word == "|" { - if pipe_parity { - ops.push(Op::LocalCycleEnd); - } else { - ops.push(Op::ListStart); - } - pipe_parity = !pipe_parity; - } else if word == "if" { - let (then_ops, else_ops, consumed, then_span, else_span) = compile_if(&tokens[i + 1..], dict)?; - i += consumed; - if else_ops.is_empty() { - ops.push(Op::BranchIfZero(then_ops.len(), then_span, None)); - ops.extend(then_ops); - } else { - ops.push(Op::BranchIfZero(then_ops.len() + 1, then_span, else_span)); - ops.extend(then_ops); - ops.push(Op::Branch(else_ops.len())); - ops.extend(else_ops); - } - } else if !compile_word(word, Some(*span), &mut ops, dict) { - return Err(format!("unknown word: {word}")); - } - } - } - i += 1; - } - - Ok(ops) -} - -fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec, usize, usize), String> { - let mut depth = 1; - let mut end_idx = None; - - for (i, tok) in tokens.iter().enumerate() { - match tok { - Token::QuoteStart(_) => depth += 1, - Token::QuoteEnd(_) => { - depth -= 1; - if depth == 0 { - end_idx = Some(i); - break; - } - } - _ => {} - } - } - - let end_idx = end_idx.ok_or("missing }")?; - let byte_pos = match &tokens[end_idx] { - Token::QuoteEnd(pos) => *pos, - _ => unreachable!(), - }; - let quote_ops = compile(&tokens[..end_idx], dict)?; - Ok((quote_ops, end_idx + 1, byte_pos)) -} - -fn token_span(tok: &Token) -> Option { - match tok { - Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s), - Token::QuoteStart(p) => Some(SourceSpan { start: *p, end: *p + 1 }), - Token::QuoteEnd(p) => Some(SourceSpan { start: *p, end: *p + 1 }), - } -} - -fn compile_colon_def(tokens: &[Token], dict: &Dictionary) -> Result<(usize, String, Vec), String> { - if tokens.is_empty() { - return Err("expected word name after ':'".into()); - } - let name = match &tokens[0] { - Token::Word(w, _) => w.clone(), - _ => return Err("expected word name after ':'".into()), - }; - let mut semi_pos = None; - for (i, tok) in tokens[1..].iter().enumerate() { - if let Token::Word(w, _) = tok { - if w == ";" { - semi_pos = Some(i + 1); - break; - } - } - } - let semi_pos = semi_pos.ok_or("missing ';' in word definition")?; - let body_tokens = &tokens[1..semi_pos]; - let body_ops = compile(body_tokens, dict)?; - // consumed = name + body + semicolon - Ok((semi_pos + 1, name, body_ops)) -} - -fn tokens_span(tokens: &[Token]) -> Option { - let first = tokens.first().and_then(token_span)?; - let last = tokens.last().and_then(token_span)?; - Some(SourceSpan { start: first.start, end: last.end }) -} - -#[allow(clippy::type_complexity)] -fn compile_if(tokens: &[Token], dict: &Dictionary) -> Result<(Vec, Vec, usize, Option, Option), String> { - let mut depth = 1; - let mut else_pos = None; - let mut then_pos = None; - - for (i, tok) in tokens.iter().enumerate() { - if let Token::Word(w, _) = tok { - match w.as_str() { - "if" => depth += 1, - "else" if depth == 1 => else_pos = Some(i), - "then" => { - depth -= 1; - if depth == 0 { - then_pos = Some(i); - break; - } - } - _ => {} - } - } - } - - let then_pos = then_pos.ok_or("missing 'then'")?; - - let (then_ops, else_ops, then_span, else_span) = if let Some(ep) = else_pos { - let then_slice = &tokens[..ep]; - let else_slice = &tokens[ep + 1..then_pos]; - let then_span = tokens_span(then_slice); - let else_span = tokens_span(else_slice); - let then_ops = compile(then_slice, dict)?; - let else_ops = compile(else_slice, dict)?; - (then_ops, else_ops, then_span, else_span) - } else { - let then_slice = &tokens[..then_pos]; - let then_span = tokens_span(then_slice); - let then_ops = compile(then_slice, dict)?; - (then_ops, Vec::new(), then_span, None) - }; - - Ok((then_ops, else_ops, then_pos + 1, then_span, else_span)) -} - -pub type Stack = Arc>>; - -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: Arc::new(Mutex::new(Vec::new())), - vars, - dict, - rng, - } - } - - #[allow(dead_code)] - pub fn stack(&self) -> Vec { - self.stack.lock().unwrap().clone() - } - - #[allow(dead_code)] - pub fn clear_stack(&self) { - self.stack.lock().unwrap().clear(); - } - - pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result, String> { - self.evaluate_impl(script, ctx, None) - } - - pub fn evaluate_with_trace( - &self, - script: &str, - ctx: &StepContext, - trace: &mut ExecutionTrace, - ) -> Result, String> { - self.evaluate_impl(script, ctx, Some(trace)) - } - - fn evaluate_impl( - &self, - script: &str, - ctx: &StepContext, - trace: Option<&mut ExecutionTrace>, - ) -> Result, String> { - if script.trim().is_empty() { - return Err("empty script".into()); - } - - let tokens = tokenize(script); - let ops = compile(&tokens, &self.dict)?; - self.execute(&ops, ctx, trace) - } - - fn execute( - &self, - ops: &[Op], - ctx: &StepContext, - trace: Option<&mut ExecutionTrace>, - ) -> Result, String> { - let mut stack = self.stack.lock().unwrap(); - let mut outputs: Vec = Vec::new(); - let mut time_stack: Vec = vec![TimeContext { - start: 0.0, - duration: ctx.step_duration(), - subdivisions: None, - iteration_index: None, - }]; - let mut cmd = CmdRegister::default(); - - self.execute_ops( - ops, - ctx, - &mut stack, - &mut outputs, - &mut time_stack, - &mut cmd, - trace, - )?; - - Ok(outputs) - } - - #[allow(clippy::too_many_arguments)] - fn execute_ops( - &self, - ops: &[Op], - ctx: &StepContext, - stack: &mut Vec, - outputs: &mut Vec, - time_stack: &mut Vec, - cmd: &mut CmdRegister, - trace: Option<&mut ExecutionTrace>, - ) -> Result<(), String> { - let mut pc = 0; - let trace_cell = std::cell::RefCell::new(trace); - - 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::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 => binary_op(stack, |a, b| a / b)?, - Op::Mod => { - let b = stack.pop().ok_or("stack underflow")?.as_int()?; - let a = stack.pop().ok_or("stack underflow")?.as_int()?; - stack.push(Value::Int(a % b, None)); - } - Op::Neg => { - let v = stack.pop().ok_or("stack underflow")?; - match v { - Value::Int(i, s) => stack.push(Value::Int(-i, s)), - Value::Float(f, s) => stack.push(Value::Float(-f, s)), - _ => return Err("expected number".into()), - } - } - Op::Abs => { - let v = stack.pop().ok_or("stack underflow")?; - match v { - Value::Int(i, s) => stack.push(Value::Int(i.abs(), s)), - Value::Float(f, s) => stack.push(Value::Float(f.abs(), s)), - _ => return Err("expected number".into()), - } - } - Op::Floor => { - let v = stack.pop().ok_or("stack underflow")?.as_float()?; - stack.push(Value::Int(v.floor() as i64, None)); - } - Op::Ceil => { - let v = stack.pop().ok_or("stack underflow")?.as_float()?; - stack.push(Value::Int(v.ceil() as i64, None)); - } - Op::Round => { - let v = stack.pop().ok_or("stack underflow")?.as_float()?; - stack.push(Value::Int(v.round() as i64, None)); - } - Op::Min => binary_op(stack, |a, b| a.min(b))?, - Op::Max => binary_op(stack, |a, b| a.max(b))?, - - 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::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 => { - let name = stack.pop().ok_or("stack underflow")?; - let name = name.as_str()?; - cmd.set_sound(name.to_string()); - } - Op::SetParam(param) => { - let val = stack.pop().ok_or("stack underflow")?; - cmd.set_param(param.clone(), val.to_param_string()); - } - Op::Emit => { - let (sound, mut params) = cmd.take().ok_or("no sound set")?; - let mut pairs = vec![("sound".into(), sound)]; - pairs.append(&mut params); - let time_ctx = time_stack.last().ok_or("time stack underflow")?; - if time_ctx.start > 0.0 { - pairs.push(("delta".into(), time_ctx.start.to_string())); - } - if !pairs.iter().any(|(k, _)| k == "dur") { - pairs.push(("dur".into(), time_ctx.duration.to_string())); - } - if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { - let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); - pairs[idx].1 = (ratio * time_ctx.duration).to_string(); - } else { - pairs.push(("delaytime".into(), time_ctx.duration.to_string())); - } - outputs.push(format_cmd(&pairs)); - } - - Op::Get => { - let name = stack.pop().ok_or("stack underflow")?; - let name = name.as_str()?; - let vars = self.vars.lock().unwrap(); - let val = vars.get(name).cloned().unwrap_or(Value::Int(0, None)); - 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")?; - self.vars.lock().unwrap().insert(name, val); - } - - Op::GetContext(name) => { - let val = match name.as_str() { - "step" => Value::Int(ctx.step as i64, None), - "beat" => Value::Float(ctx.beat, 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), - _ => Value::Int(0, None), - }; - stack.push(val); - } - - Op::Rand => { - let max = stack.pop().ok_or("stack underflow")?.as_float()?; - let min = stack.pop().ok_or("stack underflow")?.as_float()?; - let val = self.rng.lock().unwrap().gen_range(min..max); - stack.push(Value::Float(val, None)); - } - Op::Rrand => { - let max = stack.pop().ok_or("stack underflow")?.as_int()?; - let min = stack.pop().ok_or("stack underflow")?.as_int()?; - let val = self.rng.lock().unwrap().gen_range(min..=max); - stack.push(Value::Int(val, None)); - } - Op::Seed => { - let s = stack.pop().ok_or("stack underflow")?.as_int()?; - *self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64); - } - - Op::Cycle => { - let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; - if count == 0 { - return Err("cycle count must be > 0".into()); - } - if stack.len() < count { - return Err("stack underflow".into()); - } - let start = stack.len() - count; - let values: Vec = stack.drain(start..).collect(); - let idx = ctx.runs % count; - let selected = values[idx].clone(); - if let Some(span) = selected.span() { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - stack.push(selected); - } - - Op::PCycle => { - let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; - if count == 0 { - return Err("pcycle count must be > 0".into()); - } - if stack.len() < count { - return Err("stack underflow".into()); - } - let start = stack.len() - count; - let values: Vec = stack.drain(start..).collect(); - let idx = ctx.iter % count; - let selected = values[idx].clone(); - if let Some(span) = selected.span() { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - stack.push(selected); - } - - 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()); - } - if stack.len() < count { - return Err("stack underflow".into()); - } - let start = stack.len() - count; - let values: Vec = stack.drain(start..).collect(); - let idx = self.rng.lock().unwrap().gen_range(0..count); - let selected = values[idx].clone(); - if let Some(span) = selected.span() { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - stack.push(selected); - } - - Op::ChanceExec => { - let prob = stack.pop().ok_or("stack underflow")?.as_float()?; - let quot = stack.pop().ok_or("stack underflow")?; - let val: f64 = self.rng.lock().unwrap().gen(); - if val < prob { - 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(); - self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } - } - } - - Op::ProbExec => { - let pct = stack.pop().ok_or("stack underflow")?.as_float()?; - let quot = stack.pop().ok_or("stack underflow")?; - let val: f64 = self.rng.lock().unwrap().gen(); - if val < pct / 100.0 { - 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(); - self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } - } - } - - Op::Coin => { - let val: f64 = self.rng.lock().unwrap().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 => { - let cond = stack.pop().ok_or("stack underflow")?; - let quot = stack.pop().ok_or("stack underflow")?; - if cond.is_truthy() { - 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(); - self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } - } - } - - Op::Unless => { - let cond = stack.pop().ok_or("stack underflow")?; - let quot = stack.pop().ok_or("stack underflow")?; - if !cond.is_truthy() { - 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(); - self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } - } - } - - 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::At => { - let pos = stack.pop().ok_or("stack underflow")?.as_float()?; - let parent = time_stack.last().ok_or("time stack underflow")?; - let new_start = parent.start + parent.duration * pos; - time_stack.push(TimeContext { - start: new_start, - duration: parent.duration * (1.0 - pos), - subdivisions: None, - iteration_index: parent.iteration_index, - }); - } - - Op::Window => { - let end = stack.pop().ok_or("stack underflow")?.as_float()?; - let start_pos = stack.pop().ok_or("stack underflow")?.as_float()?; - let parent = time_stack.last().ok_or("time stack underflow")?; - let new_start = parent.start + parent.duration * start_pos; - let new_duration = parent.duration * (end - start_pos); - time_stack.push(TimeContext { - start: new_start, - duration: new_duration, - subdivisions: None, - iteration_index: parent.iteration_index, - }); - } - - Op::Scale => { - let factor = stack.pop().ok_or("stack underflow")?.as_float()?; - let parent = time_stack.last().ok_or("time stack underflow")?; - time_stack.push(TimeContext { - start: parent.start, - duration: parent.duration * factor, - subdivisions: None, - iteration_index: parent.iteration_index, - }); - } - - Op::Pop => { - if time_stack.len() <= 1 { - return Err("cannot pop root time context".into()); - } - time_stack.pop(); - } - - Op::Subdivide => { - let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; - if n == 0 { - return Err("subdivide count must be > 0".into()); - } - let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; - let sub_duration = time_ctx.duration / n as f64; - let mut subs = Vec::with_capacity(n); - for i in 0..n { - subs.push((time_ctx.start + sub_duration * i as f64, sub_duration)); - } - time_ctx.subdivisions = Some(subs); - } - - Op::Each => { - let (sound, params) = cmd.take().ok_or("no sound set")?; - let time_ctx = time_stack.last().ok_or("time stack underflow")?; - let subs = time_ctx - .subdivisions - .as_ref() - .ok_or("each requires subdivide first")?; - for (sub_start, sub_dur) in subs { - let mut pairs = vec![("sound".into(), sound.clone())]; - pairs.extend(params.iter().cloned()); - if *sub_start > 0.0 { - pairs.push(("delta".into(), sub_start.to_string())); - } - if !pairs.iter().any(|(k, _)| k == "dur") { - pairs.push(("dur".into(), sub_dur.to_string())); - } - if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { - let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); - pairs[idx].1 = (ratio * sub_dur).to_string(); - } else { - pairs.push(("delaytime".into(), sub_dur.to_string())); - } - outputs.push(format_cmd(&pairs)); - } - } - - Op::SetTempo => { - let tempo = stack.pop().ok_or("stack underflow")?.as_float()?; - let clamped = tempo.clamp(20.0, 300.0); - self.vars - .lock() - .unwrap() - .insert("__tempo__".to_string(), Value::Float(clamped, None)); - } - - Op::ListStart => { - stack.push(Value::Marker); - } - - Op::ListEnd => { - let mut count = 0; - let mut values = Vec::new(); - while let Some(v) = stack.pop() { - if v.is_marker() { - break; - } - values.push(v); - count += 1; - } - values.reverse(); - for v in values { - stack.push(v); - } - stack.push(Value::Int(count, None)); - } - - Op::ListEndCycle => { - let mut values = Vec::new(); - while let Some(v) = stack.pop() { - if v.is_marker() { - break; - } - values.push(v); - } - if values.is_empty() { - return Err("empty cycle list".into()); - } - values.reverse(); - let idx = ctx.runs % values.len(); - let selected = values[idx].clone(); - if let Some(span) = selected.span() { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - stack.push(selected); - } - - Op::ListEndPCycle => { - let mut values = Vec::new(); - while let Some(v) = stack.pop() { - if v.is_marker() { - break; - } - values.push(v); - } - if values.is_empty() { - return Err("empty pattern cycle list".into()); - } - values.reverse(); - let idx = ctx.iter % values.len(); - let selected = values[idx].clone(); - if let Some(span) = selected.span() { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - stack.push(selected); - } - - 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".into(), a.to_param_string()); - cmd.set_param("decay".into(), d.to_param_string()); - cmd.set_param("sustain".into(), s.to_param_string()); - cmd.set_param("release".into(), r.to_param_string()); - } - - Op::Ad => { - let d = stack.pop().ok_or("stack underflow")?; - let a = stack.pop().ok_or("stack underflow")?; - cmd.set_param("attack".into(), a.to_param_string()); - cmd.set_param("decay".into(), d.to_param_string()); - cmd.set_param("sustain".into(), "0".into()); - } - - Op::Stack => { - let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; - if n == 0 { - return Err("stack count must be > 0".into()); - } - let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; - let sub_duration = time_ctx.duration / n as f64; - let mut subs = Vec::with_capacity(n); - for _ in 0..n { - subs.push((time_ctx.start, sub_duration)); - } - time_ctx.subdivisions = Some(subs); - } - - Op::Echo => { - let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; - if n == 0 { - return Err("echo count must be > 0".into()); - } - let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; - // Geometric series: d1 * (2 - 2^(1-n)) = total - let d1 = time_ctx.duration / (2.0 - 2.0_f64.powi(1 - n as i32)); - let mut subs = Vec::with_capacity(n); - for i in 0..n { - let dur = d1 / 2.0_f64.powi(i as i32); - let start = if i == 0 { - time_ctx.start - } else { - time_ctx.start + d1 * (2.0 - 2.0_f64.powi(1 - i as i32)) - }; - subs.push((start, dur)); - } - time_ctx.subdivisions = Some(subs); - } - - Op::Necho => { - let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; - if n == 0 { - return Err("necho count must be > 0".into()); - } - let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; - // Reverse geometric: d1 + 2*d1 + 4*d1 + ... = d1 * (2^n - 1) = total - let d1 = time_ctx.duration / (2.0_f64.powi(n as i32) - 1.0); - let mut subs = Vec::with_capacity(n); - for i in 0..n { - let dur = d1 * 2.0_f64.powi(i as i32); - let start = if i == 0 { - time_ctx.start - } else { - // Sum of previous durations: d1 * (2^i - 1) - time_ctx.start + d1 * (2.0_f64.powi(i as i32) - 1.0) - }; - subs.push((start, dur)); - } - time_ctx.subdivisions = Some(subs); - } - - Op::For => { - let quot = stack.pop().ok_or("stack underflow")?; - let time_ctx = time_stack.last().ok_or("time stack underflow")?; - let subs = time_ctx - .subdivisions - .clone() - .ok_or("for requires subdivide first")?; - - match quot { - Value::Quotation(quot_ops, body_span) => { - if let Some(span) = body_span { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.executed_spans.push(span); - } - } - for (i, (sub_start, sub_dur)) in subs.iter().enumerate() { - time_stack.push(TimeContext { - start: *sub_start, - duration: *sub_dur, - subdivisions: None, - iteration_index: Some(i), - }); - let mut trace_opt = trace_cell.borrow_mut().take(); - self.execute_ops( - "_ops, - ctx, - stack, - outputs, - time_stack, - cmd, - trace_opt.as_deref_mut(), - )?; - *trace_cell.borrow_mut() = trace_opt; - time_stack.pop(); - } - } - _ => return Err("expected quotation".into()), - } - } - - Op::LocalCycleEnd => { - let mut values = Vec::new(); - while let Some(v) = stack.pop() { - if v.is_marker() { - break; - } - values.push(v); - } - if values.is_empty() { - return Err("empty local cycle list".into()); - } - values.reverse(); - let time_ctx = time_stack.last().ok_or("time stack underflow")?; - let idx = time_ctx.iteration_index.unwrap_or(0) % values.len(); - let selected = values[idx].clone(); - if let Some(span) = selected.span() { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - stack.push(selected); - } - - Op::Apply => { - let quot = stack.pop().ok_or("stack underflow")?; - 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(); - self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } - } - } - pc += 1; - } - - Ok(()) - } -} - -fn binary_op(stack: &mut Vec, f: F) -> Result<(), String> -where - F: Fn(f64, f64) -> f64, -{ - let b = stack.pop().ok_or("stack underflow")?.as_float()?; - let a = stack.pop().ok_or("stack underflow")?.as_float()?; - let result = f(a, b); - if result.fract() == 0.0 && result.abs() < i64::MAX as f64 { - stack.push(Value::Int(result as i64, None)); - } else { - stack.push(Value::Float(result, None)); - } - 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")?.as_float()?; - let a = stack.pop().ok_or("stack underflow")?.as_float()?; - stack.push(Value::Int(if f(a, b) { 1 } else { 0 }, None)); - Ok(()) -} - -fn format_cmd(pairs: &[(String, String)]) -> String { - let parts: Vec = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect(); - format!("/{}", parts.join("/")) -} diff --git a/src/model/forth/compiler.rs b/src/model/forth/compiler.rs new file mode 100644 index 0000000..411f50a --- /dev/null +++ b/src/model/forth/compiler.rs @@ -0,0 +1,282 @@ +use super::ops::Op; +use super::types::{Dictionary, SourceSpan}; +use super::words::{compile_word, simple_op}; + +#[derive(Clone, Debug)] +enum Token { + Int(i64, SourceSpan), + Float(f64, SourceSpan), + Str(String, SourceSpan), + Word(String, SourceSpan), + QuoteStart(usize), + QuoteEnd(usize), +} + +pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result, String> { + let tokens = tokenize(input); + compile(&tokens, dict) +} + +fn tokenize(input: &str) -> Vec { + let mut tokens = Vec::new(); + let mut chars = input.char_indices().peekable(); + + while let Some(&(pos, c)) = chars.peek() { + if c.is_whitespace() { + chars.next(); + continue; + } + + if c == '"' { + let start = pos; + chars.next(); + let mut s = String::new(); + let mut end = start + 1; + while let Some(&(i, ch)) = chars.peek() { + end = i + ch.len_utf8(); + chars.next(); + if ch == '"' { + break; + } + s.push(ch); + } + tokens.push(Token::Str(s, SourceSpan { start, end })); + continue; + } + + if c == '(' { + while let Some(&(_, ch)) = chars.peek() { + chars.next(); + if ch == ')' { + break; + } + } + continue; + } + + if c == '{' { + chars.next(); + tokens.push(Token::QuoteStart(pos)); + continue; + } + + if c == '}' { + chars.next(); + tokens.push(Token::QuoteEnd(pos)); + continue; + } + + let start = pos; + let mut word = String::new(); + let mut end = start; + while let Some(&(i, ch)) = chars.peek() { + if ch.is_whitespace() || ch == '{' || ch == '}' { + break; + } + end = i + ch.len_utf8(); + word.push(ch); + chars.next(); + } + + let span = SourceSpan { start, end }; + if let Ok(i) = word.parse::() { + tokens.push(Token::Int(i, span)); + } else if let Ok(f) = word.parse::() { + tokens.push(Token::Float(f, span)); + } else { + tokens.push(Token::Word(word, span)); + } + } + + tokens +} + +fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { + let mut ops = Vec::new(); + let mut i = 0; + let mut pipe_parity = false; + let mut list_depth: usize = 0; + + while i < tokens.len() { + match &tokens[i] { + Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))), + Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))), + Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))), + Token::QuoteStart(start_pos) => { + let (quote_ops, consumed, end_pos) = compile_quotation(&tokens[i + 1..], dict)?; + i += consumed; + let body_span = SourceSpan { start: *start_pos, end: end_pos + 1 }; + ops.push(Op::Quotation(quote_ops, Some(body_span))); + } + Token::QuoteEnd(_) => { + return Err("unexpected }".into()); + } + Token::Word(w, span) => { + let word = w.as_str(); + if word == ":" { + let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?; + i += consumed; + dict.lock().unwrap().insert(name, body); + } else if word == ";" { + return Err("unexpected ;".into()); + } else if word == "|" { + if pipe_parity { + ops.push(Op::LocalCycleEnd); + list_depth = list_depth.saturating_sub(1); + } else { + ops.push(Op::ListStart); + list_depth += 1; + } + pipe_parity = !pipe_parity; + } else if word == "if" { + let (then_ops, else_ops, consumed, then_span, else_span) = compile_if(&tokens[i + 1..], dict)?; + i += consumed; + if else_ops.is_empty() { + ops.push(Op::BranchIfZero(then_ops.len(), then_span, None)); + ops.extend(then_ops); + } else { + ops.push(Op::BranchIfZero(then_ops.len() + 1, then_span, else_span)); + ops.extend(then_ops); + ops.push(Op::Branch(else_ops.len())); + ops.extend(else_ops); + } + } else if is_list_start(word) { + ops.push(Op::ListStart); + list_depth += 1; + } else if is_list_end(word) { + list_depth = list_depth.saturating_sub(1); + if let Some(op) = simple_op(word) { + ops.push(op); + } + } else if list_depth > 0 { + let mut word_ops = Vec::new(); + if !compile_word(word, Some(*span), &mut word_ops, dict) { + return Err(format!("unknown word: {word}")); + } + ops.push(Op::Quotation(word_ops, Some(*span))); + } else if !compile_word(word, Some(*span), &mut ops, dict) { + return Err(format!("unknown word: {word}")); + } + } + } + i += 1; + } + + Ok(ops) +} + +fn is_list_start(word: &str) -> bool { + matches!(word, "[" | "<" | "<<") +} + +fn is_list_end(word: &str) -> bool { + matches!(word, "]" | ">" | ">>") +} + +fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec, usize, usize), String> { + let mut depth = 1; + let mut end_idx = None; + + for (i, tok) in tokens.iter().enumerate() { + match tok { + Token::QuoteStart(_) => depth += 1, + Token::QuoteEnd(_) => { + depth -= 1; + if depth == 0 { + end_idx = Some(i); + break; + } + } + _ => {} + } + } + + let end_idx = end_idx.ok_or("missing }")?; + let byte_pos = match &tokens[end_idx] { + Token::QuoteEnd(pos) => *pos, + _ => unreachable!(), + }; + let quote_ops = compile(&tokens[..end_idx], dict)?; + Ok((quote_ops, end_idx + 1, byte_pos)) +} + +fn token_span(tok: &Token) -> Option { + match tok { + Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s), + Token::QuoteStart(p) => Some(SourceSpan { start: *p, end: *p + 1 }), + Token::QuoteEnd(p) => Some(SourceSpan { start: *p, end: *p + 1 }), + } +} + +fn compile_colon_def(tokens: &[Token], dict: &Dictionary) -> Result<(usize, String, Vec), String> { + if tokens.is_empty() { + return Err("expected word name after ':'".into()); + } + let name = match &tokens[0] { + Token::Word(w, _) => w.clone(), + _ => return Err("expected word name after ':'".into()), + }; + let mut semi_pos = None; + for (i, tok) in tokens[1..].iter().enumerate() { + if let Token::Word(w, _) = tok { + if w == ";" { + semi_pos = Some(i + 1); + break; + } + } + } + let semi_pos = semi_pos.ok_or("missing ';' in word definition")?; + let body_tokens = &tokens[1..semi_pos]; + let body_ops = compile(body_tokens, dict)?; + // consumed = name + body + semicolon + Ok((semi_pos + 1, name, body_ops)) +} + +fn tokens_span(tokens: &[Token]) -> Option { + let first = tokens.first().and_then(token_span)?; + let last = tokens.last().and_then(token_span)?; + Some(SourceSpan { start: first.start, end: last.end }) +} + +#[allow(clippy::type_complexity)] +fn compile_if(tokens: &[Token], dict: &Dictionary) -> Result<(Vec, Vec, usize, Option, Option), String> { + let mut depth = 1; + let mut else_pos = None; + let mut then_pos = None; + + for (i, tok) in tokens.iter().enumerate() { + if let Token::Word(w, _) = tok { + match w.as_str() { + "if" => depth += 1, + "else" if depth == 1 => else_pos = Some(i), + "then" => { + depth -= 1; + if depth == 0 { + then_pos = Some(i); + break; + } + } + _ => {} + } + } + } + + let then_pos = then_pos.ok_or("missing 'then'")?; + + let (then_ops, else_ops, then_span, else_span) = if let Some(ep) = else_pos { + let then_slice = &tokens[..ep]; + let else_slice = &tokens[ep + 1..then_pos]; + let then_span = tokens_span(then_slice); + let else_span = tokens_span(else_slice); + let then_ops = compile(then_slice, dict)?; + let else_ops = compile(else_slice, dict)?; + (then_ops, else_ops, then_span, else_span) + } else { + let then_slice = &tokens[..then_pos]; + let then_span = tokens_span(then_slice); + let then_ops = compile(then_slice, dict)?; + (then_ops, Vec::new(), then_span, None) + }; + + Ok((then_ops, else_ops, then_pos + 1, then_span, else_span)) +} diff --git a/src/model/forth/mod.rs b/src/model/forth/mod.rs new file mode 100644 index 0000000..a2f87de --- /dev/null +++ b/src/model/forth/mod.rs @@ -0,0 +1,11 @@ +mod compiler; +mod ops; +mod types; +mod vm; +mod words; + +pub use types::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Variables}; +#[allow(unused_imports)] +pub use types::Value; +pub use vm::Forth; +pub use words::{Word, WordCompile, WORDS}; diff --git a/src/model/forth/ops.rs b/src/model/forth/ops.rs new file mode 100644 index 0000000..4c7d46b --- /dev/null +++ b/src/model/forth/ops.rs @@ -0,0 +1,79 @@ +use super::types::SourceSpan; + +#[derive(Clone, Debug, PartialEq)] +pub enum Op { + PushInt(i64, Option), + PushFloat(f64, Option), + PushStr(String, Option), + Dup, + Dupn, + Drop, + Swap, + Over, + Rot, + Nip, + Tuck, + Add, + Sub, + Mul, + Div, + Mod, + Neg, + Abs, + Floor, + Ceil, + Round, + Min, + Max, + Eq, + Ne, + Lt, + Gt, + Le, + Ge, + And, + Or, + Not, + BranchIfZero(usize, Option, Option), + Branch(usize), + NewCmd, + SetParam(String), + Emit, + Get, + Set, + GetContext(String), + Rand, + Rrand, + Seed, + Cycle, + Choose, + ChanceExec, + ProbExec, + Coin, + Mtof, + Ftom, + ListStart, + ListEnd, + ListEndCycle, + PCycle, + ListEndPCycle, + At, + Window, + Scale, + Pop, + Subdivide, + SetTempo, + Each, + Every, + Quotation(Vec, Option), + When, + Unless, + Adsr, + Ad, + Stack, + For, + LocalCycleEnd, + Echo, + Necho, + Apply, +} diff --git a/src/model/forth/types.rs b/src/model/forth/types.rs new file mode 100644 index 0000000..ae1fd69 --- /dev/null +++ b/src/model/forth/types.rs @@ -0,0 +1,141 @@ +use rand::rngs::StdRng; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use super::ops::Op; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SourceSpan { + pub start: usize, + pub end: usize, +} + +#[derive(Clone, Debug, Default)] +pub struct ExecutionTrace { + pub executed_spans: Vec, + pub selected_spans: Vec, +} + +pub struct StepContext { + pub step: usize, + pub beat: f64, + pub pattern: usize, + pub tempo: f64, + pub phase: f64, + pub slot: usize, + pub runs: usize, + pub iter: usize, + pub speed: f64, + pub fill: bool, +} + +impl StepContext { + pub fn step_duration(&self) -> f64 { + 60.0 / self.tempo / 4.0 / self.speed + } +} + +pub type Variables = Arc>>; +pub type Dictionary = Arc>>>; +pub type Rng = Arc>; +pub type Stack = Arc>>; + +#[derive(Clone, Debug)] +pub enum Value { + Int(i64, Option), + Float(f64, Option), + Str(String, Option), + Marker, + Quotation(Vec, Option), +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Value::Int(a, _), Value::Int(b, _)) => a == b, + (Value::Float(a, _), Value::Float(b, _)) => a == b, + (Value::Str(a, _), Value::Str(b, _)) => a == b, + (Value::Marker, Value::Marker) => true, + (Value::Quotation(a, _), Value::Quotation(b, _)) => a == b, + _ => false, + } + } +} + +impl Value { + pub fn as_float(&self) -> Result { + match self { + Value::Float(f, _) => Ok(*f), + Value::Int(i, _) => Ok(*i as f64), + _ => Err("expected number".into()), + } + } + + pub(super) fn as_int(&self) -> Result { + match self { + Value::Int(i, _) => Ok(*i), + Value::Float(f, _) => Ok(*f as i64), + _ => Err("expected number".into()), + } + } + + pub(super) fn as_str(&self) -> Result<&str, String> { + match self { + Value::Str(s, _) => Ok(s), + _ => Err("expected string".into()), + } + } + + pub(super) fn is_truthy(&self) -> bool { + match self { + Value::Int(i, _) => *i != 0, + Value::Float(f, _) => *f != 0.0, + Value::Str(s, _) => !s.is_empty(), + Value::Marker => false, + Value::Quotation(..) => true, + } + } + + pub(super) fn is_marker(&self) -> bool { + matches!(self, Value::Marker) + } + + pub(super) fn to_param_string(&self) -> String { + match self { + Value::Int(i, _) => i.to_string(), + Value::Float(f, _) => f.to_string(), + Value::Str(s, _) => s.clone(), + Value::Marker => String::new(), + Value::Quotation(..) => String::new(), + } + } + + pub(super) fn span(&self) -> Option { + match self { + Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s, + Value::Marker | Value::Quotation(..) => None, + } + } +} + +#[derive(Clone, Debug, Default)] +pub(super) struct CmdRegister { + sound: Option, + params: Vec<(String, String)>, +} + +impl CmdRegister { + pub(super) fn set_sound(&mut self, name: String) { + self.sound = Some(name); + } + + pub(super) fn set_param(&mut self, key: String, value: String) { + self.params.push((key, value)); + } + + pub(super) fn take(&mut self) -> Option<(String, Vec<(String, String)>)> { + let sound = self.sound.take()?; + let params = std::mem::take(&mut self.params); + Some((sound, params)) + } +} diff --git a/src/model/forth/vm.rs b/src/model/forth/vm.rs new file mode 100644 index 0000000..349decb --- /dev/null +++ b/src/model/forth/vm.rs @@ -0,0 +1,920 @@ +use rand::rngs::StdRng; +use rand::{Rng as RngTrait, SeedableRng}; + +use super::compiler::compile_script; +use super::ops::Op; +use super::types::{ + CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables, +}; + +#[derive(Clone, Debug)] +struct TimeContext { + start: f64, + duration: f64, + subdivisions: Option>, + iteration_index: Option, +} + +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: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), + vars, + dict, + rng, + } + } + + #[allow(dead_code)] + pub fn stack(&self) -> Vec { + self.stack.lock().unwrap().clone() + } + + #[allow(dead_code)] + pub fn clear_stack(&self) { + self.stack.lock().unwrap().clear(); + } + + pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result, String> { + self.evaluate_impl(script, ctx, None) + } + + pub fn evaluate_with_trace( + &self, + script: &str, + ctx: &StepContext, + trace: &mut ExecutionTrace, + ) -> Result, String> { + self.evaluate_impl(script, ctx, Some(trace)) + } + + fn evaluate_impl( + &self, + script: &str, + ctx: &StepContext, + trace: Option<&mut ExecutionTrace>, + ) -> Result, 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, String> { + let mut stack = self.stack.lock().unwrap(); + let mut outputs: Vec = Vec::new(); + let mut time_stack: Vec = vec![TimeContext { + start: 0.0, + duration: ctx.step_duration() * 4.0, + subdivisions: None, + iteration_index: None, + }]; + let mut cmd = CmdRegister::default(); + + self.execute_ops( + ops, + ctx, + &mut stack, + &mut outputs, + &mut time_stack, + &mut cmd, + trace, + )?; + + Ok(outputs) + } + + #[allow(clippy::too_many_arguments)] + fn execute_ops( + &self, + ops: &[Op], + ctx: &StepContext, + stack: &mut Vec, + outputs: &mut Vec, + time_stack: &mut Vec, + cmd: &mut CmdRegister, + trace: Option<&mut ExecutionTrace>, + ) -> Result<(), String> { + let mut pc = 0; + let trace_cell = std::cell::RefCell::new(trace); + + 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::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 => binary_op(stack, |a, b| a / b)?, + Op::Mod => { + let b = stack.pop().ok_or("stack underflow")?.as_int()?; + let a = stack.pop().ok_or("stack underflow")?.as_int()?; + stack.push(Value::Int(a % b, None)); + } + Op::Neg => { + let v = stack.pop().ok_or("stack underflow")?; + match v { + Value::Int(i, s) => stack.push(Value::Int(-i, s)), + Value::Float(f, s) => stack.push(Value::Float(-f, s)), + _ => return Err("expected number".into()), + } + } + Op::Abs => { + let v = stack.pop().ok_or("stack underflow")?; + match v { + Value::Int(i, s) => stack.push(Value::Int(i.abs(), s)), + Value::Float(f, s) => stack.push(Value::Float(f.abs(), s)), + _ => return Err("expected number".into()), + } + } + Op::Floor => { + let v = stack.pop().ok_or("stack underflow")?.as_float()?; + stack.push(Value::Int(v.floor() as i64, None)); + } + Op::Ceil => { + let v = stack.pop().ok_or("stack underflow")?.as_float()?; + stack.push(Value::Int(v.ceil() as i64, None)); + } + Op::Round => { + let v = stack.pop().ok_or("stack underflow")?.as_float()?; + stack.push(Value::Int(v.round() as i64, None)); + } + Op::Min => binary_op(stack, |a, b| a.min(b))?, + Op::Max => binary_op(stack, |a, b| a.max(b))?, + + 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::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 => { + let name = stack.pop().ok_or("stack underflow")?; + let name = name.as_str()?; + cmd.set_sound(name.to_string()); + } + Op::SetParam(param) => { + let val = stack.pop().ok_or("stack underflow")?; + cmd.set_param(param.clone(), val.to_param_string()); + } + Op::Emit => { + let (sound, mut params) = cmd.take().ok_or("no sound set")?; + let mut pairs = vec![("sound".into(), sound)]; + pairs.append(&mut params); + let time_ctx = time_stack.last().ok_or("time stack underflow")?; + if time_ctx.start > 0.0 { + pairs.push(("delta".into(), time_ctx.start.to_string())); + } + if !pairs.iter().any(|(k, _)| k == "dur") { + pairs.push(("dur".into(), time_ctx.duration.to_string())); + } + if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { + let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); + pairs[idx].1 = (ratio * time_ctx.duration).to_string(); + } else { + pairs.push(("delaytime".into(), time_ctx.duration.to_string())); + } + outputs.push(format_cmd(&pairs)); + } + + Op::Get => { + let name = stack.pop().ok_or("stack underflow")?; + let name = name.as_str()?; + let vars = self.vars.lock().unwrap(); + let val = vars.get(name).cloned().unwrap_or(Value::Int(0, None)); + 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")?; + self.vars.lock().unwrap().insert(name, val); + } + + Op::GetContext(name) => { + let val = match name.as_str() { + "step" => Value::Int(ctx.step as i64, None), + "beat" => Value::Float(ctx.beat, 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), + _ => Value::Int(0, None), + }; + stack.push(val); + } + + Op::Rand => { + let max = stack.pop().ok_or("stack underflow")?.as_float()?; + let min = stack.pop().ok_or("stack underflow")?.as_float()?; + let val = self.rng.lock().unwrap().gen_range(min..max); + stack.push(Value::Float(val, None)); + } + Op::Rrand => { + let max = stack.pop().ok_or("stack underflow")?.as_int()?; + let min = stack.pop().ok_or("stack underflow")?.as_int()?; + let val = self.rng.lock().unwrap().gen_range(min..=max); + stack.push(Value::Int(val, None)); + } + Op::Seed => { + let s = stack.pop().ok_or("stack underflow")?.as_int()?; + *self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64); + } + + Op::Cycle => { + let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; + if count == 0 { + return Err("cycle count must be > 0".into()); + } + if stack.len() < count { + return Err("stack underflow".into()); + } + let start = stack.len() - count; + let values: Vec = stack.drain(start..).collect(); + let idx = ctx.runs % count; + let selected = values[idx].clone(); + if let Some(span) = selected.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } + } + if let Value::Quotation(quot_ops, body_span) = selected { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } else { + stack.push(selected); + } + } + + Op::PCycle => { + let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; + if count == 0 { + return Err("pcycle count must be > 0".into()); + } + if stack.len() < count { + return Err("stack underflow".into()); + } + let start = stack.len() - count; + let values: Vec = stack.drain(start..).collect(); + let idx = ctx.iter % count; + let selected = values[idx].clone(); + if let Some(span) = selected.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } + } + if let Value::Quotation(quot_ops, body_span) = selected { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } else { + stack.push(selected); + } + } + + 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()); + } + if stack.len() < count { + return Err("stack underflow".into()); + } + let start = stack.len() - count; + let values: Vec = stack.drain(start..).collect(); + let idx = self.rng.lock().unwrap().gen_range(0..count); + let selected = values[idx].clone(); + if let Some(span) = selected.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } + } + if let Value::Quotation(quot_ops, body_span) = selected { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } else { + stack.push(selected); + } + } + + Op::ChanceExec => { + let prob = stack.pop().ok_or("stack underflow")?.as_float()?; + let quot = stack.pop().ok_or("stack underflow")?; + let val: f64 = self.rng.lock().unwrap().gen(); + if val < prob { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } + _ => return Err("expected quotation".into()), + } + } + } + + Op::ProbExec => { + let pct = stack.pop().ok_or("stack underflow")?.as_float()?; + let quot = stack.pop().ok_or("stack underflow")?; + let val: f64 = self.rng.lock().unwrap().gen(); + if val < pct / 100.0 { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } + _ => return Err("expected quotation".into()), + } + } + } + + Op::Coin => { + let val: f64 = self.rng.lock().unwrap().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 => { + let cond = stack.pop().ok_or("stack underflow")?; + let quot = stack.pop().ok_or("stack underflow")?; + if cond.is_truthy() { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } + _ => return Err("expected quotation".into()), + } + } + } + + Op::Unless => { + let cond = stack.pop().ok_or("stack underflow")?; + let quot = stack.pop().ok_or("stack underflow")?; + if !cond.is_truthy() { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } + _ => return Err("expected quotation".into()), + } + } + } + + 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::At => { + let pos = stack.pop().ok_or("stack underflow")?.as_float()?; + let parent = time_stack.last().ok_or("time stack underflow")?; + let new_start = parent.start + parent.duration * pos; + time_stack.push(TimeContext { + start: new_start, + duration: parent.duration * (1.0 - pos), + subdivisions: None, + iteration_index: parent.iteration_index, + }); + } + + Op::Window => { + let end = stack.pop().ok_or("stack underflow")?.as_float()?; + let start_pos = stack.pop().ok_or("stack underflow")?.as_float()?; + let parent = time_stack.last().ok_or("time stack underflow")?; + let new_start = parent.start + parent.duration * start_pos; + let new_duration = parent.duration * (end - start_pos); + time_stack.push(TimeContext { + start: new_start, + duration: new_duration, + subdivisions: None, + iteration_index: parent.iteration_index, + }); + } + + Op::Scale => { + let factor = stack.pop().ok_or("stack underflow")?.as_float()?; + let parent = time_stack.last().ok_or("time stack underflow")?; + time_stack.push(TimeContext { + start: parent.start, + duration: parent.duration * factor, + subdivisions: None, + iteration_index: parent.iteration_index, + }); + } + + Op::Pop => { + if time_stack.len() <= 1 { + return Err("cannot pop root time context".into()); + } + time_stack.pop(); + } + + Op::Subdivide => { + let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; + if n == 0 { + return Err("subdivide count must be > 0".into()); + } + let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; + let sub_duration = time_ctx.duration / n as f64; + let mut subs = Vec::with_capacity(n); + for i in 0..n { + subs.push((time_ctx.start + sub_duration * i as f64, sub_duration)); + } + time_ctx.subdivisions = Some(subs); + } + + Op::Each => { + let (sound, params) = cmd.take().ok_or("no sound set")?; + let time_ctx = time_stack.last().ok_or("time stack underflow")?; + let subs = time_ctx + .subdivisions + .as_ref() + .ok_or("each requires subdivide first")?; + for (sub_start, sub_dur) in subs { + let mut pairs = vec![("sound".into(), sound.clone())]; + pairs.extend(params.iter().cloned()); + if *sub_start > 0.0 { + pairs.push(("delta".into(), sub_start.to_string())); + } + if !pairs.iter().any(|(k, _)| k == "dur") { + pairs.push(("dur".into(), sub_dur.to_string())); + } + if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { + let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); + pairs[idx].1 = (ratio * sub_dur).to_string(); + } else { + pairs.push(("delaytime".into(), sub_dur.to_string())); + } + outputs.push(format_cmd(&pairs)); + } + } + + Op::SetTempo => { + let tempo = stack.pop().ok_or("stack underflow")?.as_float()?; + let clamped = tempo.clamp(20.0, 300.0); + self.vars + .lock() + .unwrap() + .insert("__tempo__".to_string(), Value::Float(clamped, None)); + } + + Op::ListStart => { + stack.push(Value::Marker); + } + + Op::ListEnd => { + let mut count = 0; + let mut values = Vec::new(); + while let Some(v) = stack.pop() { + if v.is_marker() { + break; + } + values.push(v); + count += 1; + } + values.reverse(); + for v in values { + stack.push(v); + } + stack.push(Value::Int(count, None)); + } + + Op::ListEndCycle => { + let mut values = Vec::new(); + while let Some(v) = stack.pop() { + if v.is_marker() { + break; + } + values.push(v); + } + if values.is_empty() { + return Err("empty cycle list".into()); + } + values.reverse(); + let idx = ctx.runs % values.len(); + let selected = values[idx].clone(); + if let Some(span) = selected.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } + } + if let Value::Quotation(quot_ops, body_span) = selected { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } else { + stack.push(selected); + } + } + + Op::ListEndPCycle => { + let mut values = Vec::new(); + while let Some(v) = stack.pop() { + if v.is_marker() { + break; + } + values.push(v); + } + if values.is_empty() { + return Err("empty pattern cycle list".into()); + } + values.reverse(); + let idx = ctx.iter % values.len(); + let selected = values[idx].clone(); + if let Some(span) = selected.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } + } + if let Value::Quotation(quot_ops, body_span) = selected { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } else { + stack.push(selected); + } + } + + 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".into(), a.to_param_string()); + cmd.set_param("decay".into(), d.to_param_string()); + cmd.set_param("sustain".into(), s.to_param_string()); + cmd.set_param("release".into(), r.to_param_string()); + } + + Op::Ad => { + let d = stack.pop().ok_or("stack underflow")?; + let a = stack.pop().ok_or("stack underflow")?; + cmd.set_param("attack".into(), a.to_param_string()); + cmd.set_param("decay".into(), d.to_param_string()); + cmd.set_param("sustain".into(), "0".into()); + } + + Op::Stack => { + let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; + if n == 0 { + return Err("stack count must be > 0".into()); + } + let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; + let sub_duration = time_ctx.duration / n as f64; + let mut subs = Vec::with_capacity(n); + for _ in 0..n { + subs.push((time_ctx.start, sub_duration)); + } + time_ctx.subdivisions = Some(subs); + } + + Op::Echo => { + let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; + if n == 0 { + return Err("echo count must be > 0".into()); + } + let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; + // Geometric series: d1 * (2 - 2^(1-n)) = total + let d1 = time_ctx.duration / (2.0 - 2.0_f64.powi(1 - n as i32)); + let mut subs = Vec::with_capacity(n); + for i in 0..n { + let dur = d1 / 2.0_f64.powi(i as i32); + let start = if i == 0 { + time_ctx.start + } else { + time_ctx.start + d1 * (2.0 - 2.0_f64.powi(1 - i as i32)) + }; + subs.push((start, dur)); + } + time_ctx.subdivisions = Some(subs); + } + + Op::Necho => { + let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize; + if n == 0 { + return Err("necho count must be > 0".into()); + } + let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?; + // Reverse geometric: d1 + 2*d1 + 4*d1 + ... = d1 * (2^n - 1) = total + let d1 = time_ctx.duration / (2.0_f64.powi(n as i32) - 1.0); + let mut subs = Vec::with_capacity(n); + for i in 0..n { + let dur = d1 * 2.0_f64.powi(i as i32); + let start = if i == 0 { + time_ctx.start + } else { + // Sum of previous durations: d1 * (2^i - 1) + time_ctx.start + d1 * (2.0_f64.powi(i as i32) - 1.0) + }; + subs.push((start, dur)); + } + time_ctx.subdivisions = Some(subs); + } + + Op::For => { + let quot = stack.pop().ok_or("stack underflow")?; + let time_ctx = time_stack.last().ok_or("time stack underflow")?; + let subs = time_ctx + .subdivisions + .clone() + .ok_or("for requires subdivide first")?; + + match quot { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } + } + for (i, (sub_start, sub_dur)) in subs.iter().enumerate() { + time_stack.push(TimeContext { + start: *sub_start, + duration: *sub_dur, + subdivisions: None, + iteration_index: Some(i), + }); + let mut trace_opt = trace_cell.borrow_mut().take(); + self.execute_ops( + "_ops, + ctx, + stack, + outputs, + time_stack, + cmd, + trace_opt.as_deref_mut(), + )?; + *trace_cell.borrow_mut() = trace_opt; + time_stack.pop(); + } + } + _ => return Err("expected quotation".into()), + } + } + + Op::LocalCycleEnd => { + let mut values = Vec::new(); + while let Some(v) = stack.pop() { + if v.is_marker() { + break; + } + values.push(v); + } + if values.is_empty() { + return Err("empty local cycle list".into()); + } + values.reverse(); + let time_ctx = time_stack.last().ok_or("time stack underflow")?; + let idx = time_ctx.iteration_index.unwrap_or(0) % values.len(); + let selected = values[idx].clone(); + if let Some(span) = selected.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } + } + if let Value::Quotation(quot_ops, body_span) = selected { + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } else { + stack.push(selected); + } + } + + Op::Apply => { + let quot = stack.pop().ok_or("stack underflow")?; + 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(); + self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?; + *trace_cell.borrow_mut() = trace_opt; + } + _ => return Err("expected quotation".into()), + } + } + } + pc += 1; + } + + Ok(()) + } +} + +fn binary_op(stack: &mut Vec, f: F) -> Result<(), String> +where + F: Fn(f64, f64) -> f64, +{ + let b = stack.pop().ok_or("stack underflow")?.as_float()?; + let a = stack.pop().ok_or("stack underflow")?.as_float()?; + let result = f(a, b); + if result.fract() == 0.0 && result.abs() < i64::MAX as f64 { + stack.push(Value::Int(result as i64, None)); + } else { + stack.push(Value::Float(result, None)); + } + 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")?.as_float()?; + let a = stack.pop().ok_or("stack underflow")?.as_float()?; + stack.push(Value::Int(if f(a, b) { 1 } else { 0 }, None)); + Ok(()) +} + +fn format_cmd(pairs: &[(String, String)]) -> String { + let parts: Vec = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect(); + format!("/{}", parts.join("/")) +} diff --git a/src/model/forth/words.rs b/src/model/forth/words.rs new file mode 100644 index 0000000..85cb53a --- /dev/null +++ b/src/model/forth/words.rs @@ -0,0 +1,1631 @@ +use super::ops::Op; +use super::types::{Dictionary, SourceSpan}; + +pub enum WordCompile { + Simple, + Context(&'static str), + Param, + Alias(&'static str), + Probability(f64), +} + +pub struct Word { + pub name: &'static str, + pub stack: &'static str, + pub desc: &'static str, + pub example: &'static str, + pub compile: WordCompile, +} + +use WordCompile::*; + +pub const WORDS: &[Word] = &[ + // Stack manipulation + Word { + name: "dup", + stack: "(a -- a a)", + desc: "Duplicate top of stack", + example: "3 dup => 3 3", + compile: Simple, + }, + Word { + name: "dupn", + stack: "(a n -- a a ... a)", + desc: "Duplicate a onto stack n times", + example: "2 4 dupn => 2 2 2 2", + compile: Simple, + }, + Word { + name: "!", + stack: "(a n -- a a ... a)", + desc: "Duplicate a onto stack n times (alias for dupn)", + example: "2 4 ! => 2 2 2 2", + compile: Alias("dupn"), + }, + Word { + name: "drop", + stack: "(a --)", + desc: "Remove top of stack", + example: "1 2 drop => 1", + compile: Simple, + }, + Word { + name: "swap", + stack: "(a b -- b a)", + desc: "Exchange top two items", + example: "1 2 swap => 2 1", + compile: Simple, + }, + Word { + name: "over", + stack: "(a b -- a b a)", + desc: "Copy second to top", + example: "1 2 over => 1 2 1", + compile: Simple, + }, + Word { + name: "rot", + stack: "(a b c -- b c a)", + desc: "Rotate top three", + example: "1 2 3 rot => 2 3 1", + compile: Simple, + }, + Word { + name: "nip", + stack: "(a b -- b)", + desc: "Remove second item", + example: "1 2 nip => 2", + compile: Simple, + }, + Word { + name: "tuck", + stack: "(a b -- b a b)", + desc: "Copy top under second", + example: "1 2 tuck => 2 1 2", + compile: Simple, + }, + // Arithmetic + Word { + name: "+", + stack: "(a b -- a+b)", + desc: "Add", + example: "2 3 + => 5", + compile: Simple, + }, + Word { + name: "-", + stack: "(a b -- a-b)", + desc: "Subtract", + example: "5 3 - => 2", + compile: Simple, + }, + Word { + name: "*", + stack: "(a b -- a*b)", + desc: "Multiply", + example: "3 4 * => 12", + compile: Simple, + }, + Word { + name: "/", + stack: "(a b -- a/b)", + desc: "Divide", + example: "10 2 / => 5", + compile: Simple, + }, + Word { + name: "mod", + stack: "(a b -- a%b)", + desc: "Modulo", + example: "7 3 mod => 1", + compile: Simple, + }, + Word { + name: "neg", + stack: "(a -- -a)", + desc: "Negate", + example: "5 neg => -5", + compile: Simple, + }, + Word { + name: "abs", + stack: "(a -- |a|)", + desc: "Absolute value", + example: "-5 abs => 5", + compile: Simple, + }, + Word { + name: "floor", + stack: "(f -- n)", + desc: "Round down to integer", + example: "3.7 floor => 3", + compile: Simple, + }, + Word { + name: "ceil", + stack: "(f -- n)", + desc: "Round up to integer", + example: "3.2 ceil => 4", + compile: Simple, + }, + Word { + name: "round", + stack: "(f -- n)", + desc: "Round to nearest integer", + example: "3.5 round => 4", + compile: Simple, + }, + Word { + name: "min", + stack: "(a b -- min)", + desc: "Minimum of two values", + example: "3 5 min => 3", + compile: Simple, + }, + Word { + name: "max", + stack: "(a b -- max)", + desc: "Maximum of two values", + example: "3 5 max => 5", + compile: Simple, + }, + // Comparison + Word { + name: "=", + stack: "(a b -- bool)", + desc: "Equal", + example: "3 3 = => 1", + compile: Simple, + }, + Word { + name: "<>", + stack: "(a b -- bool)", + desc: "Not equal", + example: "3 4 <> => 1", + compile: Simple, + }, + Word { + name: "lt", + stack: "(a b -- bool)", + desc: "Less than", + example: "2 3 lt => 1", + compile: Simple, + }, + Word { + name: "gt", + stack: "(a b -- bool)", + desc: "Greater than", + example: "3 2 gt => 1", + compile: Simple, + }, + Word { + name: "<=", + stack: "(a b -- bool)", + desc: "Less or equal", + example: "3 3 <= => 1", + compile: Simple, + }, + Word { + name: ">=", + stack: "(a b -- bool)", + desc: "Greater or equal", + example: "3 3 >= => 1", + compile: Simple, + }, + // Logic + Word { + name: "and", + stack: "(a b -- bool)", + desc: "Logical and", + example: "1 1 and => 1", + compile: Simple, + }, + Word { + name: "or", + stack: "(a b -- bool)", + desc: "Logical or", + example: "0 1 or => 1", + compile: Simple, + }, + Word { + name: "not", + stack: "(a -- bool)", + desc: "Logical not", + example: "0 not => 1", + compile: Simple, + }, + // Sound + Word { + name: "sound", + stack: "(name --)", + desc: "Begin sound command", + example: "\"kick\" sound", + compile: Simple, + }, + Word { + name: "s", + stack: "(name --)", + desc: "Alias for sound", + example: "\"kick\" s", + compile: Alias("sound"), + }, + Word { + name: "emit", + stack: "(--)", + desc: "Output current sound", + example: "\"kick\" s emit", + compile: Simple, + }, + Word { + name: "@", + stack: "(--)", + desc: "Alias for emit", + example: "\"kick\" s 0.5 at @ pop", + compile: Alias("emit"), + }, + // Variables (prefix syntax: @name to fetch, !name to store) + Word { + name: "@", + stack: "( -- val)", + desc: "Fetch variable value", + example: "@freq => 440", + compile: Simple, + }, + Word { + name: "!", + stack: "(val --)", + desc: "Store value in variable", + example: "440 !freq", + compile: Simple, + }, + // Randomness + Word { + name: "rand", + stack: "(min max -- f)", + desc: "Random float in range", + example: "0 1 rand => 0.42", + compile: Simple, + }, + Word { + name: "rrand", + stack: "(min max -- n)", + desc: "Random int in range", + example: "1 6 rrand => 4", + compile: Simple, + }, + Word { + name: "seed", + stack: "(n --)", + desc: "Set random seed", + example: "12345 seed", + compile: Simple, + }, + Word { + name: "coin", + stack: "(-- bool)", + desc: "50/50 random boolean", + example: "coin => 0 or 1", + compile: Simple, + }, + Word { + name: "chance", + stack: "(quot prob --)", + desc: "Execute quotation with probability (0.0-1.0)", + example: "{ 2 distort } 0.75 chance", + compile: Simple, + }, + Word { + name: "prob", + stack: "(quot pct --)", + desc: "Execute quotation with probability (0-100)", + example: "{ 2 distort } 75 prob", + compile: Simple, + }, + Word { + name: "choose", + stack: "(..n n -- val)", + desc: "Random pick from n items", + example: "1 2 3 3 choose", + compile: Simple, + }, + Word { + name: "cycle", + stack: "(..n n -- val)", + desc: "Cycle through n items by step", + example: "1 2 3 3 cycle", + compile: Simple, + }, + Word { + name: "pcycle", + stack: "(..n n -- val)", + desc: "Cycle through n items by pattern", + example: "1 2 3 3 pcycle", + compile: Simple, + }, + Word { + name: "every", + stack: "(n -- bool)", + desc: "True every nth iteration", + example: "4 every", + compile: Simple, + }, + // Probability shortcuts + Word { + name: "always", + stack: "(quot --)", + desc: "Always execute quotation", + example: "{ 2 distort } always", + compile: Probability(1.0), + }, + Word { + name: "never", + stack: "(quot --)", + desc: "Never execute quotation", + example: "{ 2 distort } never", + compile: Probability(0.0), + }, + Word { + name: "often", + stack: "(quot --)", + desc: "Execute quotation 75% of the time", + example: "{ 2 distort } often", + compile: Probability(0.75), + }, + Word { + name: "sometimes", + stack: "(quot --)", + desc: "Execute quotation 50% of the time", + example: "{ 2 distort } sometimes", + compile: Probability(0.5), + }, + Word { + name: "rarely", + stack: "(quot --)", + desc: "Execute quotation 25% of the time", + example: "{ 2 distort } rarely", + compile: Probability(0.25), + }, + Word { + name: "almostNever", + stack: "(quot --)", + desc: "Execute quotation 10% of the time", + example: "{ 2 distort } almostNever", + compile: Probability(0.1), + }, + Word { + name: "almostAlways", + stack: "(quot --)", + desc: "Execute quotation 90% of the time", + example: "{ 2 distort } almostAlways", + compile: Probability(0.9), + }, + // Context + Word { + name: "step", + stack: "(-- n)", + desc: "Current step index", + example: "step => 0", + compile: Context("step"), + }, + Word { + name: "beat", + stack: "(-- f)", + desc: "Current beat position", + example: "beat => 4.5", + compile: Context("beat"), + }, + Word { + name: "bank", + stack: "(str --)", + desc: "Set sample bank suffix", + example: "\"a\" bank", + compile: Param, + }, + Word { + name: "pattern", + stack: "(-- n)", + desc: "Current pattern index", + example: "pattern => 0", + compile: Context("pattern"), + }, + Word { + name: "tempo", + stack: "(-- f)", + desc: "Current BPM", + example: "tempo => 120.0", + compile: Context("tempo"), + }, + Word { + name: "phase", + stack: "(-- f)", + desc: "Phase in bar (0-1)", + example: "phase => 0.25", + compile: Context("phase"), + }, + Word { + name: "slot", + stack: "(-- n)", + desc: "Current slot number", + example: "slot => 0", + compile: Context("slot"), + }, + Word { + name: "runs", + stack: "(-- n)", + desc: "Times this step ran", + example: "runs => 3", + compile: Context("runs"), + }, + Word { + name: "iter", + stack: "(-- n)", + desc: "Pattern iteration count", + example: "iter => 2", + compile: Context("iter"), + }, + Word { + name: "stepdur", + stack: "(-- f)", + desc: "Step duration in seconds", + example: "stepdur => 0.125", + compile: Context("stepdur"), + }, + // Live keys + Word { + name: "fill", + stack: "(-- bool)", + desc: "True when fill is on (f key)", + example: "{ 4 div each } fill ?", + compile: Context("fill"), + }, + // Music + Word { + name: "mtof", + stack: "(midi -- hz)", + desc: "MIDI note to frequency", + example: "69 mtof => 440.0", + compile: Simple, + }, + Word { + name: "ftom", + stack: "(hz -- midi)", + desc: "Frequency to MIDI note", + example: "440 ftom => 69.0", + compile: Simple, + }, + // Time + Word { + name: "at", + stack: "(pos --)", + desc: "Position in time (push context)", + example: "\"kick\" s 0.5 at emit pop", + compile: Simple, + }, + Word { + name: "zoom", + stack: "(start end --)", + desc: "Zoom into time region", + example: "0.0 0.5 zoom", + compile: Simple, + }, + Word { + name: "scale!", + stack: "(factor --)", + desc: "Scale time context duration", + example: "2 scale!", + compile: Simple, + }, + Word { + name: "pop", + stack: "(--)", + desc: "Pop time context", + example: "pop", + compile: Simple, + }, + Word { + name: "div", + stack: "(n --)", + desc: "Subdivide time into n", + example: "4 div", + compile: Simple, + }, + Word { + name: "each", + stack: "(--)", + desc: "Emit at each subdivision", + example: "4 div each", + compile: Simple, + }, + Word { + name: "stack", + stack: "(n --)", + desc: "Create n subdivisions at same time", + example: "3 stack", + compile: Simple, + }, + Word { + name: "echo", + stack: "(n --)", + desc: "Create n subdivisions with halving durations (stutter)", + example: "3 echo", + compile: Simple, + }, + Word { + name: "necho", + stack: "(n --)", + desc: "Create n subdivisions with doubling durations (swell)", + example: "3 necho", + compile: Simple, + }, + Word { + name: "for", + stack: "(quot --)", + desc: "Execute quotation for each subdivision", + example: "{ emit } 3 div for", + compile: Simple, + }, + Word { + name: "|", + stack: "(-- marker)", + desc: "Start local cycle list", + example: "| 60 62 64 |", + compile: Simple, + }, + Word { + name: "tempo!", + stack: "(bpm --)", + desc: "Set global tempo", + example: "140 tempo!", + compile: Simple, + }, + // Lists + Word { + name: "[", + stack: "(-- marker)", + desc: "Start list", + example: "[ 1 2 3 ]", + compile: Simple, + }, + Word { + name: "]", + stack: "(marker..n -- n)", + desc: "End list, push count", + example: "[ 1 2 3 ] => 3", + compile: Simple, + }, + Word { + name: "<", + stack: "(-- marker)", + desc: "Start cycle list", + example: "< 1 2 3 >", + compile: Alias("["), + }, + Word { + name: ">", + stack: "(marker..n -- val)", + desc: "End cycle list, pick by step", + example: "< 1 2 3 > => cycles through 1, 2, 3", + compile: Simple, + }, + Word { + name: "<<", + stack: "(-- marker)", + desc: "Start pattern cycle list", + example: "<< 1 2 3 >>", + compile: Alias("["), + }, + Word { + name: ">>", + stack: "(marker..n -- val)", + desc: "End pattern cycle list, pick by pattern", + example: "<< 1 2 3 >> => cycles through 1, 2, 3 per pattern", + compile: Simple, + }, + // Quotations + Word { + name: "?", + stack: "(quot bool --)", + desc: "Execute quotation if true", + example: "{ 2 distort } 0.5 chance ?", + compile: Simple, + }, + Word { + name: "!?", + stack: "(quot bool --)", + desc: "Execute quotation if false", + example: "{ 1 distort } 0.5 chance !?", + compile: Simple, + }, + // Parameters (synthesis) + Word { + name: "time", + stack: "(f --)", + desc: "Set time offset", + example: "0.1 time", + compile: Param, + }, + Word { + name: "repeat", + stack: "(n --)", + desc: "Set repeat count", + example: "4 repeat", + compile: Param, + }, + Word { + name: "dur", + stack: "(f --)", + desc: "Set duration", + example: "0.5 dur", + compile: Param, + }, + Word { + name: "gate", + stack: "(f --)", + desc: "Set gate time", + example: "0.8 gate", + compile: Param, + }, + Word { + name: "freq", + stack: "(f --)", + desc: "Set frequency (Hz)", + example: "440 freq", + compile: Param, + }, + Word { + name: "detune", + stack: "(f --)", + desc: "Set detune amount", + example: "0.01 detune", + compile: Param, + }, + Word { + name: "speed", + stack: "(f --)", + desc: "Set playback speed", + example: "1.5 speed", + compile: Param, + }, + Word { + name: "glide", + stack: "(f --)", + desc: "Set glide/portamento", + example: "0.1 glide", + compile: Param, + }, + Word { + name: "pw", + stack: "(f --)", + desc: "Set pulse width", + example: "0.5 pw", + compile: Param, + }, + Word { + name: "spread", + stack: "(f --)", + desc: "Set stereo spread", + example: "0.5 spread", + compile: Param, + }, + Word { + name: "mult", + stack: "(f --)", + desc: "Set multiplier", + example: "2 mult", + compile: Param, + }, + Word { + name: "warp", + stack: "(f --)", + desc: "Set warp amount", + example: "0.5 warp", + compile: Param, + }, + Word { + name: "mirror", + stack: "(f --)", + desc: "Set mirror", + example: "1 mirror", + compile: Param, + }, + Word { + name: "harmonics", + stack: "(f --)", + desc: "Set harmonics", + example: "4 harmonics", + compile: Param, + }, + Word { + name: "timbre", + stack: "(f --)", + desc: "Set timbre", + example: "0.5 timbre", + compile: Param, + }, + Word { + name: "morph", + stack: "(f --)", + desc: "Set morph", + example: "0.5 morph", + compile: Param, + }, + Word { + name: "begin", + stack: "(f --)", + desc: "Set sample start (0-1)", + example: "0.25 begin", + compile: Param, + }, + Word { + name: "end", + stack: "(f --)", + desc: "Set sample end (0-1)", + example: "0.75 end", + compile: Param, + }, + Word { + name: "gain", + stack: "(f --)", + desc: "Set volume (0-1)", + example: "0.8 gain", + compile: Param, + }, + Word { + name: "postgain", + stack: "(f --)", + desc: "Set post gain", + example: "1.2 postgain", + compile: Param, + }, + Word { + name: "velocity", + stack: "(f --)", + desc: "Set velocity", + example: "100 velocity", + compile: Param, + }, + Word { + name: "pan", + stack: "(f --)", + desc: "Set pan (-1 to 1)", + example: "0.5 pan", + compile: Param, + }, + Word { + name: "attack", + stack: "(f --)", + desc: "Set attack time", + example: "0.01 attack", + compile: Param, + }, + Word { + name: "decay", + stack: "(f --)", + desc: "Set decay time", + example: "0.1 decay", + compile: Param, + }, + Word { + name: "sustain", + stack: "(f --)", + desc: "Set sustain level", + example: "0.5 sustain", + compile: Param, + }, + Word { + name: "release", + stack: "(f --)", + desc: "Set release time", + example: "0.3 release", + compile: Param, + }, + Word { + name: "adsr", + stack: "(a d s r --)", + desc: "Set attack, decay, sustain, release", + example: "0.01 0.1 0.5 0.3 adsr", + compile: Simple, + }, + Word { + name: "ad", + stack: "(a d --)", + desc: "Set attack, decay (sustain=0)", + example: "0.01 0.1 ad", + compile: Simple, + }, + Word { + name: "lpf", + stack: "(f --)", + desc: "Set lowpass frequency", + example: "2000 lpf", + compile: Param, + }, + Word { + name: "lpq", + stack: "(f --)", + desc: "Set lowpass resonance", + example: "0.5 lpq", + compile: Param, + }, + Word { + name: "lpe", + stack: "(f --)", + desc: "Set lowpass envelope", + example: "0.5 lpe", + compile: Param, + }, + Word { + name: "lpa", + stack: "(f --)", + desc: "Set lowpass attack", + example: "0.01 lpa", + compile: Param, + }, + Word { + name: "lpd", + stack: "(f --)", + desc: "Set lowpass decay", + example: "0.1 lpd", + compile: Param, + }, + Word { + name: "lps", + stack: "(f --)", + desc: "Set lowpass sustain", + example: "0.5 lps", + compile: Param, + }, + Word { + name: "lpr", + stack: "(f --)", + desc: "Set lowpass release", + example: "0.3 lpr", + compile: Param, + }, + Word { + name: "hpf", + stack: "(f --)", + desc: "Set highpass frequency", + example: "100 hpf", + compile: Param, + }, + Word { + name: "hpq", + stack: "(f --)", + desc: "Set highpass resonance", + example: "0.5 hpq", + compile: Param, + }, + Word { + name: "hpe", + stack: "(f --)", + desc: "Set highpass envelope", + example: "0.5 hpe", + compile: Param, + }, + Word { + name: "hpa", + stack: "(f --)", + desc: "Set highpass attack", + example: "0.01 hpa", + compile: Param, + }, + Word { + name: "hpd", + stack: "(f --)", + desc: "Set highpass decay", + example: "0.1 hpd", + compile: Param, + }, + Word { + name: "hps", + stack: "(f --)", + desc: "Set highpass sustain", + example: "0.5 hps", + compile: Param, + }, + Word { + name: "hpr", + stack: "(f --)", + desc: "Set highpass release", + example: "0.3 hpr", + compile: Param, + }, + Word { + name: "bpf", + stack: "(f --)", + desc: "Set bandpass frequency", + example: "1000 bpf", + compile: Param, + }, + Word { + name: "bpq", + stack: "(f --)", + desc: "Set bandpass resonance", + example: "0.5 bpq", + compile: Param, + }, + Word { + name: "bpe", + stack: "(f --)", + desc: "Set bandpass envelope", + example: "0.5 bpe", + compile: Param, + }, + Word { + name: "bpa", + stack: "(f --)", + desc: "Set bandpass attack", + example: "0.01 bpa", + compile: Param, + }, + Word { + name: "bpd", + stack: "(f --)", + desc: "Set bandpass decay", + example: "0.1 bpd", + compile: Param, + }, + Word { + name: "bps", + stack: "(f --)", + desc: "Set bandpass sustain", + example: "0.5 bps", + compile: Param, + }, + Word { + name: "bpr", + stack: "(f --)", + desc: "Set bandpass release", + example: "0.3 bpr", + compile: Param, + }, + Word { + name: "ftype", + stack: "(n --)", + desc: "Set filter type", + example: "1 ftype", + compile: Param, + }, + Word { + name: "penv", + stack: "(f --)", + desc: "Set pitch envelope", + example: "0.5 penv", + compile: Param, + }, + Word { + name: "patt", + stack: "(f --)", + desc: "Set pitch attack", + example: "0.01 patt", + compile: Param, + }, + Word { + name: "pdec", + stack: "(f --)", + desc: "Set pitch decay", + example: "0.1 pdec", + compile: Param, + }, + Word { + name: "psus", + stack: "(f --)", + desc: "Set pitch sustain", + example: "0 psus", + compile: Param, + }, + Word { + name: "prel", + stack: "(f --)", + desc: "Set pitch release", + example: "0.1 prel", + compile: Param, + }, + Word { + name: "vib", + stack: "(f --)", + desc: "Set vibrato rate", + example: "5 vib", + compile: Param, + }, + Word { + name: "vibmod", + stack: "(f --)", + desc: "Set vibrato depth", + example: "0.5 vibmod", + compile: Param, + }, + Word { + name: "vibshape", + stack: "(f --)", + desc: "Set vibrato shape", + example: "0 vibshape", + compile: Param, + }, + Word { + name: "fm", + stack: "(f --)", + desc: "Set FM frequency", + example: "200 fm", + compile: Param, + }, + Word { + name: "fmh", + stack: "(f --)", + desc: "Set FM harmonic ratio", + example: "2 fmh", + compile: Param, + }, + Word { + name: "fmshape", + stack: "(f --)", + desc: "Set FM shape", + example: "0 fmshape", + compile: Param, + }, + Word { + name: "fme", + stack: "(f --)", + desc: "Set FM envelope", + example: "0.5 fme", + compile: Param, + }, + Word { + name: "fma", + stack: "(f --)", + desc: "Set FM attack", + example: "0.01 fma", + compile: Param, + }, + Word { + name: "fmd", + stack: "(f --)", + desc: "Set FM decay", + example: "0.1 fmd", + compile: Param, + }, + Word { + name: "fms", + stack: "(f --)", + desc: "Set FM sustain", + example: "0.5 fms", + compile: Param, + }, + Word { + name: "fmr", + stack: "(f --)", + desc: "Set FM release", + example: "0.1 fmr", + compile: Param, + }, + Word { + name: "am", + stack: "(f --)", + desc: "Set AM frequency", + example: "10 am", + compile: Param, + }, + Word { + name: "amdepth", + stack: "(f --)", + desc: "Set AM depth", + example: "0.5 amdepth", + compile: Param, + }, + Word { + name: "amshape", + stack: "(f --)", + desc: "Set AM shape", + example: "0 amshape", + compile: Param, + }, + Word { + name: "rm", + stack: "(f --)", + desc: "Set RM frequency", + example: "100 rm", + compile: Param, + }, + Word { + name: "rmdepth", + stack: "(f --)", + desc: "Set RM depth", + example: "0.5 rmdepth", + compile: Param, + }, + Word { + name: "rmshape", + stack: "(f --)", + desc: "Set RM shape", + example: "0 rmshape", + compile: Param, + }, + Word { + name: "phaser", + stack: "(f --)", + desc: "Set phaser rate", + example: "1 phaser", + compile: Param, + }, + Word { + name: "phaserdepth", + stack: "(f --)", + desc: "Set phaser depth", + example: "0.5 phaserdepth", + compile: Param, + }, + Word { + name: "phasersweep", + stack: "(f --)", + desc: "Set phaser sweep", + example: "0.5 phasersweep", + compile: Param, + }, + Word { + name: "phasercenter", + stack: "(f --)", + desc: "Set phaser center", + example: "1000 phasercenter", + compile: Param, + }, + Word { + name: "flanger", + stack: "(f --)", + desc: "Set flanger rate", + example: "0.5 flanger", + compile: Param, + }, + Word { + name: "flangerdepth", + stack: "(f --)", + desc: "Set flanger depth", + example: "0.5 flangerdepth", + compile: Param, + }, + Word { + name: "flangerfeedback", + stack: "(f --)", + desc: "Set flanger feedback", + example: "0.5 flangerfeedback", + compile: Param, + }, + Word { + name: "chorus", + stack: "(f --)", + desc: "Set chorus rate", + example: "1 chorus", + compile: Param, + }, + Word { + name: "chorusdepth", + stack: "(f --)", + desc: "Set chorus depth", + example: "0.5 chorusdepth", + compile: Param, + }, + Word { + name: "chorusdelay", + stack: "(f --)", + desc: "Set chorus delay", + example: "0.02 chorusdelay", + compile: Param, + }, + Word { + name: "comb", + stack: "(f --)", + desc: "Set comb filter mix", + example: "0.5 comb", + compile: Param, + }, + Word { + name: "combfreq", + stack: "(f --)", + desc: "Set comb frequency", + example: "200 combfreq", + compile: Param, + }, + Word { + name: "combfeedback", + stack: "(f --)", + desc: "Set comb feedback", + example: "0.5 combfeedback", + compile: Param, + }, + Word { + name: "combdamp", + stack: "(f --)", + desc: "Set comb damping", + example: "0.5 combdamp", + compile: Param, + }, + Word { + name: "coarse", + stack: "(f --)", + desc: "Set coarse tune", + example: "12 coarse", + compile: Param, + }, + Word { + name: "crush", + stack: "(f --)", + desc: "Set bit crush", + example: "8 crush", + compile: Param, + }, + Word { + name: "fold", + stack: "(f --)", + desc: "Set wave fold", + example: "2 fold", + compile: Param, + }, + Word { + name: "wrap", + stack: "(f --)", + desc: "Set wave wrap", + example: "0.5 wrap", + compile: Param, + }, + Word { + name: "distort", + stack: "(f --)", + desc: "Set distortion", + example: "0.5 distort", + compile: Param, + }, + Word { + name: "distortvol", + stack: "(f --)", + desc: "Set distortion volume", + example: "0.8 distortvol", + compile: Param, + }, + Word { + name: "delay", + stack: "(f --)", + desc: "Set delay mix", + example: "0.3 delay", + compile: Param, + }, + Word { + name: "delaytime", + stack: "(f --)", + desc: "Set delay time", + example: "0.25 delaytime", + compile: Param, + }, + Word { + name: "delayfeedback", + stack: "(f --)", + desc: "Set delay feedback", + example: "0.5 delayfeedback", + compile: Param, + }, + Word { + name: "delaytype", + stack: "(n --)", + desc: "Set delay type", + example: "1 delaytype", + compile: Param, + }, + Word { + name: "verb", + stack: "(f --)", + desc: "Set reverb mix", + example: "0.3 verb", + compile: Param, + }, + Word { + name: "verbdecay", + stack: "(f --)", + desc: "Set reverb decay", + example: "2 verbdecay", + compile: Param, + }, + Word { + name: "verbdamp", + stack: "(f --)", + desc: "Set reverb damping", + example: "0.5 verbdamp", + compile: Param, + }, + Word { + name: "verbpredelay", + stack: "(f --)", + desc: "Set reverb predelay", + example: "0.02 verbpredelay", + compile: Param, + }, + Word { + name: "verbdiff", + stack: "(f --)", + desc: "Set reverb diffusion", + example: "0.7 verbdiff", + compile: Param, + }, + Word { + name: "voice", + stack: "(n --)", + desc: "Set voice number", + example: "1 voice", + compile: Param, + }, + Word { + name: "orbit", + stack: "(n --)", + desc: "Set orbit/bus", + example: "0 orbit", + compile: Param, + }, + Word { + name: "note", + stack: "(n --)", + desc: "Set MIDI note", + example: "60 note", + compile: Param, + }, + Word { + name: "size", + stack: "(f --)", + desc: "Set size", + example: "1 size", + compile: Param, + }, + Word { + name: "n", + stack: "(n --)", + desc: "Set sample number", + example: "0 n", + compile: Param, + }, + Word { + name: "cut", + stack: "(n --)", + desc: "Set cut group", + example: "1 cut", + compile: Param, + }, + Word { + name: "reset", + stack: "(n --)", + desc: "Reset parameter", + example: "1 reset", + compile: Param, + }, + // Quotation execution + Word { + name: "apply", + stack: "(quot --)", + desc: "Execute quotation unconditionally", + example: "{ 2 * } apply", + compile: Simple, + }, + // Word definitions + Word { + name: ":", + stack: "( -- )", + desc: "Begin word definition", + example: ": kick \"kick\" s emit ;", + compile: Simple, + }, + Word { + name: ";", + stack: "( -- )", + desc: "End word definition", + example: ": kick \"kick\" s emit ;", + compile: Simple, + }, +]; + +pub(super) fn simple_op(name: &str) -> Option { + Some(match name { + "dup" => Op::Dup, + "dupn" => Op::Dupn, + "drop" => Op::Drop, + "swap" => Op::Swap, + "over" => Op::Over, + "rot" => Op::Rot, + "nip" => Op::Nip, + "tuck" => Op::Tuck, + "+" => Op::Add, + "-" => Op::Sub, + "*" => Op::Mul, + "/" => Op::Div, + "mod" => Op::Mod, + "neg" => Op::Neg, + "abs" => Op::Abs, + "floor" => Op::Floor, + "ceil" => Op::Ceil, + "round" => Op::Round, + "min" => Op::Min, + "max" => Op::Max, + "=" => Op::Eq, + "<>" => Op::Ne, + "lt" => Op::Lt, + "gt" => Op::Gt, + "<=" => Op::Le, + ">=" => Op::Ge, + "and" => Op::And, + "or" => Op::Or, + "not" => Op::Not, + "sound" => Op::NewCmd, + "emit" => Op::Emit, + "rand" => Op::Rand, + "rrand" => Op::Rrand, + "seed" => Op::Seed, + "cycle" => Op::Cycle, + "pcycle" => Op::PCycle, + "choose" => Op::Choose, + "every" => Op::Every, + "chance" => Op::ChanceExec, + "prob" => Op::ProbExec, + "coin" => Op::Coin, + "mtof" => Op::Mtof, + "ftom" => Op::Ftom, + "?" => Op::When, + "!?" => Op::Unless, + "at" => Op::At, + "zoom" => Op::Window, + "scale!" => Op::Scale, + "pop" => Op::Pop, + "div" => Op::Subdivide, + "each" => Op::Each, + "tempo!" => Op::SetTempo, + "[" => Op::ListStart, + "]" => Op::ListEnd, + ">" => Op::ListEndCycle, + ">>" => Op::ListEndPCycle, + "adsr" => Op::Adsr, + "ad" => Op::Ad, + "stack" => Op::Stack, + "for" => Op::For, + "echo" => Op::Echo, + "necho" => Op::Necho, + "apply" => Op::Apply, + _ => return None, + }) +} + +/// Parse note names like c4, c#4, cs4, eb4 into MIDI numbers. +/// C4 = 60 (middle C), A4 = 69 (440 Hz reference). +fn parse_note_name(name: &str) -> Option { + let name = name.to_lowercase(); + let bytes = name.as_bytes(); + + if bytes.len() < 2 { + return None; + } + + let base = match bytes[0] { + b'c' => 0, + b'd' => 2, + b'e' => 4, + b'f' => 5, + b'g' => 7, + b'a' => 9, + b'b' => 11, + _ => return None, + }; + + let (modifier, octave_start) = match bytes[1] { + b'#' | b's' => (1, 2), + b'b' if bytes.len() > 2 && bytes[2].is_ascii_digit() => (-1, 2), // flat: eb4, bb4 + b'0'..=b'9' => (0, 1), + _ => return None, + }; + + let octave_str = &name[octave_start..]; + let octave: i64 = octave_str.parse().ok()?; + + if !(-1..=9).contains(&octave) { + return None; + } + + // MIDI: C4 = 60, so C-1 = 0 + Some((octave + 1) * 12 + base + modifier) +} + +/// Parse interval names like m3, M3, P5 into semitone counts. +/// Supports simple intervals (1-8) and compound intervals (9-15). +fn parse_interval(name: &str) -> Option { + // Simple intervals: unison through octave + let simple = match name { + "P1" | "unison" => 0, + "m2" => 1, + "M2" => 2, + "m3" => 3, + "M3" => 4, + "P4" => 5, + "aug4" | "dim5" | "tritone" => 6, + "P5" => 7, + "m6" => 8, + "M6" => 9, + "m7" => 10, + "M7" => 11, + "P8" | "oct" => 12, + // Compound intervals (octave + simple) + "m9" => 13, + "M9" => 14, + "m10" => 15, + "M10" => 16, + "P11" => 17, + "aug11" => 18, + "P12" => 19, + "m13" => 20, + "M13" => 21, + "m14" => 22, + "M14" => 23, + "P15" => 24, + _ => return None, + }; + Some(simple) +} + +pub(super) fn compile_word(name: &str, span: Option, ops: &mut Vec, dict: &Dictionary) -> bool { + for word in WORDS { + if word.name == name { + match &word.compile { + Simple => { + if let Some(op) = simple_op(name) { + ops.push(op); + } + } + Context(ctx) => ops.push(Op::GetContext((*ctx).into())), + Param => ops.push(Op::SetParam(name.into())), + Alias(target) => return compile_word(target, span, ops, dict), + Probability(p) => { + ops.push(Op::PushFloat(*p, None)); + ops.push(Op::ChanceExec); + } + } + return true; + } + } + + // @varname - fetch variable + if let Some(var_name) = name.strip_prefix('@') { + if !var_name.is_empty() { + ops.push(Op::PushStr(var_name.to_string(), span)); + ops.push(Op::Get); + return true; + } + } + + // !varname - store into variable + if let Some(var_name) = name.strip_prefix('!') { + if !var_name.is_empty() { + ops.push(Op::PushStr(var_name.to_string(), span)); + ops.push(Op::Set); + return true; + } + } + + // Note names: c4, c#4, cs4, eb4, etc. -> MIDI number + if let Some(midi) = parse_note_name(name) { + ops.push(Op::PushInt(midi, span)); + return true; + } + + // Intervals: m3, M3, P5, etc. -> dup top, add semitones (for chord building) + if let Some(semitones) = parse_interval(name) { + ops.push(Op::Dup); + ops.push(Op::PushInt(semitones, span)); + ops.push(Op::Add); + return true; + } + + // Internal ops not exposed in WORDS + if let Some(op) = simple_op(name) { + ops.push(op); + return true; + } + + // User-defined words from dictionary + if let Some(body) = dict.lock().unwrap().get(name) { + ops.extend(body.iter().cloned()); + return true; + } + + false +} diff --git a/tests/forth.rs b/tests/forth.rs index d0c95cb..da68029 100644 --- a/tests/forth.rs +++ b/tests/forth.rs @@ -45,3 +45,6 @@ mod intervals; #[path = "forth/definitions.rs"] mod definitions; + +#[path = "forth/list_words.rs"] +mod list_words; diff --git a/tests/forth/iteration.rs b/tests/forth/iteration.rs index 0e35b25..9f3b718 100644 --- a/tests/forth/iteration.rs +++ b/tests/forth/iteration.rs @@ -181,14 +181,14 @@ fn empty_local_cycle() { #[test] fn echo_creates_decaying_subdivisions() { - // stepdur = 0.125, echo 3 - // d1 + d1/2 + d1/4 = d1 * 1.75 = 0.125 - // d1 = 0.125 / 1.75 = 0.0714285714... + // default dur = 0.5, echo 3 + // d1 + d1/2 + d1/4 = d1 * 1.75 = 0.5 + // d1 = 0.5 / 1.75 let outputs = expect_outputs(r#""kick" s 3 echo each"#, 3); let durs = get_durs(&outputs); let deltas = get_deltas(&outputs); - let d1 = 0.125 / 1.75; + let d1 = 0.5 / 1.75; let d2 = d1 / 2.0; let d3 = d1 / 4.0; @@ -220,14 +220,14 @@ fn echo_error_zero_count() { #[test] fn necho_creates_growing_subdivisions() { - // stepdur = 0.125, necho 3 - // d1 + 2*d1 + 4*d1 = d1 * 7 = 0.125 - // d1 = 0.125 / 7 + // default dur = 0.5, necho 3 + // d1 + 2*d1 + 4*d1 = d1 * 7 = 0.5 + // d1 = 0.5 / 7 let outputs = expect_outputs(r#""kick" s 3 necho each"#, 3); let durs = get_durs(&outputs); let deltas = get_deltas(&outputs); - let d1 = 0.125 / 7.0; + let d1 = 0.5 / 7.0; let d2 = d1 * 2.0; let d3 = d1 * 4.0; diff --git a/tests/forth/list_words.rs b/tests/forth/list_words.rs new file mode 100644 index 0000000..83a4360 --- /dev/null +++ b/tests/forth/list_words.rs @@ -0,0 +1,134 @@ +use super::harness::*; + +#[test] +fn choose_word_from_list() { + // 1 2 [ + - ] choose: picks + or -, applies to 1 2 + // With seed 42, choose picks one deterministically + let f = forth(); + let ctx = default_ctx(); + f.evaluate("1 2 [ + - ] choose", &ctx).unwrap(); + let val = stack_int(&f); + assert!(val == 3 || val == -1, "expected 3 or -1, got {}", val); +} + +#[test] +fn cycle_word_from_list() { + // At runs=0, picks first word (dup) + let ctx = ctx_with(|c| c.runs = 0); + let f = run_ctx("5 < dup nip >", &ctx); + assert_eq!(stack_int(&f), 5); // dup leaves 5 5, but stack check takes top + + // At runs=1, picks second word (2 *) + let f = forth(); + let ctx = ctx_with(|c| c.runs = 1); + f.evaluate(": double 2 * ; 5 < dup double >", &ctx).unwrap(); + assert_eq!(stack_int(&f), 10); +} + +#[test] +fn user_word_in_list() { + let f = forth(); + let ctx = ctx_with(|c| c.runs = 0); + f.evaluate(": add3 3 + ; : add5 5 + ; 10 < add3 add5 >", &ctx).unwrap(); + assert_eq!(stack_int(&f), 13); // runs=0 picks add3 +} + +#[test] +fn user_word_in_list_second() { + let f = forth(); + let ctx = ctx_with(|c| c.runs = 1); + f.evaluate(": add3 3 + ; : add5 5 + ; 10 < add3 add5 >", &ctx).unwrap(); + assert_eq!(stack_int(&f), 15); // runs=1 picks add5 +} + +#[test] +fn values_in_list_still_work() { + // Numbers inside lists should still push as values (not quotations) + let ctx = ctx_with(|c| c.runs = 0); + let f = run_ctx("< 10 20 30 >", &ctx); + assert_eq!(stack_int(&f), 10); +} + +#[test] +fn values_in_list_cycle() { + let ctx = ctx_with(|c| c.runs = 2); + let f = run_ctx("< 10 20 30 >", &ctx); + assert_eq!(stack_int(&f), 30); +} + +#[test] +fn mixed_values_and_words() { + // Values stay as values, words become quotations + // [ 10 20 ] choose just picks a number + let f = forth(); + let ctx = default_ctx(); + f.evaluate("[ 10 20 ] choose", &ctx).unwrap(); + let val = stack_int(&f); + assert!(val == 10 || val == 20, "expected 10 or 20, got {}", val); +} + +#[test] +fn word_with_sound_params() { + let f = forth(); + let ctx = ctx_with(|c| c.runs = 0); + let outputs = f.evaluate( + ": myverb 0.5 verb ; \"sine\" s 440 freq < myverb > emit", + &ctx + ).unwrap(); + assert_eq!(outputs.len(), 1); + assert!(outputs[0].contains("verb/0.5"), "expected verb/0.5 in {}", outputs[0]); +} + +#[test] +fn arithmetic_word_in_list() { + // 3 4 [ + ] choose -> picks + (only option), applies to 3 4 = 7 + let ctx = ctx_with(|c| c.runs = 0); + let f = run_ctx("3 4 < + >", &ctx); + assert_eq!(stack_int(&f), 7); +} + +#[test] +fn pcycle_word_from_list() { + let ctx = ctx_with(|c| c.iter = 0); + let f = run_ctx("10 << dup 2 * >>", &ctx); + // iter=0 picks dup: 10 10 + let stack = f.stack(); + assert_eq!(stack.len(), 2); + assert_eq!(stack_int(&f), 10); +} + +#[test] +fn pcycle_word_second() { + let ctx = ctx_with(|c| c.iter = 1); + let f = run_ctx("10 << dup 2 * >>", &ctx); + // iter=1 picks "2 *" — but wait, each token is its own element + // so << dup 2 * >> has 3 elements: {dup}, 2, {*} + // iter=1 picks element index 1 which is value 2 + assert_eq!(stack_int(&f), 2); +} + +#[test] +fn multi_op_quotation_in_list() { + // Use { } for multi-op quotations inside lists + let ctx = ctx_with(|c| c.runs = 0); + let f = run_ctx("10 < { 2 * } { 3 + } >", &ctx); + assert_eq!(stack_int(&f), 20); // runs=0 picks {2 *} +} + +#[test] +fn multi_op_quotation_second() { + let ctx = ctx_with(|c| c.runs = 1); + let f = run_ctx("10 < { 2 * } { 3 + } >", &ctx); + assert_eq!(stack_int(&f), 13); // runs=1 picks {3 +} +} + +#[test] +fn pipe_syntax_with_words() { + // | word1 word2 | uses LocalCycleEnd which should auto-apply quotations + // LocalCycleEnd uses time_ctx.iteration_index, which defaults to 0 outside for loops + let f = forth(); + let ctx = default_ctx(); + f.evaluate(": add3 3 + ; : add5 5 + ; 10 | add3 add5 |", &ctx).unwrap(); + // iteration_index defaults to 0, picks first word (add3) + assert_eq!(stack_int(&f), 13); +} diff --git a/tests/forth/temporal.rs b/tests/forth/temporal.rs index 289b13e..b52eeed 100644 --- a/tests/forth/temporal.rs +++ b/tests/forth/temporal.rs @@ -28,6 +28,7 @@ fn approx_eq(a: f64, b: f64) -> bool { } // At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s +// Default duration = 4 * stepdur = 0.5s #[test] fn stepdur_baseline() { @@ -47,23 +48,24 @@ fn emit_no_delta() { #[test] fn at_half() { - // at 0.5 in root zoom (0..0.125) => delta = 0.5 * 0.125 = 0.0625 + // at 0.5 in root (0..0.5) => delta = 0.5 * 0.5 = 0.25 let outputs = expect_outputs(r#""kick" s 0.5 at emit pop"#, 1); let deltas = get_deltas(&outputs); assert!( - approx_eq(deltas[0], 0.0625), - "at 0.5 should be delta 0.0625, got {}", + approx_eq(deltas[0], 0.25), + "at 0.5 should be delta 0.25, got {}", deltas[0] ); } #[test] fn at_quarter() { + // at 0.25 in root (0..0.5) => delta = 0.25 * 0.5 = 0.125 let outputs = expect_outputs(r#""kick" s 0.25 at emit pop"#, 1); let deltas = get_deltas(&outputs); assert!( - approx_eq(deltas[0], 0.03125), - "at 0.25 should be delta 0.03125, got {}", + approx_eq(deltas[0], 0.125), + "at 0.25 should be delta 0.125, got {}", deltas[0] ); } @@ -77,23 +79,23 @@ fn at_zero() { #[test] fn div_2_each() { - // 2 subdivisions: deltas at 0 and 0.0625 (half of 0.125) + // 2 subdivisions: deltas at 0 and 0.25 (half of 0.5) let outputs = expect_outputs(r#""kick" s 2 div each"#, 2); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0), "first subdivision at 0"); assert!( - approx_eq(deltas[1], 0.0625), - "second subdivision at 0.0625, got {}", + approx_eq(deltas[1], 0.25), + "second subdivision at 0.25, got {}", deltas[1] ); } #[test] fn div_4_each() { - // 4 subdivisions: 0, 0.03125, 0.0625, 0.09375 + // 4 subdivisions: 0, 0.125, 0.25, 0.375 let outputs = expect_outputs(r#""kick" s 4 div each"#, 4); let deltas = get_deltas(&outputs); - let expected = [0.0, 0.03125, 0.0625, 0.09375]; + let expected = [0.0, 0.125, 0.25, 0.375]; for (i, (got, exp)) in deltas.iter().zip(expected.iter()).enumerate() { assert!( approx_eq(*got, *exp), @@ -107,10 +109,10 @@ fn div_4_each() { #[test] fn div_3_each() { - // 3 subdivisions: 0, 0.125/3, 2*0.125/3 + // 3 subdivisions: 0, 0.5/3, 2*0.5/3 let outputs = expect_outputs(r#""kick" s 3 div each"#, 3); let deltas = get_deltas(&outputs); - let step = 0.125 / 3.0; + let step = 0.5 / 3.0; assert!(approx_eq(deltas[0], 0.0)); assert!(approx_eq(deltas[1], step), "got {}", deltas[1]); assert!(approx_eq(deltas[2], 2.0 * step), "got {}", deltas[2]); @@ -118,56 +120,56 @@ fn div_3_each() { #[test] fn zoom_full() { - // zoom 0.0 1.0 is the full step, same as root + // zoom 0.0 1.0 is the full duration, same as root let outputs = expect_outputs(r#"0.0 1.0 zoom "kick" s 0.5 at emit pop"#, 1); let deltas = get_deltas(&outputs); - assert!(approx_eq(deltas[0], 0.0625), "full zoom at 0.5 = 0.0625"); + assert!(approx_eq(deltas[0], 0.25), "full zoom at 0.5 = 0.25"); } #[test] fn zoom_first_half() { - // zoom 0.0 0.5 restricts to first half (0..0.0625) - // at 0.5 within that = 0.25 of full step = 0.03125 + // zoom 0.0 0.5 restricts to first half (0..0.25) + // at 0.5 within that = 0.125 let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 0.5 at emit pop"#, 1); let deltas = get_deltas(&outputs); assert!( - approx_eq(deltas[0], 0.03125), - "first-half zoom at 0.5 = 0.03125, got {}", + approx_eq(deltas[0], 0.125), + "first-half zoom at 0.5 = 0.125, got {}", deltas[0] ); } #[test] fn zoom_second_half() { - // zoom 0.5 1.0 restricts to second half (0.0625..0.125) - // at 0.0 within that = start of second half = 0.0625 + // zoom 0.5 1.0 restricts to second half (0.25..0.5) + // at 0.0 within that = start of second half = 0.25 let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1); let deltas = get_deltas(&outputs); assert!( - approx_eq(deltas[0], 0.0625), - "second-half zoom at 0.0 = 0.0625, got {}", + approx_eq(deltas[0], 0.25), + "second-half zoom at 0.0 = 0.25, got {}", deltas[0] ); } #[test] fn zoom_second_half_middle() { - // zoom 0.5 1.0, at 0.5 within that = 0.75 of full step = 0.09375 + // zoom 0.5 1.0, at 0.5 within that = 0.75 of full duration = 0.375 let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.5 at emit pop"#, 1); let deltas = get_deltas(&outputs); - assert!(approx_eq(deltas[0], 0.09375), "got {}", deltas[0]); + assert!(approx_eq(deltas[0], 0.375), "got {}", deltas[0]); } #[test] fn nested_zooms() { // zoom 0.0 0.5, then zoom 0.5 1.0 within that - // outer: 0..0.0625, inner: 0.5..1.0 of that = 0.03125..0.0625 - // at 0.0 in inner = 0.03125 + // outer: 0..0.25, inner: 0.5..1.0 of that = 0.125..0.25 + // at 0.0 in inner = 0.125 let outputs = expect_outputs(r#"0.0 0.5 zoom 0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1); let deltas = get_deltas(&outputs); assert!( - approx_eq(deltas[0], 0.03125), - "nested zoom at 0.0 = 0.03125, got {}", + approx_eq(deltas[0], 0.125), + "nested zoom at 0.0 = 0.125, got {}", deltas[0] ); } @@ -175,7 +177,7 @@ fn nested_zooms() { #[test] fn zoom_pop_sequence() { // First in zoom 0.0 0.5 at 0.0 -> delta 0 - // Pop at context and zoom, then in zoom 0.5 1.0 at 0.0 -> delta 0.0625 + // Pop at context and zoom, then in zoom 0.5 1.0 at 0.0 -> delta 0.25 let outputs = expect_outputs( r#"0.0 0.5 zoom "kick" s 0.0 at emit pop pop 0.5 1.0 zoom "snare" s 0.0 at emit pop"#, 2, @@ -183,7 +185,7 @@ fn zoom_pop_sequence() { let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0), "first zoom start"); assert!( - approx_eq(deltas[1], 0.0625), + approx_eq(deltas[1], 0.25), "second zoom start, got {}", deltas[1] ); @@ -191,12 +193,12 @@ fn zoom_pop_sequence() { #[test] fn div_in_zoom() { - // zoom 0.0 0.5 (duration 0.0625), then div 2 each - // subdivisions at 0 and 0.03125 + // zoom 0.0 0.5 (duration 0.25), then div 2 each + // subdivisions at 0 and 0.125 let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 2 div each"#, 2); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.03125), "got {}", deltas[1]); + assert!(approx_eq(deltas[1], 0.125), "got {}", deltas[1]); } #[test] @@ -219,11 +221,11 @@ fn speed_affects_stepdur() { #[test] fn div_each_at_different_tempo() { - // At 60 BPM: stepdur = 0.25, so div 2 each => 0, 0.125 + // At 60 BPM: stepdur = 0.25, default dur = 1.0, so div 2 each => 0, 0.5 let ctx = ctx_with(|c| c.tempo = 60.0); let f = forth(); let outputs = f.evaluate(r#""kick" s 2 div each"#, &ctx).unwrap(); let deltas = get_deltas(&outputs); assert!(approx_eq(deltas[0], 0.0)); - assert!(approx_eq(deltas[1], 0.125), "got {}", deltas[1]); + assert!(approx_eq(deltas[1], 0.5), "got {}", deltas[1]); }