1use crate::eval::{Expr, Op, expr_to_string};
2
3pub type StmtEntry = (Stmt, bool, usize);
8
9#[derive(Debug)]
11pub enum Stmt {
12 Assign(String, Expr),
14 Expr(Expr),
16 If {
18 cond: Expr,
20 body: Vec<StmtEntry>,
22 elseif_branches: Vec<(Expr, Vec<StmtEntry>)>,
24 else_body: Option<Vec<StmtEntry>>,
26 },
27 For {
29 var: String,
31 range_expr: Expr,
33 body: Vec<StmtEntry>,
35 },
36 While {
38 cond: Expr,
40 body: Vec<StmtEntry>,
42 },
43 Break,
45 Continue,
47 Switch {
53 expr: Expr,
55 cases: Vec<(Vec<Expr>, Vec<StmtEntry>)>,
57 otherwise_body: Option<Vec<StmtEntry>>,
59 },
60 DoUntil {
64 body: Vec<StmtEntry>,
66 cond: Expr,
68 },
69 FunctionDef {
74 name: String,
76 outputs: Vec<String>,
78 params: Vec<String>,
80 body_source: String,
82 doc: Option<String>,
84 },
85 Return,
90 MultiAssign {
96 targets: Vec<String>,
98 expr: Expr,
100 },
101 TryCatch {
106 try_body: Vec<StmtEntry>,
108 catch_var: Option<String>,
110 catch_body: Vec<StmtEntry>,
112 },
113 CellSet(String, Expr, Expr),
117 FieldSet(String, Vec<String>, Expr),
128 DynFieldSet(String, Expr, Expr),
135 StructArrayFieldSet(String, Expr, Vec<String>, Expr),
146 IndexSet {
153 name: String,
155 indices: Vec<Expr>,
157 value: Expr,
159 },
160 Global(Vec<String>),
166 Persistent(Vec<String>),
172}
173
174#[derive(Debug, Clone)]
175enum Token {
176 Number(f64),
177 Ident(String),
178 Str(String), StringObj(String), Plus,
181 Minus,
182 Star,
183 Slash,
184 Caret,
185 DotStar,
186 DotSlash,
187 DotCaret,
188 Apostrophe,
189 LParen,
190 RParen,
191 Comma,
192 LBracket,
193 RBracket,
194 Semicolon,
195 Newline, Colon,
197 PlusEq, MinusEq, StarEq, SlashEq, PlusPlus, MinusMinus, EqEq, NotEq, Lt, Gt, LtEq, GtEq, AmpAmp, PipePipe, Amp, Pipe, Tilde, At, LBrace, RBrace, StarStar, DotApostrophe, Backslash, Dot, }
227
228fn parse_integer_literal(
229 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
230 radix: u32,
231 prefix: &str,
232) -> Result<f64, String> {
233 let mut digit_str = String::new();
234 while let Some(&d) = chars.peek() {
235 let valid = match radix {
236 16 => d.is_ascii_hexdigit(),
237 2 => d == '0' || d == '1',
238 8 => ('0'..='7').contains(&d),
239 _ => false,
240 };
241 if valid {
242 digit_str.push(d);
243 chars.next();
244 } else {
245 break;
246 }
247 }
248 if digit_str.is_empty() {
249 return Err(format!("Expected digits after '{prefix}'"));
250 }
251 i64::from_str_radix(&digit_str, radix)
252 .map(|i| i as f64)
253 .map_err(|_| format!("Invalid {prefix} literal: '{prefix}{digit_str}'"))
254}
255
256fn try_consume_sci_exponent(
259 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
260 num_str: &mut String,
261) {
262 if !matches!(chars.peek(), Some('e') | Some('E')) {
263 return;
264 }
265 let mut lookahead = chars.clone();
266 let e_char = lookahead.next().unwrap();
267 match lookahead.peek().copied() {
268 Some('+') | Some('-') => {
269 let sign = lookahead.next().unwrap();
270 if lookahead.peek().is_some_and(|d| d.is_ascii_digit()) {
271 chars.next();
272 chars.next();
273 num_str.push(e_char);
274 num_str.push(sign);
275 while let Some(&d) = chars.peek() {
276 if d.is_ascii_digit() {
277 num_str.push(d);
278 chars.next();
279 } else {
280 break;
281 }
282 }
283 }
284 }
285 Some(d) if d.is_ascii_digit() => {
286 chars.next();
287 num_str.push(e_char);
288 while let Some(&d) = chars.peek() {
289 if d.is_ascii_digit() {
290 num_str.push(d);
291 chars.next();
292 } else {
293 break;
294 }
295 }
296 }
297 _ => {}
298 }
299}
300
301#[inline]
305fn push_imag_suffix(chars: &mut std::iter::Peekable<std::str::Chars<'_>>, tokens: &mut Vec<Token>) {
306 if matches!(chars.peek(), Some('i') | Some('j')) {
307 let mut la = chars.clone();
308 la.next();
309 if !la.peek().is_some_and(|c| c.is_alphanumeric() || *c == '_') {
310 chars.next(); tokens.push(Token::Star);
312 tokens.push(Token::Ident("i".to_string()));
313 }
314 }
315}
316
317fn tokenize(input: &str) -> Result<Vec<Token>, String> {
318 let mut tokens = Vec::new();
319 let mut chars = input.chars().peekable();
320 let mut prev_was_ws = true; let mut bracket_depth: i32 = 0;
327
328 while let Some(&c) = chars.peek() {
329 match c {
330 ' ' | '\t' => {
331 chars.next();
332 prev_was_ws = true;
333 continue; }
335 '\r' => {
336 chars.next();
337 continue; }
339 '\n' => {
340 chars.next();
341 if bracket_depth > 0 {
342 tokens.push(Token::Newline);
344 }
345 prev_was_ws = true;
346 continue;
347 }
348 '+' => {
349 chars.next();
350 match chars.peek() {
351 Some('=') => {
352 chars.next();
353 tokens.push(Token::PlusEq);
354 }
355 Some('+') => {
356 chars.next();
357 tokens.push(Token::PlusPlus);
358 }
359 _ => tokens.push(Token::Plus),
360 }
361 }
362 '-' => {
363 chars.next();
364 match chars.peek() {
365 Some('=') => {
366 chars.next();
367 tokens.push(Token::MinusEq);
368 }
369 Some('-') => {
370 chars.next();
371 tokens.push(Token::MinusMinus);
372 }
373 _ => tokens.push(Token::Minus),
374 }
375 }
376 '*' => {
377 chars.next();
378 match chars.peek() {
379 Some('=') => {
380 chars.next();
381 tokens.push(Token::StarEq);
382 }
383 Some('*') => {
384 chars.next();
385 tokens.push(Token::StarStar);
386 }
387 _ => tokens.push(Token::Star),
388 }
389 }
390 '/' => {
391 chars.next();
392 if chars.peek() == Some(&'=') {
393 chars.next();
394 tokens.push(Token::SlashEq);
395 } else {
396 tokens.push(Token::Slash);
397 }
398 }
399 '^' => {
400 tokens.push(Token::Caret);
401 chars.next();
402 }
403 '\'' => {
404 let is_transpose = !prev_was_ws
410 && matches!(
411 tokens.last(),
412 Some(
413 Token::Number(_)
414 | Token::Ident(_)
415 | Token::RParen
416 | Token::RBracket
417 | Token::Apostrophe
418 | Token::Str(_)
419 )
420 );
421 chars.next(); if is_transpose {
423 tokens.push(Token::Apostrophe);
424 } else {
425 let mut content = String::new();
427 loop {
428 match chars.next() {
429 None => return Err("Unterminated string literal".to_string()),
430 Some('\'') => {
431 if chars.peek().copied() == Some('\'') {
433 chars.next();
434 content.push('\'');
435 } else {
436 break;
437 }
438 }
439 Some(c) => content.push(c),
440 }
441 }
442 tokens.push(Token::Str(content));
443 }
444 }
445 '"' => {
446 chars.next(); let mut content = String::new();
448 loop {
449 match chars.next() {
450 None => return Err("Unterminated string literal".to_string()),
451 Some('"') => {
452 if chars.peek().copied() == Some('"') {
454 chars.next();
455 content.push('"');
456 } else {
457 break;
458 }
459 }
460 Some('\\') => match chars.next() {
461 Some('n') => content.push('\n'),
462 Some('t') => content.push('\t'),
463 Some('\\') => content.push('\\'),
464 Some('\'') => content.push('\''),
465 Some('"') => content.push('"'),
466 Some(other) => {
467 content.push('\\');
468 content.push(other);
469 }
470 None => return Err("Unterminated string literal".to_string()),
471 },
472 Some(c) => content.push(c),
473 }
474 }
475 tokens.push(Token::StringObj(content));
476 }
477 '.' => {
478 chars.next();
479 match chars.peek().copied() {
480 Some('.') => {
481 chars.next(); if chars.peek() == Some(&'.') {
484 chars.next(); while chars.next().is_some() {}
487 } else {
488 return Err("Unexpected '..'".to_string());
489 }
490 }
491 Some('\'') => {
492 chars.next();
493 tokens.push(Token::DotApostrophe);
494 }
495 Some('*') => {
496 chars.next();
497 tokens.push(Token::DotStar);
498 }
499 Some('/') => {
500 chars.next();
501 tokens.push(Token::DotSlash);
502 }
503 Some('^') => {
504 chars.next();
505 tokens.push(Token::DotCaret);
506 }
507 Some(d) if d.is_ascii_digit() => {
508 let mut num_str = String::from(".");
509 while let Some(&d) = chars.peek() {
510 if d.is_ascii_digit() {
511 num_str.push(d);
512 chars.next();
513 } else {
514 break;
515 }
516 }
517 try_consume_sci_exponent(&mut chars, &mut num_str);
518 let n: f64 = num_str
519 .parse()
520 .map_err(|_| format!("Invalid number: '{num_str}'"))?;
521 tokens.push(Token::Number(n));
522 }
523 Some(c) if c.is_ascii_alphabetic() || c == '_' => {
526 tokens.push(Token::Dot);
527 }
528 Some('(') => {
529 tokens.push(Token::Dot);
530 }
531 _ => return Err("Unexpected '.'".to_string()),
532 }
533 }
534 '%' | '#' => {
535 break;
537 }
538 '!' => {
539 chars.next();
540 if chars.peek().copied() == Some('=') {
541 chars.next();
542 tokens.push(Token::NotEq);
543 } else {
544 tokens.push(Token::Tilde);
545 }
546 }
547 '(' => {
548 tokens.push(Token::LParen);
549 chars.next();
550 }
551 ')' => {
552 tokens.push(Token::RParen);
553 chars.next();
554 }
555 ',' => {
556 tokens.push(Token::Comma);
557 chars.next();
558 }
559 '[' => {
560 bracket_depth += 1;
561 tokens.push(Token::LBracket);
562 chars.next();
563 }
564 ']' => {
565 if bracket_depth > 0 {
566 bracket_depth -= 1;
567 }
568 tokens.push(Token::RBracket);
569 chars.next();
570 }
571 '{' => {
572 tokens.push(Token::LBrace);
573 chars.next();
574 }
575 '}' => {
576 tokens.push(Token::RBrace);
577 chars.next();
578 }
579 ';' => {
580 tokens.push(Token::Semicolon);
581 chars.next();
582 }
583 ':' => {
584 tokens.push(Token::Colon);
585 chars.next();
586 }
587 '=' => {
588 chars.next();
589 if chars.peek().copied() == Some('=') {
590 chars.next();
591 tokens.push(Token::EqEq);
592 } else {
593 return Err("Unexpected '=': use '==' for comparison".to_string());
594 }
595 }
596 '~' => {
597 chars.next();
598 if chars.peek().copied() == Some('=') {
599 chars.next();
600 tokens.push(Token::NotEq);
601 } else {
602 tokens.push(Token::Tilde);
603 }
604 }
605 '<' => {
606 chars.next();
607 if chars.peek().copied() == Some('=') {
608 chars.next();
609 tokens.push(Token::LtEq);
610 } else {
611 tokens.push(Token::Lt);
612 }
613 }
614 '>' => {
615 chars.next();
616 if chars.peek().copied() == Some('=') {
617 chars.next();
618 tokens.push(Token::GtEq);
619 } else {
620 tokens.push(Token::Gt);
621 }
622 }
623 '&' => {
624 chars.next();
625 if chars.peek().copied() == Some('&') {
626 chars.next();
627 tokens.push(Token::AmpAmp);
628 } else {
629 tokens.push(Token::Amp);
630 }
631 }
632 '|' => {
633 chars.next();
634 if chars.peek().copied() == Some('|') {
635 chars.next();
636 tokens.push(Token::PipePipe);
637 } else {
638 tokens.push(Token::Pipe);
639 }
640 }
641 '0'..='9' => {
642 if c == '0' {
643 chars.next();
644 match chars.peek().copied() {
645 Some('x') | Some('X') => {
646 chars.next();
647 let n = parse_integer_literal(&mut chars, 16, "0x")?;
648 tokens.push(Token::Number(n));
649 }
650 Some('b') | Some('B') => {
651 chars.next();
652 let n = parse_integer_literal(&mut chars, 2, "0b")?;
653 tokens.push(Token::Number(n));
654 }
655 Some('o') | Some('O') => {
656 chars.next();
657 let n = parse_integer_literal(&mut chars, 8, "0o")?;
658 tokens.push(Token::Number(n));
659 }
660 _ => {
661 let mut num_str = String::from("0");
662 while let Some(&d) = chars.peek() {
663 if d.is_ascii_digit() {
664 num_str.push(d);
665 chars.next();
666 } else if d == '.' {
667 let mut la = chars.clone();
669 la.next();
670 if matches!(la.peek(), Some('*') | Some('/') | Some('^')) {
671 break;
672 }
673 num_str.push('.');
674 chars.next();
675 } else {
676 break;
677 }
678 }
679 try_consume_sci_exponent(&mut chars, &mut num_str);
680 let n: f64 = num_str
681 .parse()
682 .map_err(|_| format!("Invalid number: '{num_str}'"))?;
683 tokens.push(Token::Number(n));
684 push_imag_suffix(&mut chars, &mut tokens);
685 }
686 }
687 } else {
688 let mut num_str = String::new();
689 while let Some(&d) = chars.peek() {
690 if d.is_ascii_digit() {
691 num_str.push(d);
692 chars.next();
693 } else if d == '.' {
694 let mut la = chars.clone();
696 la.next();
697 if matches!(la.peek(), Some('*') | Some('/') | Some('^')) {
698 break;
699 }
700 num_str.push('.');
701 chars.next();
702 } else {
703 break;
704 }
705 }
706 try_consume_sci_exponent(&mut chars, &mut num_str);
707 let n: f64 = num_str
708 .parse()
709 .map_err(|_| format!("Invalid number: '{num_str}'"))?;
710 tokens.push(Token::Number(n));
711 push_imag_suffix(&mut chars, &mut tokens);
712 }
713 }
714 '@' => {
715 tokens.push(Token::At);
716 chars.next();
717 }
718 '\\' => {
719 tokens.push(Token::Backslash);
720 chars.next();
721 }
722 'a'..='z' | 'A'..='Z' | '_' => {
723 let mut ident = String::new();
724 while let Some(&c) = chars.peek() {
725 if c.is_alphanumeric() || c == '_' {
726 ident.push(c);
727 chars.next();
728 } else {
729 break;
730 }
731 }
732 tokens.push(Token::Ident(ident));
733 }
734 _ => return Err(format!("Unexpected character: '{c}'")),
735 }
736 prev_was_ws = false;
737 }
738
739 Ok(tokens)
740}
741
742fn try_split_struct_array_field_assign(input: &str) -> Option<(String, &str, Vec<String>, &str)> {
746 let trimmed = input.trim();
747 let bytes = trimmed.as_bytes();
748 let mut i = 0;
749
750 if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
752 return None;
753 }
754 let base_start = i;
755 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
756 i += 1;
757 }
758 let base_var = trimmed[base_start..i].to_string();
759
760 if i >= bytes.len() || bytes[i] != b'(' {
762 return None;
763 }
764 i += 1;
765
766 let idx_start = i;
768 let mut depth = 1usize;
769 while i < bytes.len() && depth > 0 {
770 match bytes[i] {
771 b'(' | b'[' | b'{' => depth += 1,
772 b')' | b']' | b'}' => depth -= 1,
773 _ => {}
774 }
775 i += 1;
776 }
777 if depth != 0 {
778 return None;
779 }
780 let idx_str = &trimmed[idx_start..i - 1]; if i >= bytes.len() || bytes[i] != b'.' {
784 return None;
785 }
786 let mut fields = Vec::new();
787 while i < bytes.len() && bytes[i] == b'.' {
788 i += 1;
789 if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
790 return None;
791 }
792 let field_start = i;
793 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
794 i += 1;
795 }
796 fields.push(trimmed[field_start..i].to_string());
797 }
798 if fields.is_empty() {
799 return None;
800 }
801
802 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
804 i += 1;
805 }
806 if i >= bytes.len() || bytes[i] != b'=' {
807 return None;
808 }
809 i += 1;
810 if i < bytes.len() && bytes[i] == b'=' {
811 return None; }
813
814 let rhs = trimmed[i..].trim();
815 if rhs.is_empty() {
816 return None;
817 }
818 Some((base_var, idx_str, fields, rhs))
819}
820
821fn try_split_dyn_field_assign(input: &str) -> Option<(String, &str, &str)> {
825 let trimmed = input.trim();
826 let bytes = trimmed.as_bytes();
827 let mut i = 0;
828
829 if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
831 return None;
832 }
833 let name_start = i;
834 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
835 i += 1;
836 }
837 let base_var = &trimmed[name_start..i];
838
839 if i >= bytes.len() || bytes[i] != b'.' {
841 return None;
842 }
843 i += 1;
844 if i >= bytes.len() || bytes[i] != b'(' {
845 return None;
846 }
847 i += 1; let field_start = i;
851 let mut depth = 1usize;
852 while i < bytes.len() && depth > 0 {
853 match bytes[i] {
854 b'(' | b'[' | b'{' => depth += 1,
855 b')' | b']' | b'}' => depth -= 1,
856 _ => {}
857 }
858 i += 1;
859 }
860 if depth != 0 {
861 return None;
862 }
863 let field_str = trimmed[field_start..i - 1].trim();
865 if field_str.is_empty() {
866 return None;
867 }
868
869 let rest = trimmed[i..].trim_start();
871 if !rest.starts_with('=') || rest.starts_with("==") {
872 return None;
873 }
874 let rhs = rest[1..].trim();
875 if rhs.is_empty() {
876 return None;
877 }
878 Some((base_var.to_string(), field_str, rhs))
879}
880
881fn try_split_field_assign(input: &str) -> Option<(String, Vec<String>, &str)> {
886 let trimmed = input.trim();
887 let bytes = trimmed.as_bytes();
888 let mut i = 0;
889
890 if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
892 return None;
893 }
894 let base_start = i;
895 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
896 i += 1;
897 }
898 let base_var = trimmed[base_start..i].to_string();
899
900 let mut fields = Vec::new();
902 while i < bytes.len() && bytes[i] == b'.' {
903 i += 1;
904 if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
905 return None;
906 }
907 let field_start = i;
908 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
909 i += 1;
910 }
911 fields.push(trimmed[field_start..i].to_string());
912 }
913 if fields.is_empty() {
914 return None;
915 }
916
917 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
919 i += 1;
920 }
921 if i >= bytes.len() || bytes[i] != b'=' {
922 return None;
923 }
924 i += 1;
925 if i < bytes.len() && bytes[i] == b'=' {
926 return None; }
928
929 let rhs = trimmed[i..].trim();
930 if rhs.is_empty() {
931 return None;
932 }
933 Some((base_var, fields, rhs))
934}
935
936fn parse_name_list(rest: &str) -> Result<Vec<String>, String> {
940 let names: Vec<String> = rest
941 .split(|c: char| c.is_whitespace() || c == ',')
942 .filter(|s| !s.is_empty())
943 .map(String::from)
944 .collect();
945 if names.is_empty() {
946 return Err("Expected at least one variable name".to_string());
947 }
948 for name in &names {
949 if !name.starts_with(|c: char| c.is_alphabetic() || c == '_')
950 || name.chars().any(|c| !c.is_alphanumeric() && c != '_')
951 {
952 return Err(format!("Invalid variable name: '{name}'"));
953 }
954 }
955 Ok(names)
956}
957
958pub fn parse(input: &str) -> Result<Stmt, String> {
963 let trimmed = input.trim();
964
965 if let Some(rest) = trimmed
967 .strip_prefix("global")
968 .filter(|r| r.is_empty() || r.starts_with(|c: char| c.is_whitespace() || c == ','))
969 {
970 return Ok(Stmt::Global(parse_name_list(rest)?));
971 }
972
973 if let Some(rest) = trimmed
975 .strip_prefix("persistent")
976 .filter(|r| r.is_empty() || r.starts_with(|c: char| c.is_whitespace() || c == ','))
977 {
978 return Ok(Stmt::Persistent(parse_name_list(rest)?));
979 }
980
981 if trimmed == "return" {
983 return Ok(Stmt::Return);
984 }
985
986 if let Some((base_var, idx_str, fields, rhs)) = try_split_struct_array_field_assign(trimmed) {
988 let idx_tokens = tokenize(idx_str)?;
989 if idx_tokens.is_empty() {
990 return Err("Expected index expression inside '()'".to_string());
991 }
992 let mut idx_pos = 0;
993 let idx_expr = parse_logical_or(&idx_tokens, &mut idx_pos)?;
994 if idx_pos != idx_tokens.len() {
995 return Err("Unexpected token in struct array index expression".to_string());
996 }
997 let rhs_tokens = tokenize(rhs)?;
998 if rhs_tokens.is_empty() {
999 return Err("Expected expression after '='".to_string());
1000 }
1001 let mut rhs_pos = 0;
1002 let rhs_expr = parse_logical_or(&rhs_tokens, &mut rhs_pos)?;
1003 if rhs_pos != rhs_tokens.len() {
1004 return Err("Unexpected token after expression".to_string());
1005 }
1006 return Ok(Stmt::StructArrayFieldSet(
1007 base_var, idx_expr, fields, rhs_expr,
1008 ));
1009 }
1010
1011 if let Some((name, idx_str, rhs)) = try_split_index_assign(trimmed) {
1013 let idx_tokens = tokenize(idx_str)?;
1014 let indices = parse_index_args(&idx_tokens)?;
1015 if indices.len() > 2 {
1016 return Err("Indexed assignment supports at most 2 indices".to_string());
1017 }
1018 let rhs_tokens = tokenize(rhs)?;
1019 if rhs_tokens.is_empty() {
1020 return Err("Expected expression after '='".to_string());
1021 }
1022 let mut rhs_pos = 0;
1023 let value = parse_logical_or(&rhs_tokens, &mut rhs_pos)?;
1024 if rhs_pos != rhs_tokens.len() {
1025 return Err("Unexpected token after expression".to_string());
1026 }
1027 return Ok(Stmt::IndexSet {
1028 name,
1029 indices,
1030 value,
1031 });
1032 }
1033
1034 if let Some((base_var, field_str, rhs)) = try_split_dyn_field_assign(trimmed) {
1036 let field_tokens = tokenize(field_str)?;
1037 if field_tokens.is_empty() {
1038 return Err("Expected expression inside '.()'".to_string());
1039 }
1040 let mut fpos = 0;
1041 let field_expr = parse_logical_or(&field_tokens, &mut fpos)?;
1042 if fpos != field_tokens.len() {
1043 return Err("Unexpected token in dynamic field expression".to_string());
1044 }
1045 let rhs_tokens = tokenize(rhs)?;
1046 if rhs_tokens.is_empty() {
1047 return Err("Expected expression after '='".to_string());
1048 }
1049 let mut rpos = 0;
1050 let rhs_expr = parse_logical_or(&rhs_tokens, &mut rpos)?;
1051 if rpos != rhs_tokens.len() {
1052 return Err("Unexpected token after expression".to_string());
1053 }
1054 return Ok(Stmt::DynFieldSet(base_var, field_expr, rhs_expr));
1055 }
1056
1057 if let Some((base_var, fields, rhs)) = try_split_field_assign(trimmed) {
1059 let tokens = tokenize(rhs)?;
1060 if tokens.is_empty() {
1061 return Err("Expected expression after '='".to_string());
1062 }
1063 let mut pos = 0;
1064 let rhs_expr = parse_logical_or(&tokens, &mut pos)?;
1065 if pos != tokens.len() {
1066 return Err("Unexpected token after expression".to_string());
1067 }
1068 return Ok(Stmt::FieldSet(base_var, fields, rhs_expr));
1069 }
1070
1071 if let Some((name, idx_str, rhs)) = try_split_cell_assign(trimmed) {
1073 let idx_tokens = tokenize(idx_str)?;
1074 if idx_tokens.is_empty() {
1075 return Err("Expected index expression inside '{}'".to_string());
1076 }
1077 let mut idx_pos = 0;
1078 let idx_expr = parse_logical_or(&idx_tokens, &mut idx_pos)?;
1079 if idx_pos != idx_tokens.len() {
1080 return Err("Unexpected token in cell index expression".to_string());
1081 }
1082 let rhs_tokens = tokenize(rhs)?;
1083 if rhs_tokens.is_empty() {
1084 return Err("Expected expression after '='".to_string());
1085 }
1086 let mut rhs_pos = 0;
1087 let rhs_expr = parse_logical_or(&rhs_tokens, &mut rhs_pos)?;
1088 if rhs_pos != rhs_tokens.len() {
1089 return Err("Unexpected token after expression".to_string());
1090 }
1091 return Ok(Stmt::CellSet(name.to_string(), idx_expr, rhs_expr));
1092 }
1093
1094 if let Some((targets, rhs)) = try_split_multi_assign(trimmed) {
1096 let tokens = tokenize(rhs)?;
1097 if tokens.is_empty() {
1098 return Err("Expected expression after '='".to_string());
1099 }
1100 let mut pos = 0;
1101 let expr = parse_logical_or(&tokens, &mut pos)?;
1102 if pos != tokens.len() {
1103 return Err("Unexpected token after expression".to_string());
1104 }
1105 return Ok(Stmt::MultiAssign { targets, expr });
1106 }
1107
1108 if let Some((name, rhs)) = try_split_assignment(trimmed) {
1109 let tokens = tokenize(rhs)?;
1110 if tokens.is_empty() {
1111 return Err("Expected expression after '='".to_string());
1112 }
1113 let mut pos = 0;
1114 let expr = parse_logical_or(&tokens, &mut pos)?;
1115 if pos != tokens.len() {
1116 return Err("Unexpected token after expression".to_string());
1117 }
1118 return Ok(Stmt::Assign(name.to_string(), expr));
1119 }
1120
1121 let tokens = tokenize(trimmed)?;
1122 if tokens.is_empty() {
1123 return Err("Empty expression".to_string());
1124 }
1125
1126 if let Some(stmt) = try_parse_compound(&tokens)? {
1129 return Ok(stmt);
1130 }
1131
1132 let mut pos = 0;
1133 let expr = parse_logical_or(&tokens, &mut pos)?;
1134 if pos != tokens.len() {
1135 return Err("Unexpected token after expression".to_string());
1136 }
1137 Ok(Stmt::Expr(expr))
1138}
1139
1140fn try_parse_compound(tokens: &[Token]) -> Result<Option<Stmt>, String> {
1153 if tokens.len() == 2
1155 && let Token::Ident(name) = &tokens[1]
1156 {
1157 let op = match &tokens[0] {
1158 Token::PlusPlus => Some(Op::Add),
1159 Token::MinusMinus => Some(Op::Sub),
1160 _ => None,
1161 };
1162 if let Some(op) = op {
1163 let expr = Expr::BinOp(
1164 Box::new(Expr::Var(name.clone())),
1165 op,
1166 Box::new(Expr::Number(1.0)),
1167 );
1168 return Ok(Some(Stmt::Assign(name.clone(), expr)));
1169 }
1170 }
1171
1172 let name = match tokens.first() {
1174 Some(Token::Ident(n)) => n.clone(),
1175 _ => return Ok(None),
1176 };
1177
1178 if tokens.len() < 2 {
1179 return Ok(None);
1180 }
1181
1182 match &tokens[1] {
1183 Token::PlusPlus | Token::MinusMinus if tokens.len() == 2 => {
1185 let op = if matches!(&tokens[1], Token::PlusPlus) {
1186 Op::Add
1187 } else {
1188 Op::Sub
1189 };
1190 let expr = Expr::BinOp(
1191 Box::new(Expr::Var(name.clone())),
1192 op,
1193 Box::new(Expr::Number(1.0)),
1194 );
1195 Ok(Some(Stmt::Assign(name, expr)))
1196 }
1197
1198 Token::PlusEq | Token::MinusEq | Token::StarEq | Token::SlashEq => {
1200 let op = match &tokens[1] {
1201 Token::PlusEq => Op::Add,
1202 Token::MinusEq => Op::Sub,
1203 Token::StarEq => Op::Mul,
1204 Token::SlashEq => Op::Div,
1205 _ => unreachable!(),
1206 };
1207 let rhs_tokens = &tokens[2..];
1208 if rhs_tokens.is_empty() {
1209 let op_str = match op {
1210 Op::Add => "+=",
1211 Op::Sub => "-=",
1212 Op::Mul => "*=",
1213 Op::Div => "/=",
1214 _ => "op=",
1215 };
1216 return Err(format!("Expected expression after '{op_str}'"));
1217 }
1218 let mut pos = 0;
1219 let rhs = parse_logical_or(rhs_tokens, &mut pos)?;
1220 if pos != rhs_tokens.len() {
1221 return Err("Unexpected token after expression".to_string());
1222 }
1223 let expr = Expr::BinOp(Box::new(Expr::Var(name.clone())), op, Box::new(rhs));
1224 Ok(Some(Stmt::Assign(name, expr)))
1225 }
1226
1227 _ => Ok(None),
1228 }
1229}
1230
1231pub fn is_partial(input: &str) -> bool {
1234 let mut chars = input.trim_start().chars();
1235 match chars.next() {
1236 Some('+') => !matches!(chars.next(), Some('+')),
1238 Some('-') => !matches!(chars.next(), Some('-')),
1239 Some('*' | '/' | '^' | '<' | '>') => true,
1240 Some('.') => matches!(chars.next(), Some('*' | '/' | '^')),
1242 Some('=') => chars.next() == Some('='),
1244 Some('~') => chars.next() == Some('='),
1245 Some('&') => chars.next() == Some('&'),
1247 Some('|') => chars.next() == Some('|'),
1248 _ => false,
1249 }
1250}
1251
1252pub fn split_stmts(input: &str) -> Vec<(&str, bool)> {
1262 let mut separators: Vec<(usize, bool)> = Vec::new();
1264 let mut comment_at = input.len();
1265 let mut in_sq = false;
1266 let mut in_dq = false;
1267 let mut paren_depth: i32 = 0;
1268 let mut bracket_depth: i32 = 0;
1269 let mut brace_depth: i32 = 0;
1270
1271 let chars: Vec<(usize, char)> = input.char_indices().collect();
1272 let mut ci = 0;
1273 while ci < chars.len() {
1274 let (i, c) = chars[ci];
1275 let at_depth0 =
1276 !in_sq && !in_dq && paren_depth == 0 && bracket_depth == 0 && brace_depth == 0;
1277 match c {
1278 '\'' if !in_dq => {
1279 if in_sq {
1280 let next = chars.get(ci + 1).map(|&(_, c)| c);
1282 if next == Some('\'') {
1283 ci += 1; } else {
1285 in_sq = false;
1286 }
1287 } else {
1288 let before = input[..i].trim_end_matches([' ', '\t']);
1289 let is_transpose = before.ends_with(|c: char| {
1290 c.is_alphanumeric()
1291 || c == '_'
1292 || c == ')'
1293 || c == ']'
1294 || c == '\''
1295 || c == '.'
1296 });
1297 if !is_transpose {
1298 in_sq = true;
1299 }
1300 }
1301 }
1302 '"' if !in_sq => in_dq = !in_dq,
1303 '(' if !in_sq && !in_dq => paren_depth += 1,
1304 ')' if !in_sq && !in_dq && paren_depth > 0 => {
1305 paren_depth -= 1;
1306 }
1307 '[' if !in_sq && !in_dq => bracket_depth += 1,
1308 ']' if !in_sq && !in_dq && bracket_depth > 0 => {
1309 bracket_depth -= 1;
1310 }
1311 '{' if !in_sq && !in_dq => brace_depth += 1,
1312 '}' if !in_sq && !in_dq && brace_depth > 0 => {
1313 brace_depth -= 1;
1314 }
1315 '%' | '#' if at_depth0 => {
1316 comment_at = i;
1317 break;
1318 }
1319 ';' if at_depth0 => separators.push((i, true)),
1320 ',' if at_depth0 => separators.push((i, false)),
1321 _ => {}
1322 }
1323 ci += 1;
1324 }
1325
1326 let content = input[..comment_at].trim_end();
1327 if content.is_empty() {
1328 return Vec::new();
1329 }
1330
1331 let mut result = Vec::new();
1332 let mut start = 0;
1333 for &(sc, silent) in &separators {
1334 if sc >= content.len() {
1335 break;
1336 }
1337 let part = content[start..sc].trim();
1338 if !part.is_empty() {
1339 result.push((part, silent));
1340 }
1341 start = sc + 1;
1342 }
1343 if start <= content.len() {
1344 let last = content[start..].trim();
1345 if !last.is_empty() {
1346 result.push((last, false));
1347 }
1348 }
1349 result
1350}
1351
1352pub fn block_depth_delta(line: &str) -> i32 {
1357 let trimmed = line.trim();
1358 if trimmed.starts_with("%{") || trimmed.starts_with("#{") {
1361 let rest = &trimmed[2..];
1362 return if rest.contains("%}") || rest.contains("#}") {
1364 0
1365 } else {
1366 1
1367 };
1368 }
1369 if trimmed.starts_with("%}") || trimmed.starts_with("#}") {
1370 return -1;
1371 }
1372 let stripped = strip_line_comment(line).trim();
1373 match leading_keyword(stripped) {
1374 Some("if") | Some("for") | Some("while") | Some("switch") | Some("do")
1375 | Some("function") | Some("try") => 1,
1376 Some("end") | Some("until") => -1,
1377 _ => 0,
1378 }
1379}
1380
1381pub fn bracket_depth_delta(line: &str) -> i32 {
1386 let mut depth: i32 = 0;
1387 let mut in_sq = false;
1388 let mut in_dq = false;
1389 let chars: Vec<(usize, char)> = line.char_indices().collect();
1390 let mut ci = 0;
1391 while ci < chars.len() {
1392 let (i, c) = chars[ci];
1393 match c {
1394 '\'' if !in_dq => {
1395 if in_sq {
1396 let next = chars.get(ci + 1).map(|&(_, c)| c);
1397 if next == Some('\'') {
1398 ci += 1; } else {
1400 in_sq = false;
1401 }
1402 } else {
1403 let before = line[..i].trim_end_matches([' ', '\t']);
1404 let is_transpose = before.ends_with(|c: char| {
1405 c.is_alphanumeric()
1406 || c == '_'
1407 || c == ')'
1408 || c == ']'
1409 || c == '\''
1410 || c == '.'
1411 });
1412 if !is_transpose {
1413 in_sq = true;
1414 }
1415 }
1416 }
1417 '"' if !in_sq => in_dq = !in_dq,
1418 '%' | '#' if !in_sq && !in_dq => break, '[' if !in_sq && !in_dq => depth += 1,
1420 ']' if !in_sq && !in_dq => depth -= 1,
1421 _ => {}
1422 }
1423 ci += 1;
1424 }
1425 depth
1426}
1427
1428pub fn is_single_line_block(line: &str) -> bool {
1433 let stripped = strip_line_comment(line).trim();
1434 if !matches!(
1435 leading_keyword(stripped),
1436 Some("if" | "for" | "while" | "switch" | "do")
1437 ) {
1438 return false;
1439 }
1440 let parts = split_block_line(stripped);
1441 matches!(
1442 parts.last().map(|s| leading_keyword(s.trim())),
1443 Some(Some("end" | "until"))
1444 )
1445}
1446
1447fn strip_block_comments(lines: &[&str]) -> Result<Vec<String>, String> {
1457 let mut result = Vec::with_capacity(lines.len());
1458 let mut in_block = false;
1459
1460 for &line in lines {
1461 let trimmed = line.trim();
1462
1463 if !in_block {
1464 if trimmed.starts_with("%{") || trimmed.starts_with("#{") {
1465 let rest = &trimmed[2..];
1466 if rest.contains("%}") || rest.contains("#}") {
1467 result.push(String::new());
1469 } else {
1470 in_block = true;
1471 result.push(String::new());
1472 }
1473 } else {
1474 result.push(line.to_string());
1475 }
1476 } else {
1477 if trimmed.starts_with("%}") || trimmed.starts_with("#}") {
1478 in_block = false;
1479 }
1480 result.push(String::new());
1481 }
1482 }
1483
1484 if in_block {
1485 Err("Unterminated block comment: missing closing '%}'".to_string())
1486 } else {
1487 Ok(result)
1488 }
1489}
1490
1491fn join_line_continuations(input: &str) -> String {
1496 let mut result = String::new();
1497 let mut pending = String::new();
1498
1499 for line in input.lines() {
1500 let stripped = strip_line_comment(line);
1501 let trimmed = stripped.trim_end();
1502 if let Some(before_dots) = trimmed.strip_suffix("...") {
1503 pending.push_str(before_dots);
1505 pending.push(' ');
1506 } else if pending.is_empty() {
1507 result.push_str(line);
1508 result.push('\n');
1509 } else {
1510 pending.push_str(line.trim_start());
1512 result.push_str(&pending);
1513 result.push('\n');
1514 pending.clear();
1515 }
1516 }
1517 if !pending.is_empty() {
1519 result.push_str(pending.trim_end());
1520 }
1521 result
1522}
1523
1524fn split_block_line(line: &str) -> Vec<String> {
1527 let mut parts = Vec::new();
1528 let mut current = String::new();
1529 let mut in_sq = false;
1530 let mut in_dq = false;
1531 let mut paren: i32 = 0;
1532 let mut bracket: i32 = 0;
1533 let mut brace: i32 = 0;
1534
1535 for c in line.chars() {
1536 let at_depth0 = !in_sq && !in_dq && paren == 0 && bracket == 0 && brace == 0;
1537 match c {
1538 '\'' if !in_dq => {
1539 in_sq = !in_sq;
1540 current.push(c);
1541 }
1542 '"' if !in_sq => {
1543 in_dq = !in_dq;
1544 current.push(c);
1545 }
1546 '(' if !in_sq && !in_dq => {
1547 paren += 1;
1548 current.push(c);
1549 }
1550 ')' if !in_sq && !in_dq => {
1551 if paren > 0 {
1552 paren -= 1;
1553 }
1554 current.push(c);
1555 }
1556 '[' if !in_sq && !in_dq => {
1557 bracket += 1;
1558 current.push(c);
1559 }
1560 ']' if !in_sq && !in_dq => {
1561 if bracket > 0 {
1562 bracket -= 1;
1563 }
1564 current.push(c);
1565 }
1566 '{' if !in_sq && !in_dq => {
1567 brace += 1;
1568 current.push(c);
1569 }
1570 '}' if !in_sq && !in_dq => {
1571 if brace > 0 {
1572 brace -= 1;
1573 }
1574 current.push(c);
1575 }
1576 ';' if at_depth0 => {
1577 let trimmed = current.trim().to_string();
1578 if !trimmed.is_empty() {
1579 parts.push(trimmed);
1580 }
1581 current.clear();
1582 }
1583 _ => current.push(c),
1584 }
1585 }
1586 let last = current.trim().to_string();
1587 if !last.is_empty() {
1588 parts.push(last);
1589 }
1590 parts
1591}
1592
1593pub fn parse_stmts(input: &str) -> Result<Vec<StmtEntry>, String> {
1600 let raw_lines: Vec<&str> = input.lines().collect();
1601 let stripped = strip_block_comments(&raw_lines)?;
1602 let stripped_str = stripped.join("\n");
1603 let joined = join_line_continuations(&stripped_str);
1604 let lines: Vec<&str> = joined.lines().collect();
1605 let mut pos = 0;
1606 parse_stmts_from_lines(&lines, &mut pos, &[])
1607}
1608
1609fn parse_stmts_from_lines(
1612 lines: &[&str],
1613 pos: &mut usize,
1614 stop_at: &[&str],
1615) -> Result<Vec<StmtEntry>, String> {
1616 let mut stmts = Vec::new();
1617
1618 while *pos < lines.len() {
1619 let stmt_line = *pos + 1; let raw = lines[*pos];
1621 let line = strip_line_comment(raw).trim();
1622
1623 if line.is_empty() {
1624 *pos += 1;
1625 continue;
1626 }
1627
1628 if let Some(kw) = leading_keyword(line)
1630 && stop_at.contains(&kw)
1631 {
1632 return Ok(stmts);
1633 }
1634
1635 if matches!(
1639 leading_keyword(line),
1640 Some("if" | "for" | "while" | "switch" | "do")
1641 ) {
1642 let virtual_parts = split_block_line(line);
1643 let last_kw = virtual_parts
1644 .last()
1645 .map(|s| leading_keyword(s.trim()))
1646 .unwrap_or(None);
1647 if matches!(last_kw, Some("end") | Some("until")) {
1648 let virtual_refs: Vec<&str> = virtual_parts.iter().map(|s| s.as_str()).collect();
1649 let mut vpos = 0;
1650 let inner = parse_stmts_from_lines(&virtual_refs, &mut vpos, stop_at)?;
1651 stmts.extend(
1653 inner
1654 .into_iter()
1655 .map(|(s, silent, _)| (s, silent, stmt_line)),
1656 );
1657 *pos += 1;
1658 continue;
1659 }
1660 }
1661
1662 match leading_keyword(line) {
1663 Some("if") => {
1665 let cond_str = line["if".len()..].trim();
1666 if cond_str.is_empty() {
1667 return Err("Expected condition after 'if'".to_string());
1668 }
1669 let cond = parse_condition(cond_str)?;
1670 *pos += 1;
1671
1672 let body = parse_stmts_from_lines(lines, pos, &["elseif", "else", "end"])?;
1673
1674 let mut elseif_branches = Vec::new();
1675 loop {
1676 if *pos >= lines.len() {
1677 return Err(
1678 "Unexpected end of input inside 'if': expected 'end'".to_string()
1679 );
1680 }
1681 let kw_line = strip_line_comment(lines[*pos]).trim();
1682 if leading_keyword(kw_line) == Some("elseif") {
1683 let ei_str = kw_line["elseif".len()..].trim();
1684 if ei_str.is_empty() {
1685 return Err("Expected condition after 'elseif'".to_string());
1686 }
1687 let ei_cond = parse_condition(ei_str)?;
1688 *pos += 1;
1689 let ei_body =
1690 parse_stmts_from_lines(lines, pos, &["elseif", "else", "end"])?;
1691 elseif_branches.push((ei_cond, ei_body));
1692 } else {
1693 break;
1694 }
1695 }
1696
1697 let else_body = if *pos < lines.len()
1698 && leading_keyword(strip_line_comment(lines[*pos]).trim()) == Some("else")
1699 {
1700 *pos += 1; Some(parse_stmts_from_lines(lines, pos, &["end"])?)
1702 } else {
1703 None
1704 };
1705
1706 expect_end(lines, pos, "if")?;
1707
1708 stmts.push((
1709 Stmt::If {
1710 cond,
1711 body,
1712 elseif_branches,
1713 else_body,
1714 },
1715 false,
1716 stmt_line,
1717 ));
1718 }
1719
1720 Some("for") => {
1722 let rest = line["for".len()..].trim();
1723 if rest.is_empty() {
1724 return Err("Expected 'var = expr' after 'for'".to_string());
1725 }
1726 let (var, range_expr) = parse_for_header(rest)?;
1727 *pos += 1;
1728 let body = parse_stmts_from_lines(lines, pos, &["end"])?;
1729 expect_end(lines, pos, "for")?;
1730 stmts.push((
1731 Stmt::For {
1732 var,
1733 range_expr,
1734 body,
1735 },
1736 false,
1737 stmt_line,
1738 ));
1739 }
1740
1741 Some("while") => {
1743 let cond_str = line["while".len()..].trim();
1744 if cond_str.is_empty() {
1745 return Err("Expected condition after 'while'".to_string());
1746 }
1747 let cond = parse_condition(cond_str)?;
1748 *pos += 1;
1749 let body = parse_stmts_from_lines(lines, pos, &["end"])?;
1750 expect_end(lines, pos, "while")?;
1751 stmts.push((Stmt::While { cond, body }, false, stmt_line));
1752 }
1753
1754 Some("break") => {
1756 stmts.push((Stmt::Break, false, stmt_line));
1757 *pos += 1;
1758 }
1759 Some("continue") => {
1760 stmts.push((Stmt::Continue, false, stmt_line));
1761 *pos += 1;
1762 }
1763
1764 Some("switch") => {
1766 let expr_str = line["switch".len()..].trim();
1767 if expr_str.is_empty() {
1768 return Err("Expected expression after 'switch'".to_string());
1769 }
1770 let expr = parse_condition(expr_str)?;
1771 *pos += 1;
1772
1773 #[allow(clippy::type_complexity)]
1774 let mut cases: Vec<(Vec<Expr>, Vec<StmtEntry>)> = Vec::new();
1775 let mut otherwise_body: Option<Vec<StmtEntry>> = None;
1776
1777 loop {
1778 if *pos >= lines.len() {
1779 return Err(
1780 "Unexpected end of input inside 'switch': expected 'end'".to_string()
1781 );
1782 }
1783 let kw_line = strip_line_comment(lines[*pos]).trim();
1784 match leading_keyword(kw_line) {
1785 Some("case") => {
1786 let case_str = kw_line["case".len()..].trim();
1787 if case_str.is_empty() {
1788 return Err("Expected value after 'case'".to_string());
1789 }
1790 let case_expr = parse_condition(case_str)?;
1791 *pos += 1;
1792 let case_body =
1793 parse_stmts_from_lines(lines, pos, &["case", "otherwise", "end"])?;
1794 cases.push((vec![case_expr], case_body));
1795 }
1796 Some("otherwise") => {
1797 *pos += 1;
1798 let ob = parse_stmts_from_lines(lines, pos, &["end"])?;
1799 otherwise_body = Some(ob);
1800 break;
1801 }
1802 Some("end") => break,
1803 _ => {
1804 return Err(format!(
1805 "Expected 'case', 'otherwise', or 'end' in switch block, found: '{kw_line}'"
1806 ));
1807 }
1808 }
1809 }
1810
1811 expect_end(lines, pos, "switch")?;
1812 stmts.push((
1813 Stmt::Switch {
1814 expr,
1815 cases,
1816 otherwise_body,
1817 },
1818 false,
1819 stmt_line,
1820 ));
1821 }
1822
1823 Some("do") => {
1825 *pos += 1;
1826 let body = parse_stmts_from_lines(lines, pos, &["until"])?;
1827 if *pos >= lines.len() {
1828 return Err("Unexpected end of input inside 'do': expected 'until'".to_string());
1829 }
1830 let until_line = strip_line_comment(lines[*pos]).trim();
1831 if leading_keyword(until_line) != Some("until") {
1832 return Err(format!("Expected 'until', found: '{until_line}'"));
1833 }
1834 let cond_str = until_line["until".len()..].trim();
1835 if cond_str.is_empty() {
1836 return Err("Expected condition after 'until'".to_string());
1837 }
1838 let cond = parse_condition(cond_str)?;
1839 *pos += 1;
1840 stmts.push((Stmt::DoUntil { body, cond }, false, stmt_line));
1841 }
1842
1843 Some("function") => {
1845 let header = line["function".len()..].trim();
1846 if header.is_empty() {
1847 return Err("Expected function header after 'function'".to_string());
1848 }
1849 let (name, outputs, params) = parse_function_header(header)?;
1850
1851 *pos += 1;
1852 let body_start = *pos;
1855
1856 let doc = {
1859 let mut doc_lines: Vec<String> = Vec::new();
1860 let mut scan = body_start;
1861 while scan < lines.len() {
1862 let raw = lines[scan].trim();
1863 if raw.starts_with('%') || raw.starts_with('#') {
1864 let stripped = raw.trim_start_matches(['%', '#']);
1865 let text = stripped
1866 .strip_prefix(' ')
1867 .unwrap_or(stripped)
1868 .trim_end()
1869 .to_string();
1870 doc_lines.push(text);
1871 scan += 1;
1872 } else {
1873 break;
1874 }
1875 }
1876 if doc_lines.is_empty() {
1877 None
1878 } else {
1879 Some(doc_lines.join("\n"))
1880 }
1881 };
1882
1883 let mut depth: i32 = 1;
1884 while *pos < lines.len() && depth > 0 {
1885 let l = strip_line_comment(lines[*pos]).trim();
1886 let delta = if is_single_line_block(l) {
1887 0
1888 } else {
1889 block_depth_delta(l)
1890 };
1891 depth += delta;
1892 if depth == 0 {
1893 break;
1894 }
1895 *pos += 1;
1896 }
1897 if depth != 0 {
1898 return Err(format!(
1899 "Unexpected end of input: expected 'end' to close 'function {name}'"
1900 ));
1901 }
1902 let body_source = lines[body_start..*pos].join("\n");
1903 *pos += 1; stmts.push((
1905 Stmt::FunctionDef {
1906 name,
1907 outputs,
1908 params,
1909 body_source,
1910 doc,
1911 },
1912 false,
1913 stmt_line,
1914 ));
1915 }
1916
1917 Some("return") => {
1919 stmts.push((Stmt::Return, false, stmt_line));
1920 *pos += 1;
1921 }
1922
1923 Some("try") => {
1925 *pos += 1;
1926 let try_body = parse_stmts_from_lines(lines, pos, &["catch", "end"])?;
1927
1928 if *pos >= lines.len() {
1929 return Err(
1930 "Unexpected end of input inside 'try': expected 'catch' or 'end'"
1931 .to_string(),
1932 );
1933 }
1934 let kw_line = strip_line_comment(lines[*pos]).trim();
1935 let (catch_var, catch_body) = if leading_keyword(kw_line) == Some("catch") {
1936 let catch_rest = kw_line["catch".len()..].trim();
1937 let catch_var = if catch_rest.is_empty() {
1938 None
1939 } else if is_valid_ident(catch_rest) {
1940 Some(catch_rest.to_string())
1941 } else {
1942 return Err(format!(
1943 "Expected identifier after 'catch', got '{catch_rest}'"
1944 ));
1945 };
1946 *pos += 1;
1947 let catch_body = parse_stmts_from_lines(lines, pos, &["end"])?;
1948 (catch_var, catch_body)
1949 } else {
1950 (None, vec![])
1952 };
1953
1954 expect_end(lines, pos, "try")?;
1955 stmts.push((
1956 Stmt::TryCatch {
1957 try_body,
1958 catch_var,
1959 catch_body,
1960 },
1961 false,
1962 stmt_line,
1963 ));
1964 }
1965
1966 Some(kw @ ("end" | "else" | "elseif" | "case" | "otherwise" | "until" | "catch")) => {
1968 return Err(format!("Unexpected '{kw}' without matching block opener"));
1969 }
1970
1971 _ => {
1973 if line == "clear" {
1977 stmts.push((
1978 Stmt::Expr(Expr::Call("clear".to_string(), vec![])),
1979 false,
1980 stmt_line,
1981 ));
1982 *pos += 1;
1983 continue;
1984 }
1985 if let Some(rest) = line
1986 .strip_prefix("clear")
1987 .filter(|r| r.starts_with(|c: char| c.is_whitespace()))
1988 {
1989 let names: Vec<Expr> = rest
1990 .split_whitespace()
1991 .map(|n| Expr::StrLiteral(n.to_string()))
1992 .collect();
1993 stmts.push((
1994 Stmt::Expr(Expr::Call("clear".to_string(), names)),
1995 false,
1996 stmt_line,
1997 ));
1998 *pos += 1;
1999 continue;
2000 }
2001
2002 if line == "format"
2004 || line
2005 .strip_prefix("format")
2006 .is_some_and(|r| r.starts_with(|c: char| c.is_whitespace()))
2007 {
2008 let arg = line
2009 .strip_prefix("format")
2010 .map(str::trim)
2011 .unwrap_or("")
2012 .to_string();
2013 let args = if arg.is_empty() {
2014 vec![]
2015 } else {
2016 vec![Expr::StrLiteral(arg)]
2017 };
2018 stmts.push((
2019 Stmt::Expr(Expr::Call("format".to_string(), args)),
2020 true,
2021 stmt_line,
2022 ));
2023 *pos += 1;
2024 continue;
2025 }
2026
2027 let joined_buf: String;
2031 let effective_raw: &str;
2032 let stripped_first = strip_line_comment(raw);
2033 if bracket_depth_delta(stripped_first) > 0 {
2034 let mut buf = stripped_first.to_string();
2035 while bracket_depth_delta(&buf) > 0 && *pos + 1 < lines.len() {
2036 *pos += 1;
2037 buf.push('\n');
2038 buf.push_str(strip_line_comment(lines[*pos]));
2039 }
2040 joined_buf = buf;
2041 effective_raw = &joined_buf;
2042 } else {
2043 joined_buf = String::new();
2044 effective_raw = raw;
2045 }
2046 let _ = &joined_buf; for (stmt_str, silent) in split_stmts(effective_raw) {
2048 stmts.push((parse(stmt_str)?, silent, stmt_line));
2049 }
2050 *pos += 1;
2051 }
2052 }
2053 }
2054
2055 Ok(stmts)
2056}
2057
2058fn expect_end(lines: &[&str], pos: &mut usize, opener: &str) -> Result<(), String> {
2060 if *pos >= lines.len() {
2061 return Err(format!(
2062 "Unexpected end of input: expected 'end' to close '{opener}'"
2063 ));
2064 }
2065 let kw_line = strip_line_comment(lines[*pos]).trim();
2066 if leading_keyword(kw_line) != Some("end") {
2067 return Err(format!(
2068 "Expected 'end' to close '{opener}', found '{kw_line}'"
2069 ));
2070 }
2071 *pos += 1;
2072 Ok(())
2073}
2074
2075fn strip_line_comment(line: &str) -> &str {
2077 let mut in_sq = false;
2078 let mut in_dq = false;
2079 for (i, c) in line.char_indices() {
2080 match c {
2081 '\'' if !in_dq => in_sq = !in_sq,
2082 '"' if !in_sq => in_dq = !in_dq,
2083 '%' | '#' if !in_sq && !in_dq => return &line[..i],
2084 _ => {}
2085 }
2086 }
2087 line
2088}
2089
2090fn leading_keyword(line: &str) -> Option<&str> {
2094 let end = line
2095 .find(|c: char| !c.is_alphanumeric() && c != '_')
2096 .unwrap_or(line.len());
2097 let word = &line[..end];
2098 match word {
2099 "if" | "elseif" | "else" | "end" | "for" | "while" | "break" | "continue" | "switch"
2100 | "case" | "otherwise" | "do" | "until" | "function" | "return" | "try" | "catch" => {
2101 Some(word)
2102 }
2103 _ => None,
2104 }
2105}
2106
2107fn parse_function_header(header: &str) -> Result<(String, Vec<String>, Vec<String>), String> {
2114 if let Some(eq_pos) = header.find('=')
2116 && !header[eq_pos + 1..].starts_with('=')
2117 {
2118 let lhs = header[..eq_pos].trim();
2119 let rhs = header[eq_pos + 1..].trim();
2120 let outputs = parse_output_list(lhs)?;
2121 let (name, params) = parse_func_name_params(rhs)?;
2122 return Ok((name, outputs, params));
2123 }
2124 let (name, params) = parse_func_name_params(header.trim())?;
2126 Ok((name, vec![], params))
2127}
2128
2129fn parse_output_list(lhs: &str) -> Result<Vec<String>, String> {
2131 let lhs = lhs.trim();
2132 if lhs.starts_with('[') && lhs.ends_with(']') {
2133 let inner = &lhs[1..lhs.len() - 1];
2134 inner
2135 .split(',')
2136 .map(|s| {
2137 let s = s.trim();
2138 if is_valid_ident(s) {
2139 Ok(s.to_string())
2140 } else {
2141 Err(format!("Invalid output variable name: '{s}'"))
2142 }
2143 })
2144 .collect()
2145 } else if is_valid_ident(lhs) {
2146 Ok(vec![lhs.to_string()])
2147 } else {
2148 Err(format!("Invalid function output list: '{lhs}'"))
2149 }
2150}
2151
2152fn parse_func_name_params(s: &str) -> Result<(String, Vec<String>), String> {
2154 let s = s.trim();
2155 if let Some(paren_pos) = s.find('(') {
2156 let name = s[..paren_pos].trim();
2157 if !is_valid_ident(name) {
2158 return Err(format!("Invalid function name: '{name}'"));
2159 }
2160 let rest = s[paren_pos + 1..].trim();
2161 if !rest.ends_with(')') {
2162 return Err(format!("Expected ')' in function header: '{s}'"));
2163 }
2164 let params_str = rest[..rest.len() - 1].trim();
2165 let params = if params_str.is_empty() {
2166 vec![]
2167 } else {
2168 params_str
2169 .split(',')
2170 .map(|p| {
2171 let p = p.trim();
2172 if is_valid_ident(p) {
2173 Ok(p.to_string())
2174 } else {
2175 Err(format!("Invalid parameter name: '{p}'"))
2176 }
2177 })
2178 .collect::<Result<Vec<_>, _>>()?
2179 };
2180 Ok((name.to_string(), params))
2181 } else {
2182 if !is_valid_ident(s) {
2183 return Err(format!("Invalid function name: '{s}'"));
2184 }
2185 Ok((s.to_string(), vec![]))
2186 }
2187}
2188
2189fn parse_condition(cond_str: &str) -> Result<Expr, String> {
2191 match parse(cond_str)? {
2192 Stmt::Expr(e) => Ok(e),
2193 Stmt::Assign(_, _) => Err("Expected condition expression, found assignment".to_string()),
2194 _ => Err("Expected condition expression".to_string()),
2195 }
2196}
2197
2198fn parse_for_header(rest: &str) -> Result<(String, Expr), String> {
2200 match parse(rest)? {
2201 Stmt::Assign(var, expr) => Ok((var, expr)),
2202 _ => Err(format!(
2203 "Expected 'variable = expression' in 'for' header, found: '{rest}'"
2204 )),
2205 }
2206}
2207
2208fn try_split_multi_assign(input: &str) -> Option<(Vec<String>, &str)> {
2213 let trimmed = input.trim();
2214 if !trimmed.starts_with('[') {
2215 return None;
2216 }
2217 let close = trimmed.find(']')?;
2218 let rest = trimmed[close + 1..].trim();
2219 if !rest.starts_with('=') || rest.starts_with("==") {
2220 return None;
2221 }
2222 let rhs = rest[1..].trim();
2223 let inner = trimmed[1..close].trim();
2224 if inner.is_empty() {
2225 return None;
2226 }
2227 let targets: Vec<String> = inner.split(',').map(|s| s.trim().to_string()).collect();
2228 for t in &targets {
2229 if t != "~" && !is_valid_ident(t) {
2230 return None;
2231 }
2232 }
2233 Some((targets, rhs))
2234}
2235
2236fn try_split_cell_assign(input: &str) -> Option<(&str, &str, &str)> {
2239 let trimmed = input.trim();
2240 let brace_pos = trimmed.find('{')?;
2242 let name = trimmed[..brace_pos].trim();
2243 if !is_valid_ident(name) {
2244 return None;
2245 }
2246 let after_open = &trimmed[brace_pos + 1..];
2248 let close_pos = after_open.find('}')?;
2249 let idx_str = after_open[..close_pos].trim();
2250 let after_close = after_open[close_pos + 1..].trim();
2252 if !after_close.starts_with('=') || after_close.starts_with("==") {
2253 return None;
2254 }
2255 let rhs = after_close[1..].trim();
2256 Some((name, idx_str, rhs))
2257}
2258
2259fn try_split_index_assign(input: &str) -> Option<(String, &str, &str)> {
2264 let trimmed = input.trim();
2265 let bytes = trimmed.as_bytes();
2266 let mut i = 0;
2267
2268 if i >= bytes.len() || !(bytes[i].is_ascii_alphabetic() || bytes[i] == b'_') {
2270 return None;
2271 }
2272 let name_start = i;
2273 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
2274 i += 1;
2275 }
2276 let name = trimmed[name_start..i].to_string();
2277
2278 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
2280 i += 1;
2281 }
2282 if i >= bytes.len() || bytes[i] != b'(' {
2283 return None;
2284 }
2285 i += 1;
2286
2287 let idx_start = i;
2289 let mut depth = 1usize;
2290 while i < bytes.len() && depth > 0 {
2291 match bytes[i] {
2292 b'(' | b'[' | b'{' => depth += 1,
2293 b')' | b']' | b'}' => depth -= 1,
2294 _ => {}
2295 }
2296 i += 1;
2297 }
2298 if depth != 0 {
2299 return None;
2300 }
2301 let idx_str = trimmed[idx_start..i - 1].trim();
2302
2303 let rest = trimmed[i..].trim_start();
2305 if rest.starts_with('.') {
2306 return None;
2307 }
2308 if !rest.starts_with('=') || rest.starts_with("==") {
2310 return None;
2311 }
2312 let rhs = rest[1..].trim();
2313 if rhs.is_empty() {
2314 return None;
2315 }
2316 Some((name, idx_str, rhs))
2317}
2318
2319fn parse_index_args(tokens: &[Token]) -> Result<Vec<Expr>, String> {
2321 if tokens.is_empty() {
2322 return Err("Expected index expression inside '()'".to_string());
2323 }
2324 let mut pos = 0;
2325 let mut args = Vec::new();
2326 loop {
2327 args.push(parse_call_arg(tokens, &mut pos)?);
2328 match tokens.get(pos) {
2329 Some(Token::Comma) => {
2330 pos += 1;
2331 }
2332 None => break,
2333 Some(_) => return Err("Unexpected token in index expression".to_string()),
2334 }
2335 }
2336 Ok(args)
2337}
2338
2339fn try_split_assignment(input: &str) -> Option<(&str, &str)> {
2342 let trimmed = input.trim();
2343 let eq_pos = trimmed.find('=')?;
2344 if trimmed[eq_pos + 1..].starts_with('=') {
2346 return None;
2347 }
2348 let lhs = trimmed[..eq_pos].trim();
2349 let rhs = trimmed[eq_pos + 1..].trim();
2350 if is_valid_ident(lhs) {
2351 Some((lhs, rhs))
2352 } else {
2353 None
2354 }
2355}
2356
2357fn is_valid_ident(s: &str) -> bool {
2358 let mut chars = s.chars();
2359 match chars.next() {
2360 Some(c) if c.is_alphabetic() || c == '_' => chars.all(|c| c.is_alphanumeric() || c == '_'),
2361 _ => false,
2362 }
2363}
2364
2365fn parse_call_arg(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2369 if matches!(tokens.get(*pos), Some(Token::Colon)) {
2370 *pos += 1;
2371 return Ok(Expr::Colon);
2372 }
2373 parse_logical_or(tokens, pos)
2374}
2375
2376fn parse_logical_or(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2378 let mut left = parse_logical_and(tokens, pos)?;
2379 while matches!(tokens.get(*pos), Some(Token::PipePipe)) {
2380 *pos += 1;
2381 let right = parse_logical_and(tokens, pos)?;
2382 left = Expr::BinOp(Box::new(left), Op::Or, Box::new(right));
2383 }
2384 Ok(left)
2385}
2386
2387fn parse_logical_and(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2389 let mut left = parse_elem_or(tokens, pos)?;
2390 while matches!(tokens.get(*pos), Some(Token::AmpAmp)) {
2391 *pos += 1;
2392 let right = parse_elem_or(tokens, pos)?;
2393 left = Expr::BinOp(Box::new(left), Op::And, Box::new(right));
2394 }
2395 Ok(left)
2396}
2397
2398fn parse_elem_or(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2400 let mut left = parse_elem_and(tokens, pos)?;
2401 while matches!(tokens.get(*pos), Some(Token::Pipe)) {
2402 *pos += 1;
2403 let right = parse_elem_and(tokens, pos)?;
2404 left = Expr::BinOp(Box::new(left), Op::ElemOr, Box::new(right));
2405 }
2406 Ok(left)
2407}
2408
2409fn parse_elem_and(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2411 let mut left = parse_comparison(tokens, pos)?;
2412 while matches!(tokens.get(*pos), Some(Token::Amp)) {
2413 *pos += 1;
2414 let right = parse_comparison(tokens, pos)?;
2415 left = Expr::BinOp(Box::new(left), Op::ElemAnd, Box::new(right));
2416 }
2417 Ok(left)
2418}
2419
2420fn parse_comparison(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2423 let left = parse_range(tokens, pos)?;
2424 let op = match tokens.get(*pos) {
2425 Some(Token::EqEq) => Op::Eq,
2426 Some(Token::NotEq) => Op::NotEq,
2427 Some(Token::Lt) => Op::Lt,
2428 Some(Token::Gt) => Op::Gt,
2429 Some(Token::LtEq) => Op::LtEq,
2430 Some(Token::GtEq) => Op::GtEq,
2431 _ => return Ok(left),
2432 };
2433 *pos += 1;
2434 let right = parse_range(tokens, pos)?;
2435 Ok(Expr::BinOp(Box::new(left), op, Box::new(right)))
2436}
2437
2438fn parse_range(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2442 let start = parse_expr(tokens, pos)?;
2443 if !matches!(tokens.get(*pos), Some(Token::Colon)) {
2444 return Ok(start);
2445 }
2446 *pos += 1;
2447 let second = parse_expr(tokens, pos)?;
2448 if !matches!(tokens.get(*pos), Some(Token::Colon)) {
2449 return Ok(Expr::Range(Box::new(start), None, Box::new(second)));
2451 }
2452 *pos += 1;
2453 let third = parse_expr(tokens, pos)?;
2454 Ok(Expr::Range(
2456 Box::new(start),
2457 Some(Box::new(second)),
2458 Box::new(third),
2459 ))
2460}
2461
2462fn parse_expr(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2464 let mut left = parse_term(tokens, pos)?;
2465
2466 while *pos < tokens.len() {
2467 match &tokens[*pos] {
2468 Token::Plus => {
2469 *pos += 1;
2470 let right = parse_term(tokens, pos)?;
2471 left = Expr::BinOp(Box::new(left), Op::Add, Box::new(right));
2472 }
2473 Token::Minus => {
2474 *pos += 1;
2475 let right = parse_term(tokens, pos)?;
2476 left = Expr::BinOp(Box::new(left), Op::Sub, Box::new(right));
2477 }
2478 _ => break,
2479 }
2480 }
2481
2482 Ok(left)
2483}
2484
2485fn parse_term(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2488 let mut left = parse_unary(tokens, pos)?;
2489
2490 while *pos < tokens.len() {
2491 match &tokens[*pos] {
2492 Token::Star => {
2493 *pos += 1;
2494 let right = parse_unary(tokens, pos)?;
2495 left = Expr::BinOp(Box::new(left), Op::Mul, Box::new(right));
2496 }
2497 Token::Slash => {
2498 *pos += 1;
2499 let right = parse_unary(tokens, pos)?;
2500 left = Expr::BinOp(Box::new(left), Op::Div, Box::new(right));
2501 }
2502 Token::DotStar => {
2503 *pos += 1;
2504 let right = parse_unary(tokens, pos)?;
2505 left = Expr::BinOp(Box::new(left), Op::ElemMul, Box::new(right));
2506 }
2507 Token::DotSlash => {
2508 *pos += 1;
2509 let right = parse_unary(tokens, pos)?;
2510 left = Expr::BinOp(Box::new(left), Op::ElemDiv, Box::new(right));
2511 }
2512 Token::Backslash => {
2513 *pos += 1;
2514 let right = parse_unary(tokens, pos)?;
2515 left = Expr::BinOp(Box::new(left), Op::LDiv, Box::new(right));
2516 }
2517 Token::LParen => {
2518 let right = parse_unary(tokens, pos)?;
2520 left = Expr::BinOp(Box::new(left), Op::Mul, Box::new(right));
2521 }
2522 _ => break,
2523 }
2524 }
2525
2526 Ok(left)
2527}
2528
2529fn parse_power(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2533 let base = parse_primary(tokens, pos)?;
2534 if *pos < tokens.len() {
2535 match &tokens[*pos] {
2536 Token::Caret | Token::StarStar => {
2537 *pos += 1;
2538 let exp = parse_unary(tokens, pos)?;
2539 return Ok(Expr::BinOp(Box::new(base), Op::Pow, Box::new(exp)));
2540 }
2541 Token::DotCaret => {
2542 *pos += 1;
2543 let exp = parse_unary(tokens, pos)?;
2544 return Ok(Expr::BinOp(Box::new(base), Op::ElemPow, Box::new(exp)));
2545 }
2546 _ => {}
2547 }
2548 }
2549 Ok(base)
2550}
2551
2552fn parse_unary(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2556 if *pos < tokens.len() {
2557 match &tokens[*pos] {
2558 Token::Plus => {
2559 *pos += 1;
2560 return parse_unary(tokens, pos); }
2562 Token::Minus => {
2563 *pos += 1;
2564 let expr = parse_unary(tokens, pos)?;
2565 return Ok(Expr::UnaryMinus(Box::new(expr)));
2566 }
2567 Token::Tilde => {
2568 *pos += 1;
2569 let expr = parse_unary(tokens, pos)?;
2570 return Ok(Expr::UnaryNot(Box::new(expr)));
2571 }
2572 _ => {}
2573 }
2574 }
2575 parse_power(tokens, pos)
2576}
2577
2578fn parse_primary(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2581 if *pos >= tokens.len() {
2582 return Err("Unexpected end of expression".to_string());
2583 }
2584
2585 let mut expr = match &tokens[*pos] {
2586 Token::Number(n) => {
2587 let n = *n;
2588 *pos += 1;
2589 Expr::Number(n)
2590 }
2591 Token::LBrace => {
2592 *pos += 1;
2593 let mut elems = Vec::new();
2595 loop {
2596 match tokens.get(*pos) {
2597 None => return Err("Expected '}'".to_string()),
2598 Some(Token::RBrace) => {
2599 *pos += 1;
2600 break;
2601 }
2602 Some(Token::Comma) => {
2603 *pos += 1;
2604 }
2605 _ => {
2606 elems.push(parse_logical_or(tokens, pos)?);
2607 }
2608 }
2609 }
2610 Expr::CellLiteral(elems)
2611 }
2612 Token::Ident(name) => {
2613 let name = name.clone();
2614 *pos += 1;
2615 if *pos < tokens.len()
2617 && let Token::LBrace = &tokens[*pos]
2618 {
2619 *pos += 1;
2620 let idx = parse_logical_or(tokens, pos)?;
2621 if *pos >= tokens.len() {
2622 return Err("Expected '}'".to_string());
2623 }
2624 match &tokens[*pos] {
2625 Token::RBrace => {
2626 *pos += 1;
2627 Expr::CellIndex(Box::new(Expr::Var(name)), Box::new(idx))
2628 }
2629 _ => return Err("Expected '}'".to_string()),
2630 }
2631 } else if *pos < tokens.len()
2633 && let Token::LParen = &tokens[*pos]
2634 {
2635 *pos += 1;
2636 let args = if *pos < tokens.len() {
2637 if let Token::RParen = &tokens[*pos] {
2638 vec![]
2641 } else {
2642 let mut list = vec![parse_call_arg(tokens, pos)?];
2643 while *pos < tokens.len() {
2644 if let Token::Comma = &tokens[*pos] {
2645 *pos += 1;
2646 list.push(parse_call_arg(tokens, pos)?);
2647 } else {
2648 break;
2649 }
2650 }
2651 list
2652 }
2653 } else {
2654 return Err("Expected closing ')'".to_string());
2655 };
2656 if *pos >= tokens.len() {
2657 return Err("Expected closing ')'".to_string());
2658 }
2659 match &tokens[*pos] {
2660 Token::RParen => {
2661 *pos += 1;
2662 Expr::Call(name, args)
2663 }
2664 _ => return Err("Expected closing ')'".to_string()),
2665 }
2666 } else {
2667 match name.as_str() {
2669 "pi" => Expr::Number(std::f64::consts::PI),
2670 "e" => Expr::Var("e".to_string()),
2672 "nan" | "NaN" => Expr::Number(f64::NAN),
2673 "inf" | "Inf" => Expr::Number(f64::INFINITY),
2674 "NaT" => Expr::NaT,
2676 _ => Expr::Var(name),
2678 }
2679 }
2680 }
2681 Token::LParen => {
2682 *pos += 1;
2683 let inner = parse_logical_or(tokens, pos)?;
2684 if *pos >= tokens.len() {
2685 return Err("Expected closing ')'".to_string());
2686 }
2687 match &tokens[*pos] {
2688 Token::RParen => {
2689 *pos += 1;
2690 inner
2691 }
2692 _ => return Err("Expected closing ')'".to_string()),
2693 }
2694 }
2695 Token::LBracket => {
2696 *pos += 1;
2697 parse_matrix(tokens, pos)?
2698 }
2699 Token::Str(s) => {
2700 let s = s.clone();
2701 *pos += 1;
2702 Expr::StrLiteral(s)
2703 }
2704 Token::StringObj(s) => {
2705 let s = s.clone();
2706 *pos += 1;
2707 Expr::StringObjLiteral(s)
2708 }
2709 Token::At => {
2710 *pos += 1;
2711 if let Some(Token::Ident(name)) = tokens.get(*pos) {
2713 let name = name.clone();
2714 *pos += 1;
2715 return Ok(Expr::FuncHandle(name));
2716 }
2717 if !matches!(tokens.get(*pos), Some(Token::LParen)) {
2719 return Err("Expected '(' or identifier after '@'".to_string());
2720 }
2721 *pos += 1;
2722 let mut params = Vec::new();
2723 loop {
2724 match tokens.get(*pos) {
2725 Some(Token::RParen) => {
2726 *pos += 1;
2727 break;
2728 }
2729 Some(Token::Ident(name)) => {
2730 params.push(name.clone());
2731 *pos += 1;
2732 if matches!(tokens.get(*pos), Some(Token::Comma)) {
2733 *pos += 1;
2734 }
2735 }
2736 None => return Err("Expected ')' in lambda parameter list".to_string()),
2737 _ => return Err("Expected parameter name in lambda".to_string()),
2738 }
2739 }
2740 let body = parse_logical_or(tokens, pos)?;
2741 let source = format!("@({}) {}", params.join(", "), expr_to_string(&body));
2742 Expr::Lambda {
2743 params,
2744 body: Box::new(body),
2745 source,
2746 }
2747 }
2748 _ => {
2749 return Err(
2750 "Expected number, function, variable, string, '-', '[', '@', or '('".to_string(),
2751 );
2752 }
2753 };
2754
2755 loop {
2758 match tokens.get(*pos) {
2759 Some(Token::Dot) => {
2760 *pos += 1;
2761 match tokens.get(*pos) {
2762 Some(Token::LParen) => {
2763 *pos += 1;
2765 let field_expr = parse_logical_or(tokens, pos)?;
2766 if !matches!(tokens.get(*pos), Some(Token::RParen)) {
2767 return Err("Expected closing ')' in dynamic field access".to_string());
2768 }
2769 *pos += 1;
2770 expr = Expr::DynFieldGet(Box::new(expr), Box::new(field_expr));
2771 }
2772 Some(Token::Ident(field)) => {
2773 let field = field.clone();
2774 *pos += 1;
2775 expr = Expr::FieldGet(Box::new(expr), field);
2776 }
2777 _ => return Err("Expected field name after '.'".to_string()),
2778 }
2779 }
2780 Some(Token::LParen) => {
2781 if let Some(segs) = field_chain_segments(&expr)
2784 && segs.len() >= 2
2785 {
2786 *pos += 1;
2787 let args = if matches!(tokens.get(*pos), Some(Token::RParen)) {
2788 vec![]
2789 } else {
2790 let mut list = vec![parse_call_arg(tokens, pos)?];
2791 while matches!(tokens.get(*pos), Some(Token::Comma)) {
2792 *pos += 1;
2793 list.push(parse_call_arg(tokens, pos)?);
2794 }
2795 list
2796 };
2797 if !matches!(tokens.get(*pos), Some(Token::RParen)) {
2798 return Err("Expected closing ')'".to_string());
2799 }
2800 *pos += 1;
2801 expr = Expr::DotCall(segs, args);
2802 } else {
2803 break;
2804 }
2805 }
2806 Some(Token::Apostrophe) => {
2807 *pos += 1;
2808 expr = Expr::Transpose(Box::new(expr));
2809 }
2810 Some(Token::DotApostrophe) => {
2811 *pos += 1;
2812 expr = Expr::PlainTranspose(Box::new(expr));
2813 }
2814 _ => break,
2815 }
2816 }
2817
2818 Ok(expr)
2819}
2820
2821fn field_chain_segments(e: &Expr) -> Option<Vec<String>> {
2826 match e {
2827 Expr::Var(name) => Some(vec![name.clone()]),
2828 Expr::FieldGet(inner, field) => {
2829 let mut segs = field_chain_segments(inner)?;
2830 segs.push(field.clone());
2831 Some(segs)
2832 }
2833 _ => None,
2834 }
2835}
2836
2837fn parse_matrix(tokens: &[Token], pos: &mut usize) -> Result<Expr, String> {
2839 if matches!(tokens.get(*pos), Some(Token::RBracket)) {
2841 *pos += 1;
2842 return Ok(Expr::Matrix(vec![]));
2843 }
2844 let mut rows: Vec<Vec<Expr>> = Vec::new();
2845 let mut current_row: Vec<Expr> = Vec::new();
2846 loop {
2847 match tokens.get(*pos) {
2848 None => return Err("Expected ']'".to_string()),
2849 Some(Token::RBracket) => {
2850 *pos += 1;
2851 if !current_row.is_empty() {
2852 rows.push(current_row);
2853 }
2854 break;
2855 }
2856 Some(Token::Semicolon | Token::Newline) => {
2857 *pos += 1;
2858 if !current_row.is_empty() {
2859 rows.push(std::mem::take(&mut current_row));
2860 }
2861 }
2862 Some(Token::Comma) => {
2863 *pos += 1;
2864 }
2865 _ => {
2866 current_row.push(parse_logical_or(tokens, pos)?);
2867 }
2868 }
2869 }
2870 Ok(Expr::Matrix(rows))
2871}
2872
2873#[cfg(test)]
2874#[path = "parser_tests.rs"]
2875mod tests;