we (web engine): Experimental web browser project to understand the limits of Claude
1//! Register-based bytecode format for the JavaScript engine.
2//!
3//! Each instruction encodes register operands directly, making it suitable for
4//! JIT compilation. Instructions are variable-length, encoded as a 1-byte opcode
5//! followed by operand bytes.
6
7use std::fmt;
8
9/// A register index (0–255).
10pub type Reg = u8;
11
12/// An index into the constant pool.
13pub type ConstIdx = u16;
14
15/// An index into the name/string table.
16pub type NameIdx = u16;
17
18/// Bytecode instruction set.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[repr(u8)]
21pub enum Op {
22 // ── Register loads ──────────────────────────────────────
23 /// LoadConst dst, const_idx(u16) — load constant pool entry into register
24 LoadConst = 0x01,
25 /// LoadNull dst
26 LoadNull = 0x02,
27 /// LoadUndefined dst
28 LoadUndefined = 0x03,
29 /// LoadTrue dst
30 LoadTrue = 0x04,
31 /// LoadFalse dst
32 LoadFalse = 0x05,
33 /// Move dst, src
34 Move = 0x06,
35
36 // ── Global variable access ──────────────────────────────
37 /// LoadGlobal dst, name_idx(u16)
38 LoadGlobal = 0x07,
39 /// StoreGlobal name_idx(u16), src
40 StoreGlobal = 0x08,
41
42 // ── Arithmetic ──────────────────────────────────────────
43 /// Add dst, lhs, rhs
44 Add = 0x10,
45 /// Sub dst, lhs, rhs
46 Sub = 0x11,
47 /// Mul dst, lhs, rhs
48 Mul = 0x12,
49 /// Div dst, lhs, rhs
50 Div = 0x13,
51 /// Rem dst, lhs, rhs
52 Rem = 0x14,
53 /// Exp dst, lhs, rhs
54 Exp = 0x15,
55 /// Neg dst, src (unary minus)
56 Neg = 0x16,
57
58 // ── Bitwise ─────────────────────────────────────────────
59 /// BitAnd dst, lhs, rhs
60 BitAnd = 0x20,
61 /// BitOr dst, lhs, rhs
62 BitOr = 0x21,
63 /// BitXor dst, lhs, rhs
64 BitXor = 0x22,
65 /// ShiftLeft dst, lhs, rhs
66 ShiftLeft = 0x23,
67 /// ShiftRight dst, lhs, rhs
68 ShiftRight = 0x24,
69 /// UShiftRight dst, lhs, rhs
70 UShiftRight = 0x25,
71 /// BitNot dst, src
72 BitNot = 0x26,
73
74 // ── Comparison ──────────────────────────────────────────
75 /// Eq dst, lhs, rhs (==)
76 Eq = 0x30,
77 /// StrictEq dst, lhs, rhs (===)
78 StrictEq = 0x31,
79 /// NotEq dst, lhs, rhs (!=)
80 NotEq = 0x32,
81 /// StrictNotEq dst, lhs, rhs (!==)
82 StrictNotEq = 0x33,
83 /// LessThan dst, lhs, rhs
84 LessThan = 0x34,
85 /// LessEq dst, lhs, rhs
86 LessEq = 0x35,
87 /// GreaterThan dst, lhs, rhs
88 GreaterThan = 0x36,
89 /// GreaterEq dst, lhs, rhs
90 GreaterEq = 0x37,
91
92 // ── Logical / unary ─────────────────────────────────────
93 /// LogicalNot dst, src
94 LogicalNot = 0x38,
95 /// TypeOf dst, src
96 TypeOf = 0x39,
97 /// InstanceOf dst, lhs, rhs
98 InstanceOf = 0x3A,
99 /// In dst, lhs, rhs
100 In = 0x3B,
101 /// Void dst, src — evaluate src, produce undefined
102 Void = 0x3C,
103
104 // ── Control flow ────────────────────────────────────────
105 /// Jump offset(i32) — unconditional relative jump
106 Jump = 0x40,
107 /// JumpIfTrue reg, offset(i32)
108 JumpIfTrue = 0x41,
109 /// JumpIfFalse reg, offset(i32)
110 JumpIfFalse = 0x42,
111 /// JumpIfNullish reg, offset(i32)
112 JumpIfNullish = 0x43,
113
114 // ── Functions / calls ───────────────────────────────────
115 /// Call dst, func_reg, args_start, arg_count(u8)
116 Call = 0x50,
117 /// Return reg
118 Return = 0x51,
119 /// Throw reg
120 Throw = 0x52,
121 /// CreateClosure dst, func_idx(u16) — create a closure from a nested function
122 CreateClosure = 0x53,
123
124 // ── Object / property ───────────────────────────────────
125 /// GetProperty dst, obj_reg, key_reg
126 GetProperty = 0x60,
127 /// SetProperty obj_reg, key_reg, val_reg
128 SetProperty = 0x61,
129 /// CreateObject dst
130 CreateObject = 0x62,
131 /// CreateArray dst
132 CreateArray = 0x63,
133 /// GetPropertyByName dst, obj_reg, name_idx(u16) — optimized named access
134 GetPropertyByName = 0x64,
135 /// SetPropertyByName obj_reg, name_idx(u16), val_reg — optimized named store
136 SetPropertyByName = 0x65,
137
138 // ── Misc ────────────────────────────────────────────────
139 /// Delete dst, obj_reg, key_reg
140 Delete = 0x70,
141 /// LoadInt8 dst, i8 — load small integer without constant pool
142 LoadInt8 = 0x71,
143 /// ForInInit dst_keys, obj_reg — collect enumerable keys of object into an array
144 ForInInit = 0x72,
145 /// ForInNext dst_val, dst_done, keys_reg, idx_reg — get next key or signal done
146 ForInNext = 0x73,
147 /// SetPrototype obj_reg, proto_reg — set [[Prototype]] of obj to proto
148 SetPrototype = 0x74,
149 /// GetPrototype dst, obj_reg — get [[Prototype]] of obj
150 GetPrototype = 0x75,
151 /// PushExceptionHandler catch_reg, offset(i32) — push a try/catch handler
152 PushExceptionHandler = 0x76,
153 /// PopExceptionHandler — remove the current exception handler
154 PopExceptionHandler = 0x77,
155}
156
157impl Op {
158 /// Decode an opcode from a byte, returning `None` for unrecognized values.
159 pub fn from_byte(b: u8) -> Option<Op> {
160 match b {
161 0x01 => Some(Op::LoadConst),
162 0x02 => Some(Op::LoadNull),
163 0x03 => Some(Op::LoadUndefined),
164 0x04 => Some(Op::LoadTrue),
165 0x05 => Some(Op::LoadFalse),
166 0x06 => Some(Op::Move),
167 0x07 => Some(Op::LoadGlobal),
168 0x08 => Some(Op::StoreGlobal),
169 0x10 => Some(Op::Add),
170 0x11 => Some(Op::Sub),
171 0x12 => Some(Op::Mul),
172 0x13 => Some(Op::Div),
173 0x14 => Some(Op::Rem),
174 0x15 => Some(Op::Exp),
175 0x16 => Some(Op::Neg),
176 0x20 => Some(Op::BitAnd),
177 0x21 => Some(Op::BitOr),
178 0x22 => Some(Op::BitXor),
179 0x23 => Some(Op::ShiftLeft),
180 0x24 => Some(Op::ShiftRight),
181 0x25 => Some(Op::UShiftRight),
182 0x26 => Some(Op::BitNot),
183 0x30 => Some(Op::Eq),
184 0x31 => Some(Op::StrictEq),
185 0x32 => Some(Op::NotEq),
186 0x33 => Some(Op::StrictNotEq),
187 0x34 => Some(Op::LessThan),
188 0x35 => Some(Op::LessEq),
189 0x36 => Some(Op::GreaterThan),
190 0x37 => Some(Op::GreaterEq),
191 0x38 => Some(Op::LogicalNot),
192 0x39 => Some(Op::TypeOf),
193 0x3A => Some(Op::InstanceOf),
194 0x3B => Some(Op::In),
195 0x3C => Some(Op::Void),
196 0x40 => Some(Op::Jump),
197 0x41 => Some(Op::JumpIfTrue),
198 0x42 => Some(Op::JumpIfFalse),
199 0x43 => Some(Op::JumpIfNullish),
200 0x50 => Some(Op::Call),
201 0x51 => Some(Op::Return),
202 0x52 => Some(Op::Throw),
203 0x53 => Some(Op::CreateClosure),
204 0x60 => Some(Op::GetProperty),
205 0x61 => Some(Op::SetProperty),
206 0x62 => Some(Op::CreateObject),
207 0x63 => Some(Op::CreateArray),
208 0x64 => Some(Op::GetPropertyByName),
209 0x65 => Some(Op::SetPropertyByName),
210 0x70 => Some(Op::Delete),
211 0x71 => Some(Op::LoadInt8),
212 0x72 => Some(Op::ForInInit),
213 0x73 => Some(Op::ForInNext),
214 0x74 => Some(Op::SetPrototype),
215 0x75 => Some(Op::GetPrototype),
216 0x76 => Some(Op::PushExceptionHandler),
217 0x77 => Some(Op::PopExceptionHandler),
218 _ => None,
219 }
220 }
221}
222
223/// A constant value stored in the constant pool.
224#[derive(Debug, Clone, PartialEq)]
225pub enum Constant {
226 Number(f64),
227 String(String),
228}
229
230/// A compiled bytecode function.
231#[derive(Debug, Clone)]
232pub struct Function {
233 /// Name of the function (empty for anonymous / top-level).
234 pub name: String,
235 /// Number of named parameters.
236 pub param_count: u8,
237 /// Total register slots needed.
238 pub register_count: u8,
239 /// The bytecode bytes.
240 pub code: Vec<u8>,
241 /// Constant pool (numbers and strings).
242 pub constants: Vec<Constant>,
243 /// Name table for global/property name lookups.
244 pub names: Vec<String>,
245 /// Nested function definitions (referenced by CreateClosure).
246 pub functions: Vec<Function>,
247 /// Source map: bytecode offset → source line (sorted by offset).
248 pub source_map: Vec<(u32, u32)>,
249}
250
251impl Function {
252 pub fn new(name: String, param_count: u8) -> Self {
253 Self {
254 name,
255 param_count,
256 register_count: 0,
257 code: Vec::new(),
258 constants: Vec::new(),
259 names: Vec::new(),
260 functions: Vec::new(),
261 source_map: Vec::new(),
262 }
263 }
264}
265
266/// A builder that emits bytecode into a `Function`.
267pub struct BytecodeBuilder {
268 pub func: Function,
269}
270
271impl BytecodeBuilder {
272 pub fn new(name: String, param_count: u8) -> Self {
273 Self {
274 func: Function::new(name, param_count),
275 }
276 }
277
278 /// Current bytecode offset (position of next emitted byte).
279 pub fn offset(&self) -> usize {
280 self.func.code.len()
281 }
282
283 /// Intern a constant, returning its index.
284 pub fn add_constant(&mut self, c: Constant) -> ConstIdx {
285 // Reuse existing constant if it matches.
286 for (i, existing) in self.func.constants.iter().enumerate() {
287 match (existing, &c) {
288 (Constant::String(a), Constant::String(b)) if a == b => return i as ConstIdx,
289 (Constant::Number(a), Constant::Number(b)) if a.to_bits() == b.to_bits() => {
290 return i as ConstIdx;
291 }
292 _ => {}
293 }
294 }
295 let idx = self.func.constants.len();
296 assert!(idx <= u16::MAX as usize, "constant pool overflow");
297 self.func.constants.push(c);
298 idx as ConstIdx
299 }
300
301 /// Intern a name, returning its index.
302 pub fn add_name(&mut self, name: &str) -> NameIdx {
303 for (i, existing) in self.func.names.iter().enumerate() {
304 if existing == name {
305 return i as NameIdx;
306 }
307 }
308 let idx = self.func.names.len();
309 assert!(idx <= u16::MAX as usize, "name table overflow");
310 self.func.names.push(name.to_string());
311 idx as NameIdx
312 }
313
314 /// Add a nested function, returning its index.
315 pub fn add_function(&mut self, f: Function) -> u16 {
316 let idx = self.func.functions.len();
317 assert!(idx <= u16::MAX as usize, "function table overflow");
318 self.func.functions.push(f);
319 idx as u16
320 }
321
322 // ── Emit helpers ────────────────────────────────────────
323
324 fn emit_u8(&mut self, v: u8) {
325 self.func.code.push(v);
326 }
327
328 fn emit_u16(&mut self, v: u16) {
329 self.func.code.extend_from_slice(&v.to_le_bytes());
330 }
331
332 fn emit_i32(&mut self, v: i32) {
333 self.func.code.extend_from_slice(&v.to_le_bytes());
334 }
335
336 // ── Single-operand instructions ─────────────────────────
337
338 /// Emit: LoadNull dst | LoadUndefined dst | LoadTrue dst | LoadFalse dst
339 pub fn emit_reg(&mut self, op: Op, dst: Reg) {
340 self.emit_u8(op as u8);
341 self.emit_u8(dst);
342 }
343
344 /// Emit: Move dst, src
345 pub fn emit_reg_reg(&mut self, op: Op, a: Reg, b: Reg) {
346 self.emit_u8(op as u8);
347 self.emit_u8(a);
348 self.emit_u8(b);
349 }
350
351 /// Emit: ForInNext dst_val, dst_done, keys, idx (4-register instruction)
352 pub fn emit_reg4(&mut self, op: Op, a: Reg, b: Reg, c: Reg, d: Reg) {
353 self.emit_u8(op as u8);
354 self.emit_u8(a);
355 self.emit_u8(b);
356 self.emit_u8(c);
357 self.emit_u8(d);
358 }
359
360 /// Emit: Add dst, lhs, rhs (and other 3-register instructions)
361 pub fn emit_reg3(&mut self, op: Op, dst: Reg, lhs: Reg, rhs: Reg) {
362 self.emit_u8(op as u8);
363 self.emit_u8(dst);
364 self.emit_u8(lhs);
365 self.emit_u8(rhs);
366 }
367
368 /// Emit: LoadConst dst, const_idx | CreateClosure dst, func_idx
369 pub fn emit_reg_u16(&mut self, op: Op, dst: Reg, idx: u16) {
370 self.emit_u8(op as u8);
371 self.emit_u8(dst);
372 self.emit_u16(idx);
373 }
374
375 /// Emit: LoadGlobal dst, name_idx
376 pub fn emit_load_global(&mut self, dst: Reg, name_idx: NameIdx) {
377 self.emit_u8(Op::LoadGlobal as u8);
378 self.emit_u8(dst);
379 self.emit_u16(name_idx);
380 }
381
382 /// Emit: StoreGlobal name_idx, src
383 pub fn emit_store_global(&mut self, name_idx: NameIdx, src: Reg) {
384 self.emit_u8(Op::StoreGlobal as u8);
385 self.emit_u16(name_idx);
386 self.emit_u8(src);
387 }
388
389 /// Emit: Jump offset (placeholder, returns patch position)
390 pub fn emit_jump(&mut self, op: Op) -> usize {
391 self.emit_u8(op as u8);
392 if op == Op::JumpIfTrue || op == Op::JumpIfFalse || op == Op::JumpIfNullish {
393 panic!("use emit_cond_jump for conditional jumps");
394 }
395 let pos = self.offset();
396 self.emit_i32(0); // placeholder
397 pos
398 }
399
400 /// Emit: JumpIfTrue/JumpIfFalse/JumpIfNullish reg, offset (placeholder)
401 pub fn emit_cond_jump(&mut self, op: Op, reg: Reg) -> usize {
402 self.emit_u8(op as u8);
403 self.emit_u8(reg);
404 let pos = self.offset();
405 self.emit_i32(0); // placeholder
406 pos
407 }
408
409 /// Patch a jump offset at the given position to point to the current offset.
410 pub fn patch_jump(&mut self, pos: usize) {
411 self.patch_jump_to(pos, self.offset());
412 }
413
414 /// Patch a jump offset at the given position to point to an arbitrary target.
415 pub fn patch_jump_to(&mut self, pos: usize, target: usize) {
416 let offset = target as i32 - (pos as i32 + 4); // relative to after the i32
417 let bytes = offset.to_le_bytes();
418 self.func.code[pos] = bytes[0];
419 self.func.code[pos + 1] = bytes[1];
420 self.func.code[pos + 2] = bytes[2];
421 self.func.code[pos + 3] = bytes[3];
422 }
423
424 /// Emit: Jump with a known target (backward jump).
425 pub fn emit_jump_to(&mut self, target: usize) {
426 self.emit_u8(Op::Jump as u8);
427 let from = self.offset() as i32 + 4;
428 let offset = target as i32 - from;
429 self.emit_i32(offset);
430 }
431
432 /// Emit: JumpIfTrue/JumpIfFalse with a known target (backward jump).
433 pub fn emit_cond_jump_to(&mut self, op: Op, reg: Reg, target: usize) {
434 self.emit_u8(op as u8);
435 self.emit_u8(reg);
436 let from = self.offset() as i32 + 4;
437 let offset = target as i32 - from;
438 self.emit_i32(offset);
439 }
440
441 /// Emit: Call dst, func_reg, args_start, arg_count
442 pub fn emit_call(&mut self, dst: Reg, func: Reg, args_start: Reg, arg_count: u8) {
443 self.emit_u8(Op::Call as u8);
444 self.emit_u8(dst);
445 self.emit_u8(func);
446 self.emit_u8(args_start);
447 self.emit_u8(arg_count);
448 }
449
450 /// Emit: GetPropertyByName dst, obj, name_idx
451 pub fn emit_get_prop_name(&mut self, dst: Reg, obj: Reg, name_idx: NameIdx) {
452 self.emit_u8(Op::GetPropertyByName as u8);
453 self.emit_u8(dst);
454 self.emit_u8(obj);
455 self.emit_u16(name_idx);
456 }
457
458 /// Emit: PushExceptionHandler catch_reg, offset (placeholder, returns patch position)
459 pub fn emit_push_exception_handler(&mut self, catch_reg: Reg) -> usize {
460 self.emit_u8(Op::PushExceptionHandler as u8);
461 self.emit_u8(catch_reg);
462 let pos = self.offset();
463 self.emit_i32(0); // placeholder for catch block offset
464 pos
465 }
466
467 /// Emit: PopExceptionHandler
468 pub fn emit_pop_exception_handler(&mut self) {
469 self.emit_u8(Op::PopExceptionHandler as u8);
470 }
471
472 /// Emit: SetPropertyByName obj, name_idx, val
473 pub fn emit_set_prop_name(&mut self, obj: Reg, name_idx: NameIdx, val: Reg) {
474 self.emit_u8(Op::SetPropertyByName as u8);
475 self.emit_u8(obj);
476 self.emit_u16(name_idx);
477 self.emit_u8(val);
478 }
479
480 /// Emit: LoadInt8 dst, value
481 pub fn emit_load_int8(&mut self, dst: Reg, value: i8) {
482 self.emit_u8(Op::LoadInt8 as u8);
483 self.emit_u8(dst);
484 self.emit_u8(value as u8);
485 }
486
487 /// Add a source map entry: current bytecode offset → source line.
488 pub fn add_source_map(&mut self, line: u32) {
489 let offset = self.offset() as u32;
490 self.func.source_map.push((offset, line));
491 }
492
493 /// Finalize and return the compiled function.
494 pub fn finish(self) -> Function {
495 self.func
496 }
497}
498
499// ── Disassembler ────────────────────────────────────────────
500
501impl Function {
502 /// Disassemble the bytecode to a human-readable string.
503 pub fn disassemble(&self) -> String {
504 let mut out = String::new();
505 out.push_str(&format!(
506 "function {}({} params, {} regs):\n",
507 if self.name.is_empty() {
508 "<anonymous>"
509 } else {
510 &self.name
511 },
512 self.param_count,
513 self.register_count,
514 ));
515
516 // Constants
517 if !self.constants.is_empty() {
518 out.push_str(" constants:\n");
519 for (i, c) in self.constants.iter().enumerate() {
520 out.push_str(&format!(" [{i}] {c:?}\n"));
521 }
522 }
523
524 // Names
525 if !self.names.is_empty() {
526 out.push_str(" names:\n");
527 for (i, n) in self.names.iter().enumerate() {
528 out.push_str(&format!(" [{i}] \"{n}\"\n"));
529 }
530 }
531
532 // Instructions
533 out.push_str(" code:\n");
534 let code = &self.code;
535 let mut pc = 0;
536 while pc < code.len() {
537 let offset = pc;
538 let byte = code[pc];
539 pc += 1;
540 let Some(op) = Op::from_byte(byte) else {
541 out.push_str(&format!(" {offset:04X} <unknown 0x{byte:02X}>\n"));
542 continue;
543 };
544 let line = match op {
545 Op::LoadConst => {
546 let dst = code[pc];
547 pc += 1;
548 let idx = u16::from_le_bytes([code[pc], code[pc + 1]]);
549 pc += 2;
550 format!("LoadConst r{dst}, #{idx}")
551 }
552 Op::LoadNull => {
553 let dst = code[pc];
554 pc += 1;
555 format!("LoadNull r{dst}")
556 }
557 Op::LoadUndefined => {
558 let dst = code[pc];
559 pc += 1;
560 format!("LoadUndefined r{dst}")
561 }
562 Op::LoadTrue => {
563 let dst = code[pc];
564 pc += 1;
565 format!("LoadTrue r{dst}")
566 }
567 Op::LoadFalse => {
568 let dst = code[pc];
569 pc += 1;
570 format!("LoadFalse r{dst}")
571 }
572 Op::Move => {
573 let dst = code[pc];
574 let src = code[pc + 1];
575 pc += 2;
576 format!("Move r{dst}, r{src}")
577 }
578 Op::LoadGlobal => {
579 let dst = code[pc];
580 pc += 1;
581 let idx = u16::from_le_bytes([code[pc], code[pc + 1]]);
582 pc += 2;
583 let name = self
584 .names
585 .get(idx as usize)
586 .map(|s| s.as_str())
587 .unwrap_or("?");
588 format!("LoadGlobal r{dst}, @{idx}(\"{name}\")")
589 }
590 Op::StoreGlobal => {
591 let idx = u16::from_le_bytes([code[pc], code[pc + 1]]);
592 pc += 2;
593 let src = code[pc];
594 pc += 1;
595 let name = self
596 .names
597 .get(idx as usize)
598 .map(|s| s.as_str())
599 .unwrap_or("?");
600 format!("StoreGlobal @{idx}(\"{name}\"), r{src}")
601 }
602 Op::Add | Op::Sub | Op::Mul | Op::Div | Op::Rem | Op::Exp => {
603 let dst = code[pc];
604 let lhs = code[pc + 1];
605 let rhs = code[pc + 2];
606 pc += 3;
607 format!("{op:?} r{dst}, r{lhs}, r{rhs}")
608 }
609 Op::Neg => {
610 let dst = code[pc];
611 let src = code[pc + 1];
612 pc += 2;
613 format!("Neg r{dst}, r{src}")
614 }
615 Op::BitAnd
616 | Op::BitOr
617 | Op::BitXor
618 | Op::ShiftLeft
619 | Op::ShiftRight
620 | Op::UShiftRight => {
621 let dst = code[pc];
622 let lhs = code[pc + 1];
623 let rhs = code[pc + 2];
624 pc += 3;
625 format!("{op:?} r{dst}, r{lhs}, r{rhs}")
626 }
627 Op::BitNot => {
628 let dst = code[pc];
629 let src = code[pc + 1];
630 pc += 2;
631 format!("BitNot r{dst}, r{src}")
632 }
633 Op::Eq
634 | Op::StrictEq
635 | Op::NotEq
636 | Op::StrictNotEq
637 | Op::LessThan
638 | Op::LessEq
639 | Op::GreaterThan
640 | Op::GreaterEq
641 | Op::InstanceOf
642 | Op::In => {
643 let dst = code[pc];
644 let lhs = code[pc + 1];
645 let rhs = code[pc + 2];
646 pc += 3;
647 format!("{op:?} r{dst}, r{lhs}, r{rhs}")
648 }
649 Op::LogicalNot => {
650 let dst = code[pc];
651 let src = code[pc + 1];
652 pc += 2;
653 format!("LogicalNot r{dst}, r{src}")
654 }
655 Op::TypeOf => {
656 let dst = code[pc];
657 let src = code[pc + 1];
658 pc += 2;
659 format!("TypeOf r{dst}, r{src}")
660 }
661 Op::Void => {
662 let dst = code[pc];
663 let src = code[pc + 1];
664 pc += 2;
665 format!("Void r{dst}, r{src}")
666 }
667 Op::Jump => {
668 let off =
669 i32::from_le_bytes([code[pc], code[pc + 1], code[pc + 2], code[pc + 3]]);
670 pc += 4;
671 let target = pc as i32 + off;
672 format!("Jump {off:+} (-> {target:04X})")
673 }
674 Op::JumpIfTrue | Op::JumpIfFalse | Op::JumpIfNullish => {
675 let reg = code[pc];
676 pc += 1;
677 let off =
678 i32::from_le_bytes([code[pc], code[pc + 1], code[pc + 2], code[pc + 3]]);
679 pc += 4;
680 let target = pc as i32 + off;
681 format!("{op:?} r{reg}, {off:+} (-> {target:04X})")
682 }
683 Op::Call => {
684 let dst = code[pc];
685 let func = code[pc + 1];
686 let args_start = code[pc + 2];
687 let arg_count = code[pc + 3];
688 pc += 4;
689 format!("Call r{dst}, r{func}, r{args_start}, {arg_count}")
690 }
691 Op::Return => {
692 let reg = code[pc];
693 pc += 1;
694 format!("Return r{reg}")
695 }
696 Op::Throw => {
697 let reg = code[pc];
698 pc += 1;
699 format!("Throw r{reg}")
700 }
701 Op::CreateClosure => {
702 let dst = code[pc];
703 pc += 1;
704 let idx = u16::from_le_bytes([code[pc], code[pc + 1]]);
705 pc += 2;
706 format!("CreateClosure r{dst}, func#{idx}")
707 }
708 Op::GetProperty => {
709 let dst = code[pc];
710 let obj = code[pc + 1];
711 let key = code[pc + 2];
712 pc += 3;
713 format!("GetProperty r{dst}, r{obj}, r{key}")
714 }
715 Op::SetProperty => {
716 let obj = code[pc];
717 let key = code[pc + 1];
718 let val = code[pc + 2];
719 pc += 3;
720 format!("SetProperty r{obj}, r{key}, r{val}")
721 }
722 Op::CreateObject => {
723 let dst = code[pc];
724 pc += 1;
725 format!("CreateObject r{dst}")
726 }
727 Op::CreateArray => {
728 let dst = code[pc];
729 pc += 1;
730 format!("CreateArray r{dst}")
731 }
732 Op::GetPropertyByName => {
733 let dst = code[pc];
734 let obj = code[pc + 1];
735 pc += 2;
736 let idx = u16::from_le_bytes([code[pc], code[pc + 1]]);
737 pc += 2;
738 let name = self
739 .names
740 .get(idx as usize)
741 .map(|s| s.as_str())
742 .unwrap_or("?");
743 format!("GetPropertyByName r{dst}, r{obj}, @{idx}(\"{name}\")")
744 }
745 Op::SetPropertyByName => {
746 let obj = code[pc];
747 pc += 1;
748 let idx = u16::from_le_bytes([code[pc], code[pc + 1]]);
749 pc += 2;
750 let val = code[pc];
751 pc += 1;
752 let name = self
753 .names
754 .get(idx as usize)
755 .map(|s| s.as_str())
756 .unwrap_or("?");
757 format!("SetPropertyByName r{obj}, @{idx}(\"{name}\"), r{val}")
758 }
759 Op::Delete => {
760 let dst = code[pc];
761 let obj = code[pc + 1];
762 let key = code[pc + 2];
763 pc += 3;
764 format!("Delete r{dst}, r{obj}, r{key}")
765 }
766 Op::LoadInt8 => {
767 let dst = code[pc];
768 let val = code[pc + 1] as i8;
769 pc += 2;
770 format!("LoadInt8 r{dst}, {val}")
771 }
772 Op::ForInInit => {
773 let dst = code[pc];
774 let obj = code[pc + 1];
775 pc += 2;
776 format!("ForInInit r{dst}, r{obj}")
777 }
778 Op::ForInNext => {
779 let dst_val = code[pc];
780 let dst_done = code[pc + 1];
781 let keys = code[pc + 2];
782 let idx = code[pc + 3];
783 pc += 4;
784 format!("ForInNext r{dst_val}, r{dst_done}, r{keys}, r{idx}")
785 }
786 Op::SetPrototype => {
787 let obj = code[pc];
788 let proto = code[pc + 1];
789 pc += 2;
790 format!("SetPrototype r{obj}, r{proto}")
791 }
792 Op::GetPrototype => {
793 let dst = code[pc];
794 let obj = code[pc + 1];
795 pc += 2;
796 format!("GetPrototype r{dst}, r{obj}")
797 }
798 Op::PushExceptionHandler => {
799 let catch_reg = code[pc];
800 let b0 = code[pc + 1] as i32;
801 let b1 = code[pc + 2] as i32;
802 let b2 = code[pc + 3] as i32;
803 let b3 = code[pc + 4] as i32;
804 let off = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
805 let target = (offset as i32 + 1 + 5 + off) as usize;
806 pc += 5;
807 format!("PushExceptionHandler r{catch_reg}, @{target:04X}")
808 }
809 Op::PopExceptionHandler => "PopExceptionHandler".to_string(),
810 };
811 out.push_str(&format!(" {offset:04X} {line}\n"));
812 }
813
814 // Nested functions
815 for (i, f) in self.functions.iter().enumerate() {
816 out.push_str(&format!("\n --- nested function [{i}] ---\n"));
817 let nested_dis = f.disassemble();
818 for line in nested_dis.lines() {
819 out.push_str(&format!(" {line}\n"));
820 }
821 }
822
823 out
824 }
825}
826
827impl fmt::Display for Function {
828 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
829 write!(f, "{}", self.disassemble())
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn test_op_roundtrip() {
839 // Verify all opcodes survive byte conversion.
840 let ops = [
841 Op::LoadConst,
842 Op::LoadNull,
843 Op::LoadUndefined,
844 Op::LoadTrue,
845 Op::LoadFalse,
846 Op::Move,
847 Op::LoadGlobal,
848 Op::StoreGlobal,
849 Op::Add,
850 Op::Sub,
851 Op::Mul,
852 Op::Div,
853 Op::Rem,
854 Op::Exp,
855 Op::Neg,
856 Op::BitAnd,
857 Op::BitOr,
858 Op::BitXor,
859 Op::ShiftLeft,
860 Op::ShiftRight,
861 Op::UShiftRight,
862 Op::BitNot,
863 Op::Eq,
864 Op::StrictEq,
865 Op::NotEq,
866 Op::StrictNotEq,
867 Op::LessThan,
868 Op::LessEq,
869 Op::GreaterThan,
870 Op::GreaterEq,
871 Op::LogicalNot,
872 Op::TypeOf,
873 Op::InstanceOf,
874 Op::In,
875 Op::Void,
876 Op::Jump,
877 Op::JumpIfTrue,
878 Op::JumpIfFalse,
879 Op::JumpIfNullish,
880 Op::Call,
881 Op::Return,
882 Op::Throw,
883 Op::CreateClosure,
884 Op::GetProperty,
885 Op::SetProperty,
886 Op::CreateObject,
887 Op::CreateArray,
888 Op::GetPropertyByName,
889 Op::SetPropertyByName,
890 Op::Delete,
891 Op::LoadInt8,
892 Op::ForInInit,
893 Op::ForInNext,
894 Op::SetPrototype,
895 Op::GetPrototype,
896 Op::PushExceptionHandler,
897 Op::PopExceptionHandler,
898 ];
899 for op in ops {
900 assert_eq!(
901 Op::from_byte(op as u8),
902 Some(op),
903 "roundtrip failed for {op:?}"
904 );
905 }
906 }
907
908 #[test]
909 fn test_unknown_opcode() {
910 assert_eq!(Op::from_byte(0xFF), None);
911 assert_eq!(Op::from_byte(0x00), None);
912 }
913
914 #[test]
915 fn test_constant_pool_dedup() {
916 let mut b = BytecodeBuilder::new("test".into(), 0);
917 let i1 = b.add_constant(Constant::Number(42.0));
918 let i2 = b.add_constant(Constant::Number(42.0));
919 assert_eq!(i1, i2);
920 let i3 = b.add_constant(Constant::String("hello".into()));
921 let i4 = b.add_constant(Constant::String("hello".into()));
922 assert_eq!(i3, i4);
923 assert_ne!(i1, i3);
924 assert_eq!(b.func.constants.len(), 2);
925 }
926
927 #[test]
928 fn test_name_dedup() {
929 let mut b = BytecodeBuilder::new("test".into(), 0);
930 let i1 = b.add_name("foo");
931 let i2 = b.add_name("foo");
932 assert_eq!(i1, i2);
933 let i3 = b.add_name("bar");
934 assert_ne!(i1, i3);
935 }
936
937 #[test]
938 fn test_emit_and_disassemble() {
939 let mut b = BytecodeBuilder::new("test".into(), 0);
940 b.func.register_count = 3;
941 let ci = b.add_constant(Constant::Number(10.0));
942 b.emit_reg_u16(Op::LoadConst, 0, ci);
943 b.emit_reg(Op::LoadNull, 1);
944 b.emit_reg3(Op::Add, 2, 0, 1);
945 b.emit_reg(Op::Return, 2);
946 let func = b.finish();
947 let dis = func.disassemble();
948 assert!(dis.contains("LoadConst r0, #0"));
949 assert!(dis.contains("LoadNull r1"));
950 assert!(dis.contains("Add r2, r0, r1"));
951 assert!(dis.contains("Return r2"));
952 }
953
954 #[test]
955 fn test_jump_patching() {
956 let mut b = BytecodeBuilder::new("test".into(), 0);
957 b.func.register_count = 1;
958 b.emit_reg(Op::LoadTrue, 0);
959 let patch = b.emit_cond_jump(Op::JumpIfFalse, 0);
960 b.emit_reg(Op::LoadNull, 0);
961 b.patch_jump(patch);
962 b.emit_reg(Op::Return, 0);
963 let func = b.finish();
964 let dis = func.disassemble();
965 // The jump should target the Return instruction
966 assert!(dis.contains("JumpIfFalse"));
967 assert!(dis.contains("Return"));
968 }
969
970 #[test]
971 fn test_load_int8() {
972 let mut b = BytecodeBuilder::new("test".into(), 0);
973 b.func.register_count = 1;
974 b.emit_load_int8(0, 42);
975 b.emit_load_int8(0, -1);
976 let func = b.finish();
977 let dis = func.disassemble();
978 assert!(dis.contains("LoadInt8 r0, 42"));
979 assert!(dis.contains("LoadInt8 r0, -1"));
980 }
981}