tishlang_bytecode 3.3.0

Bytecode compiler for Tish (AST → bytecode)
Documentation
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
//! 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,
    /// String-builder append for statement-position `acc += rhs` where `acc` is a frame slot local
    /// (operand: u16 slot index). Pops `rhs`; appends it to the accumulator in amortized O(1) by
    /// keeping a growable buffer for the slot (see the frame-local builder in `run_chunk`). Leaves
    /// nothing on the stack — only emitted where the assignment's result is discarded. Slots are
    /// frame-private (never captured), so the buffer needs no cross-frame sharing.
    AppendLocal = 54,
    /// #186 — apply a unary `Math.<fn>` intrinsic to the top-of-stack number: pop one value, push
    /// `Math.fn(x)`. The u16 operand is the [`MathUnaryFn`] id. Emitted by the compiler ONLY when
    /// `Math` is provably the global builtin (never shadowed in the program), so the numeric JIT can
    /// lower it to a native op / libcall without a runtime shape guard. Behaviour is identical to
    /// `LoadVar Math; GetMember fn; <arg>; Call 1` on a number.
    MathUnary = 55,
    /// #203 — apply a BINARY `Math.<fn>` intrinsic (max / min / pow / atan2 / hypot) to the top TWO
    /// numbers: pop `b`, pop `a`, push `Math.fn(a, b)`. The u16 operand is the [`MathBinaryFn`] id.
    /// Same emission gate as [`Opcode::MathUnary`] (`Math` provably the unshadowed global) but for
    /// exactly two args. Behaviour identical to `LoadVar Math; GetMember fn; <a>; <b>; Call 2`.
    MathBinary = 56,
}

/// The unary `Math` functions the [`Opcode::MathUnary`] fast path recognizes (#186). f64→f64;
/// the discriminant is the opcode operand. Kept in sync with the VM handler and the JIT lowering.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum MathUnaryFn {
    Sqrt = 0,
    Cbrt = 1,
    Abs = 2,
    Floor = 3,
    Ceil = 4,
    Round = 5,
    Trunc = 6,
    Sign = 7,
    Sin = 8,
    Cos = 9,
    Tan = 10,
    Asin = 11,
    Acos = 12,
    Atan = 13,
    Sinh = 14,
    Cosh = 15,
    Tanh = 16,
    Exp = 17,
    Log = 18,
    Log2 = 19,
    Log10 = 20,
}

impl MathUnaryFn {
    /// Map a `Math.<name>` member to its id, or `None` if it isn't a supported unary intrinsic.
    pub fn from_name(name: &str) -> Option<MathUnaryFn> {
        Some(match name {
            "sqrt" => MathUnaryFn::Sqrt,
            "cbrt" => MathUnaryFn::Cbrt,
            "abs" => MathUnaryFn::Abs,
            "floor" => MathUnaryFn::Floor,
            "ceil" => MathUnaryFn::Ceil,
            "round" => MathUnaryFn::Round,
            "trunc" => MathUnaryFn::Trunc,
            "sign" => MathUnaryFn::Sign,
            "sin" => MathUnaryFn::Sin,
            "cos" => MathUnaryFn::Cos,
            "tan" => MathUnaryFn::Tan,
            "asin" => MathUnaryFn::Asin,
            "acos" => MathUnaryFn::Acos,
            "atan" => MathUnaryFn::Atan,
            "sinh" => MathUnaryFn::Sinh,
            "cosh" => MathUnaryFn::Cosh,
            "tanh" => MathUnaryFn::Tanh,
            "exp" => MathUnaryFn::Exp,
            "log" => MathUnaryFn::Log,
            "log2" => MathUnaryFn::Log2,
            "log10" => MathUnaryFn::Log10,
            _ => return None,
        })
    }

    /// Decode an opcode operand to the fn id (safe match — no transmute).
    pub fn from_u16(v: u16) -> Option<MathUnaryFn> {
        use MathUnaryFn::*;
        Some(match v {
            0 => Sqrt,
            1 => Cbrt,
            2 => Abs,
            3 => Floor,
            4 => Ceil,
            5 => Round,
            6 => Trunc,
            7 => Sign,
            8 => Sin,
            9 => Cos,
            10 => Tan,
            11 => Asin,
            12 => Acos,
            13 => Atan,
            14 => Sinh,
            15 => Cosh,
            16 => Tanh,
            17 => Exp,
            18 => Log,
            19 => Log2,
            20 => Log10,
            _ => return None,
        })
    }

