diff --git a/crates/forth/src/compiler.rs b/crates/forth/src/compiler.rs index 904c06e..91c0625 100644 --- a/crates/forth/src/compiler.rs +++ b/crates/forth/src/compiler.rs @@ -95,7 +95,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { let mut ops = Vec::new(); let mut i = 0; let mut list_depth: usize = 0; - let mut pipe_parity = false; while i < tokens.len() { match &tokens[i] { @@ -131,13 +130,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { ops.push(Op::Branch(else_ops.len())); ops.extend(else_ops); } - } else if word == "|" { - if pipe_parity { - ops.push(Op::InternalCycleEnd); - } else { - ops.push(Op::ListStart); - } - pipe_parity = !pipe_parity; } else if is_list_start(word) { ops.push(Op::ListStart); list_depth += 1; diff --git a/crates/forth/src/lib.rs b/crates/forth/src/lib.rs index 6a33773..a024ad8 100644 --- a/crates/forth/src/lib.rs +++ b/crates/forth/src/lib.rs @@ -6,5 +6,5 @@ mod vm; mod words; pub use types::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables}; -pub use vm::{EmissionCounter, Forth}; +pub use vm::Forth; pub use words::{Word, WordCompile, WORDS}; diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index 8c0c225..2be7eb8 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -84,7 +84,6 @@ pub enum Op { Loop, Degree(&'static [i64]), Oct, - InternalCycleEnd, DivStart, DivEnd, StackStart, diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index a2a42e8..58d0c1a 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -48,7 +48,6 @@ pub enum Value { Str(String, Option), Marker, Quotation(Vec, Option), - Alternator(Vec), } impl PartialEq for Value { @@ -59,7 +58,6 @@ impl PartialEq for Value { (Value::Str(a, _), Value::Str(b, _)) => a == b, (Value::Marker, Value::Marker) => true, (Value::Quotation(a, _), Value::Quotation(b, _)) => a == b, - (Value::Alternator(a), Value::Alternator(b)) => a == b, _ => false, } } @@ -96,7 +94,6 @@ impl Value { Value::Str(s, _) => !s.is_empty(), Value::Marker => false, Value::Quotation(..) => true, - Value::Alternator(items) => !items.is_empty(), } } @@ -111,14 +108,13 @@ impl Value { Value::Str(s, _) => s.clone(), Value::Marker => String::new(), Value::Quotation(..) => String::new(), - Value::Alternator(_) => String::new(), } } pub(super) fn span(&self) -> Option { match self { - Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s, - Value::Marker | Value::Quotation(..) | Value::Alternator(_) => None, + Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s, + Value::Marker => None, } } } diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index b4792f4..72b2410 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -5,17 +5,14 @@ use super::compiler::compile_script; use super::ops::Op; use super::types::{ CmdRegister, Dictionary, ExecutionTrace, PendingEmission, ResolvedEmission, Rng, ScopeContext, - SourceSpan, Stack, StepContext, Value, Variables, + Stack, StepContext, Value, Variables, }; -pub type EmissionCounter = std::sync::Arc>; - pub struct Forth { stack: Stack, vars: Variables, dict: Dictionary, rng: Rng, - emission_count: EmissionCounter, } impl Forth { @@ -25,22 +22,6 @@ impl Forth { vars, dict, rng, - emission_count: std::sync::Arc::new(std::sync::Mutex::new(0)), - } - } - - pub fn new_with_counter( - vars: Variables, - dict: Dictionary, - rng: Rng, - emission_count: EmissionCounter, - ) -> Self { - Self { - stack: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), - vars, - dict, - rng, - emission_count, } } @@ -92,7 +73,6 @@ impl Forth { let root_duration = ctx.step_duration() * 4.0; let mut scope_stack: Vec = vec![ScopeContext::new(0.0, root_duration)]; let mut cmd = CmdRegister::default(); - let mut emission_count = self.emission_count.lock().unwrap(); self.execute_ops( ops, @@ -102,7 +82,6 @@ impl Forth { &mut scope_stack, &mut cmd, trace, - &mut emission_count, )?; // Resolve root scope at end of script @@ -124,11 +103,122 @@ impl Forth { scope_stack: &mut Vec, cmd: &mut CmdRegister, trace: Option<&mut ExecutionTrace>, - emission_count: &mut usize, ) -> Result<(), String> { let mut pc = 0; let trace_cell = std::cell::RefCell::new(trace); + // Executes a quotation value, handling trace recording and recursive dispatch. + let run_quotation = |quot: Value, + stack: &mut Vec, + outputs: &mut Vec, + scope_stack: &mut Vec, + cmd: &mut CmdRegister| + -> Result<(), String> { + match quot { + Value::Quotation(quot_ops, body_span) => { + if let Some(span) = body_span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.executed_spans.push(span); + } + } + let mut trace_opt = trace_cell.borrow_mut().take(); + self.execute_ops( + "_ops, + ctx, + stack, + outputs, + scope_stack, + cmd, + trace_opt.as_deref_mut(), + )?; + *trace_cell.borrow_mut() = trace_opt; + Ok(()) + } + _ => Err("expected quotation".into()), + } + }; + + // Selects a value from a list, records trace, and either executes (quotation) or pushes (other). + let select_and_run = |selected: Value, + stack: &mut Vec, + outputs: &mut Vec, + scope_stack: &mut Vec, + cmd: &mut CmdRegister| + -> Result<(), String> { + if let Some(span) = selected.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } + } + if matches!(selected, Value::Quotation(..)) { + run_quotation(selected, stack, outputs, scope_stack, cmd) + } else { + stack.push(selected); + Ok(()) + } + }; + + // Drains `count` values from the stack, selects one by index, and runs it. + let drain_select_run = |count: usize, + idx: usize, + stack: &mut Vec, + outputs: &mut Vec, + scope_stack: &mut Vec, + cmd: &mut CmdRegister| + -> Result<(), String> { + if stack.len() < count { + return Err("stack underflow".into()); + } + let start = stack.len() - count; + let values: Vec = stack.drain(start..).collect(); + let selected = values[idx].clone(); + select_and_run(selected, stack, outputs, scope_stack, cmd) + }; + + // Pops all values until a marker, selects one by index, and runs it. + let drain_list_select_run = |idx_source: usize, + err_msg: &str, + stack: &mut Vec, + outputs: &mut Vec, + scope_stack: &mut Vec, + cmd: &mut CmdRegister| + -> Result<(), String> { + 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(err_msg.into()); + } + values.reverse(); + let idx = idx_source % values.len(); + let selected = values[idx].clone(); + select_and_run(selected, stack, outputs, scope_stack, cmd) + }; + + // Emits one sound event from the current command register into the current scope. + let emit_once = |cmd: &CmdRegister, + scope_stack: &mut Vec| + -> Result, String> { + let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?; + let sound = sound_val.as_str()?.to_string(); + let resolved_params: Vec<(String, String)> = params + .iter() + .map(|(k, v)| (k.clone(), v.to_param_string())) + .collect(); + let scope = scope_stack.last_mut().ok_or("scope stack underflow")?; + let slot_idx = scope.claim_slot(); + scope.pending.push(PendingEmission { + sound, + params: resolved_params, + slot_index: slot_idx, + }); + Ok(Some(sound_val)) + }; + while pc < ops.len() { match &ops[pc] { Op::PushInt(n, span) => stack.push(Value::Int(*n, *span)), @@ -304,41 +394,13 @@ impl Forth { } Op::Emit => { - let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?; - - // Resolve alternators using emission count, tracking selected spans - let (resolved_sound, sound_span) = - resolve_value_with_span(&sound_val, *emission_count); - if let Some(span) = sound_span { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); + if let Some(sound_val) = emit_once(cmd, scope_stack)? { + if let Some(span) = sound_val.span() { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.selected_spans.push(span); + } } } - let sound = resolved_sound.as_str()?.to_string(); - - let resolved_params: Vec<(String, String)> = params - .iter() - .map(|(k, v)| { - let (resolved, param_span) = - resolve_value_with_span(v, *emission_count); - if let Some(span) = param_span { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - (k.clone(), resolved.to_param_string()) - }) - .collect(); - - let scope = scope_stack.last_mut().ok_or("scope stack underflow")?; - let slot_idx = scope.claim_slot(); - scope.pending.push(PendingEmission { - sound, - params: resolved_params, - slot_index: slot_idx, - }); - - *emission_count += 1; } Op::Silence => { @@ -406,84 +468,16 @@ impl Forth { *self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64); } - Op::Cycle => { + Op::Cycle | Op::PCycle => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("cycle count must be > 0".into()); } - 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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *trace_cell.borrow_mut() = trace_opt; - } else { - stack.push(selected); - } + let idx = match &ops[pc] { + Op::Cycle => ctx.runs, + _ => ctx.iter, + } % count; + drain_select_run(count, idx, stack, outputs, scope_stack, cmd)?; } Op::Choose => { @@ -491,98 +485,20 @@ impl Forth { 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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *trace_cell.borrow_mut() = trace_opt; - } else { - stack.push(selected); - } + drain_select_run(count, idx, stack, outputs, scope_stack, cmd)?; } - Op::ChanceExec => { - let prob = stack.pop().ok_or("stack underflow")?.as_float()?; + Op::ChanceExec | Op::ProbExec => { + let threshold = stack.pop().ok_or("stack underflow")?.as_float()?; let quot = stack.pop().ok_or("stack underflow")?; let val: f64 = self.rng.lock().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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } + let limit = match &ops[pc] { + Op::ChanceExec => threshold, + _ => threshold / 100.0, + }; + if val < limit { + run_quotation(quot, stack, outputs, scope_stack, cmd)?; } } @@ -604,61 +520,15 @@ impl Forth { stack.push(Value::Quotation(quote_ops.clone(), *body_span)); } - Op::When => { + Op::When | 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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } + let should_run = match &ops[pc] { + Op::When => cond.is_truthy(), + _ => !cond.is_truthy(), + }; + if should_run { + run_quotation(quot, stack, outputs, scope_stack, cmd)?; } } @@ -667,28 +537,7 @@ impl Forth { let false_quot = stack.pop().ok_or("stack underflow")?; let true_quot = stack.pop().ok_or("stack underflow")?; let quot = if cond.is_truthy() { true_quot } else { false_quot }; - 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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } + run_quotation(quot, stack, outputs, scope_stack, cmd)?; } Op::Pick => { @@ -709,31 +558,9 @@ impl Forth { "pick index {} out of range (have {} quotations)", idx, quots.len() - ) - .into()); - } - match "s[idx] { - 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( - quot_ops, - ctx, - stack, - outputs, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => unreachable!(), + )); } + run_quotation(quots.swap_remove(idx), stack, outputs, scope_stack, cmd)?; } Op::Mtof => { @@ -821,88 +648,16 @@ impl Forth { 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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *trace_cell.borrow_mut() = trace_opt; - } else { - stack.push(selected); - } + Op::ListEndCycle | Op::ListEndPCycle => { + let idx_source = match &ops[pc] { + Op::ListEndCycle => ctx.runs, + _ => ctx.iter, + }; + let err_msg = match &ops[pc] { + Op::ListEndCycle => "empty cycle list", + _ => "empty pattern cycle list", + }; + drain_list_select_run(idx_source, err_msg, stack, outputs, scope_stack, cmd)?; } Op::Adsr => { @@ -926,29 +681,9 @@ impl Forth { 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, - scope_stack, - cmd, - trace_opt.as_deref_mut(), - emission_count, - )?; - *trace_cell.borrow_mut() = trace_opt; - } - _ => return Err("expected quotation".into()), - } + run_quotation(quot, stack, outputs, scope_stack, cmd)?; } + Op::Ramp => { let curve = stack.pop().ok_or("stack underflow")?.as_float()?; let freq = stack.pop().ok_or("stack underflow")?.as_float()?; @@ -976,21 +711,6 @@ impl Forth { stack.push(Value::Float(val, None)); } - Op::InternalCycleEnd => { - 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 internal cycle".into()); - } - values.reverse(); - stack.push(Value::Alternator(values)); - } - Op::DivStart => { let parent = scope_stack.last().ok_or("scope stack underflow")?; let mut new_scope = ScopeContext::new(parent.start, parent.duration); @@ -1005,10 +725,8 @@ impl Forth { let child = scope_stack.pop().unwrap(); if child.stacked { - // Stack doesn't claim a slot - resolve directly to outputs resolve_scope(&child, outputs); } else { - // Div claims a slot in the parent let parent = scope_stack.last_mut().ok_or("scope stack underflow")?; let parent_slot = parent.claim_slot(); resolve_scope_to_parent(&child, parent_slot, parent); @@ -1029,42 +747,10 @@ impl Forth { return Err("emit count must be >= 0".into()); } for _ in 0..n { - let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?; - - let (resolved_sound, sound_span) = - resolve_value_with_span(&sound_val, *emission_count); - if let Some(span) = sound_span { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - let sound = resolved_sound.as_str()?.to_string(); - - let resolved_params: Vec<(String, String)> = params - .iter() - .map(|(k, v)| { - let (resolved, param_span) = - resolve_value_with_span(v, *emission_count); - if let Some(span) = param_span { - if let Some(trace) = trace_cell.borrow_mut().as_mut() { - trace.selected_spans.push(span); - } - } - (k.clone(), resolved.to_param_string()) - }) - .collect(); - - let scope = scope_stack.last_mut().ok_or("scope stack underflow")?; - let slot_idx = scope.claim_slot(); - scope.pending.push(PendingEmission { - sound, - params: resolved_params, - slot_index: slot_idx, - }); - - *emission_count += 1; + emit_once(cmd, scope_stack)?; } } + } pc += 1; } @@ -1073,20 +759,6 @@ impl Forth { } } -fn resolve_value_with_span(val: &Value, emission_count: usize) -> (Value, Option) { - match val { - Value::Alternator(items) if !items.is_empty() => { - let idx = emission_count % items.len(); - let selected = &items[idx]; - let (resolved, inner_span) = resolve_value_with_span(selected, emission_count); - // Prefer inner span (for nested alternators), fall back to selected's span - let span = inner_span.or_else(|| selected.span()); - (resolved, span) - } - other => (other.clone(), other.span()), - } -} - fn resolve_scope(scope: &ScopeContext, outputs: &mut Vec) { let slot_dur = if scope.slot_count == 0 { scope.duration * scope.weight @@ -1094,26 +766,41 @@ fn resolve_scope(scope: &ScopeContext, outputs: &mut Vec) { scope.duration * scope.weight / scope.slot_count as f64 }; - // Collect all emissions with their deltas for sorting - let mut emissions: Vec<(f64, String, Vec<(String, String)>, f64)> = Vec::new(); + struct Emission { + delta: f64, + sound: String, + params: Vec<(String, String)>, + dur: f64, + } + + let mut emissions: Vec = Vec::new(); for em in &scope.pending { let delta = scope.start + slot_dur * em.slot_index as f64; - emissions.push((delta, em.sound.clone(), em.params.clone(), slot_dur)); + emissions.push(Emission { + delta, + sound: em.sound.clone(), + params: em.params.clone(), + dur: slot_dur, + }); } for em in &scope.resolved { let slot_start = slot_dur * em.parent_slot as f64; let delta = scope.start + slot_start + em.offset_in_slot * slot_dur; let dur = em.dur * slot_dur; - emissions.push((delta, em.sound.clone(), em.params.clone(), dur)); + emissions.push(Emission { + delta, + sound: em.sound.clone(), + params: em.params.clone(), + dur, + }); } - // Sort by delta to ensure temporal ordering - emissions.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); + emissions.sort_by(|a, b| a.delta.partial_cmp(&b.delta).unwrap_or(std::cmp::Ordering::Equal)); - for (delta, sound, params, dur) in emissions { - emit_output(&sound, ¶ms, delta, dur, outputs); + for em in emissions { + emit_output(&em.sound, &em.params, em.delta, em.dur, outputs); } } @@ -1202,64 +889,23 @@ fn float_to_value(result: f64) -> Value { fn lift_unary(val: Value, f: F) -> Result where - F: Fn(f64) -> f64 + Copy, + F: Fn(f64) -> f64, { - match val { - Value::Alternator(items) => { - let mapped: Result, String> = - items.into_iter().map(|v| lift_unary(v, f)).collect(); - Ok(Value::Alternator(mapped?)) - } - other => Ok(float_to_value(f(other.as_float()?))), - } + Ok(float_to_value(f(val.as_float()?))) } fn lift_unary_int(val: Value, f: F) -> Result where - F: Fn(i64) -> i64 + Copy, + F: Fn(i64) -> i64, { - match val { - Value::Alternator(items) => { - let mapped: Result, String> = - items.into_iter().map(|v| lift_unary_int(v, f)).collect(); - Ok(Value::Alternator(mapped?)) - } - other => Ok(Value::Int(f(other.as_int()?), None)), - } + Ok(Value::Int(f(val.as_int()?), None)) } fn lift_binary(a: Value, b: Value, f: F) -> Result where - F: Fn(f64, f64) -> f64 + Copy, + F: Fn(f64, f64) -> f64, { - match (a, b) { - (Value::Alternator(a_items), Value::Alternator(b_items)) => { - let len = a_items.len().max(b_items.len()); - let mapped: Result, String> = (0..len) - .map(|i| { - let ai = a_items[i % a_items.len()].clone(); - let bi = b_items[i % b_items.len()].clone(); - lift_binary(ai, bi, f) - }) - .collect(); - Ok(Value::Alternator(mapped?)) - } - (Value::Alternator(items), scalar) => { - let mapped: Result, String> = items - .into_iter() - .map(|v| lift_binary(v, scalar.clone(), f)) - .collect(); - Ok(Value::Alternator(mapped?)) - } - (scalar, Value::Alternator(items)) => { - let mapped: Result, String> = items - .into_iter() - .map(|v| lift_binary(scalar.clone(), v, f)) - .collect(); - Ok(Value::Alternator(mapped?)) - } - (a, b) => Ok(float_to_value(f(a.as_float()?, b.as_float()?))), - } + Ok(float_to_value(f(a.as_float()?, b.as_float()?))) } fn binary_op(stack: &mut Vec, f: F) -> Result<(), String> @@ -1274,45 +920,12 @@ where fn cmp_op(stack: &mut Vec, f: F) -> Result<(), String> where - F: Fn(f64, f64) -> bool + Copy, + F: Fn(f64, f64) -> bool, { - fn lift_cmp(a: Value, b: Value, f: F) -> Result - where - F: Fn(f64, f64) -> bool + Copy, - { - match (a, b) { - (Value::Alternator(a_items), Value::Alternator(b_items)) => { - let len = a_items.len().max(b_items.len()); - let mapped: Result, String> = (0..len) - .map(|i| { - let ai = a_items[i % a_items.len()].clone(); - let bi = b_items[i % b_items.len()].clone(); - lift_cmp(ai, bi, f) - }) - .collect(); - Ok(Value::Alternator(mapped?)) - } - (Value::Alternator(items), scalar) => { - let mapped: Result, String> = items - .into_iter() - .map(|v| lift_cmp(v, scalar.clone(), f)) - .collect(); - Ok(Value::Alternator(mapped?)) - } - (scalar, Value::Alternator(items)) => { - let mapped: Result, String> = items - .into_iter() - .map(|v| lift_cmp(scalar.clone(), v, f)) - .collect(); - Ok(Value::Alternator(mapped?)) - } - (a, b) => Ok(Value::Int(if f(a.as_float()?, b.as_float()?) { 1 } else { 0 }, None)), - } - } - let b = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; - stack.push(lift_cmp(a, b, f)?); + let result = if f(a.as_float()?, b.as_float()?) { 1 } else { 0 }; + stack.push(Value::Int(result, None)); Ok(()) } diff --git a/crates/ratatui/Cargo.toml b/crates/ratatui/Cargo.toml index 5a5c374..2d02998 100644 --- a/crates/ratatui/Cargo.toml +++ b/crates/ratatui/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +rand = "0.8" ratatui = "0.29" regex = "1" tui-textarea = { version = "0.7", features = ["search"] } diff --git a/crates/ratatui/src/lib.rs b/crates/ratatui/src/lib.rs index 80f782c..6e648e4 100644 --- a/crates/ratatui/src/lib.rs +++ b/crates/ratatui/src/lib.rs @@ -6,6 +6,7 @@ mod modal; mod nav_minimap; mod sample_browser; mod scope; +mod sparkles; mod spectrum; mod text_input; mod vu_meter; @@ -18,6 +19,7 @@ pub use modal::ModalFrame; pub use nav_minimap::{NavMinimap, NavTile}; pub use sample_browser::{SampleBrowser, TreeLine, TreeLineKind}; pub use scope::{Orientation, Scope}; +pub use sparkles::Sparkles; pub use spectrum::Spectrum; pub use text_input::TextInputModal; pub use vu_meter::VuMeter; diff --git a/crates/ratatui/src/sparkles.rs b/crates/ratatui/src/sparkles.rs new file mode 100644 index 0000000..731a3c3 --- /dev/null +++ b/crates/ratatui/src/sparkles.rs @@ -0,0 +1,65 @@ +use rand::Rng; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::{Color, Style}; +use ratatui::widgets::Widget; + +const CHARS: &[char] = &['·', '✦', '✧', '°', '•', '+', '⋆', '*']; +const COLORS: &[(u8, u8, u8)] = &[ + (200, 220, 255), + (255, 200, 150), + (150, 255, 200), + (255, 150, 200), + (200, 150, 255), +]; + +struct Sparkle { + x: u16, + y: u16, + char_idx: usize, + life: u8, +} + +#[derive(Default)] +pub struct Sparkles { + sparkles: Vec, +} + +impl Sparkles { + pub fn tick(&mut self, area: Rect) { + let mut rng = rand::thread_rng(); + for _ in 0..3 { + if rng.gen_bool(0.6) { + self.sparkles.push(Sparkle { + x: rng.gen_range(0..area.width), + y: rng.gen_range(0..area.height), + char_idx: rng.gen_range(0..CHARS.len()), + life: rng.gen_range(15..40), + }); + } + } + self.sparkles + .iter_mut() + .for_each(|s| s.life = s.life.saturating_sub(1)); + self.sparkles.retain(|s| s.life > 0); + } +} + +impl Widget for &Sparkles { + fn render(self, area: Rect, buf: &mut Buffer) { + for sparkle in &self.sparkles { + let color = COLORS[sparkle.char_idx % COLORS.len()]; + let intensity = (sparkle.life as f32 / 30.0).min(1.0); + let r = (color.0 as f32 * intensity) as u8; + let g = (color.1 as f32 * intensity) as u8; + let b = (color.2 as f32 * intensity) as u8; + + if sparkle.x < area.width && sparkle.y < area.height { + let x = area.x + sparkle.x; + let y = area.y + sparkle.y; + let ch = CHARS[sparkle.char_idx]; + buf[(x, y)].set_char(ch).set_style(Style::new().fg(Color::Rgb(r, g, b))); + } + } + } +} diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 4ede689..7afe59b 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -160,16 +160,6 @@ pub struct AnalysisHandle { thread: Option>, } -impl AnalysisHandle { - #[allow(dead_code)] - pub fn shutdown(mut self) { - self.running.store(false, Ordering::SeqCst); - if let Some(t) = self.thread.take() { - let _ = t.join(); - } - } -} - impl Drop for AnalysisHandle { fn drop(&mut self) { self.running.store(false, Ordering::SeqCst); diff --git a/src/main.rs b/src/main.rs index bd2e963..036bca7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -210,7 +210,10 @@ fn main() -> io::Result<()> { app.flush_queued_changes(&sequencer.cmd_tx); app.flush_dirty_patterns(&sequencer.cmd_tx); - terminal.draw(|frame| views::render(frame, &mut app, &link, &seq_snapshot))?; + if app.ui.show_title { + app.ui.sparkles.tick(terminal.get_frame().area()); + } + terminal.draw(|frame| views::render(frame, &app, &link, &seq_snapshot))?; if event::poll(Duration::from_millis(app.audio.config.refresh_rate.millis()))? { match event::read()? { diff --git a/src/state/ui.rs b/src/state/ui.rs index d899ea2..641ef55 100644 --- a/src/state/ui.rs +++ b/src/state/ui.rs @@ -1,5 +1,7 @@ use std::time::{Duration, Instant}; +use cagire_ratatui::Sparkles; + use crate::state::Modal; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -10,13 +12,6 @@ pub enum FlashKind { Info, } -pub struct Sparkle { - pub x: u16, - pub y: u16, - pub char_idx: usize, - pub life: u8, -} - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum DictFocus { #[default] @@ -25,7 +20,7 @@ pub enum DictFocus { } pub struct UiState { - pub sparkles: Vec, + pub sparkles: Sparkles, pub status_message: Option, pub flash_until: Option, pub flash_kind: FlashKind, @@ -46,7 +41,7 @@ pub struct UiState { impl Default for UiState { fn default() -> Self { Self { - sparkles: Vec::new(), + sparkles: Sparkles::default(), status_message: None, flash_until: None, flash_kind: FlashKind::Success, diff --git a/src/views/engine_view.rs b/src/views/engine_view.rs index 8971956..9922d8d 100644 --- a/src/views/engine_view.rs +++ b/src/views/engine_view.rs @@ -156,7 +156,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) { let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input; render_device_column( - frame, app, output_col, + frame, output_col, "Output", &app.audio.output_devices, app.audio.current_output_device_index(), app.audio.output_list.cursor, @@ -172,7 +172,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(Paragraph::new(sep_lines), separator); render_device_column( - frame, app, input_col, + frame, input_col, "Input", &app.audio.input_devices, app.audio.current_input_device_index(), app.audio.input_list.cursor, @@ -184,7 +184,6 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) { fn render_device_column( frame: &mut Frame, - _app: &App, area: Rect, label: &str, devices: &[doux::audio::AudioDeviceInfo], diff --git a/src/views/main_view.rs b/src/views/main_view.rs index 44b3600..0ac90c1 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -7,7 +7,7 @@ use crate::app::App; use crate::engine::SequencerSnapshot; use crate::widgets::{Orientation, Scope, Spectrum, VuMeter}; -pub fn render(frame: &mut Frame, app: &mut App, snapshot: &SequencerSnapshot, area: Rect) { +pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { let [left_area, _spacer, vu_area] = Layout::horizontal([ Constraint::Fill(1), Constraint::Length(2), diff --git a/src/views/render.rs b/src/views/render.rs index 411de7d..47844ec 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -28,14 +28,14 @@ fn adjust_spans_for_line(spans: &[SourceSpan], line_start: usize, line_len: usiz }).collect() } -pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &SequencerSnapshot) { +pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot) { let term = frame.area(); let blank = " ".repeat(term.width as usize); let lines: Vec = (0..term.height).map(|_| Line::raw(&blank)).collect(); frame.render_widget(Paragraph::new(lines), term); if app.ui.show_title { - title_view::render(frame, term, &mut app.ui); + title_view::render(frame, term, &app.ui); return; } diff --git a/src/views/title_view.rs b/src/views/title_view.rs index e43f90a..5d9d5c6 100644 --- a/src/views/title_view.rs +++ b/src/views/title_view.rs @@ -1,4 +1,3 @@ -use rand::Rng; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Style, Stylize}; use ratatui::text::{Line, Span}; @@ -6,56 +5,11 @@ use ratatui::widgets::Paragraph; use ratatui::Frame; use tui_big_text::{BigText, PixelSize}; -use crate::state::ui::{Sparkle, UiState}; +use crate::state::ui::UiState; -const SPARKLE_CHARS: &[char] = &['·', '✦', '✧', '°', '•', '+', '⋆', '*']; -const SPARKLE_COLORS: &[(u8, u8, u8)] = &[ - (200, 220, 255), - (255, 200, 150), - (150, 255, 200), - (255, 150, 200), - (200, 150, 255), -]; +pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) { + frame.render_widget(&ui.sparkles, area); -pub fn render(frame: &mut Frame, area: Rect, ui: &mut UiState) { - let mut rng = rand::thread_rng(); - - // Spawn new sparkles - for _ in 0..3 { - if rng.gen_bool(0.6) { - let x = rng.gen_range(0..area.width); - let y = rng.gen_range(0..area.height); - ui.sparkles.push(Sparkle { - x, - y, - char_idx: rng.gen_range(0..SPARKLE_CHARS.len()), - life: rng.gen_range(15..40), - }); - } - } - - // Age and remove dead sparkles - ui.sparkles.iter_mut().for_each(|s| s.life = s.life.saturating_sub(1)); - ui.sparkles.retain(|s| s.life > 0); - - // Render sparkles - for sparkle in &ui.sparkles { - let color = SPARKLE_COLORS[sparkle.char_idx % SPARKLE_COLORS.len()]; - let intensity = (sparkle.life as f32 / 30.0).min(1.0); - let r = (color.0 as f32 * intensity) as u8; - let g = (color.1 as f32 * intensity) as u8; - let b = (color.2 as f32 * intensity) as u8; - - let ch = SPARKLE_CHARS[sparkle.char_idx]; - let span = Span::styled(ch.to_string(), Style::new().fg(Color::Rgb(r, g, b))); - let para = Paragraph::new(Line::from(span)); - let sparkle_area = Rect::new(sparkle.x, sparkle.y, 1, 1); - if sparkle_area.x < area.width && sparkle_area.y < area.height { - frame.render_widget(para, sparkle_area); - } - } - - // Main content let author_style = Style::new().fg(Color::Rgb(180, 140, 200)); let link_style = Style::new().fg(Color::Rgb(120, 200, 180)); let license_style = Style::new().fg(Color::Rgb(200, 160, 100)); diff --git a/tests/forth/harness.rs b/tests/forth/harness.rs index e9edd9c..3f25e66 100644 --- a/tests/forth/harness.rs +++ b/tests/forth/harness.rs @@ -1,6 +1,6 @@ use rand::rngs::StdRng; use rand::SeedableRng; -use cagire::forth::{Dictionary, EmissionCounter, Forth, Rng, StepContext, Value, Variables}; +use cagire::forth::{Dictionary, Forth, Rng, StepContext, Value, Variables}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -46,14 +46,6 @@ pub fn forth_seeded(seed: u64) -> Forth { Forth::new(new_vars(), new_dict(), seeded_rng(seed)) } -pub fn new_emission_counter() -> EmissionCounter { - Arc::new(Mutex::new(0)) -} - -pub fn forth_with_counter(counter: EmissionCounter) -> Forth { - Forth::new_with_counter(new_vars(), new_dict(), seeded_rng(42), counter) -} - pub fn run(script: &str) -> Forth { let f = forth(); f.evaluate(script, &default_ctx()).unwrap(); @@ -139,13 +131,3 @@ pub fn expect_outputs(script: &str, count: usize) -> Vec { outputs } -#[allow(dead_code)] -pub fn expect_output_contains(script: &str, substr: &str) { - let outputs = expect_outputs(script, 1); - assert!( - outputs[0].contains(substr), - "output '{}' does not contain '{}'", - outputs[0], - substr - ); -} diff --git a/tests/forth/temporal.rs b/tests/forth/temporal.rs index 8663e35..644cddf 100644 --- a/tests/forth/temporal.rs +++ b/tests/forth/temporal.rs @@ -1,6 +1,4 @@ use super::harness::*; -#[allow(unused_imports)] -use super::harness::{forth_with_counter, new_emission_counter}; use std::collections::HashMap; fn parse_params(output: &str) -> HashMap { @@ -245,51 +243,6 @@ fn dot_with_silence() { assert!(approx_eq(deltas[1], 2.0 * step)); } -#[test] -fn internal_alternation_basic() { - let outputs = expect_outputs(r#"| "kick" "snare" | s . . . ."#, 4); - let sounds = get_sounds(&outputs); - assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]); -} - -#[test] -fn internal_alternation_three_sounds() { - let outputs = expect_outputs(r#"| "kick" "snare" "hat" | s . . . . . ."#, 6); - let sounds = get_sounds(&outputs); - assert_eq!(sounds, vec!["kick", "snare", "hat", "kick", "snare", "hat"]); -} - -#[test] -fn internal_alternation_single_item() { - let outputs = expect_outputs(r#"| "kick" | s . . . ."#, 4); - let sounds = get_sounds(&outputs); - assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]); -} - -#[test] -fn internal_alternation_with_params() { - let outputs = expect_outputs(r#"| 0.5 0.9 | gain "kick" s . ."#, 2); - fn parse_gain(output: &str) -> f64 { - let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); - for i in 0..parts.len() - 1 { - if parts[i] == "gain" { - return parts[i + 1].parse().unwrap_or(0.0); - } - } - 0.0 - } - let gains: Vec = outputs.iter().map(|o| parse_gain(o)).collect(); - assert!(approx_eq(gains[0], 0.5), "first gain should be 0.5, got {}", gains[0]); - assert!(approx_eq(gains[1], 0.9), "second gain should be 0.9, got {}", gains[1]); -} - -#[test] -fn internal_alternation_empty_error() { - let f = forth(); - let result = f.evaluate(r#"| | . ."#, &default_ctx()); - assert!(result.is_err(), "empty internal cycle should error"); -} - #[test] fn div_basic_subdivision() { let outputs = expect_outputs(r#"div "kick" s . "hat" s . ~"#, 2); @@ -355,40 +308,6 @@ fn unmatched_scope_terminator_error() { assert!(result.is_err(), "unmatched ~ should error"); } -#[test] -fn alternator_with_scale() { - let outputs = expect_outputs(r#""sine" s | 0 1 2 3 | mixolydian note . . . ."#, 4); - fn parse_note(output: &str) -> i64 { - let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); - for i in 0..parts.len() - 1 { - if parts[i] == "note" { - return parts[i + 1].parse().unwrap_or(0); - } - } - 0 - } - let notes: Vec = outputs.iter().map(|o| parse_note(o)).collect(); - // mixolydian from C4: 0->60, 1->62, 2->64, 3->65 - assert_eq!(notes, vec![60, 62, 64, 65]); -} - -#[test] -fn alternator_with_arithmetic() { - let outputs = expect_outputs(r#""sine" s | 100 200 | 2 * freq . ."#, 2); - fn parse_freq(output: &str) -> f64 { - let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect(); - for i in 0..parts.len() - 1 { - if parts[i] == "freq" { - return parts[i + 1].parse().unwrap_or(0.0); - } - } - 0.0 - } - let freqs: Vec = outputs.iter().map(|o| parse_freq(o)).collect(); - assert!(approx_eq(freqs[0], 200.0), "first freq: expected 200, got {}", freqs[0]); - assert!(approx_eq(freqs[1], 400.0), "second freq: expected 400, got {}", freqs[1]); -} - #[test] fn stack_superposes_sounds() { let outputs = expect_outputs(r#"stack "kick" s . "hat" s . ~"#, 2); @@ -452,13 +371,6 @@ fn emit_n_basic() { assert_eq!(sounds, vec!["kick", "kick", "kick", "kick"]); } -#[test] -fn emit_n_with_alternator() { - let outputs = expect_outputs(r#"| "kick" "snare" | s 4 .!"#, 4); - let sounds = get_sounds(&outputs); - assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]); -} - #[test] fn emit_n_zero() { let outputs = expect_outputs(r#""kick" s 0 .!"#, 0); @@ -472,62 +384,3 @@ fn emit_n_negative_error() { assert!(result.is_err()); } -#[test] -fn persistent_counter_across_evaluations() { - let counter = new_emission_counter(); - let ctx = default_ctx(); - - // First evaluation: kick, snare, kick, snare - let f1 = forth_with_counter(counter.clone()); - let outputs1 = f1.evaluate(r#"| "kick" "snare" | s . ."#, &ctx).unwrap(); - let sounds1 = get_sounds(&outputs1); - assert_eq!(sounds1, vec!["kick", "snare"]); - - // Second evaluation: continues from where we left off - let f2 = forth_with_counter(counter.clone()); - let outputs2 = f2.evaluate(r#"| "kick" "snare" | s . ."#, &ctx).unwrap(); - let sounds2 = get_sounds(&outputs2); - assert_eq!(sounds2, vec!["kick", "snare"]); -} - -#[test] -fn persistent_counter_three_item_cycle() { - let counter = new_emission_counter(); - let ctx = default_ctx(); - - // First eval: kick, snare - let f1 = forth_with_counter(counter.clone()); - let outputs1 = f1.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap(); - let sounds1 = get_sounds(&outputs1); - assert_eq!(sounds1, vec!["kick", "snare"]); - - // Second eval: continues from hat (index 2) - let f2 = forth_with_counter(counter.clone()); - let outputs2 = f2.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap(); - let sounds2 = get_sounds(&outputs2); - assert_eq!(sounds2, vec!["hat", "kick"]); - - // Third eval: snare, hat - let f3 = forth_with_counter(counter.clone()); - let outputs3 = f3.evaluate(r#"| "kick" "snare" "hat" | s . ."#, &ctx).unwrap(); - let sounds3 = get_sounds(&outputs3); - assert_eq!(sounds3, vec!["snare", "hat"]); -} - -#[test] -fn emit_n_with_persistent_counter() { - let counter = new_emission_counter(); - let ctx = default_ctx(); - - // First eval: 3 emits from a 4-item cycle - let f1 = forth_with_counter(counter.clone()); - let outputs1 = f1.evaluate(r#"| "a" "b" "c" "d" | s 3 .!"#, &ctx).unwrap(); - let sounds1 = get_sounds(&outputs1); - assert_eq!(sounds1, vec!["a", "b", "c"]); - - // Second eval: continues from d - let f2 = forth_with_counter(counter.clone()); - let outputs2 = f2.evaluate(r#"| "a" "b" "c" "d" | s 3 .!"#, &ctx).unwrap(); - let sounds2 = get_sounds(&outputs2); - assert_eq!(sounds2, vec!["d", "a", "b"]); -}