1use std::collections::HashMap;
2use std::rc::Rc;
3
4type BodyCache = HashMap<String, Rc<Vec<StmtEntry>>>;
6
7type ChunkCache = HashMap<String, Option<Rc<crate::vm::Chunk>>>;
13
14fn expand_tilde(path: &str) -> String {
19 if path == "~" || path.starts_with("~/") || path.starts_with("~\\") {
20 let home = std::env::var("HOME")
21 .or_else(|_| std::env::var("USERPROFILE"))
22 .unwrap_or_default();
23 if home.is_empty() {
24 return path.to_string();
25 }
26 if path == "~" {
27 home
28 } else {
29 format!("{}{}", home, &path[1..])
30 }
31 } else {
32 path.to_string()
33 }
34}
35
36use indexmap::IndexMap;
37use ndarray::Array2;
38use num_complex::Complex;
39
40use crate::env::{Env, Value};
41use crate::env::{load_workspace, save_workspace, save_workspace_vars};
42use crate::eval::{
43 Base, Expr, FormatMode, autoload_cache_insert, current_func_name, eval_with_io, format_complex,
44 format_scalar, format_value_full, get_display_base, get_display_compact, get_display_fmt,
45 global_declare, global_frame_pop, global_frame_push, global_get, global_init_if_absent,
46 global_refresh_into_env, global_set, is_global, is_persistent, persistent_declare,
47 persistent_frame_pop, persistent_frame_push, persistent_load, persistent_save,
48 set_autoload_hook, set_display_ctx, set_eval_str_hook, set_fn_call_hook, set_last_err,
49 set_nargout,
50};
51use crate::io::IoContext;
52use crate::parser::{Stmt, StmtEntry, parse_stmts};
53
54thread_local! {
55 static RUN_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
57
58 static SCRIPT_DIR_STACK: std::cell::RefCell<Vec<std::path::PathBuf>> =
64 const { std::cell::RefCell::new(Vec::new()) };
65
66 static SESSION_PATH: std::cell::RefCell<Vec<std::path::PathBuf>> =
71 const { std::cell::RefCell::new(Vec::new()) };
72
73 static BODY_CACHE: std::cell::RefCell<BodyCache> =
83 std::cell::RefCell::new(HashMap::new());
84
85 static BODY_CHUNK_CACHE: std::cell::RefCell<ChunkCache> =
90 std::cell::RefCell::new(HashMap::new());
91}
92
93fn silence_all(stmts: Vec<StmtEntry>) -> Vec<StmtEntry> {
101 stmts
102 .into_iter()
103 .map(|(stmt, _, line)| {
104 let stmt = match stmt {
105 Stmt::If {
106 cond,
107 body,
108 elseif_branches,
109 else_body,
110 } => Stmt::If {
111 cond,
112 body: silence_all(body),
113 elseif_branches: elseif_branches
114 .into_iter()
115 .map(|(c, b)| (c, silence_all(b)))
116 .collect(),
117 else_body: else_body.map(silence_all),
118 },
119 Stmt::For {
120 var,
121 range_expr,
122 body,
123 } => Stmt::For {
124 var,
125 range_expr,
126 body: silence_all(body),
127 },
128 Stmt::While { cond, body } => Stmt::While {
129 cond,
130 body: silence_all(body),
131 },
132 Stmt::DoUntil { body, cond } => Stmt::DoUntil {
133 body: silence_all(body),
134 cond,
135 },
136 Stmt::Switch {
137 expr,
138 cases,
139 otherwise_body,
140 } => Stmt::Switch {
141 expr,
142 cases: cases
143 .into_iter()
144 .map(|(v, b)| (v, silence_all(b)))
145 .collect(),
146 otherwise_body: otherwise_body.map(silence_all),
147 },
148 Stmt::TryCatch {
149 try_body,
150 catch_var,
151 catch_body,
152 } => Stmt::TryCatch {
153 try_body: silence_all(try_body),
154 catch_var,
155 catch_body: silence_all(catch_body),
156 },
157 other => other,
158 };
159 (stmt, true, line)
160 })
161 .collect()
162}
163
164fn get_or_parse_body(body_source: &str) -> Result<Rc<Vec<StmtEntry>>, String> {
170 BODY_CACHE.with(|cache| {
171 let mut cache = cache.borrow_mut();
172 if let Some(body) = cache.get(body_source) {
173 return Ok(Rc::clone(body));
174 }
175 let stmts =
176 parse_stmts(body_source).map_err(|e| format!("function body parse error: {e}"))?;
177 let silent = silence_all(stmts);
178 let rc = Rc::new(silent);
179 cache.insert(body_source.to_string(), Rc::clone(&rc));
180 Ok(rc)
181 })
182}
183
184fn annotate_line(e: String, line: usize) -> String {
188 if line == 0 || e.contains("near line") {
189 e
190 } else {
191 format!("{e} near line {line}")
192 }
193}
194
195fn strip_near_line(s: String) -> String {
200 if let Some(pos) = s.rfind(" near line ") {
201 let after = &s[pos + " near line ".len()..];
202 if !after.is_empty() && after.bytes().all(|b| b.is_ascii_digit()) {
203 return s[..pos].to_string();
204 }
205 }
206 s
207}
208
209pub enum Signal {
215 Break,
217 Continue,
219 Return,
221}
222
223pub fn init() {
227 set_fn_call_hook(call_user_function);
228 set_autoload_hook(try_autoload);
229 set_eval_str_hook(eval_str_impl);
230}
231
232fn eval_str_impl(code: &str, env: &crate::env::Env) -> Result<crate::env::Value, String> {
238 let mut env_clone = env.clone();
239 env_clone
240 .entry("ans".to_string())
241 .or_insert(crate::env::Value::Scalar(0.0));
242 let mut tmp_io = crate::io::IoContext::new();
243 let fmt = get_display_fmt();
244 let base = get_display_base();
245 let compact = get_display_compact();
246 let stmts = parse_stmts(code).map_err(|e| format!("eval: parse error: {e}"))?;
247 exec_stmts(&stmts, &mut env_clone, &mut tmp_io, &fmt, base, compact)?;
248 Ok(env_clone
249 .get("ans")
250 .cloned()
251 .unwrap_or(crate::env::Value::Void))
252}
253
254fn try_autoload(name: &str) -> bool {
261 if name.contains('.') {
262 return try_autoload_pkg(name);
263 }
264 let candidates = [format!("{name}.calc"), format!("{name}.m")];
265 for candidate in &candidates {
266 let Some(path) = resolve_script_path(candidate) else {
267 continue;
268 };
269 let Ok(content) = std::fs::read_to_string(&path) else {
270 continue;
271 };
272 let Ok(stmts) = parse_stmts(&content) else {
273 continue;
274 };
275 if !matches!(stmts.first(), Some((Stmt::FunctionDef { .. }, _, _))) {
276 continue;
277 }
278 let primary_name = match &stmts[0].0 {
279 Stmt::FunctionDef { name, .. } => name.clone(),
280 _ => continue,
281 };
282 let mut locals: IndexMap<String, Value> = IndexMap::new();
283 for (stmt, _, _) in &stmts {
284 if let Stmt::FunctionDef {
285 name: n,
286 outputs,
287 params,
288 body_source,
289 doc,
290 } = stmt
291 && n != &primary_name
292 {
293 locals.insert(
294 n.clone(),
295 Value::Function(Box::new(crate::env::FunctionData {
296 outputs: outputs.clone(),
297 params: params.clone(),
298 body_source: body_source.clone(),
299 locals: IndexMap::new(),
300 doc: doc.clone(),
301 })),
302 );
303 }
304 }
305 if let Stmt::FunctionDef {
306 outputs,
307 params,
308 body_source,
309 doc,
310 ..
311 } = &stmts[0].0
312 {
313 autoload_cache_insert(
314 primary_name,
315 Value::Function(Box::new(crate::env::FunctionData {
316 outputs: outputs.clone(),
317 params: params.clone(),
318 body_source: body_source.clone(),
319 locals,
320 doc: doc.clone(),
321 })),
322 );
323 return true;
324 }
325 }
326 false
327}
328
329fn try_autoload_pkg(qualified: &str) -> bool {
335 let parts: Vec<&str> = qualified.split('.').collect();
336 if parts.len() < 2 {
337 return false;
338 }
339 let func_name = *parts.last().unwrap();
340 let pkg_parts = &parts[..parts.len() - 1];
341
342 let mut rel_prefix = std::path::PathBuf::new();
344 for pkg in pkg_parts {
345 rel_prefix.push(format!("+{pkg}"));
346 }
347
348 let candidates = [
349 rel_prefix.join(format!("{func_name}.calc")),
350 rel_prefix.join(format!("{func_name}.m")),
351 ];
352
353 let mut search_dirs: Vec<std::path::PathBuf> = Vec::new();
355 SCRIPT_DIR_STACK.with(|s| search_dirs.extend(s.borrow().iter().cloned()));
356 search_dirs.push(std::path::PathBuf::from("."));
357 SESSION_PATH.with(|s| search_dirs.extend(s.borrow().iter().cloned()));
358
359 for dir in &search_dirs {
360 for candidate in &candidates {
361 let full = dir.join(candidate);
362 let Ok(content) = std::fs::read_to_string(&full) else {
363 continue;
364 };
365 let Ok(stmts) = parse_stmts(&content) else {
366 continue;
367 };
368 if !matches!(stmts.first(), Some((Stmt::FunctionDef { .. }, _, _))) {
369 continue;
370 }
371 let primary_name = match &stmts[0].0 {
372 Stmt::FunctionDef { name, .. } => name.clone(),
373 _ => continue,
374 };
375 let mut locals: IndexMap<String, Value> = IndexMap::new();
376 for (stmt, _, _) in &stmts {
377 if let Stmt::FunctionDef {
378 name: n,
379 outputs,
380 params,
381 body_source,
382 doc,
383 } = stmt
384 && n != &primary_name
385 {
386 locals.insert(
387 n.clone(),
388 Value::Function(Box::new(crate::env::FunctionData {
389 outputs: outputs.clone(),
390 params: params.clone(),
391 body_source: body_source.clone(),
392 locals: IndexMap::new(),
393 doc: doc.clone(),
394 })),
395 );
396 }
397 }
398 if let Stmt::FunctionDef {
399 outputs,
400 params,
401 body_source,
402 doc,
403 ..
404 } = &stmts[0].0
405 {
406 autoload_cache_insert(
407 qualified.to_string(),
408 Value::Function(Box::new(crate::env::FunctionData {
409 outputs: outputs.clone(),
410 params: params.clone(),
411 body_source: body_source.clone(),
412 locals,
413 doc: doc.clone(),
414 })),
415 );
416 return true;
417 }
418 }
419 }
420 false
421}
422
423pub fn script_dir_push(dir: &std::path::Path) {
429 SCRIPT_DIR_STACK.with(|s| s.borrow_mut().push(dir.to_path_buf()));
430}
431
432pub fn script_dir_pop() {
434 SCRIPT_DIR_STACK.with(|s| s.borrow_mut().pop());
435}
436
437pub fn session_path_init(paths: Vec<std::path::PathBuf>) {
442 SESSION_PATH.with(|p| *p.borrow_mut() = paths);
443}
444
445pub fn session_path_add(path: std::path::PathBuf, append: bool) {
450 SESSION_PATH.with(|p| {
451 let mut v = p.borrow_mut();
452 v.retain(|e| e != &path);
453 if append {
454 v.push(path);
455 } else {
456 v.insert(0, path);
457 }
458 });
459}
460
461pub fn session_path_remove(path: &std::path::Path) {
463 SESSION_PATH.with(|p| p.borrow_mut().retain(|e| e.as_path() != path));
464}
465
466pub fn session_path_list() -> Vec<std::path::PathBuf> {
468 SESSION_PATH.with(|p| p.borrow().clone())
469}
470
471fn call_user_function(
478 name: &str,
479 func: &Value,
480 args: &[Value],
481 caller_env: &Env,
482 io: &mut IoContext,
483) -> Result<Value, String> {
484 let Value::Function(fd) = func else {
485 return Err("call_user_function: not a Function value".to_string());
486 };
487 let (outputs, params, body_source, locals) =
488 (&fd.outputs, &fd.params, &fd.body_source, &fd.locals);
489
490 global_frame_push();
492 persistent_frame_push(name); let mut local_env = Env::new();
498 local_env.insert("i".to_string(), Value::Complex(0.0, 1.0));
499 local_env.insert("j".to_string(), Value::Complex(0.0, 1.0));
500 local_env.insert("ans".to_string(), Value::Scalar(0.0));
501 for (fn_name, val) in locals.iter() {
504 local_env.insert(fn_name.clone(), val.clone());
505 }
506 for (var_name, val) in caller_env.iter() {
507 if matches!(val, Value::Function(_) | Value::Lambda(_)) {
508 local_env.insert(var_name.clone(), val.clone());
509 }
510 }
511
512 let has_varargin = params.last().is_some_and(|p| p == "varargin");
514 let fixed_params = if has_varargin {
515 ¶ms[..params.len() - 1]
516 } else {
517 params.as_slice()
518 };
519
520 let effective_args = if args.len() > params.len() {
525 if !has_varargin && args.len() > params.len() + 1 {
526 return Err(format!(
527 "Too many arguments: expected at most {}, got {}",
528 params.len(),
529 args.len()
530 ));
531 }
532 if has_varargin {
533 args
534 } else {
535 &args[..params.len()]
537 }
538 } else {
539 args
540 };
541
542 for (p, a) in fixed_params.iter().zip(effective_args.iter()) {
544 local_env.insert(p.clone(), a.clone());
545 }
546
547 if has_varargin {
549 let extra: Vec<Value> = effective_args
553 .get(fixed_params.len()..)
554 .unwrap_or(&[])
555 .to_vec();
556 let varargin = Value::Cell(Box::new(extra));
557 local_env.insert("varargin".to_string(), varargin);
558 }
559
560 let nargin = effective_args.len().min(params.len());
561 local_env.insert("nargin".to_string(), Value::Scalar(nargin as f64));
562 local_env.insert("nargout".to_string(), Value::Scalar(outputs.len() as f64));
563
564 let body = get_or_parse_body(body_source)?;
566 let fmt = get_display_fmt();
567 let base = get_display_base();
568 let compact = get_display_compact();
569
570 let maybe_chunk: Option<Rc<crate::vm::Chunk>> = BODY_CHUNK_CACHE.with(|cc| {
572 let mut cache = cc.borrow_mut();
573 if let Some(entry) = cache.get(body_source) {
574 return entry.clone();
575 }
576 let result: Option<Rc<crate::vm::Chunk>> = match crate::vm::compile::compile(&body) {
577 Ok(chunk) => Some(Rc::new(chunk)),
578 Err(_) => None,
579 };
580 cache.insert(body_source.to_string(), result.clone());
581 result
582 });
583
584 let exec_result = if let Some(chunk) = maybe_chunk {
585 crate::vm::exec::vm_exec(&chunk, &mut local_env, io, &fmt, base, compact)
586 } else {
587 exec_stmts(&body, &mut local_env, io, &fmt, base, compact)
588 };
589
590 let (func_name_saved, persistent_names) = persistent_frame_pop();
592 for var_name in &persistent_names {
593 if let Some(val) = local_env.get(var_name) {
594 persistent_save(&func_name_saved, var_name, val.clone());
595 }
596 }
597 global_frame_pop();
599
600 match exec_result? {
602 None | Some(Signal::Return) => {}
603 Some(Signal::Break) => return Err("'break' outside loop".to_string()),
604 Some(Signal::Continue) => return Err("'continue' outside loop".to_string()),
605 }
606
607 if outputs.is_empty() {
609 return Ok(Value::Void);
610 }
611
612 if outputs.len() == 1 && outputs[0] == "varargout" {
614 let cell = local_env
615 .remove("varargout")
616 .unwrap_or(Value::Cell(Box::default()));
617 return match cell {
618 Value::Cell(mut v) => {
619 if v.is_empty() {
620 Ok(Value::Void)
621 } else if v.len() == 1 {
622 Ok(v.remove(0))
623 } else {
624 Ok(Value::Tuple(*v))
625 }
626 }
627 other => Ok(other),
628 };
629 }
630
631 if outputs.len() == 1 {
632 return Ok(local_env.remove(&outputs[0]).unwrap_or(Value::Void));
633 }
634 let vals: Vec<Value> = outputs
635 .iter()
636 .map(|o| local_env.remove(o).unwrap_or(Value::Void))
637 .collect();
638 Ok(Value::Tuple(vals))
639}
640
641pub fn resolve_script_path(name: &str) -> Option<std::path::PathBuf> {
647 let p = std::path::Path::new(name);
656 let mut bases: Vec<std::path::PathBuf> = Vec::new();
657
658 SCRIPT_DIR_STACK.with(|stack| {
660 for dir in stack.borrow().iter().rev() {
661 bases.push(dir.join("private").join(p));
662 bases.push(dir.join(p));
663 }
664 });
665
666 bases.push(p.to_path_buf());
668
669 SESSION_PATH.with(|sp| {
671 for dir in sp.borrow().iter() {
672 bases.push(dir.join(p));
673 }
674 });
675
676 for base in &bases {
677 if base.extension().is_some() {
678 if base.exists() {
679 return Some(base.clone());
680 }
681 continue;
683 }
684 let with_calc = base.with_extension("calc");
685 if with_calc.exists() {
686 return Some(with_calc);
687 }
688 let with_m = base.with_extension("m");
689 if with_m.exists() {
690 return Some(with_m);
691 }
692 }
693 None
694}
695
696pub(crate) fn is_truthy(val: &Value) -> bool {
704 match val {
705 Value::Scalar(n) => *n != 0.0 && !n.is_nan(),
706 Value::Matrix(m) => m.iter().all(|&x| x != 0.0 && !x.is_nan()),
707 Value::Complex(re, im) => *re != 0.0 || *im != 0.0,
708 Value::ComplexMatrix(m) => m.iter().all(|c| c.re != 0.0 || c.im != 0.0),
709 Value::Str(s) | Value::StringObj(s) => !s.is_empty(),
710 Value::Void => false,
711 Value::Lambda(_) | Value::Function(_) | Value::Tuple(_) => true,
714 Value::Cell(v) => !v.is_empty(),
716 Value::Struct(_) | Value::StructArray(_) => true,
718 Value::DateTime(ts) => !ts.is_nan(),
720 Value::Duration(s) => *s != 0.0,
721 Value::DateTimeArray(v) | Value::DurationArray(v) => !v.is_empty(),
722 Value::Map(m) => !m.is_empty(),
724 }
725}
726
727pub(crate) fn print_value(
732 label: Option<&str>,
733 val: &Value,
734 fmt: &FormatMode,
735 base: Base,
736 compact: bool,
737) {
738 match val {
739 Value::Void => {}
740 Value::Scalar(n) => {
741 if let Some(name) = label {
742 println!("{name} = {}", format_scalar(*n, base, fmt));
743 } else {
744 println!("{}", format_scalar(*n, base, fmt));
745 }
746 }
747 Value::Matrix(_) => {
748 if let Some(full) = format_value_full(val, fmt) {
749 let prefix = label.unwrap_or("ans");
750 println!("{prefix} =");
751 println!("{full}");
752 if !compact {
753 println!();
754 }
755 }
756 }
757 Value::Complex(re, im) => {
758 if let Some(name) = label {
759 println!("{name} = {}", format_complex(*re, *im, fmt));
760 } else {
761 println!("{}", format_complex(*re, *im, fmt));
762 }
763 }
764 Value::Str(s) | Value::StringObj(s) => {
765 if let Some(name) = label {
766 println!("{name} = {s}");
767 } else {
768 println!("{s}");
769 }
770 }
771 Value::Lambda(_) => {
772 if let Some(name) = label {
773 println!("{name} = @<lambda>");
774 } else {
775 println!("@<lambda>");
776 }
777 }
778 Value::Function(fd) => {
779 let params_str = fd.params.join(", ");
780 let out_str = match fd.outputs.len() {
781 0 => String::new(),
782 1 => format!("{} = ", fd.outputs[0]),
783 _ => format!("[{}] = ", fd.outputs.join(", ")),
784 };
785 if let Some(name) = label {
786 println!("{name} = @function {out_str}{name}({params_str})");
787 } else {
788 println!("@function {out_str}f({params_str})");
789 }
790 }
791 Value::Tuple(vals) => {
792 for (i, v) in vals.iter().enumerate() {
795 print_value(label.map(|_| "ans").or(Some("ans")), v, fmt, base, compact);
796 let _ = i;
797 }
798 }
799 Value::ComplexMatrix(_) => {
800 if let Some(full) = format_value_full(val, fmt) {
801 let prefix = label.unwrap_or("ans");
802 println!("{prefix} =");
803 println!("{full}");
804 if !compact {
805 println!();
806 }
807 }
808 }
809 Value::Cell(_) | Value::Struct(_) | Value::StructArray(_) => {
810 if let Some(full) = format_value_full(val, fmt) {
811 let prefix = label.unwrap_or("ans");
812 println!("{prefix} =");
813 println!("{full}");
814 if !compact {
815 println!();
816 }
817 }
818 }
819 Value::DateTime(ts) => {
820 let s = crate::datetime::format_datetime(*ts);
821 if let Some(name) = label {
822 println!("{name} = {s}");
823 } else {
824 println!("{s}");
825 }
826 }
827 Value::Duration(secs) => {
828 let s = crate::datetime::format_duration(*secs);
829 if let Some(name) = label {
830 println!("{name} = {s}");
831 } else {
832 println!("{s}");
833 }
834 }
835 Value::DateTimeArray(_) | Value::DurationArray(_) => {
836 if let Some(full) = format_value_full(val, fmt) {
837 let prefix = label.unwrap_or("ans");
838 println!("{prefix} =");
839 println!("{full}");
840 if !compact {
841 println!();
842 }
843 }
844 }
845 Value::Map(_) => {
846 if let Some(full) = format_value_full(val, fmt) {
847 let prefix = label.unwrap_or("ans");
848 println!("{prefix} =");
849 println!("{full}");
850 if !compact {
851 println!();
852 }
853 }
854 }
855 }
856}
857
858fn set_nested(
863 mut map: IndexMap<String, Value>,
864 path: &[String],
865 val: Value,
866) -> Result<IndexMap<String, Value>, String> {
867 let (first, rest) = path.split_first().expect("set_nested: empty path");
868 if rest.is_empty() {
869 map.insert(first.clone(), val);
870 } else {
871 let inner = match map.shift_remove(first) {
872 Some(Value::Struct(m)) => *m,
873 None => IndexMap::new(),
874 Some(other) => {
875 map.insert(first.clone(), other);
876 return Err(format!("'{first}' is not a struct"));
877 }
878 };
879 let updated = set_nested(inner, rest, val)?;
880 map.insert(first.clone(), Value::Struct(Box::new(updated)));
881 }
882 Ok(map)
883}
884
885fn hoist_functions(stmts: &[StmtEntry], env: &mut Env) {
892 for (stmt, _, _) in stmts {
893 if let Stmt::FunctionDef {
894 name,
895 outputs,
896 params,
897 body_source,
898 doc,
899 } = stmt
900 {
901 env.insert(
902 name.clone(),
903 Value::Function(Box::new(crate::env::FunctionData {
904 outputs: outputs.clone(),
905 params: params.clone(),
906 body_source: body_source.clone(),
907 locals: IndexMap::new(),
908 doc: doc.clone(),
909 })),
910 );
911 }
912 }
913}
914
915pub fn exec_script(
921 stmts: &[StmtEntry],
922 env: &mut Env,
923 io: &mut IoContext,
924 fmt: &FormatMode,
925 base: Base,
926 compact: bool,
927) -> Result<Option<Signal>, String> {
928 hoist_functions(stmts, env);
929 exec_stmts(stmts, env, io, fmt, base, compact)
930}
931
932pub fn exec_stmts(
939 stmts: &[StmtEntry],
940 env: &mut Env,
941 io: &mut IoContext,
942 fmt: &FormatMode,
943 base: Base,
944 compact: bool,
945) -> Result<Option<Signal>, String> {
946 if crate::vm::compile::is_compilable(stmts) {
950 match crate::vm::compile::compile(stmts) {
951 Ok(chunk) => {
952 return crate::vm::exec::vm_exec(&chunk, env, io, fmt, base, compact);
953 }
954 Err(crate::vm::CompileError::Unsupported) => {}
955 }
956 }
957
958 set_display_ctx(fmt, base, compact);
960
961 for (stmt, silent, stmt_line) in stmts {
962 match stmt {
963 Stmt::Assign(name, expr) => {
964 set_nargout(1);
965 let val = eval_with_io(expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
966 env.insert(name.clone(), val.clone());
967 if is_global(name) {
969 global_set(name, val.clone());
970 }
971 if is_persistent(name) {
973 persistent_save(¤t_func_name(), name, val.clone());
974 }
975 if !silent && !matches!(val, Value::Void) {
976 print_value(Some(name), &val, fmt, base, compact);
977 }
978 }
979
980 Stmt::Global(names) => {
981 for name in names {
982 global_declare(name);
983 global_init_if_absent(name);
984 if let Some(local_val) = env.remove(name) {
987 global_set(name, local_val.clone());
988 env.insert(name.clone(), local_val);
989 } else if let Some(global_val) = global_get(name) {
990 env.insert(name.clone(), global_val);
991 }
992 }
993 }
994
995 Stmt::Persistent(names) => {
996 let func = current_func_name();
999 for name in names {
1000 persistent_declare(name);
1001 if let Some(saved) = persistent_load(&func, name) {
1002 env.insert(name.clone(), saved);
1004 } else {
1005 env.insert(
1009 name.clone(),
1010 Value::Matrix(Box::new(ndarray::Array2::zeros((0, 0)))),
1011 );
1012 }
1013 }
1014 }
1015
1016 Stmt::Expr(expr) => {
1017 if let Expr::Call(fn_name, args) = expr
1019 && matches!(fn_name.as_str(), "addpath" | "rmpath" | "path")
1020 {
1021 match fn_name.as_str() {
1022 "addpath" => {
1023 if args.is_empty() || args.len() > 2 {
1024 return Err(
1025 "addpath: expects 1 or 2 arguments: addpath(dir) or addpath(dir, '-end')".to_string()
1026 );
1027 }
1028 let path_val = eval_with_io(&args[0], env, io)?;
1029 let path_str = match &path_val {
1030 Value::Str(s) | Value::StringObj(s) => s.clone(),
1031 _ => {
1032 return Err(
1033 "addpath: argument must be a string (directory path)"
1034 .to_string(),
1035 );
1036 }
1037 };
1038 let append = if args.len() == 2 {
1039 let flag_val = eval_with_io(&args[1], env, io)?;
1040 match &flag_val {
1041 Value::Str(s) | Value::StringObj(s) if s == "-end" => true,
1042 Value::Str(_) | Value::StringObj(_) => {
1043 return Err(
1044 "addpath: second argument must be '-end' (to append) or omitted (to prepend)".to_string()
1045 );
1046 }
1047 _ => {
1048 return Err(
1049 "addpath: second argument must be a string '-end'"
1050 .to_string(),
1051 );
1052 }
1053 }
1054 } else {
1055 false
1056 };
1057 let expanded = expand_tilde(&path_str);
1058 let pb = std::path::PathBuf::from(&expanded);
1059 session_path_add(pb, append);
1060 if !silent {
1061 for p in session_path_list() {
1062 println!("{}", p.display());
1063 }
1064 }
1065 }
1066 "rmpath" => {
1067 if args.len() != 1 {
1068 return Err("rmpath: expects exactly 1 argument".to_string());
1069 }
1070 let path_val = eval_with_io(&args[0], env, io)?;
1071 let path_str = match &path_val {
1072 Value::Str(s) | Value::StringObj(s) => s.clone(),
1073 _ => {
1074 return Err(
1075 "rmpath: argument must be a string (directory path)"
1076 .to_string(),
1077 );
1078 }
1079 };
1080 let expanded = expand_tilde(&path_str);
1081 session_path_remove(std::path::Path::new(&expanded));
1082 }
1083 "path" => {
1084 if !args.is_empty() {
1085 return Err("path: takes no arguments".to_string());
1086 }
1087 if !silent {
1088 let paths = session_path_list();
1089 if paths.is_empty() {
1090 println!("(search path is empty)");
1091 } else {
1092 for p in &paths {
1093 println!("{}", p.display());
1094 }
1095 }
1096 }
1097 }
1098 _ => unreachable!(),
1099 }
1100 continue;
1101 }
1102
1103 if let Expr::Call(fn_name, args) = expr
1106 && matches!(fn_name.as_str(), "run" | "source")
1107 && args.len() == 1
1108 {
1109 let path_val = eval_with_io(&args[0], env, io)?;
1110 let filename = match &path_val {
1111 Value::Str(s) | Value::StringObj(s) => s.clone(),
1112 _ => {
1113 return Err(format!("{fn_name}: argument must be a string (filename)"));
1114 }
1115 };
1116 let script_path = resolve_script_path(&filename)
1117 .ok_or_else(|| format!("{fn_name}: script not found: '{filename}'"))?;
1118 let content = std::fs::read_to_string(&script_path).map_err(|e| {
1119 format!("{fn_name}: cannot read '{}': {e}", script_path.display())
1120 })?;
1121 let depth = RUN_DEPTH.with(|d| d.get());
1122 if depth >= 64 {
1123 return Err(format!(
1124 "{fn_name}: maximum script nesting depth (64) exceeded"
1125 ));
1126 }
1127 RUN_DEPTH.with(|d| d.set(depth + 1));
1128 if let Some(dir) = script_path.parent() {
1131 SCRIPT_DIR_STACK.with(|s| s.borrow_mut().push(dir.to_path_buf()));
1132 }
1133 let run_stmts = parse_stmts(&content).map_err(|e| {
1134 format!("{fn_name}: parse error in '{}': {e}", script_path.display())
1135 })?;
1136
1137 let is_fn_file = !run_stmts.is_empty()
1143 && run_stmts
1144 .iter()
1145 .all(|(s, _, _)| matches!(s, Stmt::FunctionDef { .. }));
1146 let result = if is_fn_file {
1147 let primary_name = match &run_stmts[0].0 {
1148 Stmt::FunctionDef { name, .. } => name.clone(),
1149 _ => unreachable!(),
1150 };
1151 let mut locals: IndexMap<String, Value> = IndexMap::new();
1152 for (stmt, _, _) in &run_stmts {
1153 if let Stmt::FunctionDef {
1154 name,
1155 outputs,
1156 params,
1157 body_source,
1158 doc,
1159 } = stmt
1160 && name != &primary_name
1161 {
1162 locals.insert(
1163 name.clone(),
1164 Value::Function(Box::new(crate::env::FunctionData {
1165 outputs: outputs.clone(),
1166 params: params.clone(),
1167 body_source: body_source.clone(),
1168 locals: IndexMap::new(),
1169 doc: doc.clone(),
1170 })),
1171 );
1172 }
1173 }
1174 if let Stmt::FunctionDef {
1175 outputs,
1176 params,
1177 body_source,
1178 doc,
1179 ..
1180 } = &run_stmts[0].0
1181 {
1182 env.insert(
1183 primary_name,
1184 Value::Function(Box::new(crate::env::FunctionData {
1185 outputs: outputs.clone(),
1186 params: params.clone(),
1187 body_source: body_source.clone(),
1188 locals,
1189 doc: doc.clone(),
1190 })),
1191 );
1192 }
1193 Ok(None)
1194 } else {
1195 for (stmt, _, _) in run_stmts.iter() {
1198 if let Stmt::FunctionDef {
1199 name,
1200 outputs,
1201 params,
1202 body_source,
1203 doc,
1204 } = stmt
1205 {
1206 env.insert(
1207 name.clone(),
1208 Value::Function(Box::new(crate::env::FunctionData {
1209 outputs: outputs.clone(),
1210 params: params.clone(),
1211 body_source: body_source.clone(),
1212 locals: IndexMap::new(),
1213 doc: doc.clone(),
1214 })),
1215 );
1216 }
1217 }
1218 exec_stmts(&run_stmts, env, io, fmt, base, compact)
1219 };
1220 SCRIPT_DIR_STACK.with(|s| s.borrow_mut().pop());
1221 RUN_DEPTH.with(|d| d.set(depth));
1222 match result? {
1225 None => {}
1226 Some(sig) => return Ok(Some(sig)),
1227 }
1228 continue;
1229 }
1230
1231 if let Expr::Call(fn_name, args) = expr
1235 && fn_name == "eval"
1236 && (args.len() == 1 || args.len() == 2)
1237 {
1238 let code_val = eval_with_io(&args[0], env, io)?;
1239 let code_str = match code_val {
1240 Value::Str(s) | Value::StringObj(s) => s,
1241 _ => return Err("eval: argument must be a string".to_string()),
1242 };
1243 let depth = RUN_DEPTH.with(|d| d.get());
1244 if depth >= 64 {
1245 return Err("eval: maximum nesting depth (64) exceeded".to_string());
1246 }
1247 RUN_DEPTH.with(|d| d.set(depth + 1));
1248 let run_result = (|| -> Result<Option<Signal>, String> {
1249 let stmts = parse_stmts(&code_str)
1250 .map_err(|e| format!("eval: parse error: {e}"))?;
1251 exec_script(&stmts, env, io, fmt, base, compact)
1252 })();
1253 RUN_DEPTH.with(|d| d.set(depth));
1254 match run_result {
1255 Err(e) if args.len() == 2 => {
1256 set_last_err(&e);
1257 let catch_val = eval_with_io(&args[1], env, io)?;
1258 let catch_str = match catch_val {
1259 Value::Str(s) | Value::StringObj(s) => s,
1260 _ => {
1261 return Err("eval: catch argument must be a string".to_string());
1262 }
1263 };
1264 let catch_stmts = parse_stmts(&catch_str)
1265 .map_err(|e| format!("eval: catch parse error: {e}"))?;
1266 match exec_stmts(&catch_stmts, env, io, fmt, base, compact)? {
1267 None => {}
1268 Some(sig) => return Ok(Some(sig)),
1269 }
1270 }
1271 Err(e) => return Err(e),
1272 Ok(None) => {}
1273 Ok(Some(sig)) => return Ok(Some(sig)),
1274 }
1275 continue;
1276 }
1277
1278 if let Expr::Call(fn_name, args) = expr
1280 && fn_name == "clear"
1281 {
1282 if args.is_empty() {
1283 env.clear();
1284 } else {
1285 for arg in args {
1286 let key = match arg {
1287 Expr::StrLiteral(s) | Expr::StringObjLiteral(s) => s.clone(),
1288 other => match eval_with_io(other, env, io)? {
1289 Value::Str(s) | Value::StringObj(s) => s,
1290 _ => continue,
1291 },
1292 };
1293 env.remove(&key);
1294 }
1295 }
1296 continue;
1297 }
1298
1299 if let Expr::Call(fn_name, args) = expr
1301 && fn_name == "remove"
1302 && args.len() == 2
1303 && let Expr::Var(map_name) = &args[0]
1304 {
1305 let map_name = map_name.clone();
1306 let key_val = eval_with_io(&args[1], env, io)
1307 .map_err(|e| annotate_line(e, *stmt_line))?;
1308 let key = match key_val {
1309 Value::Str(s) | Value::StringObj(s) => s,
1310 _ => {
1311 return Err(annotate_line(
1312 "remove: key must be a string".to_string(),
1313 *stmt_line,
1314 ));
1315 }
1316 };
1317 match env.get_mut(&map_name) {
1318 Some(Value::Map(map)) => {
1319 map.shift_remove(&key);
1320 }
1321 Some(_) => {
1322 return Err(annotate_line(
1323 format!("remove: '{map_name}' is not a containers.Map"),
1324 *stmt_line,
1325 ));
1326 }
1327 None => {
1328 return Err(annotate_line(
1329 format!("remove: undefined variable '{map_name}'"),
1330 *stmt_line,
1331 ));
1332 }
1333 }
1334 continue;
1335 }
1336
1337 if let Expr::Call(fn_name, args) = expr
1339 && fn_name == "format"
1340 {
1341 let arg = match args.first() {
1342 Some(Expr::StrLiteral(s)) => s.as_str(),
1343 None => "",
1344 _ => "",
1345 };
1346 let new_fmt = match arg {
1347 "" | "short" => FormatMode::Short,
1348 "long" => FormatMode::Long,
1349 "shorte" | "shortE" => FormatMode::ShortE,
1350 "longe" | "longE" => FormatMode::LongE,
1351 "shortg" | "shortG" => FormatMode::ShortG,
1352 "longg" | "longG" => FormatMode::LongG,
1353 "bank" => FormatMode::Bank,
1354 "rat" => FormatMode::Rat,
1355 "hex" => FormatMode::Hex,
1356 "+" => FormatMode::Plus,
1357 "compact" | "loose" => get_display_fmt(),
1358 s => s
1359 .parse::<usize>()
1360 .map(FormatMode::Custom)
1361 .unwrap_or_else(|_| get_display_fmt()),
1362 };
1363 let new_compact = match arg {
1364 "compact" => true,
1365 "loose" => false,
1366 _ => get_display_compact(),
1367 };
1368 set_display_ctx(&new_fmt, base, new_compact);
1369 continue;
1370 }
1371
1372 if let Expr::Call(fn_name, args) = expr
1374 && matches!(fn_name.as_str(), "save" | "load" | "ws" | "wl")
1375 {
1376 let is_save = matches!(fn_name.as_str(), "save" | "ws");
1377 if is_save {
1378 let (path_opt, var_names) = if args.is_empty() {
1379 (None, vec![])
1380 } else {
1381 let path_val = eval_with_io(&args[0], env, io)?;
1382 let path_str = match path_val {
1383 Value::Str(s) | Value::StringObj(s) => s,
1384 _ => return Err("save: path argument must be a string".to_string()),
1385 };
1386 let mut vars: Vec<String> = Vec::new();
1387 for a in &args[1..] {
1388 let v = eval_with_io(a, env, io)?;
1389 match v {
1390 Value::Str(s) | Value::StringObj(s) => vars.push(s),
1391 _ => {
1392 return Err(
1393 "save: variable names must be strings".to_string()
1394 );
1395 }
1396 }
1397 }
1398 (Some(path_str), vars)
1399 };
1400 let result = match &path_opt {
1401 None => {
1402 let home = std::env::var("HOME")
1403 .or_else(|_| std::env::var("USERPROFILE"))
1404 .unwrap_or_default();
1405 let p = std::path::Path::new(&home)
1406 .join(".config")
1407 .join("ccalc")
1408 .join("workspace.toml");
1409 save_workspace(env, &p)
1410 }
1411 Some(p) if var_names.is_empty() => {
1412 save_workspace(env, std::path::Path::new(p))
1413 }
1414 Some(p) => {
1415 let refs: Vec<&str> =
1416 var_names.iter().map(String::as_str).collect();
1417 save_workspace_vars(env, std::path::Path::new(p), &refs)
1418 }
1419 };
1420 if let Err(e) = result {
1421 return Err(format!("save: {e}"));
1422 }
1423 } else {
1424 let loaded = if args.is_empty() {
1426 let home = std::env::var("HOME")
1427 .or_else(|_| std::env::var("USERPROFILE"))
1428 .unwrap_or_default();
1429 let p = std::path::Path::new(&home)
1430 .join(".config")
1431 .join("ccalc")
1432 .join("workspace.toml");
1433 load_workspace(&p)
1434 } else {
1435 let path_val = eval_with_io(&args[0], env, io)?;
1436 let path_str = match path_val {
1437 Value::Str(s) | Value::StringObj(s) => s,
1438 _ => return Err("load: path argument must be a string".to_string()),
1439 };
1440 load_workspace(std::path::Path::new(&path_str))
1441 };
1442 match loaded {
1443 Ok(ws) => env.extend(ws),
1444 Err(e) => return Err(format!("load: {e}")),
1445 }
1446 }
1447 continue;
1448 }
1449
1450 let expr_label = if let Expr::Var(name) = expr {
1451 Some(name.as_str())
1452 } else {
1453 None
1454 };
1455 let val = eval_with_io(expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1456 env.insert("ans".to_string(), val.clone());
1457 if !silent && !matches!(val, Value::Void) {
1458 print_value(expr_label, &val, fmt, base, compact);
1459 }
1460 }
1461
1462 Stmt::If {
1463 cond,
1464 body,
1465 elseif_branches,
1466 else_body,
1467 } => {
1468 let cond_val =
1469 eval_with_io(cond, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1470 let chosen: Option<&[StmtEntry]> = if is_truthy(&cond_val) {
1471 Some(body)
1472 } else {
1473 let mut found = None;
1474 for (ei_cond, ei_body) in elseif_branches {
1475 if is_truthy(
1476 &eval_with_io(ei_cond, env, io)
1477 .map_err(|e| annotate_line(e, *stmt_line))?,
1478 ) {
1479 found = Some(ei_body.as_slice());
1480 break;
1481 }
1482 }
1483 if found.is_none() {
1484 found = else_body.as_deref();
1485 }
1486 found
1487 };
1488 if let Some(body_stmts) = chosen
1489 && let Some(sig) = exec_stmts(body_stmts, env, io, fmt, base, compact)?
1490 {
1491 return Ok(Some(sig));
1492 }
1493 }
1494
1495 Stmt::For {
1496 var,
1497 range_expr,
1498 body,
1499 } => {
1500 let range_val =
1501 eval_with_io(range_expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1502 let iter_cols: Vec<Value> = match range_val {
1503 Value::Scalar(n) => vec![Value::Scalar(n)],
1504 Value::Matrix(m) => {
1505 let nrows = m.nrows();
1506 let ncols = m.ncols();
1507 (0..ncols)
1508 .map(|j| {
1509 if nrows == 1 {
1510 Value::Scalar(m[[0, j]])
1512 } else {
1513 let mut col = Array2::zeros((nrows, 1));
1515 for i in 0..nrows {
1516 col[[i, 0]] = m[[i, j]];
1517 }
1518 Value::Matrix(Box::new(col))
1519 }
1520 })
1521 .collect()
1522 }
1523 _ => return Err("'for' range must evaluate to a scalar or matrix".to_string()),
1524 };
1525
1526 'for_loop: for col_val in iter_cols {
1527 env.insert(var.clone(), col_val);
1528 match exec_stmts(body, env, io, fmt, base, compact)? {
1529 None => {}
1530 Some(Signal::Break) => break 'for_loop,
1531 Some(Signal::Continue) => continue 'for_loop,
1532 Some(Signal::Return) => return Ok(Some(Signal::Return)),
1533 }
1534 }
1535 }
1536
1537 Stmt::While { cond, body } => loop {
1538 if !is_truthy(
1539 &eval_with_io(cond, env, io).map_err(|e| annotate_line(e, *stmt_line))?,
1540 ) {
1541 break;
1542 }
1543 match exec_stmts(body, env, io, fmt, base, compact)? {
1544 None => {}
1545 Some(Signal::Break) => break,
1546 Some(Signal::Continue) => continue,
1547 Some(Signal::Return) => return Ok(Some(Signal::Return)),
1548 }
1549 },
1550
1551 Stmt::Break => return Ok(Some(Signal::Break)),
1552 Stmt::Continue => return Ok(Some(Signal::Continue)),
1553
1554 Stmt::Switch {
1556 expr,
1557 cases,
1558 otherwise_body,
1559 } => {
1560 let switch_val =
1561 eval_with_io(expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1562 let mut matched = false;
1563 'switch_loop: for (case_exprs, case_body) in cases {
1564 for case_expr in case_exprs {
1565 let case_val = eval_with_io(case_expr, env, io)
1566 .map_err(|e| annotate_line(e, *stmt_line))?;
1567 let is_match = if let Value::Cell(cell_elems) = &case_val {
1570 cell_elems.iter().any(|elem| match (&switch_val, elem) {
1571 (Value::Scalar(a), Value::Scalar(b)) => a == b,
1572 _ => {
1573 let sv = match &switch_val {
1574 Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
1575 _ => None,
1576 };
1577 let cv = match elem {
1578 Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
1579 _ => None,
1580 };
1581 matches!((sv, cv), (Some(a), Some(b)) if a == b)
1582 }
1583 })
1584 } else {
1585 match (&switch_val, &case_val) {
1586 (Value::Scalar(a), Value::Scalar(b)) => a == b,
1587 _ => {
1588 let sv = match &switch_val {
1589 Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
1590 _ => None,
1591 };
1592 let cv = match &case_val {
1593 Value::Str(s) | Value::StringObj(s) => Some(s.as_str()),
1594 _ => None,
1595 };
1596 matches!((sv, cv), (Some(a), Some(b)) if a == b)
1597 }
1598 }
1599 };
1600 if is_match {
1601 if let Some(sig) = exec_stmts(case_body, env, io, fmt, base, compact)? {
1602 return Ok(Some(sig));
1603 }
1604 matched = true;
1605 break 'switch_loop;
1606 }
1607 }
1608 }
1609 if !matched
1610 && let Some(ob) = otherwise_body
1611 && let Some(sig) = exec_stmts(ob, env, io, fmt, base, compact)?
1612 {
1613 return Ok(Some(sig));
1614 }
1615 }
1616
1617 Stmt::DoUntil { body, cond } => loop {
1619 match exec_stmts(body, env, io, fmt, base, compact)? {
1620 Some(Signal::Break) => break,
1621 Some(Signal::Continue) | None => {}
1622 Some(Signal::Return) => return Ok(Some(Signal::Return)),
1623 }
1624 if is_truthy(
1625 &eval_with_io(cond, env, io).map_err(|e| annotate_line(e, *stmt_line))?,
1626 ) {
1627 break;
1628 }
1629 },
1630
1631 Stmt::TryCatch {
1633 try_body,
1634 catch_var,
1635 catch_body,
1636 } => match exec_stmts(try_body, env, io, fmt, base, compact) {
1637 Ok(None) => {}
1638 Ok(Some(sig)) => return Ok(Some(sig)),
1639 Err(msg) => {
1640 let clean = strip_near_line(msg);
1641 set_last_err(&clean);
1642 if let Some(var) = catch_var {
1643 let mut map = IndexMap::new();
1644 map.insert("message".to_string(), Value::Str(clean));
1645 env.insert(var.clone(), Value::Struct(Box::new(map)));
1646 }
1647 if let Some(sig) = exec_stmts(catch_body, env, io, fmt, base, compact)? {
1648 return Ok(Some(sig));
1649 }
1650 }
1651 },
1652
1653 Stmt::FunctionDef {
1655 name,
1656 outputs,
1657 params,
1658 body_source,
1659 doc,
1660 } => {
1661 env.insert(
1662 name.clone(),
1663 Value::Function(Box::new(crate::env::FunctionData {
1664 outputs: outputs.clone(),
1665 params: params.clone(),
1666 body_source: body_source.clone(),
1667 locals: IndexMap::new(),
1668 doc: doc.clone(),
1669 })),
1670 );
1671 }
1672
1673 Stmt::Return => return Ok(Some(Signal::Return)),
1675
1676 Stmt::CellSet(cell_name, idx_expr, val_expr) => {
1678 let cell_len = match env.get(cell_name) {
1680 Some(Value::Cell(v)) => v.len(),
1681 _ => 0,
1682 };
1683 let _owned_end;
1684 let env_end: &Env = if crate::eval::contains_end(idx_expr) {
1685 _owned_end = write_env_with_end(env, cell_len);
1686 &_owned_end
1687 } else {
1688 env
1689 };
1690 let idx = eval_with_io(idx_expr, env_end, io)
1691 .map_err(|e| annotate_line(e, *stmt_line))?;
1692 let rhs =
1693 eval_with_io(val_expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1694 let i = match idx {
1695 Value::Scalar(n) => n as isize,
1696 _ => return Err(format!("{cell_name}{{}}: index must be a scalar integer")),
1697 };
1698 match env.get_mut(cell_name) {
1699 Some(Value::Cell(v)) => {
1700 if i < 1 {
1701 return Err(format!(
1702 "{cell_name}{{}}: index {i} out of range (1..{})",
1703 v.len()
1704 ));
1705 }
1706 let idx = (i - 1) as usize;
1707 if idx >= v.len() {
1709 v.resize(idx + 1, Value::Scalar(0.0));
1710 }
1711 v[idx] = rhs.clone();
1712 }
1713 Some(_) => {
1714 return Err(format!(
1715 "'{cell_name}' is not a cell array; use () for regular indexing"
1716 ));
1717 }
1718 None => {
1719 if i < 1 {
1721 return Err(format!("{cell_name}{{}}: index {i} must be >= 1"));
1722 }
1723 let idx = (i - 1) as usize;
1724 let mut v = vec![Value::Scalar(0.0); idx + 1];
1725 v[idx] = rhs.clone();
1726 env.insert(cell_name.clone(), Value::Cell(Box::new(v)));
1727 }
1728 }
1729 if !silent && let Some(val) = env.get(cell_name) {
1730 print_value(Some(cell_name), val, fmt, base, compact);
1731 }
1732 }
1733
1734 Stmt::FieldSet(base_name, path, rhs_expr) => {
1736 let rhs =
1737 eval_with_io(rhs_expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1738 let root = match env.remove(base_name) {
1739 Some(Value::Struct(m)) => *m,
1740 None => IndexMap::new(),
1741 Some(other) => {
1742 env.insert(base_name.clone(), other);
1743 return Err(format!("'{base_name}' is not a struct"));
1744 }
1745 };
1746 let updated = set_nested(root, path, rhs)?;
1747 let struct_val = Value::Struct(Box::new(updated));
1748 if !silent {
1749 print_value(Some(base_name), &struct_val, fmt, base, compact);
1750 }
1751 env.insert(base_name.clone(), struct_val);
1752 }
1753
1754 Stmt::DynFieldSet(base_name, field_expr, rhs_expr) => {
1756 let field_val =
1757 eval_with_io(field_expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1758 let field = match &field_val {
1759 Value::Str(s) | Value::StringObj(s) => s.clone(),
1760 _ => {
1761 return Err(annotate_line(
1762 "Dynamic field name must be a string".to_string(),
1763 *stmt_line,
1764 ));
1765 }
1766 };
1767 let rhs =
1768 eval_with_io(rhs_expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1769 let root = match env.remove(base_name) {
1770 Some(Value::Struct(m)) => *m,
1771 None => IndexMap::new(),
1772 Some(other) => {
1773 env.insert(base_name.clone(), other);
1774 return Err(annotate_line(
1775 format!("'{base_name}' is not a struct"),
1776 *stmt_line,
1777 ));
1778 }
1779 };
1780 let mut updated = root;
1781 updated.insert(field, rhs);
1782 let struct_val = Value::Struct(Box::new(updated));
1783 if !silent {
1784 print_value(Some(base_name), &struct_val, fmt, base, compact);
1785 }
1786 env.insert(base_name.clone(), struct_val);
1787 }
1788
1789 Stmt::StructArrayFieldSet(base_name, idx_expr, path, rhs_expr) => {
1791 let rhs =
1792 eval_with_io(rhs_expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1793 let idx_val =
1794 eval_with_io(idx_expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1795 let idx = match &idx_val {
1796 Value::Scalar(n) => {
1797 let i = *n as isize;
1798 if i < 1 {
1799 return Err(format!(
1800 "Struct array index must be a positive integer, got {n}"
1801 ));
1802 }
1803 i as usize
1804 }
1805 _ => return Err("Struct array index must be a scalar integer".to_string()),
1806 };
1807 let mut arr: Vec<IndexMap<String, Value>> = match env.remove(base_name) {
1809 Some(Value::StructArray(v)) => *v,
1810 Some(Value::Struct(m)) => vec![*m],
1812 None => Vec::new(),
1813 Some(other) => {
1814 env.insert(base_name.clone(), other);
1815 return Err(format!("'{base_name}' is not a struct array"));
1816 }
1817 };
1818 while arr.len() < idx {
1820 arr.push(IndexMap::new());
1821 }
1822 let elem = arr[idx - 1].clone();
1824 let updated_elem = set_nested(elem, path, rhs)?;
1825 arr[idx - 1] = updated_elem;
1826 let arr_val = Value::StructArray(Box::new(arr));
1827 if !silent {
1828 print_value(Some(base_name), &arr_val, fmt, base, compact);
1829 }
1830 env.insert(base_name.clone(), arr_val);
1831 }
1832
1833 Stmt::IndexSet {
1835 name,
1836 indices,
1837 value,
1838 } => {
1839 let rhs = eval_with_io(value, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1840 if is_persistent(name) {
1843 let func = current_func_name();
1844 if let Some(fresh) = persistent_load(&func, name) {
1845 env.insert(name.clone(), fresh);
1846 }
1847 }
1848 exec_index_set(name, indices, rhs, env, io)?;
1849 if is_persistent(name)
1851 && let Some(val) = env.get(name)
1852 {
1853 persistent_save(¤t_func_name(), name, val.clone());
1854 }
1855 if !silent && let Some(val) = env.get(name) {
1856 print_value(Some(name), val, fmt, base, compact);
1857 }
1858 }
1859
1860 Stmt::MultiAssign { targets, expr } => {
1862 set_nargout(targets.len());
1863 let val = eval_with_io(expr, env, io).map_err(|e| annotate_line(e, *stmt_line))?;
1864 let vals: Vec<Value> = match val {
1865 Value::Tuple(v) => v,
1866 other => vec![other],
1867 };
1868 for (i, target) in targets.iter().enumerate() {
1869 if target == "~" {
1870 continue; }
1872 let v = vals.get(i).cloned().unwrap_or(Value::Void);
1873 env.insert(target.clone(), v.clone());
1874 if !silent && !matches!(v, Value::Void) {
1875 print_value(Some(target), &v, fmt, base, compact);
1876 }
1877 }
1878 }
1879 }
1880 }
1881 global_refresh_into_env(env);
1885 Ok(None)
1886}
1887
1888enum WriteIdx {
1892 All,
1894 Positions(Vec<usize>),
1896}
1897
1898fn resolve_write_dim(
1904 expr: &crate::eval::Expr,
1905 dim_size: usize,
1906 env: &Env,
1907 io: &mut IoContext,
1908) -> Result<WriteIdx, String> {
1909 if matches!(expr, crate::eval::Expr::Colon) {
1910 return Ok(WriteIdx::All);
1911 }
1912 let val = eval_with_io(expr, env, io)?;
1913 let floats: Vec<f64> = match val {
1914 Value::Scalar(n) => vec![n],
1915 Value::Complex(re, im) => {
1916 if im != 0.0 {
1917 return Err("Index must be real, not complex".to_string());
1918 }
1919 vec![re]
1920 }
1921 Value::Matrix(m) => {
1922 let total = m.nrows() * m.ncols();
1923 if m.nrows() > 1 && m.ncols() > 1 && total != dim_size {
1924 return Err("Index must be a scalar or vector, not a 2-D matrix".to_string());
1925 }
1926 if m.nrows() > 1 && m.ncols() > 1 {
1928 let mut v = Vec::with_capacity(total);
1929 for col in 0..m.ncols() {
1930 for row in 0..m.nrows() {
1931 v.push(m[[row, col]]);
1932 }
1933 }
1934 v
1935 } else {
1936 m.iter().copied().collect()
1937 }
1938 }
1939 _ => return Err("Index must be numeric".to_string()),
1940 };
1941 if dim_size > 0 && floats.len() == dim_size && floats.iter().all(|&f| f == 0.0 || f == 1.0) {
1943 let positions: Vec<usize> = floats
1944 .iter()
1945 .enumerate()
1946 .filter(|&(_, &f)| f == 1.0)
1947 .map(|(i, _)| i)
1948 .collect();
1949 return Ok(WriteIdx::Positions(positions));
1950 }
1951 let positions: Result<Vec<usize>, String> = floats
1953 .iter()
1954 .map(|&n| {
1955 let i = n.round() as i64;
1956 if i < 1 {
1957 return Err(format!("Index {i} must be >= 1"));
1958 }
1959 Ok(i as usize - 1)
1960 })
1961 .collect();
1962 Ok(WriteIdx::Positions(positions?))
1963}
1964
1965fn write_env_with_end(env: &Env, dim_size: usize) -> Env {
1967 let mut e = env.clone();
1968 e.insert("end".to_string(), Value::Scalar(dim_size as f64));
1969 e
1970}
1971
1972pub(crate) fn exec_index_set(
1980 name: &str,
1981 indices: &[crate::eval::Expr],
1982 rhs: Value,
1983 env: &mut Env,
1984 io: &mut IoContext,
1985) -> Result<(), String> {
1986 if indices.len() == 1 && matches!(env.get(name), Some(Value::Map(_))) {
1988 let key_val = eval_with_io(&indices[0], env, io)?;
1989 let key = match key_val {
1990 Value::Str(s) | Value::StringObj(s) => s,
1991 _ => return Err("Map key assignment requires a string key".to_string()),
1992 };
1993 match env.get_mut(name) {
1994 Some(Value::Map(map)) => {
1995 map.insert(key, rhs);
1996 return Ok(());
1997 }
1998 _ => unreachable!(),
1999 }
2000 }
2001
2002 let needs_complex = matches!(env.get(name), Some(Value::ComplexMatrix(_)))
2004 || matches!(&rhs, Value::Complex(_, _) | Value::ComplexMatrix(_));
2005
2006 if needs_complex {
2007 return exec_index_set_complex(name, indices, rhs, env, io);
2008 }
2009
2010 match indices.len() {
2011 1 => {
2012 let (total, nrows_hint, ncols_hint) = match env.get(name) {
2014 Some(Value::Matrix(m)) => (m.nrows() * m.ncols(), m.nrows(), m.ncols()),
2015 Some(Value::Scalar(_)) => (1, 1, 1),
2016 None | Some(Value::Void) => (0, 0, 0),
2017 Some(_) => {
2018 return Err(format!(
2019 "'{name}' is not a matrix; cannot use () indexed assignment"
2020 ));
2021 }
2022 };
2023
2024 let widx = {
2027 let _owned_end;
2028 let env_end: &Env = if crate::eval::contains_end(&indices[0]) {
2029 _owned_end = write_env_with_end(env, total);
2030 &_owned_end
2031 } else {
2032 env
2033 };
2034 resolve_write_dim(&indices[0], total, env_end, io)?
2035 };
2036
2037 let positions: Vec<usize> = match widx {
2039 WriteIdx::All => (0..total).collect(),
2040 WriteIdx::Positions(p) => p,
2041 };
2042
2043 let rhs_vals: Vec<f64> = match &rhs {
2045 Value::Scalar(n) => vec![*n; positions.len()],
2046 Value::Matrix(m) => {
2047 let flat: Vec<f64> = m.iter().copied().collect();
2048 if flat.len() != positions.len() {
2049 return Err(format!(
2050 "Assignment dimension mismatch: {} positions but {} values",
2051 positions.len(),
2052 flat.len()
2053 ));
2054 }
2055 flat
2056 }
2057 _ => {
2058 return Err(
2059 "Indexed assignment: RHS must be a numeric scalar or matrix".to_string()
2060 );
2061 }
2062 };
2063
2064 let required = positions.iter().copied().max().map(|m| m + 1).unwrap_or(0);
2066 let required = required.max(total);
2067
2068 let (out_rows, out_cols) = if nrows_hint == 0 || ncols_hint == 0 {
2070 (1, required)
2072 } else if nrows_hint == 1 {
2073 (1, required)
2074 } else if ncols_hint == 1 {
2075 (required, 1)
2076 } else if required > total {
2077 return Err("Cannot grow a 2-D matrix with linear indexing".to_string());
2078 } else {
2079 (nrows_hint, ncols_hint)
2080 };
2081
2082 let mut mat = match env.remove(name) {
2085 Some(Value::Matrix(m)) => *m,
2086 Some(Value::Scalar(n)) => Array2::from_elem((1, 1), n),
2087 None | Some(Value::Void) => Array2::zeros((0, 0)),
2088 Some(other) => {
2089 env.insert(name.to_string(), other);
2090 return Err(format!(
2091 "'{name}' is not a matrix; cannot use () indexed assignment"
2092 ));
2093 }
2094 };
2095
2096 if required > total || out_rows != mat.nrows() || out_cols != mat.ncols() {
2098 let mut new_mat = Array2::<f64>::zeros((out_rows, out_cols));
2099 for old_p in 0..total {
2100 let old_row = old_p % mat.nrows().max(1);
2101 let old_col = old_p / mat.nrows().max(1);
2102 let new_row = old_p % out_rows;
2103 let new_col = old_p / out_rows;
2104 if old_row < mat.nrows() && old_col < mat.ncols() {
2105 new_mat[[new_row, new_col]] = mat[[old_row, old_col]];
2106 }
2107 }
2108 mat = new_mat;
2109 }
2110
2111 for (&pos, &val) in positions.iter().zip(rhs_vals.iter()) {
2113 let row = pos % mat.nrows();
2114 let col = pos / mat.nrows();
2115 mat[[row, col]] = val;
2116 }
2117
2118 let result = if mat.nrows() == 1 && mat.ncols() == 1 {
2119 Value::Scalar(mat[[0, 0]])
2120 } else {
2121 Value::Matrix(Box::new(mat))
2122 };
2123 env.insert(name.to_string(), result);
2124 }
2125 2 => {
2126 let (nrows, ncols) = match env.get(name) {
2128 Some(Value::Matrix(m)) => (m.nrows(), m.ncols()),
2129 Some(Value::Scalar(_)) => (1, 1),
2130 None | Some(Value::Void) => (0, 0),
2131 Some(_) => {
2132 return Err(format!(
2133 "'{name}' is not a matrix; cannot use () indexed assignment"
2134 ));
2135 }
2136 };
2137
2138 let (ridx, cidx) = {
2140 let _owned_r;
2141 let env_r: &Env = if crate::eval::contains_end(&indices[0]) {
2142 _owned_r = write_env_with_end(env, nrows);
2143 &_owned_r
2144 } else {
2145 env
2146 };
2147 let _owned_c;
2148 let env_c: &Env = if crate::eval::contains_end(&indices[1]) {
2149 _owned_c = write_env_with_end(env, ncols);
2150 &_owned_c
2151 } else {
2152 env
2153 };
2154 (
2155 resolve_write_dim(&indices[0], nrows, env_r, io)?,
2156 resolve_write_dim(&indices[1], ncols, env_c, io)?,
2157 )
2158 };
2159
2160 let rows: Vec<usize> = match ridx {
2161 WriteIdx::All => (0..nrows.max(1)).collect(),
2162 WriteIdx::Positions(p) => p,
2163 };
2164 let cols: Vec<usize> = match cidx {
2165 WriteIdx::All => (0..ncols.max(1)).collect(),
2166 WriteIdx::Positions(p) => p,
2167 };
2168
2169 let req_rows = rows
2170 .iter()
2171 .copied()
2172 .max()
2173 .map(|m| m + 1)
2174 .unwrap_or(0)
2175 .max(nrows);
2176 let req_cols = cols
2177 .iter()
2178 .copied()
2179 .max()
2180 .map(|m| m + 1)
2181 .unwrap_or(0)
2182 .max(ncols);
2183
2184 let n_sel = rows.len() * cols.len();
2186 let rhs_vals: Vec<f64> = match &rhs {
2187 Value::Scalar(n) => vec![*n; n_sel],
2188 Value::Matrix(m) => {
2189 let flat: Vec<f64> = m.iter().copied().collect();
2190 if flat.len() != n_sel {
2191 return Err(format!(
2192 "Assignment dimension mismatch: {}×{} = {} positions but {} values",
2193 rows.len(),
2194 cols.len(),
2195 n_sel,
2196 flat.len()
2197 ));
2198 }
2199 flat
2200 }
2201 _ => {
2202 return Err(
2203 "Indexed assignment: RHS must be a numeric scalar or matrix".to_string()
2204 );
2205 }
2206 };
2207
2208 let mut mat = match env.remove(name) {
2210 Some(Value::Matrix(m)) => *m,
2211 Some(Value::Scalar(n)) => Array2::from_elem((1, 1), n),
2212 None | Some(Value::Void) => Array2::zeros((0, 0)),
2213 Some(other) => {
2214 env.insert(name.to_string(), other);
2215 return Err(format!(
2216 "'{name}' is not a matrix; cannot use () indexed assignment"
2217 ));
2218 }
2219 };
2220
2221 if req_rows != nrows || req_cols != ncols {
2223 let mut new_mat = Array2::<f64>::zeros((req_rows, req_cols));
2224 for r in 0..nrows {
2225 for c in 0..ncols {
2226 new_mat[[r, c]] = mat[[r, c]];
2227 }
2228 }
2229 mat = new_mat;
2230 }
2231
2232 let mut k = 0;
2234 for &r in &rows {
2235 for &c in &cols {
2236 mat[[r, c]] = rhs_vals[k];
2237 k += 1;
2238 }
2239 }
2240
2241 let result = if mat.nrows() == 1 && mat.ncols() == 1 {
2242 Value::Scalar(mat[[0, 0]])
2243 } else {
2244 Value::Matrix(Box::new(mat))
2245 };
2246 env.insert(name.to_string(), result);
2247 }
2248 _ => return Err("Indexed assignment supports at most 2 indices".to_string()),
2249 }
2250 Ok(())
2251}
2252
2253fn exec_index_set_complex(
2258 name: &str,
2259 indices: &[crate::eval::Expr],
2260 rhs: Value,
2261 env: &mut Env,
2262 io: &mut IoContext,
2263) -> Result<(), String> {
2264 fn rhs_to_complex(rhs: &Value, n_slots: usize) -> Result<Vec<Complex<f64>>, String> {
2266 match rhs {
2267 Value::Scalar(n) => Ok(vec![Complex::new(*n, 0.0); n_slots]),
2268 Value::Complex(re, im) => Ok(vec![Complex::new(*re, *im); n_slots]),
2269 Value::Matrix(m) => {
2270 let flat: Vec<Complex<f64>> = m.iter().map(|&x| Complex::new(x, 0.0)).collect();
2271 if flat.len() != n_slots {
2272 return Err(format!(
2273 "Assignment dimension mismatch: {} positions but {} values",
2274 n_slots,
2275 flat.len()
2276 ));
2277 }
2278 Ok(flat)
2279 }
2280 Value::ComplexMatrix(m) => {
2281 let flat: Vec<Complex<f64>> = m.iter().copied().collect();
2282 if flat.len() != n_slots {
2283 return Err(format!(
2284 "Assignment dimension mismatch: {} positions but {} values",
2285 n_slots,
2286 flat.len()
2287 ));
2288 }
2289 Ok(flat)
2290 }
2291 _ => Err("Indexed assignment: RHS must be a numeric value".to_string()),
2292 }
2293 }
2294
2295 fn take_as_complex(name: &str, env: &mut Env) -> Result<Array2<Complex<f64>>, String> {
2297 match env.remove(name) {
2298 Some(Value::ComplexMatrix(m)) => Ok(*m),
2299 Some(Value::Matrix(m)) => Ok(m.mapv(|x| Complex::new(x, 0.0))),
2300 Some(Value::Scalar(n)) => Ok(Array2::from_elem((1, 1), Complex::new(n, 0.0))),
2301 None | Some(Value::Void) => Ok(Array2::zeros((0, 0))),
2302 Some(other) => {
2303 env.insert(name.to_string(), other);
2304 Err(format!(
2305 "'{name}' is not a matrix; cannot use () indexed assignment"
2306 ))
2307 }
2308 }
2309 }
2310
2311 match indices.len() {
2312 1 => {
2313 let (total, nrows_hint, ncols_hint) = match env.get(name) {
2314 Some(Value::ComplexMatrix(m)) => (m.nrows() * m.ncols(), m.nrows(), m.ncols()),
2315 Some(Value::Matrix(m)) => (m.nrows() * m.ncols(), m.nrows(), m.ncols()),
2316 Some(Value::Scalar(_)) => (1, 1, 1),
2317 None | Some(Value::Void) => (0, 0, 0),
2318 Some(_) => {
2319 return Err(format!(
2320 "'{name}' is not a matrix; cannot use () indexed assignment"
2321 ));
2322 }
2323 };
2324
2325 let widx = {
2326 let _owned_end;
2327 let env_end: &Env = if crate::eval::contains_end(&indices[0]) {
2328 _owned_end = write_env_with_end(env, total);
2329 &_owned_end
2330 } else {
2331 env
2332 };
2333 resolve_write_dim(&indices[0], total, env_end, io)?
2334 };
2335
2336 let positions: Vec<usize> = match widx {
2337 WriteIdx::All => (0..total).collect(),
2338 WriteIdx::Positions(p) => p,
2339 };
2340
2341 let rhs_vals = rhs_to_complex(&rhs, positions.len())?;
2342
2343 let required = positions.iter().copied().max().map(|m| m + 1).unwrap_or(0);
2344 let required = required.max(total);
2345
2346 let (out_rows, out_cols) = if nrows_hint == 0 || ncols_hint == 0 || nrows_hint == 1 {
2347 (1, required)
2348 } else if ncols_hint == 1 {
2349 (required, 1)
2350 } else if required > total {
2351 return Err("Cannot grow a 2-D matrix with linear indexing".to_string());
2352 } else {
2353 (nrows_hint, ncols_hint)
2354 };
2355
2356 let mut mat = take_as_complex(name, env)?;
2357
2358 if required > total || out_rows != mat.nrows() || out_cols != mat.ncols() {
2359 let mut new_mat = Array2::<Complex<f64>>::zeros((out_rows, out_cols));
2360 for old_p in 0..total {
2361 let old_row = old_p % mat.nrows().max(1);
2362 let old_col = old_p / mat.nrows().max(1);
2363 let new_row = old_p % out_rows;
2364 let new_col = old_p / out_rows;
2365 if old_row < mat.nrows() && old_col < mat.ncols() {
2366 new_mat[[new_row, new_col]] = mat[[old_row, old_col]];
2367 }
2368 }
2369 mat = new_mat;
2370 }
2371
2372 for (&pos, &val) in positions.iter().zip(rhs_vals.iter()) {
2373 let row = pos % mat.nrows();
2374 let col = pos / mat.nrows();
2375 mat[[row, col]] = val;
2376 }
2377
2378 env.insert(name.to_string(), Value::ComplexMatrix(Box::new(mat)));
2379 }
2380 2 => {
2381 let (nrows, ncols) = match env.get(name) {
2382 Some(Value::ComplexMatrix(m)) => (m.nrows(), m.ncols()),
2383 Some(Value::Matrix(m)) => (m.nrows(), m.ncols()),
2384 Some(Value::Scalar(_)) => (1, 1),
2385 None | Some(Value::Void) => (0, 0),
2386 Some(_) => {
2387 return Err(format!(
2388 "'{name}' is not a matrix; cannot use () indexed assignment"
2389 ));
2390 }
2391 };
2392
2393 let (ridx, cidx) = {
2394 let _owned_r;
2395 let env_r: &Env = if crate::eval::contains_end(&indices[0]) {
2396 _owned_r = write_env_with_end(env, nrows);
2397 &_owned_r
2398 } else {
2399 env
2400 };
2401 let _owned_c;
2402 let env_c: &Env = if crate::eval::contains_end(&indices[1]) {
2403 _owned_c = write_env_with_end(env, ncols);
2404 &_owned_c
2405 } else {
2406 env
2407 };
2408 (
2409 resolve_write_dim(&indices[0], nrows, env_r, io)?,
2410 resolve_write_dim(&indices[1], ncols, env_c, io)?,
2411 )
2412 };
2413
2414 let rows: Vec<usize> = match ridx {
2415 WriteIdx::All => (0..nrows.max(1)).collect(),
2416 WriteIdx::Positions(p) => p,
2417 };
2418 let cols: Vec<usize> = match cidx {
2419 WriteIdx::All => (0..ncols.max(1)).collect(),
2420 WriteIdx::Positions(p) => p,
2421 };
2422
2423 let req_rows = rows
2424 .iter()
2425 .copied()
2426 .max()
2427 .map(|m| m + 1)
2428 .unwrap_or(0)
2429 .max(nrows);
2430 let req_cols = cols
2431 .iter()
2432 .copied()
2433 .max()
2434 .map(|m| m + 1)
2435 .unwrap_or(0)
2436 .max(ncols);
2437
2438 let n_sel = rows.len() * cols.len();
2439 let rhs_vals = rhs_to_complex(&rhs, n_sel)?;
2440
2441 let mut mat = take_as_complex(name, env)?;
2442
2443 if req_rows != nrows || req_cols != ncols {
2444 let mut new_mat = Array2::<Complex<f64>>::zeros((req_rows, req_cols));
2445 for r in 0..nrows {
2446 for c in 0..ncols {
2447 if r < mat.nrows() && c < mat.ncols() {
2448 new_mat[[r, c]] = mat[[r, c]];
2449 }
2450 }
2451 }
2452 mat = new_mat;
2453 }
2454
2455 let mut k = 0;
2456 for &r in &rows {
2457 for &c in &cols {
2458 mat[[r, c]] = rhs_vals[k];
2459 k += 1;
2460 }
2461 }
2462
2463 env.insert(name.to_string(), Value::ComplexMatrix(Box::new(mat)));
2464 }
2465 _ => return Err("Indexed assignment supports at most 2 indices".to_string()),
2466 }
2467 Ok(())
2468}