Big Rewrite
This commit is contained in:
+185
-355
@@ -1,52 +1,37 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode},
|
||||
regify::{Block, RegInstr},
|
||||
libqjs::parse::{
|
||||
Atom, AtomRef, ClosureVar, ImmOperand, JsString, JsValue, atom_names, opcodes::OpCode,
|
||||
},
|
||||
regify::{BasicBlock, RegFn, RegInstr},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Type {
|
||||
Unknown,
|
||||
Undefined,
|
||||
Bool,
|
||||
Int,
|
||||
Float,
|
||||
Empty,
|
||||
/// A named function, by flat fn id.
|
||||
Fn(usize),
|
||||
ReturnOf((usize, Vec<Type>)),
|
||||
Any,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CodeKind {
|
||||
Code(Vec<RegInstr>),
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum FnKind {
|
||||
Id(usize),
|
||||
DefineField(AtomRef),
|
||||
PutField(AtomRef),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Signature {
|
||||
pub args: Vec<Type>,
|
||||
pub ret: Type,
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Unknown,
|
||||
Bool,
|
||||
Int,
|
||||
Float,
|
||||
Fn(FnKind),
|
||||
Any,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InferFn {
|
||||
pub name: String,
|
||||
pub cpool: Vec<JsValue>,
|
||||
pub code: CodeKind,
|
||||
pub sigs: Vec<Signature>,
|
||||
/// One entry per value id, so a value's type is `types[v]`. regify keeps
|
||||
/// the first `var_count` ids for the locals, so a local's type lives there
|
||||
/// too and `put_loc` / `get_loc` just copy between the two.
|
||||
pub types: Vec<Type>,
|
||||
pub blocks: Vec<Block>,
|
||||
}
|
||||
|
||||
/// The lattice, joining what a value already holds with what it is being
|
||||
/// given. Unknown is the bottom and gives way to anything; Any is the top and
|
||||
/// swallows everything; two types that disagree can only be described by Any.
|
||||
pub fn join(a: &Type, b: &Type) -> Type {
|
||||
/// Type lattice join rules:
|
||||
/// Unknown * T = T
|
||||
/// Any * T = Any
|
||||
/// T * T = T
|
||||
/// T * U = Any
|
||||
fn join_ty(a: &Type, b: &Type) -> Type {
|
||||
match (a, b) {
|
||||
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
|
||||
(Type::Any, _) | (_, Type::Any) => Type::Any,
|
||||
@@ -55,347 +40,192 @@ pub fn join(a: &Type, b: &Type) -> Type {
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk a function's code once, giving every value a type, and hand back what
|
||||
/// the function returns. A call is followed down into its callee.
|
||||
pub fn infer(fn_id: usize, fns: &mut Vec<InferFn>) -> Type {
|
||||
// The caller left this function's argument types here before recursing.
|
||||
let args = match fns[fn_id].sigs.first() {
|
||||
Some(sig) => sig.args.clone(),
|
||||
None => vec![],
|
||||
};
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub struct InferRequest {
|
||||
fn_id: usize,
|
||||
arg_types: Vec<Type>,
|
||||
}
|
||||
|
||||
// The two field writers have no body: their result IS the shape term that
|
||||
// names them, built from the types they were handed.
|
||||
let code = match &fns[fn_id].code {
|
||||
CodeKind::DefineField(_) | CodeKind::PutField(_) => return Type::ReturnOf((fn_id, args)),
|
||||
#[derive(Debug)]
|
||||
pub struct InferResult {
|
||||
val_types: Vec<Type>,
|
||||
ret_type: Type,
|
||||
}
|
||||
|
||||
// An empty body means this function's code is out on the stack, so we
|
||||
// have come back round a cycle. Unknown is the bottom, and a later
|
||||
// round raises it.
|
||||
CodeKind::Code(c) if c.is_empty() => return Type::Unknown,
|
||||
pub type TypeCache = HashMap<InferRequest, InferResult>;
|
||||
|
||||
CodeKind::Code(_) => {
|
||||
let taken = std::mem::replace(&mut fns[fn_id].code, CodeKind::Code(vec![]));
|
||||
let CodeKind::Code(code) = taken else {
|
||||
unreachable!()
|
||||
};
|
||||
code
|
||||
pub fn infer_types(reg_fns: &[RegFn], atoms: &[Atom]) -> TypeCache {
|
||||
// Find main fn
|
||||
let main_fn_id = reg_fns
|
||||
.iter()
|
||||
.position(|f| atom_to_str(&f.name, atoms) == "main")
|
||||
.unwrap();
|
||||
|
||||
let mut cache = TypeCache::with_capacity(reg_fns.len());
|
||||
let mut worklist = vec![];
|
||||
|
||||
worklist.push(InferRequest {
|
||||
fn_id: main_fn_id,
|
||||
arg_types: vec![],
|
||||
});
|
||||
|
||||
while let Some(req) = worklist.pop() {
|
||||
match walk_fn(&req, reg_fns, &mut cache) {
|
||||
Ok(res) => {
|
||||
cache.insert(req, res);
|
||||
}
|
||||
Err(new_req) => {
|
||||
worklist.push(new_req);
|
||||
worklist.push(req);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if fns[fn_id].sigs.is_empty() {
|
||||
fns[fn_id].sigs.push(Signature {
|
||||
args: vec![],
|
||||
ret: Type::Unknown,
|
||||
});
|
||||
}
|
||||
|
||||
// The types move out too, so a callee can be inferred through `fns` while
|
||||
// this function is still being walked. Both go back at the end.
|
||||
let mut types = std::mem::take(&mut fns[fn_id].types);
|
||||
let blocks = std::mem::take(&mut fns[fn_id].blocks);
|
||||
cache
|
||||
}
|
||||
|
||||
// The blocks, read back off the instructions. Passes have moved them, but
|
||||
// none of them adds or removes a jump, so this is the partition regify
|
||||
// found and `succ` still lines up with it. Anything inserted above the
|
||||
// first boundary belongs to the first block.
|
||||
let mut starts = (code.iter().enumerate())
|
||||
.filter(|(_, i)| i.block_start)
|
||||
.map(|(at, _)| at)
|
||||
.collect::<Vec<usize>>();
|
||||
fn walk_fn(
|
||||
req: &InferRequest,
|
||||
reg_fns: &[RegFn],
|
||||
cache: &mut TypeCache,
|
||||
) -> Result<InferResult, InferRequest> {
|
||||
let f = ®_fns[req.fn_id];
|
||||
let mut val_types = vec![Type::Unknown; f.val_count];
|
||||
let mut ret_type = Type::Unknown;
|
||||
|
||||
if let Some(first) = starts.first_mut() {
|
||||
*first = 0;
|
||||
let mut worklist = vec![0];
|
||||
|
||||
while let Some(block_id) = worklist.pop() {
|
||||
let block = &f.blocks[block_id];
|
||||
walk_block(&block, &req.arg_types, f, &mut val_types, &mut ret_type)?;
|
||||
|
||||
worklist.extend(block.succ.iter());
|
||||
}
|
||||
|
||||
// One flag in, one block out. A pass may move a boundary or insert around
|
||||
// it, but it must carry the flag when it REPLACES the instruction holding
|
||||
// one -- that is what `RegInstr::starting` is for. Miss it and the blocks
|
||||
// silently stop lining up with the edges regify recorded.
|
||||
debug_assert_eq!(
|
||||
starts.len(),
|
||||
blocks.len(),
|
||||
"{}: block starts lost or gained since regify",
|
||||
fns[fn_id].name
|
||||
);
|
||||
let mut ret = Type::Unknown;
|
||||
Ok(InferResult {
|
||||
val_types,
|
||||
ret_type,
|
||||
})
|
||||
}
|
||||
|
||||
// What each block is entered with, joined from the blocks that reach it.
|
||||
let mut entry = vec![vec![Type::Unknown; types.len()]; starts.len()];
|
||||
fn walk_block(
|
||||
block: &BasicBlock,
|
||||
arg_types: &[Type],
|
||||
f: &RegFn,
|
||||
val_types: &mut [Type],
|
||||
ret_type: &mut Type,
|
||||
) -> Result<(), InferRequest> {
|
||||
use OpCode::*;
|
||||
|
||||
for (b, &start) in starts.iter().enumerate() {
|
||||
let end = starts.get(b + 1).copied().unwrap_or(code.len());
|
||||
for instr in &f.code[block.start..block.end] {
|
||||
let ty = match instr.op {
|
||||
// ──────────────────────────────────── Literals ────────────────────────────────────
|
||||
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8
|
||||
| PushI16 | PushI32 => Type::Int,
|
||||
|
||||
// Fold in what the blocks reaching this one left behind. Two paths
|
||||
// meeting is the ONLY place the algebra applies -- everything below
|
||||
// runs in a straight line.
|
||||
for (t, e) in types.iter_mut().zip(&entry[b]) {
|
||||
*t = join(t, e);
|
||||
}
|
||||
PushTrue | PushFalse => Type::Bool,
|
||||
|
||||
// A slot at a merge keeps the ids the first path in left, so every
|
||||
// other path's value has to be folded into them. Without this the
|
||||
// merge reads one arm and believes it.
|
||||
for &(slot, incoming) in &blocks[b].phi {
|
||||
types[slot] = join(&types[slot], &types[incoming]);
|
||||
}
|
||||
PushConst | PushConst8 | Fclosure | Fclosure8 => match instr.imm {
|
||||
ImmOperand::Const(k) => match f.cpool.get(k as usize) {
|
||||
Some(JsValue::Int(_)) => Type::Int,
|
||||
Some(JsValue::Float(_)) => Type::Float,
|
||||
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
|
||||
Some(&JsValue::FnById(id)) => Type::Fn(FnKind::Id(id)),
|
||||
_ => panic!(),
|
||||
},
|
||||
_ => panic!(),
|
||||
},
|
||||
|
||||
for instr in &code[start..end] {
|
||||
// regify does not rename locals, so a load just names the local's value
|
||||
// again. It must not write to it, or it would wipe what is stored there.
|
||||
let load = matches!(
|
||||
instr.op,
|
||||
OpCode::GetLoc0
|
||||
| OpCode::GetLoc1
|
||||
| OpCode::GetLoc2
|
||||
| OpCode::GetLoc3
|
||||
| OpCode::GetLoc0Loc1
|
||||
| OpCode::GetLoc
|
||||
| OpCode::GetLoc8
|
||||
| OpCode::GetLocCheck
|
||||
);
|
||||
|
||||
let ty = match instr.op {
|
||||
// ── literals ────────────────────────────────────────────────
|
||||
OpCode::PushMinus1
|
||||
| OpCode::Push0
|
||||
| OpCode::Push1
|
||||
| OpCode::Push2
|
||||
| OpCode::Push3
|
||||
| OpCode::Push4
|
||||
| OpCode::Push5
|
||||
| OpCode::Push6
|
||||
| OpCode::Push7
|
||||
| OpCode::PushI8
|
||||
| OpCode::PushI16
|
||||
| OpCode::PushI32 => Type::Int,
|
||||
|
||||
OpCode::PushTrue | OpCode::PushFalse => Type::Bool,
|
||||
OpCode::Undefined => Type::Undefined,
|
||||
OpCode::Object => Type::Empty,
|
||||
|
||||
OpCode::PushConst | OpCode::PushConst8 | OpCode::Fclosure | OpCode::Fclosure8 => {
|
||||
match instr.imm {
|
||||
ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) {
|
||||
Some(JsValue::Int(_)) => Type::Int,
|
||||
Some(JsValue::Float(_)) => Type::Float,
|
||||
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
|
||||
Some(JsValue::Undefined) => Type::Undefined,
|
||||
Some(&JsValue::FnById(id)) => Type::Fn(id),
|
||||
_ => Type::Any,
|
||||
},
|
||||
_ => Type::Any,
|
||||
}
|
||||
}
|
||||
|
||||
// ── field reads ─────────────────────────────────────────────
|
||||
// Walk the shape term from the outside in: the newest write of the
|
||||
// field wins, which is what makes a later write shadow an earlier.
|
||||
OpCode::GetField => {
|
||||
let field = match instr.imm {
|
||||
ImmOperand::Atom(a) => a,
|
||||
_ => AtomRef::TaggedInt(u32::MAX),
|
||||
};
|
||||
|
||||
let mut ty = types[instr.args[0]].clone();
|
||||
|
||||
loop {
|
||||
let (id, args) = match ty {
|
||||
Type::ReturnOf((id, args)) if args.len() == 2 => (id, args),
|
||||
Type::Empty => break Type::Undefined, // No fields at all
|
||||
_ => break Type::Any,
|
||||
};
|
||||
|
||||
match &fns[id].code {
|
||||
CodeKind::DefineField(a) | CodeKind::PutField(a) if *a == field => {
|
||||
break args[1].clone();
|
||||
}
|
||||
CodeKind::DefineField(_) | CodeKind::PutField(_) => {
|
||||
ty = args[0].clone()
|
||||
}
|
||||
CodeKind::Code(_) => break Type::Any, // Not a shape we built
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── arithmetic ──────────────────────────────────────────────
|
||||
// A float anywhere makes the result a float; anything else can be
|
||||
// a string concatenation or an object coercion, so it is Any.
|
||||
OpCode::Add
|
||||
| OpCode::Sub
|
||||
| OpCode::Mul
|
||||
| OpCode::Div
|
||||
| OpCode::Mod
|
||||
| OpCode::Pow => match (&types[instr.args[0]], &types[instr.args[1]]) {
|
||||
// ─────────────────────────────────── Arithmetic ───────────────────────────────────
|
||||
Add | Sub | Mul | Div | Mod | Pow => {
|
||||
match (&val_types[instr.args[0]], &val_types[instr.args[1]]) {
|
||||
(Type::Int, Type::Int) => Type::Int,
|
||||
(Type::Float, Type::Float)
|
||||
| (Type::Int, Type::Float)
|
||||
| (Type::Float, Type::Int) => Type::Float,
|
||||
_ => Type::Any,
|
||||
},
|
||||
|
||||
OpCode::Neg | OpCode::Plus | OpCode::Inc | OpCode::Dec => {
|
||||
match types[instr.args[0]] {
|
||||
Type::Int => Type::Int,
|
||||
Type::Float => Type::Float,
|
||||
_ => Type::Any,
|
||||
}
|
||||
}
|
||||
|
||||
OpCode::Lt
|
||||
| OpCode::Lte
|
||||
| OpCode::Gt
|
||||
| OpCode::Gte
|
||||
| OpCode::Eq
|
||||
| OpCode::Neq
|
||||
| OpCode::StrictEq
|
||||
| OpCode::StrictNeq
|
||||
| OpCode::Not => Type::Bool,
|
||||
|
||||
// ── calls ───────────────────────────────────────────────────
|
||||
// demutate names a lifted callee in the immediate. Anything else
|
||||
// is a callee off the stack, which this does not resolve yet.
|
||||
OpCode::Call
|
||||
| OpCode::Call0
|
||||
| OpCode::Call1
|
||||
| OpCode::Call2
|
||||
| OpCode::Call3
|
||||
| OpCode::TailCall => {
|
||||
// demutate names its callee in the immediate; every other call
|
||||
// takes it off the stack, where it is deepest and so comes last.
|
||||
// Reversing the rest puts them back in written order.
|
||||
let (callee, args) = match instr.imm {
|
||||
ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) {
|
||||
Some(&JsValue::FnById(id)) => (Some(id), &instr.args[..]),
|
||||
_ => (None, &instr.args[..]),
|
||||
},
|
||||
_ => match instr.args.last().map(|a| &types[*a]) {
|
||||
Some(&Type::Fn(id)) => (Some(id), &instr.args[..instr.args.len() - 1]),
|
||||
_ => (None, &instr.args[..]),
|
||||
},
|
||||
};
|
||||
|
||||
let result = match callee {
|
||||
Some(callee) => {
|
||||
let args = args.iter().rev().map(|a| types[*a].clone()).collect();
|
||||
fns[callee].sigs = vec![Signature {
|
||||
args,
|
||||
ret: Type::Unknown,
|
||||
}];
|
||||
infer(callee, fns)
|
||||
}
|
||||
None => Type::Any,
|
||||
};
|
||||
|
||||
// A tail call hands its result straight back, so it ends
|
||||
// the function as much as a `return` does.
|
||||
if instr.op == OpCode::TailCall {
|
||||
ret = join(&ret, &result);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// A method call keeps its receiver under the callee, so the
|
||||
// callee is not the last argument and the lookup above does not
|
||||
// find it. Resolving that is not done yet.
|
||||
OpCode::CallMethod | OpCode::TailCallMethod => {
|
||||
if instr.op == OpCode::TailCallMethod {
|
||||
ret = join(&ret, &Type::Any);
|
||||
}
|
||||
|
||||
Type::Any
|
||||
}
|
||||
|
||||
// ── globals ─────────────────────────────────────────────────
|
||||
// A free name is a property of the global object, which nothing
|
||||
// here models -- every built-in reaches this way too. Writing
|
||||
// one (`put_var`) records nothing, because a global is not a
|
||||
// value and has no id to record it against.
|
||||
OpCode::GetVar | OpCode::GetVarUndef => Type::Any,
|
||||
|
||||
// ── arguments ───────────────────────────────────────────────
|
||||
OpCode::GetArg0 => args.first().cloned().unwrap_or(Type::Undefined),
|
||||
OpCode::GetArg1 => args.get(1).cloned().unwrap_or(Type::Undefined),
|
||||
OpCode::GetArg2 => args.get(2).cloned().unwrap_or(Type::Undefined),
|
||||
OpCode::GetArg3 => args.get(3).cloned().unwrap_or(Type::Undefined),
|
||||
OpCode::GetArg => match instr.imm {
|
||||
ImmOperand::Arg(k) => args.get(k as usize).cloned().unwrap_or(Type::Undefined),
|
||||
_ => Type::Any,
|
||||
},
|
||||
|
||||
// ── value movers ────────────────────────────────────────────
|
||||
// These hand the value straight back, so the type goes through.
|
||||
// dup pushes it twice, which the assignment below spreads over
|
||||
// both destinations.
|
||||
// A store into a local is one of these too: the local IS the
|
||||
// destination value, so the type simply moves across.
|
||||
OpCode::SetName
|
||||
| OpCode::SetNameComputed
|
||||
| OpCode::SetHomeObject
|
||||
| OpCode::Dup
|
||||
| OpCode::PutLoc0
|
||||
| OpCode::PutLoc1
|
||||
| OpCode::PutLoc2
|
||||
| OpCode::PutLoc3
|
||||
| OpCode::PutLoc
|
||||
| OpCode::PutLoc8
|
||||
| OpCode::PutLocCheck
|
||||
| OpCode::PutLocCheckInit
|
||||
| OpCode::SetLoc0
|
||||
| OpCode::SetLoc1
|
||||
| OpCode::SetLoc2
|
||||
| OpCode::SetLoc3
|
||||
| OpCode::SetLoc
|
||||
| OpCode::SetLoc8 => types[instr.args[0]].clone(),
|
||||
|
||||
OpCode::SetLocUninitialized => Type::Unknown,
|
||||
|
||||
// ── returns ─────────────────────────────────────────────────
|
||||
// Every way out has to feed `ret`. Missing one leaves it at
|
||||
// Unknown, the BOTTOM, which a caller folds away and believes
|
||||
// -- where Any would have been distrusted.
|
||||
OpCode::Return | OpCode::ReturnAsync => {
|
||||
ret = join(&ret, &types[instr.args[0]]);
|
||||
Type::Unknown
|
||||
}
|
||||
OpCode::ReturnUndef => {
|
||||
ret = join(&ret, &Type::Undefined);
|
||||
Type::Unknown
|
||||
}
|
||||
|
||||
// ── everything else ─────────────────────────────────────────
|
||||
_ => Type::Any,
|
||||
};
|
||||
|
||||
// A write joins with what the value already holds, so
|
||||
// reassigning a local a different type widens it to Any. Almost
|
||||
// every reassignment keeps the type, and join(T, T) = T, so the
|
||||
// common path costs nothing.
|
||||
//
|
||||
// Nothing branches inside a block, so plain `types[dst] = ty`
|
||||
// would be sound here and would keep `let x = 1; x = 2.5` as
|
||||
// Float. That is a one-line swap; the blocks do their own job
|
||||
// either way, which is to join where paths MEET.
|
||||
if !load {
|
||||
for &dst in &instr.dsts {
|
||||
types[dst] = join(&types[dst], &ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hand the state on to every block this one reaches.
|
||||
for &s in &blocks[b].succ {
|
||||
for (e, t) in entry[s].iter_mut().zip(&types) {
|
||||
*e = join(e, t);
|
||||
Neg | Plus | Inc | Dec => match val_types[instr.args[0]] {
|
||||
Type::Int => Type::Int,
|
||||
Type::Float => Type::Float,
|
||||
_ => Type::Any,
|
||||
},
|
||||
|
||||
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
|
||||
|
||||
// ────────────────────────────────── Value Movers ──────────────────────────────────
|
||||
Dup | PutLoc0 | PutLoc1 | PutLoc2 | PutLoc3 | PutLoc | PutLoc8 | PutLocCheck
|
||||
| PutLocCheckInit | SetLoc0 | SetLoc1 | SetLoc2 | SetLoc3 | SetLoc | SetLoc8 => {
|
||||
val_types[instr.args[0]].clone()
|
||||
}
|
||||
|
||||
SetLocUninitialized => Type::Unknown,
|
||||
|
||||
// ────────────────────────────────────── Returns ───────────────────────────────────
|
||||
Return | ReturnAsync | ReturnUndef => val_types[instr.args[0]].clone(),
|
||||
|
||||
_ => Type::Unknown,
|
||||
};
|
||||
|
||||
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
|
||||
let is_loc_read = matches!(
|
||||
instr.op,
|
||||
OpCode::GetLoc0
|
||||
| OpCode::GetLoc1
|
||||
| OpCode::GetLoc2
|
||||
| OpCode::GetLoc3
|
||||
| OpCode::GetLoc0Loc1
|
||||
| OpCode::GetLoc
|
||||
| OpCode::GetLoc8
|
||||
| OpCode::GetLocCheck
|
||||
);
|
||||
|
||||
// TODO: is_loc_read logic is ungraceful
|
||||
if is_ret {
|
||||
*ret_type = join_ty(ret_type, &ty);
|
||||
} else if !is_loc_read {
|
||||
for &dst in &instr.dsts {
|
||||
val_types[dst] = join_ty(&val_types[dst], &ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fns[fn_id].types = types;
|
||||
fns[fn_id].blocks = blocks;
|
||||
fns[fn_id].code = CodeKind::Code(code);
|
||||
fns[fn_id].sigs[0].ret = ret.clone();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
ret
|
||||
pub fn atom_to_str(a: &AtomRef, atoms: &[Atom]) -> String {
|
||||
match a {
|
||||
AtomRef::TaggedInt(n) => n.to_string(),
|
||||
AtomRef::Predefined(i) => atom_names::PREDEFINED_ATOMS
|
||||
.get(*i as usize)
|
||||
.copied()
|
||||
.unwrap_or("<predef?>")
|
||||
.to_string(),
|
||||
AtomRef::FileLocal(i) => atoms
|
||||
.get(*i)
|
||||
.map(atom_text)
|
||||
.unwrap_or_else(|| format!("<file_atom {i}>")),
|
||||
}
|
||||
}
|
||||
|
||||
fn atom_text(a: &Atom) -> String {
|
||||
match a {
|
||||
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s),
|
||||
Atom::Predefined(n) => format!("<predef {n}>"),
|
||||
}
|
||||
}
|
||||
|
||||
fn js_string(s: &JsString) -> String {
|
||||
match s {
|
||||
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
|
||||
JsString::UTF16(b) => {
|
||||
let u: Vec<u16> = b
|
||||
.chunks_exact(2)
|
||||
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||
.collect();
|
||||
String::from_utf16_lossy(&u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user