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    #[allow(unused_assignments)]
260    pub fn translate(mut self) -> RegChunk {
261        let start_sp = self.start_sp;
262        self.heights = compute_heights(self.src_code, start_sp);
263        self.out.n_regs = self.out.n_regs.max(start_sp);
264        for (sip, ins) in self.src_code.iter().enumerate() {
265            self.ip_map.push(self.out.code.len() as u32);
266            let mut sp: u16 = self.heights[sip];
267            match ins.opcode {
268                Opcode::Push => {
269                    let k = ins.operand.unwrap_or(0);
270                    let dst = sp;
271                    self.use_reg(dst);
272                    self.emit(RegOp::LoadConst { dst, k });
273                    sp += 1;
274                }
275                Opcode::Pop => {
276                    sp = sp.saturating_sub(1);
277                }
278                Opcode::Dup => {
279                    let src = sp.saturating_sub(1);
280                    let dst = sp;
281                    self.use_reg(dst);
282                    self.emit(RegOp::Mov { dst, src });
283                    sp += 1;
284                }
285                Opcode::LoadLocal => {
286                    let idx = ins.operand.unwrap_or(0) as u16;
287                    let dst = sp;
288                    self.use_reg(idx.max(dst));
289                    self.emit(RegOp::Mov { dst, src: idx });
290                    sp += 1;
291                }
292                Opcode::StoreLocal => {
293                    let idx = ins.operand.unwrap_or(0) as u16;
294                    let src = sp.saturating_sub(1);
295                    self.use_reg(idx.max(src));
296                    self.emit(RegOp::Mov { dst: idx, src });
297                    sp = sp.saturating_sub(1);
298                }
299                Opcode::LoadGlobal => {
300                    let slot = ins.operand.unwrap_or(0);
301                    let dst = sp;
302                    self.use_reg(dst);
303                    self.emit(RegOp::LoadGlobal { dst, slot });
304                    sp += 1;
305                }
306                Opcode::StoreGlobal => {
307                    let slot = ins.operand.unwrap_or(0);
308                    let src = sp.saturating_sub(1);
309                    self.use_reg(src);
310                    self.emit(RegOp::StoreGlobal { slot, src });
311                    sp = sp.saturating_sub(1);
312                }
313                Opcode::LoadUpvalue => {
314                    let idx = ins.operand.unwrap_or(0);
315                    let dst = sp;
316                    self.use_reg(dst);
317                    self.emit(RegOp::LoadUpvalue { dst, idx });
318                    sp += 1;
319                }
320                Opcode::StoreUpvalue => {
321                    let idx = ins.operand.unwrap_or(0);
322                    let src = sp.saturating_sub(1);
323                    self.use_reg(src);
324                    self.emit(RegOp::StoreUpvalue { idx, src });
325                    sp = sp.saturating_sub(1);
326                }
327                Opcode::Add
328                | Opcode::Sub
329                | Opcode::Mul
330                | Opcode::Div
331                | Opcode::Mod
332                | Opcode::Pow
333                | Opcode::BitAnd
334                | Opcode::BitOr
335                | Opcode::BitXor
336                | Opcode::Shl
337                | Opcode::Shr
338                | Opcode::And
339                | Opcode::Or => {
340                    let kind = bin_kind(ins.opcode);
341                    let rhs = sp.saturating_sub(1);
342                    let lhs = sp.saturating_sub(2);
343                    let dst = lhs;
344                    self.use_reg(rhs.max(dst));
345                    self.emit(RegOp::Bin {
346                        op: kind,
347                        dst,
348                        lhs,
349                        rhs,
350                    });
351                    sp = sp.saturating_sub(1);
352                }
353                Opcode::AddImm
354                | Opcode::SubImm
355                | Opcode::MulImm
356                | Opcode::EqImm
357                | Opcode::NeImm
358                | Opcode::LtImm
359                | Opcode::LeImm
360                | Opcode::GtImm
361                | Opcode::GeImm => {
362                    let imm = ins.operand.unwrap_or(0) as i64;
363                    let lhs = sp.saturating_sub(1);
364                    let dst = lhs;
365                    self.use_reg(dst);
366                    let op = ins.opcode;
367                    if matches!(op, Opcode::AddImm | Opcode::SubImm | Opcode::MulImm) {
368                        self.emit(RegOp::BinImm {
369                            op: bin_kind_imm(op),
370                            dst,
371                            lhs,
372                            imm,
373                        });
374                    } else {
375                        self.emit(RegOp::CmpImm {
376                            op: cmp_kind_imm(op),
377                            dst,
378                            lhs,
379                            imm,
380                        });
381                    }
382                }
383                Opcode::Neg | Opcode::Not | Opcode::BitNot => {
384                    let src = sp.saturating_sub(1);
385                    let dst = src;
386                    self.use_reg(dst);
387                    self.emit(RegOp::Un {
388                        op: un_kind(ins.opcode),
389                        dst,
390                        src,
391                    });
392                }
393                Opcode::Eq | Opcode::Ne | Opcode::Lt | Opcode::Le | Opcode::Gt | Opcode::Ge => {
394                    let rhs = sp.saturating_sub(1);
395                    let lhs = sp.saturating_sub(2);
396                    let dst = lhs;
397                    self.use_reg(rhs.max(dst));
398                    self.emit(RegOp::Cmp {
399                        op: cmp_kind(ins.opcode),
400                        dst,
401                        lhs,
402                        rhs,
403                    });
404                    sp = sp.saturating_sub(1);
405                }
406                Opcode::Jump => {
407                    let t = ins.operand.unwrap_or(0) as usize;
408                    let site = self.emit(RegOp::Jump { target: 0 });
409                    self.jumps.push((t, site));
410                }
411                Opcode::JumpIfFalse => {
412                    let t = ins.operand.unwrap_or(0) as usize;
413                    let cond = sp.saturating_sub(1);
414                    self.use_reg(cond);
415                    let site = self.emit(RegOp::JumpIfFalse { target: 0, cond });
416                    self.jumps.push((t, site));
417                }
418                Opcode::JumpIfTrue => {
419                    let t = ins.operand.unwrap_or(0) as usize;
420                    let cond = sp.saturating_sub(1);
421                    self.use_reg(cond);
422                    let site = self.emit(RegOp::JumpIfTrue { target: 0, cond });
423                    self.jumps.push((t, site));
424                }
425                Opcode::Call => {
426                    let argc = ins.operand.unwrap_or(0) as u8;
427                    let argc_us = argc as u16;
428                    // stack: [func, a0, a1, …]
429                    let func = sp.saturating_sub(argc_us + 1);
430                    let args = func + 1;
431                    let dst = func;
432                    self.use_reg(sp.saturating_sub(1).max(dst));
433                    self.emit(RegOp::Call {
434                        dst,
435                        func,
436                        args,
437                        argc,
438                    });
439                    sp = func + 1;
440                }
441                Opcode::TailCall => {
442                    let argc = ins.operand.unwrap_or(0) as u8;
443                    let argc_us = argc as u16;
444                    let func = sp.saturating_sub(argc_us + 1);
445                    let args = func + 1;
446                    self.use_reg(sp.saturating_sub(1));
447                    self.emit(RegOp::TailCall { func, args, argc });
448                    sp = 0;
449                }
450                Opcode::Return => {
451                    let src = sp.saturating_sub(1);
452                    self.use_reg(src);
453                    self.emit(RegOp::Ret { src });
454                    sp = 0;
455                }
456                Opcode::GetProperty => {
457                    let name = ins.operand.unwrap_or(0);
458                    let obj = sp.saturating_sub(1);
459                    let dst = obj;
460                    self.use_reg(dst);
461                    self.emit(RegOp::GetProp { dst, obj, name });
462                }
463                Opcode::SetProperty => {
464                    let name = ins.operand.unwrap_or(0);
465                    let src = sp.saturating_sub(1);
466                    let obj = sp.saturating_sub(2);
467                    self.use_reg(src.max(obj));
468                    self.emit(RegOp::SetProp { obj, name, src });
469                    // SetProperty leaves the object on the stack in the stack IR
470                    // (value becomes the new stack top after consuming both).
471                    // Stack IR: [obj, val] → [obj]. Our src/obj consumed val;
472                    // keep obj as top: sp = obj + 1.
473                    sp = obj + 1;
474                }
475                Opcode::Invoke => {
476                    let packed = ins.operand.unwrap_or(0);
477                    let argc = (packed >> 16) as u8;
478                    let name = packed & 0xFFFF;
479                    let argc_us = argc as u16;
480                    let obj = sp.saturating_sub(argc_us + 1);
481                    let args = obj + 1;
482                    let dst = obj;
483                    self.use_reg(sp.saturating_sub(1).max(dst));
484                    self.emit(RegOp::Invoke {
485                        dst,
486                        obj,
487                        name,
488                        args,
489                        argc,
490                    });
491                    sp = obj + 1;
492                }
493                Opcode::BuildArray => {
494                    let count = ins.operand.unwrap_or(0) as u16;
495                    let start = sp.saturating_sub(count);
496                    let dst = start;
497                    self.use_reg(sp.saturating_sub(1).max(dst));
498                    self.emit(RegOp::BuildArr { dst, start, count });
499                    sp = start + 1;
500                }
501                Opcode::BuildMap => {
502                    let count = ins.operand.unwrap_or(0) as u16; // pairs
503                    let slots = count * 2;
504                    let start = sp.saturating_sub(slots);
505                    let dst = start;
506                    self.use_reg(sp.saturating_sub(1).max(dst));
507                    self.emit(RegOp::BuildMap {
508                        dst,
509                        start,
510                        count: slots,
511                    });
512                    sp = start + 1;
513                }
514                Opcode::IndexGet => {
515                    let idx = sp.saturating_sub(1);
516                    let obj = sp.saturating_sub(2);
517                    let dst = obj;
518                    self.use_reg(idx.max(dst));
519                    self.emit(RegOp::IndexGet { dst, obj, idx });
520                    sp = obj + 1;
521                }
522                Opcode::IndexSet => {
523                    let src = sp.saturating_sub(1);
524                    let idx = sp.saturating_sub(2);
525                    let obj = sp.saturating_sub(3);
526                    self.use_reg(src.max(obj));
527                    self.emit(RegOp::IndexSet { obj, idx, src });
528                    // Stack IR: [obj, idx, val] → [] after IndexSet+Pop in compiler.
529                    sp = obj;
530                }
531                Opcode::CreateClass => {
532                    let k = ins.operand.unwrap_or(0);
533                    let dst = sp;
534                    self.use_reg(dst);
535                    self.emit(RegOp::CreateClass { dst, k });
536                    sp += 1;
537                }
538                Opcode::CreateInstance => {
539                    // Stack: [class, args...] → [instance]
540                    let argc = 0u8; // class call uses Call path in stack IR
541                    let _ = argc;
542                    let class = sp.saturating_sub(1);
543                    let dst = class;
544                    self.use_reg(dst);
545                    self.emit(RegOp::CreateInstance {
546                        dst,
547                        class,
548                        args: class,
549                        argc: 0,
550                    });
551                }
552                Opcode::GetMethod => {
553                    let name = ins.operand.unwrap_or(0);
554                    let obj = sp.saturating_sub(1);
555                    let dst = obj;
556                    self.use_reg(dst);
557                    self.emit(RegOp::GetProp { dst, obj, name });
558                }
559                Opcode::MatchPattern => {
560                    // Stack VM pushes Bool(true); keep the same effect.
561                    let dst = sp;
562                    self.use_reg(dst);
563                    let k = {
564                        // Reuse/add a true constant.
565                        let v = Value::Bool(true);
566                        if let Some(i) = self.out.constants.iter().position(|c| c == &v) {
567                            i as u32
568                        } else {
569                            self.out.constants.push(v);
570                            (self.out.constants.len() - 1) as u32
571                        }
572                    };
573                    self.emit(RegOp::LoadConst { dst, k });
574                }
575                Opcode::MakeClosure => {
576                    let count = ins.operand.unwrap_or(0) as u8;
577                    let count_us = count as u16;
578                    let upvals = sp.saturating_sub(count_us + 1);
579                    let func = sp.saturating_sub(count_us + 1);
580                    // stack: [func, uv…]
581                    let func_r = sp - count_us - 1;
582                    let upvals_r = func_r + 1;
583                    let dst = func_r;
584                    self.use_reg(sp.saturating_sub(1).max(dst));
585                    self.emit(RegOp::MakeClosure {
586                        dst,
587                        func: func_r,
588                        upvals: upvals_r,
589                        count,
590                    });
591                    sp = func_r + 1;
592                    let _ = (upvals, func);
593                }
594                Opcode::Throw => {
595                    let src = sp.saturating_sub(1);
596                    self.use_reg(src);
597                    self.emit(RegOp::Ret { src });
598                    sp = 0;
599                }
600                Opcode::TryBegin | Opcode::TryEnd | Opcode::Nop => {
601                    self.emit(RegOp::Nop);
602                }
603                Opcode::WrapOk | Opcode::WrapErr | Opcode::WrapSome => {
604                    let kind = match ins.opcode {
605                        Opcode::WrapOk => WrapKind::Ok,
606                        Opcode::WrapErr => WrapKind::Err,
607                        _ => WrapKind::Some,
608                    };
609                    let src = sp.saturating_sub(1);
610                    let dst = src;
611                    self.use_reg(dst);
612                    self.emit(RegOp::Wrap { kind, dst, src });
613                }
614                Opcode::UnwrapOk | Opcode::UnwrapErr | Opcode::UnwrapSome => {
615                    let kind = match ins.opcode {
616                        Opcode::UnwrapOk => WrapKind::Ok,
617                        Opcode::UnwrapErr => WrapKind::Err,
618                        _ => WrapKind::Some,
619                    };
620                    // Success: [inner, true] (dst, flag). Fail: [false] at dst,
621                    // and flag is also set false so a following JumpIfFalse
622                    // (which peeks the success-layout top) still branches.
623                    let src = sp.saturating_sub(1);
624                    let dst = src;
625                    let flag = src + 1;
626                    self.use_reg(flag);
627                    self.emit(RegOp::Unwrap {
628                        kind,
629                        dst,
630                        flag,
631                        src,
632                    });
633                    // Dataflow already models out-height as +1 (success layout).
634                    let _ = sp;
635                }
636                Opcode::Try => {
637                    let src = sp.saturating_sub(1);
638                    let dst = src;
639                    self.use_reg(dst);
640                    self.emit(RegOp::TryQ { dst, src });
641                }
642                Opcode::Argc => {
643                    let dst = sp;
644                    self.use_reg(dst);
645                    self.emit(RegOp::Argc { dst });
646                    sp += 1;
647                }
648                Opcode::Halt => {
649                    let src = sp.saturating_sub(1);
650                    self.use_reg(src);
651                    self.emit(RegOp::Halt { src });
652                    sp = 0;
653                }
654                Opcode::CallDirect => {
655                    let packed = ins.operand.unwrap_or(0);
656                    let chunk = packed >> 16;
657                    let argc = (packed & 0xFFFF) as u8;
658                    let argc_us = argc as u16;
659                    // Stack: [args…] only (no func slot) in CallDirect
660                    let args = sp.saturating_sub(argc_us);
661                    let dst = args;
662                    self.use_reg(sp.saturating_sub(1).max(dst));
663                    self.emit(RegOp::CallChunk {
664                        dst,
665                        chunk,
666                        args,
667                        argc,
668                    });
669                    sp = args + 1;
670                }
671            }
672        }
673        // Patch jumps
674        let mut code = core::mem::take(&mut self.out.code);
675        let ip_map = self.ip_map.clone();
676        let jumps = self.jumps.clone();
677        for (stack_target, site) in jumps {
678            let reg_target = if stack_target < ip_map.len() {
679                ip_map[stack_target]
680            } else {
681                code.len() as u32
682            };
683            match &mut code[site] {
684                RegOp::Jump { ref mut target } => *target = reg_target,
685                RegOp::JumpIfFalse { ref mut target, .. } => *target = reg_target,
686                RegOp::JumpIfTrue { ref mut target, .. } => *target = reg_target,
687                _ => {}
688            }
689        }
690        self.out.code = code;
691        self.out
692    }
693
694    /// Declare parameter counts (for CallChunk arity checks).
695    pub fn with_arity(self, arity: usize, required: usize) -> RegChunk {
696        let mut c = self.translate();
697        c.arity = arity;
698        c.required = required;
699        c
700    }
701
702    pub fn translate_named(self) -> RegChunk {
703        self.translate()
704    }
705}
706
707fn bin_kind(op: Opcode) -> BinKind {
708    match op {
709        Opcode::Add | Opcode::AddImm => BinKind::Add,
710        Opcode::Sub | Opcode::SubImm => BinKind::Sub,
711        Opcode::Mul | Opcode::MulImm => BinKind::Mul,
712        Opcode::Div => BinKind::Div,
713        Opcode::Mod => BinKind::Mod,
714        Opcode::Pow => BinKind::Pow,
715        Opcode::BitAnd => BinKind::BitAnd,
716        Opcode::BitOr => BinKind::BitOr,
717        Opcode::BitXor => BinKind::BitXor,
718        Opcode::Shl => BinKind::Shl,
719        Opcode::Shr => BinKind::Shr,
720        Opcode::And => BinKind::And,
721        Opcode::Or => BinKind::Or,
722        _ => BinKind::Add,
723    }
724}
725
726fn bin_kind_imm(op: Opcode) -> BinKind {
727    match op {
728        Opcode::SubImm => BinKind::Sub,
729        Opcode::MulImm => BinKind::Mul,
730        _ => BinKind::Add,
731    }
732}
733
734fn cmp_kind(op: Opcode) -> CmpKind {
735    match op {
736        Opcode::Eq | Opcode::EqImm => CmpKind::Eq,
737        Opcode::Ne | Opcode::NeImm => CmpKind::Ne,
738        Opcode::Lt | Opcode::LtImm => CmpKind::Lt,
739        Opcode::Le | Opcode::LeImm => CmpKind::Le,
740        Opcode::Gt | Opcode::GtImm => CmpKind::Gt,
741        Opcode::Ge | Opcode::GeImm => CmpKind::Ge,
742        _ => CmpKind::Eq,
743    }
744}
745
746fn cmp_kind_imm(op: Opcode) -> CmpKind {
747    cmp_kind(op)
748}
749
750fn un_kind(op: Opcode) -> UnKind {
751    match op {
752        Opcode::Not => UnKind::Not,
753        Opcode::BitNot => UnKind::BitNot,
754        _ => UnKind::Neg,
755    }
756}
757
758fn is_unwrap(op: Opcode) -> bool {
759    matches!(
760        op,
761        Opcode::UnwrapOk | Opcode::UnwrapErr | Opcode::UnwrapSome
762    )
763}
764
765/// Stack height after `op` given height `h` before it (success layout for Unwrap).
766fn stack_out(op: Opcode, h: u16, operand: Option<u32>) -> u16 {
767    match op {
768        Opcode::Push
769        | Opcode::Dup
770        | Opcode::LoadLocal
771        | Opcode::LoadGlobal
772        | Opcode::LoadUpvalue
773        | Opcode::Argc
774        | Opcode::CreateClass
775        | Opcode::CreateInstance
776        | Opcode::GetMethod
777        | Opcode::MatchPattern => h.saturating_add(1),
778        Opcode::Pop | Opcode::StoreGlobal | Opcode::StoreUpvalue | Opcode::Throw => {
779            h.saturating_sub(1)
780        }
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}