Skip to main content

bop/
parser.rs

1#[cfg(feature = "no_std")]
2use alloc::{boxed::Box, format, string::{String, ToString}, vec, vec::Vec};
3
4use crate::error::BopError;
5use crate::lexer::{SpannedToken, StringPart, Token};
6use crate::naming;
7
8// ─── Naming helpers ─────────────────────────────────────────────────────
9//
10// Enforce the shape rules defined in `bop::naming` at every
11// identifier-introducing site. Each `ensure_*_name` returns a
12// parse error whose `message` says what the site expects and
13// whose `friendly_hint` offers a concrete rename — cheap to
14// generate, and makes errors read like the compiler wants to
15// help rather than just complain.
16
17fn ident_shape_error(site: &str, expected: &str, actual: &str, line: u32) -> BopError {
18    let actual_label = naming::kind_label(naming::classify(actual));
19    let message = format!(
20        "{} `{}` looks like a {}, but a {} name is required here",
21        site, actual, actual_label, expected
22    );
23    let mut err = BopError::runtime(message, line);
24    err.friendly_hint = Some(naming::hint_for(expected, actual));
25    err
26}
27
28/// Require a lowercase-first (or leading-underscore) identifier
29/// at a `let` / `fn` / param / field / method / alias /
30/// match-binding / `for-in` / `use` alias site.
31fn ensure_value_name(name: &str, site: &str, line: u32) -> Result<(), BopError> {
32    if naming::is_value_name(name) {
33        Ok(())
34    } else {
35        Err(ident_shape_error(site, "value", name, line))
36    }
37}
38
39/// Require an uppercase-first identifier at a `struct` / `enum` /
40/// variant site. Both PascalCase and ALL_CAPS are accepted —
41/// `struct Entity {}`, `enum Dir { N, E, S, W }`, and
42/// `struct HTTP {}` all pass.
43fn ensure_type_name(name: &str, site: &str, line: u32) -> Result<(), BopError> {
44    if naming::is_type_name(name) {
45        Ok(())
46    } else {
47        Err(ident_shape_error(site, "type", name, line))
48    }
49}
50
51/// Require an all-uppercase identifier at a `const` site.
52fn ensure_constant_name(name: &str, site: &str, line: u32) -> Result<(), BopError> {
53    if naming::is_constant_name(name) {
54        Ok(())
55    } else {
56        Err(ident_shape_error(site, "constant", name, line))
57    }
58}
59
60// ─── AST ───────────────────────────────────────────────────────────────────
61
62#[derive(Debug, Clone)]
63pub struct Expr {
64    pub kind: ExprKind,
65    pub line: u32,
66    /// 1-indexed column where this expression starts in the
67    /// source. `None` on synthetic nodes that don't correspond
68    /// to a specific source position. Niche-packed into 4 bytes
69    /// so the column field costs nothing beyond a plain `u32`
70    /// — runtime error construction reads it out to render a
71    /// carat under the offending character.
72    pub column: Option<core::num::NonZeroU32>,
73}
74
75impl Expr {
76    /// Build an `Expr` from its kind and a 1-indexed source
77    /// line, leaving `column` unset. Convenience constructor
78    /// for call sites that don't have a token column handy
79    /// (synthetic / desugared nodes, for instance).
80    pub fn line(kind: ExprKind, line: u32) -> Self {
81        Self { kind, line, column: None }
82    }
83
84    /// Build an `Expr` with a full source position. `column`
85    /// is typically `NonZeroU32::new(tok.column)`.
86    pub fn at(kind: ExprKind, line: u32, column: Option<core::num::NonZeroU32>) -> Self {
87        Self { kind, line, column }
88    }
89}
90
91#[derive(Debug, Clone)]
92pub enum ExprKind {
93    /// Integer literal (phase 6). Produced by integer tokens
94    /// like `42`; distinct from `Number` so each engine can
95    /// emit a `Value::Int(i64)` directly.
96    Int(i64),
97    Number(f64),
98    Str(String),
99    StringInterp(Vec<StringPart>),
100    Bool(bool),
101    None,
102    Ident(String),
103    BinaryOp {
104        left: Box<Expr>,
105        op: BinOp,
106        right: Box<Expr>,
107    },
108    UnaryOp {
109        op: UnaryOp,
110        expr: Box<Expr>,
111    },
112    Call {
113        callee: Box<Expr>,
114        args: Vec<Expr>,
115    },
116    MethodCall {
117        object: Box<Expr>,
118        method: String,
119        args: Vec<Expr>,
120    },
121    /// Bare field read: `obj.field` (no parens after the field
122    /// name). Distinct from `MethodCall`, which always has `(…)`.
123    FieldAccess {
124        object: Box<Expr>,
125        field: String,
126    },
127    /// Struct literal: `Point { x: 1, y: 2 }`. Only parsed in
128    /// contexts where struct literals are allowed — control-flow
129    /// conditions and `for-in` iterables disallow them so that
130    /// `if foo { body }` stays unambiguous.
131    StructConstruct {
132        /// `Some("m")` for `m.Entity { ... }` — a namespaced
133        /// struct literal through a module alias. `None` for
134        /// the unqualified `Entity { ... }` form. The walker /
135        /// VM / AOT resolve the (namespace, type_name) pair via
136        /// the current scope's type-alias table before
137        /// constructing the `Value::Struct`.
138        namespace: Option<String>,
139        type_name: String,
140        fields: Vec<(String, Expr)>,
141    },
142    /// Enum variant construction: `Shape::Circle(5)`,
143    /// `Shape::Rectangle { w: 4, h: 3 }`, `Shape::Empty`. The
144    /// payload shape is determined at parse time from the syntax
145    /// at the construction site.
146    EnumConstruct {
147        /// `Some("r")` for `r.Result::Ok(v)` — a namespaced
148        /// variant constructor through a module alias. `None`
149        /// for unqualified `Result::Ok(v)`.
150        namespace: Option<String>,
151        type_name: String,
152        variant: String,
153        payload: VariantPayload,
154    },
155    Index {
156        object: Box<Expr>,
157        index: Box<Expr>,
158    },
159    Array(Vec<Expr>),
160    Dict(Vec<(String, Expr)>),
161    IfExpr {
162        condition: Box<Expr>,
163        then_expr: Box<Expr>,
164        else_expr: Box<Expr>,
165    },
166    /// Anonymous function expression: `fn(params) { body }`.
167    /// Captures the referenced free variables from the enclosing
168    /// scope when evaluated; see the evaluator for capture rules.
169    Lambda {
170        params: Vec<String>,
171        body: Vec<Stmt>,
172    },
173    /// `match scrutinee { pat => body, ... }` — checks each arm
174    /// top-to-bottom, evaluates the first matching arm's body,
175    /// and returns its value. Raises a runtime error if no arm
176    /// matches (exhaustiveness isn't checked statically in v1).
177    Match {
178        scrutinee: Box<Expr>,
179        arms: Vec<MatchArm>,
180    },
181
182    /// `try <expr>` — inspect a Result-shaped enum variant.
183    /// If `<expr>` evaluates to an `Ok(value)`-shaped variant,
184    /// unwrap to `value`. If it evaluates to an `Err(...)`-shaped
185    /// variant, short-circuit the enclosing function's return
186    /// with that value (same mechanism as a `return` statement).
187    /// At top-level scope (no enclosing fn) or on a non-Result
188    /// scrutinee, `try` raises a runtime error.
189    ///
190    /// Desugars roughly to the match:
191    /// ```text
192    /// match <expr> {
193    ///     Ok(v) => v,
194    ///     Err(_) => return <expr>,
195    /// }
196    /// ```
197    /// but is its own AST node so each engine can compile it
198    /// directly without paying the pattern-construction cost.
199    Try(Box<Expr>),
200}
201
202#[derive(Debug, Clone)]
203pub struct MatchArm {
204    pub pattern: Pattern,
205    /// Optional guard expression — evaluated after the pattern
206    /// matches. A `false` guard skips to the next arm.
207    pub guard: Option<Expr>,
208    pub body: Expr,
209    pub line: u32,
210}
211
212/// A pattern appears in `match` arms (phase 4 introduces this;
213/// future phases may reuse it in `let` destructuring and fn
214/// params). Structurally mirrors the runtime `Value` enum so each
215/// variant's matcher reads as "does this value fit here?".
216#[derive(Debug, Clone)]
217pub enum Pattern {
218    /// Matches a specific value verbatim: `1`, `"foo"`, `true`,
219    /// `false`, `none`.
220    Literal(LiteralPattern),
221    /// `_` — matches anything, binds nothing.
222    Wildcard,
223    /// Bare identifier — matches anything, binds the value to
224    /// this name for the arm's body.
225    Binding(String),
226    /// `Type::Variant` / `Type::Variant(...)` / `Type::Variant { ... }`,
227    /// optionally namespaced via `ns.Type::Variant(...)`.
228    EnumVariant {
229        namespace: Option<String>,
230        type_name: String,
231        variant: String,
232        payload: VariantPatternPayload,
233    },
234    /// `Type { field: pat, field, .. }` destructures a struct,
235    /// optionally namespaced via `ns.Type { ... }`.
236    Struct {
237        namespace: Option<String>,
238        type_name: String,
239        fields: Vec<(String, Pattern)>,
240        rest: bool,
241    },
242    /// `[a, b, ..rest]` destructures an array.
243    Array {
244        elements: Vec<Pattern>,
245        rest: Option<ArrayRest>,
246    },
247    /// `p1 | p2 | p3` — match if any alternative matches. Every
248    /// alternative must introduce the same set of bindings.
249    Or(Vec<Pattern>),
250}
251
252#[derive(Debug, Clone)]
253pub enum LiteralPattern {
254    /// Integer literal pattern (e.g. `match x { 1 => ... }`).
255    /// Added in phase 6 alongside `Value::Int`.
256    Int(i64),
257    Number(f64),
258    Str(String),
259    Bool(bool),
260    None,
261}
262
263#[derive(Debug, Clone)]
264pub enum VariantPatternPayload {
265    Unit,
266    Tuple(Vec<Pattern>),
267    Struct {
268        fields: Vec<(String, Pattern)>,
269        rest: bool,
270    },
271}
272
273/// What `..rest` does at the tail of an array pattern.
274#[derive(Debug, Clone)]
275pub enum ArrayRest {
276    /// `..` — matches any remaining elements, binds nothing.
277    Ignored,
278    /// `..name` — captures remaining elements as an array.
279    Named(String),
280}
281
282#[derive(Debug, Clone, Copy)]
283pub enum BinOp {
284    Add,
285    Sub,
286    Mul,
287    Div,
288    Mod,
289    Eq,
290    NotEq,
291    Lt,
292    Gt,
293    LtEq,
294    GtEq,
295    And,
296    Or,
297}
298
299#[derive(Debug, Clone, Copy)]
300pub enum UnaryOp {
301    Neg,
302    Not,
303}
304
305#[derive(Debug, Clone)]
306pub struct Stmt {
307    pub kind: StmtKind,
308    pub line: u32,
309    /// 1-indexed column where this statement starts. See
310    /// [`Expr::column`] — same niche-packed shape, same
311    /// purpose (carat rendering on runtime errors).
312    pub column: Option<core::num::NonZeroU32>,
313}
314
315impl Stmt {
316    /// Build a `Stmt` from its kind and a 1-indexed source
317    /// line, leaving `column` unset. See
318    /// [`Expr::line`].
319    pub fn line(kind: StmtKind, line: u32) -> Self {
320        Self { kind, line, column: None }
321    }
322
323    /// Build a `Stmt` with a full source position.
324    pub fn at(kind: StmtKind, line: u32, column: Option<core::num::NonZeroU32>) -> Self {
325        Self { kind, line, column }
326    }
327}
328
329#[derive(Debug, Clone)]
330pub enum StmtKind {
331    /// `let NAME = expr` (value binding, mutable) and `const NAME
332    /// = expr` (constant binding, immutable). The `is_const` flag
333    /// flips enforcement at use/assign sites: reassigning a
334    /// constant is a compile-time error (the parser refuses any
335    /// assignment whose LHS is an all-uppercase identifier — see
336    /// [`parse_assign`]).
337    Let {
338        name: String,
339        value: Expr,
340        is_const: bool,
341    },
342    Assign {
343        target: AssignTarget,
344        op: AssignOp,
345        value: Expr,
346    },
347    If {
348        condition: Expr,
349        body: Vec<Stmt>,
350        else_ifs: Vec<(Expr, Vec<Stmt>)>,
351        else_body: Option<Vec<Stmt>>,
352    },
353    While {
354        condition: Expr,
355        body: Vec<Stmt>,
356    },
357    Repeat {
358        count: Expr,
359        body: Vec<Stmt>,
360    },
361    ForIn {
362        var: String,
363        iterable: Expr,
364        body: Vec<Stmt>,
365    },
366    FnDecl {
367        name: String,
368        params: Vec<String>,
369        body: Vec<Stmt>,
370    },
371    /// `fn Type.method(self, ...) { body }` — declares a method
372    /// on a user-defined struct or enum. At call time
373    /// (`obj.method(...)`) the receiver is passed as the first
374    /// parameter (conventionally named `self`), followed by the
375    /// rest.
376    MethodDecl {
377        type_name: String,
378        method_name: String,
379        params: Vec<String>,
380        body: Vec<Stmt>,
381    },
382    Return {
383        value: Option<Expr>,
384    },
385    Break,
386    Continue,
387    /// `use foo.bar.baz` — resolves the module named by the
388    /// dot-joined path through `BopHost::resolve_module`,
389    /// evaluates its top-level statements in a fresh scope, and
390    /// injects its exports into the importer's scope. The shape
391    /// of the injection depends on the optional `items` / `alias`:
392    ///
393    /// - `use foo`                  — glob: all non-private
394    ///   top-level names land unqualified.
395    /// - `use foo.{a, b}`           — selective: only `a` and
396    ///   `b` land unqualified. `_`-prefixed names can be
397    ///   explicitly listed.
398    /// - `use foo as m`             — aliased: all exports
399    ///   (including `_`-prefixed) hang off a new `m` namespace
400    ///   value. Access via `m.a`, `m.Entity`, etc.
401    /// - `use foo.{a, b} as m`      — selective + aliased:
402    ///   `m` namespace contains only `a` and `b`.
403    ///
404    /// Glob imports skip `_`-prefixed top-level names (privacy
405    /// convention). Aliased and selective imports pass them
406    /// through when the user asks for them explicitly.
407    Use {
408        path: String,
409        /// `Some` iff the caller used the selective `.{a, b}`
410        /// form. The listed names are injected (whatever shape
411        /// they have); anything not listed is skipped.
412        items: Option<Vec<String>>,
413        /// `Some("m")` iff the caller used the `as m` form.
414        /// Exports are bound inside a `Value::Module` under this
415        /// name rather than in the caller's scope directly.
416        alias: Option<String>,
417    },
418    /// `struct Point { x, y }` — registers a user-defined struct
419    /// type with the listed field names. Field values get their
420    /// types from the construction site (`Point { x: 1, y: 2 }`).
421    StructDecl {
422        name: String,
423        fields: Vec<String>,
424    },
425    /// `enum Shape { Circle(radius), Rectangle { w, h }, Empty }`
426    /// — registers a user-defined sum type with named variants.
427    EnumDecl {
428        name: String,
429        variants: Vec<VariantDecl>,
430    },
431    ExprStmt(Expr),
432}
433
434/// One variant of an `enum` declaration.
435#[derive(Debug, Clone, PartialEq)]
436pub struct VariantDecl {
437    pub name: String,
438    pub kind: VariantKind,
439}
440
441/// What shape a variant's payload takes.
442#[derive(Debug, Clone, PartialEq)]
443pub enum VariantKind {
444    /// No payload — `Empty`.
445    Unit,
446    /// Positional payload — `Circle(radius)`.
447    Tuple(Vec<String>),
448    /// Named payload — `Rectangle { width, height }`.
449    Struct(Vec<String>),
450}
451
452/// Runtime-side payload at a `T::Variant(...)` construction site.
453#[derive(Debug, Clone)]
454pub enum VariantPayload {
455    Unit,
456    Tuple(Vec<Expr>),
457    Struct(Vec<(String, Expr)>),
458}
459
460#[derive(Debug, Clone)]
461pub enum AssignTarget {
462    Variable(String),
463    Index { object: Expr, index: Expr },
464    /// Assignment to a struct field: `obj.field = v`. Like
465    /// `Index`, only a bare `Ident` for `object` is currently
466    /// assignable — the runtime clones out, mutates, and writes
467    /// back through the variable.
468    Field { object: Expr, field: String },
469}
470
471#[derive(Debug, Clone, Copy)]
472pub enum AssignOp {
473    Eq,
474    AddEq,
475    SubEq,
476    MulEq,
477    DivEq,
478    ModEq,
479}
480
481// ─── Parser ────────────────────────────────────────────────────────────────
482
483const MAX_PARSE_DEPTH: usize = 128;
484
485pub fn parse(tokens: Vec<SpannedToken>) -> Result<Vec<Stmt>, BopError> {
486    let mut parser = Parser::new(tokens);
487    parser.parse_program()
488}
489
490struct Parser {
491    tokens: Vec<SpannedToken>,
492    pos: usize,
493    depth: usize,
494    /// When false, `Ident { ... }` at expression position is
495    /// *not* parsed as a struct literal — the `{` is left for the
496    /// enclosing control-flow construct (e.g. `if foo { body }`,
497    /// `for x in arr { body }`). Flipped off while parsing `if`
498    /// / `while` conditions and `for-in` iterables.
499    allow_struct_literal: bool,
500}
501
502impl Parser {
503    fn new(tokens: Vec<SpannedToken>) -> Self {
504        Self {
505            tokens,
506            pos: 0,
507            depth: 0,
508            allow_struct_literal: true,
509        }
510    }
511
512    fn enter(&mut self) -> Result<(), BopError> {
513        self.depth += 1;
514        if self.depth > MAX_PARSE_DEPTH {
515            Err(self.error(self.peek_line(), "Code is nested too deeply"))
516        } else {
517            Ok(())
518        }
519    }
520
521    fn leave(&mut self) {
522        self.depth -= 1;
523    }
524
525    fn peek(&self) -> &Token {
526        self.tokens
527            .get(self.pos)
528            .map(|t| &t.token)
529            .unwrap_or(&Token::Eof)
530    }
531
532    fn peek_line(&self) -> u32 {
533        self.tokens.get(self.pos).map(|t| t.line).unwrap_or(0)
534    }
535
536    /// 1-indexed column of the current token's first character.
537    /// Used for parse-error reporting so the carat under the
538    /// offending line points at the right place.
539    fn peek_column(&self) -> u32 {
540        self.tokens.get(self.pos).map(|t| t.column).unwrap_or(0)
541    }
542
543    /// Grab both the line and the niche-packed column of the
544    /// current token in one shot. Shorthand used at the head
545    /// of parse fns that build an `Expr` / `Stmt` — rather than
546    /// calling `peek_line()` then having to re-fetch column
547    /// later, capture the source position once.
548    fn peek_pos(&self) -> (u32, Option<core::num::NonZeroU32>) {
549        let tok = self.tokens.get(self.pos);
550        let line = tok.map(|t| t.line).unwrap_or(0);
551        let column = tok
552            .map(|t| t.column)
553            .and_then(core::num::NonZeroU32::new);
554        (line, column)
555    }
556
557    fn peek_at(&self, offset: usize) -> &Token {
558        self.tokens
559            .get(self.pos + offset)
560            .map(|t| &t.token)
561            .unwrap_or(&Token::Eof)
562    }
563
564    /// Run `f` with `allow_struct_literal = false`, restoring the
565    /// prior value on exit. Used for `if` / `while` conditions and
566    /// `for-in` iterables so `if foo { body }` doesn't mis-parse
567    /// as `if (struct-literal-foo) { … }`.
568    fn without_struct_literal<F, R>(&mut self, f: F) -> R
569    where
570        F: FnOnce(&mut Self) -> R,
571    {
572        let saved = self.allow_struct_literal;
573        self.allow_struct_literal = false;
574        let result = f(self);
575        self.allow_struct_literal = saved;
576        result
577    }
578
579    fn advance(&mut self) -> &Token {
580        let tok = self
581            .tokens
582            .get(self.pos)
583            .map(|t| &t.token)
584            .unwrap_or(&Token::Eof);
585        if self.pos < self.tokens.len() {
586            self.pos += 1;
587        }
588        tok
589    }
590
591    fn is_at_end(&self) -> bool {
592        matches!(self.peek(), Token::Eof)
593    }
594
595    fn expect(&mut self, expected: &Token) -> Result<u32, BopError> {
596        let (line, _column) = self.peek_pos();
597        if self.peek() == expected {
598            self.advance();
599            Ok(line)
600        } else {
601            Err(self.error(
602                line,
603                format!(
604                    "Expected `{}` but found `{}`",
605                    fmt_token(expected),
606                    fmt_token(self.peek())
607                ),
608            ))
609        }
610    }
611
612    fn expect_ident(&mut self) -> Result<(String, u32), BopError> {
613        let (line, _column) = self.peek_pos();
614        if let Token::Ident(name) = self.peek().clone() {
615            self.advance();
616            Ok((name, line))
617        } else {
618            Err(self.error(
619                line,
620                format!("Expected a name but found `{}`", fmt_token(self.peek())),
621            ))
622        }
623    }
624
625    fn skip_semicolons(&mut self) {
626        while matches!(self.peek(), Token::Semicolon) {
627            self.advance();
628        }
629    }
630
631    fn error(&self, line: u32, message: impl Into<String>) -> BopError {
632        // Use the current token's column; if we've already
633        // advanced past the token that triggered the complaint,
634        // column 0 (unknown) is the honest answer rather than
635        // silently misreporting.
636        let column = if self.tokens.get(self.pos).map(|t| t.line) == Some(line) {
637            Some(self.peek_column())
638        } else {
639            None
640        };
641        BopError {
642            line: Some(line),
643            column,
644            message: message.into(),
645            friendly_hint: None,
646            is_fatal: false,
647            is_try_return: false,
648        }
649    }
650
651    // ─── Program & Blocks ──────────────────────────────────────────────
652
653    fn parse_program(&mut self) -> Result<Vec<Stmt>, BopError> {
654        let mut stmts = Vec::new();
655        self.skip_semicolons();
656        while !self.is_at_end() {
657            stmts.push(self.parse_statement()?);
658            self.skip_semicolons();
659        }
660        Ok(stmts)
661    }
662
663    fn parse_block(&mut self) -> Result<Vec<Stmt>, BopError> {
664        self.enter()?;
665        self.expect(&Token::LBrace)?;
666        let mut stmts = Vec::new();
667        self.skip_semicolons();
668        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
669            stmts.push(self.parse_statement()?);
670            self.skip_semicolons();
671        }
672        self.expect(&Token::RBrace)?;
673        self.leave();
674        Ok(stmts)
675    }
676
677    // ─── Statements ────────────────────────────────────────────────────
678
679    fn parse_statement(&mut self) -> Result<Stmt, BopError> {
680        let (line, column) = self.peek_pos();
681        match self.peek() {
682            Token::Let => self.parse_let(),
683            Token::Const => self.parse_const(),
684            Token::If => self.parse_if_stmt(),
685            Token::While => self.parse_while(),
686            Token::For => self.parse_for(),
687            Token::Repeat => self.parse_repeat(),
688            // `fn name(...)` is a declaration; `fn(...)` is a
689            // lambda expression at statement position — delegate
690            // to the expression parser so it becomes an `ExprStmt`.
691            Token::Fn if matches!(self.peek_at(1), Token::Ident(_)) => self.parse_fn_decl(),
692            Token::Return => self.parse_return(),
693            Token::Break => {
694                self.advance();
695                Ok(Stmt {
696                    kind: StmtKind::Break,
697                    line,
698                    column,
699                })
700            }
701            Token::Continue => {
702                self.advance();
703                Ok(Stmt {
704                    kind: StmtKind::Continue,
705                    line,
706                    column,
707                })
708            }
709            Token::Use => self.parse_use(),
710            Token::Struct => self.parse_struct_decl(),
711            Token::Enum => self.parse_enum_decl(),
712            _ => self.parse_expr_or_assign(),
713        }
714    }
715
716    fn parse_struct_decl(&mut self) -> Result<Stmt, BopError> {
717        let (line, column) = self.peek_pos();
718        self.advance(); // consume `struct`
719        let (name, name_line) = self.expect_ident()?;
720        ensure_type_name(&name, "`struct` declaration", name_line)?;
721        self.expect(&Token::LBrace)?;
722        let mut fields = Vec::new();
723        if !matches!(self.peek(), Token::RBrace) {
724            let (f, f_line) = self.expect_ident()?;
725            ensure_value_name(&f, "struct field", f_line)?;
726            fields.push(f);
727            while matches!(self.peek(), Token::Comma) {
728                self.advance();
729                if matches!(self.peek(), Token::RBrace) {
730                    break; // trailing comma
731                }
732                let (f, f_line) = self.expect_ident()?;
733                ensure_value_name(&f, "struct field", f_line)?;
734                fields.push(f);
735            }
736        }
737        self.expect(&Token::RBrace)?;
738        Ok(Stmt {
739            kind: StmtKind::StructDecl { name, fields },
740            line,
741                    column,
742        })
743    }
744
745    fn parse_enum_decl(&mut self) -> Result<Stmt, BopError> {
746        let (line, column) = self.peek_pos();
747        self.advance(); // consume `enum`
748        let (name, name_line) = self.expect_ident()?;
749        ensure_type_name(&name, "`enum` declaration", name_line)?;
750        self.expect(&Token::LBrace)?;
751        let mut variants: Vec<VariantDecl> = Vec::new();
752        if !matches!(self.peek(), Token::RBrace) {
753            variants.push(self.parse_variant_decl()?);
754            while matches!(self.peek(), Token::Comma) {
755                self.advance();
756                if matches!(self.peek(), Token::RBrace) {
757                    break; // trailing comma
758                }
759                variants.push(self.parse_variant_decl()?);
760            }
761        }
762        self.expect(&Token::RBrace)?;
763        Ok(Stmt {
764            kind: StmtKind::EnumDecl { name, variants },
765            line,
766                    column,
767        })
768    }
769
770    fn parse_variant_decl(&mut self) -> Result<VariantDecl, BopError> {
771        let (name, name_line) = self.expect_ident()?;
772        ensure_type_name(&name, "enum variant", name_line)?;
773        let kind = match self.peek() {
774            Token::LParen => {
775                self.advance();
776                let mut fields: Vec<String> = Vec::new();
777                if !matches!(self.peek(), Token::RParen) {
778                    let (f, f_line) = self.expect_ident()?;
779                    ensure_value_name(&f, "variant payload field", f_line)?;
780                    fields.push(f);
781                    while matches!(self.peek(), Token::Comma) {
782                        self.advance();
783                        if matches!(self.peek(), Token::RParen) {
784                            break;
785                        }
786                        let (f, f_line) = self.expect_ident()?;
787                        ensure_value_name(&f, "variant payload field", f_line)?;
788                        fields.push(f);
789                    }
790                }
791                self.expect(&Token::RParen)?;
792                VariantKind::Tuple(fields)
793            }
794            Token::LBrace => {
795                self.advance();
796                let mut fields: Vec<String> = Vec::new();
797                if !matches!(self.peek(), Token::RBrace) {
798                    let (f, f_line) = self.expect_ident()?;
799                    ensure_value_name(&f, "variant payload field", f_line)?;
800                    fields.push(f);
801                    while matches!(self.peek(), Token::Comma) {
802                        self.advance();
803                        if matches!(self.peek(), Token::RBrace) {
804                            break;
805                        }
806                        let (f, f_line) = self.expect_ident()?;
807                        ensure_value_name(&f, "variant payload field", f_line)?;
808                        fields.push(f);
809                    }
810                }
811                self.expect(&Token::RBrace)?;
812                VariantKind::Struct(fields)
813            }
814            _ => VariantKind::Unit,
815        };
816        Ok(VariantDecl { name, kind })
817    }
818
819    fn parse_use(&mut self) -> Result<Stmt, BopError> {
820        let (line, column) = self.peek_pos();
821        self.advance(); // consume `use`
822        let (first, first_line) = self.expect_ident()?;
823        ensure_value_name(&first, "module path segment", first_line)?;
824        let mut path = first;
825
826        // Consume dotted path segments. Breaks early if we see a
827        // `.{` — the selective-import opener.
828        loop {
829            if !matches!(self.peek(), Token::Dot) {
830                break;
831            }
832            // Peek ahead one more token: `.{` opens selective
833            // imports; `.ident` continues the path.
834            if matches!(self.peek_at(1), Token::LBrace) {
835                break;
836            }
837            self.advance(); // consume '.'
838            let (seg, seg_line) = self.expect_ident()?;
839            ensure_value_name(&seg, "module path segment", seg_line)?;
840            path.push('.');
841            path.push_str(&seg);
842        }
843
844        // Selective import: `use foo.bar.{a, b, c}`.
845        let items = if matches!(self.peek(), Token::Dot) {
846            self.advance(); // '.'
847            self.expect(&Token::LBrace)?;
848            let mut list: Vec<String> = Vec::new();
849            if !matches!(self.peek(), Token::RBrace) {
850                let (name, _) = self.expect_ident()?;
851                list.push(name);
852                while matches!(self.peek(), Token::Comma) {
853                    self.advance();
854                    if matches!(self.peek(), Token::RBrace) {
855                        break; // trailing comma
856                    }
857                    let (name, _) = self.expect_ident()?;
858                    list.push(name);
859                }
860            }
861            self.expect(&Token::RBrace)?;
862            Some(list)
863        } else {
864            None
865        };
866
867        // Optional `as alias`.
868        let alias = if matches!(self.peek(), Token::As) {
869            self.advance();
870            let (name, name_line) = self.expect_ident()?;
871            ensure_value_name(&name, "`use` alias", name_line)?;
872            Some(name)
873        } else {
874            None
875        };
876
877        Ok(Stmt {
878            kind: StmtKind::Use { path, items, alias },
879            line,
880                    column,
881        })
882    }
883
884    fn parse_let(&mut self) -> Result<Stmt, BopError> {
885        let (line, column) = self.peek_pos();
886        self.advance(); // consume 'let'
887        let (name, _) = self.expect_ident()?;
888        ensure_value_name(&name, "`let` binding", line)?;
889        self.expect(&Token::Eq)?;
890        let value = self.parse_expr()?;
891        Ok(Stmt {
892            kind: StmtKind::Let { name, value, is_const: false },
893            line,
894                    column,
895        })
896    }
897
898    /// `const NAME = expr` — immutable binding, SCREAMING_SNAKE_CASE
899    /// name enforced. Shares the `StmtKind::Let` variant with a
900    /// `is_const: true` flag — the runtime treats constants as
901    /// let bindings that were parsed in a way that makes
902    /// reassignment impossible (the parser rejects any `=` whose
903    /// LHS is an all-uppercase identifier).
904    fn parse_const(&mut self) -> Result<Stmt, BopError> {
905        let (line, column) = self.peek_pos();
906        self.advance(); // consume 'const'
907        let (name, _) = self.expect_ident()?;
908        ensure_constant_name(&name, "`const` declaration", line)?;
909        self.expect(&Token::Eq)?;
910        let value = self.parse_expr()?;
911        Ok(Stmt {
912            kind: StmtKind::Let { name, value, is_const: true },
913            line,
914                    column,
915        })
916    }
917
918    fn parse_if_stmt(&mut self) -> Result<Stmt, BopError> {
919        let (line, column) = self.peek_pos();
920        self.advance(); // consume 'if'
921        let condition = self.without_struct_literal(|p| p.parse_expr())?;
922        let body = self.parse_block()?;
923
924        let mut else_ifs = Vec::new();
925        let mut else_body = None;
926
927        while matches!(self.peek(), Token::Else) {
928            self.advance(); // consume 'else'
929            if matches!(self.peek(), Token::If) {
930                self.advance(); // consume 'if'
931                let cond = self.without_struct_literal(|p| p.parse_expr())?;
932                let block = self.parse_block()?;
933                else_ifs.push((cond, block));
934            } else {
935                else_body = Some(self.parse_block()?);
936                break;
937            }
938        }
939
940        Ok(Stmt {
941            kind: StmtKind::If {
942                condition,
943                body,
944                else_ifs,
945                else_body,
946            },
947            line,
948                    column,
949        })
950    }
951
952    fn parse_while(&mut self) -> Result<Stmt, BopError> {
953        let (line, column) = self.peek_pos();
954        self.advance(); // consume 'while'
955        let condition = self.without_struct_literal(|p| p.parse_expr())?;
956        let body = self.parse_block()?;
957        Ok(Stmt {
958            kind: StmtKind::While { condition, body },
959            line,
960                    column,
961        })
962    }
963
964    fn parse_for(&mut self) -> Result<Stmt, BopError> {
965        let (line, column) = self.peek_pos();
966        self.advance(); // consume 'for'
967        let (var, var_line) = self.expect_ident()?;
968        ensure_value_name(&var, "`for` loop variable", var_line)?;
969        self.expect(&Token::In)?;
970        let iterable = self.without_struct_literal(|p| p.parse_expr())?;
971        let body = self.parse_block()?;
972        Ok(Stmt {
973            kind: StmtKind::ForIn {
974                var,
975                iterable,
976                body,
977            },
978            line,
979                    column,
980        })
981    }
982
983    fn parse_repeat(&mut self) -> Result<Stmt, BopError> {
984        let (line, column) = self.peek_pos();
985        self.advance(); // consume 'repeat'
986        let count = self.without_struct_literal(|p| p.parse_expr())?;
987        let body = self.parse_block()?;
988        Ok(Stmt {
989            kind: StmtKind::Repeat { count, body },
990            line,
991                    column,
992        })
993    }
994
995    fn parse_fn_decl(&mut self) -> Result<Stmt, BopError> {
996        let (line, column) = self.peek_pos();
997        self.advance(); // consume 'fn'
998        let (name, name_line) = self.expect_ident()?;
999
1000        // Method declaration: `fn Type.method(...)`. The leading
1001        // ident is the receiver's type; the post-dot ident is the
1002        // method's name. Everything else matches a regular fn
1003        // decl.
1004        if matches!(self.peek(), Token::Dot) {
1005            ensure_type_name(&name, "method receiver", name_line)?;
1006            self.advance();
1007            let (method_name, method_line) = self.expect_ident()?;
1008            ensure_value_name(&method_name, "method name", method_line)?;
1009            self.expect(&Token::LParen)?;
1010            let mut params = Vec::new();
1011            if !matches!(self.peek(), Token::RParen) {
1012                let (p, p_line) = self.expect_ident()?;
1013                ensure_value_name(&p, "method parameter", p_line)?;
1014                params.push(p);
1015                while matches!(self.peek(), Token::Comma) {
1016                    self.advance();
1017                    let (p, p_line) = self.expect_ident()?;
1018                    ensure_value_name(&p, "method parameter", p_line)?;
1019                    params.push(p);
1020                }
1021            }
1022            self.expect(&Token::RParen)?;
1023            let body = self.parse_block()?;
1024            return Ok(Stmt {
1025                kind: StmtKind::MethodDecl {
1026                    type_name: name,
1027                    method_name,
1028                    params,
1029                    body,
1030                },
1031                line,
1032                    column,
1033            });
1034        }
1035
1036        ensure_value_name(&name, "`fn` declaration", name_line)?;
1037        self.expect(&Token::LParen)?;
1038
1039        let mut params = Vec::new();
1040        if !matches!(self.peek(), Token::RParen) {
1041            let (p, p_line) = self.expect_ident()?;
1042            ensure_value_name(&p, "function parameter", p_line)?;
1043            params.push(p);
1044            while matches!(self.peek(), Token::Comma) {
1045                self.advance();
1046                let (p, p_line) = self.expect_ident()?;
1047                ensure_value_name(&p, "function parameter", p_line)?;
1048                params.push(p);
1049            }
1050        }
1051        self.expect(&Token::RParen)?;
1052        let body = self.parse_block()?;
1053        Ok(Stmt {
1054            kind: StmtKind::FnDecl { name, params, body },
1055            line,
1056                    column,
1057        })
1058    }
1059
1060    fn parse_return(&mut self) -> Result<Stmt, BopError> {
1061        let (line, column) = self.peek_pos();
1062        self.advance(); // consume 'return'
1063        let value = if matches!(self.peek(), Token::Semicolon | Token::RBrace | Token::Eof) {
1064            None
1065        } else {
1066            Some(self.parse_expr()?)
1067        };
1068        Ok(Stmt {
1069            kind: StmtKind::Return { value },
1070            line,
1071                    column,
1072        })
1073    }
1074
1075    fn parse_expr_or_assign(&mut self) -> Result<Stmt, BopError> {
1076        let (line, column) = self.peek_pos();
1077        let expr = self.parse_expr()?;
1078
1079        let op = match self.peek() {
1080            Token::Eq => Some(AssignOp::Eq),
1081            Token::PlusEq => Some(AssignOp::AddEq),
1082            Token::MinusEq => Some(AssignOp::SubEq),
1083            Token::StarEq => Some(AssignOp::MulEq),
1084            Token::SlashEq => Some(AssignOp::DivEq),
1085            Token::PercentEq => Some(AssignOp::ModEq),
1086            _ => None,
1087        };
1088
1089        if let Some(op) = op {
1090            self.advance(); // consume assignment operator
1091            let target = expr_to_assign_target(expr, line)?;
1092            let value = self.parse_expr()?;
1093            Ok(Stmt {
1094                kind: StmtKind::Assign { target, op, value },
1095                line,
1096                    column,
1097            })
1098        } else {
1099            Ok(Stmt {
1100                kind: StmtKind::ExprStmt(expr),
1101                line,
1102                    column,
1103            })
1104        }
1105    }
1106
1107    // ─── Expressions ───────────────────────────────────────────────────
1108
1109    fn parse_expr(&mut self) -> Result<Expr, BopError> {
1110        self.parse_or()
1111    }
1112
1113    fn parse_or(&mut self) -> Result<Expr, BopError> {
1114        let mut left = self.parse_and()?;
1115        while matches!(self.peek(), Token::PipePipe) {
1116            let (line, column) = self.peek_pos();
1117            self.advance();
1118            let right = self.parse_and()?;
1119            left = Expr {
1120                kind: ExprKind::BinaryOp {
1121                    left: Box::new(left),
1122                    op: BinOp::Or,
1123                    right: Box::new(right),
1124                },
1125                line,
1126                    column,
1127            };
1128        }
1129        Ok(left)
1130    }
1131
1132    fn parse_and(&mut self) -> Result<Expr, BopError> {
1133        let mut left = self.parse_equality()?;
1134        while matches!(self.peek(), Token::AmpAmp) {
1135            let (line, column) = self.peek_pos();
1136            self.advance();
1137            let right = self.parse_equality()?;
1138            left = Expr {
1139                kind: ExprKind::BinaryOp {
1140                    left: Box::new(left),
1141                    op: BinOp::And,
1142                    right: Box::new(right),
1143                },
1144                line,
1145                    column,
1146            };
1147        }
1148        Ok(left)
1149    }
1150
1151    fn parse_equality(&mut self) -> Result<Expr, BopError> {
1152        let mut left = self.parse_comparison()?;
1153        while matches!(self.peek(), Token::EqEq | Token::BangEq) {
1154            let (line, column) = self.peek_pos();
1155            let op = if matches!(self.peek(), Token::EqEq) {
1156                BinOp::Eq
1157            } else {
1158                BinOp::NotEq
1159            };
1160            self.advance();
1161            let right = self.parse_comparison()?;
1162            left = Expr {
1163                kind: ExprKind::BinaryOp {
1164                    left: Box::new(left),
1165                    op,
1166                    right: Box::new(right),
1167                },
1168                line,
1169                    column,
1170            };
1171        }
1172        Ok(left)
1173    }
1174
1175    fn parse_comparison(&mut self) -> Result<Expr, BopError> {
1176        let mut left = self.parse_addition()?;
1177        while matches!(
1178            self.peek(),
1179            Token::Lt | Token::Gt | Token::LtEq | Token::GtEq
1180        ) {
1181            let (line, column) = self.peek_pos();
1182            let op = match self.peek() {
1183                Token::Lt => BinOp::Lt,
1184                Token::Gt => BinOp::Gt,
1185                Token::LtEq => BinOp::LtEq,
1186                _ => BinOp::GtEq,
1187            };
1188            self.advance();
1189            let right = self.parse_addition()?;
1190            left = Expr {
1191                kind: ExprKind::BinaryOp {
1192                    left: Box::new(left),
1193                    op,
1194                    right: Box::new(right),
1195                },
1196                line,
1197                    column,
1198            };
1199        }
1200        Ok(left)
1201    }
1202
1203    fn parse_addition(&mut self) -> Result<Expr, BopError> {
1204        let mut left = self.parse_multiply()?;
1205        while matches!(self.peek(), Token::Plus | Token::Minus) {
1206            let (line, column) = self.peek_pos();
1207            let op = if matches!(self.peek(), Token::Plus) {
1208                BinOp::Add
1209            } else {
1210                BinOp::Sub
1211            };
1212            self.advance();
1213            let right = self.parse_multiply()?;
1214            left = Expr {
1215                kind: ExprKind::BinaryOp {
1216                    left: Box::new(left),
1217                    op,
1218                    right: Box::new(right),
1219                },
1220                line,
1221                    column,
1222            };
1223        }
1224        Ok(left)
1225    }
1226
1227    fn parse_multiply(&mut self) -> Result<Expr, BopError> {
1228        let mut left = self.parse_unary()?;
1229        while matches!(
1230            self.peek(),
1231            Token::Star | Token::Slash | Token::Percent
1232        ) {
1233            let (line, column) = self.peek_pos();
1234            let op = match self.peek() {
1235                Token::Star => BinOp::Mul,
1236                Token::Slash => BinOp::Div,
1237                _ => BinOp::Mod,
1238            };
1239            self.advance();
1240            let right = self.parse_unary()?;
1241            left = Expr {
1242                kind: ExprKind::BinaryOp {
1243                    left: Box::new(left),
1244                    op,
1245                    right: Box::new(right),
1246                },
1247                line,
1248                    column,
1249            };
1250        }
1251        Ok(left)
1252    }
1253
1254    fn parse_unary(&mut self) -> Result<Expr, BopError> {
1255        self.enter()?;
1256        let (line, column) = self.peek_pos();
1257        let result = match self.peek() {
1258            Token::Bang => {
1259                self.advance();
1260                let expr = self.parse_unary()?;
1261                Ok(Expr {
1262                    kind: ExprKind::UnaryOp {
1263                        op: UnaryOp::Not,
1264                        expr: Box::new(expr),
1265                    },
1266                    line,
1267                    column,
1268                })
1269            }
1270            Token::Minus => {
1271                self.advance();
1272                let expr = self.parse_unary()?;
1273                Ok(Expr {
1274                    kind: ExprKind::UnaryOp {
1275                        op: UnaryOp::Neg,
1276                        expr: Box::new(expr),
1277                    },
1278                    line,
1279                    column,
1280                })
1281            }
1282            Token::Try => {
1283                // `try <expr>` binds tighter than binary ops but
1284                // looser than postfix (calls, methods, indexing),
1285                // mirroring Rust's `?`. Recursing into
1286                // `parse_unary` lets `try try foo()` parse as
1287                // `try (try foo())` without a special case.
1288                self.advance();
1289                let expr = self.parse_unary()?;
1290                Ok(Expr {
1291                    kind: ExprKind::Try(Box::new(expr)),
1292                    line,
1293                    column,
1294                })
1295            }
1296            _ => self.parse_postfix(),
1297        };
1298        self.leave();
1299        result
1300    }
1301
1302    fn parse_postfix(&mut self) -> Result<Expr, BopError> {
1303        let mut expr = self.parse_primary()?;
1304
1305        loop {
1306            match self.peek() {
1307                Token::LParen => {
1308                    let (line, column) = self.peek_pos();
1309                    self.advance();
1310                    let args = self.parse_args()?;
1311                    self.expect(&Token::RParen)?;
1312                    expr = Expr {
1313                        kind: ExprKind::Call {
1314                            callee: Box::new(expr),
1315                            args,
1316                        },
1317                        line,
1318                    column,
1319                    };
1320                }
1321                Token::LBracket => {
1322                    let (line, column) = self.peek_pos();
1323                    self.advance();
1324                    let index = self.parse_expr()?;
1325                    self.expect(&Token::RBracket)?;
1326                    expr = Expr {
1327                        kind: ExprKind::Index {
1328                            object: Box::new(expr),
1329                            index: Box::new(index),
1330                        },
1331                        line,
1332                    column,
1333                    };
1334                }
1335                Token::Dot => {
1336                    let (line, column) = self.peek_pos();
1337                    self.advance();
1338                    let (name, _) = self.expect_ident()?;
1339
1340                    // `a.B::V(...)` / `a.B { ... }` — namespaced
1341                    // type access through a module alias. We only
1342                    // take this path when the receiver is a bare
1343                    // `Ident` (the alias) and the field is a
1344                    // type-shape name. Anything else (method
1345                    // call, plain field read) falls through.
1346                    if let ExprKind::Ident(ns) = &expr.kind {
1347                        if naming::is_type_name(&name) {
1348                            match self.peek() {
1349                                Token::ColonColon => {
1350                                    let ns_owned = ns.clone();
1351                                    expr = self.parse_enum_variant_tail(
1352                                        name,
1353                                        Some(ns_owned),
1354                                        line,
1355                                        expr.column,
1356                                    )?;
1357                                    continue;
1358                                }
1359                                Token::LBrace if self.allow_struct_literal => {
1360                                    let ns_owned = ns.clone();
1361                                    expr = self.parse_struct_literal(
1362                                        name,
1363                                        Some(ns_owned),
1364                                        line,
1365                                        expr.column,
1366                                    )?;
1367                                    continue;
1368                                }
1369                                _ => {}
1370                            }
1371                        }
1372                    }
1373
1374                    if matches!(self.peek(), Token::LParen) {
1375                        // Method call: `.name(args)`.
1376                        self.advance();
1377                        let args = self.parse_args()?;
1378                        self.expect(&Token::RParen)?;
1379                        expr = Expr {
1380                            kind: ExprKind::MethodCall {
1381                                object: Box::new(expr),
1382                                method: name,
1383                                args,
1384                            },
1385                            line,
1386                    column,
1387                        };
1388                    } else {
1389                        // Bare field read: `.name`.
1390                        expr = Expr {
1391                            kind: ExprKind::FieldAccess {
1392                                object: Box::new(expr),
1393                                field: name,
1394                            },
1395                            line,
1396                    column,
1397                        };
1398                    }
1399                }
1400                _ => break,
1401            }
1402        }
1403
1404        Ok(expr)
1405    }
1406
1407    fn parse_args(&mut self) -> Result<Vec<Expr>, BopError> {
1408        let mut args = Vec::new();
1409        if !matches!(self.peek(), Token::RParen) {
1410            args.push(self.parse_expr()?);
1411            while matches!(self.peek(), Token::Comma) {
1412                self.advance();
1413                args.push(self.parse_expr()?);
1414            }
1415        }
1416        Ok(args)
1417    }
1418
1419    fn parse_primary(&mut self) -> Result<Expr, BopError> {
1420        let (line, column) = self.peek_pos();
1421
1422        match self.peek().clone() {
1423            Token::Int(n) => {
1424                self.advance();
1425                Ok(Expr {
1426                    kind: ExprKind::Int(n),
1427                    line,
1428                    column,
1429                })
1430            }
1431            Token::Number(n) => {
1432                self.advance();
1433                Ok(Expr {
1434                    kind: ExprKind::Number(n),
1435                    line,
1436                    column,
1437                })
1438            }
1439            Token::Str(s) => {
1440                self.advance();
1441                Ok(Expr {
1442                    kind: ExprKind::Str(s),
1443                    line,
1444                    column,
1445                })
1446            }
1447            Token::StringInterp(parts) => {
1448                self.advance();
1449                Ok(Expr {
1450                    kind: ExprKind::StringInterp(parts),
1451                    line,
1452                    column,
1453                })
1454            }
1455            Token::True => {
1456                self.advance();
1457                Ok(Expr {
1458                    kind: ExprKind::Bool(true),
1459                    line,
1460                    column,
1461                })
1462            }
1463            Token::False => {
1464                self.advance();
1465                Ok(Expr {
1466                    kind: ExprKind::Bool(false),
1467                    line,
1468                    column,
1469                })
1470            }
1471            Token::None => {
1472                self.advance();
1473                Ok(Expr {
1474                    kind: ExprKind::None,
1475                    line,
1476                    column,
1477                })
1478            }
1479            Token::Ident(name) => {
1480                self.advance();
1481                // Sugar: bare `Ok(args)` / `Err(args)` desugars
1482                // to the built-in `Result::Ok(args)` /
1483                // `Result::Err(args)`. Bop's case rules already
1484                // reserve uppercase idents for type / variant
1485                // names, so `Ok` and `Err` can't collide with a
1486                // user fn or variable. Users who want the `Ok` /
1487                // `Err` variants of a *different* enum have to
1488                // name it explicitly (`MyEnum::Ok(...)`).
1489                if (name == "Ok" || name == "Err")
1490                    && matches!(self.peek(), Token::LParen)
1491                {
1492                    return self.parse_result_shorthand(name, line, column);
1493                }
1494                // Enum variant construction: `Type::Variant…`.
1495                // Always parse (the `::` is unambiguous); the
1496                // payload shape is determined by what follows
1497                // the variant name.
1498                if matches!(self.peek(), Token::ColonColon) {
1499                    return self.parse_enum_variant_tail(name, None, line, column);
1500                }
1501                // Struct literal: `Name { field: value, ... }`.
1502                // Parsed only when struct literals are allowed in
1503                // the current context (see
1504                // `without_struct_literal`). This keeps `if foo {
1505                // body }` / `for x in arr { body }` parseable.
1506                if self.allow_struct_literal && matches!(self.peek(), Token::LBrace) {
1507                    return self.parse_struct_literal(name, None, line, column);
1508                }
1509                Ok(Expr {
1510                    kind: ExprKind::Ident(name),
1511                    line,
1512                    column,
1513                })
1514            }
1515            Token::LParen => {
1516                self.enter()?;
1517                self.advance();
1518                let expr = self.parse_expr()?;
1519                self.expect(&Token::RParen)?;
1520                self.leave();
1521                Ok(expr)
1522            }
1523            Token::LBracket => self.parse_array_literal(),
1524            Token::LBrace => self.parse_dict_literal(),
1525            Token::If => self.parse_if_expr(),
1526            Token::Fn => self.parse_lambda(),
1527            Token::Match => self.parse_match_expr(),
1528            _ => Err(self.error(
1529                line,
1530                format!("I didn't expect `{}` here", fmt_token(self.peek())),
1531            )),
1532        }
1533    }
1534
1535    fn parse_enum_variant_tail(
1536        &mut self,
1537        type_name: String,
1538        namespace: Option<String>,
1539        line: u32,
1540        column: Option<core::num::NonZeroU32>,
1541    ) -> Result<Expr, BopError> {
1542        self.advance(); // consume `::`
1543        let (variant, _) = self.expect_ident()?;
1544        let payload = match self.peek() {
1545            Token::LParen => {
1546                self.enter()?;
1547                self.advance();
1548                let mut args: Vec<Expr> = Vec::new();
1549                if !matches!(self.peek(), Token::RParen) {
1550                    args.push(self.parse_expr()?);
1551                    while matches!(self.peek(), Token::Comma) {
1552                        self.advance();
1553                        if matches!(self.peek(), Token::RParen) {
1554                            break;
1555                        }
1556                        args.push(self.parse_expr()?);
1557                    }
1558                }
1559                self.expect(&Token::RParen)?;
1560                self.leave();
1561                VariantPayload::Tuple(args)
1562            }
1563            Token::LBrace if self.allow_struct_literal => {
1564                self.enter()?;
1565                self.advance();
1566                let mut fields: Vec<(String, Expr)> = Vec::new();
1567                if !matches!(self.peek(), Token::RBrace) {
1568                    let (fname, _) = self.expect_ident()?;
1569                    self.expect(&Token::Colon)?;
1570                    let fvalue = self.parse_expr()?;
1571                    fields.push((fname, fvalue));
1572                    while matches!(self.peek(), Token::Comma) {
1573                        self.advance();
1574                        if matches!(self.peek(), Token::RBrace) {
1575                            break;
1576                        }
1577                        let (fname, _) = self.expect_ident()?;
1578                        self.expect(&Token::Colon)?;
1579                        let fvalue = self.parse_expr()?;
1580                        fields.push((fname, fvalue));
1581                    }
1582                }
1583                self.expect(&Token::RBrace)?;
1584                self.leave();
1585                VariantPayload::Struct(fields)
1586            }
1587            _ => VariantPayload::Unit,
1588        };
1589        Ok(Expr {
1590            kind: ExprKind::EnumConstruct {
1591                namespace,
1592                type_name,
1593                variant,
1594                payload,
1595            },
1596            line,
1597                    column,
1598        })
1599    }
1600
1601    /// `Ok(args)` / `Err(args)` — parser-level sugar for the
1602    /// built-in `Result::Ok(args)` / `Result::Err(args)` so user
1603    /// code can skip the `Result::` prefix for the two variants
1604    /// used overwhelmingly often. The caller already advanced
1605    /// past the identifier and verified the lookahead is
1606    /// `LParen`; `variant` must be `"Ok"` or `"Err"`.
1607    fn parse_result_shorthand(
1608        &mut self,
1609        variant: String,
1610        line: u32,
1611        column: Option<core::num::NonZeroU32>,
1612    ) -> Result<Expr, BopError> {
1613        debug_assert!(variant == "Ok" || variant == "Err");
1614        self.enter()?;
1615        self.expect(&Token::LParen)?;
1616        let mut args: Vec<Expr> = Vec::new();
1617        if !matches!(self.peek(), Token::RParen) {
1618            args.push(self.parse_expr()?);
1619            while matches!(self.peek(), Token::Comma) {
1620                self.advance();
1621                if matches!(self.peek(), Token::RParen) {
1622                    break;
1623                }
1624                args.push(self.parse_expr()?);
1625            }
1626        }
1627        self.expect(&Token::RParen)?;
1628        self.leave();
1629        Ok(Expr {
1630            kind: ExprKind::EnumConstruct {
1631                namespace: None,
1632                type_name: String::from("Result"),
1633                variant,
1634                payload: VariantPayload::Tuple(args),
1635            },
1636            line,
1637            column,
1638        })
1639    }
1640
1641    fn parse_struct_literal(
1642        &mut self,
1643        type_name: String,
1644        namespace: Option<String>,
1645        line: u32,
1646        column: Option<core::num::NonZeroU32>,
1647    ) -> Result<Expr, BopError> {
1648        self.enter()?;
1649        self.expect(&Token::LBrace)?;
1650        let mut fields: Vec<(String, Expr)> = Vec::new();
1651        if !matches!(self.peek(), Token::RBrace) {
1652            let (fname, _) = self.expect_ident()?;
1653            self.expect(&Token::Colon)?;
1654            let fvalue = self.parse_expr()?;
1655            fields.push((fname, fvalue));
1656            while matches!(self.peek(), Token::Comma) {
1657                self.advance();
1658                if matches!(self.peek(), Token::RBrace) {
1659                    break; // trailing comma
1660                }
1661                let (fname, _) = self.expect_ident()?;
1662                self.expect(&Token::Colon)?;
1663                let fvalue = self.parse_expr()?;
1664                fields.push((fname, fvalue));
1665            }
1666        }
1667        self.expect(&Token::RBrace)?;
1668        self.leave();
1669        Ok(Expr {
1670            kind: ExprKind::StructConstruct {
1671                namespace,
1672                type_name,
1673                fields,
1674            },
1675            line,
1676                    column,
1677        })
1678    }
1679
1680    fn parse_array_literal(&mut self) -> Result<Expr, BopError> {
1681        let (line, column) = self.peek_pos();
1682        self.advance(); // consume [
1683        let mut elements = Vec::new();
1684        if !matches!(self.peek(), Token::RBracket) {
1685            elements.push(self.parse_expr()?);
1686            while matches!(self.peek(), Token::Comma) {
1687                self.advance();
1688                if matches!(self.peek(), Token::RBracket) {
1689                    break; // trailing comma
1690                }
1691                elements.push(self.parse_expr()?);
1692            }
1693        }
1694        self.expect(&Token::RBracket)?;
1695        Ok(Expr {
1696            kind: ExprKind::Array(elements),
1697            line,
1698                    column,
1699        })
1700    }
1701
1702    fn parse_dict_literal(&mut self) -> Result<Expr, BopError> {
1703        let (line, column) = self.peek_pos();
1704        self.advance(); // consume {
1705        let mut entries = Vec::new();
1706        if !matches!(self.peek(), Token::RBrace) {
1707            let key = self.expect_string_key()?;
1708            self.expect(&Token::Colon)?;
1709            let value = self.parse_expr()?;
1710            entries.push((key, value));
1711            while matches!(self.peek(), Token::Comma) {
1712                self.advance();
1713                if matches!(self.peek(), Token::RBrace) {
1714                    break; // trailing comma
1715                }
1716                let key = self.expect_string_key()?;
1717                self.expect(&Token::Colon)?;
1718                let value = self.parse_expr()?;
1719                entries.push((key, value));
1720            }
1721        }
1722        self.expect(&Token::RBrace)?;
1723        Ok(Expr {
1724            kind: ExprKind::Dict(entries),
1725            line,
1726                    column,
1727        })
1728    }
1729
1730    fn expect_string_key(&mut self) -> Result<String, BopError> {
1731        let (line, _column) = self.peek_pos();
1732        match self.peek().clone() {
1733            Token::Str(s) => {
1734                self.advance();
1735                Ok(s)
1736            }
1737            _ => Err(self.error(line, "Dict keys must be strings (in quotes)")),
1738        }
1739    }
1740
1741    fn parse_match_expr(&mut self) -> Result<Expr, BopError> {
1742        let (line, column) = self.peek_pos();
1743        self.advance(); // consume 'match'
1744        // Scrutinee reads without struct-literal parsing — same
1745        // rule as `if` / `while` / `for`, so `match foo { ... }`
1746        // stays parseable.
1747        let scrutinee = self.without_struct_literal(|p| p.parse_expr())?;
1748        self.expect(&Token::LBrace)?;
1749        let mut arms: Vec<MatchArm> = Vec::new();
1750        self.skip_semicolons();
1751        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1752            arms.push(self.parse_match_arm()?);
1753            // Arms separate by `,` (common) or `;` (auto-semi from
1754            // newline). Accept and continue; also accept trailing
1755            // separators before the closing brace.
1756            while matches!(self.peek(), Token::Comma | Token::Semicolon) {
1757                self.advance();
1758            }
1759        }
1760        self.expect(&Token::RBrace)?;
1761        Ok(Expr {
1762            kind: ExprKind::Match {
1763                scrutinee: Box::new(scrutinee),
1764                arms,
1765            },
1766            line,
1767                    column,
1768        })
1769    }
1770
1771    fn parse_match_arm(&mut self) -> Result<MatchArm, BopError> {
1772        let (line, _column) = self.peek_pos();
1773        let pattern = self.parse_pattern()?;
1774        let guard = if matches!(self.peek(), Token::If) {
1775            self.advance();
1776            Some(self.without_struct_literal(|p| p.parse_expr())?)
1777        } else {
1778            None
1779        };
1780        self.expect(&Token::FatArrow)?;
1781        let body = self.parse_expr()?;
1782        Ok(MatchArm {
1783            pattern,
1784            guard,
1785            body,
1786            line,
1787        })
1788    }
1789
1790    fn parse_pattern(&mut self) -> Result<Pattern, BopError> {
1791        let first = self.parse_pattern_single()?;
1792        // Or-pattern: `p1 | p2 | p3`. Keep the left-associative
1793        // tree flat in a single `Or` variant for ergonomic
1794        // matching later.
1795        if matches!(self.peek(), Token::Pipe) {
1796            let mut alts = vec![first];
1797            while matches!(self.peek(), Token::Pipe) {
1798                self.advance();
1799                alts.push(self.parse_pattern_single()?);
1800            }
1801            Ok(Pattern::Or(alts))
1802        } else {
1803            Ok(first)
1804        }
1805    }
1806
1807    fn parse_pattern_single(&mut self) -> Result<Pattern, BopError> {
1808        self.enter()?;
1809        let (line, _column) = self.peek_pos();
1810        let result = match self.peek().clone() {
1811            Token::Int(n) => {
1812                self.advance();
1813                Ok(Pattern::Literal(LiteralPattern::Int(n)))
1814            }
1815            Token::Number(n) => {
1816                self.advance();
1817                Ok(Pattern::Literal(LiteralPattern::Number(n)))
1818            }
1819            Token::Str(s) => {
1820                self.advance();
1821                Ok(Pattern::Literal(LiteralPattern::Str(s)))
1822            }
1823            Token::True => {
1824                self.advance();
1825                Ok(Pattern::Literal(LiteralPattern::Bool(true)))
1826            }
1827            Token::False => {
1828                self.advance();
1829                Ok(Pattern::Literal(LiteralPattern::Bool(false)))
1830            }
1831            Token::None => {
1832                self.advance();
1833                Ok(Pattern::Literal(LiteralPattern::None))
1834            }
1835            Token::Minus => {
1836                // Negative number literal: `-1`, `-3.14`.
1837                self.advance();
1838                match self.peek().clone() {
1839                    Token::Int(n) => {
1840                        self.advance();
1841                        // `i64::MIN` has no positive counterpart
1842                        // — negating it overflows. Raise a clear
1843                        // parse error rather than silently
1844                        // wrapping.
1845                        match n.checked_neg() {
1846                            Some(neg) => Ok(Pattern::Literal(LiteralPattern::Int(neg))),
1847                            None => Err(self.error(
1848                                line,
1849                                format!(
1850                                    "Integer literal `-{}` is out of range for i64",
1851                                    n
1852                                ),
1853                            )),
1854                        }
1855                    }
1856                    Token::Number(n) => {
1857                        self.advance();
1858                        Ok(Pattern::Literal(LiteralPattern::Number(-n)))
1859                    }
1860                    other => Err(self.error(
1861                        line,
1862                        format!(
1863                            "Expected a number after `-` in pattern, got `{}`",
1864                            fmt_token(&other)
1865                        ),
1866                    )),
1867                }
1868            }
1869            Token::LBracket => self.parse_pattern_array(),
1870            Token::Ident(name) if name == "_" => {
1871                self.advance();
1872                Ok(Pattern::Wildcard)
1873            }
1874            Token::Ident(name) => {
1875                self.advance();
1876                // Sugar: `Ok(p)` / `Err(p)` as patterns — mirror
1877                // the expression-side shortcut. Reduces the
1878                // `match r { Result::Ok(v) => ..., Result::Err(e)
1879                // => ... }` boilerplate to plain `Ok(v)` / `Err(e)`.
1880                if (name == "Ok" || name == "Err")
1881                    && matches!(self.peek(), Token::LParen)
1882                {
1883                    return self.parse_result_shorthand_pattern(name);
1884                }
1885                // `Type::Variant[...]` path pattern.
1886                if matches!(self.peek(), Token::ColonColon) {
1887                    self.parse_pattern_variant_tail(name, None)
1888                } else if matches!(self.peek(), Token::LBrace) {
1889                    // `Type { ... }` struct pattern — only when
1890                    // the `LBrace` is syntactically plausible as
1891                    // a struct pattern. Inside a match arm pattern
1892                    // it always is.
1893                    self.parse_pattern_struct(name, None)
1894                } else if matches!(self.peek(), Token::Dot)
1895                    && naming::is_value_name(&name)
1896                {
1897                    // `ns.Type...` — namespaced variant / struct
1898                    // pattern through a module alias. Only fires
1899                    // when the first segment is value-shaped
1900                    // (an alias), to keep bare `Type.field` from
1901                    // being misread as a pattern.
1902                    self.advance(); // consume '.'
1903                    let (type_name, type_line) = self.expect_ident()?;
1904                    if !naming::is_type_name(&type_name) {
1905                        return Err(self.error(
1906                            type_line,
1907                            format!(
1908                                "Expected a type name after `{}.` in pattern, got `{}`",
1909                                name, type_name
1910                            ),
1911                        ));
1912                    }
1913                    if matches!(self.peek(), Token::ColonColon) {
1914                        self.parse_pattern_variant_tail(type_name, Some(name))
1915                    } else if matches!(self.peek(), Token::LBrace) {
1916                        self.parse_pattern_struct(type_name, Some(name))
1917                    } else {
1918                        Err(self.error(
1919                            type_line,
1920                            format!(
1921                                "Expected `::Variant(...)` or `{{...}}` after `{}.{}` in pattern",
1922                                name, type_name
1923                            ),
1924                        ))
1925                    }
1926                } else {
1927                    // Bare identifier = binding. `_` is handled
1928                    // above as wildcard.
1929                    Ok(Pattern::Binding(name))
1930                }
1931            }
1932            other => Err(self.error(
1933                line,
1934                format!("Expected a pattern, got `{}`", fmt_token(&other)),
1935            )),
1936        };
1937        self.leave();
1938        result
1939    }
1940
1941    fn parse_pattern_array(&mut self) -> Result<Pattern, BopError> {
1942        self.advance(); // consume `[`
1943        let mut elements: Vec<Pattern> = Vec::new();
1944        let mut rest: Option<ArrayRest> = None;
1945        if !matches!(self.peek(), Token::RBracket) {
1946            loop {
1947                if matches!(self.peek(), Token::DotDot) {
1948                    self.advance();
1949                    // Optional name binding after `..`.
1950                    let captured = match self.peek().clone() {
1951                        Token::Ident(n) if n != "_" => {
1952                            self.advance();
1953                            ArrayRest::Named(n)
1954                        }
1955                        _ => ArrayRest::Ignored,
1956                    };
1957                    rest = Some(captured);
1958                    // `..` must be the last element in the array
1959                    // pattern; the parser enforces this by
1960                    // stopping here.
1961                    break;
1962                }
1963                elements.push(self.parse_pattern()?);
1964                if matches!(self.peek(), Token::Comma) {
1965                    self.advance();
1966                    if matches!(self.peek(), Token::RBracket) {
1967                        break; // trailing comma
1968                    }
1969                } else {
1970                    break;
1971                }
1972            }
1973        }
1974        self.expect(&Token::RBracket)?;
1975        Ok(Pattern::Array { elements, rest })
1976    }
1977
1978    fn parse_pattern_variant_tail(
1979        &mut self,
1980        type_name: String,
1981        namespace: Option<String>,
1982    ) -> Result<Pattern, BopError> {
1983        self.advance(); // consume `::`
1984        let (variant, _) = self.expect_ident()?;
1985        let payload = match self.peek() {
1986            Token::LParen => {
1987                self.advance();
1988                let mut items: Vec<Pattern> = Vec::new();
1989                if !matches!(self.peek(), Token::RParen) {
1990                    items.push(self.parse_pattern()?);
1991                    while matches!(self.peek(), Token::Comma) {
1992                        self.advance();
1993                        if matches!(self.peek(), Token::RParen) {
1994                            break;
1995                        }
1996                        items.push(self.parse_pattern()?);
1997                    }
1998                }
1999                self.expect(&Token::RParen)?;
2000                VariantPatternPayload::Tuple(items)
2001            }
2002            Token::LBrace => {
2003                let (fields, rest) = self.parse_pattern_field_list()?;
2004                VariantPatternPayload::Struct { fields, rest }
2005            }
2006            _ => VariantPatternPayload::Unit,
2007        };
2008        Ok(Pattern::EnumVariant {
2009            namespace,
2010            type_name,
2011            variant,
2012            payload,
2013        })
2014    }
2015
2016    /// Pattern-side mirror of [`Self::parse_result_shorthand`]:
2017    /// `Ok(p)` / `Err(p)` in a pattern desugar to the built-in
2018    /// `Result::Ok(p)` / `Result::Err(p)`. Caller has already
2019    /// advanced past the ident and verified `LParen` follows;
2020    /// `variant` must be `"Ok"` or `"Err"`.
2021    fn parse_result_shorthand_pattern(
2022        &mut self,
2023        variant: String,
2024    ) -> Result<Pattern, BopError> {
2025        debug_assert!(variant == "Ok" || variant == "Err");
2026        self.expect(&Token::LParen)?;
2027        let mut items: Vec<Pattern> = Vec::new();
2028        if !matches!(self.peek(), Token::RParen) {
2029            items.push(self.parse_pattern()?);
2030            while matches!(self.peek(), Token::Comma) {
2031                self.advance();
2032                if matches!(self.peek(), Token::RParen) {
2033                    break;
2034                }
2035                items.push(self.parse_pattern()?);
2036            }
2037        }
2038        self.expect(&Token::RParen)?;
2039        Ok(Pattern::EnumVariant {
2040            namespace: None,
2041            type_name: String::from("Result"),
2042            variant,
2043            payload: VariantPatternPayload::Tuple(items),
2044        })
2045    }
2046
2047    fn parse_pattern_struct(
2048        &mut self,
2049        type_name: String,
2050        namespace: Option<String>,
2051    ) -> Result<Pattern, BopError> {
2052        let (fields, rest) = self.parse_pattern_field_list()?;
2053        Ok(Pattern::Struct {
2054            namespace,
2055            type_name,
2056            fields,
2057            rest,
2058        })
2059    }
2060
2061    fn parse_pattern_field_list(
2062        &mut self,
2063    ) -> Result<(Vec<(String, Pattern)>, bool), BopError> {
2064        self.expect(&Token::LBrace)?;
2065        let mut fields: Vec<(String, Pattern)> = Vec::new();
2066        let mut rest = false;
2067        if !matches!(self.peek(), Token::RBrace) {
2068            loop {
2069                if matches!(self.peek(), Token::DotDot) {
2070                    self.advance();
2071                    rest = true;
2072                    break;
2073                }
2074                let (fname, _) = self.expect_ident()?;
2075                // Shorthand `{ field }` binds the field value to
2076                // a local named `field`. Full form `{ field: pat }`
2077                // lets the user nest or wildcard.
2078                let sub = if matches!(self.peek(), Token::Colon) {
2079                    self.advance();
2080                    self.parse_pattern()?
2081                } else {
2082                    Pattern::Binding(fname.clone())
2083                };
2084                fields.push((fname, sub));
2085                if matches!(self.peek(), Token::Comma) {
2086                    self.advance();
2087                    if matches!(self.peek(), Token::RBrace) {
2088                        break;
2089                    }
2090                } else {
2091                    break;
2092                }
2093            }
2094        }
2095        self.expect(&Token::RBrace)?;
2096        Ok((fields, rest))
2097    }
2098
2099    fn parse_lambda(&mut self) -> Result<Expr, BopError> {
2100        let (line, column) = self.peek_pos();
2101        self.advance(); // consume 'fn'
2102        self.expect(&Token::LParen)?;
2103
2104        let mut params = Vec::new();
2105        if !matches!(self.peek(), Token::RParen) {
2106            let (p, _) = self.expect_ident()?;
2107            params.push(p);
2108            while matches!(self.peek(), Token::Comma) {
2109                self.advance();
2110                let (p, _) = self.expect_ident()?;
2111                params.push(p);
2112            }
2113        }
2114        self.expect(&Token::RParen)?;
2115        let body = self.parse_block()?;
2116        Ok(Expr {
2117            kind: ExprKind::Lambda { params, body },
2118            line,
2119                    column,
2120        })
2121    }
2122
2123    fn parse_if_expr(&mut self) -> Result<Expr, BopError> {
2124        let (line, column) = self.peek_pos();
2125        self.advance(); // consume 'if'
2126        let condition = self.without_struct_literal(|p| p.parse_expr())?;
2127        self.expect(&Token::LBrace)?;
2128        let then_expr = self.parse_expr()?;
2129        self.expect(&Token::RBrace)?;
2130        self.expect(&Token::Else)?;
2131        self.expect(&Token::LBrace)?;
2132        let else_expr = self.parse_expr()?;
2133        self.expect(&Token::RBrace)?;
2134        Ok(Expr {
2135            kind: ExprKind::IfExpr {
2136                condition: Box::new(condition),
2137                then_expr: Box::new(then_expr),
2138                else_expr: Box::new(else_expr),
2139            },
2140            line,
2141                    column,
2142        })
2143    }
2144}
2145
2146// ─── Instruction counting ───────────────────────────────────────────────────
2147
2148/// Count instructions in a list of statements (AST-based, format-independent).
2149///
2150/// Every `Stmt` counts as 1 instruction. Compound statements (if/while/repeat/for)
2151/// recurse into their body. `FnDecl` counts as 1 but does NOT recurse into the
2152/// function body — this rewards defining reusable functions.
2153pub fn count_instructions(stmts: &[Stmt]) -> u32 {
2154    let mut count = 0u32;
2155    for stmt in stmts {
2156        count += 1; // the statement itself
2157        match &stmt.kind {
2158            StmtKind::If {
2159                body,
2160                else_ifs,
2161                else_body,
2162                ..
2163            } => {
2164                count += count_instructions(body);
2165                for (_, branch_body) in else_ifs {
2166                    count += count_instructions(branch_body);
2167                }
2168                if let Some(eb) = else_body {
2169                    count += count_instructions(eb);
2170                }
2171            }
2172            StmtKind::While { body, .. }
2173            | StmtKind::Repeat { body, .. }
2174            | StmtKind::ForIn { body, .. } => {
2175                count += count_instructions(body);
2176            }
2177            StmtKind::FnDecl { .. } => {
2178                // Don't recurse into function body — reward reuse
2179            }
2180            _ => {}
2181        }
2182    }
2183    count
2184}
2185
2186// ─── Helpers ───────────────────────────────────────────────────────────────
2187
2188fn expr_to_assign_target(expr: Expr, line: u32) -> Result<AssignTarget, BopError> {
2189    match expr.kind {
2190        ExprKind::Ident(name) => {
2191            // All-caps LHS → reassigning a constant. Refused at
2192            // parse time without needing scope tracking: the
2193            // parser already forbids `let` / `fn` bindings with
2194            // an all-caps shape, so any all-caps identifier in
2195            // the source must have come from a `const` declaration
2196            // (or is undeclared, in which case we give the user
2197            // the right kind of diagnostic anyway).
2198            if naming::is_constant_name(&name) {
2199                let mut err = BopError::runtime(
2200                    format!("can't reassign `{}` — it's a constant", name),
2201                    line,
2202                );
2203                err.friendly_hint = Some(
2204                    "constants are immutable. Use `let` if you want a mutable binding."
2205                        .to_string(),
2206                );
2207                return Err(err);
2208            }
2209            Ok(AssignTarget::Variable(name))
2210        }
2211        ExprKind::Index { object, index } => Ok(AssignTarget::Index {
2212            object: *object,
2213            index: *index,
2214        }),
2215        ExprKind::FieldAccess { object, field } => Ok(AssignTarget::Field {
2216            object: *object,
2217            field,
2218        }),
2219        _ => Err(BopError {
2220            line: Some(line),
2221            column: None,
2222            message:
2223                "You can only assign to a variable, an index (`arr[0]`), or a struct field (`point.x`)"
2224                    .to_string(),
2225            friendly_hint: None,
2226            is_fatal: false,
2227            is_try_return: false,
2228        }),
2229    }
2230}
2231
2232pub fn fmt_token(token: &Token) -> &'static str {
2233    match token {
2234        Token::Int(_) => "an integer",
2235        Token::Number(_) => "a number",
2236        Token::Str(_) | Token::StringInterp(_) => "a string",
2237        Token::True => "true",
2238        Token::False => "false",
2239        Token::None => "none",
2240        Token::Ident(_) => "a name",
2241        Token::Let => "let",
2242        Token::Const => "const",
2243        Token::Fn => "fn",
2244        Token::Return => "return",
2245        Token::If => "if",
2246        Token::Else => "else",
2247        Token::While => "while",
2248        Token::For => "for",
2249        Token::In => "in",
2250        Token::Repeat => "repeat",
2251        Token::Break => "break",
2252        Token::Continue => "continue",
2253        Token::Use => "use",
2254        Token::As => "as",
2255        Token::Struct => "struct",
2256        Token::Enum => "enum",
2257        Token::Match => "match",
2258        Token::Try => "try",
2259        Token::ColonColon => "::",
2260        Token::DotDot => "..",
2261        Token::FatArrow => "=>",
2262        Token::Pipe => "|",
2263        Token::Plus => "+",
2264        Token::Minus => "-",
2265        Token::Star => "*",
2266        Token::Slash => "/",
2267        Token::Percent => "%",
2268        Token::EqEq => "==",
2269        Token::BangEq => "!=",
2270        Token::Lt => "<",
2271        Token::Gt => ">",
2272        Token::LtEq => "<=",
2273        Token::GtEq => ">=",
2274        Token::AmpAmp => "&&",
2275        Token::PipePipe => "||",
2276        Token::Bang => "!",
2277        Token::Eq => "=",
2278        Token::PlusEq => "+=",
2279        Token::MinusEq => "-=",
2280        Token::StarEq => "*=",
2281        Token::SlashEq => "/=",
2282        Token::PercentEq => "%=",
2283        Token::LParen => "(",
2284        Token::RParen => ")",
2285        Token::LBracket => "[",
2286        Token::RBracket => "]",
2287        Token::LBrace => "{",
2288        Token::RBrace => "}",
2289        Token::Comma => ",",
2290        Token::Colon => ":",
2291        Token::Dot => ".",
2292        Token::Semicolon => ";",
2293        Token::Newline => "newline",
2294        Token::Eof => "end of code",
2295    }
2296}