cleaning
This commit is contained in:
@@ -95,7 +95,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, 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<Vec<Op>, 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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -84,7 +84,6 @@ pub enum Op {
|
||||
Loop,
|
||||
Degree(&'static [i64]),
|
||||
Oct,
|
||||
InternalCycleEnd,
|
||||
DivStart,
|
||||
DivEnd,
|
||||
StackStart,
|
||||
|
||||
@@ -48,7 +48,6 @@ pub enum Value {
|
||||
Str(String, Option<SourceSpan>),
|
||||
Marker,
|
||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||
Alternator(Vec<Value>),
|
||||
}
|
||||
|
||||
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<SourceSpan> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<std::sync::Mutex<usize>>;
|
||||
|
||||
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<ScopeContext> = 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<ScopeContext>,
|
||||
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<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
scope_stack: &mut Vec<ScopeContext>,
|
||||
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<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
scope_stack: &mut Vec<ScopeContext>,
|
||||
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<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
scope_stack: &mut Vec<ScopeContext>,
|
||||
cmd: &mut CmdRegister|
|
||||
-> Result<(), String> {
|
||||
if stack.len() < count {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - count;
|
||||
let values: Vec<Value> = 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<Value>,
|
||||
outputs: &mut Vec<String>,
|
||||
scope_stack: &mut Vec<ScopeContext>,
|
||||
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<ScopeContext>|
|
||||
-> Result<Option<Value>, 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<Value> = 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<Value> = 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<Value> = 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<SourceSpan>) {
|
||||
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<String>) {
|
||||
let slot_dur = if scope.slot_count == 0 {
|
||||
scope.duration * scope.weight
|
||||
@@ -1094,26 +766,41 @@ fn resolve_scope(scope: &ScopeContext, outputs: &mut Vec<String>) {
|
||||
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<Emission> = 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<F>(val: Value, f: F) -> Result<Value, String>
|
||||
where
|
||||
F: Fn(f64) -> f64 + Copy,
|
||||
F: Fn(f64) -> f64,
|
||||
{
|
||||
match val {
|
||||
Value::Alternator(items) => {
|
||||
let mapped: Result<Vec<Value>, 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<F>(val: Value, f: F) -> Result<Value, String>
|
||||
where
|
||||
F: Fn(i64) -> i64 + Copy,
|
||||
F: Fn(i64) -> i64,
|
||||
{
|
||||
match val {
|
||||
Value::Alternator(items) => {
|
||||
let mapped: Result<Vec<Value>, 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<F>(a: Value, b: Value, f: F) -> Result<Value, String>
|
||||
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<Vec<Value>, 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<Vec<Value>, String> = items
|
||||
.into_iter()
|
||||
.map(|v| lift_binary(v, scalar.clone(), f))
|
||||
.collect();
|
||||
Ok(Value::Alternator(mapped?))
|
||||
}
|
||||
(scalar, Value::Alternator(items)) => {
|
||||
let mapped: Result<Vec<Value>, 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<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
||||
@@ -1274,45 +920,12 @@ where
|
||||
|
||||
fn cmp_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
||||
where
|
||||
F: Fn(f64, f64) -> bool + Copy,
|
||||
F: Fn(f64, f64) -> bool,
|
||||
{
|
||||
fn lift_cmp<F>(a: Value, b: Value, f: F) -> Result<Value, String>
|
||||
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<Vec<Value>, 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<Vec<Value>, String> = items
|
||||
.into_iter()
|
||||
.map(|v| lift_cmp(v, scalar.clone(), f))
|
||||
.collect();
|
||||
Ok(Value::Alternator(mapped?))
|
||||
}
|
||||
(scalar, Value::Alternator(items)) => {
|
||||
let mapped: Result<Vec<Value>, 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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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;
|
||||
|
||||
65
crates/ratatui/src/sparkles.rs
Normal file
65
crates/ratatui/src/sparkles.rs
Normal file
@@ -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<Sparkle>,
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,16 +160,6 @@ pub struct AnalysisHandle {
|
||||
thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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()? {
|
||||
|
||||
@@ -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<Sparkle>,
|
||||
pub sparkles: Sparkles,
|
||||
pub status_message: Option<String>,
|
||||
pub flash_until: Option<Instant>,
|
||||
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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<Line> = (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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<String> {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<String, f64> {
|
||||
@@ -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<f64> = 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<i64> = 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<f64> = 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"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user