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::{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/// A register-mode chunk (function body).
179#[derive(Debug, Clone)]
180pub struct RegChunk {
181    pub name: String,
182    pub code: Vec<RegOp>,
183    pub constants: Vec<Value>,
184    /// Number of registers the frame must allocate (max index + 1).
185    pub n_regs: u16,
186    /// Declared parameter count (max).
187    pub arity: usize,
188    pub required: usize,
189}
190
191impl RegChunk {
192    pub fn new(name: impl Into<String>) -> Self {
193        Self {
194            name: name.into(),
195            code: Vec::new(),
196            constants: Vec::new(),
197            n_regs: 0,
198            arity: 0,
199            required: 0,
200        }
201    }
202}
203
204/// Lower a stack instruction stream to register form.
205///
206/// Strategy: simulate the operand stack with **dataflow stack heights** so
207/// control-flow joins (match arms, if/else, Unwrap success vs fail) get the
208/// correct register indices. Registers are the frame's contiguous stack slots
209/// (locals occupy `0..start_sp`, expression temps grow above).
210pub struct StackToRegister<'a> {
211    src_code: &'a [crate::Instruction],
212    #[allow(dead_code)]
213    src_consts: &'a [Value],
214    out: RegChunk,
215    /// Maps stack IP → register IP (for jump patching).
216    ip_map: Vec<u32>,
217    /// Pending (stack_target, reg_site) jumps to patch.
218    jumps: Vec<(usize, usize)>,
219    /// Initial stack height (= parameter / `this` slot count).
220    start_sp: u16,
221    /// Height before each source instruction (dataflow).
222    heights: Vec<u16>,
223}
224
225impl<'a> StackToRegister<'a> {
226    pub fn new(name: &str, code: &'a [crate::Instruction], consts: &'a [Value]) -> Self {
227        let mut out = RegChunk::new(name);
228        out.constants = consts.to_vec();
229        out.arity = 0;
230        out.required = 0;
231        Self {
232            src_code: code,
233            src_consts: consts,
234            out,
235            ip_map: Vec::new(),
236            jumps: Vec::new(),
237            start_sp: 0,
238            heights: Vec::new(),
239        }
240    }
241
242    /// Set the frame's initial stack height (parameter count, including `this`).
243    pub fn with_start_sp(mut self, start_sp: u16) -> Self {
244        self.start_sp = start_sp;
245        self
246    }
247
248    fn emit(&mut self, op: RegOp) -> usize {
249        self.out.code.push(op);
250        self.out.code.len() - 1
251    }
252
253    fn use_reg(&mut self, r: u16) {
254        if r + 1 > self.out.n_regs {
255            self.out.n_regs = r + 1;
256        }
257    }
258
259    pub fn translate(mut self) -> RegChunk {
260        let start_sp = self.start_sp;
261        self.heights = compute_heights(self.src_code, start_sp);
262        self.out.n_regs = self.out.n_regs.max(start_sp);
263        for (sip, ins) in self.src_code.iter().enumerate() {
264            self.ip_map.push(self.out.code.len() as u32);
265            let mut sp: u16 = self.heights[sip];
266            match ins.opcode {
267                Opcode::Push => {
268                    let k = ins.operand.unwrap_or(0);
269                    let dst = sp;
270                    self.use_reg(dst);
271                    self.emit(RegOp::LoadConst { dst, k });
272                    sp += 1;
273                }
274                Opcode::Pop => {
275                    sp = sp.saturating_sub(1);
276                }
277                Opcode::Dup => {
278                    let src = sp.saturating_sub(1);
279                    let dst = sp;
280                    self.use_reg(dst);
281                    self.emit(RegOp::Mov { dst, src });
282                    sp += 1;
283                }
284                Opcode::LoadLocal => {
285                    let idx = ins.operand.unwrap_or(0) as u16;
286                    let dst = sp;
287                    self.use_reg(idx.max(dst));
288                    self.emit(RegOp::Mov { dst, src: idx });
289                    sp += 1;
290                }
291                Opcode::StoreLocal => {
292                    let idx = ins.operand.unwrap_or(0) as u16;
293                    let src = sp.saturating_sub(1);
294                    self.use_reg(idx.max(src));
295                    self.emit(RegOp::Mov { dst: idx, src });
296                    sp = sp.saturating_sub(1);
297                }
298                Opcode::LoadGlobal => {
299                    let slot = ins.operand.unwrap_or(0);
300                    let dst = sp;
301                    self.use_reg(dst);
302                    self.emit(RegOp::LoadGlobal { dst, slot });
303                    sp += 1;
304                }
305                Opcode::StoreGlobal => {
306                    let slot = ins.operand.unwrap_or(0);
307                    let src = sp.saturating_sub(1);
308                    self.use_reg(src);
309                    self.emit(RegOp::StoreGlobal { slot, src });
310                    sp = sp.saturating_sub(1);
311                }
312                Opcode::LoadUpvalue => {
313                    let idx = ins.operand.unwrap_or(0);
314                    let dst = sp;
315                    self.use_reg(dst);
316                    self.emit(RegOp::LoadUpvalue { dst, idx });
317                    sp += 1;
318                }
319                Opcode::StoreUpvalue => {
320                    let idx = ins.operand.unwrap_or(0);
321                    let src = sp.saturating_sub(1);
322                    self.use_reg(src);
323                    self.emit(RegOp::StoreUpvalue { idx, src });
324                    sp = sp.saturating_sub(1);
325                }
326                Opcode::Add
327                | Opcode::Sub
328                | Opcode::Mul
329                | Opcode::Div
330                | Opcode::Mod
331                | Opcode::Pow
332                | Opcode::BitAnd
333                | Opcode::BitOr
334                | Opcode::BitXor
335                | Opcode::Shl
336                | Opcode::Shr
337                | Opcode::And
338                | Opcode::Or => {
339                    let kind = bin_kind(ins.opcode);
340                    let rhs = sp.saturating_sub(1);
341                    let lhs = sp.saturating_sub(2);
342                    let dst = lhs;
343                    self.use_reg(rhs.max(dst));
344                    self.emit(RegOp::Bin {
345                        op: kind,
346                        dst,
347                        lhs,
348                        rhs,
349                    });
350                    sp = sp.saturating_sub(1);
351                }
352                Opcode::AddImm
353                | Opcode::SubImm
354                | Opcode::MulImm
355                | Opcode::EqImm
356                | Opcode::NeImm
357                | Opcode::LtImm
358                | Opcode::LeImm
359                | Opcode::GtImm
360                | Opcode::GeImm => {
361                    let imm = ins.operand.unwrap_or(0) as i64;
362                    let lhs = sp.saturating_sub(1);
363                    let dst = lhs;
364                    self.use_reg(dst);
365                    let op = ins.opcode;
366                    if matches!(op, Opcode::AddImm | Opcode::SubImm | Opcode::MulImm) {
367                        self.emit(RegOp::BinImm {
368                            op: bin_kind_imm(op),
369                            dst,
370                            lhs,
371                            imm,
372                        });
373                    } else {
374                        self.emit(RegOp::CmpImm {
375                            op: cmp_kind_imm(op),
376                            dst,
377                            lhs,
378                            imm,
379                        });
380                    }
381                }
382                Opcode::Neg | Opcode::Not | Opcode::BitNot => {
383                    let src = sp.saturating_sub(1);
384                    let dst = src;
385                    self.use_reg(dst);
386                    self.emit(RegOp::Un {
387                        op: un_kind(ins.opcode),
388                        dst,
389                        src,
390                    });
391                }
392                Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge => {
393                    let rhs = sp.saturating_sub(1);
394                    let lhs = sp.saturating_sub(2);
395                    let dst = lhs;
396                    self.use_reg(rhs.max(dst));
397                    self.emit(RegOp::Cmp {
398                        op: cmp_kind(ins.opcode),
399                        dst,
400                        lhs,
401                        rhs,
402                    });
403                    sp = sp.saturating_sub(1);
404                }
405                Opcode::Jump => {
406                    let t = ins.operand.unwrap_or(0) as usize;
407                    let site = self.emit(RegOp::Jump { target: 0 });
408                    self.jumps.push((t, site));
409                }
410                Opcode::JumpIfFalse => {
411                    let t = ins.operand.unwrap_or(0) as usize;
412                    let cond = sp.saturating_sub(1);
413                    self.use_reg(cond);
414                    let site = self.emit(RegOp::JumpIfFalse { target: 0, cond });
415                    self.jumps.push((t, site));
416                }
417                Opcode::JumpIfTrue => {
418                    let t = ins.operand.unwrap_or(0) as usize;
419                    let cond = sp.saturating_sub(1);
420                    self.use_reg(cond);
421                    let site = self.emit(RegOp::JumpIfTrue { target: 0, cond });
422                    self.jumps.push((t, site));
423                }
424                Opcode::Call => {
425                    let argc = ins.operand.unwrap_or(0) as u8;
426                    let argc_us = argc as u16;
427                    // stack: [func, a0, a1, …]
428                    let func = sp.saturating_sub(argc_us + 1);
429                    let args = func + 1;
430                    let dst = func;
431                    self.use_reg(sp.saturating_sub(1).max(dst));
432                    self.emit(RegOp::Call {
433                        dst,
434                        func,
435                        args,
436                        argc,
437                    });
438                    sp = func + 1;
439                }
440                Opcode::TailCall => {
441                    let argc = ins.operand.unwrap_or(0) as u8;
442                    let argc_us = argc as u16;
443                    let func = sp.saturating_sub(argc_us + 1);
444                    let args = func + 1;
445                    self.use_reg(sp.saturating_sub(1));
446                    self.emit(RegOp::TailCall { func, args, argc });
447                    sp = 0;
448                }
449                Opcode::Return => {
450                    let src = sp.saturating_sub(1);
451                    self.use_reg(src);
452                    self.emit(RegOp::Ret { src });
453                    sp = 0;
454                }
455                Opcode::GetProperty => {
456                    let name = ins.operand.unwrap_or(0);
457                    let obj = sp.saturating_sub(1);
458                    let dst = obj;
459                    self.use_reg(dst);
460                    self.emit(RegOp::GetProp { dst, obj, name });
461                }
462                Opcode::SetProperty => {
463                    let name = ins.operand.unwrap_or(0);
464                    let src = sp.saturating_sub(1);
465                    let obj = sp.saturating_sub(2);
466                    self.use_reg(src.max(obj));
467                    self.emit(RegOp::SetProp { obj, name, src });
468                    // SetProperty leaves the object on the stack in the stack IR
469                    // (value becomes the new stack top after consuming both).
470                    // Stack IR: [obj, val] → [obj]. Our src/obj consumed val;
471                    // keep obj as top: sp = obj + 1.
472                    sp = obj + 1;
473                }
474                Opcode::Invoke => {
475                    let packed = ins.operand.unwrap_or(0);
476                    let argc = (packed >> 16) as u8;
477                    let name = packed & 0xFFFF;
478                    let argc_us = argc as u16;
479                    let obj = sp.saturating_sub(argc_us + 1);
480                    let args = obj + 1;
481                    let dst = obj;
482                    self.use_reg(sp.saturating_sub(1).max(dst));
483                    self.emit(RegOp::Invoke {
484                        dst,
485                        obj,
486                        name,
487                        args,
488                        argc,
489                    });
490                    sp = obj + 1;
491                }
492                Opcode::BuildArray => {
493                    let count = ins.operand.unwrap_or(0) as u16;
494                    let start = sp.saturating_sub(count);
495                    let dst = start;
496                    self.use_reg(sp.saturating_sub(1).max(dst));
497                    self.emit(RegOp::BuildArr { dst, start, count });
498                    sp = start + 1;
499                }
500                Opcode::BuildMap => {
501                    let count = ins.operand.unwrap_or(0) as u16; // pairs
502                    let slots = count * 2;
503                    let start = sp.saturating_sub(slots);
504                    let dst = start;
505                    self.use_reg(sp.saturating_sub(1).max(dst));
506                    self.emit(RegOp::BuildMap {
507                        dst,
508                        start,
509                        count: slots,
510                    });
511                    sp = start + 1;
512                }
513                Opcode::IndexGet => {
514                    let idx = sp.saturating_sub(1);
515                    let obj = sp.saturating_sub(2);
516                    let dst = obj;
517                    self.use_reg(idx.max(dst));
518                    self.emit(RegOp::IndexGet { dst, obj, idx });
519                    sp = obj + 1;
520                }
521                Opcode::IndexSet => {
522                    let src = sp.saturating_sub(1);
523                    let idx = sp.saturating_sub(2);
524                    let obj = sp.saturating_sub(3);
525                    self.use_reg(src.max(obj));
526                    self.emit(RegOp::IndexSet { obj, idx, src });
527                    // Stack IR: [obj, idx, val] → [] after IndexSet+Pop in compiler.
528                    sp = obj;
529                }
530                Opcode::CreateClass => {
531                    let k = ins.operand.unwrap_or(0);
532                    let dst = sp;
533                    self.use_reg(dst);
534                    self.emit(RegOp::CreateClass { dst, k });
535                    sp += 1;
536                }
537                Opcode::CreateInstance => {
538                    // Stack: [class, args...] → [instance]
539                    let argc = 0u8; // class call uses Call path in stack IR
540                    let _ = argc;
541                    let class = sp.saturating_sub(1);
542                    let dst = class;
543                    self.use_reg(dst);
544                    self.emit(RegOp::CreateInstance {
545                        dst,
546                        class,
547                        args: class,
548                        argc: 0,
549                    });
550                }
551                Opcode::GetMethod => {
552                    let name = ins.operand.unwrap_or(0);
553                    let obj = sp.saturating_sub(1);
554                    let dst = obj;
555                    self.use_reg(dst);
556                    self.emit(RegOp::GetProp { dst, obj, name });
557                }
558                Opcode::MatchPattern => {
559                    // Stack VM pushes Bool(true); keep the same effect.
560                    let dst = sp;
561                    self.use_reg(dst);
562                    let k = {
563                        // Reuse/add a true constant.
564                        let v = Value::Bool(true);
565                        if let Some(i) = self.out.constants.iter().position(|c| c == &v) {
566                            i as u32
567                        } else {
568                            self.out.constants.push(v);
569                            (self.out.constants.len() - 1) as u32
570                        }
571                    };
572                    self.emit(RegOp::LoadConst { dst, k });
573                }
574                Opcode::MakeClosure => {
575                    let count = ins.operand.unwrap_or(0) as u8;
576                    let count_us = count as u16;
577                    let upvals = sp.saturating_sub(count_us + 1);
578                    let func = sp.saturating_sub(count_us + 1);
579                    // stack: [func, uv…]
580                    let func_r = sp - count_us - 1;
581                    let upvals_r = func_r + 1;
582                    let dst = func_r;
583                    self.use_reg(sp.saturating_sub(1).max(dst));
584                    self.emit(RegOp::MakeClosure {
585                        dst,
586                        func: func_r,
587                        upvals: upvals_r,
588                        count,
589                    });
590                    sp = func_r + 1;
591                    let _ = (upvals, func);
592                }
593                Opcode::Throw => {
594                    let src = sp.saturating_sub(1);
595                    self.use_reg(src);
596                    self.emit(RegOp::Ret { src });
597                    sp = 0;
598                }
599                Opcode::TryBegin | Opcode::TryEnd | Opcode::Nop => {
600                    self.emit(RegOp::Nop);
601                }
602                Opcode::WrapOk | Opcode::WrapErr | Opcode::WrapSome => {
603                    let kind = match ins.opcode {
604                        Opcode::WrapOk => WrapKind::Ok,
605                        Opcode::WrapErr => WrapKind::Err,
606                        _ => WrapKind::Some,
607                    };
608                    let src = sp.saturating_sub(1);
609                    let dst = src;
610                    self.use_reg(dst);
611                    self.emit(RegOp::Wrap { kind, dst, src });
612                }
613                Opcode::UnwrapOk | Opcode::UnwrapErr | Opcode::UnwrapSome => {
614                    let kind = match ins.opcode {
615                        Opcode::UnwrapOk => WrapKind::Ok,
616                        Opcode::UnwrapErr => WrapKind::Err,
617                        _ => WrapKind::Some,
618                    };
619                    // Success: [inner, true] (dst, flag). Fail: [false] at dst,
620                    // and flag is also set false so a following JumpIfFalse
621                    // (which peeks the success-layout top) still branches.
622                    let src = sp.saturating_sub(1);
623                    let dst = src;
624                    let flag = src + 1;
625                    self.use_reg(flag);
626                    self.emit(RegOp::Unwrap {
627                        kind,
628                        dst,
629                        flag,
630                        src,
631                    });
632                    // Dataflow already models out-height as +1 (success layout).
633                    let _ = sp;
634                }
635                Opcode::Try => {
636                    let src = sp.saturating_sub(1);
637                    let dst = src;
638                    self.use_reg(dst);
639                    self.emit(RegOp::TryQ { dst, src });
640                }
641                Opcode::Argc => {
642                    let dst = sp;
643                    self.use_reg(dst);
644                    self.emit(RegOp::Argc { dst });
645                    sp += 1;
646                }
647                Opcode::Halt => {
648                    let src = sp.saturating_sub(1);
649                    self.use_reg(src);
650                    self.emit(RegOp::Halt { src });
651                    sp = 0;
652                }
653                Opcode::CallDirect => {
654                    let packed = ins.operand.unwrap_or(0);
655                    let chunk = packed >> 16;
656                    let argc = (packed & 0xFFFF) as u8;
657                    let argc_us = argc as u16;
658                    // Stack: [args…] only (no func slot) in CallDirect
659                    let args = sp.saturating_sub(argc_us);
660                    let dst = args;
661                    self.use_reg(sp.saturating_sub(1).max(dst));
662                    self.emit(RegOp::CallChunk {
663                        dst,
664                        chunk,
665                        args,
666                        argc,
667                    });
668                    sp = args + 1;
669                }
670            }
671        }
672        // Patch jumps
673        let mut code = core::mem::take(&mut self.out.code);
674        let ip_map = self.ip_map.clone();
675        let jumps = self.jumps.clone();
676        for (stack_target, site) in jumps {
677            let reg_target = if stack_target < ip_map.len() {
678                ip_map[stack_target]
679            } else {
680                code.len() as u32
681            };
682            match &mut code[site] {
683                RegOp::Jump { ref mut target } => *target = reg_target,
684                RegOp::JumpIfFalse { ref mut target, .. } => *target = reg_target,
685                RegOp::JumpIfTrue { ref mut target, .. } => *target = reg_target,
686                _ => {}
687            }
688        }
689        self.out.code = code;
690        self.out
691    }
692
693    /// Declare parameter counts (for CallChunk arity checks).
694    pub fn with_arity(self, arity: usize, required: usize) -> RegChunk {
695        let mut c = self.translate();
696        c.arity = arity;
697        c.required = required;
698        c
699    }
700
701    pub fn translate_named(self) -> RegChunk {
702        self.translate()
703    }
704}
705
706fn bin_kind(op: Opcode) -> BinKind {
707    match op {
708        Opcode::Add | Opcode::AddImm => BinKind::Add,
709        Opcode::Sub | Opcode::SubImm => BinKind::Sub,
710        Opcode::Mul | Opcode::MulImm => BinKind::Mul,
711        Opcode::Div => BinKind::Div,
712        Opcode::Mod => BinKind::Mod,
713        Opcode::Pow => BinKind::Pow,
714        Opcode::BitAnd => BinKind::BitAnd,
715        Opcode::BitOr => BinKind::BitOr,
716        Opcode::BitXor => BinKind::BitXor,
717        Opcode::Shl => BinKind::Shl,
718        Opcode::Shr => BinKind::Shr,
719        Opcode::And => BinKind::And,
720        Opcode::Or => BinKind::Or,
721        _ => BinKind::Add,
722    }
723}
724
725fn bin_kind_imm(op: Opcode) -> BinKind {
726    match op {
727        Opcode::SubImm => BinKind::Sub,
728        Opcode::MulImm => BinKind::Mul,
729        _ => BinKind::Add,
730    }
731}
732
733fn cmp_kind(op: Opcode) -> CmpKind {
734    match op {
735        Opcode::Eq | Opcode::EqImm => CmpKind::Eq,
736        Opcode::Ne | Opcode::NeImm => CmpKind::Ne,
737        Opcode::Lt | Opcode::LtImm => CmpKind::Lt,
738        Opcode::Le | Opcode::LeImm => CmpKind::Le,
739        Opcode::Gt | Opcode::GtImm => CmpKind::Gt,
740        Opcode::Ge | Opcode::GeImm => CmpKind::Ge,
741        _ => CmpKind::Eq,
742    }
743}
744
745fn cmp_kind_imm(op: Opcode) -> CmpKind {
746    cmp_kind(op)
747}
748
749fn un_kind(op: Opcode) -> UnKind {
750    match op {
751        Opcode::Not => UnKind::Not,
752        Opcode::BitNot => UnKind::BitNot,
753        _ => UnKind::Neg,
754    }
755}
756
757fn is_unwrap(op: Opcode) -> bool {
758    matches!(
759        op,
760        Opcode::UnwrapOk | Opcode::UnwrapErr | Opcode::UnwrapSome
761    )
762}
763
764/// Stack height after `op` given height `h` before it (success layout for Unwrap).
765fn stack_out(op: Opcode, h: u16, operand: Option<u32>) -> u16 {
766    match op {
767        Opcode::Push
768        | Opcode::Dup
769        | Opcode::LoadLocal
770        | Opcode::LoadGlobal
771        | Opcode::LoadUpvalue
772        | Opcode::Argc
773        | Opcode::CreateClass
774        | Opcode::CreateInstance
775        | Opcode::GetMethod
776        | Opcode::MatchPattern => h.saturating_add(1),
777        Opcode::Pop
778        | Opcode::StoreGlobal
779        | Opcode::StoreUpvalue
780        | Opcode::Throw => h.saturating_sub(1),
781        Opcode::StoreLocal => {
782            let idx = operand.unwrap_or(0) as u16;
783            let after = h.saturating_sub(1);
784            if idx >= after {
785                idx + 1
786            } else {
787                after
788            }
789        }
790        Opcode::Add
791        | Opcode::Sub
792        | Opcode::Mul
793        | Opcode::Div
794        | Opcode::Mod
795        | Opcode::Pow
796        | Opcode::BitAnd
797        | Opcode::BitOr
798        | Opcode::BitXor
799        | Opcode::Shl
800        | Opcode::Shr
801        | Opcode::And
802        | Opcode::Or
803        | Opcode::Eq
804        | Opcode::Ne
805        | Opcode::Lt
806        | Opcode::Le
807        | Opcode::Gt
808        | Opcode::Ge
809        | Opcode::IndexGet => h.saturating_sub(1),
810        Opcode::IndexSet => h.saturating_sub(2),
811        Opcode::Call => {
812            let argc = operand.unwrap_or(0) as u16;
813            // pop func + argc, push result
814            h.saturating_sub(argc + 1).saturating_add(1)
815        }
816        Opcode::CallDirect => {
817            let argc = operand.unwrap_or(0) as u16;
818            h.saturating_sub(argc).saturating_add(1)
819        }
820        Opcode::Invoke => {
821            let packed = operand.unwrap_or(0);
822            let argc = (packed >> 16) as u16;
823            h.saturating_sub(argc + 1).saturating_add(1)
824        }
825        Opcode::BuildArray => {
826            let count = operand.unwrap_or(0) as u16;
827            h.saturating_sub(count).saturating_add(1)
828        }
829        Opcode::BuildMap => {
830            let pairs = operand.unwrap_or(0) as u16;
831            h.saturating_sub(pairs * 2).saturating_add(1)
832        }
833        Opcode::SetProperty => h.saturating_sub(1),
834        // pop object, push property value → net 0
835        Opcode::GetProperty => h,
836        Opcode::MakeClosure => {
837            let count = operand.unwrap_or(0) as u16;
838            h.saturating_sub(count + 1).saturating_add(1)
839        }
840        Opcode::WrapOk | Opcode::WrapErr | Opcode::WrapSome => h,
841        // Success layout: pop 1, push 2 → +1. Fail path is handled by the
842        // JumpIfFalse taken-edge rule (one less value).
843        Opcode::UnwrapOk | Opcode::UnwrapErr | Opcode::UnwrapSome => h.saturating_add(1),
844        Opcode::Try => h,
845        Opcode::AddImm
846        | Opcode::SubImm
847        | Opcode::MulImm
848        | Opcode::EqImm
849        | Opcode::NeImm
850        | Opcode::LtImm
851        | Opcode::LeImm
852        | Opcode::GtImm
853        | Opcode::GeImm
854        | Opcode::Neg
855        | Opcode::Not
856        | Opcode::BitNot
857        | Opcode::Jump
858        | Opcode::JumpIfFalse
859        | Opcode::JumpIfTrue
860        | Opcode::TryBegin
861        | Opcode::TryEnd
862        | Opcode::Nop => h,
863        Opcode::Return | Opcode::Halt | Opcode::TailCall => h,
864    }
865}
866
867/// Dataflow stack height before each instruction.
868fn compute_heights(code: &[crate::Instruction], start_sp: u16) -> Vec<u16> {
869    let n = code.len();
870    if n == 0 {
871        return Vec::new();
872    }
873    let mut height = vec![u16::MAX; n];
874    let mut work = alloc::collections::VecDeque::new();
875    height[0] = start_sp;
876    work.push_back(0usize);
877
878    while let Some(ip) = work.pop_front() {
879        if ip >= n {
880            continue;
881        }
882        let h = height[ip];
883        let ins = &code[ip];
884        let out = stack_out(ins.opcode, h, ins.operand);
885        match ins.opcode {
886            Opcode::Jump => {
887                let t = ins.operand.unwrap_or(0) as usize;
888                if t < n {
889                    let cur = height[t];
890                    if cur == u16::MAX {
891                        height[t] = out;
892                        work.push_back(t);
893                    } else if cur != out {
894                        let m = cur.max(out);
895                        if m != cur {
896                            height[t] = m;
897                            work.push_back(t);
898                        }
899                    }
900                }
901            }
902            Opcode::JumpIfFalse | Opcode::JumpIfTrue => {
903                // Peeks top; both edges keep `out` unless the previous op was
904                // Unwrap, whose fail path has one fewer value.
905                let taken = if ip > 0 && is_unwrap(code[ip - 1].opcode) {
906                    out.saturating_sub(1)
907                } else {
908                    out
909                };
910                // fallthrough
911                let ft = ip + 1;
912                if ft < n {
913                    let cur = height[ft];
914                    if cur == u16::MAX {
915                        height[ft] = out;
916                        work.push_back(ft);
917                    } else if cur != out {
918                        let m = cur.max(out);
919                        if m != cur {
920                            height[ft] = m;
921                            work.push_back(ft);
922                        }
923                    }
924                }
925                let t = ins.operand.unwrap_or(0) as usize;
926                if t < n {
927                    let cur = height[t];
928                    if cur == u16::MAX {
929                        height[t] = taken;
930                        work.push_back(t);
931                    } else if cur != taken {
932                        let m = cur.max(taken);
933                        if m != cur {
934                            height[t] = m;
935                            work.push_back(t);
936                        }
937                    }
938                }
939            }
940            Opcode::Return | Opcode::Halt | Opcode::TailCall | Opcode::Throw => {
941                if ip + 1 < n {
942                    let cur = height[ip + 1];
943                    if cur == u16::MAX {
944                        height[ip + 1] = out;
945                        work.push_back(ip + 1);
946                    }
947                }
948            }
949            _ => {
950                let ft = ip + 1;
951                if ft < n {
952                    let cur = height[ft];
953                    if cur == u16::MAX {
954                        height[ft] = out;
955                        work.push_back(ft);
956                    } else if cur != out {
957                        let m = cur.max(out);
958                        if m != cur {
959                            height[ft] = m;
960                            work.push_back(ft);
961                        }
962                    }
963                }
964            }
965        }
966    }
967
968    for h in height.iter_mut() {
969        if *h == u16::MAX {
970            *h = start_sp;
971        }
972    }
973    height
974}