use crate::bytecode::{Decoder, FamilyLayout, Instr, Operand};
use crate::serializer::{CodeCache, Elem, ObjId, Ref, SlotValue};
use crate::tables::VersionTable;
use std::fmt::Write as _;
mod fallback_slots {
pub const SFI_FUNCTION_DATA: usize = 1;
pub const SFI_NAME: usize = 2;
pub const BCA_CONSTANT_POOL: usize = 2;
pub const BCA_HANDLER_TABLE: usize = 3;
pub const BCA_SOURCE_POSITION_TABLE: usize = 4;
}
pub struct Disassembler<'a> {
cache: &'a CodeCache<'a>,
table: &'a VersionTable,
layout: FamilyLayout,
source_len: u32,
ro_map: Option<std::rc::Rc<crate::decompile::RoMap>>,
}
impl<'a> Disassembler<'a> {
pub fn sfi_function_data_slots(&self) -> Vec<usize> {
let ts = self.layout.tagged_size;
if let Some(sfi) = &self.table.shared_function_info {
let v: Vec<usize> = ["function_data", "trusted_function_data", "untrusted_function_data"]
.iter()
.filter_map(|n| sfi.slot(ts, n))
.collect();
if !v.is_empty() {
return v;
}
}
vec![fallback_slots::SFI_FUNCTION_DATA]
}
pub fn sfi_name_slot(&self) -> usize {
let ts = self.layout.tagged_size;
self.table
.shared_function_info
.as_ref()
.and_then(|s| s.slot(ts, "name_or_scope_info"))
.unwrap_or(fallback_slots::SFI_NAME)
}
fn bca_slot(&self, names: &[&str], fallback: usize) -> usize {
let ts = self.layout.tagged_size;
self.table
.bytecode_array
.as_ref()
.and_then(|b| b.fields.get(*names.first().unwrap()).copied().map(|o| o / ts))
.or_else(|| {
self.table
.bytecode_array
.as_ref()
.and_then(|b| names.iter().find_map(|n| b.fields.get(*n).copied().map(|o| o / ts)))
})
.unwrap_or(fallback)
}
pub fn bca_constant_pool_slot(&self) -> usize {
self.bca_slot(&["constant_pool"], fallback_slots::BCA_CONSTANT_POOL)
}
pub fn bca_handler_table_slot(&self) -> usize {
self.bca_slot(&["handler_table"], fallback_slots::BCA_HANDLER_TABLE)
}
pub fn bca_source_position_slot(&self) -> usize {
self.bca_slot(&["source_position_table"], fallback_slots::BCA_SOURCE_POSITION_TABLE)
}
pub fn new(cache: &'a CodeCache<'a>, table: &'a VersionTable, layout: FamilyLayout) -> Self {
Disassembler {
cache,
table,
layout,
source_len: 0,
ro_map: None,
}
}
pub fn with_ro_map(mut self, m: Option<std::rc::Rc<crate::decompile::RoMap>>) -> Self {
self.ro_map = m;
self
}
pub fn render_all(&self, filter: Option<&str>) -> Result<String, String> {
let mut out = String::new();
for id in 0..self.cache.objects.len() {
if !self.cache.obj(id).ty.is(self.table, "SharedFunctionInfo") {
continue;
}
let name = self.sfi_name(id);
if let Some(f) = filter {
if !name.contains(f) {
continue;
}
}
self.render_sfi(id, &name, &mut out)?;
}
Ok(out)
}
pub fn sfi_name(&self, id: ObjId) -> String {
if std::env::var("JSCD_DBG_SFI").is_ok() {
let n = self.cache.array_len(id).max(16);
let mut out = Vec::new();
for i in 0..n.min(16) {
out.push(match self.cache.slot_at(id, i) {
Some(SlotValue::Ref(r)) => format!("{i}:{}", self.describe_ref(r)),
Some(other) => format!("{i}:{other:?}"),
None => format!("{i}:-"),
});
}
eprintln!(
"[sfi] id={id} name_slot={} slots={out:?}",
self.sfi_name_slot()
);
if let Some(SlotValue::Ref(Ref::Object(sid))) = self.cache.slot_at(id, self.sfi_name_slot())
{
let sid = *sid;
if self.cache.obj(sid).ty.is(self.table, "ScopeInfo") {
let ts = self.layout.tagged_size;
let raw: Vec<String> = (0..6)
.map(|k| {
self.cache
.raw_at(sid, k * ts, 8)
.map(|d| {
let v = u64::from_le_bytes(d.try_into().unwrap());
format!("{v:#x}")
})
.unwrap_or_else(|| "-".into())
})
.collect();
eprintln!(
"[sfi] scope id={sid} byte_size={} array_len={} ts={ts} words={raw:?}",
self.cache.obj(sid).byte_size,
self.cache.array_len(sid)
);
}
}
}
let Some(SlotValue::Ref(r)) = self.cache.slot_at(id, self.sfi_name_slot()) else {
return String::new();
};
match r {
Ref::Object(sid) if self.cache.obj(*sid).ty.is(self.table, "ScopeInfo") => {
let name = self.scope_function_name(*sid);
if name.is_empty() {
self.scope_inferred_name(*sid)
} else {
name
}
}
other => self.name_from_ref(*other),
}
}
fn name_from_ref(&self, r: Ref) -> String {
match r {
Ref::Object(sid) => {
let o = self.cache.obj(sid);
if o.ty.is_string(self.table) {
self.string_value(sid).unwrap_or_default()
} else {
String::new()
}
}
Ref::Root(i) => {
let n = self.table.root_name(i).unwrap_or_default().to_string();
n.strip_prefix("String:").map(str::to_string).unwrap_or_default()
}
Ref::RoRef(c, off) => self
.ro_map
.as_deref()
.and_then(|m| m.get(c, off))
.map(str::to_string)
.unwrap_or_default(),
_ => String::new(),
}
}
fn num_field(&self, id: ObjId, slot: usize, flags_smi: bool) -> Option<u32> {
let raw = self
.cache
.raw_at(id, slot * self.layout.tagged_size, 4)
.map(|d| u32::from_le_bytes(d.try_into().unwrap()));
let smi = self.smi_slot(id, slot).map(|v| v as u32);
match (flags_smi, raw, smi) {
(true, _, Some(s)) => Some(s),
(false, Some(0), Some(s)) if s != 0 => Some(s),
(false, Some(r), _) => Some(r),
(_, _, s) => s.or(raw),
}
}
fn scope_name_slots(&self, id: ObjId) -> (Option<usize>, Option<usize>) {
let legacy = self.table.v8.starts_with('8')
|| self.table.v8.starts_with('7')
|| self.table.v8.starts_with('6');
let primary = if legacy { 2 } else { 1 };
let mut best = (None, None);
let mut best_score = -1i32;
for base in [primary, if primary == 1 { 2 } else { 1 }] {
let (n, inf) = self.scope_name_slots_with(id, base);
let mut score = 0i32;
if n.map(|s| self.slot_looks_like_name(id, s)).unwrap_or(false) {
score += 2;
}
if inf.map(|s| self.slot_looks_like_name(id, s)).unwrap_or(false) {
score += 1;
}
if score > best_score {
best_score = score;
best = (n, inf);
}
if score >= 2 {
break; }
}
best
}
fn slot_looks_like_name(&self, id: ObjId, slot: usize) -> bool {
match self.cache.slot_at(id, slot) {
Some(SlotValue::Ref(Ref::Object(o))) => self.cache.obj(*o).ty.is_string(self.table),
Some(SlotValue::Ref(Ref::Root(r))) => self
.table
.root_name(*r)
.map(|n| n.starts_with("String:"))
.unwrap_or(false),
_ => false,
}
}
fn scope_name_slots_with(&self, id: ObjId, base: usize) -> (Option<usize>, Option<usize>) {
let ts = self.layout.tagged_size;
let cfg = self.table.scope_info.clone().unwrap_or(crate::tables::ScopeInfoLayout {
flags_smi: true,
position_info_early: false,
max_inlined_names: 75,
saved_class_bit: 10,
has_outer_bit: None,
function_variable_bits: [12, 13],
receiver_bits: [7, 8],
has_inferred_bit: 14,
context_local_count_slot: None,
variable_part: None,
parameter_names_first: false,
});
let legacy = base == 2;
let flags_smi = if self.table.scope_info.is_some() { cfg.flags_smi } else { legacy };
let flags = self.num_field(id, base, flags_smi).unwrap_or(0) as u64;
let has_saved = (flags >> cfg.saved_class_bit) & 1 == 1;
let function_var = ((flags >> cfg.function_variable_bits[0])
& ((1 << (cfg.function_variable_bits[1] - cfg.function_variable_bits[0] + 1)) - 1))
as u32;
let has_inferred = (flags >> cfg.has_inferred_bit) & 1 == 1;
let receiver_var = ((flags >> cfg.receiver_bits[0])
& ((1 << (cfg.receiver_bits[1] - cfg.receiver_bits[0] + 1)) - 1)) as u32;
let has_receiver = matches!(receiver_var, 1 | 2);
if std::env::var("JSCD_DBG_SFI").is_ok() {
eprintln!(
"[sfi] id={id} flags={flags:#x} function_var={function_var} has_inferred={has_inferred} has_saved={has_saved} has_receiver={has_receiver} len={}",
self.cache.array_len(id)
);
}
if !cfg.position_info_early {
let clc_slot = cfg.context_local_count_slot.unwrap_or(base + 2);
let Some(n) = self.num_field(id, clc_slot, flags_smi) else {
return (None, None);
};
let vpart = cfg
.variable_part
.unwrap_or(if legacy { 5 } else { 4 });
let mut cursor =
vpart + 2 * n as usize + has_saved as usize + has_receiver as usize;
if cfg.parameter_names_first {
let param = self.num_field(id, clc_slot.wrapping_sub(2), flags_smi).unwrap_or(0);
let stack = self.num_field(id, clc_slot.wrapping_sub(1), flags_smi).unwrap_or(0);
cursor += param as usize + 1 + stack as usize;
}
let function_name = if function_var != 0 {
let slot = cursor;
cursor += 2;
Some(slot)
} else {
None
};
let inferred = if has_inferred { Some(cursor) } else { None };
return (function_name, inferred);
}
let param_off = ts + 8;
let clc_off = ts + 16;
let _ = param_off;
let Some(n) = self
.cache
.raw_at(id, clc_off, ts)
.and_then(|d| self.decode_smi_bytes(d))
else {
return (None, None);
};
let n = n as usize;
let mut off = clc_off + ts; off += 2 * ts; let scope_type = flags & 0xF;
if scope_type == 3 {
off += ts; }
if n < cfg.max_inlined_names {
off += n * ts; } else {
off += ts; }
off += n * ts; if has_saved {
off += ts;
}
let function_name = if function_var != 0 {
let slot = off / ts;
off += 2 * ts;
Some(slot)
} else {
None
};
let inferred = if has_inferred { Some(off / ts) } else { None };
(function_name, inferred)
}
fn scope_function_name(&self, id: ObjId) -> String {
if std::env::var("JSCD_DBG_SFI").is_ok() {
let (a, b) = self.scope_name_slots(id);
eprintln!(
"[sfi] scope={id} name_slot={a:?} inferred_slot={b:?} v0={:?} v1={:?}",
a.and_then(|s| self.cache.slot_at(id, s)),
b.and_then(|s| self.cache.slot_at(id, s))
);
}
match self.scope_name_slots(id).0 {
Some(slot) => match self.cache.slot_at(id, slot) {
Some(SlotValue::Ref(r)) => self.name_from_ref(*r),
_ => String::new(),
},
None => String::new(),
}
}
fn scope_inferred_name(&self, id: ObjId) -> String {
match self.scope_name_slots(id).1 {
Some(slot) => match self.cache.slot_at(id, slot) {
Some(SlotValue::Ref(r)) => self.name_from_ref(*r),
_ => String::new(),
},
None => String::new(),
}
}
fn decode_smi_bytes(&self, d: &[u8]) -> Option<i64> {
if d.len() == 8 {
Some((read_u64(d) >> 32) as u32 as i32 as i64)
} else if d.len() == 4 {
let raw = u32::from_le_bytes(d.try_into().ok()?);
if raw & 1 == 0 {
None
} else {
Some(((raw as i32) >> 1) as i64)
}
} else {
None
}
}
fn smi_slot(&self, id: ObjId, slot: usize) -> Option<i64> {
let ts = self.layout.tagged_size;
let d = self.cache.raw_at(id, slot * ts, ts)?;
if ts == 8 {
Some((read_u64(d) >> 32) as u32 as i32 as i64)
} else {
let raw = u32::from_le_bytes(d.try_into().ok()?);
Some(((raw as i32) >> 1) as i64)
}
}
fn render_sfi(&self, id: ObjId, name: &str, out: &mut String) -> Result<(), String> {
let mut bca_id = None;
for slot in self.sfi_function_data_slots() {
if let Some(SlotValue::Ref(Ref::Object(b))) = self.cache.slot_at(id, slot) {
if self.cache.obj(*b).ty.is(self.table, "BytecodeArray") {
bca_id = Some(*b);
break;
}
}
}
let Some(bca_id) = bca_id else {
return Ok(()); };
let ba_layout = self
.table
.bytecode_array
.clone()
.unwrap_or_else(|| crate::tables::BytecodeArrayLayout::fallback(self.layout.tagged_size));
let header = ba_layout.header_size;
let obj = self.cache.obj(bca_id);
let bytecode_len = self
.cache
.raw_at(bca_id, self.layout.tagged_size, self.layout.tagged_size)
.map(|d| read_u64(d) >> 32)
.unwrap_or(0) as usize;
let frame_size = ba_layout
.off("frame_size")
.and_then(|o| self.cache.raw_at(bca_id, o, 4))
.map(|d| u32::from_le_bytes(d.try_into().unwrap()))
.unwrap_or(0);
let parameter_size = ba_layout
.off("parameter_size")
.and_then(|o| self.cache.raw_at(bca_id, o, 4))
.map(|d| u32::from_le_bytes(d.try_into().unwrap()) as i32)
.unwrap_or(0);
let parameter_count = if self
.table
.parameter_count
.map(|c| c.direct)
.unwrap_or(false)
{
parameter_size & 0xFFFF
} else {
parameter_size / 8
};
let osr = ba_layout
.off("osr_loop_nesting_level")
.or_else(|| ba_layout.off("osr_urgency_and_install_target"))
.and_then(|o| self.cache.raw_at(bca_id, o, 1))
.map(|d| d[0])
.unwrap_or(0);
let age = ba_layout
.off("bytecode_age")
.and_then(|o| self.cache.raw_at(bca_id, o, 1))
.map(|d| d[0])
.unwrap_or(0);
let code = self
.cache
.raw_at(bca_id, header, bytecode_len)
.unwrap_or_default();
let decoder = Decoder::new(self.table, self.layout, parameter_count);
let instrs = decoder.decode(code).map_err(|e| {
format!("decode bytecode of '{}' ({} bytes): {e}", name, code.len())
})?;
let register_count = frame_size / 8;
let _ = writeln!(
out,
"[generated bytecode for function: {name} (jscd <SharedFunctionInfo {name}>)]"
);
let _ = writeln!(out, "Bytecode length: {bytecode_len}");
let _ = writeln!(out, "Parameter count {parameter_count}");
let _ = writeln!(out, "Register count {register_count}");
let _ = writeln!(out, "Frame size {frame_size}");
let _ = writeln!(out, "OSR nesting level: {osr}");
let _ = writeln!(out, "Bytecode Age: {age}");
for ins in &instrs {
let hex = ins
.bytes
.iter()
.map(|b| format!("{b:02x} "))
.collect::<String>();
let ops = ins
.operands
.iter()
.map(|o| decoder.render_operand(o))
.collect::<Vec<_>>()
.join(", ");
let jump = self
.jump_target(bca_id, ins)
.map(|t| format!(" (jscd @ {t})"))
.unwrap_or_default();
let switch = self
.jump_table(bca_id, ins)
.map(|entries| {
let body = entries
.iter()
.map(|(c, t)| format!(" {c}: @{t}"))
.collect::<Vec<_>>()
.join(",");
format!(" {{{body} }}")
})
.unwrap_or_default();
let _ = writeln!(
out,
"{:>10} @ {:>4} : {:<18} {}{}{}{}",
"",
ins.offset,
hex.trim_end(),
ins.name,
if ops.is_empty() { String::new() } else { format!(" {ops}") },
jump,
switch
);
let _ = &switch;
}
self.render_constant_pool(bca_id, out);
self.render_byte_array(
"Handler Table",
self.cache.slot_at(bca_id, self.bca_handler_table_slot()).and_then(|v| v.as_ref()),
out,
);
self.render_byte_array(
"Source Position Table",
self.cache
.slot_at(bca_id, self.bca_source_position_slot())
.and_then(|v| v.as_ref()),
out,
);
let _ = writeln!(out);
let _ = obj;
Ok(())
}
fn render_byte_array(&self, label: &str, r: Option<Ref>, out: &mut String) {
match r {
Some(Ref::Object(id)) if self.cache.obj(id).ty.is(self.table, "ByteArray") => {
let obj_bytes = self.cache.obj(id).byte_size;
let word = self
.cache
.raw_at(id, self.layout.tagged_size, 8.min(self.layout.tagged_size));
let len = match word {
Some(d) if d.len() == 8 => {
let w = read_u64(d);
let hi = (w >> 32) as u32 as usize;
let lo = (w & 0xffff_ffff) as u32 as usize;
if hi <= obj_bytes && (lo > obj_bytes || hi >= lo) {
hi
} else {
lo
}
}
Some(d) if d.len() == 4 => {
u32::from_le_bytes(d.try_into().unwrap()) as usize
}
_ => 0,
};
let _ = writeln!(out, "{label} (size = {len})");
}
Some(Ref::Root(i)) => {
let n = self.table.root_name(i).unwrap_or_default().to_string();
if n.contains("empty_byte_array") || n.contains("EmptyByteArray") {
let _ = writeln!(out, "{label} (size = 0)");
} else {
let _ = writeln!(out, "{label} (size = ?)");
}
}
_ => {
let _ = writeln!(out, "{label} (size = 0)");
}
}
}
fn render_constant_pool(&self, bca_id: ObjId, out: &mut String) {
let pool = self
.cache
.slot_at(bca_id, self.bca_constant_pool_slot())
.and_then(|v| v.as_ref())
.and_then(|r| self.cache.ref_object(r));
let Some(pool_id) = pool else {
let _ = writeln!(out, "Constant pool (size = 0)");
return;
};
let entries = self.fixed_array_entries(pool_id);
let _ = writeln!(out, "Constant pool (size = {})", entries.len());
for (i, e) in entries.iter().enumerate() {
let _ = writeln!(out, "{:>12}: {}", i, e);
}
}
fn jump_target(&self, bca_id: ObjId, ins: &Instr) -> Option<usize> {
let prefix = if ins.scale > 1 { 1 } else { 0 };
let here = ins.offset + prefix;
let base = ins.name.trim_end_matches(".Wide").trim_end_matches(".ExtraWide");
let is_constant = base.ends_with("Constant");
let rel: i64 = if is_constant {
let idx = match ins.operands.first()? {
Operand::Idx(v) => *v as usize,
_ => return None,
};
let pool = self
.cache
.slot_at(bca_id, self.bca_constant_pool_slot())
.and_then(|v| v.as_ref())
.and_then(|r| self.cache.ref_object(r))?;
self.cache.array_elem(pool, idx).and_then(|e| e.as_smi())?
} else {
match ins.operands.first()? {
Operand::Idx(v) => *v as i64,
Operand::Imm(v) => *v,
_ => return None,
}
};
match base {
"Jump" | "JumpConstant" | "JumpIfTrue" | "JumpIfTrueConstant" | "JumpIfFalse"
| "JumpIfFalseConstant" | "JumpIfToBooleanTrue" | "JumpIfToBooleanTrueConstant"
| "JumpIfToBooleanFalse" | "JumpIfToBooleanFalseConstant" | "JumpIfNull"
| "JumpIfNullConstant" | "JumpIfNotNull" | "JumpIfNotNullConstant"
| "JumpIfUndefined" | "JumpIfUndefinedConstant" | "JumpIfUndefinedOrNull"
| "JumpIfUndefinedOrNullConstant" | "JumpIfNotUndefined"
| "JumpIfNotUndefinedConstant" | "JumpIfJSReceiver" | "JumpIfJSReceiverConstant"
| "JumpIfNotHole" | "JumpIfNotHoleConstant" | "JumpIfReferenceError"
| "JumpIfReferenceErrorConstant" | "JumpIfNotReferenceError"
| "JumpIfNotReferenceErrorConstant" => {
Some((here as i64 + rel) as usize)
}
"JumpLoop" | "JumpLoopConstant" => {
(ins.offset as i64 + prefix as i64).checked_sub(rel).map(|v| v as usize)
}
_ => None,
}
}
fn jump_table(&self, bca_id: ObjId, ins: &Instr) -> Option<Vec<(i32, usize)>> {
let (start_idx, size_idx, base_idx, base_fixed) = match ins.name.as_str() {
"SwitchOnSmiNoFeedback" => (0usize, 1usize, Some(2usize), 0i32),
"SwitchOnGeneratorState" => (1, 2, None, 0),
_ => return None,
};
let table_start = match ins.operands.get(start_idx)? {
Operand::Idx(v) => *v as usize,
_ => return None,
};
let table_size = match ins.operands.get(size_idx)? {
Operand::Idx(v) => *v as usize,
Operand::Imm(v) => *v as usize,
_ => return None,
};
let case_base = match base_idx {
Some(i) => match ins.operands.get(i)? {
Operand::Imm(v) => *v as i32,
Operand::Idx(v) => *v as i32,
_ => return None,
},
None => base_fixed,
};
let pool = self
.cache
.slot_at(bca_id, self.bca_constant_pool_slot())
.and_then(|v| v.as_ref())
.and_then(|r| self.cache.ref_object(r))?;
let mut out = Vec::new();
for i in 0..table_size {
if let Some(Elem::Smi(v)) = self.cache.array_elem(pool, table_start + i) {
let prefix = if ins.scale > 1 { 1 } else { 0 };
out.push((case_base + i as i32, ins.offset + prefix + v as usize));
}
}
Some(out)
}
pub fn array_elem_public(&self, id: ObjId, index: usize) -> Option<Elem<'a>> {
self.cache.array_elem(id, index)
}
pub fn parameter_count_direct(&self) -> bool {
self.table.parameter_count.map(|c| c.direct).unwrap_or(false)
}
pub fn fixed_array_len(&self, id: ObjId) -> usize {
self.cache.array_len(id)
}
pub fn fixed_array_entries(&self, id: ObjId) -> Vec<String> {
let len = self.fixed_array_len(id);
(0..len)
.map(|i| match self.cache.array_elem(id, i) {
Some(Elem::Smi(v)) => v.to_string(),
Some(Elem::Ref(r)) => self.describe_ref(&r),
Some(Elem::Bytes(b)) => format!("<raw {}>", hex(b)),
None => "<hole>".into(),
})
.collect()
}
pub fn fixed_array_smis(&self, id: ObjId) -> Vec<Option<i64>> {
let len = self.fixed_array_len(id);
(0..len)
.map(|i| self.cache.array_elem(id, i).and_then(|e| e.as_smi()))
.collect()
}
pub fn describe_ref(&self, r: &Ref) -> String {
match r {
Ref::Object(id) => {
let o = self.cache.obj(*id);
match o.ty.name(self.table).as_ref() {
t if t.contains("String") => self
.string_value(*id)
.map(|s| format!("<String[{}]: #{} >", s.chars().count(), s))
.unwrap_or_else(|| "<String>".into()),
"SharedFunctionInfo" => {
format!("<SharedFunctionInfo {}>", self.sfi_name(*id))
}
"HeapNumber" => {
let v = self
.cache
.raw_at(*id, self.layout.tagged_size, 8)
.map(|d| f64::from_le_bytes(d.try_into().unwrap()))
.unwrap_or(f64::NAN);
format!("<HeapNumber {v}>")
}
"ScopeInfo" => "<ScopeInfo>".into(),
"FixedArray" | "FixedCOWArray" => format!("<FixedArray[{}]>", self.fixed_array_entries(*id).len()),
"ObjectBoilerplateDescription" => "<ObjectBoilerplateDescription>".into(),
"ArrayBoilerplateDescription" => "<ArrayBoilerplateDescription>".into(),
"ByteArray" | "EmptyByteArray" => "<ByteArray>".into(),
t if t.contains("BigInt") => self
.bigint_value(*id)
.map(|v| format!("<BigInt {v}>"))
.unwrap_or_else(|| "<BigInt>".into()),
other => format!("<{other}>"),
}
}
Ref::Root(i) => {
let name = self
.table
.roots
.get(*i)
.cloned()
.unwrap_or_else(|| format!("root#{i}"));
match name.as_str() {
n if n.starts_with("String:") => format!("<String: {}>", &n[7..]),
n if n.starts_with("Symbol:") => format!("<Symbol: {}>", &n[7..]),
"undefined_value" => "<undefined>".into(),
"null_value" => "<null>".into(),
"true_value" => "<true>".into(),
"false_value" => "<false>".into(),
"the_hole_value" => "<the_hole>".into(),
"uninitialized_value" => "<uninitialized>".into(),
"empty_fixed_array" => "<EmptyFixedArray>".into(),
"empty_string" => "<String[0]: #>".into(),
other => format!("<{other}>"),
}
}
Ref::Attached(0) => {
let len = self.attached_source_len();
format!("<String[{len}]: #(source placeholder)>")
}
Ref::Attached(i) => format!("<Attached({i})>"),
Ref::RoRef(c, o) => format!("<ro:{c}/{o}>"),
}
}
fn attached_source_len(&self) -> u32 {
self.source_len
}
pub fn string_value(&self, id: ObjId) -> Option<String> {
decode_string(self.cache, self.table, id)
}
pub fn bigint_value(&self, id: ObjId) -> Option<String> {
let ts = self.layout.tagged_size;
let bf = self.cache.raw_at(id, ts, ts.min(8))?;
let bf = u32::from_le_bytes(bf[..4].try_into().ok()?);
let negative = bf & 1 == 1;
let len = (bf >> 1) as usize;
if len == 0 {
return Some("0".into());
}
let bytes = self.cache.raw_at(id, 2 * ts, len * ts)?;
let mut words: Vec<u64> = Vec::with_capacity(len);
for chunk in bytes.chunks(ts) {
words.push(match chunk.len() {
4 => u32::from_le_bytes(chunk.try_into().ok()?) as u64,
8 => u64::from_le_bytes(chunk.try_into().ok()?),
_ => return None,
});
}
let mut out: Vec<u32> = Vec::new();
while words.iter().any(|&w| w != 0) {
let mut rem: u128 = 0;
for w in words.iter_mut().rev() {
let cur = (rem << (8 * ts)) | (*w as u128);
*w = (cur / 1_000_000_000u128) as u64;
rem = cur % 1_000_000_000u128;
}
out.push(rem as u32);
}
let mut s = String::new();
if negative {
s.push('-');
}
s.push_str(&out.pop().unwrap_or(0).to_string());
while let Some(c) = out.pop() {
s.push_str(&format!("{c:09}"));
}
Some(s)
}
pub fn with_source_len(mut self, len: u32) -> Self {
self.source_len = len;
self
}
}
pub fn decode_string(cache: &CodeCache<'_>, table: &VersionTable, id: ObjId) -> Option<String> {
let obj = cache.obj(id);
let len_off = table.string_length_offset.unwrap_or(12);
let length = if table.string_length_smi {
let ts = table.tagged_size as usize;
let d = cache.raw_at(id, len_off, ts)?;
if ts == 8 {
(read_u64(d) >> 32) as u32 as usize
} else {
let raw = u32::from_le_bytes(d.try_into().ok()?);
((raw as i32) >> 1) as usize
}
} else {
cache
.raw_at(id, len_off, 4)
.map(|d| u32::from_le_bytes(d.try_into().unwrap()) as usize)?
};
let is_one_byte = match obj.ty {
crate::serializer::Ty::Str(crate::serializer::StrKind::OneByte) => true,
crate::serializer::Ty::Str(crate::serializer::StrKind::TwoByte) => false,
_ => obj.ty.name(table).contains("OneByte"),
};
let chars_off = table.string_chars_offset.unwrap_or(16);
let d = cache.raw_at(id, chars_off, length)?;
if is_one_byte {
Some(String::from_utf8_lossy(d).into_owned())
} else {
let u16s: Vec<u16> = d
.chunks(2)
.map(|c| u16::from_le_bytes([c[0], *c.get(1).unwrap_or(&0)]))
.collect();
Some(String::from_utf16_lossy(&u16s))
}
}
pub fn name_of_ref(cache: &CodeCache<'_>, table: &VersionTable, r: Ref) -> Option<String> {
match r {
Ref::Object(id) => {
let o = cache.obj(id);
if o.ty.is_string(table) {
decode_string(cache, table, id)
} else {
None
}
}
Ref::Root(i) => table.root_name(i).map(|n| {
match n {
"empty_string" | "EmptyString" | "undefined_value" | "UndefinedValue"
| "uninitialized_value" | "UninitializedValue" | "the_hole_value"
| "TheHoleValue" => String::new(),
other => other
.strip_prefix("String:")
.or_else(|| other.strip_prefix("Symbol:"))
.unwrap_or(other)
.to_string(),
}
}),
_ => None,
}
}
fn read_u64(d: &[u8]) -> u64 {
let mut v = 0u64;
for (i, b) in d.iter().take(8).enumerate() {
v |= (*b as u64) << (8 * i);
}
v
}
fn hex(d: &[u8]) -> String {
d.iter().map(|b| format!("{b:02x}")).collect::<Vec<_>>().join("")
}
#[allow(dead_code)]
fn max_register(instrs: &[Instr]) -> Option<i32> {
let mut max: Option<i32> = None;
for ins in instrs {
for op in &ins.operands {
let idx = match op {
Operand::Reg(i) => Some(*i),
Operand::RegList { first, count } => Some(if *count > 0 {
first + (*count as i32) - 1
} else {
*first
}),
_ => None,
};
if let Some(i) = idx {
if i >= 0 {
max = Some(max.map_or(i, |m: i32| m.max(i)));
}
}
}
}
max
}