use std::collections::HashMap;
use std::sync::{
atomic::{AtomicU64, Ordering},
RwLock,
};
use once_cell::sync::Lazy;
use runmat_builtins::{CharArray, HandleRef, IntValue, LogicalArray, StructValue, Tensor, Value};
use runmat_macros::runtime_builtin;
use crate::builtins::common::random_args::keyword_of;
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::containers::type_resolvers::{
map_cell_type, map_handle_type, map_is_key_type, map_unknown_type,
};
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
const CLASS_NAME: &str = "containers.Map";
const MISSING_KEY_ERR: &str = "containers.Map: The specified key is not present in this container.";
const BUILTIN_CONSTRUCTOR: &str = "containers.Map";
const BUILTIN_KEYS: &str = "containers.Map.keys";
const BUILTIN_VALUES: &str = "containers.Map.values";
const BUILTIN_IS_KEY: &str = "containers.Map.isKey";
const BUILTIN_REMOVE: &str = "containers.Map.remove";
const BUILTIN_SUBSREF: &str = "containers.Map.subsref";
const BUILTIN_SUBSASGN: &str = "containers.Map.subsasgn";
#[runmat_macros::register_gpu_spec(
builtin_path = "crate::builtins::containers::map::containers_map"
)]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "containers.Map",
op_kind: GpuOpKind::Custom("map"),
supported_precisions: &[],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::GatherImmediately,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes: "Map storage is host-resident; GPU inputs are gathered only when split into multiple entries.",
};
fn map_error(message: impl Into<String>, builtin: &'static str) -> RuntimeError {
build_runtime_error(message).with_builtin(builtin).build()
}
fn attach_builtin_context(mut error: RuntimeError, builtin: &'static str) -> RuntimeError {
if error.context.builtin.is_none() {
error.context = error.context.with_builtin(builtin);
}
error
}
#[runmat_macros::register_fusion_spec(
builtin_path = "crate::builtins::containers::map::containers_map"
)]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "containers.Map",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "Handles act as fusion sinks; map construction terminates GPU fusion plans.",
};
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
static MAP_REGISTRY: Lazy<RwLock<HashMap<u64, MapStore>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum KeyType {
Char,
String,
Double,
Single,
Int32,
UInt32,
Int64,
UInt64,
Logical,
}
impl KeyType {
fn matlab_name(self) -> &'static str {
match self {
KeyType::Char => "char",
KeyType::String => "string",
KeyType::Double => "double",
KeyType::Single => "single",
KeyType::Int32 => "int32",
KeyType::UInt32 => "uint32",
KeyType::Int64 => "int64",
KeyType::UInt64 => "uint64",
KeyType::Logical => "logical",
}
}
fn parse(value: &Value, builtin: &'static str) -> BuiltinResult<Self> {
let text = string_from_value(value, "containers.Map: expected a KeyType string", builtin)?;
match text.to_ascii_lowercase().as_str() {
"char" | "character" => Ok(KeyType::Char),
"string" => Ok(KeyType::String),
"double" => Ok(KeyType::Double),
"single" => Ok(KeyType::Single),
"int32" => Ok(KeyType::Int32),
"uint32" => Ok(KeyType::UInt32),
"int64" => Ok(KeyType::Int64),
"uint64" => Ok(KeyType::UInt64),
"logical" => Ok(KeyType::Logical),
other => Err(map_error(
format!(
"containers.Map: unsupported KeyType '{other}'. Valid types: char, string, double, int32, uint32, int64, uint64, logical."
),
builtin,
)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ValueType {
Any,
Char,
String,
Double,
Single,
Logical,
}
impl ValueType {
fn matlab_name(self) -> &'static str {
match self {
ValueType::Any => "any",
ValueType::Char => "char",
ValueType::String => "string",
ValueType::Double => "double",
ValueType::Single => "single",
ValueType::Logical => "logical",
}
}
fn parse(value: &Value, builtin: &'static str) -> BuiltinResult<Self> {
let text = string_from_value(
value,
"containers.Map: expected a ValueType string",
builtin,
)?;
match text.to_ascii_lowercase().as_str() {
"any" => Ok(ValueType::Any),
"char" | "character" => Ok(ValueType::Char),
"string" => Ok(ValueType::String),
"double" => Ok(ValueType::Double),
"single" => Ok(ValueType::Single),
"logical" => Ok(ValueType::Logical),
other => Err(map_error(
format!(
"containers.Map: unsupported ValueType '{other}'. Valid types: any, char, string, double, single, logical."
),
builtin,
)),
}
}
fn normalize(&self, value: Value, builtin: &'static str) -> BuiltinResult<Value> {
match self {
ValueType::Any => Ok(value),
ValueType::Char => {
let chars = char_array_from_value(&value, builtin)?;
Ok(Value::CharArray(chars))
}
ValueType::String => {
let text = string_from_value(
&value,
"containers.Map: values must be string scalars",
builtin,
)?;
Ok(Value::String(text))
}
ValueType::Double | ValueType::Single => normalize_numeric_value(value, builtin),
ValueType::Logical => normalize_logical_value(value, builtin),
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
enum NormalizedKey {
String(String),
Float(u64),
Int(i64),
UInt(u64),
Bool(bool),
}
#[derive(Clone)]
struct MapEntry {
normalized: NormalizedKey,
key_value: Value,
value: Value,
}
struct MapStore {
key_type: KeyType,
value_type: ValueType,
uniform_values: bool,
uniform_class: Option<ValueClass>,
entries: Vec<MapEntry>,
index: HashMap<NormalizedKey, usize>,
}
impl MapStore {
fn new(key_type: KeyType, value_type: ValueType, uniform_values: bool) -> Self {
Self {
key_type,
value_type,
uniform_values,
uniform_class: None,
entries: Vec::new(),
index: HashMap::new(),
}
}
fn len(&self) -> usize {
self.entries.len()
}
fn contains(&self, key: &NormalizedKey) -> bool {
self.index.contains_key(key)
}
fn get(&self, key: &NormalizedKey) -> Option<Value> {
self.index
.get(key)
.map(|&idx| self.entries[idx].value.clone())
}
fn insert_new(&mut self, mut entry: MapEntry, builtin: &'static str) -> BuiltinResult<()> {
if self.index.contains_key(&entry.normalized) {
return Err(map_error(
"containers.Map: Duplicate key name was provided.",
builtin,
));
}
entry.value = self.normalize_value(entry.value, builtin)?;
self.track_uniform_class(&entry.value, builtin)?;
let idx = self.entries.len();
self.entries.push(entry.clone());
self.index.insert(entry.normalized, idx);
Ok(())
}
fn set(&mut self, mut entry: MapEntry, builtin: &'static str) -> BuiltinResult<()> {
entry.value = self.normalize_value(entry.value, builtin)?;
self.track_uniform_class(&entry.value, builtin)?;
if let Some(&idx) = self.index.get(&entry.normalized) {
self.entries[idx].value = entry.value.clone();
self.entries[idx].key_value = entry.key_value;
} else {
let idx = self.entries.len();
self.entries.push(entry.clone());
self.index.insert(entry.normalized, idx);
}
Ok(())
}
fn remove(&mut self, key: &NormalizedKey, builtin: &'static str) -> BuiltinResult<()> {
let idx = match self.index.get(key) {
Some(&idx) => idx,
None => {
return Err(map_error(MISSING_KEY_ERR, builtin));
}
};
self.entries.remove(idx);
self.index.clear();
for (pos, entry) in self.entries.iter().enumerate() {
self.index.insert(entry.normalized.clone(), pos);
}
if self.entries.is_empty() {
self.uniform_class = None;
}
Ok(())
}
fn keys(&self) -> Vec<Value> {
self.entries
.iter()
.map(|entry| entry.key_value.clone())
.collect()
}
fn values(&self) -> Vec<Value> {
self.entries
.iter()
.map(|entry| entry.value.clone())
.collect()
}
fn normalize_value(&self, value: Value, builtin: &'static str) -> BuiltinResult<Value> {
self.value_type.normalize(value, builtin)
}
fn track_uniform_class(&mut self, value: &Value, builtin: &'static str) -> BuiltinResult<()> {
if !self.uniform_values {
return Ok(());
}
let class = ValueClass::from_value(value);
if let Some(existing) = &self.uniform_class {
if existing != &class {
return Err(map_error(
"containers.Map: UniformValues=true requires all values to share the same MATLAB class.",
builtin,
));
}
} else {
self.uniform_class = Some(class);
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum ValueClass {
Char,
String,
Double,
Logical,
Int,
UInt,
Cell,
Struct,
Object,
Other(&'static str),
}
impl ValueClass {
fn from_value(value: &Value) -> Self {
match value {
Value::CharArray(_) => ValueClass::Char,
Value::String(_) | Value::StringArray(_) => ValueClass::String,
Value::Num(_) | Value::Tensor(_) | Value::ComplexTensor(_) => ValueClass::Double,
Value::Bool(_) | Value::LogicalArray(_) => ValueClass::Logical,
Value::Int(i) => match i {
IntValue::I8(_) | IntValue::I16(_) | IntValue::I32(_) | IntValue::I64(_) => {
ValueClass::Int
}
IntValue::U8(_) | IntValue::U16(_) | IntValue::U32(_) | IntValue::U64(_) => {
ValueClass::UInt
}
},
Value::Cell(_) => ValueClass::Cell,
Value::Struct(_) => ValueClass::Struct,
Value::Object(_) | Value::HandleObject(_) | Value::Listener(_) => ValueClass::Object,
_ => ValueClass::Other("other"),
}
}
}
struct ConstructorArgs {
key_type: KeyType,
value_type: ValueType,
uniform_values: bool,
keys: Vec<KeyCandidate>,
values: Vec<Value>,
}
struct KeyCandidate {
normalized: NormalizedKey,
canonical: Value,
}
#[runtime_builtin(
name = "containers.Map",
category = "containers/map",
summary = "Create MATLAB-style dictionary objects that map keys to values.",
keywords = "map,containers.Map,dictionary,hash map,lookup",
accel = "metadata",
sink = true,
type_resolver(map_handle_type),
builtin_path = "crate::builtins::containers::map::containers_map"
)]
async fn containers_map_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
let parsed = parse_constructor_args(args, BUILTIN_CONSTRUCTOR).await?;
let store = build_store(parsed, BUILTIN_CONSTRUCTOR)?;
allocate_handle(store, BUILTIN_CONSTRUCTOR)
}
#[runtime_builtin(
name = "containers.Map.keys",
type_resolver(map_cell_type),
builtin_path = "crate::builtins::containers::map::containers_map"
)]
async fn containers_map_keys(map: Value) -> crate::BuiltinResult<Value> {
with_store(&map, BUILTIN_KEYS, |store| {
let values = store.keys();
make_row_cell(values, BUILTIN_KEYS)
})
}
#[runtime_builtin(
name = "containers.Map.values",
type_resolver(map_cell_type),
builtin_path = "crate::builtins::containers::map::containers_map"
)]
async fn containers_map_values(map: Value) -> crate::BuiltinResult<Value> {
with_store(&map, BUILTIN_VALUES, |store| {
let values = store.values();
make_row_cell(values, BUILTIN_VALUES)
})
}
#[runtime_builtin(
name = "containers.Map.isKey",
type_resolver(map_is_key_type),
builtin_path = "crate::builtins::containers::map::containers_map"
)]
async fn containers_map_is_key(map: Value, key_spec: Value) -> crate::BuiltinResult<Value> {
let key_type = with_store(&map, BUILTIN_IS_KEY, |store| Ok(store.key_type))?;
let collection = collect_key_spec(&key_spec, key_type, BUILTIN_IS_KEY).await?;
with_store(&map, BUILTIN_IS_KEY, |store| {
let mut flags = Vec::with_capacity(collection.values.len());
for value in &collection.values {
let normalized = normalize_key(value, store.key_type, BUILTIN_IS_KEY)?;
flags.push(store.contains(&normalized));
}
if collection.values.len() == 1 {
Ok(Value::Bool(flags[0]))
} else {
let data: Vec<u8> = flags.into_iter().map(|b| if b { 1 } else { 0 }).collect();
let logical = LogicalArray::new(data, collection.shape)
.map_err(|e| map_error(format!("containers.Map: {e}"), BUILTIN_IS_KEY))?;
Ok(Value::LogicalArray(logical))
}
})
}
#[runtime_builtin(
name = "containers.Map.remove",
type_resolver(map_handle_type),
builtin_path = "crate::builtins::containers::map::containers_map"
)]
async fn containers_map_remove(map: Value, key_spec: Value) -> crate::BuiltinResult<Value> {
let key_type = with_store(&map, BUILTIN_REMOVE, |store| Ok(store.key_type))?;
let collection = collect_key_spec(&key_spec, key_type, BUILTIN_REMOVE).await?;
with_store_mut(&map, BUILTIN_REMOVE, |store| {
for value in &collection.values {
let normalized = normalize_key(value, store.key_type, BUILTIN_REMOVE)?;
store.remove(&normalized, BUILTIN_REMOVE)?;
}
Ok(())
})?;
Ok(map)
}
#[runtime_builtin(
name = "containers.Map.subsref",
type_resolver(map_unknown_type),
builtin_path = "crate::builtins::containers::map::containers_map"
)]
async fn containers_map_subsref(
map: Value,
kind: String,
payload: Value,
) -> crate::BuiltinResult<Value> {
if !matches!(map, Value::HandleObject(_)) {
return Err(map_error(
format!("containers.Map: subsref expects a containers.Map handle, got {map:?}"),
BUILTIN_SUBSREF,
));
}
match kind.as_str() {
"()" => {
let mut args = extract_key_arguments(&payload, BUILTIN_SUBSREF)?;
if args.is_empty() {
return Err(map_error(
"containers.Map: indexing requires at least one key",
BUILTIN_SUBSREF,
));
}
if args.len() != 1 {
return Err(map_error(
"containers.Map: indexing expects a single key argument",
BUILTIN_SUBSREF,
));
}
let key_arg = args.remove(0);
let key_type = with_store(&map, BUILTIN_SUBSREF, |store| Ok(store.key_type))?;
let collection = collect_key_spec(&key_arg, key_type, BUILTIN_SUBSREF).await?;
with_store(&map, BUILTIN_SUBSREF, |store| {
if collection.values.is_empty() {
return crate::make_cell_with_shape(Vec::new(), collection.shape.clone())
.map_err(|e| map_error(format!("containers.Map: {e}"), BUILTIN_SUBSREF));
}
if collection.values.len() == 1 {
let normalized =
normalize_key(&collection.values[0], store.key_type, BUILTIN_SUBSREF)?;
store
.get(&normalized)
.ok_or_else(|| map_error(MISSING_KEY_ERR, BUILTIN_SUBSREF))
} else {
let mut results = Vec::with_capacity(collection.values.len());
for value in &collection.values {
let normalized = normalize_key(value, store.key_type, BUILTIN_SUBSREF)?;
let stored = store
.get(&normalized)
.ok_or_else(|| map_error(MISSING_KEY_ERR, BUILTIN_SUBSREF))?;
results.push(stored);
}
crate::make_cell_with_shape(results, collection.shape.clone())
.map_err(|e| map_error(format!("containers.Map: {e}"), BUILTIN_SUBSREF))
}
})
}
"." => {
let field = string_from_value(
&payload,
"containers.Map: property name must be text",
BUILTIN_SUBSREF,
)?;
with_store(&map, BUILTIN_SUBSREF, |store| {
match field.to_ascii_lowercase().as_str() {
"count" => Ok(Value::Num(store.len() as f64)),
"keytype" => char_array_value(store.key_type.matlab_name(), BUILTIN_SUBSREF),
"valuetype" => {
char_array_value(store.value_type.matlab_name(), BUILTIN_SUBSREF)
}
other => Err(map_error(
format!("containers.Map: no such property '{other}'"),
BUILTIN_SUBSREF,
)),
}
})
}
"{}" => Err(map_error(
"containers.Map: curly-brace indexing is not supported.",
BUILTIN_SUBSREF,
)),
other => Err(map_error(
format!("containers.Map: unsupported indexing kind '{other}'"),
BUILTIN_SUBSREF,
)),
}
}
#[runtime_builtin(
name = "containers.Map.subsasgn",
type_resolver(map_handle_type),
builtin_path = "crate::builtins::containers::map::containers_map"
)]
async fn containers_map_subsasgn(
map: Value,
kind: String,
payload: Value,
rhs: Value,
) -> crate::BuiltinResult<Value> {
if !matches!(map, Value::HandleObject(_)) {
return Err(map_error(
format!("containers.Map: subsasgn expects a containers.Map handle, got {map:?}"),
BUILTIN_SUBSASGN,
));
}
match kind.as_str() {
"()" => {
let mut args = extract_key_arguments(&payload, BUILTIN_SUBSASGN)?;
if args.is_empty() {
return Err(map_error(
"containers.Map: assignment requires at least one key",
BUILTIN_SUBSASGN,
));
}
if args.len() != 1 {
return Err(map_error(
"containers.Map: assignment expects a single key argument",
BUILTIN_SUBSASGN,
));
}
let key_arg = args.remove(0);
let key_type = with_store(&map, BUILTIN_SUBSASGN, |store| Ok(store.key_type))?;
let KeyCollection {
values: key_values, ..
} = collect_key_spec(&key_arg, key_type, BUILTIN_SUBSASGN).await?;
let values =
expand_assignment_values(rhs.clone(), key_values.len(), BUILTIN_SUBSASGN).await?;
with_store_mut(&map, BUILTIN_SUBSASGN, move |store| {
for (key_raw, value) in key_values.into_iter().zip(values.into_iter()) {
let (normalized, canonical) =
canonicalize_key(key_raw, store.key_type, BUILTIN_SUBSASGN)?;
let entry = MapEntry {
normalized,
key_value: canonical,
value,
};
store.set(entry, BUILTIN_SUBSASGN)?;
}
Ok(())
})?;
Ok(map)
}
"." => Err(map_error(
"containers.Map: property assignments are not supported.",
BUILTIN_SUBSASGN,
)),
"{}" => Err(map_error(
"containers.Map: curly-brace assignment is not supported.",
BUILTIN_SUBSASGN,
)),
other => Err(map_error(
format!("containers.Map: unsupported assignment kind '{other}'"),
BUILTIN_SUBSASGN,
)),
}
}
async fn parse_constructor_args(
args: Vec<Value>,
builtin: &'static str,
) -> BuiltinResult<ConstructorArgs> {
let mut index = 0usize;
let mut keys_input: Option<Value> = None;
let mut values_input: Option<Value> = None;
if index < args.len() && keyword_of(&args[index]).is_none() {
if args.len() < 2 {
return Err(map_error(
"containers.Map: constructor requires both keys and values when either is provided.",
builtin,
));
}
keys_input = Some(args[index].clone());
values_input = Some(args[index + 1].clone());
index += 2;
}
let mut key_type = KeyType::Char;
let mut value_type = ValueType::Any;
let mut uniform_values = false;
while index < args.len() {
let keyword = keyword_of(&args[index]).ok_or_else(|| {
map_error(
"containers.Map: expected option name (e.g. 'KeyType')",
builtin,
)
})?;
index += 1;
let Some(value) = args.get(index) else {
return Err(map_error(
format!("containers.Map: missing value for option '{keyword}'"),
builtin,
));
};
index += 1;
match keyword.as_str() {
"keytype" => key_type = KeyType::parse(value, builtin)?,
"valuetype" => value_type = ValueType::parse(value, builtin)?,
"uniformvalues" => {
uniform_values = bool_from_value(
value,
"containers.Map: UniformValues must be logical",
builtin,
)?
}
"comparisonmethod" => {
let text = string_from_value(
value,
"containers.Map: ComparisonMethod must be a string",
builtin,
)?;
let lowered = text.to_ascii_lowercase();
if lowered != "strcmp" {
return Err(map_error(
"containers.Map: only ComparisonMethod='strcmp' is supported.",
builtin,
));
}
}
other => {
return Err(map_error(
format!("containers.Map: unrecognised option '{other}'"),
builtin,
));
}
}
}
let keys = match keys_input {
Some(value) => prepare_keys(value, key_type, builtin).await?,
None => Vec::new(),
};
let values = match values_input {
Some(value) => prepare_values(value, builtin).await?,
None => Vec::new(),
};
if keys.len() != values.len() {
return Err(map_error(
format!(
"containers.Map: number of keys ({}) must match number of values ({})",
keys.len(),
values.len()
),
builtin,
));
}
Ok(ConstructorArgs {
key_type,
value_type,
uniform_values,
keys,
values,
})
}
fn build_store(args: ConstructorArgs, builtin: &'static str) -> BuiltinResult<MapStore> {
let mut store = MapStore::new(args.key_type, args.value_type, args.uniform_values);
for (candidate, value) in args.keys.into_iter().zip(args.values.into_iter()) {
store.insert_new(
MapEntry {
normalized: candidate.normalized,
key_value: candidate.canonical,
value,
},
builtin,
)?;
}
Ok(store)
}
fn allocate_handle(store: MapStore, builtin: &'static str) -> BuiltinResult<Value> {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
MAP_REGISTRY
.write()
.map_err(|_| map_error("containers.Map: registry lock poisoned", builtin))?
.insert(id, store);
let mut struct_value = StructValue::new();
struct_value
.fields
.insert("id".to_string(), Value::Int(IntValue::U64(id)));
let storage = Value::Struct(struct_value);
let gc = runmat_gc::gc_allocate(storage)
.map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
Ok(Value::HandleObject(HandleRef {
class_name: CLASS_NAME.to_string(),
target: gc,
valid: true,
}))
}
fn with_store<F, R>(map: &Value, builtin: &'static str, f: F) -> BuiltinResult<R>
where
F: FnOnce(&MapStore) -> BuiltinResult<R>,
{
let handle = extract_handle(map, builtin)?;
ensure_handle(handle, builtin)?;
let id = map_id(handle, builtin)?;
let guard = MAP_REGISTRY
.read()
.map_err(|_| map_error("containers.Map: registry lock poisoned", builtin))?;
let store = guard
.get(&id)
.ok_or_else(|| map_error("containers.Map: internal storage not found", builtin))?;
f(store)
}
fn with_store_mut<F, R>(map: &Value, builtin: &'static str, f: F) -> BuiltinResult<R>
where
F: FnOnce(&mut MapStore) -> BuiltinResult<R>,
{
let handle = extract_handle(map, builtin)?;
ensure_handle(handle, builtin)?;
let id = map_id(handle, builtin)?;
let mut guard = MAP_REGISTRY
.write()
.map_err(|_| map_error("containers.Map: registry lock poisoned", builtin))?;
let store = guard
.get_mut(&id)
.ok_or_else(|| map_error("containers.Map: internal storage not found", builtin))?;
f(store)
}
fn extract_handle<'a>(value: &'a Value, builtin: &'static str) -> BuiltinResult<&'a HandleRef> {
match value {
Value::HandleObject(handle) => Ok(handle),
_ => Err(map_error(
"containers.Map: expected a containers.Map handle",
builtin,
)),
}
}
fn ensure_handle(handle: &HandleRef, builtin: &'static str) -> BuiltinResult<()> {
if !handle.valid {
return Err(map_error("containers.Map: handle is invalid", builtin));
}
if handle.class_name != CLASS_NAME {
return Err(map_error(
format!(
"containers.Map: expected handle of class '{}', got '{}'",
CLASS_NAME, handle.class_name
),
builtin,
));
}
Ok(())
}
fn map_id(handle: &HandleRef, builtin: &'static str) -> BuiltinResult<u64> {
let storage = unsafe { &*handle.target.as_raw() };
match storage {
Value::Struct(StructValue { fields }) => match fields.get("id") {
Some(Value::Int(IntValue::U64(id))) => Ok(*id),
Some(Value::Int(other)) => {
let id = other.to_i64();
if id < 0 {
Err(map_error(
"containers.Map: negative map identifier",
builtin,
))
} else {
Ok(id as u64)
}
}
Some(Value::Num(n)) if *n >= 0.0 => Ok(*n as u64),
_ => Err(map_error(
"containers.Map: corrupted storage identifier",
builtin,
)),
},
other => Err(map_error(
format!("containers.Map: internal storage has unexpected shape {other:?}"),
builtin,
)),
}
}
async fn prepare_keys(
value: Value,
key_type: KeyType,
builtin: &'static str,
) -> BuiltinResult<Vec<KeyCandidate>> {
let host = gather_if_needed_async(&value)
.await
.map_err(|err| attach_builtin_context(err, builtin))?;
let flattened = flatten_keys(&host, key_type, builtin).await?;
let mut out = Vec::with_capacity(flattened.len());
for raw_key in flattened {
let (normalized, canonical) = canonicalize_key(raw_key, key_type, builtin)?;
out.push(KeyCandidate {
normalized,
canonical,
});
}
Ok(out)
}
async fn prepare_values(value: Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
let host = gather_if_needed_async(&value)
.await
.map_err(|err| attach_builtin_context(err, builtin))?;
flatten_values(&host, builtin).await
}
async fn flatten_keys(
value: &Value,
key_type: KeyType,
builtin: &'static str,
) -> BuiltinResult<Vec<Value>> {
match value {
Value::Cell(cell) => {
let mut out = Vec::with_capacity(cell.data.len());
for ptr in &cell.data {
let element = unsafe { &*ptr.as_raw() };
if matches!(element, Value::Cell(_)) {
return Err(map_error(
"containers.Map: nested cell arrays are not supported for keys",
builtin,
));
}
out.push(
gather_if_needed_async(element)
.await
.map_err(|err| attach_builtin_context(err, builtin))?,
);
}
Ok(out)
}
Value::StringArray(sa) => Ok(sa
.data
.iter()
.map(|text| Value::String(text.clone()))
.collect()),
Value::CharArray(ca) => Ok(char_array_rows(ca, builtin)?),
Value::LogicalArray(arr) => {
if key_type != KeyType::Logical {
return Err(map_error(
"containers.Map: logical arrays can only be used with KeyType='logical'",
builtin,
));
}
Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect())
}
Value::Tensor(t) => {
if !t.shape.is_empty() && t.data.len() != 1 && !is_vector_shape(&t.shape) {
return Err(map_error(
"containers.Map: numeric keys must be scalar or vector shaped",
builtin,
));
}
Ok(t.data.iter().map(|&v| Value::Num(v)).collect())
}
Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::String(_) => {
Ok(vec![value.clone()])
}
Value::GpuTensor(_) => Err(map_error(
"containers.Map: GPU keys must be gathered to the host before construction",
builtin,
)),
other => Err(map_error(
format!("containers.Map: unsupported key container {other:?}"),
builtin,
)),
}
}
async fn flatten_values(value: &Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
match value {
Value::Cell(cell) => {
let mut out = Vec::with_capacity(cell.data.len());
for ptr in &cell.data {
out.push(
gather_if_needed_async(unsafe { &*ptr.as_raw() })
.await
.map_err(|err| attach_builtin_context(err, builtin))?,
);
}
Ok(out)
}
Value::StringArray(sa) => Ok(sa
.data
.iter()
.map(|text| Value::String(text.clone()))
.collect()),
Value::CharArray(ca) => Ok(char_array_rows(ca, builtin)?),
Value::LogicalArray(arr) => Ok(arr.data.iter().map(|&b| Value::Bool(b != 0)).collect()),
Value::Tensor(t) => {
if !t.shape.is_empty() && !is_vector_shape(&t.shape) && t.data.len() != 1 {
return Err(map_error(
"containers.Map: numeric values must be scalar or vector shaped",
builtin,
));
}
Ok(t.data.iter().map(|&v| Value::Num(v)).collect())
}
_ => Ok(vec![value.clone()]),
}
}
fn char_array_rows(ca: &CharArray, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
if ca.rows == 0 {
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(ca.rows);
for row in 0..ca.rows {
let mut text = String::with_capacity(ca.cols);
for col in 0..ca.cols {
text.push(ca.data[row * ca.cols + col]);
}
let chars: Vec<char> = text.chars().collect();
let array = CharArray::new(chars.clone(), 1, chars.len())
.map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
out.push(Value::CharArray(array));
}
Ok(out)
}
fn is_vector_shape(shape: &[usize]) -> bool {
match shape.len() {
0 => true,
1 => true,
2 => shape[0] == 1 || shape[1] == 1,
_ => false,
}
}
fn canonicalize_key(
value: Value,
key_type: KeyType,
builtin: &'static str,
) -> BuiltinResult<(NormalizedKey, Value)> {
let normalized = normalize_key(&value, key_type, builtin)?;
let canonical = match key_type {
KeyType::Char => Value::CharArray(char_array_from_value(&value, builtin)?),
KeyType::String => Value::String(string_from_value(
&value,
"containers.Map: keys must be string scalars",
builtin,
)?),
KeyType::Double => Value::Num(numeric_from_value(
&value,
"containers.Map: keys must be numeric scalars",
builtin,
)?),
KeyType::Single => Value::Num(numeric_from_value(
&value,
"containers.Map: keys must be numeric scalars",
builtin,
)?),
KeyType::Int32 => Value::Int(IntValue::I32(integer_from_value(
&value,
i32::MIN as i64,
i32::MAX as i64,
"containers.Map: int32 keys must be integers",
builtin,
)? as i32)),
KeyType::UInt32 => Value::Int(IntValue::U32(unsigned_from_value(
&value,
u32::MAX as u64,
"containers.Map: uint32 keys must be unsigned integers",
builtin,
)? as u32)),
KeyType::Int64 => Value::Int(IntValue::I64(integer_from_value(
&value,
i64::MIN,
i64::MAX,
"containers.Map: int64 keys must be integers",
builtin,
)?)),
KeyType::UInt64 => Value::Int(IntValue::U64(unsigned_from_value(
&value,
u64::MAX,
"containers.Map: uint64 keys must be unsigned integers",
builtin,
)?)),
KeyType::Logical => Value::Bool(bool_from_value(
&value,
"containers.Map: logical keys must be logical scalars",
builtin,
)?),
};
Ok((normalized, canonical))
}
fn normalize_key(
value: &Value,
key_type: KeyType,
builtin: &'static str,
) -> BuiltinResult<NormalizedKey> {
match key_type {
KeyType::Char | KeyType::String => {
let text =
string_from_value(value, "containers.Map: keys must be text scalars", builtin)?;
Ok(NormalizedKey::String(text))
}
KeyType::Double | KeyType::Single => {
let numeric = numeric_from_value(
value,
"containers.Map: keys must be numeric scalars",
builtin,
)?;
if !numeric.is_finite() {
return Err(map_error(
"containers.Map: keys must be finite numeric scalars",
builtin,
));
}
let canonical = if numeric == 0.0 { 0.0 } else { numeric };
Ok(NormalizedKey::Float(canonical.to_bits()))
}
KeyType::Int32 | KeyType::Int64 => {
let bounds = if key_type == KeyType::Int32 {
(i32::MIN as i64, i32::MAX as i64)
} else {
(i64::MIN, i64::MAX)
};
let value = integer_from_value(
value,
bounds.0,
bounds.1,
"containers.Map: integer keys must be whole numbers",
builtin,
)?;
Ok(NormalizedKey::Int(value))
}
KeyType::UInt32 | KeyType::UInt64 => {
let limit = if key_type == KeyType::UInt32 {
u32::MAX as u64
} else {
u64::MAX
};
let value = unsigned_from_value(
value,
limit,
"containers.Map: unsigned keys must be non-negative integers",
builtin,
)?;
Ok(NormalizedKey::UInt(value))
}
KeyType::Logical => {
let flag = bool_from_value(
value,
"containers.Map: logical keys must be logical scalars",
builtin,
)?;
Ok(NormalizedKey::Bool(flag))
}
}
}
fn string_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<String> {
match value {
Value::String(s) => Ok(s.clone()),
Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
Value::CharArray(ca) if ca.rows == 1 => Ok(ca.data.iter().collect()),
_ => Err(map_error(context, builtin)),
}
}
fn char_array_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<CharArray> {
match value {
Value::CharArray(ca) if ca.rows == 1 => Ok(ca.clone()),
Value::String(s) => {
let chars: Vec<char> = s.chars().collect();
CharArray::new(chars.clone(), 1, chars.len())
.map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
}
Value::StringArray(sa) if sa.data.len() == 1 => {
let chars: Vec<char> = sa.data[0].chars().collect();
CharArray::new(chars.clone(), 1, chars.len())
.map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
}
_ => Err(map_error(
"containers.Map: keys must be character vectors",
builtin,
)),
}
}
fn char_array_value(text: &str, builtin: &'static str) -> BuiltinResult<Value> {
let chars: Vec<char> = text.chars().collect();
CharArray::new(chars.clone(), 1, chars.len())
.map(Value::CharArray)
.map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
}
fn normalize_numeric_value(value: Value, builtin: &'static str) -> BuiltinResult<Value> {
match value {
Value::Num(_) | Value::Tensor(_) => Ok(value),
Value::Int(i) => Ok(Value::Num(i.to_f64())),
Value::Bool(b) => Ok(Value::Num(if b { 1.0 } else { 0.0 })),
Value::LogicalArray(arr) => {
let data: Vec<f64> = arr
.data
.iter()
.map(|&b| if b != 0 { 1.0 } else { 0.0 })
.collect();
let tensor = Tensor::new(data, arr.shape.clone())
.map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
Ok(Value::Tensor(tensor))
}
Value::Cell(_)
| Value::Struct(_)
| Value::Object(_)
| Value::HandleObject(_)
| Value::Listener(_)
| Value::String(_)
| Value::StringArray(_)
| Value::CharArray(_)
| Value::Complex(_, _)
| Value::ComplexTensor(_)
| Value::FunctionHandle(_)
| Value::Closure(_)
| Value::ClassRef(_)
| Value::MException(_)
| Value::GpuTensor(_)
| Value::OutputList(_) => Err(map_error(
"containers.Map: values must be numeric when ValueType is 'double' or 'single'",
builtin,
)),
}
}
fn normalize_logical_value(value: Value, builtin: &'static str) -> BuiltinResult<Value> {
match value {
Value::Bool(_) | Value::LogicalArray(_) => Ok(value),
Value::Int(i) => Ok(Value::Bool(i.to_i64() != 0)),
Value::Num(n) => Ok(Value::Bool(n != 0.0)),
Value::Tensor(t) => {
let flags: Vec<u8> = t
.data
.iter()
.map(|&v| if v != 0.0 { 1 } else { 0 })
.collect();
let logical = LogicalArray::new(flags, t.shape.clone())
.map_err(|e| map_error(format!("containers.Map: {e}"), builtin))?;
Ok(Value::LogicalArray(logical))
}
Value::CharArray(_)
| Value::String(_)
| Value::StringArray(_)
| Value::Struct(_)
| Value::Cell(_)
| Value::Object(_)
| Value::HandleObject(_)
| Value::Listener(_)
| Value::Complex(_, _)
| Value::ComplexTensor(_)
| Value::FunctionHandle(_)
| Value::Closure(_)
| Value::ClassRef(_)
| Value::MException(_)
| Value::GpuTensor(_)
| Value::OutputList(_) => Err(map_error(
"containers.Map: values must be logical when ValueType is 'logical'",
builtin,
)),
}
}
fn numeric_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<f64> {
match value {
Value::Num(n) => Ok(*n),
Value::Int(i) => Ok(i.to_f64()),
Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
Value::LogicalArray(arr) if arr.data.len() == 1 => {
Ok(if arr.data[0] != 0 { 1.0 } else { 0.0 })
}
_ => Err(map_error(context, builtin)),
}
}
fn integer_from_value(
value: &Value,
min: i64,
max: i64,
context: &str,
builtin: &'static str,
) -> BuiltinResult<i64> {
match value {
Value::Int(i) => {
let v = i.to_i64();
if v < min || v > max {
return Err(map_error(context, builtin));
}
Ok(v)
}
Value::Num(n) => {
if !n.is_finite() {
return Err(map_error(context, builtin));
}
if (*n < min as f64) || (*n > max as f64) {
return Err(map_error(context, builtin));
}
if (n.round() - n).abs() > f64::EPSILON {
return Err(map_error(context, builtin));
}
Ok(n.round() as i64)
}
Value::Bool(b) => {
let v = if *b { 1 } else { 0 };
if v < min || v > max {
return Err(map_error(context, builtin));
}
Ok(v)
}
_ => Err(map_error(context, builtin)),
}
}
fn unsigned_from_value(
value: &Value,
max: u64,
context: &str,
builtin: &'static str,
) -> BuiltinResult<u64> {
match value {
Value::Int(i) => {
let v = i.to_i64();
if v < 0 || v as u64 > max {
return Err(map_error(context, builtin));
}
Ok(v as u64)
}
Value::Num(n) => {
if !n.is_finite() || *n < 0.0 || *n > max as f64 {
return Err(map_error(context, builtin));
}
if (n.round() - n).abs() > f64::EPSILON {
return Err(map_error(context, builtin));
}
Ok(n.round() as u64)
}
Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
_ => Err(map_error(context, builtin)),
}
}
fn bool_from_value(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<bool> {
match value {
Value::Bool(b) => Ok(*b),
Value::LogicalArray(arr) if arr.data.len() == 1 => Ok(arr.data[0] != 0),
Value::Int(i) => Ok(i.to_i64() != 0),
Value::Num(n) => Ok(*n != 0.0),
_ => Err(map_error(context, builtin)),
}
}
fn make_row_cell(values: Vec<Value>, builtin: &'static str) -> BuiltinResult<Value> {
let cols = values.len();
crate::make_cell_with_shape(values, vec![1, cols])
.map_err(|e| map_error(format!("containers.Map: {e}"), builtin))
}
fn extract_key_arguments(payload: &Value, builtin: &'static str) -> BuiltinResult<Vec<Value>> {
match payload {
Value::Cell(cell) => {
let mut out = Vec::with_capacity(cell.data.len());
for ptr in &cell.data {
out.push(unsafe { &*ptr.as_raw() }.clone());
}
Ok(out)
}
other => Err(map_error(
format!("containers.Map: expected key arguments in a cell array, got {other:?}"),
builtin,
)),
}
}
async fn expand_assignment_values(
value: Value,
expected: usize,
builtin: &'static str,
) -> BuiltinResult<Vec<Value>> {
let host = gather_if_needed_async(&value)
.await
.map_err(|err| attach_builtin_context(err, builtin))?;
let values = flatten_values(&host, builtin).await?;
if expected == 1 {
if values.is_empty() {
return Err(map_error(
"containers.Map: assignment requires a value",
builtin,
));
}
Ok(vec![values.into_iter().next().unwrap()])
} else {
if values.len() != expected {
return Err(map_error(
format!(
"containers.Map: assignment with {} keys requires {} values (got {})",
expected,
expected,
values.len()
),
builtin,
));
}
Ok(values)
}
}
struct KeyCollection {
values: Vec<Value>,
shape: Vec<usize>,
}
async fn collect_key_spec(
value: &Value,
key_type: KeyType,
builtin: &'static str,
) -> BuiltinResult<KeyCollection> {
let host = gather_if_needed_async(value)
.await
.map_err(|err| attach_builtin_context(err, builtin))?;
match &host {
Value::Cell(cell) => {
let mut values = Vec::with_capacity(cell.data.len());
for ptr in &cell.data {
values.push(
gather_if_needed_async(unsafe { &*ptr.as_raw() })
.await
.map_err(|err| attach_builtin_context(err, builtin))?,
);
}
Ok(KeyCollection {
values,
shape: vec![cell.rows, cell.cols],
})
}
Value::StringArray(sa) => Ok(KeyCollection {
values: sa.data.iter().map(|s| Value::String(s.clone())).collect(),
shape: vec![sa.rows(), sa.cols()],
}),
Value::CharArray(ca) => {
let rows = if ca.rows == 0 { 0 } else { ca.rows };
Ok(KeyCollection {
values: char_array_rows(ca, builtin)?,
shape: vec![rows, 1],
})
}
Value::LogicalArray(arr) if key_type == KeyType::Logical => Ok(KeyCollection {
values: arr.data.iter().map(|&b| Value::Bool(b != 0)).collect(),
shape: arr.shape.clone(),
}),
Value::Tensor(t) if key_type != KeyType::Char && key_type != KeyType::String => {
Ok(KeyCollection {
values: t.data.iter().map(|&n| Value::Num(n)).collect(),
shape: t.shape.clone(),
})
}
_ => Ok(KeyCollection {
values: vec![host.clone()],
shape: vec![1, 1],
}),
}
}
pub fn map_length(value: &Value) -> Option<usize> {
if let Value::HandleObject(handle) = value {
if handle.valid && handle.class_name == CLASS_NAME {
if let Ok(id) = map_id(handle, BUILTIN_CONSTRUCTOR) {
if let Ok(registry) = MAP_REGISTRY.read() {
return registry.get(&id).map(|store| store.len());
}
}
}
}
None
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::builtins::common::test_support;
use futures::executor::block_on;
use runmat_builtins::{ResolveContext, Type};
fn error_message(err: crate::RuntimeError) -> String {
err.message.clone()
}
fn containers_map_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
block_on(super::containers_map_builtin(args))
}
fn containers_map_keys(map: Value) -> BuiltinResult<Value> {
block_on(super::containers_map_keys(map))
}
fn containers_map_is_key(map: Value, key_spec: Value) -> BuiltinResult<Value> {
block_on(super::containers_map_is_key(map, key_spec))
}
fn containers_map_remove(map: Value, key_spec: Value) -> BuiltinResult<Value> {
block_on(super::containers_map_remove(map, key_spec))
}
fn containers_map_subsref(map: Value, kind: String, payload: Value) -> BuiltinResult<Value> {
block_on(super::containers_map_subsref(map, kind, payload))
}
fn containers_map_subsasgn(
map: Value,
kind: String,
payload: Value,
rhs: Value,
) -> BuiltinResult<Value> {
block_on(super::containers_map_subsasgn(map, kind, payload, rhs))
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn construct_empty_map_defaults() {
let map = containers_map_builtin(Vec::new()).expect("map");
let count = containers_map_subsref(
map.clone(),
".".to_string(),
Value::from("Count".to_string()),
)
.expect("Count");
assert_eq!(count, Value::Num(0.0));
let key_type = containers_map_subsref(
map.clone(),
".".to_string(),
Value::from("KeyType".to_string()),
)
.expect("KeyType");
assert_eq!(
key_type,
Value::CharArray(CharArray::new("char".chars().collect(), 1, 4).unwrap())
);
let value_type = containers_map_subsref(
map.clone(),
".".to_string(),
Value::from("ValueType".to_string()),
)
.expect("ValueType");
assert_eq!(
value_type,
Value::CharArray(CharArray::new("any".chars().collect(), 1, 3).unwrap())
);
}
#[test]
fn map_type_resolvers_basics() {
let ctx = ResolveContext::new(Vec::new());
assert_eq!(map_handle_type(&[Type::Unknown], &ctx), Type::Unknown);
assert_eq!(map_cell_type(&[], &ctx), Type::cell());
assert_eq!(map_is_key_type(&[Type::String], &ctx), Type::logical());
assert_eq!(map_unknown_type(&[], &ctx), Type::Unknown);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn constructor_with_cells_lookup() {
let keys = crate::make_cell(vec![Value::from("apple"), Value::from("pear")], 1, 2).unwrap();
let values = crate::make_cell(vec![Value::Num(5.0), Value::Num(7.0)], 1, 2).unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let apple = containers_map_subsref(
map.clone(),
"()".to_string(),
crate::make_cell(vec![Value::from("apple")], 1, 1).unwrap(),
)
.expect("lookup");
assert_eq!(apple, Value::Num(5.0));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn constructor_rejects_duplicate_keys() {
let keys = crate::make_cell(vec![Value::from("dup"), Value::from("dup")], 1, 2).unwrap();
let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
let err = containers_map_builtin(vec![keys, values]).expect_err("duplicate check");
let message = error_message(err);
assert!(message.contains("Duplicate key name"));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn constructor_errors_when_value_count_mismatch() {
let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
let err = containers_map_builtin(vec![keys, values]).expect_err("count mismatch");
let message = error_message(err);
assert!(message.contains("number of keys"));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn comparison_method_rejects_unknown_values() {
let keys = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
let err = containers_map_builtin(vec![
keys,
values,
Value::from("ComparisonMethod"),
Value::from("caseinsensitive"),
])
.expect_err("comparison method");
let message = error_message(err);
assert!(message.contains("ComparisonMethod"));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn key_type_single_roundtrip() {
let map = containers_map_builtin(vec![Value::from("KeyType"), Value::from("single")])
.expect("map");
let key_type = containers_map_subsref(map.clone(), ".".to_string(), Value::from("KeyType"))
.expect("keytype");
assert_eq!(
key_type,
Value::CharArray(CharArray::new("single".chars().collect(), 1, 6).unwrap())
);
let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(7.0))
.expect("assign");
let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn value_type_double_converts_integers() {
let keys = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
let values = crate::make_cell(vec![Value::Int(IntValue::I32(7))], 1, 1).unwrap();
let map = containers_map_builtin(vec![
keys,
values,
Value::from("KeyType"),
Value::from("double"),
Value::from("ValueType"),
Value::from("double"),
])
.expect("map");
let payload = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
assert!(matches!(value, Value::Num(n) if (n - 7.0).abs() < 1e-12));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn value_type_logical_converts_numeric_arrays() {
let keys = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
let values = crate::make_cell(vec![Value::Tensor(tensor.clone())], 1, 1).unwrap();
let map = containers_map_builtin(vec![
keys,
values,
Value::from("ValueType"),
Value::from("logical"),
])
.expect("map");
let payload = crate::make_cell(vec![Value::from("mask")], 1, 1).unwrap();
let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
match value {
Value::LogicalArray(arr) => {
assert_eq!(arr.shape, vec![3, 1]);
assert_eq!(arr.data, vec![0, 1, 1]);
}
other => panic!("expected logical array, got {:?}", other),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn uniform_values_enforced_on_assignment() {
let map = containers_map_builtin(vec![Value::from("UniformValues"), Value::from(true)])
.expect("map");
let payload = crate::make_cell(vec![Value::from("x")], 1, 1).unwrap();
let map = containers_map_subsasgn(map, "()".to_string(), payload.clone(), Value::Num(1.0))
.expect("assign");
let err = containers_map_subsasgn(map, "()".to_string(), payload, Value::from("text"))
.expect_err("uniform enforcement");
let message = error_message(err);
assert!(message.contains("UniformValues"));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn assignment_updates_and_inserts() {
let map = containers_map_builtin(Vec::new()).expect("map");
let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
let updated = containers_map_subsasgn(
map.clone(),
"()".to_string(),
payload.clone(),
Value::Num(1.0),
)
.expect("assign");
let updated = containers_map_subsasgn(
updated.clone(),
"()".to_string(),
payload.clone(),
Value::Num(5.0),
)
.expect("update");
let beta_payload = crate::make_cell(vec![Value::from("beta")], 1, 1).unwrap();
let updated = containers_map_subsasgn(
updated.clone(),
"()".to_string(),
beta_payload,
Value::Num(9.0),
)
.expect("insert");
let value = containers_map_subsref(updated, "()".to_string(), payload).expect("lookup");
assert_eq!(value, Value::Num(5.0));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn subsref_multiple_keys_preserves_shape() {
let keys = crate::make_cell(
vec![Value::from("a"), Value::from("b"), Value::from("c")],
1,
3,
)
.unwrap();
let values = crate::make_cell(
vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1,
3,
)
.unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let request = crate::make_cell(vec![Value::from("a"), Value::from("c")], 1, 2).unwrap();
let payload = crate::make_cell(vec![request], 1, 1).unwrap();
let result =
containers_map_subsref(map.clone(), "()".to_string(), payload).expect("lookup");
match result {
Value::Cell(cell) => {
assert_eq!(cell.rows, 1);
assert_eq!(cell.cols, 2);
assert_eq!(cell.get(0, 0).expect("cell 0,0"), Value::Num(1.0));
assert_eq!(cell.get(0, 1).expect("cell 0,1"), Value::Num(3.0));
}
other => panic!("expected cell array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn subsref_empty_key_collection_returns_empty_cell() {
let keys = crate::make_cell(vec![Value::from("z")], 1, 1).unwrap();
let values = crate::make_cell(vec![Value::Num(42.0)], 1, 1).unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
let result = containers_map_subsref(map, "()".to_string(), payload).expect("lookup empty");
match result {
Value::Cell(cell) => {
assert_eq!(cell.rows, 1);
assert_eq!(cell.cols, 0);
assert!(cell.data.is_empty());
}
other => panic!("expected empty cell, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn subsasgn_with_cell_keys_updates_all_targets() {
let keys = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let key_spec = crate::make_cell(vec![Value::from("a"), Value::from("b")], 1, 2).unwrap();
let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
let new_values = crate::make_cell(vec![Value::Num(10.0), Value::Num(20.0)], 1, 2).unwrap();
let updated = containers_map_subsasgn(map.clone(), "()".to_string(), payload, new_values)
.expect("assign");
let a_payload = crate::make_cell(vec![Value::from("a")], 1, 1).unwrap();
let b_payload = crate::make_cell(vec![Value::from("b")], 1, 1).unwrap();
let a_value =
containers_map_subsref(updated.clone(), "()".to_string(), a_payload).expect("a lookup");
let b_value =
containers_map_subsref(updated, "()".to_string(), b_payload).expect("b lookup");
assert_eq!(a_value, Value::Num(10.0));
assert_eq!(b_value, Value::Num(20.0));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn assignment_value_count_mismatch_errors() {
let keys = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
let values = crate::make_cell(vec![Value::Num(1.0), Value::Num(2.0)], 1, 2).unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let key_spec = crate::make_cell(vec![Value::from("x"), Value::from("y")], 1, 2).unwrap();
let payload = crate::make_cell(vec![key_spec], 1, 1).unwrap();
let rhs = crate::make_cell(vec![Value::Num(99.0)], 1, 1).unwrap();
let err =
containers_map_subsasgn(map, "()".to_string(), payload, rhs).expect_err("value count");
let message = error_message(err);
assert!(message.contains("requires 2 values"));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn subsasgn_empty_key_collection_is_noop() {
let keys = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
let values = crate::make_cell(vec![Value::Num(7.0)], 1, 1).unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let empty_keys = crate::make_cell(Vec::new(), 1, 0).unwrap();
let payload = crate::make_cell(vec![empty_keys], 1, 1).unwrap();
let rhs = crate::make_cell(Vec::new(), 1, 0).unwrap();
let updated =
containers_map_subsasgn(map.clone(), "()".to_string(), payload, rhs).expect("assign");
let lookup_payload = crate::make_cell(vec![Value::from("root")], 1, 1).unwrap();
let value =
containers_map_subsref(updated, "()".to_string(), lookup_payload).expect("lookup");
assert_eq!(value, Value::Num(7.0));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn keys_values_iskey_remove() {
let keys = crate::make_cell(
vec![Value::from("a"), Value::from("b"), Value::from("c")],
1,
3,
)
.unwrap();
let values = crate::make_cell(
vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1,
3,
)
.unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let key_list = containers_map_keys(map.clone()).expect("keys");
match key_list {
Value::Cell(cell) => assert_eq!(cell.data.len(), 3),
other => panic!("expected cell array, got {other:?}"),
}
let mask = containers_map_is_key(
map.clone(),
crate::make_cell(vec![Value::from("a"), Value::from("z")], 1, 2).unwrap(),
)
.expect("mask");
match mask {
Value::LogicalArray(arr) => {
assert_eq!(arr.data, vec![1, 0]);
}
other => panic!("expected logical array, got {:?}", other),
}
let removed = containers_map_remove(
map.clone(),
crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
)
.expect("remove");
let mask = containers_map_is_key(
removed,
crate::make_cell(vec![Value::from("b")], 1, 1).unwrap(),
)
.expect("mask");
assert_eq!(mask, Value::Bool(false));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn remove_missing_key_returns_error() {
let keys = crate::make_cell(vec![Value::from("key")], 1, 1).unwrap();
let values = crate::make_cell(vec![Value::Num(1.0)], 1, 1).unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let err = containers_map_remove(
map,
crate::make_cell(vec![Value::from("missing")], 1, 1).unwrap(),
)
.expect_err("remove missing");
let message = error_message(err);
assert_eq!(message, MISSING_KEY_ERR);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn length_delegates_to_map_count() {
let keys = crate::make_cell(
vec![Value::from("a"), Value::from("b"), Value::from("c")],
1,
3,
)
.unwrap();
let values = crate::make_cell(
vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1,
3,
)
.unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
assert_eq!(map_length(&map), Some(3));
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn map_constructor_gathers_gpu_values() {
test_support::with_test_provider(|provider| {
let keys = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
let data = vec![1.0, 2.0, 3.0];
let shape = vec![3, 1];
let view = runmat_accelerate_api::HostTensorView {
data: &data,
shape: &shape,
};
let handle = provider.upload(&view).expect("upload");
let values = crate::make_cell(vec![Value::GpuTensor(handle)], 1, 1).unwrap();
let map = containers_map_builtin(vec![keys, values]).expect("map");
let payload = crate::make_cell(vec![Value::from("alpha")], 1, 1).unwrap();
let value = containers_map_subsref(map, "()".to_string(), payload).expect("lookup");
match value {
Value::Tensor(t) => {
assert_eq!(t.shape, shape);
assert_eq!(t.data, data);
}
other => panic!("expected tensor, got {:?}", other),
}
});
}
}