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}