feat(core): REMEMBER + EMPTY/GILD; marker rollback covers namespace state
- REMEMBER <name>: SwiftForth-style re-runnable marker -- restores to just AFTER its own definition and survives execution. The edit-reload-test loop: REMEMBER fresh ... fresh ... fresh. - EMPTY rolls back to the boot dictionary; GILD re-baselines EMPTY to the current state. Baseline captured at VM construction. - MarkerState now also snapshots search order, wid allocation, compilation wordlist, REPLACES table, and ABORT" texts; restore discards marker entries newer than the snapshot (was: newer than the executing marker id only). - Shared snapshot_marker_state/apply_marker_state used by MARKER, REMEMBER, EMPTY, and GILD. Known ambiguity (standard-conformant): a DEFER defined before a marker but retargeted at a word defined after it dangles after rollback; stale function-table slots are name-unreachable and get overwritten by later definitions.
This commit is contained in:
+139
-28
@@ -162,6 +162,7 @@ struct DoesDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Saved VM state for a MARKER word.
|
/// Saved VM state for a MARKER word.
|
||||||
|
#[derive(Clone)]
|
||||||
struct MarkerState {
|
struct MarkerState {
|
||||||
dict_state: DictionaryState,
|
dict_state: DictionaryState,
|
||||||
user_here: u32,
|
user_here: u32,
|
||||||
@@ -172,6 +173,16 @@ struct MarkerState {
|
|||||||
host_word_names: HashMap<WordId, String>,
|
host_word_names: HashMap<WordId, String>,
|
||||||
two_value_words: std::collections::HashSet<u32>,
|
two_value_words: std::collections::HashSet<u32>,
|
||||||
fvalue_words: std::collections::HashSet<u32>,
|
fvalue_words: std::collections::HashSet<u32>,
|
||||||
|
// Namespace + text state: search order, wordlist allocation,
|
||||||
|
// REPLACES table, ABORT" texts
|
||||||
|
search_order: Vec<u32>,
|
||||||
|
next_wid: u32,
|
||||||
|
current_wid: u32,
|
||||||
|
substitutions: HashMap<String, Vec<u8>>,
|
||||||
|
abort_messages_len: usize,
|
||||||
|
// REMEMBER-style marker: state was saved just AFTER the marker word
|
||||||
|
// was defined, and the entry survives its own execution (re-runnable)
|
||||||
|
keep: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -257,6 +268,8 @@ pub struct ForthVM<R: Runtime> {
|
|||||||
recording_toplevel: bool,
|
recording_toplevel: bool,
|
||||||
/// Saved states for MARKER words: `marker_id` -> `MarkerState`
|
/// Saved states for MARKER words: `marker_id` -> `MarkerState`
|
||||||
marker_states: HashMap<u32, MarkerState>,
|
marker_states: HashMap<u32, MarkerState>,
|
||||||
|
/// EMPTY's rollback target: boot state, or wherever GILD re-baselined.
|
||||||
|
gild_state: Option<Box<MarkerState>>,
|
||||||
/// Pending MARKER restore: after a marker word executes, restore this state
|
/// Pending MARKER restore: after a marker word executes, restore this state
|
||||||
pending_marker_restore: Arc<Mutex<Option<u32>>>,
|
pending_marker_restore: Arc<Mutex<Option<u32>>>,
|
||||||
/// Conditional compilation skip depth: >0 means we're skipping tokens for [IF]/[ELSE]
|
/// Conditional compilation skip depth: >0 means we're skipping tokens for [IF]/[ELSE]
|
||||||
@@ -455,6 +468,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
toplevel_ir: Vec::new(),
|
toplevel_ir: Vec::new(),
|
||||||
recording_toplevel: false,
|
recording_toplevel: false,
|
||||||
marker_states: HashMap::new(),
|
marker_states: HashMap::new(),
|
||||||
|
gild_state: None,
|
||||||
pending_marker_restore: Arc::new(Mutex::new(None)),
|
pending_marker_restore: Arc::new(Mutex::new(None)),
|
||||||
conditional_skip_depth: 0,
|
conditional_skip_depth: 0,
|
||||||
next_block_label: 0,
|
next_block_label: 0,
|
||||||
@@ -488,6 +502,9 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
vm.register_primitives()?;
|
vm.register_primitives()?;
|
||||||
|
|
||||||
|
// Boot state is the default EMPTY target (until GILD re-baselines)
|
||||||
|
vm.gild_state = Some(Box::new(vm.snapshot_marker_state(true)));
|
||||||
|
|
||||||
Ok(vm)
|
Ok(vm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -932,7 +949,18 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
"BUFFER:" => return self.define_buffer(),
|
"BUFFER:" => return self.define_buffer(),
|
||||||
"MARKER" => return self.define_marker(),
|
"MARKER" => return self.define_marker(false),
|
||||||
|
"REMEMBER" => return self.define_marker(true),
|
||||||
|
"GILD" => {
|
||||||
|
self.gild_state = Some(Box::new(self.snapshot_marker_state(true)));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
"EMPTY" => {
|
||||||
|
if let Some(state) = self.gild_state.clone() {
|
||||||
|
self.apply_marker_state(*state);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
"2CONSTANT" => return self.define_2constant(),
|
"2CONSTANT" => return self.define_2constant(),
|
||||||
"2VARIABLE" => return self.define_2variable(),
|
"2VARIABLE" => return self.define_2variable(),
|
||||||
"2VALUE" => return self.define_2value(),
|
"2VALUE" => return self.define_2value(),
|
||||||
@@ -3430,13 +3458,9 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
/// MARKER <name> -- create a marker that restores dictionary state.
|
/// MARKER <name> -- create a marker that restores dictionary state.
|
||||||
/// Saves a snapshot of the VM; when the marker word is executed, restores it.
|
/// Saves a snapshot of the VM; when the marker word is executed, restores it.
|
||||||
fn define_marker(&mut self) -> anyhow::Result<()> {
|
/// Snapshot everything a marker rollback restores.
|
||||||
let name = self
|
fn snapshot_marker_state(&self, keep: bool) -> MarkerState {
|
||||||
.next_token()
|
MarkerState {
|
||||||
.ok_or_else(|| anyhow::anyhow!("MARKER: expected name"))?;
|
|
||||||
|
|
||||||
// Save state BEFORE creating the marker word itself
|
|
||||||
let saved = MarkerState {
|
|
||||||
dict_state: self.dictionary.save_state(),
|
dict_state: self.dictionary.save_state(),
|
||||||
user_here: self.user_here,
|
user_here: self.user_here,
|
||||||
next_table_index: self.next_table_index,
|
next_table_index: self.next_table_index,
|
||||||
@@ -3446,16 +3470,58 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
host_word_names: self.host_word_names.clone(),
|
host_word_names: self.host_word_names.clone(),
|
||||||
two_value_words: self.two_value_words.clone(),
|
two_value_words: self.two_value_words.clone(),
|
||||||
fvalue_words: self.fvalue_words.clone(),
|
fvalue_words: self.fvalue_words.clone(),
|
||||||
};
|
search_order: self.search_order.lock().unwrap().clone(),
|
||||||
|
next_wid: *self.next_wid.lock().unwrap(),
|
||||||
|
current_wid: self.dictionary.current_wid(),
|
||||||
|
substitutions: self.substitutions.lock().unwrap().clone(),
|
||||||
|
abort_messages_len: self.abort_messages.lock().unwrap().len(),
|
||||||
|
keep,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Roll the VM back to a marker snapshot. Also discards marker
|
||||||
|
/// entries created after the snapshot (their words no longer exist).
|
||||||
|
fn apply_marker_state(&mut self, state: MarkerState) {
|
||||||
|
self.dictionary.restore_state(state.dict_state);
|
||||||
|
self.dictionary.set_current_wid(state.current_wid);
|
||||||
|
self.user_here = state.user_here;
|
||||||
|
self.next_table_index = state.next_table_index;
|
||||||
|
self.word_pfa_map = state.word_pfa_map;
|
||||||
|
self.ir_bodies = state.ir_bodies;
|
||||||
|
self.does_definitions = state.does_definitions;
|
||||||
|
self.host_word_names = state.host_word_names;
|
||||||
|
self.two_value_words = state.two_value_words;
|
||||||
|
self.fvalue_words = state.fvalue_words;
|
||||||
|
*self.search_order.lock().unwrap() = state.search_order;
|
||||||
|
*self.next_wid.lock().unwrap() = state.next_wid;
|
||||||
|
*self.substitutions.lock().unwrap() = state.substitutions;
|
||||||
|
self.abort_messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.truncate(state.abort_messages_len);
|
||||||
|
self.marker_states
|
||||||
|
.retain(|&k, _| k < state.next_table_index);
|
||||||
|
self.sync_here_cell();
|
||||||
|
self.rebuild_word_lookup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MARKER <name> (keep = false): rollback to just BEFORE the marker
|
||||||
|
/// was defined; the marker removes itself.
|
||||||
|
/// REMEMBER <name> (keep = true): rollback to just AFTER the marker
|
||||||
|
/// was defined; the marker survives and can be re-run.
|
||||||
|
fn define_marker(&mut self, keep: bool) -> anyhow::Result<()> {
|
||||||
|
let name = self
|
||||||
|
.next_token()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("MARKER: expected name"))?;
|
||||||
|
|
||||||
|
// MARKER saves state before its own word exists
|
||||||
|
let before = (!keep).then(|| self.snapshot_marker_state(false));
|
||||||
|
|
||||||
let word_id = self
|
let word_id = self
|
||||||
.dictionary
|
.dictionary
|
||||||
.create(&name, false)
|
.create(&name, false)
|
||||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
|
||||||
// Store the saved state keyed by word_id
|
|
||||||
self.marker_states.insert(word_id.0, saved);
|
|
||||||
|
|
||||||
// Compile the marker word: push marker_id, call _MARKER_RESTORE_
|
// Compile the marker word: push marker_id, call _MARKER_RESTORE_
|
||||||
let restore_id = self
|
let restore_id = self
|
||||||
.dictionary
|
.dictionary
|
||||||
@@ -3476,6 +3542,10 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
self.dictionary.reveal();
|
self.dictionary.reveal();
|
||||||
self.next_table_index = self.next_table_index.max(word_id.0 + 1);
|
self.next_table_index = self.next_table_index.max(word_id.0 + 1);
|
||||||
|
|
||||||
|
// REMEMBER saves state after its own word is fully in place
|
||||||
|
let saved = before.unwrap_or_else(|| self.snapshot_marker_state(true));
|
||||||
|
self.marker_states.insert(word_id.0, saved);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5379,22 +5449,18 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let mut p = self.pending_marker_restore.lock().unwrap();
|
let mut p = self.pending_marker_restore.lock().unwrap();
|
||||||
p.take()
|
p.take()
|
||||||
};
|
};
|
||||||
if let Some(id) = marker_id
|
if let Some(id) = marker_id {
|
||||||
&& let Some(state) = self.marker_states.remove(&id)
|
// REMEMBER-style entries survive their own execution; MARKER
|
||||||
{
|
// entries remove themselves. apply_marker_state discards all
|
||||||
self.dictionary.restore_state(state.dict_state);
|
// entries newer than the snapshot either way.
|
||||||
self.user_here = state.user_here;
|
let state = match self.marker_states.get(&id) {
|
||||||
self.next_table_index = state.next_table_index;
|
Some(s) if s.keep => Some(s.clone()),
|
||||||
self.word_pfa_map = state.word_pfa_map;
|
Some(_) => self.marker_states.remove(&id),
|
||||||
self.ir_bodies = state.ir_bodies;
|
None => None,
|
||||||
self.does_definitions = state.does_definitions;
|
};
|
||||||
self.host_word_names = state.host_word_names;
|
if let Some(state) = state {
|
||||||
self.two_value_words = state.two_value_words;
|
self.apply_marker_state(state);
|
||||||
self.fvalue_words = state.fvalue_words;
|
}
|
||||||
self.sync_here_cell();
|
|
||||||
self.rebuild_word_lookup();
|
|
||||||
// Remove any marker states that were created after this one
|
|
||||||
self.marker_states.retain(|&k, _| k < id);
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -9272,6 +9338,51 @@ mod tests {
|
|||||||
vm.evaluate("MARKER MARK1").unwrap();
|
vm.evaluate("MARKER MARK1").unwrap();
|
||||||
// MARK1 should exist and be callable
|
// MARK1 should exist and be callable
|
||||||
vm.evaluate("MARK1").unwrap();
|
vm.evaluate("MARK1").unwrap();
|
||||||
|
// ...and remove itself when executed
|
||||||
|
assert!(vm.evaluate("MARK1").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remember_survives_and_reruns() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate("REMEMBER RST : AA 1 ;").unwrap();
|
||||||
|
vm.evaluate("RST").unwrap();
|
||||||
|
assert!(vm.evaluate("AA").is_err(), "AA must be pruned");
|
||||||
|
// RST survived its own execution and is re-runnable
|
||||||
|
vm.evaluate(": BB 2 ; RST").unwrap();
|
||||||
|
assert!(vm.evaluate("BB").is_err(), "BB must be pruned");
|
||||||
|
// VM stays fully usable afterwards
|
||||||
|
vm.evaluate(": CC 3 ; CC").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remember_restores_wordlist_allocation() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate("REMEMBER RST WORDLIST DROP RST WORDLIST")
|
||||||
|
.unwrap();
|
||||||
|
// wid allocation rolled back: the post-RST WORDLIST reuses wid 2
|
||||||
|
assert_eq!(vm.data_stack(), vec![2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_returns_to_boot() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": ZZ 9 ;").unwrap();
|
||||||
|
vm.evaluate("EMPTY").unwrap();
|
||||||
|
assert!(vm.evaluate("ZZ").is_err(), "ZZ must be pruned");
|
||||||
|
vm.evaluate("1 2 + .").unwrap();
|
||||||
|
assert_eq!(vm.take_output(), "3 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gild_rebaselines_empty() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": KEEP1 1 ; GILD : GONE1 2 ;").unwrap();
|
||||||
|
vm.evaluate("EMPTY").unwrap();
|
||||||
|
assert!(vm.evaluate("GONE1").is_err(), "GONE1 must be pruned");
|
||||||
|
vm.evaluate("KEEP1 .").unwrap();
|
||||||
|
assert_eq!(vm.take_output(), "1 ");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user