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