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