[BREAKING] Feat: quotation is now using ()

This commit is contained in:
2026-02-28 20:25:59 +01:00
parent ec98274dfe
commit 651ed1219d
24 changed files with 182 additions and 169 deletions

View File

@@ -187,30 +187,30 @@ fn nor_ff() {
#[test]
fn ifelse_true() {
expect_int("{ 42 } { 99 } 1 ifelse", 42);
expect_int("( 42 ) ( 99 ) 1 ifelse", 42);
}
#[test]
fn ifelse_false() {
expect_int("{ 42 } { 99 } 0 ifelse", 99);
expect_int("( 42 ) ( 99 ) 0 ifelse", 99);
}
#[test]
fn select_first() {
expect_int("{ 10 } { 20 } { 30 } 0 select", 10);
expect_int("( 10 ) ( 20 ) ( 30 ) 0 select", 10);
}
#[test]
fn select_second() {
expect_int("{ 10 } { 20 } { 30 } 1 select", 20);
expect_int("( 10 ) ( 20 ) ( 30 ) 1 select", 20);
}
#[test]
fn select_third() {
expect_int("{ 10 } { 20 } { 30 } 2 select", 30);
expect_int("( 10 ) ( 20 ) ( 30 ) 2 select", 30);
}
#[test]
fn select_preserves_stack() {
expect_int("5 { 10 } { 20 } 0 select +", 15);
expect_int("5 ( 10 ) ( 20 ) 0 select +", 15);
}

View File

@@ -59,14 +59,14 @@ fn iter() {
#[test]
fn every_true_on_zero() {
let ctx = ctx_with(|c| c.iter = 0);
let f = run_ctx("{ 100 } 4 every", &ctx);
let f = run_ctx("( 100 ) 4 every", &ctx);
assert_eq!(stack_int(&f), 100);
}
#[test]
fn every_true_on_multiple() {
let ctx = ctx_with(|c| c.iter = 8);
let f = run_ctx("{ 100 } 4 every", &ctx);
let f = run_ctx("( 100 ) 4 every", &ctx);
assert_eq!(stack_int(&f), 100);
}
@@ -74,14 +74,14 @@ fn every_true_on_multiple() {
fn every_false_between() {
for i in 1..4 {
let ctx = ctx_with(|c| c.iter = i);
let f = run_ctx("{ 100 } 4 every", &ctx);
let f = run_ctx("( 100 ) 4 every", &ctx);
assert!(f.stack().is_empty(), "iter={} should not execute quotation", i);
}
}
#[test]
fn every_zero_count() {
expect_error("{ 1 } 0 every", "every count must be > 0");
expect_error("( 1 ) 0 every", "every count must be > 0");
}
#[test]
@@ -105,7 +105,7 @@ fn bjork_tresillo() {
// Bresenham(3,8) hits at positions 0, 2, 5
for runs in 0..8 {
let ctx = ctx_with(|c| c.runs = runs);
let f = run_ctx("{ 100 } 3 8 bjork", &ctx);
let f = run_ctx("( 100 ) 3 8 bjork", &ctx);
let hit = ((runs + 1) * 3) / 8 != (runs * 3) / 8;
if hit {
assert_eq!(stack_int(&f), 100, "runs={} should hit", runs);
@@ -121,7 +121,7 @@ fn bjork_hit_count() {
let mut hit_count = 0;
for runs in 0..8 {
let ctx = ctx_with(|c| c.runs = runs);
let f = run_ctx("{ 100 } 3 8 bjork", &ctx);
let f = run_ctx("( 100 ) 3 8 bjork", &ctx);
if !f.stack().is_empty() {
hit_count += 1;
}
@@ -132,20 +132,20 @@ fn bjork_hit_count() {
#[test]
fn bjork_all_hits() {
let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("{ 100 } 8 8 bjork", &ctx);
let f = run_ctx("( 100 ) 8 8 bjork", &ctx);
assert_eq!(stack_int(&f), 100);
}
#[test]
fn bjork_zero_hits() {
let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("{ 100 } 0 8 bjork", &ctx);
let f = run_ctx("( 100 ) 0 8 bjork", &ctx);
assert!(f.stack().is_empty());
}
#[test]
fn bjork_invalid() {
expect_error("{ 1 } 3 0 bjork", "bjork");
expect_error("( 1 ) 3 0 bjork", "bjork");
}
// pbjork (iter-based)
@@ -155,7 +155,7 @@ fn pbjork_cinquillo() {
let mut hit_count = 0;
for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter);
let f = run_ctx("{ 100 } 5 8 pbjork", &ctx);
let f = run_ctx("( 100 ) 5 8 pbjork", &ctx);
if !f.stack().is_empty() {
hit_count += 1;
}
@@ -167,7 +167,7 @@ fn pbjork_cinquillo() {
fn pbjork_wraps() {
let ctx0 = ctx_with(|c| c.iter = 0);
let ctx8 = ctx_with(|c| c.iter = 8);
let f0 = run_ctx("{ 100 } 3 8 pbjork", &ctx0);
let f8 = run_ctx("{ 100 } 3 8 pbjork", &ctx8);
let f0 = run_ctx("( 100 ) 3 8 pbjork", &ctx0);
let f8 = run_ctx("( 100 ) 3 8 pbjork", &ctx8);
assert_eq!(f0.stack().is_empty(), f8.stack().is_empty());
}

View File

@@ -68,12 +68,12 @@ fn unexpected_semicolon_errors() {
#[test]
fn apply_executes_quotation() {
expect_int("5 { 2 * } apply", 10);
expect_int("5 ( 2 * ) apply", 10);
}
#[test]
fn apply_with_stack_ops() {
expect_int("3 4 { + } apply", 7);
expect_int("3 4 ( + ) apply", 7);
}
#[test]
@@ -88,12 +88,12 @@ fn apply_non_quotation_errors() {
#[test]
fn apply_nested() {
expect_int("2 { { 3 * } apply } apply", 6);
expect_int("2 ( ( 3 * ) apply ) apply", 6);
}
#[test]
fn define_word_containing_quotation() {
expect_int(": dbl { 2 * } apply ; 7 dbl", 14);
expect_int(": dbl ( 2 * ) apply ; 7 dbl", 14);
}
#[test]

View File

@@ -33,7 +33,7 @@ fn range_underflow() {
#[test]
fn gen_basic() {
expect_stack("{ 42 } 3 gen", &[int(42), int(42), int(42)]);
expect_stack("( 42 ) 3 gen", &[int(42), int(42), int(42)]);
}
#[test]
@@ -42,7 +42,7 @@ fn gen_with_computation() {
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
// So we get [0, 1, 1, 1] - the 0 stays, we collect three 1s
expect_stack("0 { dup 1 + } 3 gen", &[int(0), int(1), int(1), int(1)]);
expect_stack("0 ( dup 1 + ) 3 gen", &[int(0), int(1), int(1), int(1)]);
}
#[test]
@@ -50,12 +50,12 @@ fn gen_chained() {
// Start with 1, each iteration: dup, multiply by 2
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
expect_stack("1 { dup 2 * } 3 gen", &[int(1), int(2), int(2), int(2)]);
expect_stack("1 ( dup 2 * ) 3 gen", &[int(1), int(2), int(2), int(2)]);
}
#[test]
fn gen_zero() {
expect_stack("{ 1 } 0 gen", &[]);
expect_stack("( 1 ) 0 gen", &[]);
}
#[test]
@@ -65,17 +65,17 @@ fn gen_underflow() {
#[test]
fn gen_not_a_number() {
expect_error("{ 1 } gen", "expected number");
expect_error("( 1 ) gen", "expected number");
}
#[test]
fn gen_negative() {
expect_error("{ 1 } -1 gen", "gen count must be >= 0");
expect_error("( 1 ) -1 gen", "gen count must be >= 0");
}
#[test]
fn gen_empty_quot_error() {
expect_error("{ } 3 gen", "quotation must produce");
expect_error("( ) 3 gen", "quotation must produce");
}
#[test]

View File

@@ -46,20 +46,20 @@ fn pcycle_by_iter() {
#[test]
fn cycle_with_quotations() {
let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx);
let f = run_ctx("5 ( dup ) ( 2 * ) 2 cycle", &ctx);
let stack = f.stack();
assert_eq!(stack.len(), 2);
assert_eq!(stack_int(&f), 5);
let ctx = ctx_with(|c| c.runs = 1);
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx);
let f = run_ctx("5 ( dup ) ( 2 * ) 2 cycle", &ctx);
assert_eq!(stack_int(&f), 10);
}
#[test]
fn cycle_executes_quotation() {
let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("10 { 3 + } { 5 + } 2 cycle", &ctx);
let f = run_ctx("10 ( 3 + ) ( 5 + ) 2 cycle", &ctx);
assert_eq!(stack_int(&f), 13);
}
@@ -108,11 +108,11 @@ fn bracket_cycle() {
#[test]
fn bracket_with_quotations() {
let ctx = ctx_with(|c| c.runs = 0);
let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx);
let f = run_ctx("5 [ ( 3 + ) ( 5 + ) ] cycle", &ctx);
assert_eq!(stack_int(&f), 8);
let ctx = ctx_with(|c| c.runs = 1);
let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx);
let f = run_ctx("5 [ ( 3 + ) ( 5 + ) ] cycle", &ctx);
assert_eq!(stack_int(&f), 10);
}
@@ -172,7 +172,7 @@ fn index_negative_wraps() {
#[test]
fn index_with_quotation() {
expect_int("5 [ { 3 + } { 5 + } ] 0 index", 8);
expect_int("5 [ ( 3 + ) ( 5 + ) ] 0 index", 8);
}
#[test]

View File

@@ -4,26 +4,26 @@ use super::harness::*;
fn quotation_on_stack() {
// Quotation should be pushable to stack
let f = forth();
let result = f.evaluate("{ 1 2 + }", &default_ctx());
let result = f.evaluate("( 1 2 + )", &default_ctx());
assert!(result.is_ok());
}
#[test]
fn when_true_executes() {
let f = run("{ 42 } 1 ?");
let f = run("( 42 ) 1 ?");
assert_eq!(stack_int(&f), 42);
}
#[test]
fn when_false_skips() {
let f = run("99 { 42 } 0 ?");
let f = run("99 ( 42 ) 0 ?");
// Stack should still have 99, quotation not executed
assert_eq!(stack_int(&f), 99);
}
#[test]
fn when_with_arithmetic() {
let f = run("10 { 5 + } 1 ?");
let f = run("10 ( 5 + ) 1 ?");
assert_eq!(stack_int(&f), 15);
}
@@ -31,48 +31,48 @@ fn when_with_arithmetic() {
fn when_with_every() {
// iter=0, every 2 executes quotation
let ctx = ctx_with(|c| c.iter = 0);
let f = run_ctx("{ 100 } 2 every", &ctx);
let f = run_ctx("( 100 ) 2 every", &ctx);
assert_eq!(stack_int(&f), 100);
// iter=1, every 2 skips quotation
let ctx = ctx_with(|c| c.iter = 1);
let f = run_ctx("50 { 100 } 2 every", &ctx);
let f = run_ctx("50 ( 100 ) 2 every", &ctx);
assert_eq!(stack_int(&f), 50); // quotation not executed
}
#[test]
fn when_with_chance_deterministic() {
// 1.0 chance always executes quotation
let f = run("{ 42 } 1.0 chance");
let f = run("( 42 ) 1.0 chance");
assert_eq!(stack_int(&f), 42);
// 0.0 chance never executes quotation
let f = run("99 { 42 } 0.0 chance");
let f = run("99 ( 42 ) 0.0 chance");
assert_eq!(stack_int(&f), 99);
}
#[test]
fn nested_quotations() {
let f = run("{ { 42 } 1 ? } 1 ?");
let f = run("( ( 42 ) 1 ? ) 1 ?");
assert_eq!(stack_int(&f), 42);
}
#[test]
fn quotation_with_param() {
let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? ."#, 1);
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 1 ? ."#, 1);
assert!(outputs[0].contains("distort/2"));
}
#[test]
fn quotation_skips_param() {
let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? ."#, 1);
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 0 ? ."#, 1);
assert!(!outputs[0].contains("distort"));
}
#[test]
fn quotation_with_emit() {
// When true, . should fire
let outputs = expect_outputs(r#""kick" s { . } 1 ?"#, 1);
let outputs = expect_outputs(r#""kick" s ( . ) 1 ?"#, 1);
assert!(outputs[0].contains("kick"));
}
@@ -81,7 +81,7 @@ fn quotation_skips_emit() {
// When false, . should not fire
let f = forth();
let outputs = f
.evaluate(r#""kick" s { . } 0 ?"#, &default_ctx())
.evaluate(r#""kick" s ( . ) 0 ?"#, &default_ctx())
.unwrap();
// No output since . was skipped and no implicit emit
assert_eq!(outputs.len(), 0);
@@ -94,22 +94,22 @@ fn missing_quotation_error() {
#[test]
fn unclosed_quotation_error() {
expect_error("{ 1 2", "missing }");
expect_error("( 1 2", "missing )");
}
#[test]
fn unexpected_close_error() {
expect_error("1 2 }", "unexpected }");
expect_error("1 2 )", "unexpected )");
}
#[test]
fn every_with_quotation_integration() {
// { 2 distort } 2 every — on even iterations, distort is applied
// ( 2 distort ) 2 every — on even iterations, distort is applied
for iter in 0..4 {
let ctx = ctx_with(|c| c.iter = iter);
let f = forth();
let outputs = f
.evaluate(r#""kick" s { 2 distort } 2 every ."#, &ctx)
.evaluate(r#""kick" s ( 2 distort ) 2 every ."#, &ctx)
.unwrap();
if iter % 2 == 0 {
assert!(
@@ -134,7 +134,7 @@ fn bjork_with_sound() {
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
let f = forth();
let outputs = f
.evaluate(r#""kick" s { 2 distort } 3 8 bjork ."#, &ctx)
.evaluate(r#""kick" s ( 2 distort ) 3 8 bjork ."#, &ctx)
.unwrap();
assert!(outputs[0].contains("distort/2"));
}
@@ -143,13 +143,13 @@ fn bjork_with_sound() {
#[test]
fn unless_false_executes() {
let f = run("{ 42 } 0 !?");
let f = run("( 42 ) 0 !?");
assert_eq!(stack_int(&f), 42);
}
#[test]
fn unless_true_skips() {
let f = run("99 { 42 } 1 !?");
let f = run("99 ( 42 ) 1 !?");
assert_eq!(stack_int(&f), 99);
}
@@ -161,7 +161,7 @@ fn when_and_unless_complementary() {
let f = forth();
let outputs = f
.evaluate(
r#""kick" s { 2 distort } iter 2 mod 0 = ? { 4 distort } iter 2 mod 0 = !? ."#,
r#""kick" s ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#,
&ctx,
)
.unwrap();

View File

@@ -38,14 +38,14 @@ fn coin_binary() {
#[test]
fn chance_zero() {
// 0.0 probability should never execute the quotation
let f = run("99 { 42 } 0.0 chance");
let f = run("99 ( 42 ) 0.0 chance");
assert_eq!(stack_int(&f), 99); // quotation not executed, 99 still on stack
}
#[test]
fn chance_one() {
// 1.0 probability should always execute the quotation
let f = run("{ 42 } 1.0 chance");
let f = run("( 42 ) 1.0 chance");
assert_eq!(stack_int(&f), 42);
}
@@ -281,7 +281,7 @@ fn wchoose_negative_weight() {
#[test]
fn wchoose_quotation() {
let f = forth_seeded(42);
f.evaluate("{ 10 } 0.0 { 20 } 1.0 2 wchoose", &default_ctx())
f.evaluate("( 10 ) 0.0 ( 20 ) 1.0 2 wchoose", &default_ctx())
.unwrap();
assert_eq!(stack_int(&f), 20);
}

View File

@@ -99,7 +99,7 @@ fn cycle_picks_by_runs() {
for runs in 0..4 {
let ctx = ctx_with(|c| c.runs = runs);
let f = forth();
let outputs = f.evaluate(r#""kick" s { . } { } 2 cycle"#, &ctx).unwrap();
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 cycle"#, &ctx).unwrap();
if runs % 2 == 0 {
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
} else {
@@ -113,7 +113,7 @@ fn pcycle_picks_by_iter() {
for iter in 0..4 {
let ctx = ctx_with(|c| c.iter = iter);
let f = forth();
let outputs = f.evaluate(r#""kick" s { . } { } 2 pcycle"#, &ctx).unwrap();
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
if iter % 2 == 0 {
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
} else {
@@ -128,7 +128,7 @@ fn cycle_with_sounds() {
let ctx = ctx_with(|c| c.runs = runs);
let f = forth();
let outputs = f.evaluate(
r#"{ "kick" s . } { "hat" s . } { "snare" s . } 3 cycle"#,
r#"( "kick" s . ) ( "hat" s . ) ( "snare" s . ) 3 cycle"#,
&ctx
).unwrap();
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
@@ -346,7 +346,7 @@ fn every_offset_fires_at_offset() {
for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter);
let f = forth();
let outputs = f.evaluate(r#""kick" s { . } 4 2 every+"#, &ctx).unwrap();
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 every+"#, &ctx).unwrap();
if iter % 4 == 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
} else {
@@ -361,7 +361,7 @@ fn every_offset_wraps_large_offset() {
for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter);
let f = forth();
let outputs = f.evaluate(r#""kick" s { . } 4 6 every+"#, &ctx).unwrap();
let outputs = f.evaluate(r#""kick" s ( . ) 4 6 every+"#, &ctx).unwrap();
if iter % 4 == 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
} else {
@@ -375,7 +375,7 @@ fn except_offset_inverse() {
for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter);
let f = forth();
let outputs = f.evaluate(r#""kick" s { . } 4 2 except+"#, &ctx).unwrap();
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 except+"#, &ctx).unwrap();
if iter % 4 != 2 {
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
} else {
@@ -389,8 +389,8 @@ fn every_offset_zero_is_same_as_every() {
for iter in 0..8 {
let ctx = ctx_with(|c| c.iter = iter);
let f = forth();
let a = f.evaluate(r#""kick" s { . } 3 every"#, &ctx).unwrap();
let b = f.evaluate(r#""kick" s { . } 3 0 every+"#, &ctx).unwrap();
let a = f.evaluate(r#""kick" s ( . ) 3 every"#, &ctx).unwrap();
let b = f.evaluate(r#""kick" s ( . ) 3 0 every+"#, &ctx).unwrap();
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
}
}