seq_runtime/map_ops.rs
1//! Map operations for Seq
2//!
3//! Dictionary/hash map operations with O(1) lookup.
4//! Maps use hashable keys (Int, String, Bool) and can store any Value.
5//!
6//! # Examples
7//!
8//! ```seq
9//! # Create empty map and add entries
10//! make-map "name" "Alice" map-set "age" 30 map-set
11//!
12//! # Get value by key
13//! my-map "name" map-get # -> "Alice"
14//!
15//! # Check if key exists
16//! my-map "email" map-has? # -> 0 (false)
17//!
18//! # Get keys/values as lists
19//! my-map map-keys # -> ["name", "age"]
20//! my-map map-values # -> ["Alice", 30]
21//! ```
22//!
23//! # Error Handling
24//!
25//! - `map-get` returns (value Bool) - false if key not found (errors are values, not crashes)
26//! - Type errors (invalid key types, non-Map values) still panic (internal bugs)
27//!
28//! # Performance Notes
29//!
30//! - Operations use functional style: `map-set` and `map-remove` return new maps
31//! - Each mutation clones the underlying HashMap (O(n) for n entries)
32//! - For small maps (<100 entries), this is typically fast enough
33//! - Key/value iteration order is not guaranteed (HashMap iteration order)
34
35use crate::seqstring::global_string;
36use crate::stack::{Stack, drop_stack_value, heap_value_mut, pop, pop_sv, push};
37use crate::value::{MapKey, Value, VariantData};
38use std::sync::Arc;
39
40/// Create an empty map
41///
42/// Stack effect: ( -- Map )
43///
44/// # Safety
45/// Stack can be any valid stack pointer (including null for empty stack)
46#[unsafe(no_mangle)]
47pub unsafe extern "C" fn patch_seq_make_map(stack: Stack) -> Stack {
48 unsafe { push(stack, Value::Map(Box::default())) }
49}
50
51/// Get a value from the map by key
52///
53/// Stack effect: ( Map key -- value Bool )
54///
55/// Returns (value true) if found, or (0 false) if not found.
56/// Errors are values, not crashes.
57/// Panics only for internal bugs (invalid key type, non-Map value).
58///
59/// # Safety
60/// Stack must have a hashable key on top and a Map below
61#[unsafe(no_mangle)]
62pub unsafe extern "C" fn patch_seq_map_get(stack: Stack) -> Stack {
63 unsafe {
64 // Pop key
65 let (stack, key_val) = pop(stack);
66 let key = MapKey::from_value(&key_val).unwrap_or_else(|| {
67 panic!(
68 "map-get: key must be Int, String, or Bool, got {:?}",
69 key_val
70 )
71 });
72
73 // Pop map
74 let (stack, map_val) = pop(stack);
75 let map = match map_val {
76 Value::Map(m) => m,
77 _ => panic!("map-get: expected Map, got {:?}", map_val),
78 };
79
80 // Look up value - return success flag instead of panicking
81 match map.get(&key) {
82 Some(value) => {
83 let stack = push(stack, value.clone());
84 push(stack, Value::Bool(true))
85 }
86 None => {
87 let stack = push(stack, Value::Int(0)); // placeholder value
88 push(stack, Value::Bool(false)) // not found
89 }
90 }
91 }
92}
93
94/// Set a key-value pair in the map with COW optimization.
95///
96/// Stack effect: ( Map key value -- Map )
97///
98/// Fast path: if the map (at sp-3) is sole-owned, pops key and value,
99/// inserts directly into the map in place — no Box alloc/dealloc cycle.
100/// Slow path: pops all three, clones the map, inserts, pushes new map.
101///
102/// # Safety
103/// Stack must have value on top, key below, and Map at third position
104#[unsafe(no_mangle)]
105pub unsafe extern "C" fn patch_seq_map_set(stack: Stack) -> Stack {
106 unsafe {
107 // Fast path: peek at the map at sp-3 without popping.
108 // SAFETY: map.set requires three values on the stack (enforced by
109 // the type checker), so stack.sub(3) is valid.
110 if let Some(Value::Map(map)) = heap_value_mut(stack.sub(3)) {
111 // Sole owner — pop key and value, mutate map in place.
112 let (stack, value) = pop(stack);
113 let (stack, key_val) = pop(stack);
114 let key = MapKey::from_value(&key_val).unwrap_or_else(|| {
115 panic!(
116 "map-set: key must be Int, String, or Bool, got {:?}",
117 key_val
118 )
119 });
120 // Safety: `pop` only touches sp-1 per call; the map at
121 // the original sp-3 (now sp-1) is not invalidated.
122 map.insert(key, value);
123 return stack; // Map is still at sp-1, mutated in place
124 }
125
126 // Slow path: pop all three, clone map, insert, push
127 let (stack, value) = pop(stack);
128 let (stack, key_val) = pop(stack);
129 let key = MapKey::from_value(&key_val).unwrap_or_else(|| {
130 panic!(
131 "map-set: key must be Int, String, or Bool, got {:?}",
132 key_val
133 )
134 });
135 let (stack, map_val) = pop(stack);
136 let mut map = match map_val {
137 Value::Map(m) => *m,
138 _ => panic!("map-set: expected Map, got {:?}", map_val),
139 };
140 map.insert(key, value);
141 push(stack, Value::Map(Box::new(map)))
142 }
143}
144
145/// Check if a key exists in the map
146///
147/// Stack effect: ( Map key -- Int )
148///
149/// Returns 1 if the key exists, 0 otherwise.
150/// Panics if the key type is not hashable.
151///
152/// # Safety
153/// Stack must have a hashable key on top and a Map below
154#[unsafe(no_mangle)]
155pub unsafe extern "C" fn patch_seq_map_has(stack: Stack) -> Stack {
156 unsafe {
157 // Pop key
158 let (stack, key_val) = pop(stack);
159 let key = MapKey::from_value(&key_val).unwrap_or_else(|| {
160 panic!(
161 "map-has?: key must be Int, String, or Bool, got {:?}",
162 key_val
163 )
164 });
165
166 // Pop map
167 let (stack, map_val) = pop(stack);
168 let map = match map_val {
169 Value::Map(m) => m,
170 _ => panic!("map-has?: expected Map, got {:?}", map_val),
171 };
172
173 let has_key = map.contains_key(&key);
174 push(stack, Value::Bool(has_key))
175 }
176}
177
178/// Remove a key from the map with COW optimization.
179///
180/// Stack effect: ( Map key -- Map )
181///
182/// Fast path: if the map (at sp-2) is sole-owned, pops key and
183/// removes directly from the map in place.
184/// Slow path: pops both, clones, removes, pushes new map.
185///
186/// # Safety
187/// Stack must have a hashable key on top and a Map below
188#[unsafe(no_mangle)]
189pub unsafe extern "C" fn patch_seq_map_remove(stack: Stack) -> Stack {
190 unsafe {
191 // Fast path: peek at the map at sp-2 without popping.
192 // SAFETY: map.remove requires two values on the stack (enforced by
193 // the type checker), so stack.sub(2) is valid.
194 if let Some(Value::Map(map)) = heap_value_mut(stack.sub(2)) {
195 let (stack, key_val) = pop(stack);
196 let key = MapKey::from_value(&key_val).unwrap_or_else(|| {
197 panic!(
198 "map-remove: key must be Int, String, or Bool, got {:?}",
199 key_val
200 )
201 });
202 // Safety: pop only touches sp-1; the map at the original
203 // sp-2 (now sp-1) is not invalidated.
204 map.remove(&key);
205 return stack; // Map is still at sp-1, mutated in place
206 }
207
208 // Slow path: pop both, clone map, remove, push
209 let (stack, key_val) = pop(stack);
210 let key = MapKey::from_value(&key_val).unwrap_or_else(|| {
211 panic!(
212 "map-remove: key must be Int, String, or Bool, got {:?}",
213 key_val
214 )
215 });
216 let (stack, map_val) = pop(stack);
217 let mut map = match map_val {
218 Value::Map(m) => *m,
219 _ => panic!("map-remove: expected Map, got {:?}", map_val),
220 };
221 map.remove(&key);
222 push(stack, Value::Map(Box::new(map)))
223 }
224}
225
226/// Get all keys from the map as a list
227///
228/// Stack effect: ( Map -- Variant )
229///
230/// Returns a Variant containing all keys in the map.
231/// Note: Order is not guaranteed (HashMap iteration order).
232///
233/// # Safety
234/// Stack must have a Map on top
235#[unsafe(no_mangle)]
236pub unsafe extern "C" fn patch_seq_map_keys(stack: Stack) -> Stack {
237 unsafe {
238 let (stack, map_val) = pop(stack);
239 let map = match map_val {
240 Value::Map(m) => m,
241 _ => panic!("map-keys: expected Map, got {:?}", map_val),
242 };
243
244 let keys: Vec<Value> = map.keys().map(|k| k.to_value()).collect();
245 let variant = Value::Variant(Arc::new(VariantData::new(
246 global_string("List".to_string()),
247 keys,
248 )));
249 push(stack, variant)
250 }
251}
252
253/// Get all values from the map as a list
254///
255/// Stack effect: ( Map -- Variant )
256///
257/// Returns a Variant containing all values in the map.
258/// Note: Order is not guaranteed (HashMap iteration order).
259///
260/// # Safety
261/// Stack must have a Map on top
262#[unsafe(no_mangle)]
263pub unsafe extern "C" fn patch_seq_map_values(stack: Stack) -> Stack {
264 unsafe {
265 let (stack, map_val) = pop(stack);
266 let map = match map_val {
267 Value::Map(m) => m,
268 _ => panic!("map-values: expected Map, got {:?}", map_val),
269 };
270
271 let values: Vec<Value> = map.values().cloned().collect();
272 let variant = Value::Variant(Arc::new(VariantData::new(
273 global_string("List".to_string()),
274 values,
275 )));
276 push(stack, variant)
277 }
278}
279
280/// Get the number of entries in the map
281///
282/// Stack effect: ( Map -- Int )
283///
284/// # Safety
285/// Stack must have a Map on top
286#[unsafe(no_mangle)]
287pub unsafe extern "C" fn patch_seq_map_size(stack: Stack) -> Stack {
288 unsafe {
289 let (stack, map_val) = pop(stack);
290 let map = match map_val {
291 Value::Map(m) => m,
292 _ => panic!("map-size: expected Map, got {:?}", map_val),
293 };
294
295 push(stack, Value::Int(map.len() as i64))
296 }
297}
298
299/// Check if the map is empty
300///
301/// Stack effect: ( Map -- Int )
302///
303/// Returns 1 if the map has no entries, 0 otherwise.
304///
305/// # Safety
306/// Stack must have a Map on top
307#[unsafe(no_mangle)]
308pub unsafe extern "C" fn patch_seq_map_empty(stack: Stack) -> Stack {
309 unsafe {
310 let (stack, map_val) = pop(stack);
311 let map = match map_val {
312 Value::Map(m) => m,
313 _ => panic!("map-empty?: expected Map, got {:?}", map_val),
314 };
315
316 let is_empty = map.is_empty();
317 push(stack, Value::Bool(is_empty))
318 }
319}
320
321/// Iterate over all key-value pairs in a map, calling a quotation for each.
322///
323/// Stack effect: ( Map Quotation -- )
324/// where Quotation : ( key value -- )
325///
326/// The quotation receives each key and value on a fresh stack.
327/// Iteration order is not guaranteed.
328///
329/// # Safety
330/// Stack must have a Quotation/Closure on top and a Map below
331#[unsafe(no_mangle)]
332pub unsafe extern "C" fn patch_seq_map_each(stack: Stack) -> Stack {
333 unsafe {
334 // Pop quotation
335 let (stack, callable) = pop(stack);
336 match &callable {
337 Value::Quotation { .. } | Value::Closure { .. } => {}
338 _ => panic!(
339 "map.each: expected Quotation or Closure, got {:?}",
340 callable
341 ),
342 }
343
344 // Pop map
345 let (stack, map_val) = pop(stack);
346 let map = match &map_val {
347 Value::Map(m) => m,
348 _ => panic!("map.each: expected Map, got {:?}", map_val),
349 };
350
351 // Call quotation for each key-value pair
352 for (key, value) in map.iter() {
353 let temp_base = crate::stack::alloc_stack();
354 let temp_stack = push(temp_base, key.to_value());
355 let temp_stack = push(temp_stack, value.clone());
356 let temp_stack = invoke_callable(temp_stack, &callable);
357 // Drain any leftover values
358 drain_to_base(temp_stack, temp_base);
359 }
360
361 stack
362 }
363}
364
365/// Fold over all key-value pairs in a map with an accumulator.
366///
367/// Stack effect: ( Map init Quotation -- result )
368/// where Quotation : ( acc key value -- acc' )
369///
370/// The quotation receives the accumulator, key, and value, and must
371/// return the new accumulator. Iteration order is not guaranteed.
372///
373/// # Safety
374/// Stack must have Quotation on top, init below, and Map below that
375#[unsafe(no_mangle)]
376pub unsafe extern "C" fn patch_seq_map_fold(stack: Stack) -> Stack {
377 unsafe {
378 // Pop quotation
379 let (stack, callable) = pop(stack);
380 match &callable {
381 Value::Quotation { .. } | Value::Closure { .. } => {}
382 _ => panic!(
383 "map.fold: expected Quotation or Closure, got {:?}",
384 callable
385 ),
386 }
387
388 // Pop initial accumulator
389 let (stack, mut acc) = pop(stack);
390
391 // Pop map
392 let (stack, map_val) = pop(stack);
393 let map = match &map_val {
394 Value::Map(m) => m,
395 _ => panic!("map.fold: expected Map, got {:?}", map_val),
396 };
397
398 // Fold over each key-value pair
399 for (key, value) in map.iter() {
400 let temp_base = crate::stack::alloc_stack();
401 let temp_stack = push(temp_base, acc);
402 let temp_stack = push(temp_stack, key.to_value());
403 let temp_stack = push(temp_stack, value.clone());
404 let temp_stack = invoke_callable(temp_stack, &callable);
405 // Pop new accumulator
406 if temp_stack <= temp_base {
407 panic!("map.fold: quotation consumed accumulator without producing result");
408 }
409 let (remaining, new_acc) = pop(temp_stack);
410 acc = new_acc;
411 // Drain any extra values left by the quotation
412 if remaining > temp_base {
413 drain_to_base(remaining, temp_base);
414 }
415 }
416
417 push(stack, acc)
418 }
419}
420
421use crate::quotations::invoke_callable;
422
423/// Drain stack values back to base, properly freeing heap-allocated values.
424unsafe fn drain_to_base(mut stack: Stack, base: Stack) {
425 unsafe {
426 while stack > base {
427 let (rest, sv) = pop_sv(stack);
428 drop_stack_value(sv);
429 stack = rest;
430 }
431 }
432}
433
434// Public re-exports
435pub use patch_seq_make_map as make_map;
436pub use patch_seq_map_each as map_each;
437pub use patch_seq_map_empty as map_empty;
438pub use patch_seq_map_fold as map_fold;
439pub use patch_seq_map_get as map_get;
440pub use patch_seq_map_has as map_has;
441pub use patch_seq_map_keys as map_keys;
442pub use patch_seq_map_remove as map_remove;
443pub use patch_seq_map_set as map_set;
444pub use patch_seq_map_size as map_size;
445pub use patch_seq_map_values as map_values;
446
447#[cfg(test)]
448mod tests;