Add (LOCAL) per Forth 2012 §13.6.1.0086
Implement `(LOCAL)` as a host primitive that defers its effect to the
outer-interpreter compile state via two new `PendingAction` variants:
- `DeclareLocal(name)` — a non-sentinel `(LOCAL)` call with `u > 0`
appends the name to `compiling_locals` as an int local.
- `DeclareLocalEnd` — the `0 0 (LOCAL)` sentinel emits reverse-order
`ForthLocalSet` IR for the batch declared since the last sentinel,
reusing the same IR shape as the `{: ... :}` locals flow.
`local_batch_base` tracks where the current batch started; it is
saved/restored across nested compile frames and cleared on
`finish_colon_def`. Int-only, per spec — float locals remain `{F: :}`.
Also fix `\` per §6.2.2535: parse-and-discard must stop at the next
`\n`, not at `#TIB`. Under line-wrapped `evaluate` calls (common in
test files) the old behaviour consumed the trailing `;` of a multi-line
`:` definition, silently leaving state in compile mode.
Tighten `compliance.rs`: `load_file` now returns a line-failure count,
every prerequisite is asserted against `expected_load_failures(path)`,
and a new `load_file_whole` handles multi-line definitions (`DOES>`
split across lines in `errorreport.fth`) that the per-line loader
cannot stitch. Baselines document known gaps for `core.fr` (nested
`:`, SOURCE/>IN via EVALUATE), `coreexttest.fth` (SAVE-INPUT, `.(`
inside `[...]`), `exceptiontest.fth` (one garbled parse after
CATCH/THROW source stacking), and `toolstest.fth` (37 `\?`-guarded
lines where `SOURCE >IN ! DROP` fails to skip under per-line
`evaluate`). Each entry is a tech-debt ledger item, not an allowlist.
Regression tests: LT32 (the localstest case that silently skipped
before `(LOCAL)` existed), the `0 0 (LOCAL)` sentinel-only no-op, a
multi-line `:` followed by `VARIABLE` after a `\` comment, and a
direct `\` stops-at-newline case.
Incidental: clear two `implicit_clone` clippy lints in the RANDOM
determinism test (`.to_vec()` → `.clone()`).
This commit is contained in:
+206
-12
@@ -119,6 +119,13 @@ enum PendingAction {
|
|||||||
CsRoll(u32),
|
CsRoll(u32),
|
||||||
/// Compile a control-flow operation (from POSTPONE of compile-time keywords).
|
/// Compile a control-flow operation (from POSTPONE of compile-time keywords).
|
||||||
CompileControl(i32),
|
CompileControl(i32),
|
||||||
|
/// Forth 2012 §13.6.1.0086 `(LOCAL)` non-sentinel: declare a local of the
|
||||||
|
/// given name. Name is already ASCII-uppercased by the host primitive.
|
||||||
|
DeclareLocal(String),
|
||||||
|
/// Forth 2012 §13.6.1.0086 `(LOCAL)` sentinel (`0 0 (LOCAL)`): emit the
|
||||||
|
/// init code for locals declared since the last sentinel (or start of
|
||||||
|
/// the current colon definition).
|
||||||
|
DeclareLocalEnd,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control-flow action codes for PendingAction::CompileControl
|
// Control-flow action codes for PendingAction::CompileControl
|
||||||
@@ -254,6 +261,11 @@ pub struct ForthVM<R: Runtime> {
|
|||||||
compiling_locals: Vec<String>,
|
compiling_locals: Vec<String>,
|
||||||
/// Parallel to `compiling_locals`: kind of each local (Int or Float).
|
/// Parallel to `compiling_locals`: kind of each local (Int or Float).
|
||||||
compiling_local_kinds: Vec<LocalKind>,
|
compiling_local_kinds: Vec<LocalKind>,
|
||||||
|
/// Forth 2012 §13.6.1.0086 `(LOCAL)` batch base: index into
|
||||||
|
/// `compiling_locals` where the current `(LOCAL)` batch started.
|
||||||
|
/// `None` means no pending batch. Set on the first `DeclareLocal` of a
|
||||||
|
/// batch, cleared on `DeclareLocalEnd`, reset on `finish_colon_def`.
|
||||||
|
local_batch_base: Option<usize>,
|
||||||
/// Substitution table for SUBSTITUTE/REPLACES (String word set)
|
/// Substitution table for SUBSTITUTE/REPLACES (String word set)
|
||||||
substitutions: Arc<Mutex<HashMap<String, Vec<u8>>>>,
|
substitutions: Arc<Mutex<HashMap<String, Vec<u8>>>>,
|
||||||
/// Search order: list of wordlist IDs (first = top of search order).
|
/// Search order: list of wordlist IDs (first = top of search order).
|
||||||
@@ -283,6 +295,7 @@ struct CompileFrame {
|
|||||||
saw_create_in_def: bool,
|
saw_create_in_def: bool,
|
||||||
compiling_locals: Vec<String>,
|
compiling_locals: Vec<String>,
|
||||||
compiling_local_kinds: Vec<LocalKind>,
|
compiling_local_kinds: Vec<LocalKind>,
|
||||||
|
local_batch_base: Option<usize>,
|
||||||
state: i32,
|
state: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +308,24 @@ pub enum LocalKind {
|
|||||||
Float,
|
Float,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Advance past the next `\n` in `buf`, starting at `from`. Returns the
|
||||||
|
/// byte index of the first character on the next line (or `buf.len()` if
|
||||||
|
/// there's no more newline). Used by the `\` line-comment handler per
|
||||||
|
/// Forth 2012 §6.2.2535 to correctly stop at end-of-line instead of
|
||||||
|
/// end-of-input when the input buffer spans multiple lines.
|
||||||
|
fn skip_to_end_of_line(buf: &str, from: usize) -> usize {
|
||||||
|
let bytes = buf.as_bytes();
|
||||||
|
let mut i = from;
|
||||||
|
while i < bytes.len() {
|
||||||
|
let ch = bytes[i];
|
||||||
|
i += 1;
|
||||||
|
if ch == b'\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i
|
||||||
|
}
|
||||||
|
|
||||||
impl<R: Runtime> ForthVM<R> {
|
impl<R: Runtime> ForthVM<R> {
|
||||||
/// Boot a new Forth VM with all primitives registered.
|
/// Boot a new Forth VM with all primitives registered.
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
@@ -358,6 +389,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
next_block_label: 0,
|
next_block_label: 0,
|
||||||
compiling_locals: Vec::new(),
|
compiling_locals: Vec::new(),
|
||||||
compiling_local_kinds: Vec::new(),
|
compiling_local_kinds: Vec::new(),
|
||||||
|
local_batch_base: None,
|
||||||
substitutions: Arc::new(Mutex::new(HashMap::new())),
|
substitutions: Arc::new(Mutex::new(HashMap::new())),
|
||||||
search_order: Arc::new(Mutex::new(vec![1])),
|
search_order: Arc::new(Mutex::new(vec![1])),
|
||||||
next_wid: Arc::new(Mutex::new(2)),
|
next_wid: Arc::new(Mutex::new(2)),
|
||||||
@@ -367,7 +399,11 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_nanos() as u64)
|
.map(|d| d.as_nanos() as u64)
|
||||||
.unwrap_or(0xDEAD_BEEF_CAFE_BABE);
|
.unwrap_or(0xDEAD_BEEF_CAFE_BABE);
|
||||||
Arc::new(Mutex::new(if seed == 0 { 0xDEAD_BEEF_CAFE_BABE } else { seed }))
|
Arc::new(Mutex::new(if seed == 0 {
|
||||||
|
0xDEAD_BEEF_CAFE_BABE
|
||||||
|
} else {
|
||||||
|
seed
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
compile_frames: Vec::new(),
|
compile_frames: Vec::new(),
|
||||||
compiling_word_addr: 0,
|
compiling_word_addr: 0,
|
||||||
@@ -399,6 +435,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
self.compiling_word_id = None;
|
self.compiling_word_id = None;
|
||||||
self.compiling_locals.clear();
|
self.compiling_locals.clear();
|
||||||
self.compiling_local_kinds.clear();
|
self.compiling_local_kinds.clear();
|
||||||
|
self.local_batch_base = None;
|
||||||
self.compile_frames.clear();
|
self.compile_frames.clear();
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
@@ -750,8 +787,10 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if token_upper == "\\" {
|
if token_upper == "\\" {
|
||||||
// Line comment -- skip rest of input
|
// Forth 2012 §6.2.2535: `\` parses and discards the remainder
|
||||||
self.input_pos = self.input_buffer.len();
|
// of the *line*, not the remainder of the input buffer. Stop
|
||||||
|
// at the first `\n`; fall through to end-of-buffer otherwise.
|
||||||
|
self.input_pos = skip_to_end_of_line(&self.input_buffer, self.input_pos);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -938,7 +977,8 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if token_upper == "\\" {
|
if token_upper == "\\" {
|
||||||
self.input_pos = self.input_buffer.len();
|
// See interpret-mode branch: `\` ends at `\n`, not at `#TIB`.
|
||||||
|
self.input_pos = skip_to_end_of_line(&self.input_buffer, self.input_pos);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1948,6 +1988,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
saw_create_in_def: self.saw_create_in_def,
|
saw_create_in_def: self.saw_create_in_def,
|
||||||
compiling_locals: std::mem::take(&mut self.compiling_locals),
|
compiling_locals: std::mem::take(&mut self.compiling_locals),
|
||||||
compiling_local_kinds: std::mem::take(&mut self.compiling_local_kinds),
|
compiling_local_kinds: std::mem::take(&mut self.compiling_local_kinds),
|
||||||
|
local_batch_base: self.local_batch_base.take(),
|
||||||
state: self.state,
|
state: self.state,
|
||||||
};
|
};
|
||||||
self.compile_frames.push(frame);
|
self.compile_frames.push(frame);
|
||||||
@@ -1993,6 +2034,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
self.saw_create_in_def = frame.saw_create_in_def;
|
self.saw_create_in_def = frame.saw_create_in_def;
|
||||||
self.compiling_locals = frame.compiling_locals;
|
self.compiling_locals = frame.compiling_locals;
|
||||||
self.compiling_local_kinds = frame.compiling_local_kinds;
|
self.compiling_local_kinds = frame.compiling_local_kinds;
|
||||||
|
self.local_batch_base = frame.local_batch_base;
|
||||||
self.state = frame.state;
|
self.state = frame.state;
|
||||||
|
|
||||||
if self.state != 0 {
|
if self.state != 0 {
|
||||||
@@ -2095,6 +2137,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
self.compiling_locals.clear();
|
self.compiling_locals.clear();
|
||||||
self.compiling_local_kinds.clear();
|
self.compiling_local_kinds.clear();
|
||||||
|
self.local_batch_base = None;
|
||||||
|
|
||||||
let name = self
|
let name = self
|
||||||
.compiling_name
|
.compiling_name
|
||||||
@@ -2686,6 +2729,9 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// CS-PICK, CS-ROLL, __CTRL__ for Programming-Tools / POSTPONE of control words
|
// CS-PICK, CS-ROLL, __CTRL__ for Programming-Tools / POSTPONE of control words
|
||||||
self.register_cs_pick_roll()?;
|
self.register_cs_pick_roll()?;
|
||||||
|
|
||||||
|
// (LOCAL) for Forth 2012 §13.6.1.0086 lower-level locals primitive
|
||||||
|
self.register_local_paren()?;
|
||||||
|
|
||||||
// Runtime DOES> patch for double-DOES> support
|
// Runtime DOES> patch for double-DOES> support
|
||||||
self.register_does_patch()?;
|
self.register_does_patch()?;
|
||||||
|
|
||||||
@@ -4229,6 +4275,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let saved_control = std::mem::take(&mut self.control_stack);
|
let saved_control = std::mem::take(&mut self.control_stack);
|
||||||
let saved_locals = std::mem::take(&mut self.compiling_locals);
|
let saved_locals = std::mem::take(&mut self.compiling_locals);
|
||||||
let saved_local_kinds = std::mem::take(&mut self.compiling_local_kinds);
|
let saved_local_kinds = std::mem::take(&mut self.compiling_local_kinds);
|
||||||
|
let saved_local_batch_base = self.local_batch_base.take();
|
||||||
|
|
||||||
self.compiling_ir.clear();
|
self.compiling_ir.clear();
|
||||||
self.compiling_name = Some("_does_action_".to_string());
|
self.compiling_name = Some("_does_action_".to_string());
|
||||||
@@ -4273,6 +4320,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
self.control_stack = saved_control;
|
self.control_stack = saved_control;
|
||||||
self.compiling_locals = saved_locals;
|
self.compiling_locals = saved_locals;
|
||||||
self.compiling_local_kinds = saved_local_kinds;
|
self.compiling_local_kinds = saved_local_kinds;
|
||||||
|
self.local_batch_base = saved_local_batch_base;
|
||||||
|
|
||||||
// Register the defining word as a "does-defining" word.
|
// Register the defining word as a "does-defining" word.
|
||||||
let has_create = self.saw_create_in_def;
|
let has_create = self.saw_create_in_def;
|
||||||
@@ -4738,6 +4786,45 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register `(LOCAL)` per Forth 2012 §13.6.1.0086.
|
||||||
|
///
|
||||||
|
/// Compile-time `( c-addr u -- )`. When `u > 0`, declare a local named by
|
||||||
|
/// the byte slice at `c-addr`/`u`. When `u = 0`, emit the initialization
|
||||||
|
/// code for all locals declared since the last sentinel (the runtime
|
||||||
|
/// `ForthLocalSet`s that pop args from the data stack in reverse
|
||||||
|
/// declaration order).
|
||||||
|
///
|
||||||
|
/// The word is non-immediate: it runs when its containing immediate word
|
||||||
|
/// (typically user-defined `LOCAL` or `END-LOCALS`) executes during the
|
||||||
|
/// outer compilation loop. Because `HostAccess` cannot reach into the
|
||||||
|
/// outer-interpreter compile state directly, the actual mutation is
|
||||||
|
/// deferred via `PendingAction::DeclareLocal` / `DeclareLocalEnd` and
|
||||||
|
/// processed in `handle_pending_actions` once the immediate word returns.
|
||||||
|
fn register_local_paren(&mut self) -> anyhow::Result<()> {
|
||||||
|
let pending = Arc::clone(&self.pending_actions);
|
||||||
|
|
||||||
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
|
// ( c-addr u -- ) — pop both cells.
|
||||||
|
let sp = ctx.get_dsp();
|
||||||
|
let u = ctx.mem_read_i32(sp) as u32;
|
||||||
|
let addr = ctx.mem_read_i32(sp + CELL_SIZE) as u32;
|
||||||
|
ctx.set_dsp(sp + 2 * CELL_SIZE);
|
||||||
|
|
||||||
|
let action = if u == 0 {
|
||||||
|
PendingAction::DeclareLocalEnd
|
||||||
|
} else {
|
||||||
|
let bytes = ctx.mem_read_slice(addr, u as usize);
|
||||||
|
let name = String::from_utf8_lossy(&bytes).to_ascii_uppercase();
|
||||||
|
PendingAction::DeclareLocal(name)
|
||||||
|
};
|
||||||
|
pending.lock().unwrap().push(action);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
self.register_host_primitive("(LOCAL)", false, func)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Register `_does_patch_` as a host function for runtime DOES> patching.
|
/// Register `_does_patch_` as a host function for runtime DOES> patching.
|
||||||
/// ( `does_action_id` -- ) Signals the outer interpreter to patch the most
|
/// ( `does_action_id` -- ) Signals the outer interpreter to patch the most
|
||||||
/// recently `CREATEd` word with a new DOES> action.
|
/// recently `CREATEd` word with a new DOES> action.
|
||||||
@@ -5011,6 +5098,39 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
CTRL_AHEAD => self.compile_ahead()?,
|
CTRL_AHEAD => self.compile_ahead()?,
|
||||||
_ => anyhow::bail!("unknown control code: {code}"),
|
_ => anyhow::bail!("unknown control code: {code}"),
|
||||||
},
|
},
|
||||||
|
// Forth 2012 §13.6.1.0086 `(LOCAL)`: append the named local
|
||||||
|
// to the current compile context. Locals declared via
|
||||||
|
// `(LOCAL)` are int-only per spec (float locals are not
|
||||||
|
// covered by this word).
|
||||||
|
PendingAction::DeclareLocal(name) => {
|
||||||
|
if self.state == 0 {
|
||||||
|
anyhow::bail!("(LOCAL): only valid during compilation");
|
||||||
|
}
|
||||||
|
if self.local_batch_base.is_none() {
|
||||||
|
self.local_batch_base = Some(self.compiling_locals.len());
|
||||||
|
}
|
||||||
|
self.compiling_locals.push(name);
|
||||||
|
self.compiling_local_kinds.push(LocalKind::Int);
|
||||||
|
}
|
||||||
|
// Forth 2012 §13.6.1.0086 `(LOCAL)` sentinel: emit init
|
||||||
|
// code for the batch of locals just declared. Pop the
|
||||||
|
// runtime args from the data stack in reverse declaration
|
||||||
|
// order — consistent with `compile_locals_block` at the
|
||||||
|
// `{: ... :}` flow.
|
||||||
|
PendingAction::DeclareLocalEnd => {
|
||||||
|
if let Some(base) = self.local_batch_base.take() {
|
||||||
|
for slot in (base..self.compiling_locals.len()).rev() {
|
||||||
|
let kind_idx = self.compiling_local_kinds[0..slot]
|
||||||
|
.iter()
|
||||||
|
.filter(|k| **k == LocalKind::Int)
|
||||||
|
.count() as u32;
|
||||||
|
self.push_ir(IrOp::ForthLocalSet(kind_idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No-op if no batch is pending — spec-permissible for
|
||||||
|
// a user that calls `0 0 (LOCAL)` at the top of a
|
||||||
|
// definition before declaring anything.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -5088,11 +5208,24 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
/// Register `\` as an immediate host function that sets >IN to end of input.
|
/// Register `\` as an immediate host function that sets >IN to end of input.
|
||||||
fn register_backslash(&mut self) -> anyhow::Result<()> {
|
fn register_backslash(&mut self) -> anyhow::Result<()> {
|
||||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
// Read #TIB (input buffer length)
|
// Forth 2012 §6.2.2535 `\`: "Parse and discard the remainder of
|
||||||
|
// the parse area." The parse area extends to the end of the
|
||||||
|
// current **line**, not the end of the input buffer. Scan from
|
||||||
|
// the current `>IN` forward for the first `\n`, and set `>IN`
|
||||||
|
// to the position after it. If there's no newline, stop at
|
||||||
|
// `#TIB` (end of buffer), matching the single-line case.
|
||||||
let b: [u8; 4] = ctx.mem_read_i32(SYSVAR_NUM_TIB as u32).to_le_bytes();
|
let b: [u8; 4] = ctx.mem_read_i32(SYSVAR_NUM_TIB as u32).to_le_bytes();
|
||||||
let num_tib = u32::from_le_bytes(b);
|
let num_tib = u32::from_le_bytes(b);
|
||||||
// Set >IN to end of input
|
let b: [u8; 4] = ctx.mem_read_i32(SYSVAR_TO_IN as u32).to_le_bytes();
|
||||||
ctx.mem_write_i32(SYSVAR_TO_IN as u32, num_tib as i32);
|
let mut to_in = u32::from_le_bytes(b);
|
||||||
|
while to_in < num_tib {
|
||||||
|
let ch = ctx.mem_read_u8(INPUT_BUFFER_BASE + to_in);
|
||||||
|
to_in += 1;
|
||||||
|
if ch == b'\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.mem_write_i32(SYSVAR_TO_IN as u32, to_in as i32);
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -5300,7 +5433,11 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let seed = ctx.mem_read_i32(sp as u32) as u32 as u64;
|
let seed = ctx.mem_read_i32(sp as u32) as u32 as u64;
|
||||||
ctx.set_dsp(sp + CELL_SIZE);
|
ctx.set_dsp(sp + CELL_SIZE);
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
*s = if seed == 0 { 0xDEAD_BEEF_CAFE_BABE } else { seed };
|
*s = if seed == 0 {
|
||||||
|
0xDEAD_BEEF_CAFE_BABE
|
||||||
|
} else {
|
||||||
|
seed
|
||||||
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
self.register_host_primitive("RND-SEED", false, func)?;
|
self.register_host_primitive("RND-SEED", false, func)?;
|
||||||
@@ -7914,6 +8051,56 @@ mod tests {
|
|||||||
assert_eq!(vm.data_stack(), vec![7]);
|
assert_eq!(vm.data_stack(), vec![7]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_primitive_lt32() {
|
||||||
|
// Forth 2012 §13.6.1.0086 `(LOCAL)` — replica of LT32 from
|
||||||
|
// localstest.fth line 118-120 (the test that was silently skipped
|
||||||
|
// before `(LOCAL)` was implemented).
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": LOCAL BL WORD COUNT (LOCAL) ; IMMEDIATE")
|
||||||
|
.unwrap();
|
||||||
|
vm.evaluate(": END-LOCALS 0 0 (LOCAL) ; IMMEDIATE").unwrap();
|
||||||
|
vm.evaluate(": LT32 LOCAL A LOCAL B LOCAL C END-LOCALS A B C ;")
|
||||||
|
.unwrap();
|
||||||
|
vm.evaluate("61 62 63 LT32").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![63, 62, 61]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_colon_then_variable() {
|
||||||
|
// Regression: combined `:` def across newlines must leave state at
|
||||||
|
// interpret afterwards. Earlier, WAFER's `\` (backslash comment)
|
||||||
|
// consumed to `#TIB` instead of the next `\n`, so multi-line chunks
|
||||||
|
// lost the closing `;` inside a comment and left state in compile
|
||||||
|
// mode. The symptom was a later `VARIABLE X 0 X !` erroring on
|
||||||
|
// `unknown word: X`, because the outer `:` never actually closed.
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": EMPTY-STACK\n DEPTH ?DUP IF DUP 0< IF NEGATE 0 DO 0 LOOP ELSE 0 DO DROP LOOP THEN THEN ;").unwrap();
|
||||||
|
vm.evaluate("VARIABLE #ERRORS 0 #ERRORS !").unwrap();
|
||||||
|
vm.evaluate("#ERRORS @").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backslash_stops_at_newline() {
|
||||||
|
// Forth 2012 §6.2.2535 `\`: parse-and-discard ends at end-of-line,
|
||||||
|
// not end of input buffer. Multi-line input must survive a `\`
|
||||||
|
// comment on a prior line.
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate("\\ comment line\n42").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![42]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_local_primitive_end_sentinel_only() {
|
||||||
|
// `0 0 (LOCAL)` with no prior names must be a harmless no-op.
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": END-LOCALS 0 0 (LOCAL) ; IMMEDIATE").unwrap();
|
||||||
|
vm.evaluate(": T END-LOCALS 42 ;").unwrap();
|
||||||
|
vm.evaluate("T").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![42]);
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// Quotations: [: ... ;]
|
// Quotations: [: ... ;]
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -7999,11 +8186,11 @@ mod tests {
|
|||||||
fn test_random_deterministic_after_seed() {
|
fn test_random_deterministic_after_seed() {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
vm.evaluate("42 RND-SEED RANDOM RANDOM RANDOM").unwrap();
|
vm.evaluate("42 RND-SEED RANDOM RANDOM RANDOM").unwrap();
|
||||||
let first = vm.data_stack().to_vec();
|
let first = vm.data_stack().clone();
|
||||||
|
|
||||||
let mut vm2 = ForthVM::<NativeRuntime>::new().unwrap();
|
let mut vm2 = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
vm2.evaluate("42 RND-SEED RANDOM RANDOM RANDOM").unwrap();
|
vm2.evaluate("42 RND-SEED RANDOM RANDOM RANDOM").unwrap();
|
||||||
let second = vm2.data_stack().to_vec();
|
let second = vm2.data_stack().clone();
|
||||||
|
|
||||||
assert_eq!(first, second, "same seed must produce same sequence");
|
assert_eq!(first, second, "same seed must produce same sequence");
|
||||||
assert_eq!(first.len(), 3);
|
assert_eq!(first.len(), 3);
|
||||||
@@ -8021,7 +8208,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
// xorshift64's low-32 sequence repeats after a long period; 1000 pulls
|
// xorshift64's low-32 sequence repeats after a long period; 1000 pulls
|
||||||
// should hit at least 900 unique cells.
|
// should hit at least 900 unique cells.
|
||||||
assert!(seen.len() >= 900, "only {} distinct out of 1000", seen.len());
|
assert!(
|
||||||
|
seen.len() >= 900,
|
||||||
|
"only {} distinct out of 1000",
|
||||||
|
seen.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -8030,7 +8221,10 @@ mod tests {
|
|||||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
vm.evaluate("0 RND-SEED RANDOM RANDOM").unwrap();
|
vm.evaluate("0 RND-SEED RANDOM RANDOM").unwrap();
|
||||||
let stack = vm.data_stack();
|
let stack = vm.data_stack();
|
||||||
assert!(stack[0] != 0 || stack[1] != 0, "seed-0 must not freeze the stream");
|
assert!(
|
||||||
|
stack[0] != 0 || stack[1] != 0,
|
||||||
|
"seed-0 must not freeze the stream"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|||||||
+180
-24
@@ -13,41 +13,165 @@ const SUITE_DIR: &str = concat!(
|
|||||||
"/../../tests/forth2012-test-suite/src"
|
"/../../tests/forth2012-test-suite/src"
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Load a file and evaluate it line by line, ignoring errors on individual lines.
|
/// Load a file line-by-line, returning the number of lines that raised an
|
||||||
fn load_file(vm: &mut ForthVM<NativeRuntime>, path: &str) {
|
/// `evaluate` error. Each failing line is printed (visible under
|
||||||
|
/// `cargo test -- --nocapture`) so failures can be triaged without a
|
||||||
|
/// debugger.
|
||||||
|
///
|
||||||
|
/// Historically this helper discarded errors silently, which caused tests
|
||||||
|
/// like LT32 in `localstest.fth` (compile errors from unknown words such
|
||||||
|
/// as `(LOCAL)` before it was implemented) to vanish — the T{ }T error
|
||||||
|
/// counter was never incremented because the `:` definition never ran.
|
||||||
|
/// Returning the count surfaces silent skips as real failures.
|
||||||
|
///
|
||||||
|
/// **Note on multi-line definitions.** WAFER's DOES> handler collects
|
||||||
|
/// the does-body to `;` via `next_token()` within a *single* `evaluate`
|
||||||
|
/// call and treats end-of-input as end-of-body. Files with a `DOES>`
|
||||||
|
/// split across lines (e.g. `errorreport.fth`) therefore cannot be
|
||||||
|
/// loaded line-by-line; use [`load_file_whole`] for those.
|
||||||
|
fn load_file(vm: &mut ForthVM<NativeRuntime>, path: &str) -> u32 {
|
||||||
let source = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path}"));
|
let source = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path}"));
|
||||||
for line in source.lines() {
|
let mut fails = 0u32;
|
||||||
let _ = vm.evaluate(line);
|
for (lineno, line) in source.lines().enumerate() {
|
||||||
|
if let Err(e) = vm.evaluate(line) {
|
||||||
|
fails += 1;
|
||||||
|
eprintln!("{path}:{}: {e}\n line: {line}", lineno + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
vm.take_output(); // discard output
|
vm.take_output(); // discard output
|
||||||
|
fails
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a file as a single `evaluate` call (not line-by-line). Required
|
||||||
|
/// for files with multi-line definitions that WAFER's per-line handlers
|
||||||
|
/// can't stitch across calls (notably `: X ... DOES> ... ;` spanning
|
||||||
|
/// lines — see [`load_file`] note).
|
||||||
|
///
|
||||||
|
/// Returns `1` on any failure, `0` on success, so the caller can apply
|
||||||
|
/// baselines the same way as [`load_file`].
|
||||||
|
fn load_file_whole(vm: &mut ForthVM<NativeRuntime>, path: &str) -> u32 {
|
||||||
|
let source = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path}"));
|
||||||
|
let fails = match vm.evaluate(&source) {
|
||||||
|
Ok(()) => 0,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{path}: {e}");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
vm.take_output();
|
||||||
|
fails
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Baseline of *known* line-level failures per prerequisite file. The runner
|
||||||
|
/// asserts `load_fails == expected_load_failures(path)`, so any regression
|
||||||
|
/// above (or silently-fixed case below) the baseline is caught.
|
||||||
|
///
|
||||||
|
/// Baselines are not an allowlist to paper over bugs — they are an explicit
|
||||||
|
/// tech-debt ledger. Each non-zero entry here is a bug that should be fixed
|
||||||
|
/// and the baseline lowered to zero. See the in-tree follow-up tasks.
|
||||||
|
fn expected_load_failures(path: &str) -> u32 {
|
||||||
|
// core.fr exercises two constructs WAFER does not yet support:
|
||||||
|
// 1. Nested colon definitions (`: NOP : POSTPONE ; ;` at line 751,
|
||||||
|
// defining NOP, NOP1, NOP2 — four silent lines).
|
||||||
|
// 2. `SOURCE`/`>IN` round-trip through `EVALUATE` at line 797
|
||||||
|
// (GS1 definition) — one line.
|
||||||
|
// Total: 5. Fix these and drop the baseline to 0.
|
||||||
|
if path.ends_with("/core.fr") {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
// coreexttest.fth uses two Core-Extension features WAFER lacks:
|
||||||
|
// 1. SAVE-INPUT / RESTORE-INPUT at line 548 — not implemented.
|
||||||
|
// 2. `.(` inside `[ ... ]` brackets at line 559 — `.(` isn't
|
||||||
|
// handled by `compile_token`'s `[ ... ]` interpret-mode path,
|
||||||
|
// so `First message via .(` tokens leak to the compiler as
|
||||||
|
// undefined words.
|
||||||
|
// Total: 2. Fix these and drop the baseline to 0.
|
||||||
|
if path.ends_with("/coreexttest.fth") {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
// exceptiontest.fth line 95 fails with a garbled parse ("unknown word"
|
||||||
|
// over non-ASCII bytes): WAFER's parser reads past a prior test's
|
||||||
|
// scratch region after the preceding `C6` / `T9` frame exercises
|
||||||
|
// CATCH/THROW source stacking. Root cause not yet diagnosed; baseline
|
||||||
|
// until fixed.
|
||||||
|
if path.ends_with("/exceptiontest.fth") {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// toolstest.fth uses the `\?` conditional-skip idiom defined in
|
||||||
|
// utilities.fth:37 as `: \? (\?) @ IF EXIT THEN SOURCE >IN ! DROP ;
|
||||||
|
// IMMEDIATE`. Under WAFER's per-line `evaluate` loader, the
|
||||||
|
// `SOURCE >IN ! DROP` path does not consume the remainder of the
|
||||||
|
// current line correctly, so 37 `\?`-guarded lines inside the
|
||||||
|
// TRAVERSE-WORDLIST / NAME>COMPILE / NAME>INTERPRET blocks leak as
|
||||||
|
// unknown-word errors. Fix the SOURCE/`>IN` interaction with
|
||||||
|
// line-mode input and drop this to 0.
|
||||||
|
if path.ends_with("/toolstest.fth") {
|
||||||
|
return 37;
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert a file loaded with exactly its baseline number of line-level
|
||||||
|
/// failures. Used for prerequisites; keeps the runner tight without
|
||||||
|
/// blocking the whole suite on known gaps.
|
||||||
|
fn assert_load_fails_within_baseline(path: &str, fails: u32) {
|
||||||
|
let expected = expected_load_failures(path);
|
||||||
|
assert_eq!(
|
||||||
|
fails, expected,
|
||||||
|
"{path} had {fails} line-level failures (expected baseline: {expected})"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Boot a WAFER VM with full prerequisites loaded.
|
/// Boot a WAFER VM with full prerequisites loaded.
|
||||||
|
///
|
||||||
|
/// Every prerequisite file must load with zero line-level errors. Any
|
||||||
|
/// regression here points to a missing primitive or a parser bug and must
|
||||||
|
/// be fixed, not silently tolerated.
|
||||||
fn boot_with_prerequisites() -> ForthVM<NativeRuntime> {
|
fn boot_with_prerequisites() -> ForthVM<NativeRuntime> {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
|
|
||||||
// Load test framework
|
// Load test framework
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
// Load core tests (prerequisite)
|
// Load core tests (prerequisite)
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
// Switch to decimal and load utilities
|
// Switch to decimal and load utilities
|
||||||
let _ = vm.evaluate("DECIMAL");
|
let _ = vm.evaluate("DECIMAL");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/utilities.fth"));
|
let util_path = format!("{SUITE_DIR}/utilities.fth");
|
||||||
|
let f3 = load_file(&mut vm, &util_path);
|
||||||
|
assert_load_fails_within_baseline(&util_path, f3);
|
||||||
|
// errorreport.fth defines SET-ERROR-COUNT and the per-wordset counter
|
||||||
|
// accessors (CORE-ERRORS, STRING-ERRORS, LOCALS-ERRORS, ...). Every
|
||||||
|
// suite's final `X-ERRORS SET-ERROR-COUNT` line depends on this file,
|
||||||
|
// and silently errored before the runner was tightened.
|
||||||
|
let errorreport_path = format!("{SUITE_DIR}/errorreport.fth");
|
||||||
|
let f_err = load_file_whole(&mut vm, &errorreport_path);
|
||||||
|
assert_load_fails_within_baseline(&errorreport_path, f_err);
|
||||||
// Load core extensions
|
// Load core extensions
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/coreexttest.fth"));
|
let ext_path = format!("{SUITE_DIR}/coreexttest.fth");
|
||||||
|
let f4 = load_file(&mut vm, &ext_path);
|
||||||
|
assert_load_fails_within_baseline(&ext_path, f4);
|
||||||
|
|
||||||
vm
|
vm
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a test suite file and return the #ERRORS count.
|
/// Run a test suite file and return the *total* error count:
|
||||||
|
/// `#ERRORS` from the Forth test framework plus any lines where
|
||||||
|
/// `vm.evaluate` itself failed (e.g. unknown word in a `:` definition
|
||||||
|
/// outside `T{ }T`, which the framework cannot catch).
|
||||||
fn run_suite(vm: &mut ForthVM<NativeRuntime>, test_file: &str) -> u32 {
|
fn run_suite(vm: &mut ForthVM<NativeRuntime>, test_file: &str) -> u32 {
|
||||||
// Reset error counter
|
// Reset error counter
|
||||||
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
|
|
||||||
// Load the test file
|
// Load the test file
|
||||||
load_file(vm, &format!("{SUITE_DIR}/{test_file}"));
|
let file_path = format!("{SUITE_DIR}/{test_file}");
|
||||||
|
let load_fails = load_file(vm, &file_path);
|
||||||
|
assert_load_fails_within_baseline(&file_path, load_fails);
|
||||||
|
|
||||||
// Read error count -- try multiple approaches to be robust
|
// Read error count -- try multiple approaches to be robust
|
||||||
let _ = vm.evaluate("DECIMAL");
|
let _ = vm.evaluate("DECIMAL");
|
||||||
@@ -76,8 +200,12 @@ fn run_suite(vm: &mut ForthVM<NativeRuntime>, test_file: &str) -> u32 {
|
|||||||
#[test]
|
#[test]
|
||||||
fn compliance_core() {
|
fn compliance_core() {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
|
|
||||||
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
||||||
let errors = vm.data_stack().first().copied().unwrap_or(-1);
|
let errors = vm.data_stack().first().copied().unwrap_or(-1);
|
||||||
@@ -96,17 +224,31 @@ fn compliance_core_ext() {
|
|||||||
// Core Extensions are loaded as part of prerequisites.
|
// Core Extensions are loaded as part of prerequisites.
|
||||||
// Run from scratch to get a clean error count.
|
// Run from scratch to get a clean error count.
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
let _ = vm.evaluate("DECIMAL");
|
let _ = vm.evaluate("DECIMAL");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/utilities.fth"));
|
let util_path = format!("{SUITE_DIR}/utilities.fth");
|
||||||
|
let f3 = load_file(&mut vm, &util_path);
|
||||||
|
assert_load_fails_within_baseline(&util_path, f3);
|
||||||
|
let errorreport_path = format!("{SUITE_DIR}/errorreport.fth");
|
||||||
|
let f_err = load_file_whole(&mut vm, &errorreport_path);
|
||||||
|
assert_load_fails_within_baseline(&errorreport_path, f_err);
|
||||||
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/coreexttest.fth"));
|
let ext_path = format!("{SUITE_DIR}/coreexttest.fth");
|
||||||
|
let load_fails = load_file(&mut vm, &ext_path);
|
||||||
|
assert_load_fails_within_baseline(&ext_path, load_fails);
|
||||||
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
||||||
let errors = vm.data_stack().first().copied().unwrap_or(-1) as u32;
|
let framework_errors = vm.data_stack().first().copied().unwrap_or(-1) as u32;
|
||||||
assert_eq!(errors, 0, "Core Extensions: {errors} test failures");
|
assert_eq!(
|
||||||
|
framework_errors, 0,
|
||||||
|
"Core Extensions: {framework_errors} framework test failures"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -164,17 +306,31 @@ fn compliance_string() {
|
|||||||
// Run from scratch -- the stringtest includes CoreExt tests that
|
// Run from scratch -- the stringtest includes CoreExt tests that
|
||||||
// cascade failures when run on top of an already-loaded CoreExt suite.
|
// cascade failures when run on top of an already-loaded CoreExt suite.
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
let _ = vm.evaluate("DECIMAL");
|
let _ = vm.evaluate("DECIMAL");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/utilities.fth"));
|
let util_path = format!("{SUITE_DIR}/utilities.fth");
|
||||||
|
let f3 = load_file(&mut vm, &util_path);
|
||||||
|
assert_load_fails_within_baseline(&util_path, f3);
|
||||||
|
let errorreport_path = format!("{SUITE_DIR}/errorreport.fth");
|
||||||
|
let f_err = load_file_whole(&mut vm, &errorreport_path);
|
||||||
|
assert_load_fails_within_baseline(&errorreport_path, f_err);
|
||||||
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
|
||||||
vm.take_output();
|
vm.take_output();
|
||||||
load_file(&mut vm, &format!("{SUITE_DIR}/stringtest.fth"));
|
let str_path = format!("{SUITE_DIR}/stringtest.fth");
|
||||||
|
let load_fails = load_file(&mut vm, &str_path);
|
||||||
|
assert_load_fails_within_baseline(&str_path, load_fails);
|
||||||
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
||||||
let errors = vm.data_stack().first().copied().unwrap_or(-1) as u32;
|
let framework_errors = vm.data_stack().first().copied().unwrap_or(-1) as u32;
|
||||||
assert_eq!(errors, 0, "String: {errors} test failures");
|
assert_eq!(
|
||||||
|
framework_errors, 0,
|
||||||
|
"String: {framework_errors} framework test failures"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user