fix: locals beat hardcoded tokens in compile_token
compile_token matched hardcoded tokens (S, ." etc) before
checking compiling_locals. Local named `s` got hijacked by
the `S` string shortcut. Forth 2012 §13.3.3.2 — locals
supersede dict names in scope. Move locals check to top of
compile_token for uniform precedence.
Tests: S-hijack repro, get+set round-trip, int-uninit pipe
syntax coverage (`{: | name :}`).
This commit is contained in:
+56
-18
@@ -852,6 +852,29 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
fn compile_token(&mut self, token: &str) -> anyhow::Result<()> {
|
fn compile_token(&mut self, token: &str) -> anyhow::Result<()> {
|
||||||
let token_upper = token.to_ascii_uppercase();
|
let token_upper = token.to_ascii_uppercase();
|
||||||
|
|
||||||
|
// Forth 2012 §13.3.3.2 — locals supersede dictionary names (and,
|
||||||
|
// by extension, hardcoded compile-mode shortcuts) within their
|
||||||
|
// declaration scope. Checked here, before any hardcoded token
|
||||||
|
// handling, to keep that precedence uniform — otherwise e.g. a
|
||||||
|
// local named `s` would be hijacked by the `S` string shortcut
|
||||||
|
// below.
|
||||||
|
if let Some(idx) = self
|
||||||
|
.compiling_locals
|
||||||
|
.iter()
|
||||||
|
.position(|n| n.eq_ignore_ascii_case(token))
|
||||||
|
{
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
|
||||||
// Handle string literals in compile mode
|
// Handle string literals in compile mode
|
||||||
if token_upper == ".\"" {
|
if token_upper == ".\"" {
|
||||||
// Parse until closing quote, emit characters as EMIT calls
|
// Parse until closing quote, emit characters as EMIT calls
|
||||||
@@ -1160,24 +1183,6 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for local variable reference (locals supersede dictionary words)
|
|
||||||
if let Some(idx) = self
|
|
||||||
.compiling_locals
|
|
||||||
.iter()
|
|
||||||
.position(|n| n.eq_ignore_ascii_case(token))
|
|
||||||
{
|
|
||||||
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(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up in dictionary (search order, then fallback to all wordlists)
|
// Look up in dictionary (search order, then fallback to all wordlists)
|
||||||
if let Some((_addr, word_id, is_immediate)) = self.dictionary.find(token) {
|
if let Some((_addr, word_id, is_immediate)) = self.dictionary.find(token) {
|
||||||
if is_immediate {
|
if is_immediate {
|
||||||
@@ -7876,6 +7881,39 @@ mod tests {
|
|||||||
assert_eq!(vm.data_stack(), vec![9]);
|
assert_eq!(vm.data_stack(), vec![9]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_named_s_not_hijacked_by_s_shortcut() {
|
||||||
|
// Forth 2012 §13.3.3.2: locals supersede dictionary names within
|
||||||
|
// their scope. Regression — local `s` was previously hijacked by
|
||||||
|
// the compile-mode `S` string shortcut in compile_token.
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate("VARIABLE V 42 V !").unwrap();
|
||||||
|
vm.evaluate(": T {: | s :} V TO s s @ ;").unwrap();
|
||||||
|
vm.evaluate("T").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![42]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_named_s_with_fetch_and_store() {
|
||||||
|
// Exercises both ForthLocalGet and ForthLocalSet for a local named `s`.
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate("VARIABLE V 0 V !").unwrap();
|
||||||
|
vm.evaluate(": STORE-VIA-S {: | s :} V TO s 99 s ! ;")
|
||||||
|
.unwrap();
|
||||||
|
vm.evaluate("STORE-VIA-S V @").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![99]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_int_uninit_local_via_pipe_syntax() {
|
||||||
|
// Missing coverage: int uninit locals via `{: | name :}` — only the
|
||||||
|
// float variant was covered (test_flocal_uninit).
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": U {: | tmp :} 7 TO tmp tmp ;").unwrap();
|
||||||
|
vm.evaluate("U").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![7]);
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// Quotations: [: ... ;]
|
// Quotations: [: ... ;]
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user