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}