1use std::collections::HashMap;
2use std::rc::Rc;
3
4type BodyCache = HashMap<String, Rc<Vec<(Stmt, bool)>>>;
6
7fn expand_tilde(path: &str) -> String {
12 if path == "~" || path.starts_with("~/") || path.starts_with("~\\") {
13 let home = std::env::var("HOME")
14 .or_else(|_| std::env::var("USERPROFILE"))
15 .unwrap_or_default();
16 if home.is_empty() {
17 return path.to_string();
18 }
19 if path == "~" {
20 home
21 } else {
22 format!("{}{}", home, &path[1..])
23 }
24 } else {
25 path.to_string()
26 }
27}
28
29use indexmap::IndexMap;
30use ndarray::Array2;
31
32use crate::env::{Env, Value};
33use crate::env::{load_workspace, save_workspace, save_workspace_vars};
34use crate::eval::{
35 Base, Expr, FormatMode, autoload_cache_insert, current_func_name, eval_with_io, format_complex,
36 format_scalar, format_value_full, get_display_base, get_display_compact, get_display_fmt,
37 global_declare, global_frame_pop, global_frame_push, global_get, global_init_if_absent,
38 global_refresh_into_env, global_set, is_global, is_persistent, persistent_declare,
39 persistent_frame_pop, persistent_frame_push, persistent_load, persistent_save,
40 set_autoload_hook, set_display_ctx, set_fn_call_hook, set_last_err, set_nargout,
41};
42use crate::io::IoContext;
43use crate::parser::{Stmt, parse_stmts};
44
45thread_local! {
46 static RUN_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
48
49 static SCRIPT_DIR_STACK: std::cell::RefCell<Vec<std::path::PathBuf>> =
55 const { std::cell::RefCell::new(Vec::new()) };
56
57 static SESSION_PATH: std::cell::RefCell<Vec<std::path::PathBuf>> =
62 const { std::cell::RefCell::new(Vec::new()) };
63
64 static BODY_CACHE: std::cell::RefCell<BodyCache> =
74 std::cell::RefCell::new(HashMap::new());
75}
76
77fn silence_all(stmts: Vec<(Stmt, bool)>) -> Vec<(Stmt, bool)> {
85 stmts
86 .into_iter()
87 .map(|(stmt, _)| {
88 let stmt = match stmt {
89 Stmt::If {
90 cond,
91 body,
92 elseif_branches,
93 else_body,
94 } => Stmt::If {
95 cond,
96 body: silence_all(body),
97 elseif_branches: elseif_branches
98 .into_iter()
99 .map(|(c, b)| (c, silence_all(b)))
100 .collect(),
101 else_body: else_body.map(silence_all),
102 },
103 Stmt::For {
104 var,
105 range_expr,
106 body,
107 } => Stmt::For {
108 var,
109 range_expr,
110 body: silence_all(body),
111 },
112 Stmt::While { cond, body } => Stmt::While {
113 cond,
114 body: silence_all(body),
115 },
116 Stmt::DoUntil { body, cond } => Stmt::DoUntil {
117 body: silence_all(body),
118 cond,
119 },
120 Stmt::Switch {
121 expr,
122 cases,
123 otherwise_body,
124 } => Stmt::Switch {
125 expr,
126 cases: cases
127 .into_iter()
128 .map(|(v, b)| (v, silence_all(b)))
129 .collect(),
130 otherwise_body: otherwise_body.map(silence_all),
131 },
132 Stmt::TryCatch {
133 try_body,
134 catch_var,
135 catch_body,
136 } => Stmt::TryCatch {
137 try_body: silence_all(try_body),
138 catch_var,
139 catch_body: silence_all(catch_body),
140 },
141 other => other,
142 };
143 (stmt, true)
144 })
145 .collect()
146}
147
148fn get_or_parse_body(body_source: &str) -> Result<Rc<Vec<(Stmt, bool)>>, String> {
154 BODY_CACHE.with(|cache| {
155 let mut cache = cache.borrow_mut();
156 if let Some(body) = cache.get(body_source) {
157 return Ok(Rc::clone(body));
158 }
159 let stmts =
160 parse_stmts(body_source).map_err(|e| format!("function body parse error: {e}"))?;
161 let silent = silence_all(stmts);
162 let rc = Rc::new(silent);
163 cache.insert(body_source.to_string(), Rc::clone(&rc));
164 Ok(rc)
165 })
166}
167
168pub enum Signal {
174 Break,
176 Continue,
178 Return,
180}
181
182pub fn init() {
186 set_fn_call_hook(call_user_function);
187 set_autoload_hook(try_autoload);
188}
189
190fn try_autoload(name: &str) -> bool {
197 if name.contains('.') {
198 return try_autoload_pkg(name);
199 }
200 let candidates = [format!("{name}.calc"), format!("{name}.m")];
201 for candidate in &candidates {
202 let Some(path) = resolve_script_path(candidate) else {
203 continue;
204 };
205 let Ok(content) = std::fs::read_to_string(&path) else {
206 continue;
207 };
208 let Ok(stmts) = parse_stmts(&content) else {
209 continue;
210 };
211 if !matches!(stmts.first(), Some((Stmt::FunctionDef { .. }, _))) {
212 continue;
213 }
214 let primary_name = match &stmts[0].0 {
215 Stmt::FunctionDef { name, .. } => name.clone(),
216 _ => continue,
217 };
218 let mut locals: IndexMap<String, Value> = IndexMap::new();
219 for (stmt, _) in &stmts {
220 if let Stmt::FunctionDef {
221 name: n,
222 outputs,
223 params,
224 body_source,
225 doc,
226 } = stmt
227 && n != &primary_name
228 {
229 locals.insert(
230 n.clone(),
231 Value::Function {
232 outputs: outputs.clone(),
233 params: params.clone(),
234 body_source: body_source.clone(),
235 locals: IndexMap::new(),
236 doc: doc.clone(),
237 },
238 );
239 }
240 }
241 if let Stmt::FunctionDef {
242 outputs,
243 params,
244 body_source,
245 doc,
246 ..
247 } = &stmts[0].0
248 {
249 autoload_cache_insert(
250 primary_name,
251 Value::Function {
252 outputs: outputs.clone(),
253 params: params.clone(),
254 body_source: body_source.clone(),
255 locals,
256 doc: doc.clone(),
257 },
258 );
259 return true;
260 }
261 }
262 false
263}
264
265fn try_autoload_pkg(qualified: &str) -> bool {
271 let parts: Vec<&str> = qualified.split('.').collect();
272 if parts.len() < 2 {
273 return false;
274 }
275 let func_name = *parts.last().unwrap();
276 let pkg_parts = &parts[..parts.len() - 1];
277
278 let mut rel_prefix = std::path::PathBuf::new();
280 for pkg in pkg_parts {
281 rel_prefix.push(format!("+{pkg}"));
282 }
283
284 let candidates = [
285 rel_prefix.join(format!("{func_name}.calc")),
286 rel_prefix.join(format!("{func_name}.m")),
287 ];
288
289 let mut search_dirs: Vec<std::path::PathBuf> = Vec::new();
291 SCRIPT_DIR_STACK.with(|s| search_dirs.extend(s.borrow().iter().cloned()));
292 search_dirs.push(std::path::PathBuf::from("."));
293 SESSION_PATH.with(|s| search_dirs.extend(s.borrow().iter().cloned()));
294
295 for dir in &search_dirs {
296 for candidate in &candidates {
297 let full = dir.join(candidate);
298 let Ok(content) = std::fs::read_to_string(&full) else {
299 continue;
300 };
301 let Ok(stmts) = parse_stmts(&content) else {
302 continue;
303 };
304 if !matches!(stmts.first(), Some((Stmt::FunctionDef { .. }, _))) {
305 continue;
306 }
307 let primary_name = match &stmts[0].0 {
308 Stmt::FunctionDef { name, .. } => name.clone(),
309 _ => continue,
310 };
311 let mut locals: IndexMap<String, Value> = IndexMap::new();
312 for (stmt, _) in &stmts {
313 if let Stmt::FunctionDef {
314 name: n,
315 outputs,
316 params,
317 body_source,
318 doc,
319 } = stmt
320 && n != &primary_name
321 {
322 locals.insert(
323 n.clone(),
324 Value::Function {
325 outputs: outputs.clone(),
326 params: params.clone(),
327 body_source: body_source.clone(),
328 locals: IndexMap::new(),
329 doc: doc.clone(),
330 },
331 );
332 }
333 }
334 if let Stmt::FunctionDef {
335 outputs,
336 params,
337 body_source,
338 doc,
339 ..
340 } = &stmts[0].0
341 {
342 autoload_cache_insert(
343 qualified.to_string(),
344 Value::Function {
345 outputs: outputs.clone(),
346 params: params.clone(),
347 body_source: body_source.clone(),
348 locals,
349 doc: doc.clone(),
350 },
351 );
352 return true;
353 }
354 }
355 }
356 false
357}
358
359pub fn script_dir_push(dir: &std::path::Path) {
365 SCRIPT_DIR_STACK.with(|s| s.borrow_mut().push(dir.to_path_buf()));
366}
367
368pub fn script_dir_pop() {
370 SCRIPT_DIR_STACK.with(|s| s.borrow_mut().pop());
371}
372
373pub fn session_path_init(paths: Vec<std::path::PathBuf>) {
378 SESSION_PATH.with(|p| *p.borrow_mut() = paths);
379}
380
381pub fn session_path_add(path: std::path::PathBuf, append: bool) {
386 SESSION_PATH.with(|p| {
387 let mut v = p.borrow_mut();
388 v.retain(|e| e != &path);
389 if append {
390 v.push(path);
391 } else {
392 v.insert(0, path);
393 }
394 });
395}
396
397pub fn session_path_remove(path: &std::path::Path) {
399 SESSION_PATH.with(|p| p.borrow_mut().retain(|e| e.as_path() != path));
400}
401
402pub fn session_path_list() -> Vec<std::path::PathBuf> {
404 SESSION_PATH.with(|p| p.borrow().clone())
405}
406
407fn call_user_function(
414 name: &str,
415 func: &Value,
416 args: &[Value],
417 caller_env: &Env,
418 io: &mut IoContext,
419) -> Result<Value, String> {
420 let Value::Function {
421 outputs,
422 params,
423 body_source,
424 locals,
425 ..
426 } = func
427 else {
428 return Err("call_user_function: not a Function value".to_string());
429 };
430
431 global_frame_push();
433 persistent_frame_push(name); let mut local_env = Env::new();
439 local_env.insert("i".to_string(), Value::Complex(0.0, 1.0));
440 local_env.insert("j".to_string(), Value::Complex(0.0, 1.0));
441 local_env.insert("ans".to_string(), Value::Scalar(0.0));
442 for (fn_name, val) in locals.iter() {
445 local_env.insert(fn_name.clone(), val.clone());
446 }
447 for (var_name, val) in caller_env.iter() {
448 if matches!(val, Value::Function { .. } | Value::Lambda(_)) {
449 local_env.insert(var_name.clone(), val.clone());
450 }
451 }
452
453 let has_varargin = params.last().is_some_and(|p| p == "varargin");
455 let fixed_params = if has_varargin {
456 ¶ms[..params.len() - 1]
457 } else {
458 params.as_slice()
459 };
460
461 let effective_args = if args.len() > params.len() {
466 if !has_varargin && args.len() > params.len() + 1 {
467 return Err(format!(
468 "Too many arguments: expected at most {}, got {}",
469 params.len(),
470 args.len()
471 ));
472 }
473 if has_varargin {
474 args
475 } else {
476 &args[..params.len()]
478 }
479 } else {
480 args
481 };
482
483 for (p, a) in fixed_params.iter().zip(effective_args.iter()) {
485 local_env.insert(p.clone(), a.clone());
486 }
487
488 if has_varargin {
490 let extra: Vec<Value> = effective_args
494 .get(fixed_params.len()..)
495 .unwrap_or(&[])
496 .to_vec();
497 let varargin = Value::Cell(extra);
498 local_env.insert("varargin".to_string(), varargin);
499 }
500
501 let nargin = effective_args.len().min(params.len());
502 local_env.insert("nargin".to_string(), Value::Scalar(nargin as f64));
503 local_env.insert("nargout".to_string(), Value::Scalar(outputs.len() as f64));
504
505 let body = get_or_parse_body(body_source)?;
507 let fmt = get_display_fmt();
508 let base = get_display_base();
509 let compact = get_display_compact();
510 let exec_result = exec_stmts(&body, &mut local_env, io, &fmt, base, compact);
511
512 let (func_name_saved, persistent_names) = persistent_frame_pop();
514 for var_name in &persistent_names {
515 if let Some(val) = local_env.get(var_name) {
516 persistent_save(&func_name_saved, var_name, val.clone());
517 }
518 }
519 global_frame_pop();
521
522 match exec_result? {
524 None | Some(Signal::Return) => {}
525 Some(Signal::Break) => return Err("'break' outside loop".to_string()),
526 Some(Signal::Continue) => return Err("'continue' outside loop".to_string()),
527 }
528
529 if outputs.is_empty() {
531 return Ok(Value::Void);
532 }
533
534 if outputs.len() == 1 && outputs[0] == "varargout" {
536 let cell = local_env.remove("varargout").unwrap_or(Value::Cell(vec![]));
537 return match cell {
538 Value::Cell(mut v) => {
539 if v.is_empty() {
540 Ok(Value::Void)
541 } else if v.len() == 1 {
542 Ok(v.remove(0))
543 } else {
544 Ok(Value::Tuple(v))
545 }
546 }
547 other => Ok(other),
548 };
549 }
550
551 if outputs.len() == 1 {
552 return Ok(local_env.remove(&outputs[0]).unwrap_or(Value::Void));
553 }
554 let vals: Vec<Value> = outputs
555 .iter()
556 .map(|o| local_env.remove(o).unwrap_or(Value::Void))
557 .collect();
558 Ok(Value::Tuple(vals))
559}
560
561pub fn resolve_script_path(name: &str) -> Option<std::path::PathBuf> {
567 let p = std::path::Path::new(name);
576 let mut bases: Vec<std::path::PathBuf> = Vec::new();
577
578 SCRIPT_DIR_STACK.with(|stack| {
580 for dir in stack.borrow().iter().rev() {
581 bases.push(dir.join("private").join(p));
582 bases.push(dir.join(p));
583 }
584 });
585
586 bases.push(p.to_path_buf());
588
589 SESSION_PATH.with(|sp| {
591 for dir in sp.borrow().iter() {
592 bases.push(dir.join(p));
593 }
594 });
595
596 for base in &bases {
597 if base.extension().is_some() {
598 if base.exists() {
599 return Some(base.clone());
600 }
601 continue;
603 }
604 let with_calc = base.with_extension("calc");
605 if with_calc.exists() {
606 return Some(with_calc);
607 }
608 let with_m = base.with_extension("m");
609 if with_m.exists() {
610 return Some(with_m);
611 }
612 }
613 None
614}
615
616fn is_truthy(val: &Value) -> bool {
624 match val {
625 Value::Scalar(n) => *n != 0.0 && !n.is_nan(),
626 Value::Matrix(m) => m.iter().all(|&x| x != 0.0 && !x.is_nan()),
627 Value::Complex(re, im) => *re != 0.0 || *im != 0.0,
628 Value::Str(s) | Value::StringObj(s) => !s.is_empty(),
629 Value::Void => false,
630 Value::Lambda(_) | Value::Function { .. } | Value::Tuple(_) => true,
633 Value::Cell(v) => !v.is_empty(),
635 Value::Struct(_) | Value::StructArray(_) => true,
637 }
638}
639
640fn print_value(label: Option<&str>, val: &Value, fmt: &FormatMode, base: Base, compact: bool) {
645 match val {
646 Value::Void => {}
647 Value::Scalar(n) => {
648 if let Some(name) = label {
649 println!("{name} = {}", format_scalar(*n, base, fmt));
650 } else {
651 println!("{}", format_scalar(*n, base, fmt));
652 }
653 }
654 Value::Matrix(_) => {
655 if let Some(full) = format_value_full(val, fmt) {
656 let prefix = label.unwrap_or("ans");
657 println!("{prefix} =");
658 println!("{full}");
659 if !compact {
660 println!();
661 }
662 }
663 }
664 Value::Complex(re, im) => {
665 if let Some(name) = label {
666 println!("{name} = {}", format_complex(*re, *im, fmt));
667 } else {
668 println!("{}", format_complex(*re, *im, fmt));
669 }
670 }
671 Value::Str(s) | Value::StringObj(s) => {
672 if let Some(name) = label {
673 println!("{name} = {s}");
674 } else {
675 println!("{s}");
676 }
677 }
678 Value::Lambda(_) => {
679 if let Some(name) = label {
680 println!("{name} = @<lambda>");
681 } else {
682 println!("@<lambda>");
683 }
684 }
685 Value::Function {
686 outputs, params, ..
687 } => {
688 let params_str = params.join(", ");
689 let out_str = match outputs.len() {
690 0 => String::new(),
691 1 => format!("{} = ", outputs[0]),
692 _ => format!("[{}] = ", outputs.join(", ")),
693 };
694 if let Some(name) = label {
695 println!("{name} = @function {out_str}{name}({params_str})");
696 } else {
697 println!("@function {out_str}f({params_str})");
698 }
699 }
700 Value::Tuple(vals) => {
701 for (i, v) in vals.iter().enumerate() {
704 print_value(label.map(|_| "ans").or(Some("ans")), v, fmt, base, compact);
705 let _ = i;
706 }
707 }
708 Value::Cell(_) | Value::Struct(_) | Value::StructArray(_) => {
709 if let Some(full) = format_value_full(val, fmt) {
710 let prefix = label.unwrap_or("ans");
711 println!("{prefix} =");
712 println!("{full}");
713 if !compact {
714 println!();
715 }
716 }
717 }
718 }
719}
720
721fn set_nested(
726 mut map: IndexMap<String, Value>,
727 path: &[String],
728 val: Value,
729) -> Result<IndexMap<String, Value>, String> {
730 let (first, rest) = path.split_first().expect("set_nested: empty path");
731 if rest.is_empty() {
732 map.insert(first.clone(), val);
733 } else {
734 let inner = match map.shift_remove(first) {
735 Some(Value::Struct(m)) => m,
736 None => IndexMap::new(),
737 Some(other) => {
738 map.insert(first.clone(), other);
739 return Err(format!("'{first}' is not a struct"));
740 }
741 };
742 let updated = set_nested(inner, rest, val)?;
743 map.insert(first.clone(), Value::Struct(updated));
744 }
745 Ok(map)
746}
747
748pub fn exec_stmts(
759 stmts: &[(Stmt, bool)],
760 env: &mut Env,
761 io: &mut IoContext,
762 fmt: &FormatMode,
763 base: Base,
764 compact: bool,
765) -> Result<Option<Signal>, String> {
766 set_display_ctx(fmt, base, compact);
768
769 for (stmt, silent) in stmts {
770 match stmt {
771 Stmt::Assign(name, expr) => {
772 set_nargout(1);
773 let val = eval_with_io(expr, env, io)?;
774 env.insert(name.clone(), val.clone());
775 if is_global(name) {
777 global_set(name, val.clone());
778 }
779 if is_persistent(name) {
781 persistent_save(¤t_func_name(), name, val.clone());
782 }
783 if !silent && !matches!(val, Value::Void) {
784 print_value(Some(name), &val, fmt, base, compact);
785 }
786 }
787
788 Stmt::Global(names) => {
789 for name in names {
790 global_declare(name);
791 global_init_if_absent(name);
792 if let Some(local_val) = env.remove(name) {
795 global_set(name, local_val.clone());
796 env.insert(name.clone(), local_val);
797 } else if let Some(global_val) = global_get(name) {
798 env.insert(name.clone(), global_val);
799 }
800 }
801 }
802
803 Stmt::Persistent(names) => {
804 let func = current_func_name();
807 for name in names {
808 persistent_declare(name);
809 if let Some(saved) = persistent_load(&func, name) {
810 env.insert(name.clone(), saved);
812 } else {
813 env.insert(name.clone(), Value::Matrix(ndarray::Array2::zeros((0, 0))));
817 }
818 }
819 }
820
821 Stmt::Expr(expr) => {
822 if let Expr::Call(fn_name, args) = expr
824 && matches!(fn_name.as_str(), "addpath" | "rmpath" | "path")
825 {
826 match fn_name.as_str() {
827 "addpath" => {
828 if args.is_empty() || args.len() > 2 {
829 return Err(
830 "addpath: expects 1 or 2 arguments: addpath(dir) or addpath(dir, '-end')".to_string()
831 );
832 }
833 let path_val = eval_with_io(&args[0], env, io)?;
834 let path_str = match &path_val {
835 Value::Str(s) | Value::StringObj(s) => s.clone(),
836 _ => {
837 return Err(
838 "addpath: argument must be a string (directory path)"
839 .to_string(),
840 );
841 }
842 };
843 let append = if args.len() == 2 {
844 let flag_val = eval_with_io(&args[1], env, io)?;
845 match &flag_val {
846 Value::Str(s) | Value::StringObj(s) if s == "-end" => true,
847 Value::Str(_) | Value::StringObj(_) => {
848 return Err(
849 "addpath: second argument must be '-end' (to append) or omitted (to prepend)".to_string()
850 );
851 }
852 _ => {
853 return Err(
854 "addpath: second argument must be a string '-end'"
855 .to_string(),
856 );
857 }
858 }
859 } else {
860 false
861 };
862 let expanded = expand_tilde(&path_str);
863 let pb = std::path::PathBuf::from(&expanded);
864 session_path_add(pb, append);
865 if !silent {
866 for p in session_path_list() {
867 println!("{}", p.display());
868 }
869 }
870 }
871 "rmpath" => {
872 if args.len() != 1 {
873 return Err("rmpath: expects exactly 1 argument".to_string());
874 }
875 let path_val = eval_with_io(&args[0], env, io)?;
876 let path_str = match &path_val {
877 Value::Str(s) | Value::StringObj(s) => s.clone(),
878 _ => {
879 return Err(
880 "rmpath: argument must be a string (directory path)"
881 .to_string(),
882 );
883 }
884 };
885 let expanded = expand_tilde(&path_str);
886 session_path_remove(std::path::Path::new(&expanded));
887 }
888 "path" => {
889 if !args.is_empty() {
890 return Err("path: takes no arguments".to_string());
891 }
892 if !silent {
893 let paths = session_path_list();
894 if paths.is_empty() {
895 println!("(search path is empty)");
896 } else {
897 for p in &paths {
898 println!("{}", p.display());
899 }
900 }
901 }
902 }
903 _ => unreachable!(),
904 }
905 continue;
906 }
907
908 if let Expr::Call(fn_name, args) = expr
911 && matches!(fn_name.as_str(), "run" | "source")
912 && args.len() == 1
913 {
914 let path_val = eval_with_io(&args[0], env, io)?;
915 let filename = match &path_val {
916 Value::Str(s) | Value::StringObj(s) => s.clone(),
917 _ => {
918 return Err(format!("{fn_name}: argument must be a string (filename)"));
919 }
920 };
921 let script_path = resolve_script_path(&filename)
922 .ok_or_else(|| format!("{fn_name}: script not found: '{filename}'"))?;
923 let content = std::fs::read_to_string(&script_path).map_err(|e| {
924 format!("{fn_name}: cannot read '{}': {e}", script_path.display())
925 })?;
926 let depth = RUN_DEPTH.with(|d| d.get());
927 if depth >= 64 {
928 return Err(format!(
929 "{fn_name}: maximum script nesting depth (64) exceeded"
930 ));
931 }
932 RUN_DEPTH.with(|d| d.set(depth + 1));
933 if let Some(dir) = script_path.parent() {
936 SCRIPT_DIR_STACK.with(|s| s.borrow_mut().push(dir.to_path_buf()));
937 }
938 let run_stmts = parse_stmts(&content).map_err(|e| {
939 format!("{fn_name}: parse error in '{}': {e}", script_path.display())
940 })?;
941
942 let is_fn_file = !run_stmts.is_empty()
948 && run_stmts
949 .iter()
950 .all(|(s, _)| matches!(s, Stmt::FunctionDef { .. }));
951 let result = if is_fn_file {
952 let primary_name = match &run_stmts[0].0 {
953 Stmt::FunctionDef { name, .. } => name.clone(),
954 _ => unreachable!(),
955 };
956 let mut locals: IndexMap<String, Value> = IndexMap::new();
957 for (stmt, _) in &run_stmts {
958 if let Stmt::FunctionDef {
959 name,
960 outputs,
961 params,
962 body_source,
963 doc,
964 } = stmt
965 && name != &primary_name
966 {
967 locals.insert(
968 name.clone(),
969 Value::Function {
970 outputs: outputs.clone(),
971 params: params.clone(),
972 body_source: body_source.clone(),
973 locals: IndexMap::new(),
974 doc: doc.clone(),
975 },
976 );
977 }
978 }
979 if let Stmt::FunctionDef {
980 outputs,
981 params,
982 body_source,
983 doc,
984 ..
985 } = &run_stmts[0].0
986 {
987 env.insert(
988 primary_name,
989 Value::Function {
990 outputs: outputs.clone(),
991 params: params.clone(),
992 body_source: body_source.clone(),
993 locals,
994 doc: doc.clone(),
995 },
996 );
997 }
998 Ok(None)
999 } else {
1000 for (stmt, _) in run_stmts.iter() {
1003 if let Stmt::FunctionDef {
1004 name,
1005 outputs,
1006 params,
1007 body_source,
1008 doc,
1009 } = stmt
1010 {
1011 env.insert(
1012 name.clone(),
1013 Value::Function {
1014 outputs: outputs.clone(),
1015 params: params.clone(),
1016 body_source: body_source.clone(),
1017 locals: IndexMap::new(),
1018 doc: doc.clone(),
1019 },
1020 );
1021 }
1022 }
1023 exec_stmts(&run_stmts, env, io, fmt, base, compact)
1024 };
1025 SCRIPT_DIR_STACK.with(|s| s.borrow_mut().pop());
1026 RUN_DEPTH.with(|d| d.set(depth));
1027 match result? {
1030 None => {}
1031 Some(sig) => return Ok(Some(sig)),
1032 }
1033 continue;
1034 }
1035
1036 if let Expr::Call(fn_name, args) = expr
1038 && fn_name == "clear"
1039 {
1040 if args.is_empty() {
1041 env.clear();
1042 } else {
1043 for arg in args {
1044 let key = match arg {
1045 Expr::StrLiteral(s) | Expr::StringObjLiteral(s) => s.clone(),
1046 other => match eval_with_io(other, env, io)? {
1047 Value::Str(s) | Value::StringObj(s) => s,
1048 _ => continue,
1049 },
1050 };
1051 env.remove(&key);
1052 }
1053 }
1054 continue;
1055 }
1056
1057 if let Expr::Call(fn_name, args) = expr
1059 && fn_name == "format"
1060 {
1061 let arg = match args.first() {
1062 Some(Expr::StrLiteral(s)) => s.as_str(),
1063 None => "",
1064 _ => "",
1065 };
1066 let new_fmt = match arg {
1067 "" | "short" => FormatMode::Short,
1068 "long" => FormatMode::Long,
1069 "shorte" | "shortE" => FormatMode::ShortE,
1070 "longe" | "longE" => FormatMode::LongE,
1071 "shortg" | "shortG" => FormatMode::ShortG,
1072 "longg" | "longG" => FormatMode::LongG,
1073 "bank" => FormatMode::Bank,
1074 "rat" => FormatMode::Rat,
1075 "hex" => FormatMode::Hex,
1076 "+" => FormatMode::Plus,
1077 "compact" | "loose" => get_display_fmt(),
1078 s => s
1079 .parse::<usize>()
1080 .map(FormatMode::Custom)
1081 .unwrap_or_else(|_| get_display_fmt()),
1082 };
1083 let new_compact = match arg {
1084 "compact" => true,
1085 "loose" => false,
1086 _ => get_display_compact(),
1087 };
1088 set_display_ctx(&new_fmt, base, new_compact);
1089 continue;
1090 }
1091
1092 if let Expr::Call(fn_name, args) = expr
1094 && matches!(fn_name.as_str(), "save" | "load" | "ws" | "wl")
1095 {
1096 let is_save = matches!(fn_name.as_str(), "save" | "ws");
1097 if is_save {
1098 let (path_opt, var_names) = if args.is_empty() {
1099 (None, vec![])
1100 } else {
1101 let path_val = eval_with_io(&args[0], env, io)?;
1102 let path_str = match path_val {
1103 Value::Str(s) | Value::StringObj(s) => s,
1104 _ => return Err("save: path argument must be a string".to_string()),
1105 };
1106 let mut vars: Vec<String> = Vec::new();
1107 for a in &args[1..] {
1108 let v = eval_with_io(a, env, io)?;
1109 match v {
1110 Value::Str(s) | Value::StringObj(s) => vars.push(s),
1111 _ => {
1112 return Err(
1113 "save: variable names must be strings".to_string()
1114 );
1115 }
1116 }
1117 }
1118 (Some(path_str), vars)
1119 };
1120 let result = match &path_opt {
1121 None => {
1122 let home = std::env::var("HOME")
1123 .or_else(|_| std::env::var("USERPROFILE"))
1124 .unwrap_or_default();
1125 let p = std::path::Path::new(&home)
1126 .join(".config")
1127 .join("ccalc")
1128 .join("workspace.toml");
1129 save_workspace(env, &p)
1130 }
1131 Some(p) if var_names.is_empty() => {
1132 save_workspace(env, std::path::Path::new(p))
1133 }
1134 Some(p) => {
1135 let refs: Vec<&str> =
1136 var_names.iter().map(String::as_str).collect();
1137 save_workspace_vars(env, std::path::Path::new(p), &refs)
1138 }
1139 };
1140 if let Err(e) = result {
1141 return Err(format!("save: {e}"));
1142 }
1143 } else {
1144 let loaded = if args.is_empty() {
1146 let home = std::env::var("HOME")
1147 .or_else(|_| std::env::var("USERPROFILE"))
1148 .unwrap_or_default();
1149 let p = std::path::Path::new(&home)
1150 .join(".config")
1151 .join("ccalc")
1152 .join("workspace.toml");
1153 load_workspace(&p)
1154 } else {
1155 let path_val = eval_with_io(&args[0], env, io)?;
1156 let path_str = match path_val {
1157 Value::Str(s) | Value::StringObj(s) => s,
1158 _ => return Err("load: path argument must be a string".to_string()),
1159 };
1160 load_workspace(std::path::Path::new(&path_str))
1161 };
1162 match loaded {
1163 Ok(ws) => env.extend(ws),
1164 Err(e) => return Err(format!("load: {e}")),
1165 }
1166 }
1167 continue;
1168 }
1169
1170 let val = eval_with_io(expr, env, io)?;
1171 env.insert("ans".to_string(), val.clone());
1172 if !silent && !matches!(val, Value::Void) {
1173 print_value(None, &val, fmt, base, compact);
1174 }
1175 }
1176
1177 Stmt::If {
1178 cond,
1179 body,
1180 elseif_branches,
1181 else_body,
1182 } => {
1183 let cond_val = eval_with_io(cond, env, io)?;
1184 let chosen: Option<&[(Stmt, bool)]> = if is_truthy(&cond_val) {
1185 Some(body)
1186 } else {
1187 let mut found = None;
1188 for (ei_cond, ei_body) in elseif_branches {
1189 if is_truthy(&eval_with_io(ei_cond, env, io)?) {
1190 found = Some(ei_body.as_slice());
1191 break;
1192 }
1193 }
1194 if found.is_none() {
1195 found = else_body.as_deref();
1196 }
1197 found
1198 };
1199 if let Some(body_stmts) = chosen
1200 && let Some(sig) = exec_stmts(body_stmts, env, io, fmt, base, compact)?
1201 {
1202 return Ok(Some(sig));
1203 }
1204 }
1205
1206 Stmt::For {
1207 var,
1208 range_expr,
1209 body,
1210 } => {
1211 let range_val = eval_with_io(range_expr, env, io)?;
1212 let iter_cols: Vec<Value> = match range_val {
1213 Value::Scalar(n) => vec![Value::Scalar(n)],
1214 Value::Matrix(m) => {
1215 let nrows = m.nrows();
1216 let ncols = m.ncols();
1217 (0..ncols)
1218 .map(|j| {
1219 if nrows == 1 {
1220 Value::Scalar(m[[0, j]])
1222 } else {
1223 let mut col = Array2::zeros((nrows, 1));
1225 for i in 0..nrows {
1226 col[[i, 0]] = m[[i, j]];
1227 }
1228 Value::Matrix(col)
1229 }
1230 })
1231 .collect()
1232 }
1233 _ => return Err("'for' range must evaluate to a scalar or matrix".to_string()),
1234 };
1235
1236 'for_loop: for col_val in iter_cols {
1237 env.insert(var.clone(), col_val);
1238 match exec_stmts(body, env, io, fmt, base, compact)? {
1239 None => {}
1240 Some(Signal::Break) => break 'for_loop,
1241 Some(Signal::Continue) => continue 'for_loop,
1242 Some(Signal::Return) => return Ok(Some(Signal::Return)),
1243 }
1244 }
1245 }
1246
1247 Stmt::While { cond, body } => loop {
1248 if !is_truthy(&eval_with_io(cond, env, io)?) {
1249 break;
1250 }
1251 match exec_stmts(body, env, io, fmt, base, compact)? {
1252 None => {}
1253 Some(Signal::Break) => break,
1254 Some(Signal::Continue) => continue,
1255 Some(Signal::Return) => return Ok(Some(Signal::Return)),
1256 }
1257 },
1258
1259 Stmt::Break => return Ok(Some(Signal::Break)),
1260 Stmt::Continue => return Ok(Some(Signal::Continue)),
1261
1262 Stmt::Switch {
1264 expr,
1265 cases,
1266 otherwise_body,
1267 } => {
1268 let switch_val = eval_with_io(expr, env, io)?;
1269 let mut matched = false;
1270 'switch_loop: for (case_exprs, case_body) in cases {
1271 for case_expr in case_exprs {
1272 let case_val = eval_with_io(case_expr, env, io)?;
1273 let is_match = if let Value::Cell(cell_elems) = &case_val {
1276 cell_elems.iter().any(|elem| match (&switch_val, elem) {
1277 (Value::Scalar(a), Value::Scalar(b)) => a == b,
1278 _ => {
1279 let sv = match &switch_val {
1280 Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
1281 _ => None,
1282 };
1283 let cv = match elem {
1284 Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
1285 _ => None,
1286 };
1287 matches!((sv, cv), (Some(a), Some(b)) if a == b)
1288 }
1289 })
1290 } else {
1291 match (&switch_val, &case_val) {
1292 (Value::Scalar(a), Value::Scalar(b)) => a == b,
1293 _ => {
1294 let sv = match &switch_val {
1295 Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
1296 _ => None,
1297 };
1298 let cv = match &case_val {
1299 Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
1300 _ => None,
1301 };
1302 matches!((sv, cv), (Some(a), Some(b)) if a == b)
1303 }
1304 }
1305 };
1306 if is_match {
1307 if let Some(sig) = exec_stmts(case_body, env, io, fmt, base, compact)? {
1308 return Ok(Some(sig));
1309 }
1310 matched = true;
1311 break 'switch_loop;
1312 }
1313 }
1314 }
1315 if !matched
1316 && let Some(ob) = otherwise_body
1317 && let Some(sig) = exec_stmts(ob, env, io, fmt, base, compact)?
1318 {
1319 return Ok(Some(sig));
1320 }
1321 }
1322
1323 Stmt::DoUntil { body, cond } => loop {
1325 match exec_stmts(body, env, io, fmt, base, compact)? {
1326 Some(Signal::Break) => break,
1327 Some(Signal::Continue) | None => {}
1328 Some(Signal::Return) => return Ok(Some(Signal::Return)),
1329 }
1330 if is_truthy(&eval_with_io(cond, env, io)?) {
1331 break;
1332 }
1333 },
1334
1335 Stmt::TryCatch {
1337 try_body,
1338 catch_var,
1339 catch_body,
1340 } => match exec_stmts(try_body, env, io, fmt, base, compact) {
1341 Ok(None) => {}
1342 Ok(Some(sig)) => return Ok(Some(sig)),
1343 Err(msg) => {
1344 set_last_err(&msg);
1345 if let Some(var) = catch_var {
1346 let mut map = IndexMap::new();
1347 map.insert("message".to_string(), Value::Str(msg));
1348 env.insert(var.clone(), Value::Struct(map));
1349 }
1350 if let Some(sig) = exec_stmts(catch_body, env, io, fmt, base, compact)? {
1351 return Ok(Some(sig));
1352 }
1353 }
1354 },
1355
1356 Stmt::FunctionDef {
1358 name,
1359 outputs,
1360 params,
1361 body_source,
1362 doc,
1363 } => {
1364 env.insert(
1365 name.clone(),
1366 Value::Function {
1367 outputs: outputs.clone(),
1368 params: params.clone(),
1369 body_source: body_source.clone(),
1370 locals: IndexMap::new(),
1371 doc: doc.clone(),
1372 },
1373 );
1374 }
1375
1376 Stmt::Return => return Ok(Some(Signal::Return)),
1378
1379 Stmt::CellSet(cell_name, idx_expr, val_expr) => {
1381 let cell_len = match env.get(cell_name) {
1383 Some(Value::Cell(v)) => v.len(),
1384 _ => 0,
1385 };
1386 let env_end = write_env_with_end(env, cell_len);
1387 let idx = eval_with_io(idx_expr, &env_end, io)?;
1388 let rhs = eval_with_io(val_expr, env, io)?;
1389 let i = match idx {
1390 Value::Scalar(n) => n as isize,
1391 _ => return Err(format!("{cell_name}{{}}: index must be a scalar integer")),
1392 };
1393 match env.get_mut(cell_name) {
1394 Some(Value::Cell(v)) => {
1395 if i < 1 {
1396 return Err(format!(
1397 "{cell_name}{{}}: index {i} out of range (1..{})",
1398 v.len()
1399 ));
1400 }
1401 let idx = (i - 1) as usize;
1402 if idx >= v.len() {
1404 v.resize(idx + 1, Value::Scalar(0.0));
1405 }
1406 v[idx] = rhs.clone();
1407 }
1408 Some(_) => {
1409 return Err(format!(
1410 "'{cell_name}' is not a cell array; use () for regular indexing"
1411 ));
1412 }
1413 None => {
1414 if i < 1 {
1416 return Err(format!("{cell_name}{{}}: index {i} must be >= 1"));
1417 }
1418 let idx = (i - 1) as usize;
1419 let mut v = vec![Value::Scalar(0.0); idx + 1];
1420 v[idx] = rhs.clone();
1421 env.insert(cell_name.clone(), Value::Cell(v));
1422 }
1423 }
1424 if !silent && let Some(val) = env.get(cell_name) {
1425 print_value(Some(cell_name), val, fmt, base, compact);
1426 }
1427 }
1428
1429 Stmt::FieldSet(base_name, path, rhs_expr) => {
1431 let rhs = eval_with_io(rhs_expr, env, io)?;
1432 let root = match env.remove(base_name) {
1433 Some(Value::Struct(m)) => m,
1434 None => IndexMap::new(),
1435 Some(other) => {
1436 env.insert(base_name.clone(), other);
1437 return Err(format!("'{base_name}' is not a struct"));
1438 }
1439 };
1440 let updated = set_nested(root, path, rhs)?;
1441 let struct_val = Value::Struct(updated);
1442 if !silent {
1443 print_value(Some(base_name), &struct_val, fmt, base, compact);
1444 }
1445 env.insert(base_name.clone(), struct_val);
1446 }
1447
1448 Stmt::StructArrayFieldSet(base_name, idx_expr, path, rhs_expr) => {
1450 let rhs = eval_with_io(rhs_expr, env, io)?;
1451 let idx_val = eval_with_io(idx_expr, env, io)?;
1452 let idx = match &idx_val {
1453 Value::Scalar(n) => {
1454 let i = *n as isize;
1455 if i < 1 {
1456 return Err(format!(
1457 "Struct array index must be a positive integer, got {n}"
1458 ));
1459 }
1460 i as usize
1461 }
1462 _ => return Err("Struct array index must be a scalar integer".to_string()),
1463 };
1464 let mut arr: Vec<IndexMap<String, Value>> = match env.remove(base_name) {
1466 Some(Value::StructArray(v)) => v,
1467 Some(Value::Struct(m)) => vec![m],
1469 None => Vec::new(),
1470 Some(other) => {
1471 env.insert(base_name.clone(), other);
1472 return Err(format!("'{base_name}' is not a struct array"));
1473 }
1474 };
1475 while arr.len() < idx {
1477 arr.push(IndexMap::new());
1478 }
1479 let elem = arr[idx - 1].clone();
1481 let updated_elem = set_nested(elem, path, rhs)?;
1482 arr[idx - 1] = updated_elem;
1483 let arr_val = Value::StructArray(arr);
1484 if !silent {
1485 print_value(Some(base_name), &arr_val, fmt, base, compact);
1486 }
1487 env.insert(base_name.clone(), arr_val);
1488 }
1489
1490 Stmt::IndexSet {
1492 name,
1493 indices,
1494 value,
1495 } => {
1496 let rhs = eval_with_io(value, env, io)?;
1497 if is_persistent(name) {
1500 let func = current_func_name();
1501 if let Some(fresh) = persistent_load(&func, name) {
1502 env.insert(name.clone(), fresh);
1503 }
1504 }
1505 exec_index_set(name, indices, rhs, env, io)?;
1506 if is_persistent(name)
1508 && let Some(val) = env.get(name)
1509 {
1510 persistent_save(¤t_func_name(), name, val.clone());
1511 }
1512 if !silent && let Some(val) = env.get(name) {
1513 print_value(Some(name), val, fmt, base, compact);
1514 }
1515 }
1516
1517 Stmt::MultiAssign { targets, expr } => {
1519 set_nargout(targets.len());
1520 let val = eval_with_io(expr, env, io)?;
1521 let vals: Vec<Value> = match val {
1522 Value::Tuple(v) => v,
1523 other => vec![other],
1524 };
1525 for (i, target) in targets.iter().enumerate() {
1526 if target == "~" {
1527 continue; }
1529 let v = vals.get(i).cloned().unwrap_or(Value::Void);
1530 env.insert(target.clone(), v.clone());
1531 if !silent && !matches!(v, Value::Void) {
1532 print_value(Some(target), &v, fmt, base, compact);
1533 }
1534 }
1535 }
1536 }
1537 }
1538 global_refresh_into_env(env);
1542 Ok(None)
1543}
1544
1545enum WriteIdx {
1549 All,
1551 Positions(Vec<usize>),
1553}
1554
1555fn resolve_write_dim(
1561 expr: &crate::eval::Expr,
1562 dim_size: usize,
1563 env: &Env,
1564 io: &mut IoContext,
1565) -> Result<WriteIdx, String> {
1566 if matches!(expr, crate::eval::Expr::Colon) {
1567 return Ok(WriteIdx::All);
1568 }
1569 let val = eval_with_io(expr, env, io)?;
1570 let floats: Vec<f64> = match val {
1571 Value::Scalar(n) => vec![n],
1572 Value::Complex(re, im) => {
1573 if im != 0.0 {
1574 return Err("Index must be real, not complex".to_string());
1575 }
1576 vec![re]
1577 }
1578 Value::Matrix(m) => {
1579 let total = m.nrows() * m.ncols();
1580 if m.nrows() > 1 && m.ncols() > 1 && total != dim_size {
1581 return Err("Index must be a scalar or vector, not a 2-D matrix".to_string());
1582 }
1583 if m.nrows() > 1 && m.ncols() > 1 {
1585 let mut v = Vec::with_capacity(total);
1586 for col in 0..m.ncols() {
1587 for row in 0..m.nrows() {
1588 v.push(m[[row, col]]);
1589 }
1590 }
1591 v
1592 } else {
1593 m.iter().copied().collect()
1594 }
1595 }
1596 _ => return Err("Index must be numeric".to_string()),
1597 };
1598 if dim_size > 0 && floats.len() == dim_size && floats.iter().all(|&f| f == 0.0 || f == 1.0) {
1600 let positions: Vec<usize> = floats
1601 .iter()
1602 .enumerate()
1603 .filter(|&(_, &f)| f == 1.0)
1604 .map(|(i, _)| i)
1605 .collect();
1606 return Ok(WriteIdx::Positions(positions));
1607 }
1608 let positions: Result<Vec<usize>, String> = floats
1610 .iter()
1611 .map(|&n| {
1612 let i = n.round() as i64;
1613 if i < 1 {
1614 return Err(format!("Index {i} must be >= 1"));
1615 }
1616 Ok(i as usize - 1)
1617 })
1618 .collect();
1619 Ok(WriteIdx::Positions(positions?))
1620}
1621
1622fn write_env_with_end(env: &Env, dim_size: usize) -> Env {
1624 let mut e = env.clone();
1625 e.insert("end".to_string(), Value::Scalar(dim_size as f64));
1626 e
1627}
1628
1629fn exec_index_set(
1637 name: &str,
1638 indices: &[crate::eval::Expr],
1639 rhs: Value,
1640 env: &mut Env,
1641 io: &mut IoContext,
1642) -> Result<(), String> {
1643 let (mut mat, was_scalar) = match env.get(name) {
1645 Some(Value::Matrix(m)) => (m.clone(), false),
1646 Some(Value::Scalar(n)) => (Array2::from_elem((1, 1), *n), true),
1647 None | Some(Value::Void) => (Array2::zeros((0, 0)), false),
1648 Some(_) => {
1649 return Err(format!(
1650 "'{name}' is not a matrix; cannot use () indexed assignment"
1651 ));
1652 }
1653 };
1654
1655 match indices.len() {
1656 1 => {
1657 let total = mat.nrows() * mat.ncols();
1658 let env_end = write_env_with_end(env, total);
1659 let widx = resolve_write_dim(&indices[0], total, &env_end, io)?;
1660
1661 let positions: Vec<usize> = match widx {
1663 WriteIdx::All => (0..total).collect(),
1664 WriteIdx::Positions(p) => p,
1665 };
1666
1667 let rhs_vals: Vec<f64> = match &rhs {
1669 Value::Scalar(n) => vec![*n; positions.len()],
1670 Value::Matrix(m) => {
1671 let flat: Vec<f64> = m.iter().copied().collect();
1672 if flat.len() != positions.len() {
1673 return Err(format!(
1674 "Assignment dimension mismatch: {} positions but {} values",
1675 positions.len(),
1676 flat.len()
1677 ));
1678 }
1679 flat
1680 }
1681 _ => {
1682 return Err(
1683 "Indexed assignment: RHS must be a numeric scalar or matrix".to_string()
1684 );
1685 }
1686 };
1687
1688 let required = positions.iter().copied().max().map(|m| m + 1).unwrap_or(0);
1690 let required = required.max(total);
1691
1692 let (out_rows, out_cols) = if mat.nrows() == 0 || mat.ncols() == 0 {
1694 (1, required)
1696 } else if mat.nrows() == 1 {
1697 (1, required)
1698 } else if mat.ncols() == 1 {
1699 (required, 1)
1700 } else if required > total {
1701 return Err("Cannot grow a 2-D matrix with linear indexing".to_string());
1702 } else {
1703 (mat.nrows(), mat.ncols())
1704 };
1705
1706 if required > total || out_rows != mat.nrows() || out_cols != mat.ncols() {
1708 let mut new_mat = Array2::<f64>::zeros((out_rows, out_cols));
1709 for old_p in 0..total {
1710 let old_row = old_p % mat.nrows().max(1);
1711 let old_col = old_p / mat.nrows().max(1);
1712 let new_row = old_p % out_rows;
1713 let new_col = old_p / out_rows;
1714 if old_row < mat.nrows() && old_col < mat.ncols() {
1715 new_mat[[new_row, new_col]] = mat[[old_row, old_col]];
1716 }
1717 }
1718 mat = new_mat;
1719 }
1720
1721 for (&pos, &val) in positions.iter().zip(rhs_vals.iter()) {
1723 let row = pos % mat.nrows();
1724 let col = pos / mat.nrows();
1725 mat[[row, col]] = val;
1726 }
1727 }
1728 2 => {
1729 let nrows = mat.nrows();
1730 let ncols = mat.ncols();
1731 let env_r = write_env_with_end(env, nrows);
1732 let env_c = write_env_with_end(env, ncols);
1733 let ridx = resolve_write_dim(&indices[0], nrows, &env_r, io)?;
1734 let cidx = resolve_write_dim(&indices[1], ncols, &env_c, io)?;
1735
1736 let rows: Vec<usize> = match ridx {
1737 WriteIdx::All => (0..nrows.max(1)).collect(),
1738 WriteIdx::Positions(p) => p,
1739 };
1740 let cols: Vec<usize> = match cidx {
1741 WriteIdx::All => (0..ncols.max(1)).collect(),
1742 WriteIdx::Positions(p) => p,
1743 };
1744
1745 let req_rows = rows
1746 .iter()
1747 .copied()
1748 .max()
1749 .map(|m| m + 1)
1750 .unwrap_or(0)
1751 .max(nrows);
1752 let req_cols = cols
1753 .iter()
1754 .copied()
1755 .max()
1756 .map(|m| m + 1)
1757 .unwrap_or(0)
1758 .max(ncols);
1759
1760 if req_rows != nrows || req_cols != ncols {
1762 let mut new_mat = Array2::<f64>::zeros((req_rows, req_cols));
1763 for r in 0..nrows {
1764 for c in 0..ncols {
1765 new_mat[[r, c]] = mat[[r, c]];
1766 }
1767 }
1768 mat = new_mat;
1769 }
1770
1771 let n_sel = rows.len() * cols.len();
1773 let rhs_vals: Vec<f64> = match &rhs {
1774 Value::Scalar(n) => vec![*n; n_sel],
1775 Value::Matrix(m) => {
1776 let flat: Vec<f64> = m.iter().copied().collect();
1777 if flat.len() != n_sel {
1778 return Err(format!(
1779 "Assignment dimension mismatch: {}×{} = {} positions but {} values",
1780 rows.len(),
1781 cols.len(),
1782 n_sel,
1783 flat.len()
1784 ));
1785 }
1786 flat
1787 }
1788 _ => {
1789 return Err(
1790 "Indexed assignment: RHS must be a numeric scalar or matrix".to_string()
1791 );
1792 }
1793 };
1794
1795 let mut k = 0;
1797 for &r in &rows {
1798 for &c in &cols {
1799 mat[[r, c]] = rhs_vals[k];
1800 k += 1;
1801 }
1802 }
1803 }
1804 _ => return Err("Indexed assignment supports at most 2 indices".to_string()),
1805 }
1806
1807 let result = if mat.nrows() == 1 && mat.ncols() == 1 {
1809 Value::Scalar(mat[[0, 0]])
1810 } else {
1811 Value::Matrix(mat)
1812 };
1813 let _ = was_scalar;
1814 env.insert(name.to_string(), result);
1815 Ok(())
1816}