1use std::collections::HashMap;
4use std::sync::{
5 atomic::{AtomicU64, Ordering},
6 RwLock,
7};
8
9use once_cell::sync::Lazy;
10use runmat_builtins::{CharArray, HandleRef, IntValue, LogicalArray, StructValue, Tensor, Value};
11use runmat_macros::runtime_builtin;
12
13use crate::builtins::common::random_args::keyword_of;
14use crate::builtins::common::spec::{
15 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
16 ReductionNaN, ResidencyPolicy, ShapeRequirements,
17};
18#[cfg(feature = "doc_export")]
19use crate::register_builtin_doc_text;
20use crate::{gather_if_needed, register_builtin_fusion_spec, register_builtin_gpu_spec};
21
22const CLASS_NAME: &str = "containers.Map";
23const MISSING_KEY_ERR: &str = "containers.Map: The specified key is not present in this container.";
24
25#[cfg(feature = "doc_export")]
26pub const DOC_MD: &str = r#"---
27title: "containers.Map"
28category: "containers/map"
29keywords: ["containers.Map", "map", "dictionary", "hash map", "lookup"]
30summary: "Create MATLAB-compatible dictionary objects that map keys to values."
31references:
32 - https://www.mathworks.com/help/matlab/ref/containers.map-class.html
33gpu_support:
34 elementwise: false
35 reduction: false
36 precisions: []
37 broadcasting: "none"
38 notes: "Map storage lives on the host. GPU inputs are gathered when constructing maps or fetching values."
39fusion:
40 elementwise: false
41 reduction: false
42 max_inputs: 0
43 constants: "inline"
44requires_feature: null
45tested:
46 unit: "builtins::containers::map::containers_map::tests"
47 integration: "builtins::containers::map::containers_map::tests::doc_examples_present"
48---
49
50# What does the `containers.Map` function do in MATLAB / RunMat?
51`containers.Map` builds dictionary objects that associate unique keys with values. Keys can be
52character vectors, string scalars, or numeric scalars (double/int32/uint32/int64/uint64/logical).
53Values default to MATLAB's `'any'` semantics, letting you store arbitrary scalars, arrays, structs,
54or handle objects. Each map tracks insertion order, supports key-based indexing, and exposes methods
55such as `keys`, `values`, `isKey`, and `remove`.
56
57## How does the `containers.Map` function behave in MATLAB / RunMat?
58- Keys must be unique. Constructing a map or assigning a key that already exists overwrites the
59 stored value (matching MATLAB's behaviour).
60- The `KeyType`, `ValueType`, and `Count` properties are readable with dot-indexing.
61- `map(key)` returns the associated value; requesting a missing key raises the MATLAB-compatible
62 error *"The specified key is not present in this container."*
63- Assignments of the form `map(key) = value` update or insert entries.
64- Methods `keys(map)`, `values(map)`, `isKey(map, keySpec)`, and `remove(map, keySpec)` are fully
65 compatible. When `keySpec` is a cell or string array, the result matches MATLAB's shape.
66- GPU tensors presented as values are gathered to host memory before insertion. When values or keys
67 arrive on the GPU and need to be expanded element-wise (for example, vector-valued constructor
68 arguments), RunMat downloads them to materialise individual scalars.
69- The `'UniformValues'` flag is accepted; when `true`, RunMat validates that every inserted value has
70 the same MATLAB class. Retrieval still returns a cell array, matching MATLAB behaviour when the
71 value type is `'any'`.
72
73## `containers.Map` Function GPU Execution Behaviour
74The data structure itself resides on the CPU. When you construct a map with GPU arrays, RunMat first
75downloads the inputs so it can perform MATLAB-compatible validation and coercion. Maps never retain
76device buffers internally, so the GPU provider does not need to implement special hooks for this
77builtin.
78
79## Examples of using the `containers.Map` function in MATLAB / RunMat
80
81### Create an empty map with default types
82```matlab
83m = containers.Map();
84m.KeyType
85m.ValueType
86m.Count
87```
88
89Expected output:
90```matlab
91ans =
92 'char'
93ans =
94 'any'
95ans =
96 0
97```
98
99### Build a map from paired cell arrays
100```matlab
101keys = {'apple', 'pear', 'banana'};
102vals = {42, [1 2 3], true};
103fruit = containers.Map(keys, vals);
104energy = fruit('apple');
105```
106
107Expected output:
108```matlab
109energy =
110 42
111```
112
113### Update an existing key and add a new one
114```matlab
115fruit('apple') = 99;
116fruit('peach') = struct('ripe', true);
117```
118
119Expected output:
120```matlab
121fruit('apple')
122ans =
123 99
124```
125
126### Query keys, values, and membership
127```matlab
128allKeys = keys(fruit);
129allVals = values(fruit);
130mask = isKey(fruit, {'apple', 'durian'});
131```
132
133Expected output:
134```matlab
135allKeys =
136 1×4 cell array
137 {'apple'} {'pear'} {'banana'} {'peach'}
138
139allVals =
140 1×4 cell array
141 {[99]} {[1 2 3]} {[1]} {1×1 struct}
142
143mask =
144 1×2 logical array
145 1 0
146```
147
148### Remove keys and inspect the map length
149```matlab
150remove(fruit, {'pear', 'banana'});
151n = length(fruit);
152remaining = keys(fruit);
153```
154
155Expected output:
156```matlab
157n =
158 2
159remaining =
160 1×2 cell array
161 {'apple'} {'peach'}
162```
163
164## FAQ
165
166### Which key types are supported?
167`containers.Map` accepts `'char'`, `'string'`, `'double'`, `'single'`, `'int32'`, `'uint32'`, `'int64'`,
168`'uint64'`, and `'logical'`. Keys supplied during construction or assignment are coerced to the
169declared type and must be scalar.
170
171### What happens when I provide duplicate keys at construction time?
172Duplicate keys raise the same error as MATLAB: *"Duplicate key name was provided."* During
173assignment, duplicate keys overwrite the existing value.
174
175### Does RunMat honour `'UniformValues', true`?
176Yes. When this option is set, RunMat enforces that each inserted value matches the MATLAB class of
177the first value. Retrieval still uses cell arrays, mirroring MATLAB when `'ValueType'` is `'any'`.
178
179### Can I store GPU arrays as map values?
180Yes. RunMat automatically gathers GPU tensors to host memory before inserting them so it can apply
181the same validation and coercion rules as MATLAB. This ensures constructors that rely on vector
182expansion continue to produce predictable host-side values.
183
184### How does `length(map)` behave?
185`length(map)` returns the number of stored keys (identical to the `Count` property). `size(map)`
186remains `[1 1]`, matching MATLAB's handle semantics.
187
188### What error is raised when a key is missing?
189Indexing a missing key produces the MATLAB-compatible error message *"The specified key is not
190present in this container."*
191
192### Does the map preserve insertion order?
193Yes. `keys(map)` and `values(map)` return entries in the order they were first inserted, matching the
194behaviour of MATLAB's `containers.Map`.
195
196### Is the implementation thread-safe?
197Yes. A global read/write lock guards the backing storage so concurrent reads are allowed while write
198operations remain exclusive.
199
200### How do I remove every entry?
201Call `remove(map, keys(map))` or reassign a new empty map. RunMat currently keeps the internal
202storage until the handle is cleared, matching MATLAB's lifetime semantics.
203
204### What happens if I pass a non-scalar key?
205Keys must be scalar. Passing vectors, matrices, or nested cell arrays of keys raises a descriptive
206error pointing to the offending argument.
207
208## See Also
209[keys](./containers.Map.keys), [values](./containers.Map.values), [isKey](./containers.Map.isKey),
210[remove](./containers.Map.remove), [length](../../array/introspection/length)
211"#;
212
213pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
214 name: "containers.Map",
215 op_kind: GpuOpKind::Custom("map"),
216 supported_precisions: &[],
217 broadcast: BroadcastSemantics::None,
218 provider_hooks: &[],
219 constant_strategy: ConstantStrategy::InlineLiteral,
220 residency: ResidencyPolicy::GatherImmediately,
221 nan_mode: ReductionNaN::Include,
222 two_pass_threshold: None,
223 workgroup_size: None,
224 accepts_nan_mode: false,
225 notes: "Map storage is host-resident; GPU inputs are gathered only when split into multiple entries.",
226};
227
228register_builtin_gpu_spec!(GPU_SPEC);
229
230pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
231 name: "containers.Map",
232 shape: ShapeRequirements::Any,
233 constant_strategy: ConstantStrategy::InlineLiteral,
234 elementwise: None,
235 reduction: None,
236 emits_nan: false,
237 notes: "Handles act as fusion sinks; map construction terminates GPU fusion plans.",
238};
239
240register_builtin_fusion_spec!(FUSION_SPEC);
241
242#[cfg(feature = "doc_export")]
243register_builtin_doc_text!("containers.Map", DOC_MD);
244
245static NEXT_ID: AtomicU64 = AtomicU64::new(1);
246static MAP_REGISTRY: Lazy<RwLock<HashMap<u64, MapStore>>> =
247 Lazy::new(|| RwLock::new(HashMap::new()));
248
249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250enum KeyType {
251 Char,
252 String,
253 Double,
254 Single,
255 Int32,
256 UInt32,
257 Int64,
258 UInt64,
259 Logical,
260}
261
262impl KeyType {
263 fn matlab_name(self) -> &'static str {
264 match self {
265 KeyType::Char => "char",
266 KeyType::String => "string",
267 KeyType::Double => "double",
268 KeyType::Single => "single",
269 KeyType::Int32 => "int32",
270 KeyType::UInt32 => "uint32",
271 KeyType::Int64 => "int64",
272 KeyType::UInt64 => "uint64",
273 KeyType::Logical => "logical",
274 }
275 }
276
277 fn parse(value: &Value) -> Result<Self, String> {
278 let text = string_from_value(value, "containers.Map: expected a KeyType string")?;
279 match text.to_ascii_lowercase().as_str() {
280 "char" | "character" => Ok(KeyType::Char),
281 "string" => Ok(KeyType::String),
282 "double" => Ok(KeyType::Double),
283 "single" => Ok(KeyType::Single),
284 "int32" => Ok(KeyType::Int32),
285 "uint32" => Ok(KeyType::UInt32),
286 "int64" => Ok(KeyType::Int64),
287 "uint64" => Ok(KeyType::UInt64),
288 "logical" => Ok(KeyType::Logical),
289 other => Err(format!(
290 "containers.Map: unsupported KeyType '{other}'. Valid types: char, string, double, int32, uint32, int64, uint64, logical."
291 )),
292 }
293 }
294}
295
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297enum ValueType {
298 Any,
299 Char,
300 String,
301 Double,
302 Single,
303 Logical,
304}
305
306impl ValueType {
307 fn matlab_name(self) -> &'static str {
308 match self {
309 ValueType::Any => "any",
310 ValueType::Char => "char",
311 ValueType::String => "string",
312 ValueType::Double => "double",
313 ValueType::Single => "single",
314 ValueType::Logical => "logical",
315 }
316 }
317
318 fn parse(value: &Value) -> Result<Self, String> {
319 let text = string_from_value(value, "containers.Map: expected a ValueType string")?;
320 match text.to_ascii_lowercase().as_str() {
321 "any" => Ok(ValueType::Any),
322 "char" | "character" => Ok(ValueType::Char),
323 "string" => Ok(ValueType::String),
324 "double" => Ok(ValueType::Double),
325 "single" => Ok(ValueType::Single),
326 "logical" => Ok(ValueType::Logical),
327 other => Err(format!(
328 "containers.Map: unsupported ValueType '{other}'. Valid types: any, char, string, double, single, logical."
329 )),
330 }
331 }
332
333 fn normalize(&self, value: Value) -> Result<Value, String> {
334 match self {
335 ValueType::Any => Ok(value),
336 ValueType::Char => {
337 let chars = char_array_from_value(&value)?;
338 Ok(Value::CharArray(chars))
339 }
340 ValueType::String => {
341 let text =
342 string_from_value(&value, "containers.Map: values must be string scalars")?;
343 Ok(Value::String(text))
344 }
345 ValueType::Double | ValueType::Single => normalize_numeric_value(value),
346 ValueType::Logical => normalize_logical_value(value),
347 }
348 }
349}
350
351#[derive(Clone, PartialEq, Eq, Hash)]
352enum NormalizedKey {
353 String(String),
354 Float(u64),
355 Int(i64),
356 UInt(u64),
357 Bool(bool),
358}
359
360#[derive(Clone)]
361struct MapEntry {
362 normalized: NormalizedKey,
363 key_value: Value,
364 value: Value,
365}
366
367struct MapStore {
368 key_type: KeyType,
369 value_type: ValueType,
370 uniform_values: bool,
371 uniform_class: Option<ValueClass>,
372 entries: Vec<MapEntry>,
373 index: HashMap<NormalizedKey, usize>,
374}
375
376impl MapStore {
377 fn new(key_type: KeyType, value_type: ValueType, uniform_values: bool) -> Self {
378 Self {
379 key_type,
380 value_type,
381 uniform_values,
382 uniform_class: None,
383 entries: Vec::new(),
384 index: HashMap::new(),
385 }
386 }
387
388 fn len(&self) -> usize {
389 self.entries.len()
390 }
391
392 fn contains(&self, key: &NormalizedKey) -> bool {
393 self.index.contains_key(key)
394 }
395
396 fn get(&self, key: &NormalizedKey) -> Option<Value> {
397 self.index
398 .get(key)
399 .map(|&idx| self.entries[idx].value.clone())
400 }
401
402 fn insert_new(&mut self, mut entry: MapEntry) -> Result<(), String> {
403 if self.index.contains_key(&entry.normalized) {
404 return Err("containers.Map: Duplicate key name was provided.".to_string());
405 }
406 entry.value = self.normalize_value(entry.value)?;
407 self.track_uniform_class(&entry.value)?;
408 let idx = self.entries.len();
409 self.entries.push(entry.clone());
410 self.index.insert(entry.normalized, idx);
411 Ok(())
412 }
413
414 fn set(&mut self, mut entry: MapEntry) -> Result<(), String> {
415 entry.value = self.normalize_value(entry.value)?;
416 self.track_uniform_class(&entry.value)?;
417 if let Some(&idx) = self.index.get(&entry.normalized) {
418 self.entries[idx].value = entry.value.clone();
419 self.entries[idx].key_value = entry.key_value;
420 } else {
421 let idx = self.entries.len();
422 self.entries.push(entry.clone());
423 self.index.insert(entry.normalized, idx);
424 }
425 Ok(())
426 }
427
428 fn remove(&mut self, key: &NormalizedKey) -> Result<(), String> {
429 let idx = match self.index.get(key) {
430 Some(&idx) => idx,
431 None => {
432 return Err(MISSING_KEY_ERR.to_string());
433 }
434 };
435 self.entries.remove(idx);
436 self.index.clear();
437 for (pos, entry) in self.entries.iter().enumerate() {
438 self.index.insert(entry.normalized.clone(), pos);
439 }
440 if self.entries.is_empty() {
441 self.uniform_class = None;
442 }
443 Ok(())
444 }
445
446 fn keys(&self) -> Vec<Value> {
447 self.entries
448 .iter()
449 .map(|entry| entry.key_value.clone())
450 .collect()
451 }
452
453 fn values(&self) -> Vec<Value> {
454 self.entries
455 .iter()
456 .map(|entry| entry.value.clone())
457 .collect()
458 }
459
460 fn normalize_value(&self, value: Value) -> Result<Value, String> {
461 self.value_type.normalize(value)
462 }
463
464 fn track_uniform_class(&mut self, value: &Value) -> Result<(), String> {
465 if !self.uniform_values {
466 return Ok(());
467 }
468 let class = ValueClass::from_value(value);
469 if let Some(existing) = &self.uniform_class {
470 if existing != &class {
471 return Err("containers.Map: UniformValues=true requires all values to share the same MATLAB class."
472 .to_string());
473 }
474 } else {
475 self.uniform_class = Some(class);
476 }
477 Ok(())
478 }
479}
480
481#[derive(Clone, Debug, PartialEq, Eq)]
482enum ValueClass {
483 Char,
484 String,
485 Double,
486 Logical,
487 Int,
488 UInt,
489 Cell,
490 Struct,
491 Object,
492 Other(&'static str),
493}
494
495impl ValueClass {
496 fn from_value(value: &Value) -> Self {
497 match value {
498 Value::CharArray(_) => ValueClass::Char,
499 Value::String(_) | Value::StringArray(_) => ValueClass::String,
500 Value::Num(_) | Value::Tensor(_) | Value::ComplexTensor(_) => ValueClass::Double,
501 Value::Bool(_) | Value::LogicalArray(_) => ValueClass::Logical,
502 Value::Int(i) => match i {
503 IntValue::I8(_) | IntValue::I16(_) | IntValue::I32(_) | IntValue::I64(_) => {
504 ValueClass::Int
505 }
506 IntValue::U8(_) | IntValue::U16(_) | IntValue::U32(_) | IntValue::U64(_) => {
507 ValueClass::UInt
508 }
509 },
510 Value::Cell(_) => ValueClass::Cell,
511 Value::Struct(_) => ValueClass::Struct,
512 Value::Object(_) | Value::HandleObject(_) | Value::Listener(_) => ValueClass::Object,
513 _ => ValueClass::Other("other"),
514 }
515 }
516}
517
518struct ConstructorArgs {
519 key_type: KeyType,
520 value_type: ValueType,
521 uniform_values: bool,
522 keys: Vec<KeyCandidate>,
523 values: Vec<Value>,
524}
525
526struct KeyCandidate {
527 normalized: NormalizedKey,
528 canonical: Value,
529}
530
531#[runtime_builtin(
532 name = "containers.Map",
533 category = "containers/map",
534 summary = "Create MATLAB-style dictionary objects that map keys to values.",
535 keywords = "map,containers.Map,dictionary,hash map,lookup",
536 accel = "metadata",
537 sink = true
538)]
539fn containers_map_builtin(args: Vec<Value>) -> Result<Value, String> {
540 let parsed = parse_constructor_args(args)?;
541 let store = build_store(parsed)?;
542 allocate_handle(store)
543}
544
545#[runtime_builtin(name = "containers.Map.keys")]
546fn containers_map_keys(map: Value) -> Result<Value, String> {
547 with_store(&map, |store| {
548 let values = store.keys();
549 make_row_cell(values)
550 })
551}
552
553#[runtime_builtin(name = "containers.Map.values")]
554fn containers_map_values(map: Value) -> Result<Value, String> {
555 with_store(&map, |store| {
556 let values = store.values();
557 make_row_cell(values)
558 })
559}
560
561#[runtime_builtin(name = "containers.Map.isKey")]
562fn containers_map_is_key(map: Value, key_spec: Value) -> Result<Value, String> {
563 with_store(&map, |store| {
564 let collection = collect_key_spec(&key_spec, store.key_type)?;
565 let mut flags = Vec::with_capacity(collection.values.len());
566 for value in &collection.values {
567 let normalized = normalize_key(value, store.key_type)?;
568 flags.push(store.contains(&normalized));
569 }
570 if collection.values.len() == 1 {
571 Ok(Value::Bool(flags[0]))
572 } else {
573 let data: Vec<u8> = flags.into_iter().map(|b| if b { 1 } else { 0 }).collect();
574 let logical = LogicalArray::new(data, collection.shape)
575 .map_err(|e| format!("containers.Map: {e}"))?;
576 Ok(Value::LogicalArray(logical))
577 }
578 })
579}
580
581#[runtime_builtin(name = "containers.Map.remove")]
582fn containers_map_remove(map: Value, key_spec: Value) -> Result<Value, String> {
583 with_store_mut(&map, |store| {
584 let collection = collect_key_spec(&key_spec, store.key_type)?;
585 for value in &collection.values {
586 let normalized = normalize_key(value, store.key_type)?;
587 store.remove(&normalized)?;
588 }
589 Ok(())
590 })?;
591 Ok(map)
592}
593
594#[runtime_builtin(name = "containers.Map.subsref")]
595fn containers_map_subsref(map: Value, kind: String, payload: Value) -> Result<Value, String> {
596 if !matches!(map, Value::HandleObject(_)) {
597 return Err(format!(
598 "containers.Map: subsref expects a containers.Map handle, got {map:?}"
599 ));
600 }
601 match kind.as_str() {
602 "()" => {
603 let mut args = extract_key_arguments(&payload)?;
604 if args.is_empty() {
605 return Err("containers.Map: indexing requires at least one key".to_string());
606 }
607 if args.len() != 1 {
608 return Err("containers.Map: indexing expects a single key argument".to_string());
609 }
610 let key_arg = args.remove(0);
611 with_store(&map, |store| {
612 let collection = collect_key_spec(&key_arg, store.key_type)?;
613 if collection.values.is_empty() {
614 return crate::make_cell_with_shape(Vec::new(), collection.shape.clone())
615 .map_err(|e| format!("containers.Map: {e}"));
616 }
617 if collection.values.len() == 1 {
618 let normalized = normalize_key(&collection.values[0], store.key_type)?;
619 store
620 .get(&normalized)
621 .ok_or_else(|| MISSING_KEY_ERR.to_string())
622 } else {
623 let mut results = Vec::with_capacity(collection.values.len());
624 for value in &collection.values {
625 let normalized = normalize_key(value, store.key_type)?;
626 let stored = store
627 .get(&normalized)
628 .ok_or_else(|| MISSING_KEY_ERR.to_string())?;
629 results.push(stored);
630 }
631 crate::make_cell_with_shape(results, collection.shape.clone())
632 .map_err(|e| format!("containers.Map: {e}"))
633 }
634 })
635 }
636 "." => {
637 let field = string_from_value(&payload, "containers.Map: property name must be text")?;
638 with_store(&map, |store| match field.to_ascii_lowercase().as_str() {
639 "count" => Ok(Value::Num(store.len() as f64)),
640 "keytype" => char_array_value(store.key_type.matlab_name()),
641 "valuetype" => char_array_value(store.value_type.matlab_name()),
642 other => Err(format!("containers.Map: no such property '{other}'")),
643 })
644 }
645 "{}" => Err("containers.Map: curly-brace indexing is not supported.".to_string()),
646 other => Err(format!(
647 "containers.Map: unsupported indexing kind '{other}'"
648 )),
649 }
650}
651
652#[runtime_builtin(name = "containers.Map.subsasgn")]
653fn containers_map_subsasgn(
654 map: Value,
655 kind: String,
656 payload: Value,
657 rhs: Value,
658) -> Result<Value, String> {
659 if !matches!(map, Value::HandleObject(_)) {
660 return Err(format!(
661 "containers.Map: subsasgn expects a containers.Map handle, got {map:?}"
662 ));
663 }
664 match kind.as_str() {
665 "()" => {
666 let mut args = extract_key_arguments(&payload)?;
667 if args.is_empty() {
668 return Err("containers.Map: assignment requires at least one key".to_string());
669 }
670 if args.len() != 1 {
671 return Err("containers.Map: assignment expects a single key argument".to_string());
672 }
673 let key_arg = args.remove(0);
674 with_store_mut(&map, move |store| {
675 let KeyCollection {
676 values: key_values, ..
677 } = collect_key_spec(&key_arg, store.key_type)?;
678 let values = expand_assignment_values(rhs.clone(), key_values.len())?;
679 for (key_raw, value) in key_values.into_iter().zip(values.into_iter()) {
680 let (normalized, canonical) = canonicalize_key(key_raw, store.key_type)?;
681 let entry = MapEntry {
682 normalized,
683 key_value: canonical,
684 value,
685 };
686 store.set(entry)?;
687 }
688 Ok(())
689 })?;
690 Ok(map)
691 }
692 "." => Err("containers.Map: property assignments are not supported.".to_string()),
693 "{}" => Err("containers.Map: curly-brace assignment is not supported.".to_string()),
694 other => Err(format!(
695 "containers.Map: unsupported assignment kind '{other}'"
696 )),
697 }
698}
699
700fn parse_constructor_args(args: Vec<Value>) -> Result<ConstructorArgs, String> {
701 let mut index = 0usize;
702 let mut keys_input: Option<Value> = None;
703 let mut values_input: Option<Value> = None;
704
705 if index < args.len() && keyword_of(&args[index]).is_none() {
706 if args.len() < 2 {
707 return Err("containers.Map: constructor requires both keys and values when either is provided."
708 .to_string());
709 }
710 keys_input = Some(args[index].clone());
711 values_input = Some(args[index + 1].clone());
712 index += 2;
713 }
714
715 let mut key_type = KeyType::Char;
716 let mut value_type = ValueType::Any;
717 let mut uniform_values = false;
718 while index < args.len() {
719 let keyword = keyword_of(&args[index])
720 .ok_or_else(|| "containers.Map: expected option name (e.g. 'KeyType')".to_string())?;
721 index += 1;
722 let Some(value) = args.get(index) else {
723 return Err(format!(
724 "containers.Map: missing value for option '{keyword}'"
725 ));
726 };
727 index += 1;
728 match keyword.as_str() {
729 "keytype" => key_type = KeyType::parse(value)?,
730 "valuetype" => value_type = ValueType::parse(value)?,
731 "uniformvalues" => {
732 uniform_values =
733 bool_from_value(value, "containers.Map: UniformValues must be logical")?
734 }
735 "comparisonmethod" => {
736 let text =
737 string_from_value(value, "containers.Map: ComparisonMethod must be a string")?;
738 let lowered = text.to_ascii_lowercase();
739 if lowered != "strcmp" {
740 return Err(
741 "containers.Map: only ComparisonMethod='strcmp' is supported.".to_string(),
742 );
743 }
744 }
745 other => {
746 return Err(format!("containers.Map: unrecognised option '{other}'"));
747 }
748 }
749 }
750
751 let keys = match keys_input {
752 Some(value) => prepare_keys(value, key_type)?,
753 None => Vec::new(),
754 };
755
756 let values = match values_input {
757 Some(value) => prepare_values(value)?,
758 None => Vec::new(),
759 };
760
761 if keys.len() != values.len() {
762 return Err(format!(
763 "containers.Map: number of keys ({}) must match number of values ({})",
764 keys.len(),
765 values.len()
766 ));
767 }
768
769 Ok(ConstructorArgs {
770 key_type,
771 value_type,
772 uniform_values,
773 keys,
774 values,
775 })
776}
777
778fn build_store(args: ConstructorArgs) -> Result<MapStore, String> {
779 let mut store = MapStore::new(args.key_type, args.value_type, args.uniform_values);
780 for (candidate, value) in args.keys.into_iter().zip(args.values.into_iter()) {
781 store.insert_new(MapEntry {
782 normalized: candidate.normalized,
783 key_value: candidate.canonical,
784 value,
785 })?;
786 }
787 Ok(store)
788}
789
790fn allocate_handle(store: MapStore) -> Result<Value, String> {
791 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
792 MAP_REGISTRY
793 .write()
794 .map_err(|_| "containers.Map: registry lock poisoned".to_string())?
795 .insert(id, store);
796 let mut struct_value = StructValue::new();
797 struct_value
798 .fields
799 .insert("id".to_string(), Value::Int(IntValue::U64(id)));
800 let storage = Value::Struct(struct_value);
801 let gc = runmat_gc::gc_allocate(storage).map_err(|e| format!("containers.Map: {e}"))?;
802 Ok(Value::HandleObject(HandleRef {
803 class_name: CLASS_NAME.to_string(),
804 target: gc,
805 valid: true,
806 }))
807}
808
809fn with_store<F, R>(map: &Value, f: F) -> Result<R, String>
810where
811 F: FnOnce(&MapStore) -> Result<R, String>,
812{
813 let handle = extract_handle(map)?;
814 ensure_handle(handle)?;
815 let id = map_id(handle)?;
816 let guard = MAP_REGISTRY
817 .read()
818 .map_err(|_| "containers.Map: registry lock poisoned".to_string())?;
819 let store = guard
820 .get(&id)
821 .ok_or_else(|| "containers.Map: internal storage not found".to_string())?;
822 f(store)
823}
824
825fn with_store_mut<F, R>(map: &Value, f: F) -> Result<R, String>
826where
827 F: FnOnce(&mut MapStore) -> Result<R, String>,
828{
829 let handle = extract_handle(map)?;
830 ensure_handle(handle)?;
831 let id = map_id(handle)?;
832 let mut guard = MAP_REGISTRY
833 .write()
834 .map_err(|_| "containers.Map: registry lock poisoned".to_string())?;
835 let store = guard
836 .get_mut(&id)
837 .ok_or_else(|| "containers.Map: internal storage not found".to_string())?;
838 f(store)
839}
840
841fn extract_handle(value: &Value) -> Result<&HandleRef, String> {
842 match value {
843 Value::HandleObject(handle) => Ok(handle),
844 _ => Err("containers.Map: expected a containers.Map handle".to_string()),
845 }
846}
847
848fn ensure_handle(handle: &HandleRef) -> Result<(), String> {
849 if !handle.valid {
850 return Err("containers.Map: handle is invalid".to_string());
851 }
852 if handle.class_name != CLASS_NAME {
853 return Err(format!(
854 "containers.Map: expected handle of class '{}', got '{}'",
855 CLASS_NAME, handle.class_name
856 ));
857 }
858 Ok(())
859}
860
861fn map_id(handle: &HandleRef) -> Result<u64, String> {
862 let storage = unsafe { &*handle.target.as_raw() };
863 match storage {
864 Value::Struct(StructValue { fields }) => match fields.get("id") {
865 Some(Value::Int(IntValue::U64(id))) => Ok(*id),
866 Some(Value::Int(other)) => {
867 let id = other.to_i64();
868 if id < 0 {
869 Err("containers.Map: negative map identifier".to_string())
870 } else {
871 Ok(id as u64)
872 }
873 }
874 Some(Value::Num(n)) if *n >= 0.0 => Ok(*n as u64),
875 _ => Err("containers.Map: corrupted storage identifier".to_string()),
876 },
877 other => Err(format!(
878 "containers.Map: internal storage has unexpected shape {other:?}"
879 )),
880 }
881}
882
883fn prepare_keys(value: Value, key_type: KeyType) -> Result<Vec<KeyCandidate>, String> {
884 let host = gather_if_needed(&value).map_err(|e| format!("containers.Map: {e}"))?;
885 let flattened = flatten_keys(&host, key_type)?;
886 let mut out = Vec::with_capacity(flattened.len());
887 for raw_key in flattened {
888 let (normalized, canonical) = canonicalize_key(raw_key, key_type)?;
889 out.push(KeyCandidate {
890 normalized,
891 canonical,
892 });
893 }
894 Ok(out)
895}
896
897fn prepare_values(value: Value) -> Result<Vec<Value>, String> {
898 let host = gather_if_needed(&value).map_err(|e| format!("containers.Map: {e}"))?;
899 flatten_values(&host)
900}
901
902fn flatten_keys(value: &Value, key_type: KeyType) -> Result<Vec<Value>, String> {
903 match value {
904 Value::Cell(cell) => {
905 let mut out = Vec::with_capacity(cell.data.len());
906 for ptr in &cell.data {
907 let element = unsafe { &*ptr.as_raw() };
908 if matches!(element, Value::Cell(_)) {
909 return Err(
910 "containers.Map: nested cell arrays are not supported for keys".to_string(),
911 );
912 }
913 out.push(gather_if_needed(element).map_err(|e| format!("containers.Map: {e}"))?);
914 }
915 Ok(out)
916 }
917 Value::StringArray(sa) => Ok(sa
918 .data
919 .iter()
920 .map(|text| Value::String(text.clone()))
921 .collect()),
922 Value::CharArray(ca) => Ok(char_array_rows(ca)),
923 Value::LogicalArray(arr) => {
924 if key_type != KeyType::Logical {
925 return Err(
926 "containers.Map: logical arrays can only be used with KeyType='logical'"
927 .to_string(),
928 );
929 }
930 Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect())
931 }
932 Value::Tensor(t) => {
933 if !t.shape.is_empty() && t.data.len() != 1 && !is_vector_shape(&t.shape) {
934 return Err(
935 "containers.Map: numeric keys must be scalar or vector shaped".to_string(),
936 );
937 }
938 Ok(t.data.iter().map(|&v| Value::Num(v)).collect())
939 }
940 Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::String(_) => {
941 Ok(vec![value.clone()])
942 }
943 Value::GpuTensor(_) => Err(
944 "containers.Map: GPU keys must be gathered to the host before construction".to_string(),
945 ),
946 other => Err(format!(
947 "containers.Map: unsupported key container {other:?}"
948 )),
949 }
950}
951
952fn flatten_values(value: &Value) -> Result<Vec<Value>, String> {
953 match value {
954 Value::Cell(cell) => {
955 let mut out = Vec::with_capacity(cell.data.len());
956 for ptr in &cell.data {
957 out.push(
958 gather_if_needed(unsafe { &*ptr.as_raw() })
959 .map_err(|e| format!("containers.Map: {e}"))?,
960 );
961 }
962 Ok(out)
963 }
964 Value::StringArray(sa) => Ok(sa
965 .data
966 .iter()
967 .map(|text| Value::String(text.clone()))
968 .collect()),
969 Value::CharArray(ca) => Ok(char_array_rows(ca)),
970 Value::LogicalArray(arr) => Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect()),
971 Value::Tensor(t) => {
972 if !t.shape.is_empty() && !is_vector_shape(&t.shape) && t.data.len() != 1 {
973 return Err(
974 "containers.Map: numeric values must be scalar or vector shaped".to_string(),
975 );
976 }
977 Ok(t.data.iter().map(|&v| Value::Num(v)).collect())
978 }
979 _ => Ok(vec![value.clone()]),
980 }
981}
982
983fn char_array_rows(ca: &CharArray) -> Vec<Value> {
984 if ca.rows == 0 {
985 return Vec::new();
986 }
987 let mut out = Vec::with_capacity(ca.rows);
988 for row in 0..ca.rows {
989 let mut text = String::with_capacity(ca.cols);
990 for col in 0..ca.cols {
991 text.push(ca.data[row * ca.cols + col]);
992 }
993 out.push(Value::CharArray(
994 CharArray::new(text.chars().collect(), 1, text.chars().count())
995 .expect("char array new"),
996 ));
997 }
998 out
999}
1000
1001fn is_vector_shape(shape: &[usize]) -> bool {
1002 match shape.len() {
1003 0 => true,
1004 1 => true,
1005 2 => shape[0] == 1 || shape[1] == 1,
1006 _ => false,
1007 }
1008}
1009
1010fn canonicalize_key(value: Value, key_type: KeyType) -> Result<(NormalizedKey, Value), String> {
1011 let normalized = normalize_key(&value, key_type)?;
1012 let canonical = match key_type {
1013 KeyType::Char => Value::CharArray(char_array_from_value(&value)?),
1014 KeyType::String => Value::String(string_from_value(
1015 &value,
1016 "containers.Map: keys must be string scalars",
1017 )?),
1018 KeyType::Double => Value::Num(numeric_from_value(
1019 &value,
1020 "containers.Map: keys must be numeric scalars",
1021 )?),
1022 KeyType::Single => Value::Num(numeric_from_value(
1023 &value,
1024 "containers.Map: keys must be numeric scalars",
1025 )?),
1026 KeyType::Int32 => Value::Int(IntValue::I32(integer_from_value(
1027 &value,
1028 i32::MIN as i64,
1029 i32::MAX as i64,
1030 "containers.Map: int32 keys must be integers",
1031 )? as i32)),
1032 KeyType::UInt32 => Value::Int(IntValue::U32(unsigned_from_value(
1033 &value,
1034 u32::MAX as u64,
1035 "containers.Map: uint32 keys must be unsigned integers",
1036 )? as u32)),
1037 KeyType::Int64 => Value::Int(IntValue::I64(integer_from_value(
1038 &value,
1039 i64::MIN,
1040 i64::MAX,
1041 "containers.Map: int64 keys must be integers",
1042 )?)),
1043 KeyType::UInt64 => Value::Int(IntValue::U64(unsigned_from_value(
1044 &value,
1045 u64::MAX,
1046 "containers.Map: uint64 keys must be unsigned integers",
1047 )?)),
1048 KeyType::Logical => Value::Bool(bool_from_value(
1049 &value,
1050 "containers.Map: logical keys must be logical scalars",
1051 )?),
1052 };
1053 Ok((normalized, canonical))
1054}
1055
1056fn normalize_key(value: &Value, key_type: KeyType) -> Result<NormalizedKey, String> {
1057 match key_type {
1058 KeyType::Char | KeyType::String => {
1059 let text = string_from_value(value, "containers.Map: keys must be text scalars")?;
1060 Ok(NormalizedKey::String(text))
1061 }
1062 KeyType::Double | KeyType::Single => {
1063 let numeric =
1064 numeric_from_value(value, "containers.Map: keys must be numeric scalars")?;
1065 if !numeric.is_finite() {
1066 return Err("containers.Map: keys must be finite numeric scalars".to_string());
1067 }
1068 let canonical = if numeric == 0.0 { 0.0 } else { numeric };
1069 Ok(NormalizedKey::Float(canonical.to_bits()))
1070 }
1071 KeyType::Int32 | KeyType::Int64 => {
1072 let bounds = if key_type == KeyType::Int32 {
1073 (i32::MIN as i64, i32::MAX as i64)
1074 } else {
1075 (i64::MIN, i64::MAX)
1076 };
1077 let value = integer_from_value(
1078 value,
1079 bounds.0,
1080 bounds.1,
1081 "containers.Map: integer keys must be whole numbers",
1082 )?;
1083 Ok(NormalizedKey::Int(value))
1084 }
1085 KeyType::UInt32 | KeyType::UInt64 => {
1086 let limit = if key_type == KeyType::UInt32 {
1087 u32::MAX as u64
1088 } else {
1089 u64::MAX
1090 };
1091 let value = unsigned_from_value(
1092 value,
1093 limit,
1094 "containers.Map: unsigned keys must be non-negative integers",
1095 )?;
1096 Ok(NormalizedKey::UInt(value))
1097 }
1098 KeyType::Logical => {
1099 let flag = bool_from_value(
1100 value,
1101 "containers.Map: logical keys must be logical scalars",
1102 )?;
1103 Ok(NormalizedKey::Bool(flag))
1104 }
1105 }
1106}
1107
1108fn string_from_value(value: &Value, context: &str) -> Result<String, String> {
1109 match value {
1110 Value::String(s) => Ok(s.clone()),
1111 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
1112 Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
1113 _ => Err(context.to_string()),
1114 }
1115}
1116
1117fn char_array_from_value(value: &Value) -> Result<CharArray, String> {
1118 match value {
1119 Value::CharArray(ca) if ca.rows == 1 => Ok(ca.clone()),
1120 Value::String(s) => {
1121 let chars: Vec<char> = s.chars().collect();
1122 CharArray::new(chars.clone(), 1, chars.len())
1123 .map_err(|e| format!("containers.Map: {e}"))
1124 }
1125 Value::StringArray(sa) if sa.data.len() == 1 => {
1126 let chars: Vec<char> = sa.data[0].chars().collect();
1127 CharArray::new(chars.clone(), 1, chars.len())
1128 .map_err(|e| format!("containers.Map: {e}"))
1129 }
1130 _ => Err("containers.Map: keys must be character vectors".to_string()),
1131 }
1132}
1133
1134fn char_array_value(text: &str) -> Result<Value, String> {
1135 let chars: Vec<char> = text.chars().collect();
1136 CharArray::new(chars.clone(), 1, chars.len())
1137 .map(Value::CharArray)
1138 .map_err(|e| format!("containers.Map: {e}"))
1139}
1140
1141fn normalize_numeric_value(value: Value) -> Result<Value, String> {
1142 match value {
1143 Value::Num(_) | Value::Tensor(_) => Ok(value),
1144 Value::Int(i) => Ok(Value::Num(i.to_f64())),
1145 Value::Bool(b) => Ok(Value::Num(if b { 1.0 } else { 0.0 })),
1146 Value::LogicalArray(arr) => {
1147 let data: Vec<f64> = arr
1148 .data
1149 .iter()
1150 .map(|&b| if b != 0 { 1.0 } else { 0.0 })
1151 .collect();
1152 let tensor =
1153 Tensor::new(data, arr.shape.clone()).map_err(|e| format!("containers.Map: {e}"))?;
1154 Ok(Value::Tensor(tensor))
1155 }
1156 Value::Cell(_)
1157 | Value::Struct(_)
1158 | Value::Object(_)
1159 | Value::HandleObject(_)
1160 | Value::Listener(_)
1161 | Value::String(_)
1162 | Value::StringArray(_)
1163 | Value::CharArray(_)
1164 | Value::Complex(_, _)
1165 | Value::ComplexTensor(_)
1166 | Value::FunctionHandle(_)
1167 | Value::Closure(_)
1168 | Value::ClassRef(_)
1169 | Value::MException(_)
1170 | Value::GpuTensor(_) => Err(
1171 "containers.Map: values must be numeric when ValueType is 'double' or 'single'"
1172 .to_string(),
1173 ),
1174 }
1175}
1176
1177fn normalize_logical_value(value: Value) -> Result<Value, String> {
1178 match value {
1179 Value::Bool(_) | Value::LogicalArray(_) => Ok(value),
1180 Value::Int(i) => Ok(Value::Bool(i.to_i64() != 0)),
1181 Value::Num(n) => Ok(Value::Bool(n != 0.0)),
1182 Value::Tensor(t) => {
1183 let flags: Vec<u8> = t
1184 .data
1185 .iter()
1186 .map(|&v| if v != 0.0 { 1 } else { 0 })
1187 .collect();
1188 let logical = LogicalArray::new(flags, t.shape.clone())
1189 .map_err(|e| format!("containers.Map: {e}"))?;
1190 Ok(Value::LogicalArray(logical))
1191 }
1192 Value::CharArray(_)
1193 | Value::String(_)
1194 | Value::StringArray(_)
1195 | Value::Struct(_)
1196 | Value::Cell(_)
1197 | Value::Object(_)
1198 | Value::HandleObject(_)
1199 | Value::Listener(_)
1200 | Value::Complex(_, _)
1201 | Value::ComplexTensor(_)
1202 | Value::FunctionHandle(_)
1203 | Value::Closure(_)
1204 | Value::ClassRef(_)
1205 | Value::MException(_)
1206 | Value::GpuTensor(_) => {
1207 Err("containers.Map: values must be logical when ValueType is 'logical'".to_string())
1208 }
1209 }
1210}
1211
1212fn numeric_from_value(value: &Value, context: &str) -> Result<f64, String> {
1213 match value {
1214 Value::Num(n) => Ok(*n),
1215 Value::Int(i) => Ok(i.to_f64()),
1216 Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
1217 Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
1218 Value::LogicalArray(arr) if arr.data.len() == 1 => {
1219 Ok(if arr.data[0] != 0 { 1.0 } else { 0.0 })
1220 }
1221 _ => Err(context.to_string()),
1222 }
1223}
1224
1225fn integer_from_value(value: &Value, min: i64, max: i64, context: &str) -> Result<i64, String> {
1226 match value {
1227 Value::Int(i) => {
1228 let v = i.to_i64();
1229 if v < min || v > max {
1230 return Err(context.to_string());
1231 }
1232 Ok(v)
1233 }
1234 Value::Num(n) => {
1235 if !n.is_finite() {
1236 return Err(context.to_string());
1237 }
1238 if (*n < min as f64) || (*n > max as f64) {
1239 return Err(context.to_string());
1240 }
1241 if (n.round() - n).abs() > f64::EPSILON {
1242 return Err(context.to_string());
1243 }
1244 Ok(n.round() as i64)
1245 }
1246 Value::Bool(b) => {
1247 let v = if *b { 1 } else { 0 };
1248 if v < min || v > max {
1249 return Err(context.to_string());
1250 }
1251 Ok(v)
1252 }
1253 _ => Err(context.to_string()),
1254 }
1255}
1256
1257fn unsigned_from_value(value: &Value, max: u64, context: &str) -> Result<u64, String> {
1258 match value {
1259 Value::Int(i) => {
1260 let v = i.to_i64();
1261 if v < 0 || v as u64 > max {
1262 return Err(context.to_string());
1263 }
1264 Ok(v as u64)
1265 }
1266 Value::Num(n) => {
1267 if !n.is_finite() || *n < 0.0 || *n > max as f64 {
1268 return Err(context.to_string());
1269 }
1270 if (n.round() - n).abs() > f64::EPSILON {
1271 return Err(context.to_string());
1272 }
1273 Ok(n.round() as u64)
1274 }
1275 Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
1276 _ => Err(context.to_string()),
1277 }
1278}
1279
1280fn bool_from_value(value: &Value, context: &str) -> Result<bool, String> {
1281 match value {
1282 Value::Bool(b) => Ok(*b),
1283 Value::LogicalArray(arr) if arr.data.len() == 1 => Ok(arr.data[0] != 0),
1284 Value::Int(i) => Ok(i.to_i64() != 0),
1285 Value::Num(n) => Ok(*n != 0.0),
1286 _ => Err(context.to_string()),
1287 }
1288}
1289
1290fn make_row_cell(values: Vec<Value>) -> Result<Value, String> {
1291 let cols = values.len();
1292 crate::make_cell_with_shape(values, vec![1, cols])
1293}
1294
1295fn extract_key_arguments(payload: &Value) -> Result<Vec<Value>, String> {
1296 match payload {
1297 Value::Cell(cell) => {
1298 let mut out = Vec::with_capacity(cell.data.len());
1299 for ptr in &cell.data {
1300 out.push(unsafe { &*ptr.as_raw() }.clone());
1301 }
1302 Ok(out)
1303 }
1304 other => Err(format!(
1305 "containers.Map: expected key arguments in a cell array, got {other:?}"
1306 )),
1307 }
1308}
1309
1310fn expand_assignment_values(value: Value, expected: usize) -> Result<Vec<Value>, String> {
1311 let host = gather_if_needed(&value).map_err(|e| format!("containers.Map: {e}"))?;
1312 let values = flatten_values(&host)?;
1313 if expected == 1 {
1314 if values.is_empty() {
1315 return Err("containers.Map: assignment requires a value".to_string());
1316 }
1317 Ok(vec![values.into_iter().next().unwrap()])
1318 } else {
1319 if values.len() != expected {
1320 return Err(format!(
1321 "containers.Map: assignment with {} keys requires {} values (got {})",
1322 expected,
1323 expected,
1324 values.len()
1325 ));
1326 }
1327 Ok(values)
1328 }
1329}
1330
1331struct KeyCollection {
1332 values: Vec<Value>,
1333 shape: Vec<usize>,
1334}
1335
1336fn collect_key_spec(value: &Value, key_type: KeyType) -> Result<KeyCollection, String> {
1337 let host = gather_if_needed(value).map_err(|e| format!("containers.Map: {e}"))?;
1338 match &host {
1339 Value::Cell(cell) => {
1340 let mut values = Vec::with_capacity(cell.data.len());
1341 for ptr in &cell.data {
1342 values.push(
1343 gather_if_needed(unsafe { &*ptr.as_raw() })
1344 .map_err(|e| format!("containers.Map: {e}"))?,
1345 );
1346 }
1347 Ok(KeyCollection {
1348 values,
1349 shape: vec![cell.rows, cell.cols],
1350 })
1351 }
1352 Value::StringArray(sa) => Ok(KeyCollection {
1353 values: sa.data.iter().map(|s| Value::String(s.clone())).collect(),
1354 shape: vec![sa.rows(), sa.cols()],
1355 }),
1356 Value::CharArray(ca) => {
1357 let rows = if ca.rows == 0 { 0 } else { ca.rows };
1358 Ok(KeyCollection {
1359 values: char_array_rows(ca),
1360 shape: vec![rows, 1],
1361 })
1362 }
1363 Value::LogicalArray(arr) if key_type == KeyType::Logical => Ok(KeyCollection {
1364 values: arr.data.iter().map(|&b| Value::Bool(b != 0)).collect(),
1365 shape: arr.shape.clone(),
1366 }),
1367 Value::Tensor(t) if key_type != KeyType::Char && key_type != KeyType::String => {
1368 Ok(KeyCollection {
1369 values: t.data.iter().map(|&n| Value::Num(n)).collect(),
1370 shape: t.shape.clone(),
1371 })
1372 }
1373 _ => Ok(KeyCollection {
1374 values: vec![host.clone()],
1375 shape: vec![1, 1],
1376 }),
1377 }
1378}
1379
1380pub fn map_length(value: &Value) -> Option<usize> {
1381 if let Value::HandleObject(handle) = value {
1382 if handle.valid && handle.class_name == CLASS_NAME {
1383 if let Ok(id) = map_id(handle) {
1384 if let Ok(registry) = MAP_REGISTRY.read() {
1385 return registry.get(&id).map(|store| store.len());
1386 }
1387 }
1388 }
1389 }
1390 None
1391}
1392
1393#[cfg(test)]
1394mod tests {
1395 use super::*;
1396 use crate::builtins::common::test_support;
1397
1398 #[test]
1399 fn construct_empty_map_defaults() {
1400 let map = containers_map_builtin(Vec::new()).expect("map");
1401 let count = containers_map_subsref(
1402 map.clone(),
1403 ".".to_string(),
1404 Value::from("Count".to_string()),
1405 )
1406 .expect("Count");
1407 assert_eq!(count, Value::Num(0.0));
1408
1409 let key_type = containers_map_subsref(
1410 map.clone(),
1411 ".".to_string(),
1412 Value::from("KeyType".to_string()),
1413 )
1414 .expect("KeyType");
1415 assert_eq!(
1416 key_type,
1417 Value::CharArray(CharArray::new("char".chars().collect(), 1, 4).unwrap())
1418 );
1419
1420 let value_type = containers_map_subsref(
1421 map.clone(),
1422 ".".to_string(),
1423 Value::from("ValueType".to_string()),
1424 )
1425 .expect("ValueType");
1426 assert_eq!(
1427 value_type,
1428 Value::CharArray(CharArray::new("any".chars().collect(), 1, 3).unwrap())
1429 );
1430 }
1431
1432 #[test]
1433 fn constructor_with_cells_lookup() {
1434 let keys = crate::make_cell(vec![Value::from("apple"), Value::from("pear")], 1, 2).unwrap();
1435 let values = crate::make_cell(vec![Value::Num(5.0), Value::Num(7.0)], 1, 2).unwrap();
1436 let map = containers_map_builtin(vec![keys, values]).expect("map");
1437 let apple = containers_map_subsref(
1438 map.clone(),
1439 "()".to_string(),
1440 crate::make_cell(vec![Value::from("apple")], 1, 1).unwrap(),
1441 )
1442 .expect("lookup");
1443 assert_eq!(apple, Value::Num(5.0));
1444 }
1445
1446 #[test]
1447 fn constructor_rejects_duplicate_keys() {
1448 let keys = crate::make_cell(vec![Value::from("dup"), Value::from("dup")], 1, 2).unwrap();
1449 let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
1450 let err = containers_map_builtin(vec![keys, values]).expect_err("duplicate check");
1451 assert!(err.contains("Duplicate key name"));
1452 }
1453
1454 #[test]
1455 fn constructor_errors_when_value_count_mismatch() {
1456 let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
1457 let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1458 let err = containers_map_builtin(vec![keys, values]).expect_err("count mismatch");
1459 assert!(err.contains("number of keys"));
1460 }
1461
1462 #[test]
1463 fn comparison_method_rejects_unknown_values() {
1464 let keys = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
1465 let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1466 let err = containers_map_builtin(vec![
1467 keys,
1468 values,
1469 Value::from("ComparisonMethod"),
1470 Value::from("caseinsensitive"),
1471 ])
1472 .expect_err("comparison method");
1473 assert!(err.contains("ComparisonMethod"));
1474 }
1475
1476 #[test]
1477 fn key_type_single_roundtrip() {
1478 let map = containers_map_builtin(vec![Value::from("KeyType"), Value::from("single")])
1479 .expect("map");
1480 let key_type = containers_map_subsref(map.clone(), ".".to_string(), Value::from("KeyType"))
1481 .expect("keytype");
1482 assert_eq!(
1483 key_type,
1484 Value::CharArray(CharArray::new("single".chars().collect(), 1, 6).unwrap())
1485 );
1486
1487 let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1488 let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(7.0))
1489 .expect("assign");
1490 let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1491 assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
1492 }
1493
1494 #[test]
1495 fn value_type_double_converts_integers() {
1496 let keys = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1497 let values = crate::make_cell(vec![Value::Int(IntValue::I32(7))], 1, 1).unwrap();
1498 let map = containers_map_builtin(vec![
1499 keys,
1500 values,
1501 Value::from("KeyType"),
1502 Value::from("double"),
1503 Value::from("ValueType"),
1504 Value::from("double"),
1505 ])
1506 .expect("map");
1507 let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1508 let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1509 assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
1510 }
1511
1512 #[test]
1513 fn value_type_logical_converts_numeric_arrays() {
1514 let keys = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
1515 let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
1516 let values = crate::make_cell(vec![Value::Tensor(tensor.clone())], 1, 1).unwrap();
1517 let map = containers_map_builtin(vec![
1518 keys,
1519 values,
1520 Value::from("ValueType"),
1521 Value::from("logical"),
1522 ])
1523 .expect("map");
1524 let payload = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
1525 let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1526 match value {
1527 Value::LogicalArray(arr) => {
1528 assert_eq!(arr.shape, vec![3, 1]);
1529 assert_eq!(arr.data, vec![0, 1, 1]);
1530 }
1531 other => panic!("expected logical array, got {:?}", other),
1532 }
1533 }
1534
1535 #[test]
1536 fn uniform_values_enforced_on_assignment() {
1537 let map = containers_map_builtin(vec![Value::from("UniformValues"), Value::from(true)])
1538 .expect("map");
1539 let payload = crate::make_cell(vec![Value::from("x")], 1, 1).unwrap();
1540 let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(1.0))
1541 .expect("assign");
1542 let err = containers_map_subsasgn(map, "()".to_string(), payload, Value::from("text"))
1543 .expect_err("uniform enforcement");
1544 assert!(err.contains("UniformValues"));
1545 }
1546
1547 #[test]
1548 fn assignment_updates_and_inserts() {
1549 let map = containers_map_builtin(Vec::new()).expect("map");
1550 let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
1551 let updated = containers_map_subsasgn(
1552 map.clone(),
1553 "()".to_string(),
1554 payload.clone(),
1555 Value::Num(1.0),
1556 )
1557 .expect("assign");
1558 let updated = containers_map_subsasgn(
1559 updated.clone(),
1560 "()".to_string(),
1561 payload.clone(),
1562 Value::Num(5.0),
1563 )
1564 .expect("update");
1565 let beta_payload = crate::make_cell(vec![Value::from("beta")], 1, 1).unwrap();
1566 let updated = containers_map_subsasgn(
1567 updated.clone(),
1568 "()".to_string(),
1569 beta_payload,
1570 Value::Num(9.0),
1571 )
1572 .expect("insert");
1573 let value = containers_map_subsref(updated, "()".to_string(), payload).expect("lookup");
1574 assert_eq!(value, Value::Num(5.0));
1575 }
1576
1577 #[test]
1578 fn subsref_multiple_keys_preserves_shape() {
1579 let keys = crate::make_cell(
1580 vec![Value::from("a"), Value::from("b"), Value::from("c")],
1581 1,
1582 3,
1583 )
1584 .unwrap();
1585 let values = crate::make_cell(
1586 vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1587 1,
1588 3,
1589 )
1590 .unwrap();
1591 let map = containers_map_builtin(vec![keys, values]).expect("map");
1592 let request = crate::make_cell(vec![Value::from("a"), Value::from("c")], 1, 2).unwrap();
1593 let payload = crate::make_cell(vec![request], 1, 1).unwrap();
1594 let result =
1595 containers_map_subsref(map.clone(), "()".to_string(), payload).expect("lookup");
1596 match result {
1597 Value::Cell(cell) => {
1598 assert_eq!(cell.rows, 1);
1599 assert_eq!(cell.cols, 2);
1600 assert_eq!(cell.get(0, 0).expect("cell 0,0"), Value::Num(1.0));
1601 assert_eq!(cell.get(0, 1).expect("cell 0,1"), Value::Num(3.0));
1602 }
1603 other => panic!("expected cell array, got {other:?}"),
1604 }
1605 }
1606
1607 #[test]
1608 fn subsref_empty_key_collection_returns_empty_cell() {
1609 let keys = crate::make_cell(vec![Value::from("z")], 1, 1).unwrap();
1610 let values = crate::make_cell(vec![Value::Num(42.0)], 1, 1).unwrap();
1611 let map = containers_map_builtin(vec![keys, values]).expect("map");
1612 let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
1613 let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
1614 let result = containers_map_subsref(map, "()".to_string(), payload).expect("lookup empty");
1615 match result {
1616 Value::Cell(cell) => {
1617 assert_eq!(cell.rows, 1);
1618 assert_eq!(cell.cols, 0);
1619 assert!(cell.data.is_empty());
1620 }
1621 other => panic!("expected empty cell, got {other:?}"),
1622 }
1623 }
1624
1625 #[test]
1626 fn subsasgn_with_cell_keys_updates_all_targets() {
1627 let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
1628 let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
1629 let map = containers_map_builtin(vec![keys, values]).expect("map");
1630 let key_spec = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
1631 let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
1632 let new_values = crate::make_cell(vec![Value::Num(10.0), Value::Num(20.0)], 1, 2).unwrap();
1633 let updated = containers_map_subsasgn(map.clone(), "()".to_string(), payload, new_values)
1634 .expect("assign");
1635 let a_payload = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
1636 let b_payload = crate::make_cell(vec![Value::from("b")], 1, 1).unwrap();
1637 let a_value =
1638 containers_map_subsref(updated.clone(), "()".to_string(), a_payload).expect("a lookup");
1639 let b_value =
1640 containers_map_subsref(updated, "()".to_string(), b_payload).expect("b lookup");
1641 assert_eq!(a_value, Value::Num(10.0));
1642 assert_eq!(b_value, Value::Num(20.0));
1643 }
1644
1645 #[test]
1646 fn assignment_value_count_mismatch_errors() {
1647 let keys = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
1648 let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
1649 let map = containers_map_builtin(vec![keys, values]).expect("map");
1650 let key_spec = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
1651 let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
1652 let rhs = crate::make_cell(vec![Value::Num(99.0)], 1, 1).unwrap();
1653 let err =
1654 containers_map_subsasgn(map, "()".to_string(), payload, rhs).expect_err("value count");
1655 assert!(err.contains("requires 2 values"));
1656 }
1657
1658 #[test]
1659 fn subsasgn_empty_key_collection_is_noop() {
1660 let keys = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
1661 let values = crate::make_cell(vec![Value::Num(7.0)], 1, 1).unwrap();
1662 let map = containers_map_builtin(vec![keys, values]).expect("map");
1663 let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
1664 let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
1665 let rhs = crate::make_cell(Vec::new(), 1, 0).unwrap();
1666 let updated =
1667 containers_map_subsasgn(map.clone(), "()".to_string(), payload, rhs).expect("assign");
1668 let lookup_payload = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
1669 let value =
1670 containers_map_subsref(updated, "()".to_string(), lookup_payload).expect("lookup");
1671 assert_eq!(value, Value::Num(7.0));
1672 }
1673
1674 #[test]
1675 fn keys_values_iskey_remove() {
1676 let keys = crate::make_cell(
1677 vec![Value::from("a"), Value::from("b"), Value::from("c")],
1678 1,
1679 3,
1680 )
1681 .unwrap();
1682 let values = crate::make_cell(
1683 vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1684 1,
1685 3,
1686 )
1687 .unwrap();
1688 let map = containers_map_builtin(vec![keys, values]).expect("map");
1689 let key_list = containers_map_keys(map.clone()).expect("keys");
1690 match key_list {
1691 Value::Cell(cell) => assert_eq!(cell.data.len(), 3),
1692 other => panic!("expected cell array, got {other:?}"),
1693 }
1694 let mask = containers_map_is_key(
1695 map.clone(),
1696 crate::make_cell(vec![Value::from("a"), Value::from("z")], 1, 2).unwrap(),
1697 )
1698 .expect("mask");
1699 match mask {
1700 Value::LogicalArray(arr) => {
1701 assert_eq!(arr.data, vec![1, 0]);
1702 }
1703 other => panic!("expected logical array, got {:?}", other),
1704 }
1705 let removed = containers_map_remove(
1706 map.clone(),
1707 crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
1708 )
1709 .expect("remove");
1710 let mask = containers_map_is_key(
1711 removed,
1712 crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
1713 )
1714 .expect("mask");
1715 assert_eq!(mask, Value::Bool(false));
1716 }
1717
1718 #[test]
1719 fn remove_missing_key_returns_error() {
1720 let keys = crate::make_cell(vec![Value::from("key")], 1, 1).unwrap();
1721 let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
1722 let map = containers_map_builtin(vec![keys, values]).expect("map");
1723 let err = containers_map_remove(
1724 map,
1725 crate::make_cell(vec![Value::from("missing")], 1, 1).unwrap(),
1726 )
1727 .expect_err("remove missing");
1728 assert_eq!(err, MISSING_KEY_ERR);
1729 }
1730
1731 #[test]
1732 fn length_delegates_to_map_count() {
1733 let keys = crate::make_cell(
1734 vec![Value::from("a"), Value::from("b"), Value::from("c")],
1735 1,
1736 3,
1737 )
1738 .unwrap();
1739 let values = crate::make_cell(
1740 vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1741 1,
1742 3,
1743 )
1744 .unwrap();
1745 let map = containers_map_builtin(vec![keys, values]).expect("map");
1746 assert_eq!(map_length(&map), Some(3));
1747 }
1748
1749 #[test]
1750 fn map_constructor_gathers_gpu_values() {
1751 test_support::with_test_provider(|provider| {
1752 let keys = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
1753 let data = vec![1.0, 2.0, 3.0];
1754 let shape = vec![3, 1];
1755 let view = runmat_accelerate_api::HostTensorView {
1756 data: &data,
1757 shape: &shape,
1758 };
1759 let handle = provider.upload(&view).expect("upload");
1760 let values = crate::make_cell(vec![Value::GpuTensor(handle)], 1, 1).unwrap();
1761 let map = containers_map_builtin(vec![keys, values]).expect("map");
1762 let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
1763 let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
1764 match value {
1765 Value::Tensor(t) => {
1766 assert_eq!(t.shape, shape);
1767 assert_eq!(t.data, data);
1768 }
1769 other => panic!("expected tensor, got {:?}", other),
1770 }
1771 });
1772 }
1773
1774 #[test]
1775 #[cfg(feature = "doc_export")]
1776 fn doc_examples_present() {
1777 let examples = test_support::doc_examples(DOC_MD);
1778 assert!(!examples.is_empty());
1779 }
1780}