    /// Apply the intrinsic (the single source of truth for VM + JIT + interp result parity).
    #[inline]
    pub fn apply(self, x: f64) -> f64 {
        match self {
            MathUnaryFn::Sqrt => x.sqrt(),
            MathUnaryFn::Cbrt => x.cbrt(),
            MathUnaryFn::Abs => x.abs(),
            MathUnaryFn::Floor => x.floor(),
            MathUnaryFn::Ceil => x.ceil(),
            // JS `Math.round`: ties toward +∞, `[-0.5, 0) → -0`; EXACTLY `tishlang_builtins::math::
            // round_f64` (replicated here — this crate sits below builtins — so VM/JIT == interp).
            MathUnaryFn::Round => {
                if x.is_nan() || x.is_infinite() || x == 0.0 {
                    x
                } else if (-0.5..0.5).contains(&x) {
                    if x < 0.0 {
                        -0.0
                    } else {
                        0.0
                    }
                } else {
                    (x + 0.5).floor()
                }
            }
            MathUnaryFn::Trunc => x.trunc(),
            // JS `Math.sign` (matches `tishlang_builtins::math::sign`): NaN→NaN, else ±1; and for
            // ±0 return `x` unchanged so `Math.sign(-0)` is `-0` (`x < 0.0` is false for -0, so it
            // lands in the else — bare `0.0` would drop the sign).
            MathUnaryFn::Sign => {
                if x.is_nan() {
                    f64::NAN
                } else if x > 0.0 {
                    1.0
                } else if x < 0.0 {
                    -1.0
                } else {
                    x
                }
            }
            MathUnaryFn::Sin => x.sin(),
            MathUnaryFn::Cos => x.cos(),
            MathUnaryFn::Tan => x.tan(),
            MathUnaryFn::Asin => x.asin(),
            MathUnaryFn::Acos => x.acos(),
            MathUnaryFn::Atan => x.atan(),
            MathUnaryFn::Sinh => x.sinh(),
            MathUnaryFn::Cosh => x.cosh(),
            MathUnaryFn::Tanh => x.tanh(),
            MathUnaryFn::Exp => x.exp(),
            MathUnaryFn::Log => x.ln(),
            MathUnaryFn::Log2 => x.log2(),
            MathUnaryFn::Log10 => x.log10(),
        }
    }
}

/// The binary `Math` functions the [`Opcode::MathBinary`] fast path recognizes (#203). `(f64, f64) →
/// f64`; the discriminant is the opcode operand. Kept in sync with the VM handler and JIT lowering.
/// (`hypot` is intentionally NOT here — it stays a correct generic call to avoid replicating its
/// scaled-sum-of-squares in this below-builtins crate.)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum MathBinaryFn {
    Max = 0,
    Min = 1,
    Pow = 2,
    Atan2 = 3,
}

impl MathBinaryFn {
    /// Map a `Math.<name>` member to its id, or `None` if it isn't a supported 2-arg intrinsic.
    pub fn from_name(name: &str) -> Option<MathBinaryFn> {
        Some(match name {
            "max" => MathBinaryFn::Max,
            "min" => MathBinaryFn::Min,
            "pow" => MathBinaryFn::Pow,
            "atan2" => MathBinaryFn::Atan2,
            _ => return None,
        })
    }

    /// Decode an opcode operand to the fn id (safe match — no transmute).
    pub fn from_u16(v: u16) -> Option<MathBinaryFn> {
        use MathBinaryFn::*;
        Some(match v {
            0 => Max,
            1 => Min,
            2 => Pow,
            3 => Atan2,
            _ => return None,
        })
    }

    /// Apply the intrinsic — the single source of truth for VM + JIT result parity. Replicates the JS
    /// semantics of `tishlang_builtins::math` (this crate sits below builtins), so VM/JIT == interp.
    #[inline]
    pub fn apply(self, a: f64, b: f64) -> f64 {
        match self {
            // Math.max(a,b): NaN if either is NaN; else the larger, with +0 preferred over -0. Exactly
            // `max_f64(&[a, b])`.
            MathBinaryFn::Max => {
                if a.is_nan() || b.is_nan() {
                    f64::NAN
                } else if a > b || (a == 0.0 && b == 0.0 && a.is_sign_positive()) {
                    a
                } else {
                    b
                }
            }
            // Math.min(a,b): NaN if either is NaN; else the smaller, with -0 preferred. Exactly
            // `min_f64(&[a, b])`.
            MathBinaryFn::Min => {
                if a.is_nan() || b.is_nan() {
                    f64::NAN
                } else if a < b || (a == 0.0 && b == 0.0 && a.is_sign_negative()) {
                    a
                } else {
                    b
                }
            }
            MathBinaryFn::Pow => a.powf(b),
            MathBinaryFn::Atan2 => a.atan2(b),
        }
    }
}

impl Opcode {
    /// Decode byte to opcode. Safe for b in 0..=56 (matches #[repr(u8)] discriminants).
    #[inline]
    pub fn from_u8(b: u8) -> Option<Opcode> {
        if b <= 56 {
            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)
    }
}