WIP simplify
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use super::ops::Op;
|
use super::ops::Op;
|
||||||
use super::types::{Dictionary, SourceSpan};
|
use super::types::{Dictionary, SourceSpan};
|
||||||
use super::words::{compile_word, simple_op};
|
use super::words::compile_word;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Token {
|
enum Token {
|
||||||
@@ -25,6 +25,11 @@ fn tokenize(input: &str) -> Vec<Token> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c == '(' || c == ')' {
|
||||||
|
chars.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if c == '"' {
|
if c == '"' {
|
||||||
let start = pos;
|
let start = pos;
|
||||||
chars.next();
|
chars.next();
|
||||||
@@ -88,7 +93,6 @@ fn tokenize(input: &str) -> Vec<Token> {
|
|||||||
fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||||
let mut ops = Vec::new();
|
let mut ops = Vec::new();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
let mut list_depth: usize = 0;
|
|
||||||
|
|
||||||
while i < tokens.len() {
|
while i < tokens.len() {
|
||||||
match &tokens[i] {
|
match &tokens[i] {
|
||||||
@@ -122,20 +126,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
|||||||
ops.push(Op::Branch(else_ops.len()));
|
ops.push(Op::Branch(else_ops.len()));
|
||||||
ops.extend(else_ops);
|
ops.extend(else_ops);
|
||||||
}
|
}
|
||||||
} else if is_list_start(word) {
|
|
||||||
ops.push(Op::ListStart);
|
|
||||||
list_depth += 1;
|
|
||||||
} else if is_list_end(word) {
|
|
||||||
list_depth = list_depth.saturating_sub(1);
|
|
||||||
if let Some(op) = simple_op(word) {
|
|
||||||
ops.push(op);
|
|
||||||
}
|
|
||||||
} else if list_depth > 0 {
|
|
||||||
let mut word_ops = Vec::new();
|
|
||||||
if !compile_word(word, Some(*span), &mut word_ops, dict) {
|
|
||||||
return Err(format!("unknown word: {word}"));
|
|
||||||
}
|
|
||||||
ops.push(Op::Quotation(word_ops, Some(*span)));
|
|
||||||
} else if !compile_word(word, Some(*span), &mut ops, dict) {
|
} else if !compile_word(word, Some(*span), &mut ops, dict) {
|
||||||
return Err(format!("unknown word: {word}"));
|
return Err(format!("unknown word: {word}"));
|
||||||
}
|
}
|
||||||
@@ -147,14 +137,6 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
|||||||
Ok(ops)
|
Ok(ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_list_start(word: &str) -> bool {
|
|
||||||
matches!(word, "[" | "<" | "<<")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_list_end(word: &str) -> bool {
|
|
||||||
matches!(word, "]" | ">" | ">>")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize, SourceSpan), String> {
|
fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize, SourceSpan), String> {
|
||||||
let mut depth = 1;
|
let mut depth = 1;
|
||||||
let mut end_idx = None;
|
let mut end_idx = None;
|
||||||
|
|||||||
@@ -55,17 +55,14 @@ pub enum Op {
|
|||||||
Rand,
|
Rand,
|
||||||
Seed,
|
Seed,
|
||||||
Cycle,
|
Cycle,
|
||||||
|
PCycle,
|
||||||
|
TCycle,
|
||||||
Choose,
|
Choose,
|
||||||
ChanceExec,
|
ChanceExec,
|
||||||
ProbExec,
|
ProbExec,
|
||||||
Coin,
|
Coin,
|
||||||
Mtof,
|
Mtof,
|
||||||
Ftom,
|
Ftom,
|
||||||
ListStart,
|
|
||||||
ListEnd,
|
|
||||||
ListEndCycle,
|
|
||||||
PCycle,
|
|
||||||
ListEndPCycle,
|
|
||||||
SetTempo,
|
SetTempo,
|
||||||
Every,
|
Every,
|
||||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||||
@@ -85,4 +82,5 @@ pub enum Op {
|
|||||||
EmitN,
|
EmitN,
|
||||||
ClearCmd,
|
ClearCmd,
|
||||||
SetSpeed,
|
SetSpeed,
|
||||||
|
At,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ pub enum Value {
|
|||||||
Int(i64, Option<SourceSpan>),
|
Int(i64, Option<SourceSpan>),
|
||||||
Float(f64, Option<SourceSpan>),
|
Float(f64, Option<SourceSpan>),
|
||||||
Str(String, Option<SourceSpan>),
|
Str(String, Option<SourceSpan>),
|
||||||
Marker,
|
|
||||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||||
|
CycleList(Vec<Value>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Value {
|
impl PartialEq for Value {
|
||||||
@@ -57,8 +57,8 @@ impl PartialEq for Value {
|
|||||||
(Value::Int(a, _), Value::Int(b, _)) => a == b,
|
(Value::Int(a, _), Value::Int(b, _)) => a == b,
|
||||||
(Value::Float(a, _), Value::Float(b, _)) => a == b,
|
(Value::Float(a, _), Value::Float(b, _)) => a == b,
|
||||||
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
||||||
(Value::Marker, Value::Marker) => true,
|
|
||||||
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
||||||
|
(Value::CycleList(a), Value::CycleList(b)) => a == b,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,29 +93,25 @@ impl Value {
|
|||||||
Value::Int(i, _) => *i != 0,
|
Value::Int(i, _) => *i != 0,
|
||||||
Value::Float(f, _) => *f != 0.0,
|
Value::Float(f, _) => *f != 0.0,
|
||||||
Value::Str(s, _) => !s.is_empty(),
|
Value::Str(s, _) => !s.is_empty(),
|
||||||
Value::Marker => false,
|
|
||||||
Value::Quotation(..) => true,
|
Value::Quotation(..) => true,
|
||||||
|
Value::CycleList(items) => !items.is_empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn is_marker(&self) -> bool {
|
|
||||||
matches!(self, Value::Marker)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn to_param_string(&self) -> String {
|
pub(super) fn to_param_string(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Value::Int(i, _) => i.to_string(),
|
Value::Int(i, _) => i.to_string(),
|
||||||
Value::Float(f, _) => f.to_string(),
|
Value::Float(f, _) => f.to_string(),
|
||||||
Value::Str(s, _) => s.clone(),
|
Value::Str(s, _) => s.clone(),
|
||||||
Value::Marker => String::new(),
|
|
||||||
Value::Quotation(..) => String::new(),
|
Value::Quotation(..) => String::new(),
|
||||||
|
Value::CycleList(_) => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn span(&self) -> Option<SourceSpan> {
|
pub(super) fn span(&self) -> Option<SourceSpan> {
|
||||||
match self {
|
match self {
|
||||||
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
|
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
|
||||||
Value::Marker => None,
|
Value::CycleList(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +120,7 @@ impl Value {
|
|||||||
pub(super) struct CmdRegister {
|
pub(super) struct CmdRegister {
|
||||||
sound: Option<Value>,
|
sound: Option<Value>,
|
||||||
params: Vec<(String, Value)>,
|
params: Vec<(String, Value)>,
|
||||||
|
deltas: Vec<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CmdRegister {
|
impl CmdRegister {
|
||||||
@@ -135,6 +132,14 @@ impl CmdRegister {
|
|||||||
self.params.push((key, val));
|
self.params.push((key, val));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_deltas(&mut self, deltas: Vec<Value>) {
|
||||||
|
self.deltas = deltas;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn deltas(&self) -> &[Value] {
|
||||||
|
&self.deltas
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn snapshot(&self) -> Option<(Value, Vec<(String, Value)>)> {
|
pub(super) fn snapshot(&self) -> Option<(Value, Vec<(String, Value)>)> {
|
||||||
self.sound
|
self.sound
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
@@ -145,35 +145,26 @@ impl Forth {
|
|||||||
select_and_run(selected, stack, outputs, cmd)
|
select_and_run(selected, stack, outputs, cmd)
|
||||||
};
|
};
|
||||||
|
|
||||||
let drain_list_select_run = |idx_source: usize,
|
let emit_with_cycling = |cmd: &CmdRegister, emit_idx: usize, delta_secs: f64, outputs: &mut Vec<String>| -> Result<Option<Value>, String> {
|
||||||
err_msg: &str,
|
|
||||||
stack: &mut Vec<Value>,
|
|
||||||
outputs: &mut Vec<String>,
|
|
||||||
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, cmd)
|
|
||||||
};
|
|
||||||
|
|
||||||
let emit_once = |cmd: &CmdRegister, outputs: &mut Vec<String>| -> Result<Option<Value>, String> {
|
|
||||||
let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?;
|
let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?;
|
||||||
let sound = sound_val.as_str()?.to_string();
|
let resolved_sound_val = resolve_cycling(&sound_val, emit_idx);
|
||||||
|
// Note: sound span is recorded by Op::Emit, not here
|
||||||
|
let sound = resolved_sound_val.as_str()?.to_string();
|
||||||
let resolved_params: Vec<(String, String)> =
|
let resolved_params: Vec<(String, String)> =
|
||||||
params.iter().map(|(k, v)| (k.clone(), v.to_param_string())).collect();
|
params.iter().map(|(k, v)| {
|
||||||
emit_output(&sound, &resolved_params, ctx.step_duration(), ctx.nudge_secs, outputs);
|
let resolved = resolve_cycling(v, emit_idx);
|
||||||
Ok(Some(sound_val))
|
// Record selected span for params if they came from a CycleList
|
||||||
|
if let Value::CycleList(_) = v {
|
||||||
|
if let Some(span) = resolved.span() {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.selected_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(k.clone(), resolved.to_param_string())
|
||||||
|
}).collect();
|
||||||
|
emit_output(&sound, &resolved_params, ctx.step_duration(), delta_secs, outputs);
|
||||||
|
Ok(Some(resolved_sound_val))
|
||||||
};
|
};
|
||||||
|
|
||||||
while pc < ops.len() {
|
while pc < ops.len() {
|
||||||
@@ -361,12 +352,28 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Op::Emit => {
|
Op::Emit => {
|
||||||
if let Some(sound_val) = emit_once(cmd, outputs)? {
|
let deltas = if cmd.deltas().is_empty() {
|
||||||
if let Some(span) = sound_val.span() {
|
vec![Value::Float(0.0, None)]
|
||||||
|
} else {
|
||||||
|
cmd.deltas().to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
for (emit_idx, delta_val) in deltas.iter().enumerate() {
|
||||||
|
let delta_frac = delta_val.as_float()?;
|
||||||
|
let delta_secs = ctx.nudge_secs + delta_frac * ctx.step_duration();
|
||||||
|
// Record delta span for highlighting
|
||||||
|
if let Some(span) = delta_val.span() {
|
||||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
trace.selected_spans.push(span);
|
trace.selected_spans.push(span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(sound_val) = emit_with_cycling(cmd, emit_idx, delta_secs, outputs)? {
|
||||||
|
if let Some(span) = sound_val.span() {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.selected_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +445,19 @@ impl Forth {
|
|||||||
drain_select_run(count, idx, stack, outputs, cmd)?;
|
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Op::TCycle => {
|
||||||
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
|
if count == 0 {
|
||||||
|
return Err("tcycle 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();
|
||||||
|
stack.push(Value::CycleList(values));
|
||||||
|
}
|
||||||
|
|
||||||
Op::Choose => {
|
Op::Choose => {
|
||||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
@@ -606,37 +626,23 @@ impl Forth {
|
|||||||
cmd.set_param("dur".into(), Value::Float(dur, None));
|
cmd.set_param("dur".into(), Value::Float(dur, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::ListStart => {
|
Op::At => {
|
||||||
stack.push(Value::Marker);
|
let top = stack.pop().ok_or("stack underflow")?;
|
||||||
}
|
let deltas = match &top {
|
||||||
|
Value::Float(..) => vec![top],
|
||||||
Op::ListEnd => {
|
Value::Int(n, _) if *n > 0 && stack.len() >= *n as usize => {
|
||||||
let mut count = 0;
|
let count = *n as usize;
|
||||||
let mut values = Vec::new();
|
let mut vals = Vec::with_capacity(count);
|
||||||
while let Some(v) = stack.pop() {
|
for _ in 0..count {
|
||||||
if v.is_marker() {
|
vals.push(stack.pop().ok_or("stack underflow")?);
|
||||||
break;
|
}
|
||||||
|
vals.reverse();
|
||||||
|
vals
|
||||||
}
|
}
|
||||||
values.push(v);
|
Value::Int(..) => vec![top],
|
||||||
count += 1;
|
_ => return Err("at expects number or list".into()),
|
||||||
}
|
|
||||||
values.reverse();
|
|
||||||
for v in values {
|
|
||||||
stack.push(v);
|
|
||||||
}
|
|
||||||
stack.push(Value::Int(count, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::ListEndCycle | Op::ListEndPCycle => {
|
|
||||||
let idx_source = match &ops[pc] {
|
|
||||||
Op::ListEndCycle => ctx.runs,
|
|
||||||
_ => ctx.iter,
|
|
||||||
};
|
};
|
||||||
let err_msg = match &ops[pc] {
|
cmd.set_deltas(deltas);
|
||||||
Op::ListEndCycle => "empty cycle list",
|
|
||||||
_ => "empty pattern cycle list",
|
|
||||||
};
|
|
||||||
drain_list_select_run(idx_source, err_msg, stack, outputs, cmd)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Adsr => {
|
Op::Adsr => {
|
||||||
@@ -699,8 +705,8 @@ impl Forth {
|
|||||||
if n < 0 {
|
if n < 0 {
|
||||||
return Err("emit count must be >= 0".into());
|
return Err("emit count must be >= 0".into());
|
||||||
}
|
}
|
||||||
for _ in 0..n {
|
for i in 0..n as usize {
|
||||||
emit_once(cmd, outputs)?;
|
emit_with_cycling(cmd, i, ctx.nudge_secs, outputs)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -846,3 +852,12 @@ fn format_cmd(pairs: &[(String, String)]) -> String {
|
|||||||
let parts: Vec<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
let parts: Vec<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
||||||
format!("/{}", parts.join("/"))
|
format!("/{}", parts.join("/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_cycling(val: &Value, emit_idx: usize) -> Value {
|
||||||
|
match val {
|
||||||
|
Value::CycleList(items) if !items.is_empty() => {
|
||||||
|
items[emit_idx % items.len()].clone()
|
||||||
|
}
|
||||||
|
other => other.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub const WORDS: &[Word] = &[
|
|||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "dupn",
|
name: "dupn",
|
||||||
aliases: &[],
|
aliases: &["!"],
|
||||||
category: "Stack",
|
category: "Stack",
|
||||||
stack: "(a n -- a a ... a)",
|
stack: "(a n -- a a ... a)",
|
||||||
desc: "Duplicate a onto stack n times",
|
desc: "Duplicate a onto stack n times",
|
||||||
@@ -482,19 +482,28 @@ pub const WORDS: &[Word] = &[
|
|||||||
Word {
|
Word {
|
||||||
name: "cycle",
|
name: "cycle",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Lists",
|
category: "Selection",
|
||||||
stack: "(..n n -- val)",
|
stack: "(v1..vn n -- selected)",
|
||||||
desc: "Cycle through n items by step",
|
desc: "Cycle through n items by step runs",
|
||||||
example: "1 2 3 3 cycle",
|
example: "60 64 67 3 cycle",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "pcycle",
|
name: "pcycle",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Lists",
|
category: "Selection",
|
||||||
stack: "(..n n -- val)",
|
stack: "(v1..vn n -- selected)",
|
||||||
desc: "Cycle through n items by pattern",
|
desc: "Cycle through n items by pattern iteration",
|
||||||
example: "1 2 3 3 pcycle",
|
example: "60 64 67 3 pcycle",
|
||||||
|
compile: Simple,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "tcycle",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Selection",
|
||||||
|
stack: "(v1..vn n -- CycleList)",
|
||||||
|
desc: "Create cycle list for emit-time resolution",
|
||||||
|
example: "60 64 67 3 tcycle note",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
@@ -799,41 +808,13 @@ pub const WORDS: &[Word] = &[
|
|||||||
example: "1 4 chain",
|
example: "1 4 chain",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
},
|
},
|
||||||
// Lists
|
|
||||||
Word {
|
Word {
|
||||||
name: "[",
|
name: "at",
|
||||||
aliases: &["<", "<<"],
|
|
||||||
category: "Lists",
|
|
||||||
stack: "(-- marker)",
|
|
||||||
desc: "Start list",
|
|
||||||
example: "[ 1 2 3 ]",
|
|
||||||
compile: Simple,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "]",
|
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Lists",
|
category: "Time",
|
||||||
stack: "(marker..n -- n)",
|
stack: "(list|n --)",
|
||||||
desc: "End list, push count",
|
desc: "Set delta context for emit timing",
|
||||||
example: "[ 1 2 3 ] => 3",
|
example: "[ 0 0.5 ] at kick s . => emits at 0 and 0.5 of step",
|
||||||
compile: Simple,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: ">",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Lists",
|
|
||||||
stack: "(marker..n -- val)",
|
|
||||||
desc: "End cycle list, pick by step",
|
|
||||||
example: "< 1 2 3 > => cycles through 1, 2, 3",
|
|
||||||
compile: Simple,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: ">>",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Lists",
|
|
||||||
stack: "(marker..n -- val)",
|
|
||||||
desc: "End pattern cycle list, pick by pattern",
|
|
||||||
example: "<< 1 2 3 >> => cycles through 1, 2, 3 per pattern",
|
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
},
|
},
|
||||||
// Quotations
|
// Quotations
|
||||||
@@ -2050,6 +2031,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"seed" => Op::Seed,
|
"seed" => Op::Seed,
|
||||||
"cycle" => Op::Cycle,
|
"cycle" => Op::Cycle,
|
||||||
"pcycle" => Op::PCycle,
|
"pcycle" => Op::PCycle,
|
||||||
|
"tcycle" => Op::TCycle,
|
||||||
"choose" => Op::Choose,
|
"choose" => Op::Choose,
|
||||||
"every" => Op::Every,
|
"every" => Op::Every,
|
||||||
"chance" => Op::ChanceExec,
|
"chance" => Op::ChanceExec,
|
||||||
@@ -2061,10 +2043,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"!?" => Op::Unless,
|
"!?" => Op::Unless,
|
||||||
"tempo!" => Op::SetTempo,
|
"tempo!" => Op::SetTempo,
|
||||||
"speed!" => Op::SetSpeed,
|
"speed!" => Op::SetSpeed,
|
||||||
"[" => Op::ListStart,
|
"at" => Op::At,
|
||||||
"]" => Op::ListEnd,
|
|
||||||
">" => Op::ListEndCycle,
|
|
||||||
">>" => Op::ListEndPCycle,
|
|
||||||
"adsr" => Op::Adsr,
|
"adsr" => Op::Adsr,
|
||||||
"ad" => Op::Ad,
|
"ad" => Op::Ad,
|
||||||
"apply" => Op::Apply,
|
"apply" => Op::Apply,
|
||||||
|
|||||||
@@ -476,6 +476,9 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
KeyCode::Char('p') if ctrl => {
|
KeyCode::Char('p') if ctrl => {
|
||||||
editor.search_prev();
|
editor.search_prev();
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('k') if ctrl => {
|
||||||
|
ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack;
|
||||||
|
}
|
||||||
KeyCode::Char('a') if ctrl => {
|
KeyCode::Char('a') if ctrl => {
|
||||||
editor.select_all();
|
editor.select_all();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ pub struct EditorContext {
|
|||||||
pub editor: Editor,
|
pub editor: Editor,
|
||||||
pub selection_anchor: Option<usize>,
|
pub selection_anchor: Option<usize>,
|
||||||
pub copied_steps: Option<CopiedSteps>,
|
pub copied_steps: Option<CopiedSteps>,
|
||||||
|
pub show_stack: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -94,6 +95,7 @@ impl Default for EditorContext {
|
|||||||
editor: Editor::new(),
|
editor: Editor::new(),
|
||||||
selection_anchor: None,
|
selection_anchor: None,
|
||||||
copied_steps: None,
|
copied_steps: None,
|
||||||
|
show_stack: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
use rand::SeedableRng;
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table};
|
use ratatui::widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
use cagire_forth::Forth;
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::engine::{LinkState, SequencerSnapshot};
|
use crate::engine::{LinkState, SequencerSnapshot};
|
||||||
use crate::model::SourceSpan;
|
use crate::model::{SourceSpan, StepContext, Value};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel};
|
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel};
|
||||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||||
@@ -20,6 +25,64 @@ use super::{
|
|||||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor) -> String {
|
||||||
|
let cursor_line = editor.cursor().0;
|
||||||
|
let partial: Vec<&str> = lines.iter().take(cursor_line + 1).map(|s| s.as_str()).collect();
|
||||||
|
let script = partial.join("\n");
|
||||||
|
|
||||||
|
if script.trim().is_empty() {
|
||||||
|
return "Stack: []".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let vars = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let dict = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42)));
|
||||||
|
let forth = Forth::new(vars, dict, rng);
|
||||||
|
|
||||||
|
let ctx = StepContext {
|
||||||
|
step: 0,
|
||||||
|
beat: 0.0,
|
||||||
|
bank: 0,
|
||||||
|
pattern: 0,
|
||||||
|
tempo: 120.0,
|
||||||
|
phase: 0.0,
|
||||||
|
slot: 0,
|
||||||
|
runs: 0,
|
||||||
|
iter: 0,
|
||||||
|
speed: 1.0,
|
||||||
|
fill: false,
|
||||||
|
nudge_secs: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
match forth.evaluate(&script, &ctx) {
|
||||||
|
Ok(_) => {
|
||||||
|
let stack = forth.stack();
|
||||||
|
let formatted: Vec<String> = stack.iter().map(format_value).collect();
|
||||||
|
format!("Stack: [{}]", formatted.join(" "))
|
||||||
|
}
|
||||||
|
Err(e) => format!("Error: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_value(v: &Value) -> String {
|
||||||
|
match v {
|
||||||
|
Value::Int(n, _) => n.to_string(),
|
||||||
|
Value::Float(f, _) => {
|
||||||
|
if f.fract() == 0.0 && f.abs() < 1_000_000.0 {
|
||||||
|
format!("{f:.1}")
|
||||||
|
} else {
|
||||||
|
format!("{f:.4}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Str(s, _) => format!("\"{s}\""),
|
||||||
|
Value::Quotation(..) => "[...]".to_string(),
|
||||||
|
Value::CycleList(items) => {
|
||||||
|
let inner: Vec<String> = items.iter().map(format_value).collect();
|
||||||
|
format!("({})", inner.join(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn adjust_spans_for_line(
|
fn adjust_spans_for_line(
|
||||||
spans: &[SourceSpan],
|
spans: &[SourceSpan],
|
||||||
line_start: usize,
|
line_start: usize,
|
||||||
@@ -619,28 +682,24 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
let show_search = app.editor_ctx.editor.search_active()
|
let show_search = app.editor_ctx.editor.search_active()
|
||||||
|| !app.editor_ctx.editor.search_query().is_empty();
|
|| !app.editor_ctx.editor.search_query().is_empty();
|
||||||
|
|
||||||
let (search_area, editor_area, hint_area) = if show_search {
|
let reserved_lines = 1 + if show_search { 1 } else { 0 };
|
||||||
let search_area = Rect::new(inner.x, inner.y, inner.width, 1);
|
let editor_height = inner.height.saturating_sub(reserved_lines);
|
||||||
let editor_area = Rect::new(
|
|
||||||
inner.x,
|
let mut y = inner.y;
|
||||||
inner.y + 1,
|
|
||||||
inner.width,
|
let search_area = if show_search {
|
||||||
inner.height.saturating_sub(2),
|
let area = Rect::new(inner.x, y, inner.width, 1);
|
||||||
);
|
y += 1;
|
||||||
let hint_area =
|
Some(area)
|
||||||
Rect::new(inner.x, inner.y + 1 + editor_area.height, inner.width, 1);
|
|
||||||
(Some(search_area), editor_area, hint_area)
|
|
||||||
} else {
|
} else {
|
||||||
let editor_area = Rect::new(
|
None
|
||||||
inner.x,
|
|
||||||
inner.y,
|
|
||||||
inner.width,
|
|
||||||
inner.height.saturating_sub(1),
|
|
||||||
);
|
|
||||||
let hint_area = Rect::new(inner.x, inner.y + editor_area.height, inner.width, 1);
|
|
||||||
(None, editor_area, hint_area)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let editor_area = Rect::new(inner.x, y, inner.width, editor_height);
|
||||||
|
y += editor_height;
|
||||||
|
|
||||||
|
let hint_area = Rect::new(inner.x, y, inner.width, 1);
|
||||||
|
|
||||||
if let Some(sa) = search_area {
|
if let Some(sa) = search_area {
|
||||||
let style = if app.editor_ctx.editor.search_active() {
|
let style = if app.editor_ctx.editor.search_active() {
|
||||||
Style::default().fg(Color::Yellow)
|
Style::default().fg(Color::Yellow)
|
||||||
@@ -671,32 +730,52 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
|
|
||||||
let dim = Style::default().fg(Color::DarkGray);
|
let dim = Style::default().fg(Color::DarkGray);
|
||||||
let key = Style::default().fg(Color::Yellow);
|
let key = Style::default().fg(Color::Yellow);
|
||||||
let hint = if app.editor_ctx.editor.search_active() {
|
|
||||||
Line::from(vec![
|
if app.editor_ctx.editor.search_active() {
|
||||||
|
let hint = Line::from(vec![
|
||||||
Span::styled("Enter", key),
|
Span::styled("Enter", key),
|
||||||
Span::styled(" confirm ", dim),
|
Span::styled(" confirm ", dim),
|
||||||
Span::styled("Esc", key),
|
Span::styled("Esc", key),
|
||||||
Span::styled(" cancel", dim),
|
Span::styled(" cancel", dim),
|
||||||
|
]);
|
||||||
|
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||||
|
} else if app.editor_ctx.show_stack {
|
||||||
|
let stack_text = compute_stack_display(text_lines, &app.editor_ctx.editor);
|
||||||
|
let hint = Line::from(vec![
|
||||||
|
Span::styled("Esc", key),
|
||||||
|
Span::styled(" save ", dim),
|
||||||
|
Span::styled("C-e", key),
|
||||||
|
Span::styled(" eval ", dim),
|
||||||
|
Span::styled("C-k", key),
|
||||||
|
Span::styled(" hide", dim),
|
||||||
|
]);
|
||||||
|
let [hint_left, stack_right] = Layout::horizontal([
|
||||||
|
Constraint::Length(hint.width() as u16),
|
||||||
|
Constraint::Fill(1),
|
||||||
])
|
])
|
||||||
|
.areas(hint_area);
|
||||||
|
frame.render_widget(Paragraph::new(hint), hint_left);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right),
|
||||||
|
stack_right,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Line::from(vec![
|
let hint = Line::from(vec![
|
||||||
Span::styled("Esc", key),
|
Span::styled("Esc", key),
|
||||||
Span::styled(" save ", dim),
|
Span::styled(" save ", dim),
|
||||||
Span::styled("C-e", key),
|
Span::styled("C-e", key),
|
||||||
Span::styled(" eval ", dim),
|
Span::styled(" eval ", dim),
|
||||||
Span::styled("C-f", key),
|
Span::styled("C-f", key),
|
||||||
Span::styled(" find ", dim),
|
Span::styled(" find ", dim),
|
||||||
Span::styled("C-n", key),
|
Span::styled("C-k", key),
|
||||||
Span::styled("/", dim),
|
Span::styled(" stack ", dim),
|
||||||
Span::styled("C-p", key),
|
|
||||||
Span::styled(" next/prev ", dim),
|
|
||||||
Span::styled("C-u", key),
|
Span::styled("C-u", key),
|
||||||
Span::styled("/", dim),
|
Span::styled("/", dim),
|
||||||
Span::styled("C-r", key),
|
Span::styled("C-r", key),
|
||||||
Span::styled(" undo/redo", dim),
|
Span::styled(" undo/redo", dim),
|
||||||
])
|
]);
|
||||||
};
|
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
}
|
||||||
}
|
}
|
||||||
Modal::PatternProps {
|
Modal::PatternProps {
|
||||||
bank,
|
bank,
|
||||||
|
|||||||
@@ -51,22 +51,6 @@ fn string_with_spaces() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_count() {
|
|
||||||
let f = run("[ 1 2 3 ]");
|
|
||||||
assert_eq!(stack_int(&f), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_empty() {
|
|
||||||
expect_int("[ ]", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_preserves_values() {
|
|
||||||
expect_int("[ 10 20 ] drop +", 30);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn conditional_based_on_step() {
|
fn conditional_based_on_step() {
|
||||||
let ctx0 = ctx_with(|c| c.step = 0);
|
let ctx0 = ctx_with(|c| c.step = 0);
|
||||||
|
|||||||
@@ -1,124 +1,106 @@
|
|||||||
use super::harness::*;
|
use super::harness::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn choose_word_from_list() {
|
fn choose_from_stack() {
|
||||||
// 1 2 [ + - ] choose: picks + or -, applies to 1 2
|
|
||||||
// With seed 42, choose picks one deterministically
|
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let ctx = default_ctx();
|
let ctx = default_ctx();
|
||||||
f.evaluate("1 2 [ + - ] choose", &ctx).unwrap();
|
f.evaluate("1 2 3 3 choose", &ctx).unwrap();
|
||||||
let val = stack_int(&f);
|
let val = stack_int(&f);
|
||||||
assert!(val == 3 || val == -1, "expected 3 or -1, got {}", val);
|
assert!(val >= 1 && val <= 3, "expected 1, 2, or 3, got {}", val);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_word_from_list() {
|
fn cycle_by_runs() {
|
||||||
// At runs=0, picks first word (dup)
|
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
let f = run_ctx("5 < dup nip >", &ctx);
|
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 5); // dup leaves 5 5, but stack check takes top
|
|
||||||
|
|
||||||
// At runs=1, picks second word (2 *)
|
|
||||||
let f = forth();
|
|
||||||
let ctx = ctx_with(|c| c.runs = 1);
|
|
||||||
f.evaluate(": double 2 * ; 5 < dup double >", &ctx).unwrap();
|
|
||||||
assert_eq!(stack_int(&f), 10);
|
assert_eq!(stack_int(&f), 10);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn user_word_in_list() {
|
|
||||||
let f = forth();
|
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
|
||||||
f.evaluate(": add3 3 + ; : add5 5 + ; 10 < add3 add5 >", &ctx).unwrap();
|
|
||||||
assert_eq!(stack_int(&f), 13); // runs=0 picks add3
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn user_word_in_list_second() {
|
|
||||||
let f = forth();
|
|
||||||
let ctx = ctx_with(|c| c.runs = 1);
|
let ctx = ctx_with(|c| c.runs = 1);
|
||||||
f.evaluate(": add3 3 + ; : add5 5 + ; 10 < add3 add5 >", &ctx).unwrap();
|
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 15); // runs=1 picks add5
|
assert_eq!(stack_int(&f), 20);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn values_in_list_still_work() {
|
|
||||||
// Numbers inside lists should still push as values (not quotations)
|
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
|
||||||
let f = run_ctx("< 10 20 30 >", &ctx);
|
|
||||||
assert_eq!(stack_int(&f), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn values_in_list_cycle() {
|
|
||||||
let ctx = ctx_with(|c| c.runs = 2);
|
let ctx = ctx_with(|c| c.runs = 2);
|
||||||
let f = run_ctx("< 10 20 30 >", &ctx);
|
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||||
|
assert_eq!(stack_int(&f), 30);
|
||||||
|
|
||||||
|
let ctx = ctx_with(|c| c.runs = 3);
|
||||||
|
let f = run_ctx("10 20 30 3 cycle", &ctx);
|
||||||
|
assert_eq!(stack_int(&f), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pcycle_by_iter() {
|
||||||
|
let ctx = ctx_with(|c| c.iter = 0);
|
||||||
|
let f = run_ctx("10 20 30 3 pcycle", &ctx);
|
||||||
|
assert_eq!(stack_int(&f), 10);
|
||||||
|
|
||||||
|
let ctx = ctx_with(|c| c.iter = 1);
|
||||||
|
let f = run_ctx("10 20 30 3 pcycle", &ctx);
|
||||||
|
assert_eq!(stack_int(&f), 20);
|
||||||
|
|
||||||
|
let ctx = ctx_with(|c| c.iter = 2);
|
||||||
|
let f = run_ctx("10 20 30 3 pcycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 30);
|
assert_eq!(stack_int(&f), 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mixed_values_and_words() {
|
fn cycle_with_quotations() {
|
||||||
// Values stay as values, words become quotations
|
|
||||||
// [ 10 20 ] choose just picks a number
|
|
||||||
let f = forth();
|
|
||||||
let ctx = default_ctx();
|
|
||||||
f.evaluate("[ 10 20 ] choose", &ctx).unwrap();
|
|
||||||
let val = stack_int(&f);
|
|
||||||
assert!(val == 10 || val == 20, "expected 10 or 20, got {}", val);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn word_with_sound_params() {
|
|
||||||
let f = forth();
|
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
let outputs = f.evaluate(
|
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx);
|
||||||
": myverb 0.5 verb ; \"sine\" s 440 freq < myverb > .",
|
|
||||||
&ctx
|
|
||||||
).unwrap();
|
|
||||||
assert_eq!(outputs.len(), 1);
|
|
||||||
assert!(outputs[0].contains("verb/0.5"), "expected verb/0.5 in {}", outputs[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn arithmetic_word_in_list() {
|
|
||||||
// 3 4 [ + ] choose -> picks + (only option), applies to 3 4 = 7
|
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
|
||||||
let f = run_ctx("3 4 < + >", &ctx);
|
|
||||||
assert_eq!(stack_int(&f), 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pcycle_word_from_list() {
|
|
||||||
let ctx = ctx_with(|c| c.iter = 0);
|
|
||||||
let f = run_ctx("10 << dup 2 * >>", &ctx);
|
|
||||||
// iter=0 picks dup: 10 10
|
|
||||||
let stack = f.stack();
|
let stack = f.stack();
|
||||||
assert_eq!(stack.len(), 2);
|
assert_eq!(stack.len(), 2);
|
||||||
|
assert_eq!(stack_int(&f), 5);
|
||||||
|
|
||||||
|
let ctx = ctx_with(|c| c.runs = 1);
|
||||||
|
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 10);
|
assert_eq!(stack_int(&f), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pcycle_word_second() {
|
fn cycle_executes_quotation() {
|
||||||
let ctx = ctx_with(|c| c.iter = 1);
|
|
||||||
let f = run_ctx("10 << dup 2 * >>", &ctx);
|
|
||||||
// iter=1 picks "2 *" — but wait, each token is its own element
|
|
||||||
// so << dup 2 * >> has 3 elements: {dup}, 2, {*}
|
|
||||||
// iter=1 picks element index 1 which is value 2
|
|
||||||
assert_eq!(stack_int(&f), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multi_op_quotation_in_list() {
|
|
||||||
// Use { } for multi-op quotations inside lists
|
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
let f = run_ctx("10 < { 2 * } { 3 + } >", &ctx);
|
let f = run_ctx("10 { 3 + } { 5 + } 2 cycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 20); // runs=0 picks {2 *}
|
assert_eq!(stack_int(&f), 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multi_op_quotation_second() {
|
fn dupn_basic() {
|
||||||
let ctx = ctx_with(|c| c.runs = 1);
|
// 5 3 dupn -> 5 5 5, then + + -> 15
|
||||||
let f = run_ctx("10 < { 2 * } { 3 + } >", &ctx);
|
expect_int("5 3 dupn + +", 15);
|
||||||
assert_eq!(stack_int(&f), 13); // runs=1 picks {3 +}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dupn_alias() {
|
||||||
|
expect_int("5 3 ! + +", 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tcycle_creates_cycle_list() {
|
||||||
|
let outputs = expect_outputs(r#"0.0 at 60 64 67 3 tcycle note sine s ."#, 1);
|
||||||
|
assert!(outputs[0].contains("note/60"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tcycle_with_multiple_emits() {
|
||||||
|
let f = forth();
|
||||||
|
let ctx = default_ctx();
|
||||||
|
let outputs = f.evaluate(r#"0 0.5 2 at 60 64 2 tcycle note sine s ."#, &ctx).unwrap();
|
||||||
|
assert_eq!(outputs.len(), 2);
|
||||||
|
assert!(outputs[0].contains("note/60"));
|
||||||
|
assert!(outputs[1].contains("note/64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_zero_count_error() {
|
||||||
|
expect_error("1 2 3 0 cycle", "cycle count must be > 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn choose_zero_count_error() {
|
||||||
|
expect_error("1 2 3 0 choose", "choose count must be > 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tcycle_zero_count_error() {
|
||||||
|
expect_error("1 2 3 0 tcycle", "tcycle count must be > 0");
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ fn get_sounds(outputs: &[String]) -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_param(outputs: &[String], param: &str) -> Vec<f64> {
|
||||||
|
outputs
|
||||||
|
.iter()
|
||||||
|
.map(|o| parse_params(o).get(param).copied().unwrap_or(0.0))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
const EPSILON: f64 = 1e-9;
|
const EPSILON: f64 = 1e-9;
|
||||||
|
|
||||||
fn approx_eq(a: f64, b: f64) -> bool {
|
fn approx_eq(a: f64, b: f64) -> bool {
|
||||||
@@ -95,29 +102,29 @@ fn dur_is_step_duration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_picks_by_step() {
|
fn cycle_picks_by_runs() {
|
||||||
for runs in 0..4 {
|
for runs in 0..4 {
|
||||||
let ctx = ctx_with(|c| c.runs = runs);
|
let ctx = ctx_with(|c| c.runs = runs);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s < . _ >"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" s { . } { } 2 cycle"#, &ctx).unwrap();
|
||||||
if runs % 2 == 0 {
|
if runs % 2 == 0 {
|
||||||
assert_eq!(outputs.len(), 1, "runs={}: . should be picked", runs);
|
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(outputs.len(), 0, "runs={}: _ should be picked", runs);
|
assert_eq!(outputs.len(), 0, "runs={}: no-op should be picked", runs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pcycle_picks_by_pattern() {
|
fn pcycle_picks_by_iter() {
|
||||||
for iter in 0..4 {
|
for iter in 0..4 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s << . _ >>"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" s { . } { } 2 pcycle"#, &ctx).unwrap();
|
||||||
if iter % 2 == 0 {
|
if iter % 2 == 0 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: . should be picked", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(outputs.len(), 0, "iter={}: _ should be picked", iter);
|
assert_eq!(outputs.len(), 0, "iter={}: no-op should be picked", iter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +134,10 @@ fn cycle_with_sounds() {
|
|||||||
for runs in 0..3 {
|
for runs in 0..3 {
|
||||||
let ctx = ctx_with(|c| c.runs = runs);
|
let ctx = ctx_with(|c| c.runs = runs);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#"< { "kick" s . } { "hat" s . } { "snare" s . } >"#, &ctx).unwrap();
|
let outputs = f.evaluate(
|
||||||
|
r#"{ "kick" s . } { "hat" s . } { "snare" s . } 3 cycle"#,
|
||||||
|
&ctx
|
||||||
|
).unwrap();
|
||||||
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
||||||
let sounds = get_sounds(&outputs);
|
let sounds = get_sounds(&outputs);
|
||||||
let expected = ["kick", "hat", "snare"][runs % 3];
|
let expected = ["kick", "hat", "snare"][runs % 3];
|
||||||
@@ -154,3 +164,107 @@ fn emit_n_negative_error() {
|
|||||||
let result = f.evaluate(r#""kick" s -1 .!"#, &default_ctx());
|
let result = f.evaluate(r#""kick" s -1 .!"#, &default_ctx());
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_single_delta() {
|
||||||
|
let outputs = expect_outputs(r#"0.5 at "kick" s ."#, 1);
|
||||||
|
let deltas = get_deltas(&outputs);
|
||||||
|
let step_dur = 0.125;
|
||||||
|
assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_list_deltas() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s ."#, 2);
|
||||||
|
let deltas = get_deltas(&outputs);
|
||||||
|
let step_dur = 0.125;
|
||||||
|
assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
|
||||||
|
assert!(approx_eq(deltas[1], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_three_deltas() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.33 0.67 3 at "kick" s ."#, 3);
|
||||||
|
let deltas = get_deltas(&outputs);
|
||||||
|
let step_dur = 0.125;
|
||||||
|
assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
|
||||||
|
assert!((deltas[1] - 0.33 * step_dur).abs() < 0.001, "expected delta at 0.33 of step");
|
||||||
|
assert!((deltas[2] - 0.67 * step_dur).abs() < 0.001, "expected delta at 0.67 of step");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_persists_across_emits() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s . "hat" s ."#, 4);
|
||||||
|
let sounds = get_sounds(&outputs);
|
||||||
|
assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tcycle_basic() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.5 0.75 3 at 60 64 67 3 tcycle note sine s ."#, 3);
|
||||||
|
let notes = get_param(&outputs, "note");
|
||||||
|
assert_eq!(notes, vec![60.0, 64.0, 67.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tcycle_wraps() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.33 0.67 3 at 60 64 2 tcycle note sine s ."#, 3);
|
||||||
|
let notes = get_param(&outputs, "note");
|
||||||
|
assert_eq!(notes, vec![60.0, 64.0, 60.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tcycle_with_sound() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.5 2 at kick hat 2 tcycle s ."#, 2);
|
||||||
|
let sounds = get_sounds(&outputs);
|
||||||
|
assert_eq!(sounds, vec!["kick", "hat"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tcycle_multiple_params() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.5 0.75 3 at 60 64 67 3 tcycle note 0.5 1.0 2 tcycle gain sine s ."#, 3);
|
||||||
|
let notes = get_param(&outputs, "note");
|
||||||
|
let gains = get_param(&outputs, "gain");
|
||||||
|
assert_eq!(notes, vec![60.0, 64.0, 67.0]);
|
||||||
|
assert_eq!(gains, vec![0.5, 1.0, 0.5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_reset_with_zero() {
|
||||||
|
let outputs = expect_outputs(r#"0 0.5 2 at "kick" s . 0.0 at "hat" s ."#, 3);
|
||||||
|
let sounds = get_sounds(&outputs);
|
||||||
|
assert_eq!(sounds, vec!["kick", "kick", "hat"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tcycle_records_selected_spans() {
|
||||||
|
use cagire::forth::ExecutionTrace;
|
||||||
|
|
||||||
|
let f = forth();
|
||||||
|
let mut trace = ExecutionTrace::default();
|
||||||
|
let script = r#"0 0.5 2 at kick hat 2 tcycle s ."#;
|
||||||
|
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
||||||
|
|
||||||
|
// Should have 4 selected spans:
|
||||||
|
// - 2 for at deltas (0 and 0.5)
|
||||||
|
// - 2 for tcycle sound values (kick and hat)
|
||||||
|
assert_eq!(trace.selected_spans.len(), 4, "expected 4 selected spans (2 at + 2 tcycle)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_records_selected_spans() {
|
||||||
|
use cagire::forth::ExecutionTrace;
|
||||||
|
|
||||||
|
let f = forth();
|
||||||
|
let mut trace = ExecutionTrace::default();
|
||||||
|
let script = r#"0 0.5 0.75 3 at "kick" s ."#;
|
||||||
|
f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();
|
||||||
|
|
||||||
|
// Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
|
||||||
|
assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)");
|
||||||
|
|
||||||
|
// Verify at delta spans (even indices: 0, 2, 4)
|
||||||
|
assert_eq!(&script[trace.selected_spans[0].start..trace.selected_spans[0].end], "0");
|
||||||
|
assert_eq!(&script[trace.selected_spans[2].start..trace.selected_spans[2].end], "0.5");
|
||||||
|
assert_eq!(&script[trace.selected_spans[4].start..trace.selected_spans[4].end], "0.75");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user