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