Feat: improve 'at' in cagire grammar

This commit is contained in:
2026-03-20 23:29:47 +01:00
parent 609fe108bc
commit f020b5a172
13 changed files with 293 additions and 277 deletions

View File

@@ -176,6 +176,13 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
ops.push(Op::Branch(else_ops.len()));
ops.extend(else_ops);
}
} else if word == "at" {
if let Some((body_ops, consumed)) = compile_at(&tokens[i + 1..], dict)? {
i += consumed;
ops.push(Op::AtLoop(Arc::from(body_ops)));
} else if !compile_word(word, Some(*span), &mut ops, dict) {
return Err(format!("unknown word: {word}"));
}
} else if word == "case" {
let (case_ops, consumed) = compile_case(&tokens[i + 1..], dict)?;
i += consumed;
@@ -355,6 +362,37 @@ fn compile_if(
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
}
fn compile_at(tokens: &[Token], dict: &Dictionary) -> Result<Option<(Vec<Op>, usize)>, String> {
let mut depth = 1;
enum AtCloser { Dot, MidiDot, Done }
let mut found: Option<(usize, AtCloser)> = None;
for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok {
match w.as_str() {
"at" => depth += 1,
"." if depth == 1 => { found = Some((i, AtCloser::Dot)); break; }
"m." if depth == 1 => { found = Some((i, AtCloser::MidiDot)); break; }
"done" if depth == 1 => { found = Some((i, AtCloser::Done)); break; }
"." | "m." | "done" => depth -= 1,
_ => {}
}
}
}
let Some((pos, closer)) = found else {
return Ok(None);
};
let mut body_ops = compile(&tokens[..pos], dict)?;
match closer {
AtCloser::Dot => body_ops.push(Op::Emit),
AtCloser::MidiDot => body_ops.push(Op::MidiEmit),
AtCloser::Done => {}
}
Ok(Some((body_ops, pos + 1)))
}
fn compile_case(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize), String> {
let mut depth = 1;
let mut endcase_pos = None;

View File

@@ -110,7 +110,8 @@ pub enum Op {
ClearCmd,
SetSpeed,
At,
Arp,
AtLoop(Arc<[Op]>),
IntRange,
StepRange,
Generate,

View File

@@ -96,7 +96,7 @@ pub enum Value {
Str(Arc<str>, Option<SourceSpan>),
Quotation(Arc<[Op]>, Option<SourceSpan>),
CycleList(Arc<[Value]>),
ArpList(Arc<[Value]>),
}
impl PartialEq for Value {
@@ -107,7 +107,7 @@ impl PartialEq for Value {
(Value::Str(a, _), Value::Str(b, _)) => a == b,
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
(Value::CycleList(a), Value::CycleList(b)) => a == b,
(Value::ArpList(a), Value::ArpList(b)) => a == b,
_ => false,
}
}
@@ -143,7 +143,7 @@ impl Value {
Value::Float(f, _) => *f != 0.0,
Value::Str(s, _) => !s.is_empty(),
Value::Quotation(..) => true,
Value::CycleList(items) | Value::ArpList(items) => !items.is_empty(),
Value::CycleList(items) => !items.is_empty(),
}
}
@@ -153,14 +153,14 @@ impl Value {
Value::Float(f, _) => f.to_string(),
Value::Str(s, _) => s.to_string(),
Value::Quotation(..) => String::new(),
Value::CycleList(_) | Value::ArpList(_) => String::new(),
Value::CycleList(_) => String::new(),
}
}
pub(super) fn span(&self) -> Option<SourceSpan> {
match self {
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
Value::CycleList(_) | Value::ArpList(_) => None,
Value::CycleList(_) => None,
}
}
}
@@ -171,6 +171,7 @@ pub(super) struct CmdRegister {
params: Vec<(&'static str, Value)>,
deltas: Vec<Value>,
global_params: Vec<(&'static str, Value)>,
delta_secs: Option<f64>,
}
impl CmdRegister {
@@ -180,6 +181,7 @@ impl CmdRegister {
params: Vec::with_capacity(16),
deltas: Vec::with_capacity(4),
global_params: Vec::new(),
delta_secs: None,
}
}
@@ -237,9 +239,26 @@ impl CmdRegister {
std::mem::take(&mut self.global_params)
}
pub(super) fn set_delta_secs(&mut self, secs: f64) {
self.delta_secs = Some(secs);
}
pub(super) fn take_delta_secs(&mut self) -> Option<f64> {
self.delta_secs.take()
}
pub(super) fn clear_sound(&mut self) {
self.sound = None;
}
pub(super) fn clear_params(&mut self) {
self.params.clear();
}
pub(super) fn clear(&mut self) {
self.sound = None;
self.params.clear();
self.deltas.clear();
self.delta_secs = None;
}
}

View File

@@ -241,31 +241,7 @@ impl Forth {
sound_len.max(param_max)
};
let has_arp_list = |cmd: &CmdRegister| -> bool {
matches!(cmd.sound(), Some(Value::ArpList(_)))
|| cmd.global_params().iter().chain(cmd.params().iter())
.any(|(_, v)| matches!(v, Value::ArpList(_)))
};
let compute_arp_count = |cmd: &CmdRegister| -> usize {
let sound_len = match cmd.sound() {
Some(Value::ArpList(items)) => items.len(),
_ => 0,
};
let param_max = cmd
.params()
.iter()
.map(|(_, v)| match v {
Value::ArpList(items) => items.len(),
_ => 0,
})
.max()
.unwrap_or(0);
sound_len.max(param_max).max(1)
};
let emit_with_cycling = |cmd: &CmdRegister,
arp_idx: usize,
poly_idx: usize,
delta_secs: f64,
outputs: &mut Vec<String>|
@@ -277,7 +253,7 @@ impl Forth {
return Err("nothing to emit".into());
}
let resolved_sound_val =
cmd.sound().map(|sv| resolve_value(sv, arp_idx, poly_idx));
cmd.sound().map(|sv| resolve_value(sv, poly_idx));
let sound_str = match &resolved_sound_val {
Some(v) => Some(v.as_str()?.to_string()),
None => None,
@@ -286,8 +262,8 @@ impl Forth {
.iter()
.chain(cmd.params().iter())
.map(|(k, v)| {
let resolved = resolve_value(v, arp_idx, poly_idx);
if let Value::CycleList(_) | Value::ArpList(_) = v {
let resolved = resolve_value(v, poly_idx);
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);
@@ -595,47 +571,17 @@ impl Forth {
}
Op::Emit => {
if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd);
if let Some(dsecs) = cmd.take_delta_secs() {
let poly_count = compute_poly_count(cmd);
let explicit_deltas = !cmd.deltas().is_empty();
let delta_list: Vec<Value> = if explicit_deltas {
cmd.deltas().to_vec()
} else {
Vec::new()
};
let count = if explicit_deltas {
arp_count.max(delta_list.len())
} else {
arp_count
};
for i in 0..count {
let delta_secs = if explicit_deltas {
let dv = &delta_list[i % delta_list.len()];
let frac = dv.as_float()?;
if let Some(span) = dv.span() {
for poly_idx in 0..poly_count {
if let Some(sound_val) =
emit_with_cycling(cmd, poly_idx, dsecs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
ctx.nudge_secs + frac * ctx.step_duration()
} else {
ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration()
};
for poly_i in 0..poly_count {
if let Some(sound_val) =
emit_with_cycling(cmd, i, poly_i, delta_secs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) =
trace_cell.borrow_mut().as_mut()
{
trace.selected_spans.push(span);
}
}
}
}
}
} else {
@@ -657,7 +603,7 @@ impl Forth {
}
}
if let Some(sound_val) =
emit_with_cycling(cmd, 0, poly_idx, delta_secs, outputs)?
emit_with_cycling(cmd, poly_idx, delta_secs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) =
@@ -1196,12 +1142,60 @@ impl Forth {
cmd.set_deltas(deltas);
}
Op::Arp => {
Op::AtLoop(body_ops) => {
ensure(stack, 1)?;
let values = std::mem::take(stack);
stack.push(Value::ArpList(Arc::from(values)));
let deltas = std::mem::take(stack);
let n = deltas.len();
for (i, delta_val) in deltas.iter().enumerate() {
let frac = delta_val.as_float()?;
let delta_secs = ctx.nudge_secs + frac * ctx.step_duration();
let iter_ctx = StepContext {
step: ctx.step,
beat: ctx.beat,
bank: ctx.bank,
pattern: ctx.pattern,
tempo: ctx.tempo,
phase: ctx.phase,
slot: ctx.slot,
runs: ctx.runs * n + i,
iter: ctx.iter,
speed: ctx.speed,
fill: ctx.fill,
nudge_secs: ctx.nudge_secs,
sr: ctx.sr,
cc_access: ctx.cc_access,
speed_key: ctx.speed_key,
mouse_x: ctx.mouse_x,
mouse_y: ctx.mouse_y,
mouse_down: ctx.mouse_down,
};
cmd.set_delta_secs(delta_secs);
let mut trace_opt = trace_cell.borrow_mut().take();
let mut var_writes_guard = var_writes_cell.borrow_mut();
let vw = var_writes_guard.as_mut().expect("var_writes taken");
self.execute_ops(
body_ops,
&iter_ctx,
stack,
outputs,
cmd,
trace_opt.as_deref_mut(),
vars_snapshot,
vw,
)?;
drop(var_writes_guard);
*trace_cell.borrow_mut() = trace_opt;
cmd.clear_params();
cmd.clear_sound();
}
}
Op::Adsr => {
let r = pop(stack)?;
let s = pop(stack)?;
@@ -1499,35 +1493,13 @@ impl Forth {
// MIDI operations
Op::MidiEmit => {
let at_loop_delta = cmd.take_delta_secs();
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
// Build schedule: (arp_idx, poly_idx, delta_secs)
let schedule: Vec<(usize, usize, f64)> = if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd);
// Build schedule: (poly_idx, delta_secs)
let schedule: Vec<(usize, f64)> = if let Some(dsecs) = at_loop_delta {
let poly_count = compute_poly_count(cmd);
let explicit = !cmd.deltas().is_empty();
let delta_list = cmd.deltas();
let count = if explicit {
arp_count.max(delta_list.len())
} else {
arp_count
};
let mut sched = Vec::with_capacity(count * poly_count);
for i in 0..count {
let delta_secs = if explicit {
let frac = delta_list[i % delta_list.len()]
.as_float()
.unwrap_or(0.0);
ctx.nudge_secs + frac * ctx.step_duration()
} else {
ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration()
};
for poly_i in 0..poly_count {
sched.push((i, poly_i, delta_secs));
}
}
sched
(0..poly_count).map(|pi| (pi, dsecs)).collect()
} else {
let poly_count = compute_poly_count(cmd);
let deltas: Vec<f64> = if cmd.deltas().is_empty() {
@@ -1542,7 +1514,6 @@ impl Forth {
for poly_idx in 0..poly_count {
for &frac in &deltas {
sched.push((
0,
poly_idx,
ctx.nudge_secs + frac * ctx.step_duration(),
));
@@ -1551,14 +1522,14 @@ impl Forth {
sched
};
for (arp_idx, poly_idx, delta_secs) in schedule {
for (poly_idx, delta_secs) in schedule {
let get_int = |name: &str| -> Option<i64> {
params
.iter()
.rev()
.find(|(k, _)| *k == name)
.and_then(|(_, v)| {
resolve_value(v, arp_idx, poly_idx).as_int().ok()
resolve_value(v, poly_idx).as_int().ok()
})
};
let get_float = |name: &str| -> Option<f64> {
@@ -1567,7 +1538,7 @@ impl Forth {
.rev()
.find(|(k, _)| *k == name)
.and_then(|(_, v)| {
resolve_value(v, arp_idx, poly_idx).as_float().ok()
resolve_value(v, poly_idx).as_float().ok()
})
};
let chan = get_int("chan")
@@ -1960,10 +1931,6 @@ where
F: Fn(f64) -> f64 + Copy,
{
match val {
Value::ArpList(items) => {
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
Value::CycleList(items) => {
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
@@ -1977,11 +1944,6 @@ where
F: Fn(i64) -> i64 + Copy,
{
match val {
Value::ArpList(items) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_unary_int(x, f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
Value::CycleList(items) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_unary_int(x, f)).collect();
@@ -1996,16 +1958,6 @@ where
F: Fn(f64, f64) -> f64 + Copy,
{
match (a, b) {
(Value::ArpList(items), b) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(x, b, f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
(a, Value::ArpList(items)) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(a, x, f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
(Value::CycleList(items), b) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(x, b, f)).collect();
@@ -2045,11 +1997,8 @@ where
Ok(())
}
fn resolve_value(val: &Value, arp_idx: usize, poly_idx: usize) -> Cow<'_, Value> {
fn resolve_value(val: &Value, poly_idx: usize) -> Cow<'_, Value> {
match val {
Value::ArpList(items) if !items.is_empty() => {
Cow::Owned(items[arp_idx % items.len()].clone())
}
Value::CycleList(items) if !items.is_empty() => {
Cow::Owned(items[poly_idx % items.len()].clone())
}

View File

@@ -88,7 +88,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"tempo!" => Op::SetTempo,
"speed!" => Op::SetSpeed,
"at" => Op::At,
"arp" => Op::Arp,
"adsr" => Op::Adsr,
"ad" => Op::Ad,
"apply" => Op::Apply,

View File

@@ -309,9 +309,9 @@ pub(super) const WORDS: &[Word] = &[
name: "at",
aliases: &[],
category: "Time",
stack: "(v1..vn --)",
desc: "Set delta context for emit timing",
example: "0 0.5 at kick s . => emits at 0 and 0.5 of step",
stack: "(v1..vn -- )",
desc: "Looping block: re-executes body per delta. Close with . (audio), m. (MIDI), or done (no emit)",
example: "0 0.5 at kick snd 1 2 rand freq . | 0 0.5 at 60 note m. | 0 0.5 at !x done",
compile: Simple,
varargs: true,
},

View File

@@ -24,16 +24,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "arp",
aliases: &[],
category: "Sound",
stack: "(v1..vn -- arplist)",
desc: "Wrap stack values as arpeggio list for spreading across deltas",
example: "c4 e4 g4 b4 arp note => arpeggio",
compile: Simple,
varargs: true,
},
Word {
name: "clear",
aliases: &[],