aver/vm/opcode.rs
1// Aver VM bytecode opcodes.
2//
3// Stack-based: operands are pushed/popped from the operand stack.
4// Variable-width encoding: opcode (1 byte) + operands (0-3 bytes).
5
6// -- Stack / locals ----------------------------------------------------------
7
8/// No-op, used as padding after superinstruction fusion.
9pub const NOP: u8 = 0x00;
10
11/// Push `stack[bp + slot]` onto the operand stack.
12pub const LOAD_LOCAL: u8 = 0x01; // slot:u8
13
14/// Push `stack[bp + slot]` and clear the slot (move semantics).
15/// Used for last-use variables: caller releases sole ownership so
16/// callees and builtins see refcount=1 and can mutate in-place.
17pub const MOVE_LOCAL: u8 = 0x48; // slot:u8
18
19/// Pop top and store into `stack[bp + slot]`.
20pub const STORE_LOCAL: u8 = 0x02; // slot:u8
21
22/// Push `constants[idx]` onto the operand stack.
23pub const LOAD_CONST: u8 = 0x03; // idx:u16
24
25/// Push `globals[idx]` onto the operand stack.
26pub const LOAD_GLOBAL: u8 = 0x04; // idx:u16
27
28/// Pop top and store into `globals[idx]`.
29pub const STORE_GLOBAL: u8 = 0x0A; // idx:u16
30
31/// Discard the top value.
32pub const POP: u8 = 0x05;
33
34/// Duplicate the top value.
35pub const DUP: u8 = 0x06;
36
37/// Push `NanValue::UNIT`.
38pub const LOAD_UNIT: u8 = 0x07;
39
40/// Push `NanValue::TRUE`.
41pub const LOAD_TRUE: u8 = 0x08;
42
43/// Push `NanValue::FALSE`.
44pub const LOAD_FALSE: u8 = 0x09;
45
46// -- Arithmetic --------------------------------------------------------------
47
48/// Pop b, pop a, push a + b.
49pub const ADD: u8 = 0x10;
50
51/// Pop b, pop a, push a - b.
52pub const SUB: u8 = 0x11;
53
54/// Pop b, pop a, push a * b.
55pub const MUL: u8 = 0x12;
56
57/// Pop b, pop a, push a / b.
58pub const DIV: u8 = 0x13;
59
60/// Pop b, pop a, push a % b.
61pub const MOD: u8 = 0x14;
62
63/// Pop a, push -a.
64pub const NEG: u8 = 0x15;
65
66/// Pop a, push !a (boolean not).
67pub const NOT: u8 = 0x16;
68
69// -- Comparison --------------------------------------------------------------
70
71/// Pop b, pop a, push a == b.
72pub const EQ: u8 = 0x20;
73
74/// Pop b, pop a, push a < b.
75pub const LT: u8 = 0x21;
76
77/// Pop b, pop a, push a > b.
78pub const GT: u8 = 0x22;
79
80// -- String ------------------------------------------------------------------
81
82/// Pop b, pop a, push str(a) ++ str(b).
83pub const CONCAT: u8 = 0x28;
84
85// -- Control flow ------------------------------------------------------------
86
87/// Unconditional relative jump: ip += offset.
88pub const JUMP: u8 = 0x30; // offset:i16
89
90/// Pop top, if falsy: ip += offset.
91pub const JUMP_IF_FALSE: u8 = 0x31; // offset:i16
92
93// -- Calls -------------------------------------------------------------------
94
95/// Call a known function by id. Args already on stack.
96pub const CALL_KNOWN: u8 = 0x40; // fn_id:u16, argc:u8
97
98/// Call a function value on the stack (under args).
99pub const CALL_VALUE: u8 = 0x41; // argc:u8
100
101/// Call a builtin service function.
102pub const CALL_BUILTIN: u8 = 0x42; // symbol_id:u32, argc:u8
103
104/// Like CALL_BUILTIN but with owned-argument bitmask from reuse analysis.
105/// Builtins receiving owned args can mutate in-place instead of cloning.
106pub const CALL_BUILTIN_OWNED: u8 = 0x46; // symbol_id:u32, argc:u8, owned:u8
107
108/// Like CALL_KNOWN but with owned-argument bitmask from reuse analysis.
109pub const CALL_KNOWN_OWNED: u8 = 0x47; // fn_id:u16, argc:u8, owned:u8
110
111/// Self tail-call: reuse current frame with new args.
112pub const TAIL_CALL_SELF: u8 = 0x43; // argc:u8
113
114/// Mutual tail-call to a known function: reuse frame, switch target.
115pub const TAIL_CALL_KNOWN: u8 = 0x44; // fn_id:u16, argc:u8
116
117/// Return top of stack to caller.
118pub const RETURN: u8 = 0x50;
119
120// -- Structured values -------------------------------------------------------
121
122/// Push Nil (empty cons list).
123pub const LIST_NIL: u8 = 0x60;
124
125/// Pop tail, pop head, push Cons(head, tail).
126pub const LIST_CONS: u8 = 0x61;
127
128/// Pop `count` items, build cons list from them (first item = head), push list.
129pub const LIST_NEW: u8 = 0x62; // count:u8
130
131/// Pop `count` field values, push a new record with `type_id`.
132pub const RECORD_NEW: u8 = 0x63; // type_id:u16, count:u8
133
134/// Pop record, push `fields[field_idx]` (compile-time resolved index).
135pub const RECORD_GET: u8 = 0x64; // field_idx:u8
136
137/// Pop record, lookup field by interned field symbol, push value.
138pub const RECORD_GET_NAMED: u8 = 0x67; // field_symbol_id:u32
139
140/// Pop `count` field values, push a new variant.
141pub const VARIANT_NEW: u8 = 0x65; // type_id:u16, variant_id:u16, count:u8
142
143/// Pop value, push wrapped value. kind: 0=Ok, 1=Err, 2=Some.
144pub const WRAP: u8 = 0x66; // kind:u8
145
146/// Pop `count` items, build a tuple from them, push tuple.
147pub const TUPLE_NEW: u8 = 0x68; // count:u8
148
149/// Parallel function calls for independent products (?! / !).
150/// Pops N callable values plus their args from the stack, dispatches them via
151/// the same callable resolution rules as CALL_VALUE, then builds the result tuple.
152/// Enters/exits replay group around parallel dispatch.
153///
154/// Encoding: CALL_PAR count:u8 unwrap:u8 [argc:u8 × count]
155/// unwrap=1 (?!): unwrap each Result, propagate first Err.
156/// unwrap=0 (!): return raw tuple.
157pub const CALL_PAR: u8 = 0x86;
158
159/// Update selected fields on a record, preserving the rest from the base value.
160/// Stack: [..., base_record, update_0, ..., update_n-1] -> [..., updated_record]
161pub const RECORD_UPDATE: u8 = 0x69; // type_id:u16, count:u8, field_idx[count]:u8
162
163/// Propagate `Result.Err` to caller or unwrap `Result.Ok` in place.
164pub const PROPAGATE_ERR: u8 = 0x6A;
165
166/// Pop list, push its length as Int.
167pub const LIST_LEN: u8 = 0x6B;
168
169// 0x6C and 0x6D were LIST_GET and LIST_APPEND — removed.
170
171/// Pop list, pop value, push prepended list.
172pub const LIST_PREPEND: u8 = 0x6E;
173
174// 0x6F was LIST_GET_MATCH — removed.
175
176// -- Pattern matching --------------------------------------------------------
177
178/// Peek top: if NaN tag != expected, ip += fail_offset.
179pub const MATCH_TAG: u8 = 0x70; // expected_tag:u8, fail_offset:i16
180
181/// Peek top (must be variant): if variant_id != expected, ip += fail_offset.
182pub const MATCH_VARIANT: u8 = 0x71; // ctor_id:u16, fail_offset:i16
183
184/// Peek top: if not wrapper of `kind`, ip += fail_offset.
185/// If matches, replace top with inner value (unwrap in-place).
186/// kind: 0=Ok, 1=Err, 2=Some.
187pub const MATCH_UNWRAP: u8 = 0x72; // kind:u8, fail_offset:i16
188
189/// Peek top: if not Nil, ip += fail_offset.
190pub const MATCH_NIL: u8 = 0x73; // fail_offset:i16
191
192/// Peek top: if Nil (not a cons), ip += fail_offset.
193pub const MATCH_CONS: u8 = 0x74; // fail_offset:i16
194
195/// Pop cons cell, push tail then push head.
196pub const LIST_HEAD_TAIL: u8 = 0x75;
197
198/// Peek top (record/variant), push `fields[field_idx]` (non-destructive).
199pub const EXTRACT_FIELD: u8 = 0x76; // field_idx:u8
200
201/// Peek top: if not a tuple of `count` items, ip += fail_offset.
202pub const MATCH_TUPLE: u8 = 0x78; // count:u8, fail_offset:i16
203
204/// Peek top tuple, push `items[item_idx]` (non-destructive).
205pub const EXTRACT_TUPLE_ITEM: u8 = 0x79; // item_idx:u8
206
207/// Non-exhaustive match error at source line.
208pub const MATCH_FAIL: u8 = 0x77; // line:u16
209
210/// Unified prefix/exact dispatch on NanValue bits.
211///
212/// Encoding:
213/// MATCH_DISPATCH count:u8 default_offset:i16
214/// [(kind:u8, expected:u64, offset:i16) × count]
215///
216/// kind=0: exact match — `val.bits() == expected`
217/// kind=1: tag match — `(val.bits() & TAG_MASK_FULL) == expected`
218/// where TAG_MASK_FULL = 0xFFFF_C000_0000_0000 (QNAN 14 bits + tag 4 bits)
219///
220/// Pops subject. Scans entries in order; first match wins → ip += offset.
221/// No match → ip += default_offset.
222/// All offsets are relative to the end of the full instruction.
223pub const MATCH_DISPATCH: u8 = 0x7A;
224
225/// Like MATCH_DISPATCH but every entry carries an inline result instead
226/// of a jump offset. When an entry matches, the result is pushed directly
227/// onto the stack and the match body is skipped entirely.
228///
229/// Encoding:
230/// MATCH_DISPATCH_CONST count:u8 default_offset:i16
231/// [(kind:u8, expected:u64, result:u64) × count]
232///
233/// Hit → pop subject, push result NanValue.
234/// Miss → pop subject, ip += default_offset (execute default arm body).
235///
236/// Emitted when ALL dispatchable arms have constant bodies (literals).
237pub const MATCH_DISPATCH_CONST: u8 = 0x7B;
238
239/// Tail-call self for thin frames: no arena finalization needed.
240/// The compiler emits this instead of TAIL_CALL_SELF when the function
241/// is known to be "thin" (no heap allocations within the frame).
242/// Skips finalize_frame_locals_for_tail_call entirely — just copies
243/// args in-place and resets ip.
244pub const TAIL_CALL_SELF_THIN: u8 = 0x45; // argc:u8
245
246/// Inline Option.withDefault: pop default, pop option → push inner or default.
247/// Stack: [option, default] → [result]
248/// If option is Some → push unwrapped inner value.
249/// If option is None → push default.
250pub const UNWRAP_OR: u8 = 0x7C;
251
252/// Inline Result.withDefault: pop default, pop result → push inner or default.
253/// Stack: [result, default] → [value]
254/// If result is Ok → push unwrapped inner value.
255/// If result is Err → push default.
256pub const UNWRAP_RESULT_OR: u8 = 0x7D;
257
258/// Frameless call to a leaf+thin+args-only function.
259/// No CallFrame is pushed — just saves (fn_id, ip) in the dispatch loop,
260/// sets bp to the args already on stack, and jumps to the target.
261/// On RETURN, restores the caller's state directly.
262/// Format: fn_id:u16, argc:u8 (same as CALL_KNOWN).
263pub const CALL_LEAF: u8 = 0x7E;
264
265// ─── Superinstructions ──────────────────────────────────────
266
267/// Push two locals in one dispatch. Format: slot_a:u8, slot_b:u8.
268pub const LOAD_LOCAL_2: u8 = 0x80;
269
270/// Push one local + one constant in one dispatch. Format: slot:u8, const_idx:u16.
271pub const LOAD_LOCAL_CONST: u8 = 0x81;
272
273/// Inline Vector.get: pop index, pop vector → push Option (Some/None).
274/// Stack: [vector, index] → [option]
275pub const VECTOR_GET: u8 = 0x82;
276
277/// Fused Vector.get + Option.withDefault: pop default, pop index, pop vector → push value.
278/// Stack: [vector, index, default] → [value]
279/// Combines CALL_BUILTIN(Vector.get) + LOAD_CONST + UNWRAP_OR into one opcode.
280pub const VECTOR_GET_OR: u8 = 0x83;
281
282/// Inline Vector.set: pop value, pop index, pop vector → push Option<Vector>.
283/// Stack: [vector, index, value] → [option_vector]
284pub const VECTOR_SET: u8 = 0x84;
285
286/// Fused Vector.set + Option.withDefault(vec): pop value, pop index, pop vector → push vector.
287/// Stack: [vector, index, value] → [vector]
288pub const VECTOR_SET_OR_KEEP: u8 = 0x85;
289
290// -- Deforestation buffer (0.15 Traversal) -----------------------------------
291//
292// Mutable byte-buffer scratch backing the synthesizer's `__buf_*` intrinsics.
293// Buffer values travel as opaque `Int(handle)` NanValues — handles are
294// indices into `vm.buffer_pool: Vec<Option<String>>`. This keeps Buffer
295// out of `ArenaEntry` (no exhaustiveness ripples) and out of GC marking
296// (handle is an inline value, the underlying String lives on the host
297// heap unaffected by frame compactions). Buffers are use-once: created
298// by `BUFFER_NEW`, mutated through `BUFFER_APPEND_*`, finalized to an
299// arena `String` (Rc<str>) by `BUFFER_FINALIZE` which also frees the slot.
300
301/// Allocate a fresh String buffer. Pop cap_hint:i64 → push handle:Int(buffer_idx).
302pub const BUFFER_NEW: u8 = 0x90;
303
304/// Append the bytes of a string to a buffer. Pop str, pop buf →
305/// push buf (same handle). The String pointed to by `buf.handle` is
306/// mutated in place via `String::push_str`.
307pub const BUFFER_APPEND_STR: u8 = 0x91;
308
309/// Append separator only when buffer is non-empty. Pop sep, pop buf →
310/// push buf. No-op for the first append, so the synthesized `__buffered`
311/// loop body can place the separator before each element uniformly.
312pub const BUFFER_APPEND_SEP_UNLESS_FIRST: u8 = 0x92;
313
314/// Drain a buffer into an arena `OBJ_STRING`. Pop buf → push string.
315/// Frees the underlying `vm.buffer_pool` slot; the handle becomes invalid.
316pub const BUFFER_FINALIZE: u8 = 0x93;
317
318/// Opcode name for debug/disassembly.
319pub fn opcode_name(op: u8) -> &'static str {
320 match op {
321 LOAD_LOCAL => "LOAD_LOCAL",
322 MOVE_LOCAL => "MOVE_LOCAL",
323 STORE_LOCAL => "STORE_LOCAL",
324 LOAD_CONST => "LOAD_CONST",
325 LOAD_GLOBAL => "LOAD_GLOBAL",
326 POP => "POP",
327 DUP => "DUP",
328 LOAD_UNIT => "LOAD_UNIT",
329 LOAD_TRUE => "LOAD_TRUE",
330 LOAD_FALSE => "LOAD_FALSE",
331 ADD => "ADD",
332 SUB => "SUB",
333 MUL => "MUL",
334 DIV => "DIV",
335 MOD => "MOD",
336 NEG => "NEG",
337 NOT => "NOT",
338 EQ => "EQ",
339 LT => "LT",
340 GT => "GT",
341 CONCAT => "CONCAT",
342 JUMP => "JUMP",
343 JUMP_IF_FALSE => "JUMP_IF_FALSE",
344 CALL_KNOWN => "CALL_KNOWN",
345 CALL_VALUE => "CALL_VALUE",
346 CALL_BUILTIN => "CALL_BUILTIN",
347 CALL_BUILTIN_OWNED => "CALL_BUILTIN_OWNED",
348 CALL_KNOWN_OWNED => "CALL_KNOWN_OWNED",
349 TAIL_CALL_SELF => "TAIL_CALL_SELF",
350 TAIL_CALL_KNOWN => "TAIL_CALL_KNOWN",
351 RETURN => "RETURN",
352 LIST_NIL => "LIST_NIL",
353 LIST_CONS => "LIST_CONS",
354 LIST_NEW => "LIST_NEW",
355 RECORD_NEW => "RECORD_NEW",
356 STORE_GLOBAL => "STORE_GLOBAL",
357 RECORD_GET => "RECORD_GET",
358 RECORD_GET_NAMED => "RECORD_GET_NAMED",
359 VARIANT_NEW => "VARIANT_NEW",
360 WRAP => "WRAP",
361 TUPLE_NEW => "TUPLE_NEW",
362 RECORD_UPDATE => "RECORD_UPDATE",
363 PROPAGATE_ERR => "PROPAGATE_ERR",
364 LIST_LEN => "LIST_LEN",
365 LIST_PREPEND => "LIST_PREPEND",
366 MATCH_TAG => "MATCH_TAG",
367 MATCH_VARIANT => "MATCH_VARIANT",
368 MATCH_UNWRAP => "MATCH_UNWRAP",
369 MATCH_NIL => "MATCH_NIL",
370 MATCH_CONS => "MATCH_CONS",
371 LIST_HEAD_TAIL => "LIST_HEAD_TAIL",
372 EXTRACT_FIELD => "EXTRACT_FIELD",
373 MATCH_TUPLE => "MATCH_TUPLE",
374 EXTRACT_TUPLE_ITEM => "EXTRACT_TUPLE_ITEM",
375 MATCH_FAIL => "MATCH_FAIL",
376 MATCH_DISPATCH => "MATCH_DISPATCH",
377 MATCH_DISPATCH_CONST => "MATCH_DISPATCH_CONST",
378 TAIL_CALL_SELF_THIN => "TAIL_CALL_SELF_THIN",
379 UNWRAP_OR => "UNWRAP_OR",
380 UNWRAP_RESULT_OR => "UNWRAP_RESULT_OR",
381 CALL_LEAF => "CALL_LEAF",
382 LOAD_LOCAL_2 => "LOAD_LOCAL_2",
383 LOAD_LOCAL_CONST => "LOAD_LOCAL_CONST",
384 VECTOR_GET => "VECTOR_GET",
385 VECTOR_GET_OR => "VECTOR_GET_OR",
386 VECTOR_SET => "VECTOR_SET",
387 VECTOR_SET_OR_KEEP => "VECTOR_SET_OR_KEEP",
388 BUFFER_NEW => "BUFFER_NEW",
389 BUFFER_APPEND_STR => "BUFFER_APPEND_STR",
390 BUFFER_APPEND_SEP_UNLESS_FIRST => "BUFFER_APPEND_SEP_UNLESS_FIRST",
391 BUFFER_FINALIZE => "BUFFER_FINALIZE",
392 CALL_PAR => "CALL_PAR",
393 NOP => "NOP",
394 _ => "UNKNOWN",
395 }
396}
397
398/// Operand byte width after the opcode byte. Single source of truth —
399/// all bytecode traversal functions must use this.
400pub fn opcode_operand_width(op: u8, code: &[u8], ip: usize) -> usize {
401 match op {
402 // 0-byte (stack-only)
403 POP
404 | DUP
405 | LOAD_UNIT
406 | LOAD_TRUE
407 | LOAD_FALSE
408 | ADD
409 | SUB
410 | MUL
411 | DIV
412 | MOD
413 | NEG
414 | NOT
415 | EQ
416 | LT
417 | GT
418 | RETURN
419 | PROPAGATE_ERR
420 | LIST_HEAD_TAIL
421 | LIST_NIL
422 | LIST_CONS
423 | LIST_LEN
424 | LIST_PREPEND
425 | UNWRAP_OR
426 | UNWRAP_RESULT_OR
427 | CONCAT
428 | VECTOR_GET
429 | VECTOR_SET
430 | BUFFER_NEW
431 | BUFFER_APPEND_STR
432 | BUFFER_APPEND_SEP_UNLESS_FIRST
433 | BUFFER_FINALIZE
434 | NOP => 0,
435
436 // 1-byte
437 LOAD_LOCAL | MOVE_LOCAL | STORE_LOCAL | CALL_VALUE | RECORD_GET | EXTRACT_FIELD
438 | EXTRACT_TUPLE_ITEM | LIST_NEW | WRAP | TUPLE_NEW | TAIL_CALL_SELF
439 | TAIL_CALL_SELF_THIN | VECTOR_SET_OR_KEEP => 1,
440
441 // 2-byte (u16 or u8+u8)
442 LOAD_CONST | LOAD_GLOBAL | STORE_GLOBAL | JUMP | JUMP_IF_FALSE | MATCH_FAIL | MATCH_NIL
443 | MATCH_CONS | LOAD_LOCAL_2 | VECTOR_GET_OR => 2,
444
445 // 3-byte
446 CALL_KNOWN | CALL_LEAF | MATCH_TAG | MATCH_UNWRAP | MATCH_TUPLE | RECORD_NEW
447 | LOAD_LOCAL_CONST => 3,
448
449 // 4-byte
450 CALL_KNOWN_OWNED => 4, // fn_id:u16 + argc:u8 + owned:u8
451
452 // 4-byte
453 MATCH_VARIANT | RECORD_GET_NAMED => 4,
454
455 // 5-byte
456 CALL_BUILTIN | VARIANT_NEW => 5,
457
458 // 6-byte
459 CALL_BUILTIN_OWNED => 6, // symbol_id:u32 + argc:u8 + owned:u8
460
461 // Variable-length
462 MATCH_DISPATCH | MATCH_DISPATCH_CONST if ip < code.len() => {
463 let count = code[ip] as usize;
464 let entry_size = if op == MATCH_DISPATCH { 11 } else { 17 };
465 3 + count * entry_size
466 }
467 RECORD_UPDATE if ip + 2 < code.len() => 3 + code[ip + 2] as usize,
468 // CALL_PAR count:u8 unwrap:u8 [argc:u8 × count]
469 CALL_PAR if ip < code.len() => {
470 let count = code[ip] as usize;
471 2 + count
472 }
473 _ => 0,
474 }
475}