Skip to main content

lua_parse/
lib.rs

1//! Lua parser — translates the token stream produced by the lexer into
2//! bytecode prototypes (`LuaProto`).
3//!
4//! # C source
5//! `reference/lua-5.4.7/src/lparser.c` (1968 lines, 95 functions)
6//!
7//! # Design notes (Phase A)
8//! * `BlockCnt` and `LhsAssign` form intrusive linked lists in C via raw
9//!   pointers to stack-allocated nodes. In Rust they become
10//!   `Option<Box<...>>` chains; `enter_block` pushes, `leave_block` pops.
11//! * `FuncState.prev` similarly uses `Option<Box<FuncState>>`.
12//! * `FuncState.f` is `Box<LuaProto>` during compilation (owned, mutably
13//!   accessible). types.tsv maps it to `GcRef<LuaProto>` but interior-
14//!   mutability via `Rc<RefCell<...>>` would be too noisy; Phase B can
15//!   switch. PORT NOTE: FuncState.f is Box<LuaProto>, not GcRef<LuaProto>.
16//! * `LexState` is logically defined in `lua-lex`; a minimal stub is declared
17//!   here for Phase A. Phase B will replace with `lua_lex::LexState` once
18//!   inter-crate deps are wired.
19//! * Cross-crate calls to `lua_code::luaK_*` and `lua_lex::luaX_*` are
20//!   written as qualified paths and will resolve in Phase B.
21//! * `LuaState` is from `lua-vm`; referenced here as an unresolved import.
22
23use lua_types::{AbsLineInfo, GcRef, LuaError, LuaString, LuaValue, LuaProto, UpvalDesc, LocalVar};
24
25// TODO(port): these imports resolve in Phase B when inter-crate deps land.
26// use lua_vm::LuaState;
27// use lua_code::{self, UnOpr, BinOpr, OpCode};
28
29// ── Token kind constants ────────────────────────────────────────────────────
30// C: RESERVED enum from llex.h; FIRST_RESERVED = 257 (UCHAR_MAX + 1).
31// TODO(port): replace with lua_lex::TokenKind enum when lua-lex lands.
32
33pub type TokenKind = i32;
34pub const TK_AND: TokenKind = 257;
35pub const TK_BREAK: TokenKind = 258;
36pub const TK_DO: TokenKind = 259;
37pub const TK_ELSE: TokenKind = 260;
38pub const TK_ELSEIF: TokenKind = 261;
39pub const TK_END: TokenKind = 262;
40pub const TK_FALSE: TokenKind = 263;
41pub const TK_FOR: TokenKind = 264;
42pub const TK_FUNCTION: TokenKind = 265;
43pub const TK_GOTO: TokenKind = 266;
44pub const TK_IF: TokenKind = 267;
45pub const TK_IN: TokenKind = 268;
46pub const TK_LOCAL: TokenKind = 269;
47pub const TK_NIL: TokenKind = 270;
48pub const TK_NOT: TokenKind = 271;
49pub const TK_OR: TokenKind = 272;
50pub const TK_REPEAT: TokenKind = 273;
51pub const TK_RETURN: TokenKind = 274;
52pub const TK_THEN: TokenKind = 275;
53pub const TK_TRUE: TokenKind = 276;
54pub const TK_UNTIL: TokenKind = 277;
55pub const TK_WHILE: TokenKind = 278;
56pub const TK_IDIV: TokenKind = 279;
57pub const TK_CONCAT: TokenKind = 280;
58pub const TK_DOTS: TokenKind = 281;
59pub const TK_EQ: TokenKind = 282;
60pub const TK_GE: TokenKind = 283;
61pub const TK_LE: TokenKind = 284;
62pub const TK_NE: TokenKind = 285;
63pub const TK_SHL: TokenKind = 286;
64pub const TK_SHR: TokenKind = 287;
65pub const TK_DBCOLON: TokenKind = 288;
66pub const TK_EOS: TokenKind = 289;
67pub const TK_FLT: TokenKind = 290;
68pub const TK_INT: TokenKind = 291;
69pub const TK_NAME: TokenKind = 292;
70pub const TK_STRING: TokenKind = 293;
71
72// ── Parser constants ────────────────────────────────────────────────────────
73
74/// C: #define MAXVARS 200
75const MAX_VARS: i32 = 200;
76
77/// C: NO_JUMP from lcode.h
78const NO_JUMP: i32 = -1;
79
80/// C: UNARY_PRIORITY 12
81const UNARY_PRIORITY: i32 = 12;
82
83/// C: LUA_MULTRET from lua.h
84const LUA_MULTRET: i32 = -1;
85
86/// C: MAXUPVAL 255 from lfunc.h
87const MAX_UPVAL: u8 = 255;
88
89/// C: MAXARG_Bx — max value for Bx field in an iABx instruction.
90/// TODO(port): should come from lua_types::opcode constants.
91const MAXARG_BX: i32 = (1 << 17) - 1;
92
93/// C: LFIELDS_PER_FLUSH 50 from lopcodes.h
94const LFIELDS_PER_FLUSH: i32 = 50;
95
96// ── Variable kind constants ─────────────────────────────────────────────────
97// C: VDKREG / RDKCONST / RDKTOCLOSE / RDKCTC from lparser.h
98// macros.tsv maps these to VarKind enum variants.
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101#[repr(u8)]
102pub enum VarKind {
103    /// C: VDKREG 0 — regular local variable
104    Reg = 0,
105    /// C: RDKCONST 1 — read-only const variable
106    Const = 1,
107    /// C: RDKTOCLOSE 2 — to-be-closed variable
108    ToBeClosed = 2,
109    /// C: RDKCTC 3 — compile-time constant (no register)
110    CompileTimeConst = 3,
111}
112
113impl VarKind {
114    pub fn from_u8(v: u8) -> Self {
115        match v {
116            0 => VarKind::Reg,
117            1 => VarKind::Const,
118            2 => VarKind::ToBeClosed,
119            3 => VarKind::CompileTimeConst,
120            _ => VarKind::Reg,
121        }
122    }
123    pub fn as_u8(self) -> u8 { self as u8 }
124}
125
126// ── ExprKind ────────────────────────────────────────────────────────────────
127
128/// C: expkind — the kind of a deferred expression or variable.
129/// Variants correspond exactly to the C enum in lparser.h.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ExprKind {
132    Void,       // VVOID: empty expression list
133    Nil,        // VNIL: constant nil
134    True,       // VTRUE: constant true
135    False,      // VFALSE: constant false
136    K,          // VK: constant in k[]; info = index
137    KFlt,       // VKFLT: float constant; u.nval
138    KInt,       // VKINT: integer constant; u.ival
139    KStr,       // VKSTR: string constant; u.strval
140    NonReloc,   // VNONRELOC: value in fixed register; info = reg
141    Local,      // VLOCAL: local variable; u.var.ridx, u.var.vidx
142    UpVal,      // VUPVAL: upvalue; info = upvalue index
143    Const,      // VCONST: compile-time const; info = absolute actvar index
144    Indexed,    // VINDEXED: indexed by reg key; u.ind.t, u.ind.idx
145    IndexUp,    // VINDEXUP: indexed upvalue; u.ind.t, u.ind.idx
146    IndexI,     // VINDEXI: indexed by int; u.ind.t, u.ind.idx
147    IndexStr,   // VINDEXSTR: indexed by string; u.ind.t, u.ind.idx
148    Jmp,        // VJMP: test/comparison; info = jump instruction pc
149    Reloc,      // VRELOC: result in any register; info = instruction pc
150    Call,       // VCALL: function call; info = instruction pc
151    VarArg,     // VVARARG: vararg; info = instruction pc
152}
153
154impl ExprKind {
155    /// C: hasmultret(k) — ((k) == VCALL || (k) == VVARARG)
156    #[inline]
157    pub fn has_mult_ret(self) -> bool {
158        matches!(self, ExprKind::Call | ExprKind::VarArg)
159    }
160
161    /// C: vkisvar(k) — VLOCAL <= k <= VINDEXSTR
162    #[inline]
163    pub fn is_var(self) -> bool {
164        matches!(
165            self,
166            ExprKind::Local
167                | ExprKind::UpVal
168                | ExprKind::Const
169                | ExprKind::Indexed
170                | ExprKind::IndexUp
171                | ExprKind::IndexI
172                | ExprKind::IndexStr
173        )
174    }
175
176    /// C: vkisindexed(k) — VINDEXED <= k <= VINDEXSTR
177    #[inline]
178    pub fn is_indexed(self) -> bool {
179        matches!(
180            self,
181            ExprKind::Indexed | ExprKind::IndexUp | ExprKind::IndexI | ExprKind::IndexStr
182        )
183    }
184}
185
186// ── ExprPayload ─────────────────────────────────────────────────────────────
187
188/// C: the `u` union inside `expdesc`.
189/// PORT NOTE: C uses a union; all arms share memory. Rust keeps all fields in
190///   one struct for Phase A simplicity. Phase B may refactor to a proper enum.
191#[derive(Debug, Clone, Default)]
192pub struct ExprPayload {
193    /// C: u.ival — for VKINT
194    pub ival: i64,
195    /// C: u.nval — for VKFLT
196    pub nval: f64,
197    /// C: u.strval — for VKSTR
198    pub strval: Option<GcRef<LuaString>>,
199    /// C: u.info — for VK/VNONRELOC/VUPVAL/VCONST/VJMP/VRELOC/VCALL/VVARARG
200    pub info: i32,
201    /// C: u.ind.idx — register or K index for VINDEXED/VINDEXUP/VINDEXI/VINDEXSTR
202    pub ind_idx: i16,
203    /// C: u.ind.t — table register or upvalue index
204    pub ind_t: u8,
205    /// C: u.var.ridx — register holding the local variable (VLOCAL)
206    pub var_ridx: u8,
207    /// C: u.var.vidx — compiler index in actvar.arr (VLOCAL)
208    pub var_vidx: u16,
209}
210
211// ── ExprDesc ────────────────────────────────────────────────────────────────
212
213/// C: expdesc — describes a potentially-deferred expression/variable.
214/// Field `t`/`f` are patch-lists for short-circuit boolean evaluation.
215#[derive(Debug, Clone)]
216pub struct ExprDesc {
217    pub k: ExprKind,
218    pub u: ExprPayload,
219    /// C: e.t — patch list for 'exit when true'; NO_JUMP if none.
220    pub t: i32,
221    /// C: e.f — patch list for 'exit when false'; NO_JUMP if none.
222    pub f: i32,
223}
224
225impl Default for ExprDesc {
226    fn default() -> Self {
227        ExprDesc { k: ExprKind::Void, u: ExprPayload::default(), t: NO_JUMP, f: NO_JUMP }
228    }
229}
230
231// ── VarDesc ─────────────────────────────────────────────────────────────────
232
233/// C: Vardesc — describes an active local variable during compilation.
234/// PORT NOTE: C uses a union (vd fields + k for const value). Rust keeps all
235///   fields in a struct. The `const_val` field is only meaningful when
236///   `kind == VarKind::CompileTimeConst`.
237#[derive(Debug, Clone)]
238pub struct VarDesc {
239    /// C: vd.kind
240    pub kind: VarKind,
241    /// C: vd.ridx — register holding the variable
242    pub ridx: u8,
243    /// C: vd.pidx — index in Proto.locvars
244    pub pidx: i16,
245    /// C: vd.name — variable name
246    pub name: Option<GcRef<LuaString>>,
247    /// C: k — compile-time constant value (only valid for CompileTimeConst)
248    pub const_val: LuaValue,
249}
250
251impl Default for VarDesc {
252    fn default() -> Self {
253        VarDesc {
254            kind: VarKind::Reg,
255            ridx: 0,
256            pidx: 0,
257            name: None,
258            const_val: LuaValue::Nil,
259        }
260    }
261}
262
263// ── LabelDesc ───────────────────────────────────────────────────────────────
264
265/// C: Labeldesc — a pending goto statement or an active label.
266#[derive(Debug, Clone)]
267pub struct LabelDesc {
268    /// C: name — label identifier
269    pub name: Option<GcRef<LuaString>>,
270    /// C: pc — bytecode position
271    pub pc: i32,
272    /// C: line — source line
273    pub line: i32,
274    /// C: nactvar — active variable count at this position
275    pub nactvar: u8,
276    /// C: close — whether this goto escapes upvalues
277    pub close: bool,
278}
279
280// ── DynData ─────────────────────────────────────────────────────────────────
281
282/// C: Dyndata — parser-local mutable lists (active vars, gotos, labels).
283/// C stored C-style dynamic arrays (arr/n/size); Rust uses Vec.
284#[derive(Debug, Default)]
285pub struct DynData {
286    /// C: actvar — all currently-active local variables
287    pub actvar: Vec<VarDesc>,
288    /// C: gt — pending gotos
289    pub gt: Vec<LabelDesc>,
290    /// C: label — active labels
291    pub label: Vec<LabelDesc>,
292}
293
294// ── BlockCnt ────────────────────────────────────────────────────────────────
295
296/// C: BlockCnt — one nested block scope (defined in lparser.c, not header).
297/// In C: stack-allocated, chained via raw `*previous` pointer.
298/// In Rust: heap-allocated in an `Option<Box<BlockCnt>>` chain on FuncState.
299#[derive(Debug)]
300pub struct BlockCnt {
301    /// C: *previous — enclosing block; None at function top level.
302    pub previous: Option<Box<BlockCnt>>,
303    /// C: firstlabel — index of first label in this block (in dyd.label)
304    pub firstlabel: i32,
305    /// C: firstgoto — index of first pending goto (in dyd.gt)
306    pub firstgoto: i32,
307    /// C: nactvar — active-local count on block entry
308    pub nactvar: u8,
309    /// C: upval — true if some variable in block is an upvalue
310    pub upval: bool,
311    /// C: isloop — true if this block is a loop body
312    pub isloop: bool,
313    /// C: insidetbc — true if inside the scope of a to-be-closed variable
314    pub insidetbc: bool,
315}
316
317// ── FuncState ───────────────────────────────────────────────────────────────
318
319/// C: FuncState — per-function compile-time state.
320/// In C: stack-allocated in `body()`, chained via raw `*prev` pointer.
321/// In Rust: heap-allocated via `Option<Box<FuncState>>` in LexState.
322#[derive(Debug)]
323pub struct FuncState {
324    /// C: f — the Proto being built.
325    /// PORT NOTE: types.tsv maps this to GcRef<LuaProto>; we use Box<LuaProto>
326    ///   during compilation to avoid RefCell overhead. close_func hands it to
327    ///   the GC/parent at close time.
328    pub f: Box<LuaProto>,
329    /// C: prev — enclosing FuncState (raw pointer in C; owned Box here).
330    pub prev: Option<Box<FuncState>>,
331    /// C: bl — innermost active block
332    pub bl: Option<Box<BlockCnt>>,
333    /// C: pc — next bytecode position to emit
334    pub pc: i32,
335    /// C: lasttarget — pc of last 'jump label'
336    pub lasttarget: i32,
337    /// C: previousline — last line saved in lineinfo
338    pub previousline: i32,
339    /// C: nk — number of constants emitted
340    pub nk: i32,
341    /// C: np — number of nested prototypes emitted
342    pub np: i32,
343    /// C: nabslineinfo — number of absolute line-info records
344    pub nabslineinfo: i32,
345    /// C: firstlocal — index of first local var in dyd.actvar
346    pub firstlocal: i32,
347    /// C: firstlabel — index of first label in dyd.label
348    pub firstlabel: i32,
349    /// C: ndebugvars — entries in f.locvars
350    pub ndebugvars: i16,
351    /// C: nactvar — number of active locals
352    pub nactvar: u8,
353    /// C: nups — number of upvalues
354    pub nups: u8,
355    /// C: freereg — next free register
356    pub freereg: u8,
357    /// C: iwthabs — instructions since last absolute line info
358    pub iwthabs: u8,
359    /// C: needclose — function must close upvalues on return
360    pub needclose: bool,
361    /// Current `ls.lastline` value, mirrored on every `sync_from_lex`.
362    /// Used by `emit_inst` to attribute the line to the just-consumed token
363    /// (matching lua-c's `savelineinfo(fs, f, fs->ls->lastline)`), instead
364    /// of whatever `line` the caller threaded down. The threaded `line`
365    /// param is preserved only for explicit overrides (luaK_fixline-style).
366    pub last_token_line: i32,
367}
368
369// ── ConsControl ─────────────────────────────────────────────────────────────
370
371/// C: ConsControl — state for parsing a table constructor.
372/// PORT NOTE: C stores `expdesc *t` as a pointer to the caller's expdesc.
373///   Rust stores a copy of the table descriptor; callers must sync back
374///   if they mutate it. Phase B may restructure.
375#[derive(Debug)]
376pub struct ConsControl {
377    /// C: v — last list item read
378    pub v: ExprDesc,
379    /// C: *t — table descriptor (copied; see PORT NOTE above)
380    pub t: ExprDesc,
381    /// C: nh — total number of record elements
382    pub nh: i32,
383    /// C: na — number of array elements already stored
384    pub na: i32,
385    /// C: tostore — array elements pending store
386    pub tostore: i32,
387}
388
389// ── LhsAssign ───────────────────────────────────────────────────────────────
390
391/// C: LHS_assign — chain of assignment left-hand-side variables.
392/// In C: stack-allocated, chained via raw `*prev`. In Rust: `Option<Box<...>>`.
393#[derive(Debug)]
394pub struct LhsAssign {
395    /// C: *prev — previous (outer) assignment target; None at head.
396    pub prev: Option<Box<LhsAssign>>,
397    /// C: v — the variable being assigned
398    pub v: ExprDesc,
399}
400
401// ── Unary / binary operator enums ───────────────────────────────────────────
402// C: UnOpr and BinOpr from lcode.h; defined locally here for Phase A.
403// TODO(port): unify with lua_code::UnOpr / BinOpr when lua-code lands.
404
405/// C: UnOpr
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum UnOpr {
408    Minus,    // OPR_MINUS
409    BNot,     // OPR_BNOT
410    Not,      // OPR_NOT
411    Len,      // OPR_LEN
412    NoUnOpr,  // OPR_NOUNOPR
413}
414
415/// C: BinOpr
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum BinOpr {
418    Add,     // OPR_ADD
419    Sub,     // OPR_SUB
420    Mul,     // OPR_MUL
421    Mod,     // OPR_MOD
422    Pow,     // OPR_POW
423    Div,     // OPR_DIV
424    IDiv,    // OPR_IDIV
425    BAnd,    // OPR_BAND
426    BOr,     // OPR_BOR
427    BXor,    // OPR_BXOR
428    Shl,     // OPR_SHL
429    Shr,     // OPR_SHR
430    Concat,  // OPR_CONCAT
431    Eq,      // OPR_EQ
432    Lt,      // OPR_LT
433    Le,      // OPR_LE
434    Ne,      // OPR_NE
435    Gt,      // OPR_GT
436    Ge,      // OPR_GE
437    And,     // OPR_AND
438    Or,      // OPR_OR
439    NoBinOpr, // OPR_NOBINOPR
440}
441
442/// C: priority[] table — (left_priority, right_priority) per BinOpr.
443/// Indexed by BinOpr discriminant (0 = Add, ... 20 = Or).
444const PRIORITY: [(u8, u8); 21] = [
445    (10, 10), (10, 10),       // Add, Sub
446    (11, 11), (11, 11),       // Mul, Mod
447    (14, 13),                 // Pow (right-associative)
448    (11, 11), (11, 11),       // Div, IDiv
449    (6, 6), (4, 4), (5, 5),  // BAnd, BOr, BXor
450    (7, 7), (7, 7),           // Shl, Shr
451    (9, 8),                   // Concat (right-associative)
452    (3, 3), (3, 3), (3, 3),  // Eq, Lt, Le
453    (3, 3), (3, 3), (3, 3),  // Ne, Gt, Ge
454    (2, 2), (1, 1),           // And, Or
455];
456
457// TODO_ARCH(phase-b-reconcile): re-exporting canonical OpCode from lua-code.
458pub use lua_code::opcodes::OpCode;
459
460// ── Minimal LexState stub ───────────────────────────────────────────────────
461// PORT NOTE: In C, LexState is defined in llex.h (→ lua-lex crate).
462//   We declare a minimal stub here for Phase A so function bodies can be
463//   written. Phase B will replace with `lua_lex::LexState` and remove this.
464
465/// Semantic info attached to a token.
466/// C: SemInfo from llex.h; Rust analogue is TokenValue.
467#[derive(Debug, Clone, Default)]
468pub struct TokenValue {
469    /// C: r — float literal value (for TK_FLT)
470    pub r: f64,
471    /// C: i — integer literal value (for TK_INT)
472    pub i: i64,
473    /// C: ts — string value (for TK_NAME, TK_STRING)
474    pub ts: Option<GcRef<LuaString>>,
475}
476
477/// C: Token from llex.h.
478#[derive(Debug, Clone, Default)]
479pub struct LexToken {
480    pub token: TokenKind,
481    pub seminfo: TokenValue,
482}
483
484/// C: LexState — per-chunk lexer + parser state.
485/// PORT NOTE: This is a Phase A stub. In Phase B, `LexState` lives in
486///   `lua-lex` and `lua-parse` imports it. `FuncState` will move here
487///   or be passed separately. The `fs` field creates a circular-crate
488///   dependency that Phase B must resolve (likely: both live in one crate).
489pub struct LexState {
490    /// C: current — current character (i32; -1 = EOZ)
491    pub current: i32,
492    /// C: linenumber
493    pub linenumber: i32,
494    /// C: lastline
495    pub lastline: i32,
496    /// C: t — current token
497    pub t: LexToken,
498    /// C: lookahead — one-token lookahead
499    pub lookahead: LexToken,
500    /// C: fs — current FuncState (parser owns this)
501    pub fs: Option<Box<FuncState>>,
502    /// C: dyd — parser dynamic data
503    pub dyd: DynData,
504    /// C: source — chunk name
505    pub source: Option<GcRef<LuaString>>,
506    /// C: envn — cached "_ENV" string
507    pub envn: Option<GcRef<LuaString>>,
508    /// Underlying lexer state that owns the ZIO stream and lex buffer.
509    /// The parser drives the lexer by calling `lex_next` / `lex_lookahead`,
510    /// which forward to `lua_lex::next` / `lua_lex::lookahead` on this inner
511    /// state and then mirror the resulting token into `self.t` / `self.lookahead`.
512    pub lex: lua_lex::LexState,
513    /// Parser recursion depth for C-Lua's `enterlevel` / `leavelevel` guard.
514    pub recursion_depth: u32,
515}
516
517const PARSER_MAX_C_CALLS: u32 = 200;
518
519fn enter_level(ls: &mut LexState) -> Result<(), LuaError> {
520    ls.recursion_depth += 1;
521    if ls.recursion_depth >= PARSER_MAX_C_CALLS {
522        Err(LuaError::syntax(format_args!("C stack overflow")))
523    } else {
524        Ok(())
525    }
526}
527
528fn leave_level(ls: &mut LexState) {
529    ls.recursion_depth = ls.recursion_depth.saturating_sub(1);
530}
531
532/// Advance the lexer one token and mirror the resulting state into the
533/// parser's outer `LexState` fields. This is the canonical replacement for the
534/// Phase A `// TODO(port): lua_lex::next(ls, state)?;` stubs.
535fn lex_next(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
536    lua_lex::next(state, &mut ls.lex)?;
537    sync_from_lex(ls);
538    Ok(())
539}
540
541/// Populate the lookahead token and mirror lexer state. Replaces the
542/// `// TODO(port): lua_lex::lookahead(ls, state)?` stub.
543fn lex_lookahead(ls: &mut LexState, state: &mut LuaState) -> Result<TokenKind, LuaError> {
544    let kind = lua_lex::lookahead(state, &mut ls.lex)?;
545    sync_from_lex(ls);
546    Ok(kind)
547}
548
549/// Copy lexer-side current/line/token/lookahead values back into the parser's
550/// outer LexState. Used after every `lua_lex::next` / `lua_lex::lookahead`.
551fn sync_from_lex(ls: &mut LexState) {
552    ls.current = ls.lex.current;
553    ls.linenumber = ls.lex.linenumber;
554    ls.lastline = ls.lex.lastline;
555    ls.t = LexToken {
556        token: ls.lex.t.kind,
557        seminfo: local_token_value(&ls.lex.t.value),
558    };
559    ls.lookahead = LexToken {
560        token: ls.lex.lookahead.kind,
561        seminfo: local_token_value(&ls.lex.lookahead.value),
562    };
563    // Mirror lastline into the active FuncState so emit_inst can read it
564    // without needing access to LexState. This matches lua-c's
565    // `savelineinfo(fs, f, fs->ls->lastline)` semantics.
566    if let Some(fs) = ls.fs.as_mut() {
567        fs.last_token_line = ls.lastline;
568    }
569}
570
571// TODO_ARCH(phase-b-reconcile): re-exporting canonical LuaState from lua-vm.
572pub use lua_vm::state::LuaState;
573
574// ── Minimal inline codegen (Phase A bootstrap) ──────────────────────────────
575//
576// The full code generator lives in `lua-code` but operates on its own
577// placeholder `FuncState` / `ExprDesc` types (see `lua-code/src/codegen.rs`
578// "PHASE B PLACEHOLDERS"), so it cannot yet be called from `lua-parse` with
579// the real types defined here. Until that reconciliation lands, the parser
580// emits the small subset of bytecode required to execute simple programs
581// (global lookup + function call + string literal arg) directly, using the
582// shared `Instruction` encoding from `lua-code::opcodes`.
583//
584// These helpers mirror the behaviour of the C codegen functions they replace
585// (`luaK_codeABC`, `luaK_stringK`, `luaK_dischargevars` for the VINDEXUP
586// case, `luaK_exp2nextreg` for the VKSTR case). Phase B should delete this
587// section once lua-code is reachable from lua-parse with unified types.
588
589fn emit_inst(fs: &mut FuncState, line: i32, inst: lua_code::opcodes::Instruction) -> i32 {
590    const MAX_IWTH_ABS: i32 = 128;
591    const LIM_LINE_DIFF: i32 = 0x80;
592    const ABS_LINE_INFO: i8 = -0x80i8;
593    let pc = fs.pc as usize;
594    if fs.f.code.len() <= pc {
595        fs.f.code.resize(pc + 1, lua_types::opcode::Instruction::default());
596    }
597    fs.f.code[pc] = lua_types::opcode::Instruction::new(inst.0);
598    if fs.f.lineinfo.len() <= pc {
599        fs.f.lineinfo.resize(pc + 1, 0i8);
600    }
601    let linedif_raw = line - fs.previousline;
602    let need_abs = linedif_raw.abs() >= LIM_LINE_DIFF || {
603        let over = fs.iwthabs as i32 >= MAX_IWTH_ABS;
604        if !over { fs.iwthabs += 1; }
605        over
606    };
607    if need_abs {
608        fs.f.abslineinfo.push(AbsLineInfo { pc: pc as i32, line });
609        fs.nabslineinfo += 1;
610        fs.f.lineinfo[pc] = ABS_LINE_INFO;
611        fs.iwthabs = 1;
612    } else {
613        fs.f.lineinfo[pc] = linedif_raw as i8;
614    }
615    fs.previousline = line;
616    let result = fs.pc;
617    fs.pc += 1;
618    result
619}
620
621fn add_k_value(fs: &mut FuncState, v: LuaValue) -> i32 {
622    let idx = fs.nk;
623    if (fs.f.k.len() as i32) <= idx {
624        fs.f.k.resize((idx + 1) as usize, LuaValue::Nil);
625    }
626    fs.f.k[idx as usize] = v;
627    fs.nk += 1;
628    idx
629}
630
631fn add_k_string(fs: &mut FuncState, s: GcRef<LuaString>) -> i32 {
632    for (i, k) in fs.f.k.iter().take(fs.nk as usize).enumerate() {
633        if let LuaValue::Str(existing) = k {
634            if GcRef::ptr_eq(existing, &s) {
635                return i as i32;
636            }
637        }
638    }
639    add_k_value(fs, LuaValue::Str(s))
640}
641
642fn bump_maxstack(fs: &mut FuncState, n: u8) {
643    if fs.f.maxstacksize < n {
644        fs.f.maxstacksize = n;
645    }
646}
647
648fn reserve_reg(fs: &mut FuncState) -> Result<u8, LuaError> {
649    if fs.freereg == u8::MAX {
650        return Err(LuaError::syntax(format_args!(
651            "function or expression needs too many registers"
652        )));
653    }
654    let r = fs.freereg;
655    fs.freereg += 1;
656    bump_maxstack(fs, fs.freereg);
657    Ok(r)
658}
659
660fn reserve_regs(fs: &mut FuncState, n: i32) -> Result<(), LuaError> {
661    let newstack = fs.freereg as i32 + n;
662    if newstack >= 255 {
663        return Err(LuaError::syntax(format_args!(
664            "function or expression needs too many registers"
665        )));
666    }
667    fs.freereg = newstack as u8;
668    bump_maxstack(fs, fs.freereg);
669    Ok(())
670}
671
672/// Free `reg` if it sits above the active-local watermark.
673///
674/// Mirrors C's `freereg` from `lcode.c`: registers below `nactvar` belong to
675/// declared locals and must not be popped; temporaries above that watermark
676/// are freed by decrementing `fs.freereg`.
677fn cg_free_reg(fs: &mut FuncState, reg: i32) {
678    if reg >= fs.nactvar as i32 {
679        debug_assert_eq!(reg, fs.freereg as i32 - 1);
680        fs.freereg = fs.freereg.saturating_sub(1);
681    }
682}
683
684/// Free the temporary register held by `e` if any.
685///
686/// Mirrors C's `freeexp` from `lcode.c`: only `VNONRELOC` carries a concrete
687/// register that may need releasing.
688fn cg_free_exp(fs: &mut FuncState, e: &ExprDesc) {
689    if e.k == ExprKind::NonReloc {
690        cg_free_reg(fs, e.u.info);
691    }
692}
693
694/// Free temporary registers held by `e1` and `e2`, releasing the higher
695/// register first so the LIFO invariant on `fs.freereg` holds.
696///
697/// Mirrors C's `freeexps` from `lcode.c`.
698fn cg_free_exps(fs: &mut FuncState, e1: &ExprDesc, e2: &ExprDesc) {
699    let r1 = if e1.k == ExprKind::NonReloc { e1.u.info } else { -1 };
700    let r2 = if e2.k == ExprKind::NonReloc { e2.u.info } else { -1 };
701    if r1 > r2 {
702        cg_free_reg(fs, r1);
703        cg_free_reg(fs, r2);
704    } else {
705        cg_free_reg(fs, r2);
706        cg_free_reg(fs, r1);
707    }
708}
709
710/// Constant-folding `luaK_posfix` for arithmetic binary operators where both
711/// operands are already numeric literals (`KInt` / `KFlt`). Mirrors the
712/// `constfolding` branch in C's `luaK_posfix`: when both operands are
713/// numerals, the result is computed at compile time and stored back into
714/// `e1`. Non-foldable arithmetic / bitwise binops fall through to the
715/// two-register emit path (`OP_ADD` ... `OP_SHR`) plus an `OP_MMBIN`
716/// metamethod-dispatch instruction. `Concat` is delegated to
717/// `cg_emit_concat`; `Lt` to `cg_emit_order`; remaining logical and
718/// comparison operators still hit `todo!()` so they surface as later
719/// iterations' blockers.
720fn cg_posfix_fold(
721    fs: &mut FuncState,
722    op: BinOpr,
723    e1: &mut ExprDesc,
724    e2: &mut ExprDesc,
725    line: i32,
726) -> Result<(), LuaError> {
727    // Lua C records line info at emit time from `ls->lastline`. By the time
728    // postfix code runs, the RHS has already been parsed, so discharging RHS
729    // indexed expressions must use the current token line, not the saved
730    // operator line. The operator line is still used below for the binop/MMBIN
731    // instructions themselves.
732    let rhs_line = fs.last_token_line;
733    cg_discharge_vars(fs, rhs_line, e2)?;
734
735    let promote = |k: ExprKind, u: &ExprPayload| -> Option<f64> {
736        match k {
737            ExprKind::KInt => Some(u.ival as f64),
738            ExprKind::KFlt => Some(u.nval),
739            _ => None,
740        }
741    };
742
743    let foldable = e1.t == NO_JUMP && e1.f == NO_JUMP
744        && e2.t == NO_JUMP && e2.f == NO_JUMP;
745
746    if foldable {
747    if let (ExprKind::KInt, ExprKind::KInt) = (e1.k, e2.k) {
748        let a = e1.u.ival;
749        let b = e2.u.ival;
750        let r: Option<i64> = match op {
751            BinOpr::Add => Some(a.wrapping_add(b)),
752            BinOpr::Sub => Some(a.wrapping_sub(b)),
753            BinOpr::Mul => Some(a.wrapping_mul(b)),
754            BinOpr::Mod if b != 0 => Some(a.rem_euclid(b)),
755            BinOpr::IDiv if b != 0 => Some(a.div_euclid(b)),
756            BinOpr::BAnd => Some(a & b),
757            BinOpr::BOr  => Some(a | b),
758            BinOpr::BXor => Some(a ^ b),
759            _ => None,
760        };
761        if let Some(v) = r {
762            e1.k = ExprKind::KInt;
763            e1.u.ival = v;
764            return Ok(());
765        }
766    }
767    if let (Some(a), Some(b)) = (promote(e1.k, &e1.u), promote(e2.k, &e2.u)) {
768        let r: Option<f64> = match op {
769            BinOpr::Add => Some(a + b),
770            BinOpr::Sub => Some(a - b),
771            BinOpr::Mul => Some(a * b),
772            BinOpr::Div => Some(a / b),
773            BinOpr::Pow => Some(a.powf(b)),
774            _ => None,
775        };
776        if let Some(v) = r {
777            if v.is_finite() {
778                e1.k = ExprKind::KFlt;
779                e1.u.nval = v;
780                return Ok(());
781            }
782        }
783    }
784    }
785
786    if matches!(op, BinOpr::Lt | BinOpr::Le) {
787        return cg_emit_order(fs, op, e1, e2, line);
788    }
789
790    if matches!(op, BinOpr::Gt | BinOpr::Ge) {
791        let swap_op = if matches!(op, BinOpr::Gt) { BinOpr::Lt } else { BinOpr::Le };
792        std::mem::swap(e1, e2);
793        return cg_emit_order(fs, swap_op, e1, e2, line);
794    }
795
796    if matches!(op, BinOpr::Eq | BinOpr::Ne) {
797        return cg_emit_eq(fs, op, e1, e2, line);
798    }
799
800    if matches!(op, BinOpr::And) {
801        debug_assert_eq!(e1.t, NO_JUMP);
802        cg_concat(fs, &mut e2.f, e1.f)?;
803        *e1 = e2.clone();
804        return Ok(());
805    }
806
807    if matches!(op, BinOpr::Or) {
808        debug_assert_eq!(e1.f, NO_JUMP);
809        cg_concat(fs, &mut e2.t, e1.t)?;
810        *e1 = e2.clone();
811        return Ok(());
812    }
813
814    if matches!(op, BinOpr::Concat) {
815        return cg_emit_concat(fs, e1, e2, line);
816    }
817
818    let (opcode, event) = match op {
819        BinOpr::Add  => (lua_code::opcodes::OpCode::Add,  lua_types::tagmethod::TagMethod::Add),
820        BinOpr::Sub  => (lua_code::opcodes::OpCode::Sub,  lua_types::tagmethod::TagMethod::Sub),
821        BinOpr::Mul  => (lua_code::opcodes::OpCode::Mul,  lua_types::tagmethod::TagMethod::Mul),
822        BinOpr::Mod  => (lua_code::opcodes::OpCode::Mod,  lua_types::tagmethod::TagMethod::Mod),
823        BinOpr::Pow  => (lua_code::opcodes::OpCode::Pow,  lua_types::tagmethod::TagMethod::Pow),
824        BinOpr::Div  => (lua_code::opcodes::OpCode::Div,  lua_types::tagmethod::TagMethod::Div),
825        BinOpr::IDiv => (lua_code::opcodes::OpCode::IDiv, lua_types::tagmethod::TagMethod::Idiv),
826        BinOpr::BAnd => (lua_code::opcodes::OpCode::BAnd, lua_types::tagmethod::TagMethod::Band),
827        BinOpr::BOr  => (lua_code::opcodes::OpCode::BOr,  lua_types::tagmethod::TagMethod::Bor),
828        BinOpr::BXor => (lua_code::opcodes::OpCode::BXor, lua_types::tagmethod::TagMethod::Bxor),
829        BinOpr::Shl  => (lua_code::opcodes::OpCode::Shl,  lua_types::tagmethod::TagMethod::Shl),
830        BinOpr::Shr  => (lua_code::opcodes::OpCode::Shr,  lua_types::tagmethod::TagMethod::Shr),
831        _ => todo!(
832            "phase-b: cg_posfix_fold non-foldable binop {:?} ({:?} {:?})",
833            op, e1.k, e2.k
834        ),
835    };
836
837    cg_discharge_vars(fs, line, e1)?;
838    cg_discharge_vars(fs, line, e2)?;
839    let v2 = cg_exp_to_any_reg(fs, line, e2)?;
840    let v1 = cg_exp_to_any_reg(fs, line, e1)?;
841
842    let inst = lua_code::opcodes::Instruction::abck(opcode, 0, v1 as u32, v2 as u32, 0);
843    let pc = emit_inst(fs, line, inst);
844    cg_free_exps(fs, e1, e2);
845    e1.u.info = pc;
846    e1.k = ExprKind::Reloc;
847
848    let mm_inst = lua_code::opcodes::Instruction::abck(
849        lua_code::opcodes::OpCode::MmBin,
850        v1 as u32,
851        v2 as u32,
852        event as u32,
853        0,
854    );
855    emit_inst(fs, line, mm_inst);
856    Ok(())
857}
858
859/// Mirrors C's `codeorder` from `lcode.c` for relational binops (`<`, `<=`,
860/// `>`, `>=`). Emits a comparison opcode (with `k = 1`) followed by an
861/// `OP_JMP` with offset `NO_JUMP`; the resulting `VJMP` expression carries
862/// the jump's pc in `e1.u.info` so the surrounding control-flow logic can
863/// patch it. When one operand is a small-integer literal that fits the
864/// signed-C field, the immediate forms (`OP_LTI` / `OP_GTI`) are used;
865/// otherwise both operands are discharged to registers and the register
866/// form (`OP_LT`) is emitted.
867fn cg_emit_order(
868    fs: &mut FuncState,
869    op: BinOpr,
870    e1: &mut ExprDesc,
871    e2: &mut ExprDesc,
872    line: i32,
873) -> Result<(), LuaError> {
874    debug_assert!(matches!(op, BinOpr::Lt | BinOpr::Le));
875    let is_le = matches!(op, BinOpr::Le);
876    let (op_imm_e2, op_imm_e1, op_reg) = if is_le {
877        (
878            lua_code::opcodes::OpCode::LeI,
879            lua_code::opcodes::OpCode::GeI,
880            lua_code::opcodes::OpCode::Le,
881        )
882    } else {
883        (
884            lua_code::opcodes::OpCode::LtI,
885            lua_code::opcodes::OpCode::GtI,
886            lua_code::opcodes::OpCode::Lt,
887        )
888    };
889    let (r1, r2, cmp_op) = if let Some(im) = cg_sc_int(e2) {
890        let r1 = cg_exp_to_any_reg(fs, line, e1)?;
891        (r1, im, op_imm_e2)
892    } else if let Some(im) = cg_sc_int(e1) {
893        let r1 = cg_exp_to_any_reg(fs, line, e2)?;
894        (r1, im, op_imm_e1)
895    } else {
896        let r2 = cg_exp_to_any_reg(fs, line, e2)?;
897        let r1 = cg_exp_to_any_reg(fs, line, e1)?;
898        (r1, r2, op_reg)
899    };
900    cg_free_exps(fs, e1, e2);
901    let cmp = lua_code::opcodes::Instruction::abck(
902        cmp_op,
903        r1 as u32,
904        r2 as u32,
905        0,
906        1,
907    );
908    emit_inst(fs, line, cmp);
909    let jmp_arg = (NO_JUMP + lua_code::opcodes::OFFSET_S_J) as u32;
910    let jmp = lua_code::opcodes::Instruction::sj(
911        lua_code::opcodes::OpCode::Jmp,
912        jmp_arg,
913        0,
914    );
915    let jmp_pc = emit_inst(fs, line, jmp);
916    e1.u.info = jmp_pc;
917    e1.k = ExprKind::Jmp;
918    Ok(())
919}
920
921/// Mirrors C's `codeeq` from `lcode.c` for the equality binops (`==`, `~=`).
922/// Emits an `OP_EQ` (or its `OP_EQI` immediate form when the right operand
923/// is a small-integer literal) followed by an `OP_JMP` whose pc is stored
924/// in `e1.u.info`; `e1.k` becomes `VJMP`. The `k` bit selects between `==`
925/// (k=1) and `~=` (k=0) so the same opcode pair handles both operators.
926///
927/// The Phase-A bootstrap deliberately omits the constant-table (`OP_EQK`)
928/// fast path used by C; both operands fall back to register form when no
929/// signed-C immediate fits. Correctness is unchanged.
930fn cg_emit_eq(
931    fs: &mut FuncState,
932    op: BinOpr,
933    e1: &mut ExprDesc,
934    e2: &mut ExprDesc,
935    line: i32,
936) -> Result<(), LuaError> {
937    debug_assert!(matches!(op, BinOpr::Eq | BinOpr::Ne));
938    if e1.k != ExprKind::NonReloc {
939        std::mem::swap(e1, e2);
940    }
941    let r1 = cg_exp_to_any_reg(fs, line, e1)?;
942    let (r2, cmp_op) = if let Some(im) = cg_sc_int(e2) {
943        (im, lua_code::opcodes::OpCode::EqI)
944    } else {
945        let r = cg_exp_to_any_reg(fs, line, e2)?;
946        (r, lua_code::opcodes::OpCode::Eq)
947    };
948    cg_free_exps(fs, e1, e2);
949    let k_bit = if matches!(op, BinOpr::Eq) { 1 } else { 0 };
950    let cmp = lua_code::opcodes::Instruction::abck(
951        cmp_op,
952        r1 as u32,
953        r2 as u32,
954        0,
955        k_bit,
956    );
957    emit_inst(fs, line, cmp);
958    let jmp_pc = cg_jump(fs, line);
959    e1.u.info = jmp_pc;
960    e1.k = ExprKind::Jmp;
961    Ok(())
962}
963
964/// Mirrors C's `previousinstruction` from `lcode.c`: returns the index of the
965/// last emitted instruction, but only when `pc` is past `lasttarget` (i.e. the
966/// previous instruction is reachable without crossing a jump label). Used by
967/// peephole merges such as the `OP_CONCAT` chain fold.
968fn previous_instruction_idx(fs: &FuncState) -> Option<usize> {
969    if fs.pc > fs.lasttarget {
970        Some((fs.pc - 1) as usize)
971    } else {
972        None
973    }
974}
975
976/// Mirrors C's `codeconcat` from `lcode.c`. The left operand `e1` has
977/// already been placed on the stack by `cg_infix`'s `OPR_CONCAT` arm
978/// (`luaK_exp2nextreg`); here we only push `e2` onto the next register and
979/// emit (or fold into) the `OP_CONCAT`. When the previous instruction is
980/// itself an `OP_CONCAT` whose `A` register is exactly `e1.u.info + 1`,
981/// the chain is merged by widening that instruction's `B` field;
982/// otherwise a fresh `OP_CONCAT A=e1.u.info, B=2` is emitted. In both
983/// branches the temporary register holding `e2` is freed.
984fn cg_emit_concat(
985    fs: &mut FuncState,
986    e1: &mut ExprDesc,
987    e2: &mut ExprDesc,
988    line: i32,
989) -> Result<(), LuaError> {
990    cg_exp_to_next_reg(fs, line, e2)?;
991
992    if let Some(prev_idx) = previous_instruction_idx(fs) {
993        let prev = lua_code::opcodes::Instruction(fs.f.code[prev_idx].0);
994        if prev.opcode() == Some(lua_code::opcodes::OpCode::Concat) {
995            let n = prev.arg_b();
996            debug_assert_eq!(e1.u.info + 1, prev.arg_a() as i32);
997            cg_free_exp(fs, e2);
998            let mut updated = prev;
999            updated.set_arg_a(e1.u.info as u32);
1000            updated.set_arg_b(n + 1);
1001            fs.f.code[prev_idx] = lua_types::opcode::Instruction::new(updated.0);
1002            return Ok(());
1003        }
1004    }
1005
1006    let inst = lua_code::opcodes::Instruction::abck(
1007        lua_code::opcodes::OpCode::Concat,
1008        e1.u.info as u32,
1009        2,
1010        0,
1011        0,
1012    );
1013    emit_inst(fs, line, inst);
1014    cg_free_exp(fs, e2);
1015    Ok(())
1016}
1017
1018/// Mirrors C's `luaK_prefix` from `lcode.c`. Discharges `e`, then for
1019/// `Minus` / `BNot` / `Len` emits the unary opcode via `codeunexpval`
1020/// (place operand in a register, emit `OP_UNM` / `OP_BNOT` / `OP_LEN`
1021/// with `A` left as 0 so the result is relocatable). Constant folding
1022/// for `Minus` / `BNot` is skipped here; the runtime falls back to the
1023/// register form, matching C semantics (just less efficient). `Not`
1024/// is routed through `cg_codenot`, which performs literal folding,
1025/// JMP-condition flipping, or emits `OP_NOT` for register operands.
1026fn cg_prefix(
1027    fs: &mut FuncState,
1028    op: UnOpr,
1029    e: &mut ExprDesc,
1030    line: i32,
1031) -> Result<(), LuaError> {
1032    cg_discharge_vars(fs, line, e)?;
1033    let opcode = match op {
1034        UnOpr::Minus => lua_code::opcodes::OpCode::Unm,
1035        UnOpr::BNot  => lua_code::opcodes::OpCode::BNot,
1036        UnOpr::Len   => lua_code::opcodes::OpCode::Len,
1037        UnOpr::Not   => return cg_codenot(fs, line, e),
1038        UnOpr::NoUnOpr => return Ok(()),
1039    };
1040    let r = cg_exp_to_any_reg(fs, line, e)?;
1041    cg_free_exp(fs, e);
1042    let inst = lua_code::opcodes::Instruction::abck(opcode, 0, r as u32, 0, 0);
1043    let pc = emit_inst(fs, line, inst);
1044    e.u.info = pc;
1045    e.k = ExprKind::Reloc;
1046    Ok(())
1047}
1048
1049/// Return the pc of the test instruction that controls the jump at `pc`,
1050/// or `pc` itself if the jump is unconditional.
1051///
1052/// Mirrors C's `getjumpcontrol` from `lcode.c`: when `pc >= 1` and the
1053/// preceding opcode has the T-mode bit set (i.e. it's a test that is always
1054/// paired with a following `OP_JMP`), the control lives at `pc - 1`.
1055fn cg_get_jump_control(fs: &FuncState, pc: i32) -> i32 {
1056    if pc >= 1 {
1057        let prev = cg_inst_at(fs, pc - 1);
1058        if let Some(op) = prev.opcode() {
1059            if lua_code::opcodes::test_t_mode(op) {
1060                return pc - 1;
1061            }
1062        }
1063    }
1064    pc
1065}
1066
1067/// Patch the destination register of a `TESTSET` that controls the jump at
1068/// `node`. If the control isn't a `TESTSET`, returns `false`. With `reg ==
1069/// NO_REG` (or when `reg` already equals B), the instruction is rewritten to
1070/// a plain `OP_TEST` (preserving the original `k` bit) — the test no longer
1071/// produces a value.
1072///
1073/// Mirrors C's `patchtestreg` from `lcode.c`.
1074fn cg_patch_test_reg(fs: &mut FuncState, node: i32, reg: u32) -> bool {
1075    let ctrl_pc = cg_get_jump_control(fs, node);
1076    let mut inst = cg_inst_at(fs, ctrl_pc);
1077    if inst.opcode() != Some(lua_code::opcodes::OpCode::TestSet) {
1078        return false;
1079    }
1080    let b = inst.arg_b();
1081    let k = inst.arg_k();
1082    if reg != lua_code::opcodes::NO_REG && reg != b {
1083        inst.set_arg_a(reg);
1084        cg_set_inst_at(fs, ctrl_pc, inst);
1085    } else {
1086        let test = lua_code::opcodes::Instruction::abck(
1087            lua_code::opcodes::OpCode::Test,
1088            b,
1089            0,
1090            0,
1091            k,
1092        );
1093        cg_set_inst_at(fs, ctrl_pc, test);
1094    }
1095    true
1096}
1097
1098/// Walk the jump-list rooted at `list` and strip every `TESTSET` of its
1099/// destination register, leaving plain `OP_TEST`s behind. Used after
1100/// `not <expr>` swaps `e.t` / `e.f`: any pending value-producing tests in
1101/// the new lists would write the unnegated value, which is wrong.
1102///
1103/// Mirrors C's `removevalues` from `lcode.c`.
1104fn cg_remove_values(fs: &mut FuncState, list: i32) {
1105    let mut list = list;
1106    while list != NO_JUMP {
1107        let next = cg_get_jump(fs, list);
1108        cg_patch_test_reg(fs, list, lua_code::opcodes::NO_REG);
1109        list = next;
1110    }
1111}
1112
1113/// Mirrors C's `codenot` from `lcode.c`. Handles constant folding for `not`
1114/// (nil/false → true; any other constant → false), flips the condition bit
1115/// of a jump-result expression, or emits `OP_NOT` for in-register operands.
1116/// After negation, `e.t` and `e.f` are swapped (the old true-exit list now
1117/// fires when the negated value is false, and vice versa) and any
1118/// value-producing tests in the new lists are downgraded to plain tests via
1119/// `cg_remove_values`.
1120fn cg_codenot(fs: &mut FuncState, line: i32, e: &mut ExprDesc) -> Result<(), LuaError> {
1121    match e.k {
1122        ExprKind::Nil | ExprKind::False => {
1123            e.k = ExprKind::True;
1124        }
1125        ExprKind::K
1126        | ExprKind::KFlt
1127        | ExprKind::KInt
1128        | ExprKind::KStr
1129        | ExprKind::True => {
1130            e.k = ExprKind::False;
1131        }
1132        ExprKind::Jmp => {
1133            cg_negate_condition(fs, e);
1134        }
1135        ExprKind::Reloc | ExprKind::NonReloc => {
1136            let reg = cg_exp_to_any_reg(fs, line, e)?;
1137            cg_free_exp(fs, e);
1138            let inst = lua_code::opcodes::Instruction::abck(
1139                lua_code::opcodes::OpCode::Not,
1140                0,
1141                reg as u32,
1142                0,
1143                0,
1144            );
1145            let pc = emit_inst(fs, line, inst);
1146            e.u.info = pc;
1147            e.k = ExprKind::Reloc;
1148        }
1149        _ => debug_assert!(false, "cg_codenot: unexpected ExprKind {:?}", e.k),
1150    }
1151    std::mem::swap(&mut e.f, &mut e.t);
1152    cg_remove_values(fs, e.f);
1153    cg_remove_values(fs, e.t);
1154    Ok(())
1155}
1156
1157/// Emit OP_JMP with NO_JUMP offset; return its pc.
1158///
1159/// Mirrors C's `luaK_jump`.
1160fn cg_jump(fs: &mut FuncState, line: i32) -> i32 {
1161    let jmp_arg = (NO_JUMP + lua_code::opcodes::OFFSET_S_J) as u32;
1162    let jmp = lua_code::opcodes::Instruction::sj(
1163        lua_code::opcodes::OpCode::Jmp,
1164        jmp_arg,
1165        0,
1166    );
1167    emit_inst(fs, line, jmp)
1168}
1169
1170/// Read an instruction word from `fs.f.code` wrapped in the methodful
1171/// `lua_code::opcodes::Instruction` so accessor helpers are available.
1172fn cg_inst_at(fs: &FuncState, pc: i32) -> lua_code::opcodes::Instruction {
1173    lua_code::opcodes::Instruction(fs.f.code[pc as usize].0)
1174}
1175
1176/// Store an instruction word into `fs.f.code` from a methodful
1177/// `lua_code::opcodes::Instruction`.
1178fn cg_set_inst_at(fs: &mut FuncState, pc: i32, inst: lua_code::opcodes::Instruction) {
1179    fs.f.code[pc as usize] = lua_types::opcode::Instruction::new(inst.0);
1180}
1181
1182/// Return the absolute pc that the jump at `pc` targets, or `NO_JUMP` if the
1183/// jump's offset field is still the sentinel.
1184///
1185/// Mirrors C's `getjump` from `lcode.c`.
1186fn cg_get_jump(fs: &FuncState, pc: i32) -> i32 {
1187    let offset = cg_inst_at(fs, pc).arg_s_j();
1188    if offset == NO_JUMP { NO_JUMP } else { (pc + 1) + offset }
1189}
1190
1191/// Patch the jump at `pc` to land at absolute `dest`.
1192///
1193/// Mirrors C's `fixjump` from `lcode.c`.
1194fn cg_fix_jump(fs: &mut FuncState, pc: i32, dest: i32) -> Result<(), LuaError> {
1195    debug_assert!(dest != NO_JUMP);
1196    let offset = dest - (pc + 1);
1197    let max = lua_code::opcodes::MAXARG_S_J as i32 - lua_code::opcodes::OFFSET_S_J;
1198    let min = -lua_code::opcodes::OFFSET_S_J;
1199    if offset < min || offset > max {
1200        return Err(LuaError::syntax(format_args!("control structure too long")));
1201    }
1202    let mut inst = cg_inst_at(fs, pc);
1203    inst.set_arg_s_j(offset);
1204    cg_set_inst_at(fs, pc, inst);
1205    Ok(())
1206}
1207
1208/// Record `fs.pc` as a jump label and return it.
1209///
1210/// Mirrors C's `luaK_getlabel` from `lcode.c`.
1211fn cg_get_label(fs: &mut FuncState) -> i32 {
1212    fs.lasttarget = fs.pc;
1213    fs.pc
1214}
1215
1216/// Concatenate jump-list `l2` onto the tail of `*l1`.
1217///
1218/// Mirrors C's `luaK_concat` from `lcode.c`.
1219fn cg_concat(fs: &mut FuncState, l1: &mut i32, l2: i32) -> Result<(), LuaError> {
1220    if l2 == NO_JUMP { return Ok(()); }
1221    if *l1 == NO_JUMP { *l1 = l2; return Ok(()); }
1222    let mut list = *l1;
1223    loop {
1224        let next = cg_get_jump(fs, list);
1225        if next == NO_JUMP { break; }
1226        list = next;
1227    }
1228    cg_fix_jump(fs, list, l2)
1229}
1230
1231/// Patch every jump in the singly-linked list rooted at `list` to land at
1232/// absolute pc `target`.
1233///
1234/// Mirrors C's `luaK_patchlist`, which delegates to `patchlistaux(fs, list,
1235/// target, NO_REG, target)`: every `TESTSET` controller in the list gets
1236/// rewritten to a plain `OP_TEST` (the value-producing destination register
1237/// is no longer wanted at a fall-through target), and every jump is fixed to
1238/// `target`.
1239fn cg_patch_list(fs: &mut FuncState, list: i32, target: i32) -> Result<(), LuaError> {
1240    cg_patch_list_aux(fs, list, target, lua_code::opcodes::NO_REG, target)
1241}
1242
1243/// Patch every jump in `list` to land at the current `fs.pc`.
1244///
1245/// Mirrors C's `luaK_patchtohere` from `lcode.c`.
1246fn cg_patch_to_here(fs: &mut FuncState, list: i32) -> Result<(), LuaError> {
1247    let target = cg_get_label(fs);
1248    cg_patch_list(fs, list, target)
1249}
1250
1251/// Flip the `k` (condition) bit of the test instruction that immediately
1252/// precedes `e`'s JMP. After this, the jump fires on the opposite truth
1253/// value of the original comparison.
1254///
1255/// Mirrors C's `negatecondition` from `lcode.c`.
1256fn cg_negate_condition(fs: &mut FuncState, e: &ExprDesc) {
1257    let pc = e.u.info - 1;
1258    let mut inst = cg_inst_at(fs, pc);
1259    let k = inst.arg_k();
1260    inst.set_arg_k(k ^ 1);
1261    cg_set_inst_at(fs, pc, inst);
1262}
1263
1264/// Arrange for control to fall through when `e` is true and to jump (via the
1265/// patch list rooted at `e.f`) when `e` is false. After this call `e.t` has
1266/// been patched to the current pc and `e.f` holds the false-exit list.
1267///
1268/// Mirrors C's `luaK_goiftrue` from `lcode.c`. `VJMP` (comparison results)
1269/// negate the condition so the jump fires on false; literal-true forms emit
1270/// no jump; any other kind is forced into a register and tested with
1271/// `OP_TESTSET` via `cg_jump_on_cond`.
1272fn cg_go_if_true(fs: &mut FuncState, line: i32, e: &mut ExprDesc) -> Result<(), LuaError> {
1273    cg_discharge_vars(fs, line, e)?;
1274    let pc: i32 = match e.k {
1275        ExprKind::Jmp => {
1276            cg_negate_condition(fs, e);
1277            e.u.info
1278        }
1279        ExprKind::K | ExprKind::KFlt | ExprKind::KInt | ExprKind::KStr | ExprKind::True => {
1280            NO_JUMP
1281        }
1282        _ => cg_jump_on_cond(fs, line, e, 0)?,
1283    };
1284    cg_concat(fs, &mut e.f, pc)?;
1285    cg_patch_to_here(fs, e.t)?;
1286    e.t = NO_JUMP;
1287    Ok(())
1288}
1289
1290/// Mirror of `cg_go_if_true` for false short-circuit (`or` operator and
1291/// `while not <cond>` shaped control flow). Falls through when `e` is false
1292/// and jumps when true. After this call `e.f` has been patched to the
1293/// current pc and `e.t` holds the true-exit list.
1294///
1295/// Mirrors C's `luaK_goiffalse` from `lcode.c`.
1296fn cg_go_if_false(fs: &mut FuncState, line: i32, e: &mut ExprDesc) -> Result<(), LuaError> {
1297    cg_discharge_vars(fs, line, e)?;
1298    let pc: i32 = match e.k {
1299        ExprKind::Jmp => e.u.info,
1300        ExprKind::Nil | ExprKind::False => NO_JUMP,
1301        _ => cg_jump_on_cond(fs, line, e, 1)?,
1302    };
1303    cg_concat(fs, &mut e.t, pc)?;
1304    cg_patch_to_here(fs, e.f)?;
1305    e.f = NO_JUMP;
1306    Ok(())
1307}
1308
1309/// Emit `OP_TESTSET R[NO_REG], R[e.info], cond` followed by an `OP_JMP` so
1310/// control transfers to the jump's patch list when `e`'s truth value equals
1311/// `cond`. Returns the pc of the emitted jump so the caller can append it
1312/// to the appropriate exit list.
1313///
1314/// Mirrors C's `jumponcond` from `lcode.c`. The `OP_NOT` peephole that C
1315/// applies for `VRELOC` operands is intentionally skipped for the Phase-A
1316/// bootstrap; correctness is unaffected and the optimisation can land with
1317/// the codegen reconciliation pass.
1318fn cg_jump_on_cond(
1319    fs: &mut FuncState,
1320    line: i32,
1321    e: &mut ExprDesc,
1322    cond: u8,
1323) -> Result<i32, LuaError> {
1324    let reg = cg_exp_to_any_reg(fs, line, e)?;
1325    cg_free_exp(fs, e);
1326    let test = lua_code::opcodes::Instruction::abck(
1327        lua_code::opcodes::OpCode::TestSet,
1328        lua_code::opcodes::NO_REG,
1329        reg as u32,
1330        0,
1331        cond as u32,
1332    );
1333    emit_inst(fs, line, test);
1334    Ok(cg_jump(fs, line))
1335}
1336
1337/// First half of `luaK_posfix`: pre-process the left operand `v` of a binary
1338/// operator before the right operand is parsed. Mirrors C's `luaK_infix`
1339/// from `lcode.c`. The codegen reconciliation has not yet routed parser
1340/// calls through `lua_code::infix`, so this lives in the parser file
1341/// alongside the other `cg_*` helpers.
1342///
1343/// For `And`/`Or` the operand is converted into a short-circuit form (jump
1344/// list closed via `cg_go_if_true` / `cg_go_if_false`). For `Concat` it is
1345/// pushed onto the next register. Other arithmetic, bitwise, and comparison
1346/// operators rely on `cg_posfix_fold` to discharge their operands after the
1347/// right-hand side is known, so `cg_infix` only calls `cg_discharge_vars`
1348/// for them.
1349fn cg_infix(
1350    fs: &mut FuncState,
1351    op: BinOpr,
1352    v: &mut ExprDesc,
1353    line: i32,
1354) -> Result<(), LuaError> {
1355    match op {
1356        BinOpr::And => cg_go_if_true(fs, line, v),
1357        BinOpr::Or => cg_go_if_false(fs, line, v),
1358        BinOpr::Concat => cg_exp_to_next_reg(fs, line, v),
1359        BinOpr::Add | BinOpr::Sub | BinOpr::Mul | BinOpr::Div | BinOpr::IDiv
1360        | BinOpr::Mod | BinOpr::Pow
1361        | BinOpr::BAnd | BinOpr::BOr | BinOpr::BXor
1362        | BinOpr::Shl | BinOpr::Shr
1363        | BinOpr::Eq | BinOpr::Ne
1364        | BinOpr::Lt | BinOpr::Le | BinOpr::Gt | BinOpr::Ge => {
1365            if matches!(v.k, ExprKind::KInt | ExprKind::KFlt)
1366                && v.t == NO_JUMP && v.f == NO_JUMP
1367            {
1368                cg_discharge_vars(fs, line, v)
1369            } else {
1370                cg_exp_to_any_reg(fs, line, v).map(|_| ())
1371            }
1372        }
1373        _ => cg_discharge_vars(fs, line, v),
1374    }
1375}
1376
1377/// Mirrors C's `isSCint` from `lcode.c` (a restriction of `isSCnumber` to
1378/// the integer case): returns `Some(int2sC(ival))` if `e` is a `VKINT`
1379/// literal whose value fits the signed-C 8-bit operand field, else `None`.
1380/// The returned byte is already pre-encoded with the `OFFSET_sC` bias so
1381/// the caller can drop it straight into an `sC` argument slot.
1382fn cg_sc_int(e: &ExprDesc) -> Option<u8> {
1383    if !matches!(e.k, ExprKind::KInt) {
1384        return None;
1385    }
1386    if e.t != NO_JUMP || e.f != NO_JUMP {
1387        return None;
1388    }
1389    let biased = (e.u.ival as u64).wrapping_add(lua_code::opcodes::OFFSET_S_C as u64);
1390    if biased <= lua_code::opcodes::MAXARG_C as u64 {
1391        Some(biased as u8)
1392    } else {
1393        None
1394    }
1395}
1396
1397/// Minimal `luaK_exp2anyreg`: ensure `e` ends up in *some* register. If `e`
1398/// is already `VNONRELOC` and its register is at or above `nactvar`, keep it
1399/// there; otherwise discharge to the next free register.
1400fn cg_exp_to_any_reg(
1401    fs: &mut FuncState,
1402    line: i32,
1403    e: &mut ExprDesc,
1404) -> Result<u8, LuaError> {
1405    cg_discharge_vars(fs, line, e)?;
1406    if e.k == ExprKind::NonReloc {
1407        if e.t == NO_JUMP && e.f == NO_JUMP {
1408            return Ok(e.u.info as u8);
1409        }
1410        if e.u.info >= fs.nactvar as i32 {
1411            cg_exp_to_reg(fs, line, e, e.u.info as u8)?;
1412            return Ok(e.u.info as u8);
1413        }
1414    }
1415    cg_exp_to_next_reg(fs, line, e)?;
1416    Ok(e.u.info as u8)
1417}
1418
1419/// Minimal `luaK_dischargevars` covering the cases the parser bootstrap can
1420/// produce: `VLOCAL`, `VUpVal`, `VIndexUp`, `VKStr`. Other variants are left
1421/// untouched. Returns Ok(()) on success.
1422fn cg_discharge_vars(
1423    fs: &mut FuncState,
1424    line: i32,
1425    e: &mut ExprDesc,
1426) -> Result<(), LuaError> {
1427    match e.k {
1428        ExprKind::Local => {
1429            e.u.info = e.u.var_ridx as i32;
1430            e.k = ExprKind::NonReloc;
1431        }
1432        ExprKind::UpVal => {
1433            let inst = lua_code::opcodes::Instruction::abck(
1434                lua_code::opcodes::OpCode::GetUpVal,
1435                0,
1436                e.u.info as u32,
1437                0,
1438                0,
1439            );
1440            let pc = emit_inst(fs, line, inst);
1441            e.u.info = pc;
1442            e.k = ExprKind::Reloc;
1443        }
1444        ExprKind::IndexUp => {
1445            let inst = lua_code::opcodes::Instruction::abck(
1446                lua_code::opcodes::OpCode::GetTabUp,
1447                0,
1448                e.u.ind_t as u32,
1449                e.u.ind_idx as u32,
1450                0,
1451            );
1452            let pc = emit_inst(fs, line, inst);
1453            e.u.info = pc;
1454            e.k = ExprKind::Reloc;
1455        }
1456        ExprKind::IndexI => {
1457            cg_free_reg_if_temp(fs, e.u.ind_t as i32);
1458            let inst = lua_code::opcodes::Instruction::abck(
1459                lua_code::opcodes::OpCode::GetI,
1460                0,
1461                e.u.ind_t as u32,
1462                e.u.ind_idx as u32,
1463                0,
1464            );
1465            let pc = emit_inst(fs, line, inst);
1466            e.u.info = pc;
1467            e.k = ExprKind::Reloc;
1468        }
1469        ExprKind::IndexStr => {
1470            cg_free_reg_if_temp(fs, e.u.ind_t as i32);
1471            let inst = lua_code::opcodes::Instruction::abck(
1472                lua_code::opcodes::OpCode::GetField,
1473                0,
1474                e.u.ind_t as u32,
1475                e.u.ind_idx as u32,
1476                0,
1477            );
1478            let pc = emit_inst(fs, line, inst);
1479            e.u.info = pc;
1480            e.k = ExprKind::Reloc;
1481        }
1482        ExprKind::Indexed => {
1483            let t_reg = e.u.ind_t as i32;
1484            let idx_reg = e.u.ind_idx as i32;
1485            if idx_reg > t_reg {
1486                cg_free_reg_if_temp(fs, idx_reg);
1487                cg_free_reg_if_temp(fs, t_reg);
1488            } else {
1489                cg_free_reg_if_temp(fs, t_reg);
1490                cg_free_reg_if_temp(fs, idx_reg);
1491            }
1492            let inst = lua_code::opcodes::Instruction::abck(
1493                lua_code::opcodes::OpCode::GetTable,
1494                0,
1495                e.u.ind_t as u32,
1496                e.u.ind_idx as u32,
1497                0,
1498            );
1499            let pc = emit_inst(fs, line, inst);
1500            e.u.info = pc;
1501            e.k = ExprKind::Reloc;
1502        }
1503        ExprKind::VarArg | ExprKind::Call => {
1504            cg_set_one_ret(fs, e);
1505        }
1506        _ => {}
1507    }
1508    Ok(())
1509}
1510
1511/// C: `luaK_setoneret` — adjust a Call/VarArg expression to produce a single
1512/// result. For a Call this leaves the already-emitted instruction alone (it
1513/// was emitted with `ARG_C = 2`, i.e. exactly one result) and reclassifies
1514/// `e` as `NonReloc` pointing at the result register (the Call's `ARG_A`).
1515/// For a VarArg this patches `ARG_C = 2` and leaves `e` as `Reloc` so the
1516/// caller can place the single result into a destination register.
1517fn cg_set_one_ret(fs: &mut FuncState, e: &mut ExprDesc) {
1518    if e.k == ExprKind::Call {
1519        let pc_idx = e.u.info as usize;
1520        let lc = lua_code::opcodes::Instruction(fs.f.code[pc_idx].0);
1521        debug_assert_eq!(lc.arg_c(), 2);
1522        e.u.info = lc.arg_a() as i32;
1523        e.k = ExprKind::NonReloc;
1524    } else if e.k == ExprKind::VarArg {
1525        let pc_idx = e.u.info as usize;
1526        let mut lc = lua_code::opcodes::Instruction(fs.f.code[pc_idx].0);
1527        lc.set_arg_c(2);
1528        fs.f.code[pc_idx] = lua_types::opcode::Instruction::new(lc.0);
1529        e.k = ExprKind::Reloc;
1530    }
1531}
1532
1533/// C: `luaK_storevar` — emit code to store `ex` into the variable described
1534/// by `var`. Handles VLocal (move into register), VUpVal (OP_SETUPVAL),
1535/// VIndexUp (OP_SETTABUP), VIndexI/IndexStr/Indexed (OP_SETI/SETFIELD/SETTABLE).
1536fn cg_storevar(
1537    fs: &mut FuncState,
1538    line: i32,
1539    var: &ExprDesc,
1540    ex: &mut ExprDesc,
1541) -> Result<(), LuaError> {
1542    match var.k {
1543        ExprKind::Local => {
1544            cg_free_exp(fs, ex);
1545            cg_exp_to_reg(fs, line, ex, var.u.var_ridx as u8)?;
1546            return Ok(());
1547        }
1548        ExprKind::UpVal => {
1549            let e_reg = cg_exp_to_any_reg(fs, line, ex)?;
1550            let inst = lua_code::opcodes::Instruction::abck(
1551                lua_code::opcodes::OpCode::SetUpVal,
1552                e_reg as u32,
1553                var.u.info as u32,
1554                0,
1555                0,
1556            );
1557            emit_inst(fs, line, inst);
1558        }
1559        ExprKind::IndexUp => {
1560            cg_store_abrk(fs, line, lua_code::opcodes::OpCode::SetTabUp,
1561                var.u.ind_t as u32, var.u.ind_idx as u32, ex)?;
1562        }
1563        ExprKind::IndexI => {
1564            cg_store_abrk(fs, line, lua_code::opcodes::OpCode::SetI,
1565                var.u.ind_t as u32, var.u.ind_idx as u32, ex)?;
1566        }
1567        ExprKind::IndexStr => {
1568            cg_store_abrk(fs, line, lua_code::opcodes::OpCode::SetField,
1569                var.u.ind_t as u32, var.u.ind_idx as u32, ex)?;
1570        }
1571        ExprKind::Indexed => {
1572            cg_store_abrk(fs, line, lua_code::opcodes::OpCode::SetTable,
1573                var.u.ind_t as u32, var.u.ind_idx as u32, ex)?;
1574        }
1575        _ => {
1576            return Err(LuaError::syntax(format_args!(
1577                "internal: cg_storevar: invalid var kind {:?}", var.k
1578            )));
1579        }
1580    }
1581    cg_free_exp(fs, ex);
1582    Ok(())
1583}
1584
1585/// Helper for cg_storevar: emit an ABRK-form store. Mirrors C's `codeABRK`
1586/// for the SetTabUp/SetI/SetField/SetTable family. When `ex` is a constant
1587/// the K bit is set; otherwise the value is forced into a register.
1588fn cg_store_abrk(
1589    fs: &mut FuncState,
1590    line: i32,
1591    op: lua_code::opcodes::OpCode,
1592    a: u32,
1593    b: u32,
1594    ex: &mut ExprDesc,
1595) -> Result<(), LuaError> {
1596    let c_reg = cg_exp_to_any_reg(fs, line, ex)?;
1597    let inst = lua_code::opcodes::Instruction::abck(op, a, b, c_reg as u32, 0);
1598    emit_inst(fs, line, inst);
1599    Ok(())
1600}
1601
1602/// Mirrors C's `discharge2reg` from `lcode.c`: places the value described by
1603/// `e` into `reg`. For `Jmp` this is a no-op (the caller — `cg_exp_to_reg` —
1604/// is responsible for stitching the jump into `e.t` and emitting the
1605/// LoadTrue / LFalseSkip pair if a concrete value is needed).
1606fn cg_discharge_to_reg(
1607    fs: &mut FuncState,
1608    line: i32,
1609    e: &mut ExprDesc,
1610    reg: u8,
1611) -> Result<(), LuaError> {
1612    cg_discharge_vars(fs, line, e)?;
1613    match e.k {
1614        ExprKind::Jmp => {
1615            return Ok(());
1616        }
1617        ExprKind::NonReloc => {
1618            if e.u.info as u8 != reg {
1619                let inst = lua_code::opcodes::Instruction::abck(
1620                    lua_code::opcodes::OpCode::Move,
1621                    reg as u32,
1622                    e.u.info as u32,
1623                    0, 0,
1624                );
1625                emit_inst(fs, line, inst);
1626            }
1627        }
1628        ExprKind::Reloc => {
1629            let pc = e.u.info as usize;
1630            let mut lc = lua_code::opcodes::Instruction(fs.f.code[pc].0);
1631            lc.set_arg_a(reg as u32);
1632            fs.f.code[pc] = lua_types::opcode::Instruction::new(lc.0);
1633        }
1634        ExprKind::Nil => {
1635            let inst = lua_code::opcodes::Instruction::abck(
1636                lua_code::opcodes::OpCode::LoadNil, reg as u32, 0, 0, 0,
1637            );
1638            emit_inst(fs, line, inst);
1639        }
1640        ExprKind::True => {
1641            let inst = lua_code::opcodes::Instruction::abck(
1642                lua_code::opcodes::OpCode::LoadTrue, reg as u32, 0, 0, 0,
1643            );
1644            emit_inst(fs, line, inst);
1645        }
1646        ExprKind::False => {
1647            let inst = lua_code::opcodes::Instruction::abck(
1648                lua_code::opcodes::OpCode::LoadFalse, reg as u32, 0, 0, 0,
1649            );
1650            emit_inst(fs, line, inst);
1651        }
1652        ExprKind::KInt => {
1653            let i = e.u.ival;
1654            let max = lua_code::opcodes::MAXARG_BX as i64 - lua_code::opcodes::OFFSET_S_BX as i64;
1655            let min = -(lua_code::opcodes::OFFSET_S_BX as i64);
1656            if i >= min && i <= max {
1657                let bx = (i as i32 + lua_code::opcodes::OFFSET_S_BX) as u32;
1658                let inst = lua_code::opcodes::Instruction::abx(
1659                    lua_code::opcodes::OpCode::LoadI, reg as u32, bx,
1660                );
1661                emit_inst(fs, line, inst);
1662            } else {
1663                let k_idx = add_k_value(fs, LuaValue::Int(i));
1664                let inst = lua_code::opcodes::Instruction::abx(
1665                    lua_code::opcodes::OpCode::LoadK, reg as u32, k_idx as u32,
1666                );
1667                emit_inst(fs, line, inst);
1668            }
1669        }
1670        ExprKind::KFlt => {
1671            let f = e.u.nval;
1672            let max = lua_code::opcodes::MAXARG_BX as i64 - lua_code::opcodes::OFFSET_S_BX as i64;
1673            let min = -(lua_code::opcodes::OFFSET_S_BX as i64);
1674            let fi_opt: Option<i64> = if f.fract() == 0.0 && f.abs() < i64::MAX as f64 {
1675                Some(f as i64)
1676            } else {
1677                None
1678            };
1679            if let Some(fi) = fi_opt.filter(|fi| *fi >= min && *fi <= max) {
1680                let bx = (fi as i32 + lua_code::opcodes::OFFSET_S_BX) as u32;
1681                let inst = lua_code::opcodes::Instruction::abx(
1682                    lua_code::opcodes::OpCode::LoadF, reg as u32, bx,
1683                );
1684                emit_inst(fs, line, inst);
1685            } else {
1686                let k_idx = add_k_value(fs, LuaValue::Float(f));
1687                let inst = lua_code::opcodes::Instruction::abx(
1688                    lua_code::opcodes::OpCode::LoadK, reg as u32, k_idx as u32,
1689                );
1690                emit_inst(fs, line, inst);
1691            }
1692        }
1693        ExprKind::KStr => {
1694            let s = e.u.strval.clone()
1695                .ok_or_else(|| LuaError::syntax(format_args!("internal: VKStr with no strval")))?;
1696            let k_idx = add_k_string(fs, s);
1697            let inst = lua_code::opcodes::Instruction::abx(
1698                lua_code::opcodes::OpCode::LoadK,
1699                reg as u32,
1700                k_idx as u32,
1701            );
1702            emit_inst(fs, line, inst);
1703        }
1704        ExprKind::K => {
1705            let inst = lua_code::opcodes::Instruction::abx(
1706                lua_code::opcodes::OpCode::LoadK,
1707                reg as u32,
1708                e.u.info as u32,
1709            );
1710            emit_inst(fs, line, inst);
1711        }
1712        _ => {
1713            return Err(LuaError::syntax(format_args!(
1714                "internal: cg_discharge_to_reg cannot discharge {:?}", e.k
1715            )));
1716        }
1717    }
1718    e.u.info = reg as i32;
1719    e.k = ExprKind::NonReloc;
1720    Ok(())
1721}
1722
1723/// Mirrors C's `need_value` from `lcode.c`: walks the jump-list `list` and
1724/// returns true if any controlling instruction is *not* an `OP_TESTSET`,
1725/// meaning a concrete LoadTrue / LFalseSkip pair must be emitted to provide
1726/// the value at the fallthrough.
1727fn cg_need_value(fs: &FuncState, list: i32) -> bool {
1728    let mut list = list;
1729    while list != NO_JUMP {
1730        let ctrl_pc = cg_get_jump_control(fs, list);
1731        let ctrl = cg_inst_at(fs, ctrl_pc);
1732        if ctrl.opcode() != Some(lua_code::opcodes::OpCode::TestSet) {
1733            return true;
1734        }
1735        list = cg_get_jump(fs, list);
1736    }
1737    false
1738}
1739
1740/// Mirrors C's `code_loadbool` from `lcode.c`: records `fs.pc` as a jump
1741/// label, then emits the requested LoadTrue / LoadFalse / LFalseSkip
1742/// instruction and returns its pc.
1743fn cg_code_loadbool(fs: &mut FuncState, line: i32, reg: i32, op: lua_code::opcodes::OpCode) -> i32 {
1744    cg_get_label(fs);
1745    let inst = lua_code::opcodes::Instruction::abck(op, reg as u32, 0, 0, 0);
1746    emit_inst(fs, line, inst)
1747}
1748
1749/// Mirrors C's `patchlistaux` from `lcode.c`: walks the jump-list `list`,
1750/// rewriting `TESTSET` controllers to write `reg` (and routing them to
1751/// `vtarget`) and leaving plain tests to fall through to `dtarget`.
1752fn cg_patch_list_aux(
1753    fs: &mut FuncState,
1754    list: i32,
1755    vtarget: i32,
1756    reg: u32,
1757    dtarget: i32,
1758) -> Result<(), LuaError> {
1759    let mut list = list;
1760    while list != NO_JUMP {
1761        let next = cg_get_jump(fs, list);
1762        if cg_patch_test_reg(fs, list, reg) {
1763            cg_fix_jump(fs, list, vtarget)?;
1764        } else {
1765            cg_fix_jump(fs, list, dtarget)?;
1766        }
1767        list = next;
1768    }
1769    Ok(())
1770}
1771
1772/// Discharge `e` into the specific register `reg`. Mirrors C's `exp2reg`
1773/// from `lcode.c`: delegates to `cg_discharge_to_reg`, then folds the jump
1774/// at `e.u.info` into `e.t` (when `e` is itself a test) and patches any
1775/// pending `e.t` / `e.f` jump-lists. When the lists actually need a value
1776/// (i.e. any controller isn't a `TESTSET`), emits the LFalseSkip / LoadTrue
1777/// pair around which the jumps land.
1778fn cg_exp_to_reg(
1779    fs: &mut FuncState,
1780    line: i32,
1781    e: &mut ExprDesc,
1782    reg: u8,
1783) -> Result<(), LuaError> {
1784    cg_discharge_to_reg(fs, line, e, reg)?;
1785    if e.k == ExprKind::Jmp {
1786        let info = e.u.info;
1787        cg_concat(fs, &mut e.t, info)?;
1788    }
1789    if e.t != e.f {
1790        let mut p_f = NO_JUMP;
1791        let mut p_t = NO_JUMP;
1792        if cg_need_value(fs, e.t) || cg_need_value(fs, e.f) {
1793            let fj = if e.k == ExprKind::Jmp {
1794                NO_JUMP
1795            } else {
1796                cg_jump(fs, line)
1797            };
1798            p_f = cg_code_loadbool(fs, line, reg as i32, lua_code::opcodes::OpCode::LFalseSkip);
1799            p_t = cg_code_loadbool(fs, line, reg as i32, lua_code::opcodes::OpCode::LoadTrue);
1800            cg_patch_to_here(fs, fj)?;
1801        }
1802        let final_pc = cg_get_label(fs);
1803        cg_patch_list_aux(fs, e.f, final_pc, reg as u32, p_f)?;
1804        cg_patch_list_aux(fs, e.t, final_pc, reg as u32, p_t)?;
1805    }
1806    e.f = NO_JUMP;
1807    e.t = NO_JUMP;
1808    e.u.info = reg as i32;
1809    e.k = ExprKind::NonReloc;
1810    Ok(())
1811}
1812
1813/// Like `cg_free_reg`, but only acts when the index actually belongs to a
1814/// temporary register (one above `fs.nactvar`). Used by indexed-get
1815/// dischargers, which may operate on either a temp result or a local.
1816fn cg_free_reg_if_temp(fs: &mut FuncState, reg: i32) {
1817    if reg >= fs.nactvar as i32 {
1818        debug_assert!(reg < fs.freereg as i32);
1819        if reg == fs.freereg as i32 - 1 {
1820            fs.freereg -= 1;
1821        }
1822    }
1823}
1824
1825/// Mirrors C's `luaK_exp2nextreg` from `lcode.c`: discharge variable forms,
1826/// free any temp held by `e`, reserve the next register, then call
1827/// `cg_exp_to_reg` to place the value (handling `Jmp` and pending
1828/// `e.t` / `e.f` jump-lists through the shared `exp2reg` path).
1829fn cg_exp_to_next_reg(
1830    fs: &mut FuncState,
1831    line: i32,
1832    e: &mut ExprDesc,
1833) -> Result<(), LuaError> {
1834    cg_discharge_vars(fs, line, e)?;
1835    cg_free_exp(fs, e);
1836    let reg = reserve_reg(fs)?;
1837    cg_exp_to_reg(fs, line, e, reg)
1838}
1839
1840/// C: `luaK_setreturns` — patch the call/vararg instruction at `e.u.info` so
1841/// it produces `nresults` values (or LUA_MULTRET when `nresults == -1`).
1842fn cg_set_returns(fs: &mut FuncState, e: &mut ExprDesc, nresults: i32) {
1843    let pc_idx = e.u.info as usize;
1844    let mut lc = lua_code::opcodes::Instruction(fs.f.code[pc_idx].0);
1845    if e.k == ExprKind::Call {
1846        lc.set_arg_c((nresults + 1) as u32);
1847    } else {
1848        debug_assert_eq!(e.k, ExprKind::VarArg);
1849        lc.set_arg_c((nresults + 1) as u32);
1850        lc.set_arg_a(fs.freereg as u32);
1851        fs.freereg += 1;
1852    }
1853    fs.f.code[pc_idx] = lua_types::opcode::Instruction::new(lc.0);
1854}
1855
1856/// C: `static int finaltarget(Instruction *code, int i)` — chase consecutive
1857/// `OP_JMP` instructions to the final landing pc. Capped at 100 hops to
1858/// avoid infinite loops on malformed code.
1859fn cg_final_target(fs: &FuncState, mut i: i32) -> i32 {
1860    for _ in 0..100 {
1861        let inst = cg_inst_at(fs, i);
1862        if inst.opcode() != Some(lua_code::opcodes::OpCode::Jmp) {
1863            break;
1864        }
1865        i += inst.arg_s_j() + 1;
1866    }
1867    i
1868}
1869
1870/// C: `luaK_finish` — final pass over the emitted bytecode.
1871///
1872/// Patches `OP_RETURN`/`OP_RETURN0`/`OP_RETURN1`/`OP_TAILCALL` to record the
1873/// vararg signature (so the VM can roll back `ci->func` on return) and the
1874/// `needclose` flag (so it closes pending upvalues). Also resolves chained
1875/// `OP_JMP` jumps to their final target.
1876fn cg_finish(fs: &mut FuncState) {
1877    use lua_code::opcodes::OpCode;
1878    let needclose = fs.needclose;
1879    let is_vararg = fs.f.is_vararg;
1880    let numparams = fs.f.numparams as u32;
1881    let pc_end = fs.pc;
1882    for i in 0..pc_end {
1883        let mut inst = cg_inst_at(fs, i);
1884        match inst.opcode() {
1885            Some(OpCode::Return0) | Some(OpCode::Return1) => {
1886                if !(needclose || is_vararg) {
1887                    continue;
1888                }
1889                inst.set_opcode(OpCode::Return);
1890                if needclose {
1891                    inst.set_arg_k(1);
1892                }
1893                if is_vararg {
1894                    inst.set_arg_c(numparams + 1);
1895                }
1896                cg_set_inst_at(fs, i, inst);
1897            }
1898            Some(OpCode::Return) | Some(OpCode::TailCall) => {
1899                if needclose {
1900                    inst.set_arg_k(1);
1901                }
1902                if is_vararg {
1903                    inst.set_arg_c(numparams + 1);
1904                }
1905                cg_set_inst_at(fs, i, inst);
1906            }
1907            Some(OpCode::Jmp) => {
1908                let target = cg_final_target(fs, i);
1909                let _ = cg_fix_jump(fs, i, target);
1910            }
1911            _ => {}
1912        }
1913    }
1914}
1915
1916/// C: `luaK_ret` — emit the appropriate OP_RETURN / OP_RETURN0 / OP_RETURN1
1917/// based on `nret`. `first` is the first result register; `nret` is the
1918/// number of values to return (`LUA_MULTRET` for "all values on top").
1919fn cg_emit_return(fs: &mut FuncState, line: i32, first: i32, nret: i32) {
1920    let op = match nret {
1921        0 => lua_code::opcodes::OpCode::Return0,
1922        1 => lua_code::opcodes::OpCode::Return1,
1923        _ => lua_code::opcodes::OpCode::Return,
1924    };
1925    let inst = lua_code::opcodes::Instruction::abck(
1926        op,
1927        first as u32,
1928        (nret + 1) as u32,
1929        0,
1930        0,
1931    );
1932    emit_inst(fs, line, inst);
1933}
1934
1935// ── Free functions ──────────────────────────────────────────────────────────
1936
1937// C: static void statement(LexState *ls);  -- forward declaration
1938// C: static void expr(LexState *ls, expdesc *v);  -- forward declaration
1939// (Both defined later in this file; Rust has no forward declarations.)
1940
1941// ── §1 Error helpers ────────────────────────────────────────────────────────
1942
1943/// C: static l_noret error_expected(LexState *ls, int token)
1944/// Constructs a syntax error for a missing expected token.
1945/// In Rust, `l_noret` becomes returning `LuaError`; callers use
1946/// `return Err(error_expected(...))`.
1947fn error_expected(ls: &mut LexState, token: TokenKind) -> LuaError {
1948    // C: luaX_syntaxerror(ls, luaO_pushfstring(ls->L, "%s expected", luaX_token2str(ls, token)));
1949    let tok_str = lua_lex::token2str(&ls.lex, token);
1950    let mut msg: Vec<u8> = Vec::with_capacity(tok_str.len() + 10);
1951    msg.extend_from_slice(&tok_str);
1952    msg.extend_from_slice(b" expected");
1953    lua_lex::syntax_error(&mut ls.lex, &msg)
1954}
1955
1956/// C: static l_noret errorlimit(FuncState *fs, int limit, const char *what)
1957/// Constructs a compile-time limit-exceeded syntax error.
1958fn error_limit(fs: &FuncState, limit: i32, what: &str) -> LuaError {
1959    // C: line == 0 ? "main function" : "function at line %d"
1960    let line = fs.f.linedefined;
1961    if line == 0 {
1962        LuaError::syntax(format_args!(
1963            "too many {} (limit is {}) in main function", what, limit
1964        ))
1965    } else {
1966        LuaError::syntax(format_args!(
1967            "too many {} (limit is {}) in function at line {}", what, limit, line
1968        ))
1969    }
1970}
1971
1972/// C: static void checklimit(FuncState *fs, int v, int l, const char *what)
1973fn check_limit(fs: &FuncState, v: i32, l: i32, what: &str) -> Result<(), LuaError> {
1974    if v > l {
1975        return Err(error_limit(fs, l, what));
1976    }
1977    Ok(())
1978}
1979
1980// ── §2 Basic parse utilities ─────────────────────────────────────────────────
1981
1982/// C: static int testnext(LexState *ls, int c)
1983/// If the current token matches `c`, consume it and return true.
1984fn test_next(ls: &mut LexState, state: &mut LuaState, c: TokenKind) -> Result<bool, LuaError> {
1985    if ls.t.token == c {
1986        // C: luaX_next(ls)
1987        lex_next(ls, state)?;
1988        Ok(true)
1989    } else {
1990        Ok(false)
1991    }
1992}
1993
1994/// C: static void check(LexState *ls, int c)
1995fn check(ls: &mut LexState, c: TokenKind) -> Result<(), LuaError> {
1996    if ls.t.token != c {
1997        return Err(error_expected(ls, c));
1998    }
1999    Ok(())
2000}
2001
2002/// C: static void checknext(LexState *ls, int c)
2003fn check_next(ls: &mut LexState, state: &mut LuaState, c: TokenKind) -> Result<(), LuaError> {
2004    check(ls, c)?;
2005    // C: luaX_next(ls)
2006    lex_next(ls, state)?;
2007    Ok(())
2008}
2009
2010/// C: static TString *str_checkname(LexState *ls)
2011/// Expects TK_NAME, returns the name string, advances.
2012fn str_check_name(ls: &mut LexState, state: &mut LuaState) -> Result<GcRef<LuaString>, LuaError> {
2013    // C: check(ls, TK_NAME); ts = ls->t.seminfo.ts; luaX_next(ls); return ts;
2014    check(ls, TK_NAME)?;
2015    let ts = ls.t.seminfo.ts.clone()
2016        .ok_or_else(|| LuaError::syntax(format_args!("name expected")))?;
2017    lex_next(ls, state)?;
2018    Ok(ts)
2019}
2020
2021/// C: static void init_exp(expdesc *e, expkind k, int i)
2022fn init_exp(e: &mut ExprDesc, k: ExprKind, i: i32) {
2023    e.f = NO_JUMP;
2024    e.t = NO_JUMP;
2025    e.k = k;
2026    e.u.info = i;
2027}
2028
2029/// C: static void codestring(expdesc *e, TString *s)
2030fn codestring(e: &mut ExprDesc, s: GcRef<LuaString>) {
2031    e.f = NO_JUMP;
2032    e.t = NO_JUMP;
2033    e.k = ExprKind::KStr;
2034    e.u.strval = Some(s);
2035}
2036
2037/// C: static void codename(LexState *ls, expdesc *e)
2038fn codename(ls: &mut LexState, state: &mut LuaState, e: &mut ExprDesc) -> Result<(), LuaError> {
2039    // C: codestring(e, str_checkname(ls));
2040    let name = str_check_name(ls, state)?;
2041    codestring(e, name);
2042    Ok(())
2043}
2044
2045// ── §3 Variable handling ─────────────────────────────────────────────────────
2046
2047/// C: static int registerlocalvar(LexState *ls, FuncState *fs, TString *varname)
2048/// Registers a local variable in the proto's debug-info locvars array.
2049/// Returns the index in locvars (= fs->ndebugvars before increment).
2050fn register_local_var(
2051    ls: &mut LexState,
2052    state: &mut LuaState,
2053    fs: &mut FuncState,
2054    varname: GcRef<LuaString>,
2055) -> Result<i32, LuaError> {
2056    // C: luaM_growvector(ls->L, f->locvars, fs->ndebugvars, f->sizelocvars, LocVar, SHRT_MAX, ...)
2057    // In Rust, Vec grows automatically; just push a placeholder if needed.
2058    let idx = fs.ndebugvars as usize;
2059    while fs.f.locvars.len() <= idx {
2060        // C: f->locvars[oldsize++].varname = NULL
2061        fs.f.locvars.push(LocalVar {
2062            varname: varname.clone(), // placeholder; overwritten below
2063            startpc: 0,
2064            endpc: 0,
2065        });
2066    }
2067    fs.f.locvars[idx].varname = varname;
2068    fs.f.locvars[idx].startpc = fs.pc;
2069    // C: luaC_objbarrier(ls->L, f, varname) — no-op in Phase A
2070    let result = fs.ndebugvars as i32;
2071    fs.ndebugvars += 1;
2072    Ok(result)
2073}
2074
2075/// C: static int new_localvar(LexState *ls, TString *name)
2076/// Creates a new local variable entry in dyd.actvar.
2077/// Returns the variable's index relative to fs->firstlocal.
2078fn new_local_var(
2079    ls: &mut LexState,
2080    state: &mut LuaState,
2081    name: GcRef<LuaString>,
2082) -> Result<i32, LuaError> {
2083    // C: checklimit(fs, dyd->actvar.n + 1 - fs->firstlocal, MAXVARS, "local variables")
2084    let fs = ls.fs.as_ref().unwrap();
2085    let n = ls.dyd.actvar.len() as i32;
2086    let first_local = fs.firstlocal;
2087    check_limit(fs, n + 1 - first_local, MAX_VARS, "local variables")?;
2088
2089    // C: luaM_growvector(...) — Vec grows automatically
2090    // C: var = &dyd->actvar.arr[dyd->actvar.n++]
2091    let mut var = VarDesc::default();
2092    var.kind = VarKind::Reg;
2093    var.name = Some(name);
2094    ls.dyd.actvar.push(var);
2095    let result = ls.dyd.actvar.len() as i32 - 1 - first_local;
2096    Ok(result)
2097}
2098
2099/// C: static Vardesc *getlocalvardesc(FuncState *fs, int vidx)
2100/// Returns a reference to the VarDesc at index `fs->firstlocal + vidx`.
2101fn get_local_var_desc<'a>(ls: &'a LexState, fs: &FuncState, vidx: i32) -> &'a VarDesc {
2102    &ls.dyd.actvar[(fs.firstlocal + vidx) as usize]
2103}
2104
2105/// C: static Vardesc *getlocalvardesc — mutable variant
2106fn get_local_var_desc_mut(ls: &mut LexState, first_local: i32, vidx: i32) -> &mut VarDesc {
2107    &mut ls.dyd.actvar[(first_local + vidx) as usize]
2108}
2109
2110/// C: static int reglevel(FuncState *fs, int nvar)
2111/// Converts a compiler-index level to its register number.
2112fn reg_level(ls: &LexState, fs: &FuncState, nvar: i32) -> i32 {
2113    // C: search backwards for the highest variable in a register
2114    let mut nvar = nvar;
2115    while nvar > 0 {
2116        nvar -= 1;
2117        let vd = get_local_var_desc(ls, fs, nvar);
2118        if vd.kind != VarKind::CompileTimeConst {
2119            return vd.ridx as i32 + 1;
2120        }
2121    }
2122    0
2123}
2124
2125/// C: int luaY_nvarstack(FuncState *fs)
2126/// Returns the number of variables currently occupying registers.
2127/// LUAI_FUNC visibility.
2128pub fn nvarstack(ls: &LexState, fs: &FuncState) -> i32 {
2129    reg_level(ls, fs, fs.nactvar as i32)
2130}
2131
2132/// C: static LocVar *localdebuginfo(FuncState *fs, int vidx)
2133/// Returns a mutable reference to the debug-info entry for variable `vidx`,
2134/// or `None` if it is a compile-time constant (no debug info).
2135fn local_debug_info<'a>(ls: &LexState, fs: &'a mut FuncState, vidx: i32) -> Option<&'a mut LocalVar> {
2136    let vd = get_local_var_desc(ls, fs, vidx);
2137    if vd.kind == VarKind::CompileTimeConst {
2138        return None; // C: return NULL
2139    }
2140    let idx = vd.pidx as usize;
2141    debug_assert!((idx as i16) < fs.ndebugvars);
2142    // TODO(port): borrow conflict — vd borrows ls immutably, idx is a copy; safe to proceed.
2143    Some(&mut fs.f.locvars[idx])
2144}
2145
2146/// C: static void init_var(FuncState *fs, expdesc *e, int vidx)
2147fn init_var(ls: &LexState, fs: &FuncState, e: &mut ExprDesc, vidx: i32) {
2148    e.f = NO_JUMP;
2149    e.t = NO_JUMP;
2150    e.k = ExprKind::Local;
2151    e.u.var_vidx = vidx as u16;
2152    e.u.var_ridx = get_local_var_desc(ls, fs, vidx).ridx;
2153}
2154
2155/// C: static void check_readonly(LexState *ls, expdesc *e)
2156/// Raises an error if expression `e` describes a read-only variable.
2157fn check_readonly(ls: &mut LexState, state: &mut LuaState, e: &ExprDesc) -> Result<(), LuaError> {
2158    // C: FuncState *fs = ls->fs;  TString *varname = NULL;
2159    let varname: Option<GcRef<LuaString>> = {
2160        let fs = ls.fs.as_ref().unwrap();
2161        match e.k {
2162            ExprKind::Const => {
2163                // C: varname = ls->dyd->actvar.arr[e->u.info].vd.name
2164                ls.dyd.actvar[e.u.info as usize].name.clone()
2165            }
2166            ExprKind::Local => {
2167                let vd = get_local_var_desc(ls, fs, e.u.var_vidx as i32);
2168                if vd.kind != VarKind::Reg {
2169                    vd.name.clone()
2170                } else {
2171                    None
2172                }
2173            }
2174            ExprKind::UpVal => {
2175                let up = &fs.f.upvalues[e.u.info as usize];
2176                if VarKind::from_u8(up.kind) != VarKind::Reg {
2177                    up.name.clone()
2178                } else {
2179                    None
2180                }
2181            }
2182            _ => None,
2183        }
2184    };
2185    if let Some(vname) = varname {
2186        // C: luaK_semerror(ls, ...) prepends the `[source]:line:` prefix via
2187        // luaX_syntaxerror; route through `syntax_error` here for parity so
2188        // constructs.lua's checkload(":1: attempt to assign...") matches.
2189        let _ = state;
2190        let msg = format!(
2191            "attempt to assign to const variable '{}'",
2192            String::from_utf8_lossy(vname.as_bytes())
2193        );
2194        return Err(lua_lex::syntax_error(&mut ls.lex, msg.as_bytes()));
2195    }
2196    Ok(())
2197}
2198
2199/// C: static void adjustlocalvars(LexState *ls, int nvars)
2200/// Starts the scope for the last `nvars` created variables.
2201fn adjust_local_vars(ls: &mut LexState, state: &mut LuaState, nvars: i32) -> Result<(), LuaError> {
2202    // Extract needed data to avoid borrow conflict with ls.fs and ls.dyd
2203    let first_local = ls.fs.as_ref().unwrap().firstlocal;
2204    let nactvar_start = ls.fs.as_ref().unwrap().nactvar as i32;
2205    let mut reglevel_val = {
2206        let fs = ls.fs.as_ref().unwrap();
2207        reg_level(ls, fs, fs.nactvar as i32)
2208    };
2209
2210    for i in 0..nvars {
2211        let vidx = nactvar_start + i;
2212        ls.fs.as_mut().unwrap().nactvar += 1;
2213        // C: var->vd.ridx = reglevel++; var->vd.pidx = registerlocalvar(...)
2214        let var_name = ls.dyd.actvar[(first_local + vidx) as usize].name.clone();
2215        ls.dyd.actvar[(first_local + vidx) as usize].ridx = reglevel_val as u8;
2216        reglevel_val += 1;
2217        if let Some(vn) = var_name {
2218            let mut fs_box = ls.fs.take().unwrap();
2219            let pidx_result = register_local_var(ls, state, &mut fs_box, vn);
2220            ls.fs = Some(fs_box);
2221            let pidx = pidx_result?;
2222            ls.dyd.actvar[(first_local + vidx) as usize].pidx = pidx as i16;
2223        } else {
2224            // TODO(port): variable has no name — shouldn't happen in valid source
2225        }
2226    }
2227    Ok(())
2228}
2229
2230/// C: static void removevars(FuncState *fs, int tolevel)
2231/// Closes scope for all variables above `tolevel`, updating their endpc.
2232fn remove_vars(ls: &mut LexState, fs: &mut FuncState, tolevel: i32) {
2233    // C: fs->ls->dyd->actvar.n -= (fs->nactvar - tolevel)
2234    //
2235    // C just decrements a length counter; the underlying array memory is
2236    // untouched and the subsequent loop reads from it freely. A Rust
2237    // `truncate` would actually free the entries, leaving the loop reading
2238    // out-of-range and silently writing every iteration's endpc to
2239    // `locvars[0]` (via the `unwrap_or(0)` fallback below). Defer the
2240    // truncate until after the loop walks each soon-to-be-removed entry.
2241    let delta = fs.nactvar as i32 - tolevel;
2242    while fs.nactvar as i32 > tolevel {
2243        fs.nactvar -= 1;
2244        // C: LocVar *var = localdebuginfo(fs, --fs->nactvar); if (var) var->endpc = fs->pc
2245        let nactvar = fs.nactvar as i32;
2246        let vd_kind = {
2247            let first_local = fs.firstlocal;
2248            ls.dyd.actvar.get((first_local + nactvar) as usize)
2249                .map(|v| v.kind)
2250                .unwrap_or(VarKind::Reg)
2251        };
2252        if vd_kind != VarKind::CompileTimeConst {
2253            let vd_pidx = {
2254                let first_local = fs.firstlocal;
2255                ls.dyd.actvar.get((first_local + nactvar) as usize)
2256                    .map(|v| v.pidx)
2257                    .unwrap_or(0)
2258            };
2259            if let Some(lv) = fs.f.locvars.get_mut(vd_pidx as usize) {
2260                lv.endpc = fs.pc;
2261            }
2262        }
2263    }
2264    if delta > 0 {
2265        let new_len = ls.dyd.actvar.len().saturating_sub(delta as usize);
2266        ls.dyd.actvar.truncate(new_len);
2267    }
2268}
2269
2270// ── §4 Upvalue handling ──────────────────────────────────────────────────────
2271
2272/// C: static int searchupvalue(FuncState *fs, TString *name)
2273/// Returns the index of an upvalue named `name`, or -1 if not found.
2274/// C: pointer equality (eqstr) because all strings are interned.
2275fn search_upvalue(fs: &FuncState, name: &GcRef<LuaString>) -> i32 {
2276    for (i, up) in fs.f.upvalues.iter().enumerate() {
2277        if up.name.as_ref().map_or(false, |n| GcRef::ptr_eq(n, name)) {
2278            return i as i32;
2279        }
2280    }
2281    -1
2282}
2283
2284/// C: static Upvaldesc *allocupvalue(FuncState *fs)
2285/// Grows upvalues array and returns index of the new slot.
2286fn alloc_upvalue(fs: &mut FuncState) -> Result<usize, LuaError> {
2287    // C: checklimit(fs, fs->nups + 1, MAXUPVAL, "upvalues")
2288    if fs.nups as i32 + 1 > MAX_UPVAL as i32 {
2289        return Err(error_limit(fs, MAX_UPVAL as i32, "upvalues"));
2290    }
2291    // C: luaM_growvector — Vec handles this automatically
2292    let idx = fs.nups as usize;
2293    while fs.f.upvalues.len() <= idx {
2294        fs.f.upvalues.push(UpvalDesc { name: None, instack: false, idx: 0, kind: 0 });
2295    }
2296    fs.nups += 1;
2297    Ok(idx)
2298}
2299
2300/// C: static int newupvalue(FuncState *fs, TString *name, expdesc *v)
2301/// Adds a new upvalue descriptor and returns its index.
2302fn new_upvalue(
2303    ls: &LexState,
2304    fs: &mut FuncState,
2305    name: GcRef<LuaString>,
2306    v: &ExprDesc,
2307) -> Result<i32, LuaError> {
2308    let idx = alloc_upvalue(fs)?;
2309    let kind: u8 = if v.k == ExprKind::Local {
2310        // C: kind = getlocalvardesc(prev, v->u.var.vidx)->vd.kind
2311        let prev = fs.prev.as_deref().expect("upvalue capture requires enclosing FuncState");
2312        get_local_var_desc(ls, prev, v.u.var_vidx as i32).kind.as_u8()
2313    } else {
2314        // C: kind = prev->f->upvalues[v->u.info].kind
2315        let prev = fs.prev.as_deref().expect("upvalue chain requires enclosing FuncState");
2316        prev.f.upvalues[v.u.info as usize].kind
2317    };
2318    let up = &mut fs.f.upvalues[idx];
2319    if v.k == ExprKind::Local {
2320        // C: up->instack = 1; up->idx = v->u.var.ridx
2321        up.instack = true;
2322        up.idx = v.u.var_ridx;
2323    } else {
2324        // C: up->instack = 0; up->idx = cast_byte(v->u.info)
2325        up.instack = false;
2326        up.idx = v.u.info as u8;
2327    }
2328    up.kind = kind;
2329    up.name = Some(name);
2330    // C: luaC_objbarrier(fs->ls->L, fs->f, name) — no-op in Phase A
2331    Ok(fs.nups as i32 - 1)
2332}
2333
2334/// C: static int searchvar(FuncState *fs, TString *n, expdesc *var)
2335/// Searches for a local variable named `n`. Returns ExprKind as i32 or -1.
2336fn searchvar(
2337    ls: &LexState,
2338    fs: &FuncState,
2339    n: &GcRef<LuaString>,
2340    var: &mut ExprDesc,
2341) -> i32 {
2342    // C: for (i = cast_int(fs->nactvar) - 1; i >= 0; i--)
2343    let mut i = fs.nactvar as i32 - 1;
2344    while i >= 0 {
2345        let vd = get_local_var_desc(ls, fs, i);
2346        if vd.name.as_ref().map_or(false, |nm| GcRef::ptr_eq(nm, n)) {
2347            if vd.kind == VarKind::CompileTimeConst {
2348                // C: init_exp(var, VCONST, fs->firstlocal + i)
2349                init_exp(var, ExprKind::Const, fs.firstlocal + i);
2350            } else {
2351                init_var(ls, fs, var, i);
2352            }
2353            return var.k as i32; // PORT NOTE: encoding ExprKind as i32 for C compat
2354        }
2355        i -= 1;
2356    }
2357    -1
2358}
2359
2360/// C: static void markupval(FuncState *fs, int level)
2361/// Marks the block where the variable at `level` was defined as having an upvalue.
2362fn markupval(fs: &mut FuncState, level: i32) {
2363    // C: while (bl->nactvar > level) bl = bl->previous;  bl->upval = 1;
2364    let mut current = fs.bl.as_deref_mut();
2365    while let Some(b) = current {
2366        if (b.nactvar as i32) <= level {
2367            b.upval = true;
2368            break;
2369        }
2370        current = b.previous.as_deref_mut();
2371    }
2372    fs.needclose = true;
2373}
2374
2375/// C: static void marktobeclosed(FuncState *fs)
2376fn marktobeclosed(fs: &mut FuncState) {
2377    if let Some(bl) = fs.bl.as_mut() {
2378        bl.upval = true;
2379        bl.insidetbc = true;
2380    }
2381    fs.needclose = true;
2382}
2383
2384// ── §5 Variable resolution ───────────────────────────────────────────────────
2385
2386/// C: static void singlevaraux(FuncState *fs, TString *n, expdesc *var, int base)
2387/// Recursively finds variable `n` in `fs` and its enclosing functions.
2388/// If not found at any level, sets var->k = VVOID (global).
2389fn singlevaraux(
2390    ls: &LexState,
2391    fs: Option<&mut FuncState>,
2392    n: &GcRef<LuaString>,
2393    var: &mut ExprDesc,
2394    base: bool,
2395) -> Result<(), LuaError> {
2396    match fs {
2397        None => {
2398            init_exp(var, ExprKind::Void, 0);
2399        }
2400        Some(fs) => {
2401            let v = searchvar(ls, fs, n, var);
2402            if v >= 0 {
2403                if v == ExprKind::Local as i32 && !base {
2404                    markupval(fs, var.u.var_vidx as i32);
2405                }
2406            } else {
2407                let idx = search_upvalue(fs, n);
2408                let final_idx = if idx < 0 {
2409                    singlevaraux(ls, fs.prev.as_deref_mut(), n, var, false)?;
2410                    if var.k == ExprKind::Local || var.k == ExprKind::UpVal {
2411                        new_upvalue(ls, fs, n.clone(), var)?
2412                    } else {
2413                        return Ok(());
2414                    }
2415                } else {
2416                    idx
2417                };
2418                init_exp(var, ExprKind::UpVal, final_idx);
2419            }
2420        }
2421    }
2422    Ok(())
2423}
2424
2425/// C: static void singlevar(LexState *ls, expdesc *var)
2426/// Finds the variable named by the next TK_NAME token.
2427fn singlevar(ls: &mut LexState, state: &mut LuaState, var: &mut ExprDesc) -> Result<(), LuaError> {
2428    let varname = str_check_name(ls, state)?;
2429    let mut fs_box = ls.fs.take();
2430    let recurse_result = singlevaraux(ls, fs_box.as_deref_mut(), &varname, var, true);
2431    ls.fs = fs_box;
2432    recurse_result?;
2433    if var.k == ExprKind::Void {
2434        let envn = ls.envn.clone().expect("envn must be set when resolving globals");
2435        let mut env_var = ExprDesc::default();
2436        let mut fs_box = ls.fs.take();
2437        let r = singlevaraux(ls, fs_box.as_deref_mut(), &envn, &mut env_var, true);
2438        ls.fs = fs_box;
2439        r?;
2440        debug_assert!(env_var.k != ExprKind::Void, "_ENV must resolve");
2441        let line = ls.lastline;
2442        let fs = ls.fs.as_mut().unwrap();
2443        cg_exp_to_any_reg_up(fs, line, &mut env_var)?;
2444        let mut key = ExprDesc::default();
2445        codestring(&mut key, varname);
2446        cg_indexed(fs, line, &mut env_var, &mut key)?;
2447        *var = env_var;
2448    }
2449    Ok(())
2450}
2451
2452/// C: static void adjust_assign(LexState *ls, int nvars, int nexps, expdesc *e)
2453fn adjust_assign(
2454    ls: &mut LexState,
2455    state: &mut LuaState,
2456    nvars: i32,
2457    nexps: i32,
2458    e: &mut ExprDesc,
2459) -> Result<(), LuaError> {
2460    let needed = nvars - nexps;
2461    let line = ls.lastline;
2462    let fs = ls.fs.as_mut().unwrap();
2463    if e.k.has_mult_ret() {
2464        // C: extra = needed + 1; if (extra < 0) extra = 0; luaK_setreturns(fs, e, extra)
2465        let extra = if needed + 1 < 0 { 0 } else { needed + 1 };
2466        cg_set_returns(fs, e, extra);
2467    } else {
2468        if e.k != ExprKind::Void {
2469            cg_exp_to_next_reg(fs, line, e)?;
2470        }
2471        if needed > 0 {
2472            let from = fs.freereg as i32;
2473            cg_emit_nil(fs, line, from, needed);
2474        }
2475    }
2476    if needed > 0 {
2477        for _ in 0..needed {
2478            reserve_reg(fs)?;
2479        }
2480    } else {
2481        fs.freereg = (fs.freereg as i32 + needed) as u8;
2482    }
2483    Ok(())
2484}
2485
2486/// Emits `OP_NEWTABLE` followed by the required `OP_EXTRAARG` slot. The two
2487/// instructions are written as placeholders; `cg_settablesize` later patches
2488/// them with the final array/hash sizes. Returns the pc of `OP_NEWTABLE`.
2489fn cg_emit_newtable(fs: &mut FuncState, line: i32) -> i32 {
2490    let newtable = lua_code::opcodes::Instruction::abck(
2491        lua_code::opcodes::OpCode::NewTable, 0, 0, 0, 0,
2492    );
2493    let pc = emit_inst(fs, line, newtable);
2494    let extra = lua_code::opcodes::Instruction::ax(
2495        lua_code::opcodes::OpCode::ExtraArg, 0,
2496    );
2497    emit_inst(fs, line, extra);
2498    pc
2499}
2500
2501/// Patches a previously-emitted `OP_NEWTABLE`/`OP_EXTRAARG` pair with the
2502/// final array size (`asize`) and hash size (`hsize`). Mirrors
2503/// `luaK_settablesize` from `lcode.c`.
2504fn cg_settablesize(fs: &mut FuncState, pc: i32, ra: i32, asize: i32, hsize: i32) {
2505    let rb = if hsize != 0 {
2506        (hsize as u32).next_power_of_two().trailing_zeros() as i32 + 1
2507    } else {
2508        0
2509    };
2510    let maxc = lua_code::opcodes::MAXARG_C as i32 + 1;
2511    let extra = asize / maxc;
2512    let rc = asize % maxc;
2513    let k = if extra > 0 { 1u32 } else { 0u32 };
2514    let newtable = lua_code::opcodes::Instruction::abck(
2515        lua_code::opcodes::OpCode::NewTable,
2516        ra as u32, rb as u32, rc as u32, k,
2517    );
2518    fs.f.code[pc as usize] = lua_types::opcode::Instruction::new(newtable.0);
2519    let extra_inst = lua_code::opcodes::Instruction::ax(
2520        lua_code::opcodes::OpCode::ExtraArg, extra as u32,
2521    );
2522    fs.f.code[pc as usize + 1] = lua_types::opcode::Instruction::new(extra_inst.0);
2523}
2524
2525/// Emits `OP_SETLIST` for `tostore` elements starting at `base+1`, with
2526/// `nelems` already-stored elements preceding them. `tostore == -1` means
2527/// `LUA_MULTRET` (encoded as 0 in the B field). Also resets `fs.freereg`
2528/// to `base + 1`, mirroring `luaK_setlist`.
2529fn cg_setlist(fs: &mut FuncState, line: i32, base: i32, nelems: i32, tostore: i32) {
2530    let maxc = lua_code::opcodes::MAXARG_C as i32;
2531    let tostore_arg = if tostore == LUA_MULTRET { 0 } else { tostore };
2532    if nelems <= maxc {
2533        let inst = lua_code::opcodes::Instruction::abck(
2534            lua_code::opcodes::OpCode::SetList,
2535            base as u32, tostore_arg as u32, nelems as u32, 0,
2536        );
2537        emit_inst(fs, line, inst);
2538    } else {
2539        let extra = nelems / (maxc + 1);
2540        let nelems_lo = nelems % (maxc + 1);
2541        let inst = lua_code::opcodes::Instruction::abck(
2542            lua_code::opcodes::OpCode::SetList,
2543            base as u32, tostore_arg as u32, nelems_lo as u32, 1,
2544        );
2545        emit_inst(fs, line, inst);
2546        let extra_inst = lua_code::opcodes::Instruction::ax(
2547            lua_code::opcodes::OpCode::ExtraArg, extra as u32,
2548        );
2549        emit_inst(fs, line, extra_inst);
2550    }
2551    fs.freereg = (base + 1) as u8;
2552}
2553
2554/// Converts a table-and-key expression pair into the appropriate `VINDEX*`
2555/// variant. Mirrors `luaK_indexed` from `lcode.c`. Assumes `t` is already a
2556/// value-producing form (`VLOCAL`, `VNONRELOC`, or `VUPVAL`) and that any
2557/// short-string key has already been promoted to a `VKSTR` constant index.
2558fn cg_indexed(fs: &mut FuncState, line: i32, t: &mut ExprDesc, k: &mut ExprDesc) -> Result<(), LuaError> {
2559    if k.k == ExprKind::KStr {
2560        let s = k.u.strval.clone()
2561            .ok_or_else(|| LuaError::syntax(format_args!("internal: VKStr with no strval")))?;
2562        let k_idx = add_k_string(fs, s);
2563        k.u.info = k_idx;
2564        k.k = ExprKind::K;
2565    }
2566    let k_is_kstr = k.k == ExprKind::K
2567        && k.u.info >= 0
2568        && (k.u.info as u32) <= lua_code::opcodes::MAXARG_B;
2569    if t.k == ExprKind::UpVal && !k_is_kstr {
2570        cg_exp_to_any_reg(fs, line, t)?;
2571    }
2572    if t.k == ExprKind::UpVal {
2573        let temp = t.u.info as u8;
2574        t.u.ind_t = temp;
2575        t.u.ind_idx = k.u.info as i16;
2576        t.k = ExprKind::IndexUp;
2577        return Ok(());
2578    }
2579    let t_reg = match t.k {
2580        ExprKind::Local => t.u.var_ridx,
2581        ExprKind::NonReloc => t.u.info as u8,
2582        _ => return Err(LuaError::syntax(format_args!(
2583            "internal: cg_indexed on non-register table kind {:?}", t.k
2584        ))),
2585    };
2586    t.u.ind_t = t_reg;
2587    if k.k == ExprKind::K && k_is_kstr {
2588        t.u.ind_idx = k.u.info as i16;
2589        t.k = ExprKind::IndexStr;
2590    } else if k.k == ExprKind::KInt && cg_fits_int_key(k.u.ival) {
2591        t.u.ind_idx = k.u.ival as i16;
2592        t.k = ExprKind::IndexI;
2593    } else {
2594        cg_exp_to_any_reg(fs, line, k)?;
2595        t.u.ind_idx = k.u.info as i16;
2596        t.k = ExprKind::Indexed;
2597    }
2598    Ok(())
2599}
2600
2601fn cg_fits_int_key(i: i64) -> bool {
2602    i >= 0 && (i as u32) <= lua_code::opcodes::MAXARG_C
2603}
2604
2605/// C: void luaK_self(FuncState *fs, expdesc *e, expdesc *key)
2606/// Emits OP_SELF, converting `e:key(...)` into the equivalent of `(e.key)(e, ...)`.
2607/// Leaves `e` as VNONRELOC pointing at the function register (base); the self
2608/// register is `base + 1`. `key` must be a string expression (VKStr).
2609fn cg_self(
2610    fs: &mut FuncState,
2611    line: i32,
2612    e: &mut ExprDesc,
2613    key: &mut ExprDesc,
2614) -> Result<(), LuaError> {
2615    cg_exp_to_any_reg(fs, line, e)?;
2616    let ereg = e.u.info;
2617    cg_free_exp(fs, e);
2618    let base = fs.freereg as i32;
2619    e.u.info = base;
2620    e.k = ExprKind::NonReloc;
2621    reserve_regs(fs, 2)?;
2622    let key_str = key.u.strval.clone()
2623        .ok_or_else(|| LuaError::syntax(format_args!(
2624            "internal: cg_self expected VKStr key, got {:?}", key.k
2625        )))?;
2626    let k_idx = add_k_string(fs, key_str);
2627    let (c_arg, k_flag) = if (k_idx as u32) <= lua_code::opcodes::MAXINDEXRK {
2628        (k_idx as u32, 1u32)
2629    } else {
2630        key.k = ExprKind::K;
2631        key.u.info = k_idx;
2632        cg_exp_to_any_reg(fs, line, key)?;
2633        (key.u.info as u32, 0u32)
2634    };
2635    let inst = lua_code::opcodes::Instruction::abck(
2636        lua_code::opcodes::OpCode::Self_,
2637        base as u32,
2638        ereg as u32,
2639        c_arg,
2640        k_flag,
2641    );
2642    emit_inst(fs, line, inst);
2643    cg_free_exp(fs, key);
2644    Ok(())
2645}
2646
2647/// Minimal `luaK_exp2anyregup`: if `e` is an upvalue or constant, leave it as
2648/// is; otherwise discharge it into some register.
2649fn cg_exp_to_any_reg_up(fs: &mut FuncState, line: i32, e: &mut ExprDesc) -> Result<(), LuaError> {
2650    if matches!(e.k, ExprKind::UpVal | ExprKind::K) {
2651        return Ok(());
2652    }
2653    cg_exp_to_any_reg(fs, line, e)?;
2654    Ok(())
2655}
2656
2657/// Minimal `luaK_nil`: emits a LoadNil instruction filling `n` consecutive
2658/// registers starting at `from` with `nil`. Does not perform the C
2659/// optimization that merges with a preceding LoadNil.
2660fn cg_emit_nil(fs: &mut FuncState, line: i32, from: i32, n: i32) {
2661    let inst = lua_code::opcodes::Instruction::abck(
2662        lua_code::opcodes::OpCode::LoadNil,
2663        from as u32,
2664        (n - 1) as u32,
2665        0,
2666        0,
2667    );
2668    emit_inst(fs, line, inst);
2669}
2670
2671// ── §6 Label / goto management ───────────────────────────────────────────────
2672
2673/// C: static l_noret jumpscopeerror(LexState *ls, Labeldesc *gt)
2674fn jumpscopeerror(ls: &LexState, gt_idx: usize) -> LuaError {
2675    let gt = &ls.dyd.gt[gt_idx];
2676    let line = gt.line;
2677    let gt_name_bytes: &[u8] = gt.name.as_ref().map(|n| n.as_bytes()).unwrap_or(b"");
2678    let gt_name = String::from_utf8_lossy(gt_name_bytes);
2679    let varname_bytes: &[u8] = ls.fs.as_ref()
2680        .and_then(|fs| {
2681            let vidx = gt.nactvar as i32;
2682            if (fs.firstlocal + vidx) >= 0 && ((fs.firstlocal + vidx) as usize) < ls.dyd.actvar.len() {
2683                let vd = get_local_var_desc(ls, fs, vidx);
2684                vd.name.as_ref().map(|n| n.as_bytes())
2685            } else {
2686                None
2687            }
2688        })
2689        .unwrap_or(b"");
2690    let varname = String::from_utf8_lossy(varname_bytes);
2691    LuaError::syntax(format_args!(
2692        "<goto {}> at line {} jumps into the scope of local '{}'", gt_name, line, varname
2693    ))
2694}
2695
2696/// C: static void solvegoto(LexState *ls, int g, Labeldesc *label)
2697/// Resolves goto at index `g` to `label`, removing it from pending list.
2698fn solvegoto(
2699    ls: &mut LexState,
2700    state: &mut LuaState,
2701    g: usize,
2702    label_pc: i32,
2703    label_nactvar: u8,
2704) -> Result<(), LuaError> {
2705    // C: if (l_unlikely(gt->nactvar < label->nactvar)) jumpscopeerror(ls, gt)
2706    if ls.dyd.gt[g].nactvar < label_nactvar {
2707        return Err(jumpscopeerror(ls, g));
2708    }
2709    let gt_pc = ls.dyd.gt[g].pc;
2710    cg_patch_list(ls.fs.as_mut().unwrap(), gt_pc, label_pc)?;
2711    ls.dyd.gt.remove(g);
2712    Ok(())
2713}
2714
2715/// C: static Labeldesc *findlabel(LexState *ls, TString *name)
2716/// Searches for an active label with the given name in the current function.
2717fn findlabel(ls: &LexState, name: &GcRef<LuaString>) -> Option<usize> {
2718    let first = ls.fs.as_ref().unwrap().firstlabel as usize;
2719    for i in first..ls.dyd.label.len() {
2720        let lb = &ls.dyd.label[i];
2721        if lb.name.as_ref().map_or(false, |n| GcRef::ptr_eq(n, name)) {
2722            return Some(i);
2723        }
2724    }
2725    None
2726}
2727
2728/// C: static int newlabelentry(LexState *ls, Labellist *l, TString *name, int line, int pc)
2729/// Adds a new label/goto entry; returns its index.
2730fn new_label_entry(
2731    ls: &mut LexState,
2732    state: &mut LuaState,
2733    is_goto: bool,
2734    name: GcRef<LuaString>,
2735    line: i32,
2736    pc: i32,
2737) -> Result<usize, LuaError> {
2738    let nactvar = ls.fs.as_ref().unwrap().nactvar;
2739    let entry = LabelDesc { name: Some(name), pc, line, nactvar, close: false };
2740    let list = if is_goto { &mut ls.dyd.gt } else { &mut ls.dyd.label };
2741    // C: luaM_growvector — Vec grows automatically
2742    let n = list.len();
2743    list.push(entry);
2744    Ok(n)
2745}
2746
2747/// C: static int newgotoentry(LexState *ls, TString *name, int line, int pc)
2748fn new_goto_entry(
2749    ls: &mut LexState,
2750    state: &mut LuaState,
2751    name: GcRef<LuaString>,
2752    line: i32,
2753    pc: i32,
2754) -> Result<usize, LuaError> {
2755    new_label_entry(ls, state, true, name, line, pc)
2756}
2757
2758/// C: static int solvegotos(LexState *ls, Labeldesc *lb)
2759/// Resolves all pending gotos that match label `lb`.
2760/// Returns true if any goto needed close.
2761fn solvegotos(ls: &mut LexState, state: &mut LuaState, lb_idx: usize) -> Result<bool, LuaError> {
2762    let lb_name = ls.dyd.label[lb_idx].name.clone();
2763    let lb_pc = ls.dyd.label[lb_idx].pc;
2764    let lb_nactvar = ls.dyd.label[lb_idx].nactvar;
2765    let first_goto = ls.fs.as_ref().unwrap().bl.as_ref().map_or(0, |b| b.firstgoto) as usize;
2766
2767    let mut i = first_goto;
2768    let mut needs_close = false;
2769    while i < ls.dyd.gt.len() {
2770        let gt_name = ls.dyd.gt[i].name.clone();
2771        let names_match = lb_name.as_ref().and_then(|ln| gt_name.as_ref().map(|gn| GcRef::ptr_eq(ln, gn))).unwrap_or(false);
2772        if names_match {
2773            needs_close |= ls.dyd.gt[i].close;
2774            // solvegoto removes element i, so don't increment i
2775            solvegoto(ls, state, i, lb_pc, lb_nactvar)?;
2776        } else {
2777            i += 1;
2778        }
2779    }
2780    Ok(needs_close)
2781}
2782
2783/// C: static int createlabel(LexState *ls, TString *name, int line, int last)
2784/// Creates a new label; resolves pending gotos. Returns true if CLOSE emitted.
2785fn createlabel(
2786    ls: &mut LexState,
2787    state: &mut LuaState,
2788    name: GcRef<LuaString>,
2789    line: i32,
2790    last: bool,
2791) -> Result<bool, LuaError> {
2792    let label_pc = cg_get_label(ls.fs.as_mut().unwrap());
2793    let l = new_label_entry(ls, state, false, name, line, label_pc)?;
2794    if last {
2795        // C: ll->arr[l].nactvar = fs->bl->nactvar
2796        let bl_nactvar = ls.fs.as_ref().unwrap().bl.as_ref().map_or(0, |b| b.nactvar);
2797        ls.dyd.label[l].nactvar = bl_nactvar;
2798    }
2799    let needs_close = solvegotos(ls, state, l)?;
2800    if needs_close {
2801        // C: luaK_codeABC(fs, OP_CLOSE, luaY_nvarstack(fs), 0, 0)
2802        let nstack = nvarstack(ls, ls.fs.as_ref().unwrap()) as u32;
2803        let inst = lua_code::opcodes::Instruction::abck(
2804            lua_code::opcodes::OpCode::Close,
2805            nstack,
2806            0,
2807            0,
2808            0,
2809        );
2810        emit_inst(ls.fs.as_mut().unwrap(), line, inst);
2811        return Ok(true);
2812    }
2813    Ok(false)
2814}
2815
2816/// C: static void movegotosout(FuncState *fs, BlockCnt *bl)
2817/// Adjusts pending gotos to outer block level when leaving a block.
2818fn movegotosout(ls: &mut LexState, bl_firstgoto: usize, bl_nactvar: u8, bl_upval: bool) {
2819    let fs = ls.fs.as_ref().unwrap();
2820    let first_goto = bl_firstgoto;
2821    let n_gt = ls.dyd.gt.len();
2822    drop(fs); // release borrow before iterating
2823
2824    for i in first_goto..ls.dyd.gt.len() {
2825        let gt_nactvar = ls.dyd.gt[i].nactvar;
2826        // C: if (reglevel(fs, gt->nactvar) > reglevel(fs, bl->nactvar)) gt->close |= bl->upval
2827        // TODO(port): compute reg_level properly using ls+fs
2828        if bl_upval {
2829            ls.dyd.gt[i].close = true;
2830        }
2831        ls.dyd.gt[i].nactvar = bl_nactvar;
2832    }
2833}
2834
2835/// C: static void enterblock(FuncState *fs, BlockCnt *bl, lu_byte isloop)
2836/// Pushes a new block scope onto fs->bl.
2837fn enter_block(ls: &mut LexState, isloop: bool) {
2838    let firstlabel = ls.dyd.label.len() as i32;
2839    let firstgoto = ls.dyd.gt.len() as i32;
2840    let insidetbc = ls.fs.as_ref()
2841        .and_then(|f| f.bl.as_ref())
2842        .map_or(false, |b| b.insidetbc);
2843    let fs = ls.fs.as_mut().unwrap();
2844    let nactvar = fs.nactvar;
2845    let new_bl = Box::new(BlockCnt {
2846        previous: fs.bl.take(),
2847        firstlabel,
2848        firstgoto,
2849        nactvar,
2850        upval: false,
2851        isloop,
2852        insidetbc,
2853    });
2854    fs.bl = Some(new_bl);
2855    debug_assert!(fs.freereg as i32 == {
2856        // TODO(port): nvarstack(ls, fs) -- circular borrow
2857        fs.freereg as i32 // placeholder assertion
2858    });
2859}
2860
2861/// C: static l_noret undefgoto(LexState *ls, Labeldesc *gt)
2862fn undef_goto(ls: &LexState, gt_idx: usize) -> LuaError {
2863    let gt = &ls.dyd.gt[gt_idx];
2864    let line = gt.line;
2865    let name_bytes: &[u8] = gt.name.as_ref().map(|n| n.as_bytes()).unwrap_or(b"");
2866    if name_bytes == b"break" {
2867        LuaError::syntax(format_args!("break outside loop at line {}", line))
2868    } else {
2869        let name_str = String::from_utf8_lossy(name_bytes);
2870        LuaError::syntax(format_args!("no visible label '{}' for <goto> at line {}", name_str, line))
2871    }
2872}
2873
2874/// C: static void leaveblock(FuncState *fs)
2875/// Pops the innermost block scope, emitting CLOSE if needed.
2876fn leave_block(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
2877    // Snapshot block fields without popping; createlabel below relies on
2878    // fs->bl still pointing at this (loop) block so solvegotos can read
2879    // fs->bl->firstgoto.
2880    let (bl_nactvar, bl_isloop, bl_upval, bl_firstgoto, bl_firstlabel) = {
2881        let bl = ls
2882            .fs
2883            .as_ref()
2884            .unwrap()
2885            .bl
2886            .as_ref()
2887            .expect("leave_block: no current block");
2888        (bl.nactvar, bl.isloop, bl.upval, bl.firstgoto, bl.firstlabel)
2889    };
2890
2891    let stklevel = reg_level(ls, ls.fs.as_ref().unwrap(), bl_nactvar as i32);
2892    let mut fs_box = ls.fs.take().unwrap();
2893    remove_vars(ls, &mut fs_box, bl_nactvar as i32);
2894    debug_assert!(bl_nactvar == fs_box.nactvar);
2895    ls.fs = Some(fs_box);
2896
2897    let hasclose = if bl_isloop {
2898        // C: createlabel(ls, luaS_newliteral(ls->L, "break"), 0, 0)
2899        let break_str = state.intern_str(b"break")?;
2900        createlabel(ls, state, break_str, 0, false)?
2901    } else {
2902        false
2903    };
2904
2905    // Now pop the block off fs.bl, restoring its previous link.
2906    let mut bl_box = ls.fs.as_mut().unwrap().bl.take().unwrap();
2907    let previous = bl_box.previous.take();
2908    ls.fs.as_mut().unwrap().bl = previous;
2909
2910    let has_prev_block = ls.fs.as_ref().unwrap().bl.is_some();
2911    if !hasclose && has_prev_block && bl_upval {
2912        // C: luaK_codeABC(fs, OP_CLOSE, stklevel, 0, 0)
2913        // Use `lastline` so the OP_CLOSE attributes to the block's terminating
2914        // token (END/UNTIL) rather than whatever the parser has peeked to next.
2915        // Mirrors lua-c's `savelineinfo(fs, f, fs->ls->lastline)`.
2916        let line = ls.lastline;
2917        let inst = lua_code::opcodes::Instruction::abck(
2918            lua_code::opcodes::OpCode::Close,
2919            stklevel as u32,
2920            0,
2921            0,
2922            0,
2923        );
2924        emit_inst(ls.fs.as_mut().unwrap(), line, inst);
2925    }
2926    ls.fs.as_mut().unwrap().freereg = stklevel as u8;
2927
2928    // C: ls->dyd->label.n = bl->firstlabel
2929    ls.dyd.label.truncate(bl_firstlabel as usize);
2930
2931    if has_prev_block {
2932        movegotosout(ls, bl_firstgoto as usize, bl_nactvar, bl_upval);
2933    } else {
2934        // C: if (bl->firstgoto < ls->dyd->gt.n) undefgoto(...)
2935        if (bl_firstgoto as usize) < ls.dyd.gt.len() {
2936            return Err(undef_goto(ls, bl_firstgoto as usize));
2937        }
2938    }
2939    Ok(())
2940}
2941
2942// ── §7 Proto management ──────────────────────────────────────────────────────
2943
2944/// C: static Proto *addprototype(LexState *ls)
2945/// Adds a new prototype slot to the current function's proto list.
2946/// Returns a mutable reference to the new prototype.
2947fn add_prototype(ls: &mut LexState, state: &mut LuaState) -> Result<Box<LuaProto>, LuaError> {
2948    // C: luaM_growvector(L, f->p, fs->np, f->sizep, Proto *, MAXARG_Bx, "functions")
2949    let np = ls.fs.as_ref().unwrap().np as usize;
2950    // C: f->p[fs->np++] = clp = luaF_newproto(L)
2951    // TODO(port): allocate via state.gc().new_proto() in Phase B
2952    let new_proto = Box::new(LuaProto::placeholder());
2953    while ls.fs.as_ref().unwrap().f.p.len() <= np {
2954        ls.fs
2955            .as_mut()
2956            .unwrap()
2957            .f
2958            .p
2959            .push(GcRef::new(LuaProto::placeholder()));
2960    }
2961    ls.fs.as_mut().unwrap().np += 1;
2962    // C: luaC_objbarrier(L, f, clp) — no-op in Phase A
2963    Ok(new_proto)
2964}
2965
2966/// C: static void codeclosure(LexState *ls, expdesc *v)
2967/// Emits OP_CLOSURE in the parent function and fixes up v.
2968fn codeclosure(ls: &mut LexState, _state: &mut LuaState, v: &mut ExprDesc) -> Result<(), LuaError> {
2969    let line = ls.lastline;
2970    let mut child = ls.fs.take().expect("codeclosure: no current FuncState");
2971    let result = (|| -> Result<(), LuaError> {
2972        let parent = child.prev.as_mut().expect(
2973            "codeclosure: child FuncState has no parent (called outside body()?)",
2974        );
2975        let bx = (parent.np - 1) as u32;
2976        let inst = lua_code::opcodes::Instruction::abx(
2977            lua_code::opcodes::OpCode::Closure,
2978            0,
2979            bx,
2980        );
2981        let pc = emit_inst(parent, line, inst);
2982        init_exp(v, ExprKind::Reloc, pc);
2983        cg_exp_to_next_reg(parent, line, v)
2984    })();
2985    ls.fs = Some(child);
2986    result
2987}
2988
2989/// C: static void open_func(LexState *ls, FuncState *fs, BlockCnt *bl)
2990/// Installs `new_fs` as the current FuncState, pushing old one as `prev`.
2991fn open_func(ls: &mut LexState, state: &mut LuaState, mut new_fs: FuncState) -> Result<(), LuaError> {
2992    // C: fs->prev = ls->fs; fs->ls = ls; ls->fs = fs;
2993    new_fs.prev = ls.fs.take();
2994
2995    let f = &mut new_fs.f;
2996    // C: fs->pc = 0; fs->previousline = f->linedefined; ...
2997    new_fs.pc = 0;
2998    new_fs.previousline = f.linedefined;
2999    new_fs.iwthabs = 0;
3000    new_fs.lasttarget = 0;
3001    new_fs.freereg = 0;
3002    new_fs.nk = 0;
3003    new_fs.nabslineinfo = 0;
3004    new_fs.np = 0;
3005    new_fs.nups = 0;
3006    new_fs.ndebugvars = 0;
3007    new_fs.nactvar = 0;
3008    new_fs.needclose = false;
3009
3010    // C: fs->firstlocal = ls->dyd->actvar.n
3011    new_fs.firstlocal = ls.dyd.actvar.len() as i32;
3012    // C: fs->firstlabel = ls->dyd->label.n
3013    new_fs.firstlabel = ls.dyd.label.len() as i32;
3014    new_fs.bl = None;
3015
3016    // C: f->source = ls->source; f->maxstacksize = 2
3017    new_fs.f.source = ls.source.clone();
3018    new_fs.f.maxstacksize = 2;
3019
3020    // C: luaC_objbarrier(ls->L, f, f->source) — no-op in Phase A
3021
3022    ls.fs = Some(Box::new(new_fs));
3023
3024    // C: enterblock(fs, bl, 0)
3025    enter_block(ls, false);
3026    Ok(())
3027}
3028
3029/// C: static void close_func(LexState *ls)
3030/// Finalizes and pops the current FuncState.
3031/// Returns the completed LuaProto.
3032fn close_func(ls: &mut LexState, state: &mut LuaState) -> Result<Box<LuaProto>, LuaError> {
3033    // C: luaK_ret(fs, luaY_nvarstack(fs), 0)
3034    {
3035        let first = {
3036            let fs = ls.fs.as_ref().unwrap();
3037            nvarstack(ls, fs)
3038        };
3039        let line = ls.lastline;
3040        let fs = ls.fs.as_mut().unwrap();
3041        let inst = lua_code::opcodes::Instruction::abck(
3042            lua_code::opcodes::OpCode::Return0,
3043            first as u32,
3044            1,
3045            0,
3046            0,
3047        );
3048        emit_inst(fs, line, inst);
3049    }
3050    // C: leaveblock(fs)
3051    leave_block(ls, state)?;
3052    debug_assert!(ls.fs.as_ref().unwrap().bl.is_none());
3053
3054    // C: luaK_finish(fs) — patch OP_RETURN/RETURN0/RETURN1/TAILCALL for vararg
3055    //                     and needclose, and resolve JMP chains to final target.
3056    cg_finish(ls.fs.as_mut().unwrap());
3057
3058    // C: luaM_shrinkvector — truncate arrays to actual used size
3059    {
3060        let fs = ls.fs.as_mut().unwrap();
3061        let pc = fs.pc as usize;
3062        let nabslineinfo = fs.nabslineinfo as usize;
3063        let nk = fs.nk as usize;
3064        let np = fs.np as usize;
3065        let ndebugvars = fs.ndebugvars as usize;
3066        let nups = fs.nups as usize;
3067        fs.f.code.truncate(pc);
3068        fs.f.lineinfo.truncate(pc);
3069        fs.f.abslineinfo.truncate(nabslineinfo);
3070        fs.f.k.truncate(nk);
3071        fs.f.p.truncate(np);
3072        fs.f.locvars.truncate(ndebugvars);
3073        fs.f.upvalues.truncate(nups);
3074    }
3075
3076    // C: ls->fs = fs->prev
3077    let mut fs_box = ls.fs.take().unwrap();
3078    ls.fs = fs_box.prev.take();
3079
3080    // C: luaC_checkGC(L) — no-op in Phase A
3081    Ok(fs_box.f)
3082}
3083
3084// ── §8 Grammar rules — block / statement lists ───────────────────────────────
3085
3086/// C: static int block_follow(LexState *ls, int withuntil)
3087/// Returns true if the current token can end a block.
3088fn block_follow(ls: &LexState, withuntil: bool) -> bool {
3089    match ls.t.token {
3090        TK_ELSE | TK_ELSEIF | TK_END | TK_EOS => true,
3091        TK_UNTIL => withuntil,
3092        _ => false,
3093    }
3094}
3095
3096/// C: static void statlist(LexState *ls)
3097fn statlist(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
3098    // C: while (!block_follow(ls, 1)) { if (TK_RETURN) { statement; return; } statement; }
3099    while !block_follow(ls, true) {
3100        if ls.t.token == TK_RETURN {
3101            statement(ls, state)?;
3102            return Ok(());
3103        }
3104        statement(ls, state)?;
3105    }
3106    Ok(())
3107}
3108
3109/// C: static void fieldsel(LexState *ls, expdesc *v)
3110/// Handles '.' NAME or ':' NAME field selection.
3111fn fieldsel(ls: &mut LexState, state: &mut LuaState, v: &mut ExprDesc) -> Result<(), LuaError> {
3112    // C: luaK_exp2anyregup(fs, v); luaX_next(ls); codename(ls, &key); luaK_indexed(fs, v, &key)
3113    let line = ls.lastline;
3114    cg_exp_to_any_reg_up(ls.fs.as_mut().unwrap(), line, v)?;
3115    lex_next(ls, state)?; // skip '.' or ':'
3116    let mut key = ExprDesc::default();
3117    codename(ls, state, &mut key)?;
3118    cg_indexed(ls.fs.as_mut().unwrap(), line, v, &mut key)?;
3119    Ok(())
3120}
3121
3122/// C: static void yindex(LexState *ls, expdesc *v)
3123/// Handles '[' expr ']' indexing.
3124fn yindex(ls: &mut LexState, state: &mut LuaState, v: &mut ExprDesc) -> Result<(), LuaError> {
3125    // C: luaX_next(ls); expr(ls, v); luaK_exp2val(ls->fs, v); checknext(ls, ']')
3126    lex_next(ls, state)?;
3127    expr(ls, state, v)?;
3128    // TODO(port): lua_code::exp_to_val(ls.fs.as_mut().unwrap(), v)?;
3129    check_next(ls, state, b']' as TokenKind)?;
3130    Ok(())
3131}
3132
3133// ── §9 Constructor rules ─────────────────────────────────────────────────────
3134
3135/// C: static void recfield(LexState *ls, ConsControl *cc)
3136fn recfield(ls: &mut LexState, state: &mut LuaState, cc: &mut ConsControl) -> Result<(), LuaError> {
3137    let reg = ls.fs.as_ref().unwrap().freereg as i32;
3138    let mut key = ExprDesc::default();
3139    let mut val = ExprDesc::default();
3140    if ls.t.token == TK_NAME {
3141        // C: checklimit(fs, cc->nh, MAX_INT, "items in a constructor")
3142        let fs = ls.fs.as_ref().unwrap();
3143        check_limit(fs, cc.nh, i32::MAX, "items in a constructor")?;
3144        codename(ls, state, &mut key)?;
3145    } else {
3146        // C: yindex(ls, &key)
3147        yindex(ls, state, &mut key)?;
3148    }
3149    cc.nh += 1;
3150    check_next(ls, state, b'=' as TokenKind)?;
3151    let mut tab = cc.t.clone();
3152    let line = ls.lastline;
3153    cg_indexed(ls.fs.as_mut().unwrap(), line, &mut tab, &mut key)?;
3154    expr(ls, state, &mut val)?;
3155    cg_storevar(ls.fs.as_mut().unwrap(), line, &tab, &mut val)?;
3156    ls.fs.as_mut().unwrap().freereg = reg as u8;
3157    Ok(())
3158}
3159
3160/// C: static void closelistfield(FuncState *fs, ConsControl *cc)
3161fn closelistfield(ls: &mut LexState, state: &mut LuaState, cc: &mut ConsControl) -> Result<(), LuaError> {
3162    let _ = state;
3163    if cc.v.k == ExprKind::Void {
3164        return Ok(()); // C: if (cc->v.k == VVOID) return;
3165    }
3166    let line = ls.lastline;
3167    cg_exp_to_next_reg(ls.fs.as_mut().unwrap(), line, &mut cc.v)?;
3168    cc.v.k = ExprKind::Void;
3169    if cc.tostore == LFIELDS_PER_FLUSH {
3170        let t_info = cc.t.u.info;
3171        cg_setlist(ls.fs.as_mut().unwrap(), line, t_info, cc.na, cc.tostore);
3172        cc.na += cc.tostore;
3173        cc.tostore = 0;
3174    }
3175    Ok(())
3176}
3177
3178/// C: static void lastlistfield(FuncState *fs, ConsControl *cc)
3179fn lastlistfield(ls: &mut LexState, state: &mut LuaState, cc: &mut ConsControl) -> Result<(), LuaError> {
3180    let _ = state;
3181    if cc.tostore == 0 {
3182        return Ok(());
3183    }
3184    let t_info = cc.t.u.info;
3185    let line = ls.lastline;
3186    if cc.v.k.has_mult_ret() {
3187        cg_set_returns(ls.fs.as_mut().unwrap(), &mut cc.v, LUA_MULTRET);
3188        cg_setlist(ls.fs.as_mut().unwrap(), line, t_info, cc.na, LUA_MULTRET);
3189        cc.na -= 1;
3190    } else {
3191        if cc.v.k != ExprKind::Void {
3192            cg_exp_to_next_reg(ls.fs.as_mut().unwrap(), line, &mut cc.v)?;
3193        }
3194        cg_setlist(ls.fs.as_mut().unwrap(), line, t_info, cc.na, cc.tostore);
3195    }
3196    cc.na += cc.tostore;
3197    Ok(())
3198}
3199
3200/// C: static void listfield(LexState *ls, ConsControl *cc)
3201fn listfield(ls: &mut LexState, state: &mut LuaState, cc: &mut ConsControl) -> Result<(), LuaError> {
3202    expr(ls, state, &mut cc.v)?;
3203    cc.tostore += 1;
3204    Ok(())
3205}
3206
3207/// C: static void field(LexState *ls, ConsControl *cc)
3208fn field(ls: &mut LexState, state: &mut LuaState, cc: &mut ConsControl) -> Result<(), LuaError> {
3209    match ls.t.token {
3210        TK_NAME => {
3211            // C: if (luaX_lookahead(ls) != '=') listfield else recfield
3212            let next_is_eq = lex_lookahead(ls, state)? == b'=' as TokenKind;
3213            if !next_is_eq {
3214                listfield(ls, state, cc)?;
3215            } else {
3216                recfield(ls, state, cc)?;
3217            }
3218        }
3219        c if c == b'[' as TokenKind => {
3220            recfield(ls, state, cc)?;
3221        }
3222        _ => {
3223            listfield(ls, state, cc)?;
3224        }
3225    }
3226    Ok(())
3227}
3228
3229/// C: static void constructor(LexState *ls, expdesc *t)
3230fn constructor(ls: &mut LexState, state: &mut LuaState, t: &mut ExprDesc) -> Result<(), LuaError> {
3231    let line = ls.lastline;
3232    let pc = cg_emit_newtable(ls.fs.as_mut().unwrap(), line);
3233
3234    let freereg = ls.fs.as_ref().unwrap().freereg as i32;
3235    init_exp(t, ExprKind::NonReloc, freereg);
3236    reserve_regs(ls.fs.as_mut().unwrap(), 1)?;
3237
3238    let mut cc = ConsControl {
3239        v: ExprDesc::default(),
3240        t: t.clone(),
3241        nh: 0,
3242        na: 0,
3243        tostore: 0,
3244    };
3245
3246    check_next(ls, state, b'{' as TokenKind)?;
3247    loop {
3248        debug_assert!(cc.v.k == ExprKind::Void || cc.tostore > 0);
3249        if ls.t.token == b'}' as TokenKind {
3250            break;
3251        }
3252        closelistfield(ls, state, &mut cc)?;
3253        field(ls, state, &mut cc)?;
3254        if !test_next(ls, state, b',' as TokenKind)?
3255            && !test_next(ls, state, b';' as TokenKind)?
3256        {
3257            break;
3258        }
3259    }
3260    check_match(ls, state, b'}' as TokenKind, b'{' as TokenKind, line)?;
3261    lastlistfield(ls, state, &mut cc)?;
3262
3263    let t_info = t.u.info;
3264    cg_settablesize(ls.fs.as_mut().unwrap(), pc, t_info, cc.na, cc.nh);
3265    Ok(())
3266}
3267
3268// ── §10 Parameter list and function body ─────────────────────────────────────
3269
3270/// C: static void setvararg(FuncState *fs, int nparams)
3271fn setvararg(fs: &mut FuncState, _state: &mut LuaState, nparams: i32) -> Result<(), LuaError> {
3272    fs.f.is_vararg = true;
3273    // C: luaK_codeABC(fs, OP_VARARGPREP, nparams, 0, 0)
3274    let inst = lua_code::opcodes::Instruction::abck(
3275        lua_code::opcodes::OpCode::VarArgPrep,
3276        nparams as u32,
3277        0, 0, 0,
3278    );
3279    let line = fs.previousline;
3280    emit_inst(fs, line, inst);
3281    Ok(())
3282}
3283
3284/// C: static void parlist(LexState *ls)
3285fn parlist(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
3286    let mut nparams: i32 = 0;
3287    let mut isvararg = false;
3288    if ls.t.token != b')' as TokenKind {
3289        loop {
3290            match ls.t.token {
3291                TK_NAME => {
3292                    let name = str_check_name(ls, state)?;
3293                    new_local_var(ls, state, name)?;
3294                    nparams += 1;
3295                }
3296                TK_DOTS => {
3297                    lex_next(ls, state)?;
3298                    isvararg = true;
3299                }
3300                _ => {
3301                    return Err(LuaError::syntax(format_args!("<name> or '...' expected")));
3302                }
3303            }
3304            if isvararg || !test_next(ls, state, b',' as TokenKind)? {
3305                break;
3306            }
3307        }
3308    }
3309    adjust_local_vars(ls, state, nparams)?;
3310    let numparams = ls.fs.as_ref().unwrap().nactvar;
3311    ls.fs.as_mut().unwrap().f.numparams = numparams;
3312    if isvararg {
3313        setvararg(ls.fs.as_mut().unwrap(), state, numparams as i32)?;
3314    }
3315    // C: luaK_reserveregs(fs, fs->nactvar)
3316    let nactvar = ls.fs.as_ref().unwrap().nactvar as i32;
3317    reserve_regs(ls.fs.as_mut().unwrap(), nactvar)?;
3318    Ok(())
3319}
3320
3321/// C: static void check_match(LexState *ls, int what, int who, int where)
3322fn check_match(
3323    ls: &mut LexState,
3324    state: &mut LuaState,
3325    what: TokenKind,
3326    who: TokenKind,
3327    where_line: i32,
3328) -> Result<(), LuaError> {
3329    // C: if (l_unlikely(!testnext(ls, what)))
3330    if !test_next(ls, state, what)? {
3331        if where_line == ls.linenumber {
3332            return Err(error_expected(ls, what));
3333        } else {
3334            // C: luaX_syntaxerror(ls, luaO_pushfstring(..., "%s expected (to close %s at line %d)", ...))
3335            let what_str = lua_lex::token2str(&ls.lex, what);
3336            let who_str = lua_lex::token2str(&ls.lex, who);
3337            let mut msg: Vec<u8> = Vec::new();
3338            msg.extend_from_slice(&what_str);
3339            msg.extend_from_slice(b" expected (to close ");
3340            msg.extend_from_slice(&who_str);
3341            use std::io::Write as _;
3342            let _ = write!(msg, " at line {})", where_line);
3343            return Err(lua_lex::syntax_error(&mut ls.lex, &msg));
3344        }
3345    }
3346    Ok(())
3347}
3348
3349/// C: static void body(LexState *ls, expdesc *e, int ismethod, int line)
3350fn body(
3351    ls: &mut LexState,
3352    state: &mut LuaState,
3353    e: &mut ExprDesc,
3354    ismethod: bool,
3355    line: i32,
3356) -> Result<(), LuaError> {
3357    // C: FuncState new_fs; BlockCnt bl; new_fs.f = addprototype(ls); new_fs.f->linedefined = line
3358    let new_proto = add_prototype(ls, state)?;
3359    let mut new_fs = FuncState {
3360        f: new_proto,
3361        prev: None,
3362        bl: None,
3363        pc: 0,
3364        lasttarget: 0,
3365        previousline: line,
3366        nk: 0,
3367        np: 0,
3368        nabslineinfo: 0,
3369        firstlocal: 0,
3370        firstlabel: 0,
3371        ndebugvars: 0,
3372        nactvar: 0,
3373        nups: 0,
3374        freereg: 0,
3375        iwthabs: 0,
3376        needclose: false,
3377        last_token_line: ls.lastline,
3378    };
3379    new_fs.f.linedefined = line;
3380    open_func(ls, state, new_fs)?;
3381
3382    check_next(ls, state, b'(' as TokenKind)?;
3383    if ismethod {
3384        let self_str = state.intern_str(b"self")?;
3385        new_local_var(ls, state, self_str)?;
3386        adjust_local_vars(ls, state, 1)?;
3387    }
3388    parlist(ls, state)?;
3389    check_next(ls, state, b')' as TokenKind)?;
3390    statlist(ls, state)?;
3391    ls.fs.as_mut().unwrap().f.lastlinedefined = ls.linenumber;
3392    check_match(ls, state, TK_END, TK_FUNCTION, line)?;
3393    codeclosure(ls, state, e)?;
3394    let inner_proto = close_func(ls, state)?;
3395    let parent = ls.fs.as_mut().expect("body: close_func left no parent FuncState");
3396    let slot = (parent.np - 1) as usize;
3397    if parent.f.p.len() <= slot {
3398        parent.f.p.resize_with(slot + 1, || GcRef::new(LuaProto::placeholder()));
3399    }
3400    parent.f.p[slot] = GcRef::new(*inner_proto);
3401    Ok(())
3402}
3403
3404// ── §11 Expression list and function arguments ────────────────────────────────
3405
3406/// C: static int explist(LexState *ls, expdesc *v)
3407fn explist(ls: &mut LexState, state: &mut LuaState, v: &mut ExprDesc) -> Result<i32, LuaError> {
3408    let mut n = 1;
3409    expr(ls, state, v)?;
3410    while test_next(ls, state, b',' as TokenKind)? {
3411        let line = ls.lastline;
3412        cg_exp_to_next_reg(ls.fs.as_mut().unwrap(), line, v)?;
3413        expr(ls, state, v)?;
3414        n += 1;
3415    }
3416    Ok(n)
3417}
3418
3419/// C: static void funcargs(LexState *ls, expdesc *f)
3420fn funcargs(ls: &mut LexState, state: &mut LuaState, f: &mut ExprDesc) -> Result<(), LuaError> {
3421    let mut args = ExprDesc::default();
3422    // C: int line = ls->linenumber;  — line of `(` (or `{` / TK_STRING), captured
3423    // BEFORE consuming, so the OP_CALL/etc emissions attribute to the call site.
3424    // errors.lua tests `a\n(\n23)` expects error at line of `(`, not line of `a`.
3425    let line = ls.linenumber;
3426    match ls.t.token {
3427        c if c == b'(' as TokenKind => {
3428            lex_next(ls, state)?; // skip '('
3429            if ls.t.token == b')' as TokenKind {
3430                args.k = ExprKind::Void;
3431            } else {
3432                explist(ls, state, &mut args)?;
3433                if args.k.has_mult_ret() {
3434                    // C: luaK_setmultret(fs, &args) — patch the trailing
3435                    // Call/VarArg to produce LUA_MULTRET so all of its return
3436                    // values become arguments to the enclosing call.
3437                    cg_set_returns(ls.fs.as_mut().unwrap(), &mut args, LUA_MULTRET);
3438                }
3439            }
3440            check_match(ls, state, b')' as TokenKind, b'(' as TokenKind, line)?;
3441        }
3442        c if c == b'{' as TokenKind => {
3443            constructor(ls, state, &mut args)?;
3444        }
3445        TK_STRING => {
3446            let s = ls.t.seminfo.ts.clone()
3447                .ok_or_else(|| LuaError::syntax(format_args!("string expected")))?;
3448            codestring(&mut args, s);
3449            lex_next(ls, state)?;
3450        }
3451        _ => {
3452            return Err(LuaError::syntax(format_args!("function arguments expected")));
3453        }
3454    }
3455    debug_assert!(f.k == ExprKind::NonReloc);
3456    let base = f.u.info;
3457    let nparams: i32 = if args.k.has_mult_ret() {
3458        // TODO(port): luaK_setmultret for VVarArg / VCall args; only single
3459        // non-multret args are supported by the bootstrap codegen.
3460        LUA_MULTRET
3461    } else {
3462        if args.k != ExprKind::Void {
3463            cg_exp_to_next_reg(ls.fs.as_mut().unwrap(), line, &mut args)?;
3464        }
3465        ls.fs.as_ref().unwrap().freereg as i32 - (base + 1)
3466    };
3467    // C: init_exp(f, VCALL, luaK_codeABC(fs, OP_CALL, base, nparams+1, 2))
3468    let call_inst = lua_code::opcodes::Instruction::abck(
3469        lua_code::opcodes::OpCode::Call,
3470        base as u32,
3471        (nparams + 1) as u32,
3472        2,
3473        0,
3474    );
3475    let call_pc = emit_inst(ls.fs.as_mut().unwrap(), line, call_inst);
3476    init_exp(f, ExprKind::Call, call_pc);
3477    ls.fs.as_mut().unwrap().freereg = base as u8 + 1;
3478    Ok(())
3479}
3480
3481// ── §12 Expression parsing ────────────────────────────────────────────────────
3482
3483/// C: static void primaryexp(LexState *ls, expdesc *v)
3484fn primaryexp(ls: &mut LexState, state: &mut LuaState, v: &mut ExprDesc) -> Result<(), LuaError> {
3485    match ls.t.token {
3486        c if c == b'(' as TokenKind => {
3487            let line = ls.lastline;
3488            lex_next(ls, state)?;
3489            expr(ls, state, v)?;
3490            check_match(ls, state, b')' as TokenKind, b'(' as TokenKind, line)?;
3491            cg_discharge_vars(ls.fs.as_mut().unwrap(), line, v)?;
3492        }
3493        TK_NAME => {
3494            singlevar(ls, state, v)?;
3495        }
3496        _ => {
3497            return Err(lua_lex::syntax_error(&mut ls.lex, b"unexpected symbol"));
3498        }
3499    }
3500    Ok(())
3501}
3502
3503/// C: static void suffixedexp(LexState *ls, expdesc *v)
3504fn suffixedexp(ls: &mut LexState, state: &mut LuaState, v: &mut ExprDesc) -> Result<(), LuaError> {
3505    primaryexp(ls, state, v)?;
3506    loop {
3507        match ls.t.token {
3508            c if c == b'.' as TokenKind => {
3509                fieldsel(ls, state, v)?;
3510            }
3511            c if c == b'[' as TokenKind => {
3512                let mut key = ExprDesc::default();
3513                let line = ls.lastline;
3514                cg_exp_to_any_reg_up(ls.fs.as_mut().unwrap(), line, v)?;
3515                yindex(ls, state, &mut key)?;
3516                cg_indexed(ls.fs.as_mut().unwrap(), line, v, &mut key)?;
3517            }
3518            c if c == b':' as TokenKind => {
3519                let mut key = ExprDesc::default();
3520                lex_next(ls, state)?;
3521                codename(ls, state, &mut key)?;
3522                let line = ls.lastline;
3523                cg_self(ls.fs.as_mut().unwrap(), line, v, &mut key)?;
3524                funcargs(ls, state, v)?;
3525            }
3526            c if c == b'(' as TokenKind || c == TK_STRING || c == b'{' as TokenKind => {
3527                // C: luaK_exp2nextreg(fs, v) — places the callee in a fixed register.
3528                let line = ls.lastline;
3529                cg_exp_to_next_reg(ls.fs.as_mut().unwrap(), line, v)?;
3530                funcargs(ls, state, v)?;
3531            }
3532            _ => return Ok(()),
3533        }
3534    }
3535}
3536
3537/// C: static void simpleexp(LexState *ls, expdesc *v)
3538fn simpleexp(ls: &mut LexState, state: &mut LuaState, v: &mut ExprDesc) -> Result<(), LuaError> {
3539    match ls.t.token {
3540        TK_FLT => {
3541            init_exp(v, ExprKind::KFlt, 0);
3542            v.u.nval = ls.t.seminfo.r;
3543        }
3544        TK_INT => {
3545            init_exp(v, ExprKind::KInt, 0);
3546            v.u.ival = ls.t.seminfo.i;
3547        }
3548        TK_STRING => {
3549            let s = ls.t.seminfo.ts.clone()
3550                .ok_or_else(|| LuaError::syntax(format_args!("string value missing")))?;
3551            codestring(v, s);
3552        }
3553        TK_NIL => {
3554            init_exp(v, ExprKind::Nil, 0);
3555        }
3556        TK_TRUE => {
3557            init_exp(v, ExprKind::True, 0);
3558        }
3559        TK_FALSE => {
3560            init_exp(v, ExprKind::False, 0);
3561        }
3562        TK_DOTS => {
3563            // C: check_condition(ls, fs->f->is_vararg, "cannot use '...' outside a vararg function")
3564            let is_vararg = ls.fs.as_ref().unwrap().f.is_vararg;
3565            if !is_vararg {
3566                return Err(LuaError::syntax(format_args!(
3567                    "cannot use '...' outside a vararg function"
3568                )));
3569            }
3570            // C: init_exp(v, VVARARG, luaK_codeABC(fs, OP_VARARG, 0, 0, 1))
3571            let line = ls.lastline;
3572            let inst = lua_code::opcodes::Instruction::abck(
3573                lua_code::opcodes::OpCode::VarArg,
3574                0,
3575                0,
3576                1,
3577                0,
3578            );
3579            let pc = emit_inst(ls.fs.as_mut().unwrap(), line, inst);
3580            init_exp(v, ExprKind::VarArg, pc);
3581        }
3582        c if c == b'{' as TokenKind => {
3583            constructor(ls, state, v)?;
3584            return Ok(()); // C: return (no luaX_next)
3585        }
3586        TK_FUNCTION => {
3587            lex_next(ls, state)?;
3588            let line = ls.lastline;
3589            body(ls, state, v, false, line)?;
3590            return Ok(()); // C: return (no luaX_next)
3591        }
3592        _ => {
3593            suffixedexp(ls, state, v)?;
3594            return Ok(()); // C: return (no luaX_next)
3595        }
3596    }
3597    // C: luaX_next(ls) — for the simple literal cases
3598    lex_next(ls, state)?;
3599    Ok(())
3600}
3601
3602/// C: static UnOpr getunopr(int op)
3603fn getunopr(op: TokenKind) -> UnOpr {
3604    match op {
3605        TK_NOT => UnOpr::Not,
3606        c if c == b'-' as TokenKind => UnOpr::Minus,
3607        c if c == b'~' as TokenKind => UnOpr::BNot,
3608        c if c == b'#' as TokenKind => UnOpr::Len,
3609        _ => UnOpr::NoUnOpr,
3610    }
3611}
3612
3613/// C: static BinOpr getbinopr(int op)
3614fn getbinopr(op: TokenKind) -> BinOpr {
3615    match op {
3616        c if c == b'+' as TokenKind => BinOpr::Add,
3617        c if c == b'-' as TokenKind => BinOpr::Sub,
3618        c if c == b'*' as TokenKind => BinOpr::Mul,
3619        c if c == b'%' as TokenKind => BinOpr::Mod,
3620        c if c == b'^' as TokenKind => BinOpr::Pow,
3621        c if c == b'/' as TokenKind => BinOpr::Div,
3622        TK_IDIV => BinOpr::IDiv,
3623        c if c == b'&' as TokenKind => BinOpr::BAnd,
3624        c if c == b'|' as TokenKind => BinOpr::BOr,
3625        c if c == b'~' as TokenKind => BinOpr::BXor,
3626        TK_SHL => BinOpr::Shl,
3627        TK_SHR => BinOpr::Shr,
3628        TK_CONCAT => BinOpr::Concat,
3629        TK_NE => BinOpr::Ne,
3630        TK_EQ => BinOpr::Eq,
3631        c if c == b'<' as TokenKind => BinOpr::Lt,
3632        TK_LE => BinOpr::Le,
3633        c if c == b'>' as TokenKind => BinOpr::Gt,
3634        TK_GE => BinOpr::Ge,
3635        TK_AND => BinOpr::And,
3636        TK_OR => BinOpr::Or,
3637        _ => BinOpr::NoBinOpr,
3638    }
3639}
3640
3641/// C: static BinOpr subexpr(LexState *ls, expdesc *v, int limit)
3642/// Parses a sub-expression with operators of priority > `limit`.
3643/// Returns the first untreated (lower-priority) operator.
3644fn subexpr(
3645    ls: &mut LexState,
3646    state: &mut LuaState,
3647    v: &mut ExprDesc,
3648    limit: i32,
3649) -> Result<BinOpr, LuaError> {
3650    // C: enterlevel(ls) — luaE_incCstack(ls->L)
3651    enter_level(ls)?;
3652
3653    let uop = getunopr(ls.t.token);
3654    if uop != UnOpr::NoUnOpr {
3655        // C: int line = ls->linenumber;  — captured BEFORE consuming the op
3656        // so this is the operator's own line, not the prior token's.
3657        let line = ls.linenumber;
3658        lex_next(ls, state)?; // skip unary operator
3659        subexpr(ls, state, v, UNARY_PRIORITY)?;
3660        // C: luaK_prefix(ls->fs, uop, v, line)
3661        cg_prefix(ls.fs.as_mut().unwrap(), uop, v, line)?;
3662    } else {
3663        simpleexp(ls, state, v)?;
3664    }
3665
3666    let mut op = getbinopr(ls.t.token);
3667    while op != BinOpr::NoBinOpr && PRIORITY[op as usize].0 as i32 > limit {
3668        let mut v2 = ExprDesc::default();
3669        // C: int line = ls->linenumber;  — operator's line, captured BEFORE consuming.
3670        // errors.lua's `lineerror` cases check that runtime arith errors are
3671        // attributed to the operator's line, not the operand's.
3672        let line = ls.linenumber;
3673        lex_next(ls, state)?;
3674        cg_infix(ls.fs.as_mut().unwrap(), op, v, line)?;
3675        let nextop = subexpr(ls, state, &mut v2, PRIORITY[op as usize].1 as i32)?;
3676        cg_posfix_fold(ls.fs.as_mut().unwrap(), op, v, &mut v2, line)?;
3677        op = nextop;
3678    }
3679
3680    // C: leavelevel(ls) — L->nCcalls--
3681    leave_level(ls);
3682    Ok(op)
3683}
3684
3685/// C: static void expr(LexState *ls, expdesc *v)
3686fn expr(ls: &mut LexState, state: &mut LuaState, v: &mut ExprDesc) -> Result<(), LuaError> {
3687    subexpr(ls, state, v, 0)?;
3688    Ok(())
3689}
3690
3691// ── §13 Statement rules ───────────────────────────────────────────────────────
3692
3693/// C: static void block(LexState *ls)
3694fn block(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
3695    enter_block(ls, false);
3696    statlist(ls, state)?;
3697    leave_block(ls, state)?;
3698    Ok(())
3699}
3700
3701/// C: static void check_conflict(LexState *ls, struct LHS_assign *lh, expdesc *v)
3702/// Checks and fixes register/upvalue conflicts in multi-assignment.
3703///
3704/// When a non-indexed LHS variable `v` also appears as the table or key in an
3705/// indexed LHS variable, the indexed entry must be redirected to a copy made
3706/// before any assignments occur. For an upvalue table that becomes a register
3707/// copy, the ExprKind is changed from IndexUp to IndexStr so cg_storevar emits
3708/// SETFIELD (register table) instead of SETTABUP (upvalue table).
3709fn check_conflict(
3710    ls: &mut LexState,
3711    _state: &mut LuaState,
3712    lh: &mut LhsAssign,
3713    v: &ExprDesc,
3714) -> Result<(), LuaError> {
3715    let extra = ls.fs.as_ref().unwrap().freereg as i32;
3716    let line = ls.lastline;
3717    let mut conflict = false;
3718
3719    conflict |= check_one_lhs_entry(&mut lh.v, v, extra);
3720    let mut prev = lh.prev.as_deref_mut();
3721    while let Some(node) = prev {
3722        conflict |= check_one_lhs_entry(&mut node.v, v, extra);
3723        prev = node.prev.as_deref_mut();
3724    }
3725
3726    if conflict {
3727        let fs = ls.fs.as_mut().unwrap();
3728        let inst = if v.k == ExprKind::Local {
3729            lua_code::opcodes::Instruction::abck(
3730                lua_code::opcodes::OpCode::Move,
3731                extra as u32, v.u.var_ridx as u32, 0, 0,
3732            )
3733        } else {
3734            lua_code::opcodes::Instruction::abck(
3735                lua_code::opcodes::OpCode::GetUpVal,
3736                extra as u32, v.u.info as u32, 0, 0,
3737            )
3738        };
3739        emit_inst(fs, line, inst);
3740        reserve_regs(fs, 1)?;
3741    }
3742    Ok(())
3743}
3744
3745fn check_one_lhs_entry(entry: &mut ExprDesc, v: &ExprDesc, extra: i32) -> bool {
3746    if !entry.k.is_indexed() {
3747        return false;
3748    }
3749    let mut found = false;
3750    if entry.k == ExprKind::IndexUp {
3751        if v.k == ExprKind::UpVal && entry.u.ind_t == v.u.info as u8 {
3752            found = true;
3753            entry.k = ExprKind::IndexStr;
3754            entry.u.ind_t = extra as u8;
3755        }
3756    } else {
3757        if v.k == ExprKind::Local && entry.u.ind_t == v.u.var_ridx {
3758            found = true;
3759            entry.u.ind_t = extra as u8;
3760        }
3761        if entry.k == ExprKind::Indexed
3762            && v.k == ExprKind::Local
3763            && entry.u.ind_idx == v.u.var_ridx as i16
3764        {
3765            found = true;
3766            entry.u.ind_idx = extra as i16;
3767        }
3768    }
3769    found
3770}
3771
3772/// C: static void restassign(LexState *ls, struct LHS_assign *lh, int nvars)
3773fn restassign(
3774    ls: &mut LexState,
3775    state: &mut LuaState,
3776    lh: &mut LhsAssign,
3777    nvars: i32,
3778) -> Result<(), LuaError> {
3779    // C: check_condition(ls, vkisvar(lh->v.k), "syntax error")
3780    if !lh.v.k.is_var() {
3781        return Err(lua_lex::syntax_error(&mut ls.lex, b"syntax error"));
3782    }
3783    check_readonly(ls, state, &lh.v.clone())?;
3784
3785    if test_next(ls, state, b',' as TokenKind)? {
3786        // C: restassign -> ',' suffixedexp restassign
3787        let mut nv_assign = LhsAssign {
3788            prev: None, // We don't link here — Phase B restructures
3789            v: ExprDesc::default(),
3790        };
3791        suffixedexp(ls, state, &mut nv_assign.v)?;
3792        if !nv_assign.v.k.is_indexed() {
3793            check_conflict(ls, state, lh, &nv_assign.v.clone())?;
3794        }
3795        // C: enterlevel(ls)
3796        enter_level(ls)?;
3797        restassign(ls, state, &mut nv_assign, nvars + 1)?;
3798        // C: leavelevel(ls)
3799        leave_level(ls);
3800    } else {
3801        // C: restassign -> '=' explist
3802        let mut e = ExprDesc::default();
3803        check_next(ls, state, b'=' as TokenKind)?;
3804        let nexps = explist(ls, state, &mut e)?;
3805        if nexps != nvars {
3806            adjust_assign(ls, state, nvars, nexps, &mut e)?;
3807        } else {
3808            let line = ls.lastline;
3809            let fs = ls.fs.as_mut().unwrap();
3810            cg_set_one_ret(fs, &mut e);
3811            cg_storevar(fs, line, &lh.v, &mut e)?;
3812            return Ok(());
3813        }
3814    }
3815    let line = ls.lastline;
3816    let fs = ls.fs.as_mut().unwrap();
3817    let freereg = fs.freereg as i32 - 1;
3818    let mut e = ExprDesc::default();
3819    init_exp(&mut e, ExprKind::NonReloc, freereg);
3820    cg_storevar(fs, line, &lh.v, &mut e)?;
3821    Ok(())
3822}
3823
3824/// C: static int cond(LexState *ls)
3825/// Parses a condition expression; returns its 'exit when false' patch list.
3826fn cond(ls: &mut LexState, state: &mut LuaState) -> Result<i32, LuaError> {
3827    let mut v = ExprDesc::default();
3828    expr(ls, state, &mut v)?;
3829    if v.k == ExprKind::Nil {
3830        v.k = ExprKind::False; // C: 'falses' are all equal here
3831    }
3832    let line = ls.lastline;
3833    cg_go_if_true(ls.fs.as_mut().unwrap(), line, &mut v)?;
3834    Ok(v.f)
3835}
3836
3837/// C: static void gotostat(LexState *ls)
3838fn gotostat(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
3839    let line = ls.lastline;
3840    let name = str_check_name(ls, state)?;
3841    let lb = findlabel(ls, &name);
3842    if lb.is_none() {
3843        let pc = cg_jump(ls.fs.as_mut().unwrap(), line);
3844        new_goto_entry(ls, state, name, line, pc)?;
3845    } else {
3846        let lb_idx = lb.unwrap();
3847        let lb_pc = ls.dyd.label[lb_idx].pc;
3848        let lb_nactvar = ls.dyd.label[lb_idx].nactvar;
3849        let lblevel = reg_level(ls, ls.fs.as_ref().unwrap(), lb_nactvar as i32);
3850        let cur_nvarstack = {
3851            let fs = ls.fs.as_ref().unwrap();
3852            nvarstack(ls, fs)
3853        };
3854        if cur_nvarstack > lblevel {
3855            // C: luaK_codeABC(fs, OP_CLOSE, lblevel, 0, 0)
3856            let inst = lua_code::opcodes::Instruction::abck(
3857                lua_code::opcodes::OpCode::Close,
3858                lblevel as u32,
3859                0,
3860                0,
3861                0,
3862            );
3863            emit_inst(ls.fs.as_mut().unwrap(), line, inst);
3864        }
3865        let jpc = cg_jump(ls.fs.as_mut().unwrap(), line);
3866        cg_patch_list(ls.fs.as_mut().unwrap(), jpc, lb_pc)?;
3867    }
3868    Ok(())
3869}
3870
3871/// C: static void breakstat(LexState *ls)
3872fn breakstat(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
3873    let line = ls.lastline;
3874    // C: luaX_next(ls) — skip 'break'
3875    lex_next(ls, state)?;
3876    // C: newgotoentry(ls, luaS_newliteral(ls->L, "break"), line, luaK_jump(ls->fs))
3877    let break_str = state.intern_str(b"break")?;
3878    let pc = cg_jump(ls.fs.as_mut().unwrap(), line);
3879    new_goto_entry(ls, state, break_str, line, pc)?;
3880    Ok(())
3881}
3882
3883/// C: static void checkrepeated(LexState *ls, TString *name)
3884fn checkrepeated(ls: &LexState, name: &GcRef<LuaString>) -> Result<(), LuaError> {
3885    if let Some(lb_idx) = findlabel(ls, name) {
3886        let name_str = String::from_utf8_lossy(name.as_bytes());
3887        let line = ls.dyd.label[lb_idx].line;
3888        return Err(LuaError::syntax(format_args!(
3889            "label '{}' already defined on line {}", name_str, line
3890        )));
3891    }
3892    Ok(())
3893}
3894
3895/// C: static void labelstat(LexState *ls, TString *name, int line)
3896fn labelstat(
3897    ls: &mut LexState,
3898    state: &mut LuaState,
3899    name: GcRef<LuaString>,
3900    line: i32,
3901) -> Result<(), LuaError> {
3902    // C: checknext(ls, TK_DBCOLON)
3903    check_next(ls, state, TK_DBCOLON)?;
3904    // C: while (ls->t.token == ';' || ls->t.token == TK_DBCOLON) statement(ls)
3905    while ls.t.token == b';' as TokenKind || ls.t.token == TK_DBCOLON {
3906        statement(ls, state)?;
3907    }
3908    checkrepeated(ls, &name)?;
3909    let is_last = block_follow(ls, false);
3910    createlabel(ls, state, name, line, is_last)?;
3911    Ok(())
3912}
3913
3914/// C: static void whilestat(LexState *ls, int line)
3915fn whilestat(ls: &mut LexState, state: &mut LuaState, line: i32) -> Result<(), LuaError> {
3916    // C: luaX_next(ls) — skip WHILE
3917    lex_next(ls, state)?;
3918    // C: whileinit = luaK_getlabel(fs)
3919    let whileinit = cg_get_label(ls.fs.as_mut().unwrap());
3920    let condexit = cond(ls, state)?;
3921    enter_block(ls, true);
3922    check_next(ls, state, TK_DO)?;
3923    block(ls, state)?;
3924    // C: luaK_jumpto(fs, whileinit) === luaK_patchlist(fs, luaK_jump(fs), whileinit)
3925    // Use `lastline` (line of the just-parsed body's last token) rather than
3926    // `linenumber` (which has already advanced to END) so the back-jump's
3927    // line attribution matches lua-c's bytecode and the line hook does not
3928    // spuriously fire for the END line on every iteration.
3929    let back = cg_jump(ls.fs.as_mut().unwrap(), ls.lastline);
3930    cg_patch_list(ls.fs.as_mut().unwrap(), back, whileinit)?;
3931    check_match(ls, state, TK_END, TK_WHILE, line)?;
3932    leave_block(ls, state)?;
3933    // C: luaK_patchtohere(fs, condexit) — false conditions finish the loop
3934    cg_patch_to_here(ls.fs.as_mut().unwrap(), condexit)?;
3935    Ok(())
3936}
3937
3938/// C: static void repeatstat(LexState *ls, int line)
3939fn repeatstat(ls: &mut LexState, state: &mut LuaState, line: i32) -> Result<(), LuaError> {
3940    let repeat_init = cg_get_label(ls.fs.as_mut().unwrap());
3941    enter_block(ls, true);
3942    enter_block(ls, false);
3943    lex_next(ls, state)?;
3944    statlist(ls, state)?;
3945    check_match(ls, state, TK_UNTIL, TK_REPEAT, line)?;
3946    let condexit = cond(ls, state)?;
3947
3948    let bl2_upval = ls.fs.as_ref().unwrap().bl.as_ref().unwrap().upval;
3949    let bl2_nactvar = ls.fs.as_ref().unwrap().bl.as_ref().unwrap().nactvar as i32;
3950    leave_block(ls, state)?;
3951
3952    let mut condexit = condexit;
3953    if bl2_upval {
3954        let exit = cg_jump(ls.fs.as_mut().unwrap(), line);
3955        cg_patch_to_here(ls.fs.as_mut().unwrap(), condexit)?;
3956        let close_level = reg_level(ls, ls.fs.as_ref().unwrap(), bl2_nactvar) as u32;
3957        let close_inst = lua_code::opcodes::Instruction::abck(
3958            lua_code::opcodes::OpCode::Close,
3959            close_level,
3960            0,
3961            0,
3962            0,
3963        );
3964        emit_inst(ls.fs.as_mut().unwrap(), line, close_inst);
3965        condexit = cg_jump(ls.fs.as_mut().unwrap(), line);
3966        cg_patch_to_here(ls.fs.as_mut().unwrap(), exit)?;
3967    }
3968    cg_patch_list(ls.fs.as_mut().unwrap(), condexit, repeat_init)?;
3969    leave_block(ls, state)?;
3970    Ok(())
3971}
3972
3973/// C: static void exp1(LexState *ls)
3974/// Parse an expression and emit it to the next register.
3975fn exp1(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
3976    let mut e = ExprDesc::default();
3977    expr(ls, state, &mut e)?;
3978    let line = ls.lastline;
3979    cg_exp_to_next_reg(ls.fs.as_mut().unwrap(), line, &mut e)?;
3980    debug_assert!(e.k == ExprKind::NonReloc);
3981    Ok(())
3982}
3983
3984/// C: static void fixforjump(FuncState *fs, int pc, int dest, int back)
3985fn fixforjump(fs: &mut FuncState, pc: i32, dest: i32, back: bool) -> Result<(), LuaError> {
3986    let mut offset = dest - (pc + 1);
3987    if back {
3988        offset = -offset;
3989    }
3990    if offset > MAXARG_BX {
3991        return Err(LuaError::syntax(format_args!("control structure too long")));
3992    }
3993    let raw = fs.f.code[pc as usize].0;
3994    let mut inst = lua_code::opcodes::Instruction(raw);
3995    inst.set_arg_bx(offset as u32);
3996    fs.f.code[pc as usize] = lua_types::opcode::Instruction::new(inst.0);
3997    Ok(())
3998}
3999
4000/// C: static void forbody(LexState *ls, int base, int line, int nvars, int isgen)
4001fn forbody(
4002    ls: &mut LexState,
4003    state: &mut LuaState,
4004    base: i32,
4005    line: i32,
4006    nvars: i32,
4007    isgen: bool,
4008) -> Result<(), LuaError> {
4009    check_next(ls, state, TK_DO)?;
4010    let prep_op = if isgen { OpCode::TForPrep } else { OpCode::ForPrep };
4011    let prep = {
4012        let fs = ls.fs.as_mut().unwrap();
4013        let inst = lua_code::opcodes::Instruction::abx(prep_op, base as u32, 0);
4014        emit_inst(fs, line, inst)
4015    };
4016
4017    enter_block(ls, false);
4018    adjust_local_vars(ls, state, nvars)?;
4019    reserve_regs(ls.fs.as_mut().unwrap(), nvars)?;
4020    block(ls, state)?;
4021    leave_block(ls, state)?;
4022
4023    let label_pc = ls.fs.as_ref().unwrap().pc;
4024    fixforjump(ls.fs.as_mut().unwrap(), prep, label_pc, false)?;
4025
4026    if isgen {
4027        let fs = ls.fs.as_mut().unwrap();
4028        let inst = lua_code::opcodes::Instruction::abck(
4029            OpCode::TForCall, base as u32, 0, nvars as u32, 0,
4030        );
4031        emit_inst(fs, line, inst);
4032    }
4033    let loop_op = if isgen { OpCode::TForLoop } else { OpCode::ForLoop };
4034    let endfor = {
4035        let fs = ls.fs.as_mut().unwrap();
4036        let inst = lua_code::opcodes::Instruction::abx(loop_op, base as u32, 0);
4037        emit_inst(fs, line, inst)
4038    };
4039    fixforjump(ls.fs.as_mut().unwrap(), endfor, prep + 1, true)?;
4040    Ok(())
4041}
4042
4043/// C: static void fornum(LexState *ls, TString *varname, int line)
4044fn fornum(
4045    ls: &mut LexState,
4046    state: &mut LuaState,
4047    varname: GcRef<LuaString>,
4048    line: i32,
4049) -> Result<(), LuaError> {
4050    let base = ls.fs.as_ref().unwrap().freereg as i32;
4051    // C: new_localvarliteral(ls, "(for state)") × 3 + new_localvar(ls, varname)
4052    let for_state_str = state.intern_str(b"(for state)")?;
4053    new_local_var(ls, state, for_state_str.clone())?;
4054    new_local_var(ls, state, for_state_str.clone())?;
4055    new_local_var(ls, state, for_state_str)?;
4056    new_local_var(ls, state, varname)?;
4057    check_next(ls, state, b'=' as TokenKind)?;
4058    exp1(ls, state)?; // initial value
4059    check_next(ls, state, b',' as TokenKind)?;
4060    exp1(ls, state)?; // limit
4061    if test_next(ls, state, b',' as TokenKind)? {
4062        exp1(ls, state)?; // optional step
4063    } else {
4064        let fs = ls.fs.as_mut().unwrap();
4065        let reg = fs.freereg as u32;
4066        let bx = (1i32 + lua_code::opcodes::OFFSET_S_BX) as u32;
4067        let inst = lua_code::opcodes::Instruction::abx(
4068            lua_code::opcodes::OpCode::LoadI, reg, bx,
4069        );
4070        emit_inst(fs, line, inst);
4071        reserve_regs(fs, 1)?;
4072    }
4073    adjust_local_vars(ls, state, 3)?; // control variables
4074    forbody(ls, state, base, line, 1, false)?;
4075    Ok(())
4076}
4077
4078/// C: static void forlist(LexState *ls, TString *indexname)
4079fn forlist(
4080    ls: &mut LexState,
4081    state: &mut LuaState,
4082    indexname: GcRef<LuaString>,
4083) -> Result<(), LuaError> {
4084    let mut nvars: i32 = 5; // gen, state, control, toclose, 'indexname'
4085    let base = ls.fs.as_ref().unwrap().freereg as i32;
4086    // C: new_localvarliteral × 4 (for state vars)
4087    let for_state_str = state.intern_str(b"(for state)")?;
4088    new_local_var(ls, state, for_state_str.clone())?;
4089    new_local_var(ls, state, for_state_str.clone())?;
4090    new_local_var(ls, state, for_state_str.clone())?;
4091    new_local_var(ls, state, for_state_str)?;
4092    new_local_var(ls, state, indexname)?;
4093    while test_next(ls, state, b',' as TokenKind)? {
4094        let extra_name = str_check_name(ls, state)?;
4095        new_local_var(ls, state, extra_name)?;
4096        nvars += 1;
4097    }
4098    check_next(ls, state, TK_IN)?;
4099    // C: line = ls->linenumber;  (lparser.c:1611, after consuming TK_IN)
4100    // After `in`, linenumber is the line of the operand — used for the
4101    // for-in control instructions so runtime errors point at the operand,
4102    // not the `in` keyword. errors.lua:401 depends on this.
4103    let line = ls.linenumber;
4104    let mut e = ExprDesc::default();
4105    let nexps = explist(ls, state, &mut e)?;
4106    adjust_assign(ls, state, 4, nexps, &mut e)?;
4107    adjust_local_vars(ls, state, 4)?;
4108    marktobeclosed(ls.fs.as_mut().unwrap()); // last control var must be closed
4109    // C: luaK_checkstack(fs, 3)
4110    // TODO(port): lua_code::check_stack(ls.fs.as_mut().unwrap(), 3)?;
4111    forbody(ls, state, base, line, nvars - 4, true)?;
4112    Ok(())
4113}
4114
4115/// C: static void forstat(LexState *ls, int line)
4116fn forstat(ls: &mut LexState, state: &mut LuaState, line: i32) -> Result<(), LuaError> {
4117    enter_block(ls, true); // scope for loop and control variables
4118    // C: luaX_next(ls) — skip 'for'
4119    lex_next(ls, state)?;
4120    let varname = str_check_name(ls, state)?;
4121    match ls.t.token {
4122        c if c == b'=' as TokenKind => fornum(ls, state, varname, line)?,
4123        c if c == b',' as TokenKind || c == TK_IN => forlist(ls, state, varname)?,
4124        _ => {
4125            return Err(LuaError::syntax(format_args!("'=' or 'in' expected")));
4126        }
4127    }
4128    check_match(ls, state, TK_END, TK_FOR, line)?;
4129    leave_block(ls, state)?; // loop scope ('break' jumps to this point)
4130    Ok(())
4131}
4132
4133/// C: static void test_then_block(LexState *ls, int *escapelist)
4134fn test_then_block(
4135    ls: &mut LexState,
4136    state: &mut LuaState,
4137    escapelist: &mut i32,
4138) -> Result<(), LuaError> {
4139    // C: luaX_next(ls) — skip IF or ELSEIF
4140    lex_next(ls, state)?;
4141    let mut v = ExprDesc::default();
4142    expr(ls, state, &mut v)?;
4143    check_next(ls, state, TK_THEN)?;
4144
4145    let jf: i32;
4146    if ls.t.token == TK_BREAK {
4147        let line = ls.lastline;
4148        // C: luaK_goiffalse(ls->fs, &v) — jumps if condition is true
4149        cg_go_if_false(ls.fs.as_mut().unwrap(), line, &mut v)?;
4150        lex_next(ls, state)?; // skip 'break'
4151        enter_block(ls, false);
4152        // C: newgotoentry(ls, "break", line, v.t)
4153        let break_str = state.intern_str(b"break")?;
4154        new_goto_entry(ls, state, break_str, line, v.t)?;
4155        // C: while (testnext(ls, ';')) {} -- skip semicolons
4156        while test_next(ls, state, b';' as TokenKind)? {}
4157        if block_follow(ls, false) {
4158            leave_block(ls, state)?;
4159            return Ok(());
4160        } else {
4161            // C: jf = luaK_jump(fs)
4162            jf = cg_jump(ls.fs.as_mut().unwrap(), ls.linenumber);
4163        }
4164    } else {
4165        let line = ls.lastline;
4166        cg_go_if_true(ls.fs.as_mut().unwrap(), line, &mut v)?;
4167        enter_block(ls, false);
4168        jf = v.f;
4169    }
4170
4171    statlist(ls, state)?;
4172    leave_block(ls, state)?;
4173
4174    if ls.t.token == TK_ELSE || ls.t.token == TK_ELSEIF {
4175        // C: luaK_concat(fs, escapelist, luaK_jump(fs))
4176        let line = ls.lastline;
4177        let j = cg_jump(ls.fs.as_mut().unwrap(), line);
4178        cg_concat(ls.fs.as_mut().unwrap(), escapelist, j)?;
4179    }
4180    // C: luaK_patchtohere(fs, jf)
4181    cg_patch_to_here(ls.fs.as_mut().unwrap(), jf)?;
4182    Ok(())
4183}
4184
4185/// C: static void ifstat(LexState *ls, int line)
4186fn ifstat(ls: &mut LexState, state: &mut LuaState, line: i32) -> Result<(), LuaError> {
4187    let mut escapelist = NO_JUMP;
4188    test_then_block(ls, state, &mut escapelist)?; // IF cond THEN block
4189    while ls.t.token == TK_ELSEIF {
4190        test_then_block(ls, state, &mut escapelist)?;
4191    }
4192    if test_next(ls, state, TK_ELSE)? {
4193        block(ls, state)?;
4194    }
4195    check_match(ls, state, TK_END, TK_IF, line)?;
4196    // C: luaK_patchtohere(fs, escapelist)
4197    cg_patch_to_here(ls.fs.as_mut().unwrap(), escapelist)?;
4198    Ok(())
4199}
4200
4201/// C: static void localfunc(LexState *ls)
4202fn localfunc(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
4203    let mut b = ExprDesc::default();
4204    let fvar = ls.fs.as_ref().unwrap().nactvar as i32;
4205    let name = str_check_name(ls, state)?;
4206    new_local_var(ls, state, name)?;
4207    adjust_local_vars(ls, state, 1)?; // enter its scope
4208    let line = ls.lastline;
4209    body(ls, state, &mut b, false, line)?;
4210    // C: localdebuginfo(fs, fvar)->startpc = fs->pc
4211    let pc = ls.fs.as_ref().unwrap().pc;
4212    // TODO(port): local_debug_info(ls, ls.fs.as_mut().unwrap(), fvar).map(|lv| lv.startpc = pc);
4213    Ok(())
4214}
4215
4216/// C: static int getlocalattribute(LexState *ls)
4217/// Parses an optional '<const>' or '<close>' attribute.
4218fn getlocalattribute(ls: &mut LexState, state: &mut LuaState) -> Result<VarKind, LuaError> {
4219    if test_next(ls, state, b'<' as TokenKind)? {
4220        let attr_name = str_check_name(ls, state)?;
4221        check_next(ls, state, b'>' as TokenKind)?;
4222        let bytes = attr_name.as_bytes();
4223        if bytes == b"const" {
4224            return Ok(VarKind::Const);
4225        } else if bytes == b"close" {
4226            return Ok(VarKind::ToBeClosed);
4227        } else {
4228            let name_str = String::from_utf8_lossy(bytes);
4229            return Err(LuaError::syntax(format_args!(
4230                "unknown attribute '{}'", name_str
4231            )));
4232        }
4233    }
4234    Ok(VarKind::Reg)
4235}
4236
4237/// C: static void checktoclose(FuncState *fs, int level)
4238fn checktoclose(ls: &mut LexState, state: &mut LuaState, level: i32) -> Result<(), LuaError> {
4239    if level != -1 {
4240        marktobeclosed(ls.fs.as_mut().unwrap());
4241        // C: luaK_codeABC(fs, OP_TBC, reglevel(fs, level), 0, 0)
4242        let rl = reg_level(ls, ls.fs.as_ref().unwrap(), level);
4243        let line = ls.lastline;
4244        let inst = lua_code::opcodes::Instruction::abck(
4245            lua_code::opcodes::OpCode::Tbc,
4246            rl as u32,
4247            0,
4248            0,
4249            0,
4250        );
4251        emit_inst(ls.fs.as_mut().unwrap(), line, inst);
4252    }
4253    Ok(())
4254}
4255
4256/// C: static void localstat(LexState *ls)
4257fn localstat(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
4258    let mut toclose: i32 = -1;
4259    let mut nvars: i32 = 0;
4260    let mut vidx = 0i32;
4261    loop {
4262        let name = str_check_name(ls, state)?;
4263        vidx = new_local_var(ls, state, name)?;
4264        let kind = getlocalattribute(ls, state)?;
4265        get_local_var_desc_mut(ls, ls.fs.as_ref().unwrap().firstlocal, vidx).kind = kind;
4266        if kind == VarKind::ToBeClosed {
4267            if toclose != -1 {
4268                return Err(LuaError::syntax(format_args!(
4269                    "multiple to-be-closed variables in local list"
4270                )));
4271            }
4272            toclose = ls.fs.as_ref().unwrap().nactvar as i32 + nvars;
4273        }
4274        nvars += 1;
4275        if !test_next(ls, state, b',' as TokenKind)? {
4276            break;
4277        }
4278    }
4279    let nexps: i32;
4280    let mut e = ExprDesc::default();
4281    if test_next(ls, state, b'=' as TokenKind)? {
4282        nexps = explist(ls, state, &mut e)?;
4283    } else {
4284        e.k = ExprKind::Void;
4285        nexps = 0;
4286    }
4287    let first_local = ls.fs.as_ref().unwrap().firstlocal;
4288    let last_vd_kind = ls.dyd.actvar[(first_local + vidx) as usize].kind;
4289    if nvars == nexps
4290        && last_vd_kind == VarKind::Const
4291    {
4292        // C: luaK_exp2const(fs, &e, &var->k) — try compile-time constant
4293        // TODO(port): let is_const = lua_code::exp_to_const(ls.fs.as_mut().unwrap(), &mut e, &mut var_k)?;
4294        let is_const = false; // placeholder
4295        if is_const {
4296            ls.dyd.actvar[(first_local + vidx) as usize].kind = VarKind::CompileTimeConst;
4297            adjust_local_vars(ls, state, nvars - 1)?;
4298            ls.fs.as_mut().unwrap().nactvar += 1;
4299        } else {
4300            adjust_assign(ls, state, nvars, nexps, &mut e)?;
4301            adjust_local_vars(ls, state, nvars)?;
4302        }
4303    } else {
4304        adjust_assign(ls, state, nvars, nexps, &mut e)?;
4305        adjust_local_vars(ls, state, nvars)?;
4306    }
4307    checktoclose(ls, state, toclose)?;
4308    Ok(())
4309}
4310
4311/// C: static int funcname(LexState *ls, expdesc *v)
4312/// Parses a function name (NAME {'.' NAME} [':' NAME]). Returns ismethod.
4313fn funcname(ls: &mut LexState, state: &mut LuaState, v: &mut ExprDesc) -> Result<bool, LuaError> {
4314    let mut ismethod = false;
4315    singlevar(ls, state, v)?;
4316    while ls.t.token == b'.' as TokenKind {
4317        fieldsel(ls, state, v)?;
4318    }
4319    if ls.t.token == b':' as TokenKind {
4320        ismethod = true;
4321        fieldsel(ls, state, v)?;
4322    }
4323    Ok(ismethod)
4324}
4325
4326/// C: static void funcstat(LexState *ls, int line)
4327fn funcstat(ls: &mut LexState, state: &mut LuaState, line: i32) -> Result<(), LuaError> {
4328    // C: luaX_next(ls) — skip FUNCTION
4329    lex_next(ls, state)?;
4330    let mut v = ExprDesc::default();
4331    let mut b = ExprDesc::default();
4332    let ismethod = funcname(ls, state, &mut v)?;
4333    body(ls, state, &mut b, ismethod, line)?;
4334    check_readonly(ls, state, &v.clone())?;
4335    let fs = ls.fs.as_mut().unwrap();
4336    cg_storevar(fs, line, &v, &mut b)?;
4337    // TODO(port): lua_code::fix_line(ls.fs.as_mut().unwrap(), line);
4338    Ok(())
4339}
4340
4341/// C: static void exprstat(LexState *ls)
4342fn exprstat(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
4343    let mut v_assign = LhsAssign { prev: None, v: ExprDesc::default() };
4344    suffixedexp(ls, state, &mut v_assign.v)?;
4345    if ls.t.token == b'=' as TokenKind || ls.t.token == b',' as TokenKind {
4346        // C: stat -> assignment
4347        restassign(ls, state, &mut v_assign, 1)?;
4348    } else {
4349        // C: stat -> func call; check it's a call, fix result count
4350        if v_assign.v.k != ExprKind::Call {
4351            return Err(lua_lex::syntax_error(&mut ls.lex, b"syntax error"));
4352        }
4353        // C: SETARG_C(*inst, 1) — call statement uses no results.
4354        let info = v_assign.v.u.info as usize;
4355        let fs = ls.fs.as_mut().unwrap();
4356        let mut lc = lua_code::opcodes::Instruction(fs.f.code[info].0);
4357        lc.set_arg_c(1);
4358        fs.f.code[info] = lua_types::opcode::Instruction::new(lc.0);
4359    }
4360    Ok(())
4361}
4362
4363/// C: static void retstat(LexState *ls)
4364fn retstat(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
4365    let mut first = {
4366        let fs = ls.fs.as_ref().unwrap();
4367        nvarstack(ls, fs)
4368    };
4369    let mut nret: i32;
4370    if block_follow(ls, true) || ls.t.token == b';' as TokenKind {
4371        nret = 0;
4372    } else {
4373        let mut e = ExprDesc::default();
4374        nret = explist(ls, state, &mut e)?;
4375        if e.k.has_mult_ret() {
4376            // C: luaK_setmultret(fs, &e)
4377            cg_set_returns(ls.fs.as_mut().unwrap(), &mut e, LUA_MULTRET);
4378            if e.k == ExprKind::Call && nret == 1 {
4379                // C: tail call check — !fs->bl->insidetbc
4380                let insidetbc = ls.fs.as_ref().unwrap().bl.as_ref().map_or(false, |b| b.insidetbc);
4381                if !insidetbc {
4382                    // C: SET_OPCODE(getinstruction(fs, &e), OP_TAILCALL)
4383                    let fs = ls.fs.as_mut().unwrap();
4384                    let info = e.u.info as usize;
4385                    let mut lc = lua_code::opcodes::Instruction(fs.f.code[info].0);
4386                    lc.set_opcode(lua_code::opcodes::OpCode::TailCall);
4387                    fs.f.code[info] = lua_types::opcode::Instruction::new(lc.0);
4388                }
4389            }
4390            nret = LUA_MULTRET;
4391        } else {
4392            let line = ls.lastline;
4393            if nret == 1 {
4394                // C: first = luaK_exp2anyreg(fs, &e)
4395                first = cg_exp_to_any_reg(ls.fs.as_mut().unwrap(), line, &mut e)? as i32;
4396            } else {
4397                // C: values must go to the top of the stack
4398                cg_exp_to_next_reg(ls.fs.as_mut().unwrap(), line, &mut e)?;
4399            }
4400        }
4401    }
4402    // C: luaK_ret(fs, first, nret)
4403    let line = ls.lastline;
4404    cg_emit_return(ls.fs.as_mut().unwrap(), line, first, nret);
4405    // C: testnext(ls, ';')
4406    test_next(ls, state, b';' as TokenKind)?;
4407    Ok(())
4408}
4409
4410/// C: static void statement(LexState *ls)
4411/// Top-level statement dispatcher.
4412fn statement(ls: &mut LexState, state: &mut LuaState) -> Result<(), LuaError> {
4413    // C: int line = ls->linenumber;  (lparser.c, statement())
4414    // This is the line of the current keyword (for/while/if/...), captured
4415    // BEFORE consuming. Used both for error messages on unmatched blocks
4416    // AND for runtime-error line attribution on control-flow instructions
4417    // (FORPREP, etc). errors.lua's lineerror tests depend on this.
4418    let line = ls.linenumber;
4419    // C: enterlevel(ls)
4420    enter_level(ls)?;
4421    match ls.t.token {
4422        c if c == b';' as TokenKind => {
4423            lex_next(ls, state)?;
4424        }
4425        TK_IF => {
4426            ifstat(ls, state, line)?;
4427        }
4428        TK_WHILE => {
4429            whilestat(ls, state, line)?;
4430        }
4431        TK_DO => {
4432            lex_next(ls, state)?; // skip DO
4433            block(ls, state)?;
4434            check_match(ls, state, TK_END, TK_DO, line)?;
4435        }
4436        TK_FOR => {
4437            forstat(ls, state, line)?;
4438        }
4439        TK_REPEAT => {
4440            repeatstat(ls, state, line)?;
4441        }
4442        TK_FUNCTION => {
4443            funcstat(ls, state, line)?;
4444        }
4445        TK_LOCAL => {
4446            lex_next(ls, state)?; // skip LOCAL
4447            if test_next(ls, state, TK_FUNCTION)? {
4448                localfunc(ls, state)?;
4449            } else {
4450                localstat(ls, state)?;
4451            }
4452        }
4453        TK_DBCOLON => {
4454            lex_next(ls, state)?; // skip '::'
4455            let name = str_check_name(ls, state)?;
4456            labelstat(ls, state, name, line)?;
4457        }
4458        TK_RETURN => {
4459            lex_next(ls, state)?; // skip RETURN
4460            retstat(ls, state)?;
4461        }
4462        TK_BREAK => {
4463            breakstat(ls, state)?;
4464        }
4465        TK_GOTO => {
4466            lex_next(ls, state)?; // skip 'goto'
4467            gotostat(ls, state)?;
4468        }
4469        _ => {
4470            exprstat(ls, state)?;
4471        }
4472    }
4473    debug_assert!(
4474        ls.fs.as_ref().unwrap().f.maxstacksize >= ls.fs.as_ref().unwrap().freereg
4475            && ls.fs.as_ref().unwrap().freereg as i32
4476                >= nvarstack(ls, ls.fs.as_ref().unwrap())
4477    );
4478    let nv = nvarstack(ls, ls.fs.as_ref().unwrap());
4479    ls.fs.as_mut().unwrap().freereg = nv as u8;
4480    // C: leavelevel(ls)
4481    leave_level(ls);
4482    Ok(())
4483}
4484
4485// ── §14 Main function and entry point ────────────────────────────────────────
4486
4487/// C: static void mainfunc(LexState *ls, FuncState *fs)
4488/// Compiles the main chunk (always a vararg function with _ENV upvalue).
4489fn mainfunc(ls: &mut LexState, state: &mut LuaState, main_fs: FuncState) -> Result<Box<LuaProto>, LuaError> {
4490    // C: open_func(ls, fs, &bl)
4491    open_func(ls, state, main_fs)?;
4492
4493    // C: setvararg(fs, 0) — main function is always vararg
4494    setvararg(ls.fs.as_mut().unwrap(), state, 0)?;
4495
4496    // C: env = allocupvalue(fs); env->instack=1; env->idx=0; env->kind=VDKREG; env->name=ls->envn
4497    let env_name = ls.envn.clone();
4498    {
4499        let idx = alloc_upvalue(ls.fs.as_mut().unwrap())?;
4500        let up = &mut ls.fs.as_mut().unwrap().f.upvalues[idx];
4501        up.instack = true;
4502        up.idx = 0;
4503        up.kind = VarKind::Reg.as_u8();
4504        up.name = env_name.clone();
4505    }
4506    // C: luaC_objbarrier(ls->L, fs->f, env->name) — no-op in Phase A
4507
4508    // C: luaX_next(ls) — read first token
4509    lex_next(ls, state)?;
4510
4511    statlist(ls, state)?;
4512
4513    // C: check(ls, TK_EOS)
4514    check(ls, TK_EOS)?;
4515
4516    close_func(ls, state)
4517}
4518
4519/// C: LClosure *luaY_parser(lua_State *L, ZIO *z, Mbuffer *buff, Dyndata *dyd,
4520///                           const char *name, int firstchar)
4521/// Top-level entry point: parses a chunk and returns the main LClosure.
4522/// LUAI_FUNC visibility.
4523///
4524/// PORT NOTE: In C, returns `LClosure *` (a GC object). In Rust (Phase A),
4525///   we return `Box<LuaProto>` since we don't have GcRef<LuaLClosure> ready.
4526///   Phase B will wrap this in a proper LuaLClosure / GcRef.
4527pub fn parse(
4528    state: &mut LuaState,
4529    dyd: DynData,
4530    source: &[u8],
4531    name: &[u8],
4532    firstchar: i32,
4533) -> Result<Box<LuaProto>, LuaError> {
4534    let source_str = state.intern_str(name)?;
4535    let envn_str = state.intern_str(lua_lex::LUA_ENV)?;
4536
4537    let rest_bytes: Vec<u8> = source.iter().skip(1).copied().collect();
4538    let z = lua_lex::ZIO::from_bytes(rest_bytes);
4539
4540    let lex_ls = lua_lex::LexState {
4541        current: firstchar,
4542        linenumber: 1,
4543        lastline: 1,
4544        t: lua_lex::Token::eos(),
4545        lookahead: lua_lex::Token::eos(),
4546        fs: None,
4547        z,
4548        buff: lua_lex::LexBuffer::new(),
4549        h: None,
4550        long_str_anchor: std::collections::HashMap::new(),
4551        dyd: None,
4552        source: source_str.clone(),
4553        envn: envn_str.clone(),
4554    };
4555
4556    let mut lexstate = LexState {
4557        current: lex_ls.current,
4558        linenumber: lex_ls.linenumber,
4559        lastline: lex_ls.lastline,
4560        t: LexToken::default(),
4561        lookahead: LexToken::default(),
4562        fs: None,
4563        dyd,
4564        source: Some(source_str.clone()),
4565        envn: Some(lex_ls.envn.clone()),
4566        lex: lex_ls,
4567        recursion_depth: 0,
4568    };
4569    // C: luaX_setinput is the only setup the C parser performs before
4570    //   `mainfunc`; it does NOT pre-read the first token. `mainfunc` itself
4571    //   issues the initial `luaX_next` once its prelude (open_func, vararg
4572    //   marker, _ENV upvalue) is in place.
4573
4574    let mut main_proto = Box::new(LuaProto::placeholder());
4575    main_proto.source = Some(source_str);
4576    main_proto.is_vararg = true;
4577    let main_fs = FuncState {
4578        f: main_proto,
4579        prev: None,
4580        bl: None,
4581        pc: 0,
4582        lasttarget: 0,
4583        previousline: 0,
4584        nk: 0,
4585        np: 0,
4586        nabslineinfo: 0,
4587        firstlocal: 0,
4588        firstlabel: 0,
4589        ndebugvars: 0,
4590        nactvar: 0,
4591        nups: 0,
4592        freereg: 0,
4593        iwthabs: 0,
4594        needclose: false,
4595        last_token_line: 0,
4596    };
4597
4598    mainfunc(&mut lexstate, state, main_fs)
4599}
4600
4601/// Convert a `lua_lex::TokenValue` into the local `parse::TokenValue` flat shape.
4602///
4603/// The parser's local `LexState` predates the lex-side enum and uses a flat
4604/// (r, i, ts) record; this picks out whichever variant the lexer produced.
4605fn local_token_value(v: &lua_lex::TokenValue) -> TokenValue {
4606    match v {
4607        lua_lex::TokenValue::None => TokenValue::default(),
4608        lua_lex::TokenValue::Float(r) => TokenValue { r: *r, i: 0, ts: None },
4609        lua_lex::TokenValue::Int(i) => TokenValue { r: 0.0, i: *i, ts: None },
4610        lua_lex::TokenValue::Str(s) => TokenValue { r: 0.0, i: 0, ts: Some(s.clone()) },
4611    }
4612}
4613
4614// ──────────────────────────────────────────────────────────────────────────
4615// PORT STATUS
4616//   source:        src/lparser.c  (1968 lines, 95 functions)
4617//   target_crate:  lua-parse
4618//   confidence:    medium
4619//   todos:         184
4620//   port_notes:    14
4621//   unsafe_blocks: 0
4622//   notes:         All 95 functions translated with correct logical structure.
4623//                  184 TODO(port) stubs for cross-crate calls (lua_code::*,
4624//                  lua_lex::*, lua_vm state allocation). Key design choices:
4625//                  BlockCnt/LhsAssign use Option<Box<...>> chains; FuncState
4626//                  uses Box<LuaProto> (not GcRef) for mutable access during
4627//                  build. singlevaraux FuncState.prev chain traversal (upvalue
4628//                  capture across closures) is a known TODO — needs recursive
4629//                  descent through fs.prev without double-mutable-borrow.
4630//                  LexState is a local stub — Phase B must unify with
4631//                  lua_lex::LexState and add lua-lex as a dep. markupval
4632//                  BlockCnt chain traversal also needs Phase B restructure.
4633//                  rustc check: only E0432 (unresolved lua_types import) —
4634//                  expected Phase A name-resolution error; no syntax errors.
4635// ──────────────────────────────────────────────────────────────────────────