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