Misc cleanups

This commit is contained in:
UditDey
2026-09-28 07:47:01 +00:00
parent e93ce94ac7
commit a4d295fbd9
18 changed files with 563 additions and 1388 deletions
+71 -198
View File
@@ -1,18 +1,15 @@
#![allow(non_snake_case)]
pub mod types;
use std::collections::HashMap;
pub mod types;
use types::{Type, get_field, join_ty, set_field};
use crate::{
builtins,
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
model::EXTRA_ARGS,
native,
regify::RegFn,
};
use types::{Type, get_field, join_ty, set_field};
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct InferRequest {
pub fn_id: usize,
@@ -25,73 +22,47 @@ pub struct InferResult {
pub ret_type: Type,
}
/// Names one object shape. Emit turns each into a struct.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShapeId(pub usize);
/// What inference accumulated: a result per function-and-argument pair, and a
/// number for every distinct object shape. Shapes are kept exactly as built, so
/// two objects with the same fields in a different order are two shapes.
#[derive(Debug, Default)]
pub struct TypeCache {
results: HashMap<InferRequest, InferResult>,
shapes: HashMap<Type, ShapeId>,
next_shape: usize,
shapes: HashMap<Type, usize>,
next_shape_id: usize,
}
impl TypeCache {
pub fn get(&self, req: &InferRequest) -> Option<&InferResult> {
pub fn get_result(&self, req: &InferRequest) -> Option<&InferResult> {
self.results.get(req)
}
pub fn insert(&mut self, req: InferRequest, res: InferResult) {
pub fn insert_result(&mut self, req: InferRequest, res: InferResult) {
self.results.insert(req, res);
}
/// Number this shape, reusing the number if it has been seen before.
pub fn shape_of(&mut self, ty: &Type) -> ShapeId {
if let Some(id) = self.shapes.get(ty) {
return *id;
pub fn insert_shape(&mut self, ty: &Type) {
if !self.shapes.contains_key(ty) {
self.shapes.insert(ty.clone(), self.next_shape_id);
self.next_shape_id += 1;
}
let id = ShapeId(self.next_shape);
self.next_shape += 1;
self.shapes.insert(ty.clone(), id);
id
}
/// The number already given to this shape, if it has one.
pub fn shape_id(&self, ty: &Type) -> Option<ShapeId> {
self.shapes.get(ty).copied()
}
pub fn results(&self) -> impl Iterator<Item = (&InferRequest, &InferResult)> {
self.results.iter()
}
pub fn shapes(&self) -> impl Iterator<Item = (&Type, ShapeId)> {
self.shapes.iter().map(|(ty, id)| (ty, *id))
}
}
pub fn infer_types(reg_fns: &[RegFn]) -> TypeCache {
// The synthetic program entry is appended last by `flatten`. It is called
// by the host with nothing, so its own parameters are undefined.
let entry = reg_fns.len() - 1;
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
let mut cache = TypeCache::default();
// The synthetic program entry is appended last by `flatten`. It is called
// by the host with no args
let req = InferRequest {
fn_id: entry,
arg_types: vec![Type::Undefined; reg_fns[entry].arg_count],
fn_id: fns.len() - 1,
arg_types: vec![],
};
let res = walk_fn(&req, reg_fns, &mut cache);
cache.insert(req, res);
let res = walk_fn(&req, fns, &mut cache);
cache.insert_result(req, res);
cache
}
#[allow(non_snake_case)]
fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
use OpCode::*;
@@ -99,25 +70,23 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe
let mut val_types = vec![Type::Unknown; f.val_count];
let mut ret_type = Type::Unknown;
// Args are not renamed: arg `k` is value id `var_count + k`, and holds what
// the caller passed until the function writes it.
for (k, ty) in req.arg_types.iter().enumerate() {
val_types[f.var_count + k] = ty.clone();
let mut old_types = val_types.clone();
// arg `i` is value id `var_count + i` (see regify)
// Init them with the arg types passed in
for (i, ty) in req.arg_types.iter().enumerate() {
val_types[f.var_count + i] = ty.clone();
}
// Types only grow, so walk until they stop changing. Then every
// instruction has seen the final type of what it reads, including what a
// later write or a loop back edge adds. For that, every rule must be
// monotone: `Unknown` means nothing has reached a value yet, so a rule
// that needs it gives `Unknown` too, never a guess that it later outgrows.
// Walk instrs till types stop changing
loop {
let prev = (val_types.clone(), ret_type.clone());
old_types.clone_from_slice(&val_types); // TODO: We only compare val types not ret type
for block in &f.blocks {
let code = &f.code[block.start..block.end];
let code = code.iter().chain(&block.postfix);
'instrs: for instr in code {
for instr in code {
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
if instr.dsts.is_empty() && !is_ret {
@@ -126,7 +95,7 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe
// TODO: Some of these ops may not every show up by this point
let ty = match instr.op {
// ──────────────────────────────────── Literals ────────────────────────────────────
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7
| PushI8 | PushI16 | PushI32 => Type::Int,
@@ -137,13 +106,13 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe
Some(JsValue::Int(_)) => Type::Int,
Some(JsValue::Float(_)) => Type::Float,
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
Some(JsValue::Native(id)) => Type::Native(*id),
Some(JsValue::NativeFn(id)) => Type::NativeFn(*id),
_ => unreachable!(),
},
_ => unreachable!(),
},
// ─────────────────────────────────── Arithmetic ───────────────────────────────────
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
Add | Sub | Mul | Mod | Pow => {
match (&val_types[instr.args[0]], &val_types[instr.args[1]]) {
(Type::Unknown, _) | (_, Type::Unknown) => Type::Unknown,
@@ -164,24 +133,27 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
// ────────────────────────────────── Value Movers ──────────────────────────────────
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
SetLoc | SetName => val_types[instr.args[0]].clone(),
Undefined => Type::Undefined,
// ──────────────────────────────────── Objects ─────────────────────────────────────
Object => Type::Empty,
// A closure of the child function, with nothing captured yet.
// The captures that follow are written into its environment.
Fclosure | Fclosure8 => match instr.imm {
ImmOperand::Const(k) => match f.cpool[k as usize] {
JsValue::FnById(fn_id) => Type::Closure {
fn_id,
env: Box::new(Type::Empty),
},
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
Fclosure | Fclosure8 => {
let cpool_idx = match instr.imm {
ImmOperand::Const(idx) => idx as usize,
_ => unreachable!(),
},
_ => unreachable!(),
},
};
let fn_id = match f.cpool[cpool_idx] {
JsValue::FnById(id) => id,
_ => unreachable!(),
};
Type::Closure {
fn_id,
env: Box::new(Type::Empty),
}
}
DefineField => {
let field = match instr.imm {
ImmOperand::Atom(atom) => atom,
@@ -189,125 +161,23 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe
};
let obj = &val_types[instr.args[1]];
if *obj == Type::Unknown {
continue;
}
let ty = set_field(obj, field, &val_types[instr.args[0]]);
// Register the shape that will be emitted. A closure's is
// its environment; an empty environment needs no struct.
let shape = match &ty {
Type::Closure { env, .. } => env.as_ref(),
other => other,
};
if matches!(shape, Type::WithField { .. }) {
cache.shape_of(shape);
}
cache.insert_shape(shape);
ty
}
GetField => {
let field = match instr.imm {
ImmOperand::Atom(atom) => atom,
_ => unreachable!(),
};
Object => Type::Empty,
let ty = val_types[instr.args[0]].clone();
get_field(&ty, field)
}
// ──────────────────────────────── Function Calls ──────────────────────────────────
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod
| TailCallMethod => 'call: {
// args are in pop order: [this,] callee, arg0 .. arg_n. A
// method call carries a receiver beneath the callee; a plain
// call has none, so its `this` is undefined.
let is_method = matches!(instr.op, CallMethod | TailCallMethod);
let n = instr.args.len();
| TailCallMethod => todo!(),
let (callee_idx, this, arg_end) = if is_method {
(
instr.args[n - 2],
val_types[instr.args[n - 1]].clone(),
n - 2,
)
} else {
(instr.args[n - 1], Type::Undefined, n - 1)
};
let fn_id = match val_types[callee_idx] {
Type::Closure { fn_id, .. } => fn_id,
// A C function has no body to walk: its return type
// is declared, from the args in source order.
Type::Native(id) => {
let args: Vec<Type> = instr.args[..arg_end]
.iter()
.rev()
.map(|&v| val_types[v].clone())
.collect();
break 'call (builtins::NATIVES[id].ret)(&args);
}
// Nothing has reached the callee yet, so nothing
// reaches the result either.
Type::Unknown => continue 'instrs,
_ => unreachable!(),
};
// The callee sees its own params, then this, then the
// environment. The actuals are padded or truncated to the
// declared arity, so the last two land at a fixed index
// whatever the caller passed.
let callee = &reg_fns[fn_id];
let declared = callee.arg_count - EXTRA_ARGS;
let mut arg_types = Vec::with_capacity(callee.arg_count);
// The value each param was passed from, if any.
let mut operands = Vec::with_capacity(callee.arg_count);
for i in 0..declared {
let v = (i < arg_end).then(|| instr.args[arg_end - 1 - i]);
arg_types.push(v.map_or(Type::Undefined, |v| val_types[v].clone()));
operands.push(v);
}
arg_types.push(this);
operands.push(is_method.then(|| instr.args[n - 1]));
arg_types.push(val_types[callee_idx].clone());
operands.push(Some(callee_idx));
let req = InferRequest { fn_id, arg_types };
// What the call returns, and what its params hold at the
// end: the callee's writes to them.
let params = callee.var_count..callee.var_count + callee.arg_count;
let (ret_ty, outs) = match cache.get(&req) {
Some(res) => (res.ret_type.clone(), res.val_types[params].to_vec()),
None => {
let res = walk_fn(&req, reg_fns, cache);
let summary = (res.ret_type.clone(), res.val_types[params].to_vec());
cache.insert(req, res);
summary
}
};
// The caller passed these values, so it sees the callee's
// writes to them.
for (v, out) in operands.into_iter().zip(&outs) {
if let Some(v) = v {
val_types[v] = join_ty(&val_types[v], out);
}
}
ret_ty
}
// ────────────────────────────────────── Returns ───────────────────────────────────
// ──────────────────────────────────────── Returns ──────────────────────────────────────────────────────────
Return | ReturnAsync => val_types[instr.args[0]].clone(),
ReturnUndef => Type::Undefined,
@@ -322,26 +192,29 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe
}
}
// A copy of an object is the same object, so what is done
// through the copy is done to the original as well.
if instr.op == SetLoc && is_object(&val_types[instr.dsts[0]]) {
let (src, dst) = (instr.args[0], instr.dsts[0]);
val_types[src] = join_ty(&val_types[src], &val_types[dst]);
}
}
}
// A copy of an object created by SetLoc is the same object, (see regify)
// so what is done through the copy is done to the original as well
if instr.op == SetLoc {
let is_object = matches!(
val_types[instr.dsts[0]],
Type::Empty | Type::WithField { .. } | Type::Closure { .. }
);
if (&val_types, &ret_type) == (&prev.0, &prev.1) {
if is_object {
let (src, dst) = (instr.args[0], instr.dsts[0]);
val_types[src] = join_ty(&val_types[src], &val_types[dst]);
}
}
} // instr
} // block
if old_types == val_types {
break;
}
}
} // loop
InferResult {
val_types,
ret_type,
}
}
fn is_object(ty: &Type) -> bool {
matches!(ty, Type::Empty | Type::WithField { .. } | Type::Closure { .. })
}