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        vars.clear();
354        frame.vars_snapshot.clear();
355        for (name, idx) in ws.names.clone() {
356            mark_slot_unassigned(ws, idx, name);
357        }
358        ws.names.clear();
359        ws.assigned.clear();
360        ws.data_ptr = vars.as_ptr();
361        ws.len = vars.len();
362        Ok(())
363    })
364}
365
366pub fn workspace_remove(name: &str) -> Result<(), String> {
367    WORKSPACE_STACK.with(|stack| {
368        let mut stack = stack.borrow_mut();
369        let frame = stack
370            .last_mut()
371            .ok_or_else(|| "clear: workspace state unavailable".to_string())?;
372        let ws = &mut frame.state;
373        let vars = unsafe { &mut *frame.vars_ptr };
374        if let Some(idx) = ws.names.remove(name) {
375            ws.assigned.remove(name);
376            mark_slot_unassigned(ws, idx, name.to_string());
377            frame.vars_snapshot = vars.clone();
378            ws.data_ptr = vars.as_ptr();
379            ws.len = vars.len();
380        }
381        Ok(())
382    })
383}
384
385pub fn workspace_snapshot() -> Vec<(String, Value)> {
386    WORKSPACE_STACK.with(|stack| {
387        let stack = stack.borrow();
388        if let Some(frame) = stack.last() {
389            let ws = &frame.state;
390            let mut entries: Vec<(String, Value)> = ws
391                .names
392                .iter()
393                .filter_map(|(name, idx)| {
394                    if *idx >= ws.len {
395                        return None;
396                    }
397                    if !ws.assigned.contains(name) {
398                        return None;
399                    }
400                    unsafe {
401                        let ptr = ws.data_ptr.add(*idx);
402                        Some((name.clone(), (*ptr).clone()))
403                    }
404                })
405                .collect();
406            entries.sort_by(|a, b| a.0.cmp(&b.0));
407            entries
408        } else {
409            Vec::new()
410        }
411    })
412}
413
414pub fn workspace_target_snapshot(
415    target: WorkspaceTarget,
416) -> Result<WorkspaceTargetSnapshot, String> {
417    WORKSPACE_STACK.with(|stack| {
418        let stack = stack.borrow();
419        let index = target_frame_index(stack.len(), target)
420            .ok_or_else(|| "workspace state unavailable".to_string())?;
421        let frame = stack
422            .get(index)
423            .ok_or_else(|| "workspace state unavailable".to_string())?;
424        Ok(WorkspaceTargetSnapshot {
425            names: frame.state.names.clone(),
426            assigned: frame.state.assigned.clone(),
427            vars_ptr: frame.vars_ptr,
428        })
429    })
430}
431
432pub fn replace_workspace_target_vars_and_state(
433    target: WorkspaceTarget,
434    vars: Vec<Value>,
435    names: HashMap<String, usize>,
436    assigned: HashSet<String>,
437) -> Result<(), String> {
438    WORKSPACE_STACK.with(|stack| {
439        let mut stack = stack.borrow_mut();
440        let index = target_frame_index(stack.len(), target)
441            .ok_or_else(|| "workspace state unavailable".to_string())?;
442        let frame = stack
443            .get_mut(index)
444            .ok_or_else(|| "workspace state unavailable".to_string())?;
445        let target_vars = unsafe { &mut *frame.vars_ptr };
446        *target_vars = vars;
447        frame.vars_snapshot = target_vars.clone();
448        frame.state.names = names;
449        frame.state.assigned = assigned;
450        frame.state.slot_lifecycle =
451            lifecycle_from_names(&frame.state.names, &frame.state.assigned);
452        frame.state.data_ptr = target_vars.as_ptr();
453        frame.state.len = target_vars.len();
454        Ok(())
455    })
456}
457
458#[cfg(test)]
459pub fn set_workspace_variable(
460    name: &str,
461    value: Value,
462    vars: &mut Vec<Value>,
463) -> Result<(), String> {
464    WORKSPACE_STACK.with(|stack| {
465        let mut stack = stack.borrow_mut();
466        match stack.last_mut() {
467            Some(frame) => set_workspace_variable_in_frame(frame, name, value),
468            None => Err("load: workspace state unavailable".to_string()),
469        }
470    })?;
471    let _ = vars;
472    Ok(())
473}
474
475fn set_workspace_variable_in_frame(
476    frame: &mut WorkspaceFrame,
477    name: &str,
478    value: Value,
479) -> Result<(), String> {
480    let vars = unsafe { &mut *frame.vars_ptr };
481    let ws = &mut frame.state;
482    let idx = if let Some(idx) = ws.names.get(name).copied() {
483        idx
484    } else if let Some(idx) = find_unassigned_slot_for_name(ws, name) {
485        ws.names.insert(name.to_string(), idx);
486        idx
487    } else {
488        let idx = vars.len();
489        ws.names.insert(name.to_string(), idx);
490        idx
491    };
492    if idx >= vars.len() {
493        vars.resize(idx + 1, Value::Num(0.0));
494    }
495    vars[idx] = value;
496    frame.vars_snapshot = vars.clone();
497    ws.data_ptr = vars.as_ptr();
498    ws.len = vars.len();
499    ws.assigned.insert(name.to_string());
500    ws.assigned_names_this_execution.insert(name.to_string());
501    ws.assigned_ids_this_execution.insert(idx);
502    mark_slot_assigned(ws, idx, name.to_string());
503    Ok(())
504}
505
506pub fn ensure_workspace_slot_name(index: usize, name: &str) {
507    WORKSPACE_STACK.with(|stack| {
508        if let Some(frame) = stack.borrow_mut().last_mut() {
509            upsert_slot_lifecycle_name(&mut frame.state, index, name);
510        }
511    });
512}
513
514pub fn mark_workspace_assigned(index: usize) {
515    WORKSPACE_STACK.with(|stack| {
516        if let Some(frame) = stack.borrow_mut().last_mut() {
517            let ws = &mut frame.state;
518            if let Some(name) = ws
519                .slot_lifecycle
520                .get(&index)
521                .map(|slot| slot.name().to_string())
522            {
523                ws.assigned.insert(name.clone());
524                ws.assigned_names_this_execution.insert(name.clone());
525                ws.assigned_ids_this_execution.insert(index);
526                mark_slot_assigned(ws, index, name);
527                let vars = unsafe { &*frame.vars_ptr };
528                frame.vars_snapshot = vars.clone();
529            }
530        }
531    });
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    fn take_report_after(f: impl FnOnce(&mut Vec<Value>)) -> WorkspaceAssignedReport {
539        let _ = take_updated_workspace_assigned_report();
540        let _ = take_updated_workspace_state();
541
542        let mut vars = Vec::new();
543        {
544            let _guard = set_workspace_state(HashMap::new(), HashSet::new(), &mut vars);
545            f(&mut vars);
546        }
547
548        take_updated_workspace_assigned_report().expect("workspace report should be recorded")
549    }
550
551    #[test]
552    fn remove_preserves_assignment_report_and_records_removal() {
553        let report = take_report_after(|vars| {
554            set_workspace_variable("x", Value::Num(1.0), vars).unwrap();
555            workspace_remove("x").unwrap();
556        });
557
558        assert!(report.names.contains("x"));
559        assert!(report.ids.contains(&0));
560        assert!(report.removed_names.contains("x"));
561        assert!(report.removed_ids.contains(&0));
562    }
563
564    #[test]
565    fn clear_preserves_assignment_report_and_records_removal() {
566        let report = take_report_after(|vars| {
567            set_workspace_variable("x", Value::Num(1.0), vars).unwrap();
568            workspace_clear().unwrap();
569        });
570
571        assert!(report.names.contains("x"));
572        assert!(report.ids.contains(&0));
573        assert!(report.removed_names.contains("x"));
574        assert!(report.removed_ids.contains(&0));
575    }
576
577    #[test]
578    fn assignment_after_clear_clears_final_removal_marker() {
579        let report = take_report_after(|vars| {
580            set_workspace_variable("x", Value::Num(1.0), vars).unwrap();
581            workspace_clear().unwrap();
582            set_workspace_variable("x", Value::Num(2.0), vars).unwrap();
583        });
584
585        assert!(report.names.contains("x"));
586        assert!(report.removed_names.is_empty());
587        assert!(report.removed_ids.is_empty());
588    }
589
590    #[test]
591    fn assignment_after_remove_reuses_previous_slot() {
592        let mut vars = Vec::new();
593        let _ = take_updated_workspace_state();
594        {
595            let _guard = set_workspace_state(HashMap::new(), HashSet::new(), &mut vars);
596            set_workspace_variable("x", Value::Num(1.0), &mut vars).unwrap();
597            set_workspace_variable("z", Value::Num(9.0), &mut vars).unwrap();
598            workspace_remove("x").unwrap();
599            set_workspace_variable("x", Value::Num(42.0), &mut vars).unwrap();
600            assert_eq!(workspace_lookup("x"), Some(Value::Num(42.0)));
601            assert_eq!(vars[0], Value::Num(42.0));
602        }
603
604        let snapshot = take_updated_workspace_state().expect("workspace state should be recorded");
605        assert_eq!(snapshot.names.get("x"), Some(&0));
606        assert_eq!(snapshot.names.get("z"), Some(&1));
607        assert!(snapshot.assigned.contains("x"));
608        assert!(snapshot.assigned.contains("z"));
609        assert_eq!(snapshot.vars[0], Value::Num(42.0));
610    }
611}