Skip to main content

tishlang_bytecode/
opcode.rs

1//! Bytecode opcodes for the Tish VM.
2
3/// Stack-based bytecode opcodes.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[repr(u8)]
6pub enum Opcode {
7    /// No operation
8    Nop = 0,
9    /// Push constant from constants table (operand: u16 index)
10    LoadConst = 1,
11    /// Load variable from scope (operand: u16 name index)
12    LoadVar = 2,
13    /// Store top of stack to variable (operand: u16 name index)
14    StoreVar = 3,
15    /// Discard top of stack
16    Pop = 4,
17    /// Duplicate top of stack
18    Dup = 5,
19    /// Call function with n args (operand: u16 arg count). Callee and args on stack.
20    Call = 6,
21    /// Return from function. Top of stack is return value.
22    Return = 7,
23    /// Unconditional jump forward (operand: u16 byte offset)
24    Jump = 8,
25    /// Pop top; if falsy, jump forward (operand: u16 byte offset)
26    JumpIfFalse = 9,
27    /// Unconditional jump backward (operand: u16 byte offset)
28    JumpBack = 10,
29    /// Binary operation (operand: u8 BinOp variant). Pops left, right; pushes result.
30    BinOp = 11,
31    /// Unary operation (operand: u8 UnaryOp variant). Pops operand; pushes result.
32    UnaryOp = 12,
33    /// Get property: obj.prop (operand: u16 prop name index). Pops obj; pushes value.
34    GetMember = 13,
35    /// Set property: obj.prop = val (operand: u16 prop name index). Pops obj, val.
36    SetMember = 14,
37    /// Get index: obj[idx]. Pops obj, idx; pushes value.
38    GetIndex = 15,
39    /// Set index: obj[idx] = val. Pops obj, idx, val.
40    SetIndex = 16,
41    /// Create array with n elements (operand: u16 count). Elements on stack.
42    NewArray = 17,
43    /// Create object with n key-value pairs (operand: u16 count). Keys and vals interleaved.
44    NewObject = 18,
45    /// Load from global scope (operand: u16 name index)
46    LoadGlobal = 19,
47    /// Store to global scope (operand: u16 name index)
48    StoreGlobal = 20,
49    /// Create closure: push function (operand: u16 chunk index for nested function)
50    Closure = 21,
51    /// Pop and discard n values (operand: u16 count)
52    PopN = 22,
53    /// Load `this` or receiver (for method calls)
54    LoadThis = 23,
55    /// Throw: pop value, unwind to catch handler, push value, jump
56    Throw = 24,
57    /// EnterTry: push handler (catch offset u16). Catch offset = bytes from end of this insn.
58    EnterTry = 25,
59    /// ExitTry: pop try handler
60    ExitTry = 26,
61    /// Concat arrays: pop right, pop left, push left.concat(right). For spread.
62    ConcatArray = 27,
63    /// Merge objects: pop right, pop left, push Object.assign({}, left, right). For object spread.
64    MergeObject = 28,
65    /// Call with spread: pop args array, pop callee, call callee(...args).
66    CallSpread = 29,
67    /// Get property optional: like GetMember but returns Null if obj is null or prop missing.
68    GetMemberOptional = 30,
69    /// Pop array, sort numerically in place (operand: u8 0=asc, 1=desc), push array.
70    /// Fast path for arr.sort((a,b)=>a-b) / arr.sort((a,b)=>b-a).
71    ArraySortNumeric = 31,
72    /// Pop array, sort by numeric property (operands: u16 prop_name_const_idx, u16 0=asc/1=desc).
73    /// Fast path for arr.sort((a,b)=>a.prop-b.prop).
74    ArraySortByProperty = 32,
75    /// arr.map(x => x) - identity, returns array clone.
76    ArrayMapIdentity = 33,
77    /// arr.map(x => x op const) or arr.map(x => const op x). Operands: u8 binop, u16 const_idx, u8 param_left (0=param on left e.g. x*2, 1=param on right e.g. 2*x).
78    ArrayMapBinOp = 34,
79    /// arr.filter(x => x op const) or arr.filter(x => const op x). Operands: u8 binop, u16 const_idx, u8 param_left. Keeps elements where result is truthy.
80    ArrayFilterBinOp = 35,
81    /// Load built-in module export. Operands: u16 spec_const_idx, u16 export_name_const_idx. Pushes Value.
82    LoadNativeExport = 36,
83    /// `new callee(...args)` (operand: u16 arg count). Stack: callee, then args — same as Call.
84    Construct = 37,
85    /// `new callee(...spread)` — stack: args array, then callee (same order as CallSpread).
86    ConstructSpread = 38,
87    /// Declare `let`/`const` in the current lexical frame (operand: u16 name index). Pops value.
88    /// Does not walk enclosing scopes or globals (unlike [`StoreVar`]).
89    DeclareVar = 39,
90    /// Enter a block scope; pairs with [`ExitBlock`].
91    EnterBlock = 40,
92    /// Exit innermost block scope and restore shadowed bindings.
93    ExitBlock = 41,
94    /// Like [`DeclareVar`] but does not record block-scope undo (for `for`/`for-of` header bindings).
95    DeclareVarPlain = 42,
96    /// Pop the `await` operand value; if it is a `Promise`, block until settled, push the result,
97    /// or unwind to `catch` like `Throw` on rejection.
98    AwaitPromise = 43,
99    /// Load a local variable by frame slot (operand: u16 slot). Fast path: direct
100    /// index into the current call frame's locals, no name lookup.
101    LoadLocal = 44,
102    /// Store top of stack into a local frame slot (operand: u16 slot). Leaves nothing.
103    StoreLocal = 45,
104    /// Load a captured variable from an enclosing frame (operands: u16 hops, u16 slot).
105    /// Walks `hops` parent frames, then indexes `slot`.
106    LoadUpvalue = 46,
107    /// Store top of stack into an enclosing frame slot (operands: u16 hops, u16 slot).
108    StoreUpvalue = 47,
109    /// Begin a per-iteration binding region for a loop variable (operand: u16 name index).
110    /// Registers the name so closures created in the loop body snapshot it into a fresh overlay
111    /// (ES `let` per-iteration semantics); the rest of the frame stays shared. Emitted only when
112    /// the loop body creates a closure, so closure-free (hot) loops are untouched.
113    LoopVarsBegin = 48,
114    /// End the innermost per-iteration binding region (no operand).
115    LoopVarsEnd = 49,
116    /// Push `Bool(param_index >= argc)` — true when the positional argument at `param_index`
117    /// was not supplied by the caller (operand: u16 param index). Emitted by the function
118    /// prologue so default parameter values apply only for *missing* args, matching the
119    /// interpreter: an explicit `null` argument does NOT trigger the default.
120    ArgMissing = 50,
121    /// Direct self-recursive call (operand: u16 arg count). Emitted by the compiler ONLY when a
122    /// `fn NAME` body calls `NAME(args)` and `NAME` is provably the function itself (not shadowed
123    /// by a param/local, not reassigned anywhere in the body). Args are on the stack as for `Call`,
124    /// but the callee is implicitly the currently-executing chunk — no name lookup, no closure
125    /// dispatch. The numeric JIT lowers this to a native recursive call (the big recursion win);
126    /// the VM runs the current chunk directly. Behaviour is identical to `LoadVar NAME; Call argc`.
127    SelfCall = 51,
128    /// Normalize the top-of-stack iterable for `for…of`: a JS iterator object (one with a
129    /// callable `next()` returning `{ value, done }`, e.g. a `Map`/`Set` `.values()` result)
130    /// is drained into an array; arrays/strings/anything else pass through unchanged. Emitted
131    /// right after the iterable expression so the existing index-based loop can iterate it.
132    IterNormalize = 52,
133    /// `delete obj[key]` / `delete obj.prop`. Pops `[obj, key]`, removes the property
134    /// (objects: drop the key; arrays: set the index to a null hole), pushes `true`.
135    DeleteIndex = 53,
136    /// String-builder append for statement-position `acc += rhs` where `acc` is a frame slot local
137    /// (operand: u16 slot index). Pops `rhs`; appends it to the accumulator in amortized O(1) by
138    /// keeping a growable buffer for the slot (see the frame-local builder in `run_chunk`). Leaves
139    /// nothing on the stack — only emitted where the assignment's result is discarded. Slots are
140    /// frame-private (never captured), so the buffer needs no cross-frame sharing.
141    AppendLocal = 54,
142    /// #186 — apply a unary `Math.<fn>` intrinsic to the top-of-stack number: pop one value, push
143    /// `Math.fn(x)`. The u16 operand is the [`MathUnaryFn`] id. Emitted by the compiler ONLY when
144    /// `Math` is provably the global builtin (never shadowed in the program), so the numeric JIT can
145    /// lower it to a native op / libcall without a runtime shape guard. Behaviour is identical to
146    /// `LoadVar Math; GetMember fn; <arg>; Call 1` on a number.
147    MathUnary = 55,
148}
149
150/// The unary `Math` functions the [`Opcode::MathUnary`] fast path recognizes (#186). f64→f64;
151/// the discriminant is the opcode operand. Kept in sync with the VM handler and the JIT lowering.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153#[repr(u16)]
154pub enum MathUnaryFn {
155    Sqrt = 0,
156    Cbrt = 1,
157    Abs = 2,
158    Floor = 3,
159    Ceil = 4,
160    Round = 5,
161    Trunc = 6,
162    Sign = 7,
163    Sin = 8,
164    Cos = 9,
165    Tan = 10,
166    Asin = 11,
167    Acos = 12,
168    Atan = 13,
169    Sinh = 14,
170    Cosh = 15,
171    Tanh = 16,
172    Exp = 17,
173    Log = 18,
174    Log2 = 19,
175    Log10 = 20,
176}
177
178impl MathUnaryFn {
179    /// Map a `Math.<name>` member to its id, or `None` if it isn't a supported unary intrinsic.
180    pub fn from_name(name: &str) -> Option<MathUnaryFn> {
181        Some(match name {
182            "sqrt" => MathUnaryFn::Sqrt,
183            "cbrt" => MathUnaryFn::Cbrt,
184            "abs" => MathUnaryFn::Abs,
185            "floor" => MathUnaryFn::Floor,
186            "ceil" => MathUnaryFn::Ceil,
187            "round" => MathUnaryFn::Round,
188            "trunc" => MathUnaryFn::Trunc,
189            "sign" => MathUnaryFn::Sign,
190            "sin" => MathUnaryFn::Sin,
191            "cos" => MathUnaryFn::Cos,
192            "tan" => MathUnaryFn::Tan,
193            "asin" => MathUnaryFn::Asin,
194            "acos" => MathUnaryFn::Acos,
195            "atan" => MathUnaryFn::Atan,
196            "sinh" => MathUnaryFn::Sinh,
197            "cosh" => MathUnaryFn::Cosh,
198            "tanh" => MathUnaryFn::Tanh,
199            "exp" => MathUnaryFn::Exp,
200            "log" => MathUnaryFn::Log,
201            "log2" => MathUnaryFn::Log2,
202            "log10" => MathUnaryFn::Log10,
203            _ => return None,
204        })
205    }
206
207    /// Decode an opcode operand to the fn id (safe match — no transmute).
208    pub fn from_u16(v: u16) -> Option<MathUnaryFn> {
209        use MathUnaryFn::*;
210        Some(match v {
211            0 => Sqrt,
212            1 => Cbrt,
213            2 => Abs,
214            3 => Floor,
215            4 => Ceil,
216            5 => Round,
217            6 => Trunc,
218            7 => Sign,
219            8 => Sin,
220            9 => Cos,
221            10 => Tan,
222            11 => Asin,
223            12 => Acos,
224            13 => Atan,
225            14 => Sinh,
226            15 => Cosh,
227            16 => Tanh,
228            17 => Exp,
229            18 => Log,
230            19 => Log2,
231            20 => Log10,
232            _ => return None,
233        })
234    }
235
236    /// Apply the intrinsic (the single source of truth for VM + JIT + interp result parity).
237    #[inline]
238    pub fn apply(self, x: f64) -> f64 {
239        match self {
240            MathUnaryFn::Sqrt => x.sqrt(),
241            MathUnaryFn::Cbrt => x.cbrt(),
242            MathUnaryFn::Abs => x.abs(),
243            MathUnaryFn::Floor => x.floor(),
244            MathUnaryFn::Ceil => x.ceil(),
245            // JS `Math.round`: ties toward +∞, `[-0.5, 0) → -0`; EXACTLY `tishlang_builtins::math::
246            // round_f64` (replicated here — this crate sits below builtins — so VM/JIT == interp).
247            MathUnaryFn::Round => {
248                if x.is_nan() || x.is_infinite() || x == 0.0 {
249                    x
250                } else if (-0.5..0.5).contains(&x) {
251                    if x < 0.0 {
252                        -0.0
253                    } else {
254                        0.0
255                    }
256                } else {
257                    (x + 0.5).floor()
258                }
259            }
260            MathUnaryFn::Trunc => x.trunc(),
261            // JS `Math.sign` (matches `tishlang_builtins::math::sign`): NaN→NaN, else ±1; and for
262            // ±0 return `x` unchanged so `Math.sign(-0)` is `-0` (`x < 0.0` is false for -0, so it
263            // lands in the else — bare `0.0` would drop the sign).
264            MathUnaryFn::Sign => {
265                if x.is_nan() {
266                    f64::NAN
267                } else if x > 0.0 {
268                    1.0
269                } else if x < 0.0 {
270                    -1.0
271                } else {
272                    x
273                }
274            }
275            MathUnaryFn::Sin => x.sin(),
276            MathUnaryFn::Cos => x.cos(),
277            MathUnaryFn::Tan => x.tan(),
278            MathUnaryFn::Asin => x.asin(),
279            MathUnaryFn::Acos => x.acos(),
280            MathUnaryFn::Atan => x.atan(),
281            MathUnaryFn::Sinh => x.sinh(),
282            MathUnaryFn::Cosh => x.cosh(),
283            MathUnaryFn::Tanh => x.tanh(),
284            MathUnaryFn::Exp => x.exp(),
285            MathUnaryFn::Log => x.ln(),
286            MathUnaryFn::Log2 => x.log2(),
287            MathUnaryFn::Log10 => x.log10(),
288        }
289    }
290}
291
292impl Opcode {
293    /// Decode byte to opcode. Safe for b in 0..=55 (matches #[repr(u8)] discriminants).
294    #[inline]
295    pub fn from_u8(b: u8) -> Option<Opcode> {
296        if b <= 55 {
297            Some(unsafe { std::mem::transmute::<u8, Opcode>(b) })
298        } else {
299            None
300        }
301    }
302
303    /// Size in bytes of this instruction at `ip` (including operands). Returns None if truncated.
304    pub fn instruction_size(self, code: &[u8], ip: usize) -> Option<usize> {
305        let size = match self {
306            Opcode::Nop
307            | Opcode::Pop
308            | Opcode::Dup
309            | Opcode::Return
310            | Opcode::ExitTry
311            | Opcode::ConcatArray
312            | Opcode::MergeObject
313            | Opcode::GetIndex
314            | Opcode::SetIndex
315            | Opcode::Throw
316            | Opcode::ArrayMapIdentity
317            | Opcode::CallSpread
318            | Opcode::ConstructSpread
319            | Opcode::EnterBlock
320            | Opcode::ExitBlock
321            | Opcode::LoopVarsEnd
322            | Opcode::IterNormalize
323            | Opcode::DeleteIndex
324            | Opcode::AwaitPromise => 1,
325            Opcode::ArraySortByProperty
326            | Opcode::ArrayMapBinOp
327            | Opcode::ArrayFilterBinOp
328            | Opcode::LoadNativeExport
329            | Opcode::LoadUpvalue
330            | Opcode::StoreUpvalue => 5,
331            // LoadLocal / StoreLocal take a single u16 operand → 3 bytes (default).
332            _ => 3,
333        };
334        if ip + size > code.len() {
335            return None;
336        }
337        Some(size)
338    }
339}