Skip to main content

qala_compiler/
parser.rs

1//! the hand-written parser: [`Parser::parse`] turns the lexer's `Vec<Token>`
2//! into an untyped [`Ast`], or the first syntax error. recursive descent for
3//! items and statements, a Pratt (binding-power) loop for expressions.
4//!
5//! fail-fast: returns `Result<Ast, QalaError>` and stops at the first error.
6//! multi-error collection is the type checker's job (Phase 3); the parser does
7//! not synchronize. (the lexer's module doc mentions "Phase 2's parser has
8//! resync points" -- a wording drift; recovery is deferred per CONTEXT.md.)
9//!
10//! a [`depth`] counter on [`Parser`] guards every recursive production against
11//! pathologically nested input. Rust has no tail-call optimization; on WASM a
12//! stack overflow aborts the module. past [`MAX_DEPTH`] the parser returns a
13//! clean [`QalaError::Parse`] instead.
14//!
15//! no `unwrap` / `expect` / `panic!` / `unreachable!` is reachable from a
16//! malformed token stream -- the same discipline the lexer documents; load-
17//! bearing for Phase 6 where the compiler runs in a browser tab.
18
19use crate::ast::*;
20use crate::errors::QalaError;
21use crate::span::Span;
22use crate::token::{Token, TokenKind};
23
24/// the recursion-depth cap, in productions. chosen generously enough that real
25/// programs never approach it (a hand-written program rarely nests more than a
26/// dozen levels deep) and conservatively enough that the native and WASM call
27/// stacks have headroom. 256 frames at the rough budget of a Pratt-recursion
28/// frame fits comfortably inside the default 1 MiB WASM stack and the default
29/// 8 MiB native one.
30pub const MAX_DEPTH: u32 = 256;
31
32/// the parser state.
33///
34/// `tokens` is the lexer's output (assumed to end in [`TokenKind::Eof`], which
35/// `lexer::Lexer::tokenize` guarantees). `pos` indexes into it. `depth` counts
36/// active recursive productions for the stack-overflow guard. `open_delims`
37/// stacks the still-open opener tokens and their spans: on a delimiter
38/// mismatch the innermost (top) is blamed -- its span is the error's span,
39/// per the "blame the opener" rule.
40pub struct Parser<'t> {
41    /// the token stream; always ends in [`TokenKind::Eof`].
42    tokens: &'t [Token],
43    /// the cursor: index into `tokens`. invariant: `pos < tokens.len()`,
44    /// preserved because `advance` stops at `Eof` instead of stepping past it.
45    pos: usize,
46    /// recursion-depth counter. incremented by [`Parser::enter`], decremented
47    /// by [`Parser::exit`]. an error past `MAX_DEPTH` keeps WASM alive.
48    depth: u32,
49    /// the still-open `(` / `[` / `{` delimiters and their opening spans, in
50    /// open order. on a missing / wrong closer the top is blamed, so its span
51    /// is the error's `span` -- the "look at the opener" pattern.
52    open_delims: Vec<(TokenKind, Span)>,
53}
54
55impl<'t> Parser<'t> {
56    /// parse a token stream into an untyped [`Ast`], or return the first
57    /// syntax error.
58    ///
59    /// the input is expected to be a well-formed token stream ending in
60    /// [`TokenKind::Eof`] (what [`crate::lexer::Lexer::tokenize`] produces).
61    /// an empty program (just `[Eof]`) parses to an empty item list with no
62    /// error. fail-fast: the first parse error stops the parse and is
63    /// returned; later errors are not collected.
64    pub fn parse(tokens: &'t [Token]) -> Result<Ast, QalaError> {
65        let mut p = Parser {
66            tokens,
67            pos: 0,
68            depth: 0,
69            open_delims: Vec::new(),
70        };
71        let mut items: Ast = Vec::new();
72        while !matches!(p.peek(), TokenKind::Eof) {
73            items.push(p.parse_item()?);
74        }
75        Ok(items)
76    }
77
78    // ---- cursor --------------------------------------------------------------
79
80    /// the kind at the cursor. the stream always ends in `Eof`, so `pos` is in
81    /// range; this indexes `self.tokens[self.pos]` directly.
82    fn peek(&self) -> &TokenKind {
83        &self.tokens[self.pos].kind
84    }
85
86    /// the span at the cursor.
87    fn peek_span(&self) -> Span {
88        self.tokens[self.pos].span
89    }
90
91    /// the kind one past the cursor, or `Eof` if `pos+1` is out of range.
92    /// useful for the few two-token decisions the grammar needs (e.g. is the
93    /// token after `Ident` a `(` opening a call, or a `{` opening a struct
94    /// literal).
95    #[allow(dead_code)] // wired in by Task 3's struct-literal lookahead
96    fn peek2(&self) -> &TokenKind {
97        self.tokens
98            .get(self.pos + 1)
99            .map(|t| &t.kind)
100            .unwrap_or(&TokenKind::Eof)
101    }
102
103    /// advance past the current token unless it is `Eof`, and return a
104    /// reference to the token just consumed (or, if at `Eof`, the `Eof` token
105    /// itself with the cursor still parked on it). this keeps `pos` in range
106    /// for every subsequent `peek` -- the cursor never steps past `Eof`.
107    fn advance(&mut self) -> &Token {
108        let tok = &self.tokens[self.pos];
109        if !matches!(tok.kind, TokenKind::Eof) {
110            self.pos += 1;
111        }
112        // re-index so the borrow is independent of the local; the borrow
113        // checker is happier this way and the runtime cost is one bounds-check.
114        &self.tokens[self.pos.saturating_sub(1).min(self.tokens.len() - 1)]
115    }
116
117    /// true if the cursor is parked on a token of kind `k`. used for tentative
118    /// look-ahead before deciding which production to enter.
119    fn check(&self, k: &TokenKind) -> bool {
120        self.peek() == k
121    }
122
123    /// consume the current token if it matches `k`, returning whether it did.
124    /// the cheap "if-then-advance" idiom: `if self.eat(&Comma) { ... }`.
125    fn eat(&mut self, k: &TokenKind) -> bool {
126        if self.check(k) {
127            self.advance();
128            true
129        } else {
130            false
131        }
132    }
133
134    /// consume a token of kind `k`, returning its span, or produce an error
135    /// listing `k` as the sole expected kind.
136    fn expect(&mut self, k: &TokenKind) -> Result<Span, QalaError> {
137        if self.check(k) {
138            Ok(self.advance().span)
139        } else {
140            Err(self.unexpected(std::slice::from_ref(k)))
141        }
142    }
143
144    /// consume an [`TokenKind::Ident`], returning its decoded name and span,
145    /// or produce an "expected an identifier" error.
146    fn expect_ident(&mut self) -> Result<(String, Span), QalaError> {
147        match self.peek().clone() {
148            TokenKind::Ident(name) => {
149                let span = self.advance().span;
150                Ok((name, span))
151            }
152            _ => Err(self.unexpected(&[TokenKind::Ident(String::new())])),
153        }
154    }
155
156    // ---- errors --------------------------------------------------------------
157
158    /// build a parse error for "the current token is not legal here".
159    ///
160    /// if the cursor is at `Eof`, this is [`QalaError::UnexpectedEof`] with a
161    /// zero-width span just past the last real token (computed from the
162    /// previous token's `end`, per CONTEXT.md / RESEARCH Pitfall 6 -- not
163    /// `src.len()`). otherwise it is [`QalaError::UnexpectedToken`] with the
164    /// expected set the caller listed and the current token as `found`.
165    fn unexpected(&self, expected: &[TokenKind]) -> QalaError {
166        if matches!(self.peek(), TokenKind::Eof) {
167            // the EOF span is the zero-width point immediately after the last
168            // real token's end (or 0..0 for an empty source). this is more
169            // accurate than the `Eof` token's own span when there is trivia
170            // (whitespace, comments) before EOF.
171            let span = if self.pos > 0 {
172                let prev = &self.tokens[self.pos - 1];
173                Span::new(prev.span.end(), 0)
174            } else {
175                Span::new(0, 0)
176            };
177            QalaError::UnexpectedEof {
178                span,
179                expected: expected.to_vec(),
180            }
181        } else {
182            QalaError::UnexpectedToken {
183                span: self.peek_span(),
184                expected: expected.to_vec(),
185                found: self.peek().clone(),
186            }
187        }
188    }
189
190    // ---- depth guard ---------------------------------------------------------
191
192    /// enter a recursive production: increment `depth`, error past `MAX_DEPTH`.
193    ///
194    /// every recursive production (the Pratt loop, block parsing, pattern
195    /// parsing, match arms, eventually the statement and item productions) is
196    /// wrapped in `enter()? ... exit()` so a `((((...))))`-style input fails
197    /// gracefully instead of overflowing the call stack.
198    fn enter(&mut self) -> Result<(), QalaError> {
199        self.depth = self.depth.saturating_add(1);
200        if self.depth > MAX_DEPTH {
201            return Err(QalaError::Parse {
202                span: self.peek_span(),
203                message: "expression nests too deeply".to_string(),
204            });
205        }
206        Ok(())
207    }
208
209    /// leave a recursive production: decrement `depth`. saturating, so an
210    /// imbalance can never panic.
211    fn exit(&mut self) {
212        self.depth = self.depth.saturating_sub(1);
213    }
214
215    // ---- delimited lists -----------------------------------------------------
216
217    /// parse a `open elem (sep elem)* sep? close` list, returning the elements
218    /// and the closing delimiter's span.
219    ///
220    /// blames the opener on a mismatch: if the closer is wrong or the input
221    /// runs out before the close, the returned error is
222    /// [`QalaError::UnclosedDelimiter`] with the *opener's* span. trailing
223    /// separators are accepted for free because the loop re-checks for the
224    /// closer after every `sep`.
225    ///
226    /// the caller is expected to have peeked the opener; this consumes it.
227    fn delimited_list<T>(
228        &mut self,
229        open: TokenKind,
230        close: TokenKind,
231        sep: TokenKind,
232        mut parse_one: impl FnMut(&mut Self) -> Result<T, QalaError>,
233    ) -> Result<(Vec<T>, Span), QalaError> {
234        let open_span = self.expect(&open)?;
235        self.open_delims.push((open.clone(), open_span));
236        let mut items: Vec<T> = Vec::new();
237        loop {
238            if self.check(&close) {
239                break;
240            }
241            // depth guard the recursion through `parse_one`: a list of nested
242            // lists is depth-equivalent to nested parens.
243            items.push(parse_one(self)?);
244            if !self.eat(&sep) {
245                break;
246            }
247        }
248        if self.check(&close) {
249            let close_span = self.advance().span;
250            self.open_delims.pop();
251            Ok((items, close_span))
252        } else {
253            // the close is wrong or we hit Eof: blame the opener.
254            self.open_delims.pop();
255            Err(QalaError::UnclosedDelimiter {
256                span: open_span,
257                opener: open,
258                found: self.peek().clone(),
259            })
260        }
261    }
262
263    // ---- types ---------------------------------------------------------------
264
265    /// parse a type expression.
266    ///
267    /// type expressions appear in parameter and return positions, struct fields,
268    /// enum variant data, and `let x: T = ...`. the sub-grammar:
269    ///
270    /// - primitive: `i64` `f64` `bool` `str` `byte` `void` (tokens, not idents)
271    /// - named: an identifier; if followed by `<` it is a generic application
272    /// - generic: `Name<T1, T2,>` (trailing comma OK; nested `Foo<Bar<T>>` works
273    ///   because the lexer emits two separate `Gt` tokens, not `>>`)
274    /// - fixed array: `[T; N]` where `N` is an `Int` literal
275    /// - dyn array: `[T]`
276    /// - paren / tuple: `()` zero-tuple, `(T)` a parenthesised type (just `T`),
277    ///   `(T,)` one-tuple, `(T1, T2, ...)` n-tuple; trailing comma accepted
278    /// - function: `fn(T1, T2) -> T3`
279    ///
280    /// the depth guard is wired here too -- types can nest arbitrarily deeply
281    /// (`fn([[i64]]) -> (Foo<Bar>,)`), so pathological input must error cleanly
282    /// rather than overflow the stack.
283    fn parse_type(&mut self) -> Result<TypeExpr, QalaError> {
284        self.enter()?;
285        let result = self.parse_type_inner();
286        self.exit();
287        result
288    }
289
290    fn parse_type_inner(&mut self) -> Result<TypeExpr, QalaError> {
291        match self.peek().clone() {
292            // primitive type names: each is its own token.
293            TokenKind::I64Ty => {
294                let span = self.advance().span;
295                Ok(TypeExpr::Primitive {
296                    kind: PrimType::I64,
297                    span,
298                })
299            }
300            TokenKind::F64Ty => {
301                let span = self.advance().span;
302                Ok(TypeExpr::Primitive {
303                    kind: PrimType::F64,
304                    span,
305                })
306            }
307            TokenKind::BoolTy => {
308                let span = self.advance().span;
309                Ok(TypeExpr::Primitive {
310                    kind: PrimType::Bool,
311                    span,
312                })
313            }
314            TokenKind::StrTy => {
315                let span = self.advance().span;
316                Ok(TypeExpr::Primitive {
317                    kind: PrimType::Str,
318                    span,
319                })
320            }
321            TokenKind::ByteTy => {
322                let span = self.advance().span;
323                Ok(TypeExpr::Primitive {
324                    kind: PrimType::Byte,
325                    span,
326                })
327            }
328            TokenKind::VoidTy => {
329                let span = self.advance().span;
330                Ok(TypeExpr::Primitive {
331                    kind: PrimType::Void,
332                    span,
333                })
334            }
335            // named or generic: `Foo` or `Foo<T1, T2>`. `<` is unambiguous in
336            // type position (we never enter `parse_type` from expression
337            // context), so the lookahead is simple.
338            TokenKind::Ident(name) => {
339                let name_span = self.advance().span;
340                if self.check(&TokenKind::Lt) {
341                    let (args, close) =
342                        self.delimited_list(TokenKind::Lt, TokenKind::Gt, TokenKind::Comma, |p| {
343                            p.parse_type()
344                        })?;
345                    Ok(TypeExpr::Generic {
346                        name,
347                        args,
348                        span: name_span.to(close),
349                    })
350                } else {
351                    Ok(TypeExpr::Named {
352                        name,
353                        span: name_span,
354                    })
355                }
356            }
357            // `[T; N]` or `[T]`. the size form requires an `Int` literal; this
358            // matches the surface syntax (`[i64; 5]`) and defers any const-expr
359            // sizing to a later phase (it is not in v1).
360            TokenKind::LBracket => self.parse_bracket_type(),
361            // `()` / `(T)` / `(T,)` / `(T1, T2, ...)`. distinguishing a paren
362            // from a one-tuple needs the trailing-comma signal, which
363            // `delimited_list` does not expose -- so we hand-roll the loop.
364            TokenKind::LParen => self.parse_paren_type(),
365            // `fn(T1, T2) -> T3`.
366            TokenKind::Fn => self.parse_fn_type(),
367            _ => Err(self.unexpected(&[
368                TokenKind::I64Ty,
369                TokenKind::F64Ty,
370                TokenKind::BoolTy,
371                TokenKind::StrTy,
372                TokenKind::ByteTy,
373                TokenKind::VoidTy,
374                TokenKind::Ident(String::new()),
375                TokenKind::LBracket,
376                TokenKind::LParen,
377                TokenKind::Fn,
378            ])),
379        }
380    }
381
382    /// `[T; N]` (fixed array) or `[T]` (dyn array).
383    fn parse_bracket_type(&mut self) -> Result<TypeExpr, QalaError> {
384        let open_span = self.expect(&TokenKind::LBracket)?;
385        self.open_delims.push((TokenKind::LBracket, open_span));
386        let elem = self.parse_type()?;
387        if self.eat(&TokenKind::Semi) {
388            // `[T; N]`: read an `Int` literal as the size.
389            let size = match self.peek().clone() {
390                TokenKind::Int(v) if v >= 0 => {
391                    self.advance();
392                    v as u64
393                }
394                _ => {
395                    self.open_delims.pop();
396                    return Err(self.unexpected(&[TokenKind::Int(0)]));
397                }
398            };
399            if !self.check(&TokenKind::RBracket) {
400                self.open_delims.pop();
401                return Err(QalaError::UnclosedDelimiter {
402                    span: open_span,
403                    opener: TokenKind::LBracket,
404                    found: self.peek().clone(),
405                });
406            }
407            let close = self.advance().span;
408            self.open_delims.pop();
409            Ok(TypeExpr::Array {
410                elem: Box::new(elem),
411                size,
412                span: open_span.to(close),
413            })
414        } else {
415            // `[T]`: dyn array.
416            if !self.check(&TokenKind::RBracket) {
417                self.open_delims.pop();
418                return Err(QalaError::UnclosedDelimiter {
419                    span: open_span,
420                    opener: TokenKind::LBracket,
421                    found: self.peek().clone(),
422                });
423            }
424            let close = self.advance().span;
425            self.open_delims.pop();
426            Ok(TypeExpr::DynArray {
427                elem: Box::new(elem),
428                span: open_span.to(close),
429            })
430        }
431    }
432
433    /// `()` zero-tuple, `(T)` parenthesised (just `T`), `(T,)` one-tuple,
434    /// `(T1, T2, ...)` n-tuple. mirrors the expression-side `parse_paren_or_tuple`
435    /// so the trailing comma rule is identical in type position.
436    fn parse_paren_type(&mut self) -> Result<TypeExpr, QalaError> {
437        let open_span = self.expect(&TokenKind::LParen)?;
438        self.open_delims.push((TokenKind::LParen, open_span));
439        // empty `()`: zero-tuple. faithful to the expression side.
440        if self.check(&TokenKind::RParen) {
441            let close = self.advance().span;
442            self.open_delims.pop();
443            return Ok(TypeExpr::Tuple {
444                elems: Vec::new(),
445                span: open_span.to(close),
446            });
447        }
448        let first = self.parse_type()?;
449        // `(T)`: a parenthesised type. the inner type's span is correct on its
450        // own; we discard the parens, the same way the expression side
451        // distinguishes `Paren` from `Tuple` only when a comma is present.
452        if self.check(&TokenKind::RParen) {
453            self.advance();
454            self.open_delims.pop();
455            return Ok(first);
456        }
457        // there must be a `,` here -- `(T,)` or `(T1, T2, ...)`.
458        if !self.eat(&TokenKind::Comma) {
459            self.open_delims.pop();
460            return Err(QalaError::UnclosedDelimiter {
461                span: open_span,
462                opener: TokenKind::LParen,
463                found: self.peek().clone(),
464            });
465        }
466        let mut elems = vec![first];
467        loop {
468            if self.check(&TokenKind::RParen) {
469                break;
470            }
471            elems.push(self.parse_type()?);
472            if !self.eat(&TokenKind::Comma) {
473                break;
474            }
475        }
476        if !self.check(&TokenKind::RParen) {
477            self.open_delims.pop();
478            return Err(QalaError::UnclosedDelimiter {
479                span: open_span,
480                opener: TokenKind::LParen,
481                found: self.peek().clone(),
482            });
483        }
484        let close = self.advance().span;
485        self.open_delims.pop();
486        Ok(TypeExpr::Tuple {
487            elems,
488            span: open_span.to(close),
489        })
490    }
491
492    /// `fn(T1, T2) -> T3`. the param list uses `delimited_list` (trailing comma
493    /// accepted); a return type is required (no implicit `void` here -- a
494    /// function type without an arrow does not parse).
495    fn parse_fn_type(&mut self) -> Result<TypeExpr, QalaError> {
496        let fn_span = self.expect(&TokenKind::Fn)?;
497        let (params, _close) = self.delimited_list(
498            TokenKind::LParen,
499            TokenKind::RParen,
500            TokenKind::Comma,
501            |p| p.parse_type(),
502        )?;
503        self.expect(&TokenKind::Arrow)?;
504        let ret = self.parse_type()?;
505        let ret_span = ret.span();
506        Ok(TypeExpr::Fn {
507            params,
508            ret: Box::new(ret),
509            span: fn_span.to(ret_span),
510        })
511    }
512
513    // ---- items ---------------------------------------------------------------
514
515    /// parse one top-level item. dispatches on the leading keyword token.
516    fn parse_item(&mut self) -> Result<Item, QalaError> {
517        match self.peek() {
518            TokenKind::Fn => self.parse_fn_item(),
519            TokenKind::Struct => Ok(Item::Struct(self.parse_struct_item()?)),
520            TokenKind::Enum => Ok(Item::Enum(self.parse_enum_item()?)),
521            TokenKind::Interface => Ok(Item::Interface(self.parse_interface_item()?)),
522            _ => Err(self.unexpected(&[
523                TokenKind::Fn,
524                TokenKind::Struct,
525                TokenKind::Enum,
526                TokenKind::Interface,
527            ])),
528        }
529    }
530
531    /// `fn NAME(PARAMS) [-> RET] [is EFFECT] { BODY }`, or
532    /// `fn TYPE.METHOD(self, PARAMS) [-> RET] [is EFFECT] { BODY }` for a
533    /// method definition. an empty `{}` body is legal.
534    fn parse_fn_item(&mut self) -> Result<Item, QalaError> {
535        let fn_span = self.expect(&TokenKind::Fn)?;
536        let (first_name, _first_span) = self.expect_ident()?;
537        // `fn Type.method` vs `fn name`: a `.` after the first ident means the
538        // first ident is the receiver type's name.
539        let (type_name, name) = if self.eat(&TokenKind::Dot) {
540            let (method, _) = self.expect_ident()?;
541            (Some(first_name), method)
542        } else {
543            (None, first_name)
544        };
545        let params = self.parse_fn_params(type_name.is_some())?;
546        let ret_ty = if self.eat(&TokenKind::Arrow) {
547            Some(self.parse_type()?)
548        } else {
549            None
550        };
551        let effect = self.parse_optional_effect()?.map(|(e, _span)| e);
552        let body = self.parse_block()?;
553        let span = fn_span.to(body.span);
554        Ok(Item::Fn(FnDecl {
555            type_name,
556            name,
557            params,
558            ret_ty,
559            effect,
560            body,
561            span,
562        }))
563    }
564
565    /// parse a function or method parameter list: `( [self,] (name: TY [= DEFAULT])* )`.
566    ///
567    /// `self` is legal only as the first parameter, and only in a method
568    /// (`is_method = true`) context. a free function with a `self` parameter
569    /// errors at the `self` token, with a message that names the rule. every
570    /// other parameter requires `name: TYPE` and may carry `= default_expr`.
571    fn parse_fn_params(&mut self, is_method: bool) -> Result<Vec<Param>, QalaError> {
572        let open_span = self.expect(&TokenKind::LParen)?;
573        self.open_delims.push((TokenKind::LParen, open_span));
574        let mut params: Vec<Param> = Vec::new();
575        // handle the first parameter specially so `self` can be recognised
576        // (it is only legal in that position, and only for methods).
577        if !self.check(&TokenKind::RParen) {
578            params.push(self.parse_fn_param(true, is_method)?);
579            while self.eat(&TokenKind::Comma) {
580                if self.check(&TokenKind::RParen) {
581                    break;
582                }
583                params.push(self.parse_fn_param(false, is_method)?);
584            }
585        }
586        if !self.check(&TokenKind::RParen) {
587            self.open_delims.pop();
588            return Err(QalaError::UnclosedDelimiter {
589                span: open_span,
590                opener: TokenKind::LParen,
591                found: self.peek().clone(),
592            });
593        }
594        self.advance();
595        self.open_delims.pop();
596        Ok(params)
597    }
598
599    /// parse one parameter. `is_first` controls whether `self` is even
600    /// considered; `is_method` controls whether `self` is accepted there.
601    /// any `SelfKw` outside `(is_first && is_method)` is a hard parse error.
602    fn parse_fn_param(&mut self, is_first: bool, is_method: bool) -> Result<Param, QalaError> {
603        if self.check(&TokenKind::SelfKw) {
604            let span = self.advance().span;
605            if is_first && is_method {
606                return Ok(Param {
607                    is_self: true,
608                    name: "self".to_string(),
609                    ty: None,
610                    default: None,
611                    span,
612                });
613            }
614            // a free fn with `self`, or `self` anywhere but the first slot of a
615            // method: reject with a span on the `self` token and a message that
616            // names the rule.
617            return Err(QalaError::Parse {
618                span,
619                message: "`self` is only legal as the first parameter of a method".to_string(),
620            });
621        }
622        let (name, name_span) = self.expect_ident()?;
623        self.expect(&TokenKind::Colon)?;
624        let ty = self.parse_type()?;
625        let ty_span = ty.span();
626        let default = if self.eat(&TokenKind::Eq) {
627            Some(self.parse_expr()?)
628        } else {
629            None
630        };
631        let end_span = match &default {
632            Some(e) => e.span(),
633            None => ty_span,
634        };
635        Ok(Param {
636            is_self: false,
637            name,
638            ty: Some(ty),
639            default,
640            span: name_span.to(end_span),
641        })
642    }
643
644    /// `is pure|io|alloc|panic`, or nothing. consumed only when `is` is
645    /// present; if `is` is followed by a non-effect keyword, that is an error.
646    /// returns the effect and the span of the effect keyword so callers can
647    /// compute a span that covers the full signature including the annotation.
648    fn parse_optional_effect(&mut self) -> Result<Option<(Effect, Span)>, QalaError> {
649        if !self.eat(&TokenKind::Is) {
650            return Ok(None);
651        }
652        let effect = match self.peek() {
653            TokenKind::Pure => Effect::Pure,
654            TokenKind::Io => Effect::Io,
655            TokenKind::Alloc => Effect::Alloc,
656            TokenKind::Panic => Effect::Panic,
657            _ => {
658                return Err(self.unexpected(&[
659                    TokenKind::Pure,
660                    TokenKind::Io,
661                    TokenKind::Alloc,
662                    TokenKind::Panic,
663                ]));
664            }
665        };
666        let kw_span = self.advance().span;
667        Ok(Some((effect, kw_span)))
668    }
669
670    /// `struct NAME { (name: TYPE,)* }`. trailing comma in the field list is
671    /// accepted for free via `delimited_list`.
672    fn parse_struct_item(&mut self) -> Result<StructDecl, QalaError> {
673        let kw_span = self.expect(&TokenKind::Struct)?;
674        let (name, _) = self.expect_ident()?;
675        let (fields, close) = self.delimited_list(
676            TokenKind::LBrace,
677            TokenKind::RBrace,
678            TokenKind::Comma,
679            |p| {
680                let (fname, fname_span) = p.expect_ident()?;
681                p.expect(&TokenKind::Colon)?;
682                let fty = p.parse_type()?;
683                let fty_span = fty.span();
684                Ok(Field {
685                    name: fname,
686                    ty: fty,
687                    span: fname_span.to(fty_span),
688                })
689            },
690        )?;
691        Ok(StructDecl {
692            name,
693            fields,
694            span: kw_span.to(close),
695        })
696    }
697
698    /// `enum NAME { (VARIANT,)* }` where `VARIANT` is `Name` (no data) or
699    /// `Name(T1, T2, ...)` (data-carrying). trailing commas in both the
700    /// variant list and any variant's field list are accepted.
701    fn parse_enum_item(&mut self) -> Result<EnumDecl, QalaError> {
702        let kw_span = self.expect(&TokenKind::Enum)?;
703        let (name, _) = self.expect_ident()?;
704        let (variants, close) = self.delimited_list(
705            TokenKind::LBrace,
706            TokenKind::RBrace,
707            TokenKind::Comma,
708            |p| {
709                let (vname, vname_span) = p.expect_ident()?;
710                if p.check(&TokenKind::LParen) {
711                    let (fields, close) = p.delimited_list(
712                        TokenKind::LParen,
713                        TokenKind::RParen,
714                        TokenKind::Comma,
715                        |q| q.parse_type(),
716                    )?;
717                    Ok(Variant {
718                        name: vname,
719                        fields,
720                        span: vname_span.to(close),
721                    })
722                } else {
723                    Ok(Variant {
724                        name: vname,
725                        fields: Vec::new(),
726                        span: vname_span,
727                    })
728                }
729            },
730        )?;
731        Ok(EnumDecl {
732            name,
733            variants,
734            span: kw_span.to(close),
735        })
736    }
737
738    /// `interface NAME { (fn NAME(PARAMS) [-> RET] [is EFFECT],)* }`. interface
739    /// methods accept `self` as the first parameter, matching the spec's
740    /// `interface Printable { fn to_string(self) -> str }`; the body is just a
741    /// signature, with no `{ ... }` block to follow.
742    fn parse_interface_item(&mut self) -> Result<InterfaceDecl, QalaError> {
743        let kw_span = self.expect(&TokenKind::Interface)?;
744        let (name, _) = self.expect_ident()?;
745        let (methods, close) = self.delimited_list(
746            TokenKind::LBrace,
747            TokenKind::RBrace,
748            TokenKind::Comma,
749            |p| {
750                let fn_span = p.expect(&TokenKind::Fn)?;
751                let (mname, _mname_span) = p.expect_ident()?;
752                // interface methods can take `self` as the first param.
753                let params = p.parse_fn_params(true)?;
754                let ret_ty = if p.eat(&TokenKind::Arrow) {
755                    Some(p.parse_type()?)
756                } else {
757                    None
758                };
759                let effect_with_span = p.parse_optional_effect()?;
760                // span: `fn` keyword to the end of the last consumed token.
761                // priority: return type > effect keyword > fn keyword (fallback
762                // when the signature has only params and no return or effect).
763                let end = match (&ret_ty, &effect_with_span) {
764                    (Some(ret), _) => ret.span(),
765                    (None, Some((_e, effect_span))) => *effect_span,
766                    (None, None) => fn_span,
767                };
768                let effect = effect_with_span.map(|(e, _span)| e);
769                Ok(MethodSig {
770                    name: mname,
771                    params,
772                    ret_ty,
773                    effect,
774                    span: fn_span.to(end),
775                })
776            },
777        )?;
778        Ok(InterfaceDecl {
779            name,
780            methods,
781            span: kw_span.to(close),
782        })
783    }
784
785    // ---- expressions (Pratt) -------------------------------------------------
786    //
787    // a Pratt (precedence-climbing) loop driven by three binding-power lookup
788    // functions: [`postfix_bp`], [`prefix_bp`], [`infix_bp`]. each returns a
789    // pair of binding powers (`(lbp, rbp)` for infix, `(lbp, ())` for postfix,
790    // `((), rbp)` for prefix) that encode precedence and associativity. the
791    // technique is matklad's "simple but powerful Pratt parsing"
792    // (matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html);
793    // see `02-RESEARCH.md` Patterns 2-4.
794
795    /// parse a single expression at the loosest precedence. a thin alias for
796    /// `expr_bp(0, ExprCtx::Normal)`; statements call this to read the leaves
797    /// of the grammar.
798    pub fn parse_expr(&mut self) -> Result<Expr, QalaError> {
799        self.expr_bp(0, ExprCtx::Normal)
800    }
801
802    /// the Pratt loop.
803    ///
804    /// at the top, parse a primary (a literal, an identifier-or-struct-literal,
805    /// a paren/tuple, an array literal, a block, an interpolation, a `match`,
806    /// or a `!` / `-` / `comptime` prefix). then in a loop, peek the next
807    /// token: if it has a postfix binding power >= `min_bp`, fold it in as a
808    /// postfix; if it has an infix binding power >= `min_bp`, consume the
809    /// operator and recurse with the right-hand binding power; else stop and
810    /// return what is built so far.
811    ///
812    /// `ctx` threads the "struct literals are not allowed at the start of an
813    /// expression here" rule through the recursion: the head of `match`, and
814    /// later `if`/`while`/`for`, must not parse `Name { ... }` as a struct
815    /// literal -- per RESEARCH Pitfall 3. `ExprCtx::Normal` allows them;
816    /// `ExprCtx::NoStructLiteral` rejects them at the primary.
817    fn expr_bp(&mut self, min_bp: u8, ctx: ExprCtx) -> Result<Expr, QalaError> {
818        self.enter()?;
819        let result = self.expr_bp_inner(min_bp, ctx);
820        self.exit();
821        result
822    }
823
824    fn expr_bp_inner(&mut self, min_bp: u8, ctx: ExprCtx) -> Result<Expr, QalaError> {
825        // --- primary --------------------------------------------------------
826        let mut lhs = self.parse_primary(ctx)?;
827
828        // --- postfix and infix loop -----------------------------------------
829        loop {
830            let op = self.peek().clone();
831            // postfix? if so it binds at one tier, no rbp; the operator is
832            // applied to lhs and the loop continues from the result.
833            if let Some((lbp, ())) = postfix_bp(&op) {
834                if lbp < min_bp {
835                    break;
836                }
837                lhs = self.fold_postfix(lhs, &op)?;
838                continue;
839            }
840            // infix? consume the operator and recurse for the rhs. ctx is
841            // threaded down to the rhs so that an expression like `0..n` in a
842            // no-struct-literal head (the iterable of a `for`, say) does not
843            // pick up a trailing `{` as a struct-literal body for `n`.
844            if let Some((lbp, rbp)) = infix_bp(&op) {
845                if lbp < min_bp {
846                    break;
847                }
848                let op_tok = self.advance().clone();
849                lhs = self.fold_infix(lhs, op_tok, rbp, ctx)?;
850                continue;
851            }
852            break;
853        }
854        Ok(lhs)
855    }
856
857    /// parse the leftmost piece of an expression: a literal, an identifier or
858    /// struct literal, a parenthesised expression / tuple, an array literal /
859    /// repeat, a block, a string interpolation, a `match`, or a `!` / `-` /
860    /// `comptime` prefix.
861    fn parse_primary(&mut self, ctx: ExprCtx) -> Result<Expr, QalaError> {
862        match self.peek().clone() {
863            // literals (each carries its decoded value and span).
864            TokenKind::Int(v) => {
865                let span = self.advance().span;
866                Ok(Expr::Int { value: v, span })
867            }
868            TokenKind::Float(v) => {
869                let span = self.advance().span;
870                Ok(Expr::Float { value: v, span })
871            }
872            TokenKind::Byte(v) => {
873                let span = self.advance().span;
874                Ok(Expr::Byte { value: v, span })
875            }
876            TokenKind::Str(s) => {
877                let span = self.advance().span;
878                Ok(Expr::Str { value: s, span })
879            }
880            TokenKind::True => {
881                let span = self.advance().span;
882                Ok(Expr::Bool { value: true, span })
883            }
884            TokenKind::False => {
885                let span = self.advance().span;
886                Ok(Expr::Bool { value: false, span })
887            }
888            // identifier: either a bare reference, or, if followed by `{` and
889            // struct literals are allowed in this position, a struct literal.
890            // primitive type names are tokens, not idents, and are not legal
891            // here -- they only appear in type position.
892            TokenKind::Ident(name) => self.parse_ident_or_struct_literal(name, ctx),
893            // parenthesised: `( inner )`, `( e1, e2 )` (tuple), `( e, )` (one-tuple),
894            // or `()` (empty tuple as a unit value).
895            TokenKind::LParen => self.parse_paren_or_tuple(),
896            // array literal: `[ e1, e2, ... ]` or repeat `[ v; n ]`.
897            TokenKind::LBracket => self.parse_array_literal(),
898            // block expression: `{ stmt*; trailing? }`. value-producing.
899            TokenKind::LBrace => self.parse_block_expr(),
900            // interpolated string: re-assemble the lexer's StrStart / InterpStart
901            // / InterpEnd / StrMid / StrEnd grouping into `Expr::Interpolation`.
902            TokenKind::StrStart(_) => self.parse_interpolation(),
903            // match expression.
904            TokenKind::Match => self.parse_match(),
905            // `self` in expression position: produces `Expr::Ident { name: "self" }`.
906            // the resolver (Phase 3) checks that it only occurs inside a method body;
907            // the parser just needs to not reject it with UnexpectedToken.
908            TokenKind::SelfKw => {
909                let span = self.advance().span;
910                Ok(Expr::Ident {
911                    name: "self".to_string(),
912                    span,
913                })
914            }
915            // unary prefixes: `!`, `-`, `comptime`.
916            TokenKind::Bang | TokenKind::Minus | TokenKind::Comptime => self.parse_prefix(),
917            // anything else is not a valid start of an expression.
918            _ => Err(self.unexpected(&[
919                TokenKind::Int(0),
920                TokenKind::Float(0.0),
921                TokenKind::Byte(0),
922                TokenKind::Str(String::new()),
923                TokenKind::StrStart(String::new()),
924                TokenKind::True,
925                TokenKind::False,
926                TokenKind::Ident(String::new()),
927                TokenKind::LParen,
928                TokenKind::LBracket,
929                TokenKind::LBrace,
930                TokenKind::Match,
931                TokenKind::Bang,
932                TokenKind::Minus,
933                TokenKind::Comptime,
934            ])),
935        }
936    }
937
938    /// `name`, or `Name { field: e, ... }` when `{` follows and struct
939    /// literals are allowed in this context.
940    ///
941    /// the "no struct literals at the head of `match` / `if` / `while` /
942    /// `for`" rule is RESEARCH Pitfall 3 -- without it, `match s { Circle(r)
943    /// => ... }` would read `s { Circle(r) => ... }` as a struct literal with
944    /// a `Circle(r)` field, which is nonsense. when `ctx` is `NoStructLiteral`
945    /// we read just the ident; the trailing `{` will start the match-arm
946    /// block, not a literal body.
947    fn parse_ident_or_struct_literal(
948        &mut self,
949        name: String,
950        ctx: ExprCtx,
951    ) -> Result<Expr, QalaError> {
952        let name_span = self.advance().span;
953        if matches!(ctx, ExprCtx::Normal) && self.check(&TokenKind::LBrace) {
954            let (fields, close) = self.delimited_list(
955                TokenKind::LBrace,
956                TokenKind::RBrace,
957                TokenKind::Comma,
958                |p| p.parse_field_init(),
959            )?;
960            Ok(Expr::StructLit {
961                name,
962                fields,
963                span: name_span.to(close),
964            })
965        } else {
966            Ok(Expr::Ident {
967                name,
968                span: name_span,
969            })
970        }
971    }
972
973    /// `name: value` inside a struct literal body.
974    fn parse_field_init(&mut self) -> Result<FieldInit, QalaError> {
975        let (name, name_span) = self.expect_ident()?;
976        self.expect(&TokenKind::Colon)?;
977        let value = self.parse_expr()?;
978        let span = name_span.to(value.span());
979        Ok(FieldInit { name, value, span })
980    }
981
982    /// `( inner )` (paren), `( e1, e2, ... )` (tuple), `( e, )` (one-tuple),
983    /// or `()` (empty tuple / unit).
984    ///
985    /// the rule: zero elements (`()`) is a zero-tuple; one element with no
986    /// trailing comma is a paren; one element with a trailing comma is a
987    /// one-tuple; two or more elements (with or without trailing comma) is a
988    /// tuple. faithful to the surface syntax.
989    fn parse_paren_or_tuple(&mut self) -> Result<Expr, QalaError> {
990        let open_span = self.expect(&TokenKind::LParen)?;
991        self.open_delims.push((TokenKind::LParen, open_span));
992        // empty `()`: zero-tuple.
993        if self.check(&TokenKind::RParen) {
994            let close = self.advance().span;
995            self.open_delims.pop();
996            return Ok(Expr::Tuple {
997                elems: Vec::new(),
998                span: open_span.to(close),
999            });
1000        }
1001        let first = self.parse_expr()?;
1002        // `( expr )`: paren (no comma seen).
1003        if self.check(&TokenKind::RParen) {
1004            let close = self.advance().span;
1005            self.open_delims.pop();
1006            return Ok(Expr::Paren {
1007                inner: Box::new(first),
1008                span: open_span.to(close),
1009            });
1010        }
1011        // there must be a `,` here -- either `( e, )` or `( e, ... )`.
1012        if !self.eat(&TokenKind::Comma) {
1013            self.open_delims.pop();
1014            return Err(QalaError::UnclosedDelimiter {
1015                span: open_span,
1016                opener: TokenKind::LParen,
1017                found: self.peek().clone(),
1018            });
1019        }
1020        let mut elems = vec![first];
1021        loop {
1022            if self.check(&TokenKind::RParen) {
1023                break;
1024            }
1025            elems.push(self.parse_expr()?);
1026            if !self.eat(&TokenKind::Comma) {
1027                break;
1028            }
1029        }
1030        if !self.check(&TokenKind::RParen) {
1031            self.open_delims.pop();
1032            return Err(QalaError::UnclosedDelimiter {
1033                span: open_span,
1034                opener: TokenKind::LParen,
1035                found: self.peek().clone(),
1036            });
1037        }
1038        let close = self.advance().span;
1039        self.open_delims.pop();
1040        Ok(Expr::Tuple {
1041            elems,
1042            span: open_span.to(close),
1043        })
1044    }
1045
1046    /// `[ e1, e2, ... ]` (array literal) or `[ value; count ]` (repeat).
1047    ///
1048    /// the first element is parsed eagerly; if a `;` follows, the second
1049    /// expression is the count and the form is a repeat. otherwise it is a
1050    /// comma list.
1051    fn parse_array_literal(&mut self) -> Result<Expr, QalaError> {
1052        let open_span = self.expect(&TokenKind::LBracket)?;
1053        self.open_delims.push((TokenKind::LBracket, open_span));
1054        // empty `[]`: zero-element array literal.
1055        if self.check(&TokenKind::RBracket) {
1056            let close = self.advance().span;
1057            self.open_delims.pop();
1058            return Ok(Expr::ArrayLit {
1059                elems: Vec::new(),
1060                span: open_span.to(close),
1061            });
1062        }
1063        let first = self.parse_expr()?;
1064        // `[ value; count ]`.
1065        if self.eat(&TokenKind::Semi) {
1066            let count = self.parse_expr()?;
1067            if !self.check(&TokenKind::RBracket) {
1068                self.open_delims.pop();
1069                return Err(QalaError::UnclosedDelimiter {
1070                    span: open_span,
1071                    opener: TokenKind::LBracket,
1072                    found: self.peek().clone(),
1073                });
1074            }
1075            let close = self.advance().span;
1076            self.open_delims.pop();
1077            return Ok(Expr::ArrayRepeat {
1078                value: Box::new(first),
1079                count: Box::new(count),
1080                span: open_span.to(close),
1081            });
1082        }
1083        // `[ e1, e2, ... ]`.
1084        let mut elems = vec![first];
1085        if self.eat(&TokenKind::Comma) {
1086            loop {
1087                if self.check(&TokenKind::RBracket) {
1088                    break;
1089                }
1090                elems.push(self.parse_expr()?);
1091                if !self.eat(&TokenKind::Comma) {
1092                    break;
1093                }
1094            }
1095        }
1096        if !self.check(&TokenKind::RBracket) {
1097            self.open_delims.pop();
1098            return Err(QalaError::UnclosedDelimiter {
1099                span: open_span,
1100                opener: TokenKind::LBracket,
1101                found: self.peek().clone(),
1102            });
1103        }
1104        let close = self.advance().span;
1105        self.open_delims.pop();
1106        Ok(Expr::ArrayLit {
1107            elems,
1108            span: open_span.to(close),
1109        })
1110    }
1111
1112    /// `{ ... }` used as an expression -- a value-producing block.
1113    fn parse_block_expr(&mut self) -> Result<Expr, QalaError> {
1114        let block = self.parse_block()?;
1115        let span = block.span;
1116        Ok(Expr::Block { block, span })
1117    }
1118
1119    /// `{ stmt stmt ... trailing? }`.
1120    ///
1121    /// statements are NOT `;`-separated in the source -- the lexer emits no
1122    /// `Newline` token, so the bundled examples like `let f = open(path)\ndefer
1123    /// close(f)\n...` separate statements purely by the next statement-head
1124    /// keyword or `}`. resolution: each iteration, if the cursor is on a
1125    /// statement-head keyword (`let`/`if`/`while`/`for`/`return`/`break`/
1126    /// `continue`/`defer`), dispatch to `parse_stmt`; otherwise parse an
1127    /// expression and decide -- a following `;` makes it an expression-statement,
1128    /// `}` makes it the block's trailing value, anything else (the start of
1129    /// another statement) makes it an expression-statement with its value
1130    /// discarded.
1131    ///
1132    /// the depth guard is wired here so `{ { { { ... } } } }`-style block
1133    /// nesting fails cleanly instead of overflowing the call stack.
1134    fn parse_block(&mut self) -> Result<Block, QalaError> {
1135        self.enter()?;
1136        let result = self.parse_block_inner();
1137        self.exit();
1138        result
1139    }
1140
1141    fn parse_block_inner(&mut self) -> Result<Block, QalaError> {
1142        let open_span = self.expect(&TokenKind::LBrace)?;
1143        self.open_delims.push((TokenKind::LBrace, open_span));
1144        let mut stmts: Vec<Stmt> = Vec::new();
1145        let mut value: Option<Box<Expr>> = None;
1146        loop {
1147            if self.check(&TokenKind::RBrace) {
1148                break;
1149            }
1150            if is_stmt_head(self.peek()) {
1151                // a keyword-headed statement. an optional `;` after it is
1152                // harmless (real programs do not use one, but the grammar
1153                // accepts it for symmetry with expression-statements).
1154                let stmt = self.parse_stmt()?;
1155                stmts.push(stmt);
1156                self.eat(&TokenKind::Semi);
1157                continue;
1158            }
1159            // no statement-head keyword: an expression. it is either an
1160            // expression-statement (value discarded) or the block's trailing
1161            // value, distinguished by what follows.
1162            let expr = self.parse_expr()?;
1163            let expr_span = expr.span();
1164            if self.eat(&TokenKind::Semi) {
1165                // `expr;` -- definitely a statement.
1166                stmts.push(Stmt::Expr {
1167                    expr,
1168                    span: expr_span,
1169                });
1170                continue;
1171            }
1172            if self.check(&TokenKind::RBrace) {
1173                // last expression with no `;` and no following statement: the
1174                // block's trailing value.
1175                value = Some(Box::new(expr));
1176                break;
1177            }
1178            // the next token starts another statement (a keyword head, or
1179            // another expression). discard this expression's value and keep
1180            // going.
1181            stmts.push(Stmt::Expr {
1182                expr,
1183                span: expr_span,
1184            });
1185        }
1186        if !self.check(&TokenKind::RBrace) {
1187            self.open_delims.pop();
1188            return Err(QalaError::UnclosedDelimiter {
1189                span: open_span,
1190                opener: TokenKind::LBrace,
1191                found: self.peek().clone(),
1192            });
1193        }
1194        let close = self.advance().span;
1195        self.open_delims.pop();
1196        Ok(Block {
1197            stmts,
1198            value,
1199            span: open_span.to(close),
1200        })
1201    }
1202
1203    // ---- statements ----------------------------------------------------------
1204
1205    /// parse a keyword-headed statement: `let`, `if`, `while`, `for`,
1206    /// `return`, `break`, `continue`, or `defer`. an expression-statement is
1207    /// handled by `parse_block` directly (it needs to peek the token after the
1208    /// expression to decide between stmt and trailing value).
1209    ///
1210    /// the depth guard wraps the entire statement parse: `if cond { if cond {
1211    /// if cond { ... } } }` nests through `parse_stmt`/`parse_block` calls and
1212    /// must error cleanly at the limit, not overflow.
1213    fn parse_stmt(&mut self) -> Result<Stmt, QalaError> {
1214        self.enter()?;
1215        let result = self.parse_stmt_inner();
1216        self.exit();
1217        result
1218    }
1219
1220    fn parse_stmt_inner(&mut self) -> Result<Stmt, QalaError> {
1221        match self.peek() {
1222            TokenKind::Let => self.parse_let_stmt(),
1223            TokenKind::If => self.parse_if_stmt(),
1224            TokenKind::While => self.parse_while_stmt(),
1225            TokenKind::For => self.parse_for_stmt(),
1226            TokenKind::Return => self.parse_return_stmt(),
1227            TokenKind::Break => {
1228                let span = self.advance().span;
1229                Ok(Stmt::Break { span })
1230            }
1231            TokenKind::Continue => {
1232                let span = self.advance().span;
1233                Ok(Stmt::Continue { span })
1234            }
1235            TokenKind::Defer => self.parse_defer_stmt(),
1236            // `parse_block` filters non-statement-heads out before calling
1237            // `parse_stmt`, but the dispatch must be exhaustive.
1238            _ => Err(self.unexpected(&[
1239                TokenKind::Let,
1240                TokenKind::If,
1241                TokenKind::While,
1242                TokenKind::For,
1243                TokenKind::Return,
1244                TokenKind::Break,
1245                TokenKind::Continue,
1246                TokenKind::Defer,
1247            ])),
1248        }
1249    }
1250
1251    /// `let [mut] NAME [: TYPE] = EXPR`. the initializer is required (no
1252    /// uninitialised bindings in Qala v1); the type is inferred from `init`
1253    /// when omitted.
1254    fn parse_let_stmt(&mut self) -> Result<Stmt, QalaError> {
1255        let kw_span = self.expect(&TokenKind::Let)?;
1256        let is_mut = self.eat(&TokenKind::Mut);
1257        let (name, _name_span) = self.expect_ident()?;
1258        let ty = if self.eat(&TokenKind::Colon) {
1259            Some(self.parse_type()?)
1260        } else {
1261            None
1262        };
1263        // `=` is required: `let x: i64` with no `= ...` errors here.
1264        self.expect(&TokenKind::Eq)?;
1265        let init = self.parse_expr()?;
1266        let span = kw_span.to(init.span());
1267        Ok(Stmt::Let {
1268            is_mut,
1269            name,
1270            ty,
1271            init,
1272            span,
1273        })
1274    }
1275
1276    /// `if COND { ... } [else if COND { ... }]* [else { ... }]`. `if`/`else`
1277    /// is a statement in v1, not an expression; there is no `let x = if c { a
1278    /// } else { b }`. the head expression is parsed in no-struct-literal mode
1279    /// so the trailing `{` is the `then` block, not a struct-literal body.
1280    fn parse_if_stmt(&mut self) -> Result<Stmt, QalaError> {
1281        let kw_span = self.expect(&TokenKind::If)?;
1282        let cond = self.parse_expr_no_struct()?;
1283        let then_block = self.parse_block()?;
1284        let mut tail_span = then_block.span;
1285        let else_branch = if self.eat(&TokenKind::Else) {
1286            if self.check(&TokenKind::If) {
1287                // `else if ...` -- recurse via `parse_stmt`. the inner stmt is
1288                // always another `Stmt::If`, by construction.
1289                let inner = self.parse_stmt()?;
1290                tail_span = tail_span.to(inner.span());
1291                Some(ElseBranch::If(Box::new(inner)))
1292            } else {
1293                let block = self.parse_block()?;
1294                tail_span = tail_span.to(block.span);
1295                Some(ElseBranch::Block(block))
1296            }
1297        } else {
1298            None
1299        };
1300        Ok(Stmt::If {
1301            cond,
1302            then_block,
1303            else_branch,
1304            span: kw_span.to(tail_span),
1305        })
1306    }
1307
1308    /// `while COND { ... }`. the head is parsed in no-struct-literal mode.
1309    fn parse_while_stmt(&mut self) -> Result<Stmt, QalaError> {
1310        let kw_span = self.expect(&TokenKind::While)?;
1311        let cond = self.parse_expr_no_struct()?;
1312        let body = self.parse_block()?;
1313        let span = kw_span.to(body.span);
1314        Ok(Stmt::While { cond, body, span })
1315    }
1316
1317    /// `for IDENT in EXPR { ... }`. `EXPR` is any expression that yields an
1318    /// iterable -- a range like `0..15` is just one kind; an identifier like
1319    /// `shapes` is another. parsed in no-struct-literal mode for the same
1320    /// reason as `if`/`while`.
1321    fn parse_for_stmt(&mut self) -> Result<Stmt, QalaError> {
1322        let kw_span = self.expect(&TokenKind::For)?;
1323        let (var, _) = self.expect_ident()?;
1324        self.expect(&TokenKind::In)?;
1325        let iter = self.parse_expr_no_struct()?;
1326        let body = self.parse_block()?;
1327        let span = kw_span.to(body.span);
1328        Ok(Stmt::For {
1329            var,
1330            iter,
1331            body,
1332            span,
1333        })
1334    }
1335
1336    /// `return` or `return EXPR`. bare `return` (in a `void` function) is
1337    /// legal; the type checker (Phase 3) is responsible for rejecting it in a
1338    /// non-`void` function.
1339    fn parse_return_stmt(&mut self) -> Result<Stmt, QalaError> {
1340        let kw_span = self.expect(&TokenKind::Return)?;
1341        // a `return` with no value is followed by `}` (end of block) or the
1342        // start of the next statement (a keyword head). otherwise read an
1343        // expression.
1344        if self.check(&TokenKind::RBrace)
1345            || matches!(self.peek(), TokenKind::Eof)
1346            || is_stmt_head(self.peek())
1347        {
1348            return Ok(Stmt::Return {
1349                value: None,
1350                span: kw_span,
1351            });
1352        }
1353        let value = self.parse_expr()?;
1354        let span = kw_span.to(value.span());
1355        Ok(Stmt::Return {
1356            value: Some(value),
1357            span,
1358        })
1359    }
1360
1361    /// `defer EXPR` -- run `EXPR` at scope exit, LIFO with other defers. in
1362    /// practice `EXPR` is a call (`close(f)`), but the parser accepts any
1363    /// expression and leaves shape-checking to a later phase.
1364    fn parse_defer_stmt(&mut self) -> Result<Stmt, QalaError> {
1365        let kw_span = self.expect(&TokenKind::Defer)?;
1366        let expr = self.parse_expr()?;
1367        let span = kw_span.to(expr.span());
1368        Ok(Stmt::Defer { expr, span })
1369    }
1370
1371    /// parse an expression in no-struct-literal mode. used at the head of
1372    /// `if`/`while`/`for` (and inside `match`) so `if x { ... }` reads `x` as
1373    /// the condition rather than `x { ... }` as a struct literal body.
1374    fn parse_expr_no_struct(&mut self) -> Result<Expr, QalaError> {
1375        self.expr_bp(0, ExprCtx::NoStructLiteral)
1376    }
1377
1378    /// reassemble an interpolated string `StrStart text0 (InterpStart expr
1379    /// InterpEnd StrMid text)* InterpStart expr InterpEnd StrEnd textN` into
1380    /// an `Expr::Interpolation` whose `parts` list alternates `Literal(text)`
1381    /// and `Expr(...)`. RESEARCH Pattern 4.
1382    fn parse_interpolation(&mut self) -> Result<Expr, QalaError> {
1383        // consume the `StrStart(text0)`.
1384        let (start_text, start_span) = match self.peek().clone() {
1385            TokenKind::StrStart(s) => {
1386                let span = self.advance().span;
1387                (s, span)
1388            }
1389            _ => return Err(self.unexpected(&[TokenKind::StrStart(String::new())])),
1390        };
1391        let mut parts: Vec<InterpPart> = vec![InterpPart::Literal(start_text)];
1392        let end_span: Span;
1393        loop {
1394            // every loop iteration sees an `InterpStart ... InterpEnd` and
1395            // then either another `StrMid` (continue) or the closing `StrEnd`
1396            // (break).
1397            self.expect(&TokenKind::InterpStart)?;
1398            let inner = self.parse_expr()?;
1399            parts.push(InterpPart::Expr(inner));
1400            self.expect(&TokenKind::InterpEnd)?;
1401            match self.peek().clone() {
1402                TokenKind::StrMid(t) => {
1403                    let _span = self.advance().span;
1404                    parts.push(InterpPart::Literal(t));
1405                    // and around again -- there is another interpolation.
1406                }
1407                TokenKind::StrEnd(t) => {
1408                    let span = self.advance().span;
1409                    parts.push(InterpPart::Literal(t));
1410                    end_span = span;
1411                    break;
1412                }
1413                _ => {
1414                    return Err(self.unexpected(&[
1415                        TokenKind::StrMid(String::new()),
1416                        TokenKind::StrEnd(String::new()),
1417                    ]));
1418                }
1419            }
1420        }
1421        Ok(Expr::Interpolation {
1422            parts,
1423            span: start_span.to(end_span),
1424        })
1425    }
1426
1427    /// `match scrutinee { arm, arm, ... }`.
1428    ///
1429    /// the scrutinee is parsed in `NoStructLiteral` context so the `{` that
1430    /// opens the match body is not consumed as a struct-literal body. at
1431    /// least one arm is required (RESEARCH Pitfall 7 / success criterion 2);
1432    /// zero arms is a parse error.
1433    fn parse_match(&mut self) -> Result<Expr, QalaError> {
1434        let kw_span = self.expect(&TokenKind::Match)?;
1435        let scrutinee = self.expr_bp(0, ExprCtx::NoStructLiteral)?;
1436        let open_span = self.expect(&TokenKind::LBrace)?;
1437        self.open_delims.push((TokenKind::LBrace, open_span));
1438        let mut arms: Vec<MatchArm> = Vec::new();
1439        loop {
1440            if self.check(&TokenKind::RBrace) {
1441                break;
1442            }
1443            let pattern = self.parse_pattern()?;
1444            let pattern_span = pattern.span();
1445            // the guard precedes `=>` which precedes `{`; parsing with
1446            // parse_expr_no_struct prevents `Name { f: v }` in the guard
1447            // from consuming the `{` that opens the arm body.
1448            let guard = if self.eat(&TokenKind::If) {
1449                Some(self.parse_expr_no_struct()?)
1450            } else {
1451                None
1452            };
1453            self.expect(&TokenKind::FatArrow)?;
1454            let body = if self.check(&TokenKind::LBrace) {
1455                MatchArmBody::Block(self.parse_block()?)
1456            } else {
1457                MatchArmBody::Expr(Box::new(self.parse_expr()?))
1458            };
1459            let body_span = match &body {
1460                MatchArmBody::Expr(e) => e.span(),
1461                MatchArmBody::Block(b) => b.span,
1462            };
1463            arms.push(MatchArm {
1464                pattern,
1465                guard,
1466                body,
1467                span: pattern_span.to(body_span),
1468            });
1469            // an arm's trailing comma is optional (per pattern-matching.qala
1470            // the `Triangle` arm has none); if absent, the next iteration
1471            // sees `}` and breaks.
1472            if !self.eat(&TokenKind::Comma) {
1473                break;
1474            }
1475        }
1476        if arms.is_empty() {
1477            // zero arms: error before consuming the `}` so the span is the
1478            // match keyword through the offending `}` (or `Eof`).
1479            let close_span = self.peek_span();
1480            self.open_delims.pop();
1481            return Err(QalaError::Parse {
1482                span: kw_span.to(close_span),
1483                message: "match needs at least one arm".to_string(),
1484            });
1485        }
1486        if !self.check(&TokenKind::RBrace) {
1487            self.open_delims.pop();
1488            return Err(QalaError::UnclosedDelimiter {
1489                span: open_span,
1490                opener: TokenKind::LBrace,
1491                found: self.peek().clone(),
1492            });
1493        }
1494        let close_span = self.advance().span;
1495        self.open_delims.pop();
1496        Ok(Expr::Match {
1497            scrutinee: Box::new(scrutinee),
1498            arms,
1499            span: kw_span.to(close_span),
1500        })
1501    }
1502
1503    /// parse one pattern.
1504    ///
1505    /// the v1 set (per Plan 01's `Pattern` enum):
1506    /// - `Name(sub, ...)`: variant destructure (sub-patterns recursively)
1507    /// - `_`: wildcard
1508    /// - `name`: binding (bare ident -- the parser does not distinguish a
1509    ///   capitalized nullary-variant from a binding; that is Phase 3's call,
1510    ///   matching CONTEXT.md's "simpler, more-faithful choice")
1511    /// - integer / float / byte / string / boolean literal patterns
1512    fn parse_pattern(&mut self) -> Result<Pattern, QalaError> {
1513        match self.peek().clone() {
1514            TokenKind::Ident(name) => {
1515                let name_span = self.advance().span;
1516                // `_` (the lexer treats `_` as a regular identifier).
1517                if name == "_" {
1518                    return Ok(Pattern::Wildcard { span: name_span });
1519                }
1520                // `Name(sub, ...)`: variant destructure.
1521                if self.check(&TokenKind::LParen) {
1522                    let (sub, close) = self.delimited_list(
1523                        TokenKind::LParen,
1524                        TokenKind::RParen,
1525                        TokenKind::Comma,
1526                        |p| p.parse_pattern(),
1527                    )?;
1528                    return Ok(Pattern::Variant {
1529                        name,
1530                        sub,
1531                        span: name_span.to(close),
1532                    });
1533                }
1534                // bare ident: binding. Phase 3 reinterprets a capitalised
1535                // ident as a nullary variant if name-resolution finds one.
1536                Ok(Pattern::Binding {
1537                    name,
1538                    span: name_span,
1539                })
1540            }
1541            TokenKind::Int(v) => {
1542                let span = self.advance().span;
1543                Ok(Pattern::Int { value: v, span })
1544            }
1545            TokenKind::Float(v) => {
1546                let span = self.advance().span;
1547                Ok(Pattern::Float { value: v, span })
1548            }
1549            TokenKind::Byte(v) => {
1550                let span = self.advance().span;
1551                Ok(Pattern::Byte { value: v, span })
1552            }
1553            TokenKind::Str(s) => {
1554                let span = self.advance().span;
1555                Ok(Pattern::Str { value: s, span })
1556            }
1557            TokenKind::True => {
1558                let span = self.advance().span;
1559                Ok(Pattern::Bool { value: true, span })
1560            }
1561            TokenKind::False => {
1562                let span = self.advance().span;
1563                Ok(Pattern::Bool { value: false, span })
1564            }
1565            _ => Err(self.unexpected(&[
1566                TokenKind::Ident(String::new()),
1567                TokenKind::Int(0),
1568                TokenKind::Float(0.0),
1569                TokenKind::Byte(0),
1570                TokenKind::Str(String::new()),
1571                TokenKind::True,
1572                TokenKind::False,
1573            ])),
1574        }
1575    }
1576
1577    /// `! operand`, `- operand`, or `comptime operand`. each binds at unary
1578    /// precedence: `comptime a + b` is `(comptime a) + b`, `-a.b` is
1579    /// `-(a.b)` because postfix `.` binds tighter than prefix `-`.
1580    fn parse_prefix(&mut self) -> Result<Expr, QalaError> {
1581        let op_tok = self.advance().clone();
1582        let ((), rbp) = prefix_bp(&op_tok.kind);
1583        let operand = self.expr_bp(rbp, ExprCtx::Normal)?;
1584        let span = op_tok.span.to(operand.span());
1585        match op_tok.kind {
1586            TokenKind::Bang => Ok(Expr::Unary {
1587                op: UnaryOp::Not,
1588                operand: Box::new(operand),
1589                span,
1590            }),
1591            TokenKind::Minus => Ok(Expr::Unary {
1592                op: UnaryOp::Neg,
1593                operand: Box::new(operand),
1594                span,
1595            }),
1596            TokenKind::Comptime => Ok(Expr::Comptime {
1597                body: Box::new(operand),
1598                span,
1599            }),
1600            // not reachable from `parse_primary`'s prefix arm, but the match
1601            // must be exhaustive in shape.
1602            _ => Err(QalaError::Parse {
1603                span: op_tok.span,
1604                message: "internal: prefix on a non-prefix token".to_string(),
1605            }),
1606        }
1607    }
1608
1609    /// apply one postfix operator to `lhs`. the dispatch matches each of the
1610    /// four kinds that `postfix_bp` says are postfix (`.`, `(`, `[`, `?`).
1611    fn fold_postfix(&mut self, lhs: Expr, op: &TokenKind) -> Result<Expr, QalaError> {
1612        match op {
1613            TokenKind::Dot => {
1614                self.advance(); // consume the `.`
1615                let (name, name_span) = self.expect_ident()?;
1616                // `.name(args)` is a method call; `.name` alone is a field
1617                // access. the parser decides; Phase 3 resolves it.
1618                if self.check(&TokenKind::LParen) {
1619                    let (args, close) = self.delimited_list(
1620                        TokenKind::LParen,
1621                        TokenKind::RParen,
1622                        TokenKind::Comma,
1623                        |p| p.parse_expr(),
1624                    )?;
1625                    let lhs_span = lhs.span();
1626                    Ok(Expr::MethodCall {
1627                        receiver: Box::new(lhs),
1628                        name,
1629                        args,
1630                        span: lhs_span.to(close),
1631                    })
1632                } else {
1633                    let lhs_span = lhs.span();
1634                    Ok(Expr::FieldAccess {
1635                        obj: Box::new(lhs),
1636                        name,
1637                        span: lhs_span.to(name_span),
1638                    })
1639                }
1640            }
1641            TokenKind::LParen => {
1642                let (args, close) = self.delimited_list(
1643                    TokenKind::LParen,
1644                    TokenKind::RParen,
1645                    TokenKind::Comma,
1646                    |p| p.parse_expr(),
1647                )?;
1648                let lhs_span = lhs.span();
1649                Ok(Expr::Call {
1650                    callee: Box::new(lhs),
1651                    args,
1652                    span: lhs_span.to(close),
1653                })
1654            }
1655            TokenKind::LBracket => {
1656                let open_span = self.advance().span;
1657                self.open_delims.push((TokenKind::LBracket, open_span));
1658                let idx = self.parse_expr()?;
1659                if !self.check(&TokenKind::RBracket) {
1660                    self.open_delims.pop();
1661                    return Err(QalaError::UnclosedDelimiter {
1662                        span: open_span,
1663                        opener: TokenKind::LBracket,
1664                        found: self.peek().clone(),
1665                    });
1666                }
1667                let close = self.advance().span;
1668                self.open_delims.pop();
1669                let lhs_span = lhs.span();
1670                Ok(Expr::Index {
1671                    obj: Box::new(lhs),
1672                    index: Box::new(idx),
1673                    span: lhs_span.to(close),
1674                })
1675            }
1676            TokenKind::Question => {
1677                let q_span = self.advance().span;
1678                let lhs_span = lhs.span();
1679                Ok(Expr::Try {
1680                    expr: Box::new(lhs),
1681                    span: lhs_span.to(q_span),
1682                })
1683            }
1684            _ => Err(QalaError::Parse {
1685                span: self.peek_span(),
1686                message: "internal: postfix_bp returned Some for a non-postfix token".to_string(),
1687            }),
1688        }
1689    }
1690
1691    /// apply one infix operator (already consumed by the Pratt loop) with
1692    /// right-binding-power `rbp` to `lhs`. `ctx` is the no-struct-literal
1693    /// context inherited from the outer loop -- propagated to the rhs so e.g.
1694    /// `0..n {` inside a `for ... in HEAD { ... }` head does not consume the
1695    /// `{` as a struct-literal body for `n`.
1696    fn fold_infix(
1697        &mut self,
1698        lhs: Expr,
1699        op_tok: Token,
1700        rbp: u8,
1701        ctx: ExprCtx,
1702    ) -> Result<Expr, QalaError> {
1703        match op_tok.kind {
1704            TokenKind::PipeGt => {
1705                // the pipeline rhs must be a callable-shaped expression (an
1706                // ident / call / field-access / method-call); a literal or a
1707                // binary on the rhs is a parse error whose span is the `|>`
1708                // token, per CONTEXT.md.
1709                let rhs = self.parse_pipeline_rhs(op_tok.span)?;
1710                let lhs_span = lhs.span();
1711                let rhs_span = rhs.span();
1712                Ok(Expr::Pipeline {
1713                    lhs: Box::new(lhs),
1714                    call: Box::new(rhs),
1715                    span: lhs_span.to(rhs_span),
1716                })
1717            }
1718            TokenKind::Or => {
1719                let rhs = self.expr_bp(rbp, ctx)?;
1720                let lhs_span = lhs.span();
1721                let rhs_span = rhs.span();
1722                Ok(Expr::OrElse {
1723                    expr: Box::new(lhs),
1724                    fallback: Box::new(rhs),
1725                    span: lhs_span.to(rhs_span),
1726                })
1727            }
1728            TokenKind::DotDot | TokenKind::DotDotEq => {
1729                // ranges are non-associative: `0..1..2` is a parse error.
1730                // right-chaining is blocked by rbp > lbp (the Pratt loop in
1731                // `expr_bp(rbp)` stops on the next `..` because its lbp=60
1732                // is below the new min_bp=61, so it doesn't fold further).
1733                // left-chaining is blocked here: if lhs is already a Range,
1734                // reject it so the user can't pre-parenthesise around the rule.
1735                if matches!(lhs, Expr::Range { .. }) {
1736                    return Err(QalaError::Parse {
1737                        span: op_tok.span,
1738                        message: "range operators cannot be chained; use parentheses if nested ranges are intended".to_string(),
1739                    });
1740                }
1741                let inclusive = matches!(op_tok.kind, TokenKind::DotDotEq);
1742                let rhs = self.expr_bp(rbp, ctx)?;
1743                let lhs_span = lhs.span();
1744                let rhs_span = rhs.span();
1745                Ok(Expr::Range {
1746                    start: Some(Box::new(lhs)),
1747                    end: Some(Box::new(rhs)),
1748                    inclusive,
1749                    span: lhs_span.to(rhs_span),
1750                })
1751            }
1752            ref k if binop_of(k).is_some() => {
1753                let bop = binop_of(k).expect("checked by the guard");
1754                let rhs = self.expr_bp(rbp, ctx)?;
1755                let lhs_span = lhs.span();
1756                let rhs_span = rhs.span();
1757                Ok(Expr::Binary {
1758                    op: bop,
1759                    lhs: Box::new(lhs),
1760                    rhs: Box::new(rhs),
1761                    span: lhs_span.to(rhs_span),
1762                })
1763            }
1764            _ => Err(QalaError::Parse {
1765                span: op_tok.span,
1766                message: "internal: infix_bp returned Some for an unhandled token".to_string(),
1767            }),
1768        }
1769    }
1770
1771    /// parse the right-hand side of `|>`. it must be a "callable" expression
1772    /// (Ident / Call / FieldAccess / MethodCall); per `pipeline.qala` a bare
1773    /// ident is allowed, but a literal or a binary is not.
1774    ///
1775    /// the violation case `x |> 5` produces an error whose span is the `|>`
1776    /// token; `x |>` with nothing after (an Eof / closer) does the same --
1777    /// per CONTEXT.md and success criterion PARSE-06.
1778    fn parse_pipeline_rhs(&mut self, pipe_span: Span) -> Result<Expr, QalaError> {
1779        // fast-reject: if the next token cannot even begin an expression that
1780        // might be callable, error now with the pipe span so the diagnostic
1781        // points at `|>` rather than something inside the rhs.
1782        // LParen is included because `(expr).method()` can be callable-shaped
1783        // once postfix operators fold in; a bare `(expr)` that is not callable
1784        // reaches the is_callable_shape check below and is rejected there with
1785        // the same |> span.
1786        if !matches!(self.peek(), TokenKind::Ident(_) | TokenKind::LParen) {
1787            return Err(QalaError::Parse {
1788                span: pipe_span,
1789                message: "expected a call after `|>`".to_string(),
1790            });
1791        }
1792        // parse a sub-expression at one tier above `|>` so we do not consume
1793        // the next `|>` (left-associativity is handled in the outer loop).
1794        // we explicitly DO want postfix `.` / `()` / `[]` / `?` to bind to
1795        // the rhs (so `x |> f()` and `xs |> a.b()` work).
1796        let rhs = self.expr_bp(infix_lbp_pipeline() + 1, ExprCtx::Normal)?;
1797        if !is_callable_shape(&rhs) {
1798            return Err(QalaError::Parse {
1799                span: pipe_span,
1800                message: "the right side of `|>` must be a call".to_string(),
1801            });
1802        }
1803        Ok(rhs)
1804    }
1805}
1806
1807/// the context an expression is being parsed in.
1808///
1809/// the only knob v1 needs is "are struct literals allowed here?" -- they are
1810/// not at the head of `match` / `if` / `while` / `for`. RESEARCH Pitfall 3.
1811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1812enum ExprCtx {
1813    /// struct literals are allowed at this primary.
1814    Normal,
1815    /// `Name { ... }` here is NOT a struct literal; the `{` belongs to the
1816    /// enclosing construct's body.
1817    NoStructLiteral,
1818}
1819
1820/// postfix binding power: the four kinds that bind tightest. `()` here is the
1821/// "no rhs" marker -- postfix operators do not have a right-binding power.
1822fn postfix_bp(k: &TokenKind) -> Option<(u8, ())> {
1823    match k {
1824        TokenKind::Dot | TokenKind::LParen | TokenKind::LBracket | TokenKind::Question => {
1825            Some((100, ()))
1826        }
1827        _ => None,
1828    }
1829}
1830
1831/// prefix binding power: `!`, `-`, `comptime` -- all at unary precedence,
1832/// one tier below postfix, so `-a.b` parses as `-(a.b)`. caller is expected
1833/// to have already confirmed the token is one of these via the primary
1834/// dispatch; an unrecognised kind is an internal bug, surfaced as a Parse
1835/// error rather than a panic.
1836fn prefix_bp(k: &TokenKind) -> ((), u8) {
1837    match k {
1838        TokenKind::Bang | TokenKind::Minus | TokenKind::Comptime => ((), 90),
1839        // a non-prefix token reaching here would be a bug in the caller; we
1840        // return a high rbp so the recursion does not consume anything by
1841        // accident, and the surrounding code's exhaustive match catches the
1842        // mismatch on the way out.
1843        _ => ((), u8::MAX),
1844    }
1845}
1846
1847/// infix binding power: `(lbp, rbp)`. left-associative operators have
1848/// `rbp = lbp + 1` so a same-tier operator on the right does not re-enter at
1849/// equal-or-higher binding power. ranges are non-associative: both lbp and rbp
1850/// are 60, which prevents right-chaining. left-chaining (`Range` on the lhs of
1851/// a second `..`) is rejected explicitly in `fold_infix` -- a `Range` lhs for a
1852/// range operator is an error. this two-part enforcement is the Pratt
1853/// non-associativity pattern for operators that must not chain in either
1854/// direction.
1855///
1856/// `Eq` (the assignment token) is deliberately absent -- `let x = ...` is
1857/// statement syntax (Plan 03), not an expression operator.
1858fn infix_bp(k: &TokenKind) -> Option<(u8, u8)> {
1859    Some(match k {
1860        // tightest infix: multiplicative.
1861        TokenKind::Star | TokenKind::Slash | TokenKind::Percent => (80, 81),
1862        // additive.
1863        TokenKind::Plus | TokenKind::Minus => (70, 71),
1864        // ranges -- non-associative (see comment above): rbp = 61 blocks
1865        // right-chaining; the fold_infix guard blocks left-chaining.
1866        TokenKind::DotDot | TokenKind::DotDotEq => (60, 61),
1867        // comparison and equality -- left-associative.
1868        TokenKind::Lt
1869        | TokenKind::LtEq
1870        | TokenKind::Gt
1871        | TokenKind::GtEq
1872        | TokenKind::EqEq
1873        | TokenKind::BangEq => (50, 51),
1874        // boolean and / or.
1875        TokenKind::AmpAmp => (40, 41),
1876        TokenKind::PipePipe => (30, 31),
1877        // pipeline -- left-associative.
1878        TokenKind::PipeGt => (20, 21),
1879        // `or` fallback -- loosest, left-associative.
1880        TokenKind::Or => (10, 11),
1881        _ => return None,
1882    })
1883}
1884
1885/// `infix_bp(PipeGt)`'s `lbp`, factored out so `parse_pipeline_rhs` can ask
1886/// "one tier above `|>`" without re-typing the magic number.
1887fn infix_lbp_pipeline() -> u8 {
1888    20
1889}
1890
1891/// map a binary-operator token kind to its [`BinOp`] enum value. returns
1892/// `None` for tokens whose `infix_bp` arm is handled specially in
1893/// `fold_infix` (`PipeGt`, `Or`, `DotDot`, `DotDotEq`).
1894fn binop_of(k: &TokenKind) -> Option<BinOp> {
1895    Some(match k {
1896        TokenKind::Plus => BinOp::Add,
1897        TokenKind::Minus => BinOp::Sub,
1898        TokenKind::Star => BinOp::Mul,
1899        TokenKind::Slash => BinOp::Div,
1900        TokenKind::Percent => BinOp::Rem,
1901        TokenKind::EqEq => BinOp::Eq,
1902        TokenKind::BangEq => BinOp::Ne,
1903        TokenKind::Lt => BinOp::Lt,
1904        TokenKind::LtEq => BinOp::Le,
1905        TokenKind::Gt => BinOp::Gt,
1906        TokenKind::GtEq => BinOp::Ge,
1907        TokenKind::AmpAmp => BinOp::And,
1908        TokenKind::PipePipe => BinOp::Or,
1909        _ => return None,
1910    })
1911}
1912
1913/// is `e` shaped like something that can be called?
1914///
1915/// per CONTEXT.md and `pipeline.qala`, a pipeline rhs is either a bare
1916/// identifier, a call, a field access, or a method call -- anything that
1917/// post-desugar in Phase 4 becomes `f(x, ...)`. a literal, a binary, a
1918/// range, etc., is rejected.
1919fn is_callable_shape(e: &Expr) -> bool {
1920    matches!(
1921        e,
1922        Expr::Ident { .. } | Expr::Call { .. } | Expr::FieldAccess { .. } | Expr::MethodCall { .. }
1923    )
1924}
1925
1926/// true if `k` is one of the keyword tokens that starts a statement form.
1927///
1928/// the bundled examples like `fibonacci.qala` have no `;` anywhere -- they
1929/// rely on the next statement-head keyword (or `}`) to terminate the previous
1930/// statement. `parse_block`'s loop uses this predicate to decide whether to
1931/// dispatch to `parse_stmt` or fall through to the expression branch, and
1932/// `parse_return_stmt` uses it to decide whether `return` was bare.
1933fn is_stmt_head(k: &TokenKind) -> bool {
1934    matches!(
1935        k,
1936        TokenKind::Let
1937            | TokenKind::If
1938            | TokenKind::While
1939            | TokenKind::For
1940            | TokenKind::Return
1941            | TokenKind::Break
1942            | TokenKind::Continue
1943            | TokenKind::Defer
1944    )
1945}
1946
1947#[cfg(test)]
1948mod tests {
1949    use super::*;
1950    use crate::lexer::Lexer;
1951
1952    /// tokenize, asserting success; the parser's tests want a `Vec<Token>`,
1953    /// not the lexer's `Result`.
1954    fn lex(src: &str) -> Vec<Token> {
1955        Lexer::tokenize(src).expect("expected successful tokenize")
1956    }
1957
1958    /// parse a whole program, asserting success.
1959    #[allow(dead_code)] // exercised by Plan 03's item tests
1960    fn parse_ok(src: &str) -> Ast {
1961        Parser::parse(&lex(src)).expect("expected a successful parse")
1962    }
1963
1964    /// parse a whole program, asserting it failed, and return the error.
1965    fn parse_err(src: &str) -> QalaError {
1966        Parser::parse(&lex(src)).expect_err("expected a parse error")
1967    }
1968
1969    /// parse `src` as a single expression (using the Pratt entry point
1970    /// directly), asserting success and that the cursor ends at `Eof`. used
1971    /// to test the expression engine without dragging in statement / item
1972    /// parsing (Plan 03).
1973    fn expr_of(src: &str) -> Expr {
1974        let toks = lex(src);
1975        let mut p = Parser {
1976            tokens: &toks,
1977            pos: 0,
1978            depth: 0,
1979            open_delims: Vec::new(),
1980        };
1981        let e = p
1982            .parse_expr()
1983            .expect("expected a successful expression parse");
1984        assert!(
1985            matches!(p.peek(), TokenKind::Eof),
1986            "expr_of left tokens unconsumed: next = {:?}",
1987            p.peek()
1988        );
1989        e
1990    }
1991
1992    /// parse `src` as a single expression, asserting it failed; return the
1993    /// error. mirror of `parse_err` for the expression-engine tests.
1994    fn expr_err(src: &str) -> QalaError {
1995        let toks = lex(src);
1996        let mut p = Parser {
1997            tokens: &toks,
1998            pos: 0,
1999            depth: 0,
2000            open_delims: Vec::new(),
2001        };
2002        p.parse_expr()
2003            .expect_err("expected an expression parse error")
2004    }
2005
2006    // ---- skeleton + parse() entry point -------------------------------------
2007
2008    #[test]
2009    fn empty_program_parses_to_no_items() {
2010        // `Ast = Vec<Item>`; the empty program is an empty vec, no error.
2011        let ast = parse_ok("");
2012        assert!(ast.is_empty());
2013    }
2014
2015    #[test]
2016    fn whitespace_and_comments_only_parses_to_no_items() {
2017        // the lexer skips trivia; parser sees only `Eof` and is happy.
2018        let ast = parse_ok("   \n// a comment\n   ");
2019        assert!(ast.is_empty());
2020    }
2021
2022    #[test]
2023    fn a_program_with_a_non_item_token_at_the_top_level_is_a_clean_error() {
2024        // a token that cannot start an item lists the four item kinds as the
2025        // expected set. (a `fn ...` succeeds now -- the items wave is wired.)
2026        let err = parse_err("let x = 5");
2027        match err {
2028            QalaError::UnexpectedToken {
2029                ref expected,
2030                ref found,
2031                ..
2032            } => {
2033                assert!(expected.contains(&TokenKind::Fn));
2034                assert!(expected.contains(&TokenKind::Struct));
2035                assert!(expected.contains(&TokenKind::Enum));
2036                assert!(expected.contains(&TokenKind::Interface));
2037                assert_eq!(found, &TokenKind::Let);
2038            }
2039            other => panic!("expected UnexpectedToken, got {other:?}"),
2040        }
2041        assert!(err.message().contains("expected"));
2042    }
2043
2044    // ---- type sub-grammar ---------------------------------------------------
2045    //
2046    // helper: parse a type via a fixture `fn f() -> TYPE {}`, pull the
2047    // `ret_ty` out. this is the simplest way to exercise `parse_type` through
2048    // the public `Parser::parse` entry point without exposing it directly.
2049    fn type_of(src: &str) -> TypeExpr {
2050        let program = format!("fn f() -> {src} {{}}");
2051        let ast = parse_ok(&program);
2052        match ast.into_iter().next().expect("one item") {
2053            Item::Fn(d) => d.ret_ty.expect("a return type is present"),
2054            other => panic!("expected fn item, got {other:?}"),
2055        }
2056    }
2057
2058    #[test]
2059    fn primitive_type_names_parse_to_typeexpr_primitive() {
2060        let cases = [
2061            ("i64", PrimType::I64),
2062            ("f64", PrimType::F64),
2063            ("bool", PrimType::Bool),
2064            ("str", PrimType::Str),
2065            ("byte", PrimType::Byte),
2066            ("void", PrimType::Void),
2067        ];
2068        for (src, want) in cases {
2069            match type_of(src) {
2070                TypeExpr::Primitive { kind, .. } => assert_eq!(kind, want, "{src}"),
2071                other => panic!("expected Primitive for {src}, got {other:?}"),
2072            }
2073        }
2074    }
2075
2076    #[test]
2077    fn a_bare_ident_parses_to_typeexpr_named() {
2078        match type_of("Shape") {
2079            TypeExpr::Named { name, .. } => assert_eq!(name, "Shape"),
2080            other => panic!("expected Named, got {other:?}"),
2081        }
2082    }
2083
2084    #[test]
2085    fn a_fixed_array_type_carries_its_int_size() {
2086        match type_of("[i64; 5]") {
2087            TypeExpr::Array { elem, size, .. } => {
2088                assert!(matches!(
2089                    *elem,
2090                    TypeExpr::Primitive {
2091                        kind: PrimType::I64,
2092                        ..
2093                    }
2094                ));
2095                assert_eq!(size, 5);
2096            }
2097            other => panic!("expected Array, got {other:?}"),
2098        }
2099    }
2100
2101    #[test]
2102    fn a_dyn_array_type_has_no_size() {
2103        match type_of("[i64]") {
2104            TypeExpr::DynArray { elem, .. } => {
2105                assert!(matches!(
2106                    *elem,
2107                    TypeExpr::Primitive {
2108                        kind: PrimType::I64,
2109                        ..
2110                    }
2111                ));
2112            }
2113            other => panic!("expected DynArray, got {other:?}"),
2114        }
2115    }
2116
2117    #[test]
2118    fn tuple_versus_paren_type_distinguished_by_a_trailing_comma() {
2119        // `(i64, str)` -- a tuple of two primitives.
2120        match type_of("(i64, str)") {
2121            TypeExpr::Tuple { elems, .. } => assert_eq!(elems.len(), 2),
2122            other => panic!("expected Tuple, got {other:?}"),
2123        }
2124        // `(i64,)` -- a one-tuple.
2125        match type_of("(i64,)") {
2126            TypeExpr::Tuple { elems, .. } => assert_eq!(elems.len(), 1),
2127            other => panic!("expected Tuple, got {other:?}"),
2128        }
2129        // `()` -- zero-tuple.
2130        match type_of("()") {
2131            TypeExpr::Tuple { elems, .. } => assert!(elems.is_empty()),
2132            other => panic!("expected zero-tuple, got {other:?}"),
2133        }
2134        // `(i64)` -- a parenthesised type, equivalent to `i64`.
2135        match type_of("(i64)") {
2136            TypeExpr::Primitive {
2137                kind: PrimType::I64,
2138                ..
2139            } => {}
2140            other => panic!("expected unwrapped i64, got {other:?}"),
2141        }
2142    }
2143
2144    #[test]
2145    fn a_function_type_parses() {
2146        match type_of("fn(i64, bool) -> str") {
2147            TypeExpr::Fn { params, ret, .. } => {
2148                assert_eq!(params.len(), 2);
2149                assert!(matches!(
2150                    params[0],
2151                    TypeExpr::Primitive {
2152                        kind: PrimType::I64,
2153                        ..
2154                    }
2155                ));
2156                assert!(matches!(
2157                    params[1],
2158                    TypeExpr::Primitive {
2159                        kind: PrimType::Bool,
2160                        ..
2161                    }
2162                ));
2163                assert!(matches!(
2164                    *ret,
2165                    TypeExpr::Primitive {
2166                        kind: PrimType::Str,
2167                        ..
2168                    }
2169                ));
2170            }
2171            other => panic!("expected Fn, got {other:?}"),
2172        }
2173    }
2174
2175    #[test]
2176    fn a_generic_type_parses_with_arguments() {
2177        match type_of("Result<str, str>") {
2178            TypeExpr::Generic { name, args, .. } => {
2179                assert_eq!(name, "Result");
2180                assert_eq!(args.len(), 2);
2181            }
2182            other => panic!("expected Generic, got {other:?}"),
2183        }
2184    }
2185
2186    #[test]
2187    fn nested_generics_use_two_gt_tokens_not_one_shift() {
2188        // `Result<Option<i64>, str>` -- the inner `>>` is two separate `Gt`
2189        // tokens (the lexer never combines them), so the nested
2190        // `delimited_list(Lt, Gt, ...)` calls each consume one.
2191        match type_of("Result<Option<i64>, str>") {
2192            TypeExpr::Generic { name, args, .. } => {
2193                assert_eq!(name, "Result");
2194                assert_eq!(args.len(), 2);
2195                match &args[0] {
2196                    TypeExpr::Generic {
2197                        name: inner_name,
2198                        args: inner_args,
2199                        ..
2200                    } => {
2201                        assert_eq!(inner_name, "Option");
2202                        assert_eq!(inner_args.len(), 1);
2203                        assert!(matches!(
2204                            inner_args[0],
2205                            TypeExpr::Primitive {
2206                                kind: PrimType::I64,
2207                                ..
2208                            }
2209                        ));
2210                    }
2211                    other => panic!("expected nested Generic, got {other:?}"),
2212                }
2213                assert!(matches!(
2214                    args[1],
2215                    TypeExpr::Primitive {
2216                        kind: PrimType::Str,
2217                        ..
2218                    }
2219                ));
2220            }
2221            other => panic!("expected outer Generic, got {other:?}"),
2222        }
2223    }
2224
2225    // ---- item parsers -------------------------------------------------------
2226
2227    #[test]
2228    fn a_minimal_fn_parses_with_no_params_no_return_no_effect() {
2229        let ast = parse_ok("fn g() {}");
2230        assert_eq!(ast.len(), 1);
2231        match &ast[0] {
2232            Item::Fn(d) => {
2233                assert_eq!(d.name, "g");
2234                assert!(d.type_name.is_none());
2235                assert!(d.params.is_empty());
2236                assert!(d.ret_ty.is_none());
2237                assert!(d.effect.is_none());
2238                assert!(d.body.stmts.is_empty());
2239                assert!(d.body.value.is_none());
2240            }
2241            other => panic!("expected Fn, got {other:?}"),
2242        }
2243    }
2244
2245    #[test]
2246    fn a_fn_parses_with_params_return_default_and_effect() {
2247        let ast = parse_ok("fn f(a: i64, b: i64 = 10) -> i64 is pure {}");
2248        match &ast[0] {
2249            Item::Fn(d) => {
2250                assert_eq!(d.name, "f");
2251                assert_eq!(d.params.len(), 2);
2252                assert_eq!(d.params[0].name, "a");
2253                assert!(d.params[0].default.is_none());
2254                assert_eq!(d.params[1].name, "b");
2255                match &d.params[1].default {
2256                    Some(Expr::Int { value: 10, .. }) => {}
2257                    other => panic!("expected default Int(10), got {other:?}"),
2258                }
2259                assert!(matches!(
2260                    d.ret_ty,
2261                    Some(TypeExpr::Primitive {
2262                        kind: PrimType::I64,
2263                        ..
2264                    })
2265                ));
2266                assert_eq!(d.effect, Some(Effect::Pure));
2267            }
2268            other => panic!("expected Fn, got {other:?}"),
2269        }
2270    }
2271
2272    #[test]
2273    fn each_of_the_four_effect_keywords_parses() {
2274        for (src, want) in [
2275            ("fn f() is pure {}", Effect::Pure),
2276            ("fn f() is io {}", Effect::Io),
2277            ("fn f() is alloc {}", Effect::Alloc),
2278            ("fn f() is panic {}", Effect::Panic),
2279        ] {
2280            let ast = parse_ok(src);
2281            match &ast[0] {
2282                Item::Fn(d) => assert_eq!(d.effect, Some(want.clone()), "{src}"),
2283                other => panic!("expected Fn for {src}, got {other:?}"),
2284            }
2285        }
2286    }
2287
2288    #[test]
2289    fn a_method_definition_parses_with_self_as_the_first_param() {
2290        let ast = parse_ok("fn Point.area(self) -> f64 is pure { 3.14 }");
2291        match &ast[0] {
2292            Item::Fn(d) => {
2293                assert_eq!(d.type_name.as_deref(), Some("Point"));
2294                assert_eq!(d.name, "area");
2295                assert_eq!(d.params.len(), 1);
2296                assert!(d.params[0].is_self);
2297                assert_eq!(d.params[0].name, "self");
2298                assert!(d.params[0].ty.is_none());
2299                assert_eq!(d.effect, Some(Effect::Pure));
2300            }
2301            other => panic!("expected Fn, got {other:?}"),
2302        }
2303    }
2304
2305    #[test]
2306    fn self_on_a_free_function_is_a_parse_error() {
2307        // a free fn (no `Type.`) cannot have a `self` parameter.
2308        let err = parse_err("fn f(self) {}");
2309        match err {
2310            QalaError::Parse { message, .. } => {
2311                assert!(message.contains("self"), "message was {message:?}");
2312            }
2313            other => panic!("expected QalaError::Parse, got {other:?}"),
2314        }
2315    }
2316
2317    #[test]
2318    fn self_outside_the_first_param_slot_of_a_method_is_a_parse_error() {
2319        let err = parse_err("fn Point.f(a: i64, self) {}");
2320        match err {
2321            QalaError::Parse { message, .. } => {
2322                assert!(message.contains("self"), "message was {message:?}");
2323            }
2324            other => panic!("expected QalaError::Parse, got {other:?}"),
2325        }
2326    }
2327
2328    #[test]
2329    fn self_in_method_body_parses_as_ident() {
2330        // `self` in expression position inside a method body must produce
2331        // Expr::Ident { name: "self" } -- the same node the type checker uses.
2332        // Phase 3 is responsible for rejecting `self` outside a method body.
2333        let ast = parse_ok("fn Point.area(self) -> f64 is pure { self.x }");
2334        match &ast[0] {
2335            Item::Fn(d) => {
2336                // the body's trailing value is a FieldAccess on Ident("self").
2337                match d.body.value.as_deref() {
2338                    Some(Expr::FieldAccess { obj, name, .. }) => {
2339                        assert!(
2340                            matches!(**obj, Expr::Ident { ref name, .. } if name == "self"),
2341                            "expected Ident(\"self\"), got {:?}",
2342                            obj
2343                        );
2344                        assert_eq!(name, "x");
2345                    }
2346                    other => panic!("expected FieldAccess on self, got {other:?}"),
2347                }
2348            }
2349            other => panic!("expected Fn, got {other:?}"),
2350        }
2351    }
2352
2353    #[test]
2354    fn self_method_call_in_body_parses() {
2355        // `self.foo()` without a trailing `;` is the block's trailing value,
2356        // not a statement. MethodCall whose receiver is Expr::Ident { name: "self" }.
2357        let ast = parse_ok("fn Point.go(self) { self.foo() }");
2358        match &ast[0] {
2359            Item::Fn(d) => match d.body.value.as_deref() {
2360                Some(Expr::MethodCall { receiver, name, .. }) => {
2361                    assert!(
2362                        matches!(**receiver, Expr::Ident { ref name, .. } if name == "self"),
2363                        "expected Ident(\"self\") as receiver, got {receiver:?}"
2364                    );
2365                    assert_eq!(name, "foo");
2366                }
2367                other => panic!("expected MethodCall trailing value, got {other:?}"),
2368            },
2369            other => panic!("expected Fn, got {other:?}"),
2370        }
2371    }
2372
2373    #[test]
2374    fn a_struct_with_typed_fields_parses() {
2375        let ast = parse_ok("struct Point { x: f64, y: f64 }");
2376        match &ast[0] {
2377            Item::Struct(d) => {
2378                assert_eq!(d.name, "Point");
2379                assert_eq!(d.fields.len(), 2);
2380                assert_eq!(d.fields[0].name, "x");
2381                assert!(matches!(
2382                    d.fields[0].ty,
2383                    TypeExpr::Primitive {
2384                        kind: PrimType::F64,
2385                        ..
2386                    }
2387                ));
2388                assert_eq!(d.fields[1].name, "y");
2389            }
2390            other => panic!("expected Struct, got {other:?}"),
2391        }
2392    }
2393
2394    #[test]
2395    fn a_struct_with_a_trailing_comma_parses() {
2396        let ast = parse_ok("struct S { x: i64, y: i64, }");
2397        match &ast[0] {
2398            Item::Struct(d) => assert_eq!(d.fields.len(), 2),
2399            other => panic!("expected Struct, got {other:?}"),
2400        }
2401    }
2402
2403    #[test]
2404    fn an_enum_with_data_carrying_variants_parses() {
2405        let ast = parse_ok("enum Shape { Circle(f64), Rect(f64, f64), Triangle(f64, f64, f64) }");
2406        match &ast[0] {
2407            Item::Enum(d) => {
2408                assert_eq!(d.name, "Shape");
2409                assert_eq!(d.variants.len(), 3);
2410                assert_eq!(d.variants[0].name, "Circle");
2411                assert_eq!(d.variants[0].fields.len(), 1);
2412                assert_eq!(d.variants[1].name, "Rect");
2413                assert_eq!(d.variants[1].fields.len(), 2);
2414                assert_eq!(d.variants[2].name, "Triangle");
2415                assert_eq!(d.variants[2].fields.len(), 3);
2416            }
2417            other => panic!("expected Enum, got {other:?}"),
2418        }
2419    }
2420
2421    #[test]
2422    fn an_enum_with_a_no_data_variant_parses() {
2423        let ast = parse_ok("enum E { A, B(i64), }");
2424        match &ast[0] {
2425            Item::Enum(d) => {
2426                assert_eq!(d.variants.len(), 2);
2427                assert_eq!(d.variants[0].name, "A");
2428                assert!(d.variants[0].fields.is_empty());
2429                assert_eq!(d.variants[1].fields.len(), 1);
2430            }
2431            other => panic!("expected Enum, got {other:?}"),
2432        }
2433    }
2434
2435    #[test]
2436    fn an_interface_with_method_signatures_parses() {
2437        let ast = parse_ok("interface Printable { fn to_string(self) -> str }");
2438        match &ast[0] {
2439            Item::Interface(d) => {
2440                assert_eq!(d.name, "Printable");
2441                assert_eq!(d.methods.len(), 1);
2442                assert_eq!(d.methods[0].name, "to_string");
2443                assert_eq!(d.methods[0].params.len(), 1);
2444                assert!(d.methods[0].params[0].is_self);
2445                assert!(matches!(
2446                    d.methods[0].ret_ty,
2447                    Some(TypeExpr::Primitive {
2448                        kind: PrimType::Str,
2449                        ..
2450                    })
2451                ));
2452            }
2453            other => panic!("expected Interface, got {other:?}"),
2454        }
2455    }
2456
2457    #[test]
2458    fn an_interface_with_a_trailing_comma_parses() {
2459        let ast = parse_ok("interface I { fn m(self) -> str, }");
2460        match &ast[0] {
2461            Item::Interface(d) => assert_eq!(d.methods.len(), 1),
2462            other => panic!("expected Interface, got {other:?}"),
2463        }
2464    }
2465
2466    #[test]
2467    fn interface_method_sig_span_covers_effect_when_no_return_type() {
2468        // regression: when a method has `is io` but no `-> T`, the span was
2469        // collapsing to just the `fn` keyword. the span must cover the full
2470        // signature including the effect keyword.
2471        let src = "interface I { fn m(self) is io }";
2472        let ast = parse_ok(src);
2473        match &ast[0] {
2474            Item::Interface(d) => {
2475                let sig = &d.methods[0];
2476                assert_eq!(sig.effect, Some(Effect::Io));
2477                assert!(sig.ret_ty.is_none());
2478                // span must start at `fn` and end at or after `io`.
2479                // "fn m(self) is io" is 16 bytes wide; the `fn` starts at
2480                // byte 14 (after "interface I { "). the span must be wider
2481                // than a single token (fn = 2 bytes).
2482                assert!(
2483                    sig.span.len > 2,
2484                    "span covers only the fn keyword (len={}), expected full sig",
2485                    sig.span.len
2486                );
2487                // the span text should cover at least up to `io`.
2488                let sig_text = sig.span.slice(src);
2489                assert!(
2490                    sig_text.ends_with("io"),
2491                    "sig span text should end with 'io', got {sig_text:?}"
2492                );
2493            }
2494            other => panic!("expected Interface, got {other:?}"),
2495        }
2496    }
2497
2498    #[test]
2499    fn multiple_items_parse_in_source_order() {
2500        let ast = parse_ok("fn f() {} struct S { x: i64 } enum E { A } fn g() {}");
2501        assert_eq!(ast.len(), 4);
2502        assert!(matches!(ast[0], Item::Fn(_)));
2503        assert!(matches!(ast[1], Item::Struct(_)));
2504        assert!(matches!(ast[2], Item::Enum(_)));
2505        assert!(matches!(ast[3], Item::Fn(_)));
2506    }
2507
2508    // ---- cursor helpers -----------------------------------------------------
2509
2510    #[test]
2511    fn advance_does_not_step_past_eof() {
2512        // the cursor stays on Eof regardless of how many times advance is
2513        // called -- the invariant the rest of the parser relies on.
2514        let toks = lex("");
2515        let mut p = Parser {
2516            tokens: &toks,
2517            pos: 0,
2518            depth: 0,
2519            open_delims: Vec::new(),
2520        };
2521        assert!(matches!(p.peek(), TokenKind::Eof));
2522        for _ in 0..10 {
2523            p.advance();
2524        }
2525        assert!(matches!(p.peek(), TokenKind::Eof));
2526    }
2527
2528    #[test]
2529    fn check_eat_and_expect_compose() {
2530        // a stream of `+ - ;`: check sees the +, eat consumes it, expect
2531        // consumes the -, then expect on `;` succeeds, leaving Eof.
2532        let toks = lex("+ - ;");
2533        let mut p = Parser {
2534            tokens: &toks,
2535            pos: 0,
2536            depth: 0,
2537            open_delims: Vec::new(),
2538        };
2539        assert!(p.check(&TokenKind::Plus));
2540        assert!(!p.check(&TokenKind::Minus));
2541        assert!(p.eat(&TokenKind::Plus));
2542        assert!(!p.eat(&TokenKind::Plus)); // already consumed
2543        p.expect(&TokenKind::Minus)
2544            .expect("the `-` should be there");
2545        p.expect(&TokenKind::Semi).expect("the `;` should be there");
2546        assert!(matches!(p.peek(), TokenKind::Eof));
2547    }
2548
2549    #[test]
2550    fn expect_on_wrong_kind_returns_unexpected_token() {
2551        // expecting `)` when the cursor is on `,` produces UnexpectedToken
2552        // with the comma's span and the right expected/found data.
2553        let toks = lex(",");
2554        let mut p = Parser {
2555            tokens: &toks,
2556            pos: 0,
2557            depth: 0,
2558            open_delims: Vec::new(),
2559        };
2560        match p.expect(&TokenKind::RParen) {
2561            Err(QalaError::UnexpectedToken {
2562                span,
2563                expected,
2564                found,
2565            }) => {
2566                assert_eq!(span, Span::new(0, 1));
2567                assert_eq!(expected, vec![TokenKind::RParen]);
2568                assert_eq!(found, TokenKind::Comma);
2569            }
2570            other => panic!("expected UnexpectedToken, got {other:?}"),
2571        }
2572    }
2573
2574    #[test]
2575    fn expect_at_eof_returns_unexpected_eof_with_a_zero_width_span() {
2576        // "+" lexes to [Plus, Eof]; advance past the +, then expect a `;`.
2577        // The error should be UnexpectedEof, span zero-width right after the +.
2578        let toks = lex("+");
2579        let mut p = Parser {
2580            tokens: &toks,
2581            pos: 0,
2582            depth: 0,
2583            open_delims: Vec::new(),
2584        };
2585        p.advance(); // consume the `+`
2586        match p.expect(&TokenKind::Semi) {
2587            Err(QalaError::UnexpectedEof { span, expected }) => {
2588                // the `+` ends at byte 1; the EOF span is 1..1 (zero-width).
2589                assert_eq!(span, Span::new(1, 0));
2590                assert_eq!(expected, vec![TokenKind::Semi]);
2591            }
2592            other => panic!("expected UnexpectedEof, got {other:?}"),
2593        }
2594    }
2595
2596    #[test]
2597    fn unexpected_at_eof_of_empty_source_is_a_zero_zero_span() {
2598        // an empty source: the EOF span falls back to 0..0 because there is
2599        // no previous token to compute from.
2600        let toks = lex("");
2601        let p = Parser {
2602            tokens: &toks,
2603            pos: 0,
2604            depth: 0,
2605            open_delims: Vec::new(),
2606        };
2607        match p.unexpected(&[TokenKind::Fn]) {
2608            QalaError::UnexpectedEof { span, .. } => {
2609                assert_eq!(span, Span::new(0, 0));
2610            }
2611            other => panic!("expected UnexpectedEof, got {other:?}"),
2612        }
2613    }
2614
2615    #[test]
2616    fn expect_ident_returns_name_and_span() {
2617        let toks = lex("foo");
2618        let mut p = Parser {
2619            tokens: &toks,
2620            pos: 0,
2621            depth: 0,
2622            open_delims: Vec::new(),
2623        };
2624        let (name, span) = p.expect_ident().expect("expected ident");
2625        assert_eq!(name, "foo");
2626        assert_eq!(span, Span::new(0, 3));
2627        // expecting again at EOF gives an UnexpectedEof.
2628        match p.expect_ident() {
2629            Err(QalaError::UnexpectedEof { .. }) => {}
2630            other => panic!("expected UnexpectedEof, got {other:?}"),
2631        }
2632    }
2633
2634    #[test]
2635    fn expect_ident_on_a_keyword_returns_unexpected_token() {
2636        // `fn` is a keyword; expecting an ident there should error, not bind
2637        // the keyword as a name.
2638        let toks = lex("fn");
2639        let mut p = Parser {
2640            tokens: &toks,
2641            pos: 0,
2642            depth: 0,
2643            open_delims: Vec::new(),
2644        };
2645        match p.expect_ident() {
2646            Err(QalaError::UnexpectedToken { found, .. }) => {
2647                assert_eq!(found, TokenKind::Fn);
2648            }
2649            other => panic!("expected UnexpectedToken, got {other:?}"),
2650        }
2651    }
2652
2653    // ---- depth guard --------------------------------------------------------
2654
2655    #[test]
2656    fn the_depth_guard_returns_a_clean_error_past_the_limit() {
2657        // enter() can be called MAX_DEPTH times without error; one more call
2658        // returns QalaError::Parse with a message mentioning deep nesting.
2659        let toks = lex("");
2660        let mut p = Parser {
2661            tokens: &toks,
2662            pos: 0,
2663            depth: 0,
2664            open_delims: Vec::new(),
2665        };
2666        for _ in 0..MAX_DEPTH {
2667            p.enter().expect("under the limit");
2668        }
2669        match p.enter() {
2670            Err(QalaError::Parse { message, .. }) => {
2671                assert!(message.contains("deeply"), "message was {message:?}");
2672            }
2673            other => panic!("expected QalaError::Parse, got {other:?}"),
2674        }
2675    }
2676
2677    #[test]
2678    fn exit_undoes_enter_so_balanced_pairs_reset_the_counter() {
2679        let toks = lex("");
2680        let mut p = Parser {
2681            tokens: &toks,
2682            pos: 0,
2683            depth: 0,
2684            open_delims: Vec::new(),
2685        };
2686        for _ in 0..(MAX_DEPTH / 2) {
2687            p.enter().expect("under the limit");
2688            p.exit();
2689        }
2690        assert_eq!(p.depth, 0);
2691        // a long balanced run does not trip the guard.
2692        for _ in 0..(MAX_DEPTH * 10) {
2693            p.enter().expect("under the limit -- balanced");
2694            p.exit();
2695        }
2696        assert_eq!(p.depth, 0);
2697    }
2698
2699    // ---- delimited list -----------------------------------------------------
2700    //
2701    // delimited_list is exercised end-to-end through the expression engine
2702    // (Task 3) on `(args)`, `[elems]`, struct literal `{ fields }`, match arms,
2703    // etc. it is also unit-tested here against a tiny "parse a list of idents"
2704    // closure to pin the open / sep / close behaviour and the "blame the
2705    // opener" rule independently of any expression machinery.
2706
2707    #[test]
2708    fn delimited_list_handles_empty_and_trailing_separators() {
2709        // empty `()`.
2710        let toks = lex("()");
2711        let mut p = Parser {
2712            tokens: &toks,
2713            pos: 0,
2714            depth: 0,
2715            open_delims: Vec::new(),
2716        };
2717        let (items, close): (Vec<(String, Span)>, Span) = p
2718            .delimited_list(
2719                TokenKind::LParen,
2720                TokenKind::RParen,
2721                TokenKind::Comma,
2722                |q| q.expect_ident(),
2723            )
2724            .expect("empty list parses");
2725        assert!(items.is_empty());
2726        assert_eq!(close, Span::new(1, 1));
2727
2728        // a list with a trailing comma: `(a, b,)`.
2729        let toks = lex("(a, b,)");
2730        let mut p = Parser {
2731            tokens: &toks,
2732            pos: 0,
2733            depth: 0,
2734            open_delims: Vec::new(),
2735        };
2736        let (items, _): (Vec<(String, Span)>, Span) = p
2737            .delimited_list(
2738                TokenKind::LParen,
2739                TokenKind::RParen,
2740                TokenKind::Comma,
2741                |q| q.expect_ident(),
2742            )
2743            .expect("trailing comma OK");
2744        assert_eq!(items.len(), 2);
2745        assert_eq!(items[0].0, "a");
2746        assert_eq!(items[1].0, "b");
2747    }
2748
2749    #[test]
2750    fn delimited_list_blames_the_opener_on_a_missing_closer() {
2751        // `(a, b` without the `)`: the error's span is the opening `(`.
2752        let toks = lex("(a, b");
2753        let mut p = Parser {
2754            tokens: &toks,
2755            pos: 0,
2756            depth: 0,
2757            open_delims: Vec::new(),
2758        };
2759        match p.delimited_list(
2760            TokenKind::LParen,
2761            TokenKind::RParen,
2762            TokenKind::Comma,
2763            |q| q.expect_ident(),
2764        ) {
2765            Err(QalaError::UnclosedDelimiter {
2766                span,
2767                opener,
2768                found,
2769            }) => {
2770                assert_eq!(span, Span::new(0, 1));
2771                assert_eq!(opener, TokenKind::LParen);
2772                assert_eq!(found, TokenKind::Eof);
2773            }
2774            other => panic!("expected UnclosedDelimiter, got {other:?}"),
2775        }
2776    }
2777
2778    #[test]
2779    fn delimited_list_blames_the_opener_on_a_wrong_closer() {
2780        // `(a]`: the wrong closer. blame the `(`.
2781        let toks = lex("(a]");
2782        let mut p = Parser {
2783            tokens: &toks,
2784            pos: 0,
2785            depth: 0,
2786            open_delims: Vec::new(),
2787        };
2788        match p.delimited_list(
2789            TokenKind::LParen,
2790            TokenKind::RParen,
2791            TokenKind::Comma,
2792            |q| q.expect_ident(),
2793        ) {
2794            Err(QalaError::UnclosedDelimiter {
2795                span,
2796                opener,
2797                found,
2798            }) => {
2799                assert_eq!(span, Span::new(0, 1));
2800                assert_eq!(opener, TokenKind::LParen);
2801                assert_eq!(found, TokenKind::RBracket);
2802            }
2803            other => panic!("expected UnclosedDelimiter, got {other:?}"),
2804        }
2805    }
2806
2807    // ---- determinism --------------------------------------------------------
2808
2809    #[test]
2810    fn parsing_is_deterministic() {
2811        // the parser holds no global state and uses no maps with hashing-order
2812        // dependence (the precedence tables are `match` arms, not HashMaps;
2813        // TokenKind is not Eq, so it could not key one anyway).
2814        let src = "";
2815        let a = Parser::parse(&lex(src));
2816        let b = Parser::parse(&lex(src));
2817        assert_eq!(a, b);
2818        // also stable across a genuine error case: two runs on the same bad
2819        // input must produce byte-equal errors (no pointer addresses, no OS
2820        // state, no random salt leaking into the error value).
2821        let bad = "fn f("; // unclosed paren -- a real parse error
2822        assert_eq!(Parser::parse(&lex(bad)), Parser::parse(&lex(bad)));
2823    }
2824
2825    // ---- expression engine: precedence table --------------------------------
2826    //
2827    // one assertion per precedence level (success criterion 3), each pinning
2828    // the parsed-tree shape so a tweak to `infix_bp`/`prefix_bp`/`postfix_bp`
2829    // that drops a level by one bp is caught.
2830
2831    /// helper: a fresh integer literal at any span; used in `matches!` arms.
2832    /// the precedence tests only care about the SHAPE of the tree, not the
2833    /// spans, so destructuring lets us ignore them.
2834    fn is_ident(e: &Expr, want: &str) -> bool {
2835        matches!(e, Expr::Ident { name, .. } if name == want)
2836    }
2837
2838    #[test]
2839    fn mul_binds_tighter_than_add() {
2840        // `1 + 2 * 3` -> Binary(Add, Int(1), Binary(Mul, Int(2), Int(3))).
2841        let e = expr_of("1 + 2 * 3");
2842        match e {
2843            Expr::Binary {
2844                op: BinOp::Add,
2845                lhs,
2846                rhs,
2847                ..
2848            } => {
2849                assert!(matches!(*lhs, Expr::Int { value: 1, .. }));
2850                match *rhs {
2851                    Expr::Binary {
2852                        op: BinOp::Mul,
2853                        lhs,
2854                        rhs,
2855                        ..
2856                    } => {
2857                        assert!(matches!(*lhs, Expr::Int { value: 2, .. }));
2858                        assert!(matches!(*rhs, Expr::Int { value: 3, .. }));
2859                    }
2860                    other => panic!("expected mul-binary on the right, got {other:?}"),
2861                }
2862            }
2863            other => panic!("expected add-binary, got {other:?}"),
2864        }
2865        // `1 * 2 + 3` -> Binary(Add, Binary(Mul, Int(1), Int(2)), Int(3)).
2866        let e = expr_of("1 * 2 + 3");
2867        match e {
2868            Expr::Binary {
2869                op: BinOp::Add,
2870                lhs,
2871                rhs,
2872                ..
2873            } => {
2874                assert!(matches!(*lhs, Expr::Binary { op: BinOp::Mul, .. }));
2875                assert!(matches!(*rhs, Expr::Int { value: 3, .. }));
2876            }
2877            other => panic!("expected add-binary at the top, got {other:?}"),
2878        }
2879    }
2880
2881    #[test]
2882    fn and_binds_tighter_than_or() {
2883        // `a || b && c` -> Binary(Or, Ident(a), Binary(And, b, c)).
2884        let e = expr_of("a || b && c");
2885        match e {
2886            Expr::Binary {
2887                op: BinOp::Or,
2888                lhs,
2889                rhs,
2890                ..
2891            } => {
2892                assert!(is_ident(&lhs, "a"));
2893                match *rhs {
2894                    Expr::Binary {
2895                        op: BinOp::And,
2896                        lhs,
2897                        rhs,
2898                        ..
2899                    } => {
2900                        assert!(is_ident(&lhs, "b"));
2901                        assert!(is_ident(&rhs, "c"));
2902                    }
2903                    other => panic!("expected and-binary on the right, got {other:?}"),
2904                }
2905            }
2906            other => panic!("expected or-binary at the top, got {other:?}"),
2907        }
2908        // `a && b || c` -> Binary(Or, Binary(And, a, b), c).
2909        let e = expr_of("a && b || c");
2910        match e {
2911            Expr::Binary {
2912                op: BinOp::Or,
2913                lhs,
2914                rhs,
2915                ..
2916            } => {
2917                assert!(matches!(*lhs, Expr::Binary { op: BinOp::And, .. }));
2918                assert!(is_ident(&rhs, "c"));
2919            }
2920            other => panic!("expected or-binary at the top, got {other:?}"),
2921        }
2922    }
2923
2924    #[test]
2925    fn comparison_binds_tighter_than_logical_and() {
2926        // `a == b && c` -> Binary(And, Binary(Eq, a, b), c).
2927        let e = expr_of("a == b && c");
2928        match e {
2929            Expr::Binary {
2930                op: BinOp::And,
2931                lhs,
2932                rhs,
2933                ..
2934            } => {
2935                assert!(matches!(*lhs, Expr::Binary { op: BinOp::Eq, .. }));
2936                assert!(is_ident(&rhs, "c"));
2937            }
2938            other => panic!("expected and-binary, got {other:?}"),
2939        }
2940    }
2941
2942    #[test]
2943    fn postfix_dot_binds_tighter_than_prefix_minus() {
2944        // `-a.b` -> Unary(Neg, FieldAccess(Ident(a), "b")).
2945        let e = expr_of("-a.b");
2946        match e {
2947            Expr::Unary {
2948                op: UnaryOp::Neg,
2949                operand,
2950                ..
2951            } => match *operand {
2952                Expr::FieldAccess { obj, name, .. } => {
2953                    assert!(is_ident(&obj, "a"));
2954                    assert_eq!(name, "b");
2955                }
2956                other => panic!("expected field access inside unary, got {other:?}"),
2957            },
2958            other => panic!("expected unary at the top, got {other:?}"),
2959        }
2960    }
2961
2962    #[test]
2963    fn comptime_is_a_prefix_at_unary_precedence() {
2964        // `comptime a + b` -> Binary(Add, Comptime(Ident(a)), Ident(b)).
2965        let e = expr_of("comptime a + b");
2966        match e {
2967            Expr::Binary {
2968                op: BinOp::Add,
2969                lhs,
2970                rhs,
2971                ..
2972            } => {
2973                assert!(matches!(*lhs, Expr::Comptime { .. }));
2974                assert!(is_ident(&rhs, "b"));
2975                if let Expr::Comptime { body, .. } = *lhs {
2976                    assert!(is_ident(&body, "a"));
2977                }
2978            }
2979            other => panic!("expected add-binary, got {other:?}"),
2980        }
2981        // `comptime (a + b)` -- the whole binary is inside the comptime.
2982        let e = expr_of("comptime (a + b)");
2983        match e {
2984            Expr::Comptime { body, .. } => match *body {
2985                Expr::Paren { inner, .. } => {
2986                    assert!(matches!(*inner, Expr::Binary { op: BinOp::Add, .. }));
2987                }
2988                other => panic!("expected paren inside comptime, got {other:?}"),
2989            },
2990            other => panic!("expected comptime at the top, got {other:?}"),
2991        }
2992        // `comptime { 1 }`: the operand is a block expression.
2993        let e = expr_of("comptime { 1 }");
2994        match e {
2995            Expr::Comptime { body, .. } => {
2996                assert!(matches!(*body, Expr::Block { .. }));
2997            }
2998            other => panic!("expected comptime at the top, got {other:?}"),
2999        }
3000    }
3001
3002    #[test]
3003    fn pipeline_is_left_associative_and_accepts_call_or_bare_callable() {
3004        // `a |> f() |> g()` -> Pipeline(Pipeline(a, Call(f)), Call(g)).
3005        let e = expr_of("a |> f() |> g()");
3006        match e {
3007            Expr::Pipeline { lhs, call, .. } => {
3008                assert!(matches!(*call, Expr::Call { .. }));
3009                match *lhs {
3010                    Expr::Pipeline { lhs, call, .. } => {
3011                        assert!(is_ident(&lhs, "a"));
3012                        assert!(matches!(*call, Expr::Call { .. }));
3013                    }
3014                    other => panic!("expected nested pipeline on the left, got {other:?}"),
3015                }
3016            }
3017            other => panic!("expected pipeline at the top, got {other:?}"),
3018        }
3019        // bare-ident rhs (per `pipeline.qala`): `5 |> double`.
3020        let e = expr_of("5 |> double");
3021        match e {
3022            Expr::Pipeline { lhs, call, .. } => {
3023                assert!(matches!(*lhs, Expr::Int { value: 5, .. }));
3024                assert!(is_ident(&call, "double"));
3025            }
3026            other => panic!("expected pipeline, got {other:?}"),
3027        }
3028    }
3029
3030    #[test]
3031    fn or_fallback_is_left_associative_and_loosest() {
3032        // `x or y or z` -> OrElse(OrElse(x, y), z).
3033        let e = expr_of("x or y or z");
3034        match e {
3035            Expr::OrElse { expr, fallback, .. } => {
3036                assert!(is_ident(&fallback, "z"));
3037                match *expr {
3038                    Expr::OrElse { expr, fallback, .. } => {
3039                        assert!(is_ident(&expr, "x"));
3040                        assert!(is_ident(&fallback, "y"));
3041                    }
3042                    other => panic!("expected nested OrElse on the left, got {other:?}"),
3043                }
3044            }
3045            other => panic!("expected OrElse, got {other:?}"),
3046        }
3047        // `or` is looser than `|>`: `x |> f() or g` -> OrElse(Pipeline(...), g).
3048        let e = expr_of("x |> f() or g");
3049        match e {
3050            Expr::OrElse { expr, fallback, .. } => {
3051                assert!(matches!(*expr, Expr::Pipeline { .. }));
3052                assert!(is_ident(&fallback, "g"));
3053            }
3054            other => panic!("expected OrElse at the top, got {other:?}"),
3055        }
3056    }
3057
3058    #[test]
3059    fn question_mark_is_postfix_at_tightest_tier() {
3060        // `f()?.g` -> FieldAccess(Try(Call(f)), "g").
3061        let e = expr_of("f()?.g");
3062        match e {
3063            Expr::FieldAccess { obj, name, .. } => {
3064                assert_eq!(name, "g");
3065                match *obj {
3066                    Expr::Try { expr, .. } => {
3067                        assert!(matches!(*expr, Expr::Call { .. }));
3068                    }
3069                    other => panic!("expected Try on the obj, got {other:?}"),
3070                }
3071            }
3072            other => panic!("expected FieldAccess, got {other:?}"),
3073        }
3074        // `a + b?` -> Binary(Add, a, Try(b)).
3075        let e = expr_of("a + b?");
3076        match e {
3077            Expr::Binary {
3078                op: BinOp::Add,
3079                lhs,
3080                rhs,
3081                ..
3082            } => {
3083                assert!(is_ident(&lhs, "a"));
3084                match *rhs {
3085                    Expr::Try { expr, .. } => assert!(is_ident(&expr, "b")),
3086                    other => panic!("expected Try on the rhs, got {other:?}"),
3087                }
3088            }
3089            other => panic!("expected add-binary, got {other:?}"),
3090        }
3091        // `parse(s)?` -- Try on a call.
3092        let e = expr_of("parse(s)?");
3093        match e {
3094            Expr::Try { expr, .. } => assert!(matches!(*expr, Expr::Call { .. })),
3095            other => panic!("expected Try, got {other:?}"),
3096        }
3097    }
3098
3099    #[test]
3100    fn ranges_are_infix_at_a_tier_above_plus() {
3101        // `1..n+1` -> Range(Int(1), Binary(Add, Ident(n), Int(1)), false).
3102        let e = expr_of("1..n+1");
3103        match e {
3104            Expr::Range {
3105                start,
3106                end,
3107                inclusive,
3108                ..
3109            } => {
3110                assert!(!inclusive);
3111                let s = start.expect("start present");
3112                assert!(matches!(*s, Expr::Int { value: 1, .. }));
3113                let en = end.expect("end present");
3114                assert!(matches!(*en, Expr::Binary { op: BinOp::Add, .. }));
3115            }
3116            other => panic!("expected Range, got {other:?}"),
3117        }
3118        // `0..=15` -- inclusive range.
3119        let e = expr_of("0..=15");
3120        match e {
3121            Expr::Range { inclusive, .. } => assert!(inclusive),
3122            other => panic!("expected Range, got {other:?}"),
3123        }
3124    }
3125
3126    #[test]
3127    fn chained_range_is_a_parse_error() {
3128        // `0..1..2` must fail: ranges are non-associative (rbp 61 > lbp 60,
3129        // so the Pratt loop stops after the first `..` and `1` is returned as
3130        // the rhs). the full program parser then sees `..2` at item level and
3131        // returns an error.
3132        parse_err("fn f() { 0..1..2 }");
3133    }
3134
3135    #[test]
3136    fn additive_is_left_associative() {
3137        // `a + b - c` -> Binary(Sub, Binary(Add, a, b), c).
3138        let e = expr_of("a + b - c");
3139        match e {
3140            Expr::Binary {
3141                op: BinOp::Sub,
3142                lhs,
3143                rhs,
3144                ..
3145            } => {
3146                assert!(matches!(*lhs, Expr::Binary { op: BinOp::Add, .. }));
3147                assert!(is_ident(&rhs, "c"));
3148            }
3149            other => panic!("expected sub-binary at the top, got {other:?}"),
3150        }
3151    }
3152
3153    // ---- expression engine: primaries ---------------------------------------
3154
3155    #[test]
3156    fn literals_parse_to_their_decoded_variants() {
3157        assert!(matches!(expr_of("42"), Expr::Int { value: 42, .. }));
3158        assert!(matches!(expr_of("3.14"), Expr::Float { .. }));
3159        assert!(matches!(expr_of("b'A'"), Expr::Byte { value: 65, .. }));
3160        assert!(matches!(expr_of("\"hello\""), Expr::Str { .. }));
3161        assert!(matches!(expr_of("true"), Expr::Bool { value: true, .. }));
3162        assert!(matches!(expr_of("false"), Expr::Bool { value: false, .. }));
3163        assert!(is_ident(&expr_of("foo"), "foo"));
3164    }
3165
3166    #[test]
3167    fn calls_parse_with_args_and_trailing_comma() {
3168        // `f(1, 2)` -> Call(Ident(f), [Int(1), Int(2)]).
3169        match expr_of("f(1, 2)") {
3170            Expr::Call { callee, args, .. } => {
3171                assert!(is_ident(&callee, "f"));
3172                assert_eq!(args.len(), 2);
3173            }
3174            other => panic!("expected Call, got {other:?}"),
3175        }
3176        // trailing comma OK.
3177        match expr_of("f(1, 2,)") {
3178            Expr::Call { args, .. } => assert_eq!(args.len(), 2),
3179            other => panic!("expected Call, got {other:?}"),
3180        }
3181        // a zero-arg call.
3182        match expr_of("g()") {
3183            Expr::Call { args, .. } => assert!(args.is_empty()),
3184            other => panic!("expected Call, got {other:?}"),
3185        }
3186    }
3187
3188    #[test]
3189    fn method_call_is_distinct_from_field_access() {
3190        // `p.area()` -- a method call.
3191        match expr_of("p.area()") {
3192            Expr::MethodCall {
3193                receiver,
3194                name,
3195                args,
3196                ..
3197            } => {
3198                assert!(is_ident(&receiver, "p"));
3199                assert_eq!(name, "area");
3200                assert!(args.is_empty());
3201            }
3202            other => panic!("expected MethodCall, got {other:?}"),
3203        }
3204        // `p.x` -- a field access (no `(` after).
3205        match expr_of("p.x") {
3206            Expr::FieldAccess { obj, name, .. } => {
3207                assert!(is_ident(&obj, "p"));
3208                assert_eq!(name, "x");
3209            }
3210            other => panic!("expected FieldAccess, got {other:?}"),
3211        }
3212    }
3213
3214    #[test]
3215    fn index_indexes_into_an_array() {
3216        match expr_of("xs[0]") {
3217            Expr::Index { obj, index, .. } => {
3218                assert!(is_ident(&obj, "xs"));
3219                assert!(matches!(*index, Expr::Int { value: 0, .. }));
3220            }
3221            other => panic!("expected Index, got {other:?}"),
3222        }
3223    }
3224
3225    #[test]
3226    fn postfix_chain_nests_correctly() {
3227        // `f()?.g(1)[2]?` -> Try(Index(MethodCall(FieldAccess(Try(Call(f)),"g"), "g", [1]), 2))
3228        // wait -- it should be: Try(Index(MethodCall(Try(Call(f)),"g",[1]), 2)).
3229        let e = expr_of("f()?.g(1)[2]?");
3230        match e {
3231            Expr::Try { expr, .. } => match *expr {
3232                Expr::Index { obj, index, .. } => {
3233                    assert!(matches!(*index, Expr::Int { value: 2, .. }));
3234                    match *obj {
3235                        Expr::MethodCall {
3236                            receiver,
3237                            name,
3238                            args,
3239                            ..
3240                        } => {
3241                            assert_eq!(name, "g");
3242                            assert_eq!(args.len(), 1);
3243                            assert!(matches!(*receiver, Expr::Try { .. }));
3244                        }
3245                        other => panic!("expected MethodCall, got {other:?}"),
3246                    }
3247                }
3248                other => panic!("expected Index, got {other:?}"),
3249            },
3250            other => panic!("expected Try at the top, got {other:?}"),
3251        }
3252    }
3253
3254    #[test]
3255    fn array_literal_parses_with_elements_and_trailing_comma() {
3256        match expr_of("[1, 2, 3]") {
3257            Expr::ArrayLit { elems, .. } => assert_eq!(elems.len(), 3),
3258            other => panic!("expected ArrayLit, got {other:?}"),
3259        }
3260        // trailing comma OK.
3261        match expr_of("[1, 2, 3,]") {
3262            Expr::ArrayLit { elems, .. } => assert_eq!(elems.len(), 3),
3263            other => panic!("expected ArrayLit, got {other:?}"),
3264        }
3265        // empty array.
3266        match expr_of("[]") {
3267            Expr::ArrayLit { elems, .. } => assert!(elems.is_empty()),
3268            other => panic!("expected ArrayLit, got {other:?}"),
3269        }
3270    }
3271
3272    #[test]
3273    fn array_repeat_parses_as_value_semi_count() {
3274        match expr_of("[0; 100]") {
3275            Expr::ArrayRepeat { value, count, .. } => {
3276                assert!(matches!(*value, Expr::Int { value: 0, .. }));
3277                assert!(matches!(*count, Expr::Int { value: 100, .. }));
3278            }
3279            other => panic!("expected ArrayRepeat, got {other:?}"),
3280        }
3281    }
3282
3283    #[test]
3284    fn paren_versus_tuple_distinguished_by_a_trailing_comma() {
3285        // `(1 + 2)` -- a paren around a binary; not a tuple.
3286        match expr_of("(1 + 2)") {
3287            Expr::Paren { inner, .. } => {
3288                assert!(matches!(*inner, Expr::Binary { op: BinOp::Add, .. }));
3289            }
3290            other => panic!("expected Paren, got {other:?}"),
3291        }
3292        // `(1, 2)` -- a tuple of two ints.
3293        match expr_of("(1, 2)") {
3294            Expr::Tuple { elems, .. } => assert_eq!(elems.len(), 2),
3295            other => panic!("expected Tuple, got {other:?}"),
3296        }
3297        // `(1,)` -- a one-tuple (the trailing comma is significant).
3298        match expr_of("(1,)") {
3299            Expr::Tuple { elems, .. } => assert_eq!(elems.len(), 1),
3300            other => panic!("expected Tuple, got {other:?}"),
3301        }
3302        // `()` -- a zero-tuple.
3303        match expr_of("()") {
3304            Expr::Tuple { elems, .. } => assert!(elems.is_empty()),
3305            other => panic!("expected Tuple, got {other:?}"),
3306        }
3307    }
3308
3309    #[test]
3310    fn struct_literal_parses_with_field_inits_and_trailing_comma() {
3311        // `Point { x: 1.0, y: 2.0 }`.
3312        match expr_of("Point { x: 1.0, y: 2.0 }") {
3313            Expr::StructLit { name, fields, .. } => {
3314                assert_eq!(name, "Point");
3315                assert_eq!(fields.len(), 2);
3316                assert_eq!(fields[0].name, "x");
3317                assert_eq!(fields[1].name, "y");
3318            }
3319            other => panic!("expected StructLit, got {other:?}"),
3320        }
3321        // trailing comma OK.
3322        match expr_of("Point { x: 1.0, y: 2.0, }") {
3323            Expr::StructLit { fields, .. } => assert_eq!(fields.len(), 2),
3324            other => panic!("expected StructLit, got {other:?}"),
3325        }
3326    }
3327
3328    #[test]
3329    fn match_with_three_arms_two_expr_one_block() {
3330        // from pattern-matching.qala (areas).
3331        let src = "match s { Circle(r) => 3.14159 * r * r, Rect(w, h) => w * h, Triangle(a, b, c) => { sqrt(s) } }";
3332        match expr_of(src) {
3333            Expr::Match { arms, .. } => {
3334                assert_eq!(arms.len(), 3);
3335                assert!(matches!(arms[0].body, MatchArmBody::Expr(_)));
3336                assert!(matches!(arms[1].body, MatchArmBody::Expr(_)));
3337                assert!(matches!(arms[2].body, MatchArmBody::Block(_)));
3338                // each arm starts with a Variant pattern.
3339                for arm in &arms {
3340                    assert!(
3341                        matches!(arm.pattern, Pattern::Variant { .. }),
3342                        "expected Variant pattern, got {:?}",
3343                        arm.pattern
3344                    );
3345                }
3346            }
3347            other => panic!("expected Match, got {other:?}"),
3348        }
3349    }
3350
3351    #[test]
3352    fn match_with_guards_and_wildcard() {
3353        // from pattern-matching.qala (classify).
3354        let src =
3355            "match value { v if v > 0 => \"positive\", v if v < 0 => \"negative\", _ => \"zero\" }";
3356        match expr_of(src) {
3357            Expr::Match { arms, .. } => {
3358                assert_eq!(arms.len(), 3);
3359                assert!(arms[0].guard.is_some());
3360                assert!(arms[1].guard.is_some());
3361                assert!(arms[2].guard.is_none());
3362                assert!(matches!(arms[0].pattern, Pattern::Binding { .. }));
3363                assert!(matches!(arms[2].pattern, Pattern::Wildcard { .. }));
3364            }
3365            other => panic!("expected Match, got {other:?}"),
3366        }
3367    }
3368
3369    #[test]
3370    fn match_with_exactly_one_arm_and_no_trailing_comma() {
3371        // a one-arm match without a trailing comma after the only arm.
3372        match expr_of("match x { _ => 0 }") {
3373            Expr::Match { arms, .. } => assert_eq!(arms.len(), 1),
3374            other => panic!("expected Match, got {other:?}"),
3375        }
3376    }
3377
3378    #[test]
3379    fn interpolation_parses_embedded_expressions_into_parts() {
3380        // `"fib({i}) = {fibonacci(i)}"` -- the parts list alternates Literal
3381        // and Expr, starting and ending with a Literal (possibly empty).
3382        match expr_of("\"fib({i}) = {fibonacci(i)}\"") {
3383            Expr::Interpolation { parts, .. } => {
3384                // parts: [Literal("fib("), Expr(Ident i), Literal(") = "),
3385                //         Expr(Call(fibonacci,[Ident i])), Literal("")]
3386                assert_eq!(parts.len(), 5);
3387                assert!(matches!(parts[0], InterpPart::Literal(_)));
3388                match &parts[1] {
3389                    InterpPart::Expr(Expr::Ident { name, .. }) => assert_eq!(name, "i"),
3390                    other => panic!("expected Ident(i), got {other:?}"),
3391                }
3392                assert!(matches!(parts[2], InterpPart::Literal(_)));
3393                match &parts[3] {
3394                    InterpPart::Expr(Expr::Call { callee, args, .. }) => {
3395                        assert!(is_ident(callee, "fibonacci"));
3396                        assert_eq!(args.len(), 1);
3397                    }
3398                    other => panic!("expected Call(fibonacci, ...), got {other:?}"),
3399                }
3400                assert!(matches!(parts[4], InterpPart::Literal(_)));
3401            }
3402            other => panic!("expected Interpolation, got {other:?}"),
3403        }
3404    }
3405
3406    #[test]
3407    fn plain_string_with_no_interpolations_parses_to_expr_str() {
3408        // a string with no `{}` is a `TokenKind::Str`, not `StrStart` -- so
3409        // it lands in the `Str(_)` arm, not `parse_interpolation`.
3410        match expr_of("\"hello\"") {
3411            Expr::Str { value, .. } => assert_eq!(value, "hello"),
3412            other => panic!("expected Str, got {other:?}"),
3413        }
3414    }
3415
3416    // ---- expression engine: negatives ---------------------------------------
3417
3418    #[test]
3419    fn x_pipe_with_no_call_errors_on_the_pipe_span() {
3420        // `x |>` -- the rhs is missing; the error's span is the `|>` token.
3421        let err = expr_err("x |>");
3422        match err {
3423            QalaError::Parse { span, message } => {
3424                // the `|>` is at byte offset 2, length 2.
3425                assert_eq!(span, Span::new(2, 2));
3426                assert!(message.contains("`|>`"), "message was {message:?}");
3427            }
3428            other => panic!("expected QalaError::Parse, got {other:?}"),
3429        }
3430    }
3431
3432    #[test]
3433    fn x_pipe_with_a_non_callable_rhs_errors_on_the_pipe_span() {
3434        // `x |> 5` -- 5 is a literal, not callable. error span on the `|>`.
3435        let err = expr_err("x |> 5");
3436        match err {
3437            QalaError::Parse { span, message } => {
3438                assert_eq!(span, Span::new(2, 2));
3439                assert!(message.contains("`|>`"), "message was {message:?}");
3440            }
3441            other => panic!("expected QalaError::Parse, got {other:?}"),
3442        }
3443    }
3444
3445    #[test]
3446    fn match_arm_guard_ending_in_struct_literal_does_not_consume_arm_body_brace() {
3447        // the guard is parsed in NoStructLiteral context, so `Foo { y: 1 }`
3448        // in a guard would try to parse `Foo` as a struct literal and consume
3449        // the `{` that opens the arm body -- causing a parse failure. with the
3450        // fix, the guard stops before the `{` and the arm body parses correctly.
3451        // here the guard is a simple comparison that doesn't involve a struct
3452        // literal; this confirms the no-struct-literal context doesn't break
3453        // ordinary guards.
3454        let e = expr_of("match v { x if x > 0 => { 1 }, _ => { 0 } }");
3455        match e {
3456            Expr::Match { arms, .. } => {
3457                assert_eq!(arms.len(), 2);
3458                // first arm has a guard.
3459                assert!(arms[0].guard.is_some(), "expected guard on first arm");
3460                // second arm has no guard.
3461                assert!(arms[1].guard.is_none());
3462            }
3463            other => panic!("expected Match, got {other:?}"),
3464        }
3465        // a guard followed by `=>` then a block: the `{` must open the block,
3466        // not a struct literal.  use a name-only guard to confirm the `{` routing.
3467        let e = expr_of("match s { v if v => { 1 } }");
3468        match e {
3469            Expr::Match { arms, .. } => {
3470                assert_eq!(arms.len(), 1);
3471                assert!(matches!(arms[0].body, MatchArmBody::Block(_)));
3472            }
3473            other => panic!("expected Match, got {other:?}"),
3474        }
3475    }
3476
3477    #[test]
3478    fn match_with_zero_arms_errors() {
3479        let err = expr_err("match x { }");
3480        match err {
3481            QalaError::Parse { message, .. } => {
3482                assert!(
3483                    message.contains("at least one arm"),
3484                    "message was {message:?}"
3485                );
3486            }
3487            other => panic!("expected QalaError::Parse, got {other:?}"),
3488        }
3489    }
3490
3491    #[test]
3492    fn an_unclosed_paren_errors_on_the_open_paren() {
3493        // `(a + b` -- never closes; the error's span is the opening `(`.
3494        let err = expr_err("(a + b");
3495        match err {
3496            QalaError::UnclosedDelimiter { span, opener, .. } => {
3497                assert_eq!(span, Span::new(0, 1));
3498                assert_eq!(opener, TokenKind::LParen);
3499            }
3500            other => panic!("expected UnclosedDelimiter, got {other:?}"),
3501        }
3502    }
3503
3504    #[test]
3505    fn an_unclosed_bracket_errors_on_the_open_bracket() {
3506        // `[1, 2` -- never closes; the error's span is the opening `[`.
3507        let err = expr_err("[1, 2");
3508        match err {
3509            QalaError::UnclosedDelimiter { span, opener, .. } => {
3510                assert_eq!(span, Span::new(0, 1));
3511                assert_eq!(opener, TokenKind::LBracket);
3512            }
3513            other => panic!("expected UnclosedDelimiter, got {other:?}"),
3514        }
3515    }
3516
3517    #[test]
3518    fn the_assignment_token_is_not_an_expression_operator() {
3519        // `Eq` is statement syntax (`let x = ...`), not infix. an attempt to
3520        // parse `a = b` as an expression should stop at the `=`, leaving the
3521        // cursor parked there -- but `expr_of` asserts it consumed everything.
3522        // so use the raw parser and check the cursor.
3523        let toks = lex("a = b");
3524        let mut p = Parser {
3525            tokens: &toks,
3526            pos: 0,
3527            depth: 0,
3528            open_delims: Vec::new(),
3529        };
3530        let lhs = p.parse_expr().expect("expected ident a to parse");
3531        assert!(is_ident(&lhs, "a"));
3532        // the `=` was NOT consumed -- it is not in any binding-power table.
3533        assert!(matches!(p.peek(), TokenKind::Eq));
3534    }
3535
3536    #[test]
3537    fn the_binding_power_tables_omit_assignment() {
3538        // structural assertion: postfix_bp / prefix_bp / infix_bp all return
3539        // None for `Eq`, so the Pratt loop never consumes it.
3540        assert_eq!(postfix_bp(&TokenKind::Eq), None);
3541        assert_eq!(infix_bp(&TokenKind::Eq), None);
3542        // prefix_bp returns `((), u8::MAX)` for non-prefix tokens, a sentinel
3543        // -- it is only called after parse_primary already saw `Bang`/`Minus`/
3544        // `Comptime`, so an `Eq` cannot reach it. for documentation, assert
3545        // the sentinel:
3546        let ((), rbp) = prefix_bp(&TokenKind::Eq);
3547        assert_eq!(rbp, u8::MAX);
3548    }
3549
3550    #[test]
3551    fn postfix_bp_only_returns_some_for_dot_lparen_lbracket_question() {
3552        // pin the postfix table: the four tokens are postfix, every other
3553        // token is not.
3554        assert!(postfix_bp(&TokenKind::Dot).is_some());
3555        assert!(postfix_bp(&TokenKind::LParen).is_some());
3556        assert!(postfix_bp(&TokenKind::LBracket).is_some());
3557        assert!(postfix_bp(&TokenKind::Question).is_some());
3558        for k in [
3559            TokenKind::Plus,
3560            TokenKind::Minus,
3561            TokenKind::Star,
3562            TokenKind::PipeGt,
3563            TokenKind::Or,
3564            TokenKind::DotDot,
3565            TokenKind::DotDotEq,
3566            TokenKind::Eq,
3567            TokenKind::Comma,
3568            TokenKind::RBrace,
3569        ] {
3570            assert!(postfix_bp(&k).is_none(), "{k:?} should not be postfix");
3571        }
3572    }
3573
3574    #[test]
3575    fn infix_bp_includes_ranges_but_not_assignment() {
3576        // pin the infix table's key claims.
3577        assert!(infix_bp(&TokenKind::DotDot).is_some());
3578        assert!(infix_bp(&TokenKind::DotDotEq).is_some());
3579        assert!(infix_bp(&TokenKind::PipeGt).is_some());
3580        assert!(infix_bp(&TokenKind::Or).is_some());
3581        // `Eq` is absent.
3582        assert!(infix_bp(&TokenKind::Eq).is_none());
3583        // `?` is postfix, not infix.
3584        assert!(infix_bp(&TokenKind::Question).is_none());
3585    }
3586
3587    #[test]
3588    fn or_keyword_is_looser_than_pipe_gt() {
3589        // `or` has lower binding power than `|>`, so `|>` binds first.
3590        let (or_lbp, _) = infix_bp(&TokenKind::Or).unwrap();
3591        let (pipe_lbp, _) = infix_bp(&TokenKind::PipeGt).unwrap();
3592        assert!(
3593            or_lbp < pipe_lbp,
3594            "`or` ({or_lbp}) should be looser than `|>` ({pipe_lbp})"
3595        );
3596    }
3597
3598    #[test]
3599    fn comptime_prefix_bp_matches_other_unary_prefixes() {
3600        // `comptime`, `!`, `-` all bind at the same tier, so all three nest
3601        // identically with postfix operators.
3602        let ((), c) = prefix_bp(&TokenKind::Comptime);
3603        let ((), b) = prefix_bp(&TokenKind::Bang);
3604        let ((), m) = prefix_bp(&TokenKind::Minus);
3605        assert_eq!(c, b);
3606        assert_eq!(b, m);
3607    }
3608
3609    #[test]
3610    fn expression_parsing_is_deterministic_on_a_mixed_input() {
3611        // a single complex expression parses to identical trees across two
3612        // runs -- pins the "no hashing-order dependence" claim.
3613        let src = "1 + 2 * 3 |> f() or g()";
3614        assert_eq!(expr_of(src), expr_of(src));
3615    }
3616
3617    // ---- statements ---------------------------------------------------------
3618    //
3619    // helper: parse a statement via a fixture `fn f() { STMT }`, pull the
3620    // first statement out. avoids exposing `parse_stmt` directly.
3621    fn first_stmt_of(stmt_src: &str) -> Stmt {
3622        let program = format!("fn f() {{ {stmt_src} }}");
3623        let ast = parse_ok(&program);
3624        match ast.into_iter().next().expect("one item") {
3625            Item::Fn(d) => d.body.stmts.into_iter().next().expect("a statement"),
3626            other => panic!("expected fn item, got {other:?}"),
3627        }
3628    }
3629
3630    /// helper: parse the body of `fn f() { ... }` and return it whole.
3631    fn body_of(body_src: &str) -> Block {
3632        let program = format!("fn f() {{ {body_src} }}");
3633        let ast = parse_ok(&program);
3634        match ast.into_iter().next().expect("one item") {
3635            Item::Fn(d) => d.body,
3636            other => panic!("expected fn item, got {other:?}"),
3637        }
3638    }
3639
3640    #[test]
3641    fn let_without_mut_or_type_parses() {
3642        match first_stmt_of("let x = 5") {
3643            Stmt::Let {
3644                is_mut,
3645                name,
3646                ty,
3647                init,
3648                ..
3649            } => {
3650                assert!(!is_mut);
3651                assert_eq!(name, "x");
3652                assert!(ty.is_none());
3653                assert!(matches!(init, Expr::Int { value: 5, .. }));
3654            }
3655            other => panic!("expected Let, got {other:?}"),
3656        }
3657    }
3658
3659    #[test]
3660    fn let_mut_sets_is_mut_true() {
3661        match first_stmt_of("let mut y = 10") {
3662            Stmt::Let { is_mut, name, .. } => {
3663                assert!(is_mut);
3664                assert_eq!(name, "y");
3665            }
3666            other => panic!("expected Let, got {other:?}"),
3667        }
3668    }
3669
3670    #[test]
3671    fn let_with_an_explicit_type_records_it() {
3672        match first_stmt_of("let z: i64 = 0") {
3673            Stmt::Let { ty, init, .. } => {
3674                assert!(matches!(
3675                    ty,
3676                    Some(TypeExpr::Primitive {
3677                        kind: PrimType::I64,
3678                        ..
3679                    })
3680                ));
3681                assert!(matches!(init, Expr::Int { value: 0, .. }));
3682            }
3683            other => panic!("expected Let, got {other:?}"),
3684        }
3685    }
3686
3687    #[test]
3688    fn let_with_no_initializer_is_a_parse_error() {
3689        // `let x: i64` with no `= ...` errors at the missing `=`.
3690        let err = parse_err("fn f() { let x: i64 }");
3691        assert!(matches!(
3692            err,
3693            QalaError::UnexpectedToken { .. } | QalaError::UnexpectedEof { .. }
3694        ));
3695        assert!(
3696            err.message().contains("`=`"),
3697            "message was {}",
3698            err.message()
3699        );
3700    }
3701
3702    #[test]
3703    fn if_without_else_parses() {
3704        match first_stmt_of("if c { return 1 }") {
3705            Stmt::If { else_branch, .. } => assert!(else_branch.is_none()),
3706            other => panic!("expected If, got {other:?}"),
3707        }
3708    }
3709
3710    #[test]
3711    fn if_with_else_block_parses() {
3712        match first_stmt_of("if c { 1 } else { 2 }") {
3713            Stmt::If { else_branch, .. } => {
3714                assert!(matches!(else_branch, Some(ElseBranch::Block(_))));
3715            }
3716            other => panic!("expected If, got {other:?}"),
3717        }
3718    }
3719
3720    #[test]
3721    fn else_if_chain_parses_as_nested_if() {
3722        // `if a { 1 } else if b { 2 } else { 3 }` ->
3723        // If { else: If(box If { else: Block }) }.
3724        match first_stmt_of("if a { 1 } else if b { 2 } else { 3 }") {
3725            Stmt::If { else_branch, .. } => match else_branch {
3726                Some(ElseBranch::If(boxed)) => match *boxed {
3727                    Stmt::If {
3728                        else_branch: Some(ElseBranch::Block(_)),
3729                        ..
3730                    } => {}
3731                    other => panic!("expected If with else-block, got {other:?}"),
3732                },
3733                other => panic!("expected else-if (ElseBranch::If), got {other:?}"),
3734            },
3735            other => panic!("expected If at the top, got {other:?}"),
3736        }
3737    }
3738
3739    #[test]
3740    fn while_loop_parses() {
3741        match first_stmt_of("while c { x }") {
3742            Stmt::While { cond, body, .. } => {
3743                assert!(matches!(cond, Expr::Ident { .. }));
3744                assert!(body.value.is_some());
3745            }
3746            other => panic!("expected While, got {other:?}"),
3747        }
3748    }
3749
3750    #[test]
3751    fn for_loop_over_a_range_iterable_parses() {
3752        match first_stmt_of("for i in 0..n { x }") {
3753            Stmt::For { var, iter, .. } => {
3754                assert_eq!(var, "i");
3755                assert!(matches!(iter, Expr::Range { .. }));
3756            }
3757            other => panic!("expected For, got {other:?}"),
3758        }
3759    }
3760
3761    #[test]
3762    fn for_loop_over_an_ident_iterable_parses() {
3763        // the no-struct-literal head rule keeps `shapes` from being read as a
3764        // `shapes { x }` struct literal.
3765        match first_stmt_of("for s in shapes { x }") {
3766            Stmt::For { var, iter, .. } => {
3767                assert_eq!(var, "s");
3768                assert!(matches!(iter, Expr::Ident { name, .. } if name == "shapes"));
3769            }
3770            other => panic!("expected For, got {other:?}"),
3771        }
3772    }
3773
3774    #[test]
3775    fn return_bare_has_no_value() {
3776        match first_stmt_of("return") {
3777            Stmt::Return { value, .. } => assert!(value.is_none()),
3778            other => panic!("expected Return, got {other:?}"),
3779        }
3780    }
3781
3782    #[test]
3783    fn return_with_an_expression_carries_it() {
3784        match first_stmt_of("return n + 1") {
3785            Stmt::Return { value, .. } => {
3786                assert!(matches!(value, Some(Expr::Binary { op: BinOp::Add, .. })));
3787            }
3788            other => panic!("expected Return, got {other:?}"),
3789        }
3790    }
3791
3792    #[test]
3793    fn break_and_continue_have_no_value() {
3794        let body = body_of("break continue");
3795        // both statements terminated implicitly by the next statement-head.
3796        assert_eq!(body.stmts.len(), 2);
3797        assert!(matches!(body.stmts[0], Stmt::Break { .. }));
3798        assert!(matches!(body.stmts[1], Stmt::Continue { .. }));
3799    }
3800
3801    #[test]
3802    fn defer_takes_an_arbitrary_expression() {
3803        match first_stmt_of("defer close(f)") {
3804            Stmt::Defer { expr, .. } => assert!(matches!(expr, Expr::Call { .. })),
3805            other => panic!("expected Defer, got {other:?}"),
3806        }
3807    }
3808
3809    #[test]
3810    fn a_block_with_a_trailing_value_has_some_value() {
3811        let body = body_of("let x = 1; x");
3812        assert_eq!(body.stmts.len(), 1);
3813        assert!(matches!(body.stmts[0], Stmt::Let { .. }));
3814        assert!(matches!(body.value.as_deref(), Some(Expr::Ident { .. })));
3815    }
3816
3817    #[test]
3818    fn an_empty_block_has_no_stmts_and_no_value() {
3819        let body = body_of("");
3820        assert!(body.stmts.is_empty());
3821        assert!(body.value.is_none());
3822    }
3823
3824    #[test]
3825    fn newline_separated_statements_with_no_semi_all_parse() {
3826        // the bundled examples use NO `;` -- statements are separated by the
3827        // next statement-head keyword or `}`. this fixture mirrors
3828        // `defer-demo.qala`'s `let f = open(path) defer close(f) let content
3829        // = f.read_all()? ...` shape.
3830        let body = body_of("let f = open(path) defer close(f) let g = 1");
3831        assert_eq!(body.stmts.len(), 3);
3832        assert!(matches!(body.stmts[0], Stmt::Let { .. }));
3833        assert!(matches!(body.stmts[1], Stmt::Defer { .. }));
3834        assert!(matches!(body.stmts[2], Stmt::Let { .. }));
3835    }
3836
3837    #[test]
3838    fn expression_statements_chain_without_semicolons() {
3839        // two expressions in a row, no `;` -- mirrors hello.qala's body.
3840        // resolution: the first expression is a statement (the next token
3841        // starts another expression); the LAST expression with no following
3842        // `;` is the block's trailing value. so `println("a") println("b")`
3843        // yields one Expr stmt and a Some(value).
3844        let body = body_of("println(\"a\") println(\"b\")");
3845        assert_eq!(body.stmts.len(), 1);
3846        match &body.stmts[0] {
3847            Stmt::Expr { expr, .. } => assert!(matches!(expr, Expr::Call { .. })),
3848            other => panic!("expected Expr stmt, got {other:?}"),
3849        }
3850        assert!(matches!(body.value.as_deref(), Some(Expr::Call { .. })));
3851    }
3852
3853    #[test]
3854    fn let_x_eq_followed_by_eof_errors_with_a_zero_width_span_at_the_eq() {
3855        // KEY PARSE-09 case: the EOF span is zero-width, just past the `=`.
3856        let err = parse_err("fn f() { let x =\n");
3857        match err {
3858            QalaError::UnexpectedEof { span, .. } => {
3859                // the `=` ends at byte offset 16 (`fn f() { let x =` -- 0..16).
3860                assert_eq!(span.len, 0);
3861                // the message says it ran out of input.
3862                assert!(err.message().contains("end of input"));
3863            }
3864            other => panic!("expected UnexpectedEof, got {other:?}"),
3865        }
3866    }
3867
3868    #[test]
3869    fn mismatched_paren_inside_a_block_blames_the_open_paren() {
3870        // `fn f() { let x = ( a + b } }`. the `(` is opened then never closed;
3871        // a `}` shows up where `)` or another expr-rhs was expected.
3872        let err = parse_err("fn f() { let x = ( a + b } }");
3873        match err {
3874            QalaError::UnclosedDelimiter { span, opener, .. } => {
3875                assert_eq!(opener, TokenKind::LParen);
3876                // the `(` starts at byte offset 17.
3877                assert_eq!(span.start, 17);
3878                assert_eq!(span.len, 1);
3879            }
3880            other => panic!("expected UnclosedDelimiter, got {other:?}"),
3881        }
3882    }
3883
3884    #[test]
3885    fn there_is_no_expr_if_in_the_parser() {
3886        // `let x = if c { a } else { b }` is NOT legal in v1 -- `if` cannot
3887        // start an expression. the parser should error at the `if` token.
3888        let err = parse_err("fn f() { let x = if c { 1 } else { 2 } }");
3889        match err {
3890            QalaError::UnexpectedToken { found, .. } => {
3891                assert_eq!(found, TokenKind::If);
3892            }
3893            other => panic!("expected UnexpectedToken at `if`, got {other:?}"),
3894        }
3895    }
3896
3897    // ---- the verification battery (PARSE-09 negatives + integration) --------
3898
3899    #[test]
3900    fn the_six_examples_parse_with_no_error() {
3901        // the analogue of the lexer's six-examples smoke test: every bundled
3902        // `.qala` file parses cleanly with the v1 grammar. the path is
3903        // relative to this crate's manifest, so the test is cwd-independent;
3904        // a missing file fails loudly via `panic!`.
3905        for name in [
3906            "hello",
3907            "fibonacci",
3908            "effects",
3909            "pattern-matching",
3910            "pipeline",
3911            "defer-demo",
3912        ] {
3913            let path = format!(
3914                "{}/../../playground/public/examples/{}.qala",
3915                env!("CARGO_MANIFEST_DIR"),
3916                name
3917            );
3918            let src = std::fs::read_to_string(&path)
3919                .unwrap_or_else(|e| panic!("could not read example {path}: {e}"));
3920            let toks = crate::lexer::Lexer::tokenize(&src)
3921                .unwrap_or_else(|e| panic!("example {name}.qala failed to tokenize: {e:?}"));
3922            let ast = Parser::parse(&toks)
3923                .unwrap_or_else(|e| panic!("example {name}.qala failed to parse: {e:?}"));
3924            assert!(
3925                !ast.is_empty(),
3926                "example {name}.qala should produce top-level items"
3927            );
3928        }
3929    }
3930
3931    // PARSE-09: spans-on-the-operator, blame-the-opener, zero-width EOF span,
3932    // "expected X, found Y", trailing commas, empty body, zero match arms,
3933    // `self` misplacement. some overlap with prior tests is fine; this
3934    // section is the consolidated battery.
3935
3936    #[test]
3937    fn pipeline_with_no_rhs_call_blames_the_pipe_operator() {
3938        // `fn f() is io { let r = x |> }` -- the `|>` token's span is the error.
3939        let err = parse_err("fn f() is io { let r = x |> }");
3940        match err {
3941            QalaError::Parse { message, span } => {
3942                assert!(message.contains("`|>`"), "message was {message:?}");
3943                // the `|>` is two bytes wide.
3944                assert_eq!(span.len, 2);
3945            }
3946            other => panic!("expected QalaError::Parse, got {other:?}"),
3947        }
3948    }
3949
3950    #[test]
3951    fn a_mismatched_delimiter_blames_the_opener() {
3952        // `fn f() { let x = ( a + b } }` -- the `(` opens, but `}` shows up.
3953        // the error's span is the `(`, not the `}`.
3954        let err = parse_err("fn f() { let x = ( a + b } }");
3955        let msg = err.message();
3956        match &err {
3957            QalaError::UnclosedDelimiter {
3958                span,
3959                opener,
3960                found,
3961            } => {
3962                assert_eq!(*opener, TokenKind::LParen);
3963                assert_eq!(span.len, 1, "the `(` span is one byte wide");
3964                assert_eq!(*found, TokenKind::RBrace);
3965            }
3966            other => panic!("expected UnclosedDelimiter, got {other:?}"),
3967        }
3968        // and the rendered message mentions both the opener and the surprise.
3969        assert!(msg.contains("`(`"), "msg was {msg:?}");
3970        assert!(msg.contains("`}`"), "msg was {msg:?}");
3971    }
3972
3973    #[test]
3974    fn unexpected_eof_reports_a_zero_width_span_after_the_last_token() {
3975        // `fn f() { let x =\n` -- input ends mid-statement. the span is
3976        // zero-width, just past the `=` (the last real token).
3977        let err = parse_err("fn f() { let x =\n");
3978        match err {
3979            QalaError::UnexpectedEof { span, .. } => {
3980                assert_eq!(span.len, 0, "EOF span must be zero-width");
3981                assert!(err.message().contains("end of input"));
3982            }
3983            other => panic!("expected UnexpectedEof, got {other:?}"),
3984        }
3985    }
3986
3987    #[test]
3988    fn an_unexpected_token_says_expected_x_found_y() {
3989        // `fn f() { let 5 = 0 }` -- where an identifier is expected, an int.
3990        let err = parse_err("fn f() { let 5 = 0 }");
3991        assert!(matches!(err, QalaError::UnexpectedToken { .. }));
3992        let msg = err.message();
3993        assert!(msg.contains("expected"), "{msg:?}");
3994        assert!(msg.contains("identifier"), "{msg:?}");
3995        assert!(msg.contains("found"), "{msg:?}");
3996    }
3997
3998    #[test]
3999    fn trailing_commas_are_accepted_everywhere() {
4000        // every list position the grammar can grow.
4001        assert!(Parser::parse(&lex("fn f(a: i64, b: i64,) {}")).is_ok());
4002        assert!(Parser::parse(&lex("struct S { x: i64, y: i64, }")).is_ok());
4003        assert!(Parser::parse(&lex("enum E { A, B(i64), }")).is_ok());
4004        assert!(Parser::parse(&lex("interface I { fn m(self) -> str, }")).is_ok());
4005        assert!(Parser::parse(&lex("fn g() is io { let xs = [1, 2, 3,] }")).is_ok());
4006        assert!(Parser::parse(&lex("fn h() is io { let t = (1, 2,) }")).is_ok());
4007        // generic-arg trailing comma.
4008        assert!(Parser::parse(&lex("fn k(x: Result<i64, str,>) {}")).is_ok());
4009    }
4010
4011    #[test]
4012    fn an_empty_function_body_is_legal() {
4013        let ast = parse_ok("fn f() {}");
4014        match &ast[0] {
4015            Item::Fn(d) => {
4016                assert!(d.body.stmts.is_empty());
4017                assert!(d.body.value.is_none());
4018            }
4019            other => panic!("expected Fn, got {other:?}"),
4020        }
4021    }
4022
4023    #[test]
4024    fn zero_match_arms_is_an_error_in_a_full_program() {
4025        // the expression-level test covers this; pin the same behaviour
4026        // through the public `Parser::parse` path.
4027        let err = parse_err("fn f() is io { match x { } }");
4028        match err {
4029            QalaError::Parse { message, .. } => {
4030                assert!(message.contains("at least one arm"), "{message:?}");
4031            }
4032            other => panic!("expected QalaError::Parse, got {other:?}"),
4033        }
4034    }
4035
4036    #[test]
4037    fn self_outside_a_method_definition_is_an_error_in_a_full_program() {
4038        // free fn cannot have `self`.
4039        assert!(Parser::parse(&lex("fn f(self) {}")).is_err());
4040        // `self` not in the first slot of a method.
4041        assert!(Parser::parse(&lex("fn Point.f(a: i64, self) {}")).is_err());
4042        // `self` as the first slot of a method IS legal.
4043        assert!(Parser::parse(&lex("fn Point.area(self) -> f64 is pure { 3.14 }")).is_ok());
4044    }
4045
4046    // The deep-nesting integration test: this MUST actually run without a
4047    // stack overflow. if it segfaults, the depth guard is not wired around
4048    // all recursive productions, or `MAX_DEPTH` is too high; either is a real
4049    // bug to fix in Tasks 1-2's code, not to work around here.
4050
4051    #[test]
4052    fn deeply_nested_input_fails_gracefully_not_with_a_stack_overflow() {
4053        // 20000 nested parens: an expression-nesting case. the depth guard
4054        // around `expr_bp` is supposed to fire long before the stack runs out.
4055        let deep = format!(
4056            "fn f() is io {{ let x = {}1{} }}",
4057            "(".repeat(20000),
4058            ")".repeat(20000)
4059        );
4060        let err = Parser::parse(&lex(&deep))
4061            .expect_err("expected a clean depth-limit error, not a crash");
4062        assert!(
4063            matches!(err, QalaError::Parse { .. }),
4064            "expected a Parse error, got {err:?}"
4065        );
4066        assert!(
4067            err.message().contains("deeply") || err.message().contains("nest"),
4068            "message was {}",
4069            err.message()
4070        );
4071        // 5000 nested blocks: the depth guard around `parse_block` is the
4072        // one that must catch this. it must error, not segfault.
4073        let deep_blocks = format!(
4074            "fn f() is io {{ {}1{} }}",
4075            "{".repeat(5000),
4076            "}".repeat(5000)
4077        );
4078        assert!(
4079            Parser::parse(&lex(&deep_blocks)).is_err(),
4080            "deep block nesting should error, not crash"
4081        );
4082        // 5000 nested types: the depth guard around `parse_type` covers this.
4083        // `[[[...i64...]]]` -- one bracket per nesting level.
4084        let deep_types = format!("fn f(x: {}i64{}) {{}}", "[".repeat(5000), "]".repeat(5000));
4085        assert!(
4086            Parser::parse(&lex(&deep_types)).is_err(),
4087            "deep type nesting should error, not crash"
4088        );
4089    }
4090
4091    #[test]
4092    fn ast_node_spans_point_at_the_source_bytes_they_came_from() {
4093        // pick a handful of nodes at different depths and assert their span
4094        // slices the source to the right text. PARSE-01.
4095        let src = "fn add(a: i64, b: i64) -> i64 is pure { return a + b }";
4096        let toks = lex(src);
4097        let ast = Parser::parse(&toks).expect("parses");
4098        assert_eq!(ast.len(), 1);
4099        let item = &ast[0];
4100        // the whole fn item spans the entire source (modulo trailing
4101        // whitespace, none here).
4102        assert_eq!(item.span().slice(src), src);
4103        match item {
4104            Item::Fn(d) => {
4105                // each typed param's span slices to "NAME: TYPE".
4106                let p0_text = d.params[0].span.slice(src);
4107                assert_eq!(p0_text, "a: i64");
4108                let p1_text = d.params[1].span.slice(src);
4109                assert_eq!(p1_text, "b: i64");
4110                // the return type's span slices to "i64".
4111                let ret_text = d.ret_ty.as_ref().expect("ret").span().slice(src);
4112                assert_eq!(ret_text, "i64");
4113                // body span includes both braces.
4114                assert_eq!(d.body.span.slice(src), "{ return a + b }");
4115                // the inner `return a + b` statement.
4116                let ret_stmt = &d.body.stmts[0];
4117                assert_eq!(ret_stmt.span().slice(src), "return a + b");
4118                // the inner `a + b` Binary's span.
4119                if let Stmt::Return {
4120                    value: Some(value), ..
4121                } = ret_stmt
4122                {
4123                    assert_eq!(value.span().slice(src), "a + b");
4124                } else {
4125                    panic!("expected Stmt::Return with a value, got {ret_stmt:?}");
4126                }
4127            }
4128            other => panic!("expected Fn, got {other:?}"),
4129        }
4130    }
4131
4132    #[test]
4133    fn parsing_a_full_program_is_deterministic_byte_for_byte() {
4134        // the existing `parsing_is_deterministic` covers the empty / one-item
4135        // cases. this richer fixture exercises the full v1 grammar: types,
4136        // generics, items, statements, expressions, control flow. running it
4137        // twice MUST yield equal ASTs -- the parser holds no global state.
4138        let src = "fn fib(n: i64) -> i64 is pure { if n <= 1 { return n } return fib(n - 1) + fib(n - 2) }";
4139        let a = Parser::parse(&lex(src)).expect("parses");
4140        let b = Parser::parse(&lex(src)).expect("parses");
4141        assert_eq!(a, b);
4142        // also stable across an error case.
4143        let bad = "fn f() { let x = ( }";
4144        assert_eq!(Parser::parse(&lex(bad)), Parser::parse(&lex(bad)));
4145    }
4146}