From 1119aca5ae1363b738c209bc183d477a35a2b1e1 Mon Sep 17 00:00:00 2001 From: Oleksandr Kozachuk Date: Wed, 15 Apr 2026 21:29:01 +0200 Subject: [PATCH] Add F: float locals (gforth/SwiftForth-style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `{: F: x F: y :}` now declares float-typed locals that live on the float stack. `x x F* y y F* F+ FSQRT` writes real float code without manual FSTACK juggling — previously WAFER had a 100%-compliant float wordset but no way to name intermediate float values. New IR ops `ForthFLocalGet(n)` / `ForthFLocalSet(n)` alongside the existing int-local ops. Each kind has its own index namespace so mixed declarations like `{: n F: f :}` compose cleanly. Codegen allocates f64 WASM locals after the existing f64 scratch pair; the fsp-bridge logic mirrors the existing FDup/FSwap path. Outer interpreter tracks a parallel `compiling_local_kinds` alongside `compiling_locals` (keeps the 18 existing touch-points unchanged) and extends `{:` to recognize `F:` as a per-next-name type marker. `TO` and name resolution branch on kind to pick Int vs Float get/set ops. Four tests: classic hypot, TO round-trip, mixed int/float args, and uninitialized float via `|`. Inline-inhibit for the new ops added to optimizer and is_promotable so they don't sneak into contexts that would collide with the caller's WASM locals. --- crates/core/src/codegen.rs | 64 +++++++++++++- crates/core/src/ir.rs | 4 + crates/core/src/optimizer.rs | 6 +- crates/core/src/outer.rs | 157 ++++++++++++++++++++++++++++------- 4 files changed, 196 insertions(+), 35 deletions(-) diff --git a/crates/core/src/codegen.rs b/crates/core/src/codegen.rs index 7158f1e..e2b444f 100644 --- a/crates/core/src/codegen.rs +++ b/crates/core/src/codegen.rs @@ -229,6 +229,9 @@ fn bool_to_forth_flag(f: &mut Function, tmp: u32) { struct EmitCtx { f64_local_0: u32, f64_local_1: u32, + /// Base WASM local index for float-typed Forth locals (`F:` in `{: ... :}`). + /// Float local N maps to WASM local `forth_f_local_base + N` (f64 type). + forth_f_local_base: u32, /// Base WASM local index for Forth locals ({: ... :}). /// Forth local N maps to WASM local `forth_local_base + N`. forth_local_base: u32, @@ -691,6 +694,14 @@ fn emit_op(f: &mut Function, op: &IrOp, ctx: &mut EmitCtx) { IrOp::ForthLocalSet(n) => { pop_to(f, ctx.forth_local_base + n); } + IrOp::ForthFLocalGet(n) => { + f.instruction(&Instruction::LocalGet(ctx.forth_f_local_base + n)); + fpush_via_local(f, ctx.f64_local_0); + } + IrOp::ForthFLocalSet(n) => { + fpop(f); + f.instruction(&Instruction::LocalSet(ctx.forth_f_local_base + n)); + } // -- Return stack --------------------------------------------------- IrOp::ToR => { @@ -1125,6 +1136,7 @@ fn is_promotable_body(ops: &[IrOp]) -> bool { IrOp::Call(_) | IrOp::TailCall(_) | IrOp::Execute | IrOp::SpFetch => return false, IrOp::ToR | IrOp::FromR | IrOp::Exit => return false, IrOp::ForthLocalGet(_) | IrOp::ForthLocalSet(_) => return false, + IrOp::ForthFLocalGet(_) | IrOp::ForthFLocalSet(_) => return false, IrOp::Emit | IrOp::Dot | IrOp::Cr | IrOp::Type => return false, IrOp::PushI64(_) | IrOp::PushF64(_) => return false, IrOp::FDup @@ -2360,6 +2372,34 @@ fn count_forth_locals(ops: &[IrOp]) -> u32 { max } +fn count_forth_f_locals(ops: &[IrOp]) -> u32 { + let mut max: u32 = 0; + for op in ops { + match op { + IrOp::ForthFLocalGet(n) | IrOp::ForthFLocalSet(n) => max = max.max(*n + 1), + IrOp::If { + then_body, + else_body, + } => { + max = max.max(count_forth_f_locals(then_body)); + if let Some(eb) = else_body { + max = max.max(count_forth_f_locals(eb)); + } + } + IrOp::DoLoop { body, .. } | IrOp::BeginUntil { body } | IrOp::BeginAgain { body } => { + max = max.max(count_forth_f_locals(body)); + } + IrOp::BeginWhileRepeat { test, body } => { + max = max + .max(count_forth_f_locals(test)) + .max(count_forth_f_locals(body)); + } + _ => {} + } + } + max +} + /// Generate a complete WASM module for a single compiled word. /// /// This is the JIT path: each word gets its own module that imports @@ -2467,8 +2507,14 @@ pub fn compile_word( } else { 1 + scratch_count + forth_local_count + loop_local_count }; - let has_floats = needs_f64_locals(body); - let num_f64: u32 = if has_floats { 2 } else { 0 }; + let forth_f_local_count = count_forth_f_locals(body); + // F: locals need f64 storage, which also implies the f64 scratch pair. + let has_floats = needs_f64_locals(body) || forth_f_local_count > 0; + let num_f64: u32 = if has_floats { + 2 + forth_f_local_count + } else { + 0 + }; let mut locals_decl = vec![(num_locals, ValType::I32)]; if num_f64 > 0 { locals_decl.push((num_f64, ValType::F64)); @@ -2482,9 +2528,12 @@ pub fn compile_word( 1 + scratch_count }; let loop_local_base = forth_local_base + forth_local_count; + // f64 scratch pair first (indices num_locals, num_locals+1), then F: locals. + let forth_f_local_base = num_locals + 2; let mut ctx = EmitCtx { f64_local_0: num_locals, f64_local_1: num_locals + 1, + forth_f_local_base, forth_local_base, loop_local_base, loop_locals: Vec::new(), @@ -2969,8 +3018,13 @@ fn compile_multi_word_module( } else { 1 + scratch_count + forth_local_count + loop_local_count }; - let has_floats = needs_f64_locals(body); - let num_f64: u32 = if has_floats { 2 } else { 0 }; + let forth_f_local_count = count_forth_f_locals(body); + let has_floats = needs_f64_locals(body) || forth_f_local_count > 0; + let num_f64: u32 = if has_floats { + 2 + forth_f_local_count + } else { + 0 + }; let mut locals_decl = vec![(num_locals, ValType::I32)]; if num_f64 > 0 { locals_decl.push((num_f64, ValType::F64)); @@ -2984,9 +3038,11 @@ fn compile_multi_word_module( 1 + scratch_count }; let loop_local_base = forth_local_base + forth_local_count; + let forth_f_local_base = num_locals + 2; let mut ctx = EmitCtx { f64_local_0: num_locals, f64_local_1: num_locals + 1, + forth_f_local_base, forth_local_base, loop_local_base, loop_locals: Vec::new(), diff --git a/crates/core/src/ir.rs b/crates/core/src/ir.rs index a5c936b..18130a8 100644 --- a/crates/core/src/ir.rs +++ b/crates/core/src/ir.rs @@ -139,6 +139,10 @@ pub enum IrOp { ForthLocalGet(u32), /// Set Forth local variable N: ( x -- ) ForthLocalSet(u32), + /// Push float-typed Forth local N: ( F: -- r ) + ForthFLocalGet(u32), + /// Set float-typed Forth local N: ( F: r -- ) + ForthFLocalSet(u32), // -- I/O -- /// Output character: ( char -- ) diff --git a/crates/core/src/optimizer.rs b/crates/core/src/optimizer.rs index a96c5d8..59c13a2 100644 --- a/crates/core/src/optimizer.rs +++ b/crates/core/src/optimizer.rs @@ -633,7 +633,11 @@ fn contains_call_to(ops: &[IrOp], target: WordId) -> bool { fn contains_exit(ops: &[IrOp]) -> bool { for op in ops { match op { - IrOp::Exit | IrOp::ForthLocalGet(_) | IrOp::ForthLocalSet(_) => return true, + IrOp::Exit + | IrOp::ForthLocalGet(_) + | IrOp::ForthLocalSet(_) + | IrOp::ForthFLocalGet(_) + | IrOp::ForthFLocalSet(_) => return true, IrOp::If { then_body, else_body, diff --git a/crates/core/src/outer.rs b/crates/core/src/outer.rs index 9c27b2b..fa54840 100644 --- a/crates/core/src/outer.rs +++ b/crates/core/src/outer.rs @@ -252,6 +252,8 @@ pub struct ForthVM { next_block_label: u32, /// Local variable names for the current definition ({: ... :} syntax) compiling_locals: Vec, + /// Parallel to `compiling_locals`: kind of each local (Int or Float). + compiling_local_kinds: Vec, /// Substitution table for SUBSTITUTE/REPLACES (String word set) substitutions: Arc>>>, /// Search order: list of wordlist IDs (first = top of search order). @@ -280,9 +282,19 @@ struct CompileFrame { control_stack: Vec, saw_create_in_def: bool, compiling_locals: Vec, + compiling_local_kinds: Vec, state: i32, } +/// Type of a Forth local. Int locals live on the data stack and use +/// `ForthLocalGet/Set`. Float locals live on the float stack and use +/// `ForthFLocalGet/Set`. Their WASM local index spaces are independent. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LocalKind { + Int, + Float, +} + impl ForthVM { /// Boot a new Forth VM with all primitives registered. pub fn new() -> anyhow::Result { @@ -345,6 +357,7 @@ impl ForthVM { conditional_skip_depth: 0, next_block_label: 0, compiling_locals: Vec::new(), + compiling_local_kinds: Vec::new(), substitutions: Arc::new(Mutex::new(HashMap::new())), search_order: Arc::new(Mutex::new(vec![1])), next_wid: Arc::new(Mutex::new(2)), @@ -385,6 +398,8 @@ impl ForthVM { self.control_stack.clear(); self.compiling_word_id = None; self.compiling_locals.clear(); + self.compiling_local_kinds.clear(); + self.compile_frames.clear(); return Err(e); } } @@ -1151,7 +1166,15 @@ impl ForthVM { .iter() .position(|n| n.eq_ignore_ascii_case(token)) { - self.push_ir(IrOp::ForthLocalGet(idx as u32)); + let kind = self.compiling_local_kinds[idx]; + let kind_idx = self.compiling_local_kinds[0..idx] + .iter() + .filter(|k| **k == kind) + .count() as u32; + match kind { + LocalKind::Int => self.push_ir(IrOp::ForthLocalGet(kind_idx)), + LocalKind::Float => self.push_ir(IrOp::ForthFLocalGet(kind_idx)), + } return Ok(()); } @@ -1375,8 +1398,15 @@ impl ForthVM { *bp = ahead_prefix; } // Emit a first-iteration guard: allocate a local flag. - let flag_idx = self.compiling_locals.len() as u32; + // This is an Int local; its kind-local-index is the count of + // existing Int entries. + let flag_idx = self + .compiling_local_kinds + .iter() + .filter(|k| **k == LocalKind::Int) + .count() as u32; self.compiling_locals.push("__first_iter__".to_string()); + self.compiling_local_kinds.push(LocalKind::Int); // Push flag init into the Begin's prefix (before the loop) if let ControlEntry::Begin { body: ref mut bp } = self.control_stack[bi] { bp.push(IrOp::PushI32(1)); @@ -1912,6 +1942,7 @@ impl ForthVM { control_stack: std::mem::take(&mut self.control_stack), saw_create_in_def: self.saw_create_in_def, compiling_locals: std::mem::take(&mut self.compiling_locals), + compiling_local_kinds: std::mem::take(&mut self.compiling_local_kinds), state: self.state, }; self.compile_frames.push(frame); @@ -1956,6 +1987,7 @@ impl ForthVM { self.control_stack = frame.control_stack; self.saw_create_in_def = frame.saw_create_in_def; self.compiling_locals = frame.compiling_locals; + self.compiling_local_kinds = frame.compiling_local_kinds; self.state = frame.state; if self.state != 0 { @@ -1971,11 +2003,17 @@ impl ForthVM { optimize(ir, &self.config.opt, bodies) } - /// Parse a `{: args | locals -- comment :}` block and compile local initializations. + /// Parse a `{: args | locals -- comment :}` block and compile local + /// initializations. Supports `F:` prefix (gforth/SwiftForth-style) to + /// mark the next local as float-typed. Int locals pop from the data + /// stack via `ForthLocalSet`; float locals pop from the float stack + /// via `ForthFLocalSet`. fn compile_locals_block(&mut self) -> anyhow::Result<()> { - let mut args: Vec = Vec::new(); + let mut args: Vec<(String, LocalKind)> = Vec::new(); + let mut uninits: Vec<(String, LocalKind)> = Vec::new(); let mut in_comment = false; let mut in_uninit = false; + let mut next_is_float = false; loop { let tok = self @@ -1984,44 +2022,50 @@ impl ForthVM { let tok_upper = tok.to_ascii_uppercase(); match tok_upper.as_str() { ":}" => break, - "--" => { - in_comment = true; - } - "|" => { - in_uninit = true; - } + "--" => in_comment = true, + "|" => in_uninit = true, + "F:" => next_is_float = true, _ => { if in_comment { - continue; // Skip comment tokens + continue; } - if in_uninit { - // Uninitialized local — just add to the map, no stack pop - self.compiling_locals.push(tok_upper); + let kind = if next_is_float { + LocalKind::Float } else { - // Stack-initialized arg - args.push(tok_upper); + LocalKind::Int + }; + next_is_float = false; + if in_uninit { + uninits.push((tok_upper, kind)); + } else { + args.push((tok_upper, kind)); } } } } - // Add args to locals map (they go first) let base = self.compiling_locals.len(); - for arg in &args { - self.compiling_locals.insert(base, arg.clone()); - } - // Actually, args should be at the start of the locals list - // with the first arg having the lowest index let n_args = args.len(); - let mut new_locals = args; - // Append any already-added uninit locals - new_locals.extend(self.compiling_locals.drain(base..)); - self.compiling_locals.splice(base..base, new_locals); - // Compile: pop args from data stack into locals (in reverse order) - // The first arg is deepest on the stack, last arg is on top + // Args first (assigned stack→local), then uninits (no init pop). + for (name, kind) in args.iter().chain(uninits.iter()) { + self.compiling_locals.push(name.clone()); + self.compiling_local_kinds.push(*kind); + } + + // Emit init: pop in reverse declaration order. Rightmost arg is on + // the top of its stack, so it's assigned first. for i in (0..n_args).rev() { - self.push_ir(IrOp::ForthLocalSet((base + i) as u32)); + let slot = base + i; + let kind = self.compiling_local_kinds[slot]; + let kind_idx = self.compiling_local_kinds[0..slot] + .iter() + .filter(|k| **k == kind) + .count() as u32; + match kind { + LocalKind::Int => self.push_ir(IrOp::ForthLocalSet(kind_idx)), + LocalKind::Float => self.push_ir(IrOp::ForthFLocalSet(kind_idx)), + } } Ok(()) @@ -2045,6 +2089,7 @@ impl ForthVM { } self.compiling_locals.clear(); + self.compiling_local_kinds.clear(); let name = self .compiling_name @@ -3306,7 +3351,15 @@ impl ForthVM { .iter() .position(|n| n.eq_ignore_ascii_case(&name)) { - self.push_ir(IrOp::ForthLocalSet(idx as u32)); + let kind = self.compiling_local_kinds[idx]; + let kind_idx = self.compiling_local_kinds[0..idx] + .iter() + .filter(|k| **k == kind) + .count() as u32; + match kind { + LocalKind::Int => self.push_ir(IrOp::ForthLocalSet(kind_idx)), + LocalKind::Float => self.push_ir(IrOp::ForthFLocalSet(kind_idx)), + } return Ok(()); } @@ -4170,6 +4223,7 @@ impl ForthVM { let saved_word_id = self.compiling_word_id.take(); let saved_control = std::mem::take(&mut self.control_stack); let saved_locals = std::mem::take(&mut self.compiling_locals); + let saved_local_kinds = std::mem::take(&mut self.compiling_local_kinds); self.compiling_ir.clear(); self.compiling_name = Some("_does_action_".to_string()); @@ -4213,6 +4267,7 @@ impl ForthVM { self.compiling_word_id = saved_word_id; self.control_stack = saved_control; self.compiling_locals = saved_locals; + self.compiling_local_kinds = saved_local_kinds; // Register the defining word as a "does-defining" word. let has_create = self.saw_create_in_def; @@ -7779,6 +7834,48 @@ mod tests { assert_eq!(vm.take_output(), "test"); } + // =================================================================== + // Float locals: F: prefix in {: ... :} + // =================================================================== + + #[test] + fn test_flocal_hypot() { + // Classic Pythagorean: sqrt(x*x + y*y). + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": HYPOT {: F: x F: y :} x x F* y y F* F+ FSQRT ;") + .unwrap(); + vm.evaluate("3E 4E HYPOT F>S").unwrap(); + assert_eq!(vm.data_stack(), vec![5]); + } + + #[test] + fn test_flocal_to() { + // TO on a float local reads from the float stack, not the data stack. + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": SETF {: F: a :} 10E TO a a ;").unwrap(); + vm.evaluate("1E SETF F>S").unwrap(); + assert_eq!(vm.data_stack(), vec![10]); + } + + #[test] + fn test_flocal_mixed_int_and_float_args() { + // Declaration order matters for init: rightmost arg is popped first + // from its stack. Here `n` is int (from dstack) and `f` is float (from fstack). + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": MIX {: n F: f :} f n S>F F+ ;").unwrap(); + vm.evaluate("3 4E MIX F>S").unwrap(); + assert_eq!(vm.data_stack(), vec![7]); + } + + #[test] + fn test_flocal_uninit() { + // Uninitialized float local (after `|`) starts at 0.0 until assigned. + let mut vm = ForthVM::::new().unwrap(); + vm.evaluate(": U {: | F: tmp :} 9E TO tmp tmp ;").unwrap(); + vm.evaluate("U F>S").unwrap(); + assert_eq!(vm.data_stack(), vec![9]); + } + // =================================================================== // Quotations: [: ... ;] // ===================================================================