LibJS: Move bytecode instruction dumping to Rust

Generate Rust bytecode dump helpers from Bytecode.def and route
Executable::dump() through them for instruction stream formatting.

Add a small Rust runtime::value helper for decoding encoded LibJS
Values so immediate Value operands are formatted on the Rust side. C++
callbacks remain only for local names and GC-backed Value payloads that
still need LibJS object access.

Remove the generated C++ to_byte_string_impl() methods and the old
Instruction::to_byte_string() dispatch. The bytecode dump tests cover
output compatibility.
This commit is contained in:
Andreas Kling 2026-06-14 18:12:06 +02:00 committed by Andreas Kling
parent 5aac297558
commit 7a6af95db3
15 changed files with 1098 additions and 326 deletions

View file

@ -20,6 +20,7 @@
#include <LibJS/Runtime/ExternalMemory.h>
#include <LibJS/Runtime/SharedFunctionInstanceData.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/RustIntegration.h>
#include <LibJS/SourceCode.h>
namespace JS::Bytecode {
@ -490,31 +491,6 @@ static void dump_metadata(StringBuilder& output, Executable const& executable)
}
}
static void dump_bytecode(StringBuilder& output, Executable const& executable)
{
auto constexpr magenta = "\033[35;1m"sv;
auto constexpr reset = "\033[0m"sv;
InstructionStreamIterator it(executable.bytecode, &executable);
auto basic_block_start_offsets = collect_basic_block_start_offsets(executable);
size_t basic_block_offset_index = 0;
while (!it.at_end()) {
if (basic_block_offset_index < basic_block_start_offsets.size()
&& it.offset() == basic_block_start_offsets[basic_block_offset_index]) {
if (basic_block_offset_index > 0)
output.append('\n');
output.appendff("{}block{}{}:\n", magenta, basic_block_offset_index, reset);
++basic_block_offset_index;
}
output.appendff(" [{:4x}] {}\n", it.offset(), (*it).to_byte_string(executable));
++it;
}
}
void Executable::dump() const
{
StringBuilder output;
@ -522,7 +498,7 @@ void Executable::dump() const
dump_header(output, *this);
dump_metadata(output, *this);
output.append('\n');
dump_bytecode(output, *this);
RustIntegration::dump_bytecode(output, *this);
if (!exception_handlers.is_empty()) {
output.append("\nException handlers:\n"sv);

View file

@ -81,28 +81,4 @@ inline ByteString format_operand(StringView name, Operand encoded_operand, Bytec
return builder.to_byte_string();
}
inline ByteString format_operand_list(StringView name, ReadonlySpan<Operand> operands, Bytecode::Executable const& executable)
{
StringBuilder builder;
if (!name.is_empty())
builder.appendff("\033[32m{}\033[0m:[", name);
for (size_t i = 0; i < operands.size(); ++i) {
if (i != 0)
builder.append(", "sv);
builder.appendff("{}", format_operand(""sv, operands[i], executable));
}
builder.append("]"sv);
return builder.to_byte_string();
}
inline ByteString format_value_list(StringView name, ReadonlySpan<Value> values)
{
StringBuilder builder;
if (!name.is_empty())
builder.appendff("\033[32m{}\033[0m:[", name);
builder.join(", "sv, values);
builder.append("]"sv);
return builder.to_byte_string();
}
}

View file

@ -80,7 +80,6 @@ public:
Type type() const { return m_type; }
size_t length() const;
ByteString to_byte_string(Bytecode::Executable const&) const;
void visit_labels(Function<void(Label&)> visitor);
void visit_operands(Function<void(Operand&)> visitor);

View file

@ -7,7 +7,6 @@
#include <AK/TemporaryChange.h>
#include <LibJS/Bytecode/Debug.h>
#include <LibJS/Bytecode/FormatOperand.h>
#include <LibJS/Bytecode/Instruction.h>
#include <LibJS/Bytecode/Label.h>
#include <LibJS/Bytecode/Op.h>
@ -313,19 +312,4 @@ ThrowCompletionOr<Value> VM::run_executable(ExecutionContext& context, Executabl
return reg(Register::return_value());
}
ByteString Instruction::to_byte_string(Bytecode::Executable const& executable) const
{
#define __BYTECODE_OP(op) \
case Instruction::Type::op: \
return static_cast<Bytecode::Op::op const&>(*this).to_byte_string_impl(executable);
switch (type()) {
ENUMERATE_BYTECODE_OPS(__BYTECODE_OP)
default:
VERIFY_NOT_REACHED();
}
#undef __BYTECODE_OP
}
}

View file

@ -35,6 +35,13 @@ public:
explicit Operand(Register);
static Operand from_raw(u32 raw)
{
Operand operand;
operand.m_raw = raw;
return operand;
}
[[nodiscard]] bool is_invalid() const { return m_raw == 0xffffffffu; }
[[nodiscard]] bool is_register() const { return type() == Type::Register; }
[[nodiscard]] bool is_local() const { return type() == Type::Local; }
@ -54,6 +61,8 @@ public:
}
private:
Operand() = default;
u32 m_raw { 0 };
};

View file

