Break down forth implementation properly
This commit is contained in:
2944
src/model/forth.rs
2944
src/model/forth.rs
File diff suppressed because it is too large
Load Diff
282
src/model/forth/compiler.rs
Normal file
282
src/model/forth/compiler.rs
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
use super::ops::Op;
|
||||||
|
use super::types::{Dictionary, SourceSpan};
|
||||||
|
use super::words::{compile_word, simple_op};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum Token {
|
||||||
|
Int(i64, SourceSpan),
|
||||||
|
Float(f64, SourceSpan),
|
||||||
|
Str(String, SourceSpan),
|
||||||
|
Word(String, SourceSpan),
|
||||||
|
QuoteStart(usize),
|
||||||
|
QuoteEnd(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||||
|
let tokens = tokenize(input);
|
||||||
|
compile(&tokens, dict)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenize(input: &str) -> Vec<Token> {
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
let mut chars = input.char_indices().peekable();
|
||||||
|
|
||||||
|
while let Some(&(pos, c)) = chars.peek() {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
chars.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '"' {
|
||||||
|
let start = pos;
|
||||||
|
chars.next();
|
||||||
|
let mut s = String::new();
|
||||||
|
let mut end = start + 1;
|
||||||
|
while let Some(&(i, ch)) = chars.peek() {
|
||||||
|
end = i + ch.len_utf8();
|
||||||
|
chars.next();
|
||||||
|
if ch == '"' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
s.push(ch);
|
||||||
|
}
|
||||||
|
tokens.push(Token::Str(s, SourceSpan { start, end }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '(' {
|
||||||
|
while let Some(&(_, ch)) = chars.peek() {
|
||||||
|
chars.next();
|
||||||
|
if ch == ')' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '{' {
|
||||||
|
chars.next();
|
||||||
|
tokens.push(Token::QuoteStart(pos));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '}' {
|
||||||
|
chars.next();
|
||||||
|
tokens.push(Token::QuoteEnd(pos));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = pos;
|
||||||
|
let mut word = String::new();
|
||||||
|
let mut end = start;
|
||||||
|
while let Some(&(i, ch)) = chars.peek() {
|
||||||
|
if ch.is_whitespace() || ch == '{' || ch == '}' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
end = i + ch.len_utf8();
|
||||||
|
word.push(ch);
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let span = SourceSpan { start, end };
|
||||||
|
if let Ok(i) = word.parse::<i64>() {
|
||||||
|
tokens.push(Token::Int(i, span));
|
||||||
|
} else if let Ok(f) = word.parse::<f64>() {
|
||||||
|
tokens.push(Token::Float(f, span));
|
||||||
|
} else {
|
||||||
|
tokens.push(Token::Word(word, span));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||||
|
let mut ops = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
let mut pipe_parity = false;
|
||||||
|
let mut list_depth: usize = 0;
|
||||||
|
|
||||||
|
while i < tokens.len() {
|
||||||
|
match &tokens[i] {
|
||||||
|
Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))),
|
||||||
|
Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))),
|
||||||
|
Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))),
|
||||||
|
Token::QuoteStart(start_pos) => {
|
||||||
|
let (quote_ops, consumed, end_pos) = compile_quotation(&tokens[i + 1..], dict)?;
|
||||||
|
i += consumed;
|
||||||
|
let body_span = SourceSpan { start: *start_pos, end: end_pos + 1 };
|
||||||
|
ops.push(Op::Quotation(quote_ops, Some(body_span)));
|
||||||
|
}
|
||||||
|
Token::QuoteEnd(_) => {
|
||||||
|
return Err("unexpected }".into());
|
||||||
|
}
|
||||||
|
Token::Word(w, span) => {
|
||||||
|
let word = w.as_str();
|
||||||
|
if word == ":" {
|
||||||
|
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
|
||||||
|
i += consumed;
|
||||||
|
dict.lock().unwrap().insert(name, body);
|
||||||
|
} else if word == ";" {
|
||||||
|
return Err("unexpected ;".into());
|
||||||
|
} else if word == "|" {
|
||||||
|
if pipe_parity {
|
||||||
|
ops.push(Op::LocalCycleEnd);
|
||||||
|
list_depth = list_depth.saturating_sub(1);
|
||||||
|
} else {
|
||||||
|
ops.push(Op::ListStart);
|
||||||
|
list_depth += 1;
|
||||||
|
}
|
||||||
|
pipe_parity = !pipe_parity;
|
||||||
|
} else if word == "if" {
|
||||||
|
let (then_ops, else_ops, consumed, then_span, else_span) = compile_if(&tokens[i + 1..], dict)?;
|
||||||
|
i += consumed;
|
||||||
|
if else_ops.is_empty() {
|
||||||
|
ops.push(Op::BranchIfZero(then_ops.len(), then_span, None));
|
||||||
|
ops.extend(then_ops);
|
||||||
|
} else {
|
||||||
|
ops.push(Op::BranchIfZero(then_ops.len() + 1, then_span, else_span));
|
||||||
|
ops.extend(then_ops);
|
||||||
|
ops.push(Op::Branch(else_ops.len()));
|
||||||
|
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) {
|
||||||
|
return Err(format!("unknown word: {word}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, usize), String> {
|
||||||
|
let mut depth = 1;
|
||||||
|
let mut end_idx = None;
|
||||||
|
|
||||||
|
for (i, tok) in tokens.iter().enumerate() {
|
||||||
|
match tok {
|
||||||
|
Token::QuoteStart(_) => depth += 1,
|
||||||
|
Token::QuoteEnd(_) => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
end_idx = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let end_idx = end_idx.ok_or("missing }")?;
|
||||||
|
let byte_pos = match &tokens[end_idx] {
|
||||||
|
Token::QuoteEnd(pos) => *pos,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let quote_ops = compile(&tokens[..end_idx], dict)?;
|
||||||
|
Ok((quote_ops, end_idx + 1, byte_pos))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_span(tok: &Token) -> Option<SourceSpan> {
|
||||||
|
match tok {
|
||||||
|
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),
|
||||||
|
Token::QuoteStart(p) => Some(SourceSpan { start: *p, end: *p + 1 }),
|
||||||
|
Token::QuoteEnd(p) => Some(SourceSpan { start: *p, end: *p + 1 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_colon_def(tokens: &[Token], dict: &Dictionary) -> Result<(usize, String, Vec<Op>), String> {
|
||||||
|
if tokens.is_empty() {
|
||||||
|
return Err("expected word name after ':'".into());
|
||||||
|
}
|
||||||
|
let name = match &tokens[0] {
|
||||||
|
Token::Word(w, _) => w.clone(),
|
||||||
|
_ => return Err("expected word name after ':'".into()),
|
||||||
|
};
|
||||||
|
let mut semi_pos = None;
|
||||||
|
for (i, tok) in tokens[1..].iter().enumerate() {
|
||||||
|
if let Token::Word(w, _) = tok {
|
||||||
|
if w == ";" {
|
||||||
|
semi_pos = Some(i + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let semi_pos = semi_pos.ok_or("missing ';' in word definition")?;
|
||||||
|
let body_tokens = &tokens[1..semi_pos];
|
||||||
|
let body_ops = compile(body_tokens, dict)?;
|
||||||
|
// consumed = name + body + semicolon
|
||||||
|
Ok((semi_pos + 1, name, body_ops))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokens_span(tokens: &[Token]) -> Option<SourceSpan> {
|
||||||
|
let first = tokens.first().and_then(token_span)?;
|
||||||
|
let last = tokens.last().and_then(token_span)?;
|
||||||
|
Some(SourceSpan { start: first.start, end: last.end })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn compile_if(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, Vec<Op>, usize, Option<SourceSpan>, Option<SourceSpan>), String> {
|
||||||
|
let mut depth = 1;
|
||||||
|
let mut else_pos = None;
|
||||||
|
let mut then_pos = None;
|
||||||
|
|
||||||
|
for (i, tok) in tokens.iter().enumerate() {
|
||||||
|
if let Token::Word(w, _) = tok {
|
||||||
|
match w.as_str() {
|
||||||
|
"if" => depth += 1,
|
||||||
|
"else" if depth == 1 => else_pos = Some(i),
|
||||||
|
"then" => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
then_pos = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let then_pos = then_pos.ok_or("missing 'then'")?;
|
||||||
|
|
||||||
|
let (then_ops, else_ops, then_span, else_span) = if let Some(ep) = else_pos {
|
||||||
|
let then_slice = &tokens[..ep];
|
||||||
|
let else_slice = &tokens[ep + 1..then_pos];
|
||||||
|
let then_span = tokens_span(then_slice);
|
||||||
|
let else_span = tokens_span(else_slice);
|
||||||
|
let then_ops = compile(then_slice, dict)?;
|
||||||
|
let else_ops = compile(else_slice, dict)?;
|
||||||
|
(then_ops, else_ops, then_span, else_span)
|
||||||
|
} else {
|
||||||
|
let then_slice = &tokens[..then_pos];
|
||||||
|
let then_span = tokens_span(then_slice);
|
||||||
|
let then_ops = compile(then_slice, dict)?;
|
||||||
|
(then_ops, Vec::new(), then_span, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
|
||||||
|
}
|
||||||
11
src/model/forth/mod.rs
Normal file
11
src/model/forth/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
mod compiler;
|
||||||
|
mod ops;
|
||||||
|
mod types;
|
||||||
|
mod vm;
|
||||||
|
mod words;
|
||||||
|
|
||||||
|
pub use types::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Variables};
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use types::Value;
|
||||||
|
pub use vm::Forth;
|
||||||
|
pub use words::{Word, WordCompile, WORDS};
|
||||||
79
src/model/forth/ops.rs
Normal file
79
src/model/forth/ops.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use super::types::SourceSpan;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Op {
|
||||||
|
PushInt(i64, Option<SourceSpan>),
|
||||||
|
PushFloat(f64, Option<SourceSpan>),
|
||||||
|
PushStr(String, Option<SourceSpan>),
|
||||||
|
Dup,
|
||||||
|
Dupn,
|
||||||
|
Drop,
|
||||||
|
Swap,
|
||||||
|
Over,
|
||||||
|
Rot,
|
||||||
|
Nip,
|
||||||
|
Tuck,
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Mod,
|
||||||
|
Neg,
|
||||||
|
Abs,
|
||||||
|
Floor,
|
||||||
|
Ceil,
|
||||||
|
Round,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
Eq,
|
||||||
|
Ne,
|
||||||
|
Lt,
|
||||||
|
Gt,
|
||||||
|
Le,
|
||||||
|
Ge,
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
Not,
|
||||||
|
BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>),
|
||||||
|
Branch(usize),
|
||||||
|
NewCmd,
|
||||||
|
SetParam(String),
|
||||||
|
Emit,
|
||||||
|
Get,
|
||||||
|
Set,
|
||||||
|
GetContext(String),
|
||||||
|
Rand,
|
||||||
|
Rrand,
|
||||||
|
Seed,
|
||||||
|
Cycle,
|
||||||
|
Choose,
|
||||||
|
ChanceExec,
|
||||||
|
ProbExec,
|
||||||
|
Coin,
|
||||||
|
Mtof,
|
||||||
|
Ftom,
|
||||||
|
ListStart,
|
||||||
|
ListEnd,
|
||||||
|
ListEndCycle,
|
||||||
|
PCycle,
|
||||||
|
ListEndPCycle,
|
||||||
|
At,
|
||||||
|
Window,
|
||||||
|
Scale,
|
||||||
|
Pop,
|
||||||
|
Subdivide,
|
||||||
|
SetTempo,
|
||||||
|
Each,
|
||||||
|
Every,
|
||||||
|
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||||
|
When,
|
||||||
|
Unless,
|
||||||
|
Adsr,
|
||||||
|
Ad,
|
||||||
|
Stack,
|
||||||
|
For,
|
||||||
|
LocalCycleEnd,
|
||||||
|
Echo,
|
||||||
|
Necho,
|
||||||
|
Apply,
|
||||||
|
}
|
||||||
141
src/model/forth/types.rs
Normal file
141
src/model/forth/types.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
use rand::rngs::StdRng;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use super::ops::Op;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct SourceSpan {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct ExecutionTrace {
|
||||||
|
pub executed_spans: Vec<SourceSpan>,
|
||||||
|
pub selected_spans: Vec<SourceSpan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StepContext {
|
||||||
|
pub step: usize,
|
||||||
|
pub beat: f64,
|
||||||
|
pub pattern: usize,
|
||||||
|
pub tempo: f64,
|
||||||
|
pub phase: f64,
|
||||||
|
pub slot: usize,
|
||||||
|
pub runs: usize,
|
||||||
|
pub iter: usize,
|
||||||
|
pub speed: f64,
|
||||||
|
pub fill: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StepContext {
|
||||||
|
pub fn step_duration(&self) -> f64 {
|
||||||
|
60.0 / self.tempo / 4.0 / self.speed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Variables = Arc<Mutex<HashMap<String, Value>>>;
|
||||||
|
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
|
||||||
|
pub type Rng = Arc<Mutex<StdRng>>;
|
||||||
|
pub type Stack = Arc<Mutex<Vec<Value>>>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Value {
|
||||||
|
Int(i64, Option<SourceSpan>),
|
||||||
|
Float(f64, Option<SourceSpan>),
|
||||||
|
Str(String, Option<SourceSpan>),
|
||||||
|
Marker,
|
||||||
|
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Value {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Value::Int(a, _), Value::Int(b, _)) => a == b,
|
||||||
|
(Value::Float(a, _), Value::Float(b, _)) => a == b,
|
||||||
|
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
||||||
|
(Value::Marker, Value::Marker) => true,
|
||||||
|
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
pub fn as_float(&self) -> Result<f64, String> {
|
||||||
|
match self {
|
||||||
|
Value::Float(f, _) => Ok(*f),
|
||||||
|
Value::Int(i, _) => Ok(*i as f64),
|
||||||
|
_ => Err("expected number".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn as_int(&self) -> Result<i64, String> {
|
||||||
|
match self {
|
||||||
|
Value::Int(i, _) => Ok(*i),
|
||||||
|
Value::Float(f, _) => Ok(*f as i64),
|
||||||
|
_ => Err("expected number".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn as_str(&self) -> Result<&str, String> {
|
||||||
|
match self {
|
||||||
|
Value::Str(s, _) => Ok(s),
|
||||||
|
_ => Err("expected string".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_truthy(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Value::Int(i, _) => *i != 0,
|
||||||
|
Value::Float(f, _) => *f != 0.0,
|
||||||
|
Value::Str(s, _) => !s.is_empty(),
|
||||||
|
Value::Marker => false,
|
||||||
|
Value::Quotation(..) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_marker(&self) -> bool {
|
||||||
|
matches!(self, Value::Marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn to_param_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Value::Int(i, _) => i.to_string(),
|
||||||
|
Value::Float(f, _) => f.to_string(),
|
||||||
|
Value::Str(s, _) => s.clone(),
|
||||||
|
Value::Marker => String::new(),
|
||||||
|
Value::Quotation(..) => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn span(&self) -> Option<SourceSpan> {
|
||||||
|
match self {
|
||||||
|
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) => *s,
|
||||||
|
Value::Marker | Value::Quotation(..) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub(super) struct CmdRegister {
|
||||||
|
sound: Option<String>,
|
||||||
|
params: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CmdRegister {
|
||||||
|
pub(super) fn set_sound(&mut self, name: String) {
|
||||||
|
self.sound = Some(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_param(&mut self, key: String, value: String) {
|
||||||
|
self.params.push((key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn take(&mut self) -> Option<(String, Vec<(String, String)>)> {
|
||||||
|
let sound = self.sound.take()?;
|
||||||
|
let params = std::mem::take(&mut self.params);
|
||||||
|
Some((sound, params))
|
||||||
|
}
|
||||||
|
}
|
||||||
920
src/model/forth/vm.rs
Normal file
920
src/model/forth/vm.rs
Normal file
@@ -0,0 +1,920 @@
|
|||||||
|
use rand::rngs::StdRng;
|
||||||
|
use rand::{Rng as RngTrait, SeedableRng};
|
||||||
|
|
||||||
|
use super::compiler::compile_script;
|
||||||
|
use super::ops::Op;
|
||||||
|
use super::types::{
|
||||||
|
CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct TimeContext {
|
||||||
|
start: f64,
|
||||||
|
duration: f64,
|
||||||
|
subdivisions: Option<Vec<(f64, f64)>>,
|
||||||
|
iteration_index: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Forth {
|
||||||
|
stack: Stack,
|
||||||
|
vars: Variables,
|
||||||
|
dict: Dictionary,
|
||||||
|
rng: Rng,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Forth {
|
||||||
|
pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self {
|
||||||
|
Self {
|
||||||
|
stack: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
||||||
|
vars,
|
||||||
|
dict,
|
||||||
|
rng,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn stack(&self) -> Vec<Value> {
|
||||||
|
self.stack.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn clear_stack(&self) {
|
||||||
|
self.stack.lock().unwrap().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
|
||||||
|
self.evaluate_impl(script, ctx, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate_with_trace(
|
||||||
|
&self,
|
||||||
|
script: &str,
|
||||||
|
ctx: &StepContext,
|
||||||
|
trace: &mut ExecutionTrace,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
self.evaluate_impl(script, ctx, Some(trace))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_impl(
|
||||||
|
&self,
|
||||||
|
script: &str,
|
||||||
|
ctx: &StepContext,
|
||||||
|
trace: Option<&mut ExecutionTrace>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
if script.trim().is_empty() {
|
||||||
|
return Err("empty script".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ops = compile_script(script, &self.dict)?;
|
||||||
|
self.execute(&ops, ctx, trace)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
&self,
|
||||||
|
ops: &[Op],
|
||||||
|
ctx: &StepContext,
|
||||||
|
trace: Option<&mut ExecutionTrace>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let mut stack = self.stack.lock().unwrap();
|
||||||
|
let mut outputs: Vec<String> = Vec::new();
|
||||||
|
let mut time_stack: Vec<TimeContext> = vec![TimeContext {
|
||||||
|
start: 0.0,
|
||||||
|
duration: ctx.step_duration() * 4.0,
|
||||||
|
subdivisions: None,
|
||||||
|
iteration_index: None,
|
||||||
|
}];
|
||||||
|
let mut cmd = CmdRegister::default();
|
||||||
|
|
||||||
|
self.execute_ops(
|
||||||
|
ops,
|
||||||
|
ctx,
|
||||||
|
&mut stack,
|
||||||
|
&mut outputs,
|
||||||
|
&mut time_stack,
|
||||||
|
&mut cmd,
|
||||||
|
trace,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn execute_ops(
|
||||||
|
&self,
|
||||||
|
ops: &[Op],
|
||||||
|
ctx: &StepContext,
|
||||||
|
stack: &mut Vec<Value>,
|
||||||
|
outputs: &mut Vec<String>,
|
||||||
|
time_stack: &mut Vec<TimeContext>,
|
||||||
|
cmd: &mut CmdRegister,
|
||||||
|
trace: Option<&mut ExecutionTrace>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut pc = 0;
|
||||||
|
let trace_cell = std::cell::RefCell::new(trace);
|
||||||
|
|
||||||
|
while pc < ops.len() {
|
||||||
|
match &ops[pc] {
|
||||||
|
Op::PushInt(n, span) => stack.push(Value::Int(*n, *span)),
|
||||||
|
Op::PushFloat(f, span) => stack.push(Value::Float(*f, *span)),
|
||||||
|
Op::PushStr(s, span) => stack.push(Value::Str(s.clone(), *span)),
|
||||||
|
|
||||||
|
Op::Dup => {
|
||||||
|
let v = stack.last().ok_or("stack underflow")?.clone();
|
||||||
|
stack.push(v);
|
||||||
|
}
|
||||||
|
Op::Dupn => {
|
||||||
|
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let v = stack.pop().ok_or("stack underflow")?;
|
||||||
|
for _ in 0..n {
|
||||||
|
stack.push(v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Op::Drop => {
|
||||||
|
stack.pop().ok_or("stack underflow")?;
|
||||||
|
}
|
||||||
|
Op::Swap => {
|
||||||
|
let len = stack.len();
|
||||||
|
if len < 2 {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
stack.swap(len - 1, len - 2);
|
||||||
|
}
|
||||||
|
Op::Over => {
|
||||||
|
let len = stack.len();
|
||||||
|
if len < 2 {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
let v = stack[len - 2].clone();
|
||||||
|
stack.push(v);
|
||||||
|
}
|
||||||
|
Op::Rot => {
|
||||||
|
let len = stack.len();
|
||||||
|
if len < 3 {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
let v = stack.remove(len - 3);
|
||||||
|
stack.push(v);
|
||||||
|
}
|
||||||
|
Op::Nip => {
|
||||||
|
let len = stack.len();
|
||||||
|
if len < 2 {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
stack.remove(len - 2);
|
||||||
|
}
|
||||||
|
Op::Tuck => {
|
||||||
|
let len = stack.len();
|
||||||
|
if len < 2 {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
let v = stack[len - 1].clone();
|
||||||
|
stack.insert(len - 2, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Add => binary_op(stack, |a, b| a + b)?,
|
||||||
|
Op::Sub => binary_op(stack, |a, b| a - b)?,
|
||||||
|
Op::Mul => binary_op(stack, |a, b| a * b)?,
|
||||||
|
Op::Div => binary_op(stack, |a, b| a / b)?,
|
||||||
|
Op::Mod => {
|
||||||
|
let b = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let a = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
stack.push(Value::Int(a % b, None));
|
||||||
|
}
|
||||||
|
Op::Neg => {
|
||||||
|
let v = stack.pop().ok_or("stack underflow")?;
|
||||||
|
match v {
|
||||||
|
Value::Int(i, s) => stack.push(Value::Int(-i, s)),
|
||||||
|
Value::Float(f, s) => stack.push(Value::Float(-f, s)),
|
||||||
|
_ => return Err("expected number".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Op::Abs => {
|
||||||
|
let v = stack.pop().ok_or("stack underflow")?;
|
||||||
|
match v {
|
||||||
|
Value::Int(i, s) => stack.push(Value::Int(i.abs(), s)),
|
||||||
|
Value::Float(f, s) => stack.push(Value::Float(f.abs(), s)),
|
||||||
|
_ => return Err("expected number".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Op::Floor => {
|
||||||
|
let v = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
stack.push(Value::Int(v.floor() as i64, None));
|
||||||
|
}
|
||||||
|
Op::Ceil => {
|
||||||
|
let v = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
stack.push(Value::Int(v.ceil() as i64, None));
|
||||||
|
}
|
||||||
|
Op::Round => {
|
||||||
|
let v = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
stack.push(Value::Int(v.round() as i64, None));
|
||||||
|
}
|
||||||
|
Op::Min => binary_op(stack, |a, b| a.min(b))?,
|
||||||
|
Op::Max => binary_op(stack, |a, b| a.max(b))?,
|
||||||
|
|
||||||
|
Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?,
|
||||||
|
Op::Ne => cmp_op(stack, |a, b| (a - b).abs() >= f64::EPSILON)?,
|
||||||
|
Op::Lt => cmp_op(stack, |a, b| a < b)?,
|
||||||
|
Op::Gt => cmp_op(stack, |a, b| a > b)?,
|
||||||
|
Op::Le => cmp_op(stack, |a, b| a <= b)?,
|
||||||
|
Op::Ge => cmp_op(stack, |a, b| a >= b)?,
|
||||||
|
|
||||||
|
Op::And => {
|
||||||
|
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||||
|
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||||
|
stack.push(Value::Int(if a && b { 1 } else { 0 }, None));
|
||||||
|
}
|
||||||
|
Op::Or => {
|
||||||
|
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||||
|
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||||
|
stack.push(Value::Int(if a || b { 1 } else { 0 }, None));
|
||||||
|
}
|
||||||
|
Op::Not => {
|
||||||
|
let v = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||||
|
stack.push(Value::Int(if v { 0 } else { 1 }, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::BranchIfZero(offset, then_span, else_span) => {
|
||||||
|
let v = stack.pop().ok_or("stack underflow")?;
|
||||||
|
if !v.is_truthy() {
|
||||||
|
if let Some(span) = else_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(*span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pc += offset;
|
||||||
|
} else if let Some(span) = then_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(*span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Op::Branch(offset) => {
|
||||||
|
pc += offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::NewCmd => {
|
||||||
|
let name = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let name = name.as_str()?;
|
||||||
|
cmd.set_sound(name.to_string());
|
||||||
|
}
|
||||||
|
Op::SetParam(param) => {
|
||||||
|
let val = stack.pop().ok_or("stack underflow")?;
|
||||||
|
cmd.set_param(param.clone(), val.to_param_string());
|
||||||
|
}
|
||||||
|
Op::Emit => {
|
||||||
|
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")?;
|
||||||
|
if time_ctx.start > 0.0 {
|
||||||
|
pairs.push(("delta".into(), time_ctx.start.to_string()));
|
||||||
|
}
|
||||||
|
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||||
|
pairs.push(("dur".into(), time_ctx.duration.to_string()));
|
||||||
|
}
|
||||||
|
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 * time_ctx.duration).to_string();
|
||||||
|
} else {
|
||||||
|
pairs.push(("delaytime".into(), time_ctx.duration.to_string()));
|
||||||
|
}
|
||||||
|
outputs.push(format_cmd(&pairs));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Get => {
|
||||||
|
let name = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let name = name.as_str()?;
|
||||||
|
let vars = self.vars.lock().unwrap();
|
||||||
|
let val = vars.get(name).cloned().unwrap_or(Value::Int(0, None));
|
||||||
|
stack.push(val);
|
||||||
|
}
|
||||||
|
Op::Set => {
|
||||||
|
let name = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let name = name.as_str()?.to_string();
|
||||||
|
let val = stack.pop().ok_or("stack underflow")?;
|
||||||
|
self.vars.lock().unwrap().insert(name, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::GetContext(name) => {
|
||||||
|
let val = match name.as_str() {
|
||||||
|
"step" => Value::Int(ctx.step as i64, None),
|
||||||
|
"beat" => Value::Float(ctx.beat, None),
|
||||||
|
"pattern" => Value::Int(ctx.pattern as i64, None),
|
||||||
|
"tempo" => Value::Float(ctx.tempo, None),
|
||||||
|
"phase" => Value::Float(ctx.phase, None),
|
||||||
|
"slot" => Value::Int(ctx.slot as i64, None),
|
||||||
|
"runs" => Value::Int(ctx.runs as i64, None),
|
||||||
|
"iter" => Value::Int(ctx.iter as i64, None),
|
||||||
|
"speed" => Value::Float(ctx.speed, None),
|
||||||
|
"stepdur" => Value::Float(ctx.step_duration(), None),
|
||||||
|
"fill" => Value::Int(if ctx.fill { 1 } else { 0 }, None),
|
||||||
|
_ => Value::Int(0, None),
|
||||||
|
};
|
||||||
|
stack.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Rand => {
|
||||||
|
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let val = self.rng.lock().unwrap().gen_range(min..max);
|
||||||
|
stack.push(Value::Float(val, None));
|
||||||
|
}
|
||||||
|
Op::Rrand => {
|
||||||
|
let max = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let min = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
let val = self.rng.lock().unwrap().gen_range(min..=max);
|
||||||
|
stack.push(Value::Int(val, None));
|
||||||
|
}
|
||||||
|
Op::Seed => {
|
||||||
|
let s = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
*self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Cycle => {
|
||||||
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
|
if count == 0 {
|
||||||
|
return Err("cycle count must be > 0".into());
|
||||||
|
}
|
||||||
|
if stack.len() < count {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
let start = stack.len() - count;
|
||||||
|
let values: Vec<Value> = stack.drain(start..).collect();
|
||||||
|
let idx = ctx.runs % count;
|
||||||
|
let selected = values[idx].clone();
|
||||||
|
if let Some(span) = selected.span() {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.selected_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Value::Quotation(quot_ops, body_span) = selected {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
} else {
|
||||||
|
stack.push(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::PCycle => {
|
||||||
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
|
if count == 0 {
|
||||||
|
return Err("pcycle count must be > 0".into());
|
||||||
|
}
|
||||||
|
if stack.len() < count {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
let start = stack.len() - count;
|
||||||
|
let values: Vec<Value> = stack.drain(start..).collect();
|
||||||
|
let idx = ctx.iter % count;
|
||||||
|
let selected = values[idx].clone();
|
||||||
|
if let Some(span) = selected.span() {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.selected_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Value::Quotation(quot_ops, body_span) = selected {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
} else {
|
||||||
|
stack.push(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Choose => {
|
||||||
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
|
if count == 0 {
|
||||||
|
return Err("choose count must be > 0".into());
|
||||||
|
}
|
||||||
|
if stack.len() < count {
|
||||||
|
return Err("stack underflow".into());
|
||||||
|
}
|
||||||
|
let start = stack.len() - count;
|
||||||
|
let values: Vec<Value> = stack.drain(start..).collect();
|
||||||
|
let idx = self.rng.lock().unwrap().gen_range(0..count);
|
||||||
|
let selected = values[idx].clone();
|
||||||
|
if let Some(span) = selected.span() {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.selected_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Value::Quotation(quot_ops, body_span) = selected {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
} else {
|
||||||
|
stack.push(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::ChanceExec => {
|
||||||
|
let prob = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let val: f64 = self.rng.lock().unwrap().gen();
|
||||||
|
if val < prob {
|
||||||
|
match quot {
|
||||||
|
Value::Quotation(quot_ops, body_span) => {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
}
|
||||||
|
_ => return Err("expected quotation".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::ProbExec => {
|
||||||
|
let pct = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let val: f64 = self.rng.lock().unwrap().gen();
|
||||||
|
if val < pct / 100.0 {
|
||||||
|
match quot {
|
||||||
|
Value::Quotation(quot_ops, body_span) => {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
}
|
||||||
|
_ => return Err("expected quotation".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Coin => {
|
||||||
|
let val: f64 = self.rng.lock().unwrap().gen();
|
||||||
|
stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Every => {
|
||||||
|
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
|
if n <= 0 {
|
||||||
|
return Err("every count must be > 0".into());
|
||||||
|
}
|
||||||
|
let result = ctx.iter as i64 % n == 0;
|
||||||
|
stack.push(Value::Int(if result { 1 } else { 0 }, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Quotation(quote_ops, body_span) => {
|
||||||
|
stack.push(Value::Quotation(quote_ops.clone(), *body_span));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::When => {
|
||||||
|
let cond = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
|
if cond.is_truthy() {
|
||||||
|
match quot {
|
||||||
|
Value::Quotation(quot_ops, body_span) => {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
}
|
||||||
|
_ => return Err("expected quotation".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Unless => {
|
||||||
|
let cond = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
|
if !cond.is_truthy() {
|
||||||
|
match quot {
|
||||||
|
Value::Quotation(quot_ops, body_span) => {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
}
|
||||||
|
_ => return Err("expected quotation".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Mtof => {
|
||||||
|
let note = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let freq = 440.0 * 2.0_f64.powf((note - 69.0) / 12.0);
|
||||||
|
stack.push(Value::Float(freq, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Ftom => {
|
||||||
|
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let note = 69.0 + 12.0 * (freq / 440.0).log2();
|
||||||
|
stack.push(Value::Float(note, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::At => {
|
||||||
|
let pos = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
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 => {
|
||||||
|
let end = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let start_pos = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let parent = time_stack.last().ok_or("time stack underflow")?;
|
||||||
|
let new_start = parent.start + parent.duration * start_pos;
|
||||||
|
let new_duration = parent.duration * (end - start_pos);
|
||||||
|
time_stack.push(TimeContext {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Pop => {
|
||||||
|
if time_stack.len() <= 1 {
|
||||||
|
return Err("cannot pop root time context".into());
|
||||||
|
}
|
||||||
|
time_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Subdivide => {
|
||||||
|
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
|
if n == 0 {
|
||||||
|
return Err("subdivide 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 i in 0..n {
|
||||||
|
subs.push((time_ctx.start + sub_duration * i as f64, sub_duration));
|
||||||
|
}
|
||||||
|
time_ctx.subdivisions = Some(subs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Each => {
|
||||||
|
let (sound, params) = cmd.take().ok_or("no sound set")?;
|
||||||
|
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
|
||||||
|
let subs = time_ctx
|
||||||
|
.subdivisions
|
||||||
|
.as_ref()
|
||||||
|
.ok_or("each requires subdivide first")?;
|
||||||
|
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(), sub_dur.to_string()));
|
||||||
|
}
|
||||||
|
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 * sub_dur).to_string();
|
||||||
|
} else {
|
||||||
|
pairs.push(("delaytime".into(), sub_dur.to_string()));
|
||||||
|
}
|
||||||
|
outputs.push(format_cmd(&pairs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::SetTempo => {
|
||||||
|
let tempo = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let clamped = tempo.clamp(20.0, 300.0);
|
||||||
|
self.vars
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert("__tempo__".to_string(), Value::Float(clamped, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::ListStart => {
|
||||||
|
stack.push(Value::Marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::ListEnd => {
|
||||||
|
let mut count = 0;
|
||||||
|
let mut values = Vec::new();
|
||||||
|
while let Some(v) = stack.pop() {
|
||||||
|
if v.is_marker() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values.push(v);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
values.reverse();
|
||||||
|
for v in values {
|
||||||
|
stack.push(v);
|
||||||
|
}
|
||||||
|
stack.push(Value::Int(count, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::ListEndCycle => {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
while let Some(v) = stack.pop() {
|
||||||
|
if v.is_marker() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values.push(v);
|
||||||
|
}
|
||||||
|
if values.is_empty() {
|
||||||
|
return Err("empty cycle list".into());
|
||||||
|
}
|
||||||
|
values.reverse();
|
||||||
|
let idx = ctx.runs % values.len();
|
||||||
|
let selected = values[idx].clone();
|
||||||
|
if let Some(span) = selected.span() {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.selected_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Value::Quotation(quot_ops, body_span) = selected {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
} else {
|
||||||
|
stack.push(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::ListEndPCycle => {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
while let Some(v) = stack.pop() {
|
||||||
|
if v.is_marker() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values.push(v);
|
||||||
|
}
|
||||||
|
if values.is_empty() {
|
||||||
|
return Err("empty pattern cycle list".into());
|
||||||
|
}
|
||||||
|
values.reverse();
|
||||||
|
let idx = ctx.iter % values.len();
|
||||||
|
let selected = values[idx].clone();
|
||||||
|
if let Some(span) = selected.span() {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.selected_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Value::Quotation(quot_ops, body_span) = selected {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
} else {
|
||||||
|
stack.push(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Adsr => {
|
||||||
|
let r = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let s = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let d = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let a = stack.pop().ok_or("stack underflow")?;
|
||||||
|
cmd.set_param("attack".into(), a.to_param_string());
|
||||||
|
cmd.set_param("decay".into(), d.to_param_string());
|
||||||
|
cmd.set_param("sustain".into(), s.to_param_string());
|
||||||
|
cmd.set_param("release".into(), r.to_param_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Ad => {
|
||||||
|
let d = stack.pop().ok_or("stack underflow")?;
|
||||||
|
let a = stack.pop().ok_or("stack underflow")?;
|
||||||
|
cmd.set_param("attack".into(), a.to_param_string());
|
||||||
|
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, body_span) => {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Value::Quotation(quot_ops, body_span) = selected {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
} else {
|
||||||
|
stack.push(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Apply => {
|
||||||
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
|
match quot {
|
||||||
|
Value::Quotation(quot_ops, body_span) => {
|
||||||
|
if let Some(span) = body_span {
|
||||||
|
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||||
|
trace.executed_spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut trace_opt = trace_cell.borrow_mut().take();
|
||||||
|
self.execute_ops("_ops, ctx, stack, outputs, time_stack, cmd, trace_opt.as_deref_mut())?;
|
||||||
|
*trace_cell.borrow_mut() = trace_opt;
|
||||||
|
}
|
||||||
|
_ => return Err("expected quotation".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pc += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
||||||
|
where
|
||||||
|
F: Fn(f64, f64) -> f64,
|
||||||
|
{
|
||||||
|
let b = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let a = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let result = f(a, b);
|
||||||
|
if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
|
||||||
|
stack.push(Value::Int(result as i64, None));
|
||||||
|
} else {
|
||||||
|
stack.push(Value::Float(result, None));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmp_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
||||||
|
where
|
||||||
|
F: Fn(f64, f64) -> bool,
|
||||||
|
{
|
||||||
|
let b = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let a = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
stack.push(Value::Int(if f(a, b) { 1 } else { 0 }, None));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_cmd(pairs: &[(String, String)]) -> String {
|
||||||
|
let parts: Vec<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
||||||
|
format!("/{}", parts.join("/"))
|
||||||
|
}
|
||||||
1631
src/model/forth/words.rs
Normal file
1631
src/model/forth/words.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -45,3 +45,6 @@ mod intervals;
|
|||||||
|
|
||||||
#[path = "forth/definitions.rs"]
|
#[path = "forth/definitions.rs"]
|
||||||
mod definitions;
|
mod definitions;
|
||||||
|
|
||||||
|
#[path = "forth/list_words.rs"]
|
||||||
|
mod list_words;
|
||||||
|
|||||||
@@ -181,14 +181,14 @@ fn empty_local_cycle() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn echo_creates_decaying_subdivisions() {
|
fn echo_creates_decaying_subdivisions() {
|
||||||
// stepdur = 0.125, echo 3
|
// default dur = 0.5, echo 3
|
||||||
// d1 + d1/2 + d1/4 = d1 * 1.75 = 0.125
|
// d1 + d1/2 + d1/4 = d1 * 1.75 = 0.5
|
||||||
// d1 = 0.125 / 1.75 = 0.0714285714...
|
// d1 = 0.5 / 1.75
|
||||||
let outputs = expect_outputs(r#""kick" s 3 echo each"#, 3);
|
let outputs = expect_outputs(r#""kick" s 3 echo each"#, 3);
|
||||||
let durs = get_durs(&outputs);
|
let durs = get_durs(&outputs);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
|
|
||||||
let d1 = 0.125 / 1.75;
|
let d1 = 0.5 / 1.75;
|
||||||
let d2 = d1 / 2.0;
|
let d2 = d1 / 2.0;
|
||||||
let d3 = d1 / 4.0;
|
let d3 = d1 / 4.0;
|
||||||
|
|
||||||
@@ -220,14 +220,14 @@ fn echo_error_zero_count() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn necho_creates_growing_subdivisions() {
|
fn necho_creates_growing_subdivisions() {
|
||||||
// stepdur = 0.125, necho 3
|
// default dur = 0.5, necho 3
|
||||||
// d1 + 2*d1 + 4*d1 = d1 * 7 = 0.125
|
// d1 + 2*d1 + 4*d1 = d1 * 7 = 0.5
|
||||||
// d1 = 0.125 / 7
|
// d1 = 0.5 / 7
|
||||||
let outputs = expect_outputs(r#""kick" s 3 necho each"#, 3);
|
let outputs = expect_outputs(r#""kick" s 3 necho each"#, 3);
|
||||||
let durs = get_durs(&outputs);
|
let durs = get_durs(&outputs);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
|
|
||||||
let d1 = 0.125 / 7.0;
|
let d1 = 0.5 / 7.0;
|
||||||
let d2 = d1 * 2.0;
|
let d2 = d1 * 2.0;
|
||||||
let d3 = d1 * 4.0;
|
let d3 = d1 * 4.0;
|
||||||
|
|
||||||
|
|||||||
134
tests/forth/list_words.rs
Normal file
134
tests/forth/list_words.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use super::harness::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn choose_word_from_list() {
|
||||||
|
// 1 2 [ + - ] choose: picks + or -, applies to 1 2
|
||||||
|
// With seed 42, choose picks one deterministically
|
||||||
|
let f = forth();
|
||||||
|
let ctx = default_ctx();
|
||||||
|
f.evaluate("1 2 [ + - ] choose", &ctx).unwrap();
|
||||||
|
let val = stack_int(&f);
|
||||||
|
assert!(val == 3 || val == -1, "expected 3 or -1, got {}", val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_word_from_list() {
|
||||||
|
// At runs=0, picks first word (dup)
|
||||||
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
|
let f = run_ctx("5 < dup nip >", &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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
f.evaluate(": add3 3 + ; : add5 5 + ; 10 < add3 add5 >", &ctx).unwrap();
|
||||||
|
assert_eq!(stack_int(&f), 15); // runs=1 picks add5
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 f = run_ctx("< 10 20 30 >", &ctx);
|
||||||
|
assert_eq!(stack_int(&f), 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_values_and_words() {
|
||||||
|
// 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 outputs = f.evaluate(
|
||||||
|
": myverb 0.5 verb ; \"sine\" s 440 freq < myverb > emit",
|
||||||
|
&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();
|
||||||
|
assert_eq!(stack.len(), 2);
|
||||||
|
assert_eq!(stack_int(&f), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pcycle_word_second() {
|
||||||
|
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 f = run_ctx("10 < { 2 * } { 3 + } >", &ctx);
|
||||||
|
assert_eq!(stack_int(&f), 20); // runs=0 picks {2 *}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_op_quotation_second() {
|
||||||
|
let ctx = ctx_with(|c| c.runs = 1);
|
||||||
|
let f = run_ctx("10 < { 2 * } { 3 + } >", &ctx);
|
||||||
|
assert_eq!(stack_int(&f), 13); // runs=1 picks {3 +}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pipe_syntax_with_words() {
|
||||||
|
// | word1 word2 | uses LocalCycleEnd which should auto-apply quotations
|
||||||
|
// LocalCycleEnd uses time_ctx.iteration_index, which defaults to 0 outside for loops
|
||||||
|
let f = forth();
|
||||||
|
let ctx = default_ctx();
|
||||||
|
f.evaluate(": add3 3 + ; : add5 5 + ; 10 | add3 add5 |", &ctx).unwrap();
|
||||||
|
// iteration_index defaults to 0, picks first word (add3)
|
||||||
|
assert_eq!(stack_int(&f), 13);
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ fn approx_eq(a: f64, b: f64) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s
|
// At 120 BPM, speed 1.0: stepdur = 60/120/4/1 = 0.125s
|
||||||
|
// Default duration = 4 * stepdur = 0.5s
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stepdur_baseline() {
|
fn stepdur_baseline() {
|
||||||
@@ -47,23 +48,24 @@ fn emit_no_delta() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_half() {
|
fn at_half() {
|
||||||
// at 0.5 in root zoom (0..0.125) => delta = 0.5 * 0.125 = 0.0625
|
// at 0.5 in root (0..0.5) => delta = 0.5 * 0.5 = 0.25
|
||||||
let outputs = expect_outputs(r#""kick" s 0.5 at emit pop"#, 1);
|
let outputs = expect_outputs(r#""kick" s 0.5 at emit pop"#, 1);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(
|
assert!(
|
||||||
approx_eq(deltas[0], 0.0625),
|
approx_eq(deltas[0], 0.25),
|
||||||
"at 0.5 should be delta 0.0625, got {}",
|
"at 0.5 should be delta 0.25, got {}",
|
||||||
deltas[0]
|
deltas[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn at_quarter() {
|
fn at_quarter() {
|
||||||
|
// at 0.25 in root (0..0.5) => delta = 0.25 * 0.5 = 0.125
|
||||||
let outputs = expect_outputs(r#""kick" s 0.25 at emit pop"#, 1);
|
let outputs = expect_outputs(r#""kick" s 0.25 at emit pop"#, 1);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(
|
assert!(
|
||||||
approx_eq(deltas[0], 0.03125),
|
approx_eq(deltas[0], 0.125),
|
||||||
"at 0.25 should be delta 0.03125, got {}",
|
"at 0.25 should be delta 0.125, got {}",
|
||||||
deltas[0]
|
deltas[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -77,23 +79,23 @@ fn at_zero() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn div_2_each() {
|
fn div_2_each() {
|
||||||
// 2 subdivisions: deltas at 0 and 0.0625 (half of 0.125)
|
// 2 subdivisions: deltas at 0 and 0.25 (half of 0.5)
|
||||||
let outputs = expect_outputs(r#""kick" s 2 div each"#, 2);
|
let outputs = expect_outputs(r#""kick" s 2 div each"#, 2);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(approx_eq(deltas[0], 0.0), "first subdivision at 0");
|
assert!(approx_eq(deltas[0], 0.0), "first subdivision at 0");
|
||||||
assert!(
|
assert!(
|
||||||
approx_eq(deltas[1], 0.0625),
|
approx_eq(deltas[1], 0.25),
|
||||||
"second subdivision at 0.0625, got {}",
|
"second subdivision at 0.25, got {}",
|
||||||
deltas[1]
|
deltas[1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn div_4_each() {
|
fn div_4_each() {
|
||||||
// 4 subdivisions: 0, 0.03125, 0.0625, 0.09375
|
// 4 subdivisions: 0, 0.125, 0.25, 0.375
|
||||||
let outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
|
let outputs = expect_outputs(r#""kick" s 4 div each"#, 4);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
let expected = [0.0, 0.03125, 0.0625, 0.09375];
|
let expected = [0.0, 0.125, 0.25, 0.375];
|
||||||
for (i, (got, exp)) in deltas.iter().zip(expected.iter()).enumerate() {
|
for (i, (got, exp)) in deltas.iter().zip(expected.iter()).enumerate() {
|
||||||
assert!(
|
assert!(
|
||||||
approx_eq(*got, *exp),
|
approx_eq(*got, *exp),
|
||||||
@@ -107,10 +109,10 @@ fn div_4_each() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn div_3_each() {
|
fn div_3_each() {
|
||||||
// 3 subdivisions: 0, 0.125/3, 2*0.125/3
|
// 3 subdivisions: 0, 0.5/3, 2*0.5/3
|
||||||
let outputs = expect_outputs(r#""kick" s 3 div each"#, 3);
|
let outputs = expect_outputs(r#""kick" s 3 div each"#, 3);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
let step = 0.125 / 3.0;
|
let step = 0.5 / 3.0;
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
assert!(approx_eq(deltas[0], 0.0));
|
||||||
assert!(approx_eq(deltas[1], step), "got {}", deltas[1]);
|
assert!(approx_eq(deltas[1], step), "got {}", deltas[1]);
|
||||||
assert!(approx_eq(deltas[2], 2.0 * step), "got {}", deltas[2]);
|
assert!(approx_eq(deltas[2], 2.0 * step), "got {}", deltas[2]);
|
||||||
@@ -118,56 +120,56 @@ fn div_3_each() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zoom_full() {
|
fn zoom_full() {
|
||||||
// zoom 0.0 1.0 is the full step, same as root
|
// zoom 0.0 1.0 is the full duration, same as root
|
||||||
let outputs = expect_outputs(r#"0.0 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
let outputs = expect_outputs(r#"0.0 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(approx_eq(deltas[0], 0.0625), "full zoom at 0.5 = 0.0625");
|
assert!(approx_eq(deltas[0], 0.25), "full zoom at 0.5 = 0.25");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zoom_first_half() {
|
fn zoom_first_half() {
|
||||||
// zoom 0.0 0.5 restricts to first half (0..0.0625)
|
// zoom 0.0 0.5 restricts to first half (0..0.25)
|
||||||
// at 0.5 within that = 0.25 of full step = 0.03125
|
// at 0.5 within that = 0.125
|
||||||
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 0.5 at emit pop"#, 1);
|
let outputs = expect_outputs(r#"0.0 0.5 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(
|
assert!(
|
||||||
approx_eq(deltas[0], 0.03125),
|
approx_eq(deltas[0], 0.125),
|
||||||
"first-half zoom at 0.5 = 0.03125, got {}",
|
"first-half zoom at 0.5 = 0.125, got {}",
|
||||||
deltas[0]
|
deltas[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zoom_second_half() {
|
fn zoom_second_half() {
|
||||||
// zoom 0.5 1.0 restricts to second half (0.0625..0.125)
|
// zoom 0.5 1.0 restricts to second half (0.25..0.5)
|
||||||
// at 0.0 within that = start of second half = 0.0625
|
// at 0.0 within that = start of second half = 0.25
|
||||||
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1);
|
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 1);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(
|
assert!(
|
||||||
approx_eq(deltas[0], 0.0625),
|
approx_eq(deltas[0], 0.25),
|
||||||
"second-half zoom at 0.0 = 0.0625, got {}",
|
"second-half zoom at 0.0 = 0.25, got {}",
|
||||||
deltas[0]
|
deltas[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zoom_second_half_middle() {
|
fn zoom_second_half_middle() {
|
||||||
// zoom 0.5 1.0, at 0.5 within that = 0.75 of full step = 0.09375
|
// zoom 0.5 1.0, at 0.5 within that = 0.75 of full duration = 0.375
|
||||||
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
let outputs = expect_outputs(r#"0.5 1.0 zoom "kick" s 0.5 at emit pop"#, 1);
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(approx_eq(deltas[0], 0.09375), "got {}", deltas[0]);
|
assert!(approx_eq(deltas[0], 0.375), "got {}", deltas[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nested_zooms() {
|
fn nested_zooms() {
|
||||||
// zoom 0.0 0.5, then zoom 0.5 1.0 within that
|
// 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
|
// outer: 0..0.25, inner: 0.5..1.0 of that = 0.125..0.25
|
||||||
// at 0.0 in inner = 0.03125
|
// at 0.0 in inner = 0.125
|
||||||
let outputs = expect_outputs(r#"0.0 0.5 zoom 0.5 1.0 zoom "kick" s 0.0 at emit pop"#, 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);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(
|
assert!(
|
||||||
approx_eq(deltas[0], 0.03125),
|
approx_eq(deltas[0], 0.125),
|
||||||
"nested zoom at 0.0 = 0.03125, got {}",
|
"nested zoom at 0.0 = 0.125, got {}",
|
||||||
deltas[0]
|
deltas[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -175,7 +177,7 @@ fn nested_zooms() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn zoom_pop_sequence() {
|
fn zoom_pop_sequence() {
|
||||||
// First in zoom 0.0 0.5 at 0.0 -> delta 0
|
// 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
|
// Pop at context and zoom, then in zoom 0.5 1.0 at 0.0 -> delta 0.25
|
||||||
let outputs = expect_outputs(
|
let outputs = expect_outputs(
|
||||||
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"#,
|
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,
|
2,
|
||||||
@@ -183,7 +185,7 @@ fn zoom_pop_sequence() {
|
|||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(approx_eq(deltas[0], 0.0), "first zoom start");
|
assert!(approx_eq(deltas[0], 0.0), "first zoom start");
|
||||||
assert!(
|
assert!(
|
||||||
approx_eq(deltas[1], 0.0625),
|
approx_eq(deltas[1], 0.25),
|
||||||
"second zoom start, got {}",
|
"second zoom start, got {}",
|
||||||
deltas[1]
|
deltas[1]
|
||||||
);
|
);
|
||||||
@@ -191,12 +193,12 @@ fn zoom_pop_sequence() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn div_in_zoom() {
|
fn div_in_zoom() {
|
||||||
// zoom 0.0 0.5 (duration 0.0625), then div 2 each
|
// zoom 0.0 0.5 (duration 0.25), then div 2 each
|
||||||
// subdivisions at 0 and 0.03125
|
// subdivisions at 0 and 0.125
|
||||||
let outputs = expect_outputs(r#"0.0 0.5 zoom "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);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
assert!(approx_eq(deltas[0], 0.0));
|
||||||
assert!(approx_eq(deltas[1], 0.03125), "got {}", deltas[1]);
|
assert!(approx_eq(deltas[1], 0.125), "got {}", deltas[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -219,11 +221,11 @@ fn speed_affects_stepdur() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn div_each_at_different_tempo() {
|
fn div_each_at_different_tempo() {
|
||||||
// At 60 BPM: stepdur = 0.25, so div 2 each => 0, 0.125
|
// At 60 BPM: stepdur = 0.25, default dur = 1.0, so div 2 each => 0, 0.5
|
||||||
let ctx = ctx_with(|c| c.tempo = 60.0);
|
let ctx = ctx_with(|c| c.tempo = 60.0);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s 2 div each"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" s 2 div each"#, &ctx).unwrap();
|
||||||
let deltas = get_deltas(&outputs);
|
let deltas = get_deltas(&outputs);
|
||||||
assert!(approx_eq(deltas[0], 0.0));
|
assert!(approx_eq(deltas[0], 0.0));
|
||||||
assert!(approx_eq(deltas[1], 0.125), "got {}", deltas[1]);
|
assert!(approx_eq(deltas[1], 0.5), "got {}", deltas[1]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user