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