words definition

This commit is contained in:
2026-01-23 11:15:15 +01:00
parent 8af64fc4e2
commit f7e6f96cbf
10 changed files with 237 additions and 27 deletions

View File

@@ -10,7 +10,7 @@ use crate::commands::AppCommand;
use crate::engine::{ use crate::engine::{
LinkState, PatternChange, PatternSnapshot, SeqCommand, SequencerSnapshot, StepSnapshot, LinkState, PatternChange, PatternSnapshot, SeqCommand, SequencerSnapshot, StepSnapshot,
}; };
use crate::model::{self, Bank, Pattern, Rng, ScriptEngine, StepContext, Variables}; use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, StepContext, Variables};
use crate::page::Page; use crate::page::Page;
use crate::services::pattern_editor; use crate::services::pattern_editor;
use crate::settings::Settings; use crate::settings::Settings;
@@ -35,6 +35,7 @@ pub struct App {
pub metrics: Metrics, pub metrics: Metrics,
pub script_engine: ScriptEngine, pub script_engine: ScriptEngine,
pub variables: Variables, pub variables: Variables,
pub dict: Dictionary,
pub rng: Rng, pub rng: Rng,
pub live_keys: Arc<LiveKeyState>, pub live_keys: Arc<LiveKeyState>,
pub clipboard: Option<arboard::Clipboard>, pub clipboard: Option<arboard::Clipboard>,
@@ -47,8 +48,9 @@ pub struct App {
impl App { impl App {
pub fn new() -> Self { pub fn new() -> Self {
let variables = Arc::new(Mutex::new(HashMap::new())); let variables = Arc::new(Mutex::new(HashMap::new()));
let dict = Arc::new(Mutex::new(HashMap::new()));
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0))); let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&rng)); let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), Arc::clone(&rng));
let live_keys = Arc::new(LiveKeyState::new()); let live_keys = Arc::new(LiveKeyState::new());
Self { Self {
@@ -63,6 +65,7 @@ impl App {
metrics: Metrics::default(), metrics: Metrics::default(),
variables, variables,
dict,
rng, rng,
live_keys, live_keys,
script_engine, script_engine,

View File

@@ -6,7 +6,7 @@ use std::time::Duration;
use super::LinkState; use super::LinkState;
use crate::config::{MAX_BANKS, MAX_PATTERNS}; use crate::config::{MAX_BANKS, MAX_PATTERNS};
use crate::model::{ExecutionTrace, Rng, ScriptEngine, StepContext, Variables}; use crate::model::{Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Variables};
use crate::state::LiveKeyState; use crate::state::LiveKeyState;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
@@ -177,6 +177,7 @@ pub fn spawn_sequencer(
link: Arc<LinkState>, link: Arc<LinkState>,
playing: Arc<std::sync::atomic::AtomicBool>, playing: Arc<std::sync::atomic::AtomicBool>,
variables: Variables, variables: Variables,
dict: Dictionary,
rng: Rng, rng: Rng,
quantum: f64, quantum: f64,
live_keys: Arc<LiveKeyState>, live_keys: Arc<LiveKeyState>,
@@ -197,6 +198,7 @@ pub fn spawn_sequencer(
link, link,
playing, playing,
variables, variables,
dict,
rng, rng,
quantum, quantum,
shared_state_clone, shared_state_clone,
@@ -291,6 +293,7 @@ fn sequencer_loop(
link: Arc<LinkState>, link: Arc<LinkState>,
playing: Arc<std::sync::atomic::AtomicBool>, playing: Arc<std::sync::atomic::AtomicBool>,
variables: Variables, variables: Variables,
dict: Dictionary,
rng: Rng, rng: Rng,
quantum: f64, quantum: f64,
shared_state: Arc<Mutex<SharedSequencerState>>, shared_state: Arc<Mutex<SharedSequencerState>>,
@@ -298,7 +301,7 @@ fn sequencer_loop(
) { ) {
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
let script_engine = ScriptEngine::new(Arc::clone(&variables), rng); let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng);
let mut audio_state = AudioState::new(); let mut audio_state = AudioState::new();
let mut pattern_cache = PatternCache::new(); let mut pattern_cache = PatternCache::new();
let mut runs_counter = RunsCounter::new(); let mut runs_counter = RunsCounter::new();

View File

@@ -102,6 +102,7 @@ fn main() -> io::Result<()> {
Arc::clone(&link), Arc::clone(&link),
Arc::clone(&playing), Arc::clone(&playing),
Arc::clone(&app.variables), Arc::clone(&app.variables),
Arc::clone(&app.dict),
Arc::clone(&app.rng), Arc::clone(&app.rng),
settings.link.quantum, settings.link.quantum,
Arc::clone(&app.live_keys), Arc::clone(&app.live_keys),

View File

@@ -35,6 +35,7 @@ impl StepContext {
} }
pub type Variables = Arc<Mutex<HashMap<String, Value>>>; 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 Rng = Arc<Mutex<StdRng>>;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -212,6 +213,7 @@ pub enum Op {
LocalCycleEnd, LocalCycleEnd,
Echo, Echo,
Necho, Necho,
Apply,
} }
pub enum WordCompile { pub enum WordCompile {
@@ -1606,6 +1608,29 @@ pub const WORDS: &[Word] = &[
example: "1 reset", example: "1 reset",
compile: Param, compile: Param,
}, },
// Quotation execution
Word {
name: "apply",
stack: "(quot --)",
desc: "Execute quotation unconditionally",
example: "{ 2 * } apply",
compile: Simple,
},
// Word definitions
Word {
name: ":",
stack: "( -- )",
desc: "Begin word definition",
example: ": kick \"kick\" s emit ;",
compile: Simple,
},
Word {
name: ";",
stack: "( -- )",
desc: "End word definition",
example: ": kick \"kick\" s emit ;",
compile: Simple,
},
]; ];
fn simple_op(name: &str) -> Option<Op> { fn simple_op(name: &str) -> Option<Op> {
@@ -1672,6 +1697,7 @@ fn simple_op(name: &str) -> Option<Op> {
"for" => Op::For, "for" => Op::For,
"echo" => Op::Echo, "echo" => Op::Echo,
"necho" => Op::Necho, "necho" => Op::Necho,
"apply" => Op::Apply,
_ => return None, _ => return None,
}) })
} }
@@ -1751,7 +1777,7 @@ fn parse_interval(name: &str) -> Option<i64> {
Some(simple) Some(simple)
} }
fn compile_word(name: &str, span: Option<SourceSpan>, ops: &mut Vec<Op>) -> bool { fn compile_word(name: &str, span: Option<SourceSpan>, ops: &mut Vec<Op>, dict: &Dictionary) -> bool {
for word in WORDS { for word in WORDS {
if word.name == name { if word.name == name {
match &word.compile { match &word.compile {
@@ -1762,7 +1788,7 @@ fn compile_word(name: &str, span: Option<SourceSpan>, ops: &mut Vec<Op>) -> bool
} }
Context(ctx) => ops.push(Op::GetContext((*ctx).into())), Context(ctx) => ops.push(Op::GetContext((*ctx).into())),
Param => ops.push(Op::SetParam(name.into())), Param => ops.push(Op::SetParam(name.into())),
Alias(target) => return compile_word(target, span, ops), Alias(target) => return compile_word(target, span, ops, dict),
Probability(p) => { Probability(p) => {
ops.push(Op::PushFloat(*p, None)); ops.push(Op::PushFloat(*p, None));
ops.push(Op::ChanceExec); ops.push(Op::ChanceExec);
@@ -1810,6 +1836,12 @@ fn compile_word(name: &str, span: Option<SourceSpan>, ops: &mut Vec<Op>) -> bool
return true; return true;
} }
// User-defined words from dictionary
if let Some(body) = dict.lock().unwrap().get(name) {
ops.extend(body.iter().cloned());
return true;
}
false false
} }
@@ -1905,7 +1937,7 @@ fn tokenize(input: &str) -> Vec<Token> {
tokens tokens
} }
fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> { fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
let mut ops = Vec::new(); let mut ops = Vec::new();
let mut i = 0; let mut i = 0;
let mut pipe_parity = false; let mut pipe_parity = false;
@@ -1916,7 +1948,7 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
Token::Float(f, span) => ops.push(Op::PushFloat(*f, 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::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))),
Token::QuoteStart(start_pos) => { Token::QuoteStart(start_pos) => {
let (quote_ops, consumed, end_pos) = compile_quotation(&tokens[i + 1..])?; let (quote_ops, consumed, end_pos) = compile_quotation(&tokens[i + 1..], dict)?;
i += consumed; i += consumed;
let body_span = SourceSpan { start: *start_pos, end: end_pos + 1 }; let body_span = SourceSpan { start: *start_pos, end: end_pos + 1 };
ops.push(Op::Quotation(quote_ops, Some(body_span))); ops.push(Op::Quotation(quote_ops, Some(body_span)));
@@ -1926,7 +1958,13 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
} }
Token::Word(w, span) => { Token::Word(w, span) => {
let word = w.as_str(); let word = w.as_str();
if word == "|" { 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 { if pipe_parity {
ops.push(Op::LocalCycleEnd); ops.push(Op::LocalCycleEnd);
} else { } else {
@@ -1934,7 +1972,7 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
} }
pipe_parity = !pipe_parity; pipe_parity = !pipe_parity;
} else if word == "if" { } else if word == "if" {
let (then_ops, else_ops, consumed, then_span, else_span) = compile_if(&tokens[i + 1..])?; let (then_ops, else_ops, consumed, then_span, else_span) = compile_if(&tokens[i + 1..], dict)?;
i += consumed; i += consumed;
if else_ops.is_empty() { if else_ops.is_empty() {
ops.push(Op::BranchIfZero(then_ops.len(), then_span, None)); ops.push(Op::BranchIfZero(then_ops.len(), then_span, None));
@@ -1945,7 +1983,7 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
ops.push(Op::Branch(else_ops.len())); ops.push(Op::Branch(else_ops.len()));
ops.extend(else_ops); ops.extend(else_ops);
} }
} else if !compile_word(word, Some(*span), &mut ops) { } else if !compile_word(word, Some(*span), &mut ops, dict) {
return Err(format!("unknown word: {word}")); return Err(format!("unknown word: {word}"));
} }
} }
@@ -1956,7 +1994,7 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
Ok(ops) Ok(ops)
} }
fn compile_quotation(tokens: &[Token]) -> Result<(Vec<Op>, usize, usize), String> { fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize, usize), String> {
let mut depth = 1; let mut depth = 1;
let mut end_idx = None; let mut end_idx = None;
@@ -1979,7 +2017,7 @@ fn compile_quotation(tokens: &[Token]) -> Result<(Vec<Op>, usize, usize), String
Token::QuoteEnd(pos) => *pos, Token::QuoteEnd(pos) => *pos,
_ => unreachable!(), _ => unreachable!(),
}; };
let quote_ops = compile(&tokens[..end_idx])?; let quote_ops = compile(&tokens[..end_idx], dict)?;
Ok((quote_ops, end_idx + 1, byte_pos)) Ok((quote_ops, end_idx + 1, byte_pos))
} }
@@ -1991,6 +2029,30 @@ fn token_span(tok: &Token) -> Option<SourceSpan> {
} }
} }
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> { fn tokens_span(tokens: &[Token]) -> Option<SourceSpan> {
let first = tokens.first().and_then(token_span)?; let first = tokens.first().and_then(token_span)?;
let last = tokens.last().and_then(token_span)?; let last = tokens.last().and_then(token_span)?;
@@ -1998,7 +2060,7 @@ fn tokens_span(tokens: &[Token]) -> Option<SourceSpan> {
} }
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn compile_if(tokens: &[Token]) -> Result<(Vec<Op>, Vec<Op>, usize, Option<SourceSpan>, Option<SourceSpan>), String> { fn compile_if(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, Vec<Op>, usize, Option<SourceSpan>, Option<SourceSpan>), String> {
let mut depth = 1; let mut depth = 1;
let mut else_pos = None; let mut else_pos = None;
let mut then_pos = None; let mut then_pos = None;
@@ -2027,13 +2089,13 @@ fn compile_if(tokens: &[Token]) -> Result<(Vec<Op>, Vec<Op>, usize, Option<Sourc
let else_slice = &tokens[ep + 1..then_pos]; let else_slice = &tokens[ep + 1..then_pos];
let then_span = tokens_span(then_slice); let then_span = tokens_span(then_slice);
let else_span = tokens_span(else_slice); let else_span = tokens_span(else_slice);
let then_ops = compile(then_slice)?; let then_ops = compile(then_slice, dict)?;
let else_ops = compile(else_slice)?; let else_ops = compile(else_slice, dict)?;
(then_ops, else_ops, then_span, else_span) (then_ops, else_ops, then_span, else_span)
} else { } else {
let then_slice = &tokens[..then_pos]; let then_slice = &tokens[..then_pos];
let then_span = tokens_span(then_slice); let then_span = tokens_span(then_slice);
let then_ops = compile(then_slice)?; let then_ops = compile(then_slice, dict)?;
(then_ops, Vec::new(), then_span, None) (then_ops, Vec::new(), then_span, None)
}; };
@@ -2045,14 +2107,16 @@ pub type Stack = Arc<Mutex<Vec<Value>>>;
pub struct Forth { pub struct Forth {
stack: Stack, stack: Stack,
vars: Variables, vars: Variables,
dict: Dictionary,
rng: Rng, rng: Rng,
} }
impl Forth { impl Forth {
pub fn new(vars: Variables, rng: Rng) -> Self { pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self {
Self { Self {
stack: Arc::new(Mutex::new(Vec::new())), stack: Arc::new(Mutex::new(Vec::new())),
vars, vars,
dict,
rng, rng,
} }
} }
@@ -2091,7 +2155,7 @@ impl Forth {
} }
let tokens = tokenize(script); let tokens = tokenize(script);
let ops = compile(&tokens)?; let ops = compile(&tokens, &self.dict)?;
self.execute(&ops, ctx, trace) self.execute(&ops, ctx, trace)
} }
@@ -2824,6 +2888,23 @@ impl Forth {
} }
stack.push(selected); 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(&quot_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; pc += 1;
} }

