runmat_runtime/builtins/containers/map/
containers.map.rs

1//! MATLAB-compatible `containers.Map` constructor and methods for RunMat.
2
3use std::collections::HashMap;
4use std::sync::{
5    atomic::{AtomicU64, Ordering},
6    RwLock,
7};
8
9use once_cell::sync::Lazy;
10use runmat_builtins::{CharArray, HandleRef, IntValue, LogicalArray, StructValue, Tensor, Value};
11use runmat_macros::runtime_builtin;
12
13use crate::builtins::common::random_args::keyword_of;
14use crate::builtins::common::spec::{
15    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
16    ReductionNaN, ResidencyPolicy, ShapeRequirements,
17};
18#[cfg(feature = "doc_export")]
19use crate::register_builtin_doc_text;
20use crate::{gather_if_needed, register_builtin_fusion_spec, register_builtin_gpu_spec};
21
22const CLASS_NAME: &str = "containers.Map";
23const MISSING_KEY_ERR: &str = "containers.Map: The specified key is not present in this container.";
24
25#[cfg(feature = "doc_export")]
26pub const DOC_MD: &str = r#"---
27title: "containers.Map"
28category: "containers/map"
29keywords: ["containers.Map", "map", "dictionary", "hash map", "lookup"]
30summary: "Create MATLAB-compatible dictionary objects that map keys to values."
31references:
32  - https://www.mathworks.com/help/matlab/ref/containers.map-class.html
33gpu_support:
34  elementwise: false
35  reduction: false
36  precisions: []
37  broadcasting: "none"
38  notes: "Map storage lives on the host. GPU inputs are gathered when constructing maps or fetching values."
39fusion:
40  elementwise: false
41  reduction: false
42  max_inputs: 0
43  constants: "inline"
44requires_feature: null
45tested:
46  unit: "builtins::containers::map::containers_map::tests"
47  integration: "builtins::containers::map::containers_map::tests::doc_examples_present"
48---
49
50# What does the `containers.Map` function do in MATLAB / RunMat?
51`containers.Map` builds dictionary objects that associate unique keys with values. Keys can be
52character vectors, string scalars, or numeric scalars (double/int32/uint32/int64/uint64/logical).
53Values default to MATLAB's `'any'` semantics, letting you store arbitrary scalars, arrays, structs,
54or handle objects. Each map tracks insertion order, supports key-based indexing, and exposes methods
55such as `keys`, `values`, `isKey`, and `remove`.
56
57## How does the `containers.Map` function behave in MATLAB / RunMat?
58- Keys must be unique. Constructing a map or assigning a key that already exists overwrites the
59  stored value (matching MATLAB's behaviour).
60- The `KeyType`, `ValueType`, and `Count` properties are readable with dot-indexing.
61- `map(key)` returns the associated value; requesting a missing key raises the MATLAB-compatible
62  error *"The specified key is not present in this container."*
63- Assignments of the form `map(key) = value` update or insert entries.
64- Methods `keys(map)`, `values(map)`, `isKey(map, keySpec)`, and `remove(map, keySpec)` are fully
65  compatible. When `keySpec` is a cell or string array, the result matches MATLAB's shape.
66- GPU tensors presented as values are gathered to host memory before insertion. When values or keys
67  arrive on the GPU and need to be expanded element-wise (for example, vector-valued constructor
68  arguments), RunMat downloads them to materialise individual scalars.
69- The `'UniformValues'` flag is accepted; when `true`, RunMat validates that every inserted value has
70  the same MATLAB class. Retrieval still returns a cell array, matching MATLAB behaviour when the
71  value type is `'any'`.
72
73## `containers.Map` Function GPU Execution Behaviour
74The data structure itself resides on the CPU. When you construct a map with GPU arrays, RunMat first
75downloads the inputs so it can perform MATLAB-compatible validation and coercion. Maps never retain
76device buffers internally, so the GPU provider does not need to implement special hooks for this
77builtin.
78
79## Examples of using the `containers.Map` function in MATLAB / RunMat
80
81### Create an empty map with default types
82```matlab
83m = containers.Map();
84m.KeyType
85m.ValueType
86m.Count
87```
88
89Expected output:
90```matlab
91ans =
92    'char'
93ans =
94    'any'
95ans =
96     0
97```
98
99### Build a map from paired cell arrays
100```matlab
101keys = {'apple', 'pear', 'banana'};
102vals = {42, [1 2 3], true};
103fruit = containers.Map(keys, vals);
104energy = fruit('apple');
105```
106
107Expected output:
108```matlab
109energy =
110    42
111```
112
113### Update an existing key and add a new one
114```matlab
115fruit('apple') = 99;
116fruit('peach') = struct('ripe', true);
117```
118
119Expected output:
120```matlab
121fruit('apple')
122ans =
123    99
124```
125
126### Query keys, values, and membership
127```matlab
128allKeys = keys(fruit);
129allVals = values(fruit);
130mask = isKey(fruit, {'apple', 'durian'});
131```
132
133Expected output:
134```matlab
135allKeys =
136  1×4 cell array
137    {'apple'}    {'pear'}    {'banana'}    {'peach'}
138
139allVals =
140  1×4 cell array
141    {[99]}    {[1 2 3]}    {[1]}    {1×1 struct}
142
143mask =
144  1×2 logical array
145     1     0
146```
147
148### Remove keys and inspect the map length
149```matlab
150remove(fruit, {'pear', 'banana'});
151n = length(fruit);
152remaining = keys(fruit);
153```
154
155Expected output:
156```matlab
157n =
158     2
159remaining =
160  1×2 cell array
161    {'apple'}    {'peach'}
162```
163
164## FAQ
165
166### Which key types are supported?
167`containers.Map` accepts `'char'`, `'string'`, `'double'`, `'single'`, `'int32'`, `'uint32'`, `'int64'`,
168`'uint64'`, and `'logical'`. Keys supplied during construction or assignment are coerced to the
169declared type and must be scalar.
170
171### What happens when I provide duplicate keys at construction time?
172Duplicate keys raise the same error as MATLAB: *"Duplicate key name was provided."* During
173assignment, duplicate keys overwrite the existing value.
174
175### Does RunMat honour `'UniformValues', true`?
176Yes. When this option is set, RunMat enforces that each inserted value matches the MATLAB class of
177the first value. Retrieval still uses cell arrays, mirroring MATLAB when `'ValueType'` is `'any'`.
178
179### Can I store GPU arrays as map values?
180Yes. RunMat automatically gathers GPU tensors to host memory before inserting them so it can apply
181the same validation and coercion rules as MATLAB. This ensures constructors that rely on vector
182expansion continue to produce predictable host-side values.
183
184### How does `length(map)` behave?
185`length(map)` returns the number of stored keys (identical to the `Count` property). `size(map)`
186remains `[1 1]`, matching MATLAB's handle semantics.
187
188### What error is raised when a key is missing?
189Indexing a missing key produces the MATLAB-compatible error message *"The specified key is not
190present in this container."*
191
192### Does the map preserve insertion order?
193Yes. `keys(map)` and `values(map)` return entries in the order they were first inserted, matching the
194behaviour of MATLAB's `containers.Map`.
195
196### Is the implementation thread-safe?
197Yes. A global read/write lock guards the backing storage so concurrent reads are allowed while write
198operations remain exclusive.
199
200### How do I remove every entry?
201Call `remove(map, keys(map))` or reassign a new empty map. RunMat currently keeps the internal
202storage until the handle is cleared, matching MATLAB's lifetime semantics.
203
204### What happens if I pass a non-scalar key?
205Keys must be scalar. Passing vectors, matrices, or nested cell arrays of keys raises a descriptive
206error pointing to the offending argument.
207
208## See Also
209[keys](./containers.Map.keys), [values](./containers.Map.values), [isKey](./containers.Map.isKey),
210[remove](./containers.Map.remove), [length](../../array/introspection/length)
211"#;
212
213pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
214    name: "containers.Map",
215    op_kind: GpuOpKind::Custom("map"),
216    supported_precisions: &[],
217    broadcast: BroadcastSemantics::None,
218    provider_hooks: &[],
219    constant_strategy: ConstantStrategy::InlineLiteral,
220    residency: ResidencyPolicy::GatherImmediately,
221    nan_mode: ReductionNaN::Include,
222    two_pass_threshold: None,
223    workgroup_size: None,
224    accepts_nan_mode: false,
225    notes: "Map storage is host-resident; GPU inputs are gathered only when split into multiple entries.",
226};
227
228register_builtin_gpu_spec!(GPU_SPEC);
229
230pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
231    name: "containers.Map",
232    shape: ShapeRequirements::Any,
233    constant_strategy: ConstantStrategy::InlineLiteral,
234    elementwise: None,
235    reduction: None,
236    emits_nan: false,
237    notes: "Handles act as fusion sinks; map construction terminates GPU fusion plans.",
238};
239
240register_builtin_fusion_spec!(FUSION_SPEC);
241
242#[cfg(feature = "doc_export")]
243register_builtin_doc_text!("containers.Map", DOC_MD);
244
245static NEXT_ID: AtomicU64 = AtomicU64::new(1);
246static MAP_REGISTRY: Lazy<RwLock<HashMap<u64, MapStore>>> =
247    Lazy::new(|| RwLock::new(HashMap::new()));
248
249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250enum KeyType {
251    Char,
252    String,
253    Double,
254    Single,
255    Int32,
256    UInt32,
257    Int64,
258    UInt64,
259    Logical,
260}
261
262impl KeyType {
263    fn matlab_name(self) -> &'static str {
264        match self {
265            KeyType::Char => "char",
266            KeyType::String => "string",
267            KeyType::Double => "double",
268            KeyType::Single => "single",
269            KeyType::Int32 => "int32",
270            KeyType::UInt32 => "uint32",
271            KeyType::Int64 => "int64",
272            KeyType::UInt64 => "uint64",
273            KeyType::Logical => "logical",
274        }
275    }
276
277    fn parse(value: &Value) -> Result<Self, String> {
278        let text = string_from_value(value, "containers.Map: expected a KeyType string")?;
279        match text.to_ascii_lowercase().as_str() {
280            "char" | "character" => Ok(KeyType::Char),
281            "string" => Ok(KeyType::String),
282            "double" => Ok(KeyType::Double),
283            "single" => Ok(KeyType::Single),
284            "int32" => Ok(KeyType::Int32),
285            "uint32" => Ok(KeyType::UInt32),
286            "int64" => Ok(KeyType::Int64),
287            "uint64" => Ok(KeyType::UInt64),
288            "logical" => Ok(KeyType::Logical),
289            other => Err(format!(
290                "containers.Map: unsupported KeyType '{other}'. Valid types: char, string, double, int32, uint32, int64, uint64, logical."
291            )),
292        }
293    }
294}
295
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297enum ValueType {
298    Any,
299    Char,
300    String,
301    Double,
302    Single,
303    Logical,
304}
305
306impl ValueType {
307    fn matlab_name(self) -> &'static str {
308        match self {
309            ValueType::Any => "any",
310            ValueType::Char => "char",
311            ValueType::String => "string",
312            ValueType::Double => "double",
313            ValueType::Single => "single",
314            ValueType::Logical => "logical",
315        }
316    }
317
318    fn parse(value: &Value) -> Result<Self, String> {
319        let text = string_from_value(value, "containers.Map: expected a ValueType string")?;
320        match text.to_ascii_lowercase().as_str() {
321            "any" => Ok(ValueType::Any),
322            "char" | "character" => Ok(ValueType::Char),
323            "string" => Ok(ValueType::String),
324            "double" => Ok(ValueType::Double),
325            "single" => Ok(ValueType::Single),
326            "logical" => Ok(ValueType::Logical),
327            other => Err(format!(
328                "containers.Map: unsupported ValueType '{other}'. Valid types: any, char, string, double, single, logical."
329            )),
330        }
331    }
332
333    fn normalize(&self, value: Value) -> Result<Value, String> {
334        match self {
335            ValueType::Any => Ok(value),
336            ValueType::Char => {
337                let chars = char_array_from_value(&value)?;
338                Ok(Value::CharArray(chars))
339            }
340            ValueType::String => {
341                let text =
342                    string_from_value(&value, "containers.Map: values must be string scalars")?;
343                Ok(Value::String(text))
344            }
345            ValueType::Double | ValueType::Single => normalize_numeric_value(value),
346            ValueType::Logical => normalize_logical_value(value),
347        }
348    }
349}
350
351#[derive(Clone, PartialEq, Eq, Hash)]
352enum NormalizedKey {
353    String(String),
354    Float(u64),
355    Int(i64),
356    UInt(u64),
357    Bool(bool),
358}
359
360#[derive(Clone)]
361struct MapEntry {
362    normalized: NormalizedKey,
363    key_value: Value,
364    value: Value,
365}
366
367struct MapStore {
368    key_type: KeyType,
369    value_type: ValueType,
370    uniform_values: bool,
371    uniform_class: Option<ValueClass>,
372    entries: Vec<MapEntry>,
373    index: HashMap<NormalizedKey, usize>,
374}
375
376impl MapStore {
377    fn new(key_type: KeyType, value_type: ValueType, uniform_values: bool) -> Self {
378        Self {
379            key_type,
380            value_type,
381            uniform_values,
382            uniform_class: None,
383            entries: Vec::new(),
384            index: HashMap::new(),
385        }
386    }
387
388    fn len(&self) -> usize {
389        self.entries.len()
390    }
391
392    fn contains(&self, key: &NormalizedKey) -> bool {
393        self.index.contains_key(key)
394    }
395
396    fn get(&self, key: &NormalizedKey) -> Option<Value> {
397        self.index
398            .get(key)
399            .map(|&idx| self.entries[idx].value.clone())
400    }
401
402    fn insert_new(&mut self, mut entry: MapEntry) -> Result<(), String> {
403        if self.index.contains_key(&entry.normalized) {
404            return Err("containers.Map: Duplicate key name was provided.".to_string());
405        }
406        entry.value = self.normalize_value(entry.value)?;
407        self.track_uniform_class(&entry.value)?;
408        let idx = self.entries.len();
409        self.entries.push(entry.clone());
410        self.index.insert(entry.normalized, idx);
411        Ok(())
412    }
413
414    fn set(&mut self, mut entry: MapEntry) -> Result<(), String> {
415        entry.value = self.normalize_value(entry.value)?;
416        self.track_uniform_class(&entry.value)?;
417        if let Some(&idx) = self.index.get(&entry.normalized) {
418            self.entries[idx].value = entry.value.clone();
419            self.entries[idx].key_value = entry.key_value;
420        } else {
421            let idx = self.entries.len();
422            self.entries.push(entry.clone());
423            self.index.insert(entry.normalized, idx);
424        }
425        Ok(())
426    }
427
428    fn remove(&mut self, key: &NormalizedKey) -> Result<(), String> {
429        let idx = match self.index.get(key) {
430            Some(&idx) => idx,
431            None => {
432                return Err(MISSING_KEY_ERR.to_string());
433            }
434        };
435        self.entries.remove(idx);
436        self.index.clear();
437        for (pos, entry) in self.entries.iter().enumerate() {
438            self.index.insert(entry.normalized.clone(), pos);
439        }
440        if self.entries.is_empty() {
441            self.uniform_class = None;
442        }
443        Ok(())
444    }
445
446    fn keys(&self) -> Vec<Value> {
447        self.entries
448            .iter()
449            .map(|entry| entry.key_value.clone())
450            .collect()
451    }
452
453    fn values(&self) -> Vec<Value> {
454        self.entries
455            .iter()
456            .map(|entry| entry.value.clone())
457            .collect()
458    }
459
460    fn normalize_value(&self, value: Value) -> Result<Value, String> {
461        self.value_type.normalize(value)
462    }
463
464    fn track_uniform_class(&mut self, value: &Value) -> Result<(), String> {
465        if !self.uniform_values {
466            return Ok(());
467        }
468        let class = ValueClass::from_value(value);
469        if let Some(existing) = &self.uniform_class {
470            if existing != &class {
471                return Err("containers.Map: UniformValues=true requires all values to share the same MATLAB class."
472                    .to_string());
473            }
474        } else {
475            self.uniform_class = Some(class);
476        }
477        Ok(())
478    }
479}
480
481#[derive(Clone, Debug, PartialEq, Eq)]
482enum ValueClass {
483    Char,
484    String,
485    Double,
486    Logical,
487    Int,
488    UInt,
489    Cell,
490    Struct,
491    Object,
492    Other(&'static str),
493}
494
495impl ValueClass {
496    fn from_value(value: &Value) -> Self {
497        match value {
498            Value::CharArray(_) => ValueClass::Char,
499            Value::String(_) | Value::StringArray(_) => ValueClass::String,
500            Value::Num(_) | Value::Tensor(_) | Value::ComplexTensor(_) => ValueClass::Double,
501            Value::Bool(_) | Value::LogicalArray(_) => ValueClass::Logical,
502            Value::Int(i) => match i {
503                IntValue::I8(_) | IntValue::I16(_) | IntValue::I32(_) | IntValue::I64(_) => {
504                    ValueClass::Int
505                }
506                IntValue::U8(_) | IntValue::U16(_) | IntValue::U32(_) | IntValue::U64(_) => {
507                    ValueClass::UInt
508                }
509            },
510            Value::Cell(_) => ValueClass::Cell,
511            Value::Struct(_) => ValueClass::Struct,
512            Value::Object(_) | Value::HandleObject(_) | Value::Listener(_) => ValueClass::Object,
513            _ => ValueClass::Other("other"),
514        }
515    }
516}
517
518struct ConstructorArgs {
519    key_type: KeyType,
520    value_type: ValueType,
521    uniform_values: bool,
522    keys: Vec<KeyCandidate>,
523    values: Vec<Value>,
524}
525
526struct KeyCandidate {
527    normalized: NormalizedKey,
528    canonical: Value,
529}
530
531#[runtime_builtin(
532    name = "containers.Map",
533    category = "containers/map",
534    summary = "Create MATLAB-style dictionary objects that map keys to values.",
535    keywords = "map,containers.Map,dictionary,hash map,lookup",
536    accel = "metadata",
537    sink = true
538)]
539fn containers_map_builtin(args: Vec<Value>) -> Result<Value, String> {
540    let parsed = parse_constructor_args(args)?;
541    let store = build_store(parsed)?;
542    allocate_handle(store)
543}
544
545#[runtime_builtin(name = "containers.Map.keys")]
546fn containers_map_keys(map: Value) -> Result<Value, String> {
547    with_store(&map, |store| {
548        let values = store.keys();
549        make_row_cell(values)
550    })
551}
552
553#[runtime_builtin(name = "containers.Map.values")]
554fn containers_map_values(map: Value) -> Result<Value, String> {
555    with_store(&map, |store| {
556        let values = store.values();
557        make_row_cell(values)
558    })
559}
560
561#[runtime_builtin(name = "containers.Map.isKey")]
562fn containers_map_is_key(map: Value, key_spec: Value) -> Result<Value, String> {
563    with_store(&map, |store| {
564        let collection = collect_key_spec(&key_spec, store.key_type)?;
565        let mut flags = Vec::with_capacity(collection.values.len());
566        for value in &collection.values {
567            let normalized = normalize_key(value, store.key_type)?;
568            flags.push(store.contains(&normalized));
569        }
570        if collection.values.len() == 1 {
571            Ok(Value::Bool(flags[0]))
572        } else {
573            let data: Vec<u8> = flags.into_iter().map(|b| if b { 1 } else { 0 }).collect();
574            let logical = LogicalArray::new(data, collection.shape)
575                .map_err(|e| format!("containers.Map: {e}"))?;
576            Ok(Value::LogicalArray(logical))
577        }
578    })
579}
580
581#[runtime_builtin(name = "containers.Map.remove")]
582fn containers_map_remove(map: Value, key_spec: Value) -> Result<Value, String> {
583    with_store_mut(&map, |store| {
584        let collection = collect_key_spec(&key_spec, store.key_type)?;
585        for value in &collection.values {
586            let normalized = normalize_key(value, store.key_type)?;
587            store.remove(&normalized)?;
588        }
589        Ok(())
590    })?;
591    Ok(map)
592}
593
594#[runtime_builtin(name = "containers.Map.subsref")]
595fn containers_map_subsref(map: Value, kind: String, payload: Value) -> Result<Value, String> {
596    if !matches!(map, Value::HandleObject(_)) {
597        return Err(format!(
598            "containers.Map: subsref expects a containers.Map handle, got {map:?}"
599        ));
600    }
601    match kind.as_str() {
602        "()" => {
603            let mut args = extract_key_arguments(&payload)?;
604            if args.is_empty() {
605                return Err("containers.Map: indexing requires at least one key".to_string());
606            }
607            if args.len() != 1 {
608                return Err("containers.Map: indexing expects a single key argument".to_string());
609            }
610            let key_arg = args.remove(0);
611            with_store(&map, |store| {
612                let collection = collect_key_spec(&key_arg, store.key_type)?;
613                if collection.values.is_empty() {
614                    return crate::make_cell_with_shape(Vec::new(), collection.shape.clone())
615                        .map_err(|e| format!("containers.Map: {e}"));
616                }
617                if collection.values.len() == 1 {
618                    let normalized = normalize_key(&collection.values[0], store.key_type)?;
619                    store
620                        .get(&normalized)
621                        .ok_or_else(|| MISSING_KEY_ERR.to_string())
622                } else {
623                    let mut results = Vec::with_capacity(collection.values.len());
624                    for value in &collection.values {
625                        let normalized = normalize_key(value, store.key_type)?;
626                        let stored = store
627                            .get(&normalized)
628                            .ok_or_else(|| MISSING_KEY_ERR.to_string())?;
629                        results.push(stored);
630                    }
631                    crate::make_cell_with_shape(results, collection.shape.clone())
632                        .map_err(|e| format!("containers.Map: {e}"))
633                }
634            })
635        }
636        "." => {
637            let field = string_from_value(&payload, "containers.Map: property name must be text")?;
638            with_store(&map, |store| match field.to_ascii_lowercase().as_str() {
639                "count" => Ok(Value::Num(store.len() as f64)),
640                "keytype" => char_array_value(store.key_type.matlab_name()),
641                "valuetype" => char_array_value(store.value_type.matlab_name()),
642                other => Err(format!("containers.Map: no such property '{other}'")),
643            })
644        }
645        "{}" => Err("containers.Map: curly-brace indexing is not supported.".to_string()),
646        other => Err(format!(
647            "containers.Map: unsupported indexing kind '{other}'"
648        )),
649    }
650}
651
652#[runtime_builtin(name = "containers.Map.subsasgn")]
653fn containers_map_subsasgn(
654    map: Value,
655    kind: String,
656    payload: Value,
657    rhs: Value,
658) -> Result<Value, String> {
659    if !matches!(map, Value::HandleObject(_)) {
660        return Err(format!(
661            "containers.Map: subsasgn expects a containers.Map handle, got {map:?}"
662        ));
663    }
664    match kind.as_str() {
665        "()" => {
666            let mut args = extract_key_arguments(&payload)?;
667            if args.is_empty() {
668                return Err("containers.Map: assignment requires at least one key".to_string());
669            }
670            if args.len() != 1 {
671                return Err("containers.Map: assignment expects a single key argument".to_string());
672            }
673            let key_arg = args.remove(0);
674            with_store_mut(&map, move |store| {
675                let KeyCollection {
676                    values: key_values, ..
677                } = collect_key_spec(&key_arg, store.key_type)?;
678                let values = expand_assignment_values(rhs.clone(), key_values.len())?;
679                for (key_raw, value) in key_values.into_iter().zip(values.into_iter()) {
680                    let (normalized, canonical) = canonicalize_key(key_raw, store.key_type)?;
681                    let entry = MapEntry {
682                        normalized,
683                        key_value: canonical,
684                        value,
685                    };
686                    store.set(entry)?;
687                }
688                Ok(())
689            })?;
690            Ok(map)
691        }
692        "." => Err("containers.Map: property assignments are not supported.".to_string()),
693        "{}" => Err("containers.Map: curly-brace assignment is not supported.".to_string()),
694        other => Err(format!(
695            "containers.Map: unsupported assignment kind '{other}'"
696        )),
697    }
698}
699
700fn parse_constructor_args(args: Vec<Value>) -> Result<ConstructorArgs, String> {
701    let mut index = 0usize;
702    let mut keys_input: Option<Value> = None;
703    let mut values_input: Option<Value> = None;
704
705    if index < args.len() && keyword_of(&args[index]).is_none() {
706        if args.len() < 2 {
707            return Err("containers.Map: constructor requires both keys and values when either is provided."
708                .to_string());
709        }
710        keys_input = Some(args[index].clone());
711        values_input = Some(args[index + 1].clone());
712        index += 2;
713    }
714
715    let mut key_type = KeyType::Char;
716    let mut value_type = ValueType::Any;
717    let mut uniform_values = false;
718    while index < args.len() {
719        let keyword = keyword_of(&args[index])
720            .ok_or_else(|| "containers.Map: expected option name (e.g. 'KeyType')".to_string())?;
721        index += 1;
722        let Some(value) = args.get(index) else {
723            return Err(format!(
724                "containers.Map: missing value for option '{keyword}'"
725            ));
726        };
727        index += 1;
728        match keyword.as_str() {
729            "keytype" => key_type = KeyType::parse(value)?,
730            "valuetype" => value_type = ValueType::parse(value)?,
731            "uniformvalues" => {
732                uniform_values =
733                    bool_from_value(value, "containers.Map: UniformValues must be logical")?
734            }
735            "comparisonmethod" => {
736                let text =
737                    string_from_value(value, "containers.Map: ComparisonMethod must be a string")?;
738                let lowered = text.to_ascii_lowercase();
739                if lowered != "strcmp" {
740                    return Err(
741                        "containers.Map: only ComparisonMethod='strcmp' is supported.".to_string(),
742                    );
743                }
744            }
745            other => {
746                return Err(format!("containers.Map: unrecognised option '{other}'"));
747            }
748        }
749    }
750
751    let keys = match keys_input {
752        Some(value) => prepare_keys(value, key_type)?,
753        None => Vec::new(),
754    };
755
756    let values = match values_input {
757        Some(value) => prepare_values(value)?,
758        None => Vec::new(),
759    };
760
761    if keys.len() != values.len() {
762        return Err(format!(
763            "containers.Map: number of keys ({}) must match number of values ({})",
764            keys.len(),
765            values.len()
766        ));
767    }
768
769    Ok(ConstructorArgs {
770        key_type,
771        value_type,
772        uniform_values,
773        keys,
774        values,
775    })
776}
777
778fn build_store(args: ConstructorArgs) -> Result<MapStore, String> {
779    let mut store = MapStore::new(args.key_type, args.value_type, args.uniform_values);
780    for (candidate, value) in args.keys.into_iter().zip(args.values.into_iter()) {
781        store.insert_new(MapEntry {
782            normalized: candidate.normalized,
783            key_value: candidate.canonical,
784            value,
785        })?;
786    }
787    Ok(store)
788}
789
790fn allocate_handle(store: MapStore) -> Result<Value, String> {
791    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
792    MAP_REGISTRY
793        .write()
794        .map_err(|_| "containers.Map: registry lock poisoned".to_string())?
795        .insert(id, store);
796    let mut struct_value = StructValue::new();
797    struct_value
798        .fields
799        .insert("id".to_string(), Value::Int(IntValue::U64(id)));
800    let storage = Value::Struct(struct_value);
801    let gc = runmat_gc::gc_allocate(storage).map_err(|e| format!("containers.Map: {e}"))?;
802    Ok(Value::HandleObject(HandleRef {
803        class_name: CLASS_NAME.to_string(),
804        target: gc,
805        valid: true,
806    }))
807}
808
809fn with_store<F, R>(map: &Value, f: F) -> Result<R, String>
810where
811    F: FnOnce(&MapStore) -> Result<R, String>,
812{
813    let handle = extract_handle(map)?;
814    ensure_handle(handle)?;
815    let id = map_id(handle)?;
816    let guard = MAP_REGISTRY
817        .read()
818        .map_err(|_| "containers.Map: registry lock poisoned".to_string())?;
819    let store = guard
820        .get(&id)
821        .ok_or_else(|| "containers.Map: internal storage not found".to_string())?;
822    f(store)
823}
824
825fn with_store_mut<F, R>(map: &Value, f: F) -> Result<R, String>
826where
827    F: FnOnce(&mut MapStore) -> Result<R, String>,
828{
829    let handle = extract_handle(map)?;
830    ensure_handle(handle)?;
831    let id = map_id(handle)?;
832    let mut guard = MAP_REGISTRY
833        .write()
834        .map_err(|_| "containers.Map: registry lock poisoned".to_string())?;
835    let store = guard
836        .get_mut(&id)
837        .ok_or_else(|| "containers.Map: internal storage not found".to_string())?;
838    f(store)
839}
840
841fn extract_handle(value: &Value) -> Result<&HandleRef, String> {
842    match value {
843        Value::HandleObject(handle) => Ok(handle),
844        _ => Err("containers.Map: expected a containers.Map handle".to_string()),
845    }
846}
847
848fn ensure_handle(handle: &HandleRef) -> Result<(), String> {
849    if !handle.valid {
850        return Err("containers.Map: handle is invalid".to_string());
851    }
852    if handle.class_name != CLASS_NAME {
853        return Err(format!(
854            "containers.Map: expected handle of class '{}', got '{}'",
855            CLASS_NAME, handle.class_name
856        ));
857    }
858    Ok(())
859}
860
861fn map_id(handle: &HandleRef) -> Result<u64, String> {
862    let storage = unsafe { &*handle.target.as_raw() };
863    match storage {
864        Value::Struct(StructValue { fields }) => match fields.get("id") {
865            Some(Value::Int(IntValue::U64(id))) => Ok(*id),
866            Some(Value::Int(other)) => {
867                let id = other.to_i64();
868                if id < 0 {
869                    Err("containers.Map: negative map identifier".to_string())
870                } else {
871                    Ok(id as u64)
872                }
873            }
874            Some(Value::Num(n)) if *n >= 0.0 => Ok(*n as u64),
875            _ => Err("containers.Map: corrupted storage identifier".to_string()),
876        },
877        other => Err(format!(
878            "containers.Map: internal storage has unexpected shape {other:?}"
879        )),
880    }
881}
882
883fn prepare_keys(value: Value, key_type: KeyType) -> Result<Vec<KeyCandidate>, String> {
884    let host = gather_if_needed(&value).map_err(|e| format!("containers.Map: {e}"))?;
885    let flattened = flatten_keys(&host, key_type)?;
886    let mut out = Vec::with_capacity(flattened.len());
887    for raw_key in flattened {
888        let (normalized, canonical) = canonicalize_key(raw_key, key_type)?;
889        out.push(KeyCandidate {
890            normalized,
891            canonical,
892        });
893    }
894    Ok(out)
895}
896
897fn prepare_values(value: Value) -> Result<Vec<Value>, String> {
898    let host = gather_if_needed(&value).map_err(|e| format!("containers.Map: {e}"))?;
899    flatten_values(&host)
900}
901
902fn flatten_keys(value: &Value, key_type: KeyType) -> Result<Vec<Value>, String> {
903    match value {
904        Value::Cell(cell) => {
905            let mut out = Vec::with_capacity(cell.data.len());
906            for ptr in &cell.data {
907                let element = unsafe { &*ptr.as_raw() };
908                if matches!(element, Value::Cell(_)) {
909                    return Err(
910                        "containers.Map: nested cell arrays are not supported for keys".to_string(),
911                    );
912                }
913                out.push(gather_if_needed(element).map_err(|e| format!("containers.Map: {e}"))?);
914            }
915            Ok(out)
916        }
917        Value::StringArray(sa) => Ok(sa
918            .data
919            .iter()
920            .map(|text| Value::String(text.clone()))
921            .collect()),
922        Value::CharArray(ca) => Ok(char_array_rows(ca)),
923        Value::LogicalArray(arr) => {
924            if key_type != KeyType::Logical {
925                return Err(
926                    "containers.Map: logical arrays can only be used with KeyType='logical'"
927                        .to_string(),
928                );
929            }
930            Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect())
931        }
932        Value::Tensor(t) => {
933            if !t.shape.is_empty() && t.data.len() != 1 && !is_vector_shape(&t.shape) {
934                return Err(
935                    "containers.Map: numeric keys must be scalar or vector shaped".to_string(),
936                );
937            }
938            Ok(t.data.iter().map(|&v| Value::Num(v)).collect())
939        }
940        Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::String(_) => {
941            Ok(vec![value.clone()])
942        }
943        Value::GpuTensor(_) => Err(
944            "containers.Map: GPU keys must be gathered to the host before construction".to_string(),
945        ),
946        other => Err(format!(
947            "containers.Map: unsupported key container {other:?}"
948        )),
949    }
950}
951
952fn flatten_values(value: &Value) -> Result<Vec<Value>, String> {
953    match value {
954        Value::Cell(cell) => {
955            let mut out = Vec::with_capacity(cell.data.len());
956            for ptr in &cell.data {
957                out.push(
958                    gather_if_needed(unsafe { &*ptr.as_raw() })
959                        .map_err(|e| format!("containers.Map: {e}"))?,
960                );
961            }
962            Ok(out)
963        }
964        Value::StringArray(sa) => Ok(sa
965            .data
966            .iter()
967            .map(|text| Value::String(text.clone()))
968            .collect()),
969        Value::CharArray(ca) => Ok(char_array_rows(ca)),
970        Value::LogicalArray(arr) => Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect()),
971        Value::Tensor(t) => {
972            if !t.shape.is_empty() && !is_vector_shape(&t.shape) && t.data.len() != 1 {
973                return Err(
974                    "containers.Map: numeric values must be scalar or vector shaped".to_string(),
975                );
976            }
977            Ok(t.data.iter().map(|&v| Value::Num(v)).collect())
978        }
979        _ => Ok(vec![value.clone()]),
980    }
981}
982
983fn char_array_rows(ca: &CharArray) -> Vec<Value> {
984    if ca.rows == 0 {
985        return Vec::new();
986    }
987    let mut out = Vec::with_capacity(ca.rows);
988    for row in 0..ca.rows {
989        let mut text = String::with_capacity(ca.cols);
990        for col in 0..ca.cols {
991            text.push(ca.data[row * ca.cols + col]);
992        }
993        out.push(Value::CharArray(
994            CharArray::new(text.chars().collect(), 1, text.chars().count())
995                .expect("char array new"),
996        ));
997    }
998    out
999}
1000
1001fn is_vector_shape(shape: &[usize]) -> bool {
1002    match shape.len() {
1003        0 => true,
1004        1 => true,
1005        2 => shape[0] == 1 || shape[1] == 1,
1006        _ => false,
1007    }
1008}
1009
1010fn canonicalize_key(value: Value, key_type: KeyType) -> Result<(NormalizedKey, Value), String> {
1011    let normalized = normalize_key(&value, key_type)?;
1012    let canonical = match key_type {
1013        KeyType::Char => Value::CharArray(char_array_from_value(&value)?),
1014        KeyType::String => Value::String(string_from_value(
1015            &value,
1016            "containers.Map: keys must be string scalars",
1017        )?),
1018        KeyType::Double => Value::Num(numeric_from_value(
1019            &value,
1020            "containers.Map: keys must be numeric scalars",
1021        )?),
1022        KeyType::Single => Value::Num(numeric_from_value(
1023            &value,
1024            "containers.Map: keys must be numeric scalars",
1025        )?),
1026        KeyType::Int32 => Value::Int(IntValue::I32(integer_from_value(
1027            &value,
1028            i32::MIN as i64,
1029            i32::MAX as i64,
1030            "containers.Map: int32 keys must be integers",
1031        )? as i32)),
1032        KeyType::UInt32 => Value::Int(IntValue::U32(unsigned_from_value(
1033            &value,
1034            u32::MAX as u64,
1035            "containers.Map: uint32 keys must be unsigned integers",
1036        )? as u32)),
1037        KeyType::Int64 => Value::Int(IntValue::I64(integer_from_value(
1038            &value,
1039            i64::MIN,
1040            i64::MAX,
1041            "containers.Map: int64 keys must be integers",
1042        )?)),
1043        KeyType::UInt64 => Value::Int(IntValue::U64(unsigned_from_value(
1044            &value,
1045            u64::MAX,
1046            "containers.Map: uint64 keys must be unsigned integers",
1047        )?)),
1048        KeyType::Logical => Value::Bool(bool_from_value(
1049            &value,
1050            "containers.Map: logical keys must be logical scalars",
1051        )?),
1052    };
1053    Ok((normalized, canonical))
1054}
1055
1056fn normalize_key(value: &Value, key_type: KeyType) -> Result<NormalizedKey, String> {
1057    match key_type {
1058        KeyType::Char | KeyType::String => {
1059            let text = string_from_value(value, "containers.Map: keys must be text scalars")?;
1060            Ok(NormalizedKey::String(text))
1061        }
1062        KeyType::Double | KeyType::Single => {
1063            let numeric =
1064                numeric_from_value(value, "containers.Map: keys must be numeric scalars")?;
1065            if !numeric.is_finite() {
1066                return Err("containers.Map: keys must be finite numeric scalars".to_string());
1067            }
1068            let canonical = if numeric == 0.0 { 0.0 } else { numeric };
1069            Ok(NormalizedKey::Float(canonical.to_bits()))
1070        }
1071        KeyType::Int32 | KeyType::Int64 => {
1072            let bounds = if key_type == KeyType::Int32 {
1073                (i32::MIN as i64, i32::MAX as i64)
1074            } else {
1075                (i64::MIN, i64::MAX)
1076            };
1077            let value = integer_from_value(
1078                value,
1079                bounds.0,
1080                bounds.1,
1081                "containers.Map: integer keys must be whole numbers",
1082            )?;
1083            Ok(NormalizedKey::Int(value))
1084        }
1085        KeyType::UInt32 | KeyType::UInt64 => {
1086            let limit = if key_type == KeyType::UInt32 {
1087                u32::MAX as u64
1088            } else {
1089                u64::MAX
1090            };
1091            let value = unsigned_from_value(
1092                value,
1093                limit,
1094                "containers.Map: unsigned keys must be non-negative integers",
1095            )?;
1096            Ok(NormalizedKey::UInt(value))
1097        }
1098        KeyType::Logical => {
1099            let flag = bool_from_value(
1100                value,
1101                "containers.Map: logical keys must be logical scalars",
1102            )?;
1103            Ok(NormalizedKey::Bool(flag))
1104        }
1105    }
1106}
1107
1108fn string_from_value(value: &Value, context: &str) -> Result<String, String> {
1109    match value {
1110        Value::String(s) => Ok(s.clone()),
1111        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
1112        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
1113        _ => Err(context.to_string()),
1114    }
1115}
1116
1117fn char_array_from_value(value: &Value) -> Result<CharArray, String> {
1118    match value {
1119        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.clone()),
1120        Value::String(s) => {
1121            let chars: Vec<char> = s.chars().collect();
1122            CharArray::new(chars.clone(), 1, chars.len())
1123                .map_err(|e| format!("containers.Map: {e}"))
1124        }
1125        Value::StringArray(sa) if sa.data.len() == 1 => {
1126            let chars: Vec<char> = sa.data[0].chars().collect();
1127            CharArray::new(chars.clone(), 1, chars.len())
1128                .map_err(|e| format!("containers.Map: {e}"))
1129        }
1130        _ => Err("containers.Map: keys must be character vectors".to_string()),
1131    }
1132}
1133
1134fn char_array_value(text: &str) -> Result<Value, String> {
1135    let chars: Vec<char> = text.chars().collect();
1136    CharArray::new(chars.clone(), 1, chars.len())
1137        .map(Value::CharArray)
1138        .map_err(|e| format!("containers.Map: {e}"))
1139}
1140
1141fn normalize_numeric_value(value: Value) -> Result<Value, String> {
1142    match value {
1143        Value::Num(_) | Value::Tensor(_) => Ok(value),
1144        Value::Int(i) => Ok(Value::Num(i.to_f64())),
1145        Value::Bool(b) => Ok(Value::Num(if b { 1.0 } else { 0.0 })),
1146        Value::LogicalArray(arr) => {
1147            let data: Vec<f64> = arr
1148                .data
1149                .iter()
1150                .map(|&b| if b != 0 { 1.0 } else { 0.0 })
1151                .collect();
1152            let tensor =
1153                Tensor::new(data, arr.shape.clone()).map_err(|e| format!("containers.Map: {e}"))?;
1154            Ok(Value::Tensor(tensor))
1155        }
1156        Value::Cell(_)
1157        | Value::Struct(_)
1158        | Value::Object(_)
1159        | Value::HandleObject(_)
1160        | Value::Listener(_)
1161        | Value::String(_)
1162        | Value::StringArray(_)
1163        | Value::CharArray(_)
1164        | Value::Complex(_, _)
1165        | Value::ComplexTensor(_)
1166        | Value::FunctionHandle(_)
1167        | Value::Closure(_)
1168        | Value::ClassRef(_)
1169        | Value::MException(_)
1170        | Value::GpuTensor(_) => Err(
1171            "containers.Map: values must be numeric when ValueType is 'double' or 'single'"
1172                .to_string(),
1173        ),
1174    }
1175}
1176
1177fn normalize_logical_value(value: Value) -> Result<Value, String> {
1178    match value {
1179        Value::Bool(_) | Value::LogicalArray(_) => Ok(value),
1180        Value::Int(i) => Ok(Value::Bool(i.to_i64() != 0)),
1181        Value::Num(n) => Ok(Value::Bool(n != 0.0)),
1182        Value::Tensor(t) => {
1183            let flags: Vec<u8> = t
1184                .data
1185                .iter()
1186                .map(|&v| if v != 0.0 { 1 } else { 0 })
1187                .collect();
1188            let logical = LogicalArray::new(flags, t.shape.clone())
1189                .map_err(|e| format!("containers.Map: {e}"))?;
1190            Ok(Value::LogicalArray(logical))
1191        }
1192        Value::CharArray(_)
1193        | Value::String(_)
1194        | Value::StringArray(_)
1195        | Value::Struct(_)
1196        | Value::Cell(_)
1197        | Value::Object(_)
1198        | Value::HandleObject(_)
1199        | Value::Listener(_)
1200        | Value::Complex(_, _)
1201        | Value::ComplexTensor(_)
1202        | Value::FunctionHandle(_)
1203        | Value::Closure(_)
1204        | Value::ClassRef(_)
1205        | Value::MException(_)
1206        | Value::GpuTensor(_) => {
1207            Err("containers.Map: values must be logical when ValueType is 'logical'".to_string())
1208        }
1209    }
1210}
1211
1212fn numeric_from_value(value: &Value, context: &str) -> Result<f64, String> {
1213    match value {
1214        Value::Num(n) => Ok(*n),
1215        Value::Int(i) => Ok(i.to_f64()),
1216        Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
1217        Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
1218        Value::LogicalArray(arr) if arr.data.len() == 1 => {
1219            Ok(if arr.data[0] != 0 { 1.0 } else { 0.0 })
1220        }
1221        _ => Err(context.to_string()),
1222    }
1223}
1224
1225fn integer_from_value(value: &Value, min: i64, max: i64, context: &str) -> Result<i64, String> {
1226    match value {
1227        Value::Int(i) => {
1228            let v = i.to_i64();
1229            if v < min || v > max {
1230                return Err(context.to_string());
1231            }
1232            Ok(v)
1233        }
1234        Value::Num(n) => {
1235            if !n.is_finite() {
1236                return Err(context.to_string());
1237            }
1238            if (*n < min as f64) || (*n > max as f64) {
1239                return Err(context.to_string());
1240            }
1241            if (n.round() - n).abs() > f64::EPSILON {
1242                return Err(context.to_string());
1243            }
1244            Ok(n.round() as i64)
1245        }
1246        Value::Bool(b) => {
1247            let v = if *b { 1 } else { 0 };
1248            if v < min || v > max {
1249                return Err(context.to_string());
1250            }
1251            Ok(v)
1252        }
1253        _ => Err(context.to_string()),
1254    }
1255}
1256
1257fn unsigned_from_value(value: &Value, max: u64, context: &str) -> Result<u64, String> {
1258    match value {
1259        Value::Int(i) => {
1260            let v = i.to_i64();
1261            if v < 0 || v as u64 > max {
1262                return Err(context.to_string());
1263            }
1264            Ok(v as u64)
1265        }
1266        Value::Num(n) => {
1267            if !n.is_finite() || *n < 0.0 || *n > max as f64 {
1268                return Err(context.to_string());
1269            }
1270            if (n.round() - n).abs() > f64::EPSILON {
1271                return Err(context.to_string());
1272            }
1273            Ok(n.round() as u64)
1274        }
1275        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
1276        _ => Err(context.to_string()),
1277    }
1278}
1279
1280fn bool_from_value(value: &Value, context: &str) -> Result<bool, String> {
1281    match value {
1282        Value::Bool(b) => Ok(*b),
1283        Value::LogicalArray(arr) if arr.data.len() == 1 => Ok(arr.data[0] != 0),
1284        Value::Int(i) => Ok(i.to_i64() != 0),
1285        Value::Num(n) => Ok(*n != 0.0),
1286        _ => Err(context.to_string()),
1287    }
1288}
1289
1290fn make_row_cell(values: Vec<Value>) -> Result<Value, String> {
1291    let cols = values.len();
1292    crate::make_cell_with_shape(values, vec![1, cols])
1293}
1294
1295fn extract_key_arguments(payload: &Value) -> Result<Vec<Value>, String> {
1296    match payload {
1297        Value::Cell(cell) => {
1298            let mut out = Vec::with_capacity(cell.data.len());
1299            for ptr in &cell.data {
1300                out.push(unsafe { &*ptr.as_raw() }.clone());
1301            }
1302            Ok(out)
1303        }
1304        other => Err(format!(
1305            "containers.Map: expected key arguments in a cell array, got {other:?}"
1306        )),
1307    }
1308}
1309
1310fn expand_assignment_values(value: Value, expected: usize) -> Result<Vec<Value>, String> {
1311    let host = gather_if_needed(&value).map_err(|e| format!("containers.Map: {e}"))?;
1312    let values = flatten_values(&host)?;
1313    if expected == 1 {
1314        if values.is_empty() {
1315            return Err("containers.Map: assignment requires a value".to_string());
1316        }
1317        Ok(vec![values.into_iter().next().unwrap()])
1318    } else {
1319        if values.len() != expected {
1320            return Err(format!(
1321                "containers.Map: assignment with {} keys requires {} values (got {})",
1322                expected,
1323                expected,
1324                values.len()
1325            ));
1326        }
1327        Ok(values)
1328    }
1329}
1330
1331struct KeyCollection {
1332    values: Vec<Value>,
1333    shape: Vec<usize>,
1334}
1335
1336fn collect_key_spec(value: &Value, key_type: KeyType) -> Result<KeyCollection, String> {
1337    let host = gather_if_needed(value).map_err(|e| format!("containers.Map: {e}"))?;
1338    match &host {
1339        Value::Cell(cell) => {
1340            let mut values = Vec::with_capacity(cell.data.len());
1341            for ptr in &cell.data {
1342                values.push(
1343                    gather_if_needed(unsafe { &*ptr.as_raw() })
1344                        .map_err(|e| format!("containers.Map: {e}"))?,
1345                );
1346            }
1347            Ok(KeyCollection {
1348                values,
1349                shape: vec![cell.rows, cell.cols],
1350            })
1351        }
1352        Value::StringArray(sa) => Ok(KeyCollection {
1353            values: sa.data.iter().map(|s| Value::String(s.clone())).collect(),
1354            shape: vec![sa.rows(), sa.cols()],
1355        }),
1356        Value::CharArray(ca) => {
1357            let rows = if ca.rows == 0 { 0 } else { ca.rows };
1358            Ok(KeyCollection {
1359                values: char_array_rows(ca),
1360                shape: vec![rows, 1],
1361            })
1362        }
1363        Value::LogicalArray(arr) if key_type == KeyType::Logical => Ok(KeyCollection {
1364            values: arr.data.iter().map(|&b| Value::Bool(b != 0)).collect(),
1365            shape: arr.shape.clone(),
1366        }),
1367        Value::Tensor(t) if key_type != KeyType::Char && key_type != KeyType::String => {
1368            Ok(KeyCollection {
1369                values: t.data.iter().map(|&n| Value::Num(n)).collect(),
1370                shape: t.shape.clone(),
1371            })
1372        }
1373        _ => Ok(KeyCollection {
1374            values: vec![host.clone()],
1375            shape: vec![1, 1],
1376        }),
1377    }
1378}
1379
1380pub fn map_length(value: &Value) -> Option<usize> {
1381    if let Value::HandleObject(handle) = value {
1382        if handle.valid && handle.class_name == CLASS_NAME {
1383            if let Ok(id) = map_id(handle) {
1384                if let Ok(registry) = MAP_REGISTRY.read() {
1385                    return registry.get(&id).map(|store| store.len());
1386                }
1387            }
1388        }
1389    }
1390    None
1391}
1392
1393#[cfg(test)]
1394mod tests {
1395    use super::*;
1396    use crate::builtins::common::test_support;
1397
1398    #[test]
1399    fn construct_empty_map_defaults() {
1400        let map = containers_map_builtin(Vec::new()).expect("map");
1401        let count = containers_map_subsref(
1402            map.clone(),
1403            ".".to_string(),
1404            Value::from("Count".to_string()),
1405        )
1406        .expect("Count");
1407        assert_eq!(count, Value::Num(0.0));
1408
1409        let key_type = containers_map_subsref(
1410            map.clone(),
1411            ".".to_string(),
1412            Value::from("KeyType".to_string()),
1413        )
1414        .expect("KeyType");
1415        assert_eq!(
1416            key_type,
1417            Value::CharArray(CharArray::new("char".chars().collect(), 1, 4).unwrap())
1418        );
1419
1420        let value_type = containers_map_subsref(
1421            map.clone(),
1422            ".".to_string(),
1423            Value::from("ValueType".to_string()),
1424        )
1425        .expect("ValueType");
1426        assert_eq!(
1427            value_type,
1428            Value::CharArray(CharArray::new("any".chars().collect(), 1, 3).unwrap())
1429        );
1430    }
1431
1432    #[test]
1433    fn constructor_with_cells_lookup() {
1434        let keys = crate::make_cell(vec![Value::from("apple"), Value::from("pear")], 1, 2).unwrap();
1435        let values = crate::make_cell(vec![Value::Num(5.0), Value::Num(7.0)], 1, 2).unwrap();
1436        let map = containers_map_builtin(vec![keys, values]).expect("map");
1437        let apple = containers_map_subsref(
1438            map.clone(),
1439            "()".to_string(),
1440            crate::make_cell(vec![Value::from("apple")], 1, 1).unwrap(),
1441        )
1442        .expect("lookup");
1443        assert_eq!(apple, Value::Num(5.0));
1444    }
1445
1446    #[test]
1447    fn constructor_rejects_duplicate_keys() {
1448        let keys = crate::make_cell(vec![Value::from("dup"), Value::from("dup")], 1, 2).unwrap();
1449        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
1450        let err = containers_map_builtin(vec![keys, values]).expect_err("duplicate check");
1451        assert!(err.contains("Duplicate key name"));
1452    }
1453
1454    #[test]
1455    fn constructor_errors_when_value_count_mismatch() {
1456        let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
1457        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1458        let err = containers_map_builtin(vec![keys, values]).expect_err("count mismatch");
1459        assert!(err.contains("number of keys"));
1460    }
1461
1462    #[test]
1463    fn comparison_method_rejects_unknown_values() {
1464        let keys = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
1465        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1466        let err = containers_map_builtin(vec![
1467            keys,
1468            values,
1469            Value::from("ComparisonMethod"),
1470            Value::from("caseinsensitive"),
1471        ])
1472        .expect_err("comparison method");
1473        assert!(err.contains("ComparisonMethod"));
1474    }
1475
1476    #[test]
1477    fn key_type_single_roundtrip() {
1478        let map = containers_map_builtin(vec![Value::from("KeyType"), Value::from("single")])
1479            .expect("map");
1480        let key_type = containers_map_subsref(map.clone(), ".".to_string(), Value::from("KeyType"))
1481            .expect("keytype");
1482        assert_eq!(
1483            key_type,
1484            Value::CharArray(CharArray::new("single".chars().collect(), 1, 6).unwrap())
1485        );
1486
1487        let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1488        let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(7.0))
1489            .expect("assign");
1490        let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1491        assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
1492    }
1493
1494    #[test]
1495    fn value_type_double_converts_integers() {
1496        let keys = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1497        let values = crate::make_cell(vec![Value::Int(IntValue::I32(7))], 1, 1).unwrap();
1498        let map = containers_map_builtin(vec![
1499            keys,
1500            values,
1501            Value::from("KeyType"),
1502            Value::from("double"),
1503            Value::from("ValueType"),
1504            Value::from("double"),
1505        ])
1506        .expect("map");
1507        let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1508        let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1509        assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
1510    }
1511
1512    #[test]
1513    fn value_type_logical_converts_numeric_arrays() {
1514        let keys = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
1515        let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
1516        let values = crate::make_cell(vec![Value::Tensor(tensor.clone())], 1, 1).unwrap();
1517        let map = containers_map_builtin(vec![
1518            keys,
1519            values,
1520            Value::from("ValueType"),
1521            Value::from("logical"),
1522        ])
1523        .expect("map");
1524        let payload = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
1525        let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1526        match value {
1527            Value::LogicalArray(arr) => {
1528                assert_eq!(arr.shape, vec![3, 1]);
1529                assert_eq!(arr.data, vec![0, 1, 1]);
1530            }
1531            other => panic!("expected logical array, got {:?}", other),
1532        }
1533    }
1534
1535    #[test]
1536    fn uniform_values_enforced_on_assignment() {
1537        let map = containers_map_builtin(vec![Value::from("UniformValues"), Value::from(true)])
1538            .expect("map");
1539        let payload = crate::make_cell(vec![Value::from("x")], 1, 1).unwrap();
1540        let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(1.0))
1541            .expect("assign");
1542        let err = containers_map_subsasgn(map, "()".to_string(), payload, Value::from("text"))
1543            .expect_err("uniform enforcement");
1544        assert!(err.contains("UniformValues"));
1545    }
1546
1547    #[test]
1548    fn assignment_updates_and_inserts() {
1549        let map = containers_map_builtin(Vec::new()).expect("map");
1550        let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
1551        let updated = containers_map_subsasgn(
1552            map.clone(),
1553            "()".to_string(),
1554            payload.clone(),
1555            Value::Num(1.0),
1556        )
1557        .expect("assign");
1558        let updated = containers_map_subsasgn(
1559            updated.clone(),
1560            "()".to_string(),
1561            payload.clone(),
1562            Value::Num(5.0),
1563        )
1564        .expect("update");
1565        let beta_payload = crate::make_cell(vec![Value::from("beta")], 1, 1).unwrap();
1566        let updated = containers_map_subsasgn(
1567            updated.clone(),
1568            "()".to_string(),
1569            beta_payload,
1570            Value::Num(9.0),
1571        )
1572        .expect("insert");
1573        let value = containers_map_subsref(updated, "()".to_string(), payload).expect("lookup");
1574        assert_eq!(value, Value::Num(5.0));
1575    }
1576
1577    #[test]
1578    fn subsref_multiple_keys_preserves_shape() {
1579        let keys = crate::make_cell(
1580            vec![Value::from("a"), Value::from("b"), Value::from("c")],
1581            1,
1582            3,
1583        )
1584        .unwrap();
1585        let values = crate::make_cell(
1586            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1587            1,
1588            3,
1589        )
1590        .unwrap();
1591        let map = containers_map_builtin(vec![keys, values]).expect("map");
1592        let request = crate::make_cell(vec![Value::from("a"), Value::from("c")], 1, 2).unwrap();
1593        let payload = crate::make_cell(vec![request], 1, 1).unwrap();
1594        let result =
1595            containers_map_subsref(map.clone(), "()".to_string(), payload).expect("lookup");
1596        match result {
1597            Value::Cell(cell) => {
1598                assert_eq!(cell.rows, 1);
1599                assert_eq!(cell.cols, 2);
1600                assert_eq!(cell.get(0, 0).expect("cell 0,0"), Value::Num(1.0));
1601                assert_eq!(cell.get(0, 1).expect("cell 0,1"), Value::Num(3.0));
1602            }
1603            other => panic!("expected cell array, got {other:?}"),
1604        }
1605    }
1606
1607    #[test]
1608    fn subsref_empty_key_collection_returns_empty_cell() {
1609        let keys = crate::make_cell(vec![Value::from("z")], 1, 1).unwrap();
1610        let values = crate::make_cell(vec![Value::Num(42.0)], 1, 1).unwrap();
1611        let map = containers_map_builtin(vec![keys, values]).expect("map");
1612        let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
1613        let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
1614        let result = containers_map_subsref(map, "()".to_string(), payload).expect("lookup empty");
1615        match result {
1616            Value::Cell(cell) => {
1617                assert_eq!(cell.rows, 1);
1618                assert_eq!(cell.cols, 0);
1619                assert!(cell.data.is_empty());
1620            }
1621            other => panic!("expected empty cell, got {other:?}"),
1622        }
1623    }
1624
1625    #[test]
1626    fn subsasgn_with_cell_keys_updates_all_targets() {
1627        let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
1628        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
1629        let map = containers_map_builtin(vec![keys, values]).expect("map");
1630        let key_spec = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
1631        let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
1632        let new_values = crate::make_cell(vec![Value::Num(10.0), Value::Num(20.0)], 1, 2).unwrap();
1633        let updated = containers_map_subsasgn(map.clone(), "()".to_string(), payload, new_values)
1634            .expect("assign");
1635        let a_payload = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
1636        let b_payload = crate::make_cell(vec![Value::from("b")], 1, 1).unwrap();
1637        let a_value =
1638            containers_map_subsref(updated.clone(), "()".to_string(), a_payload).expect("a lookup");
1639        let b_value =
1640            containers_map_subsref(updated, "()".to_string(), b_payload).expect("b lookup");
1641        assert_eq!(a_value, Value::Num(10.0));
1642        assert_eq!(b_value, Value::Num(20.0));
1643    }
1644
1645    #[test]
1646    fn assignment_value_count_mismatch_errors() {
1647        let keys = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
1648        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
1649        let map = containers_map_builtin(vec![keys, values]).expect("map");
1650        let key_spec = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
1651        let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
1652        let rhs = crate::make_cell(vec![Value::Num(99.0)], 1, 1).unwrap();
1653        let err =
1654            containers_map_subsasgn(map, "()".to_string(), payload, rhs).expect_err("value count");
1655        assert!(err.contains("requires 2 values"));
1656    }
1657
1658    #[test]
1659    fn subsasgn_empty_key_collection_is_noop() {
1660        let keys = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
1661        let values = crate::make_cell(vec![Value::Num(7.0)], 1, 1).unwrap();
1662        let map = containers_map_builtin(vec![keys, values]).expect("map");
1663        let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
1664        let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
1665        let rhs = crate::make_cell(Vec::new(), 1, 0).unwrap();
1666        let updated =
1667            containers_map_subsasgn(map.clone(), "()".to_string(), payload, rhs).expect("assign");
1668        let lookup_payload = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
1669        let value =
1670            containers_map_subsref(updated, "()".to_string(), lookup_payload).expect("lookup");
1671        assert_eq!(value, Value::Num(7.0));
1672    }
1673
1674    #[test]
1675    fn keys_values_iskey_remove() {
1676        let keys = crate::make_cell(
1677            vec![Value::from("a"), Value::from("b"), Value::from("c")],
1678            1,
1679            3,
1680        )
1681        .unwrap();
1682        let values = crate::make_cell(
1683            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1684            1,
1685            3,
1686        )
1687        .unwrap();
1688        let map = containers_map_builtin(vec![keys, values]).expect("map");
1689        let key_list = containers_map_keys(map.clone()).expect("keys");
1690        match key_list {
1691            Value::Cell(cell) => assert_eq!(cell.data.len(), 3),
1692            other => panic!("expected cell array, got {other:?}"),
1693        }
1694        let mask = containers_map_is_key(
1695            map.clone(),
1696            crate::make_cell(vec![Value::from("a"), Value::from("z")], 1, 2).unwrap(),
1697        )
1698        .expect("mask");
1699        match mask {
1700            Value::LogicalArray(arr) => {
1701                assert_eq!(arr.data, vec![1, 0]);
1702            }
1703            other => panic!("expected logical array, got {:?}", other),
1704        }
1705        let removed = containers_map_remove(
1706            map.clone(),
1707            crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
1708        )
1709        .expect("remove");
1710        let mask = containers_map_is_key(
1711            removed,
1712            crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
1713        )
1714        .expect("mask");
1715        assert_eq!(mask, Value::Bool(false));
1716    }
1717
1718    #[test]
1719    fn remove_missing_key_returns_error() {
1720        let keys = crate::make_cell(vec![Value::from("key")], 1, 1).unwrap();
1721        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1722        let map = containers_map_builtin(vec![keys, values]).expect("map");
1723        let err = containers_map_remove(
1724            map,
1725            crate::make_cell(vec![Value::from("missing")], 1, 1).unwrap(),
1726        )
1727        .expect_err("remove missing");
1728        assert_eq!(err, MISSING_KEY_ERR);
1729    }
1730
1731    #[test]
1732    fn length_delegates_to_map_count() {
1733        let keys = crate::make_cell(
1734            vec![Value::from("a"), Value::from("b"), Value::from("c")],
1735            1,
1736            3,
1737        )
1738        .unwrap();
1739        let values = crate::make_cell(
1740            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1741            1,
1742            3,
1743        )
1744        .unwrap();
1745        let map = containers_map_builtin(vec![keys, values]).expect("map");
1746        assert_eq!(map_length(&map), Some(3));
1747    }
1748
1749    #[test]
1750    fn map_constructor_gathers_gpu_values() {
1751        test_support::with_test_provider(|provider| {
1752            let keys = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
1753            let data = vec![1.0, 2.0, 3.0];
1754            let shape = vec![3, 1];
1755            let view = runmat_accelerate_api::HostTensorView {
1756                data: &data,
1757                shape: &shape,
1758            };
1759            let handle = provider.upload(&view).expect("upload");
1760            let values = crate::make_cell(vec![Value::GpuTensor(handle)], 1, 1).unwrap();
1761            let map = containers_map_builtin(vec![keys, values]).expect("map");
1762            let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
1763            let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1764            match value {
1765                Value::Tensor(t) => {
1766                    assert_eq!(t.shape, shape);
1767                    assert_eq!(t.data, data);
1768                }
1769                other => panic!("expected tensor, got {:?}", other),
1770            }
1771        });
1772    }
1773
1774    #[test]
1775    #[cfg(feature = "doc_export")]
1776    fn doc_examples_present() {
1777        let examples = test_support::doc_examples(DOC_MD);
1778        assert!(!examples.is_empty());
1779    }
1780}