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