Skip to main content

runmat_vm/runtime/
workspace.rs

1use runmat_builtins::Value;
2use runmat_thread_local::runmat_thread_local;
3use std::cell::RefCell;
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone)]
7enum SlotLifecycle {
8    Assigned(String),
9    Unassigned(String),
10}
11
12impl SlotLifecycle {
13    fn name(&self) -> &str {
14        match self {
15            SlotLifecycle::Assigned(name) | SlotLifecycle::Unassigned(name) => name,
16        }
17    }
18
19    fn is_assigned(&self) -> bool {
20        matches!(self, SlotLifecycle::Assigned(_))
21    }
22}
23
24struct WorkspaceState {
25    names: HashMap<String, usize>,
26    assigned: HashSet<String>,
27    assigned_names_this_execution: HashSet<String>,
28    assigned_ids_this_execution: HashSet<usize>,
29    removed_slots_this_execution: HashMap<usize, String>,
30    slot_lifecycle: HashMap<usize, SlotLifecycle>,
31    data_ptr: *const Value,
32    len: usize,
33}
34
35struct WorkspaceFrame {
36    state: WorkspaceState,
37    vars_ptr: *mut Vec<Value>,
38    vars_snapshot: Vec<Value>,
39    publish_on_drop: bool,
40}
41
42pub type WorkspaceSnapshot = (HashMap<String, usize>, HashSet<String>);
43
44#[derive(Debug, Clone)]
45pub struct WorkspaceValueSnapshot {
46    pub vars: Vec<Value>,
47    pub names: HashMap<String, usize>,
48    pub assigned: HashSet<String>,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum WorkspaceTarget {
53    Current,
54    Caller,
55    Base,
56}
57
58#[derive(Debug, Clone)]
59pub struct WorkspaceTargetSnapshot {
60    pub names: HashMap<String, usize>,
61    pub assigned: HashSet<String>,
62    pub vars_ptr: *mut Vec<Value>,
63}
64
65#[derive(Debug, Clone)]
66pub struct WorkspaceAssignedReport {
67    pub ids: HashSet<usize>,
68    pub names: HashSet<String>,
69    pub removed_ids: HashSet<usize>,
70    pub removed_names: HashSet<String>,
71}
72
73runmat_thread_local! {
74    static WORKSPACE_STACK: RefCell<Vec<WorkspaceFrame>> = const { RefCell::new(Vec::new()) };
75    static PENDING_WORKSPACE: RefCell<Option<WorkspaceSnapshot>> = const { RefCell::new(None) };
76    static LAST_WORKSPACE_STATE: RefCell<Option<WorkspaceValueSnapshot>> = const { RefCell::new(None) };
77    static LAST_WORKSPACE_ASSIGNED_REPORT: RefCell<Option<WorkspaceAssignedReport>> = const { RefCell::new(None) };
78}
79
80fn mark_slot_unassigned(ws: &mut WorkspaceState, index: usize, name: String) {
81    ws.slot_lifecycle
82        .insert(index, SlotLifecycle::Unassigned(name.clone()));
83    ws.removed_slots_this_execution.insert(index, name);
84}
85
86fn mark_slot_assigned(ws: &mut WorkspaceState, index: usize, name: String) {
87    ws.slot_lifecycle
88        .insert(index, SlotLifecycle::Assigned(name.clone()));
89    ws.removed_slots_this_execution.remove(&index);
90}
91
92fn find_unassigned_slot_for_name(ws: &WorkspaceState, name: &str) -> Option<usize> {
93    ws.slot_lifecycle.iter().find_map(|(idx, state)| {
94        matches!(state, SlotLifecycle::Unassigned(slot_name) if slot_name == name).then_some(*idx)
95    })
96}
97
98fn upsert_slot_lifecycle_name(ws: &mut WorkspaceState, index: usize, name: &str) {
99    if let Some(existing_index) = ws.names.insert(name.to_string(), index) {
100        if existing_index != index {
101            mark_slot_unassigned(ws, existing_index, name.to_string());
102            ws.assigned.remove(name);
103        }
104    }
105
106    match ws.slot_lifecycle.get_mut(&index) {
107        Some(state) => {
108            let was_assigned = state.is_assigned();
109            let old_name = state.name().to_string();
110            if old_name != name {
111                if ws.names.get(&old_name).copied() == Some(index) {
112                    ws.names.remove(&old_name);
113                }
114                ws.assigned.remove(&old_name);
115            }
116            *state = if was_assigned {
117                SlotLifecycle::Assigned(name.to_string())
118            } else {
119                SlotLifecycle::Unassigned(name.to_string())
120            };
121        }
122        None => {
123            ws.slot_lifecycle
124                .insert(index, SlotLifecycle::Unassigned(name.to_string()));
125        }
126    }
127}
128
129fn target_frame_index(len: usize, target: WorkspaceTarget) -> Option<usize> {
130    if len == 0 {
131        return None;
132    }
133    match target {
134        WorkspaceTarget::Current => Some(len - 1),
135        WorkspaceTarget::Caller => Some(len.saturating_sub(2)),
136        WorkspaceTarget::Base => Some(0),
137    }
138}
139
140fn lifecycle_from_names(
141    names: &HashMap<String, usize>,
142    assigned: &HashSet<String>,
143) -> HashMap<usize, SlotLifecycle> {
144    names
145        .iter()
146        .map(|(name, idx)| {
147            let lifecycle = if assigned.contains(name) {
148                SlotLifecycle::Assigned(name.clone())
149            } else {
150                SlotLifecycle::Unassigned(name.clone())
151            };
152            (*idx, lifecycle)
153        })
154        .collect()
155}
156
157pub struct WorkspaceStateGuard;
158
159impl Drop for WorkspaceStateGuard {
160    fn drop(&mut self) {
161        WORKSPACE_STACK.with(|stack| {
162            let mut stack = stack.borrow_mut();
163            if let Some(frame) = stack.pop() {
164                if !frame.publish_on_drop {
165                    return;
166                }
167                let ws = frame.state;
168                let removed_ids = ws.removed_slots_this_execution.keys().copied().collect();
169                let removed_names = ws.removed_slots_this_execution.values().cloned().collect();
170                LAST_WORKSPACE_ASSIGNED_REPORT.with(|slot| {
171                    *slot.borrow_mut() = Some(WorkspaceAssignedReport {
172                        ids: ws.assigned_ids_this_execution,
173                        names: ws.assigned_names_this_execution,
174                        removed_ids,
175                        removed_names,
176                    });
177                });
178                LAST_WORKSPACE_STATE.with(|slot| {
179                    *slot.borrow_mut() = Some(WorkspaceValueSnapshot {
180                        vars: frame.vars_snapshot,
181                        names: ws.names,
182                        assigned: ws.assigned,
183                    });
184                });
185            }
186        });
187    }
188}
189
190pub struct PendingWorkspaceGuard;
191
192impl Drop for PendingWorkspaceGuard {
193    fn drop(&mut self) {
194        PENDING_WORKSPACE.with(|slot| {
195            slot.borrow_mut().take();
196        });
197    }
198}
199
200pub fn push_pending_workspace(
201    names: HashMap<String, usize>,
202    assigned: HashSet<String>,
203) -> PendingWorkspaceGuard {
204    PENDING_WORKSPACE.with(|slot| {
205        *slot.borrow_mut() = Some((names, assigned));
206    });
207    PendingWorkspaceGuard
208}
209
210pub fn take_pending_workspace_state() -> Option<WorkspaceSnapshot> {
211    PENDING_WORKSPACE.with(|slot| slot.borrow_mut().take())
212}
213
214pub fn take_updated_workspace_state() -> Option<WorkspaceValueSnapshot> {
215    LAST_WORKSPACE_STATE.with(|slot| slot.borrow_mut().take())
216}
217
218pub fn take_updated_workspace_assigned_report() -> Option<WorkspaceAssignedReport> {
219    LAST_WORKSPACE_ASSIGNED_REPORT.with(|slot| slot.borrow_mut().take())
220}
221
222pub fn set_workspace_state(
223    names: HashMap<String, usize>,
224    assigned: HashSet<String>,
225    vars: &mut Vec<Value>,
226) -> WorkspaceStateGuard {
227    set_workspace_state_with_publish(names, assigned, vars, true)
228}
229
230pub fn set_transient_workspace_state(
231    names: HashMap<String, usize>,
232    assigned: HashSet<String>,
233    vars: &mut Vec<Value>,
234) -> WorkspaceStateGuard {
235    set_workspace_state_with_publish(names, assigned, vars, false)
236}
237
238fn set_workspace_state_with_publish(
239    names: HashMap<String, usize>,
240    assigned: HashSet<String>,
241    vars: &mut Vec<Value>,
242    publish_on_drop: bool,
243) -> WorkspaceStateGuard {
244    let mut slot_lifecycle = HashMap::new();
245    for (name, idx) in &names {
246        let lifecycle = if assigned.contains(name) {
247            SlotLifecycle::Assigned(name.clone())
248        } else {
249            SlotLifecycle::Unassigned(name.clone())
250        };
251        slot_lifecycle.insert(*idx, lifecycle);
252    }
253    let vars_ptr = vars as *mut Vec<Value>;
254    WORKSPACE_STACK.with(|stack| {
255        stack.borrow_mut().push(WorkspaceFrame {
256            state: WorkspaceState {
257                names,
258                assigned,
259                assigned_names_this_execution: HashSet::new(),
260                assigned_ids_this_execution: HashSet::new(),
261                removed_slots_this_execution: HashMap::new(),
262                slot_lifecycle,
263                data_ptr: vars.as_ptr(),
264                len: vars.len(),
265            },
266            vars_ptr,
267            vars_snapshot: vars.clone(),
268            publish_on_drop,
269        });
270    });
271    WorkspaceStateGuard
272}
273
274pub fn refresh_workspace_state(vars: &[Value]) {
275    WORKSPACE_STACK.with(|stack| {
276        if let Some(frame) = stack.borrow_mut().last_mut() {
277            frame.state.data_ptr = vars.as_ptr();
278            frame.state.len = vars.len();
279            frame.vars_snapshot = vars.to_vec();
280        }
281    });
282}
283
284pub fn workspace_lookup(name: &str) -> Option<Value> {
285    WORKSPACE_STACK.with(|stack| {
286        let stack = stack.borrow();
287        let frame = stack.last()?;
288        let ws = &frame.state;
289        let idx = ws.names.get(name)?;
290        if !ws.assigned.contains(name) {
291            return None;
292        }
293        if *idx >= ws.len {
294            return None;
295        }
296        unsafe {
297            let ptr = ws.data_ptr.add(*idx);
298            Some((*ptr).clone())
299        }
300    })
301}
302
303pub fn workspace_slot_assigned(index: usize) -> Option<bool> {
304    WORKSPACE_STACK.with(|stack| {
305        let stack = stack.borrow();
306        let frame = stack.last()?;
307        let ws = &frame.state;
308        ws.slot_lifecycle
309            .get(&index)
310            .map(SlotLifecycle::is_assigned)
311    })
312}
313
314pub fn workspace_slot_name(index: usize) -> Option<String> {
315    WORKSPACE_STACK.with(|stack| {
316        let stack = stack.borrow();
317        let frame = stack.last()?;
318        let ws = &frame.state;
319        ws.slot_lifecycle
320            .get(&index)
321            .map(|state| state.name().to_string())
322    })
323}
324
325pub fn workspace_assign(name: &str, value: Value) -> Result<(), String> {
326    workspace_assign_target(WorkspaceTarget::Current, name, value)
327}
328
329pub fn workspace_assign_target(
330    target: WorkspaceTarget,
331    name: &str,
332    value: Value,
333) -> Result<(), String> {
334    WORKSPACE_STACK.with(|stack| {
335        let mut stack = stack.borrow_mut();
336        let index = target_frame_index(stack.len(), target)
337            .ok_or_else(|| "load: workspace state unavailable".to_string())?;
338        let frame = stack
339            .get_mut(index)
340            .ok_or_else(|| "load: workspace state unavailable".to_string())?;
341        set_workspace_variable_in_frame(frame, name, value)
342    })
343}
344
345pub fn workspace_clear() -> Result<(), String> {
346    WORKSPACE_STACK.with(|stack| {
347        let mut stack = stack.borrow_mut();
348        let frame = stack
349            .last_mut()
350            .ok_or_else(|| "clear: workspace state unavailable".to_string())?;
351        let ws = &mut frame.state;
352        let vars = unsafe { &mut *frame.vars_ptr };
353        for (name, idx) in ws.names.clone() {
354            if let Some(value) = vars.get_mut(idx) {
355                *value = Value::Num(0.0);
356            }
357            mark_slot_unassigned(ws, idx, name);
358        }
359        ws.names.clear();
360        ws.assigned.clear();
361        frame.vars_snapshot = vars.clone();
362        ws.data_ptr = vars.as_ptr();
363        ws.len = vars.len();
364        Ok(())
365    })
366}
367
368pub fn workspace_remove(name: &str) -> Result<(), String> {
369    WORKSPACE_STACK.with(|stack| {
370        let mut stack = stack.borrow_mut();
371        let frame = stack
372            .last_mut()
373            .ok_or_else(|| "clear: workspace state unavailable".to_string())?;
374        let ws = &mut frame.state;
375        let vars = unsafe { &mut *frame.vars_ptr };
376        if let Some(idx) = ws.names.remove(name) {
377            ws.assigned.remove(name);
378            mark_slot_unassigned(ws, idx, name.to_string());
379            frame.vars_snapshot = vars.clone();
380            ws.data_ptr = vars.as_ptr();
381            ws.len = vars.len();
382        }
383        Ok(())
384    })
385}
386
387pub fn workspace_snapshot() -> Vec<(String, Value)> {
388    WORKSPACE_STACK.with(|stack| {
389        let stack = stack.borrow();
390        if let Some(frame) = stack.last() {
391            let ws = &frame.state;
392            let mut entries: Vec<(String, Value)> = ws
393                .names
394                .iter()
395                .filter_map(|(name, idx)| {
396                    if *idx >= ws.len {
397                        return None;
398                    }
399                    if !ws.assigned.contains(name) {
400                        return None;
401                    }
402                    unsafe {
403                        let ptr = ws.data_ptr.add(*idx);
404                        Some((name.clone(), (*ptr).clone()))
405                    }
406                })
407                .collect();
408            entries.sort_by(|a, b| a.0.cmp(&b.0));
409            entries
410        } else {
411            Vec::new()
412        }
413    })
414}
415
416pub fn workspace_target_snapshot(
417    target: WorkspaceTarget,
418) -> Result<WorkspaceTargetSnapshot, String> {
419    WORKSPACE_STACK.with(|stack| {
420        let stack = stack.borrow();
421        let index = target_frame_index(stack.len(), target)
422            .ok_or_else(|| "workspace state unavailable".to_string())?;
423        let frame = stack
424            .get(index)
425            .ok_or_else(|| "workspace state unavailable".to_string())?;
426        Ok(WorkspaceTargetSnapshot {
427            names: frame.state.names.clone(),
428            assigned: frame.state.assigned.clone(),
429            vars_ptr: frame.vars_ptr,
430        })
431    })
432}
433
434pub fn replace_workspace_target_vars_and_state(
435    target: WorkspaceTarget,
436    vars: Vec<Value>,
437    names: HashMap<String, usize>,
438    assigned: HashSet<String>,
439) -> Result<(), String> {
440    WORKSPACE_STACK.with(|stack| {
441        let mut stack = stack.borrow_mut();
442        let index = target_frame_index(stack.len(), target)
443            .ok_or_else(|| "workspace state unavailable".to_string())?;
444        let frame = stack
445            .get_mut(index)
446            .ok_or_else(|| "workspace state unavailable".to_string())?;
447        let target_vars = unsafe { &mut *frame.vars_ptr };
448        *target_vars = vars;
449        frame.vars_snapshot = target_vars.clone();
450        frame.state.names = names;
451        frame.state.assigned = assigned;
452        frame.state.slot_lifecycle =
453            lifecycle_from_names(&frame.state.names, &frame.state.assigned);
454        frame.state.data_ptr = target_vars.as_ptr();
455        frame.state.len = target_vars.len();
456        Ok(())
457    })
458}
459
460#[cfg(test)]
461pub fn set_workspace_variable(
462    name: &str,
463    value: Value,
464    vars: &mut Vec<Value>,
465) -> Result<(), String> {
466    WORKSPACE_STACK.with(|stack| {
467        let mut stack = stack.borrow_mut();
468        match stack.last_mut() {
469            Some(frame) => set_workspace_variable_in_frame(frame, name, value),
470            None => Err("load: workspace state unavailable".to_string()),
471        }
472    })?;
473    let _ = vars;
474    Ok(())
475}
476
477fn set_workspace_variable_in_frame(
478    frame: &mut WorkspaceFrame,
479    name: &str,
480    value: Value,
481) -> Result<(), String> {
482    let vars = unsafe { &mut *frame.vars_ptr };
483    let ws = &mut frame.state;
484    let idx = if let Some(idx) = ws.names.get(name).copied() {
485        idx
486    } else if let Some(idx) = find_unassigned_slot_for_name(ws, name) {
487        ws.names.insert(name.to_string(), idx);
488        idx
489    } else {
490        let idx = vars.len();
491        ws.names.insert(name.to_string(), idx);
492        idx
493    };
494    if idx >= vars.len() {
495        vars.resize(idx + 1, Value::Num(0.0));
496    }
497    vars[idx] = value;
498    frame.vars_snapshot = vars.clone();
499    ws.data_ptr = vars.as_ptr();
500    ws.len = vars.len();
501    ws.assigned.insert(name.to_string());
502    ws.assigned_names_this_execution.insert(name.to_string());
503    ws.assigned_ids_this_execution.insert(idx);
504    mark_slot_assigned(ws, idx, name.to_string());
505    Ok(())
506}
507
508pub fn ensure_workspace_slot_name(index: usize, name: &str) {
509    WORKSPACE_STACK.with(|stack| {
510        if let Some(frame) = stack.borrow_mut().last_mut() {
511            upsert_slot_lifecycle_name(&mut frame.state, index, name);
512        }
513    });
514}
515
516pub fn mark_workspace_assigned(index: usize) {
517    WORKSPACE_STACK.with(|stack| {
518        if let Some(frame) = stack.borrow_mut().last_mut() {
519            let ws = &mut frame.state;
520            if let Some(name) = ws
521                .slot_lifecycle
522                .get(&index)
523                .map(|slot| slot.name().to_string())
524            {
525                ws.assigned.insert(name.clone());
526                ws.assigned_names_this_execution.insert(name.clone());
527                ws.assigned_ids_this_execution.insert(index);
528                mark_slot_assigned(ws, index, name);
529                let vars = unsafe { &*frame.vars_ptr };
530                frame.vars_snapshot = vars.clone();
531            }
532        }
533    });
534}
535
536pub(crate) fn reset_thread_state_for_tests() {
537    WORKSPACE_STACK.with(|stack| stack.borrow_mut().clear());
538    PENDING_WORKSPACE.with(|slot| {
539        slot.borrow_mut().take();
540    });
541    LAST_WORKSPACE_STATE.with(|slot| {
542        slot.borrow_mut().take();
543    });
544    LAST_WORKSPACE_ASSIGNED_REPORT.with(|slot| {
545        slot.borrow_mut().take();
546    });
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    fn take_report_after(f: impl FnOnce(&mut Vec<Value>)) -> WorkspaceAssignedReport {
554        let _ = take_updated_workspace_assigned_report();
555        let _ = take_updated_workspace_state();
556
557        let mut vars = Vec::new();
558        {
559            let _guard = set_workspace_state(HashMap::new(), HashSet::new(), &mut vars);
560            f(&mut vars);
561        }
562
563        take_updated_workspace_assigned_report().expect("workspace report should be recorded")
564    }
565
566    #[test]
567    fn remove_preserves_assignment_report_and_records_removal() {
568        let report = take_report_after(|vars| {
569            set_workspace_variable("x", Value::Num(1.0), vars).unwrap();
570            workspace_remove("x").unwrap();
571        });
572
573        assert!(report.names.contains("x"));
574        assert!(report.ids.contains(&0));
575        assert!(report.removed_names.contains("x"));
576        assert!(report.removed_ids.contains(&0));
577    }
578
579    #[test]
580    fn clear_preserves_assignment_report_and_records_removal() {
581        let report = take_report_after(|vars| {
582            set_workspace_variable("x", Value::Num(1.0), vars).unwrap();
583            workspace_clear().unwrap();
584        });
585
586        assert!(report.names.contains("x"));
587        assert!(report.ids.contains(&0));
588        assert!(report.removed_names.contains("x"));
589        assert!(report.removed_ids.contains(&0));
590    }
591
592    #[test]
593    fn clear_preserves_active_frame_slots() {
594        let _ = take_updated_workspace_state();
595        let mut vars = vec![Value::Num(11.0), Value::Num(0.0), Value::Num(0.0)];
596        let mut names = HashMap::new();
597        names.insert("x".to_string(), 0);
598        let assigned = HashSet::from(["x".to_string()]);
599
600        {
601            let _guard = set_workspace_state(names, assigned, &mut vars);
602            workspace_clear().unwrap();
603
604            assert_eq!(vars.len(), 3);
605            assert_eq!(vars[0], Value::Num(0.0));
606            assert_eq!(vars[1], Value::Num(0.0));
607            assert_eq!(vars[2], Value::Num(0.0));
608            assert_eq!(workspace_lookup("x"), None);
609            assert_eq!(workspace_slot_assigned(0), Some(false));
610        }
611
612        let snapshot = take_updated_workspace_state().expect("workspace state should be recorded");
613        assert_eq!(snapshot.vars.len(), 3);
614        assert!(snapshot.names.is_empty());
615        assert!(snapshot.assigned.is_empty());
616    }
617
618    #[test]
619    fn assignment_after_clear_clears_final_removal_marker() {
620        let report = take_report_after(|vars| {
621            set_workspace_variable("x", Value::Num(1.0), vars).unwrap();
622            workspace_clear().unwrap();
623            set_workspace_variable("x", Value::Num(2.0), vars).unwrap();
624        });
625
626        assert!(report.names.contains("x"));
627        assert!(report.removed_names.is_empty());
628        assert!(report.removed_ids.is_empty());
629    }
630
631    #[test]
632    fn assignment_after_remove_reuses_previous_slot() {
633        let mut vars = Vec::new();
634        let _ = take_updated_workspace_state();
635        {
636            let _guard = set_workspace_state(HashMap::new(), HashSet::new(), &mut vars);
637            set_workspace_variable("x", Value::Num(1.0), &mut vars).unwrap();
638            set_workspace_variable("z", Value::Num(9.0), &mut vars).unwrap();
639            workspace_remove("x").unwrap();
640            set_workspace_variable("x", Value::Num(42.0), &mut vars).unwrap();
641            assert_eq!(workspace_lookup("x"), Some(Value::Num(42.0)));
642            assert_eq!(vars[0], Value::Num(42.0));
643        }
644
645        let snapshot = take_updated_workspace_state().expect("workspace state should be recorded");
646        assert_eq!(snapshot.names.get("x"), Some(&0));
647        assert_eq!(snapshot.names.get("z"), Some(&1));
648        assert!(snapshot.assigned.contains("x"));
649        assert!(snapshot.assigned.contains("z"));
650        assert_eq!(snapshot.vars[0], Value::Num(42.0));
651    }
652}