Skip to main content

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

1//! MATLAB-compatible `containers.Map` constructor and methods for RunMat.
2
3use runmat_types::MemberAccess;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{
8    atomic::{AtomicBool, Ordering as AtomicOrdering},
9    Arc,
10};
11
12use runmat_builtins::{
13    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
14    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
15    BuiltinIntegerClass, BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
16    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
17    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
18    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
19};
20use runmat_gc::{GcHandle, GcRoot, RootId, Trace, Tracer};
21use runmat_macros::runtime_builtin;
22use runmat_value::{
23    CharArray, HandleRef, IntValue, IntegerStorage, LogicalArray, NumericDType, ObjectInstance,
24    StructValue, Tensor, Value,
25};
26
27use crate::builtins::common::random_args::keyword_of;
28use crate::builtins::common::spec::{
29    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
30    ReductionNaN, ResidencyPolicy, ShapeRequirements,
31};
32use crate::builtins::common::tensor;
33use crate::builtins::containers::type_resolvers::{
34    map_cell_type, map_handle_type, map_is_key_type, map_unknown_type,
35};
36use crate::{
37    build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError, OBJECT_INDEX_BRACE,
38    OBJECT_INDEX_MEMBER, OBJECT_INDEX_PAREN, OBJECT_SUBSASGN_METHOD, OBJECT_SUBSREF_METHOD,
39};
40
41const CLASS_NAME: &str = "containers.Map";
42const BUILTIN_CONSTRUCTOR: &str = "containers.Map";
43const BUILTIN_KEYS: &str = "containers.Map.keys";
44const BUILTIN_VALUES: &str = "containers.Map.values";
45const BUILTIN_IS_KEY: &str = "containers.Map.isKey";
46const BUILTIN_REMOVE: &str = "containers.Map.remove";
47const BUILTIN_SUBSREF: &str = "containers.Map.subsref";
48const BUILTIN_SUBSASGN: &str = "containers.Map.subsasgn";
49
50fn contains_resident_value(value: &Value) -> bool {
51    match value {
52        Value::GpuTensor(_) => true,
53        Value::Cell(cell) => cell.data.iter().any(contains_resident_value),
54        Value::OutputList(values) => values.iter().any(contains_resident_value),
55        _ => false,
56    }
57}
58
59fn ensure_resident_extension(
60    value: &Value,
61    extension: &BuiltinExtensionDescriptor,
62    builtin: &'static str,
63) -> BuiltinResult<()> {
64    if contains_resident_value(value) {
65        crate::compatibility::ensure_builtin_extension_enabled(extension, builtin)?;
66    }
67    Ok(())
68}
69
70const CONTAINERS_MAP_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
71    name: "M",
72    ty: BuiltinParamType::Any,
73    arity: BuiltinParamArity::Required,
74    default: None,
75    description: "containers.Map handle object.",
76}];
77
78const CONTAINERS_MAP_INPUTS_KEYS_VALUES: [BuiltinParamDescriptor; 2] = [
79    BuiltinParamDescriptor {
80        name: "keys",
81        ty: BuiltinParamType::Any,
82        arity: BuiltinParamArity::Required,
83        default: None,
84        description: "Key container (cell, string/char, or numeric vector).",
85    },
86    BuiltinParamDescriptor {
87        name: "values",
88        ty: BuiltinParamType::Any,
89        arity: BuiltinParamArity::Required,
90        default: None,
91        description: "Value container aligned with keys.",
92    },
93];
94
95const CONTAINERS_MAP_INPUTS_KEYS_VALUES_OPTS: [BuiltinParamDescriptor; 4] = [
96    BuiltinParamDescriptor {
97        name: "keys",
98        ty: BuiltinParamType::Any,
99        arity: BuiltinParamArity::Required,
100        default: None,
101        description: "Key container (cell, string/char, or numeric vector).",
102    },
103    BuiltinParamDescriptor {
104        name: "values",
105        ty: BuiltinParamType::Any,
106        arity: BuiltinParamArity::Required,
107        default: None,
108        description: "Value container aligned with keys.",
109    },
110    BuiltinParamDescriptor {
111        name: "UniformValues",
112        ty: BuiltinParamType::StringScalar,
113        arity: BuiltinParamArity::Required,
114        default: None,
115        description: "Literal UniformValues option name.",
116    },
117    BuiltinParamDescriptor {
118        name: "isUniform",
119        ty: BuiltinParamType::LogicalArray,
120        arity: BuiltinParamArity::Required,
121        default: None,
122        description: "Logical scalar selecting uniform-value validation.",
123    },
124];
125
126const CONTAINERS_MAP_INPUTS_OPTIONS_ONLY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
127    name: "options",
128    ty: BuiltinParamType::Any,
129    arity: BuiltinParamArity::Variadic,
130    default: None,
131    description: "The required KeyType and ValueType name/value pairs, in either order.",
132}];
133
134const CONTAINERS_MAP_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
135    BuiltinSignatureDescriptor {
136        label: "M = containers.Map()",
137        inputs: &[],
138        outputs: &CONTAINERS_MAP_OUTPUT,
139    },
140    BuiltinSignatureDescriptor {
141        label: "M = containers.Map(keys, values)",
142        inputs: &CONTAINERS_MAP_INPUTS_KEYS_VALUES,
143        outputs: &CONTAINERS_MAP_OUTPUT,
144    },
145    BuiltinSignatureDescriptor {
146        label: "M = containers.Map(keys, values, 'UniformValues', isUniform)",
147        inputs: &CONTAINERS_MAP_INPUTS_KEYS_VALUES_OPTS,
148        outputs: &CONTAINERS_MAP_OUTPUT,
149    },
150    BuiltinSignatureDescriptor {
151        label: "M = containers.Map('KeyType', kType, 'ValueType', vType)",
152        inputs: &CONTAINERS_MAP_INPUTS_OPTIONS_ONLY,
153        outputs: &CONTAINERS_MAP_OUTPUT,
154    },
155];
156
157const CONTAINERS_MAP_METHOD_INPUT_MAP: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
158    name: "M",
159    ty: BuiltinParamType::Any,
160    arity: BuiltinParamArity::Required,
161    default: None,
162    description: "containers.Map handle object.",
163}];
164
165const CONTAINERS_MAP_KEYS_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
166    name: "K",
167    ty: BuiltinParamType::Any,
168    arity: BuiltinParamArity::Required,
169    default: None,
170    description: "Row cell array containing map keys.",
171}];
172
173const CONTAINERS_MAP_VALUES_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
174    name: "V",
175    ty: BuiltinParamType::Any,
176    arity: BuiltinParamArity::Required,
177    default: None,
178    description: "Row cell array containing map values.",
179}];
180const CONTAINERS_MAP_OUTPUTS_NONE: [BuiltinParamDescriptor; 0] = [];
181
182const CONTAINERS_MAP_INPUTS_KEY_SPEC: [BuiltinParamDescriptor; 2] = [
183    BuiltinParamDescriptor {
184        name: "M",
185        ty: BuiltinParamType::Any,
186        arity: BuiltinParamArity::Required,
187        default: None,
188        description: "containers.Map handle object.",
189    },
190    BuiltinParamDescriptor {
191        name: "keySet",
192        ty: BuiltinParamType::Any,
193        arity: BuiltinParamArity::Required,
194        default: None,
195        description: "Key scalar or key collection to query/mutate.",
196    },
197];
198
199const CONTAINERS_MAP_ISKEY_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
200    name: "tf",
201    ty: BuiltinParamType::LogicalArray,
202    arity: BuiltinParamArity::Required,
203    default: None,
204    description: "Logical membership result for each key.",
205}];
206
207const CONTAINERS_MAP_INPUTS_SUBSREF: [BuiltinParamDescriptor; 3] = [
208    BuiltinParamDescriptor {
209        name: "M",
210        ty: BuiltinParamType::Any,
211        arity: BuiltinParamArity::Required,
212        default: None,
213        description: "containers.Map handle object.",
214    },
215    BuiltinParamDescriptor {
216        name: "kind",
217        ty: BuiltinParamType::StringScalar,
218        arity: BuiltinParamArity::Required,
219        default: None,
220        description: "Indexing kind: (), ., or {}.",
221    },
222    BuiltinParamDescriptor {
223        name: "payload",
224        ty: BuiltinParamType::Any,
225        arity: BuiltinParamArity::Required,
226        default: None,
227        description: "Indexing payload cell/property argument.",
228    },
229];
230
231const CONTAINERS_MAP_INPUTS_SUBSASGN: [BuiltinParamDescriptor; 4] = [
232    BuiltinParamDescriptor {
233        name: "M",
234        ty: BuiltinParamType::Any,
235        arity: BuiltinParamArity::Required,
236        default: None,
237        description: "containers.Map handle object.",
238    },
239    BuiltinParamDescriptor {
240        name: "kind",
241        ty: BuiltinParamType::StringScalar,
242        arity: BuiltinParamArity::Required,
243        default: None,
244        description: "Assignment kind: (), ., or {}.",
245    },
246    BuiltinParamDescriptor {
247        name: "payload",
248        ty: BuiltinParamType::Any,
249        arity: BuiltinParamArity::Required,
250        default: None,
251        description: "Assignment payload cell/property argument.",
252    },
253    BuiltinParamDescriptor {
254        name: "rhs",
255        ty: BuiltinParamType::Any,
256        arity: BuiltinParamArity::Required,
257        default: None,
258        description: "Assigned value (scalar or collection).",
259    },
260];
261
262const CONTAINERS_MAP_SUBSREF_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
263    name: "value",
264    ty: BuiltinParamType::Any,
265    arity: BuiltinParamArity::Required,
266    default: None,
267    description: "Lookup/property value result.",
268}];
269
270const CONTAINERS_MAP_KEYS_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
271    [BuiltinSignatureDescriptor {
272        label: "K = containers.Map.keys(M)",
273        inputs: &CONTAINERS_MAP_METHOD_INPUT_MAP,
274        outputs: &CONTAINERS_MAP_KEYS_OUTPUT,
275    }];
276
277const CONTAINERS_MAP_VALUES_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
278    BuiltinSignatureDescriptor {
279        label: "V = containers.Map.values(M)",
280        inputs: &CONTAINERS_MAP_METHOD_INPUT_MAP,
281        outputs: &CONTAINERS_MAP_VALUES_OUTPUT,
282    },
283    BuiltinSignatureDescriptor {
284        label: "V = containers.Map.values(M, keySet)",
285        inputs: &CONTAINERS_MAP_INPUTS_KEY_SPEC,
286        outputs: &CONTAINERS_MAP_VALUES_OUTPUT,
287    },
288];
289
290const CONTAINERS_MAP_ISKEY_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
291    [BuiltinSignatureDescriptor {
292        label: "tf = containers.Map.isKey(M, keySet)",
293        inputs: &CONTAINERS_MAP_INPUTS_KEY_SPEC,
294        outputs: &CONTAINERS_MAP_ISKEY_OUTPUT,
295    }];
296
297const CONTAINERS_MAP_REMOVE_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
298    [BuiltinSignatureDescriptor {
299        label: "containers.Map.remove(M, keySet)",
300        inputs: &CONTAINERS_MAP_INPUTS_KEY_SPEC,
301        outputs: &CONTAINERS_MAP_OUTPUTS_NONE,
302    }];
303
304const CONTAINERS_MAP_SUBSREF_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
305    [BuiltinSignatureDescriptor {
306        label: "value = containers.Map.subsref(M, kind, payload)",
307        inputs: &CONTAINERS_MAP_INPUTS_SUBSREF,
308        outputs: &CONTAINERS_MAP_SUBSREF_OUTPUT,
309    }];
310
311const CONTAINERS_MAP_SUBSASGN_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
312    [BuiltinSignatureDescriptor {
313        label: "M = containers.Map.subsasgn(M, kind, payload, rhs)",
314        inputs: &CONTAINERS_MAP_INPUTS_SUBSASGN,
315        outputs: &CONTAINERS_MAP_OUTPUT,
316    }];
317
318const CONTAINERS_MAP_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
319    code: "RM.CONTAINERS_MAP.INVALID_ARGUMENT",
320    identifier: Some("RunMat:containers.Map:InvalidArgument"),
321    when: "Map constructor/method inputs, option grammar, or key/value payloads are invalid.",
322    message: "containers.Map: invalid argument",
323};
324
325const CONTAINERS_MAP_ERROR_MISSING_KEY: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
326    code: "RM.CONTAINERS_MAP.MISSING_KEY",
327    identifier: Some("RunMat:containers.Map:MissingKey"),
328    when: "Lookup/removal targets a key that is not present in the map.",
329    message: "containers.Map: The specified key is not present in this container.",
330};
331
332const CONTAINERS_MAP_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
333    code: "RM.CONTAINERS_MAP.INTERNAL",
334    identifier: Some("RunMat:containers.Map:Internal"),
335    when: "Map registry/storage operations fail unexpectedly.",
336    message: "containers.Map: internal operation failed",
337};
338
339const CONTAINERS_MAP_ERRORS: [BuiltinErrorDescriptor; 3] = [
340    CONTAINERS_MAP_ERROR_INVALID_ARGUMENT,
341    CONTAINERS_MAP_ERROR_MISSING_KEY,
342    CONTAINERS_MAP_ERROR_INTERNAL,
343];
344
345pub const CONTAINERS_MAP_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
346    signatures: &CONTAINERS_MAP_SIGNATURES,
347    output_mode: BuiltinOutputMode::Fixed,
348    completion_policy: BuiltinCompletionPolicy::Public,
349    errors: &CONTAINERS_MAP_ERRORS,
350};
351
352pub const CONTAINERS_MAP_KEYS_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
353    signatures: &CONTAINERS_MAP_KEYS_SIGNATURES,
354    output_mode: BuiltinOutputMode::Fixed,
355    completion_policy: BuiltinCompletionPolicy::Public,
356    errors: &CONTAINERS_MAP_ERRORS,
357};
358
359pub const CONTAINERS_MAP_VALUES_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
360    signatures: &CONTAINERS_MAP_VALUES_SIGNATURES,
361    output_mode: BuiltinOutputMode::Fixed,
362    completion_policy: BuiltinCompletionPolicy::Public,
363    errors: &CONTAINERS_MAP_ERRORS,
364};
365
366pub const CONTAINERS_MAP_ISKEY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
367    signatures: &CONTAINERS_MAP_ISKEY_SIGNATURES,
368    output_mode: BuiltinOutputMode::Fixed,
369    completion_policy: BuiltinCompletionPolicy::Public,
370    errors: &CONTAINERS_MAP_ERRORS,
371};
372
373pub const CONTAINERS_MAP_REMOVE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
374    signatures: &CONTAINERS_MAP_REMOVE_SIGNATURES,
375    output_mode: BuiltinOutputMode::Fixed,
376    completion_policy: BuiltinCompletionPolicy::Public,
377    errors: &CONTAINERS_MAP_ERRORS,
378};
379
380pub const CONTAINERS_MAP_SUBSREF_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
381    signatures: &CONTAINERS_MAP_SUBSREF_SIGNATURES,
382    output_mode: BuiltinOutputMode::Fixed,
383    completion_policy: BuiltinCompletionPolicy::Public,
384    errors: &CONTAINERS_MAP_ERRORS,
385};
386
387pub const CONTAINERS_MAP_SUBSASGN_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
388    signatures: &CONTAINERS_MAP_SUBSASGN_SIGNATURES,
389    output_mode: BuiltinOutputMode::Fixed,
390    completion_policy: BuiltinCompletionPolicy::Public,
391    errors: &CONTAINERS_MAP_ERRORS,
392};
393
394const MAP_KEY_INTEGER_CLASSES: [BuiltinIntegerClass; 4] = [
395    BuiltinIntegerClass::Int32,
396    BuiltinIntegerClass::Uint32,
397    BuiltinIntegerClass::Int64,
398    BuiltinIntegerClass::Uint64,
399];
400const MAP_KEY_INPUT: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
401    name: "keySet",
402    classes: &MAP_KEY_INTEGER_CLASSES,
403    availability: BuiltinIntegerInputAvailability::Documented,
404    scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
405    notes: "Public numeric Map keys are scalar single/double/int32/uint32/int64/uint64; these four integer key classes retain exact native identity.",
406}];
407const MAP_VALUE_INPUT: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
408    name: "valueSet",
409    classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
410    availability: BuiltinIntegerInputAvailability::Documented,
411    scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
412    notes: "Every integer class is a documented ValueType and retains its exact class and payload.",
413}];
414const MAP_STORED_KEY_INPUT: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
415    name: "stored keys",
416    classes: &MAP_KEY_INTEGER_CLASSES,
417    availability: BuiltinIntegerInputAvailability::Documented,
418    scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
419    notes: "Integer keys are returned in their declared key class inside the output cell array.",
420}];
421const MAP_STORED_VALUE_INPUT: [BuiltinIntegerInputCapability; 1] =
422    [BuiltinIntegerInputCapability {
423        name: "stored values",
424        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
425        availability: BuiltinIntegerInputAvailability::Documented,
426        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
427        notes: "Integer values pass through the host Map store and output cells with exact class and value.",
428    }];
429const MAP_ASSIGN_INPUTS: [BuiltinIntegerInputCapability; 2] = [
430    BuiltinIntegerInputCapability {
431        name: "keySet",
432        classes: &MAP_KEY_INTEGER_CLASSES,
433        availability: BuiltinIntegerInputAvailability::Documented,
434        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
435        notes: "Public integer Map keys are int32/uint32/int64/uint64 and retain exact native identity.",
436    },
437    BuiltinIntegerInputCapability {
438        name: "rhs",
439        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
440        availability: BuiltinIntegerInputAvailability::Documented,
441        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
442        notes: "All integer rhs classes are exact for ValueType='any' and their matching declared integer ValueType.",
443    },
444];
445const MAP_RESIDENT_KEY_INPUT: [BuiltinIntegerInputCapability; 1] =
446    [BuiltinIntegerInputCapability {
447        name: "resident keySet",
448        classes: &MAP_KEY_INTEGER_CLASSES,
449        availability: BuiltinIntegerInputAvailability::RunMatOnly,
450        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
451        notes: "Resident int32/uint32/int64/uint64 keys are a gated RunMat extension and gather before host Map lookup.",
452    }];
453const MAP_RESIDENT_VALUE_INPUT: [BuiltinIntegerInputCapability; 1] =
454    [BuiltinIntegerInputCapability {
455        name: "resident valueSet or rhs",
456        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
457        availability: BuiltinIntegerInputAvailability::RunMatOnly,
458        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
459        notes: "Resident integer values are a gated RunMat extension and gather before exact host storage or declared ValueType conversion.",
460    }];
461const MAP_RESIDENT_ASSIGN_INPUTS: [BuiltinIntegerInputCapability; 2] = [
462    BuiltinIntegerInputCapability {
463        name: "resident keySet",
464        classes: &MAP_KEY_INTEGER_CLASSES,
465        availability: BuiltinIntegerInputAvailability::RunMatOnly,
466        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
467        notes: "Resident integer keys are gated and gathered before host key normalization.",
468    },
469    BuiltinIntegerInputCapability {
470        name: "resident rhs",
471        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
472        availability: BuiltinIntegerInputAvailability::RunMatOnly,
473        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
474        notes:
475            "Resident integer rhs values are gated and gathered before host ValueType conversion.",
476    },
477];
478
479pub const CONTAINERS_MAP_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 4] = [
480    BuiltinIntegerCapabilityDescriptor {
481        form: "M = containers.Map(integer_keySet, valueSet)",
482        inputs: &MAP_KEY_INPUT,
483        computation_domain: BuiltinIntegerComputationDomain::Structural,
484        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
485        overflow: BuiltinIntegerOverflowRule::Error,
486        backend: BuiltinIntegerBackendRule::HostOnly,
487        overload: BuiltinIntegerOverloadKind::StructuralParameter,
488        notes: "KeyType is inferred exactly for int32/uint32/int64/uint64; unsupported int8/uint8/int16/uint16 key arrays reject rather than alias through double.",
489    },
490    BuiltinIntegerCapabilityDescriptor {
491        form: "M = containers.Map(keySet, integer_valueSet)",
492        inputs: &MAP_VALUE_INPUT,
493        computation_domain: BuiltinIntegerComputationDomain::Structural,
494        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
495        overflow: BuiltinIntegerOverflowRule::Saturate,
496        backend: BuiltinIntegerBackendRule::HostOnly,
497        overload: BuiltinIntegerOverloadKind::StructuralParameter,
498        notes: "Uniform constructor values infer their exact integer ValueType; ValueType='any' and explicit integer ValueType preserve or deliberately cast native storage.",
499    },
500    BuiltinIntegerCapabilityDescriptor {
501        form: "M = containers.Map(resident_integer_keySet, valueSet)",
502        inputs: &MAP_RESIDENT_KEY_INPUT,
503        computation_domain: BuiltinIntegerComputationDomain::Structural,
504        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
505        overflow: BuiltinIntegerOverflowRule::Error,
506        backend: BuiltinIntegerBackendRule::GatherFallback,
507        overload: BuiltinIntegerOverloadKind::StructuralParameter,
508        notes: "RunMat-only resident keys gather after compatibility admission and before host key-type inference and exact identity storage.",
509    },
510    BuiltinIntegerCapabilityDescriptor {
511        form: "M = containers.Map(keySet, resident_integer_valueSet)",
512        inputs: &MAP_RESIDENT_VALUE_INPUT,
513        computation_domain: BuiltinIntegerComputationDomain::Structural,
514        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
515        overflow: BuiltinIntegerOverflowRule::Saturate,
516        backend: BuiltinIntegerBackendRule::GatherFallback,
517        overload: BuiltinIntegerOverloadKind::StructuralParameter,
518        notes: "RunMat-only resident values gather after compatibility admission; the resulting Map and all outputs remain host-resident.",
519    },
520];
521pub const CONTAINERS_MAP_KEYS_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
522    [BuiltinIntegerCapabilityDescriptor {
523        form: "K = containers.Map.keys(M_with_integer_keys)",
524        inputs: &MAP_STORED_KEY_INPUT,
525        computation_domain: BuiltinIntegerComputationDomain::Structural,
526        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
527        overflow: BuiltinIntegerOverflowRule::NotApplicable,
528        backend: BuiltinIntegerBackendRule::HostOnly,
529        overload: BuiltinIntegerOverloadKind::StructuralParameter,
530        notes: "The row cell contains exact typed scalar keys; the Map itself is host-resident.",
531    }];
532pub const CONTAINERS_MAP_VALUES_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
533    BuiltinIntegerCapabilityDescriptor {
534        form: "V = containers.Map.values(M_with_integer_values, keySet?)",
535        inputs: &MAP_STORED_VALUE_INPUT,
536        computation_domain: BuiltinIntegerComputationDomain::Structural,
537        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
538        overflow: BuiltinIntegerOverflowRule::NotApplicable,
539        backend: BuiltinIntegerBackendRule::HostOnly,
540        overload: BuiltinIntegerOverloadKind::StructuralParameter,
541        notes: "All-values output is a row cell; selected-values output matches keySet cell shape and preserves each stored integer class.",
542    },
543    BuiltinIntegerCapabilityDescriptor {
544        form: "V = containers.Map.values(M, resident_integer_keySet)",
545        inputs: &MAP_RESIDENT_KEY_INPUT,
546        computation_domain: BuiltinIntegerComputationDomain::Structural,
547        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
548        overflow: BuiltinIntegerOverflowRule::Error,
549        backend: BuiltinIntegerBackendRule::GatherFallback,
550        overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
551        notes: "RunMat-only resident selected keys gather after compatibility admission; the host cell output preserves keySet shape.",
552    },
553];
554pub const CONTAINERS_MAP_ISKEY_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
555    BuiltinIntegerCapabilityDescriptor {
556        form: "tf = containers.Map.isKey(M, integer_keySet)",
557        inputs: &MAP_KEY_INPUT,
558        computation_domain: BuiltinIntegerComputationDomain::Structural,
559        output_class: BuiltinIntegerOutputClassRule::Logical,
560        overflow: BuiltinIntegerOverflowRule::Error,
561        backend: BuiltinIntegerBackendRule::HostOnly,
562        overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
563        notes: "Scalar input returns scalar logical; a cell keySet returns a same-shape logical array with exact wide-key identity.",
564    },
565    BuiltinIntegerCapabilityDescriptor {
566        form: "tf = containers.Map.isKey(M, resident_integer_keySet)",
567        inputs: &MAP_RESIDENT_KEY_INPUT,
568        computation_domain: BuiltinIntegerComputationDomain::Structural,
569        output_class: BuiltinIntegerOutputClassRule::Logical,
570        overflow: BuiltinIntegerOverflowRule::Error,
571        backend: BuiltinIntegerBackendRule::GatherFallback,
572        overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
573        notes: "RunMat-only resident keys gather after compatibility admission; the logical result is host-resident and shape preserving.",
574    },
575];
576pub const CONTAINERS_MAP_REMOVE_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
577    BuiltinIntegerCapabilityDescriptor {
578        form: "containers.Map.remove(M, integer_keySet)",
579        inputs: &MAP_KEY_INPUT,
580        computation_domain: BuiltinIntegerComputationDomain::Structural,
581        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
582        overflow: BuiltinIntegerOverflowRule::Error,
583        backend: BuiltinIntegerBackendRule::HostOnly,
584        overload: BuiltinIntegerOverloadKind::StructuralParameter,
585        notes: "Removal mutates the host handle object and performs exact native key lookup.",
586    },
587    BuiltinIntegerCapabilityDescriptor {
588        form: "containers.Map.remove(M, resident_integer_keySet)",
589        inputs: &MAP_RESIDENT_KEY_INPUT,
590        computation_domain: BuiltinIntegerComputationDomain::Structural,
591        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
592        overflow: BuiltinIntegerOverflowRule::Error,
593        backend: BuiltinIntegerBackendRule::GatherFallback,
594        overload: BuiltinIntegerOverloadKind::StructuralParameter,
595        notes: "RunMat-only resident keys gather after compatibility admission before the host handle is mutated.",
596    },
597];
598pub const CONTAINERS_MAP_SUBSREF_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 4] = [
599    BuiltinIntegerCapabilityDescriptor {
600        form: "count = containers.Map.subsref(M, '.', 'Count')",
601        inputs: &[],
602        computation_domain: BuiltinIntegerComputationDomain::Structural,
603        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
604        overflow: BuiltinIntegerOverflowRule::NotApplicable,
605        backend: BuiltinIntegerBackendRule::HostOnly,
606        overload: BuiltinIntegerOverloadKind::ScalarOnly,
607        notes: "The read-only Count property is a scalar uint64 and is derived from the host Map entry count without floating conversion.",
608    },
609    BuiltinIntegerCapabilityDescriptor {
610        form: "value = containers.Map.subsref(M, '()', integer_key)",
611        inputs: &MAP_KEY_INPUT,
612        computation_domain: BuiltinIntegerComputationDomain::Structural,
613        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
614        overflow: BuiltinIntegerOverflowRule::Error,
615        backend: BuiltinIntegerBackendRule::HostOnly,
616        overload: BuiltinIntegerOverloadKind::StructuralParameter,
617        notes: "The integer controls lookup only; output class is the stored value class.",
618    },
619    BuiltinIntegerCapabilityDescriptor {
620        form: "integer_value = containers.Map.subsref(M_with_integer_value, '()', key)",
621        inputs: &MAP_STORED_VALUE_INPUT,
622        computation_domain: BuiltinIntegerComputationDomain::Structural,
623        output_class: BuiltinIntegerOutputClassRule::PreserveInput,
624        overflow: BuiltinIntegerOverflowRule::NotApplicable,
625        backend: BuiltinIntegerBackendRule::HostOnly,
626        overload: BuiltinIntegerOverloadKind::StructuralParameter,
627        notes:
628            "Lookup returns the exact stored integer scalar or array without floating conversion.",
629    },
630    BuiltinIntegerCapabilityDescriptor {
631        form: "value = containers.Map.subsref(M, '()', resident_integer_key)",
632        inputs: &MAP_RESIDENT_KEY_INPUT,
633        computation_domain: BuiltinIntegerComputationDomain::Structural,
634        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
635        overflow: BuiltinIntegerOverflowRule::Error,
636        backend: BuiltinIntegerBackendRule::GatherFallback,
637        overload: BuiltinIntegerOverloadKind::StructuralParameter,
638        notes: "RunMat-only resident keys gather after compatibility admission; lookup returns the host-stored value.",
639    },
640];
641pub const CONTAINERS_MAP_SUBSASGN_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
642    BuiltinIntegerCapabilityDescriptor {
643        form: "containers.Map.subsasgn(M, '()', integer_key, integer_rhs)",
644        inputs: &MAP_ASSIGN_INPUTS,
645        computation_domain: BuiltinIntegerComputationDomain::Structural,
646        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
647        overflow: BuiltinIntegerOverflowRule::Saturate,
648        backend: BuiltinIntegerBackendRule::HostOnly,
649        overload: BuiltinIntegerOverloadKind::StructuralParameter,
650        notes: "Exact key identity is independent of exact rhs storage; a declared integer ValueType applies MATLAB integer conversion before storage.",
651    },
652    BuiltinIntegerCapabilityDescriptor {
653        form: "containers.Map.subsasgn(M, '()', resident_integer_key, resident_integer_rhs)",
654        inputs: &MAP_RESIDENT_ASSIGN_INPUTS,
655        computation_domain: BuiltinIntegerComputationDomain::Structural,
656        output_class: BuiltinIntegerOutputClassRule::NotApplicable,
657        overflow: BuiltinIntegerOverflowRule::Saturate,
658        backend: BuiltinIntegerBackendRule::GatherFallback,
659        overload: BuiltinIntegerOverloadKind::StructuralParameter,
660        notes: "RunMat-only resident assignment data gathers after compatibility admission and before host key normalization and rhs conversion.",
661    },
662];
663
664const MAP_RESIDENT_CONSTRUCTOR_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
665    id: "containers-map-resident-constructor-input",
666    mode: BuiltinExtensionMode::RunMatOnly,
667    description:
668        "containers.Map gathering resident constructor keys or values is a RunMat extension",
669    error_identifier: Some("RunMat:compatibility:ContainersMapResidentConstructorExtension"),
670};
671const MAP_RESIDENT_ISKEY_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
672    id: "containers-map-resident-iskey-input",
673    mode: BuiltinExtensionMode::RunMatOnly,
674    description: "containers.Map.isKey gathering resident keys is a RunMat extension",
675    error_identifier: Some("RunMat:compatibility:ContainersMapResidentIsKeyExtension"),
676};
677const MAP_RESIDENT_VALUES_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
678    id: "containers-map-resident-values-input",
679    mode: BuiltinExtensionMode::RunMatOnly,
680    description: "containers.Map.values gathering resident selected keys is a RunMat extension",
681    error_identifier: Some("RunMat:compatibility:ContainersMapResidentValuesExtension"),
682};
683const MAP_RESIDENT_REMOVE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
684    id: "containers-map-resident-remove-input",
685    mode: BuiltinExtensionMode::RunMatOnly,
686    description: "containers.Map.remove gathering resident keys is a RunMat extension",
687    error_identifier: Some("RunMat:compatibility:ContainersMapResidentRemoveExtension"),
688};
689const MAP_RESIDENT_SUBSREF_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
690    id: "containers-map-resident-subsref-input",
691    mode: BuiltinExtensionMode::RunMatOnly,
692    description: "containers.Map.subsref gathering resident keys is a RunMat extension",
693    error_identifier: Some("RunMat:compatibility:ContainersMapResidentSubsrefExtension"),
694};
695const MAP_RESIDENT_SUBSASGN_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
696    id: "containers-map-resident-subsasgn-input",
697    mode: BuiltinExtensionMode::RunMatOnly,
698    description:
699        "containers.Map.subsasgn gathering resident keys or rhs values is a RunMat extension",
700    error_identifier: Some("RunMat:compatibility:ContainersMapResidentSubsasgnExtension"),
701};
702pub const CONTAINERS_MAP_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
703    [MAP_RESIDENT_CONSTRUCTOR_EXTENSION];
704pub const CONTAINERS_MAP_ISKEY_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
705    [MAP_RESIDENT_ISKEY_EXTENSION];
706pub const CONTAINERS_MAP_VALUES_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
707    [MAP_RESIDENT_VALUES_EXTENSION];
708pub const CONTAINERS_MAP_REMOVE_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
709    [MAP_RESIDENT_REMOVE_EXTENSION];
710pub const CONTAINERS_MAP_SUBSREF_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
711    [MAP_RESIDENT_SUBSREF_EXTENSION];
712pub const CONTAINERS_MAP_SUBSASGN_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
713    [MAP_RESIDENT_SUBSASGN_EXTENSION];
714
715#[runmat_macros::register_gpu_spec(
716    builtin_path = "crate::builtins::containers::map::containers_map"
717)]
718pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
719    name: "containers.Map",
720    op_kind: GpuOpKind::Custom("map"),
721    supported_precisions: &[],
722    broadcast: BroadcastSemantics::None,
723    provider_hooks: &[],
724    constant_strategy: ConstantStrategy::InlineLiteral,
725    residency: ResidencyPolicy::GatherImmediately,
726    nan_mode: ReductionNaN::Include,
727    two_pass_threshold: None,
728    workgroup_size: None,
729    accepts_nan_mode: false,
730    notes: "Map storage and outputs are host-resident; resident inputs gather only through explicitly gated RunMat extensions.",
731};
732
733fn map_error_with_detail(
734    error: &'static BuiltinErrorDescriptor,
735    detail: impl AsRef<str>,
736    builtin: &'static str,
737) -> RuntimeError {
738    let raw = detail.as_ref().trim();
739    let normalized = raw
740        .strip_prefix("containers.Map:")
741        .map(str::trim)
742        .unwrap_or(raw);
743    let message = if normalized.is_empty() {
744        error.message.to_string()
745    } else {
746        format!("{}: {}", error.message, normalized)
747    };
748    let mut builder = build_runtime_error(message).with_builtin(builtin);
749    if let Some(identifier) = error.identifier {
750        builder = builder.with_identifier(identifier);
751    }
752    builder.build()
753}
754
755fn map_descriptor_error(
756    error: &'static BuiltinErrorDescriptor,
757    builtin: &'static str,
758) -> RuntimeError {
759    map_error_with_detail(error, "", builtin)
760}
761
762fn map_invalid(detail: impl AsRef<str>, builtin: &'static str) -> RuntimeError {
763    map_error_with_detail(&CONTAINERS_MAP_ERROR_INVALID_ARGUMENT, detail, builtin)
764}
765
766fn map_internal(detail: impl AsRef<str>, builtin: &'static str) -> RuntimeError {
767    map_error_with_detail(&CONTAINERS_MAP_ERROR_INTERNAL, detail, builtin)
768}
769
770fn map_error(message: impl Into<String>, builtin: &'static str) -> RuntimeError {
771    map_invalid(message.into(), builtin)
772}
773
774fn attach_builtin_context(mut error: RuntimeError, builtin: &'static str) -> RuntimeError {
775    if error.context.builtin.is_none() {
776        error.context = error.context.with_builtin(builtin);
777    }
778    error
779}
780
781#[runmat_macros::register_fusion_spec(
782    builtin_path = "crate::builtins::containers::map::containers_map"
783)]
784pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
785    name: "containers.Map",
786    shape: ShapeRequirements::Any,
787    constant_strategy: ConstantStrategy::InlineLiteral,
788    elementwise: None,
789    reduction: None,
790    emits_nan: false,
791    notes: "Handles act as fusion sinks; map construction terminates GPU fusion plans.",
792};
793
794static NEXT_ID: AtomicU64 = AtomicU64::new(1);
795
796thread_local! {
797    static MAP_REGISTRY: RefCell<HashMap<u64, MapStore>> = RefCell::new(HashMap::new());
798    static MAP_ROOT_STATE: RefCell<Option<MapRootState>> = const { RefCell::new(None) };
799}
800
801static CONTAINERS_MAP_CLASS_REGISTERED: crate::class_registry::ClassRegistration =
802    crate::class_registry::ClassRegistration::new(CLASS_NAME);
803
804struct MapRootState {
805    root_id: RootId,
806    active: Arc<AtomicBool>,
807}
808
809struct MapRegistryRoot {
810    active: Arc<AtomicBool>,
811}
812
813impl GcRoot for MapRegistryRoot {
814    fn scan(&self) -> Vec<GcHandle> {
815        struct RootCollector {
816            roots: Vec<GcHandle>,
817        }
818
819        impl Tracer for RootCollector {
820            fn mark(&mut self, handle: GcHandle) {
821                self.roots.push(handle);
822            }
823        }
824
825        MAP_REGISTRY.with(|registry| {
826            let registry = registry.borrow();
827            let mut collector = RootCollector { roots: Vec::new() };
828            for store in registry.values() {
829                if let Some(storage) = store.storage {
830                    collector.mark(storage);
831                }
832                for entry in &store.entries {
833                    entry.key_value.trace(&mut collector);
834                    entry.value.trace(&mut collector);
835                }
836            }
837            collector.roots
838        })
839    }
840
841    fn description(&self) -> String {
842        "containers.Map registry values".to_string()
843    }
844
845    fn is_active(&self) -> bool {
846        self.active.load(AtomicOrdering::Acquire)
847            && MAP_REGISTRY.with(|registry| !registry.borrow().is_empty())
848    }
849}
850
851fn ensure_map_registry_root_registered(builtin: &'static str) -> BuiltinResult<()> {
852    MAP_ROOT_STATE.with(|state| {
853        if state
854            .borrow()
855            .as_ref()
856            .is_some_and(|state| state.active.load(AtomicOrdering::Acquire))
857        {
858            return Ok(());
859        }
860
861        let active = Arc::new(AtomicBool::new(true));
862        let root_id = runmat_gc::gc_register_root(Box::new(MapRegistryRoot {
863            active: Arc::clone(&active),
864        }))
865        .map_err(|e| {
866            map_internal(
867                format!("containers.Map: failed to register GC root: {e}"),
868                builtin,
869            )
870        })?;
871        *state.borrow_mut() = Some(MapRootState { root_id, active });
872        Ok(())
873    })
874}
875
876fn deactivate_map_registry_root_if_empty() {
877    let empty = MAP_REGISTRY.with(|registry| {
878        registry
879            .try_borrow()
880            .map(|registry| registry.is_empty())
881            .unwrap_or(false)
882    });
883    if empty {
884        MAP_ROOT_STATE.with(|state| {
885            if let Some(state) = state.borrow_mut().take() {
886                state.active.store(false, AtomicOrdering::Release);
887                if let Err(err) = runmat_gc::gc_unregister_root(state.root_id) {
888                    log::warn!("containers.Map: failed to unregister empty registry root: {err}");
889                }
890            }
891        });
892    }
893}
894
895fn ensure_containers_map_class_registered() {
896    CONTAINERS_MAP_CLASS_REGISTERED.ensure(|| {
897        let mut properties = HashMap::new();
898        for name in ["Count", "KeyType", "ValueType"] {
899            properties.insert(
900                name.to_string(),
901                crate::class_registry::RuntimeProperty {
902                    name: name.to_string(),
903                    is_static: false,
904                    is_constant: false,
905                    is_dependent: true,
906                    get_access: MemberAccess::Public,
907                    set_access: MemberAccess::Private,
908                    default_value: None,
909                },
910            );
911        }
912
913        let mut methods = HashMap::new();
914        for (name, function_name) in [
915            ("keys", BUILTIN_KEYS),
916            ("values", BUILTIN_VALUES),
917            ("isKey", BUILTIN_IS_KEY),
918            ("remove", BUILTIN_REMOVE),
919            (OBJECT_SUBSREF_METHOD, BUILTIN_SUBSREF),
920            (OBJECT_SUBSASGN_METHOD, BUILTIN_SUBSASGN),
921        ] {
922            methods.insert(
923                name.to_string(),
924                crate::class_registry::RuntimeMethod {
925                    name: name.to_string(),
926                    is_static: false,
927                    is_abstract: false,
928                    is_sealed: false,
929                    access: MemberAccess::Public,
930                    function_name: function_name.to_string(),
931                    implicit_class_argument: None,
932                },
933            );
934        }
935
936        crate::class_registry::register_class(crate::class_registry::RuntimeClass {
937            name: CLASS_NAME.to_string(),
938            parent: None,
939            properties,
940            methods,
941        });
942    });
943}
944
945#[derive(Clone, Copy, Debug, PartialEq, Eq)]
946enum KeyType {
947    Char,
948    Double,
949    Single,
950    Int32,
951    UInt32,
952    Int64,
953    UInt64,
954}
955
956impl KeyType {
957    fn matlab_name(self) -> &'static str {
958        match self {
959            KeyType::Char => "char",
960            KeyType::Double => "double",
961            KeyType::Single => "single",
962            KeyType::Int32 => "int32",
963            KeyType::UInt32 => "uint32",
964            KeyType::Int64 => "int64",
965            KeyType::UInt64 => "uint64",
966        }
967    }
968
969    fn parse(value: &Value, builtin: &'static str) -> BuiltinResult<Self> {
970        let text = string_from_value(value, "containers.Map: expected a KeyType string", builtin)?;
971        match text.to_ascii_lowercase().as_str() {
972            "char" | "character" => Ok(KeyType::Char),
973            "double" => Ok(KeyType::Double),
974            "single" => Ok(KeyType::Single),
975            "int32" => Ok(KeyType::Int32),
976            "uint32" => Ok(KeyType::UInt32),
977            "int64" => Ok(KeyType::Int64),
978            "uint64" => Ok(KeyType::UInt64),
979            other => Err(map_error(
980                format!(
981                    "containers.Map: unsupported KeyType '{other}'. Valid types: char, double, single, int32, uint32, int64, uint64."
982                ),
983                builtin,
984            )),
985        }
986    }
987}
988
989#[derive(Clone, Copy, Debug, PartialEq, Eq)]
990enum ValueType {
991    Any,
992    Char,
993    Double,
994    Single,
995    Logical,
996    Int8,
997    UInt8,
998    Int16,
999    UInt16,
1000    Int32,
1001    UInt32,
1002    Int64,
1003    UInt64,
1004}
1005
1006impl ValueType {
1007    fn matlab_name(self) -> &'static str {
1008        match self {
1009            ValueType::Any => "any",
1010            ValueType::Char => "char",
1011            ValueType::Double => "double",
1012            ValueType::Single => "single",
1013            ValueType::Logical => "logical",
1014            ValueType::Int8 => "int8",
1015            ValueType::UInt8 => "uint8",
1016            ValueType::Int16 => "int16",
1017            ValueType::UInt16 => "uint16",
1018            ValueType::Int32 => "int32",
1019            ValueType::UInt32 => "uint32",
1020            ValueType::Int64 => "int64",
1021            ValueType::UInt64 => "uint64",
1022        }
1023    }
1024
1025    fn parse(value: &Value, builtin: &'static str) -> BuiltinResult<Self> {
1026        let text = string_from_value(
1027            value,
1028            "containers.Map: expected a ValueType string",
1029            builtin,
1030        )?;
1031        match text.to_ascii_lowercase().as_str() {
1032            "any" => Ok(ValueType::Any),
1033            "char" | "character" => Ok(ValueType::Char),
1034            "double" => Ok(ValueType::Double),
1035            "single" => Ok(ValueType::Single),
1036            "logical" => Ok(ValueType::Logical),
1037            "int8" => Ok(ValueType::Int8),
1038            "uint8" => Ok(ValueType::UInt8),
1039            "int16" => Ok(ValueType::Int16),
1040            "uint16" => Ok(ValueType::UInt16),
1041            "int32" => Ok(ValueType::Int32),
1042            "uint32" => Ok(ValueType::UInt32),
1043            "int64" => Ok(ValueType::Int64),
1044            "uint64" => Ok(ValueType::UInt64),
1045            other => Err(map_error(
1046                format!(
1047                    "containers.Map: unsupported ValueType '{other}'. Valid types: any, char, logical, double, single, int8, uint8, int16, uint16, int32, uint32, int64, uint64."
1048                ),
1049                builtin,
1050            )),
1051        }
1052    }
1053
1054    fn normalize(&self, value: Value, builtin: &'static str) -> BuiltinResult<Value> {
1055        match self {
1056            ValueType::Any => Ok(value),
1057            ValueType::Char => {
1058                let chars = char_array_from_value(&value, builtin)?;
1059                Ok(Value::CharArray(chars))
1060            }
1061            ValueType::Double => normalize_numeric_value(value, NumericDType::F64, builtin),
1062            ValueType::Single => normalize_numeric_value(value, NumericDType::F32, builtin),
1063            ValueType::Logical => normalize_logical_value(value, builtin),
1064            integer_type => normalize_integer_value(value, *integer_type, builtin),
1065        }
1066    }
1067}
1068
1069#[derive(Clone, PartialEq, Eq, Hash)]
1070enum NormalizedKey {
1071    String(String),
1072    Float(u64),
1073    Int(i64),
1074    UInt(u64),
1075}
1076
1077#[derive(Clone)]
1078struct MapEntry {
1079    normalized: NormalizedKey,
1080    key_value: Value,
1081    value: Value,
1082}
1083
1084struct MapStore {
1085    storage: Option<GcHandle>,
1086    key_type: KeyType,
1087    value_type: ValueType,
1088    uniform_values: bool,
1089    uniform_class: Option<ValueClass>,
1090    entries: Vec<MapEntry>,
1091    index: HashMap<NormalizedKey, usize>,
1092}
1093
1094impl MapStore {
1095    fn new(key_type: KeyType, value_type: ValueType, uniform_values: bool) -> Self {
1096        Self {
1097            storage: None,
1098            key_type,
1099            value_type,
1100            uniform_values,
1101            uniform_class: None,
1102            entries: Vec::new(),
1103            index: HashMap::new(),
1104        }
1105    }
1106
1107    fn len(&self) -> usize {
1108        self.entries.len()
1109    }
1110
1111    fn contains(&self, key: &NormalizedKey) -> bool {
1112        self.index.contains_key(key)
1113    }
1114
1115    fn get(&self, key: &NormalizedKey) -> Option<Value> {
1116        self.index
1117            .get(key)
1118            .map(|&idx| self.entries[idx].value.clone())
1119    }
1120
1121    fn insert_new(&mut self, mut entry: MapEntry, builtin: &'static str) -> BuiltinResult<()> {
1122        if self.index.contains_key(&entry.normalized) {
1123            return Err(map_error(
1124                "containers.Map: Duplicate key name was provided.",
1125                builtin,
1126            ));
1127        }
1128        entry.value = self.normalize_value(entry.value, builtin)?;
1129        self.track_uniform_class(&entry.value, builtin)?;
1130        let idx = self.entries.len();
1131        self.entries.push(entry.clone());
1132        self.index.insert(entry.normalized, idx);
1133        Ok(())
1134    }
1135
1136    fn set(&mut self, mut entry: MapEntry, builtin: &'static str) -> BuiltinResult<()> {
1137        entry.value = self.normalize_value(entry.value, builtin)?;
1138        self.track_uniform_class(&entry.value, builtin)?;
1139        if let Some(&idx) = self.index.get(&entry.normalized) {
1140            self.entries[idx].value = entry.value.clone();
1141            self.entries[idx].key_value = entry.key_value;
1142        } else {
1143            let idx = self.entries.len();
1144            self.entries.push(entry.clone());
1145            self.index.insert(entry.normalized, idx);
1146        }
1147        Ok(())
1148    }
1149
1150    fn remove(&mut self, key: &NormalizedKey, builtin: &'static str) -> BuiltinResult<()> {
1151        let idx = match self.index.get(key) {
1152            Some(&idx) => idx,
1153            None => {
1154                return Err(map_descriptor_error(
1155                    &CONTAINERS_MAP_ERROR_MISSING_KEY,
1156                    builtin,
1157                ));
1158            }
1159        };
1160        self.entries.remove(idx);
1161        self.index.clear();
1162        for (pos, entry) in self.entries.iter().enumerate() {
1163            self.index.insert(entry.normalized.clone(), pos);
1164        }
1165        if self.entries.is_empty() {
1166            self.uniform_class = None;
1167        }
1168        Ok(())
1169    }
1170
1171    fn keys(&self) -> Vec<Value> {
1172        self.sorted_entries()
1173            .into_iter()
1174            .map(|entry| entry.key_value.clone())
1175            .collect()
1176    }
1177
1178    fn values(&self) -> Vec<Value> {
1179        self.sorted_entries()
1180            .into_iter()
1181            .map(|entry| entry.value.clone())
1182            .collect()
1183    }
1184
1185    fn sorted_entries(&self) -> Vec<&MapEntry> {
1186        let mut entries = self.entries.iter().collect::<Vec<_>>();
1187        entries.sort_by(|left, right| match (&left.normalized, &right.normalized) {
1188            (NormalizedKey::String(left), NormalizedKey::String(right)) => left.cmp(right),
1189            (NormalizedKey::Int(left), NormalizedKey::Int(right)) => left.cmp(right),
1190            (NormalizedKey::UInt(left), NormalizedKey::UInt(right)) => left.cmp(right),
1191            (NormalizedKey::Float(left), NormalizedKey::Float(right)) => f64::from_bits(*left)
1192                .partial_cmp(&f64::from_bits(*right))
1193                .unwrap_or(std::cmp::Ordering::Equal),
1194            _ => std::cmp::Ordering::Equal,
1195        });
1196        entries
1197    }
1198
1199    fn normalize_value(&self, value: Value, builtin: &'static str) -> BuiltinResult<Value> {
1200        self.value_type.normalize(value, builtin)
1201    }
1202
1203    fn track_uniform_class(&mut self, value: &Value, builtin: &'static str) -> BuiltinResult<()> {
1204        if !self.uniform_values {
1205            return Ok(());
1206        }
1207        let class = ValueClass::from_value(value);
1208        if let Some(existing) = &self.uniform_class {
1209            if existing != &class {
1210                return Err(map_error(
1211                    "containers.Map: UniformValues=true requires all values to share the same MATLAB class.",
1212                    builtin,
1213                ));
1214            }
1215        } else {
1216            self.uniform_class = Some(class);
1217        }
1218        Ok(())
1219    }
1220}
1221
1222#[derive(Clone, Debug, PartialEq, Eq)]
1223enum ValueClass {
1224    Char,
1225    String,
1226    Numeric(NumericDType),
1227    Logical,
1228    Cell,
1229    Struct,
1230    Object,
1231    Other(&'static str),
1232}
1233
1234impl ValueClass {
1235    fn from_value(value: &Value) -> Self {
1236        match value {
1237            Value::CharArray(_) => ValueClass::Char,
1238            Value::String(_) | Value::StringArray(_) => ValueClass::String,
1239            Value::Num(_) => ValueClass::Numeric(NumericDType::F64),
1240            Value::Tensor(tensor) => ValueClass::Numeric(tensor.numeric_dtype()),
1241            Value::ComplexTensor(tensor) => ValueClass::Numeric(tensor.numeric_dtype()),
1242            Value::Bool(_) | Value::LogicalArray(_) => ValueClass::Logical,
1243            Value::Int(value) => ValueClass::Numeric(int_value_dtype(value)),
1244            Value::Cell(_) => ValueClass::Cell,
1245            Value::Struct(_) => ValueClass::Struct,
1246            Value::ObjectArray(_)
1247            | Value::Object(_)
1248            | Value::HandleObject(_)
1249            | Value::Listener(_) => ValueClass::Object,
1250            _ => ValueClass::Other("other"),
1251        }
1252    }
1253}
1254
1255struct ConstructorArgs {
1256    key_type: KeyType,
1257    value_type: ValueType,
1258    uniform_values: bool,
1259    keys: Vec<KeyCandidate>,
1260    values: Vec<Value>,
1261}
1262
1263struct KeyCandidate {
1264    normalized: NormalizedKey,
1265    canonical: Value,
1266}
1267
1268#[runtime_builtin(
1269    name = "containers.Map",
1270    category = "containers/map",
1271    summary = "Create key-value dictionary objects.",
1272    keywords = "map,containers.Map,dictionary,hash map,lookup",
1273    accel = "metadata",
1274    sink = true,
1275    type_resolver(map_handle_type),
1276    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_DESCRIPTOR),
1277    extensions(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_EXTENSIONS),
1278    integer_capabilities(
1279        crate::builtins::containers::map::containers_map::CONTAINERS_MAP_INTEGER_CAPABILITIES
1280    ),
1281    builtin_path = "crate::builtins::containers::map::containers_map"
1282)]
1283async fn containers_map_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
1284    if args.iter().any(contains_resident_value) {
1285        crate::compatibility::ensure_builtin_extension_enabled(
1286            &MAP_RESIDENT_CONSTRUCTOR_EXTENSION,
1287            BUILTIN_CONSTRUCTOR,
1288        )?;
1289    }
1290    let mut host_args = Vec::with_capacity(args.len());
1291    for value in args {
1292        host_args.push(
1293            gather_if_needed_async(&value)
1294                .await
1295                .map_err(|err| attach_builtin_context(err, BUILTIN_CONSTRUCTOR))?,
1296        );
1297    }
1298    let parsed = parse_constructor_args(host_args, BUILTIN_CONSTRUCTOR).await?;
1299    let store = build_store(parsed, BUILTIN_CONSTRUCTOR)?;
1300    allocate_handle(store, BUILTIN_CONSTRUCTOR)
1301}
1302
1303#[runtime_builtin(
1304    name = "containers.Map.keys",
1305    type_resolver(map_cell_type),
1306    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_KEYS_DESCRIPTOR),
1307    integer_capabilities(
1308        crate::builtins::containers::map::containers_map::CONTAINERS_MAP_KEYS_INTEGER_CAPABILITIES
1309    ),
1310    builtin_path = "crate::builtins::containers::map::containers_map"
1311)]
1312async fn containers_map_keys(map: Value) -> crate::BuiltinResult<Value> {
1313    with_store(&map, BUILTIN_KEYS, |store| {
1314        let values = store.keys();
1315        make_row_cell(values, BUILTIN_KEYS)
1316    })
1317}
1318
1319#[runtime_builtin(
1320    name = "containers.Map.values",
1321    type_resolver(map_cell_type),
1322    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_VALUES_DESCRIPTOR),
1323    extensions(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_VALUES_EXTENSIONS),
1324    integer_capabilities(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_VALUES_INTEGER_CAPABILITIES),
1325    builtin_path = "crate::builtins::containers::map::containers_map"
1326)]
1327async fn containers_map_values(map: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
1328    if rest.iter().any(contains_resident_value) {
1329        crate::compatibility::ensure_builtin_extension_enabled(
1330            &MAP_RESIDENT_VALUES_EXTENSION,
1331            BUILTIN_VALUES,
1332        )?;
1333    }
1334    if rest.is_empty() {
1335        return with_store(&map, BUILTIN_VALUES, |store| {
1336            make_row_cell(store.values(), BUILTIN_VALUES)
1337        });
1338    }
1339    if rest.len() != 1 {
1340        return Err(map_error(
1341            "containers.Map: values expects M or M,keySet",
1342            BUILTIN_VALUES,
1343        ));
1344    }
1345    let Value::Cell(key_set) = &rest[0] else {
1346        return Err(map_error(
1347            "containers.Map: values keySet must be a cell array",
1348            BUILTIN_VALUES,
1349        ));
1350    };
1351    let mut keys = Vec::with_capacity(key_set.data.len());
1352    for key in &key_set.data {
1353        keys.push(
1354            gather_if_needed_async(key)
1355                .await
1356                .map_err(|err| attach_builtin_context(err, BUILTIN_VALUES))?,
1357        );
1358    }
1359    with_store(&map, BUILTIN_VALUES, |store| {
1360        let mut values = Vec::with_capacity(keys.len());
1361        for key in &keys {
1362            let normalized = normalize_key(key, store.key_type, BUILTIN_VALUES)?;
1363            values.push(store.get(&normalized).ok_or_else(|| {
1364                map_descriptor_error(&CONTAINERS_MAP_ERROR_MISSING_KEY, BUILTIN_VALUES)
1365            })?);
1366        }
1367        crate::make_cell_with_shape(values, vec![key_set.rows, key_set.cols])
1368            .map_err(|err| map_error(format!("containers.Map: {err}"), BUILTIN_VALUES))
1369    })
1370}
1371
1372#[runtime_builtin(
1373    name = "containers.Map.isKey",
1374    type_resolver(map_is_key_type),
1375    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_ISKEY_DESCRIPTOR),
1376    extensions(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_ISKEY_EXTENSIONS),
1377    integer_capabilities(
1378        crate::builtins::containers::map::containers_map::CONTAINERS_MAP_ISKEY_INTEGER_CAPABILITIES
1379    ),
1380    builtin_path = "crate::builtins::containers::map::containers_map"
1381)]
1382async fn containers_map_is_key(map: Value, key_spec: Value) -> crate::BuiltinResult<Value> {
1383    ensure_resident_extension(&key_spec, &MAP_RESIDENT_ISKEY_EXTENSION, BUILTIN_IS_KEY)?;
1384    let key_type = with_store(&map, BUILTIN_IS_KEY, |store| Ok(store.key_type))?;
1385    let collection = collect_key_spec(&key_spec, key_type, BUILTIN_IS_KEY).await?;
1386    with_store(&map, BUILTIN_IS_KEY, |store| {
1387        let mut flags = Vec::with_capacity(collection.values.len());
1388        for value in &collection.values {
1389            let normalized = normalize_key(value, store.key_type, BUILTIN_IS_KEY)?;
1390            flags.push(store.contains(&normalized));
1391        }
1392        if collection.values.len() == 1 {
1393            Ok(Value::Bool(flags[0]))
1394        } else {
1395            let data: Vec<u8> = flags.into_iter().map(|b| if b { 1 } else { 0 }).collect();
1396            let logical = LogicalArray::new(data, collection.shape)
1397                .map_err(|e| map_error(format!("containers.Map: {e}"), BUILTIN_IS_KEY))?;
1398            Ok(Value::LogicalArray(logical))
1399        }
1400    })
1401}
1402
1403#[runtime_builtin(
1404    name = "containers.Map.remove",
1405    type_resolver(map_handle_type),
1406    descriptor(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_REMOVE_DESCRIPTOR),
1407    extensions(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_REMOVE_EXTENSIONS),
1408    integer_capabilities(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_REMOVE_INTEGER_CAPABILITIES),
1409    builtin_path = "crate::builtins::containers::map::containers_map"
1410)]
1411async fn containers_map_remove(map: Value, key_spec: Value) -> crate::BuiltinResult<Value> {
1412    ensure_resident_extension(&key_spec, &MAP_RESIDENT_REMOVE_EXTENSION, BUILTIN_REMOVE)?;
1413    let key_type = with_store(&map, BUILTIN_REMOVE, |store| Ok(store.key_type))?;
1414    let collection = collect_key_spec(&key_spec, key_type, BUILTIN_REMOVE).await?;
1415    with_store_mut(&map, BUILTIN_REMOVE, |store| {
1416        for value in &collection.values {
1417            let normalized = normalize_key(value, store.key_type, BUILTIN_REMOVE)?;
1418            store.remove(&normalized, BUILTIN_REMOVE)?;
1419        }
1420        Ok(())
1421    })?;
1422    Ok(map)
1423}
1424
1425#[runtime_builtin(
1426    name = "containers.Map.subsref",
1427    type_resolver(map_unknown_type),
1428    descriptor(
1429        crate::builtins::containers::map::containers_map::CONTAINERS_MAP_SUBSREF_DESCRIPTOR
1430    ),
1431    extensions(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_SUBSREF_EXTENSIONS),
1432    integer_capabilities(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_SUBSREF_INTEGER_CAPABILITIES),
1433    builtin_path = "crate::builtins::containers::map::containers_map"
1434)]
1435async fn containers_map_subsref(
1436    map: Value,
1437    kind: String,
1438    payload: Value,
1439) -> crate::BuiltinResult<Value> {
1440    ensure_resident_extension(&payload, &MAP_RESIDENT_SUBSREF_EXTENSION, BUILTIN_SUBSREF)?;
1441    if !matches!(map, Value::HandleObject(_)) {
1442        return Err(map_error(
1443            format!("containers.Map: subsref expects a containers.Map handle, got {map:?}"),
1444            BUILTIN_SUBSREF,
1445        ));
1446    }
1447    match kind.as_str() {
1448        OBJECT_INDEX_PAREN => {
1449            let mut args = extract_key_arguments(&payload, BUILTIN_SUBSREF)?;
1450            if args.is_empty() {
1451                return Err(map_error(
1452                    "containers.Map: indexing requires at least one key",
1453                    BUILTIN_SUBSREF,
1454                ));
1455            }
1456            if args.len() != 1 {
1457                return Err(map_error(
1458                    "containers.Map: indexing expects a single key argument",
1459                    BUILTIN_SUBSREF,
1460                ));
1461            }
1462            let key_arg = args.remove(0);
1463            let key_type = with_store(&map, BUILTIN_SUBSREF, |store| Ok(store.key_type))?;
1464            let collection = collect_key_spec(&key_arg, key_type, BUILTIN_SUBSREF).await?;
1465            if collection.values.len() != 1 {
1466                return Err(map_error(
1467                    "containers.Map: indexing requires exactly one scalar key",
1468                    BUILTIN_SUBSREF,
1469                ));
1470            }
1471            with_store(&map, BUILTIN_SUBSREF, |store| {
1472                let normalized =
1473                    normalize_key(&collection.values[0], store.key_type, BUILTIN_SUBSREF)?;
1474                store.get(&normalized).ok_or_else(|| {
1475                    map_descriptor_error(&CONTAINERS_MAP_ERROR_MISSING_KEY, BUILTIN_SUBSREF)
1476                })
1477            })
1478        }
1479        OBJECT_INDEX_MEMBER => {
1480            let field = string_from_value(
1481                &payload,
1482                "containers.Map: property name must be text",
1483                BUILTIN_SUBSREF,
1484            )?;
1485            with_store(&map, BUILTIN_SUBSREF, |store| {
1486                match field.to_ascii_lowercase().as_str() {
1487                    "count" => Ok(Value::Int(IntValue::U64(store.len() as u64))),
1488                    "keytype" => char_array_value(store.key_type.matlab_name(), BUILTIN_SUBSREF),
1489                    "valuetype" => {
1490                        char_array_value(store.value_type.matlab_name(), BUILTIN_SUBSREF)
1491                    }
1492                    other => Err(map_error(
1493                        format!("containers.Map: no such property '{other}'"),
1494                        BUILTIN_SUBSREF,
1495                    )),
1496                }
1497            })
1498        }
1499        OBJECT_INDEX_BRACE => Err(map_error(
1500            "containers.Map: curly-brace indexing is not supported.",
1501            BUILTIN_SUBSREF,
1502        )),
1503        other => Err(map_error(
1504            format!("containers.Map: unsupported indexing kind '{other}'"),
1505            BUILTIN_SUBSREF,
1506        )),
1507    }
1508}
1509
1510#[runtime_builtin(
1511    name = "containers.Map.subsasgn",
1512    type_resolver(map_handle_type),
1513    descriptor(
1514        crate::builtins::containers::map::containers_map::CONTAINERS_MAP_SUBSASGN_DESCRIPTOR
1515    ),
1516    extensions(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_SUBSASGN_EXTENSIONS),
1517    integer_capabilities(crate::builtins::containers::map::containers_map::CONTAINERS_MAP_SUBSASGN_INTEGER_CAPABILITIES),
1518    builtin_path = "crate::builtins::containers::map::containers_map"
1519)]
1520async fn containers_map_subsasgn(
1521    map: Value,
1522    kind: String,
1523    payload: Value,
1524    rhs: Value,
1525) -> crate::BuiltinResult<Value> {
1526    if contains_resident_value(&payload) || contains_resident_value(&rhs) {
1527        crate::compatibility::ensure_builtin_extension_enabled(
1528            &MAP_RESIDENT_SUBSASGN_EXTENSION,
1529            BUILTIN_SUBSASGN,
1530        )?;
1531    }
1532    if !matches!(map, Value::HandleObject(_)) {
1533        return Err(map_error(
1534            format!("containers.Map: subsasgn expects a containers.Map handle, got {map:?}"),
1535            BUILTIN_SUBSASGN,
1536        ));
1537    }
1538    match kind.as_str() {
1539        OBJECT_INDEX_PAREN => {
1540            let mut args = extract_key_arguments(&payload, BUILTIN_SUBSASGN)?;
1541            if args.is_empty() {
1542                return Err(map_error(
1543                    "containers.Map: assignment requires at least one key",
1544                    BUILTIN_SUBSASGN,
1545                ));
1546            }
1547            if args.len() != 1 {
1548                return Err(map_error(
1549                    "containers.Map: assignment expects a single key argument",
1550                    BUILTIN_SUBSASGN,
1551                ));
1552            }
1553            let key_arg = args.remove(0);
1554            let key_type = with_store(&map, BUILTIN_SUBSASGN, |store| Ok(store.key_type))?;
1555            let KeyCollection {
1556                values: key_values, ..
1557            } = collect_key_spec(&key_arg, key_type, BUILTIN_SUBSASGN).await?;
1558            if key_values.len() != 1 {
1559                return Err(map_error(
1560                    "containers.Map: assignment requires exactly one scalar key",
1561                    BUILTIN_SUBSASGN,
1562                ));
1563            }
1564            let values =
1565                expand_assignment_values(rhs.clone(), key_values.len(), BUILTIN_SUBSASGN).await?;
1566            with_store_mut(&map, BUILTIN_SUBSASGN, move |store| {
1567                for (key_raw, value) in key_values.into_iter().zip(values.into_iter()) {
1568                    let (normalized, canonical) =
1569                        canonicalize_key(key_raw, store.key_type, BUILTIN_SUBSASGN)?;
1570                    let entry = MapEntry {
1571                        normalized,
1572                        key_value: canonical,
1573                        value,
1574                    };
1575                    store.set(entry, BUILTIN_SUBSASGN)?;
1576                }
1577                Ok(())
1578            })?;
1579            Ok(map)
1580        }
1581        OBJECT_INDEX_MEMBER => Err(map_error(
1582            "containers.Map: property assignments are not supported.",
1583            BUILTIN_SUBSASGN,
1584        )),
1585        OBJECT_INDEX_BRACE => Err(map_error(
1586            "containers.Map: curly-brace assignment is not supported.",
1587            BUILTIN_SUBSASGN,
1588        )),
1589        other => Err(map_error(
1590            format!("containers.Map: unsupported assignment kind '{other}'"),
1591            BUILTIN_SUBSASGN,
1592        )),
1593    }
1594}
1595
1596async fn parse_constructor_args(
1597    args: Vec<Value>,
1598    builtin: &'static str,
1599) -> BuiltinResult<ConstructorArgs> {
1600    let mut index = 0usize;
1601    let mut keys_input: Option<Value> = None;
1602    let mut values_input: Option<Value> = None;
1603
1604    if index < args.len() && keyword_of(&args[index]).is_none() {
1605        if args.len() < 2 {
1606            return Err(map_error(
1607                "containers.Map: constructor requires both keys and values when either is provided.",
1608                builtin,
1609            ));
1610        }
1611        keys_input = Some(args[index].clone());
1612        values_input = Some(args[index + 1].clone());
1613        index += 2;
1614    }
1615
1616    let has_data = keys_input.is_some();
1617    let mut key_type = KeyType::Char;
1618    let mut value_type = ValueType::Any;
1619    let mut uniform_values = has_data;
1620    let mut key_type_explicit = false;
1621    let mut value_type_explicit = false;
1622    while index < args.len() {
1623        let keyword = keyword_of(&args[index]).ok_or_else(|| {
1624            map_error(
1625                "containers.Map: expected option name (e.g. 'KeyType')",
1626                builtin,
1627            )
1628        })?;
1629        index += 1;
1630        let Some(value) = args.get(index) else {
1631            return Err(map_error(
1632                format!("containers.Map: missing value for option '{keyword}'"),
1633                builtin,
1634            ));
1635        };
1636        index += 1;
1637        match keyword.as_str() {
1638            "keytype" => {
1639                if has_data {
1640                    return Err(map_error(
1641                        "containers.Map: KeyType is only valid for an empty Map constructor",
1642                        builtin,
1643                    ));
1644                }
1645                key_type = KeyType::parse(value, builtin)?;
1646                key_type_explicit = true;
1647            }
1648            "valuetype" => {
1649                if has_data {
1650                    return Err(map_error(
1651                        "containers.Map: ValueType is only valid for an empty Map constructor",
1652                        builtin,
1653                    ));
1654                }
1655                value_type = ValueType::parse(value, builtin)?;
1656                value_type_explicit = true;
1657            }
1658            "uniformvalues" => {
1659                if !has_data {
1660                    return Err(map_error(
1661                        "containers.Map: UniformValues requires keySet and valueSet",
1662                        builtin,
1663                    ));
1664                }
1665                uniform_values = bool_from_value(
1666                    value,
1667                    "containers.Map: UniformValues must be logical",
1668                    builtin,
1669                )?
1670            }
1671            other => {
1672                return Err(map_error(
1673                    format!("containers.Map: unrecognised option '{other}'"),
1674                    builtin,
1675                ));
1676            }
1677        }
1678    }
1679
1680    if !has_data && (key_type_explicit != value_type_explicit) {
1681        return Err(map_error(
1682            "containers.Map: KeyType and ValueType must both be specified for an empty typed Map",
1683            builtin,
1684        ));
1685    }
1686    if let Some(keys) = &keys_input {
1687        key_type = infer_key_type(keys, builtin)?;
1688    }
1689    if let Some(values) = &values_input {
1690        value_type = if uniform_values {
1691            infer_value_type(values)
1692        } else {
1693            ValueType::Any
1694        };
1695    }
1696
1697    let keys = match keys_input {
1698        Some(value) => prepare_keys(value, key_type, builtin).await?,
1699        None => Vec::new(),
1700    };
1701
1702    let values = match values_input {
1703        Some(value) => prepare_values(value, builtin).await?,
1704        None => Vec::new(),
1705    };
1706
1707    if keys.len() != values.len() {
1708        return Err(map_error(
1709            format!(
1710                "containers.Map: number of keys ({}) must match number of values ({})",
1711                keys.len(),
1712                values.len()
1713            ),
1714            builtin,
1715        ));
1716    }
1717
1718    Ok(ConstructorArgs {
1719        key_type,
1720        value_type,
1721        uniform_values,
1722        keys,
1723        values,
1724    })
1725}
1726
1727fn infer_key_type(value: &Value, builtin: &'static str) -> BuiltinResult<KeyType> {
1728    match value {
1729        Value::CharArray(_) | Value::StringArray(_) | Value::String(_) => Ok(KeyType::Char),
1730        Value::Cell(cell)
1731            if cell.data.iter().all(|value| {
1732                matches!(value, Value::CharArray(_) | Value::String(_) | Value::StringArray(_))
1733            }) =>
1734        {
1735            Ok(KeyType::Char)
1736        }
1737        Value::Num(_) => Ok(KeyType::Double),
1738        Value::Tensor(tensor) => key_type_from_dtype(tensor.numeric_dtype()).ok_or_else(|| {
1739            map_error(
1740                "containers.Map: numeric key arrays must be double, single, int32, uint32, int64, or uint64",
1741                builtin,
1742            )
1743        }),
1744        Value::Int(value) => key_type_from_dtype(int_value_dtype(value)).ok_or_else(|| {
1745            map_error(
1746                "containers.Map: integer keys must be int32, uint32, int64, or uint64",
1747                builtin,
1748            )
1749        }),
1750        Value::GpuTensor(handle) => {
1751            if let Some(integer_type) = runmat_accelerate_api::handle_integer_type(handle) {
1752                match integer_type {
1753                    runmat_accelerate_api::IntegerElementType::I32 => Ok(KeyType::Int32),
1754                    runmat_accelerate_api::IntegerElementType::U32 => Ok(KeyType::UInt32),
1755                    runmat_accelerate_api::IntegerElementType::I64 => Ok(KeyType::Int64),
1756                    runmat_accelerate_api::IntegerElementType::U64 => Ok(KeyType::UInt64),
1757                    _ => Err(map_error(
1758                        "containers.Map: resident integer keys must be int32, uint32, int64, or uint64",
1759                        builtin,
1760                    )),
1761                }
1762            } else if runmat_accelerate_api::handle_precision(handle)
1763                == Some(runmat_accelerate_api::ProviderPrecision::F32)
1764            {
1765                Ok(KeyType::Single)
1766            } else {
1767                Ok(KeyType::Double)
1768            }
1769        }
1770        _ => Err(map_error(
1771            "containers.Map: keys must be a numeric array, cell array of character vectors, or string array",
1772            builtin,
1773        )),
1774    }
1775}
1776
1777fn key_type_from_dtype(dtype: NumericDType) -> Option<KeyType> {
1778    match dtype {
1779        NumericDType::F64 => Some(KeyType::Double),
1780        NumericDType::F32 => Some(KeyType::Single),
1781        NumericDType::I32 => Some(KeyType::Int32),
1782        NumericDType::U32 => Some(KeyType::UInt32),
1783        NumericDType::I64 => Some(KeyType::Int64),
1784        NumericDType::U64 => Some(KeyType::UInt64),
1785        _ => None,
1786    }
1787}
1788
1789fn int_value_dtype(value: &IntValue) -> NumericDType {
1790    IntegerStorage::from_scalar(value.clone()).numeric_dtype()
1791}
1792
1793fn infer_value_type(value: &Value) -> ValueType {
1794    match value {
1795        Value::Num(_) => ValueType::Double,
1796        Value::Tensor(tensor) => value_type_from_dtype(tensor.numeric_dtype()),
1797        Value::Int(value) => value_type_from_dtype(int_value_dtype(value)),
1798        Value::Bool(_) | Value::LogicalArray(_) => ValueType::Logical,
1799        Value::CharArray(_) => ValueType::Char,
1800        Value::GpuTensor(handle) => {
1801            if runmat_accelerate_api::handle_is_logical(handle) {
1802                ValueType::Logical
1803            } else if let Some(integer_type) = runmat_accelerate_api::handle_integer_type(handle) {
1804                match integer_type {
1805                    runmat_accelerate_api::IntegerElementType::I8 => ValueType::Int8,
1806                    runmat_accelerate_api::IntegerElementType::U8 => ValueType::UInt8,
1807                    runmat_accelerate_api::IntegerElementType::I16 => ValueType::Int16,
1808                    runmat_accelerate_api::IntegerElementType::U16 => ValueType::UInt16,
1809                    runmat_accelerate_api::IntegerElementType::I32 => ValueType::Int32,
1810                    runmat_accelerate_api::IntegerElementType::U32 => ValueType::UInt32,
1811                    runmat_accelerate_api::IntegerElementType::I64 => ValueType::Int64,
1812                    runmat_accelerate_api::IntegerElementType::U64 => ValueType::UInt64,
1813                }
1814            } else if runmat_accelerate_api::handle_precision(handle)
1815                == Some(runmat_accelerate_api::ProviderPrecision::F32)
1816            {
1817                ValueType::Single
1818            } else {
1819                ValueType::Double
1820            }
1821        }
1822        Value::Cell(cell) => {
1823            let mut iter = cell.data.iter();
1824            let Some(first) = iter.next() else {
1825                return ValueType::Any;
1826            };
1827            let inferred = infer_value_type(first);
1828            if inferred != ValueType::Any && iter.all(|value| infer_value_type(value) == inferred) {
1829                inferred
1830            } else {
1831                ValueType::Any
1832            }
1833        }
1834        _ => ValueType::Any,
1835    }
1836}
1837
1838fn value_type_from_dtype(dtype: NumericDType) -> ValueType {
1839    match dtype {
1840        NumericDType::F64 => ValueType::Double,
1841        NumericDType::F32 => ValueType::Single,
1842        NumericDType::I8 => ValueType::Int8,
1843        NumericDType::U8 => ValueType::UInt8,
1844        NumericDType::I16 => ValueType::Int16,
1845        NumericDType::U16 => ValueType::UInt16,
1846        NumericDType::I32 => ValueType::Int32,
1847        NumericDType::U32 => ValueType::UInt32,
1848        NumericDType::I64 => ValueType::Int64,
1849        NumericDType::U64 => ValueType::UInt64,
1850    }
1851}
1852
1853fn build_store(args: ConstructorArgs, builtin: &'static str) -> BuiltinResult<MapStore> {
1854    let mut store = MapStore::new(args.key_type, args.value_type, args.uniform_values);
1855    for (candidate, value) in args.keys.into_iter().zip(args.values.into_iter()) {
1856        store.insert_new(
1857            MapEntry {
1858                normalized: candidate.normalized,
1859                key_value: candidate.canonical,
1860                value,
1861            },
1862            builtin,
1863        )?;
1864    }
1865    Ok(store)
1866}
1867
1868fn allocate_handle(store: MapStore, builtin: &'static str) -> BuiltinResult<Value> {
1869    ensure_containers_map_class_registered();
1870
1871    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
1872    ensure_map_registry_root_registered(builtin)?;
1873    MAP_REGISTRY.with(|registry| {
1874        registry
1875            .try_borrow_mut()
1876            .map_err(|_| map_internal("containers.Map: registry is already borrowed", builtin))?
1877            .insert(id, store);
1878        Ok::<(), RuntimeError>(())
1879    })?;
1880    let mut storage = ObjectInstance::new(CLASS_NAME.to_string());
1881    storage
1882        .properties
1883        .insert("id".to_string(), Value::Int(IntValue::U64(id)));
1884    let gc = match runmat_gc::gc_allocate(Value::Object(storage)) {
1885        Ok(gc) => gc,
1886        Err(e) => {
1887            MAP_REGISTRY.with(|registry| {
1888                if let Ok(mut registry) = registry.try_borrow_mut() {
1889                    registry.remove(&id);
1890                }
1891            });
1892            deactivate_map_registry_root_if_empty();
1893            return Err(map_error(format!("containers.Map: {e}"), builtin));
1894        }
1895    };
1896    MAP_REGISTRY.with(|registry| {
1897        let mut registry = registry
1898            .try_borrow_mut()
1899            .map_err(|_| map_internal("containers.Map: registry is already borrowed", builtin))?;
1900        let store = registry
1901            .get_mut(&id)
1902            .ok_or_else(|| map_internal("containers.Map: internal storage not found", builtin))?;
1903        store.storage = Some(gc);
1904        Ok::<(), RuntimeError>(())
1905    })?;
1906    Ok(Value::HandleObject(HandleRef {
1907        class_name: CLASS_NAME.to_string(),
1908        target: gc,
1909        valid: true,
1910    }))
1911}
1912
1913fn with_store<F, R>(map: &Value, builtin: &'static str, f: F) -> BuiltinResult<R>
1914where
1915    F: FnOnce(&MapStore) -> BuiltinResult<R>,
1916{
1917    let handle = extract_handle(map, builtin)?;
1918    ensure_handle(handle, builtin)?;
1919    let id = map_id(handle, builtin)?;
1920    MAP_REGISTRY.with(|registry| {
1921        let registry = registry
1922            .try_borrow()
1923            .map_err(|_| map_internal("containers.Map: registry already borrowed", builtin))?;
1924        let store = registry
1925            .get(&id)
1926            .ok_or_else(|| map_internal("containers.Map: internal storage not found", builtin))?;
1927        f(store)
1928    })
1929}
1930
1931fn with_store_mut<F, R>(map: &Value, builtin: &'static str, f: F) -> BuiltinResult<R>
1932where
1933    F: FnOnce(&mut MapStore) -> BuiltinResult<R>,
1934{
1935    let handle = extract_handle(map, builtin)?;
1936    ensure_handle(handle, builtin)?;
1937    let id = map_id(handle, builtin)?;
1938    MAP_REGISTRY.with(|registry| {
1939        let mut registry = registry
1940            .try_borrow_mut()
1941            .map_err(|_| map_internal("containers.Map: registry already borrowed", builtin))?;
1942        let store = registry
1943            .get_mut(&id)
1944            .ok_or_else(|| map_internal("containers.Map: internal storage not found", builtin))?;
1945        f(store)
1946    })
1947}
1948
1949fn extract_handle<'a>(value: &'a Value, builtin: &'static str) -> BuiltinResult<&'a HandleRef> {
1950    match value {
1951        Value::HandleObject(handle) => Ok(handle),
1952        _ => Err(map_error(
1953            "containers.Map: expected a containers.Map handle",
1954            builtin,
1955        )),
1956    }
1957}
1958
1959fn ensure_handle(handle: &HandleRef, builtin: &'static str) -> BuiltinResult<()> {
1960    if !crate::is_handle_valid(handle) {
1961        return Err(map_error("containers.Map: handle is invalid", builtin));
1962    }
1963    if handle.class_name != CLASS_NAME {
1964        return Err(map_error(
1965            format!(
1966                "containers.Map: expected handle of class '{}', got '{}'",
1967                CLASS_NAME, handle.class_name
1968            ),
1969            builtin,
1970        ));
1971    }
1972    Ok(())
1973}
1974
1975fn map_id(handle: &HandleRef, builtin: &'static str) -> BuiltinResult<u64> {
1976    let storage = runmat_gc::gc_clone_value(&handle.target).map_err(|e| {
1977        map_internal(
1978            format!("containers.Map: invalid handle storage: {e}"),
1979            builtin,
1980        )
1981    })?;
1982    let id_value = match &storage {
1983        Value::Object(object) if object.class_name == CLASS_NAME => object.properties.get("id"),
1984        Value::Struct(StructValue { fields }) => fields.get("id"),
1985        other => {
1986            return Err(map_internal(
1987                format!("containers.Map: internal storage has unexpected shape {other:?}"),
1988                builtin,
1989            ));
1990        }
1991    };
1992    match id_value {
1993        Some(Value::Int(IntValue::U64(id))) => Ok(*id),
1994        Some(Value::Int(other)) => {
1995            let id = other.to_i64();
1996            if id < 0 {
1997                Err(map_internal(
1998                    "containers.Map: negative map identifier",
1999                    builtin,
2000                ))
2001            } else {
2002                Ok(id as u64)
2003            }
2004        }
2005        Some(Value::Num(n)) if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 => {
2006            if *n >= u64::MAX as f64 {
2007                Err(map_internal(
2008                    "containers.Map: map identifier out of range",
2009                    builtin,
2010                ))
2011            } else {
2012                Ok(*n as u64)
2013            }
2014        }
2015        _ => Err(map_internal(
2016            "containers.Map: corrupted storage identifier",
2017            builtin,
2018        )),
2019    }
2020}
2021
2022async fn prepare_keys(
2023    value: Value,
2024    key_type: KeyType,
2025    builtin: &'static str,
2026) -> BuiltinResult<Vec<KeyCandidate>> {
2027    let host = gather_if_needed_async(&value)
2028        .await
2029        .map_err(|err| attach_builtin_context(err, builtin))?;
2030    let flattened = flatten_keys(&host, key_type, builtin).await?;
2031    let mut out = Vec::with_capacity(flattened.len());
2032    for raw_key in flattened {
2033        let (normalized, canonical) = canonicalize_key(raw_key, key_type, builtin)?;
2034        out.push(KeyCandidate {
2035            normalized,
2036            canonical,
2037        });
2038    }
2039    Ok(out)
2040}
2041
2042async fn prepare_values(value: Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
2043    let host = gather_if_needed_async(&value)
2044        .await
2045        .map_err(|err| attach_builtin_context(err, builtin))?;
2046    flatten_values(&host, builtin).await
2047}
2048
2049async fn flatten_keys(
2050    value: &Value,
2051    _key_type: KeyType,
2052    builtin: &'static str,
2053) -> BuiltinResult<Vec<Value>> {
2054    match value {
2055        Value::Cell(cell) => {
2056            let mut out = Vec::with_capacity(cell.data.len());
2057            for ptr in &cell.data {
2058                let element = ptr;
2059                if matches!(element, Value::Cell(_)) {
2060                    return Err(map_error(
2061                        "containers.Map: nested cell arrays are not supported for keys",
2062                        builtin,
2063                    ));
2064                }
2065                out.push(
2066                    gather_if_needed_async(element)
2067                        .await
2068                        .map_err(|err| attach_builtin_context(err, builtin))?,
2069                );
2070            }
2071            Ok(out)
2072        }
2073        Value::StringArray(sa) => Ok(sa
2074            .data
2075            .iter()
2076            .map(|text| Value::String(text.clone()))
2077            .collect()),
2078        Value::CharArray(ca) => Ok(char_array_rows(ca, builtin)?),
2079        Value::LogicalArray(_) => Err(map_error(
2080            "containers.Map: logical arrays are not supported as Map keys",
2081            builtin,
2082        )),
2083        Value::Tensor(t) => {
2084            if !t.shape.is_empty()
2085                && tensor::tensor_element_len(t) != 1
2086                && !is_vector_shape(&t.shape)
2087            {
2088                return Err(map_error(
2089                    "containers.Map: numeric keys must be scalar or vector shaped",
2090                    builtin,
2091                ));
2092            }
2093            Ok(tensor_elements_to_values(t))
2094        }
2095        Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::String(_) => {
2096            Ok(vec![value.clone()])
2097        }
2098        Value::GpuTensor(_) => Err(map_error(
2099            "containers.Map: GPU keys must be gathered to the host before construction",
2100            builtin,
2101        )),
2102        other => Err(map_error(
2103            format!("containers.Map: unsupported key container {other:?}"),
2104            builtin,
2105        )),
2106    }
2107}
2108
2109async fn flatten_values(value: &Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
2110    match value {
2111        Value::Cell(cell) => {
2112            let mut out = Vec::with_capacity(cell.data.len());
2113            for ptr in &cell.data {
2114                out.push(
2115                    gather_if_needed_async(ptr)
2116                        .await
2117                        .map_err(|err| attach_builtin_context(err, builtin))?,
2118                );
2119            }
2120            Ok(out)
2121        }
2122        Value::StringArray(sa) => Ok(sa
2123            .data
2124            .iter()
2125            .map(|text| Value::String(text.clone()))
2126            .collect()),
2127        Value::CharArray(ca) => Ok(char_array_rows(ca, builtin)?),
2128        Value::LogicalArray(arr) => Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect()),
2129        Value::Tensor(t) => {
2130            if !t.shape.is_empty()
2131                && !is_vector_shape(&t.shape)
2132                && tensor::tensor_element_len(t) != 1
2133            {
2134                return Err(map_error(
2135                    "containers.Map: numeric values must be scalar or vector shaped",
2136                    builtin,
2137                ));
2138            }
2139            Ok(tensor_elements_to_values(t))
2140        }
2141        _ => Ok(vec![value.clone()]),
2142    }
2143}
2144
2145fn char_array_rows(ca: &CharArray, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
2146    if ca.rows == 0 {
2147        return Ok(Vec::new());
2148    }
2149    let mut out = Vec::with_capacity(ca.rows);
2150    for row in 0..ca.rows {
2151        let mut text = String::with_capacity(ca.cols);
2152        for col in 0..ca.cols {
2153            text.push(ca.data[row * ca.cols + col]);
2154        }
2155        let chars: Vec<char> = text.chars().collect();
2156        let array = CharArray::new(chars.clone(), 1, chars.len())
2157            .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
2158        out.push(Value::CharArray(array));
2159    }
2160    Ok(out)
2161}
2162
2163fn is_vector_shape(shape: &[usize]) -> bool {
2164    match shape.len() {
2165        0 => true,
2166        1 => true,
2167        2 => shape[0] == 1 || shape[1] == 1,
2168        _ => false,
2169    }
2170}
2171
2172fn tensor_elements_to_values(tensor: &Tensor) -> Vec<Value> {
2173    if let Some(storage) = tensor.integer_storage() {
2174        return storage.exact_values().into_iter().map(Value::Int).collect();
2175    }
2176    match tensor.numeric_dtype() {
2177        NumericDType::F64 => tensor
2178            .as_f64_slice()
2179            .expect("double tensor has native double storage")
2180            .iter()
2181            .copied()
2182            .map(Value::Num)
2183            .collect(),
2184        NumericDType::F32 => (0..tensor.len())
2185            .map(|index| {
2186                let value = match tensor.numeric_value_at(index) {
2187                    Some(runmat_value::NumericScalar::F32(value)) => value,
2188                    _ => unreachable!("single tensor has native single storage"),
2189                };
2190                Value::Tensor(
2191                    Tensor::from_f32(vec![value], vec![1, 1])
2192                        .expect("single scalar tensor construction"),
2193                )
2194            })
2195            .collect(),
2196        _ => unreachable!("integer tensor returned through exact key/value path"),
2197    }
2198}
2199
2200fn canonicalize_key(
2201    value: Value,
2202    key_type: KeyType,
2203    builtin: &'static str,
2204) -> BuiltinResult<(NormalizedKey, Value)> {
2205    let normalized = normalize_key(&value, key_type, builtin)?;
2206    let canonical = match key_type {
2207        KeyType::Char => Value::CharArray(char_array_from_value(&value, builtin)?),
2208        KeyType::Double => Value::Num(numeric_from_value(
2209            &value,
2210            "containers.Map: keys must be numeric scalars",
2211            builtin,
2212        )?),
2213        KeyType::Single => Value::Tensor(
2214            Tensor::from_f32(
2215                vec![numeric_from_value(
2216                    &value,
2217                    "containers.Map: keys must be numeric scalars",
2218                    builtin,
2219                )? as f32],
2220                vec![1, 1],
2221            )
2222            .map_err(|err| map_error(format!("containers.Map: {err}"), builtin))?,
2223        ),
2224        KeyType::Int32 => Value::Int(IntValue::I32(integer_from_value(
2225            &value,
2226            i32::MIN as i64,
2227            i32::MAX as i64,
2228            "containers.Map: int32 keys must be integers",
2229            builtin,
2230        )? as i32)),
2231        KeyType::UInt32 => Value::Int(IntValue::U32(unsigned_from_value(
2232            &value,
2233            u32::MAX as u64,
2234            "containers.Map: uint32 keys must be unsigned integers",
2235            builtin,
2236        )? as u32)),
2237        KeyType::Int64 => Value::Int(IntValue::I64(integer_from_value(
2238            &value,
2239            i64::MIN,
2240            i64::MAX,
2241            "containers.Map: int64 keys must be integers",
2242            builtin,
2243        )?)),
2244        KeyType::UInt64 => Value::Int(IntValue::U64(unsigned_from_value(
2245            &value,
2246            u64::MAX,
2247            "containers.Map: uint64 keys must be unsigned integers",
2248            builtin,
2249        )?)),
2250    };
2251    Ok((normalized, canonical))
2252}
2253
2254fn normalize_key(
2255    value: &Value,
2256    key_type: KeyType,
2257    builtin: &'static str,
2258) -> BuiltinResult<NormalizedKey> {
2259    if !value_matches_key_type(value, key_type) {
2260        return Err(map_error(
2261            format!(
2262                "containers.Map: key class must match the map KeyType '{}'",
2263                key_type.matlab_name()
2264            ),
2265            builtin,
2266        ));
2267    }
2268    match key_type {
2269        KeyType::Char => {
2270            let text =
2271                string_from_value(value, "containers.Map: keys must be text scalars", builtin)?;
2272            Ok(NormalizedKey::String(text))
2273        }
2274        KeyType::Double | KeyType::Single => {
2275            let numeric = numeric_from_value(
2276                value,
2277                "containers.Map: keys must be numeric scalars",
2278                builtin,
2279            )?;
2280            if !numeric.is_finite() {
2281                return Err(map_error(
2282                    "containers.Map: keys must be finite numeric scalars",
2283                    builtin,
2284                ));
2285            }
2286            let numeric = if key_type == KeyType::Single {
2287                f64::from(numeric as f32)
2288            } else {
2289                numeric
2290            };
2291            let canonical = if numeric == 0.0 { 0.0 } else { numeric };
2292            Ok(NormalizedKey::Float(canonical.to_bits()))
2293        }
2294        KeyType::Int32 | KeyType::Int64 => {
2295            let bounds = if key_type == KeyType::Int32 {
2296                (i32::MIN as i64, i32::MAX as i64)
2297            } else {
2298                (i64::MIN, i64::MAX)
2299            };
2300            let value = integer_from_value(
2301                value,
2302                bounds.0,
2303                bounds.1,
2304                "containers.Map: integer keys must be whole numbers",
2305                builtin,
2306            )?;
2307            Ok(NormalizedKey::Int(value))
2308        }
2309        KeyType::UInt32 | KeyType::UInt64 => {
2310            let limit = if key_type == KeyType::UInt32 {
2311                u32::MAX as u64
2312            } else {
2313                u64::MAX
2314            };
2315            let value = unsigned_from_value(
2316                value,
2317                limit,
2318                "containers.Map: unsigned keys must be non-negative integers",
2319                builtin,
2320            )?;
2321            Ok(NormalizedKey::UInt(value))
2322        }
2323    }
2324}
2325
2326fn value_matches_key_type(value: &Value, key_type: KeyType) -> bool {
2327    match key_type {
2328        KeyType::Char => {
2329            matches!(
2330                value,
2331                Value::CharArray(chars) if chars.rows == 1
2332            ) || matches!(value, Value::String(_))
2333                || matches!(value, Value::StringArray(strings) if strings.data.len() == 1)
2334        }
2335        KeyType::Double => {
2336            matches!(value, Value::Num(_))
2337                || matches!(value, Value::Tensor(tensor) if tensor.len() == 1 && tensor.numeric_dtype() == NumericDType::F64)
2338        }
2339        KeyType::Single => {
2340            matches!(value, Value::Tensor(tensor) if tensor.len() == 1 && tensor.numeric_dtype() == NumericDType::F32)
2341        }
2342        KeyType::Int32 => {
2343            matches!(value, Value::Int(IntValue::I32(_)))
2344                || matches!(value, Value::Tensor(tensor) if tensor.len() == 1 && tensor.numeric_dtype() == NumericDType::I32)
2345        }
2346        KeyType::UInt32 => {
2347            matches!(value, Value::Int(IntValue::U32(_)))
2348                || matches!(value, Value::Tensor(tensor) if tensor.len() == 1 && tensor.numeric_dtype() == NumericDType::U32)
2349        }
2350        KeyType::Int64 => {
2351            matches!(value, Value::Int(IntValue::I64(_)))
2352                || matches!(value, Value::Tensor(tensor) if tensor.len() == 1 && tensor.numeric_dtype() == NumericDType::I64)
2353        }
2354        KeyType::UInt64 => {
2355            matches!(value, Value::Int(IntValue::U64(_)))
2356                || matches!(value, Value::Tensor(tensor) if tensor.len() == 1 && tensor.numeric_dtype() == NumericDType::U64)
2357        }
2358    }
2359}
2360
2361fn string_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<String> {
2362    match value {
2363        Value::String(s) => Ok(s.clone()),
2364        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
2365        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
2366        _ => Err(map_error(context, builtin)),
2367    }
2368}
2369
2370fn char_array_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<CharArray> {
2371    match value {
2372        Value::CharArray(ca) if ca.rows == 1 => Ok(ca.clone()),
2373        Value::String(s) => {
2374            let chars: Vec<char> = s.chars().collect();
2375            CharArray::new(chars.clone(), 1, chars.len())
2376                .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
2377        }
2378        Value::StringArray(sa) if sa.data.len() == 1 => {
2379            let chars: Vec<char> = sa.data[0].chars().collect();
2380            CharArray::new(chars.clone(), 1, chars.len())
2381                .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
2382        }
2383        _ => Err(map_error(
2384            "containers.Map: keys must be character vectors",
2385            builtin,
2386        )),
2387    }
2388}
2389
2390fn char_array_value(text: &str, builtin: &'static str) -> BuiltinResult<Value> {
2391    let chars: Vec<char> = text.chars().collect();
2392    CharArray::new(chars.clone(), 1, chars.len())
2393        .map(Value::CharArray)
2394        .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
2395}
2396
2397fn normalize_numeric_value(
2398    value: Value,
2399    dtype: NumericDType,
2400    builtin: &'static str,
2401) -> BuiltinResult<Value> {
2402    match value {
2403        Value::Num(value) if dtype == NumericDType::F64 => Ok(Value::Num(value)),
2404        Value::Num(value) => Tensor::from_f32(vec![value as f32], vec![1, 1])
2405            .map(Value::Tensor)
2406            .map_err(|err| map_error(format!("containers.Map: {err}"), builtin)),
2407        Value::Tensor(tensor) if tensor.len() == 1 => {
2408            Ok(Value::Tensor(tensor::coerce_tensor_dtype(tensor, dtype)))
2409        }
2410        Value::Int(value) => normalize_numeric_value(Value::Num(value.to_f64()), dtype, builtin),
2411        Value::Bool(value) => {
2412            normalize_numeric_value(Value::Num(if value { 1.0 } else { 0.0 }), dtype, builtin)
2413        }
2414        Value::LogicalArray(arr) if arr.data.len() == 1 => {
2415            let data: Vec<f64> = arr
2416                .data
2417                .iter()
2418                .map(|&b| if b != 0 { 1.0 } else { 0.0 })
2419                .collect();
2420            let tensor = Tensor::new(data, arr.shape.clone())
2421                .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
2422            Ok(Value::Tensor(tensor::coerce_tensor_dtype(tensor, dtype)))
2423        }
2424        Value::Cell(_)
2425        | Value::SparseTensor(_)
2426        | Value::Struct(_)
2427        | Value::ObjectArray(_)
2428        | Value::Object(_)
2429        | Value::HandleObject(_)
2430        | Value::Listener(_)
2431        | Value::String(_)
2432        | Value::StringArray(_)
2433        | Value::CharArray(_)
2434        | Value::Complex(_, _)
2435        | Value::ComplexTensor(_)
2436        | Value::Symbolic(_)
2437        | Value::SymbolicArray(_)
2438        | Value::FunctionHandle(_)
2439        | Value::ExternalFunctionHandle(_)
2440        | Value::MethodFunctionHandle(_)
2441        | Value::BoundFunctionHandle { .. }
2442        | Value::Closure(_)
2443        | Value::ClassRef(_)
2444        | Value::MException(_)
2445        | Value::Future(_)
2446        | Value::Task(_)
2447        | Value::Pool(_)
2448        | Value::Job(_)
2449        | Value::Foreign(_)
2450        | Value::GpuTensor(_)
2451        | Value::OutputList(_)
2452        | Value::Tensor(_)
2453        | Value::LogicalArray(_) => Err(map_error(
2454            "containers.Map: values must be numeric scalars when ValueType is 'double' or 'single'",
2455            builtin,
2456        )),
2457    }
2458}
2459
2460fn normalize_logical_value(value: Value, builtin: &'static str) -> BuiltinResult<Value> {
2461    match value {
2462        Value::Bool(_) => Ok(value),
2463        Value::LogicalArray(ref array) if array.data.len() == 1 => Ok(value),
2464        Value::Int(i) => Ok(Value::Bool(!i.is_zero())),
2465        Value::Num(n) => Ok(Value::Bool(n != 0.0)),
2466        Value::Tensor(t) if t.len() == 1 => {
2467            let flags: Vec<u8> = if let Some(storage) = t.integer_storage() {
2468                storage
2469                    .exact_values()
2470                    .into_iter()
2471                    .map(|value| if value.is_zero() { 0 } else { 1 })
2472                    .collect()
2473            } else {
2474                (0..t.len())
2475                    .map(|index| {
2476                        if tensor::tensor_value_f64(&t, index) != 0.0 {
2477                            1
2478                        } else {
2479                            0
2480                        }
2481                    })
2482                    .collect()
2483            };
2484            let logical = LogicalArray::new(flags, t.shape.clone())
2485                .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
2486            Ok(Value::LogicalArray(logical))
2487        }
2488        Value::CharArray(_)
2489        | Value::SparseTensor(_)
2490        | Value::String(_)
2491        | Value::StringArray(_)
2492        | Value::Struct(_)
2493        | Value::Cell(_)
2494        | Value::ObjectArray(_)
2495        | Value::Object(_)
2496        | Value::HandleObject(_)
2497        | Value::Listener(_)
2498        | Value::Complex(_, _)
2499        | Value::ComplexTensor(_)
2500        | Value::Symbolic(_)
2501        | Value::SymbolicArray(_)
2502        | Value::FunctionHandle(_)
2503        | Value::ExternalFunctionHandle(_)
2504        | Value::MethodFunctionHandle(_)
2505        | Value::BoundFunctionHandle { .. }
2506        | Value::Closure(_)
2507        | Value::ClassRef(_)
2508        | Value::MException(_)
2509        | Value::Future(_)
2510        | Value::Task(_)
2511        | Value::Pool(_)
2512        | Value::Job(_)
2513        | Value::Foreign(_)
2514        | Value::GpuTensor(_)
2515        | Value::OutputList(_)
2516        | Value::Tensor(_)
2517        | Value::LogicalArray(_) => Err(map_error(
2518            "containers.Map: values must be logical scalars when ValueType is 'logical'",
2519            builtin,
2520        )),
2521    }
2522}
2523
2524fn integer_dtype_for_value_type(value_type: ValueType) -> Option<NumericDType> {
2525    match value_type {
2526        ValueType::Int8 => Some(NumericDType::I8),
2527        ValueType::UInt8 => Some(NumericDType::U8),
2528        ValueType::Int16 => Some(NumericDType::I16),
2529        ValueType::UInt16 => Some(NumericDType::U16),
2530        ValueType::Int32 => Some(NumericDType::I32),
2531        ValueType::UInt32 => Some(NumericDType::U32),
2532        ValueType::Int64 => Some(NumericDType::I64),
2533        ValueType::UInt64 => Some(NumericDType::U64),
2534        _ => None,
2535    }
2536}
2537
2538fn normalize_integer_value(
2539    value: Value,
2540    value_type: ValueType,
2541    builtin: &'static str,
2542) -> BuiltinResult<Value> {
2543    let dtype = integer_dtype_for_value_type(value_type)
2544        .ok_or_else(|| map_internal("containers.Map: invalid integer ValueType", builtin))?;
2545    let (tensor, scalar) = match value {
2546        Value::Tensor(tensor) if tensor.len() == 1 => (tensor, true),
2547        Value::Int(value) => (
2548            Tensor::new_integer(integer_storage_from_scalar(value), vec![1, 1])
2549                .map_err(|err| map_error(format!("containers.Map: {err}"), builtin))?,
2550            true,
2551        ),
2552        Value::Num(value) => (
2553            Tensor::new(vec![value], vec![1, 1])
2554                .map_err(|err| map_error(format!("containers.Map: {err}"), builtin))?,
2555            true,
2556        ),
2557        Value::Bool(value) => (
2558            Tensor::new(vec![if value { 1.0 } else { 0.0 }], vec![1, 1])
2559                .map_err(|err| map_error(format!("containers.Map: {err}"), builtin))?,
2560            true,
2561        ),
2562        Value::LogicalArray(array) if array.data.len() == 1 => (
2563            Tensor::new(
2564                array
2565                    .data
2566                    .iter()
2567                    .map(|value| f64::from(*value != 0))
2568                    .collect(),
2569                array.shape,
2570            )
2571            .map_err(|err| map_error(format!("containers.Map: {err}"), builtin))?,
2572            true,
2573        ),
2574        _ => {
2575            return Err(map_error(
2576                "containers.Map: value cannot be converted to the declared integer ValueType",
2577                builtin,
2578            ))
2579        }
2580    };
2581    let converted = tensor::coerce_tensor_dtype(tensor, dtype);
2582    if scalar {
2583        let value = converted
2584            .integer_storage()
2585            .and_then(|storage| storage.value_at(0))
2586            .ok_or_else(|| {
2587                map_internal("containers.Map: integer scalar conversion failed", builtin)
2588            })?;
2589        Ok(Value::Int(value))
2590    } else {
2591        Ok(Value::Tensor(converted))
2592    }
2593}
2594
2595fn integer_storage_from_scalar(value: IntValue) -> IntegerStorage {
2596    match value {
2597        IntValue::I8(value) => IntegerStorage::I8(vec![value]),
2598        IntValue::I16(value) => IntegerStorage::I16(vec![value]),
2599        IntValue::I32(value) => IntegerStorage::I32(vec![value]),
2600        IntValue::I64(value) => IntegerStorage::I64(vec![value]),
2601        IntValue::U8(value) => IntegerStorage::U8(vec![value]),
2602        IntValue::U16(value) => IntegerStorage::U16(vec![value]),
2603        IntValue::U32(value) => IntegerStorage::U32(vec![value]),
2604        IntValue::U64(value) => IntegerStorage::U64(vec![value]),
2605    }
2606}
2607
2608fn numeric_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<f64> {
2609    match value {
2610        Value::Num(n) => Ok(*n),
2611        Value::Int(i) => Ok(i.to_f64()),
2612        Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
2613        Value::Tensor(t) if tensor::is_scalar_tensor(t) => Ok(tensor::tensor_value_f64(t, 0)),
2614        Value::LogicalArray(arr) if arr.data.len() == 1 => {
2615            Ok(if arr.data[0] != 0 { 1.0 } else { 0.0 })
2616        }
2617        _ => Err(map_error(context, builtin)),
2618    }
2619}
2620
2621fn integer_from_value(
2622    value: &Value,
2623    min: i64,
2624    max: i64,
2625    context: &str,
2626    builtin: &'static str,
2627) -> BuiltinResult<i64> {
2628    match value {
2629        Value::Int(i) => {
2630            let Some(v) = i.try_to_i64() else {
2631                return Err(map_error(context, builtin));
2632            };
2633            if v < min || v > max {
2634                return Err(map_error(context, builtin));
2635            }
2636            Ok(v)
2637        }
2638        Value::Tensor(t) if tensor::is_scalar_tensor(t) => {
2639            if let Some(storage) = t.integer_storage() {
2640                let Some(v) = storage.value_at(0).and_then(|value| value.try_to_i64()) else {
2641                    return Err(map_error(context, builtin));
2642                };
2643                if v < min || v > max {
2644                    return Err(map_error(context, builtin));
2645                }
2646                Ok(v)
2647            } else {
2648                integer_from_value(
2649                    &Value::Num(tensor::tensor_value_f64(t, 0)),
2650                    min,
2651                    max,
2652                    context,
2653                    builtin,
2654                )
2655            }
2656        }
2657        Value::Num(n) => {
2658            if !n.is_finite() {
2659                return Err(map_error(context, builtin));
2660            }
2661            if (*n < min as f64) || (*n > max as f64) {
2662                return Err(map_error(context, builtin));
2663            }
2664            if (n.round() - n).abs() > f64::EPSILON {
2665                return Err(map_error(context, builtin));
2666            }
2667            Ok(n.round() as i64)
2668        }
2669        Value::Bool(b) => {
2670            let v = if *b { 1 } else { 0 };
2671            if v < min || v > max {
2672                return Err(map_error(context, builtin));
2673            }
2674            Ok(v)
2675        }
2676        _ => Err(map_error(context, builtin)),
2677    }
2678}
2679
2680fn unsigned_from_value(
2681    value: &Value,
2682    max: u64,
2683    context: &str,
2684    builtin: &'static str,
2685) -> BuiltinResult<u64> {
2686    match value {
2687        Value::Int(i) => {
2688            let Some(v) = i.try_to_u64() else {
2689                return Err(map_error(context, builtin));
2690            };
2691            if v > max {
2692                return Err(map_error(context, builtin));
2693            }
2694            Ok(v)
2695        }
2696        Value::Tensor(t) if tensor::is_scalar_tensor(t) => {
2697            if let Some(storage) = t.integer_storage() {
2698                let Some(v) = storage.value_at(0).and_then(|value| value.try_to_u64()) else {
2699                    return Err(map_error(context, builtin));
2700                };
2701                if v > max {
2702                    return Err(map_error(context, builtin));
2703                }
2704                Ok(v)
2705            } else {
2706                unsigned_from_value(
2707                    &Value::Num(tensor::tensor_value_f64(t, 0)),
2708                    max,
2709                    context,
2710                    builtin,
2711                )
2712            }
2713        }
2714        Value::Num(n) => {
2715            if !n.is_finite() || *n < 0.0 || *n > max as f64 {
2716                return Err(map_error(context, builtin));
2717            }
2718            if (n.round() - n).abs() > f64::EPSILON {
2719                return Err(map_error(context, builtin));
2720            }
2721            Ok(n.round() as u64)
2722        }
2723        Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
2724        _ => Err(map_error(context, builtin)),
2725    }
2726}
2727
2728fn bool_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<bool> {
2729    match value {
2730        Value::Bool(b) => Ok(*b),
2731        Value::LogicalArray(arr) if arr.data.len() == 1 => Ok(arr.data[0] != 0),
2732        Value::Int(i) => Ok(!i.is_zero()),
2733        Value::Num(n) => Ok(*n != 0.0),
2734        Value::Tensor(t) if tensor::is_scalar_tensor(t) => {
2735            if let Some(storage) = t.integer_storage() {
2736                Ok(!storage
2737                    .value_at(0)
2738                    .ok_or_else(|| map_error(context, builtin))?
2739                    .is_zero())
2740            } else {
2741                Ok(tensor::tensor_value_f64(t, 0) != 0.0)
2742            }
2743        }
2744        _ => Err(map_error(context, builtin)),
2745    }
2746}
2747
2748fn make_row_cell(values: Vec<Value>, builtin: &'static str) -> BuiltinResult<Value> {
2749    let cols = values.len();
2750    crate::make_cell_with_shape(values, vec![1, cols])
2751        .map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
2752}
2753
2754fn extract_key_arguments(payload: &Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
2755    match payload {
2756        Value::Cell(cell) => {
2757            let mut out = Vec::with_capacity(cell.data.len());
2758            for ptr in &cell.data {
2759                out.push(ptr.clone());
2760            }
2761            Ok(out)
2762        }
2763        other => Err(map_error(
2764            format!("containers.Map: expected key arguments in a cell array, got {other:?}"),
2765            builtin,
2766        )),
2767    }
2768}
2769
2770async fn expand_assignment_values(
2771    value: Value,
2772    expected: usize,
2773    builtin: &'static str,
2774) -> BuiltinResult<Vec<Value>> {
2775    let host = gather_if_needed_async(&value)
2776        .await
2777        .map_err(|err| attach_builtin_context(err, builtin))?;
2778    if expected == 1 {
2779        Ok(vec![host])
2780    } else {
2781        let values = flatten_values(&host, builtin).await?;
2782        if values.len() != expected {
2783            return Err(map_error(
2784                format!(
2785                    "containers.Map: assignment with {} keys requires {} values (got {})",
2786                    expected,
2787                    expected,
2788                    values.len()
2789                ),
2790                builtin,
2791            ));
2792        }
2793        Ok(values)
2794    }
2795}
2796
2797struct KeyCollection {
2798    values: Vec<Value>,
2799    shape: Vec<usize>,
2800}
2801
2802async fn collect_key_spec(
2803    value: &Value,
2804    key_type: KeyType,
2805    builtin: &'static str,
2806) -> BuiltinResult<KeyCollection> {
2807    let host = gather_if_needed_async(value)
2808        .await
2809        .map_err(|err| attach_builtin_context(err, builtin))?;
2810    match &host {
2811        Value::Cell(cell) => {
2812            let mut values = Vec::with_capacity(cell.data.len());
2813            for ptr in &cell.data {
2814                values.push(
2815                    gather_if_needed_async(ptr)
2816                        .await
2817                        .map_err(|err| attach_builtin_context(err, builtin))?,
2818                );
2819            }
2820            Ok(KeyCollection {
2821                values,
2822                shape: vec![cell.rows, cell.cols],
2823            })
2824        }
2825        Value::StringArray(sa) if sa.data.len() == 1 => Ok(KeyCollection {
2826            values: vec![Value::String(sa.data[0].clone())],
2827            shape: vec![1, 1],
2828        }),
2829        Value::CharArray(ca) if ca.rows == 1 => Ok(KeyCollection {
2830            values: vec![Value::CharArray(ca.clone())],
2831            shape: vec![1, 1],
2832        }),
2833        Value::Tensor(t) if key_type != KeyType::Char && t.len() == 1 => Ok(KeyCollection {
2834            values: tensor_elements_to_values(t),
2835            shape: vec![1, 1],
2836        }),
2837        Value::StringArray(_) | Value::CharArray(_) | Value::Tensor(_) => Err(map_error(
2838            "containers.Map: multiple keys must be supplied in a cell array",
2839            builtin,
2840        )),
2841        _ => Ok(KeyCollection {
2842            values: vec![host.clone()],
2843            shape: vec![1, 1],
2844        }),
2845    }
2846}
2847
2848pub fn map_length(value: &Value) -> Option<usize> {
2849    if let Value::HandleObject(handle) = value {
2850        if crate::is_handle_valid(handle) && handle.class_name == CLASS_NAME {
2851            if let Ok(id) = map_id(handle, BUILTIN_CONSTRUCTOR) {
2852                return MAP_REGISTRY.with(|registry| {
2853                    registry
2854                        .try_borrow()
2855                        .ok()
2856                        .and_then(|registry| registry.get(&id).map(|store| store.len()))
2857                });
2858            }
2859        }
2860    }
2861    None
2862}
2863
2864#[cfg(test)]
2865pub(crate) mod tests {
2866    use super::*;
2867    use crate::builtins::common::{gpu_helpers, test_support};
2868    use futures::executor::block_on;
2869    use runmat_builtins::{ResolveContext, Type};
2870    use runmat_value::IntegerStorage;
2871
2872    fn error_message(err: crate::RuntimeError) -> String {
2873        err.message.clone()
2874    }
2875
2876    fn containers_map_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
2877        block_on(super::containers_map_builtin(args))
2878    }
2879
2880    fn containers_map_keys(map: Value) -> BuiltinResult<Value> {
2881        block_on(super::containers_map_keys(map))
2882    }
2883
2884    fn containers_map_values(map: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
2885        block_on(super::containers_map_values(map, rest))
2886    }
2887
2888    fn containers_map_is_key(map: Value, key_spec: Value) -> BuiltinResult<Value> {
2889        block_on(super::containers_map_is_key(map, key_spec))
2890    }
2891
2892    fn containers_map_remove(map: Value, key_spec: Value) -> BuiltinResult<Value> {
2893        block_on(super::containers_map_remove(map, key_spec))
2894    }
2895
2896    fn containers_map_subsref(map: Value, kind: String, payload: Value) -> BuiltinResult<Value> {
2897        block_on(super::containers_map_subsref(map, kind, payload))
2898    }
2899
2900    fn containers_map_subsasgn(
2901        map: Value,
2902        kind: String,
2903        payload: Value,
2904        rhs: Value,
2905    ) -> BuiltinResult<Value> {
2906        block_on(super::containers_map_subsasgn(map, kind, payload, rhs))
2907    }
2908
2909    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2910    #[test]
2911    fn construct_empty_map_defaults() {
2912        let map = containers_map_builtin(Vec::new()).expect("map");
2913        let count = containers_map_subsref(
2914            map.clone(),
2915            ".".to_string(),
2916            Value::from("Count".to_string()),
2917        )
2918        .expect("Count");
2919        assert_eq!(count, Value::Int(IntValue::U64(0)));
2920
2921        let key_type = containers_map_subsref(
2922            map.clone(),
2923            ".".to_string(),
2924            Value::from("KeyType".to_string()),
2925        )
2926        .expect("KeyType");
2927        assert_eq!(
2928            key_type,
2929            Value::CharArray(CharArray::new("char".chars().collect(), 1, 4).unwrap())
2930        );
2931
2932        let value_type = containers_map_subsref(
2933            map.clone(),
2934            ".".to_string(),
2935            Value::from("ValueType".to_string()),
2936        )
2937        .expect("ValueType");
2938        assert_eq!(
2939            value_type,
2940            Value::CharArray(CharArray::new("any".chars().collect(), 1, 3).unwrap())
2941        );
2942    }
2943
2944    #[test]
2945    fn map_type_resolvers_basics() {
2946        let ctx = ResolveContext::new(Vec::new());
2947        assert_eq!(map_handle_type(&[Type::Unknown], &ctx), Type::Unknown);
2948        assert_eq!(map_cell_type(&[], &ctx), Type::cell());
2949        assert_eq!(map_is_key_type(&[Type::String], &ctx), Type::logical());
2950        assert_eq!(map_unknown_type(&[], &ctx), Type::Unknown);
2951    }
2952
2953    #[test]
2954    fn containers_map_descriptor_includes_constructor_and_method_signatures() {
2955        let constructor_labels: Vec<&str> = CONTAINERS_MAP_DESCRIPTOR
2956            .signatures
2957            .iter()
2958            .map(|sig| sig.label)
2959            .collect();
2960        assert!(constructor_labels.contains(&"M = containers.Map()"));
2961        assert!(constructor_labels.contains(&"M = containers.Map(keys, values)"));
2962
2963        let method_labels: Vec<&str> = CONTAINERS_MAP_SUBSREF_DESCRIPTOR
2964            .signatures
2965            .iter()
2966            .map(|sig| sig.label)
2967            .collect();
2968        assert!(method_labels.contains(&"value = containers.Map.subsref(M, kind, payload)"));
2969    }
2970
2971    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2972    #[test]
2973    fn constructor_with_cells_lookup() {
2974        let keys = crate::make_cell(vec![Value::from("apple"), Value::from("pear")], 1, 2).unwrap();
2975        let values = crate::make_cell(vec![Value::Num(5.0), Value::Num(7.0)], 1, 2).unwrap();
2976        let map = containers_map_builtin(vec![keys, values]).expect("map");
2977        let apple = containers_map_subsref(
2978            map.clone(),
2979            "()".to_string(),
2980            crate::make_cell(vec![Value::from("apple")], 1, 1).unwrap(),
2981        )
2982        .expect("lookup");
2983        assert_eq!(apple, Value::Num(5.0));
2984    }
2985
2986    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2987    #[test]
2988    fn constructor_rejects_duplicate_keys() {
2989        let keys = crate::make_cell(vec![Value::from("dup"), Value::from("dup")], 1, 2).unwrap();
2990        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
2991        let err = containers_map_builtin(vec![keys, values]).expect_err("duplicate check");
2992        let message = error_message(err);
2993        assert!(message.contains("Duplicate key name"));
2994    }
2995
2996    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2997    #[test]
2998    fn constructor_errors_when_value_count_mismatch() {
2999        let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
3000        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
3001        let err = containers_map_builtin(vec![keys, values]).expect_err("count mismatch");
3002        let message = error_message(err);
3003        assert!(message.contains("number of keys"));
3004    }
3005
3006    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3007    #[test]
3008    fn comparison_method_rejects_unknown_values() {
3009        let keys = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
3010        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
3011        let err = containers_map_builtin(vec![
3012            keys,
3013            values,
3014            Value::from("ComparisonMethod"),
3015            Value::from("caseinsensitive"),
3016        ])
3017        .expect_err("comparison method");
3018        let message = error_message(err);
3019        assert!(message.contains("unrecognised option"));
3020    }
3021
3022    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3023    #[test]
3024    fn key_type_single_roundtrip() {
3025        let map = containers_map_builtin(vec![
3026            Value::from("KeyType"),
3027            Value::from("single"),
3028            Value::from("ValueType"),
3029            Value::from("any"),
3030        ])
3031        .expect("map");
3032        let key_type = containers_map_subsref(map.clone(), ".".to_string(), Value::from("KeyType"))
3033            .expect("keytype");
3034        assert_eq!(
3035            key_type,
3036            Value::CharArray(CharArray::new("single".chars().collect(), 1, 6).unwrap())
3037        );
3038
3039        let single_key = Value::Tensor(Tensor::from_f32(vec![1.0], vec![1, 1]).unwrap());
3040        let payload = crate::make_cell(vec![single_key], 1, 1).unwrap();
3041        let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(7.0))
3042            .expect("assign");
3043        let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
3044        assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
3045    }
3046
3047    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3048    #[test]
3049    fn value_type_double_converts_integers() {
3050        let map = containers_map_builtin(vec![
3051            Value::from("KeyType"),
3052            Value::from("double"),
3053            Value::from("ValueType"),
3054            Value::from("double"),
3055        ])
3056        .expect("map");
3057        let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
3058        let map = containers_map_subsasgn(
3059            map,
3060            "()".to_string(),
3061            payload.clone(),
3062            Value::Int(IntValue::I32(7)),
3063        )
3064        .expect("assign");
3065        let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
3066        assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
3067    }
3068
3069    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3070    #[test]
3071    fn value_type_logical_rejects_nonscalar_numeric_arrays() {
3072        let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
3073        let map = containers_map_builtin(vec![
3074            Value::from("KeyType"),
3075            Value::from("char"),
3076            Value::from("ValueType"),
3077            Value::from("logical"),
3078        ])
3079        .expect("map");
3080        let payload = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
3081        let error = containers_map_subsasgn(map, "()".to_string(), payload, Value::Tensor(tensor))
3082            .expect_err("declared logical values must be scalar");
3083        assert!(error.to_string().contains("logical scalars"));
3084    }
3085
3086    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3087    #[test]
3088    fn uniform_values_enforced_on_assignment() {
3089        let map = containers_map_builtin(vec![
3090            crate::make_cell(vec![Value::from("x")], 1, 1).unwrap(),
3091            crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap(),
3092        ])
3093        .expect("map");
3094        let payload = crate::make_cell(vec![Value::from("x")], 1, 1).unwrap();
3095        let err = containers_map_subsasgn(map, "()".to_string(), payload, Value::from("text"))
3096            .expect_err("uniform enforcement");
3097        let message = error_message(err);
3098        assert!(message.contains("numeric scalar"));
3099    }
3100
3101    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3102    #[test]
3103    fn assignment_updates_and_inserts() {
3104        let map = containers_map_builtin(Vec::new()).expect("map");
3105        let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
3106        let updated = containers_map_subsasgn(
3107            map.clone(),
3108            "()".to_string(),
3109            payload.clone(),
3110            Value::Num(1.0),
3111        )
3112        .expect("assign");
3113        let updated = containers_map_subsasgn(
3114            updated.clone(),
3115            "()".to_string(),
3116            payload.clone(),
3117            Value::Num(5.0),
3118        )
3119        .expect("update");
3120        let beta_payload = crate::make_cell(vec![Value::from("beta")], 1, 1).unwrap();
3121        let updated = containers_map_subsasgn(
3122            updated.clone(),
3123            "()".to_string(),
3124            beta_payload,
3125            Value::Num(9.0),
3126        )
3127        .expect("insert");
3128        let value = containers_map_subsref(updated, "()".to_string(), payload).expect("lookup");
3129        assert_eq!(value, Value::Num(5.0));
3130    }
3131
3132    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3133    #[test]
3134    fn subsref_rejects_multiple_keys() {
3135        let keys = crate::make_cell(
3136            vec![Value::from("a"), Value::from("b"), Value::from("c")],
3137            1,
3138            3,
3139        )
3140        .unwrap();
3141        let values = crate::make_cell(
3142            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
3143            1,
3144            3,
3145        )
3146        .unwrap();
3147        let map = containers_map_builtin(vec![keys, values]).expect("map");
3148        let request = crate::make_cell(vec![Value::from("a"), Value::from("c")], 1, 2).unwrap();
3149        let payload = crate::make_cell(vec![request], 1, 1).unwrap();
3150        let error = containers_map_subsref(map, "()".to_string(), payload).unwrap_err();
3151        assert!(error.to_string().contains("exactly one scalar key"));
3152    }
3153
3154    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3155    #[test]
3156    fn subsref_rejects_empty_key_collection() {
3157        let keys = crate::make_cell(vec![Value::from("z")], 1, 1).unwrap();
3158        let values = crate::make_cell(vec![Value::Num(42.0)], 1, 1).unwrap();
3159        let map = containers_map_builtin(vec![keys, values]).expect("map");
3160        let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
3161        let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
3162        let error = containers_map_subsref(map, "()".to_string(), payload).unwrap_err();
3163        assert!(error.to_string().contains("exactly one scalar key"));
3164    }
3165
3166    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3167    #[test]
3168    fn subsasgn_rejects_multiple_keys() {
3169        let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
3170        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
3171        let map = containers_map_builtin(vec![keys, values]).expect("map");
3172        let key_spec = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
3173        let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
3174        let new_values = crate::make_cell(vec![Value::Num(10.0), Value::Num(20.0)], 1, 2).unwrap();
3175        let error =
3176            containers_map_subsasgn(map, "()".to_string(), payload, new_values).unwrap_err();
3177        assert!(error.to_string().contains("exactly one scalar key"));
3178    }
3179
3180    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3181    #[test]
3182    fn assignment_value_count_mismatch_errors() {
3183        let keys = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
3184        let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
3185        let map = containers_map_builtin(vec![keys, values]).expect("map");
3186        let key_spec = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
3187        let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
3188        let rhs = crate::make_cell(vec![Value::Num(99.0)], 1, 1).unwrap();
3189        let err =
3190            containers_map_subsasgn(map, "()".to_string(), payload, rhs).expect_err("value count");
3191        let message = error_message(err);
3192        assert!(message.contains("exactly one scalar key"));
3193    }
3194
3195    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3196    #[test]
3197    fn subsasgn_rejects_empty_key_collection() {
3198        let keys = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
3199        let values = crate::make_cell(vec![Value::Num(7.0)], 1, 1).unwrap();
3200        let map = containers_map_builtin(vec![keys, values]).expect("map");
3201        let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
3202        let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
3203        let rhs = crate::make_cell(Vec::new(), 1, 0).unwrap();
3204        let error = containers_map_subsasgn(map, "()".to_string(), payload, rhs).unwrap_err();
3205        assert!(error.to_string().contains("exactly one scalar key"));
3206    }
3207
3208    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3209    #[test]
3210    fn keys_values_iskey_remove() {
3211        let keys = crate::make_cell(
3212            vec![Value::from("a"), Value::from("b"), Value::from("c")],
3213            1,
3214            3,
3215        )
3216        .unwrap();
3217        let values = crate::make_cell(
3218            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
3219            1,
3220            3,
3221        )
3222        .unwrap();
3223        let map = containers_map_builtin(vec![keys, values]).expect("map");
3224        let key_list = containers_map_keys(map.clone()).expect("keys");
3225        match key_list {
3226            Value::Cell(cell) => assert_eq!(cell.data.len(), 3),
3227            other => panic!("expected cell array, got {other:?}"),
3228        }
3229        let mask = containers_map_is_key(
3230            map.clone(),
3231            crate::make_cell(vec![Value::from("a"), Value::from("z")], 1, 2).unwrap(),
3232        )
3233        .expect("mask");
3234        match mask {
3235            Value::LogicalArray(arr) => {
3236                assert_eq!(arr.data, vec![1, 0]);
3237            }
3238            other => panic!("expected logical array, got {:?}", other),
3239        }
3240        let removed = containers_map_remove(
3241            map.clone(),
3242            crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
3243        )
3244        .expect("remove");
3245        let mask = containers_map_is_key(
3246            removed,
3247            crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
3248        )
3249        .expect("mask");
3250        assert_eq!(mask, Value::Bool(false));
3251    }
3252
3253    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3254    #[test]
3255    fn remove_missing_key_returns_error() {
3256        let keys = crate::make_cell(vec![Value::from("key")], 1, 1).unwrap();
3257        let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
3258        let map = containers_map_builtin(vec![keys, values]).expect("map");
3259        let err = containers_map_remove(
3260            map,
3261            crate::make_cell(vec![Value::from("missing")], 1, 1).unwrap(),
3262        )
3263        .expect_err("remove missing");
3264        assert_eq!(
3265            err.identifier(),
3266            CONTAINERS_MAP_ERROR_MISSING_KEY.identifier
3267        );
3268        let message = error_message(err);
3269        assert_eq!(message, CONTAINERS_MAP_ERROR_MISSING_KEY.message);
3270    }
3271
3272    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3273    #[test]
3274    fn length_delegates_to_map_count() {
3275        let keys = crate::make_cell(
3276            vec![Value::from("a"), Value::from("b"), Value::from("c")],
3277            1,
3278            3,
3279        )
3280        .unwrap();
3281        let values = crate::make_cell(
3282            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
3283            1,
3284            3,
3285        )
3286        .unwrap();
3287        let map = containers_map_builtin(vec![keys, values]).expect("map");
3288        assert_eq!(map_length(&map), Some(3));
3289    }
3290
3291    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3292    #[test]
3293    fn map_id_rejects_corrupted_numeric_identifiers() {
3294        for id_value in [
3295            Value::Num(1.9),
3296            Value::Num(f64::INFINITY),
3297            Value::Num(u64::MAX as f64),
3298        ] {
3299            let mut storage = ObjectInstance::new(CLASS_NAME.to_string());
3300            storage.properties.insert("id".to_string(), id_value);
3301            let target = runmat_gc::gc_allocate(Value::Object(storage)).expect("storage");
3302            let handle = HandleRef {
3303                class_name: CLASS_NAME.to_string(),
3304                target,
3305                valid: true,
3306            };
3307
3308            let err =
3309                map_id(&handle, BUILTIN_CONSTRUCTOR).expect_err("corrupted map id should reject");
3310            assert_eq!(err.identifier(), CONTAINERS_MAP_ERROR_INTERNAL.identifier);
3311        }
3312    }
3313
3314    #[test]
3315    fn typed_map_keys_preserve_full_uint64_range() {
3316        assert_eq!(
3317            unsigned_from_value(
3318                &Value::Int(IntValue::U64(u64::MAX)),
3319                u64::MAX,
3320                "key",
3321                BUILTIN_CONSTRUCTOR,
3322            )
3323            .expect("uint64 key"),
3324            u64::MAX
3325        );
3326        assert!(integer_from_value(
3327            &Value::Int(IntValue::U64(u64::MAX)),
3328            i64::MIN,
3329            i64::MAX,
3330            "key",
3331            BUILTIN_CONSTRUCTOR,
3332        )
3333        .is_err());
3334    }
3335
3336    #[test]
3337    fn scalar_map_helpers_read_typed_integer_tensor_storage_exactly() {
3338        let u64_tensor = Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
3339            .expect("uint64 key");
3340        assert_eq!(
3341            unsigned_from_value(
3342                &Value::Tensor(u64_tensor),
3343                u64::MAX,
3344                "key",
3345                BUILTIN_CONSTRUCTOR,
3346            )
3347            .expect("uint64 key"),
3348            u64::MAX
3349        );
3350
3351        let i32_tensor =
3352            Tensor::new_integer(IntegerStorage::I32(vec![-7]), vec![1, 1]).expect("int32 key");
3353        assert_eq!(
3354            integer_from_value(
3355                &Value::Tensor(i32_tensor),
3356                i32::MIN as i64,
3357                i32::MAX as i64,
3358                "key",
3359                BUILTIN_CONSTRUCTOR,
3360            )
3361            .expect("int32 key"),
3362            -7
3363        );
3364
3365        let logical_tensor =
3366            Tensor::new_integer(IntegerStorage::U8(vec![1]), vec![1, 1]).expect("logical key");
3367        assert!(
3368            bool_from_value(&Value::Tensor(logical_tensor), "key", BUILTIN_CONSTRUCTOR,)
3369                .expect("logical key")
3370        );
3371    }
3372
3373    #[test]
3374    fn vector_map_helpers_preserve_native_single_and_logicalize_typed_values() {
3375        let single = Tensor::from_f32(vec![1.25, 2.5], vec![1, 2]).expect("single values");
3376        let values = tensor_elements_to_values(&single);
3377        assert_eq!(values.len(), 2);
3378        for (value, expected) in values.into_iter().zip([1.25_f32, 2.5]) {
3379            let Value::Tensor(tensor) = value else {
3380                panic!("expected native-single scalar tensor");
3381            };
3382            assert_eq!(tensor.numeric_dtype(), NumericDType::F32);
3383            assert_eq!(
3384                tensor.numeric_value_at(0),
3385                Some(runmat_value::NumericScalar::F32(expected))
3386            );
3387        }
3388
3389        let logical = normalize_logical_value(
3390            Value::Tensor(
3391                Tensor::from_f32(vec![0.0, -0.0, 2.0, f32::NAN], vec![2, 2])
3392                    .expect("single logical source"),
3393            ),
3394            BUILTIN_CONSTRUCTOR,
3395        );
3396        assert!(logical.is_err());
3397    }
3398
3399    #[test]
3400    fn vector_map_keys_and_values_read_typed_integer_storage_exactly() {
3401        let keys = Tensor::new_integer(
3402            IntegerStorage::U64(vec![u64::MAX - 1, u64::MAX]),
3403            vec![1, 2],
3404        )
3405        .expect("uint64 keys");
3406        let values =
3407            Tensor::new_integer(IntegerStorage::I32(vec![11, 22]), vec![1, 2]).expect("values");
3408
3409        let map =
3410            containers_map_builtin(vec![Value::Tensor(keys), Value::Tensor(values)]).expect("map");
3411
3412        let key_payload = crate::make_cell(
3413            vec![
3414                Value::Int(IntValue::U64(u64::MAX - 1)),
3415                Value::Int(IntValue::U64(u64::MAX)),
3416            ],
3417            1,
3418            2,
3419        )
3420        .unwrap();
3421        let result = containers_map_values(map, vec![key_payload]).expect("selected values");
3422        match result {
3423            Value::Cell(cell) => {
3424                assert_eq!((cell.rows, cell.cols), (1, 2));
3425                assert_eq!(cell.data[0], Value::Int(IntValue::I32(11)));
3426                assert_eq!(cell.data[1], Value::Int(IntValue::I32(22)));
3427            }
3428            other => panic!("expected cell lookup result, got {:?}", other),
3429        }
3430    }
3431
3432    #[test]
3433    fn value_type_logical_reads_typed_integer_tensor_storage_exactly() {
3434        let tensor = Tensor::new_integer(IntegerStorage::U64(vec![0, u64::MAX]), vec![1, 2])
3435            .expect("integer logical source");
3436        let map = containers_map_builtin(vec![
3437            Value::from("KeyType"),
3438            Value::from("char"),
3439            Value::from("ValueType"),
3440            Value::from("logical"),
3441        ])
3442        .expect("map");
3443        let payload = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
3444        let error = containers_map_subsasgn(map, "()".to_string(), payload, Value::Tensor(tensor))
3445            .expect_err("declared logical ValueType accepts scalar values only");
3446        assert!(error.to_string().contains("logical scalars"));
3447    }
3448
3449    fn all_integer_value_storages() -> Vec<(IntegerStorage, &'static str)> {
3450        vec![
3451            (IntegerStorage::I8(vec![i8::MIN, i8::MAX]), "int8"),
3452            (IntegerStorage::U8(vec![0, u8::MAX]), "uint8"),
3453            (IntegerStorage::I16(vec![i16::MIN, i16::MAX]), "int16"),
3454            (IntegerStorage::U16(vec![0, u16::MAX]), "uint16"),
3455            (IntegerStorage::I32(vec![i32::MIN, i32::MAX]), "int32"),
3456            (IntegerStorage::U32(vec![0, u32::MAX]), "uint32"),
3457            (IntegerStorage::I64(vec![i64::MIN, i64::MAX]), "int64"),
3458            (IntegerStorage::U64(vec![u64::MAX - 1, u64::MAX]), "uint64"),
3459        ]
3460    }
3461
3462    fn public_integer_key_storages() -> Vec<(IntegerStorage, &'static str)> {
3463        vec![
3464            (IntegerStorage::I32(vec![i32::MIN, i32::MAX]), "int32"),
3465            (IntegerStorage::U32(vec![0, u32::MAX]), "uint32"),
3466            (IntegerStorage::I64(vec![i64::MIN, i64::MAX]), "int64"),
3467            (IntegerStorage::U64(vec![u64::MAX - 1, u64::MAX]), "uint64"),
3468        ]
3469    }
3470
3471    fn property_text(map: Value, name: &str) -> String {
3472        let value = containers_map_subsref(map, ".".to_string(), Value::from(name)).unwrap();
3473        let Value::CharArray(chars) = value else {
3474            panic!("expected char property");
3475        };
3476        chars.data.iter().collect()
3477    }
3478
3479    #[test]
3480    fn constructor_subsref_and_values_preserve_all_integer_value_classes_exactly() {
3481        for (storage, class_name) in all_integer_value_storages() {
3482            let expected = storage.exact_values();
3483            let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
3484            let values = Value::Tensor(Tensor::new_integer(storage, vec![1, 2]).unwrap());
3485            let map = containers_map_builtin(vec![keys, values]).unwrap();
3486            assert_eq!(property_text(map.clone(), "ValueType"), class_name);
3487
3488            let payload = crate::make_cell(vec![Value::from("b")], 1, 1).unwrap();
3489            assert_eq!(
3490                containers_map_subsref(map.clone(), "()".to_string(), payload).unwrap(),
3491                Value::Int(expected[1].clone())
3492            );
3493
3494            let Value::Cell(all_values) = containers_map_values(map.clone(), Vec::new()).unwrap()
3495            else {
3496                panic!("expected values cell");
3497            };
3498            assert_eq!(
3499                all_values.data,
3500                vec![
3501                    Value::Int(expected[0].clone()),
3502                    Value::Int(expected[1].clone())
3503                ]
3504            );
3505
3506            let selected_keys =
3507                crate::make_cell(vec![Value::from("b"), Value::from("a")], 2, 1).unwrap();
3508            let Value::Cell(selected) =
3509                containers_map_values(map, vec![selected_keys]).expect("selected values")
3510            else {
3511                panic!("expected selected values cell");
3512            };
3513            assert_eq!((selected.rows, selected.cols), (2, 1));
3514            assert_eq!(
3515                selected.data,
3516                vec![
3517                    Value::Int(expected[1].clone()),
3518                    Value::Int(expected[0].clone())
3519                ]
3520            );
3521        }
3522    }
3523
3524    #[test]
3525    fn keys_iskey_and_remove_preserve_all_public_integer_key_classes() {
3526        for (storage, class_name) in public_integer_key_storages() {
3527            let expected = storage.exact_values();
3528            let key_tensor =
3529                Value::Tensor(Tensor::new_integer(storage.clone(), vec![1, 2]).unwrap());
3530            let value_tensor = Value::Tensor(Tensor::new(vec![10.0, 20.0], vec![1, 2]).unwrap());
3531            let map = containers_map_builtin(vec![key_tensor.clone(), value_tensor]).unwrap();
3532            assert_eq!(property_text(map.clone(), "KeyType"), class_name);
3533
3534            let Value::Cell(keys) = containers_map_keys(map.clone()).unwrap() else {
3535                panic!("expected keys cell");
3536            };
3537            assert_eq!(
3538                keys.data,
3539                vec![
3540                    Value::Int(expected[0].clone()),
3541                    Value::Int(expected[1].clone())
3542                ]
3543            );
3544
3545            let key_cell =
3546                crate::make_cell(expected.iter().cloned().map(Value::Int).collect(), 1, 2).unwrap();
3547            let Value::LogicalArray(found) =
3548                containers_map_is_key(map.clone(), key_cell).expect("isKey")
3549            else {
3550                panic!("expected logical array");
3551            };
3552            assert_eq!(found.shape, vec![1, 2]);
3553            assert_eq!(found.data, vec![1, 1]);
3554
3555            let removed = containers_map_remove(map, Value::Int(expected[1].clone())).unwrap();
3556            assert_eq!(
3557                containers_map_is_key(removed, Value::Int(expected[1].clone())).unwrap(),
3558                Value::Bool(false)
3559            );
3560        }
3561    }
3562
3563    #[test]
3564    fn integer_key_methods_reject_class_mismatch_and_binary64_aliases() {
3565        let wide = 9_007_199_254_740_993_u64;
3566        let keys = Value::Tensor(
3567            Tensor::new_integer(IntegerStorage::U64(vec![wide]), vec![1, 1]).unwrap(),
3568        );
3569        let map = containers_map_builtin(vec![keys, Value::Num(7.0)]).unwrap();
3570        let rounded_double = Value::Num(wide as f64);
3571        let payload = crate::make_cell(vec![rounded_double.clone()], 1, 1).unwrap();
3572        for error in [
3573            containers_map_subsref(map.clone(), "()".to_string(), payload.clone()).unwrap_err(),
3574            containers_map_is_key(map.clone(), rounded_double.clone()).unwrap_err(),
3575            containers_map_remove(map.clone(), rounded_double.clone()).unwrap_err(),
3576            containers_map_subsasgn(map.clone(), "()".to_string(), payload, Value::Num(9.0))
3577                .unwrap_err(),
3578        ] {
3579            assert!(error.to_string().contains("KeyType 'uint64'"));
3580        }
3581        let exact_payload = crate::make_cell(vec![Value::Int(IntValue::U64(wide))], 1, 1).unwrap();
3582        assert_eq!(
3583            containers_map_subsref(map, "()".to_string(), exact_payload).unwrap(),
3584            Value::Num(7.0)
3585        );
3586    }
3587
3588    #[test]
3589    fn direct_noncell_multi_key_method_inputs_reject() {
3590        let keys = Value::Tensor(
3591            Tensor::new_integer(IntegerStorage::I32(vec![1, 2]), vec![1, 2]).unwrap(),
3592        );
3593        let map = containers_map_builtin(vec![
3594            keys.clone(),
3595            Value::Tensor(Tensor::new(vec![10.0, 20.0], vec![1, 2]).unwrap()),
3596        ])
3597        .unwrap();
3598        let is_key_error = containers_map_is_key(map.clone(), keys.clone()).unwrap_err();
3599        assert!(is_key_error.to_string().contains("cell array"));
3600        let remove_error = containers_map_remove(map, keys).unwrap_err();
3601        assert!(remove_error.to_string().contains("cell array"));
3602    }
3603
3604    #[test]
3605    fn subsasgn_explicit_integer_value_types_cover_all_eight_classes() {
3606        for (storage, class_name) in all_integer_value_storages() {
3607            let expected = storage.exact_values()[1].clone();
3608            let map = containers_map_builtin(vec![
3609                Value::from("KeyType"),
3610                Value::from("uint64"),
3611                Value::from("ValueType"),
3612                Value::from(class_name),
3613            ])
3614            .unwrap();
3615            let payload =
3616                crate::make_cell(vec![Value::Int(IntValue::U64(u64::MAX))], 1, 1).unwrap();
3617            let updated = containers_map_subsasgn(
3618                map,
3619                "()".to_string(),
3620                payload.clone(),
3621                Value::Int(expected.clone()),
3622            )
3623            .unwrap();
3624            assert_eq!(
3625                containers_map_subsref(updated, "()".to_string(), payload).unwrap(),
3626                Value::Int(expected)
3627            );
3628        }
3629    }
3630
3631    #[test]
3632    fn any_preserves_nonscalar_integer_arrays_and_declared_types_reject_them() {
3633        for (storage, class_name) in all_integer_value_storages() {
3634            let expected = storage.clone();
3635            let array = Value::Tensor(Tensor::new_integer(storage, vec![1, 2]).unwrap());
3636            let any_map = containers_map_builtin(vec![
3637                Value::from("KeyType"),
3638                Value::from("char"),
3639                Value::from("ValueType"),
3640                Value::from("any"),
3641            ])
3642            .unwrap();
3643            let payload = crate::make_cell(vec![Value::from("array")], 1, 1).unwrap();
3644            let any_map =
3645                containers_map_subsasgn(any_map, "()".to_string(), payload.clone(), array.clone())
3646                    .unwrap();
3647            let Value::Tensor(stored) =
3648                containers_map_subsref(any_map, "()".to_string(), payload.clone()).unwrap()
3649            else {
3650                panic!("expected exact integer tensor");
3651            };
3652            assert_eq!(stored.integer_storage(), Some(&expected));
3653
3654            let typed_map = containers_map_builtin(vec![
3655                Value::from("KeyType"),
3656                Value::from("char"),
3657                Value::from("ValueType"),
3658                Value::from(class_name),
3659            ])
3660            .unwrap();
3661            assert!(containers_map_subsasgn(typed_map, "()".to_string(), payload, array,).is_err());
3662        }
3663    }
3664
3665    #[test]
3666    fn unsupported_narrow_integer_key_classes_reject_without_double_aliasing() {
3667        for storage in [
3668            IntegerStorage::I8(vec![1]),
3669            IntegerStorage::U8(vec![1]),
3670            IntegerStorage::I16(vec![1]),
3671            IntegerStorage::U16(vec![1]),
3672        ] {
3673            let error = containers_map_builtin(vec![
3674                Value::Tensor(Tensor::new_integer(storage, vec![1, 1]).unwrap()),
3675                Value::Tensor(Tensor::new(vec![1.0], vec![1, 1]).unwrap()),
3676            ])
3677            .unwrap_err();
3678            assert!(error.to_string().contains("int32"));
3679        }
3680    }
3681
3682    #[test]
3683    fn resident_extensions_gate_before_provider_access_for_integer_surfaces() {
3684        let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
3685        let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
3686            shape: vec![1, 1],
3687            device_id: 77,
3688            buffer_id: 99,
3689            descriptor: Default::default(),
3690        });
3691        let char_map = containers_map_builtin(Vec::new()).unwrap();
3692        let checks = [
3693            block_on(super::containers_map_builtin(vec![
3694                resident.clone(),
3695                Value::Num(1.0),
3696            ]))
3697            .unwrap_err(),
3698            block_on(super::containers_map_is_key(
3699                char_map.clone(),
3700                resident.clone(),
3701            ))
3702            .unwrap_err(),
3703            block_on(super::containers_map_remove(
3704                char_map.clone(),
3705                resident.clone(),
3706            ))
3707            .unwrap_err(),
3708            block_on(super::containers_map_values(
3709                char_map.clone(),
3710                vec![crate::make_cell(vec![resident.clone()], 1, 1).unwrap()],
3711            ))
3712            .unwrap_err(),
3713            block_on(super::containers_map_subsref(
3714                char_map.clone(),
3715                "()".to_string(),
3716                crate::make_cell(vec![resident.clone()], 1, 1).unwrap(),
3717            ))
3718            .unwrap_err(),
3719            block_on(super::containers_map_subsasgn(
3720                char_map,
3721                "()".to_string(),
3722                crate::make_cell(vec![Value::from("x")], 1, 1).unwrap(),
3723                resident,
3724            ))
3725            .unwrap_err(),
3726        ];
3727        for error in checks {
3728            assert!(error
3729                .identifier()
3730                .is_some_and(|identifier| identifier
3731                    .starts_with("RunMat:compatibility:ContainersMapResident")));
3732        }
3733    }
3734
3735    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3736    #[test]
3737    fn map_constructor_gathers_gpu_values() {
3738        test_support::with_test_provider(|provider| {
3739            let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3740            let keys = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
3741            let data = vec![1.0, 2.0, 3.0];
3742            let shape = vec![3, 1];
3743            let view = runmat_accelerate_api::HostTensorView {
3744                data: &data,
3745                shape: &shape,
3746            };
3747            let handle = provider.upload(&view).expect("upload");
3748            let values = crate::make_cell(vec![Value::GpuTensor(handle)], 1, 1).unwrap();
3749            let map = containers_map_builtin(vec![
3750                keys,
3751                values,
3752                Value::from("UniformValues"),
3753                Value::Bool(false),
3754            ])
3755            .expect("map");
3756            let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
3757            let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
3758            match value {
3759                Value::Tensor(t) => {
3760                    assert_eq!(t.shape, shape);
3761                    assert_eq!(t.materialize_f64(), data);
3762                }
3763                other => panic!("expected tensor, got {:?}", other),
3764            }
3765        });
3766    }
3767
3768    #[test]
3769    fn map_resident_integer_keys_and_values_gather_exactly() {
3770        test_support::with_test_provider(|provider| {
3771            let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
3772            let wide = 9_007_199_254_740_993_u64;
3773            let key_tensor =
3774                Tensor::new_integer(IntegerStorage::U64(vec![wide]), vec![1, 1]).unwrap();
3775            let key_handle = gpu_helpers::upload_tensor(provider, &key_tensor).expect("key upload");
3776            let map = containers_map_builtin(vec![Value::GpuTensor(key_handle), Value::Num(7.0)])
3777                .expect("resident key constructor");
3778            let payload = crate::make_cell(vec![Value::Int(IntValue::U64(wide))], 1, 1).unwrap();
3779            assert_eq!(
3780                containers_map_subsref(map, "()".to_string(), payload).unwrap(),
3781                Value::Num(7.0)
3782            );
3783
3784            let value_tensor =
3785                Tensor::new_integer(IntegerStorage::U64(vec![wide, u64::MAX]), vec![2, 1]).unwrap();
3786            let value_handle =
3787                gpu_helpers::upload_tensor(provider, &value_tensor).expect("value upload");
3788            let keys = crate::make_cell(vec![Value::from("wide")], 1, 1).unwrap();
3789            let values = crate::make_cell(vec![Value::GpuTensor(value_handle)], 1, 1).unwrap();
3790            let map = containers_map_builtin(vec![
3791                keys,
3792                values,
3793                Value::from("UniformValues"),
3794                Value::Bool(false),
3795            ])
3796            .expect("resident value constructor");
3797            let payload = crate::make_cell(vec![Value::from("wide")], 1, 1).unwrap();
3798            let Value::Tensor(gathered) =
3799                containers_map_subsref(map, "()".to_string(), payload).unwrap()
3800            else {
3801                panic!("expected exact integer tensor value");
3802            };
3803            assert_eq!(gathered.integer_storage(), value_tensor.integer_storage());
3804        });
3805    }
3806}