All about temporal semantics
This commit is contained in:
11
src/main.rs
11
src/main.rs
@@ -18,10 +18,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use crossterm::event::{
|
||||
self, Event, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
|
||||
PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::event::{self, Event};
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
@@ -134,11 +131,6 @@ fn main() -> io::Result<()> {
|
||||
|
||||
enable_raw_mode()?;
|
||||
io::stdout().execute(EnterAlternateScreen)?;
|
||||
let _ = io::stdout().execute(PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
|
||||
));
|
||||
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
@@ -217,7 +209,6 @@ fn main() -> io::Result<()> {
|
||||
|
||||
}
|
||||
|
||||
let _ = io::stdout().execute(PopKeyboardEnhancementFlags);
|
||||
disable_raw_mode()?;
|
||||
io::stdout().execute(LeaveAlternateScreen)?;
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@ pub enum Op {
|
||||
ListEndPCycle,
|
||||
At,
|
||||
Window,
|
||||
Scale,
|
||||
Pop,
|
||||
Subdivide,
|
||||
SetTempo,
|
||||
@@ -205,6 +206,11 @@ pub enum Op {
|
||||
Unless,
|
||||
Adsr,
|
||||
Ad,
|
||||
Stack,
|
||||
For,
|
||||
LocalCycleEnd,
|
||||
Echo,
|
||||
Necho,
|
||||
}
|
||||
|
||||
pub enum WordCompile {
|
||||
@@ -448,19 +454,26 @@ pub const WORDS: &[Word] = &[
|
||||
example: "\"kick\" s emit",
|
||||
compile: Simple,
|
||||
},
|
||||
// Variables
|
||||
Word {
|
||||
name: "get",
|
||||
stack: "(name -- val)",
|
||||
desc: "Get variable value",
|
||||
example: "\"x\" get",
|
||||
name: "@",
|
||||
stack: "(--)",
|
||||
desc: "Alias for emit",
|
||||
example: "\"kick\" s 0.5 at @ pop",
|
||||
compile: Alias("emit"),
|
||||
},
|
||||
// Variables (prefix syntax: @name to fetch, !name to store)
|
||||
Word {
|
||||
name: "@<var>",
|
||||
stack: "( -- val)",
|
||||
desc: "Fetch variable value",
|
||||
example: "@freq => 440",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "set",
|
||||
stack: "(val name --)",
|
||||
desc: "Set variable value",
|
||||
example: "42 \"x\" set",
|
||||
name: "!<var>",
|
||||
stack: "(val --)",
|
||||
desc: "Store value in variable",
|
||||
example: "440 !freq",
|
||||
compile: Simple,
|
||||
},
|
||||
// Randomness
|
||||
@@ -682,22 +695,22 @@ pub const WORDS: &[Word] = &[
|
||||
Word {
|
||||
name: "at",
|
||||
stack: "(pos --)",
|
||||
desc: "Emit at position in window",
|
||||
example: "0.5 at",
|
||||
desc: "Position in time (push context)",
|
||||
example: "\"kick\" s 0.5 at emit pop",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "@",
|
||||
stack: "(pos --)",
|
||||
desc: "Alias for at",
|
||||
example: "\"kick\" s 0.5 @",
|
||||
compile: Alias("at"),
|
||||
name: "zoom",
|
||||
stack: "(start end --)",
|
||||
desc: "Zoom into time region",
|
||||
example: "0.0 0.5 zoom",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "window",
|
||||
stack: "(start end --)",
|
||||
desc: "Create time window",
|
||||
example: "0.0 0.5 window",
|
||||
name: "scale!",
|
||||
stack: "(factor --)",
|
||||
desc: "Scale time context duration",
|
||||
example: "2 scale!",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
@@ -721,6 +734,41 @@ pub const WORDS: &[Word] = &[
|
||||
example: "4 div each",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "stack",
|
||||
stack: "(n --)",
|
||||
desc: "Create n subdivisions at same time",
|
||||
example: "3 stack",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "echo",
|
||||
stack: "(n --)",
|
||||
desc: "Create n subdivisions with halving durations (stutter)",
|
||||
example: "3 echo",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "necho",
|
||||
stack: "(n --)",
|
||||
desc: "Create n subdivisions with doubling durations (swell)",
|
||||
example: "3 necho",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "for",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation for each subdivision",
|
||||
example: "{ emit } 3 div for",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "|",
|
||||
stack: "(-- marker)",
|
||||
desc: "Start local cycle list",
|
||||
example: "| 60 62 64 |",
|
||||
compile: Simple,
|
||||
},
|
||||
Word {
|
||||
name: "tempo!",
|
||||
stack: "(bpm --)",
|
||||
@@ -1577,8 +1625,6 @@ fn simple_op(name: &str) -> Option<Op> {
|
||||
"not" => Op::Not,
|
||||
"sound" => Op::NewCmd,
|
||||
"emit" => Op::Emit,
|
||||
"get" => Op::Get,
|
||||
"set" => Op::Set,
|
||||
"rand" => Op::Rand,
|
||||
"rrand" => Op::Rrand,
|
||||
"seed" => Op::Seed,
|
||||
@@ -1594,7 +1640,8 @@ fn simple_op(name: &str) -> Option<Op> {
|
||||
"?" => Op::When,
|
||||
"!?" => Op::Unless,
|
||||
"at" => Op::At,
|
||||
"window" => Op::Window,
|
||||
"zoom" => Op::Window,
|
||||
"scale!" => Op::Scale,
|
||||
"pop" => Op::Pop,
|
||||
"div" => Op::Subdivide,
|
||||
"each" => Op::Each,
|
||||
@@ -1605,6 +1652,10 @@ fn simple_op(name: &str) -> Option<Op> {
|
||||
">>" => Op::ListEndPCycle,
|
||||
"adsr" => Op::Adsr,
|
||||
"ad" => Op::Ad,
|
||||
"stack" => Op::Stack,
|
||||
"for" => Op::For,
|
||||
"echo" => Op::Echo,
|
||||
"necho" => Op::Necho,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -1629,6 +1680,31 @@ fn compile_word(name: &str, ops: &mut Vec<Op>) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// @varname - fetch variable
|
||||
if let Some(var_name) = name.strip_prefix('@') {
|
||||
if !var_name.is_empty() {
|
||||
ops.push(Op::PushStr(var_name.to_string(), None));
|
||||
ops.push(Op::Get);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// !varname - store into variable
|
||||
if let Some(var_name) = name.strip_prefix('!') {
|
||||
if !var_name.is_empty() {
|
||||
ops.push(Op::PushStr(var_name.to_string(), None));
|
||||
ops.push(Op::Set);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Internal ops not exposed in WORDS
|
||||
if let Some(op) = simple_op(name) {
|
||||
ops.push(op);
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1637,6 +1713,7 @@ struct TimeContext {
|
||||
start: f64,
|
||||
duration: f64,
|
||||
subdivisions: Option<Vec<(f64, f64)>>,
|
||||
iteration_index: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1734,6 +1811,7 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
|
||||
let mut ops = Vec::new();
|
||||
let mut i = 0;
|
||||
let mut pipe_parity = false;
|
||||
|
||||
while i < tokens.len() {
|
||||
match &tokens[i] {
|
||||
@@ -1750,7 +1828,14 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
|
||||
}
|
||||
Token::Word(w, _) => {
|
||||
let word = w.as_str();
|
||||
if word == "if" {
|
||||
if word == "|" {
|
||||
if pipe_parity {
|
||||
ops.push(Op::LocalCycleEnd);
|
||||
} else {
|
||||
ops.push(Op::ListStart);
|
||||
}
|
||||
pipe_parity = !pipe_parity;
|
||||
} else if word == "if" {
|
||||
let (then_ops, else_ops, consumed) = compile_if(&tokens[i + 1..])?;
|
||||
i += consumed;
|
||||
if else_ops.is_empty() {
|
||||
@@ -1897,6 +1982,7 @@ impl Forth {
|
||||
start: 0.0,
|
||||
duration: ctx.step_duration(),
|
||||
subdivisions: None,
|
||||
iteration_index: None,
|
||||
}];
|
||||
let mut cmd = CmdRegister::default();
|
||||
|
||||
@@ -1910,16 +1996,6 @@ impl Forth {
|
||||
trace,
|
||||
)?;
|
||||
|
||||
if outputs.is_empty() {
|
||||
if let Some((sound, params)) = cmd.take() {
|
||||
let mut pairs = vec![("sound".into(), sound)];
|
||||
pairs.extend(params);
|
||||
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||
pairs.push(("delaytime".into(), ctx.step_duration().to_string()));
|
||||
outputs.push(format_cmd(&pairs));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
@@ -2078,14 +2154,13 @@ impl Forth {
|
||||
pairs.push(("delta".into(), time_ctx.start.to_string()));
|
||||
}
|
||||
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||
pairs.push(("dur".into(), time_ctx.duration.to_string()));
|
||||
}
|
||||
let stepdur = ctx.step_duration();
|
||||
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||
pairs[idx].1 = (ratio * stepdur).to_string();
|
||||
pairs[idx].1 = (ratio * time_ctx.duration).to_string();
|
||||
} else {
|
||||
pairs.push(("delaytime".into(), stepdur.to_string()));
|
||||
pairs.push(("delaytime".into(), time_ctx.duration.to_string()));
|
||||
}
|
||||
outputs.push(format_cmd(&pairs));
|
||||
}
|
||||
@@ -2206,7 +2281,9 @@ impl Forth {
|
||||
if val < prob {
|
||||
match quot {
|
||||
Value::Quotation(quot_ops) => {
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, None)?;
|
||||
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||
*trace_cell.borrow_mut() = trace_opt;
|
||||
}
|
||||
_ => return Err("expected quotation".into()),
|
||||
}
|
||||
@@ -2220,7 +2297,9 @@ impl Forth {
|
||||
if val < pct / 100.0 {
|
||||
match quot {
|
||||
Value::Quotation(quot_ops) => {
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, None)?;
|
||||
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||
*trace_cell.borrow_mut() = trace_opt;
|
||||
}
|
||||
_ => return Err("expected quotation".into()),
|
||||
}
|
||||
@@ -2251,7 +2330,9 @@ impl Forth {
|
||||
if cond.is_truthy() {
|
||||
match quot {
|
||||
Value::Quotation(quot_ops) => {
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, None)?;
|
||||
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||
*trace_cell.borrow_mut() = trace_opt;
|
||||
}
|
||||
_ => return Err("expected quotation".into()),
|
||||
}
|
||||
@@ -2264,7 +2345,9 @@ impl Forth {
|
||||
if !cond.is_truthy() {
|
||||
match quot {
|
||||
Value::Quotation(quot_ops) => {
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, None)?;
|
||||
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||
*trace_cell.borrow_mut() = trace_opt;
|
||||
}
|
||||
_ => return Err("expected quotation".into()),
|
||||
}
|
||||
@@ -2285,25 +2368,14 @@ impl Forth {
|
||||
|
||||
Op::At => {
|
||||
let pos = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let (sound, mut params) = cmd.take().ok_or("no sound set")?;
|
||||
let mut pairs = vec![("sound".into(), sound)];
|
||||
pairs.append(&mut params);
|
||||
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
|
||||
let absolute_time = time_ctx.start + time_ctx.duration * pos;
|
||||
if absolute_time > 0.0 {
|
||||
pairs.push(("delta".into(), absolute_time.to_string()));
|
||||
}
|
||||
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||
}
|
||||
let stepdur = ctx.step_duration();
|
||||
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||
pairs[idx].1 = (ratio * stepdur).to_string();
|
||||
} else {
|
||||
pairs.push(("delaytime".into(), stepdur.to_string()));
|
||||
}
|
||||
outputs.push(format_cmd(&pairs));
|
||||
let parent = time_stack.last().ok_or("time stack underflow")?;
|
||||
let new_start = parent.start + parent.duration * pos;
|
||||
time_stack.push(TimeContext {
|
||||
start: new_start,
|
||||
duration: parent.duration * (1.0 - pos),
|
||||
subdivisions: None,
|
||||
iteration_index: parent.iteration_index,
|
||||
});
|
||||
}
|
||||
|
||||
Op::Window => {
|
||||
@@ -2316,6 +2388,18 @@ impl Forth {
|
||||
start: new_start,
|
||||
duration: new_duration,
|
||||
subdivisions: None,
|
||||
iteration_index: parent.iteration_index,
|
||||
});
|
||||
}
|
||||
|
||||
Op::Scale => {
|
||||
let factor = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let parent = time_stack.last().ok_or("time stack underflow")?;
|
||||
time_stack.push(TimeContext {
|
||||
start: parent.start,
|
||||
duration: parent.duration * factor,
|
||||
subdivisions: None,
|
||||
iteration_index: parent.iteration_index,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2347,21 +2431,20 @@ impl Forth {
|
||||
.subdivisions
|
||||
.as_ref()
|
||||
.ok_or("each requires subdivide first")?;
|
||||
for (sub_start, _sub_dur) in subs {
|
||||
for (sub_start, sub_dur) in subs {
|
||||
let mut pairs = vec![("sound".into(), sound.clone())];
|
||||
pairs.extend(params.iter().cloned());
|
||||
if *sub_start > 0.0 {
|
||||
pairs.push(("delta".into(), sub_start.to_string()));
|
||||
}
|
||||
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||
pairs.push(("dur".into(), sub_dur.to_string()));
|
||||
}
|
||||
let stepdur = ctx.step_duration();
|
||||
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||
pairs[idx].1 = (ratio * stepdur).to_string();
|
||||
pairs[idx].1 = (ratio * sub_dur).to_string();
|
||||
} else {
|
||||
pairs.push(("delaytime".into(), stepdur.to_string()));
|
||||
pairs.push(("delaytime".into(), sub_dur.to_string()));
|
||||
}
|
||||
outputs.push(format_cmd(&pairs));
|
||||
}
|
||||
@@ -2459,6 +2542,121 @@ impl Forth {
|
||||
cmd.set_param("decay".into(), d.to_param_string());
|
||||
cmd.set_param("sustain".into(), "0".into());
|
||||
}
|
||||
|
||||
Op::Stack => {
|
||||
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if n == 0 {
|
||||
return Err("stack count must be > 0".into());
|
||||
}
|
||||
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
|
||||
let sub_duration = time_ctx.duration / n as f64;
|
||||
let mut subs = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
subs.push((time_ctx.start, sub_duration));
|
||||
}
|
||||
time_ctx.subdivisions = Some(subs);
|
||||
}
|
||||
|
||||
Op::Echo => {
|
||||
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if n == 0 {
|
||||
return Err("echo count must be > 0".into());
|
||||
}
|
||||
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
|
||||
// Geometric series: d1 * (2 - 2^(1-n)) = total
|
||||
let d1 = time_ctx.duration / (2.0 - 2.0_f64.powi(1 - n as i32));
|
||||
let mut subs = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let dur = d1 / 2.0_f64.powi(i as i32);
|
||||
let start = if i == 0 {
|
||||
time_ctx.start
|
||||
} else {
|
||||
time_ctx.start + d1 * (2.0 - 2.0_f64.powi(1 - i as i32))
|
||||
};
|
||||
subs.push((start, dur));
|
||||
}
|
||||
time_ctx.subdivisions = Some(subs);
|
||||
}
|
||||
|
||||
Op::Necho => {
|
||||
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if n == 0 {
|
||||
return Err("necho count must be > 0".into());
|
||||
}
|
||||
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
|
||||
// Reverse geometric: d1 + 2*d1 + 4*d1 + ... = d1 * (2^n - 1) = total
|
||||
let d1 = time_ctx.duration / (2.0_f64.powi(n as i32) - 1.0);
|
||||
let mut subs = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let dur = d1 * 2.0_f64.powi(i as i32);
|
||||
let start = if i == 0 {
|
||||
time_ctx.start
|
||||
} else {
|
||||
// Sum of previous durations: d1 * (2^i - 1)
|
||||
time_ctx.start + d1 * (2.0_f64.powi(i as i32) - 1.0)
|
||||
};
|
||||
subs.push((start, dur));
|
||||
}
|
||||
time_ctx.subdivisions = Some(subs);
|
||||
}
|
||||
|
||||
Op::For => {
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
|
||||
let subs = time_ctx
|
||||
.subdivisions
|
||||
.clone()
|
||||
.ok_or("for requires subdivide first")?;
|
||||
|
||||
match quot {
|
||||
Value::Quotation(quot_ops) => {
|
||||
for (i, (sub_start, sub_dur)) in subs.iter().enumerate() {
|
||||
time_stack.push(TimeContext {
|
||||
start: *sub_start,
|
||||
duration: *sub_dur,
|
||||
subdivisions: None,
|
||||
iteration_index: Some(i),
|
||||
});
|
||||
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||
self.execute_ops(
|
||||
"_ops,
|
||||
ctx,
|
||||
stack,
|
||||
outputs,
|
||||
time_stack,
|
||||
cmd,
|
||||
trace_opt.as_deref_mut(),
|
||||
)?;
|
||||
*trace_cell.borrow_mut() = trace_opt;
|
||||
time_stack.pop();
|
||||
}
|
||||
}
|
||||
_ => return Err("expected quotation".into()),
|
||||
}
|
||||
}
|
||||
|
||||
Op::LocalCycleEnd => {
|
||||
let mut values = Vec::new();
|
||||
while let Some(v) = stack.pop() {
|
||||
if v.is_marker() {
|
||||
break;
|
||||
}
|
||||
values.push(v);
|
||||
}
|
||||
if values.is_empty() {
|
||||
return Err("empty local cycle list".into());
|
||||
}
|
||||
values.reverse();
|
||||
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
|
||||
let idx = time_ctx.iteration_index.unwrap_or(0) % values.len();
|
||||
let selected = values[idx].clone();
|
||||
if let Some(span) = selected.span() {
|
||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||
trace.selected_spans.push(span);
|
||||
}
|
||||
}
|
||||
stack.push(selected);
|
||||
}
|
||||
}
|
||||
pc += 1;
|
||||
}
|
||||
|
||||
@@ -33,3 +33,6 @@ mod variables;
|
||||
|
||||
#[path = "forth/quotations.rs"]
|
||||
mod quotations;
|
||||
|
||||
#[path = "forth/iteration.rs"]
|
||||
mod iteration;
|
||||
|
||||
@@ -20,16 +20,6 @@ fn string_not_number() {
|
||||
expect_error(r#""hello" neg"#, "expected number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_expects_string() {
|
||||
expect_error("42 get", "expected string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_expects_string() {
|
||||
expect_error("1 2 set", "expected string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_ignored() {
|
||||
expect_int("1 (this is a comment) 2 +", 3);
|
||||
@@ -54,7 +44,7 @@ fn float_literal() {
|
||||
|
||||
#[test]
|
||||
fn string_with_spaces() {
|
||||
let f = run(r#""hello world" "x" set "x" get"#);
|
||||
let f = run(r#""hello world" !x @x"#);
|
||||
match stack_top(&f) {
|
||||
cagire::model::forth::Value::Str(s, _) => assert_eq!(s, "hello world"),
|
||||
other => panic!("expected string, got {:?}", other),
|
||||
@@ -63,7 +53,6 @@ fn string_with_spaces() {
|
||||
|
||||
#[test]
|
||||
fn list_count() {
|
||||
// [ 1 2 3 ] => stack: 1 2 3 3 (items + count on top)
|
||||
let f = run("[ 1 2 3 ]");
|
||||
assert_eq!(stack_int(&f), 3);
|
||||
}
|
||||
@@ -75,9 +64,6 @@ fn list_empty() {
|
||||
|
||||
#[test]
|
||||
fn list_preserves_values() {
|
||||
// [ 10 20 ] => stack: 10 20 2
|
||||
// drop => 10 20
|
||||
// + => 30
|
||||
expect_int("[ 10 20 ] drop +", 30);
|
||||
}
|
||||
|
||||
@@ -97,10 +83,10 @@ fn conditional_based_on_step() {
|
||||
fn accumulator() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"0 "acc" set"#, &ctx).unwrap();
|
||||
f.evaluate(r#"0 !acc"#, &ctx).unwrap();
|
||||
for _ in 0..5 {
|
||||
f.clear_stack();
|
||||
f.evaluate(r#""acc" get 1 + dup "acc" set"#, &ctx).unwrap();
|
||||
f.evaluate(r#"@acc 1 + dup !acc"#, &ctx).unwrap();
|
||||
}
|
||||
assert_eq!(stack_int(&f), 5);
|
||||
}
|
||||
|
||||
256
tests/forth/iteration.rs
Normal file
256
tests/forth/iteration.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use super::harness::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn parse_params(output: &str) -> HashMap<String, f64> {
|
||||
let mut params = HashMap::new();
|
||||
let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
|
||||
let mut i = 0;
|
||||
while i + 1 < parts.len() {
|
||||
if let Ok(v) = parts[i + 1].parse::<f64>() {
|
||||
params.insert(parts[i].to_string(), v);
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
fn get_deltas(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_durs(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_notes(outputs: &[String]) -> Vec<f64> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| parse_params(o).get("note").copied().unwrap_or(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_sounds(outputs: &[String]) -> Vec<String> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|o| {
|
||||
let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect();
|
||||
for i in (0..parts.len()).step_by(2) {
|
||||
if parts[i] == "sound" && i + 1 < parts.len() {
|
||||
return parts[i + 1].to_string();
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
const EPSILON: f64 = 1e-9;
|
||||
|
||||
fn approx_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < EPSILON
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_creates_subdivisions_at_same_time() {
|
||||
let outputs = expect_outputs(r#""kick" s 3 stack each"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.0));
|
||||
assert!(approx_eq(deltas[2], 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_vs_div_timing() {
|
||||
let stack_outputs = expect_outputs(r#""kick" s 3 stack each"#, 3);
|
||||
let div_outputs = expect_outputs(r#""kick" s 3 div each"#, 3);
|
||||
|
||||
let stack_deltas = get_deltas(&stack_outputs);
|
||||
let div_deltas = get_deltas(&div_outputs);
|
||||
|
||||
for d in stack_deltas {
|
||||
assert!(approx_eq(d, 0.0), "stack should have all delta=0");
|
||||
}
|
||||
|
||||
assert!(approx_eq(div_deltas[0], 0.0));
|
||||
assert!(!approx_eq(div_deltas[1], 0.0), "div should spread in time");
|
||||
assert!(!approx_eq(div_deltas[2], 0.0), "div should spread in time");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_with_div_arpeggio() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 div for"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(!approx_eq(deltas[1], 0.0));
|
||||
assert!(!approx_eq(deltas[2], 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_with_stack_chord() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 stack for"#, 3);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
for d in deltas {
|
||||
assert!(approx_eq(d, 0.0), "stack for should have all delta=0");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_with_for() {
|
||||
let outputs = expect_outputs(r#"{ | 60 62 64 | note "sine" s emit } 3 div for"#, 3);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 62.0));
|
||||
assert!(approx_eq(notes[2], 64.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_wraps_around() {
|
||||
let outputs = expect_outputs(r#"{ | 60 62 | note "sine" s emit } 4 div for"#, 4);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 62.0));
|
||||
assert!(approx_eq(notes[2], 60.0));
|
||||
assert!(approx_eq(notes[3], 62.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_local_cycles() {
|
||||
let outputs =
|
||||
expect_outputs(r#"{ | "bd" "sn" | s | 60 64 | note emit } 2 stack for"#, 2);
|
||||
let sounds = get_sounds(&outputs);
|
||||
let notes = get_notes(&outputs);
|
||||
|
||||
assert_eq!(sounds[0], "bd");
|
||||
assert_eq!(sounds[1], "sn");
|
||||
assert!(approx_eq(notes[0], 60.0));
|
||||
assert!(approx_eq(notes[1], 64.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_cycle_outside_for_defaults_to_first() {
|
||||
expect_int("| 60 62 64 |", 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polymetric_cycles() {
|
||||
let outputs = expect_outputs(
|
||||
r#"{ | 0 1 | n | "a" "b" "c" | s emit } 6 div for"#,
|
||||
6,
|
||||
);
|
||||
let sounds = get_sounds(&outputs);
|
||||
|
||||
assert_eq!(sounds[0], "a");
|
||||
assert_eq!(sounds[1], "b");
|
||||
assert_eq!(sounds[2], "c");
|
||||
assert_eq!(sounds[3], "a");
|
||||
assert_eq!(sounds[4], "b");
|
||||
assert_eq!(sounds[5], "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 stack each"#, "stack count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_requires_subdivide() {
|
||||
expect_error(r#"{ "kick" s emit } for"#, "for requires subdivide first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_requires_quotation() {
|
||||
expect_error(r#"42 3 div for"#, "expected quotation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_local_cycle() {
|
||||
expect_error("| |", "empty local cycle list");
|
||||
}
|
||||
|
||||
// Echo tests - stutter effect with halving durations
|
||||
|
||||
#[test]
|
||||
fn echo_creates_decaying_subdivisions() {
|
||||
// stepdur = 0.125, echo 3
|
||||
// d1 + d1/2 + d1/4 = d1 * 1.75 = 0.125
|
||||
// d1 = 0.125 / 1.75 = 0.0714285714...
|
||||
let outputs = expect_outputs(r#""kick" s 3 echo each"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
let d1 = 0.125 / 1.75;
|
||||
let d2 = d1 / 2.0;
|
||||
let d3 = d1 / 4.0;
|
||||
|
||||
assert!(approx_eq(durs[0], d1), "first dur should be {}, got {}", d1, durs[0]);
|
||||
assert!(approx_eq(durs[1], d2), "second dur should be {}, got {}", d2, durs[1]);
|
||||
assert!(approx_eq(durs[2], d3), "third dur should be {}, got {}", d3, durs[2]);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0), "first delta should be 0");
|
||||
assert!(approx_eq(deltas[1], d1), "second delta should be {}, got {}", d1, deltas[1]);
|
||||
assert!(approx_eq(deltas[2], d1 + d2), "third delta should be {}, got {}", d1 + d2, deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_with_for() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 echo for"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
|
||||
// Each subsequent duration should be half the previous
|
||||
assert!(approx_eq(durs[1], durs[0] / 2.0), "second should be half of first");
|
||||
assert!(approx_eq(durs[2], durs[1] / 2.0), "third should be half of second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 echo each"#, "echo count must be > 0");
|
||||
}
|
||||
|
||||
// Necho tests - reverse echo (durations grow)
|
||||
|
||||
#[test]
|
||||
fn necho_creates_growing_subdivisions() {
|
||||
// stepdur = 0.125, necho 3
|
||||
// d1 + 2*d1 + 4*d1 = d1 * 7 = 0.125
|
||||
// d1 = 0.125 / 7
|
||||
let outputs = expect_outputs(r#""kick" s 3 necho each"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
let deltas = get_deltas(&outputs);
|
||||
|
||||
let d1 = 0.125 / 7.0;
|
||||
let d2 = d1 * 2.0;
|
||||
let d3 = d1 * 4.0;
|
||||
|
||||
assert!(approx_eq(durs[0], d1), "first dur should be {}, got {}", d1, durs[0]);
|
||||
assert!(approx_eq(durs[1], d2), "second dur should be {}, got {}", d2, durs[1]);
|
||||
assert!(approx_eq(durs[2], d3), "third dur should be {}, got {}", d3, durs[2]);
|
||||
|
||||
assert!(approx_eq(deltas[0], 0.0), "first delta should be 0");
|
||||
assert!(approx_eq(deltas[1], d1), "second delta should be {}, got {}", d1, deltas[1]);
|
||||
assert!(approx_eq(deltas[2], d1 + d2), "third delta should be {}, got {}", d1 + d2, deltas[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn necho_with_for() {
|
||||
let outputs = expect_outputs(r#"{ "kick" s emit } 3 necho for"#, 3);
|
||||
let durs = get_durs(&outputs);
|
||||
|
||||
// Each subsequent duration should be double the previous
|
||||
assert!(approx_eq(durs[1], durs[0] * 2.0), "second should be double first");
|
||||
assert!(approx_eq(durs[2], durs[1] * 2.0), "third should be double second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn necho_error_zero_count() {
|
||||
expect_error(r#""kick" s 0 necho each"#, "necho count must be > 0");
|
||||
}
|
||||
@@ -83,8 +83,8 @@ fn quotation_skips_emit() {
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s { emit } 0 ?"#, &default_ctx())
|
||||
.unwrap();
|
||||
// Should have 1 output from implicit emit at end (since cmd is set but not emitted)
|
||||
assert_eq!(outputs.len(), 1);
|
||||
// No output since emit was skipped and no implicit emit
|
||||
assert_eq!(outputs.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -37,13 +37,6 @@ fn emit_no_sound() {
|
||||
expect_error("emit", "no sound set");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_emit() {
|
||||
let outputs = expect_outputs(r#""kick" s 440 freq"#, 1);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
assert!(outputs[0].contains("freq/440"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_emits() {
|
||||
let outputs = expect_outputs(r#""kick" s emit "snare" s emit"#, 2);
|
||||
@@ -57,9 +50,9 @@ fn subdivide_each() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_pop() {
|
||||
fn zoom_pop() {
|
||||
let outputs = expect_outputs(
|
||||
r#"0.0 0.5 window "kick" s emit pop 0.5 1.0 window "snare" s emit"#,
|
||||
r#"0.0 0.5 zoom "kick" s emit pop 0.5 1.0 zoom "snare" s emit"#,
|
||||
2,
|
||||
);
|
||||
assert!(outputs[0].contains("sound/kick"));
|
||||
|
||||
@@ -47,8 +47,8 @@ fn emit_no_delta() {
|
||||
|
||||
#[test]
|
||||
fn at_half() {
|
||||
// at 0.5 in root window (0..0.125) => delta = 0.5 * 0.125 = 0.0625
|
||||
let outputs = expect_outputs(r#""kick" s 0.5 at"#, 1);
|
||||
// at 0.5 in root zoom (0..0.125) => delta = 0.5 * 0.125 = 0.0625
|
||||
let outputs = expect_outputs(r#""kick" s 0.5 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.0625),
|
||||
@@ -59,7 +59,7 @@ fn at_half() {
|
||||
|
||||
#[test]
|
||||
fn at_quarter() {
|
||||
let outputs = expect_outputs(r#""kick" s 0.25 at"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" s 0.25 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.03125),
|
||||
@@ -70,7 +70,7 @@ fn at_quarter() {
|
||||
|
||||
#[test]
|
||||
fn at_zero() {
|
||||
let outputs = expect_outputs(r#""kick" s 0.0 at"#, 1);
|
||||
let outputs = expect_outputs(r#""kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "at 0.0 should be delta 0");
|
||||
}
|
||||
@@ -117,83 +117,83 @@ fn div_3_each() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_full() {
|
||||
// window 0.0 1.0 is the full step, same as root
|
||||
let outputs = expect_outputs(r#"0.0 1.0 window "kick" s 0.5 at"#, 1);
|
||||
fn zoom_full() {
|
||||
// zoom 0.0 1.0 is the full step, same as root
|
||||
let outputs = expect_outputs(r#"0.0 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0625), "full window at 0.5 = 0.0625");
|
||||
assert!(approx_eq(deltas[0], 0.0625), "full zoom at 0.5 = 0.0625");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_first_half() {
|
||||
// window 0.0 0.5 restricts to first half (0..0.0625)
|
||||
fn zoom_first_half() {
|
||||
// zoom 0.0 0.5 restricts to first half (0..0.0625)
|
||||
// at 0.5 within that = 0.25 of full step = 0.03125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 window "kick" s 0.5 at"#, 1);
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.03125),
|
||||
"first-half window at 0.5 = 0.03125, got {}",
|
||||
"first-half zoom at 0.5 = 0.03125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_second_half() {
|
||||
// window 0.5 1.0 restricts to second half (0.0625..0.125)
|
||||
fn zoom_second_half() {
|
||||
// zoom 0.5 1.0 restricts to second half (0.0625..0.125)
|
||||
// at 0.0 within that = start of second half = 0.0625
|
||||
let outputs = expect_outputs(r#"0.5 1.0 window "kick" s 0.0 at"#, 1);
|
||||
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.0625),
|
||||
"second-half window at 0.0 = 0.0625, got {}",
|
||||
"second-half zoom at 0.0 = 0.0625, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_second_half_middle() {
|
||||
// window 0.5 1.0, at 0.5 within that = 0.75 of full step = 0.09375
|
||||
let outputs = expect_outputs(r#"0.5 1.0 window "kick" s 0.5 at"#, 1);
|
||||
fn zoom_second_half_middle() {
|
||||
// zoom 0.5 1.0, at 0.5 within that = 0.75 of full step = 0.09375
|
||||
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.09375), "got {}", deltas[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_windows() {
|
||||
// window 0.0 0.5, then window 0.5 1.0 within that
|
||||
fn nested_zooms() {
|
||||
// zoom 0.0 0.5, then zoom 0.5 1.0 within that
|
||||
// outer: 0..0.0625, inner: 0.5..1.0 of that = 0.03125..0.0625
|
||||
// at 0.0 in inner = 0.03125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 window 0.5 1.0 window "kick" s 0.0 at"#, 1);
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom 0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(
|
||||
approx_eq(deltas[0], 0.03125),
|
||||
"nested window at 0.0 = 0.03125, got {}",
|
||||
"nested zoom at 0.0 = 0.03125, got {}",
|
||||
deltas[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_pop_sequence() {
|
||||
// First in window 0.0 0.5 at 0.0 -> delta 0
|
||||
// Pop, then in window 0.5 1.0 at 0.0 -> delta 0.0625
|
||||
fn zoom_pop_sequence() {
|
||||
// First in zoom 0.0 0.5 at 0.0 -> delta 0
|
||||
// Pop at context and zoom, then in zoom 0.5 1.0 at 0.0 -> delta 0.0625
|
||||
let outputs = expect_outputs(
|
||||
r#"0.0 0.5 window "kick" s 0.0 at pop 0.5 1.0 window "snare" s 0.0 at"#,
|
||||
r#"0.0 0.5 zoom "kick" s 0.0 at emit pop pop 0.5 1.0 zoom "snare" s 0.0 at emit pop"#,
|
||||
2,
|
||||
);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0), "first window start");
|
||||
assert!(approx_eq(deltas[0], 0.0), "first zoom start");
|
||||
assert!(
|
||||
approx_eq(deltas[1], 0.0625),
|
||||
"second window start, got {}",
|
||||
"second zoom start, got {}",
|
||||
deltas[1]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_in_window() {
|
||||
// window 0.0 0.5 (duration 0.0625), then div 2 each
|
||||
fn div_in_zoom() {
|
||||
// zoom 0.0 0.5 (duration 0.0625), then div 2 each
|
||||
// subdivisions at 0 and 0.03125
|
||||
let outputs = expect_outputs(r#"0.0 0.5 window "kick" s 2 div each"#, 2);
|
||||
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 2 div each"#, 2);
|
||||
let deltas = get_deltas(&outputs);
|
||||
assert!(approx_eq(deltas[0], 0.0));
|
||||
assert!(approx_eq(deltas[1], 0.03125), "got {}", deltas[1]);
|
||||
|
||||
@@ -1,39 +1,44 @@
|
||||
use super::harness::*;
|
||||
|
||||
#[test]
|
||||
fn set_get() {
|
||||
expect_int(r#"42 "x" set "x" get"#, 42);
|
||||
fn fetch_store() {
|
||||
expect_int(r#"42 !x @x"#, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_nonexistent() {
|
||||
expect_int(r#""novar" get"#, 0);
|
||||
fn fetch_nonexistent() {
|
||||
expect_int(r#"@novar"#, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistence_across_evals() {
|
||||
let f = forth();
|
||||
let ctx = default_ctx();
|
||||
f.evaluate(r#"10 "counter" set"#, &ctx).unwrap();
|
||||
f.evaluate(r#"10 !counter"#, &ctx).unwrap();
|
||||
f.clear_stack();
|
||||
f.evaluate(r#""counter" get 1 +"#, &ctx).unwrap();
|
||||
f.evaluate(r#"@counter 1 +"#, &ctx).unwrap();
|
||||
assert_eq!(stack_int(&f), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overwrite() {
|
||||
expect_int(r#"1 "x" set 99 "x" set "x" get"#, 99);
|
||||
expect_int(r#"1 !x 99 !x @x"#, 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_vars() {
|
||||
let f = run(r#"10 "a" set 20 "b" set "a" get "b" get +"#);
|
||||
let f = run(r#"10 !a 20 !b @a @b +"#);
|
||||
assert_eq!(stack_int(&f), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_var() {
|
||||
let f = run(r#"3.14 "pi" set "pi" get"#);
|
||||
let f = run(r#"3.14 !pi @pi"#);
|
||||
let val = stack_float(&f);
|
||||
assert!((val - 3.14).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn increment_pattern() {
|
||||
expect_int(r#"0 !n @n 1 + !n @n 1 + !n @n"#, 2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user