Skip to main content

runmat_parser/
lib.rs

1use runmat_lexer::Token;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
5#[serde(rename_all = "snake_case")]
6pub enum CompatMode {
7    #[default]
8    Matlab,
9    Strict,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub struct ParserOptions {
14    #[serde(default)]
15    pub compat_mode: CompatMode,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
19pub struct Span {
20    pub start: usize,
21    pub end: usize,
22}
23
24impl Default for ParserOptions {
25    fn default() -> Self {
26        Self {
27            compat_mode: CompatMode::Matlab,
28        }
29    }
30}
31
32impl ParserOptions {
33    pub fn new(compat_mode: CompatMode) -> Self {
34        Self { compat_mode }
35    }
36}
37
38#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
39pub enum Expr {
40    Number(String, Span),
41    String(String, Span),
42    Ident(String, Span),
43    EndKeyword(Span), // 'end' used in indexing contexts
44    Unary(UnOp, Box<Expr>, Span),
45    Binary(Box<Expr>, BinOp, Box<Expr>, Span),
46    Tensor(Vec<Vec<Expr>>, Span),
47    Cell(Vec<Vec<Expr>>, Span),
48    Index(Box<Expr>, Vec<Expr>, Span),
49    IndexCell(Box<Expr>, Vec<Expr>, Span),
50    Range(Box<Expr>, Option<Box<Expr>>, Box<Expr>, Span),
51    Colon(Span),
52    FuncCall(String, Vec<Expr>, Span),
53    Member(Box<Expr>, String, Span),
54    // Dynamic field: s.(expr)
55    MemberDynamic(Box<Expr>, Box<Expr>, Span),
56    DottedInvoke(Box<Expr>, String, Vec<Expr>, Span),
57    MethodCall(Box<Expr>, String, Vec<Expr>, Span),
58    AnonFunc {
59        params: Vec<String>,
60        body: Box<Expr>,
61        span: Span,
62    },
63    FuncHandle(String, Span),
64    MetaClass(String, Span),
65}
66
67impl Expr {
68    pub fn span(&self) -> Span {
69        match self {
70            Expr::Number(_, span)
71            | Expr::String(_, span)
72            | Expr::Ident(_, span)
73            | Expr::EndKeyword(span)
74            | Expr::Unary(_, _, span)
75            | Expr::Binary(_, _, _, span)
76            | Expr::Tensor(_, span)
77            | Expr::Cell(_, span)
78            | Expr::Index(_, _, span)
79            | Expr::IndexCell(_, _, span)
80            | Expr::Range(_, _, _, span)
81            | Expr::Colon(span)
82            | Expr::FuncCall(_, _, span)
83            | Expr::Member(_, _, span)
84            | Expr::MemberDynamic(_, _, span)
85            | Expr::DottedInvoke(_, _, _, span)
86            | Expr::MethodCall(_, _, _, span)
87            | Expr::FuncHandle(_, span)
88            | Expr::MetaClass(_, span) => *span,
89            Expr::AnonFunc { span, .. } => *span,
90        }
91    }
92
93    pub fn with_span(self, span: Span) -> Expr {
94        match self {
95            Expr::Number(value, _) => Expr::Number(value, span),
96            Expr::String(value, _) => Expr::String(value, span),
97            Expr::Ident(value, _) => Expr::Ident(value, span),
98            Expr::EndKeyword(_) => Expr::EndKeyword(span),
99            Expr::Unary(op, expr, _) => Expr::Unary(op, expr, span),
100            Expr::Binary(lhs, op, rhs, _) => Expr::Binary(lhs, op, rhs, span),
101            Expr::Tensor(rows, _) => Expr::Tensor(rows, span),
102            Expr::Cell(rows, _) => Expr::Cell(rows, span),
103            Expr::Index(base, indices, _) => Expr::Index(base, indices, span),
104            Expr::IndexCell(base, indices, _) => Expr::IndexCell(base, indices, span),
105            Expr::Range(start, step, end, _) => Expr::Range(start, step, end, span),
106            Expr::Colon(_) => Expr::Colon(span),
107            Expr::FuncCall(name, args, _) => Expr::FuncCall(name, args, span),
108            Expr::Member(base, name, _) => Expr::Member(base, name, span),
109            Expr::MemberDynamic(base, name, _) => Expr::MemberDynamic(base, name, span),
110            Expr::DottedInvoke(base, name, args, _) => Expr::DottedInvoke(base, name, args, span),
111            Expr::MethodCall(base, name, args, _) => Expr::MethodCall(base, name, args, span),
112            Expr::AnonFunc { params, body, .. } => Expr::AnonFunc { params, body, span },
113            Expr::FuncHandle(name, _) => Expr::FuncHandle(name, span),
114            Expr::MetaClass(name, _) => Expr::MetaClass(name, span),
115        }
116    }
117}
118
119#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
120pub enum BinOp {
121    Add,
122    Sub,
123    Mul,
124    RightDiv,
125    Pow,
126    LeftDiv,
127    Colon,
128    // Element-wise operations
129    ElemMul,     // .*
130    ElemDiv,     // ./
131    ElemPow,     // .^
132    ElemLeftDiv, // .\
133    // Logical operations
134    AndAnd, // && (short-circuit)
135    OrOr,   // || (short-circuit)
136    BitAnd, // &
137    BitOr,  // |
138    // Comparison operations
139    Equal,        // ==
140    NotEqual,     // ~=
141    Less,         // <
142    LessEqual,    // <=
143    Greater,      // >
144    GreaterEqual, // >=
145}
146
147#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
148pub enum UnOp {
149    Plus,
150    Minus,
151    Transpose,
152    NonConjugateTranspose,
153    Not, // ~
154}
155
156#[derive(Debug, PartialEq)]
157pub enum Stmt {
158    ExprStmt(Expr, bool, Span), // Expression and whether it's semicolon-terminated (suppressed)
159    Assign(String, Expr, bool, Span), // Variable, Expression, and whether it's semicolon-terminated (suppressed)
160    MultiAssign(Vec<String>, Expr, bool, Span),
161    AssignLValue(LValue, Expr, bool, Span),
162    If {
163        cond: Expr,
164        then_body: Vec<Stmt>,
165        elseif_blocks: Vec<(Expr, Vec<Stmt>)>,
166        else_body: Option<Vec<Stmt>>,
167        span: Span,
168    },
169    While {
170        cond: Expr,
171        body: Vec<Stmt>,
172        span: Span,
173    },
174    For {
175        var: String,
176        expr: Expr,
177        body: Vec<Stmt>,
178        span: Span,
179    },
180    Switch {
181        expr: Expr,
182        cases: Vec<(Expr, Vec<Stmt>)>,
183        otherwise: Option<Vec<Stmt>>,
184        span: Span,
185    },
186    TryCatch {
187        try_body: Vec<Stmt>,
188        catch_var: Option<String>,
189        catch_body: Vec<Stmt>,
190        span: Span,
191    },
192    Global(Vec<String>, Span),
193    Persistent(Vec<String>, Span),
194    Break(Span),
195    Continue(Span),
196    Return(Span),
197    Function {
198        name: String,
199        params: Vec<String>,
200        outputs: Vec<String>,
201        body: Vec<Stmt>,
202        span: Span,
203    },
204    Import {
205        path: Vec<String>,
206        wildcard: bool,
207        span: Span,
208    },
209    ClassDef {
210        name: String,
211        super_class: Option<String>,
212        members: Vec<ClassMember>,
213        span: Span,
214    },
215}
216
217impl Stmt {
218    pub fn span(&self) -> Span {
219        match self {
220            Stmt::ExprStmt(_, _, span)
221            | Stmt::Assign(_, _, _, span)
222            | Stmt::MultiAssign(_, _, _, span)
223            | Stmt::AssignLValue(_, _, _, span)
224            | Stmt::Global(_, span)
225            | Stmt::Persistent(_, span)
226            | Stmt::Break(span)
227            | Stmt::Continue(span)
228            | Stmt::Return(span) => *span,
229            Stmt::If { span, .. }
230            | Stmt::While { span, .. }
231            | Stmt::For { span, .. }
232            | Stmt::Switch { span, .. }
233            | Stmt::TryCatch { span, .. }
234            | Stmt::Function { span, .. }
235            | Stmt::Import { span, .. }
236            | Stmt::ClassDef { span, .. } => *span,
237        }
238    }
239}
240
241#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
242pub enum LValue {
243    Var(String),
244    Member(Box<Expr>, String),
245    MemberDynamic(Box<Expr>, Box<Expr>),
246    Index(Box<Expr>, Vec<Expr>),
247    IndexCell(Box<Expr>, Vec<Expr>),
248}
249
250#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
251pub struct Attr {
252    pub name: String,
253    pub value: Option<String>,
254}
255
256#[derive(Debug, PartialEq)]
257pub enum ClassMember {
258    Properties {
259        attributes: Vec<Attr>,
260        names: Vec<String>,
261    },
262    Methods {
263        attributes: Vec<Attr>,
264        body: Vec<Stmt>,
265    },
266    Events {
267        attributes: Vec<Attr>,
268        names: Vec<String>,
269    },
270    Enumeration {
271        attributes: Vec<Attr>,
272        names: Vec<String>,
273    },
274    Arguments {
275        attributes: Vec<Attr>,
276        names: Vec<String>,
277    },
278}
279
280#[derive(Debug, PartialEq)]
281pub struct Program {
282    pub body: Vec<Stmt>,
283}
284
285#[derive(Clone)]
286struct TokenInfo {
287    token: Token,
288    lexeme: String,
289    position: usize,
290    end: usize,
291}
292
293#[derive(Debug)]
294pub struct SyntaxError {
295    pub message: String,
296    pub position: usize,
297    pub found_token: Option<String>,
298    pub expected: Option<String>,
299}
300
301pub fn parse(input: &str) -> Result<Program, SyntaxError> {
302    parse_with_options(input, ParserOptions::default())
303}
304
305pub fn parse_with_options(input: &str, options: ParserOptions) -> Result<Program, SyntaxError> {
306    use runmat_lexer::tokenize_detailed;
307
308    let toks = tokenize_detailed(input);
309    let mut tokens = Vec::new();
310
311    for t in toks {
312        if matches!(t.token, Token::Error) {
313            return Err(SyntaxError {
314                message: format!("Invalid token: '{}'", t.lexeme),
315                position: t.start,
316                found_token: Some(t.lexeme),
317                expected: None,
318            });
319        }
320        // Skip layout-only tokens from lexing
321        if matches!(t.token, Token::Ellipsis | Token::Section) {
322            continue;
323        }
324        tokens.push(TokenInfo {
325            token: t.token,
326            lexeme: t.lexeme,
327            position: t.start,
328            end: t.end,
329        });
330    }
331
332    let mut parser = Parser {
333        tokens,
334        pos: 0,
335        input: input.to_string(),
336        options,
337        in_matrix_expr: false,
338    };
339    parser.parse_program()
340}
341
342impl std::fmt::Display for SyntaxError {
343    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344        write!(
345            f,
346            "Syntax error at position {}: {}",
347            self.position, self.message
348        )?;
349        if let Some(found) = &self.found_token {
350            write!(f, " (found: '{found}')")?;
351        }
352        if let Some(expected) = &self.expected {
353            write!(f, " (expected: {expected})")?;
354        }
355        Ok(())
356    }
357}
358
359impl std::error::Error for SyntaxError {}
360
361impl From<String> for SyntaxError {
362    fn from(value: String) -> Self {
363        SyntaxError {
364            message: value,
365            position: 0,
366            found_token: None,
367            expected: None,
368        }
369    }
370}
371
372impl From<SyntaxError> for String {
373    fn from(error: SyntaxError) -> Self {
374        error.to_string()
375    }
376}
377
378struct Parser {
379    tokens: Vec<TokenInfo>,
380    pos: usize,
381    input: String,
382    options: ParserOptions,
383    in_matrix_expr: bool,
384}
385
386#[derive(Clone, Copy)]
387struct CommandVerb {
388    name: &'static str,
389    arg_kind: CommandArgKind,
390}
391
392#[derive(Clone, Copy)]
393enum CommandArgKind {
394    Keyword {
395        allowed: &'static [&'static str],
396        optional: bool,
397    },
398    Any,
399    StringifyWords,
400}
401
402const COMMAND_VERBS: &[CommandVerb] = &[
403    CommandVerb {
404        name: "hold",
405        arg_kind: CommandArgKind::Keyword {
406            allowed: &["on", "off", "all", "reset"],
407            optional: false,
408        },
409    },
410    CommandVerb {
411        name: "grid",
412        arg_kind: CommandArgKind::Keyword {
413            allowed: &["on", "off"],
414            optional: false,
415        },
416    },
417    CommandVerb {
418        name: "box",
419        arg_kind: CommandArgKind::Keyword {
420            allowed: &["on", "off"],
421            optional: false,
422        },
423    },
424    CommandVerb {
425        name: "axis",
426        arg_kind: CommandArgKind::Keyword {
427            allowed: &["auto", "manual", "tight", "equal", "ij", "xy"],
428            optional: false,
429        },
430    },
431    CommandVerb {
432        name: "shading",
433        arg_kind: CommandArgKind::Keyword {
434            allowed: &["flat", "interp", "faceted"],
435            optional: false,
436        },
437    },
438    CommandVerb {
439        name: "colormap",
440        arg_kind: CommandArgKind::Keyword {
441            allowed: &[
442                "parula", "jet", "hsv", "hot", "cool", "spring", "summer", "autumn", "winter",
443                "gray", "bone", "copper", "pink",
444            ],
445            optional: false,
446        },
447    },
448    CommandVerb {
449        name: "colorbar",
450        arg_kind: CommandArgKind::Keyword {
451            allowed: &["on", "off"],
452            optional: true,
453        },
454    },
455    CommandVerb {
456        name: "figure",
457        arg_kind: CommandArgKind::Any,
458    },
459    CommandVerb {
460        name: "subplot",
461        arg_kind: CommandArgKind::Any,
462    },
463    CommandVerb {
464        name: "clf",
465        arg_kind: CommandArgKind::Any,
466    },
467    CommandVerb {
468        name: "cla",
469        arg_kind: CommandArgKind::Any,
470    },
471    CommandVerb {
472        name: "close",
473        arg_kind: CommandArgKind::StringifyWords,
474    },
475    CommandVerb {
476        name: "clear",
477        arg_kind: CommandArgKind::StringifyWords,
478    },
479];
480
481impl Parser {
482    fn skip_newlines(&mut self) {
483        while self.consume(&Token::Newline) {}
484    }
485
486    fn tokens_adjacent(&self, left: usize, right: usize) -> bool {
487        match (self.tokens.get(left), self.tokens.get(right)) {
488            (Some(a), Some(b)) => a.end == b.position,
489            _ => false,
490        }
491    }
492
493    fn span_from(&self, start: usize, end: usize) -> Span {
494        Span { start, end }
495    }
496
497    fn span_between(&self, start: Span, end: Span) -> Span {
498        Span {
499            start: start.start,
500            end: end.end,
501        }
502    }
503
504    fn last_token_end(&self) -> usize {
505        self.tokens
506            .get(self.pos.saturating_sub(1))
507            .map(|t| t.end)
508            .unwrap_or(self.input.len())
509    }
510
511    fn make_binary(&self, left: Expr, op: BinOp, right: Expr) -> Expr {
512        let span = self.span_between(left.span(), right.span());
513        Expr::Binary(Box::new(left), op, Box::new(right), span)
514    }
515
516    fn make_unary(&self, op: UnOp, operand: Expr, op_start: usize) -> Expr {
517        let span = self.span_from(op_start, operand.span().end);
518        Expr::Unary(op, Box::new(operand), span)
519    }
520
521    fn is_simple_assignment_ahead(&self) -> bool {
522        // Heuristic: at statement start, if we see Ident ... '=' before a terminator, treat as assignment
523        self.peek_token() == Some(&Token::Ident) && self.peek_token_at(1) == Some(&Token::Assign)
524    }
525    fn parse_program(&mut self) -> Result<Program, SyntaxError> {
526        let mut body = Vec::new();
527        while self.pos < self.tokens.len() {
528            if self.consume(&Token::Semicolon)
529                || self.consume(&Token::Comma)
530                || self.consume(&Token::Newline)
531            {
532                continue;
533            }
534            body.push(self.parse_stmt_with_semicolon()?);
535        }
536        Ok(Program { body })
537    }
538
539    fn error(&self, message: &str) -> SyntaxError {
540        SyntaxError {
541            message: message.to_string(),
542            position: self.current_position(),
543            found_token: self.peek().map(|t| t.lexeme.clone()),
544            expected: None,
545        }
546    }
547
548    fn error_with_expected(&self, message: &str, expected: &str) -> SyntaxError {
549        SyntaxError {
550            message: message.to_string(),
551            position: self.current_position(),
552            found_token: self.peek().map(|t| t.lexeme.clone()),
553            expected: Some(expected.to_string()),
554        }
555    }
556
557    fn parse_stmt_with_semicolon(&mut self) -> Result<Stmt, SyntaxError> {
558        let stmt = self.parse_stmt()?;
559        let is_semicolon_terminated = self.consume(&Token::Semicolon);
560
561        // Expression statements: semicolon indicates output suppression.
562        // Assignments/lvalues are now suppressed whenever a semicolon is present, even at EOF.
563        match stmt {
564            Stmt::ExprStmt(expr, _, span) => {
565                Ok(Stmt::ExprStmt(expr, is_semicolon_terminated, span))
566            }
567            Stmt::Assign(name, expr, _, span) => {
568                Ok(Stmt::Assign(name, expr, is_semicolon_terminated, span))
569            }
570            Stmt::MultiAssign(names, expr, _, span) => Ok(Stmt::MultiAssign(
571                names,
572                expr,
573                is_semicolon_terminated,
574                span,
575            )),
576            Stmt::AssignLValue(lv, expr, _, span) => {
577                Ok(Stmt::AssignLValue(lv, expr, is_semicolon_terminated, span))
578            }
579            other => Ok(other),
580        }
581    }
582
583    fn parse_stmt(&mut self) -> Result<Stmt, SyntaxError> {
584        match self.peek_token() {
585            Some(Token::If) => self.parse_if().map_err(|e| e.into()),
586            Some(Token::For) => self.parse_for().map_err(|e| e.into()),
587            Some(Token::While) => self.parse_while().map_err(|e| e.into()),
588            Some(Token::Switch) => self.parse_switch().map_err(|e| e.into()),
589            Some(Token::Try) => self.parse_try_catch().map_err(|e| e.into()),
590            Some(Token::Import) => self.parse_import().map_err(|e| e.into()),
591            Some(Token::ClassDef) => self.parse_classdef().map_err(|e| e.into()),
592            Some(Token::Global) => self.parse_global().map_err(|e| e.into()),
593            Some(Token::Persistent) => self.parse_persistent().map_err(|e| e.into()),
594            Some(Token::Break) => {
595                let token = &self.tokens[self.pos];
596                self.pos += 1;
597                Ok(Stmt::Break(self.span_from(token.position, token.end)))
598            }
599            Some(Token::Continue) => {
600                let token = &self.tokens[self.pos];
601                self.pos += 1;
602                Ok(Stmt::Continue(self.span_from(token.position, token.end)))
603            }
604            Some(Token::Return) => {
605                let token = &self.tokens[self.pos];
606                self.pos += 1;
607                Ok(Stmt::Return(self.span_from(token.position, token.end)))
608            }
609            Some(Token::Function) => self.parse_function().map_err(|e| e.into()),
610            // Multi-assign like [a,b] = f()
611            Some(Token::LBracket) => {
612                if matches!(self.peek_token_at(1), Some(Token::Ident | Token::Tilde)) {
613                    match self.try_parse_multi_assign() {
614                        Ok(stmt) => Ok(stmt),
615                        Err(msg) => Err(self.error(&msg)),
616                    }
617                } else {
618                    let expr = self.parse_expr()?;
619                    let span = expr.span();
620                    Ok(Stmt::ExprStmt(expr, false, span))
621                }
622            }
623            _ => {
624                if self.peek_token() == Some(&Token::Ident)
625                    && self.peek_token_at(1) == Some(&Token::Assign)
626                {
627                    let name_token = self
628                        .next()
629                        .ok_or_else(|| self.error("expected identifier"))?;
630                    if !self.consume(&Token::Assign) {
631                        return Err(self.error_with_expected("expected assignment operator", "'='"));
632                    }
633                    let expr = self.parse_expr()?;
634                    let span = self.span_from(name_token.position, expr.span().end);
635                    Ok(Stmt::Assign(name_token.lexeme, expr, false, span))
636                } else if self.is_simple_assignment_ahead() {
637                    // Fallback: treat as simple assignment if '=' appears before terminator
638                    let name = self.expect_ident().map_err(|e| self.error(&e))?;
639                    let start = self.tokens[self.pos.saturating_sub(1)].position;
640                    if !self.consume(&Token::Assign) {
641                        return Err(self.error_with_expected("expected assignment operator", "'='"));
642                    }
643                    let expr = self.parse_expr()?;
644                    let span = self.span_from(start, expr.span().end);
645                    Ok(Stmt::Assign(name, expr, false, span))
646                } else if self.peek_token() == Some(&Token::Ident) {
647                    // First, try complex lvalue assignment starting from an identifier: A(1)=x, A{1}=x, s.f=x, s.(n)=x
648                    if let Some(lv) = self.try_parse_lvalue_assign()? {
649                        return Ok(lv);
650                    }
651                    // Command-form at statement start if it looks like a sequence of simple arguments
652                    // and is not immediately followed by indexing/member syntax.
653                    if self.can_start_command_form() {
654                        if self.options.compat_mode == CompatMode::Strict {
655                            return Err(self.error(
656                                "Command syntax is disabled in strict compatibility mode; call functions with parentheses.",
657                            ));
658                        }
659                        let name_token = self.next().unwrap();
660                        let mut args = self.parse_command_args();
661                        if let Some(command) = self.lookup_command(&name_token.lexeme) {
662                            self.normalize_command_args(command, &mut args[..])?;
663                        }
664                        let end = self.last_token_end();
665                        let span = self.span_from(name_token.position, end);
666                        Ok(Stmt::ExprStmt(
667                            Expr::FuncCall(name_token.lexeme, args, span),
668                            false,
669                            span,
670                        ))
671                    } else {
672                        // If we see Ident <space> Ident immediately followed by postfix opener,
673                        // this is an ambiguous adjacency (e.g., "foo b(1)"). Emit a targeted error.
674                        if matches!(self.peek_token_at(1), Some(Token::Ident))
675                            && matches!(
676                                self.peek_token_at(2),
677                                Some(
678                                    Token::LParen
679                                        | Token::Dot
680                                        | Token::LBracket
681                                        | Token::LBrace
682                                        | Token::Transpose
683                                )
684                            )
685                        {
686                            return Err(self.error(
687                                "Unexpected adjacency: interpret as function call? Use parentheses (e.g., foo(b(1))).",
688                            ));
689                        }
690                        let expr = self.parse_expr()?;
691                        let span = expr.span();
692                        Ok(Stmt::ExprStmt(expr, false, span))
693                    }
694                } else {
695                    let expr = self.parse_expr()?;
696                    let span = expr.span();
697                    Ok(Stmt::ExprStmt(expr, false, span))
698                }
699            }
700        }
701    }
702
703    fn can_start_command_form(&self) -> bool {
704        // At entry, peek_token() is Some(Ident) for callee
705        let Some(current) = self.tokens.get(self.pos) else {
706            return false;
707        };
708        let verb = current.lexeme.as_str();
709        let command = self.lookup_command(verb);
710        let zero_arg_allowed = matches!(
711            command,
712            Some(CommandVerb {
713                arg_kind: CommandArgKind::Any,
714                ..
715            })
716        ) || matches!(
717            command,
718            Some(CommandVerb {
719                arg_kind: CommandArgKind::Keyword { optional: true, .. },
720                ..
721            })
722        );
723
724        let mut i = 1;
725        let mut saw_arg = false;
726        while matches!(
727            self.peek_token_at(i),
728            Some(Token::Newline | Token::Ellipsis)
729        ) {
730            i += 1;
731        }
732        // At least one simple arg must follow
733        if !matches!(
734            self.peek_token_at(i),
735            Some(Token::Ident | Token::Integer | Token::Float | Token::Str | Token::End)
736        ) {
737            if !zero_arg_allowed {
738                return false;
739            }
740        } else {
741            saw_arg = true;
742        }
743        // Consume all contiguous simple args
744        loop {
745            match self.peek_token_at(i) {
746                Some(Token::Ident | Token::Integer | Token::Float | Token::Str | Token::End) => {
747                    saw_arg = true;
748                    i += 1;
749                }
750                Some(Token::Newline | Token::Ellipsis) => {
751                    i += 1;
752                }
753                _ => break,
754            }
755        }
756        if !saw_arg && !zero_arg_allowed {
757            return false;
758        }
759        // If the next token begins indexing/member or other expression syntax, do not use command-form
760        match self.peek_token_at(i) {
761            Some(Token::LParen)
762            | Some(Token::Dot)
763            | Some(Token::LBracket)
764            | Some(Token::LBrace)
765            | Some(Token::Transpose) => false,
766            // If next token is assignment, also not a command-form (would be ambiguous)
767            Some(Token::Assign) => false,
768            // End of statement is okay for command-form
769            None | Some(Token::Semicolon) | Some(Token::Comma) | Some(Token::Newline) => true,
770            // Otherwise conservatively allow
771            _ => true,
772        }
773    }
774
775    fn parse_command_args(&mut self) -> Vec<Expr> {
776        let mut args = Vec::new();
777        loop {
778            if self.consume(&Token::Newline) {
779                continue;
780            }
781            if self.consume(&Token::Ellipsis) {
782                continue;
783            }
784            match self.peek_token() {
785                Some(Token::Ident) => {
786                    let token = self.next().unwrap();
787                    let span = self.span_from(token.position, token.end);
788                    args.push(Expr::Ident(token.lexeme, span));
789                }
790                // In command-form, accept 'end' as a literal identifier token for compatibility
791                Some(Token::End) => {
792                    let token = &self.tokens[self.pos];
793                    self.pos += 1;
794                    let span = self.span_from(token.position, token.end);
795                    args.push(Expr::Ident("end".to_string(), span));
796                }
797                Some(Token::Integer) | Some(Token::Float) => {
798                    let token = self.next().unwrap();
799                    let span = self.span_from(token.position, token.end);
800                    args.push(Expr::Number(token.lexeme, span));
801                }
802                Some(Token::Str) => {
803                    let token = self.next().unwrap();
804                    let span = self.span_from(token.position, token.end);
805                    args.push(Expr::String(token.lexeme, span));
806                }
807                // Stop on tokens that would start normal expression syntax
808                Some(Token::Slash)
809                | Some(Token::Star)
810                | Some(Token::Backslash)
811                | Some(Token::Plus)
812                | Some(Token::Minus)
813                | Some(Token::LParen)
814                | Some(Token::Dot)
815                | Some(Token::LBracket)
816                | Some(Token::LBrace)
817                | Some(Token::Transpose) => break,
818                _ => break,
819            }
820        }
821        args
822    }
823
824    fn lookup_command(&self, name: &str) -> Option<&'static CommandVerb> {
825        COMMAND_VERBS
826            .iter()
827            .find(|cmd| cmd.name.eq_ignore_ascii_case(name))
828    }
829
830    fn normalize_command_args(
831        &self,
832        command: &CommandVerb,
833        args: &mut [Expr],
834    ) -> Result<(), SyntaxError> {
835        match command.arg_kind {
836            CommandArgKind::Keyword { allowed, optional } => {
837                if args.is_empty() {
838                    if optional {
839                        return Ok(());
840                    }
841                    return Err(self.error(&format!(
842                        "'{}' command syntax requires an argument",
843                        command.name
844                    )));
845                }
846                if args.len() > 1 {
847                    return Err(self.error(&format!(
848                        "'{}' command syntax accepts only one argument",
849                        command.name
850                    )));
851                }
852                let keyword = extract_keyword(&args[0]).ok_or_else(|| {
853                    self.error(&format!(
854                        "'{}' command syntax expects a keyword argument",
855                        command.name
856                    ))
857                })?;
858                if allowed
859                    .iter()
860                    .any(|candidate| candidate.eq_ignore_ascii_case(&keyword))
861                {
862                    let span = args[0].span();
863                    args[0] = Expr::String(format!("\"{}\"", keyword), span);
864                } else {
865                    return Err(self.error(&format!(
866                        "'{}' command syntax does not support '{}'",
867                        command.name, keyword
868                    )));
869                }
870            }
871            CommandArgKind::Any => {
872                // Accept general expressions; no normalization needed.
873            }
874            CommandArgKind::StringifyWords => {
875                for arg in args {
876                    let span = arg.span();
877                    match arg {
878                        Expr::Ident(word, _) => {
879                            *arg = Expr::String(format!("\"{}\"", word), span);
880                        }
881                        Expr::EndKeyword(_) => {
882                            *arg = Expr::String("\"end\"".to_string(), span);
883                        }
884                        _ => {}
885                    }
886                }
887            }
888        }
889        Ok(())
890    }
891
892    fn try_parse_lvalue_assign(&mut self) -> Result<Option<Stmt>, SyntaxError> {
893        let save = self.pos;
894        // Parse potential LValue: Member/Index/IndexCell
895        let lvalue = if self.peek_token() == Some(&Token::Ident) {
896            // Start with primary
897            let base_token = self.next().unwrap();
898            let base_span = self.span_from(base_token.position, base_token.end);
899            let mut base = Expr::Ident(base_token.lexeme, base_span);
900            loop {
901                if self.consume(&Token::LParen) {
902                    let mut args = Vec::new();
903                    if !self.consume(&Token::RParen) {
904                        args.push(self.parse_expr()?);
905                        while self.consume(&Token::Comma) {
906                            args.push(self.parse_expr()?);
907                        }
908                        if !self.consume(&Token::RParen) {
909                            return Err(self.error_with_expected("expected ')' after indices", ")"));
910                        }
911                    }
912                    let end = self.last_token_end();
913                    let span = self.span_from(base.span().start, end);
914                    base = Expr::Index(Box::new(base), args, span);
915                } else if self.consume(&Token::LBracket) {
916                    let mut idxs = Vec::new();
917                    idxs.push(self.parse_expr()?);
918                    while self.consume(&Token::Comma) {
919                        idxs.push(self.parse_expr()?);
920                    }
921                    if !self.consume(&Token::RBracket) {
922                        return Err(self.error_with_expected("expected ']'", "]"));
923                    }
924                    let end = self.last_token_end();
925                    let span = self.span_from(base.span().start, end);
926                    base = Expr::Index(Box::new(base), idxs, span);
927                } else if self.consume(&Token::LBrace) {
928                    let mut idxs = Vec::new();
929                    idxs.push(self.parse_expr()?);
930                    while self.consume(&Token::Comma) {
931                        idxs.push(self.parse_expr()?);
932                    }
933                    if !self.consume(&Token::RBrace) {
934                        return Err(self.error_with_expected("expected '}'", "}"));
935                    }
936                    let end = self.last_token_end();
937                    let span = self.span_from(base.span().start, end);
938                    base = Expr::IndexCell(Box::new(base), idxs, span);
939                } else if self.peek_token() == Some(&Token::Dot) {
940                    // If this is .', it's a non-conjugate transpose, not a member
941                    if self.peek_token_at(1) == Some(&Token::Transpose) {
942                        break;
943                    }
944                    // If this is .+ or .-, it's an additive operator, not a member
945                    if self.peek_token_at(1) == Some(&Token::Plus)
946                        || self.peek_token_at(1) == Some(&Token::Minus)
947                    {
948                        break;
949                    }
950                    // Otherwise, member access
951                    self.pos += 1; // consume '.'
952                                   // Support dynamic field: .(expr) or static .ident
953                    if self.consume(&Token::LParen) {
954                        let name_expr = self.parse_expr()?;
955                        if !self.consume(&Token::RParen) {
956                            return Err(self.error_with_expected(
957                                "expected ')' after dynamic field expression",
958                                ")",
959                            ));
960                        }
961                        let end = self.last_token_end();
962                        let span = self.span_from(base.span().start, end);
963                        base = Expr::MemberDynamic(Box::new(base), Box::new(name_expr), span);
964                    } else {
965                        let name = self.expect_ident()?;
966                        let end = self.last_token_end();
967                        let span = self.span_from(base.span().start, end);
968                        base = Expr::Member(Box::new(base), name, span);
969                    }
970                } else {
971                    break;
972                }
973            }
974            base
975        } else {
976            self.pos = save;
977            return Ok(None);
978        };
979        if !self.consume(&Token::Assign) {
980            self.pos = save;
981            return Ok(None);
982        }
983        let rhs = self.parse_expr()?;
984        let stmt_span = self.span_between(lvalue.span(), rhs.span());
985        let stmt = match lvalue {
986            Expr::Member(b, name, _) => {
987                Stmt::AssignLValue(LValue::Member(b, name), rhs, false, stmt_span)
988            }
989            Expr::MemberDynamic(b, n, _) => {
990                Stmt::AssignLValue(LValue::MemberDynamic(b, n), rhs, false, stmt_span)
991            }
992            Expr::Index(b, idxs, _) => {
993                Stmt::AssignLValue(LValue::Index(b, idxs), rhs, false, stmt_span)
994            }
995            Expr::IndexCell(b, idxs, _) => {
996                Stmt::AssignLValue(LValue::IndexCell(b, idxs), rhs, false, stmt_span)
997            }
998            Expr::Ident(v, _) => Stmt::Assign(v, rhs, false, stmt_span),
999            _ => {
1000                self.pos = save;
1001                return Ok(None);
1002            }
1003        };
1004        Ok(Some(stmt))
1005    }
1006
1007    fn parse_expr(&mut self) -> Result<Expr, SyntaxError> {
1008        self.parse_logical_or()
1009    }
1010
1011    fn parse_logical_or(&mut self) -> Result<Expr, SyntaxError> {
1012        let mut node = self.parse_logical_and()?;
1013        while self.consume(&Token::OrOr) {
1014            let rhs = self.parse_logical_and()?;
1015            node = self.make_binary(node, BinOp::OrOr, rhs);
1016        }
1017        Ok(node)
1018    }
1019
1020    fn parse_logical_and(&mut self) -> Result<Expr, SyntaxError> {
1021        let mut node = self.parse_bitwise_or()?;
1022        while self.consume(&Token::AndAnd) {
1023            let rhs = self.parse_bitwise_or()?;
1024            node = self.make_binary(node, BinOp::AndAnd, rhs);
1025        }
1026        Ok(node)
1027    }
1028
1029    fn parse_bitwise_or(&mut self) -> Result<Expr, SyntaxError> {
1030        let mut node = self.parse_bitwise_and()?;
1031        while self.consume(&Token::Or) {
1032            let rhs = self.parse_bitwise_and()?;
1033            node = self.make_binary(node, BinOp::BitOr, rhs);
1034        }
1035        Ok(node)
1036    }
1037
1038    fn parse_bitwise_and(&mut self) -> Result<Expr, SyntaxError> {
1039        let mut node = self.parse_range()?;
1040        while self.consume(&Token::And) {
1041            let rhs = self.parse_range()?;
1042            node = self.make_binary(node, BinOp::BitAnd, rhs);
1043        }
1044        Ok(node)
1045    }
1046
1047    fn parse_range(&mut self) -> Result<Expr, SyntaxError> {
1048        let mut node = self.parse_comparison()?;
1049        if self.consume(&Token::Colon) {
1050            let mid = self.parse_comparison()?;
1051            if self.consume(&Token::Colon) {
1052                let end = self.parse_comparison()?;
1053                let span = self.span_between(node.span(), end.span());
1054                node = Expr::Range(Box::new(node), Some(Box::new(mid)), Box::new(end), span);
1055            } else {
1056                let span = self.span_between(node.span(), mid.span());
1057                node = Expr::Range(Box::new(node), None, Box::new(mid), span);
1058            }
1059        }
1060        Ok(node)
1061    }
1062
1063    fn parse_comparison(&mut self) -> Result<Expr, SyntaxError> {
1064        let mut node = self.parse_add_sub()?;
1065        loop {
1066            let op = match self.peek_token() {
1067                Some(Token::Equal) => BinOp::Equal,
1068                Some(Token::NotEqual) => BinOp::NotEqual,
1069                Some(Token::Less) => BinOp::Less,
1070                Some(Token::LessEqual) => BinOp::LessEqual,
1071                Some(Token::Greater) => BinOp::Greater,
1072                Some(Token::GreaterEqual) => BinOp::GreaterEqual,
1073                _ => break,
1074            };
1075            self.pos += 1; // consume op
1076            let rhs = self.parse_add_sub()?;
1077            node = self.make_binary(node, op, rhs);
1078        }
1079        Ok(node)
1080    }
1081
1082    fn parse_add_sub(&mut self) -> Result<Expr, String> {
1083        let mut node = self.parse_mul_div()?;
1084        loop {
1085            // In matrix expressions, `[1 +2]` (space before but not after) is element
1086            // separation yielding [1, 2], while `[1 + 2]` (space on both sides) is
1087            // addition yielding [3]. Break only when there's space before the operator
1088            // but the operator is adjacent to the next token.
1089            if self.in_matrix_expr
1090                && matches!(self.peek_token(), Some(Token::Plus | Token::Minus))
1091                && self.pos > 0
1092                && !self.tokens_adjacent(self.pos - 1, self.pos)
1093                && self.tokens_adjacent(self.pos, self.pos + 1)
1094            {
1095                let rhs_index = self.pos + 1;
1096                let rhs_is_imag_literal = self
1097                    .tokens
1098                    .get(rhs_index)
1099                    .map(|info| matches!(info.token, Token::Integer | Token::Float))
1100                    .unwrap_or(false)
1101                    && self
1102                        .tokens
1103                        .get(rhs_index + 1)
1104                        .map(|info| {
1105                            matches!(info.token, Token::Ident)
1106                                && (info.lexeme.eq_ignore_ascii_case("i")
1107                                    || info.lexeme.eq_ignore_ascii_case("j"))
1108                                && self.tokens_adjacent(rhs_index, rhs_index + 1)
1109                        })
1110                        .unwrap_or(false);
1111                if !rhs_is_imag_literal {
1112                    break;
1113                }
1114            }
1115            let op = if self.consume(&Token::Plus) {
1116                Some(BinOp::Add)
1117            } else if self.consume(&Token::Minus) {
1118                Some(BinOp::Sub)
1119            } else if self.peek_token() == Some(&Token::Dot)
1120                && (self.peek_token_at(1) == Some(&Token::Plus)
1121                    || self.peek_token_at(1) == Some(&Token::Minus))
1122            {
1123                // '.+' or '.-' tokenized as Dot then Plus/Minus; treat like Add/Sub
1124                // consume two tokens
1125                self.pos += 2;
1126                if self.tokens[self.pos - 1].token == Token::Plus {
1127                    Some(BinOp::Add)
1128                } else {
1129                    Some(BinOp::Sub)
1130                }
1131            } else {
1132                None
1133            };
1134            let Some(op) = op else { break };
1135            let rhs = self.parse_mul_div()?;
1136            node = self.make_binary(node, op, rhs);
1137        }
1138        Ok(node)
1139    }
1140
1141    fn parse_mul_div(&mut self) -> Result<Expr, String> {
1142        let mut node = self.parse_unary()?;
1143        loop {
1144            if self.peek_token() == Some(&Token::Ident) && self.pos > 0 {
1145                let prev = &self.tokens[self.pos - 1];
1146                let curr = &self.tokens[self.pos];
1147                let is_adjacent = self.tokens_adjacent(self.pos - 1, self.pos);
1148                let is_imag =
1149                    curr.lexeme.eq_ignore_ascii_case("i") || curr.lexeme.eq_ignore_ascii_case("j");
1150                if is_adjacent && is_imag && matches!(prev.token, Token::Integer | Token::Float) {
1151                    let token = self.next().unwrap();
1152                    let rhs = Expr::Ident(token.lexeme, self.span_from(token.position, token.end));
1153                    node = self.make_binary(node, BinOp::Mul, rhs);
1154                    continue;
1155                }
1156            }
1157            let op = match self.peek_token() {
1158                Some(Token::Star) => BinOp::Mul,
1159                Some(Token::DotStar) => BinOp::ElemMul,
1160                Some(Token::Slash) => BinOp::RightDiv,
1161                Some(Token::DotSlash) => BinOp::ElemDiv,
1162                Some(Token::Backslash) => BinOp::LeftDiv,
1163                Some(Token::DotBackslash) => BinOp::ElemLeftDiv,
1164                _ => break,
1165            };
1166            self.pos += 1; // consume op
1167            let rhs = self.parse_unary()?;
1168            node = self.make_binary(node, op, rhs);
1169        }
1170        Ok(node)
1171    }
1172
1173    fn parse_pow(&mut self) -> Result<Expr, String> {
1174        let node = self.parse_postfix()?;
1175        if let Some(token) = self.peek_token() {
1176            let op = match token {
1177                Token::Caret => BinOp::Pow,
1178                Token::DotCaret => BinOp::ElemPow,
1179                _ => return Ok(node),
1180            };
1181            self.pos += 1; // consume
1182            let rhs = self.parse_pow()?; // right associative
1183            Ok(self.make_binary(node, op, rhs))
1184        } else {
1185            Ok(node)
1186        }
1187    }
1188
1189    fn parse_postfix_with_base(&mut self, mut expr: Expr) -> Result<Expr, String> {
1190        loop {
1191            if self.consume(&Token::LParen) {
1192                let start = expr.span().start;
1193                let mut args = Vec::new();
1194                if !self.consume(&Token::RParen) {
1195                    args.push(self.parse_expr()?);
1196                    while self.consume(&Token::Comma) {
1197                        args.push(self.parse_expr()?);
1198                    }
1199                    if !self.consume(&Token::RParen) {
1200                        return Err("expected ')' after arguments".into());
1201                    }
1202                }
1203                let end = self.last_token_end();
1204                let span = self.span_from(start, end);
1205                // Binder-based disambiguation:
1206                // If the callee is an identifier, defer call vs. index to HIR binding.
1207                // Parse as a function call now; HIR will rewrite to Index if a variable shadows the function.
1208                if let Expr::Ident(ref name, _) = expr {
1209                    expr = Expr::FuncCall(name.clone(), args, span);
1210                } else {
1211                    // For non-ident bases (e.g., X(1), (A+B)(1)), this is indexing.
1212                    expr = Expr::Index(Box::new(expr), args, span);
1213                }
1214            } else if self.consume(&Token::LBracket) {
1215                // Array indexing
1216                let start = expr.span().start;
1217                let mut indices = Vec::new();
1218                indices.push(self.parse_expr()?);
1219                while self.consume(&Token::Comma) {
1220                    indices.push(self.parse_expr()?);
1221                }
1222                if !self.consume(&Token::RBracket) {
1223                    return Err("expected ']'".into());
1224                }
1225                let end = self.last_token_end();
1226                let span = self.span_from(start, end);
1227                expr = Expr::Index(Box::new(expr), indices, span);
1228            } else if self.consume(&Token::LBrace) {
1229                // Cell content indexing
1230                let start = expr.span().start;
1231                let mut indices = Vec::new();
1232                indices.push(self.parse_expr()?);
1233                while self.consume(&Token::Comma) {
1234                    indices.push(self.parse_expr()?);
1235                }
1236                if !self.consume(&Token::RBrace) {
1237                    return Err("expected '}'".into());
1238                }
1239                let end = self.last_token_end();
1240                let span = self.span_from(start, end);
1241                expr = Expr::IndexCell(Box::new(expr), indices, span);
1242            } else if self.peek_token() == Some(&Token::Dot) {
1243                // Could be .', .+ , .- or member access
1244                if self.peek_token_at(1) == Some(&Token::Transpose) {
1245                    self.pos += 2; // '.' and '''
1246                    let end = self.last_token_end();
1247                    let span = self.span_from(expr.span().start, end);
1248                    expr = Expr::Unary(UnOp::NonConjugateTranspose, Box::new(expr), span);
1249                    continue;
1250                }
1251                if self.peek_token_at(1) == Some(&Token::Plus)
1252                    || self.peek_token_at(1) == Some(&Token::Minus)
1253                {
1254                    // '.+' or '.-' belong to additive level; stop postfix loop
1255                    break;
1256                }
1257                // Otherwise, member access
1258                self.pos += 1; // consume '.'
1259                let name_token = match self.next() {
1260                    Some(TokenInfo {
1261                        token: Token::Ident,
1262                        lexeme,
1263                        position,
1264                        end,
1265                    }) => (lexeme, position, end),
1266                    _ => return Err("expected member name after '.'".into()),
1267                };
1268                if self.consume(&Token::LParen) {
1269                    let mut args = Vec::new();
1270                    if !self.consume(&Token::RParen) {
1271                        args.push(self.parse_expr()?);
1272                        while self.consume(&Token::Comma) {
1273                            args.push(self.parse_expr()?);
1274                        }
1275                        if !self.consume(&Token::RParen) {
1276                            return Err("expected ')' after method arguments".into());
1277                        }
1278                    }
1279                    let end = self.last_token_end();
1280                    let span = self.span_from(expr.span().start, end);
1281                    if matches!(expr, Expr::MetaClass(_, _)) {
1282                        expr = Expr::MethodCall(Box::new(expr), name_token.0, args, span);
1283                    } else {
1284                        expr = Expr::DottedInvoke(Box::new(expr), name_token.0, args, span);
1285                    }
1286                } else {
1287                    let span = self.span_from(expr.span().start, name_token.2);
1288                    expr = Expr::Member(Box::new(expr), name_token.0, span);
1289                }
1290            } else if self.consume(&Token::Transpose) {
1291                // Matrix transpose (postfix operator)
1292                let end = self.last_token_end();
1293                let span = self.span_from(expr.span().start, end);
1294                expr = Expr::Unary(UnOp::Transpose, Box::new(expr), span);
1295            } else {
1296                break;
1297            }
1298        }
1299        Ok(expr)
1300    }
1301
1302    fn parse_postfix(&mut self) -> Result<Expr, String> {
1303        let expr = self.parse_primary()?;
1304        self.parse_postfix_with_base(expr)
1305    }
1306
1307    fn parse_unary(&mut self) -> Result<Expr, String> {
1308        if self.peek_token() == Some(&Token::Plus) {
1309            let start = self.tokens[self.pos].position;
1310            self.pos += 1;
1311            let expr = self.parse_unary()?;
1312            Ok(self.make_unary(UnOp::Plus, expr, start))
1313        } else if self.peek_token() == Some(&Token::Minus) {
1314            let start = self.tokens[self.pos].position;
1315            self.pos += 1;
1316            let expr = self.parse_unary()?;
1317            Ok(self.make_unary(UnOp::Minus, expr, start))
1318        } else if self.peek_token() == Some(&Token::Tilde) {
1319            let start = self.tokens[self.pos].position;
1320            self.pos += 1;
1321            let expr = self.parse_unary()?;
1322            Ok(self.make_unary(UnOp::Not, expr, start))
1323        } else if self.peek_token() == Some(&Token::Question) {
1324            let start = self.tokens[self.pos].position;
1325            self.pos += 1;
1326            // Meta-class query with controlled qualified name consumption to allow postfix chaining
1327            // Consume packages (lowercase-leading) and exactly one Class segment (uppercase-leading), then stop.
1328            let mut parts: Vec<String> = Vec::new();
1329            let first = self.expect_ident()?;
1330            let class_consumed = first
1331                .chars()
1332                .next()
1333                .map(|c| c.is_uppercase())
1334                .unwrap_or(false);
1335            parts.push(first);
1336            while self.peek_token() == Some(&Token::Dot)
1337                && matches!(self.peek_token_at(1), Some(Token::Ident))
1338            {
1339                // Lookahead at the next identifier lexeme
1340                let next_lex = if let Some(ti) = self.tokens.get(self.pos + 1) {
1341                    ti.lexeme.clone()
1342                } else {
1343                    String::new()
1344                };
1345                let is_upper = next_lex
1346                    .chars()
1347                    .next()
1348                    .map(|c| c.is_uppercase())
1349                    .unwrap_or(false);
1350                if class_consumed {
1351                    break;
1352                }
1353                // Consume dot and ident
1354                self.pos += 1; // consume '.'
1355                let seg = self.expect_ident()?;
1356                parts.push(seg);
1357                if is_upper {
1358                    break;
1359                }
1360            }
1361            let end = self.last_token_end();
1362            let span = self.span_from(start, end);
1363            let base = Expr::MetaClass(parts.join("."), span);
1364            self.parse_postfix_with_base(base)
1365        } else {
1366            self.parse_pow()
1367        }
1368    }
1369
1370    fn parse_primary(&mut self) -> Result<Expr, String> {
1371        match self.next() {
1372            Some(info) => match info.token {
1373                Token::Integer | Token::Float => {
1374                    let span = self.span_from(info.position, info.end);
1375                    Ok(Expr::Number(info.lexeme, span))
1376                }
1377                Token::Str => {
1378                    let span = self.span_from(info.position, info.end);
1379                    Ok(Expr::String(info.lexeme, span))
1380                }
1381                Token::True => {
1382                    let span = self.span_from(info.position, info.end);
1383                    Ok(Expr::Ident("true".into(), span))
1384                }
1385                Token::False => {
1386                    let span = self.span_from(info.position, info.end);
1387                    Ok(Expr::Ident("false".into(), span))
1388                }
1389                Token::Ident => {
1390                    let span = self.span_from(info.position, info.end);
1391                    Ok(Expr::Ident(info.lexeme, span))
1392                }
1393                // Treat 'end' as EndKeyword in expression contexts; in command-form we allow 'end' to be consumed as an identifier via command-args path.
1394                Token::End => {
1395                    let span = self.span_from(info.position, info.end);
1396                    Ok(Expr::EndKeyword(span))
1397                }
1398                Token::At => {
1399                    let start = info.position;
1400                    // Anonymous function or function handle
1401                    if self.consume(&Token::LParen) {
1402                        let mut params = Vec::new();
1403                        if !self.consume(&Token::RParen) {
1404                            params.push(self.expect_ident()?);
1405                            while self.consume(&Token::Comma) {
1406                                params.push(self.expect_ident()?);
1407                            }
1408                            if !self.consume(&Token::RParen) {
1409                                return Err(
1410                                    "expected ')' after anonymous function parameters".into()
1411                                );
1412                            }
1413                        }
1414                        let body = self.parse_expr().map_err(|e| e.message)?;
1415                        let span = self.span_from(start, body.span().end);
1416                        Ok(Expr::AnonFunc {
1417                            params,
1418                            body: Box::new(body),
1419                            span,
1420                        })
1421                    } else {
1422                        // function handle @name
1423                        let name = self.expect_ident()?;
1424                        let end = self.last_token_end();
1425                        let span = self.span_from(start, end);
1426                        Ok(Expr::FuncHandle(name, span))
1427                    }
1428                }
1429                Token::LParen => {
1430                    let start = info.position;
1431                    let expr = self.parse_expr()?;
1432                    if !self.consume(&Token::RParen) {
1433                        return Err("expected ')' to close parentheses".into());
1434                    }
1435                    let end = self.last_token_end();
1436                    let span = self.span_from(start, end);
1437                    Ok(expr.with_span(span))
1438                }
1439                Token::LBracket => {
1440                    let start = info.position;
1441                    let matrix = self.parse_matrix()?;
1442                    if !self.consume(&Token::RBracket) {
1443                        return Err("expected ']' to close matrix literal".into());
1444                    }
1445                    let end = self.last_token_end();
1446                    let span = self.span_from(start, end);
1447                    Ok(matrix.with_span(span))
1448                }
1449                Token::LBrace => {
1450                    let start = info.position;
1451                    let cell = self.parse_cell()?;
1452                    if !self.consume(&Token::RBrace) {
1453                        return Err("expected '}' to close cell literal".into());
1454                    }
1455                    let end = self.last_token_end();
1456                    let span = self.span_from(start, end);
1457                    Ok(cell.with_span(span))
1458                }
1459                Token::Colon => {
1460                    let span = self.span_from(info.position, info.end);
1461                    Ok(Expr::Colon(span))
1462                }
1463                _ => Err(format!("unexpected token: {:?}", info.token)),
1464            },
1465            None => Err("unexpected end of input".into()),
1466        }
1467    }
1468
1469    fn parse_matrix(&mut self) -> Result<Expr, String> {
1470        self.skip_newlines();
1471        let mut rows = Vec::new();
1472        if self.peek_token() == Some(&Token::RBracket) {
1473            return Ok(Expr::Tensor(rows, Span::default()));
1474        }
1475        loop {
1476            self.skip_newlines();
1477            if self.peek_token() == Some(&Token::RBracket) {
1478                break;
1479            }
1480            let mut row = Vec::new();
1481            // First element in the row
1482            row.push(self.parse_matrix_expr()?);
1483            // Accept either comma-separated or whitespace-separated elements until ';' or ']'
1484            loop {
1485                if self.consume(&Token::Newline) {
1486                    continue;
1487                }
1488                if self.consume(&Token::Comma) {
1489                    row.push(self.parse_matrix_expr()?);
1490                    continue;
1491                }
1492                // If next token ends the row/matrix, stop
1493                if matches!(
1494                    self.peek_token(),
1495                    Some(Token::Semicolon) | Some(Token::RBracket)
1496                ) {
1497                    break;
1498                }
1499                // Otherwise, treat whitespace as a separator and parse the next element
1500                // Only proceed if the next token can start an expression
1501                match self.peek_token() {
1502                    Some(
1503                        Token::Ident
1504                        | Token::Integer
1505                        | Token::Float
1506                        | Token::Str
1507                        | Token::LParen
1508                        | Token::LBracket
1509                        | Token::LBrace
1510                        | Token::At
1511                        | Token::Plus
1512                        | Token::Minus
1513                        | Token::Colon
1514                        | Token::True
1515                        | Token::False,
1516                    ) => {
1517                        row.push(self.parse_matrix_expr()?);
1518                    }
1519                    _ => {
1520                        break;
1521                    }
1522                }
1523            }
1524            rows.push(row);
1525            if self.consume(&Token::Semicolon) {
1526                self.skip_newlines();
1527                continue;
1528            } else {
1529                break;
1530            }
1531        }
1532        self.skip_newlines();
1533        Ok(Expr::Tensor(rows, Span::default()))
1534    }
1535
1536    fn parse_matrix_expr(&mut self) -> Result<Expr, String> {
1537        let prior = self.in_matrix_expr;
1538        self.in_matrix_expr = true;
1539        let expr = self.parse_expr().map_err(|e| e.message);
1540        self.in_matrix_expr = prior;
1541        expr
1542    }
1543
1544    fn parse_if(&mut self) -> Result<Stmt, String> {
1545        let start = self.tokens[self.pos].position;
1546        self.consume(&Token::If);
1547        let cond = self.parse_expr()?;
1548        let then_body =
1549            self.parse_block(|t| matches!(t, Token::Else | Token::ElseIf | Token::End))?;
1550        let mut elseif_blocks = Vec::new();
1551        while self.consume(&Token::ElseIf) {
1552            let c = self.parse_expr()?;
1553            let body =
1554                self.parse_block(|t| matches!(t, Token::Else | Token::ElseIf | Token::End))?;
1555            elseif_blocks.push((c, body));
1556        }
1557        let else_body = if self.consume(&Token::Else) {
1558            Some(self.parse_block(|t| matches!(t, Token::End))?)
1559        } else {
1560            None
1561        };
1562        if !self.consume(&Token::End) {
1563            return Err("expected 'end'".into());
1564        }
1565        let end = self.last_token_end();
1566        Ok(Stmt::If {
1567            cond,
1568            then_body,
1569            elseif_blocks,
1570            else_body,
1571            span: self.span_from(start, end),
1572        })
1573    }
1574
1575    fn parse_while(&mut self) -> Result<Stmt, String> {
1576        let start = self.tokens[self.pos].position;
1577        self.consume(&Token::While);
1578        let cond = self.parse_expr()?;
1579        let body = self.parse_block(|t| matches!(t, Token::End))?;
1580        if !self.consume(&Token::End) {
1581            return Err("expected 'end'".into());
1582        }
1583        let end = self.last_token_end();
1584        Ok(Stmt::While {
1585            cond,
1586            body,
1587            span: self.span_from(start, end),
1588        })
1589    }
1590
1591    fn parse_for(&mut self) -> Result<Stmt, String> {
1592        let start = self.tokens[self.pos].position;
1593        self.consume(&Token::For);
1594        let var = self.expect_ident()?;
1595        if !self.consume(&Token::Assign) {
1596            return Err("expected '='".into());
1597        }
1598        let expr = self.parse_expr()?;
1599        let body = self.parse_block(|t| matches!(t, Token::End))?;
1600        if !self.consume(&Token::End) {
1601            return Err("expected 'end'".into());
1602        }
1603        let end = self.last_token_end();
1604        Ok(Stmt::For {
1605            var,
1606            expr,
1607            body,
1608            span: self.span_from(start, end),
1609        })
1610    }
1611
1612    fn parse_function(&mut self) -> Result<Stmt, String> {
1613        let start = self.tokens[self.pos].position;
1614        self.consume(&Token::Function);
1615        let mut outputs = Vec::new();
1616        if self.consume(&Token::LBracket) {
1617            outputs.push(self.expect_ident_or_tilde()?);
1618            while self.consume(&Token::Comma) {
1619                outputs.push(self.expect_ident_or_tilde()?);
1620            }
1621            if !self.consume(&Token::RBracket) {
1622                return Err("expected ']'".into());
1623            }
1624            if !self.consume(&Token::Assign) {
1625                return Err("expected '='".into());
1626            }
1627        } else if self.peek_token() == Some(&Token::Ident)
1628            && self.peek_token_at(1) == Some(&Token::Assign)
1629        {
1630            outputs.push(self.next().unwrap().lexeme);
1631            self.consume(&Token::Assign);
1632        }
1633        let name = self.expect_ident()?;
1634        if !self.consume(&Token::LParen) {
1635            return Err("expected '('".into());
1636        }
1637        let mut params = Vec::new();
1638        if !self.consume(&Token::RParen) {
1639            params.push(self.expect_ident()?);
1640            while self.consume(&Token::Comma) {
1641                params.push(self.expect_ident()?);
1642            }
1643            if !self.consume(&Token::RParen) {
1644                return Err("expected ')'".into());
1645            }
1646        }
1647
1648        // Enforce varargs placement constraints at parse time
1649        // varargin: at most once, must be last in params if present
1650        if let Some(idx) = params.iter().position(|p| p == "varargin") {
1651            if idx != params.len() - 1 {
1652                return Err("'varargin' must be the last input parameter".into());
1653            }
1654            if params.iter().filter(|p| p.as_str() == "varargin").count() > 1 {
1655                return Err("'varargin' cannot appear more than once".into());
1656            }
1657        }
1658        // varargout: at most once, must be last in outputs if present
1659        if let Some(idx) = outputs.iter().position(|o| o == "varargout") {
1660            if idx != outputs.len() - 1 {
1661                return Err("'varargout' must be the last output parameter".into());
1662            }
1663            if outputs.iter().filter(|o| o.as_str() == "varargout").count() > 1 {
1664                return Err("'varargout' cannot appear more than once".into());
1665            }
1666        }
1667
1668        // Optional function-level arguments block
1669        // arguments ... end  (we accept a sequence of identifiers, validation semantics handled in HIR/runtime)
1670        if self.peek_token() == Some(&Token::Arguments) {
1671            self.pos += 1; // consume 'arguments'
1672                           // Accept a flat list of identifiers optionally separated by commas/semicolons
1673            loop {
1674                if self.consume(&Token::End) {
1675                    break;
1676                }
1677                if self.consume(&Token::Semicolon) || self.consume(&Token::Comma) {
1678                    continue;
1679                }
1680                if matches!(self.peek_token(), Some(Token::Ident)) {
1681                    let _ = self.expect_ident()?;
1682                    continue;
1683                }
1684                // Tolerate newlines/whitespace-only between entries
1685                if self.peek_token().is_none() {
1686                    break;
1687                }
1688                break;
1689            }
1690        }
1691
1692        let body = self.parse_block(|t| matches!(t, Token::End))?;
1693        if !self.consume(&Token::End) {
1694            return Err("expected 'end'".into());
1695        }
1696        let end = self.last_token_end();
1697        Ok(Stmt::Function {
1698            name,
1699            params,
1700            outputs,
1701            body,
1702            span: self.span_from(start, end),
1703        })
1704    }
1705
1706    fn parse_block<F>(&mut self, term: F) -> Result<Vec<Stmt>, String>
1707    where
1708        F: Fn(&Token) -> bool,
1709    {
1710        let mut body = Vec::new();
1711        while let Some(tok) = self.peek_token() {
1712            if term(tok) {
1713                break;
1714            }
1715            if self.consume(&Token::Semicolon)
1716                || self.consume(&Token::Comma)
1717                || self.consume(&Token::Newline)
1718            {
1719                continue;
1720            }
1721            // Fast-path: handle multi-assign LHS at statement start inside blocks reliably
1722            let stmt = if self.peek_token() == Some(&Token::LBracket) {
1723                self.try_parse_multi_assign()?
1724            } else {
1725                self.parse_stmt().map_err(|e| e.message)?
1726            };
1727            let is_semicolon_terminated = self.consume(&Token::Semicolon);
1728
1729            let final_stmt = match stmt {
1730                Stmt::ExprStmt(expr, _, span) => {
1731                    Stmt::ExprStmt(expr, is_semicolon_terminated, span)
1732                }
1733                Stmt::Assign(name, expr, _, span) => {
1734                    Stmt::Assign(name, expr, is_semicolon_terminated, span)
1735                }
1736                Stmt::MultiAssign(names, expr, _, span) => {
1737                    Stmt::MultiAssign(names, expr, is_semicolon_terminated, span)
1738                }
1739                Stmt::AssignLValue(lv, expr, _, span) => {
1740                    Stmt::AssignLValue(lv, expr, is_semicolon_terminated, span)
1741                }
1742                other => other,
1743            };
1744            body.push(final_stmt);
1745        }
1746        Ok(body)
1747    }
1748
1749    fn parse_cell(&mut self) -> Result<Expr, String> {
1750        let mut rows = Vec::new();
1751        self.skip_newlines();
1752        if self.peek_token() == Some(&Token::RBrace) {
1753            return Ok(Expr::Cell(rows, Span::default()));
1754        }
1755        loop {
1756            self.skip_newlines();
1757            if self.peek_token() == Some(&Token::RBrace) {
1758                break;
1759            }
1760            let mut row = Vec::new();
1761            row.push(self.parse_expr()?);
1762            while self.consume(&Token::Comma) {
1763                row.push(self.parse_expr()?);
1764            }
1765            rows.push(row);
1766            if self.consume(&Token::Semicolon) {
1767                self.skip_newlines();
1768                continue;
1769            } else {
1770                break;
1771            }
1772        }
1773        self.skip_newlines();
1774        Ok(Expr::Cell(rows, Span::default()))
1775    }
1776
1777    fn parse_switch(&mut self) -> Result<Stmt, String> {
1778        let start = self.tokens[self.pos].position;
1779        self.consume(&Token::Switch);
1780        let control = self.parse_expr()?;
1781        let mut cases = Vec::new();
1782        let mut otherwise: Option<Vec<Stmt>> = None;
1783        loop {
1784            if self.consume(&Token::Newline) || self.consume(&Token::Semicolon) {
1785                continue;
1786            }
1787            if self.consume(&Token::Case) {
1788                let val = self.parse_expr()?;
1789                let body =
1790                    self.parse_block(|t| matches!(t, Token::Case | Token::Otherwise | Token::End))?;
1791                cases.push((val, body));
1792            } else if self.consume(&Token::Otherwise) {
1793                let body = self.parse_block(|t| matches!(t, Token::End))?;
1794                otherwise = Some(body);
1795            } else if self.consume(&Token::Comma) {
1796                continue;
1797            } else {
1798                break;
1799            }
1800        }
1801        if !self.consume(&Token::End) {
1802            return Err("expected 'end' for switch".into());
1803        }
1804        let end = self.last_token_end();
1805        Ok(Stmt::Switch {
1806            expr: control,
1807            cases,
1808            otherwise,
1809            span: self.span_from(start, end),
1810        })
1811    }
1812
1813    fn parse_try_catch(&mut self) -> Result<Stmt, String> {
1814        let start = self.tokens[self.pos].position;
1815        self.consume(&Token::Try);
1816        let try_body = self.parse_block(|t| matches!(t, Token::Catch | Token::End))?;
1817        if !self.consume(&Token::Catch) {
1818            return Err("expected 'catch' after try".into());
1819        }
1820        let catch_var = if self.peek_token() == Some(&Token::Ident) {
1821            Some(self.expect_ident()?)
1822        } else {
1823            None
1824        };
1825        let catch_body = self.parse_block(|t| matches!(t, Token::End))?;
1826        if !self.consume(&Token::End) {
1827            return Err("expected 'end' after catch".into());
1828        }
1829        let end = self.last_token_end();
1830        Ok(Stmt::TryCatch {
1831            try_body,
1832            catch_var,
1833            catch_body,
1834            span: self.span_from(start, end),
1835        })
1836    }
1837
1838    fn parse_import(&mut self) -> Result<Stmt, String> {
1839        let start = self.tokens[self.pos].position;
1840        self.consume(&Token::Import);
1841        // import pkg.sub.Class or import pkg.*
1842        let mut path = Vec::new();
1843        path.push(self.expect_ident()?);
1844        let mut wildcard = false;
1845        loop {
1846            if self.consume(&Token::DotStar) {
1847                wildcard = true;
1848                break;
1849            }
1850            if self.consume(&Token::Dot) {
1851                if self.consume(&Token::Star) {
1852                    wildcard = true;
1853                    break;
1854                } else {
1855                    path.push(self.expect_ident()?);
1856                    continue;
1857                }
1858            }
1859            break;
1860        }
1861        let end = self.last_token_end();
1862        Ok(Stmt::Import {
1863            path,
1864            wildcard,
1865            span: self.span_from(start, end),
1866        })
1867    }
1868
1869    fn parse_classdef(&mut self) -> Result<Stmt, String> {
1870        let start = self.tokens[self.pos].position;
1871        self.consume(&Token::ClassDef);
1872        let name = self.parse_qualified_name()?;
1873        let mut super_class = None;
1874        if self.consume(&Token::Less) {
1875            super_class = Some(self.parse_qualified_name()?);
1876        }
1877        let mut members: Vec<ClassMember> = Vec::new();
1878        loop {
1879            // Skip layout separators between member blocks
1880            if self.consume(&Token::Semicolon)
1881                || self.consume(&Token::Comma)
1882                || self.consume(&Token::Newline)
1883            {
1884                continue;
1885            }
1886            match self.peek_token() {
1887                Some(Token::Properties) => {
1888                    self.pos += 1;
1889                    let attrs = self.parse_optional_attr_list();
1890                    let props = self.parse_properties_names_block()?;
1891                    if !self.consume(&Token::End) {
1892                        return Err("expected 'end' after properties".into());
1893                    }
1894                    members.push(ClassMember::Properties {
1895                        attributes: attrs,
1896                        names: props,
1897                    });
1898                }
1899                Some(Token::Methods) => {
1900                    self.pos += 1;
1901                    let attrs = self.parse_optional_attr_list();
1902                    let body = self.parse_block(|t| matches!(t, Token::End))?;
1903                    if !self.consume(&Token::End) {
1904                        return Err("expected 'end' after methods".into());
1905                    }
1906                    members.push(ClassMember::Methods {
1907                        attributes: attrs,
1908                        body,
1909                    });
1910                }
1911                Some(Token::Events) => {
1912                    self.pos += 1;
1913                    let attrs = self.parse_optional_attr_list();
1914                    let names = self.parse_name_block()?;
1915                    if !self.consume(&Token::End) {
1916                        return Err("expected 'end' after events".into());
1917                    }
1918                    members.push(ClassMember::Events {
1919                        attributes: attrs,
1920                        names,
1921                    });
1922                }
1923                Some(Token::Enumeration) => {
1924                    self.pos += 1;
1925                    let attrs = self.parse_optional_attr_list();
1926                    let names = self.parse_name_block()?;
1927                    if !self.consume(&Token::End) {
1928                        return Err("expected 'end' after enumeration".into());
1929                    }
1930                    members.push(ClassMember::Enumeration {
1931                        attributes: attrs,
1932                        names,
1933                    });
1934                }
1935                Some(Token::Arguments) => {
1936                    self.pos += 1;
1937                    let attrs = self.parse_optional_attr_list();
1938                    let names = self.parse_name_block()?;
1939                    if !self.consume(&Token::End) {
1940                        return Err("expected 'end' after arguments".into());
1941                    }
1942                    members.push(ClassMember::Arguments {
1943                        attributes: attrs,
1944                        names,
1945                    });
1946                }
1947                Some(Token::End) => {
1948                    self.pos += 1;
1949                    break;
1950                }
1951                _ => break,
1952            }
1953        }
1954        let end = self.last_token_end();
1955        Ok(Stmt::ClassDef {
1956            name,
1957            super_class,
1958            members,
1959            span: self.span_from(start, end),
1960        })
1961    }
1962
1963    fn parse_name_block(&mut self) -> Result<Vec<String>, String> {
1964        let mut names = Vec::new();
1965        while let Some(tok) = self.peek_token() {
1966            if matches!(tok, Token::End) {
1967                break;
1968            }
1969            if self.consume(&Token::Semicolon)
1970                || self.consume(&Token::Comma)
1971                || self.consume(&Token::Newline)
1972            {
1973                continue;
1974            }
1975            if let Some(Token::Ident) = self.peek_token() {
1976                names.push(self.expect_ident()?);
1977            } else {
1978                break;
1979            }
1980        }
1981        Ok(names)
1982    }
1983
1984    fn parse_properties_names_block(&mut self) -> Result<Vec<String>, String> {
1985        // Accept identifiers with optional default assignment: name, name = expr
1986        let mut names = Vec::new();
1987        while let Some(tok) = self.peek_token() {
1988            if matches!(tok, Token::End) {
1989                break;
1990            }
1991            if self.consume(&Token::Semicolon)
1992                || self.consume(&Token::Comma)
1993                || self.consume(&Token::Newline)
1994            {
1995                continue;
1996            }
1997            if let Some(Token::Ident) = self.peek_token() {
1998                names.push(self.expect_ident()?);
1999                // Optional default initializer: skip over `= expr` syntactically
2000                if self.consume(&Token::Assign) {
2001                    // Parse and discard expression to keep the grammar permissive; initializer is HIR/semantics concern
2002                    let _ = self.parse_expr().map_err(|e| e.message)?;
2003                }
2004            } else {
2005                break;
2006            }
2007        }
2008        Ok(names)
2009    }
2010
2011    fn parse_optional_attr_list(&mut self) -> Vec<Attr> {
2012        // Minimal parsing of attribute lists: (Attr, Attr=Value, ...)
2013        let mut attrs: Vec<Attr> = Vec::new();
2014        if !self.consume(&Token::LParen) {
2015            return attrs;
2016        }
2017        loop {
2018            if self.consume(&Token::RParen) {
2019                break;
2020            }
2021            match self.peek_token() {
2022                Some(Token::Ident) => {
2023                    let name = self.expect_ident().unwrap_or_else(|_| "".to_string());
2024                    let mut value: Option<String> = None;
2025                    if self.consume(&Token::Assign) {
2026                        // Value could be ident, string or number; capture raw lexeme
2027                        if let Some(tok) = self.next() {
2028                            value = Some(tok.lexeme);
2029                        }
2030                    }
2031                    attrs.push(Attr { name, value });
2032                    let _ = self.consume(&Token::Comma);
2033                }
2034                Some(Token::Comma) => {
2035                    self.pos += 1;
2036                }
2037                Some(Token::RParen) => {
2038                    self.pos += 1;
2039                    break;
2040                }
2041                Some(_) => {
2042                    self.pos += 1;
2043                }
2044                None => {
2045                    break;
2046                }
2047            }
2048        }
2049        attrs
2050    }
2051
2052    fn parse_global(&mut self) -> Result<Stmt, String> {
2053        let start = self.tokens[self.pos].position;
2054        self.consume(&Token::Global);
2055        let mut names = Vec::new();
2056        names.push(self.expect_ident()?);
2057        loop {
2058            if self.consume(&Token::Comma) {
2059                names.push(self.expect_ident()?);
2060                continue;
2061            }
2062            if self.peek_token() == Some(&Token::Ident) {
2063                names.push(self.expect_ident()?);
2064                continue;
2065            }
2066            break;
2067        }
2068        let end = self.last_token_end();
2069        Ok(Stmt::Global(names, self.span_from(start, end)))
2070    }
2071
2072    fn parse_persistent(&mut self) -> Result<Stmt, String> {
2073        let start = self.tokens[self.pos].position;
2074        self.consume(&Token::Persistent);
2075        let mut names = Vec::new();
2076        names.push(self.expect_ident()?);
2077        loop {
2078            if self.consume(&Token::Comma) {
2079                names.push(self.expect_ident()?);
2080                continue;
2081            }
2082            if self.peek_token() == Some(&Token::Ident) {
2083                names.push(self.expect_ident()?);
2084                continue;
2085            }
2086            break;
2087        }
2088        let end = self.last_token_end();
2089        Ok(Stmt::Persistent(names, self.span_from(start, end)))
2090    }
2091
2092    fn try_parse_multi_assign(&mut self) -> Result<Stmt, String> {
2093        if !self.consume(&Token::LBracket) {
2094            return Err("not a multi-assign".into());
2095        }
2096        let start = self.tokens[self.pos.saturating_sub(1)].position;
2097        let mut names = Vec::new();
2098        names.push(self.expect_ident_or_tilde()?);
2099        while self.consume(&Token::Comma) {
2100            names.push(self.expect_ident_or_tilde()?);
2101        }
2102        if !self.consume(&Token::RBracket) {
2103            return Err("expected ']'".into());
2104        }
2105        if !self.consume(&Token::Assign) {
2106            return Err("expected '='".into());
2107        }
2108        let rhs = self.parse_expr().map_err(|e| e.message)?;
2109        let span = self.span_from(start, rhs.span().end);
2110        Ok(Stmt::MultiAssign(names, rhs, false, span))
2111    }
2112
2113    fn parse_qualified_name(&mut self) -> Result<String, String> {
2114        let mut parts = Vec::new();
2115        parts.push(self.expect_ident()?);
2116        while self.consume(&Token::Dot) {
2117            parts.push(self.expect_ident()?);
2118        }
2119        Ok(parts.join("."))
2120    }
2121
2122    fn expect_ident(&mut self) -> Result<String, String> {
2123        match self.next() {
2124            Some(TokenInfo {
2125                token: Token::Ident,
2126                lexeme,
2127                ..
2128            }) => Ok(lexeme),
2129            _ => Err("expected identifier".into()),
2130        }
2131    }
2132
2133    fn expect_ident_or_tilde(&mut self) -> Result<String, String> {
2134        match self.next() {
2135            Some(TokenInfo {
2136                token: Token::Ident,
2137                lexeme,
2138                ..
2139            }) => Ok(lexeme),
2140            Some(TokenInfo {
2141                token: Token::Tilde,
2142                ..
2143            }) => Ok("~".to_string()),
2144            _ => Err("expected identifier or '~'".into()),
2145        }
2146    }
2147
2148    fn peek(&self) -> Option<&TokenInfo> {
2149        self.tokens.get(self.pos)
2150    }
2151
2152    fn current_position(&self) -> usize {
2153        self.peek()
2154            .map(|t| t.position)
2155            .unwrap_or_else(|| self.input.len())
2156    }
2157
2158    fn peek_token(&self) -> Option<&Token> {
2159        self.tokens.get(self.pos).map(|t| &t.token)
2160    }
2161
2162    fn peek_token_at(&self, offset: usize) -> Option<&Token> {
2163        self.tokens.get(self.pos + offset).map(|t| &t.token)
2164    }
2165
2166    fn next(&mut self) -> Option<TokenInfo> {
2167        if self.pos < self.tokens.len() {
2168            let info = self.tokens[self.pos].clone();
2169            self.pos += 1;
2170            Some(info)
2171        } else {
2172            None
2173        }
2174    }
2175
2176    fn consume(&mut self, t: &Token) -> bool {
2177        if self.peek_token() == Some(t) {
2178            self.pos += 1;
2179            true
2180        } else {
2181            false
2182        }
2183    }
2184}
2185
2186fn extract_keyword(expr: &Expr) -> Option<String> {
2187    match expr {
2188        Expr::Ident(s, _) => Some(s.clone()),
2189        Expr::String(s, _) => Some(s.trim_matches(&['"', '\''][..]).to_string()),
2190        _ => None,
2191    }
2192}