Skip to main content

aver/types/
map.rs

1/// Map namespace — immutable key/value map helpers.
2///
3/// Methods:
4///   Map.empty()                 → Map<K, V>
5///   Map.set(map, key, value)    → Map<K, V>
6///   Map.get(map, key)           → Option<V>
7///   Map.remove(map, key)        → Map<K, V>
8///   Map.has(map, key)           → Bool
9///   Map.keys(map)               → List<K>
10///   Map.values(map)             → List<V>
11///   Map.entries(map)            → List<(K, V)>
12///   Map.len(map)                → Int
13///   Map.fromList(pairs)         → Map<K, V> where each pair is (key, value)
14///
15/// Key constraint: only scalar keys are allowed (Int, Float, String, Bool).
16///
17/// No effects required.
18use std::cmp::Ordering;
19use std::collections::HashMap;
20
21use crate::value::{RuntimeError, Value, aver_repr, list_from_vec, list_slice};
22
23pub fn register(global: &mut HashMap<String, Value>) {
24    let mut members = HashMap::new();
25    for method in &[
26        "empty", "set", "get", "remove", "has", "keys", "values", "entries", "len", "fromList",
27    ] {
28        members.insert(
29            method.to_string(),
30            Value::Builtin(format!("Map.{}", method)),
31        );
32    }
33    global.insert(
34        "Map".to_string(),
35        Value::Namespace {
36            name: "Map".to_string(),
37            members,
38        },
39    );
40}
41
42pub fn effects(_name: &str) -> &'static [&'static str] {
43    &[]
44}
45
46/// Returns `Some(result)` when `name` is owned by this namespace, `None` otherwise.
47pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
48    match name {
49        "Map.empty" => Some(empty(args)),
50        "Map.set" => Some(set(args)),
51        "Map.get" => Some(get(args)),
52        "Map.remove" => Some(remove(args)),
53        "Map.has" => Some(has(args)),
54        "Map.keys" => Some(keys(args)),
55        "Map.values" => Some(values(args)),
56        "Map.entries" => Some(entries(args)),
57        "Map.len" => Some(len(args)),
58        "Map.fromList" => Some(from_list(args)),
59        _ => None,
60    }
61}
62
63fn empty(args: &[Value]) -> Result<Value, RuntimeError> {
64    if !args.is_empty() {
65        return Err(RuntimeError::Error(format!(
66            "Map.empty() takes 0 arguments, got {}",
67            args.len()
68        )));
69    }
70    Ok(Value::Map(HashMap::new()))
71}
72
73fn set(args: &[Value]) -> Result<Value, RuntimeError> {
74    let [map_val, key, value] = three_args("Map.set", args)?;
75    let Value::Map(map) = map_val else {
76        return Err(RuntimeError::Error(
77            "Map.set() first argument must be a Map".to_string(),
78        ));
79    };
80    ensure_hashable_key("Map.set", key)?;
81    let mut out = map.clone();
82    out.insert(key.clone(), value.clone());
83    Ok(Value::Map(out))
84}
85
86fn get(args: &[Value]) -> Result<Value, RuntimeError> {
87    let [map_val, key] = two_args("Map.get", args)?;
88    let Value::Map(map) = map_val else {
89        return Err(RuntimeError::Error(
90            "Map.get() first argument must be a Map".to_string(),
91        ));
92    };
93    ensure_hashable_key("Map.get", key)?;
94    Ok(match map.get(key) {
95        Some(v) => Value::Some(Box::new(v.clone())),
96        None => Value::None,
97    })
98}
99
100fn remove(args: &[Value]) -> Result<Value, RuntimeError> {
101    let [map_val, key] = two_args("Map.remove", args)?;
102    let Value::Map(map) = map_val else {
103        return Err(RuntimeError::Error(
104            "Map.remove() first argument must be a Map".to_string(),
105        ));
106    };
107    ensure_hashable_key("Map.remove", key)?;
108    let mut out = map.clone();
109    out.remove(key);
110    Ok(Value::Map(out))
111}
112
113fn has(args: &[Value]) -> Result<Value, RuntimeError> {
114    let [map_val, key] = two_args("Map.has", args)?;
115    let Value::Map(map) = map_val else {
116        return Err(RuntimeError::Error(
117            "Map.has() first argument must be a Map".to_string(),
118        ));
119    };
120    ensure_hashable_key("Map.has", key)?;
121    Ok(Value::Bool(map.contains_key(key)))
122}
123
124fn keys(args: &[Value]) -> Result<Value, RuntimeError> {
125    let [map_val] = one_arg("Map.keys", args)?;
126    let Value::Map(map) = map_val else {
127        return Err(RuntimeError::Error(
128            "Map.keys() argument must be a Map".to_string(),
129        ));
130    };
131    let mut out = map.keys().cloned().collect::<Vec<_>>();
132    out.sort_by(compare_scalar_keys);
133    Ok(list_from_vec(out))
134}
135
136fn values(args: &[Value]) -> Result<Value, RuntimeError> {
137    let [map_val] = one_arg("Map.values", args)?;
138    let Value::Map(map) = map_val else {
139        return Err(RuntimeError::Error(
140            "Map.values() argument must be a Map".to_string(),
141        ));
142    };
143    let mut entries = map.iter().collect::<Vec<_>>();
144    entries.sort_by(|(k1, _), (k2, _)| compare_scalar_keys(k1, k2));
145    let out = entries
146        .into_iter()
147        .map(|(_, v)| v.clone())
148        .collect::<Vec<_>>();
149    Ok(list_from_vec(out))
150}
151
152fn entries(args: &[Value]) -> Result<Value, RuntimeError> {
153    let [map_val] = one_arg("Map.entries", args)?;
154    let Value::Map(map) = map_val else {
155        return Err(RuntimeError::Error(
156            "Map.entries() argument must be a Map".to_string(),
157        ));
158    };
159    let mut entries = map.iter().collect::<Vec<_>>();
160    entries.sort_by(|(k1, _), (k2, _)| compare_scalar_keys(k1, k2));
161    let out = entries
162        .into_iter()
163        .map(|(k, v)| Value::Tuple(vec![k.clone(), v.clone()]))
164        .collect::<Vec<_>>();
165    Ok(list_from_vec(out))
166}
167
168fn len(args: &[Value]) -> Result<Value, RuntimeError> {
169    let [map_val] = one_arg("Map.len", args)?;
170    let Value::Map(map) = map_val else {
171        return Err(RuntimeError::Error(
172            "Map.len() argument must be a Map".to_string(),
173        ));
174    };
175    Ok(Value::Int(map.len() as i64))
176}
177
178fn from_list(args: &[Value]) -> Result<Value, RuntimeError> {
179    let [pairs] = one_arg("Map.fromList", args)?;
180    let items = list_slice(pairs).ok_or_else(|| {
181        RuntimeError::Error(
182            "Map.fromList() argument must be a List of (key, value) tuples".to_string(),
183        )
184    })?;
185
186    let mut out = HashMap::new();
187    for (idx, pair) in items.iter().enumerate() {
188        let Value::Tuple(parts) = pair else {
189            return Err(RuntimeError::Error(format!(
190                "Map.fromList() item {} must be (key, value)",
191                idx + 1
192            )));
193        };
194        if parts.len() != 2 {
195            return Err(RuntimeError::Error(format!(
196                "Map.fromList() item {} must have 2 elements",
197                idx + 1
198            )));
199        }
200
201        let key = &parts[0];
202        let value = &parts[1];
203        ensure_hashable_key("Map.fromList", key)?;
204        out.insert(key.clone(), value.clone());
205    }
206    Ok(Value::Map(out))
207}
208
209fn is_hashable_key(value: &Value) -> bool {
210    matches!(
211        value,
212        Value::Int(_) | Value::Float(_) | Value::Str(_) | Value::Bool(_)
213    )
214}
215
216fn ensure_hashable_key(name: &str, value: &Value) -> Result<(), RuntimeError> {
217    if is_hashable_key(value) {
218        Ok(())
219    } else {
220        Err(RuntimeError::Error(format!(
221            "{}: key must be Int, Float, String, or Bool",
222            name
223        )))
224    }
225}
226
227fn compare_scalar_keys(a: &Value, b: &Value) -> Ordering {
228    match (a, b) {
229        (Value::Int(x), Value::Int(y)) => x.cmp(y),
230        (Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or_else(|| {
231            let xb = x.to_bits();
232            let yb = y.to_bits();
233            xb.cmp(&yb)
234        }),
235        (Value::Str(x), Value::Str(y)) => x.cmp(y),
236        (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
237        _ => aver_repr(a).cmp(&aver_repr(b)),
238    }
239}
240
241fn one_arg<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 1], RuntimeError> {
242    if args.len() != 1 {
243        return Err(RuntimeError::Error(format!(
244            "{}() takes 1 argument, got {}",
245            name,
246            args.len()
247        )));
248    }
249    Ok([&args[0]])
250}
251
252fn two_args<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 2], RuntimeError> {
253    if args.len() != 2 {
254        return Err(RuntimeError::Error(format!(
255            "{}() takes 2 arguments, got {}",
256            name,
257            args.len()
258        )));
259    }
260    Ok([&args[0], &args[1]])
261}
262
263fn three_args<'a>(name: &str, args: &'a [Value]) -> Result<[&'a Value; 3], RuntimeError> {
264    if args.len() != 3 {
265        return Err(RuntimeError::Error(format!(
266            "{}() takes 3 arguments, got {}",
267            name,
268            args.len()
269        )));
270    }
271    Ok([&args[0], &args[1], &args[2]])
272}