Skip to main content

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::{
11    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
12    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
13    CharArray, HandleRef, IntValue, LogicalArray, StructValue, Tensor, Value,
14};
15use runmat_macros::runtime_builtin;
16
17use crate::builtins::common::random_args::keyword_of;
18use crate::builtins::common::spec::{
19    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
20    ReductionNaN, ResidencyPolicy, ShapeRequirements,
21};
22use crate::builtins::containers::type_resolvers::{
23    map_cell_type, map_handle_type, map_is_key_type, map_unknown_type,
24};
25use crate::{
26    build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError, OBJECT_INDEX_BRACE,
27    OBJECT_INDEX_MEMBER, OBJECT_INDEX_PAREN,
28};
29
30const CLASS_NAME: &str = "containers.Map";
31const BUILTIN_CONSTRUCTOR: &str = "containers.Map";
32const BUILTIN_KEYS: &str = "containers.Map.keys";
33const BUILTIN_VALUES: &str = "containers.Map.values";
34const BUILTIN_IS_KEY: &str = "containers.Map.isKey";
35const BUILTIN_REMOVE: &str = "containers.Map.remove";
36const BUILTIN_SUBSREF: &str = "containers.Map.subsref";
37const BUILTIN_SUBSASGN: &str = "containers.Map.subsasgn";
38
39const CONTAINERS_MAP_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
40    name: "M",
41    ty: BuiltinParamType::Any,
42    arity: BuiltinParamArity::Required,
43    default: None,
44    description: "containers.Map handle object.",
45}];
46
47const CONTAINERS_MAP_INPUTS_KEYS_VALUES: [BuiltinParamDescriptor; 2] = [
48    BuiltinParamDescriptor {
49        name: "keys",
50        ty: BuiltinParamType::Any,
51        arity: BuiltinParamArity::Required,
52        default: None,
53        description: "Key container (cell, string/char, or numeric vector).",
54    },
55    BuiltinParamDescriptor {
56        name: "values",
57        ty: BuiltinParamType::Any,
58        arity: BuiltinParamArity::Required,
59        default: None,
60        description: "Value container aligned with keys.",
61    },
62];
63
64const CONTAINERS_MAP_INPUTS_KEYS_VALUES_OPTS: [BuiltinParamDescriptor; 3] = [
65    BuiltinParamDescriptor {
66        name: "keys",
67        ty: BuiltinParamType::Any,
68        arity: BuiltinParamArity::Required,
69        default: None,
70        description: "Key container (cell, string/char, or numeric vector).",
71    },
72    BuiltinParamDescriptor {
73        name: "values",
74        ty: BuiltinParamType::Any,
75        arity: BuiltinParamArity::Required,
76        default: None,
77        description: "Value container aligned with keys.",
78    },
79    BuiltinParamDescriptor {
80        name: "options",
81        ty: BuiltinParamType::Any,
82        arity: BuiltinParamArity::Variadic,
83        default: None,
84        description: "Name/value options (KeyType, ValueType, UniformValues, ComparisonMethod).",
85    },
86];
87
88const CONTAINERS_MAP_INPUTS_OPTIONS_ONLY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
89    name: "options",
90    ty: BuiltinParamType::Any,
91    arity: BuiltinParamArity::Variadic,
92    default: None,
93    description: "Name/value options (KeyType, ValueType, UniformValues, ComparisonMethod).",
94}];
95
96const CONTAINERS_MAP_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
97    BuiltinSignatureDescriptor {
98        label: "M = containers.Map()",
99        inputs: &[],
100        outputs: &CONTAINERS_MAP_OUTPUT,
101    },
102    BuiltinSignatureDescriptor {
103        label: "M = containers.Map(keys, values)",
104        inputs: &CONTAINERS_MAP_INPUTS_KEYS_VALUES,
105        outputs: &CONTAINERS_MAP_OUTPUT,
106    },
107    BuiltinSignatureDescriptor {
108        label: "M = containers.Map(keys, values, Name, Value, ...)",
109        inputs: &CONTAINERS_MAP_INPUTS_KEYS_VALUES_OPTS,
110        outputs: &CONTAINERS_MAP_OUTPUT,
111    },
112    BuiltinSignatureDescriptor {
113        label: "M = containers.Map(Name, Value, ...)",
114        inputs: &CONTAINERS_MAP_INPUTS_OPTIONS_ONLY,
115        outputs: &CONTAINERS_MAP_OUTPUT,
116    },
117];
118
119const CONTAINERS_MAP_METHOD_INPUT_MAP: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
120    name: "M",
121    ty: BuiltinParamType::Any,
122    arity: BuiltinParamArity::Required,
123    default: None,
124    description: "containers.Map handle object.",
125}];
126
127const CONTAINERS_MAP_KEYS_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
128    name: "K",
129    ty: BuiltinParamType::Any,
130    arity: BuiltinParamArity::Required,
131    default: None,
132    description: "Row cell array containing map keys.",
133}];
134
135const CONTAINERS_MAP_VALUES_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
136    name: "V",
137    ty: BuiltinParamType::Any,
138    arity: BuiltinParamArity::Required,
139    default: None,
140    description: "Row cell array containing map values.",
141}];
142
143const CONTAINERS_MAP_INPUTS_KEY_SPEC: [BuiltinParamDescriptor; 2] = [
144    BuiltinParamDescriptor {
145        name: "M",
146        ty: BuiltinParamType::Any,
147        arity: BuiltinParamArity::Required,
148        default: None,
149        description: "containers.Map handle object.",
150    },
151    BuiltinParamDescriptor {
152        name: "keySet",
153        ty: BuiltinParamType::Any,
154        arity: BuiltinParamArity::Required,
155        default: None,
156        description: "Key scalar or key collection to query/mutate.",
157    },
158];
159
160const CONTAINERS_MAP_ISKEY_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
161    name: "tf",
162    ty: BuiltinParamType::LogicalArray,
163    arity: BuiltinParamArity::Required,
164    default: None,
165    description: "Logical membership result for each key.",
166}];
167
168const CONTAINERS_MAP_INPUTS_SUBSREF: [BuiltinParamDescriptor; 3] = [
169    BuiltinParamDescriptor {
170        name: "M",
171        ty: BuiltinParamType::Any,
172        arity: BuiltinParamArity::Required,
173        default: None,
174        description: "containers.Map handle object.",
175    },
176    BuiltinParamDescriptor {
177        name: "kind",
178        ty: BuiltinParamType::StringScalar,
179        arity: BuiltinParamArity::Required,
180        default: None,
181        description: "Indexing kind: (), ., or {}.",
182    },
183    BuiltinParamDescriptor {
184        name: "payload",
185        ty: BuiltinParamType::Any,
186        arity: BuiltinParamArity::Required,
187        default: None,
188        description: "Indexing payload cell/property argument.",
189    },
190];
191
192const CONTAINERS_MAP_INPUTS_SUBSASGN: [BuiltinParamDescriptor; 4] = [
193    BuiltinParamDescriptor {
194        name: "M",
195        ty: BuiltinParamType::Any,
196        arity: BuiltinParamArity::Required,
197        default: None,
198        description: "containers.Map handle object.",
199    },
200    BuiltinParamDescriptor {
201        name: "kind",
202        ty: BuiltinParamType::StringScalar,
203        arity: BuiltinParamArity::Required,
204        default: None,
205        description: "Assignment kind: (), ., or {}.",
206    },
207    BuiltinParamDescriptor {
208        name: "payload",
209        ty: BuiltinParamType::Any,
210        arity: BuiltinParamArity::Required,
211        default: None,
212        description: "Assignment payload cell/property argument.",
213    },
214    BuiltinParamDescriptor {
215        name: "rhs",
216        ty: BuiltinParamType::Any,
217        arity: BuiltinParamArity::Required,
218        default: None,
219        description: "Assigned value (scalar or collection).",
220    },
221];
222
223const CONTAINERS_MAP_SUBSREF_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
224    name: "value",
225    ty: BuiltinParamType::Any,
226    arity: BuiltinParamArity::Required,
227    default: None,
228    description: "Lookup/property value result.",
229}];
230
231const CONTAINERS_MAP_KEYS_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
232    [BuiltinSignatureDescriptor {
233        label: "K = containers.Map.keys(M)",
234        inputs: &CONTAINERS_MAP_METHOD_INPUT_MAP,
235        outputs: &CONTAINERS_MAP_KEYS_OUTPUT,
236    }];
237
238const CONTAINERS_MAP_VALUES_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
239    [BuiltinSignatureDescriptor {
240        label: "V = containers.Map.values(M)",
241        inputs: &CONTAINERS_MAP_METHOD_INPUT_MAP,
242        outputs: &CONTAINERS_MAP_VALUES_OUTPUT,
243    }];
244
245const CONTAINERS_MAP_ISKEY_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
246    [BuiltinSignatureDescriptor {
247        label: "tf = containers.Map.isKey(M, keySet)",
248        inputs: &CONTAINERS_MAP_INPUTS_KEY_SPEC,
249        outputs: &CONTAINERS_MAP_ISKEY_OUTPUT,
250    }];
251
252const CONTAINERS_MAP_REMOVE_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
253    [BuiltinSignatureDescriptor {
254        label: "M = containers.Map.remove(M, keySet)",
255        inputs: &CONTAINERS_MAP_INPUTS_KEY_SPEC,
256        outputs: &CONTAINERS_MAP_OUTPUT,
257    }];
258
259const CONTAINERS_MAP_SUBSREF_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
260    [BuiltinSignatureDescriptor {
261        label: "value = containers.Map.subsref(M, kind, payload)",
262        inputs: &CONTAINERS_MAP_INPUTS_SUBSREF,
263        outputs: &CONTAINERS_MAP_SUBSREF_OUTPUT,
264    }];
265
266const CONTAINERS_MAP_SUBSASGN_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
267    [BuiltinSignatureDescriptor {
268        label: "M = containers.Map.subsasgn(M, kind, payload, rhs)",
269        inputs: &CONTAINERS_MAP_INPUTS_SUBSASGN,
270        outputs: &CONTAINERS_MAP_OUTPUT,
271    }];
272
273const CONTAINERS_MAP_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
274    code: "RM.CONTAINERS_MAP.INVALID_ARGUMENT",
275    identifier: Some("RunMat:containers.Map:InvalidArgument"),
276    when: "Map constructor/method inputs, option grammar, or key/value payloads are invalid.",
277    message: "containers.Map: invalid argument",
278};
279
280const CONTAINERS_MAP_ERROR_MISSING_KEY: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
281    code: "RM.CONTAINERS_MAP.MISSING_KEY",
282    identifier: Some("RunMat:containers.Map:MissingKey"),
283    when: "Lookup/removal targets a key that is not present in the map.",
284    message: "containers.Map: The specified key is not present in this container.",
285};
286
287const CONTAINERS_MAP_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
288    code: "RM.CONTAINERS_MAP.INTERNAL",
289    identifier: Some("RunMat:containers.Map:Internal"),
290    when: "Map registry/storage operations fail unexpectedly.",
291    message: "containers.Map: internal operation failed",
292};
293
294const CONTAINERS_MAP_ERRORS: [BuiltinErrorDescriptor; 3] = [
295    CONTAINERS_MAP_ERROR_INVALID_ARGUMENT,
296    CONTAINERS_MAP_ERROR_MISSING_KEY,
297    CONTAINERS_MAP_ERROR_INTERNAL,
298];
299
300pub const CONTAINERS_MAP_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
301    signatures: &CONTAINERS_MAP_SIGNATURES,
302    output_mode: BuiltinOutputMode::Fixed,
303    completion_policy: BuiltinCompletionPolicy::Public,
304    errors: &CONTAINERS_MAP_ERRORS,
305};
306
307pub const CONTAINERS_MAP_KEYS_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
308    signatures: &CONTAINERS_MAP_KEYS_SIGNATURES,
309    output_mode: BuiltinOutputMode::Fixed,
310    completion_policy: BuiltinCompletionPolicy::Public,
311    errors: &CONTAINERS_MAP_ERRORS,
312};
313
314pub const CONTAINERS_MAP_VALUES_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
315    signatures: &CONTAINERS_MAP_VALUES_SIGNATURES,
316    output_mode: BuiltinOutputMode::Fixed,
317    completion_policy: BuiltinCompletionPolicy::Public,
318    errors: &CONTAINERS_MAP_ERRORS,
319};
320
321pub const CONTAINERS_MAP_ISKEY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
322    signatures: &CONTAINERS_MAP_ISKEY_SIGNATURES,
323    output_mode: BuiltinOutputMode::Fixed,
324    completion_policy: BuiltinCompletionPolicy::Public,
325    errors: &CONTAINERS_MAP_ERRORS,
326};
327
328pub const CONTAINERS_MAP_REMOVE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
329    signatures: &CONTAINERS_MAP_REMOVE_SIGNATURES,
330    output_mode: BuiltinOutputMode::Fixed,
331    completion_policy: BuiltinCompletionPolicy::Public,
332    errors: &CONTAINERS_MAP_ERRORS,
333};
334
335pub const CONTAINERS_MAP_SUBSREF_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
336    signatures: &CONTAINERS_MAP_SUBSREF_SIGNATURES,
337    output_mode: BuiltinOutputMode::Fixed,
338    completion_policy: BuiltinCompletionPolicy::Public,
339    errors: &CONTAINERS_MAP_ERRORS,
340};
341
342pub const CONTAINERS_MAP_SUBSASGN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
343    signatures: &CONTAINERS_MAP_SUBSASGN_SIGNATURES,
344    output_mode: BuiltinOutputMode::Fixed,
345    completion_policy: BuiltinCompletionPolicy::Public,
346    errors: &CONTAINERS_MAP_ERRORS,
347};
348
349#[runmat_macros::register_gpu_spec(
350    builtin_path = "crate::builtins::containers::map::containers_map"
351)]
352pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
353    name: "containers.Map",
354    op_kind: GpuOpKind::Custom("map"),
355    supported_precisions: &[],
356    broadcast: BroadcastSemantics::None,
357    provider_hooks: &[],
358    constant_strategy: ConstantStrategy::InlineLiteral,
359    residency: ResidencyPolicy::GatherImmediately,
360    nan_mode: ReductionNaN::Include,
361    two_pass_threshold: None,
362    workgroup_size: None,
363    accepts_nan_mode: false,
364    notes: "Map storage is host-resident; GPU inputs are gathered only when split into multiple entries.",
365};
366
367fn map_error_with_detail(
368    error: &'static BuiltinErrorDescriptor,
369    detail: impl AsRef<str>,
370    builtin: &'static str,
371) -> RuntimeError {
372    let raw = detail.as_ref().trim();
373    let normalized = raw
374        .strip_prefix("containers.Map:")
375        .map(str::trim)
376        .unwrap_or(raw);
377    let message = if normalized.is_empty() {
378        error.message.to_string()
379    } else {
380        format!("{}: {}", error.message, normalized)
381    };
382    let mut builder = build_runtime_error(message).with_builtin(builtin);
383    if let Some(identifier) = error.identifier {
384        builder = builder.with_identifier(identifier);
385    }
386    builder.build()
387}
388
389fn map_descriptor_error(
390    error: &'static BuiltinErrorDescriptor,
391    builtin: &'static str,
392) -> RuntimeError {
393    map_error_with_detail(error, "", builtin)
394}
395
396fn map_invalid(detail: impl AsRef<str>, builtin: &'static str) -> RuntimeError {
397    map_error_with_detail(&CONTAINERS_MAP_ERROR_INVALID_ARGUMENT, detail, builtin)
398}
399
400fn map_internal(detail: impl AsRef<str>, builtin: &'static str) -> RuntimeError {
401    map_error_with_detail(&CONTAINERS_MAP_ERROR_INTERNAL, detail, builtin)
402}
403
404fn map_error(message: impl Into<String>, builtin: &'static str) -> RuntimeError {
405    map_invalid(message.into(), builtin)
406}
407
408fn attach_builtin_context(mut error: RuntimeError, builtin: &'static str) -> RuntimeError {
409    if error.context.builtin.is_none() {
410        error.context = error.context.with_builtin(builtin);
411    }
412    error
413}
414
415#[runmat_macros::register_fusion_spec(
416    builtin_path = "crate::builtins::containers::map::containers_map"
417)]
418pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
419    name: "containers.Map",
420    shape: ShapeRequirements::Any,
421    constant_strategy: ConstantStrategy::InlineLiteral,
422    elementwise: None,
423    reduction: None,
424    emits_nan: false,
425    notes: "Handles act as fusion sinks; map construction terminates GPU fusion plans.",
426};
427
428static NEXT_ID: AtomicU64 = AtomicU64::new(1);
429static MAP_REGISTRY: Lazy<RwLock<HashMap<u64, MapStore>>> =
430    Lazy::new(|| RwLock::new(HashMap::new()));
431
432#[derive(Clone, Copy, Debug, PartialEq, Eq)]
433enum KeyType {
434    Char,
435    String,
436    Double,
437    Single,
438    Int32,
439    UInt32,
440    Int64,
441    UInt64,
442    Logical,
443}
444
445impl KeyType {
446    fn matlab_name(self) -> &'static str {
447        match self {
448            KeyType::Char => "char",
449            KeyType::String => "string",
450            KeyType::Double => "double",
451            KeyType::Single => "single",
452            KeyType::Int32 => "int32",
453            KeyType::UInt32 => "uint32",
454            KeyType::Int64 => "int64",
455            KeyType::UInt64 => "uint64",
456            KeyType::Logical => "logical",
457        }
458    }
459
460    fn parse(value: &Value, builtin: &'static str) -> BuiltinResult<Self> {
461        let text = string_from_value(value, "containers.Map: expected a KeyType string", builtin)?;
462        match text.to_ascii_lowercase().as_str() {
463            "char" | "character" => Ok(KeyType::Char),
464            "string" => Ok(KeyType::String),
465            "double" => Ok(KeyType::Double),
466            "single" => Ok(KeyType::Single),
467            "int32" => Ok(KeyType::Int32),
468            "uint32" => Ok(KeyType::UInt32),
469            "int64" => Ok(KeyType::Int64),
470            "uint64" => Ok(KeyType::UInt64),
471            "logical" => Ok(KeyType::Logical),
472            other => Err(map_error(
473                format!(
474                    "containers.Map: unsupported KeyType '{other}'. Valid types: char, string, double, int32, uint32, int64, uint64, logical."
475                ),
476                builtin,
477            )),
478        }
479    }
480}
481
482#[derive(Clone, Copy, Debug, PartialEq, Eq)]
483enum ValueType {
484    Any,
485    Char,
486    String,
487    Double,
488    Single,
489    Logical,
490}
491
492impl ValueType {
493    fn matlab_name(self) -> &'static str {
494        match self {
495            ValueType::Any => "any",
496            ValueType::Char => "char",
497            ValueType::String => "string",
498            ValueType::Double => "double",
499            ValueType::Single => "single",
500            ValueType::Logical => "logical",
501        }
502    }
503
504    fn parse(value: &Value, builtin: &'static str) -> BuiltinResult<Self> {
505        let text = string_from_value(
506            value,
507            "containers.Map: expected a ValueType string",
508            builtin,
509        )?;
510        match text.to_ascii_lowercase().as_str() {
511            "any" => Ok(ValueType::Any),
512            "char" | "character" => Ok(ValueType::Char),
513            "string" => Ok(ValueType::String),
514            "double" => Ok(ValueType::Double),
515            "single" => Ok(ValueType::Single),
516            "logical" => Ok(ValueType::Logical),
517            other => Err(map_error(
518                format!(
519                    "containers.Map: unsupported ValueType '{other}'. Valid types: any, char, string, double, single, logical."
520                ),
521                builtin,
522            )),
523        }
524    }
525
526    fn normalize(&self, value: Value, builtin: &'static str) -> BuiltinResult<Value> {
527        match self {
528            ValueType::Any => Ok(value),
529            ValueType::Char => {
530                let chars = char_array_from_value(&value, builtin)?;
531                Ok(Value::CharArray(chars))
532            }
533            ValueType::String => {
534                let text = string_from_value(
535                    &value,
536                    "containers.Map: values must be string scalars",
537                    builtin,
538                )?;
539                Ok(Value::String(text))
540            }
541            ValueType::Double | ValueType::Single => normalize_numeric_value(value, builtin),
542            ValueType::Logical => normalize_logical_value(value, builtin),
543        }
544    }
545}
546
547#[derive(Clone, PartialEq, Eq, Hash)]
548enum NormalizedKey {
549    String(String),
550    Float(u64),
551    Int(i64),
552    UInt(u64),
553    Bool(bool),
554}
555
556#[derive(Clone)]
557struct MapEntry {
558    normalized: NormalizedKey,
559    key_value: Value,
560    value: Value,
561}
562
563struct MapStore {
564    key_type: KeyType,
565    value_type: ValueType,
566    uniform_values: bool,
567    uniform_class: Option<ValueClass>,
568    entries: Vec<MapEntry>,
569    index: HashMap<NormalizedKey, usize>,
570}
571
572impl MapStore {
573    fn new(key_type: KeyType, value_type: ValueType, uniform_values: bool) -> Self {
574        Self {
575            key_type,
576            value_type,
577            uniform_values,
578            uniform_class: None,
579            entries: Vec::new(),
580            index: HashMap::new(),
581        }
582    }
583
584    fn len(&self) -> usize {
585        self.entries.len()
586    }
587
588    fn contains(&self, key: &NormalizedKey) -> bool {
589        self.index.contains_key(key)
590    }
591
592    fn get(&self, key: &NormalizedKey) -> Option<Value> {
593        self.index
594            .get(key)
595            .map(|&idx| self.entries[idx].value.clone())
596    }
597
598    fn insert_new(&mut self, mut entry: MapEntry, builtin: &'static str) -> BuiltinResult<()> {
599        if self.index.contains_key(&entry.normalized) {
600            return Err(map_error(
601                "containers.Map: Duplicate key name was provided.",
602                builtin,
603            ));
604        }
605        entry.value = self.normalize_value(entry.value, builtin)?;
606        self.track_uniform_class(&entry.value, builtin)?;
607        let idx = self.entries.len();
608        self.entries.push(entry.clone());
609        self.index.insert(entry.normalized, idx);
610        Ok(())
611    }
612
613    fn set(&mut self, mut entry: MapEntry, builtin: &'static str) -> BuiltinResult<()> {
614        entry.value = self.normalize_value(entry.value, builtin)?;
615        self.track_uniform_class(&entry.value, builtin)?;
616        if let Some(&idx) = self.index.get(&entry.normalized) {
617            self.entries[idx].value = entry.value.clone();
618            self.entries[idx].key_value = entry.key_value;
619        } else {
620            let idx = self.entries.len();
621            self.entries.push(entry.clone());
622            self.index.insert(entry.normalized, idx);
623        }
624        Ok(())
625    }
626
627    fn remove(&mut self, key: &NormalizedKey, builtin: &'static str) -> BuiltinResult<()> {
628        let idx = match self.index.get(key) {
629            Some(&idx) => idx,
630            None => {
631                return Err(map_descriptor_error(
632                    &CONTAINERS_MAP_ERROR_MISSING_KEY,
633                    builtin,
634                ));
635            }
636        };
637        self.entries.remove(idx);
638        self.index.clear();
639        for (pos, entry) in self.entries.iter().enumerate() {
640            self.index.insert(entry.normalized.clone(), pos);
641        }
642        if self.entries.is_empty() {
643            self.uniform_class = None;
644        }
645        Ok(())
646    }
647
648    fn keys(&self) -> Vec<Value> {
649        self.entries
650            .iter()
651            .map(|entry| entry.key_value.clone())
652            .collect()
653    }
654
655    fn values(&self) -> Vec<Value> {
656        self.entries
657            .iter()
658            .map(|entry| entry.value.clone())
659            .collect()
660    }
661
662    fn normalize_value(&self, value: Value, builtin: &'static str) -> BuiltinResult<Value> {
663        self.value_type.normalize(value, builtin)
664    }
665
666    fn track_uniform_class(&mut self, value: &Value, builtin: &'static str) -> BuiltinResult<()> {
667        if !self.uniform_values {
668            return Ok(());
669        }
670        let class = ValueClass::from_value(value);
671        if let Some(existing) = &self.uniform_class {
672            if existing != &class {
673                return Err(map_error(
674                    "containers.Map: UniformValues=true requires all values to share the same MATLAB class.",
675                    builtin,
676                ));
677            }
678        } else {
679            self.uniform_class = Some(class);
680        }
681        Ok(())
682    }
683}
684
685#[derive(Clone, Debug, PartialEq, Eq)]
686enum ValueClass {
687    Char,
688    String,
689    Double,
690    Logical,
691    Int,
692    UInt,
693    Cell,
694    Struct,
695    Object,
696    Other(&'static str),
697}
698
699impl ValueClass {
700    fn from_value(value: &Value) -> Self {
701        match value {
702            Value::CharArray(_) => ValueClass::Char,
703            Value::String(_) | Value::StringArray(_) => ValueClass::String,
704            Value::Num(_) | Value::Tensor(_) | Value::ComplexTensor(_) => ValueClass::Double,
705            Value::Bool(_) | Value::LogicalArray(_) => ValueClass::Logical,
706            Value::Int(i) => match i {
707                IntValue::I8(_) | IntValue::I16(_) | IntValue::I32(_) | IntValue::I64(_) => {
708                    ValueClass::Int
709                }
710                IntValue::U8(_) | IntValue::U16(_) | IntValue::U32(_) | IntValue::U64(_) => {
711                    ValueClass::UInt
712                }
713            },
714            Value::Cell(_) => ValueClass::Cell,
715            Value::Struct(_) => ValueClass::Struct,
716            Value::Object(_) | Value::HandleObject(_) | Value::Listener(_) => ValueClass::Object,
717            _ => ValueClass::Other("other"),
718        }
719    }
720}
721
722struct ConstructorArgs {
723    key_type: KeyType,
724    value_type: ValueType,
725    uniform_values: bool,
726    keys: Vec<KeyCandidate>,
727    values: Vec<Value>,
728}
729
730struct KeyCandidate {
731    normalized: NormalizedKey,
732    canonical: Value,
733}
734
735#[runtime_builtin(
736    name = "containers.Map",
737    category = "containers/map",
738    summary = "Create key-value dictionary objects.",
739    keywords = "map,containers.Map,dictionary,hash map,lookup",
740    accel = "metadata",
741    sink = true,
742    type_resolver(map_handle_type),
743    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_DESCRIPTOR),
744    builtin_path = "crate::builtins::containers::map::containers_map"
745)]
746async fn containers_map_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
747    let parsed = parse_constructor_args(args, BUILTIN_CONSTRUCTOR).await?;
748    let store = build_store(parsed, BUILTIN_CONSTRUCTOR)?;
749    allocate_handle(store, BUILTIN_CONSTRUCTOR)
750}
751
752#[runtime_builtin(
753    name = "containers.Map.keys",
754    type_resolver(map_cell_type),
755    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_KEYS_DESCRIPTOR),
756    builtin_path = "crate::builtins::containers::map::containers_map"
757)]
758async fn containers_map_keys(map: Value) -> crate::BuiltinResult<Value> {
759    with_store(&map, BUILTIN_KEYS, |store| {
760        let values = store.keys();
761        make_row_cell(values, BUILTIN_KEYS)
762    })
763}
764
765#[runtime_builtin(
766    name = "containers.Map.values",
767    type_resolver(map_cell_type),
768    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_VALUES_DESCRIPTOR),
769    builtin_path = "crate::builtins::containers::map::containers_map"
770)]
771async fn containers_map_values(map: Value) -> crate::BuiltinResult<Value> {
772    with_store(&map, BUILTIN_VALUES, |store| {
773        let values = store.values();
774        make_row_cell(values, BUILTIN_VALUES)
775    })
776}
777
778#[runtime_builtin(
779    name = "containers.Map.isKey",
780    type_resolver(map_is_key_type),
781    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_ISKEY_DESCRIPTOR),
782    builtin_path = "crate::builtins::containers::map::containers_map"
783)]
784async fn containers_map_is_key(map: Value, key_spec: Value) -> crate::BuiltinResult<Value> {
785    let key_type = with_store(&map, BUILTIN_IS_KEY, |store| Ok(store.key_type))?;
786    let collection = collect_key_spec(&key_spec, key_type, BUILTIN_IS_KEY).await?;
787    with_store(&map, BUILTIN_IS_KEY, |store| {
788        let mut flags = Vec::with_capacity(collection.values.len());
789        for value in &collection.values {
790            let normalized = normalize_key(value, store.key_type, BUILTIN_IS_KEY)?;
791            flags.push(store.contains(&normalized));
792        }
793        if collection.values.len() == 1 {
794            Ok(Value::Bool(flags[0]))
795        } else {
796            let data: Vec<u8> = flags.into_iter().map(|b| if b { 1 } else { 0 }).collect();
797            let logical = LogicalArray::new(data, collection.shape)
798                .map_err(|e| map_error(format!("containers.Map: {e}"), BUILTIN_IS_KEY))?;
799            Ok(Value::LogicalArray(logical))
800        }
801    })
802}
803
804#[runtime_builtin(
805    name = "containers.Map.remove",
806    type_resolver(map_handle_type),
807    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_REMOVE_DESCRIPTOR),
808    builtin_path = "crate::builtins::containers::map::containers_map"
809)]
810async fn containers_map_remove(map: Value, key_spec: Value) -> crate::BuiltinResult<Value> {
811    let key_type = with_store(&map, BUILTIN_REMOVE, |store| Ok(store.key_type))?;
812    let collection = collect_key_spec(&key_spec, key_type, BUILTIN_REMOVE).await?;
813    with_store_mut(&map, BUILTIN_REMOVE, |store| {
814        for value in &collection.values {
815            let normalized = normalize_key(value, store.key_type, BUILTIN_REMOVE)?;
816            store.remove(&normalized, BUILTIN_REMOVE)?;
817        }
818        Ok(())
819    })?;
820    Ok(map)
821}
822
823#[runtime_builtin(
824    name = "containers.Map.subsref",
825    type_resolver(map_unknown_type),
826    descriptor(
827        crate::builtins::containers::map::containers_map::CONTAINERS_MAP_SUBSREF_DESCRIPTOR
828    ),
829    builtin_path = "crate::builtins::containers::map::containers_map"
830)]
831async fn containers_map_subsref(
832    map: Value,
833    kind: String,
834    payload: Value,
835) -> crate::BuiltinResult<Value> {
836    if !matches!(map, Value::HandleObject(_)) {
837        return Err(map_error(
838            format!("containers.Map: subsref expects a containers.Map handle, got {map:?}"),
839            BUILTIN_SUBSREF,
840        ));
841    }
842    match kind.as_str() {
843        OBJECT_INDEX_PAREN => {
844            let mut args = extract_key_arguments(&payload, BUILTIN_SUBSREF)?;
845            if args.is_empty() {
846                return Err(map_error(
847                    "containers.Map: indexing requires at least one key",
848                    BUILTIN_SUBSREF,
849                ));
850            }
851            if args.len() != 1 {
852                return Err(map_error(
853                    "containers.Map: indexing expects a single key argument",
854                    BUILTIN_SUBSREF,
855                ));
856            }
857            let key_arg = args.remove(0);
858            let key_type = with_store(&map, BUILTIN_SUBSREF, |store| Ok(store.key_type))?;
859            let collection = collect_key_spec(&key_arg, key_type, BUILTIN_SUBSREF).await?;
860            with_store(&map, BUILTIN_SUBSREF, |store| {
861                if collection.values.is_empty() {
862                    return crate::make_cell_with_shape(Vec::new(), collection.shape.clone())
863                        .map_err(|e| map_error(format!("containers.Map: {e}"), BUILTIN_SUBSREF));
864                }
865                if collection.values.len() == 1 {
866                    let normalized =
867                        normalize_key(&collection.values[0], store.key_type, BUILTIN_SUBSREF)?;
868                    store.get(&normalized).ok_or_else(|| {
869                        map_descriptor_error(&CONTAINERS_MAP_ERROR_MISSING_KEY, BUILTIN_SUBSREF)
870                    })
871                } else {
872                    let mut results = Vec::with_capacity(collection.values.len());
873                    for value in &collection.values {
874                        let normalized = normalize_key(value, store.key_type, BUILTIN_SUBSREF)?;
875                        let stored = store.get(&normalized).ok_or_else(|| {
876                            map_descriptor_error(&CONTAINERS_MAP_ERROR_MISSING_KEY, BUILTIN_SUBSREF)
877                        })?;
878                        results.push(stored);
879                    }
880                    crate::make_cell_with_shape(results, collection.shape.clone())
881                        .map_err(|e| map_error(format!("containers.Map: {e}"), BUILTIN_SUBSREF))
882                }
883            })
884        }
885        OBJECT_INDEX_MEMBER => {
886            let field = string_from_value(
887                &payload,
888                "containers.Map: property name must be text",
889                BUILTIN_SUBSREF,
890            )?;
891            with_store(&map, BUILTIN_SUBSREF, |store| {
892                match field.to_ascii_lowercase().as_str() {
893                    "count" => Ok(Value::Num(store.len() as f64)),
894                    "keytype" => char_array_value(store.key_type.matlab_name(), BUILTIN_SUBSREF),
895                    "valuetype" => {
896                        char_array_value(store.value_type.matlab_name(), BUILTIN_SUBSREF)
897                    }
898                    other => Err(map_error(
899                        format!("containers.Map: no such property '{other}'"),
900                        BUILTIN_SUBSREF,
901                    )),
902                }
903            })
904        }
905        OBJECT_INDEX_BRACE => Err(map_error(
906            "containers.Map: curly-brace indexing is not supported.",
907            BUILTIN_SUBSREF,
908        )),
909        other => Err(map_error(
910            format!("containers.Map: unsupported indexing kind '{other}'"),
911            BUILTIN_SUBSREF,
912        )),
913    }
914}
915
916#[runtime_builtin(
917    name = "containers.Map.subsasgn",
918    type_resolver(map_handle_type),
919    descriptor(
920        crate::builtins::containers::map::containers_map::CONTAINERS_MAP_SUBSASGN_DESCRIPTOR
921    ),
922    builtin_path = "crate::builtins::containers::map::containers_map"
923)]
924async fn containers_map_subsasgn(
925    map: Value,
926    kind: String,
927    payload: Value,
928    rhs: Value,
929) -> crate::BuiltinResult<Value> {
930    if !matches!(map, Value::HandleObject(_)) {
931        return Err(map_error(
932            format!("containers.Map: subsasgn expects a containers.Map handle, got {map:?}"),
933            BUILTIN_SUBSASGN,
934        ));
935    }
936    match kind.as_str() {
937        OBJECT_INDEX_PAREN => {
938            let mut args = extract_key_arguments(&payload, BUILTIN_SUBSASGN)?;
939            if args.is_empty() {
940                return Err(map_error(
941                    "containers.Map: assignment requires at least one key",
942                    BUILTIN_SUBSASGN,
943                ));
944            }
945            if args.len() != 1 {
946                return Err(map_error(
947                    "containers.Map: assignment expects a single key argument",
948                    BUILTIN_SUBSASGN,
949                ));
950            }
951            let key_arg = args.remove(0);
952            let key_type = with_store(&map, BUILTIN_SUBSASGN, |store| Ok(store.key_type))?;
953            let KeyCollection {
954                values: key_values, ..
955            } = collect_key_spec(&key_arg, key_type, BUILTIN_SUBSASGN).await?;
956            let values =
957                expand_assignment_values(rhs.clone(), key_values.len(), BUILTIN_SUBSASGN).await?;
958            with_store_mut(&map, BUILTIN_SUBSASGN, move |store| {
959                for (key_raw, value) in key_values.into_iter().zip(values.into_iter()) {
960                    let (normalized, canonical) =
961                        canonicalize_key(key_raw, store.key_type, BUILTIN_SUBSASGN)?;
962                    let entry = MapEntry {
963                        normalized,
964                        key_value: canonical,
965                        value,
966                    };
967                    store.set(entry, BUILTIN_SUBSASGN)?;
968                }
969                Ok(())
970            })?;
971            Ok(map)
972        }
973        OBJECT_INDEX_MEMBER => Err(map_error(
974            "containers.Map: property assignments are not supported.",
975            BUILTIN_SUBSASGN,
976        )),
977        OBJECT_INDEX_BRACE => Err(map_error(
978            "containers.Map: curly-brace assignment is not supported.",
979            BUILTIN_SUBSASGN,
980        )),
981        other => Err(map_error(
982            format!("containers.Map: unsupported assignment kind '{other}'"),
983            BUILTIN_SUBSASGN,
984        )),
985    }
986}
987
988async fn parse_constructor_args(
989    args: Vec<Value>,
990    builtin: &'static str,
991) -> BuiltinResult<ConstructorArgs> {
992    let mut index = 0usize;
993    let mut keys_input: Option<Value> = None;
994    let mut values_input: Option<Value> = None;
995
996    if index < args.len() && keyword_of(&args[index]).is_none() {
997        if args.len() < 2 {
998            return Err(map_error(
999                "containers.Map: constructor requires both keys and values when either is provided.",
1000                builtin,
1001            ));
1002        }
1003        keys_input = Some(args[index].clone());
1004        values_input = Some(args[index + 1].clone());
1005        index += 2;
1006    }
1007
1008    let mut key_type = KeyType::Char;
1009    let mut value_type = ValueType::Any;
1010    let mut uniform_values = false;
1011    while index < args.len() {
1012        let keyword = keyword_of(&args[index]).ok_or_else(|| {
1013            map_error(
1014                "containers.Map: expected option name (e.g. 'KeyType')",
1015                builtin,
1016            )
1017        })?;
1018        index += 1;
1019        let Some(value) = args.get(index) else {
1020            return Err(map_error(
1021                format!("containers.Map: missing value for option '{keyword}'"),
1022                builtin,
1023            ));
1024        };
1025        index += 1;
1026        match keyword.as_str() {
1027            "keytype" => key_type = KeyType::parse(value, builtin)?,
1028            "valuetype" => value_type = ValueType::parse(value, builtin)?,
1029            "uniformvalues" => {
1030                uniform_values = bool_from_value(
1031                    value,
1032                    "containers.Map: UniformValues must be logical",
1033                    builtin,
1034                )?
1035            }
1036            "comparisonmethod" => {
1037                let text = string_from_value(
1038                    value,
1039                    "containers.Map: ComparisonMethod must be a string",
1040                    builtin,
1041                )?;
1042                let lowered = text.to_ascii_lowercase();
1043                if lowered != "strcmp" {
1044                    return Err(map_error(
1045                        "containers.Map: only ComparisonMethod='strcmp' is supported.",
1046                        builtin,
1047                    ));
1048                }
1049            }
1050            other => {
1051                return Err(map_error(
1052                    format!("containers.Map: unrecognised option '{other}'"),
1053                    builtin,
1054                ));
1055            }
1056        }
1057    }
1058
1059    let keys = match keys_input {
1060        Some(value) => prepare_keys(value, key_type, builtin).await?,
1061        None => Vec::new(),
1062    };
1063
1064    let values = match values_input {
1065        Some(value) => prepare_values(value, builtin).await?,
1066        None => Vec::new(),
1067    };
1068
1069    if keys.len() != values.len() {
1070        return Err(map_error(
1071            format!(
1072                "containers.Map: number of keys ({}) must match number of values ({})",
1073                keys.len(),
1074                values.len()
1075            ),
1076            builtin,
1077        ));
1078    }
1079
1080    Ok(ConstructorArgs {
1081        key_type,
1082        value_type,
1083        uniform_values,
1084        keys,
1085        values,
1086    })
1087}
1088
1089fn build_store(args: ConstructorArgs, builtin: &'static str) -> BuiltinResult<MapStore> {
1090    let mut store = MapStore::new(args.key_type, args.value_type, args.uniform_values);
1091    for (candidate, value) in args.keys.into_iter().zip(args.values.into_iter()) {
1092        store.insert_new(
1093            MapEntry {
1094                normalized: candidate.normalized,
1095                key_value: candidate.canonical,
1096                value,
1097            },
1098            builtin,
1099        )?;
1100    }
1101    Ok(store)
1102}
1103
1104fn allocate_handle(store: MapStore, builtin: &'static str) -> BuiltinResult<Value> {
1105    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
1106    MAP_REGISTRY
1107        .write()
1108        .map_err(|_| map_internal("containers.Map: registry lock poisoned", builtin))?
1109        .insert(id, store);
1110    let mut struct_value = StructValue::new();
1111    struct_value
1112        .fields
1113        .insert("id".to_string(), Value::Int(IntValue::U64(id)));
1114    let storage = Value::Struct(struct_value);
1115    let gc = runmat_gc::gc_allocate(storage)
1116        .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
1117    Ok(Value::HandleObject(HandleRef {
1118        class_name: CLASS_NAME.to_string(),
1119        target: gc,
1120        valid: true,
1121    }))
1122}
1123
1124fn with_store<F, R>(map: &Value, builtin: &'static str, f: F) -> BuiltinResult<R>
1125where
1126    F: FnOnce(&MapStore) -> BuiltinResult<R>,
1127{
1128    let handle = extract_handle(map, builtin)?;
1129    ensure_handle(handle, builtin)?;
1130    let id = map_id(handle, builtin)?;
1131    let guard = MAP_REGISTRY
1132        .read()
1133        .map_err(|_| map_internal("containers.Map: registry lock poisoned", builtin))?;
1134    let store = guard
1135        .get(&id)
1136        .ok_or_else(|| map_internal("containers.Map: internal storage not found", builtin))?;
1137    f(store)
1138}
1139
1140fn with_store_mut<F, R>(map: &Value, builtin: &'static str, f: F) -> BuiltinResult<R>
1141where
1142    F: FnOnce(&mut MapStore) -> BuiltinResult<R>,
1143{
1144    let handle = extract_handle(map, builtin)?;
1145    ensure_handle(handle, builtin)?;
1146    let id = map_id(handle, builtin)?;
1147    let mut guard = MAP_REGISTRY
1148        .write()
1149        .map_err(|_| map_internal("containers.Map: registry lock poisoned", builtin))?;
1150    let store = guard
1151        .get_mut(&id)
1152        .ok_or_else(|| map_internal("containers.Map: internal storage not found", builtin))?;
1153    f(store)
1154}
1155
1156fn extract_handle<'a>(value: &'a Value, builtin: &'static str) -> BuiltinResult<&'a HandleRef> {
1157    match value {
1158        Value::HandleObject(handle) => Ok(handle),
1159        _ => Err(map_error(
1160            "containers.Map: expected a containers.Map handle",
1161            builtin,
1162        )),
1163    }
1164}
1165
1166fn ensure_handle(handle: &HandleRef, builtin: &'static str) -> BuiltinResult<()> {
1167    if !runmat_builtins::is_handle_valid(handle) {
1168        return Err(map_error("containers.Map: handle is invalid", builtin));
1169    }
1170    if handle.class_name != CLASS_NAME {
1171        return Err(map_error(
1172            format!(
1173                "containers.Map: expected handle of class '{}', got '{}'",
1174                CLASS_NAME, handle.class_name
1175            ),
1176            builtin,
1177        ));
1178    }
1179    Ok(())
1180}
1181
1182fn map_id(handle: &HandleRef, builtin: &'static str) -> BuiltinResult<u64> {
1183    let storage = unsafe { &*handle.target.as_raw() };
1184    match storage {
1185        Value::Struct(StructValue { fields }) => match fields.get("id") {
1186            Some(Value::Int(IntValue::U64(id))) => Ok(*id),
1187            Some(Value::Int(other)) => {
1188                let id = other.to_i64();
1189                if id < 0 {
1190                    Err(map_internal(
1191                        "containers.Map: negative map identifier",
1192                        builtin,
1193                    ))
1194                } else {
1195                    Ok(id as u64)
1196                }
1197            }
1198            Some(Value::Num(n)) if *n >= 0.0 => Ok(*n as u64),
1199            _ => Err(map_internal(
1200                "containers.Map: corrupted storage identifier",
1201                builtin,
1202            )),
1203        },
1204        other => Err(map_internal(
1205            format!("containers.Map: internal storage has unexpected shape {other:?}"),
1206            builtin,
1207        )),
1208    }
1209}
1210
1211async fn prepare_keys(
1212    value: Value,
1213    key_type: KeyType,
1214    builtin: &'static str,
1215) -> BuiltinResult<Vec<KeyCandidate>> {
1216    let host = gather_if_needed_async(&value)
1217        .await
1218        .map_err(|err| attach_builtin_context(err, builtin))?;
1219    let flattened = flatten_keys(&host, key_type, builtin).await?;
1220    let mut out = Vec::with_capacity(flattened.len());
1221    for raw_key in flattened {
1222        let (normalized, canonical) = canonicalize_key(raw_key, key_type, builtin)?;
1223        out.push(KeyCandidate {
1224            normalized,
1225            canonical,
1226        });
1227    }
1228    Ok(out)
1229}
1230
1231async fn prepare_values(value: Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
1232    let host = gather_if_needed_async(&value)
1233        .await
1234        .map_err(|err| attach_builtin_context(err, builtin))?;
1235    flatten_values(&host, builtin).await
1236}
1237
1238async fn flatten_keys(
1239    value: &Value,
1240    key_type: KeyType,
1241    builtin: &'static str,
1242) -> BuiltinResult<Vec<Value>> {
1243    match value {
1244        Value::Cell(cell) => {
1245            let mut out = Vec::with_capacity(cell.data.len());
1246            for ptr in &cell.data {
1247                let element = unsafe { &*ptr.as_raw() };
1248                if matches!(element, Value::Cell(_)) {
1249                    return Err(map_error(
1250                        "containers.Map: nested cell arrays are not supported for keys",
1251                        builtin,
1252                    ));
1253                }
1254                out.push(
1255                    gather_if_needed_async(element)
1256                        .await
1257                        .map_err(|err| attach_builtin_context(err, builtin))?,
1258                );
1259            }
1260            Ok(out)
1261        }
1262        Value::StringArray(sa) => Ok(sa
1263            .data
1264            .iter()
1265            .map(|text| Value::String(text.clone()))
1266            .collect()),
1267        Value::CharArray(ca) => Ok(char_array_rows(ca, builtin)?),
1268        Value::LogicalArray(arr) => {
1269            if key_type != KeyType::Logical {
1270                return Err(map_error(
1271                    "containers.Map: logical arrays can only be used with KeyType='logical'",
1272                    builtin,
1273                ));
1274            }
1275            Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect())
1276        }
1277        Value::Tensor(t) => {
1278            if !t.shape.is_empty() && t.data.len() != 1 && !is_vector_shape(&t.shape) {
1279                return Err(map_error(
1280                    "containers.Map: numeric keys must be scalar or vector shaped",
1281                    builtin,
1282                ));
1283            }
1284            Ok(t.data.iter().map(|&v| Value::Num(v)).collect())
1285        }
1286        Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::String(_) => {
1287            Ok(vec![value.clone()])
1288        }
1289        Value::GpuTensor(_) => Err(map_error(
1290            "containers.Map: GPU keys must be gathered to the host before construction",
1291            builtin,
1292        )),
1293        other => Err(map_error(
1294            format!("containers.Map: unsupported key container {other:?}"),
1295            builtin,
1296        )),
1297    }
1298}
1299
1300async fn flatten_values(value: &Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
1301    match value {
1302        Value::Cell(cell) => {
1303            let mut out = Vec::with_capacity(cell.data.len());
1304            for ptr in &cell.data {
1305                out.push(
1306                    gather_if_needed_async(unsafe { &*ptr.as_raw() })
1307                        .await
1308                        .map_err(|err| attach_builtin_context(err, builtin))?,
1309                );
1310            }
1311            Ok(out)
1312        }
1313        Value::StringArray(sa) => Ok(sa
1314            .data
1315            .iter()
1316            .map(|text| Value::String(text.clone()))
1317            .collect()),
1318        Value::CharArray(ca) => Ok(char_array_rows(ca, builtin)?),
1319        Value::LogicalArray(arr) => Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect()),
1320        Value::Tensor(t) => {
1321            if !t.shape.is_empty() && !is_vector_shape(&t.shape) && t.data.len() != 1 {
1322                return Err(map_error(
1323                    "containers.Map: numeric values must be scalar or vector shaped",
1324                    builtin,
1325                ));
1326            }
1327            Ok(t.data.iter().map(|&v| Value::Num(v)).collect())
1328        }
1329        _ => Ok(vec![value.clone()]),
1330    }
1331}
1332
1333fn char_array_rows(ca: &CharArray, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
1334    if ca.rows == 0 {
1335        return Ok(Vec::new());
1336    }
1337    let mut out = Vec::with_capacity(ca.rows);
1338    for row in 0..ca.rows {
1339        let mut text = String::with_capacity(ca.cols);
1340        for col in 0..ca.cols {
1341            text.push(ca.data[row * ca.cols + col]);
1342        }
1343        let chars: Vec<char> = text.chars().collect();
1344        let array = CharArray::new(chars.clone(), 1, chars.len())
1345            .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
1346        out.push(Value::CharArray(array));
1347    }
1348    Ok(out)
1349}
1350
1351fn is_vector_shape(shape: &[usize]) -> bool {
1352    match shape.len() {
1353        0 => true,
1354        1 => true,
1355        2 => shape[0] == 1 || shape[1] == 1,
1356        _ => false,
1357    }
1358}
1359
1360fn canonicalize_key(
1361    value: Value,
1362    key_type: KeyType,
1363    builtin: &'static str,
1364) -> BuiltinResult<(NormalizedKey, Value)> {
1365    let normalized = normalize_key(&value, key_type, builtin)?;
1366    let canonical = match key_type {
1367        KeyType::Char => Value::CharArray(char_array_from_value(&value, builtin)?),
1368        KeyType::String => Value::String(string_from_value(
1369            &value,
1370            "containers.Map: keys must be string scalars",
1371            builtin,
1372        )?),
1373        KeyType::Double => Value::Num(numeric_from_value(
1374            &value,
1375            "containers.Map: keys must be numeric scalars",
1376            builtin,
1377        )?),
1378        KeyType::Single => Value::Num(numeric_from_value(
1379            &value,
1380            "containers.Map: keys must be numeric scalars",
1381            builtin,
1382        )?),
1383        KeyType::Int32 => Value::Int(IntValue::I32(integer_from_value(
1384            &value,
1385            i32::MIN as i64,
1386            i32::MAX as i64,
1387            "containers.Map: int32 keys must be integers",
1388            builtin,
1389        )? as i32)),
1390        KeyType::UInt32 => Value::Int(IntValue::U32(unsigned_from_value(
1391            &value,
1392            u32::MAX as u64,
1393            "containers.Map: uint32 keys must be unsigned integers",
1394            builtin,
1395        )? as u32)),
1396        KeyType::Int64 => Value::Int(IntValue::I64(integer_from_value(
1397            &value,
1398            i64::MIN,
1399            i64::MAX,
1400            "containers.Map: int64 keys must be integers",
1401            builtin,
1402        )?)),
1403        KeyType::UInt64 => Value::Int(IntValue::U64(unsigned_from_value(
1404            &value,
1405            u64::MAX,
1406            "containers.Map: uint64 keys must be unsigned integers",
1407            builtin,
1408        )?)),
1409        KeyType::Logical => Value::Bool(bool_from_value(
1410            &value,
1411            "containers.Map: logical keys must be logical scalars",
1412            builtin,
1413        )?),
1414    };
1415    Ok((normalized, canonical))
1416}
1417
1418fn normalize_key(
1419    value: &Value,
1420    key_type: KeyType,
1421    builtin: &'static str,
1422) -> BuiltinResult<NormalizedKey> {
1423    match key_type {
1424        KeyType::Char | KeyType::String => {
1425            let text =
1426                string_from_value(value, "containers.Map: keys must be text scalars", builtin)?;
1427            Ok(NormalizedKey::String(text))
1428        }
1429        KeyType::Double | KeyType::Single => {
1430            let numeric = numeric_from_value(
1431                value,
1432                "containers.Map: keys must be numeric scalars",
1433                builtin,
1434            )?;
1435            if !numeric.is_finite() {
1436                return Err(map_error(
1437                    "containers.Map: keys must be finite numeric scalars",
1438                    builtin,
1439                ));
1440            }
1441            let canonical = if numeric == 0.0 { 0.0 } else { numeric };
1442            Ok(NormalizedKey::Float(canonical.to_bits()))
1443        }
1444        KeyType::Int32 | KeyType::Int64 => {
1445            let bounds = if key_type == KeyType::Int32 {
1446                (i32::MIN as i64, i32::MAX as i64)
1447            } else {
1448                (i64::MIN, i64::MAX)
1449            };
1450            let value = integer_from_value(
1451                value,
1452                bounds.0,
1453                bounds.1,
1454                "containers.Map: integer keys must be whole numbers",
1455                builtin,
1456            )?;
1457            Ok(NormalizedKey::Int(value))
1458        }
1459        KeyType::UInt32 | KeyType::UInt64 => {
1460            let limit = if key_type == KeyType::UInt32 {
1461                u32::MAX as u64
1462            } else {
1463                u64::MAX
1464            };
1465            let value = unsigned_from_value(
1466                value,
1467                limit,
1468                "containers.Map: unsigned keys must be non-negative integers",
1469                builtin,
1470            )?;
1471            Ok(NormalizedKey::UInt(value))
1472        }
1473        KeyType::Logical => {
1474            let flag = bool_from_value(
1475                value,
1476                "containers.Map: logical keys must be logical scalars",
1477                builtin,
1478            )?;
1479            Ok(NormalizedKey::Bool(flag))
1480        }
1481    }
1482}
1483
1484fn string_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<String> {
1485    match value {
1486        Value::String(s) => Ok(s.clone()),
1487        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
1488        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
1489        _ => Err(map_error(context, builtin)),
1490    }
1491}
1492
1493fn char_array_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<CharArray> {
1494    match value {
1495        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.clone()),
1496        Value::String(s) => {
1497            let chars: Vec<char> = s.chars().collect();
1498            CharArray::new(chars.clone(), 1, chars.len())
1499                .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
1500        }
1501        Value::StringArray(sa) if sa.data.len() == 1 => {
1502            let chars: Vec<char> = sa.data[0].chars().collect();
1503            CharArray::new(chars.clone(), 1, chars.len())
1504                .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
1505        }
1506        _ => Err(map_error(
1507            "containers.Map: keys must be character vectors",
1508            builtin,
1509        )),
1510    }
1511}
1512
1513fn char_array_value(text: &str, builtin: &'static str) -> BuiltinResult<Value> {
1514    let chars: Vec<char> = text.chars().collect();
1515    CharArray::new(chars.clone(), 1, chars.len())
1516        .map(Value::CharArray)
1517        .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
1518}
1519
1520fn normalize_numeric_value(value: Value, builtin: &'static str) -> BuiltinResult<Value> {
1521    match value {
1522        Value::Num(_) | Value::Tensor(_) => Ok(value),
1523        Value::Int(i) => Ok(Value::Num(i.to_f64())),
1524        Value::Bool(b) => Ok(Value::Num(if b { 1.0 } else { 0.0 })),
1525        Value::LogicalArray(arr) => {
1526            let data: Vec<f64> = arr
1527                .data
1528                .iter()
1529                .map(|&b| if b != 0 { 1.0 } else { 0.0 })
1530                .collect();
1531            let tensor = Tensor::new(data, arr.shape.clone())
1532                .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
1533            Ok(Value::Tensor(tensor))
1534        }
1535        Value::Cell(_)
1536        | Value::Struct(_)
1537        | Value::Object(_)
1538        | Value::HandleObject(_)
1539        | Value::Listener(_)
1540        | Value::String(_)
1541        | Value::StringArray(_)
1542        | Value::CharArray(_)
1543        | Value::Complex(_, _)
1544        | Value::ComplexTensor(_)
1545        | Value::FunctionHandle(_)
1546        | Value::ExternalFunctionHandle(_)
1547        | Value::MethodFunctionHandle(_)
1548        | Value::BoundFunctionHandle { .. }
1549        | Value::Closure(_)
1550        | Value::ClassRef(_)
1551        | Value::MException(_)
1552        | Value::GpuTensor(_)
1553        | Value::OutputList(_) => Err(map_error(
1554            "containers.Map: values must be numeric when ValueType is 'double' or 'single'",
1555            builtin,
1556        )),
1557    }
1558}
1559
1560fn normalize_logical_value(value: Value, builtin: &'static str) -> BuiltinResult<Value> {
1561    match value {
1562        Value::Bool(_) | Value::LogicalArray(_) => Ok(value),
1563        Value::Int(i) => Ok(Value::Bool(i.to_i64() != 0)),
1564        Value::Num(n) => Ok(Value::Bool(n != 0.0)),
1565        Value::Tensor(t) => {
1566            let flags: Vec<u8> = t
1567                .data
1568                .iter()
1569                .map(|&v| if v != 0.0 { 1 } else { 0 })
1570                .collect();
1571            let logical = LogicalArray::new(flags, t.shape.clone())
1572                .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
1573            Ok(Value::LogicalArray(logical))
1574        }
1575        Value::CharArray(_)
1576        | Value::String(_)
1577        | Value::StringArray(_)
1578        | Value::Struct(_)
1579        | Value::Cell(_)
1580        | Value::Object(_)
1581        | Value::HandleObject(_)
1582        | Value::Listener(_)
1583        | Value::Complex(_, _)
1584        | Value::ComplexTensor(_)
1585        | Value::FunctionHandle(_)
1586        | Value::ExternalFunctionHandle(_)
1587        | Value::MethodFunctionHandle(_)
1588        | Value::BoundFunctionHandle { .. }
1589        | Value::Closure(_)
1590        | Value::ClassRef(_)
1591        | Value::MException(_)
1592        | Value::GpuTensor(_)
1593        | Value::OutputList(_) => Err(map_error(
1594            "containers.Map: values must be logical when ValueType is 'logical'",
1595            builtin,
1596        )),
1597    }
1598}
1599
1600fn numeric_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<f64> {
1601    match value {
1602        Value::Num(n) => Ok(*n),
1603        Value::Int(i) => Ok(i.to_f64()),
1604        Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
1605        Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
1606        Value::LogicalArray(arr) if arr.data.len() == 1 => {
1607            Ok(if arr.data[0] != 0 { 1.0 } else { 0.0 })
1608        }
1609        _ => Err(map_error(context, builtin)),
1610    }
1611}
1612
1613fn integer_from_value(
1614    value: &Value,
1615    min: i64,
1616    max: i64,
1617    context: &str,
1618    builtin: &'static str,
1619) -> BuiltinResult<i64> {
1620    match value {
1621        Value::Int(i) => {
1622            let v = i.to_i64();
1623            if v < min || v > max {
1624                return Err(map_error(context, builtin));
1625            }
1626            Ok(v)
1627        }
1628        Value::Num(n) => {
1629            if !n.is_finite() {
1630                return Err(map_error(context, builtin));
1631            }
1632            if (*n < min as f64) || (*n > max as f64) {
1633                return Err(map_error(context, builtin));
1634            }
1635            if (n.round() - n).abs() > f64::EPSILON {
1636                return Err(map_error(context, builtin));
1637            }
1638            Ok(n.round() as i64)
1639        }
1640        Value::Bool(b) => {
1641            let v = if *b { 1 } else { 0 };
1642            if v < min || v > max {
1643                return Err(map_error(context, builtin));
1644            }
1645            Ok(v)
1646        }
1647        _ => Err(map_error(context, builtin)),
1648    }
1649}
1650
1651fn unsigned_from_value(
1652    value: &Value,
1653    max: u64,
1654    context: &str,
1655    builtin: &'static str,
1656) -> BuiltinResult<u64> {
1657    match value {
1658        Value::Int(i) => {
1659            let v = i.to_i64();
1660            if v < 0 || v as u64 > max {
1661                return Err(map_error(context, builtin));
1662            }
1663            Ok(v as u64)
1664        }
1665        Value::Num(n) => {
1666            if !n.is_finite() || *n < 0.0 || *n > max as f64 {
1667                return Err(map_error(context, builtin));
1668            }
1669            if (n.round() - n).abs() > f64::EPSILON {
1670                return Err(map_error(context, builtin));
1671            }
1672            Ok(n.round() as u64)
1673        }
1674        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
1675        _ => Err(map_error(context, builtin)),
1676    }
1677}
1678
1679fn bool_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<bool> {
1680    match value {
1681        Value::Bool(b) => Ok(*b),
1682        Value::LogicalArray(arr) if arr.data.len() == 1 => Ok(arr.data[0] != 0),
1683        Value::Int(i) => Ok(i.to_i64() != 0),
1684        Value::Num(n) => Ok(*n != 0.0),
1685        _ => Err(map_error(context, builtin)),
1686    }
1687}
1688
1689fn make_row_cell(values: Vec<Value>, builtin: &'static str) -> BuiltinResult<Value> {
1690    let cols = values.len();
1691    crate::make_cell_with_shape(values, vec![1, cols])
1692        .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
1693}
1694
1695fn extract_key_arguments(payload: &Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
1696    match payload {
1697        Value::Cell(cell) => {
1698            let mut out = Vec::with_capacity(cell.data.len());
1699            for ptr in &cell.data {
1700                out.push(unsafe { &*ptr.as_raw() }.clone());
1701            }
1702            Ok(out)
1703        }
1704        other => Err(map_error(
1705            format!("containers.Map: expected key arguments in a cell array, got {other:?}"),
1706            builtin,
1707        )),
1708    }
1709}
1710
1711async fn expand_assignment_values(
1712    value: Value,
1713    expected: usize,
1714    builtin: &'static str,
1715) -> BuiltinResult<Vec<Value>> {
1716    let host = gather_if_needed_async(&value)
1717        .await
1718        .map_err(|err| attach_builtin_context(err, builtin))?;
1719    let values = flatten_values(&host, builtin).await?;
1720    if expected == 1 {
1721        if values.is_empty() {
1722            return Err(map_error(
1723                "containers.Map: assignment requires a value",
1724                builtin,
1725            ));
1726        }
1727        Ok(vec![values.into_iter().next().unwrap()])
1728    } else {
1729        if values.len() != expected {
1730            return Err(map_error(
1731                format!(
1732                    "containers.Map: assignment with {} keys requires {} values (got {})",
1733                    expected,
1734                    expected,
1735                    values.len()
1736                ),
1737                builtin,
1738            ));
1739        }
1740        Ok(values)
1741    }
1742}
1743
1744struct KeyCollection {
1745    values: Vec<Value>,
1746    shape: Vec<usize>,
1747}
1748
1749async fn collect_key_spec(
1750    value: &Value,
1751    key_type: KeyType,
1752    builtin: &'static str,
1753) -> BuiltinResult<KeyCollection> {
1754    let host = gather_if_needed_async(value)
1755        .await
1756        .map_err(|err| attach_builtin_context(err, builtin))?;
1757    match &host {
1758        Value::Cell(cell) => {
1759            let mut values = Vec::with_capacity(cell.data.len());
1760            for ptr in &cell.data {
1761                values.push(
1762                    gather_if_needed_async(unsafe { &*ptr.as_raw() })
1763                        .await
1764                        .map_err(|err| attach_builtin_context(err, builtin))?,
1765                );
1766            }
1767            Ok(KeyCollection {
1768                values,
1769                shape: vec![cell.rows, cell.cols],
1770            })
1771        }
1772        Value::StringArray(sa) => Ok(KeyCollection {
1773            values: sa.data.iter().map(|s| Value::String(s.clone())).collect(),
1774            shape: vec![sa.rows(), sa.cols()],
1775        }),
1776        Value::CharArray(ca) => {
1777            let rows = if ca.rows == 0 { 0 } else { ca.rows };
1778            Ok(KeyCollection {
1779                values: char_array_rows(ca, builtin)?,
1780                shape: vec![rows, 1],
1781            })
1782        }
1783        Value::LogicalArray(arr) if key_type == KeyType::Logical => Ok(KeyCollection {
1784            values: arr.data.iter().map(|&b| Value::Bool(b != 0)).collect(),
1785            shape: arr.shape.clone(),
1786        }),
1787        Value::Tensor(t) if key_type != KeyType::Char && key_type != KeyType::String => {
1788            Ok(KeyCollection {
1789                values: t.data.iter().map(|&n| Value::Num(n)).collect(),
1790                shape: t.shape.clone(),
1791            })
1792        }
1793        _ => Ok(KeyCollection {
1794            values: vec![host.clone()],
1795            shape: vec![1, 1],
1796        }),
1797    }
1798}
1799
1800pub fn map_length(value: &Value) -> Option<usize> {
1801    if let Value::HandleObject(handle) = value {
1802        if runmat_builtins::is_handle_valid(handle) && handle.class_name == CLASS_NAME {
1803            if let Ok(id) = map_id(handle, BUILTIN_CONSTRUCTOR) {
1804                if let Ok(registry) = MAP_REGISTRY.read() {
1805                    return registry.get(&id).map(|store| store.len());
1806                }
1807            }
1808        }
1809    }
1810    None
1811}
1812
1813#[cfg(test)]
1814pub(crate) mod tests {
1815    use super::*;
1816    use crate::builtins::common::test_support;
1817    use futures::executor::block_on;
1818    use runmat_builtins::{ResolveContext, Type};
1819
1820    fn error_message(err: crate::RuntimeError) -> String {
1821        err.message.clone()
1822    }
1823
1824    fn containers_map_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
1825        block_on(super::containers_map_builtin(args))
1826    }
1827
1828    fn containers_map_keys(map: Value) -> BuiltinResult<Value> {
1829        block_on(super::containers_map_keys(map))
1830    }
1831
1832    fn containers_map_is_key(map: Value, key_spec: Value) -> BuiltinResult<Value> {
1833        block_on(super::containers_map_is_key(map, key_spec))
1834    }
1835
1836    fn containers_map_remove(map: Value, key_spec: Value) -> BuiltinResult<Value> {
1837        block_on(super::containers_map_remove(map, key_spec))
1838    }
1839
1840    fn containers_map_subsref(map: Value, kind: String, payload: Value) -> BuiltinResult<Value> {
1841        block_on(super::containers_map_subsref(map, kind, payload))
1842    }
1843
1844    fn containers_map_subsasgn(
1845        map: Value,
1846        kind: String,
1847        payload: Value,
1848        rhs: Value,
1849    ) -> BuiltinResult<Value> {
1850        block_on(super::containers_map_subsasgn(map, kind, payload, rhs))
1851    }
1852
1853    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1854    #[test]
1855    fn construct_empty_map_defaults() {
1856        let map = containers_map_builtin(Vec::new()).expect("map");
1857        let count = containers_map_subsref(
1858            map.clone(),
1859            ".".to_string(),
1860            Value::from("Count".to_string()),
1861        )
1862        .expect("Count");
1863        assert_eq!(count, Value::Num(0.0));
1864
1865        let key_type = containers_map_subsref(
1866            map.clone(),
1867            ".".to_string(),
1868            Value::from("KeyType".to_string()),
1869        )
1870        .expect("KeyType");
1871        assert_eq!(
1872            key_type,
1873            Value::CharArray(CharArray::new("char".chars().collect(), 1, 4).unwrap())
1874        );
1875
1876        let value_type = containers_map_subsref(
1877            map.clone(),
1878            ".".to_string(),
1879            Value::from("ValueType".to_string()),
1880        )
1881        .expect("ValueType");
1882        assert_eq!(
1883            value_type,
1884            Value::CharArray(CharArray::new("any".chars().collect(), 1, 3).unwrap())
1885        );
1886    }
1887
1888    #[test]
1889    fn map_type_resolvers_basics() {
1890        let ctx = ResolveContext::new(Vec::new());
1891        assert_eq!(map_handle_type(&[Type::Unknown], &ctx), Type::Unknown);
1892        assert_eq!(map_cell_type(&[], &ctx), Type::cell());
1893        assert_eq!(map_is_key_type(&[Type::String], &ctx), Type::logical());
1894        assert_eq!(map_unknown_type(&[], &ctx), Type::Unknown);
1895    }
1896
1897    #[test]
1898    fn containers_map_descriptor_includes_constructor_and_method_signatures() {
1899        let constructor_labels: Vec<&str> = CONTAINERS_MAP_DESCRIPTOR
1900            .signatures
1901            .iter()
1902            .map(|sig| sig.label)
1903            .collect();
1904        assert!(constructor_labels.contains(&"M = containers.Map()"));
1905        assert!(constructor_labels.contains(&"M = containers.Map(keys, values)"));
1906
1907        let method_labels: Vec<&str> = CONTAINERS_MAP_SUBSREF_DESCRIPTOR
1908            .signatures
1909            .iter()
1910            .map(|sig| sig.label)
1911            .collect();
1912        assert!(method_labels.contains(&"value = containers.Map.subsref(M, kind, payload)"));
1913    }
1914
1915    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1916    #[test]
1917    fn constructor_with_cells_lookup() {
1918        let keys = crate::make_cell(vec![Value::from("apple"), Value::from("pear")], 1, 2).unwrap();
1919        let values = crate::make_cell(vec![Value::Num(5.0), Value::Num(7.0)], 1, 2).unwrap();
1920        let map = containers_map_builtin(vec![keys, values]).expect("map");
1921        let apple = containers_map_subsref(
1922            map.clone(),
1923            "()".to_string(),
1924            crate::make_cell(vec![Value::from("apple")], 1, 1).unwrap(),
1925        )
1926        .expect("lookup");
1927        assert_eq!(apple, Value::Num(5.0));
1928    }
1929
1930    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1931    #[test]
1932    fn constructor_rejects_duplicate_keys() {
1933        let keys = crate::make_cell(vec![Value::from("dup"), Value::from("dup")], 1, 2).unwrap();
1934        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
1935        let err = containers_map_builtin(vec![keys, values]).expect_err("duplicate check");
1936        let message = error_message(err);
1937        assert!(message.contains("Duplicate key name"));
1938    }
1939
1940    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1941    #[test]
1942    fn constructor_errors_when_value_count_mismatch() {
1943        let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
1944        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1945        let err = containers_map_builtin(vec![keys, values]).expect_err("count mismatch");
1946        let message = error_message(err);
1947        assert!(message.contains("number of keys"));
1948    }
1949
1950    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1951    #[test]
1952    fn comparison_method_rejects_unknown_values() {
1953        let keys = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
1954        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1955        let err = containers_map_builtin(vec![
1956            keys,
1957            values,
1958            Value::from("ComparisonMethod"),
1959            Value::from("caseinsensitive"),
1960        ])
1961        .expect_err("comparison method");
1962        let message = error_message(err);
1963        assert!(message.contains("ComparisonMethod"));
1964    }
1965
1966    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1967    #[test]
1968    fn key_type_single_roundtrip() {
1969        let map = containers_map_builtin(vec![Value::from("KeyType"), Value::from("single")])
1970            .expect("map");
1971        let key_type = containers_map_subsref(map.clone(), ".".to_string(), Value::from("KeyType"))
1972            .expect("keytype");
1973        assert_eq!(
1974            key_type,
1975            Value::CharArray(CharArray::new("single".chars().collect(), 1, 6).unwrap())
1976        );
1977
1978        let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1979        let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(7.0))
1980            .expect("assign");
1981        let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1982        assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
1983    }
1984
1985    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1986    #[test]
1987    fn value_type_double_converts_integers() {
1988        let keys = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1989        let values = crate::make_cell(vec![Value::Int(IntValue::I32(7))], 1, 1).unwrap();
1990        let map = containers_map_builtin(vec![
1991            keys,
1992            values,
1993            Value::from("KeyType"),
1994            Value::from("double"),
1995            Value::from("ValueType"),
1996            Value::from("double"),
1997        ])
1998        .expect("map");
1999        let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
2000        let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
2001        assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
2002    }
2003
2004    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2005    #[test]
2006    fn value_type_logical_converts_numeric_arrays() {
2007        let keys = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
2008        let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
2009        let values = crate::make_cell(vec![Value::Tensor(tensor.clone())], 1, 1).unwrap();
2010        let map = containers_map_builtin(vec![
2011            keys,
2012            values,
2013            Value::from("ValueType"),
2014            Value::from("logical"),
2015        ])
2016        .expect("map");
2017        let payload = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
2018        let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
2019        match value {
2020            Value::LogicalArray(arr) => {
2021                assert_eq!(arr.shape, vec![3, 1]);
2022                assert_eq!(arr.data, vec![0, 1, 1]);
2023            }
2024            other => panic!("expected logical array, got {:?}", other),
2025        }
2026    }
2027
2028    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2029    #[test]
2030    fn uniform_values_enforced_on_assignment() {
2031        let map = containers_map_builtin(vec![Value::from("UniformValues"), Value::from(true)])
2032            .expect("map");
2033        let payload = crate::make_cell(vec![Value::from("x")], 1, 1).unwrap();
2034        let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(1.0))
2035            .expect("assign");
2036        let err = containers_map_subsasgn(map, "()".to_string(), payload, Value::from("text"))
2037            .expect_err("uniform enforcement");
2038        let message = error_message(err);
2039        assert!(message.contains("UniformValues"));
2040    }
2041
2042    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2043    #[test]
2044    fn assignment_updates_and_inserts() {
2045        let map = containers_map_builtin(Vec::new()).expect("map");
2046        let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
2047        let updated = containers_map_subsasgn(
2048            map.clone(),
2049            "()".to_string(),
2050            payload.clone(),
2051            Value::Num(1.0),
2052        )
2053        .expect("assign");
2054        let updated = containers_map_subsasgn(
2055            updated.clone(),
2056            "()".to_string(),
2057            payload.clone(),
2058            Value::Num(5.0),
2059        )
2060        .expect("update");
2061        let beta_payload = crate::make_cell(vec![Value::from("beta")], 1, 1).unwrap();
2062        let updated = containers_map_subsasgn(
2063            updated.clone(),
2064            "()".to_string(),
2065            beta_payload,
2066            Value::Num(9.0),
2067        )
2068        .expect("insert");
2069        let value = containers_map_subsref(updated, "()".to_string(), payload).expect("lookup");
2070        assert_eq!(value, Value::Num(5.0));
2071    }
2072
2073    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2074    #[test]
2075    fn subsref_multiple_keys_preserves_shape() {
2076        let keys = crate::make_cell(
2077            vec![Value::from("a"), Value::from("b"), Value::from("c")],
2078            1,
2079            3,
2080        )
2081        .unwrap();
2082        let values = crate::make_cell(
2083            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
2084            1,
2085            3,
2086        )
2087        .unwrap();
2088        let map = containers_map_builtin(vec![keys, values]).expect("map");
2089        let request = crate::make_cell(vec![Value::from("a"), Value::from("c")], 1, 2).unwrap();
2090        let payload = crate::make_cell(vec![request], 1, 1).unwrap();
2091        let result =
2092            containers_map_subsref(map.clone(), "()".to_string(), payload).expect("lookup");
2093        match result {
2094            Value::Cell(cell) => {
2095                assert_eq!(cell.rows, 1);
2096                assert_eq!(cell.cols, 2);
2097                assert_eq!(cell.get(0, 0).expect("cell 0,0"), Value::Num(1.0));
2098                assert_eq!(cell.get(0, 1).expect("cell 0,1"), Value::Num(3.0));
2099            }
2100            other => panic!("expected cell array, got {other:?}"),
2101        }
2102    }
2103
2104    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2105    #[test]
2106    fn subsref_empty_key_collection_returns_empty_cell() {
2107        let keys = crate::make_cell(vec![Value::from("z")], 1, 1).unwrap();
2108        let values = crate::make_cell(vec![Value::Num(42.0)], 1, 1).unwrap();
2109        let map = containers_map_builtin(vec![keys, values]).expect("map");
2110        let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
2111        let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
2112        let result = containers_map_subsref(map, "()".to_string(), payload).expect("lookup empty");
2113        match result {
2114            Value::Cell(cell) => {
2115                assert_eq!(cell.rows, 1);
2116                assert_eq!(cell.cols, 0);
2117                assert!(cell.data.is_empty());
2118            }
2119            other => panic!("expected empty cell, got {other:?}"),
2120        }
2121    }
2122
2123    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2124    #[test]
2125    fn subsasgn_with_cell_keys_updates_all_targets() {
2126        let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
2127        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
2128        let map = containers_map_builtin(vec![keys, values]).expect("map");
2129        let key_spec = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
2130        let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
2131        let new_values = crate::make_cell(vec![Value::Num(10.0), Value::Num(20.0)], 1, 2).unwrap();
2132        let updated = containers_map_subsasgn(map.clone(), "()".to_string(), payload, new_values)
2133            .expect("assign");
2134        let a_payload = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
2135        let b_payload = crate::make_cell(vec![Value::from("b")], 1, 1).unwrap();
2136        let a_value =
2137            containers_map_subsref(updated.clone(), "()".to_string(), a_payload).expect("a lookup");
2138        let b_value =
2139            containers_map_subsref(updated, "()".to_string(), b_payload).expect("b lookup");
2140        assert_eq!(a_value, Value::Num(10.0));
2141        assert_eq!(b_value, Value::Num(20.0));
2142    }
2143
2144    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2145    #[test]
2146    fn assignment_value_count_mismatch_errors() {
2147        let keys = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
2148        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
2149        let map = containers_map_builtin(vec![keys, values]).expect("map");
2150        let key_spec = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
2151        let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
2152        let rhs = crate::make_cell(vec![Value::Num(99.0)], 1, 1).unwrap();
2153        let err =
2154            containers_map_subsasgn(map, "()".to_string(), payload, rhs).expect_err("value count");
2155        let message = error_message(err);
2156        assert!(message.contains("requires 2 values"));
2157    }
2158
2159    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2160    #[test]
2161    fn subsasgn_empty_key_collection_is_noop() {
2162        let keys = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
2163        let values = crate::make_cell(vec![Value::Num(7.0)], 1, 1).unwrap();
2164        let map = containers_map_builtin(vec![keys, values]).expect("map");
2165        let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
2166        let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
2167        let rhs = crate::make_cell(Vec::new(), 1, 0).unwrap();
2168        let updated =
2169            containers_map_subsasgn(map.clone(), "()".to_string(), payload, rhs).expect("assign");
2170        let lookup_payload = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
2171        let value =
2172            containers_map_subsref(updated, "()".to_string(), lookup_payload).expect("lookup");
2173        assert_eq!(value, Value::Num(7.0));
2174    }
2175
2176    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2177    #[test]
2178    fn keys_values_iskey_remove() {
2179        let keys = crate::make_cell(
2180            vec![Value::from("a"), Value::from("b"), Value::from("c")],
2181            1,
2182            3,
2183        )
2184        .unwrap();
2185        let values = crate::make_cell(
2186            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
2187            1,
2188            3,
2189        )
2190        .unwrap();
2191        let map = containers_map_builtin(vec![keys, values]).expect("map");
2192        let key_list = containers_map_keys(map.clone()).expect("keys");
2193        match key_list {
2194            Value::Cell(cell) => assert_eq!(cell.data.len(), 3),
2195            other => panic!("expected cell array, got {other:?}"),
2196        }
2197        let mask = containers_map_is_key(
2198            map.clone(),
2199            crate::make_cell(vec![Value::from("a"), Value::from("z")], 1, 2).unwrap(),
2200        )
2201        .expect("mask");
2202        match mask {
2203            Value::LogicalArray(arr) => {
2204                assert_eq!(arr.data, vec![1, 0]);
2205            }
2206            other => panic!("expected logical array, got {:?}", other),
2207        }
2208        let removed = containers_map_remove(
2209            map.clone(),
2210            crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
2211        )
2212        .expect("remove");
2213        let mask = containers_map_is_key(
2214            removed,
2215            crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
2216        )
2217        .expect("mask");
2218        assert_eq!(mask, Value::Bool(false));
2219    }
2220
2221    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2222    #[test]
2223    fn remove_missing_key_returns_error() {
2224        let keys = crate::make_cell(vec![Value::from("key")], 1, 1).unwrap();
2225        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
2226        let map = containers_map_builtin(vec![keys, values]).expect("map");
2227        let err = containers_map_remove(
2228            map,
2229            crate::make_cell(vec![Value::from("missing")], 1, 1).unwrap(),
2230        )
2231        .expect_err("remove missing");
2232        assert_eq!(
2233            err.identifier(),
2234            CONTAINERS_MAP_ERROR_MISSING_KEY.identifier
2235        );
2236        let message = error_message(err);
2237        assert_eq!(message, CONTAINERS_MAP_ERROR_MISSING_KEY.message);
2238    }
2239
2240    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2241    #[test]
2242    fn length_delegates_to_map_count() {
2243        let keys = crate::make_cell(
2244            vec![Value::from("a"), Value::from("b"), Value::from("c")],
2245            1,
2246            3,
2247        )
2248        .unwrap();
2249        let values = crate::make_cell(
2250            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
2251            1,
2252            3,
2253        )
2254        .unwrap();
2255        let map = containers_map_builtin(vec![keys, values]).expect("map");
2256        assert_eq!(map_length(&map), Some(3));
2257    }
2258
2259    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2260    #[test]
2261    fn map_constructor_gathers_gpu_values() {
2262        test_support::with_test_provider(|provider| {
2263            let keys = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
2264            let data = vec![1.0, 2.0, 3.0];
2265            let shape = vec![3, 1];
2266            let view = runmat_accelerate_api::HostTensorView {
2267                data: &data,
2268                shape: &shape,
2269            };
2270            let handle = provider.upload(&view).expect("upload");
2271            let values = crate::make_cell(vec![Value::GpuTensor(handle)], 1, 1).unwrap();
2272            let map = containers_map_builtin(vec![keys, values]).expect("map");
2273            let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
2274            let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
2275            match value {
2276                Value::Tensor(t) => {
2277                    assert_eq!(t.shape, shape);
2278                    assert_eq!(t.data, data);
2279                }
2280                other => panic!("expected tensor, got {:?}", other),
2281            }
2282        });
2283    }
2284}