Skip to main content

mailang_bytecode/
register.rs

1//! Register-based bytecode and a stack→register translator.
2//!
3//! The classic compiler emits stack bytecode. A real **register VM** executes
4//! three-address instructions (`Add dst, lhs, rhs`) with a flat register file.
5//! This module defines that IR and a translator that lowers stack bytecode to
6//! register bytecode by simulating the operand stack and assigning each stack
7//! slot a virtual register.
8
9use crate::{Bytecode, Opcode, Value};
10use alloc::string::String;
11use alloc::vec;
12use alloc::vec::Vec;
13
14/// Three-address register instruction. Registers are `u16` indices into a
15/// flat frame-local register file. Constants live in the chunk const table.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum RegOp {
18    /// `dst = src`
19    Mov { dst: u16, src: u16 },
20    /// `dst = constants[k]`
21    LoadConst { dst: u16, k: u32 },
22    /// `dst = globals[slot]`
23    LoadGlobal { dst: u16, slot: u32 },
24    /// `globals[slot] = src`
25    StoreGlobal { slot: u32, src: u16 },
26    /// `dst = upvalues[idx]`
27    LoadUpvalue { dst: u16, idx: u32 },
28    /// `upvalues[idx] = src`
29    StoreUpvalue { idx: u32, src: u16 },
30    /// `dst = lhs <op> rhs`
31    Bin {
32        op: BinKind,
33        dst: u16,
34        lhs: u16,
35        rhs: u16,
36    },
37    /// `dst = <op> src`
38    Un { op: UnKind, dst: u16, src: u16 },
39    /// `dst = lhs <cmp> rhs`
40    Cmp {
41        op: CmpKind,
42        dst: u16,
43        lhs: u16,
44        rhs: u16,
45    },
46    /// `dst = lhs <cmp> imm` (fused int immediate)
47    CmpImm {
48        op: CmpKind,
49        dst: u16,
50        lhs: u16,
51        imm: i64,
52    },
53    /// `dst = lhs <op> imm`
54    BinImm {
55        op: BinKind,
56        dst: u16,
57        lhs: u16,
58        imm: i64,
59    },
60    /// Unconditional jump to instruction index `target` (in this chunk).
61    Jump { target: u32 },
62    /// Jump if `cond` register is falsy.
63    JumpIfFalse { target: u32, cond: u16 },
64    /// Jump if `cond` register is truthy.
65    JumpIfTrue { target: u32, cond: u16 },
66    /// `dst = call(func, args[argc])` — args live in consecutive registers.
67    Call {
68        dst: u16,
69        func: u16,
70        args: u16,
71        argc: u8,
72    },
73    /// Tail call: same as Call but reuses the frame.
74    TailCall { func: u16, args: u16, argc: u8 },
75    /// `return src`
76    Ret { src: u16 },
77    /// `dst = obj.name`
78    GetProp { dst: u16, obj: u16, name: u32 },
79    /// `obj.name = src`
80    SetProp { obj: u16, name: u32, src: u16 },
81    /// `dst = obj.name(args[argc])`
82    Invoke {
83        dst: u16,
84        obj: u16,
85        name: u32,
86        args: u16,
87        argc: u8,
88    },
89    /// `dst = [regs[start] .. start+count)`
90    BuildArr { dst: u16, start: u16, count: u16 },
91    /// `dst = { k/v pairs from regs[start] .. }`
92    BuildMap { dst: u16, start: u16, count: u16 },
93    /// `dst = obj[idx]`
94    IndexGet { dst: u16, obj: u16, idx: u16 },
95    /// `obj[idx] = src`
96    IndexSet { obj: u16, idx: u16, src: u16 },
97    /// Class / instance helpers (operand carries the same meaning as stack IR).
98    CreateClass { dst: u16, k: u32 },
99    CreateInstance {
100        dst: u16,
101        class: u16,
102        args: u16,
103        argc: u8,
104    },
105    MakeClosure {
106        dst: u16,
107        func: u16,
108        upvals: u16,
109        count: u8,
110    },
111    /// `dst = wrap(src)` for Ok/Err/Some
112    Wrap { kind: WrapKind, dst: u16, src: u16 },
113    /// Unwrap Ok/Err/Some: `dst = inner` (bool success in `flag`)
114    Unwrap {
115        kind: WrapKind,
116        dst: u16,
117        flag: u16,
118        src: u16,
119    },
120    /// `?` operator: dst = inner or early-return
121    TryQ { dst: u16, src: u16 },
122    /// Current frame argc (for default-parameter prologues)
123    Argc { dst: u16 },
124    /// Call a known chunk by index (lowered from stack `CallDirect`).
125    CallChunk {
126        dst: u16,
127        chunk: u32,
128        args: u16,
129        argc: u8,
130    },
131    /// No-op
132    Nop,
133    /// Stop and return `src`
134    Halt { src: u16 },
135}
136
137#[derive(Debug, Clone, Copy, PartialEq)]
138pub enum BinKind {
139    Add,
140    Sub,
141    Mul,
142    Div,
143    Mod,
144    Pow,
145    BitAnd,
146    BitOr,
147    BitXor,
148    Shl,
149    Shr,
150    And,
151    Or,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq)]
155pub enum UnKind {
156    Neg,
157    Not,
158    BitNot,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq)]
162pub enum CmpKind {
163    Eq,
164    Ne,
165    Lt,
166    Le,
167    Gt,
168    Ge,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub enum WrapKind {
173    Ok,
174    Err,
175    Some,
176}
177
178/// Parameter / `this` slot count for a chunk: the translator's starting stack
179/// height ([`RegChunk::param_slots`]), so `let` locals land on their own
180/// registers.
181///
182/// The register VM and the disassembler must agree on this, so it lives next
183/// to the translator. `arity` on a `RegChunk` stays 0 for chunks reached
184/// through `CallDirect` (a deep call has no `FunctionObj` to read), which is
185/// why the count is recovered from the constant pool instead.
186pub fn parameter_slots(bytecode: &Bytecode, chunk_idx: usize) -> u16 {
187    if chunk_idx == bytecode.main_chunk {
188        return 0;
189    }
190    let mut best: Option<usize> = None;
191    for ch in &bytecode.chunks {
192        for c in &ch.constants {
193            if let Value::Function(f) = c {
194                if f.chunk_index == chunk_idx {
195                    best = Some(best.map_or(f.arity, |b: usize| b.max(f.arity)));
196                }
197            }
198            if let Value::Class(cls) = c {
199                for (_, ci) in cls.methods.iter() {
200                    if *ci == chunk_idx {
201                        // Method chunk without a self-describing FunctionObj:
202                        // at least `this`.
203                        if best.is_none() {
204                            best = Some(1);
205                        }
206                    }
207                }
208            }
209        }
210    }
211    best.unwrap_or(0) as u16
212}
213
214/// A register-mode chunk (function body), i.e. one frame layout.
215///
216/// Three different quantities used to share one field: the frame's window size,
217/// the count of parameter slots to reserve at the bottom of the frame, and the
218/// declared parameter count. They are kept apart here because they answer
219/// different questions and only happen to coincide for a plain function:
220///
221/// * [`RegChunk::n_regs`] - how many registers the frame needs (window size);
222/// * [`RegChunk::param_slots`] - how many of them are reserved for the
223///   parameters and an implicit `this` (the translator's initial stack height);
224/// * [`RegChunk::arity`] / [`RegChunk::required`] - the parameter-count contract
225///   from the chunk's `FunctionObj`, used to check a call.
226#[derive(Debug, Clone)]
227pub struct RegChunk {
228    pub name: String,
229    pub code: Vec<RegOp>,
230    pub constants: Vec<Value>,
231    /// Registers the frame must allocate: the highest register index any
232    /// instruction of the chunk uses, plus one.
233    pub n_regs: u16,
234    /// Registers reserved at the bottom of the frame for parameters and an
235    /// implicit `this`: this is the translator's initial stack height, so the
236    /// chunk's own `let`/`var` locals and every expression temp live at
237    /// `param_slots..n_regs`.
238    ///
239    /// It is *not* the declared parameter count and it is not the function's
240    /// arity: a method reserves an extra slot for `this`, and a chunk reached
241    /// through `CallDirect` has no `FunctionObj` to ask.
242    pub param_slots: u16,
243    /// Declared parameter count (max) of the chunk's `FunctionObj`, used for
244    /// the arity check when the call site holds the function value.
245    /// Stays 0 for chunks that only `CallDirect` reaches.
246    pub arity: usize,
247    /// Declared required-parameter count, as [`RegChunk::arity`].
248    pub required: usize,
249}
250
251impl RegChunk {
252    pub fn new(name: impl Into<String>) -> Self {
253        Self {
254            name: name.into(),
255            code: Vec::new(),
256            constants: Vec::new(),
257            n_regs: 0,
258            param_slots: 0,
259            arity: 0,
260            required: 0,
261        }
262    }
263}
264
265/// Lower a stack instruction stream to register form.
266///
267/// Strategy: simulate the operand stack with **dataflow stack heights** so
268/// control-flow joins (match arms, if/else, Unwrap success vs fail) get the
269/// correct register indices. Registers are the frame's contiguous stack slots
270/// (parameters occupy `0..param_slots`, locals and expression temps grow above).
271pub struct StackToRegister<'a> {
272    src_code: &'a [crate::Instruction],
273    #[allow(dead_code)]
274    src_consts: &'a [Value],
275    out: RegChunk,
276    /// Maps stack IP → register IP (for jump patching).
277    ip_map: Vec<u32>,
278    /// Pending (stack_target, reg_site) jumps to patch.
279    jumps: Vec<(usize, usize)>,
280    /// Initial stack height (= parameter / `this` slot count).
281    param_slots: u16,
282    /// Height before each source instruction (dataflow).
283    heights: Vec<u16>,
284}
285
286impl<'a> StackToRegister<'a> {
287    pub fn new(name: &str, code: &'a [crate::Instruction], consts: &'a [Value]) -> Self {
288        let mut out = RegChunk::new(name);
289        out.constants = consts.to_vec();
290        out.arity = 0;
291        out.required = 0;
292        Self {
293            src_code: code,
294            src_consts: consts,
295            out,
296            ip_map: Vec::new(),
297            jumps: Vec::new(),
298            param_slots: 0,
299            heights: Vec::new(),
300        }
301    }
302
303    /// Set the frame's initial stack height: the parameter / `this` slot count
304    /// the chunk was compiled with (see [`RegChunk::param_slots`]).
305    pub fn with_param_slots(mut self, param_slots: u16) -> Self {
306        self.param_slots = param_slots;
307        self
308    }
309
310    fn emit(&mut self, op: RegOp) -> usize {
311        self.out.code.push(op);
312        self.out.code.len() - 1
313    }
314
315    fn use_reg(&mut self, r: u16) {
316        if r + 1 > self.out.n_regs {
317            self.out.n_regs = r + 1;
318        }
319    }
320
321    #[allow(unused_assignments)]
322    pub fn translate(mut self) -> RegChunk {
323        let param_slots = self.param_slots;
324        self.heights = compute_heights(self.src_code, param_slots);
325        self.out.n_regs = self.out.n_regs.max(param_slots);
326        self.out.param_slots = param_slots;
327        for (sip, ins) in self.src_code.iter().enumerate() {
328            self.ip_map.push(self.out.code.len() as u32);
329            let mut sp: u16 = self.heights[sip];
330            match ins.opcode {
331                Opcode::Push => {
332                    let k = ins.operand.unwrap_or(0);
333                    let dst = sp;
334                    self.use_reg(dst);
335                    self.emit(RegOp::LoadConst { dst, k });
336                    sp += 1;
337                }
338                Opcode::Pop => {
339                    sp = sp.saturating_sub(1);
340                }
341                Opcode::Dup => {
342                    let src = sp.saturating_sub(1);
343                    let dst = sp;
344                    self.use_reg(dst);
345                    self.emit(RegOp::Mov { dst, src });
346                    sp += 1;
347                }
348                Opcode::LoadLocal => {
349                    let idx = ins.operand.unwrap_or(0) as u16;
350                    let dst = sp;
351                    self.use_reg(idx.max(dst));
352                    self.emit(RegOp::Mov { dst, src: idx });
353                    sp += 1;
354                }
355                Opcode::StoreLocal => {
356                    let idx = ins.operand.unwrap_or(0) as u16;
357                    let src = sp.saturating_sub(1);
358                    self.use_reg(idx.max(src));
359                    self.emit(RegOp::Mov { dst: idx, src });
360                    sp = sp.saturating_sub(1);
361                }
362                Opcode::LoadGlobal => {
363                    let slot = ins.operand.unwrap_or(0);
364                    let dst = sp;
365                    self.use_reg(dst);
366                    self.emit(RegOp::LoadGlobal { dst, slot });
367                    sp += 1;
368                }
369                Opcode::StoreGlobal => {
370                    let slot = ins.operand.unwrap_or(0);
371                    let src = sp.saturating_sub(1);
372                    self.use_reg(src);
373                    self.emit(RegOp::StoreGlobal { slot, src });
374                    sp = sp.saturating_sub(1);
375                }
376                Opcode::LoadUpvalue => {
377                    let idx = ins.operand.unwrap_or(0);
378                    let dst = sp;
379                    self.use_reg(dst);
380                    self.emit(RegOp::LoadUpvalue { dst, idx });
381                    sp += 1;
382                }
383                Opcode::StoreUpvalue => {
384                    let idx = ins.operand.unwrap_or(0);
385                    let src = sp.saturating_sub(1);
386                    self.use_reg(src);
387                    self.emit(RegOp::StoreUpvalue { idx, src });
388                    sp = sp.saturating_sub(1);
389                }
390                Opcode::Add
391                | Opcode::Sub
392                | Opcode::Mul
393                | Opcode::Div
394                | Opcode::Mod
395                | Opcode::Pow
396                | Opcode::BitAnd
397                | Opcode::BitOr
398                | Opcode::BitXor
399                | Opcode::Shl
400                | Opcode::Shr
401                | Opcode::And
402                | Opcode::Or => {
403                    let kind = bin_kind(ins.opcode);
404                    let rhs = sp.saturating_sub(1);
405                    let lhs = sp.saturating_sub(2);
406                    let dst = lhs;
407                    self.use_reg(rhs.max(dst));
408                    self.emit(RegOp::Bin {
409                        op: kind,
410                        dst,
411                        lhs,
412                        rhs,
413                    });
414                    sp = sp.saturating_sub(1);
415                }
416                Opcode::AddImm
417                | Opcode::SubImm
418                | Opcode::MulImm
419                | Opcode::EqImm
420                | Opcode::NeImm
421                | Opcode::LtImm
422                | Opcode::LeImm
423                | Opcode::GtImm
424                | Opcode::GeImm => {
425                    // The compiler stores immediates as `i32` bit patterns in
426                    // `operand` (`i32::try_from` at emit time), so sign-extend
427                    // exactly like the stack VM does. Without this, `-10` is
428                    // read as `4294967286` and every `x < -10` / `x + -10`
429                    // silently computes the wrong value.
430                    let imm = ins.operand.unwrap_or(0) as i32 as i64;
431                    let lhs = sp.saturating_sub(1);
432                    let dst = lhs;
433                    self.use_reg(dst);
434                    let op = ins.opcode;
435                    if matches!(op, Opcode::AddImm | Opcode::SubImm | Opcode::MulImm) {
436                        self.emit(RegOp::BinImm {
437                            op: bin_kind_imm(op),
438                            dst,
439                            lhs,
440                            imm,
441                        });
442                    } else {
443                        self.emit(RegOp::CmpImm {
444                            op: cmp_kind_imm(op),
445                            dst,
446                            lhs,
447                            imm,
448                        });
449                    }
450                }
451                Opcode::Neg | Opcode::Not | Opcode::BitNot => {
452                    let src = sp.saturating_sub(1);
453                    let dst = src;
454                    self.use_reg(dst);
455                    self.emit(RegOp::Un {
456                        op: un_kind(ins.opcode),
457                        dst,
458                        src,
459                    });
460                }
461                Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge => {
462                    let rhs = sp.saturating_sub(1);
463                    let lhs = sp.saturating_sub(2);
464                    let dst = lhs;
465                    self.use_reg(rhs.max(dst));
466                    self.emit(RegOp::Cmp {
467                        op: cmp_kind(ins.opcode),
468                        dst,
469                        lhs,
470                        rhs,
471                    });
472                    sp = sp.saturating_sub(1);
473                }
474                Opcode::Jump => {
475                    let t = ins.operand.unwrap_or(0) as usize;
476                    let site = self.emit(RegOp::Jump { target: 0 });
477                    self.jumps.push((t, site));
478                }
479                Opcode::JumpIfFalse => {
480                    let t = ins.operand.unwrap_or(0) as usize;
481                    let cond = sp.saturating_sub(1);
482                    self.use_reg(cond);
483                    let site = self.emit(RegOp::JumpIfFalse { target: 0, cond });
484                    self.jumps.push((t, site));
485                }
486                Opcode::JumpIfTrue => {
487                    let t = ins.operand.unwrap_or(0) as usize;
488                    let cond = sp.saturating_sub(1);
489                    self.use_reg(cond);
490                    let site = self.emit(RegOp::JumpIfTrue { target: 0, cond });
491                    self.jumps.push((t, site));
492                }
493                Opcode::Call => {
494                    let argc = ins.operand.unwrap_or(0) as u8;
495                    let argc_us = argc as u16;
496                    // stack: [func, a0, a1, …]
497                    let func = sp.saturating_sub(argc_us + 1);
498                    let args = func + 1;
499                    let dst = func;
500                    self.use_reg(sp.saturating_sub(1).max(dst));
501                    self.emit(RegOp::Call {
502                        dst,
503                        func,
504                        args,
505                        argc,
506                    });
507                    sp = func + 1;
508                }
509                Opcode::TailCall => {
510                    let argc = ins.operand.unwrap_or(0) as u8;
511                    let argc_us = argc as u16;
512                    let func = sp.saturating_sub(argc_us + 1);
513                    let args = func + 1;
514                    self.use_reg(sp.saturating_sub(1));
515                    self.emit(RegOp::TailCall { func, args, argc });
516                    sp = 0;
517                }
518                Opcode::Return => {
519                    let src = sp.saturating_sub(1);
520                    self.use_reg(src);
521                    self.emit(RegOp::Ret { src });
522                    sp = 0;
523                }
524                Opcode::GetProperty => {
525                    let name = ins.operand.unwrap_or(0);
526                    let obj = sp.saturating_sub(1);
527                    let dst = obj;
528                    self.use_reg(dst);
529                    self.emit(RegOp::GetProp { dst, obj, name });
530                }
531                Opcode::SetProperty => {
532                    let name = ins.operand.unwrap_or(0);
533                    let src = sp.saturating_sub(1);
534                    let obj = sp.saturating_sub(2);
535                    self.use_reg(src.max(obj));
536                    self.emit(RegOp::SetProp { obj, name, src });
537                    // SetProperty leaves the object on the stack in the stack IR
538                    // (value becomes the new stack top after consuming both).
539                    // Stack IR: [obj, val] → [obj]. Our src/obj consumed val;
540                    // keep obj as top: sp = obj + 1.
541                    sp = obj + 1;
542                }
543                Opcode::Invoke => {
544                    let packed = ins.operand.unwrap_or(0);
545                    let argc = (packed >> 16) as u8;
546                    let name = packed & 0xFFFF;
547                    let argc_us = argc as u16;
548                    let obj = sp.saturating_sub(argc_us + 1);
549                    let args = obj + 1;
550                    let dst = obj;
551                    self.use_reg(sp.saturating_sub(1).max(dst));
552                    self.emit(RegOp::Invoke {
553                        dst,
554                        obj,
555                        name,
556                        args,
557                        argc,
558                    });
559                    sp = obj + 1;
560                }
561                Opcode::BuildArray => {
562                    let count = ins.operand.unwrap_or(0) as u16;
563                    let start = sp.saturating_sub(count);
564                    let dst = start;
565                    self.use_reg(sp.saturating_sub(1).max(dst));
566                    self.emit(RegOp::BuildArr { dst, start, count });
567                    sp = start + 1;
568                }
569                Opcode::BuildMap => {
570                    let count = ins.operand.unwrap_or(0) as u16; // pairs
571                    let slots = count * 2;
572                    let start = sp.saturating_sub(slots);
573                    let dst = start;
574                    self.use_reg(sp.saturating_sub(1).max(dst));
575                    self.emit(RegOp::BuildMap {
576                        dst,
577                        start,
578                        count: slots,
579                    });
580                    sp = start + 1;
581                }
582                Opcode::IndexGet => {
583                    let idx = sp.saturating_sub(1);
584                    let obj = sp.saturating_sub(2);
585                    let dst = obj;
586                    self.use_reg(idx.max(dst));
587                    self.emit(RegOp::IndexGet { dst, obj, idx });
588                    sp = obj + 1;
589                }
590                Opcode::IndexSet => {
591                    let src = sp.saturating_sub(1);
592                    let idx = sp.saturating_sub(2);
593                    let obj = sp.saturating_sub(3);
594                    self.use_reg(src.max(obj));
595                    self.emit(RegOp::IndexSet { obj, idx, src });
596                    // Stack IR: [obj, idx, val] → [] after IndexSet+Pop in compiler.
597                    sp = obj;
598                }
599                Opcode::CreateClass => {
600                    let k = ins.operand.unwrap_or(0);
601                    let dst = sp;
602                    self.use_reg(dst);
603                    self.emit(RegOp::CreateClass { dst, k });
604                    sp += 1;
605                }
606                Opcode::CreateInstance => {
607                    // Stack: [class, args...] → [instance]
608                    let argc = 0u8; // class call uses Call path in stack IR
609                    let _ = argc;
610                    let class = sp.saturating_sub(1);
611                    let dst = class;
612                    self.use_reg(dst);
613                    self.emit(RegOp::CreateInstance {
614                        dst,
615                        class,
616                        args: class,
617                        argc: 0,
618                    });
619                }
620                Opcode::GetMethod => {
621                    let name = ins.operand.unwrap_or(0);
622                    let obj = sp.saturating_sub(1);
623                    let dst = obj;
624                    self.use_reg(dst);
625                    self.emit(RegOp::GetProp { dst, obj, name });
626                }
627                Opcode::MatchPattern => {
628                    // Stack VM pushes Bool(true); keep the same effect.
629                    let dst = sp;
630                    self.use_reg(dst);
631                    let k = {
632                        // Reuse/add a true constant.
633                        let v = Value::Bool(true);
634                        if let Some(i) = self.out.constants.iter().position(|c| c == &v) {
635                            i as u32
636                        } else {
637                            self.out.constants.push(v);
638                            (self.out.constants.len() - 1) as u32
639                        }
640                    };
641                    self.emit(RegOp::LoadConst { dst, k });
642                }
643                Opcode::MakeClosure => {
644                    let count = ins.operand.unwrap_or(0) as u8;
645                    let count_us = count as u16;
646                    let upvals = sp.saturating_sub(count_us + 1);
647                    let func = sp.saturating_sub(count_us + 1);
648                    // stack: [func, uv…]
649                    let func_r = sp - count_us - 1;
650                    let upvals_r = func_r + 1;
651                    let dst = func_r;
652                    self.use_reg(sp.saturating_sub(1).max(dst));
653                    self.emit(RegOp::MakeClosure {
654                        dst,
655                        func: func_r,
656                        upvals: upvals_r,
657                        count,
658                    });
659                    sp = func_r + 1;
660                    let _ = (upvals, func);
661                }
662                Opcode::Throw => {
663                    let src = sp.saturating_sub(1);
664                    self.use_reg(src);
665                    self.emit(RegOp::Ret { src });
666                    sp = 0;
667                }
668                Opcode::TryBegin | Opcode::TryEnd | Opcode::Nop => {
669                    self.emit(RegOp::Nop);
670                }
671                Opcode::WrapOk | Opcode::WrapErr | Opcode::WrapSome => {
672                    let kind = match ins.opcode {
673                        Opcode::WrapOk => WrapKind::Ok,
674                        Opcode::WrapErr => WrapKind::Err,
675                        _ => WrapKind::Some,
676                    };
677                    let src = sp.saturating_sub(1);
678                    let dst = src;
679                    self.use_reg(dst);
680                    self.emit(RegOp::Wrap { kind, dst, src });
681                }
682                Opcode::UnwrapOk | Opcode::UnwrapErr | Opcode::UnwrapSome => {
683                    let kind = match ins.opcode {
684                        Opcode::UnwrapOk => WrapKind::Ok,
685                        Opcode::UnwrapErr => WrapKind::Err,
686                        _ => WrapKind::Some,
687                    };
688                    // Success: [inner, true] (dst, flag). Fail: [false] at dst,
689                    // and flag is also set false so a following JumpIfFalse
690                    // (which peeks the success-layout top) still branches.
691                    let src = sp.saturating_sub(1);
692                    let dst = src;
693                    let flag = src + 1;
694                    self.use_reg(flag);
695                    self.emit(RegOp::Unwrap {
696                        kind,
697                        dst,
698                        flag,
699                        src,
700                    });
701                    // Dataflow already models out-height as +1 (success layout).
702                    let _ = sp;
703                }
704                Opcode::Try => {
705                    let src = sp.saturating_sub(1);
706                    let dst = src;
707                    self.use_reg(dst);
708                    self.emit(RegOp::TryQ { dst, src });
709                }
710                Opcode::Argc => {
711                    let dst = sp;
712                    self.use_reg(dst);
713                    self.emit(RegOp::Argc { dst });
714                    sp += 1;
715                }
716                Opcode::Slide => {
717                    // Keep the top of the window, drop the `n` values below it
718                    // (`match` arm cleanup: the scrutinee temp and the pattern
719                    // bindings sit under the arm's result).
720                    let n = ins.operand.unwrap_or(0) as u16;
721                    if n > 0 {
722                        let src = sp.saturating_sub(1);
723                        let dst = src.saturating_sub(n);
724                        self.use_reg(src.max(dst));
725                        self.emit(RegOp::Mov { dst, src });
726                        sp = sp.saturating_sub(n);
727                    }
728                }
729                Opcode::Halt => {
730                    let src = sp.saturating_sub(1);
731                    self.use_reg(src);
732                    self.emit(RegOp::Halt { src });
733                    sp = 0;
734                }
735                Opcode::CallDirect => {
736                    let packed = ins.operand.unwrap_or(0);
737                    let chunk = packed >> 16;
738                    let argc = (packed & 0xFFFF) as u8;
739                    let argc_us = argc as u16;
740                    // Stack: [args…] only (no func slot) in CallDirect
741                    let args = sp.saturating_sub(argc_us);
742                    let dst = args;
743                    self.use_reg(sp.saturating_sub(1).max(dst));
744                    self.emit(RegOp::CallChunk {
745                        dst,
746                        chunk,
747                        args,
748                        argc,
749                    });
750                    sp = args + 1;
751                }
752            }
753        }
754        // Patch jumps
755        let mut code = core::mem::take(&mut self.out.code);
756        let ip_map = self.ip_map.clone();
757        let jumps = self.jumps.clone();
758        for (stack_target, site) in jumps {
759            let reg_target = if stack_target < ip_map.len() {
760                ip_map[stack_target]
761            } else {
762                code.len() as u32
763            };
764            match &mut code[site] {
765                RegOp::Jump { ref mut target } => *target = reg_target,
766                RegOp::JumpIfFalse { ref mut target, .. } => *target = reg_target,
767                RegOp::JumpIfTrue { ref mut target, .. } => *target = reg_target,
768                _ => {}
769            }
770        }
771        self.out.code = code;
772        self.out
773    }
774
775    /// Declare parameter counts (for CallChunk arity checks).
776    pub fn with_arity(self, arity: usize, required: usize) -> RegChunk {
777        let mut c = self.translate();
778        c.arity = arity;
779        c.required = required;
780        c
781    }
782
783    pub fn translate_named(self) -> RegChunk {
784        self.translate()
785    }
786}
787
788fn bin_kind(op: Opcode) -> BinKind {
789    match op {
790        Opcode::Add | Opcode::AddImm => BinKind::Add,
791        Opcode::Sub | Opcode::SubImm => BinKind::Sub,
792        Opcode::Mul | Opcode::MulImm => BinKind::Mul,
793        Opcode::Div => BinKind::Div,
794        Opcode::Mod => BinKind::Mod,
795        Opcode::Pow => BinKind::Pow,
796        Opcode::BitAnd => BinKind::BitAnd,
797        Opcode::BitOr => BinKind::BitOr,
798        Opcode::BitXor => BinKind::BitXor,
799        Opcode::Shl => BinKind::Shl,
800        Opcode::Shr => BinKind::Shr,
801        Opcode::And => BinKind::And,
802        Opcode::Or => BinKind::Or,
803        _ => BinKind::Add,
804    }
805}
806
807fn bin_kind_imm(op: Opcode) -> BinKind {
808    match op {
809        Opcode::SubImm => BinKind::Sub,
810        Opcode::MulImm => BinKind::Mul,
811        _ => BinKind::Add,
812    }
813}
814
815fn cmp_kind(op: Opcode) -> CmpKind {
816    match op {
817        Opcode::Eq | Opcode::EqImm => CmpKind::Eq,
818        Opcode::Ne | Opcode::NeImm => CmpKind::Ne,
819        Opcode::Lt | Opcode::LtImm => CmpKind::Lt,
820        Opcode::Le | Opcode::LeImm => CmpKind::Le,
821        Opcode::Gt | Opcode::GtImm => CmpKind::Gt,
822        Opcode::Ge | Opcode::GeImm => CmpKind::Ge,
823        _ => CmpKind::Eq,
824    }
825}
826
827fn cmp_kind_imm(op: Opcode) -> CmpKind {
828    cmp_kind(op)
829}
830
831fn un_kind(op: Opcode) -> UnKind {
832    match op {
833        Opcode::Not => UnKind::Not,
834        Opcode::BitNot => UnKind::BitNot,
835        _ => UnKind::Neg,
836    }
837}
838
839fn is_unwrap(op: Opcode) -> bool {
840    matches!(
841        op,
842        Opcode::UnwrapOk | Opcode::UnwrapErr | Opcode::UnwrapSome
843    )
844}
845
846/// Stack height after `op` given height `h` before it (success layout for Unwrap).
847fn stack_out(op: Opcode, h: u16, operand: Option<u32>) -> u16 {
848    match op {
849        Opcode::Push
850        | Opcode::Dup
851        | Opcode::LoadLocal
852        | Opcode::LoadGlobal
853        | Opcode::LoadUpvalue
854        | Opcode::Argc
855        | Opcode::CreateClass
856        | Opcode::CreateInstance
857        | Opcode::GetMethod
858        | Opcode::MatchPattern => h.saturating_add(1),
859        Opcode::Pop | Opcode::StoreGlobal | Opcode::StoreUpvalue | Opcode::Throw => {
860            h.saturating_sub(1)
861        }
862        Opcode::Slide => h.saturating_sub(operand.unwrap_or(0) as u16),
863        Opcode::StoreLocal => {
864            let idx = operand.unwrap_or(0) as u16;
865            let after = h.saturating_sub(1);
866            if idx >= after {
867                idx + 1
868            } else {
869                after
870            }
871        }
872        Opcode::Add
873        | Opcode::Sub
874        | Opcode::Mul
875        | Opcode::Div
876        | Opcode::Mod
877        | Opcode::Pow
878        | Opcode::BitAnd
879        | Opcode::BitOr
880        | Opcode::BitXor
881        | Opcode::Shl
882        | Opcode::Shr
883        | Opcode::And
884        | Opcode::Or
885        | Opcode::Eq
886        | Opcode::Ne
887        | Opcode::Lt
888        | Opcode::Le
889        | Opcode::Gt
890        | Opcode::Ge
891        | Opcode::IndexGet => h.saturating_sub(1),
892        Opcode::IndexSet => h.saturating_sub(2),
893        Opcode::Call => {
894            let argc = operand.unwrap_or(0) as u16;
895            // pop func + argc, push result
896            h.saturating_sub(argc + 1).saturating_add(1)
897        }
898        Opcode::CallDirect => {
899            let argc = operand.unwrap_or(0) as u16;
900            h.saturating_sub(argc).saturating_add(1)
901        }
902        Opcode::Invoke => {
903            let packed = operand.unwrap_or(0);
904            let argc = (packed >> 16) as u16;
905            h.saturating_sub(argc + 1).saturating_add(1)
906        }
907        Opcode::BuildArray => {
908            let count = operand.unwrap_or(0) as u16;
909            h.saturating_sub(count).saturating_add(1)
910        }
911        Opcode::BuildMap => {
912            let pairs = operand.unwrap_or(0) as u16;
913            h.saturating_sub(pairs * 2).saturating_add(1)
914        }
915        Opcode::SetProperty => h.saturating_sub(1),
916        // pop object, push property value → net 0
917        Opcode::GetProperty => h,
918        Opcode::MakeClosure => {
919            let count = operand.unwrap_or(0) as u16;
920            h.saturating_sub(count + 1).saturating_add(1)
921        }
922        Opcode::WrapOk | Opcode::WrapErr | Opcode::WrapSome => h,
923        // Success layout: pop 1, push 2 → +1. Fail path is handled by the
924        // JumpIfFalse taken-edge rule (one less value).
925        Opcode::UnwrapOk | Opcode::UnwrapErr | Opcode::UnwrapSome => h.saturating_add(1),
926        Opcode::Try => h,
927        Opcode::AddImm
928        | Opcode::SubImm
929        | Opcode::MulImm
930        | Opcode::EqImm
931        | Opcode::NeImm
932        | Opcode::LtImm
933        | Opcode::LeImm
934        | Opcode::GtImm
935        | Opcode::GeImm
936        | Opcode::Neg
937        | Opcode::Not
938        | Opcode::BitNot
939        | Opcode::Jump
940        | Opcode::JumpIfFalse
941        | Opcode::JumpIfTrue
942        | Opcode::TryBegin
943        | Opcode::TryEnd
944        | Opcode::Nop => h,
945        Opcode::Return | Opcode::Halt | Opcode::TailCall => h,
946    }
947}
948
949/// Dataflow stack height before each instruction.
950fn compute_heights(code: &[crate::Instruction], param_slots: u16) -> Vec<u16> {
951    let n = code.len();
952    if n == 0 {
953        return Vec::new();
954    }
955    let mut height = vec![u16::MAX; n];
956    let mut work = alloc::collections::VecDeque::new();
957    height[0] = param_slots;
958    work.push_back(0usize);
959
960    while let Some(ip) = work.pop_front() {
961        if ip >= n {
962            continue;
963        }
964        let h = height[ip];
965        let ins = &code[ip];
966        let out = stack_out(ins.opcode, h, ins.operand);
967        match ins.opcode {
968            Opcode::Jump => {
969                let t = ins.operand.unwrap_or(0) as usize;
970                if t < n {
971                    let cur = height[t];
972                    if cur == u16::MAX {
973                        height[t] = out;
974                        work.push_back(t);
975                    } else if cur != out {
976                        let m = cur.max(out);
977                        if m != cur {
978                            height[t] = m;
979                            work.push_back(t);
980                        }
981                    }
982                }
983            }
984            Opcode::JumpIfFalse | Opcode::JumpIfTrue => {
985                // Peeks top; both edges keep `out` unless the previous op was
986                // Unwrap, whose fail path has one fewer value.
987                let taken = if ip > 0 && is_unwrap(code[ip - 1].opcode) {
988                    out.saturating_sub(1)
989                } else {
990                    out
991                };
992                // fallthrough
993                let ft = ip + 1;
994                if ft < n {
995                    let cur = height[ft];
996                    if cur == u16::MAX {
997                        height[ft] = out;
998                        work.push_back(ft);
999                    } else if cur != out {
1000                        let m = cur.max(out);
1001                        if m != cur {
1002                            height[ft] = m;
1003                            work.push_back(ft);
1004                        }
1005                    }
1006                }
1007                let t = ins.operand.unwrap_or(0) as usize;
1008                if t < n {
1009                    let cur = height[t];
1010                    if cur == u16::MAX {
1011                        height[t] = taken;
1012                        work.push_back(t);
1013                    } else if cur != taken {
1014                        let m = cur.max(taken);
1015                        if m != cur {
1016                            height[t] = m;
1017                            work.push_back(t);
1018                        }
1019                    }
1020                }
1021            }
1022            // A frame-exiting instruction has no successor: `Return`, `Halt`,
1023            // `TailCall` and `Throw` never fall through to `ip + 1`. Letting
1024            // them propagate a height there (the old code did) meant that the
1025            // *unreachable* instruction after an early `return` joined the
1026            // height of the taken branch into code that the fall-through path
1027            // also reaches — e.g. `if c { return x } var total = …` — so every
1028            // register above the parameter slots shifted by one and the
1029            // function's own locals were read from (and written to) the wrong
1030            // register. Instructions that nothing reaches keep the
1031            // `param_slots` fallback applied at the end of this function.
1032            Opcode::Return | Opcode::Halt | Opcode::TailCall | Opcode::Throw => {}
1033            _ => {
1034                let ft = ip + 1;
1035                if ft < n {
1036                    let cur = height[ft];
1037                    if cur == u16::MAX {
1038                        height[ft] = out;
1039                        work.push_back(ft);
1040                    } else if cur != out {
1041                        let m = cur.max(out);
1042                        if m != cur {
1043                            height[ft] = m;
1044                            work.push_back(ft);
1045                        }
1046                    }
1047                }
1048            }
1049        }
1050    }
1051
1052    for h in height.iter_mut() {
1053        if *h == u16::MAX {
1054            *h = param_slots;
1055        }
1056    }
1057    height
1058}