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