use glob::Pattern;
use regex::Regex;
use runmat_builtins::{CharArray, Value};
use runmat_macros::runtime_builtin;
use std::path::PathBuf;
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
};
use crate::builtins::introspection::type_resolvers::who_type;
use crate::builtins::io::mat::load::read_mat_file_for_builtin;
use crate::{build_runtime_error, gather_if_needed_async, make_cell, BuiltinResult, RuntimeError};
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::introspection::who")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "who",
op_kind: GpuOpKind::Custom("introspection"),
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: "Host-only builtin. Arguments are gathered from the GPU if necessary; no kernels are launched.",
};
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::introspection::who")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "who",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "Introspection builtin; registered for diagnostics only.",
};
#[runtime_builtin(
name = "who",
category = "introspection",
summary = "List the names of variables in the workspace or MAT-files (MATLAB-compatible).",
keywords = "who,workspace,variables,introspection",
accel = "cpu",
type_resolver(who_type),
builtin_path = "crate::builtins::introspection::who"
)]
async fn who_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
#[cfg(all(test, feature = "wgpu"))]
{
if runmat_accelerate_api::provider().is_none() {
let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
);
}
}
let mut gathered = Vec::with_capacity(args.len());
for arg in args {
gathered.push(gather_if_needed_async(&arg).await.map_err(who_flow)?);
}
let request = parse_request(&gathered).await?;
let mut entries = match &request.source {
WhoSource::Workspace => crate::workspace::snapshot().unwrap_or_default(),
WhoSource::File(path) => read_mat_file_for_builtin(path, "who")?,
};
if matches!(request.source, WhoSource::File(_)) {
entries.sort_by(|a, b| a.0.cmp(&b.0));
}
let global_names: std::collections::HashSet<String> =
if matches!(request.source, WhoSource::Workspace) {
crate::workspace::global_names().into_iter().collect()
} else {
std::collections::HashSet::new()
};
let mut names = Vec::new();
for (name, _value) in entries {
if !matches_filters(&name, &request.selectors, &request.regex_patterns) {
continue;
}
let is_global = global_names.contains(&name);
if request.only_global && !is_global {
continue;
}
names.push(name);
}
names.sort();
let mut cells = Vec::with_capacity(names.len());
for name in names.into_iter() {
cells.push(Value::String(name));
}
let rows = cells.len();
make_cell(cells, rows, 1).map_err(|err| build_runtime_error(err).with_builtin("who").build())
}
#[derive(Debug)]
struct WhoRequest {
source: WhoSource,
selectors: Vec<NameSelector>,
regex_patterns: Vec<Regex>,
only_global: bool,
}
#[derive(Debug)]
enum WhoSource {
Workspace,
File(PathBuf),
}
#[derive(Debug)]
enum NameSelector {
Exact(String),
Wildcard(Pattern),
}
fn who_error(message: impl Into<String>) -> RuntimeError {
build_runtime_error(message).with_builtin("who").build()
}
fn who_flow(mut err: RuntimeError) -> RuntimeError {
err.context = err.context.with_builtin("who");
err
}
async fn parse_request(values: &[Value]) -> BuiltinResult<WhoRequest> {
let mut idx = 0usize;
let mut path_value: Option<Value> = None;
let mut names: Vec<String> = Vec::new();
let mut regex_patterns = Vec::new();
let mut only_global = false;
while idx < values.len() {
if let Some(token) = option_token(&values[idx])? {
match token.as_str() {
"-file" => {
idx += 1;
if idx >= values.len() {
return Err(who_error("who: '-file' requires a filename"));
}
if path_value.is_some() {
return Err(who_error("who: '-file' may only be specified once"));
}
path_value = Some(values[idx].clone());
idx += 1;
continue;
}
"-regexp" => {
idx += 1;
if idx >= values.len() {
return Err(who_error("who: '-regexp' requires at least one pattern"));
}
while idx < values.len() {
if option_token(&values[idx])?.is_some() {
break;
}
let candidates = extract_name_list(&values[idx]).await?;
if candidates.is_empty() {
return Err(who_error(
"who: '-regexp' requires non-empty pattern strings",
));
}
for pattern in candidates {
let regex = Regex::new(&pattern).map_err(|err| {
build_runtime_error(format!(
"who: invalid regular expression '{pattern}': {err}"
))
.with_builtin("who")
.with_source(err)
.build()
})?;
regex_patterns.push(regex);
}
idx += 1;
}
continue;
}
other => {
return Err(who_error(format!("who: unsupported option '{other}'")));
}
}
}
let extracted = extract_name_list(&values[idx]).await?;
if extracted.is_empty() {
idx += 1;
continue;
}
if extracted.len() == 1
&& extracted[0].eq_ignore_ascii_case("global")
&& names.is_empty()
&& regex_patterns.is_empty()
&& path_value.is_none()
{
only_global = true;
} else {
names.extend(extracted);
}
idx += 1;
}
let source = if let Some(path_value) = path_value {
let path = parse_file_path(&path_value)?;
WhoSource::File(path)
} else {
WhoSource::Workspace
};
let selectors = build_selectors(&names)?;
Ok(WhoRequest {
source,
selectors,
regex_patterns,
only_global,
})
}
fn matches_filters(name: &str, selectors: &[NameSelector], regex_patterns: &[Regex]) -> bool {
if selectors.is_empty() && regex_patterns.is_empty() {
return true;
}
if selectors.iter().any(|selector| match selector {
NameSelector::Exact(expected) => name == expected,
NameSelector::Wildcard(pattern) => pattern.matches(name),
}) {
return true;
}
regex_patterns.iter().any(|regex| regex.is_match(name))
}
fn build_selectors(names: &[String]) -> BuiltinResult<Vec<NameSelector>> {
let mut selectors = Vec::with_capacity(names.len());
for name in names {
if contains_wildcards(name) {
let pattern = Pattern::new(name).map_err(|err| {
build_runtime_error(format!("who: invalid pattern '{name}': {err}"))
.with_builtin("who")
.with_source(err)
.build()
})?;
selectors.push(NameSelector::Wildcard(pattern));
} else {
selectors.push(NameSelector::Exact(name.clone()));
}
}
Ok(selectors)
}
fn contains_wildcards(text: &str) -> bool {
text.chars().any(|ch| matches!(ch, '*' | '?' | '['))
}
fn parse_file_path(value: &Value) -> BuiltinResult<PathBuf> {
let text = value_to_string_scalar(value)
.ok_or_else(|| who_error("who: filename must be a character vector or string scalar"))?;
let mut path = PathBuf::from(text);
if path.extension().is_none() {
path.set_extension("mat");
}
Ok(path)
}
fn option_token(value: &Value) -> BuiltinResult<Option<String>> {
if let Some(token) = value_to_string_scalar(value) {
if token.starts_with('-') {
return Ok(Some(token.to_ascii_lowercase()));
}
}
Ok(None)
}
#[async_recursion::async_recursion(?Send)]
async fn extract_name_list(value: &Value) -> BuiltinResult<Vec<String>> {
match value {
Value::String(s) => Ok(vec![s.clone()]),
Value::CharArray(ca) => Ok(char_array_rows_as_strings(ca)),
Value::StringArray(sa) => Ok(sa.data.clone()),
Value::Cell(ca) => {
let mut names = Vec::with_capacity(ca.data.len());
for handle in &ca.data {
let inner = unsafe { &*handle.as_raw() };
if let Some(text) = value_to_string_scalar(inner) {
names.push(text);
continue;
}
let gathered = gather_if_needed_async(inner).await.map_err(who_flow)?;
if let Some(text) = value_to_string_scalar(&gathered) {
names.push(text);
} else {
return Err(who_error(
"who: selection cells must contain string or character scalars",
));
}
}
Ok(names)
}
Value::GpuTensor(_) => {
let gathered = gather_if_needed_async(value).await.map_err(who_flow)?;
extract_name_list(&gathered).await
}
_ => Err(who_error(
"who: selections must be character vectors, string scalars, string arrays, or cell arrays of those types",
)),
}
}
fn value_to_string_scalar(value: &Value) -> Option<String> {
match value {
Value::String(s) => Some(s.clone()),
Value::CharArray(ca) if ca.rows == 1 => char_array_rows_as_strings(ca).into_iter().next(),
Value::StringArray(sa) if sa.data.len() == 1 => Some(sa.data[0].clone()),
_ => None,
}
}
fn char_array_rows_as_strings(ca: &CharArray) -> Vec<String> {
let mut rows = Vec::with_capacity(ca.rows);
for r in 0..ca.rows {
let mut row = String::with_capacity(ca.cols);
for c in 0..ca.cols {
let idx = r * ca.cols + c;
row.push(ca.data[idx]);
}
rows.push(row.trim_end_matches([' ', '\0']).to_string());
}
rows
}
#[cfg(test)]
pub(crate) mod tests {
use super::super::whos::tests::{
char_array_from_rows as shared_char_array_from_rows,
ensure_test_resolver as ensure_shared_resolver, set_workspace as shared_set_workspace,
};
use super::*;
use crate::builtins::common::test_support;
use crate::call_builtin_async;
use futures::executor::block_on;
use runmat_builtins::{CellArray, CharArray, StringArray, Tensor};
use tempfile::tempdir;
fn who_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
block_on(super::who_builtin(args))
}
fn names_from_value(value: Value) -> Vec<String> {
match value {
Value::Cell(cell) => cell
.data
.iter()
.map(|ptr| unsafe { &*ptr.as_raw() })
.map(|value| match value {
Value::String(s) => s.clone(),
Value::CharArray(ca) if ca.rows == 1 => ca
.data
.iter()
.collect::<String>()
.trim_end_matches([' ', '\0'])
.to_string(),
other => panic!("expected string entry, got {other:?}"),
})
.collect(),
Value::String(s) => vec![s],
Value::StringArray(sa) => sa.data,
Value::CharArray(ca) => char_array_rows_as_strings(&ca),
other => panic!("expected cell array result, got {other:?}"),
}
}
fn error_message(err: crate::RuntimeError) -> String {
err.message().to_string()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_lists_workspace_variables() {
ensure_shared_resolver();
let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
shared_set_workspace(
&[("alpha", Value::Num(1.0)), ("beta", Value::Tensor(tensor))],
&[],
);
let value = who_builtin(Vec::new()).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_filters_with_wildcard() {
ensure_shared_resolver();
shared_set_workspace(
&[("alpha", Value::Num(1.0)), ("beta", Value::Num(2.0))],
&[],
);
let value = who_builtin(vec![Value::from("a*")]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["alpha".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_filters_with_regex() {
ensure_shared_resolver();
shared_set_workspace(
&[
("foo", Value::Num(1.0)),
("bar", Value::Num(2.0)),
("baz", Value::Num(3.0)),
],
&[],
);
let value = who_builtin(vec![Value::from("-regexp"), Value::from("^ba")]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["bar".to_string(), "baz".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_combines_wildcard_and_regex_filters() {
ensure_shared_resolver();
shared_set_workspace(
&[
("alpha", Value::Num(1.0)),
("beta", Value::Num(2.0)),
("gamma", Value::Num(3.0)),
("delta", Value::Num(4.0)),
],
&[],
);
let value = who_builtin(vec![
Value::from("a*"),
Value::from("-regexp"),
Value::from("ma$"),
])
.expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["alpha".to_string(), "gamma".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_filters_global_only() {
ensure_shared_resolver();
shared_set_workspace(
&[("shared", Value::Num(1.0)), ("local", Value::Num(2.0))],
&["shared"],
);
let value = who_builtin(vec![Value::from("global")]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["shared".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_global_option_is_case_insensitive() {
ensure_shared_resolver();
shared_set_workspace(
&[("Shared", Value::Num(1.0)), ("local", Value::Num(2.0))],
&["Shared"],
);
let value = who_builtin(vec![Value::from("GLOBAL")]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["Shared".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_accepts_char_array_arguments() {
ensure_shared_resolver();
shared_set_workspace(
&[
("alpha", Value::Num(1.0)),
("gamma", Value::Num(3.0)),
("omega", Value::Num(4.0)),
],
&[],
);
let arg = Value::CharArray(shared_char_array_from_rows(&["alpha", "gamma"]));
let value = who_builtin(vec![arg]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["alpha".to_string(), "gamma".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_accepts_string_array_arguments() {
ensure_shared_resolver();
shared_set_workspace(
&[
("alpha", Value::Num(1.0)),
("gamma", Value::Num(3.0)),
("omega", Value::Num(4.0)),
],
&[],
);
let array =
StringArray::new(vec!["gamma".to_string(), "alpha".to_string()], vec![2, 1]).unwrap();
let value = who_builtin(vec![Value::StringArray(array)]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["alpha".to_string(), "gamma".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_accepts_cell_array_arguments() {
ensure_shared_resolver();
shared_set_workspace(
&[
("alpha", Value::Num(1.0)),
("gamma", Value::Num(3.0)),
("omega", Value::Num(4.0)),
],
&[],
);
let cell = CellArray::new(vec![Value::from("gamma"), Value::from("alpha")], 2, 1).unwrap();
let value = who_builtin(vec![Value::Cell(cell)]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["alpha".to_string(), "gamma".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_rejects_numeric_selection() {
ensure_shared_resolver();
shared_set_workspace(&[], &[]);
let err = who_builtin(vec![Value::Num(7.0)]).expect_err("who should error");
let message = error_message(err);
assert!(
message.contains("who: selections must"),
"unexpected error: {message}"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_rejects_unknown_option() {
ensure_shared_resolver();
shared_set_workspace(&[], &[]);
let err = who_builtin(vec![Value::from("-bogus")]).expect_err("who should error");
let message = error_message(err);
assert!(
message.contains("unsupported option"),
"unexpected error: {message}"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_requires_filename_for_file_option() {
ensure_shared_resolver();
shared_set_workspace(&[], &[]);
let err = who_builtin(vec![Value::from("-file")]).expect_err("who should error");
let message = error_message(err);
assert!(
message.contains("'-file' requires a filename"),
"error: {message}"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_requires_pattern_for_regexp() {
ensure_shared_resolver();
shared_set_workspace(&[], &[]);
let err = who_builtin(vec![Value::from("-regexp")]).expect_err("who should error");
let message = error_message(err);
assert!(
message.contains("'-regexp' requires at least one pattern"),
"error: {message}"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_rejects_invalid_regex() {
ensure_shared_resolver();
shared_set_workspace(&[], &[]);
let err = who_builtin(vec![Value::from("-regexp"), Value::from("[")])
.expect_err("who should error");
let message = error_message(err);
assert!(
message.contains("invalid regular expression"),
"error: {message}"
);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_returns_empty_column_cell_when_no_match() {
ensure_shared_resolver();
shared_set_workspace(&[], &[]);
let value = who_builtin(vec![Value::from("nothing")]).expect("who");
match value {
Value::Cell(cell) => {
assert_eq!(cell.rows, 0);
assert_eq!(cell.cols, 1);
assert!(cell.data.is_empty());
}
other => panic!("expected cell array, got {other:?}"),
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_file_option_reads_mat_file() {
ensure_shared_resolver();
let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
shared_set_workspace(
&[
("alpha", Value::Num(1.0)),
("beta", Value::Tensor(tensor.clone())),
],
&[],
);
let dir = tempdir().expect("tempdir");
let file_path = dir.path().join("snapshot.mat");
let path_str = file_path.to_string_lossy().to_string();
block_on(call_builtin_async(
"save",
&[
Value::from(path_str.clone()),
Value::from("alpha"),
Value::from("beta"),
],
))
.expect("save");
shared_set_workspace(&[], &[]);
let value = who_builtin(vec![Value::from("-file"), Value::from(path_str)]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_file_option_combines_literal_and_regex_selectors() {
ensure_shared_resolver();
let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
shared_set_workspace(
&[
("alpha", Value::Num(1.0)),
("beta", Value::Tensor(tensor.clone())),
("gamma", Value::Tensor(tensor)),
],
&[],
);
let dir = tempdir().expect("tempdir");
let stem_path = dir.path().join("snapshot_combo");
let stem_str = stem_path.to_string_lossy().to_string();
block_on(call_builtin_async(
"save",
&[
Value::from(stem_str.clone()),
Value::from("alpha"),
Value::from("beta"),
Value::from("gamma"),
],
))
.expect("save");
shared_set_workspace(&[], &[]);
let value = who_builtin(vec![
Value::from("-file"),
Value::from(stem_str.clone()),
Value::from("alpha"),
Value::from("-regexp"),
Value::from("^b"),
])
.expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_file_option_adds_mat_extension() {
ensure_shared_resolver();
shared_set_workspace(&[("v", Value::Num(2.0))], &[]);
let dir = tempdir().expect("tempdir");
let stem_path = dir.path().join("snapshot_no_ext");
let stem_str = stem_path.to_string_lossy().to_string();
block_on(call_builtin_async(
"save",
&[Value::from(stem_str.clone()), Value::from("v")],
))
.expect("save");
shared_set_workspace(&[], &[]);
let value = who_builtin(vec![Value::from("-file"), Value::from(stem_str)]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["v".to_string()]);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_handles_gpu_workspace_entries() {
ensure_shared_resolver();
test_support::with_test_provider(|_| {
let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
let gpu_value = block_on(crate::call_builtin_async(
"gpuArray",
&[Value::Tensor(tensor)],
))
.expect("gpu");
shared_set_workspace(
&[("gpuVar", gpu_value.clone()), ("hostVar", Value::Num(5.0))],
&[],
);
let value = who_builtin(Vec::new()).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["gpuVar".to_string(), "hostVar".to_string()]);
shared_set_workspace(&[], &[]);
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn who_respects_global_filter_with_gpu_variables() {
ensure_shared_resolver();
test_support::with_test_provider(|_| {
shared_set_workspace(&[], &[]);
let gpu_scalar =
block_on(crate::call_builtin_async("gpuArray", &[Value::Num(3.0)])).expect("gpu");
shared_set_workspace(&[("shared_gpu", gpu_scalar)], &["shared_gpu"]);
let value = who_builtin(vec![Value::from("global")]).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["shared_gpu".to_string()]);
shared_set_workspace(&[], &[]);
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
#[cfg(feature = "wgpu")]
fn who_handles_workspace_with_wgpu_provider() {
ensure_shared_resolver();
let _provider = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
)
.expect("wgpu provider");
let tensor = Tensor::new(vec![0.0, 1.0], vec![2, 1]).unwrap();
let gpu_value = block_on(crate::call_builtin_async(
"gpuArray",
&[Value::Tensor(tensor)],
))
.expect("gpuArray");
shared_set_workspace(&[("wgpuVar", gpu_value)], &[]);
let value = who_builtin(Vec::new()).expect("who");
let names = names_from_value(value);
assert_eq!(names, vec!["wgpuVar".to_string()]);
shared_set_workspace(&[], &[]);
}
}