View File

@@ -5,4 +5,4 @@ mod script;
pub use file::{load, save}; pub use file::{load, save};
pub use project::{Bank, Pattern, PatternSpeed, Project}; pub use project::{Bank, Pattern, PatternSpeed, Project};
pub use script::{ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Variables}; pub use script::{Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Variables};

View File

@@ -1,15 +1,15 @@
use super::forth::Forth; use super::forth::Forth;
pub use super::forth::{ExecutionTrace, Rng, SourceSpan, StepContext, Variables}; pub use super::forth::{Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Variables};
pub struct ScriptEngine { pub struct ScriptEngine {
forth: Forth, forth: Forth,
} }
impl ScriptEngine { impl ScriptEngine {
pub fn new(vars: Variables, rng: Rng) -> Self { pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self {
Self { Self {
forth: Forth::new(vars, rng), forth: Forth::new(vars, dict, rng),
} }
} }

View File

@@ -55,7 +55,7 @@ const KEYWORDS: &[&str] = &[
"zoom", "scale!", "stack", "echo", "necho", "for", "div", "each", "at", "pop", "adsr", "ad", "zoom", "scale!", "stack", "echo", "necho", "for", "div", "each", "at", "pop", "adsr", "ad",
"?", "!?", "<<", ">>", "|", "@", "!", "pcycle", "tempo!", "prob", "sometimes", "often", "?", "!?", "<<", ">>", "|", "@", "!", "pcycle", "tempo!", "prob", "sometimes", "often",
"rarely", "almostAlways", "almostNever", "always", "never", "coin", "fill", "iter", "every", "rarely", "almostAlways", "almostNever", "always", "never", "coin", "fill", "iter", "every",
"gt", "lt", "gt", "lt", ":", ";", "apply",
]; ];
const SOUND: &[&str] = &["sound", "s"]; const SOUND: &[&str] = &["sound", "s"];
const CONTEXT: &[&str] = &[ const CONTEXT: &[&str] = &[

View File

@@ -42,3 +42,6 @@ mod notes;
#[path = "forth/intervals.rs"] #[path = "forth/intervals.rs"]
mod intervals; mod intervals;
#[path = "forth/definitions.rs"]
mod definitions;

115
tests/forth/definitions.rs Normal file
View File

@@ -0,0 +1,115 @@
use super::harness::*;
#[test]
fn define_and_use_word() {
expect_int(": double 2 * ; 5 double", 10);
}
#[test]
fn define_word_with_multiple_ops() {
expect_int(": triple dup dup + + ; 3 triple", 9);
}
#[test]
fn define_word_using_another_user_word() {
expect_int(": double 2 * ; : quad double double ; 3 quad", 12);
}
#[test]
fn redefine_word_overwrites() {
expect_int(": foo 10 ; : foo 20 ; foo", 20);
}
#[test]
fn word_with_param() {
let outputs = expect_outputs(": loud 0.9 gain ; \"kick\" s loud emit", 1);
assert!(outputs[0].contains("gain/0.9"));
}
#[test]
fn word_available_across_evaluations() {
let f = forth();
let ctx = default_ctx();
f.evaluate(": hi 42 ;", &ctx).unwrap();
f.evaluate("hi", &ctx).unwrap();
assert_eq!(stack_int(&f), 42);
}
#[test]
fn word_defined_in_one_forth_available_in_same() {
let f = forth();
let ctx = default_ctx();
f.evaluate(": ten 10 ;", &ctx).unwrap();
f.clear_stack();
f.evaluate("ten ten +", &ctx).unwrap();
assert_eq!(stack_int(&f), 20);
}
#[test]
fn unknown_word_errors() {
expect_error("nosuchword", "unknown word");
}
#[test]
fn missing_semicolon_errors() {
expect_error(": foo 10", "missing ';'");
}
#[test]
fn missing_name_errors() {
expect_error(":", "expected word name");
}
#[test]
fn unexpected_semicolon_errors() {
expect_error(";", "unexpected ;");
}
#[test]
fn apply_executes_quotation() {
expect_int("5 { 2 * } apply", 10);
}
#[test]
fn apply_with_stack_ops() {
expect_int("3 4 { + } apply", 7);
}
#[test]
fn apply_empty_stack_errors() {
expect_error("apply", "stack underflow");
}
#[test]
fn apply_non_quotation_errors() {
expect_error("42 apply", "expected quotation");
}
#[test]
fn apply_nested() {
expect_int("2 { { 3 * } apply } apply", 6);
}
#[test]
fn define_word_containing_quotation() {
expect_int(": dbl { 2 * } apply ; 7 dbl", 14);
}
#[test]
fn define_word_with_sound() {
let outputs = expect_outputs(": kick \"kick\" s emit ; kick", 1);
assert!(outputs[0].contains("sound/kick"));
}
#[test]
fn define_word_with_conditional() {
let f = forth();
let ctx = default_ctx();
f.evaluate(": maybe-double dup 5 gt if 2 * then ;", &ctx).unwrap();
f.clear_stack();
f.evaluate("3 maybe-double", &ctx).unwrap();
assert_eq!(stack_int(&f), 3);
f.clear_stack();
f.evaluate("10 maybe-double", &ctx).unwrap();
assert_eq!(stack_int(&f), 20);
}

View File

@@ -1,6 +1,6 @@
use rand::rngs::StdRng; use rand::rngs::StdRng;
use rand::SeedableRng; use rand::SeedableRng;
use cagire::model::forth::{Forth, Rng, StepContext, Value, Variables}; use cagire::model::forth::{Dictionary, Forth, Rng, StepContext, Value, Variables};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@@ -29,16 +29,20 @@ pub fn new_vars() -> Variables {
Arc::new(Mutex::new(HashMap::new())) Arc::new(Mutex::new(HashMap::new()))
} }
pub fn new_dict() -> Dictionary {
Arc::new(Mutex::new(HashMap::new()))
}
pub fn seeded_rng(seed: u64) -> Rng { pub fn seeded_rng(seed: u64) -> Rng {
Arc::new(Mutex::new(StdRng::seed_from_u64(seed))) Arc::new(Mutex::new(StdRng::seed_from_u64(seed)))
} }
pub fn forth() -> Forth { pub fn forth() -> Forth {
Forth::new(new_vars(), seeded_rng(42)) Forth::new(new_vars(), new_dict(), seeded_rng(42))
} }
pub fn forth_seeded(seed: u64) -> Forth { pub fn forth_seeded(seed: u64) -> Forth {
Forth::new(new_vars(), seeded_rng(seed)) Forth::new(new_vars(), new_dict(), seeded_rng(seed))
} }
pub fn run(script: &str) -> Forth { pub fn run(script: &str) -> Forth {