Big progress by Claude

This commit is contained in:
2026-09-25 19:31:15 +00:00
parent 183c4e35c9
commit e93ce94ac7
15 changed files with 1577 additions and 470 deletions
+347
View File
@@ -0,0 +1,347 @@
#![allow(non_snake_case)]
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,
regify::RegFn,
};
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct InferRequest {
pub fn_id: usize,
pub arg_types: Vec<Type>,
}
#[derive(Debug)]
pub struct InferResult {
pub val_types: Vec<Type>,
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,
}
impl TypeCache {
pub fn get(&self, req: &InferRequest) -> Option<&InferResult> {
self.results.get(req)
}
pub fn insert(&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;
}
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;
let mut cache = TypeCache::default();
let req = InferRequest {
fn_id: entry,
arg_types: vec![Type::Undefined; reg_fns[entry].arg_count],
};
let res = walk_fn(&req, reg_fns, &mut cache);
cache.insert(req, res);
cache
}
fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
use OpCode::*;
let f = &reg_fns[req.fn_id];
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();
}
// 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.
loop {
let prev = (val_types.clone(), ret_type.clone());
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 {
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
if instr.dsts.is_empty() && !is_ret {
continue;
}
// TODO: Some of these ops may not every show up by this point
let ty = match instr.op {
// ──────────────────────────────────── Literals ────────────────────────────────────
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7
| PushI8 | PushI16 | PushI32 => Type::Int,
PushTrue | PushFalse => Type::Bool,
PushConst | PushConst8 => 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::Native(id)) => Type::Native(*id),
_ => unreachable!(),
},
_ => unreachable!(),
},
// ─────────────────────────────────── Arithmetic ───────────────────────────────────
Add | Sub | Mul | Mod | Pow => {
match (&val_types[instr.args[0]], &val_types[instr.args[1]]) {
(Type::Unknown, _) | (_, Type::Unknown) => Type::Unknown,
(Type::Int, Type::Int) => Type::Int,
(Type::Float, Type::Float)
| (Type::Int, Type::Float)
| (Type::Float, Type::Int) => Type::Float,
_ => Type::Any,
}
}
Neg | Plus | Inc | Dec | PostInc | PostDec => match val_types[instr.args[0]] {
Type::Unknown => Type::Unknown,
Type::Int => Type::Int,
Type::Float => Type::Float,
_ => Type::Any,
},
Div => Type::Float,
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
// ────────────────────────────────── 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),
},
_ => unreachable!(),
},
_ => unreachable!(),
},
DefineField => {
let field = match instr.imm {
ImmOperand::Atom(atom) => atom,
_ => unreachable!(),
};
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);
}
ty
}
GetField => {
let field = match instr.imm {
ImmOperand::Atom(atom) => atom,
_ => unreachable!(),
};
let ty = val_types[instr.args[0]].clone();
get_field(&ty, field)
}
// ──────────────────────────────── 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();
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 ───────────────────────────────────
Return | ReturnAsync => val_types[instr.args[0]].clone(),
ReturnUndef => Type::Undefined,
_ => unreachable!("{instr:?}"),
};
if is_ret {
ret_type = join_ty(&ret_type, &ty);
} else {
for &dst in &instr.dsts {
val_types[dst] = join_ty(&val_types[dst], &ty);
}
}
// 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]);
}
}
}
if (&val_types, &ret_type) == (&prev.0, &prev.1) {
break;
}
}
InferResult {
val_types,
ret_type,
}
}
fn is_object(ty: &Type) -> bool {
matches!(ty, Type::Empty | Type::WithField { .. } | Type::Closure { .. })
}