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