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