Skip to main content

shape_runtime/stdlib/
set_module.rs

1//! Native `set` module for unordered collections of unique elements.
2//!
3//! Backed by HashMap for O(1) lookup.
4//! Exports: set.new(), set.from_array(arr), set.add(s, item), set.remove(s, item),
5//!          set.contains(s, item), set.union(a, b), set.intersection(a, b),
6//!          set.difference(a, b), set.to_array(s), set.size(s)
7
8use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
9use shape_value::ValueWord;
10
11/// Create an empty set (HashMap with no entries).
12fn empty_set() -> ValueWord {
13    ValueWord::from_hashmap_pairs(Vec::new(), Vec::new())
14}
15
16/// Insert a key into a set (HashMap where all values are `true`).
17/// Returns a new set with the key added.
18fn set_insert(set: &ValueWord, item: &ValueWord) -> Result<ValueWord, String> {
19    let data = set
20        .as_hashmap_data()
21        .ok_or_else(|| "set: expected a set (HashMap)".to_string())?;
22
23    // Check if item already exists
24    if data.find_key(item).is_some() {
25        return Ok(set.clone());
26    }
27
28    let mut keys = data.keys.clone();
29    let mut values = data.values.clone();
30    keys.push(item.clone());
31    values.push(ValueWord::from_bool(true));
32    Ok(ValueWord::from_hashmap_pairs(keys, values))
33}
34
35/// Create the `set` module with set operations.
36pub fn create_set_module() -> ModuleExports {
37    let mut module = ModuleExports::new("set");
38    module.description = "Unordered collection of unique elements".to_string();
39
40    // set.new() -> set
41    module.add_function_with_schema(
42        "new",
43        |_args: &[ValueWord], _ctx: &ModuleContext| Ok(empty_set()),
44        ModuleFunction {
45            description: "Create a new empty set".to_string(),
46            params: vec![],
47            return_type: Some("HashMap".to_string()),
48        },
49    );
50
51    // set.from_array(arr) -> set
52    module.add_function_with_schema(
53        "from_array",
54        |args: &[ValueWord], _ctx: &ModuleContext| {
55            let arr = args
56                .first()
57                .and_then(|a| a.as_any_array())
58                .ok_or_else(|| "set.from_array() requires an array argument".to_string())?;
59
60            let items = arr.to_generic();
61            let mut result = empty_set();
62            for item in items.iter() {
63                result = set_insert(&result, item)?;
64            }
65            Ok(result)
66        },
67        ModuleFunction {
68            description: "Create a set from an array (deduplicates)".to_string(),
69            params: vec![ModuleParam {
70                name: "arr".to_string(),
71                type_name: "Array".to_string(),
72                required: true,
73                description: "Array of items to add to the set".to_string(),
74                ..Default::default()
75            }],
76            return_type: Some("HashMap".to_string()),
77        },
78    );
79
80    // set.add(s, item) -> set
81    module.add_function_with_schema(
82        "add",
83        |args: &[ValueWord], _ctx: &ModuleContext| {
84            let s = args
85                .first()
86                .ok_or_else(|| "set.add() requires a set argument".to_string())?;
87            let item = args
88                .get(1)
89                .ok_or_else(|| "set.add() requires an item argument".to_string())?;
90
91            set_insert(s, item)
92        },
93        ModuleFunction {
94            description: "Add an item to the set, returns new set".to_string(),
95            params: vec![
96                ModuleParam {
97                    name: "s".to_string(),
98                    type_name: "HashMap".to_string(),
99                    required: true,
100                    description: "The set".to_string(),
101                    ..Default::default()
102                },
103                ModuleParam {
104                    name: "item".to_string(),
105                    type_name: "any".to_string(),
106                    required: true,
107                    description: "Item to add".to_string(),
108                    ..Default::default()
109                },
110            ],
111            return_type: Some("HashMap".to_string()),
112        },
113    );
114
115    // set.remove(s, item) -> set
116    module.add_function_with_schema(
117        "remove",
118        |args: &[ValueWord], _ctx: &ModuleContext| {
119            let s = args
120                .first()
121                .ok_or_else(|| "set.remove() requires a set argument".to_string())?;
122            let item = args
123                .get(1)
124                .ok_or_else(|| "set.remove() requires an item argument".to_string())?;
125
126            let data = s
127                .as_hashmap_data()
128                .ok_or_else(|| "set.remove(): expected a set (HashMap)".to_string())?;
129
130            if let Some(idx) = data.find_key(item) {
131                let mut keys = data.keys.clone();
132                let mut values = data.values.clone();
133                keys.remove(idx);
134                values.remove(idx);
135                Ok(ValueWord::from_hashmap_pairs(keys, values))
136            } else {
137                Ok(s.clone())
138            }
139        },
140        ModuleFunction {
141            description: "Remove an item from the set, returns new set".to_string(),
142            params: vec![
143                ModuleParam {
144                    name: "s".to_string(),
145                    type_name: "HashMap".to_string(),
146                    required: true,
147                    description: "The set".to_string(),
148                    ..Default::default()
149                },
150                ModuleParam {
151                    name: "item".to_string(),
152                    type_name: "any".to_string(),
153                    required: true,
154                    description: "Item to remove".to_string(),
155                    ..Default::default()
156                },
157            ],
158            return_type: Some("HashMap".to_string()),
159        },
160    );
161
162    // set.contains(s, item) -> bool
163    module.add_function_with_schema(
164        "contains",
165        |args: &[ValueWord], _ctx: &ModuleContext| {
166            let s = args
167                .first()
168                .ok_or_else(|| "set.contains() requires a set argument".to_string())?;
169            let item = args
170                .get(1)
171                .ok_or_else(|| "set.contains() requires an item argument".to_string())?;
172
173            let data = s
174                .as_hashmap_data()
175                .ok_or_else(|| "set.contains(): expected a set (HashMap)".to_string())?;
176
177            Ok(ValueWord::from_bool(data.find_key(item).is_some()))
178        },
179        ModuleFunction {
180            description: "Check if set contains an item".to_string(),
181            params: vec![
182                ModuleParam {
183                    name: "s".to_string(),
184                    type_name: "HashMap".to_string(),
185                    required: true,
186                    description: "The set".to_string(),
187                    ..Default::default()
188                },
189                ModuleParam {
190                    name: "item".to_string(),
191                    type_name: "any".to_string(),
192                    required: true,
193                    description: "Item to check".to_string(),
194                    ..Default::default()
195                },
196            ],
197            return_type: Some("bool".to_string()),
198        },
199    );
200
201    // set.union(a, b) -> set
202    module.add_function_with_schema(
203        "union",
204        |args: &[ValueWord], _ctx: &ModuleContext| {
205            let a = args
206                .first()
207                .ok_or_else(|| "set.union() requires two set arguments".to_string())?;
208            let b = args
209                .get(1)
210                .ok_or_else(|| "set.union() requires two set arguments".to_string())?;
211
212            let a_data = a
213                .as_hashmap_data()
214                .ok_or_else(|| "set.union(): first argument must be a set".to_string())?;
215            let b_data = b
216                .as_hashmap_data()
217                .ok_or_else(|| "set.union(): second argument must be a set".to_string())?;
218
219            let mut result = a.clone();
220            for key in &b_data.keys {
221                if a_data.find_key(key).is_none() {
222                    result = set_insert(&result, key)?;
223                }
224            }
225            Ok(result)
226        },
227        ModuleFunction {
228            description: "Union of two sets".to_string(),
229            params: vec![
230                ModuleParam {
231                    name: "a".to_string(),
232                    type_name: "HashMap".to_string(),
233                    required: true,
234                    description: "First set".to_string(),
235                    ..Default::default()
236                },
237                ModuleParam {
238                    name: "b".to_string(),
239                    type_name: "HashMap".to_string(),
240                    required: true,
241                    description: "Second set".to_string(),
242                    ..Default::default()
243                },
244            ],
245            return_type: Some("HashMap".to_string()),
246        },
247    );
248
249    // set.intersection(a, b) -> set
250    module.add_function_with_schema(
251        "intersection",
252        |args: &[ValueWord], _ctx: &ModuleContext| {
253            let a = args
254                .first()
255                .ok_or_else(|| "set.intersection() requires two set arguments".to_string())?;
256            let b = args
257                .get(1)
258                .ok_or_else(|| "set.intersection() requires two set arguments".to_string())?;
259
260            let a_data = a
261                .as_hashmap_data()
262                .ok_or_else(|| "set.intersection(): first argument must be a set".to_string())?;
263            let b_data = b
264                .as_hashmap_data()
265                .ok_or_else(|| "set.intersection(): second argument must be a set".to_string())?;
266
267            let mut result = empty_set();
268            for key in &a_data.keys {
269                if b_data.find_key(key).is_some() {
270                    result = set_insert(&result, key)?;
271                }
272            }
273            Ok(result)
274        },
275        ModuleFunction {
276            description: "Intersection of two sets".to_string(),
277            params: vec![
278                ModuleParam {
279                    name: "a".to_string(),
280                    type_name: "HashMap".to_string(),
281                    required: true,
282                    description: "First set".to_string(),
283                    ..Default::default()
284                },
285                ModuleParam {
286                    name: "b".to_string(),
287                    type_name: "HashMap".to_string(),
288                    required: true,
289                    description: "Second set".to_string(),
290                    ..Default::default()
291                },
292            ],
293            return_type: Some("HashMap".to_string()),
294        },
295    );
296
297    // set.difference(a, b) -> set
298    module.add_function_with_schema(
299        "difference",
300        |args: &[ValueWord], _ctx: &ModuleContext| {
301            let a = args
302                .first()
303                .ok_or_else(|| "set.difference() requires two set arguments".to_string())?;
304            let b = args
305                .get(1)
306                .ok_or_else(|| "set.difference() requires two set arguments".to_string())?;
307
308            let a_data = a
309                .as_hashmap_data()
310                .ok_or_else(|| "set.difference(): first argument must be a set".to_string())?;
311            let b_data = b
312                .as_hashmap_data()
313                .ok_or_else(|| "set.difference(): second argument must be a set".to_string())?;
314
315            let mut result = empty_set();
316            for key in &a_data.keys {
317                if b_data.find_key(key).is_none() {
318                    result = set_insert(&result, key)?;
319                }
320            }
321            Ok(result)
322        },
323        ModuleFunction {
324            description: "Difference (a - b)".to_string(),
325            params: vec![
326                ModuleParam {
327                    name: "a".to_string(),
328                    type_name: "HashMap".to_string(),
329                    required: true,
330                    description: "First set".to_string(),
331                    ..Default::default()
332                },
333                ModuleParam {
334                    name: "b".to_string(),
335                    type_name: "HashMap".to_string(),
336                    required: true,
337                    description: "Second set".to_string(),
338                    ..Default::default()
339                },
340            ],
341            return_type: Some("HashMap".to_string()),
342        },
343    );
344
345    // set.to_array(s) -> Array
346    module.add_function_with_schema(
347        "to_array",
348        |args: &[ValueWord], _ctx: &ModuleContext| {
349            let s = args
350                .first()
351                .ok_or_else(|| "set.to_array() requires a set argument".to_string())?;
352
353            let data = s
354                .as_hashmap_data()
355                .ok_or_else(|| "set.to_array(): expected a set (HashMap)".to_string())?;
356
357            Ok(ValueWord::from_array(std::sync::Arc::new(
358                data.keys.clone(),
359            )))
360        },
361        ModuleFunction {
362            description: "Convert set to array".to_string(),
363            params: vec![ModuleParam {
364                name: "s".to_string(),
365                type_name: "HashMap".to_string(),
366                required: true,
367                description: "The set".to_string(),
368                ..Default::default()
369            }],
370            return_type: Some("Array".to_string()),
371        },
372    );
373
374    // set.size(s) -> int
375    module.add_function_with_schema(
376        "size",
377        |args: &[ValueWord], _ctx: &ModuleContext| {
378            let s = args
379                .first()
380                .ok_or_else(|| "set.size() requires a set argument".to_string())?;
381
382            let data = s
383                .as_hashmap_data()
384                .ok_or_else(|| "set.size(): expected a set (HashMap)".to_string())?;
385
386            Ok(ValueWord::from_i64(data.keys.len() as i64))
387        },
388        ModuleFunction {
389            description: "Get the number of elements".to_string(),
390            params: vec![ModuleParam {
391                name: "s".to_string(),
392                type_name: "HashMap".to_string(),
393                required: true,
394                description: "The set".to_string(),
395                ..Default::default()
396            }],
397            return_type: Some("int".to_string()),
398        },
399    );
400
401    module
402}