diff --git a/CHANGELOG.md b/CHANGELOG.md index 08731aa..e3a3318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- Double-stack words: `2dup`, `2drop`, `2swap`, `2over`. +- `forget` word to remove user-defined words from the dictionary. + ## [0.0.3] - 2026-02-02 ### Added diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index a7b297e..c5f2d68 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -13,6 +13,11 @@ pub enum Op { Rot, Nip, Tuck, + Dup2, + Drop2, + Swap2, + Over2, + Forget, Add, Sub, Mul, diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index bed5b80..0b4f5f1 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -262,6 +262,42 @@ impl Forth { let v = stack[len - 1].clone(); stack.insert(len - 2, v); } + Op::Dup2 => { + let len = stack.len(); + if len < 2 { + return Err("stack underflow".into()); + } + let a = stack[len - 2].clone(); + let b = stack[len - 1].clone(); + stack.push(a); + stack.push(b); + } + Op::Drop2 => { + let len = stack.len(); + if len < 2 { + return Err("stack underflow".into()); + } + stack.pop(); + stack.pop(); + } + Op::Swap2 => { + let len = stack.len(); + if len < 4 { + return Err("stack underflow".into()); + } + stack.swap(len - 4, len - 2); + stack.swap(len - 3, len - 1); + } + Op::Over2 => { + let len = stack.len(); + if len < 4 { + return Err("stack underflow".into()); + } + let a = stack[len - 4].clone(); + let b = stack[len - 3].clone(); + stack.push(a); + stack.push(b); + } Op::Add => binary_op(stack, |a, b| a + b)?, Op::Sub => binary_op(stack, |a, b| a - b)?, @@ -915,6 +951,10 @@ impl Forth { .unwrap_or(0); stack.push(Value::Int(val as i64, None)); } + Op::Forget => { + let name = stack.pop().ok_or("stack underflow")?.as_str()?.to_string(); + self.dict.lock().unwrap().remove(&name); + } } pc += 1; } diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index 64429b0..403cdd2 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -107,6 +107,46 @@ pub const WORDS: &[Word] = &[ compile: Simple, varargs: false, }, + Word { + name: "2dup", + aliases: &[], + category: "Stack", + stack: "(a b -- a b a b)", + desc: "Duplicate top two values", + example: "1 2 2dup => 1 2 1 2", + compile: Simple, + varargs: false, + }, + Word { + name: "2drop", + aliases: &[], + category: "Stack", + stack: "(a b --)", + desc: "Drop top two values", + example: "1 2 3 2drop => 1", + compile: Simple, + varargs: false, + }, + Word { + name: "2swap", + aliases: &[], + category: "Stack", + stack: "(a b c d -- c d a b)", + desc: "Swap top two pairs", + example: "1 2 3 4 2swap => 3 4 1 2", + compile: Simple, + varargs: false, + }, + Word { + name: "2over", + aliases: &[], + category: "Stack", + stack: "(a b c d -- a b c d a b)", + desc: "Copy second pair to top", + example: "1 2 3 4 2over => 1 2 3 4 1 2", + compile: Simple, + varargs: false, + }, // Arithmetic Word { name: "+", @@ -2571,6 +2611,16 @@ pub const WORDS: &[Word] = &[ compile: Simple, varargs: false, }, + Word { + name: "forget", + aliases: &[], + category: "Definitions", + stack: "(name --)", + desc: "Remove user-defined word from dictionary", + example: "\"double\" forget", + compile: Simple, + varargs: false, + }, // Generator Word { name: "..", @@ -2770,6 +2820,10 @@ pub(super) fn simple_op(name: &str) -> Option { "rot" => Op::Rot, "nip" => Op::Nip, "tuck" => Op::Tuck, + "2dup" => Op::Dup2, + "2drop" => Op::Drop2, + "2swap" => Op::Swap2, + "2over" => Op::Over2, "+" => Op::Add, "-" => Op::Sub, "*" => Op::Mul, @@ -2842,6 +2896,7 @@ pub(super) fn simple_op(name: &str) -> Option { "mstart" => Op::MidiStart, "mstop" => Op::MidiStop, "mcont" => Op::MidiContinue, + "forget" => Op::Forget, _ => return None, }) } diff --git a/tests/forth/definitions.rs b/tests/forth/definitions.rs index 5c58382..f5d8e14 100644 --- a/tests/forth/definitions.rs +++ b/tests/forth/definitions.rs @@ -1,4 +1,5 @@ use super::harness::*; +use cagire::forth::Value; #[test] fn define_and_use_word() { @@ -113,3 +114,44 @@ fn define_word_with_conditional() { f.evaluate("10 maybe-double", &ctx).unwrap(); assert_eq!(stack_int(&f), 20); } + +#[test] +fn forget_removes_word() { + let f = forth(); + let ctx = default_ctx(); + f.evaluate(": double 2 * ;", &ctx).unwrap(); + f.evaluate("5 double", &ctx).unwrap(); + assert_eq!(stack_int(&f), 10); + f.clear_stack(); + f.evaluate("\"double\" forget", &ctx).unwrap(); + f.evaluate("double", &ctx).unwrap(); + let stack = f.stack(); + assert_eq!(stack.len(), 1); + match &stack[0] { + Value::Str(s, _) => assert_eq!(s, "double"), + other => panic!("expected Str, got {:?}", other), + } +} + +#[test] +fn forget_nonexistent_is_noop() { + let f = forth(); + let ctx = default_ctx(); + f.evaluate("\"nosuchword\" forget", &ctx).unwrap(); + f.evaluate("42", &ctx).unwrap(); + assert_eq!(stack_int(&f), 42); +} + +#[test] +fn forget_and_redefine() { + let f = forth(); + let ctx = default_ctx(); + f.evaluate(": foo 10 ;", &ctx).unwrap(); + f.evaluate("foo", &ctx).unwrap(); + assert_eq!(stack_int(&f), 10); + f.clear_stack(); + f.evaluate("\"foo\" forget", &ctx).unwrap(); + f.evaluate(": foo 20 ;", &ctx).unwrap(); + f.evaluate("foo", &ctx).unwrap(); + assert_eq!(stack_int(&f), 20); +} diff --git a/tests/forth/stack.rs b/tests/forth/stack.rs index f4ded94..5537afb 100644 --- a/tests/forth/stack.rs +++ b/tests/forth/stack.rs @@ -95,6 +95,46 @@ fn tuck_underflow() { expect_error("1 tuck", "stack underflow"); } +#[test] +fn dup2() { + expect_stack("1 2 2dup", &[int(1), int(2), int(1), int(2)]); +} + +#[test] +fn dup2_underflow() { + expect_error("1 2dup", "stack underflow"); +} + +#[test] +fn drop2() { + expect_stack("1 2 3 2drop", &[int(1)]); +} + +#[test] +fn drop2_underflow() { + expect_error("1 2drop", "stack underflow"); +} + +#[test] +fn swap2() { + expect_stack("1 2 3 4 2swap", &[int(3), int(4), int(1), int(2)]); +} + +#[test] +fn swap2_underflow() { + expect_error("1 2 3 2swap", "stack underflow"); +} + +#[test] +fn over2() { + expect_stack("1 2 3 4 2over", &[int(1), int(2), int(3), int(4), int(1), int(2)]); +} + +#[test] +fn over2_underflow() { + expect_error("1 2 3 2over", "stack underflow"); +} + #[test] fn stack_persists() { let f = forth();