@ -68,11 +68,31 @@ fn generate_rust_code(mut w: impl Write, ops: &[OpDef]) -> Result<(), Box<dyn st
generate_instruction_enum(&mut w, ops)?;
generate_instruction_impl(&mut w, ops)?;
generate_instruction_length_from_bytes(&mut w, ops)?;
generate_instruction_dump_from_bytes(&mut w, ops)?;
generate_visit_labels_from_bytes(&mut w, ops)?;
generate_instruction_is_terminator_from_opcode(&mut w, ops)?;
generate_validate_instruction(&mut w, ops)?;
Ok(())
}
fn generate_instruction_is_terminator_from_opcode(
mut w: impl Write,
ops: &[OpDef],
) -> Result<(), Box<dyn std::error::Error>> {
writeln!(w, "pub fn instruction_is_terminator_from_opcode(opcode: u8) -> bool {{")?;
let terminators = ops
.iter()
.enumerate()
.filter(|(_, op)| op.is_terminator)
.map(|(i, _)| i.to_string())
.collect::<Vec<_>>();
writeln!(w, " matches!(opcode, {})", terminators.join(" | "))?;
writeln!(w, "}}")?;
writeln!(w)?;
Ok(())
}
fn generate_num_opcodes_const(mut w: impl Write, ops: &[OpDef]) -> Result<(), Box<dyn std::error::Error>> {
writeln!(w, "/// Number of distinct opcodes (the valid range for the type byte).")?;
writeln!(w, "pub const NUM_OPCODES: u32 = {};", ops.len())?;
@ -153,6 +173,351 @@ fn generate_instruction_length_from_bytes(mut w: impl Write, ops: &[OpDef]) -> R
Ok(())
}
fn read_expr_for_type(ty: &str, offset: usize) -> String {
match ty {
"bool" => format!("bytes[at + {offset}] != 0"),
"u32"
| "Completion::Type"
| "IteratorHint"
| "EnvironmentMode"
| "PutKind"
| "ArgumentsKind"
| "FunctionNamePrefix"
| "PropertyLookupCacheIndex"
| "GlobalVariableCacheIndex"
| "EnvironmentCoordinateCacheIndex"
| "TemplateObjectCacheIndex"
| "ObjectShapeCacheIndex"
| "ObjectPropertyIteratorCacheIndex" => {
format!("super::validator::read_u32(bytes, at + {offset})")
}
"u64" | "Value" => format!("super::validator::read_u64(bytes, at + {offset})"),
"Operand" => format!("Operand::from_raw(super::validator::read_u32(bytes, at + {offset}))"),
"Optional<Operand>" => format!("Operand::optional_from_raw(super::validator::read_u32(bytes, at + {offset}))"),
"Label" => format!("Label(super::validator::read_u32(bytes, at + {offset}))"),
"Optional<Label>" => {
format!(
"if bytes[at + {offset} + 4] != 0 {{ Some(Label(super::validator::read_u32(bytes, at + {offset}))) }} else {{ None }}"
)
}
"IdentifierTableIndex" => format!("IdentifierTableIndex(super::validator::read_u32(bytes, at + {offset}))"),
"Optional<IdentifierTableIndex>" => {
format!("IdentifierTableIndex::optional_from_raw(super::validator::read_u32(bytes, at + {offset}))")
}
"PropertyKeyTableIndex" => format!("PropertyKeyTableIndex(super::validator::read_u32(bytes, at + {offset}))"),
"StringTableIndex" => format!("StringTableIndex(super::validator::read_u32(bytes, at + {offset}))"),
"Optional<StringTableIndex>" => {
format!("StringTableIndex::optional_from_raw(super::validator::read_u32(bytes, at + {offset}))")
}
"RegexTableIndex" => format!("RegexTableIndex(super::validator::read_u32(bytes, at + {offset}))"),
"EnvironmentCoordinate" => {
format!(
"EnvironmentCoordinate {{ hops: super::validator::read_u32(bytes, at + {offset}), index: super::validator::read_u32(bytes, at + {offset} + 4) }}"
)
}
"Builtin" => format!("bytes[at + {offset}]"),
other => unreachable!("Unknown field type: {other}"),
}
}
fn generate_field_reads(
w: &mut impl Write,
op: &OpDef,
layouts: &std::collections::HashMap<String, bytecode_def::OpLayout>,
) -> Result<(), Box<dyn std::error::Error>> {
let layout = layouts.get(&op.name).expect("layout missing for op");
for f in user_fields(op) {
if f.is_array {
continue;
}
let rname = rust_field_name(&f.name);
let offset = layout.field_offsets.get(&f.name).expect("field offset missing");
let expr = read_expr_for_type(&f.ty, *offset);
writeln!(w, " let {rname} = {expr};")?;
}
Ok(())
}
fn generate_array_bounds(
w: &mut impl Write,
op: &OpDef,
layouts: &std::collections::HashMap<String, bytecode_def::OpLayout>,
) -> Result<(), Box<dyn std::error::Error>> {
let layout = layouts.get(&op.name).expect("layout missing for op");
for f in user_fields(op) {
if !f.is_array {
continue;
}
let rname = rust_field_name(&f.name);
let count_name = rust_field_name(&find_count_field_name(op, f));
let offset = layout.field_offsets.get(&f.name).expect("array offset missing");
let elem_size = field_type_info(&f.ty).size;
writeln!(w, " let {rname}_offset = at + {offset};")?;
writeln!(w, " let {rname}_count = {count_name} as usize;")?;
writeln!(
w,
" let {rname}_end = {rname}_offset + {rname}_count * {elem_size};"
)?;
}
Ok(())
}
fn generate_instruction_dump_from_bytes(mut w: impl Write, ops: &[OpDef]) -> Result<(), Box<dyn std::error::Error>> {
let layouts = compute_layouts(ops);
writeln!(w, "#[allow(unused_variables)]")?;
writeln!(
w,
"pub fn dump_instruction_from_bytes(opcode: u8, bytes: &[u8], at: usize, dumper: &mut super::dump::BytecodeDumper<'_>) {{"
)?;
writeln!(w, " match opcode {{")?;
for (i, op) in ops.iter().enumerate() {
if op.name == "Instruction" {
continue;
}
writeln!(w, " {i} => {{")?;
generate_field_reads(&mut w, op, &layouts)?;
generate_array_bounds(&mut w, op, &layouts)?;
writeln!(w, " dumper.begin_instruction(\"{}\");", op.name)?;
let arrays: Vec<&Field> = op.fields.iter().filter(|f| f.is_array).collect();
let mut array_to_count = std::collections::HashMap::new();
let mut count_fields = std::collections::HashSet::new();
for af in arrays {
let count_field_name = find_count_field_name(op, af);
count_fields.insert(count_field_name.clone());
array_to_count.insert(af.name.clone(), rust_field_name(&count_field_name));
}
for f in &op.fields {
if f.name == "m_length" || f.name == "m_cache" {
continue;
}
let ty = f.ty.trim();
let label = rust_field_name(&f.name);
let rname = rust_field_name(&f.name);
if f.is_array {
let count_name = array_to_count.get(&f.name).expect("array count missing");
match ty {
"Operand" => {
writeln!(w, " if {count_name} != 0 {{")?;
writeln!(w, " dumper.append_piece(|dumper| {{")?;
writeln!(
w,
" dumper.append_operand_list(\"{label}\", bytes, {rname}_offset, {rname}_count);"
)?;
writeln!(w, " }});")?;
writeln!(w, " }}")?;
}
"Optional<Operand>" => {
writeln!(w, " if {count_name} != 0 {{")?;
writeln!(w, " dumper.append_piece(|dumper| {{")?;
writeln!(
w,
" dumper.append_optional_operand_list(\"{label}\", bytes, {rname}_offset, {rname}_count);"
)?;
writeln!(w, " }});")?;
writeln!(w, " }}")?;
}
"Value" => {
writeln!(w, " if {count_name} != 0 {{")?;
writeln!(w, " dumper.append_piece(|dumper| {{")?;
writeln!(
w,
" dumper.append_value_list(\"{label}\", bytes, {rname}_offset, {rname}_count);"
)?;
writeln!(w, " }});")?;
writeln!(w, " }}")?;
}
"Label" => {
writeln!(w, " if {count_name} != 0 {{")?;
writeln!(w, " dumper.append_piece(|dumper| {{")?;
writeln!(
w,
" dumper.append_label_list(\"{label}\", bytes, {rname}_offset, {rname}_count);"
)?;
writeln!(w, " }});")?;
writeln!(w, " }}")?;
}
"Optional<Label>" => {
writeln!(w, " if {count_name} != 0 {{")?;
writeln!(w, " dumper.append_piece(|dumper| {{")?;
writeln!(
w,
" dumper.append_optional_label_list(\"{label}\", bytes, {rname}_offset, {rname}_count);"
)?;
writeln!(w, " }});")?;
writeln!(w, " }}")?;
}
_ => {}
}
continue;
}
match ty {
"Operand" => writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_operand(\"{label}\", {rname}));"
)?,
"Optional<Operand>" => {
writeln!(w, " if let Some({rname}) = {rname} {{")?;
writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_operand(\"{label}\", {rname}));"
)?;
writeln!(w, " }}")?;
}
"Label" => writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_label(\"{label}\", {rname}.0));"
)?,
"Optional<Label>" => {
writeln!(w, " if let Some({rname}) = {rname} {{")?;
writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_label(\"{label}\", {rname}.0));"
)?;
writeln!(w, " }}")?;
}
"PropertyKeyTableIndex" => {
writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_property_key_quoted({rname}.0));"
)?;
}
"IdentifierTableIndex" => {
writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_identifier_quoted({rname}.0));"
)?;
}
"Optional<IdentifierTableIndex>" => {
let mut property_key_field = None;
let mut property_operand_field = None;
for other in &op.fields {
if other.ty.trim() == "PropertyKeyTableIndex" {
property_key_field = Some(rust_field_name(&other.name));
break;
}
if other.ty.trim() == "Operand" && other.name == "m_property" {
property_operand_field = Some(rust_field_name(&other.name));
break;
}
}
writeln!(w, " if let Some({rname}) = {rname} {{")?;
if let Some(property_key_field) = property_key_field {
writeln!(w, " dumper.append(\" \\u{{1b}}[37;1m(\");")?;
writeln!(w, " dumper.append_identifier_plain({rname}.0);")?;
writeln!(w, " dumper.append(\".\");")?;
writeln!(
w,
" dumper.append_property_key_plain({property_key_field}.0);"
)?;
writeln!(w, " dumper.append(\")\\u{{1b}}[0m\");")?;
} else if let Some(property_operand_field) = property_operand_field {
writeln!(w, " dumper.append(\" \\u{{1b}}[37;1m(\");")?;
writeln!(w, " dumper.append_identifier_plain({rname}.0);")?;
writeln!(w, " dumper.append(\"[\\u{{1b}}[0m\");")?;
writeln!(
w,
" dumper.append_operand(\"\", {property_operand_field});"
)?;
writeln!(w, " dumper.append(\"\\u{{1b}}[37;1m])\\u{{1b}}[0m\");")?;
} else if op.name == "GetLength" {
writeln!(w, " dumper.append(\" \\u{{1b}}[37;1m(\");")?;
writeln!(w, " dumper.append_identifier_plain({rname}.0);")?;
writeln!(w, " dumper.append(\".length)\\u{{1b}}[0m\");")?;
} else {
writeln!(w, " dumper.append(\" \\u{{1b}}[37;1m(\");")?;
writeln!(w, " dumper.append_identifier_plain({rname}.0);")?;
writeln!(w, " dumper.append(\")\\u{{1b}}[0m\");")?;
}
writeln!(w, " }}")?;
}
"StringTableIndex" => writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_string({rname}.0));"
)?,
"Optional<StringTableIndex>" => {
writeln!(w, " if let Some({rname}) = {rname} {{")?;
writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_string({rname}.0));"
)?;
writeln!(w, " }}")?;
}
"bool" => writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_bool(\"{label}\", {rname}));"
)?,
"PutKind" => writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_put_kind(\"{label}\", {rname}));"
)?,
_ if (ty == "u32" || ty == "u64" || ty == "u8") && !count_fields.contains(&f.name) => {
writeln!(
w,
" dumper.append_piece(|dumper| dumper.append_number(\"{label}\", {rname}));"
)?;
}
_ => {}
}
}
writeln!(w, " }}")?;
}
writeln!(w, " _ => unreachable!(\"unknown bytecode opcode\"),")?;
writeln!(w, " }}")?;
writeln!(w, "}}")?;
writeln!(w)?;
Ok(())
}
fn generate_visit_labels_from_bytes(mut w: impl Write, ops: &[OpDef]) -> Result<(), Box<dyn std::error::Error>> {
let layouts = compute_layouts(ops);
writeln!(w, "#[allow(unused_variables)]")?;
writeln!(
w,
"pub fn visit_labels_from_bytes(opcode: u8, bytes: &[u8], at: usize, visitor: &mut dyn FnMut(u32)) {{"
)?;
writeln!(w, " match opcode {{")?;
for (i, op) in ops.iter().enumerate() {
let label_fields: Vec<&Field> = user_fields(op)
.into_iter()
.filter(|f| f.ty == "Label" || f.ty == "Optional<Label>")
.collect();
if label_fields.is_empty() {
continue;
}
writeln!(w, " {i} => {{")?;
generate_field_reads(&mut w, op, &layouts)?;
for f in label_fields {
let rname = rust_field_name(&f.name);
if f.ty == "Label" {
writeln!(w, " visitor({rname}.0);")?;
} else {
writeln!(
w,
" if let Some({rname}) = {rname} {{ visitor({rname}.0); }}"
)?;
}
}
writeln!(w, " }}")?;
}
writeln!(w, " _ => {{}}")?;
writeln!(w, " }}")?;
writeln!(w, "}}")?;
writeln!(w)?;
Ok(())
}
fn emit_scalar_field_check(
mut w: impl Write,
field_name: &str,

View file

@ -0,0 +1,463 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
use std::ffi::c_void;
use super::instruction::dump_instruction_from_bytes;
use super::instruction::instruction_is_terminator_from_opcode;
use super::instruction::instruction_length_from_bytes;
use super::instruction::visit_labels_from_bytes;
use super::operand::Operand;
use super::validator::read_u32;
use crate::abort_on_panic;
use crate::runtime::value::EncodedValue;
use crate::runtime::value::EncodedValueKind;
#[repr(C)]
pub struct FFIDumpExceptionHandler {
pub start_offset: usize,
pub end_offset: usize,
pub handler_offset: usize,
}
#[repr(C)]
pub struct FFIBytecodeDumpCallbacks {
pub append: unsafe extern "C" fn(ctx: *mut c_void, data: *const u8, len: usize),
pub append_local: unsafe extern "C" fn(ctx: *mut c_void, index: u32),
pub append_identifier: unsafe extern "C" fn(ctx: *mut c_void, index: u32, quoted: bool),
pub append_property_key: unsafe extern "C" fn(ctx: *mut c_void, index: u32, quoted: bool),
pub append_string: unsafe extern "C" fn(ctx: *mut c_void, index: u32),
pub append_value_double: unsafe extern "C" fn(ctx: *mut c_void, value: f64),
pub append_value_string: unsafe extern "C" fn(ctx: *mut c_void, encoded: u64),
pub append_value_bigint: unsafe extern "C" fn(ctx: *mut c_void, encoded: u64),
pub append_value_fallback: unsafe extern "C" fn(ctx: *mut c_void, encoded: u64),
}
#[repr(C)]
pub struct FFIBytecodeDumpMetadata {
pub number_of_registers: u32,
pub registers_and_locals_count: u32,
pub local_index_base: u32,
pub argument_index_base: u32,
pub constants: *const u64,
pub constant_count: usize,
}
pub struct BytecodeDumper<'a> {
ctx: *mut c_void,
callbacks: &'a FFIBytecodeDumpCallbacks,
metadata: &'a FFIBytecodeDumpMetadata,
constants: &'a [u64],
basic_block_start_offsets: &'a [u32],
first_piece: bool,
}
impl<'a> BytecodeDumper<'a> {
fn new(
ctx: *mut c_void,
callbacks: &'a FFIBytecodeDumpCallbacks,
metadata: &'a FFIBytecodeDumpMetadata,
constants: &'a [u64],
basic_block_start_offsets: &'a [u32],
) -> Self {
Self {
ctx,
callbacks,
metadata,
constants,
basic_block_start_offsets,
first_piece: true,
}
}
pub fn append(&mut self, text: &str) {
unsafe {
(self.callbacks.append)(self.ctx, text.as_ptr(), text.len());
}
}
pub fn begin_instruction(&mut self, name: &str) {
self.first_piece = true;
self.append(name);
}
pub fn append_piece(&mut self, append_piece: impl FnOnce(&mut Self)) {
if self.first_piece {
self.append(" ");
self.first_piece = false;
} else {
self.append(", ");
}
append_piece(self);
}
pub fn append_operand(&mut self, name: &str, operand: Operand) {
if !name.is_empty() {
self.append("\x1b[32m");
self.append(name);
self.append("\x1b[0m:");
}
let raw = operand.raw();
if raw < self.metadata.number_of_registers {
if raw == 2 {
self.append("\x1b[33mthis\x1b[0m");
} else {
self.append("\x1b[33mreg");
self.append(&raw.to_string());
self.append("\x1b[0m");
}
} else if raw < self.metadata.registers_and_locals_count {
let index = raw - self.metadata.local_index_base;
self.append("\x1b[34m");
unsafe {
(self.callbacks.append_local)(self.ctx, index);
}
self.append("~");
self.append(&index.to_string());
self.append("\x1b[0m");
} else if raw < self.metadata.argument_index_base {
let index = raw - self.metadata.registers_and_locals_count;
self.append_value(self.constants[index as usize]);
} else {
let index = raw - self.metadata.argument_index_base;
self.append("\x1b[34marg");
self.append(&index.to_string());
self.append("\x1b[0m");
}
}
fn append_value(&mut self, encoded: u64) {
match EncodedValue::from_encoded(encoded).kind() {
EncodedValueKind::Empty => self.append("<Empty>"),
EncodedValueKind::Boolean(value) => {
self.append("Bool(");
self.append(if value { "true" } else { "false" });
self.append(")");
}
EncodedValueKind::Int32(value) => {
self.append("Int32(");
self.append(&value.to_string());
self.append(")");
}
EncodedValueKind::Double(value) => {
self.append("Double(");
unsafe {
(self.callbacks.append_value_double)(self.ctx, value);
}
self.append(")");
}
EncodedValueKind::BigInt => {
self.append("BigInt(");
unsafe {
(self.callbacks.append_value_bigint)(self.ctx, encoded);
}
self.append(")");
}
EncodedValueKind::String => {
self.append("String(\"");
unsafe {
(self.callbacks.append_value_string)(self.ctx, encoded);
}
self.append("\")");
}
EncodedValueKind::Undefined => self.append("Undefined"),
EncodedValueKind::Null => self.append("Null"),
EncodedValueKind::Other => {
self.append("Value(");
unsafe {
(self.callbacks.append_value_fallback)(self.ctx, encoded);
}
self.append(")");
}
}
}
fn append_value_without_side_effects(&mut self, encoded: u64) {
match EncodedValue::from_encoded(encoded).kind() {
EncodedValueKind::Empty => self.append("<empty>"),
EncodedValueKind::Boolean(value) => self.append(if value { "true" } else { "false" }),
EncodedValueKind::Int32(value) => self.append(&value.to_string()),
EncodedValueKind::Double(value) => unsafe {
(self.callbacks.append_value_double)(self.ctx, value);
},
EncodedValueKind::BigInt => unsafe {
(self.callbacks.append_value_bigint)(self.ctx, encoded);
},
EncodedValueKind::String => unsafe {
(self.callbacks.append_value_string)(self.ctx, encoded);
},
EncodedValueKind::Undefined => self.append("undefined"),
EncodedValueKind::Null => self.append("null"),
EncodedValueKind::Other => unsafe {
(self.callbacks.append_value_fallback)(self.ctx, encoded);
},
}
}
pub fn append_label(&mut self, name: &str, address: u32) {
if !name.is_empty() {
self.append("\x1b[32m");
self.append(name);
self.append("\x1b[0m:");
}
if let Ok(index) = self.basic_block_start_offsets.binary_search(&address) {
self.append("\x1b[35mblock");
self.append(&index.to_string());
self.append("\x1b[0m");
} else {
self.append("@");
self.append(&format!("{address:x}"));
}
}
pub fn append_identifier_quoted(&mut self, index: u32) {
unsafe {
(self.callbacks.append_identifier)(self.ctx, index, true);
}
}
pub fn append_identifier_plain(&mut self, index: u32) {
unsafe {
(self.callbacks.append_identifier)(self.ctx, index, false);
}
}
pub fn append_property_key_quoted(&mut self, index: u32) {
unsafe {
(self.callbacks.append_property_key)(self.ctx, index, true);
}
}
pub fn append_property_key_plain(&mut self, index: u32) {
unsafe {
(self.callbacks.append_property_key)(self.ctx, index, false);
}
}
pub fn append_string(&mut self, index: u32) {
unsafe {
(self.callbacks.append_string)(self.ctx, index);
}
}
pub fn append_bool(&mut self, name: &str, value: bool) {
self.append(name);
self.append(":");
self.append(if value { "true" } else { "false" });
}
pub fn append_number(&mut self, name: &str, value: impl std::fmt::Display) {
self.append(name);
self.append(":");
self.append(&value.to_string());
}
pub fn append_put_kind(&mut self, name: &str, value: u32) {
let kind = match value {
0 => "Normal",
1 => "Getter",
2 => "Setter",
3 => "Prototype",
4 => "Own",
_ => unreachable!("invalid PutKind"),
};
self.append(name);
self.append(":");
self.append(kind);
}
pub fn append_operand_list(&mut self, name: &str, bytes: &[u8], offset: usize, count: usize) {
self.append("\x1b[32m");
self.append(name);
self.append("\x1b[0m:[");
for i in 0..count {
if i != 0 {
self.append(", ");
}
self.append_operand("", Operand::from_raw(read_u32(bytes, offset + i * 4)));
}
self.append("]");
}
pub fn append_optional_operand_list(&mut self, name: &str, bytes: &[u8], offset: usize, count: usize) {
self.append(name);
self.append(":[");
let mut first_elem = true;
for i in 0..count {
let raw = read_u32(bytes, offset + i * 4);
let Some(operand) = Operand::optional_from_raw(raw) else {
continue;
};
if !first_elem {
self.append(", ");
}
first_elem = false;
self.append_operand(name, operand);
}
self.append("]");
}
pub fn append_label_list(&mut self, name: &str, bytes: &[u8], offset: usize, count: usize) {
self.append(name);
self.append(":[");
for i in 0..count {
if i != 0 {
self.append(", ");
}
self.append_label("", read_u32(bytes, offset + i * 4));
}
self.append("]");
}
pub fn append_optional_label_list(&mut self, name: &str, bytes: &[u8], offset: usize, count: usize) {
self.append(name);
self.append(":[");
let mut first_elem = true;
for i in 0..count {
let element_offset = offset + i * 8;
if bytes[element_offset + 4] == 0 {
continue;
}
if !first_elem {
self.append(", ");
}
first_elem = false;
self.append_label("", read_u32(bytes, element_offset));
}
self.append("]");
}
pub fn append_value_list(&mut self, name: &str, bytes: &[u8], offset: usize, count: usize) {
if !name.is_empty() {
self.append("\x1b[32m");
self.append(name);
self.append("\x1b[0m:[");
}
for i in 0..count {
if i != 0 {
self.append(", ");
}
let value = u64::from_ne_bytes(bytes[offset + i * 8..offset + (i + 1) * 8].try_into().unwrap());
self.append_value_without_side_effects(value);
}
self.append("]");
}
}
fn collect_basic_block_start_offsets(bytecode: &[u8], exception_handlers: &[FFIDumpExceptionHandler]) -> Vec<u32> {
let mut offsets = vec![0];
let append_offset = |offsets: &mut Vec<u32>, offset: usize| {
let offset = u32::try_from(offset).expect("bytecode offset exceeds u32::MAX");
if !offsets.contains(&offset) {
offsets.push(offset);
}
};
let append_instruction_offset = |offsets: &mut Vec<u32>, offset: usize| {
if offset < bytecode.len() {
append_offset(offsets, offset);
}
};
let mut at = 0;
while at < bytecode.len() {
let opcode = bytecode[at];
let length = instruction_length_from_bytes(opcode, bytecode, at)
.expect("validated bytecode should have valid instruction lengths");
let next_offset = at + length;
visit_labels_from_bytes(opcode, bytecode, at, &mut |address| {
append_offset(&mut offsets, address as usize);
});
if instruction_is_terminator_from_opcode(opcode) && next_offset < bytecode.len() {
append_offset(&mut offsets, next_offset);
}
at = next_offset;
}
for handler in exception_handlers {
append_instruction_offset(&mut offsets, handler.start_offset);
append_instruction_offset(&mut offsets, handler.end_offset);
append_instruction_offset(&mut offsets, handler.handler_offset);
}
offsets.sort_unstable();
offsets
}
/// Dump a validated bytecode instruction stream through C++ formatting callbacks.
///
/// # Safety
/// `bytecode_ptr` must point to `bytecode_len` bytes of validated bytecode, or
/// be null when `bytecode_len` is zero. `exception_handlers` must point to
/// `exception_handler_count` valid entries, or be null when the count is zero.
/// `metadata` and `callbacks` must point to valid structs, and every callback
/// must remain callable for the duration of this function. `metadata.constants`
/// must point to `metadata.constant_count` encoded Values, or be null when the
/// count is zero. `ctx` is passed through to callbacks and must remain valid
/// for their requirements.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_dump_bytecode(
bytecode_ptr: *const u8,
bytecode_len: usize,
exception_handlers: *const FFIDumpExceptionHandler,
exception_handler_count: usize,
metadata: *const FFIBytecodeDumpMetadata,
ctx: *mut c_void,
callbacks: *const FFIBytecodeDumpCallbacks,
) {
abort_on_panic(|| unsafe {
let bytecode = if bytecode_len == 0 {
&[]
} else {
std::slice::from_raw_parts(bytecode_ptr, bytecode_len)
};
let exception_handlers = if exception_handler_count == 0 {
&[]
} else {
std::slice::from_raw_parts(exception_handlers, exception_handler_count)
};
let metadata = metadata.as_ref().expect("rust_dump_bytecode metadata must not be null");
let constants = if metadata.constant_count == 0 {
&[]
} else {
std::slice::from_raw_parts(metadata.constants, metadata.constant_count)
};
let callbacks = callbacks
.as_ref()
.expect("rust_dump_bytecode callbacks must not be null");
let basic_block_start_offsets = collect_basic_block_start_offsets(bytecode, exception_handlers);
let mut dumper = BytecodeDumper::new(ctx, callbacks, metadata, constants, &basic_block_start_offsets);
let mut basic_block_offset_index = 0;
let mut at = 0;
while at < bytecode.len() {
if basic_block_offset_index < basic_block_start_offsets.len()
&& at == basic_block_start_offsets[basic_block_offset_index] as usize
{
if basic_block_offset_index > 0 {
dumper.append("\n");
}
dumper.append("\x1b[35;1mblock");
dumper.append(&basic_block_offset_index.to_string());
dumper.append("\x1b[0m:\n");
basic_block_offset_index += 1;
}
dumper.append(" [");
dumper.append(&format!("{at:4x}"));
dumper.append("] ");
dump_instruction_from_bytes(bytecode[at], bytecode, at, &mut dumper);
dumper.append("\n");
at += instruction_length_from_bytes(bytecode[at], bytecode, at)
.expect("validated bytecode should have valid instruction lengths");
}
});
}

