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}