Big progress by Claude
This commit is contained in:
@@ -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 = ®_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 = ®_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 { .. })
|
||||
}
|
||||
Reference in New Issue
Block a user