View file

@ -21,6 +21,7 @@
pub mod basic_block;
pub mod codegen;
pub mod dump;
pub mod ffi;
pub mod generator;
pub mod instruction;

View file

@ -96,6 +96,14 @@ impl Operand {
self.0
}
pub fn from_raw(raw: u32) -> Self {
Self(raw)
}
pub fn optional_from_raw(raw: u32) -> Option<Self> {
if raw == Self::INVALID { None } else { Some(Self(raw)) }
}
/// Offset the index by the given amount, stripping the type tag and
/// leaving a flat index into the combined
/// [registers | locals | constants | arguments] array.
@ -134,6 +142,10 @@ pub struct StringTableIndex(pub u32);
impl StringTableIndex {
pub const INVALID: u32 = 0xFFFF_FFFF;
pub fn optional_from_raw(raw: u32) -> Option<Self> {
if raw == Self::INVALID { None } else { Some(Self(raw)) }
}
}
/// Index into the identifier table.
@ -142,6 +154,10 @@ pub struct IdentifierTableIndex(pub u32);
impl IdentifierTableIndex {
pub const INVALID: u32 = 0xFFFF_FFFF;
pub fn optional_from_raw(raw: u32) -> Option<Self> {
if raw == Self::INVALID { None } else { Some(Self(raw)) }
}
}
/// Index into the property key table.

