use std::collections::HashMap;
use std::rc::Rc;
type BodyCache = HashMap<String, Rc<Vec<(Stmt, bool)>>>;
fn expand_tilde(path: &str) -> String {
if path == "~" || path.starts_with("~/") || path.starts_with("~\\") {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_default();
if home.is_empty() {
return path.to_string();
}
if path == "~" {
home
} else {
format!("{}{}", home, &path[1..])
}
} else {
path.to_string()
}
}
use indexmap::IndexMap;
use ndarray::Array2;
use crate::env::{Env, Value};
use crate::eval::{
Base, Expr, FormatMode, autoload_cache_insert, current_func_name, eval_with_io, format_complex,
format_scalar, format_value_full, get_display_base, get_display_compact, get_display_fmt,
global_declare, global_frame_pop, global_frame_push, global_get, global_init_if_absent,
global_refresh_into_env, global_set, is_global, is_persistent, persistent_declare,
persistent_frame_pop, persistent_frame_push, persistent_load, persistent_save,
set_autoload_hook, set_display_ctx, set_fn_call_hook, set_last_err,
};
use crate::io::IoContext;
use crate::parser::{Stmt, parse_stmts};
thread_local! {
static RUN_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
static SCRIPT_DIR_STACK: std::cell::RefCell<Vec<std::path::PathBuf>> =
const { std::cell::RefCell::new(Vec::new()) };
static SESSION_PATH: std::cell::RefCell<Vec<std::path::PathBuf>> =
const { std::cell::RefCell::new(Vec::new()) };
static BODY_CACHE: std::cell::RefCell<BodyCache> =
std::cell::RefCell::new(HashMap::new());
}
fn silence_all(stmts: Vec<(Stmt, bool)>) -> Vec<(Stmt, bool)> {
stmts
.into_iter()
.map(|(stmt, _)| {
let stmt = match stmt {
Stmt::If {
cond,
body,
elseif_branches,
else_body,
} => Stmt::If {
cond,
body: silence_all(body),
elseif_branches: elseif_branches
.into_iter()
.map(|(c, b)| (c, silence_all(b)))
.collect(),
else_body: else_body.map(silence_all),
},
Stmt::For {
var,
range_expr,
body,
} => Stmt::For {
var,
range_expr,
body: silence_all(body),
},
Stmt::While { cond, body } => Stmt::While {
cond,
body: silence_all(body),
},
Stmt::DoUntil { body, cond } => Stmt::DoUntil {
body: silence_all(body),
cond,
},
Stmt::Switch {
expr,
cases,
otherwise_body,
} => Stmt::Switch {
expr,
cases: cases
.into_iter()
.map(|(v, b)| (v, silence_all(b)))
.collect(),
otherwise_body: otherwise_body.map(silence_all),
},
Stmt::TryCatch {
try_body,
catch_var,
catch_body,
} => Stmt::TryCatch {
try_body: silence_all(try_body),
catch_var,
catch_body: silence_all(catch_body),
},
other => other,
};
(stmt, true)
})
.collect()
}
fn get_or_parse_body(body_source: &str) -> Result<Rc<Vec<(Stmt, bool)>>, String> {
BODY_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(body) = cache.get(body_source) {
return Ok(Rc::clone(body));
}
let stmts =
parse_stmts(body_source).map_err(|e| format!("function body parse error: {e}"))?;
let silent = silence_all(stmts);
let rc = Rc::new(silent);
cache.insert(body_source.to_string(), Rc::clone(&rc));
Ok(rc)
})
}
pub enum Signal {
Break,
Continue,
Return,
}
pub fn init() {
set_fn_call_hook(call_user_function);
set_autoload_hook(try_autoload);
}
fn try_autoload(name: &str) -> bool {
if name.contains('.') {
return try_autoload_pkg(name);
}
let candidates = [format!("{name}.calc"), format!("{name}.m")];
for candidate in &candidates {
let Some(path) = resolve_script_path(candidate) else {
continue;
};
let Ok(content) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(stmts) = parse_stmts(&content) else {
continue;
};
if !matches!(stmts.first(), Some((Stmt::FunctionDef { .. }, _))) {
continue;
}
let primary_name = match &stmts[0].0 {
Stmt::FunctionDef { name, .. } => name.clone(),
_ => continue,
};
let mut locals: IndexMap<String, Value> = IndexMap::new();
for (stmt, _) in &stmts {
if let Stmt::FunctionDef {
name: n,
outputs,
params,
body_source,
} = stmt
&& n != &primary_name
{
locals.insert(
n.clone(),
Value::Function {
outputs: outputs.clone(),
params: params.clone(),
body_source: body_source.clone(),
locals: IndexMap::new(),
},
);
}
}
if let Stmt::FunctionDef {
outputs,
params,
body_source,
..
} = &stmts[0].0
{
autoload_cache_insert(
primary_name,
Value::Function {
outputs: outputs.clone(),
params: params.clone(),
body_source: body_source.clone(),
locals,
},
);
return true;
}
}
false
}
fn try_autoload_pkg(qualified: &str) -> bool {
let parts: Vec<&str> = qualified.split('.').collect();
if parts.len() < 2 {
return false;
}
let func_name = *parts.last().unwrap();
let pkg_parts = &parts[..parts.len() - 1];
let mut rel_prefix = std::path::PathBuf::new();
for pkg in pkg_parts {
rel_prefix.push(format!("+{pkg}"));
}
let candidates = [
rel_prefix.join(format!("{func_name}.calc")),
rel_prefix.join(format!("{func_name}.m")),
];
let mut search_dirs: Vec<std::path::PathBuf> = Vec::new();
SCRIPT_DIR_STACK.with(|s| search_dirs.extend(s.borrow().iter().cloned()));
search_dirs.push(std::path::PathBuf::from("."));
SESSION_PATH.with(|s| search_dirs.extend(s.borrow().iter().cloned()));
for dir in &search_dirs {
for candidate in &candidates {
let full = dir.join(candidate);
let Ok(content) = std::fs::read_to_string(&full) else {
continue;
};
let Ok(stmts) = parse_stmts(&content) else {
continue;
};
if !matches!(stmts.first(), Some((Stmt::FunctionDef { .. }, _))) {
continue;
}
let primary_name = match &stmts[0].0 {
Stmt::FunctionDef { name, .. } => name.clone(),
_ => continue,
};
let mut locals: IndexMap<String, Value> = IndexMap::new();
for (stmt, _) in &stmts {
if let Stmt::FunctionDef {
name: n,
outputs,
params,
body_source,
} = stmt
&& n != &primary_name
{
locals.insert(
n.clone(),
Value::Function {
outputs: outputs.clone(),
params: params.clone(),
body_source: body_source.clone(),
locals: IndexMap::new(),
},
);
}
}
if let Stmt::FunctionDef {
outputs,
params,
body_source,
..
} = &stmts[0].0
{
autoload_cache_insert(
qualified.to_string(),
Value::Function {
outputs: outputs.clone(),
params: params.clone(),
body_source: body_source.clone(),
locals,
},
);
return true;
}
}
}
false
}
pub fn script_dir_push(dir: &std::path::Path) {
SCRIPT_DIR_STACK.with(|s| s.borrow_mut().push(dir.to_path_buf()));
}
pub fn script_dir_pop() {
SCRIPT_DIR_STACK.with(|s| s.borrow_mut().pop());
}
pub fn session_path_init(paths: Vec<std::path::PathBuf>) {
SESSION_PATH.with(|p| *p.borrow_mut() = paths);
}
pub fn session_path_add(path: std::path::PathBuf, append: bool) {
SESSION_PATH.with(|p| {
let mut v = p.borrow_mut();
v.retain(|e| e != &path);
if append {
v.push(path);
} else {
v.insert(0, path);
}
});
}
pub fn session_path_remove(path: &std::path::Path) {
SESSION_PATH.with(|p| p.borrow_mut().retain(|e| e.as_path() != path));
}
pub fn session_path_list() -> Vec<std::path::PathBuf> {
SESSION_PATH.with(|p| p.borrow().clone())
}
fn call_user_function(
name: &str,
func: &Value,
args: &[Value],
caller_env: &Env,
io: &mut IoContext,
) -> Result<Value, String> {
let Value::Function {
outputs,
params,
body_source,
locals,
} = func
else {
return Err("call_user_function: not a Function value".to_string());
};
global_frame_push();
persistent_frame_push(name);
let mut local_env = Env::new();
local_env.insert("i".to_string(), Value::Complex(0.0, 1.0));
local_env.insert("j".to_string(), Value::Complex(0.0, 1.0));
local_env.insert("ans".to_string(), Value::Scalar(0.0));
for (fn_name, val) in locals.iter() {
local_env.insert(fn_name.clone(), val.clone());
}
for (var_name, val) in caller_env.iter() {
if matches!(val, Value::Function { .. } | Value::Lambda(_)) {
local_env.insert(var_name.clone(), val.clone());
}
}
let has_varargin = params.last().is_some_and(|p| p == "varargin");
let fixed_params = if has_varargin {
¶ms[..params.len() - 1]
} else {
params.as_slice()
};
let effective_args = if args.len() > params.len() {
if !has_varargin && args.len() > params.len() + 1 {
return Err(format!(
"Too many arguments: expected at most {}, got {}",
params.len(),
args.len()
));
}
if has_varargin {
args
} else {
&args[..params.len()]
}
} else {
args
};
for (p, a) in fixed_params.iter().zip(effective_args.iter()) {
local_env.insert(p.clone(), a.clone());
}
if has_varargin {
let extra: Vec<Value> = effective_args
.get(fixed_params.len()..)
.unwrap_or(&[])
.to_vec();
let varargin = Value::Cell(extra);
local_env.insert("varargin".to_string(), varargin);
}
let nargin = effective_args.len().min(params.len());
local_env.insert("nargin".to_string(), Value::Scalar(nargin as f64));
local_env.insert("nargout".to_string(), Value::Scalar(outputs.len() as f64));
let body = get_or_parse_body(body_source)?;
let fmt = get_display_fmt();
let base = get_display_base();
let compact = get_display_compact();
let exec_result = exec_stmts(&body, &mut local_env, io, &fmt, base, compact);
let (func_name_saved, persistent_names) = persistent_frame_pop();
for var_name in &persistent_names {
if let Some(val) = local_env.get(var_name) {
persistent_save(&func_name_saved, var_name, val.clone());
}
}
global_frame_pop();
match exec_result? {
None | Some(Signal::Return) => {}
Some(Signal::Break) => return Err("'break' outside loop".to_string()),
Some(Signal::Continue) => return Err("'continue' outside loop".to_string()),
}
if outputs.is_empty() {
return Ok(Value::Void);
}
if outputs.len() == 1 && outputs[0] == "varargout" {
let cell = local_env.remove("varargout").unwrap_or(Value::Cell(vec![]));
return match cell {
Value::Cell(mut v) => {
if v.is_empty() {
Ok(Value::Void)
} else if v.len() == 1 {
Ok(v.remove(0))
} else {
Ok(Value::Tuple(v))
}
}
other => Ok(other),
};
}
if outputs.len() == 1 {
return Ok(local_env.remove(&outputs[0]).unwrap_or(Value::Void));
}
let vals: Vec<Value> = outputs
.iter()
.map(|o| local_env.remove(o).unwrap_or(Value::Void))
.collect();
Ok(Value::Tuple(vals))
}
pub fn resolve_script_path(name: &str) -> Option<std::path::PathBuf> {
let p = std::path::Path::new(name);
let mut bases: Vec<std::path::PathBuf> = Vec::new();
SCRIPT_DIR_STACK.with(|stack| {
for dir in stack.borrow().iter().rev() {
bases.push(dir.join("private").join(p));
bases.push(dir.join(p));
}
});
bases.push(p.to_path_buf());
SESSION_PATH.with(|sp| {
for dir in sp.borrow().iter() {
bases.push(dir.join(p));
}
});
for base in &bases {
if base.extension().is_some() {
if base.exists() {
return Some(base.clone());
}
continue;
}
let with_calc = base.with_extension("calc");
if with_calc.exists() {
return Some(with_calc);
}
let with_m = base.with_extension("m");
if with_m.exists() {
return Some(with_m);
}
}
None
}
fn is_truthy(val: &Value) -> bool {
match val {
Value::Scalar(n) => *n != 0.0 && !n.is_nan(),
Value::Matrix(m) => m.iter().all(|&x| x != 0.0 && !x.is_nan()),
Value::Complex(re, im) => *re != 0.0 || *im != 0.0,
Value::Str(s) | Value::StringObj(s) => !s.is_empty(),
Value::Void => false,
Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => true,
Value::Cell(v) => !v.is_empty(),
Value::Struct(_) | Value::StructArray(_) => true,
}
}
fn print_value(label: Option<&str>, val: &Value, fmt: &FormatMode, base: Base, compact: bool) {
match val {
Value::Void => {}
Value::Scalar(n) => {
if let Some(name) = label {
println!("{name} = {}", format_scalar(*n, base, fmt));
} else {
println!("{}", format_scalar(*n, base, fmt));
}
}
Value::Matrix(_) => {
if let Some(full) = format_value_full(val, fmt) {
let prefix = label.unwrap_or("ans");
println!("{prefix} =");
println!("{full}");
if !compact {
println!();
}
}
}
Value::Complex(re, im) => {
if let Some(name) = label {
println!("{name} = {}", format_complex(*re, *im, fmt));
} else {
println!("{}", format_complex(*re, *im, fmt));
}
}
Value::Str(s) | Value::StringObj(s) => {
if let Some(name) = label {
println!("{name} = {s}");
} else {
println!("{s}");
}
}
Value::Lambda(_) => {
if let Some(name) = label {
println!("{name} = @<lambda>");
} else {
println!("@<lambda>");
}
}
Value::Function {
outputs, params, ..
} => {
let params_str = params.join(", ");
let out_str = match outputs.len() {
0 => String::new(),
1 => format!("{} = ", outputs[0]),
_ => format!("[{}] = ", outputs.join(", ")),
};
if let Some(name) = label {
println!("{name} = @function {out_str}{name}({params_str})");
} else {
println!("@function {out_str}f({params_str})");
}
}
Value::Tuple(vals) => {
for (i, v) in vals.iter().enumerate() {
print_value(label.map(|_| "ans").or(Some("ans")), v, fmt, base, compact);
let _ = i;
}
}
Value::Cell(_) | Value::Struct(_) | Value::StructArray(_) => {
if let Some(full) = format_value_full(val, fmt) {
let prefix = label.unwrap_or("ans");
println!("{prefix} =");
println!("{full}");
if !compact {
println!();
}
}
}
}
}
fn set_nested(
mut map: IndexMap<String, Value>,
path: &[String],
val: Value,
) -> Result<IndexMap<String, Value>, String> {
let (first, rest) = path.split_first().expect("set_nested: empty path");
if rest.is_empty() {
map.insert(first.clone(), val);
} else {
let inner = match map.shift_remove(first) {
Some(Value::Struct(m)) => m,
None => IndexMap::new(),
Some(other) => {
map.insert(first.clone(), other);
return Err(format!("'{first}' is not a struct"));
}
};
let updated = set_nested(inner, rest, val)?;
map.insert(first.clone(), Value::Struct(updated));
}
Ok(map)
}
pub fn exec_stmts(
stmts: &[(Stmt, bool)],
env: &mut Env,
io: &mut IoContext,
fmt: &FormatMode,
base: Base,
compact: bool,
) -> Result<Option<Signal>, String> {
set_display_ctx(fmt, base, compact);
for (stmt, silent) in stmts {
match stmt {
Stmt::Assign(name, expr) => {
let val = eval_with_io(expr, env, io)?;
env.insert(name.clone(), val.clone());
if is_global(name) {
global_set(name, val.clone());
}
if is_persistent(name) {
persistent_save(¤t_func_name(), name, val.clone());
}
if !silent && !matches!(val, Value::Void) {
print_value(Some(name), &val, fmt, base, compact);
}
}
Stmt::Global(names) => {
for name in names {
global_declare(name);
global_init_if_absent(name);
if let Some(local_val) = env.remove(name) {
global_set(name, local_val.clone());
env.insert(name.clone(), local_val);
} else if let Some(global_val) = global_get(name) {
env.insert(name.clone(), global_val);
}
}
}
Stmt::Persistent(names) => {
let func = current_func_name();
for name in names {
persistent_declare(name);
if let Some(saved) = persistent_load(&func, name) {
env.insert(name.clone(), saved);
} else {
env.insert(name.clone(), Value::Matrix(ndarray::Array2::zeros((0, 0))));
}
}
}
Stmt::Expr(expr) => {
if let Expr::Call(fn_name, args) = expr
&& matches!(fn_name.as_str(), "addpath" | "rmpath" | "path")
{
match fn_name.as_str() {
"addpath" => {
if args.is_empty() || args.len() > 2 {
return Err(
"addpath: expects 1 or 2 arguments: addpath(dir) or addpath(dir, '-end')".to_string()
);
}
let path_val = eval_with_io(&args[0], env, io)?;
let path_str = match &path_val {
Value::Str(s) | Value::StringObj(s) => s.clone(),
_ => {
return Err(
"addpath: argument must be a string (directory path)"
.to_string(),
);
}
};
let append = if args.len() == 2 {
let flag_val = eval_with_io(&args[1], env, io)?;
match &flag_val {
Value::Str(s) | Value::StringObj(s) if s == "-end" => true,
Value::Str(_) | Value::StringObj(_) => {
return Err(
"addpath: second argument must be '-end' (to append) or omitted (to prepend)".to_string()
);
}
_ => {
return Err(
"addpath: second argument must be a string '-end'"
.to_string(),
);
}
}
} else {
false
};
let expanded = expand_tilde(&path_str);
let pb = std::path::PathBuf::from(&expanded);
session_path_add(pb, append);
if !silent {
for p in session_path_list() {
println!("{}", p.display());
}
}
}
"rmpath" => {
if args.len() != 1 {
return Err("rmpath: expects exactly 1 argument".to_string());
}
let path_val = eval_with_io(&args[0], env, io)?;
let path_str = match &path_val {
Value::Str(s) | Value::StringObj(s) => s.clone(),
_ => {
return Err(
"rmpath: argument must be a string (directory path)"
.to_string(),
);
}
};
let expanded = expand_tilde(&path_str);
session_path_remove(std::path::Path::new(&expanded));
}
"path" => {
if !args.is_empty() {
return Err("path: takes no arguments".to_string());
}
if !silent {
let paths = session_path_list();
if paths.is_empty() {
println!("(search path is empty)");
} else {
for p in &paths {
println!("{}", p.display());
}
}
}
}
_ => unreachable!(),
}
continue;
}
if let Expr::Call(fn_name, args) = expr
&& matches!(fn_name.as_str(), "run" | "source")
&& args.len() == 1
{
let path_val = eval_with_io(&args[0], env, io)?;
let filename = match &path_val {
Value::Str(s) | Value::StringObj(s) => s.clone(),
_ => {
return Err(format!("{fn_name}: argument must be a string (filename)"));
}
};
let script_path = resolve_script_path(&filename)
.ok_or_else(|| format!("{fn_name}: script not found: '{filename}'"))?;
let content = std::fs::read_to_string(&script_path).map_err(|e| {
format!("{fn_name}: cannot read '{}': {e}", script_path.display())
})?;
let depth = RUN_DEPTH.with(|d| d.get());
if depth >= 64 {
return Err(format!(
"{fn_name}: maximum script nesting depth (64) exceeded"
));
}
RUN_DEPTH.with(|d| d.set(depth + 1));
if let Some(dir) = script_path.parent() {
SCRIPT_DIR_STACK.with(|s| s.borrow_mut().push(dir.to_path_buf()));
}
let run_stmts = parse_stmts(&content).map_err(|e| {
format!("{fn_name}: parse error in '{}': {e}", script_path.display())
})?;
let is_fn_file =
matches!(run_stmts.first(), Some((Stmt::FunctionDef { .. }, _)));
let result = if is_fn_file {
let primary_name = match &run_stmts[0].0 {
Stmt::FunctionDef { name, .. } => name.clone(),
_ => unreachable!(),
};
let mut locals: IndexMap<String, Value> = IndexMap::new();
for (stmt, _) in &run_stmts {
if let Stmt::FunctionDef {
name,
outputs,
params,
body_source,
} = stmt
&& name != &primary_name
{
locals.insert(
name.clone(),
Value::Function {
outputs: outputs.clone(),
params: params.clone(),
body_source: body_source.clone(),
locals: IndexMap::new(),
},
);
}
}
if let Stmt::FunctionDef {
outputs,
params,
body_source,
..
} = &run_stmts[0].0
{
env.insert(
primary_name,
Value::Function {
outputs: outputs.clone(),
params: params.clone(),
body_source: body_source.clone(),
locals,
},
);
}
Ok(None)
} else {
exec_stmts(&run_stmts, env, io, fmt, base, compact)
};
SCRIPT_DIR_STACK.with(|s| s.borrow_mut().pop());
RUN_DEPTH.with(|d| d.set(depth));
return result;
}
let val = eval_with_io(expr, env, io)?;
env.insert("ans".to_string(), val.clone());
if !silent && !matches!(val, Value::Void) {
print_value(None, &val, fmt, base, compact);
}
}
Stmt::If {
cond,
body,
elseif_branches,
else_body,
} => {
let cond_val = eval_with_io(cond, env, io)?;
let chosen: Option<&[(Stmt, bool)]> = if is_truthy(&cond_val) {
Some(body)
} else {
let mut found = None;
for (ei_cond, ei_body) in elseif_branches {
if is_truthy(&eval_with_io(ei_cond, env, io)?) {
found = Some(ei_body.as_slice());
break;
}
}
if found.is_none() {
found = else_body.as_deref();
}
found
};
if let Some(body_stmts) = chosen
&& let Some(sig) = exec_stmts(body_stmts, env, io, fmt, base, compact)?
{
return Ok(Some(sig));
}
}
Stmt::For {
var,
range_expr,
body,
} => {
let range_val = eval_with_io(range_expr, env, io)?;
let iter_cols: Vec<Value> = match range_val {
Value::Scalar(n) => vec![Value::Scalar(n)],
Value::Matrix(m) => {
let nrows = m.nrows();
let ncols = m.ncols();
(0..ncols)
.map(|j| {
if nrows == 1 {
Value::Scalar(m[[0, j]])
} else {
let mut col = Array2::zeros((nrows, 1));
for i in 0..nrows {
col[[i, 0]] = m[[i, j]];
}
Value::Matrix(col)
}
})
.collect()
}
_ => return Err("'for' range must evaluate to a scalar or matrix".to_string()),
};
'for_loop: for col_val in iter_cols {
env.insert(var.clone(), col_val);
match exec_stmts(body, env, io, fmt, base, compact)? {
None => {}
Some(Signal::Break) => break 'for_loop,
Some(Signal::Continue) => continue 'for_loop,
Some(Signal::Return) => return Ok(Some(Signal::Return)),
}
}
}
Stmt::While { cond, body } => loop {
if !is_truthy(&eval_with_io(cond, env, io)?) {
break;
}
match exec_stmts(body, env, io, fmt, base, compact)? {
None => {}
Some(Signal::Break) => break,
Some(Signal::Continue) => continue,
Some(Signal::Return) => return Ok(Some(Signal::Return)),
}
},
Stmt::Break => return Ok(Some(Signal::Break)),
Stmt::Continue => return Ok(Some(Signal::Continue)),
Stmt::Switch {
expr,
cases,
otherwise_body,
} => {
let switch_val = eval_with_io(expr, env, io)?;
let mut matched = false;
'switch_loop: for (case_exprs, case_body) in cases {
for case_expr in case_exprs {
let case_val = eval_with_io(case_expr, env, io)?;
let is_match = if let Value::Cell(cell_elems) = &case_val {
cell_elems.iter().any(|elem| match (&switch_val, elem) {
(Value::Scalar(a), Value::Scalar(b)) => a == b,
_ => {
let sv = match &switch_val {
Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
_ => None,
};
let cv = match elem {
Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
_ => None,
};
matches!((sv, cv), (Some(a), Some(b)) if a == b)
}
})
} else {
match (&switch_val, &case_val) {
(Value::Scalar(a), Value::Scalar(b)) => a == b,
_ => {
let sv = match &switch_val {
Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
_ => None,
};
let cv = match &case_val {
Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
_ => None,
};
matches!((sv, cv), (Some(a), Some(b)) if a == b)
}
}
};
if is_match {
if let Some(sig) = exec_stmts(case_body, env, io, fmt, base, compact)? {
return Ok(Some(sig));
}
matched = true;
break 'switch_loop;
}
}
}
if !matched
&& let Some(ob) = otherwise_body
&& let Some(sig) = exec_stmts(ob, env, io, fmt, base, compact)?
{
return Ok(Some(sig));
}
}
Stmt::DoUntil { body, cond } => loop {
match exec_stmts(body, env, io, fmt, base, compact)? {
Some(Signal::Break) => break,
Some(Signal::Continue) | None => {}
Some(Signal::Return) => return Ok(Some(Signal::Return)),
}
if is_truthy(&eval_with_io(cond, env, io)?) {
break;
}
},
Stmt::TryCatch {
try_body,
catch_var,
catch_body,
} => match exec_stmts(try_body, env, io, fmt, base, compact) {
Ok(None) => {}
Ok(Some(sig)) => return Ok(Some(sig)),
Err(msg) => {
set_last_err(&msg);
if let Some(var) = catch_var {
let mut map = IndexMap::new();
map.insert("message".to_string(), Value::Str(msg));
env.insert(var.clone(), Value::Struct(map));
}
if let Some(sig) = exec_stmts(catch_body, env, io, fmt, base, compact)? {
return Ok(Some(sig));
}
}
},
Stmt::FunctionDef {
name,
outputs,
params,
body_source,
} => {
env.insert(
name.clone(),
Value::Function {
outputs: outputs.clone(),
params: params.clone(),
body_source: body_source.clone(),
locals: IndexMap::new(),
},
);
}
Stmt::Return => return Ok(Some(Signal::Return)),
Stmt::CellSet(cell_name, idx_expr, val_expr) => {
let cell_len = match env.get(cell_name) {
Some(Value::Cell(v)) => v.len(),
_ => 0,
};
let env_end = write_env_with_end(env, cell_len);
let idx = eval_with_io(idx_expr, &env_end, io)?;
let rhs = eval_with_io(val_expr, env, io)?;
let i = match idx {
Value::Scalar(n) => n as isize,
_ => return Err(format!("{cell_name}{{}}: index must be a scalar integer")),
};
match env.get_mut(cell_name) {
Some(Value::Cell(v)) => {
if i < 1 {
return Err(format!(
"{cell_name}{{}}: index {i} out of range (1..{})",
v.len()
));
}
let idx = (i - 1) as usize;
if idx >= v.len() {
v.resize(idx + 1, Value::Scalar(0.0));
}
v[idx] = rhs.clone();
}
Some(_) => {
return Err(format!(
"'{cell_name}' is not a cell array; use () for regular indexing"
));
}
None => {
if i < 1 {
return Err(format!("{cell_name}{{}}: index {i} must be >= 1"));
}
let idx = (i - 1) as usize;
let mut v = vec![Value::Scalar(0.0); idx + 1];
v[idx] = rhs.clone();
env.insert(cell_name.clone(), Value::Cell(v));
}
}
if !silent && let Some(val) = env.get(cell_name) {
print_value(Some(cell_name), val, fmt, base, compact);
}
}
Stmt::FieldSet(base_name, path, rhs_expr) => {
let rhs = eval_with_io(rhs_expr, env, io)?;
let root = match env.remove(base_name) {
Some(Value::Struct(m)) => m,
None => IndexMap::new(),
Some(other) => {
env.insert(base_name.clone(), other);
return Err(format!("'{base_name}' is not a struct"));
}
};
let updated = set_nested(root, path, rhs)?;
let struct_val = Value::Struct(updated);
if !silent {
print_value(Some(base_name), &struct_val, fmt, base, compact);
}
env.insert(base_name.clone(), struct_val);
}
Stmt::StructArrayFieldSet(base_name, idx_expr, path, rhs_expr) => {
let rhs = eval_with_io(rhs_expr, env, io)?;
let idx_val = eval_with_io(idx_expr, env, io)?;
let idx = match &idx_val {
Value::Scalar(n) => {
let i = *n as isize;
if i < 1 {
return Err(format!(
"Struct array index must be a positive integer, got {n}"
));
}
i as usize
}
_ => return Err("Struct array index must be a scalar integer".to_string()),
};
let mut arr: Vec<IndexMap<String, Value>> = match env.remove(base_name) {
Some(Value::StructArray(v)) => v,
Some(Value::Struct(m)) => vec![m],
None => Vec::new(),
Some(other) => {
env.insert(base_name.clone(), other);
return Err(format!("'{base_name}' is not a struct array"));
}
};
while arr.len() < idx {
arr.push(IndexMap::new());
}
let elem = arr[idx - 1].clone();
let updated_elem = set_nested(elem, path, rhs)?;
arr[idx - 1] = updated_elem;
let arr_val = Value::StructArray(arr);
if !silent {
print_value(Some(base_name), &arr_val, fmt, base, compact);
}
env.insert(base_name.clone(), arr_val);
}
Stmt::IndexSet {
name,
indices,
value,
} => {
let rhs = eval_with_io(value, env, io)?;
if is_persistent(name) {
let func = current_func_name();
if let Some(fresh) = persistent_load(&func, name) {
env.insert(name.clone(), fresh);
}
}
exec_index_set(name, indices, rhs, env, io)?;
if is_persistent(name)
&& let Some(val) = env.get(name)
{
persistent_save(¤t_func_name(), name, val.clone());
}
if !silent && let Some(val) = env.get(name) {
print_value(Some(name), val, fmt, base, compact);
}
}
Stmt::MultiAssign { targets, expr } => {
let val = eval_with_io(expr, env, io)?;
let vals: Vec<Value> = match val {
Value::Tuple(v) => v,
other => vec![other],
};
for (i, target) in targets.iter().enumerate() {
if target == "~" {
continue; }
let v = vals.get(i).cloned().unwrap_or(Value::Void);
env.insert(target.clone(), v.clone());
if !silent && !matches!(v, Value::Void) {
print_value(Some(target), &v, fmt, base, compact);
}
}
}
}
}
global_refresh_into_env(env);
Ok(None)
}
enum WriteIdx {
All,
Positions(Vec<usize>),
}
fn resolve_write_dim(
expr: &crate::eval::Expr,
dim_size: usize,
env: &Env,
io: &mut IoContext,
) -> Result<WriteIdx, String> {
if matches!(expr, crate::eval::Expr::Colon) {
return Ok(WriteIdx::All);
}
let val = eval_with_io(expr, env, io)?;
let floats: Vec<f64> = match val {
Value::Scalar(n) => vec![n],
Value::Complex(re, im) => {
if im != 0.0 {
return Err("Index must be real, not complex".to_string());
}
vec![re]
}
Value::Matrix(m) => {
let total = m.nrows() * m.ncols();
if m.nrows() > 1 && m.ncols() > 1 && total != dim_size {
return Err("Index must be a scalar or vector, not a 2-D matrix".to_string());
}
if m.nrows() > 1 && m.ncols() > 1 {
let mut v = Vec::with_capacity(total);
for col in 0..m.ncols() {
for row in 0..m.nrows() {
v.push(m[[row, col]]);
}
}
v
} else {
m.iter().copied().collect()
}
}
_ => return Err("Index must be numeric".to_string()),
};
if dim_size > 0 && floats.len() == dim_size && floats.iter().all(|&f| f == 0.0 || f == 1.0) {
let positions: Vec<usize> = floats
.iter()
.enumerate()
.filter(|&(_, &f)| f == 1.0)
.map(|(i, _)| i)
.collect();
return Ok(WriteIdx::Positions(positions));
}
let positions: Result<Vec<usize>, String> = floats
.iter()
.map(|&n| {
let i = n.round() as i64;
if i < 1 {
return Err(format!("Index {i} must be >= 1"));
}
Ok(i as usize - 1)
})
.collect();
Ok(WriteIdx::Positions(positions?))
}
fn write_env_with_end(env: &Env, dim_size: usize) -> Env {
let mut e = env.clone();
e.insert("end".to_string(), Value::Scalar(dim_size as f64));
e
}
fn exec_index_set(
name: &str,
indices: &[crate::eval::Expr],
rhs: Value,
env: &mut Env,
io: &mut IoContext,
) -> Result<(), String> {
let (mut mat, was_scalar) = match env.get(name) {
Some(Value::Matrix(m)) => (m.clone(), false),
Some(Value::Scalar(n)) => (Array2::from_elem((1, 1), *n), true),
None | Some(Value::Void) => (Array2::zeros((0, 0)), false),
Some(_) => {
return Err(format!(
"'{name}' is not a matrix; cannot use () indexed assignment"
));
}
};
match indices.len() {
1 => {
let total = mat.nrows() * mat.ncols();
let env_end = write_env_with_end(env, total);
let widx = resolve_write_dim(&indices[0], total, &env_end, io)?;
let positions: Vec<usize> = match widx {
WriteIdx::All => (0..total).collect(),
WriteIdx::Positions(p) => p,
};
let rhs_vals: Vec<f64> = match &rhs {
Value::Scalar(n) => vec![*n; positions.len()],
Value::Matrix(m) => {
let flat: Vec<f64> = m.iter().copied().collect();
if flat.len() != positions.len() {
return Err(format!(
"Assignment dimension mismatch: {} positions but {} values",
positions.len(),
flat.len()
));
}
flat
}
_ => {
return Err(
"Indexed assignment: RHS must be a numeric scalar or matrix".to_string()
);
}
};
let required = positions.iter().copied().max().map(|m| m + 1).unwrap_or(0);
let required = required.max(total);
let (out_rows, out_cols) = if mat.nrows() == 0 || mat.ncols() == 0 {
(1, required)
} else if mat.nrows() == 1 {
(1, required)
} else if mat.ncols() == 1 {
(required, 1)
} else if required > total {
return Err("Cannot grow a 2-D matrix with linear indexing".to_string());
} else {
(mat.nrows(), mat.ncols())
};
if required > total || out_rows != mat.nrows() || out_cols != mat.ncols() {
let mut new_mat = Array2::<f64>::zeros((out_rows, out_cols));
for old_p in 0..total {
let old_row = old_p % mat.nrows().max(1);
let old_col = old_p / mat.nrows().max(1);
let new_row = old_p % out_rows;
let new_col = old_p / out_rows;
if old_row < mat.nrows() && old_col < mat.ncols() {
new_mat[[new_row, new_col]] = mat[[old_row, old_col]];
}
}
mat = new_mat;
}
for (&pos, &val) in positions.iter().zip(rhs_vals.iter()) {
let row = pos % mat.nrows();
let col = pos / mat.nrows();
mat[[row, col]] = val;
}
}
2 => {
let nrows = mat.nrows();
let ncols = mat.ncols();
let env_r = write_env_with_end(env, nrows);
let env_c = write_env_with_end(env, ncols);
let ridx = resolve_write_dim(&indices[0], nrows, &env_r, io)?;
let cidx = resolve_write_dim(&indices[1], ncols, &env_c, io)?;
let rows: Vec<usize> = match ridx {
WriteIdx::All => (0..nrows.max(1)).collect(),
WriteIdx::Positions(p) => p,
};
let cols: Vec<usize> = match cidx {
WriteIdx::All => (0..ncols.max(1)).collect(),
WriteIdx::Positions(p) => p,
};
let req_rows = rows
.iter()
.copied()
.max()
.map(|m| m + 1)
.unwrap_or(0)
.max(nrows);
let req_cols = cols
.iter()
.copied()
.max()
.map(|m| m + 1)
.unwrap_or(0)
.max(ncols);
if req_rows != nrows || req_cols != ncols {
let mut new_mat = Array2::<f64>::zeros((req_rows, req_cols));
for r in 0..nrows {
for c in 0..ncols {
new_mat[[r, c]] = mat[[r, c]];
}
}
mat = new_mat;
}
let n_sel = rows.len() * cols.len();
let rhs_vals: Vec<f64> = match &rhs {
Value::Scalar(n) => vec![*n; n_sel],
Value::Matrix(m) => {
let flat: Vec<f64> = m.iter().copied().collect();
if flat.len() != n_sel {
return Err(format!(
"Assignment dimension mismatch: {}×{} = {} positions but {} values",
rows.len(),
cols.len(),
n_sel,
flat.len()
));
}
flat
}
_ => {
return Err(
"Indexed assignment: RHS must be a numeric scalar or matrix".to_string()
);
}
};
let mut k = 0;
for &r in &rows {
for &c in &cols {
mat[[r, c]] = rhs_vals[k];
k += 1;
}
}
}
_ => return Err("Indexed assignment supports at most 2 indices".to_string()),
}
let result = if mat.nrows() == 1 && mat.ncols() == 1 {
Value::Scalar(mat[[0, 0]])
} else {
Value::Matrix(mat)
};
let _ = was_scalar;
env.insert(name.to_string(), result);
Ok(())
}