Skip to main content

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;