View file

@ -87,6 +87,7 @@ mod bytecode_cache;
pub mod fast_hash;
pub mod lexer;
pub mod parser;
pub mod runtime;
pub mod scope_collector;
pub mod token;

View file

@ -0,0 +1,7 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
pub mod value;

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum EncodedValueKind {
Empty,
Boolean(bool),
Int32(i32),
Double(f64),
String,
BigInt,
Undefined,
Null,
Other,
}
/// The encoded representation of a LibJS `Value`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EncodedValue(u64);
impl EncodedValue {
const CANON_NAN_BITS: u64 = 0x7ff8_0000_0000_0000;
const TAG_SHIFT: u64 = 48;
const BASE_TAG: u64 = 0x7ff8;
const IS_CELL_BIT: u64 = 0x8000 | Self::BASE_TAG;
const STRING_TAG: u64 = 0b010 | Self::IS_CELL_BIT;
const BIGINT_TAG: u64 = 0b101 | Self::IS_CELL_BIT;
const UNDEFINED_TAG: u64 = 0b110 | Self::BASE_TAG;
const NULL_TAG: u64 = 0b111 | Self::BASE_TAG;
const BOOLEAN_TAG: u64 = 0b001 | Self::BASE_TAG;
const INT32_TAG: u64 = 0b010 | Self::BASE_TAG;
const EMPTY_TAG: u64 = 0b011 | Self::BASE_TAG;
pub const fn from_encoded(encoded: u64) -> Self {
Self(encoded)
}
pub const fn encoded(self) -> u64 {
self.0
}
pub fn kind(self) -> EncodedValueKind {
let tag = self.0 >> Self::TAG_SHIFT;
if self.0 == (Self::EMPTY_TAG << Self::TAG_SHIFT) {
EncodedValueKind::Empty
} else if tag == Self::BOOLEAN_TAG {
EncodedValueKind::Boolean(self.0 & 1 != 0)
} else if tag == Self::INT32_TAG {
EncodedValueKind::Int32((self.0 & 0xffff_ffff) as u32 as i32)
} else if (self.0 & Self::CANON_NAN_BITS) != Self::CANON_NAN_BITS || self.0 == Self::CANON_NAN_BITS {
EncodedValueKind::Double(f64::from_bits(self.0))
} else if tag == Self::BIGINT_TAG {
EncodedValueKind::BigInt
} else if tag == Self::STRING_TAG {
EncodedValueKind::String
} else if self.0 == (Self::UNDEFINED_TAG << Self::TAG_SHIFT) {
EncodedValueKind::Undefined
} else if self.0 == (Self::NULL_TAG << Self::TAG_SHIFT) {
EncodedValueKind::Null
} else {
EncodedValueKind::Other
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_immediate_values() {
assert_eq!(
EncodedValue::from_encoded(EncodedValue::EMPTY_TAG << EncodedValue::TAG_SHIFT).kind(),
EncodedValueKind::Empty
);
assert_eq!(
EncodedValue::from_encoded((EncodedValue::BOOLEAN_TAG << EncodedValue::TAG_SHIFT) | 1).kind(),
EncodedValueKind::Boolean(true)
);
assert_eq!(
EncodedValue::from_encoded((EncodedValue::BOOLEAN_TAG << EncodedValue::TAG_SHIFT) | 0).kind(),
EncodedValueKind::Boolean(false)
);
assert_eq!(
EncodedValue::from_encoded((EncodedValue::INT32_TAG << EncodedValue::TAG_SHIFT) | 0xffff_ffff).kind(),
EncodedValueKind::Int32(-1)
);
assert!(matches!(
EncodedValue::from_encoded(EncodedValue::CANON_NAN_BITS).kind(),
EncodedValueKind::Double(value) if value.to_bits() == EncodedValue::CANON_NAN_BITS
));
assert_eq!(
EncodedValue::from_encoded(EncodedValue::UNDEFINED_TAG << EncodedValue::TAG_SHIFT).kind(),
EncodedValueKind::Undefined
);
assert_eq!(
EncodedValue::from_encoded(EncodedValue::NULL_TAG << EncodedValue::TAG_SHIFT).kind(),
EncodedValueKind::Null
);
}
}

View file

@ -6,6 +6,7 @@
#include <LibJS/RustIntegration.h>
#include <AK/BitCast.h>
#include <AK/NumericLimits.h>
#include <AK/TemporaryChange.h>
#include <AK/Utf16String.h>
@ -71,6 +72,16 @@ static Utf16String utf16_from_raw(uint16_t const* data, size_t len)
return Utf16String::from_utf16(utf16_view_from_bytes(data, len));
}
static StringView string_view_from_rust_bytes(uint8_t const* data, size_t len)
{
return { reinterpret_cast<char const*>(data), len };
}
struct BytecodeDumpBuilder {
StringBuilder& output;
GC::Ref<Bytecode::Executable const> executable;
};
// --- Error collection callbacks ---
// Collects parse errors as a Vector<ParserError> (for Script/Module compilation).
@ -110,6 +121,122 @@ struct ScriptGdiBuilder {
namespace JS::FFI {
static void bytecode_dump_append(void* ctx, uint8_t const* data, size_t len)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
builder.output.append(JS::RustIntegration::string_view_from_rust_bytes(data, len));
}
static void bytecode_dump_append_local(void* ctx, uint32_t index)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
builder.output.append(builder.executable->local_variable_names[index].name);
}
static void bytecode_dump_append_identifier(void* ctx, uint32_t index, bool quoted)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
auto identifier = builder.executable->identifier_table->get(JS::Bytecode::IdentifierTableIndex { index });
if (quoted)
builder.output.appendff("\033[36m`{}`\033[0m", identifier);
else
builder.output.append(identifier);
}
static void bytecode_dump_append_property_key(void* ctx, uint32_t index, bool quoted)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
auto const& property_key = builder.executable->property_key_table->get(JS::Bytecode::PropertyKeyTableIndex { index });
if (quoted)
builder.output.appendff("\033[36m`{}`\033[0m", property_key);
else
builder.output.appendff("{}", property_key);
}
static void bytecode_dump_append_string(void* ctx, uint32_t index)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
builder.output.append(builder.executable->get_string(JS::Bytecode::StringTableIndex { index }));
}
static void bytecode_dump_append_value_double(void* ctx, double value)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
builder.output.appendff("{}", value);
}
static void bytecode_dump_append_value_string(void* ctx, uint64_t encoded)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
auto value = bit_cast<Value>(encoded);
builder.output.append(value.as_string().utf8_string_view());
}
static void bytecode_dump_append_value_bigint(void* ctx, uint64_t encoded)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
auto value = bit_cast<Value>(encoded);
builder.output.append(MUST(value.as_bigint().to_string()));
}
static void bytecode_dump_append_value_fallback(void* ctx, uint64_t encoded)
{
auto& builder = *static_cast<JS::RustIntegration::BytecodeDumpBuilder*>(ctx);
auto value = bit_cast<Value>(encoded);
builder.output.appendff("{}", value);
}
}
namespace JS::RustIntegration {
void dump_bytecode(StringBuilder& output, Bytecode::Executable const& executable)
{
Vector<FFI::FFIDumpExceptionHandler> exception_handlers;
exception_handlers.ensure_capacity(executable.exception_handlers.size());
for (auto const& handler : executable.exception_handlers) {
exception_handlers.append({
.start_offset = handler.start_offset,
.end_offset = handler.end_offset,
.handler_offset = handler.handler_offset,
});
}
BytecodeDumpBuilder builder { output, executable };
FFI::FFIBytecodeDumpCallbacks callbacks {
.append = FFI::bytecode_dump_append,
.append_local = FFI::bytecode_dump_append_local,
.append_identifier = FFI::bytecode_dump_append_identifier,
.append_property_key = FFI::bytecode_dump_append_property_key,
.append_string = FFI::bytecode_dump_append_string,
.append_value_double = FFI::bytecode_dump_append_value_double,
.append_value_string = FFI::bytecode_dump_append_value_string,
.append_value_bigint = FFI::bytecode_dump_append_value_bigint,
.append_value_fallback = FFI::bytecode_dump_append_value_fallback,
};
FFI::FFIBytecodeDumpMetadata metadata {
.number_of_registers = executable.number_of_registers,
.registers_and_locals_count = executable.registers_and_locals_count,
.local_index_base = executable.local_index_base,
.argument_index_base = executable.argument_index_base,
.constants = reinterpret_cast<uint64_t const*>(executable.constants.data()),
.constant_count = executable.constants.size(),
};
FFI::rust_dump_bytecode(
executable.bytecode.data(),
executable.bytecode.size(),
exception_handlers.data(),
exception_handlers.size(),
&metadata,
&builder,
&callbacks);
}
}
namespace JS::FFI {
extern "C" void script_gdi_push_lexical_name(void* ctx, uint16_t const* name, size_t len)
{
static_cast<JS::RustIntegration::ScriptGdiBuilder*>(ctx)->result.lexical_names.append(JS::RustIntegration::utf16_fly_from(name, len));

View file

@ -12,6 +12,7 @@
#include <AK/Optional.h>
#include <AK/Result.h>
#include <AK/Span.h>
#include <AK/StringBuilder.h>
#include <AK/Utf16FlyString.h>
#include <LibCore/Forward.h>
#include <LibCore/ImmutableBytes.h>
@ -191,6 +192,8 @@ Optional<Vector<GC::Root<SharedFunctionInstanceData>>> compile_builtin_file(
// Returns nullptr if Rust is not available or the SFD doesn't use Rust compilation.
GC::Ptr<Bytecode::Executable> compile_function(VM& vm, SharedFunctionInstanceData& shared_data, bool builtin_abstract_operations_enabled);
JS_API void dump_bytecode(StringBuilder&, Bytecode::Executable const&);
JS_API void* clone_function_ast(void const*);
JS_API FFI::CompiledFunction* compile_function_off_thread(void* function_ast, size_t length_in_code_units, bool builtin_abstract_operations_enabled);
// Attach a previously compiled function for lazy materialization.

View file

@ -39,11 +39,6 @@ def is_optional_label_type(t: str) -> bool:
return t.strip() == "Optional<Label>"
def is_value_type(t: str) -> bool:
t = t.strip()
return t == "Value" or t == "Optional<Value>"
def find_count_field_name(op: OpDef, array_field: Field) -> Optional[str]:
"""
Heuristic: look for a u32/size_t field matching
@ -283,8 +278,6 @@ def generate_class(op: OpDef) -> str:
lines.append(" }")
lines.append("")
lines.append(" ByteString to_byte_string_impl(Bytecode::Executable const&) const;")
visit_operands = generate_visit_operands(op)
if visit_operands:
lines.append(visit_operands)
@ -325,21 +318,6 @@ def generate_op_namespace_body(ops: List[OpDef]) -> str:
return "\n".join(lines)
NUMERIC_TYPES = {
"u8",
"u16",
"u32",
"u64",
"i8",
"i16",
"i32",
"i64",
"int",
"unsigned",
"unsigned int",
"size_t",
}
CACHE_INDEX_TYPES = {
"PropertyLookupCacheIndex",
"GlobalVariableCacheIndex",
@ -361,247 +339,10 @@ def cpp_type_for_field(t: str) -> str:
return t
def generate_to_byte_string_impl(op: OpDef) -> str:
if op.name == "Instruction":
return ""
lines: List[str] = []
lines.append(
f"ByteString {op.name}::to_byte_string_impl([[maybe_unused]] Bytecode::Executable const& executable) const"
)
lines.append("{")
lines.append(" StringBuilder builder;")
lines.append(f' builder.append("{op.name}"sv);')
lines.append("")
lines.append(" bool first = true;")
lines.append(" [[maybe_unused]] auto append_piece = [&](auto const& piece) {")
lines.append(" if (first) {")
lines.append(" builder.append(' ');")
lines.append(" first = false;")
lines.append(" } else {")
lines.append(' builder.append(", "sv);')
lines.append(" }")
lines.append(" builder.append(piece);")
lines.append(" };")
lines.append("")
arrays: List[Field] = [f for f in op.fields if f.is_array]
array_to_count = {af.name: get_count_field_name_or_die(op, af) for af in arrays}
count_fields = set(array_to_count.values())
for f in op.fields:
if f.name == "m_length" or f.name == "m_cache":
continue
t = f.type.strip()
label = getter_name_for_field(f.name)
if f.is_array:
count_name = array_to_count[f.name]
if t == "Operand":
lines.append(f" if ({count_name} != 0)")
lines.append(
f' append_piece(format_operand_list("{label}"sv, {{ {f.name}, {count_name} }}, executable));'
)
lines.append("")
continue
if t == "Optional<Operand>":
lines.append(f" if ({count_name} != 0) {{")
lines.append(" StringBuilder list_builder;")
lines.append(f' list_builder.appendff("{{}}:[", "{label}"sv);')
lines.append(" bool first_elem = true;")
lines.append(f" for (size_t i = 0; i < {count_name}; ++i) {{")
lines.append(f" if (!{f.name}[i].has_value())")
lines.append(" continue;")
lines.append(" if (!first_elem)")
lines.append(' list_builder.append(", "sv);')
lines.append(" first_elem = false;")
lines.append(
f' list_builder.append(format_operand("{label}"sv, {f.name}[i].value(), executable));'
)
lines.append(" }")
lines.append(" list_builder.append(']');")
lines.append(" append_piece(list_builder.to_byte_string());")
lines.append(" }")
lines.append("")
continue
if t == "Value":
lines.append(f" if ({count_name} != 0)")
lines.append(
f' append_piece(format_value_list("{label}"sv, ReadonlySpan<Value> {{ {f.name}, {count_name} }}));'
)
lines.append("")
continue
if t == "Label":
lines.append(f" if ({count_name} != 0) {{")
lines.append(" StringBuilder list_builder;")
lines.append(f' list_builder.appendff("{{}}:[", "{label}"sv);')
lines.append(" bool first_elem = true;")
lines.append(f" for (size_t i = 0; i < {count_name}; ++i) {{")
lines.append(" if (!first_elem)")
lines.append(' list_builder.append(", "sv);')
lines.append(" first_elem = false;")
lines.append(f' list_builder.append(format_label(""sv, {f.name}[i], executable));')
lines.append(" }")
lines.append(" list_builder.append(']');")
lines.append(" append_piece(list_builder.to_byte_string());")
lines.append(" }")
lines.append("")
continue
if t == "Optional<Label>":
lines.append(f" if ({count_name} != 0) {{")
lines.append(" StringBuilder list_builder;")
lines.append(f' list_builder.appendff("{{}}:[", "{label}"sv);')
lines.append(" bool first_elem = true;")
lines.append(f" for (size_t i = 0; i < {count_name}; ++i) {{")
lines.append(f" if (!{f.name}[i].has_value())")
lines.append(" continue;")
lines.append(" if (!first_elem)")
lines.append(' list_builder.append(", "sv);')
lines.append(" first_elem = false;")
lines.append(f' list_builder.append(format_label(""sv, {f.name}[i].value(), executable));')
lines.append(" }")
lines.append(" list_builder.append(']');")
lines.append(" append_piece(list_builder.to_byte_string());")
lines.append(" }")
lines.append("")
continue
# other array types not printed
continue
if t == "Operand":
lines.append(f' append_piece(format_operand("{label}"sv, {f.name}, executable));')
lines.append("")
continue
if t == "Optional<Operand>":
lines.append(f" if ({f.name}.has_value())")
lines.append(f' append_piece(format_operand("{label}"sv, {f.name}.value(), executable));')
lines.append("")
continue
if t == "Label":
lines.append(f' append_piece(format_label("{label}"sv, {f.name}, executable));')
lines.append("")
continue
if t == "Optional<Label>":
lines.append(f" if ({f.name}.has_value())")
lines.append(f' append_piece(format_label("{label}"sv, {f.name}.value(), executable));')
lines.append("")
continue
if t == "PropertyKeyTableIndex":
lines.append(
f' append_piece(ByteString::formatted("\\033[36m`{{}}`\\033[0m", executable.property_key_table->get({f.name})));'
)
lines.append("")
continue
if t == "IdentifierTableIndex":
lines.append(
f' append_piece(ByteString::formatted("\\033[36m`{{}}`\\033[0m", executable.identifier_table->get({f.name})));'
)
lines.append("")
continue
if t == "Optional<IdentifierTableIndex>":
# Find a property field in the same op to join with.
property_key_field = None
property_operand_field = None
for other in op.fields:
if other.type.strip() == "PropertyKeyTableIndex":
property_key_field = other
break
if other.type.strip() == "Operand" and other.name == "m_property":
property_operand_field = other
break
lines.append(f" if ({f.name}.has_value())")
if property_key_field:
lines.append(
f' builder.appendff(" \\033[37;1m({{}}.{{}})\\033[0m", executable.identifier_table->get({f.name}.value()), executable.property_key_table->get({property_key_field.name}));'
)
elif property_operand_field:
lines.append(" {")
lines.append(
f' auto property_hint = format_operand(""sv, {property_operand_field.name}, executable);'
)
lines.append(
f' builder.appendff(" \\033[37;1m({{}}[\\033[0m{{}}\\033[37;1m])\\033[0m", executable.identifier_table->get({f.name}.value()), property_hint);'
)
lines.append(" }")
elif op.name == "GetLength":
lines.append(
f' builder.appendff(" \\033[37;1m({{}}.length)\\033[0m", executable.identifier_table->get({f.name}.value()));'
)
else:
lines.append(
f' builder.appendff(" \\033[37;1m({{}})\\033[0m", executable.identifier_table->get({f.name}.value()));'
)
lines.append("")
continue
if t == "StringTableIndex":
lines.append(f" append_piece(executable.get_string({f.name}));")
lines.append("")
continue
if t == "Optional<StringTableIndex>":
lines.append(f" if ({f.name}.has_value())")
lines.append(f" append_piece(executable.get_string({f.name}.value()));")
lines.append("")
continue
if is_value_type(t):
# not printed for now
continue
if t == "bool":
lines.append(f' append_piece(ByteString::formatted("{label}:{{}}", {f.name}));')
lines.append("")
continue
if t in NUMERIC_TYPES and f.name not in count_fields:
lines.append(f' append_piece(ByteString::formatted("{label}:{{}}", {f.name}));')
lines.append("")
continue
if t == "PutKind":
lines.append(f' append_piece(ByteString::formatted("{label}:{{}}", put_kind_to_string({f.name})));')
lines.append("")
continue
# other types (enums, refs, etc.) skipped
lines.append(" return builder.to_byte_string();")
lines.append("}")
lines.append("")
return "\n".join(lines)
def generate_op_cpp_body(ops: List[OpDef]) -> str:
lines: List[str] = []
lines.append("#include <AK/StringBuilder.h>")
lines.append("#include <AK/StringView.h>")
lines.append("#include <LibJS/Bytecode/FormatOperand.h>")
lines.append("#include <LibJS/Bytecode/Op.h>")
lines.append("")
lines.append("namespace JS::Bytecode::Op {")
lines.append("")
for op in ops:
impl = generate_to_byte_string_impl(op)
if impl:
lines.append(impl)
lines.append("} // namespace JS::Bytecode::Op")
return "\n".join(lines)