1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! Bytecode opcodes for the Tish VM.
/// Stack-based bytecode opcodes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Opcode {
/// No operation
Nop = 0,
/// Push constant from constants table (operand: u16 index)
LoadConst = 1,
/// Load variable from scope (operand: u16 name index)
LoadVar = 2,
/// Store top of stack to variable (operand: u16 name index)
StoreVar = 3,
/// Discard top of stack
Pop = 4,
/// Duplicate top of stack
Dup = 5,
/// Call function with n args (operand: u16 arg count). Callee and args on stack.
Call = 6,
/// Return from function. Top of stack is return value.
Return = 7,
/// Unconditional jump forward (operand: u16 byte offset)
Jump = 8,
/// Pop top; if falsy, jump forward (operand: u16 byte offset)
JumpIfFalse = 9,
/// Unconditional jump backward (operand: u16 byte offset)
JumpBack = 10,
/// Binary operation (operand: u8 BinOp variant). Pops left, right; pushes result.
BinOp = 11,
/// Unary operation (operand: u8 UnaryOp variant). Pops operand; pushes result.
UnaryOp = 12,
/// Get property: obj.prop (operand: u16 prop name index). Pops obj; pushes value.
GetMember = 13,
/// Set property: obj.prop = val (operand: u16 prop name index). Pops obj, val.
SetMember = 14,
/// Get index: obj[idx]. Pops obj, idx; pushes value.
GetIndex = 15,
/// Set index: obj[idx] = val. Pops obj, idx, val.
SetIndex = 16,
/// Create array with n elements (operand: u16 count). Elements on stack.
NewArray = 17,
/// Create object with n key-value pairs (operand: u16 count). Keys and vals interleaved.
NewObject = 18,
/// Load from global scope (operand: u16 name index)
LoadGlobal = 19,
/// Store to global scope (operand: u16 name index)
StoreGlobal = 20,
/// Create closure: push function (operand: u16 chunk index for nested function)
Closure = 21,
/// Pop and discard n values (operand: u16 count)
PopN = 22,
/// Load `this` or receiver (for method calls)
LoadThis = 23,
/// Throw: pop value, unwind to catch handler, push value, jump
Throw = 24,
/// EnterTry: push handler (catch offset u16). Catch offset = bytes from end of this insn.
EnterTry = 25,
/// ExitTry: pop try handler
ExitTry = 26,
/// Concat arrays: pop right, pop left, push left.concat(right). For spread.
ConcatArray = 27,
/// Merge objects: pop right, pop left, push Object.assign({}, left, right). For object spread.
MergeObject = 28,
/// Call with spread: pop args array, pop callee, call callee(...args).
CallSpread = 29,
/// Get property optional: like GetMember but returns Null if obj is null or prop missing.
GetMemberOptional = 30,
/// Pop array, sort numerically in place (operand: u8 0=asc, 1=desc), push array.
/// Fast path for arr.sort((a,b)=>a-b) / arr.sort((a,b)=>b-a).
ArraySortNumeric = 31,
/// Pop array, sort by numeric property (operands: u16 prop_name_const_idx, u16 0=asc/1=desc).
/// Fast path for arr.sort((a,b)=>a.prop-b.prop).
ArraySortByProperty = 32,
/// arr.map(x => x) - identity, returns array clone.
ArrayMapIdentity = 33,
/// 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).
ArrayMapBinOp = 34,
/// 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.
ArrayFilterBinOp = 35,
/// Load built-in module export. Operands: u16 spec_const_idx, u16 export_name_const_idx. Pushes Value.
LoadNativeExport = 36,
/// `new callee(...args)` (operand: u16 arg count). Stack: callee, then args — same as Call.
Construct = 37,
/// `new callee(...spread)` — stack: args array, then callee (same order as CallSpread).
ConstructSpread = 38,
/// Declare `let`/`const` in the current lexical frame (operand: u16 name index). Pops value.
/// Does not walk enclosing scopes or globals (unlike [`StoreVar`]).
DeclareVar = 39,
/// Enter a block scope; pairs with [`ExitBlock`].
EnterBlock = 40,
/// Exit innermost block scope and restore shadowed bindings.
ExitBlock = 41,
/// Like [`DeclareVar`] but does not record block-scope undo (for `for`/`for-of` header bindings).
DeclareVarPlain = 42,
/// Pop the `await` operand value; if it is a `Promise`, block until settled, push the result,
/// or unwind to `catch` like `Throw` on rejection.
AwaitPromise = 43,
/// Load a local variable by frame slot (operand: u16 slot). Fast path: direct
/// index into the current call frame's locals, no name lookup.
LoadLocal = 44,
/// Store top of stack into a local frame slot (operand: u16 slot). Leaves nothing.
StoreLocal = 45,
/// Load a captured variable from an enclosing frame (operands: u16 hops, u16 slot).
/// Walks `hops` parent frames, then indexes `slot`.
LoadUpvalue = 46,
/// Store top of stack into an enclosing frame slot (operands: u16 hops, u16 slot).
StoreUpvalue = 47,
/// Begin a per-iteration binding region for a loop variable (operand: u16 name index).
/// Registers the name so closures created in the loop body snapshot it into a fresh overlay
/// (ES `let` per-iteration semantics); the rest of the frame stays shared. Emitted only when
/// the loop body creates a closure, so closure-free (hot) loops are untouched.
LoopVarsBegin = 48,
/// End the innermost per-iteration binding region (no operand).
LoopVarsEnd = 49,
/// Push `Bool(param_index >= argc)` — true when the positional argument at `param_index`
/// was not supplied by the caller (operand: u16 param index). Emitted by the function
/// prologue so default parameter values apply only for *missing* args, matching the
/// interpreter: an explicit `null` argument does NOT trigger the default.
ArgMissing = 50,
/// Direct self-recursive call (operand: u16 arg count). Emitted by the compiler ONLY when a
/// `fn NAME` body calls `NAME(args)` and `NAME` is provably the function itself (not shadowed
/// by a param/local, not reassigned anywhere in the body). Args are on the stack as for `Call`,
/// but the callee is implicitly the currently-executing chunk — no name lookup, no closure
/// dispatch. The numeric JIT lowers this to a native recursive call (the big recursion win);
/// the VM runs the current chunk directly. Behaviour is identical to `LoadVar NAME; Call argc`.
SelfCall = 51,
/// Normalize the top-of-stack iterable for `for…of`: a JS iterator object (one with a
/// callable `next()` returning `{ value, done }`, e.g. a `Map`/`Set` `.values()` result)
/// is drained into an array; arrays/strings/anything else pass through unchanged. Emitted
/// right after the iterable expression so the existing index-based loop can iterate it.
IterNormalize = 52,
/// `delete obj[key]` / `delete obj.prop`. Pops `[obj, key]`, removes the property
/// (objects: drop the key; arrays: set the index to a null hole), pushes `true`.
DeleteIndex = 53,
}
impl Opcode {
/// Decode byte to opcode. Safe for b in 0..=53 (matches #[repr(u8)] discriminants).
#[inline]
pub fn from_u8(b: u8) -> Option<Opcode> {
if b <= 53 {
Some(unsafe { std::mem::transmute::<u8, Opcode>(b) })
} else {
None
}
}
/// Size in bytes of this instruction at `ip` (including operands). Returns None if truncated.
pub fn instruction_size(self, code: &[u8], ip: usize) -> Option<usize> {
let size = match self {
Opcode::Nop
| Opcode::Pop
| Opcode::Dup
| Opcode::Return
| Opcode::ExitTry
| Opcode::ConcatArray
| Opcode::MergeObject
| Opcode::GetIndex
| Opcode::SetIndex
| Opcode::Throw
| Opcode::ArrayMapIdentity
| Opcode::CallSpread
| Opcode::ConstructSpread
| Opcode::EnterBlock
| Opcode::ExitBlock
| Opcode::LoopVarsEnd
| Opcode::IterNormalize
| Opcode::DeleteIndex
| Opcode::AwaitPromise => 1,
Opcode::ArraySortByProperty
| Opcode::ArrayMapBinOp
| Opcode::ArrayFilterBinOp
| Opcode::LoadNativeExport
| Opcode::LoadUpvalue
| Opcode::StoreUpvalue => 5,
// LoadLocal / StoreLocal take a single u16 operand → 3 bytes (default).
_ => 3,
};
if ip + size > code.len() {
return None;
}
Some(size)
}
}