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