New partly working cell model

This commit is contained in:
2026-10-07 16:41:33 +00:00
parent 44498b32a9
commit 2b612d19ed
14 changed files with 981 additions and 1902 deletions
+279 -299
View File
@@ -1,165 +1,188 @@
//! Infers a type for every value (see `docs/model.md`, section 3)
//! Infers a type for every value (see `docs/model.md`, section 4)
pub mod sites;
pub mod cells;
pub mod types;
use std::collections::HashMap;
use crate::{
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native::{self, ProtoOf},
native::{self, NATIVES, ProtoOf},
regify::RegFn,
transform::{FIRST_ARG, THIS_ARG},
};
use sites::{Code, CtxId, LENGTH_FIELD, Place, Protos, SiteId, SiteTable};
use types::{BaseType, EMPTY_STRING, Type, is_function, loose_eq, strict_eq, test, truthiness, typeof_name};
use cells::{Cell, Code, LENGTH_FIELD, ObjKind, Protos, read_value};
use types::{BaseType, EMPTY_STRING, Type, is_function, join, loose_eq, strict_eq, test, truthiness, typeof_name};
/// One walk of a fn with one tuple of arg types (see `docs/model.md`,
/// section 3.5)
/// The walk of one fn with one list of arg types
#[derive(Debug)]
pub struct Variant {
pub struct Walk {
pub fn_id: usize,
/// One for each of the fn's params (see `TypeCache::variant()`)
pub arg_types: Vec<Type>,
/// The context of the sites that it makes (see `SiteTable::context()`)
pub ctx: CtxId,
/// The walk in the current round, once it is done
pub result: Option<InferResult>,
/// What it returned in the rounds so far, which is what a call of it gives
/// while it is still being walked (a recursive call)
ret: Type,
pub arg_types: Vec<Type>, // one for each param
cells: Vec<(usize, Cell)>, // the cells it made, by the instr that makes their objects
ret: Type, // what it returned in the rounds so far: a recursive call gives that
walking: bool,
pub result: Option<InferResult>, // the walk in the current round, once it is done
}
#[derive(Debug)]
pub struct InferResult {
pub val_types: Vec<Type>,
/// The type each instr gives, before it is joined into its dst
pub instr_types: Vec<Type>,
pub instr_types: Vec<Type>, // the type each instr gives, before it is joined into its dst
pub ret_type: Type,
/// The blocks that can run
pub reachable: Vec<bool>,
pub reachable: Vec<bool>, // the blocks that can run
}
/// The walks of the program, by their keys
#[derive(Debug)]
pub struct TypeCache {
pub variants: Vec<Variant>,
/// Each fn's variants, by their arg types
by_args: Vec<HashMap<Vec<Type>, usize>>,
/// Scratch for a call's arg types
key: Vec<Type>,
pub site_table: SiteTable,
/// The prototype of each primitive kind, as the stdlib links it, by kind
pub protos: Protos,
/// Goes up when a variant's return or a proto changes (see `infer_types()`)
changes: usize,
pub walks: Vec<Walk>,
keys: HashMap<(usize, Vec<Type>), usize>, // the walk of each fn and list of arg types
builtins: Vec<Cell>, // the cells of the natives, then of the proto links
pub protos: Protos, // the prototype of each `ProtoOf`, as the stdlib links it
version: usize, // goes up on every change of a cell, a walk's return or a proto, and on every new cell or walk
}
impl TypeCache {
/// The variant for a call of `fn_id` with args of these types, made if it
/// is new. It is keyed by the fn's params: an arg it does not pass is
/// undefined, and an arg it passes beyond them is never read. Only an
/// arg's kind crosses a call, not a known value, else every literal passed
/// would make a new variant
pub fn variant(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
let site_table = &mut self.site_table;
self.key.clear();
self.key.extend(params(fn_id, arg_types, fns).map(|ty| site_table.widen(&ty)));
if let Some(&v) = self.by_args[fn_id].get(&self.key[..]) {
return v;
}
let v = self.variants.len();
self.by_args[fn_id].insert(self.key.clone(), v);
self.variants.push(Variant {
fn_id,
arg_types: self.key.clone(),
ctx: self.site_table.context(&self.key),
result: None,
ret: Type::Unknown,
walking: false,
});
v
/// The key of a call of `fn_id` with args of these types: the fn, and the
/// type of each of its params. An arg it does not pass is undefined, and
/// an arg it passes beyond them is never read. Only an arg's kind crosses
/// a call, not a known value, else every literal passed would make a new
/// walk. An object is its cell
fn key(fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> (usize, Vec<Type>) {
let params = (0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::Undefined, Type::widen));
(fn_id, params.collect())
}
/// The variant for a call that infer walked
/// The walk of a call that infer walked
pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
let site_table = &self.site_table;
let key: Vec<Type> = params(fn_id, arg_types, fns).map(|ty| site_table.widened(&ty)).collect();
self.by_args[fn_id][&key]
self.keys[&Self::key(fn_id, arg_types, fns)]
}
/// The join of `f` over the members of `ty` (see `SiteTable::members()`).
/// The lists of sums only grow, so a member stays where it is while `f`
/// adds sums
fn map(&mut self, ty: Type, mut f: impl FnMut(&mut Self, Type) -> Type) -> Type {
let mut out = Type::Unknown;
/// The cell of a built-in fn
pub fn builtin(&self, code: Code) -> &Cell {
match code {
Code::Native(id) => &self.builtins[id],
Code::ProtoLink(kind) => &self.builtins[NATIVES.len() + kind as usize],
Code::Fn(_) => unreachable!(),
}
}
for i in 0..self.site_table.members(&ty).len() {
let member = self.site_table.members(&ty)[i];
let ty = f(self, member);
out = self.site_table.join(&out, &ty);
/// The cells of the built-in fns
pub fn builtins(&self) -> &[Cell] {
&self.builtins
}
/// Every cell: the built-in ones, then the ones that each walk made
pub fn cells(&self) -> impl Iterator<Item = &Cell> {
let made = self.walks.iter().flat_map(|w| w.cells.iter().map(|(_, cell)| cell));
self.builtins.iter().chain(made)
}
fn new_cell(&mut self, kind: ObjKind, made: Option<(usize, usize)>) -> Cell {
self.version += 1;
Cell::new(kind, made)
}
/// A cell that instr `idx` of walk `w`'s fn made, in the key of `w` or of
/// a walk that made a cell in it, and so on: nearest first. A new cell
/// would hold the instr twice in its name, and such names can nest with
/// no end, so the walk uses that cell again
fn made_before(&self, w: usize, idx: usize) -> Option<Cell> {
let fn_id = self.walks[w].fn_id;
let mut queue = vec![w];
let mut i = 0;
while let Some(&walk) = queue.get(i) {
i += 1;
for member in self.walks[walk].arg_types.iter().flat_map(Type::members) {
let Type::Obj(cell) = member else { continue };
let Some((maker, at)) = cell.info().made else { continue };
if self.walks[maker].fn_id == fn_id && at == idx {
return Some(cell.clone());
}
if !queue.contains(&maker) {
queue.push(maker);
}
}
}
out
None
}
/// The join of `f` over each pair of members of `a` and `b`
fn map2(&mut self, a: Type, b: Type, mut f: impl FnMut(&mut Self, Type, Type) -> Type) -> Type {
self.map(a, |cache, a| cache.map(b, |cache, b| f(cache, a, b)))
/// Count a change, if there was one
fn changed(&mut self, changed: bool) {
self.version += changed as usize;
}
/// What `f` gives for every member of `ty`, if all members give the same
fn agree<T: PartialEq>(&self, ty: &Type, f: impl Fn(&Type) -> Option<T>) -> Option<T> {
let mut members = self.site_table.members(ty).iter().map(f);
let first = members.next()??;
members.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// What `f` gives for every pair of members of `a` and `b`, if all pairs
/// give the same
fn agree2<T: PartialEq>(&self, a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option<T>) -> Option<T> {
let members = |t| self.site_table.members(t).iter();
let mut pairs = members(a).flat_map(|x| members(b).map(move |y| (x, y))).map(|(x, y)| f(x, y));
let first = pairs.next()??;
pairs.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// Infer a call of `fn_id`, walking it unless an identical call was
/// already walked in this round. Gives what it returns
/// Infer a call of `fn_id`, walking it unless the walk of its key is done
/// in this round. Gives what it returns
fn call(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> Type {
let v = self.variant(fn_id, arg_types, fns);
let key = Self::key(fn_id, arg_types, fns);
match &self.variants[v] {
Variant { result: Some(res), .. } => return res.ret_type,
Variant { walking: true, ret, .. } => return *ret,
let w = match self.keys.get(&key) {
Some(&w) => w,
None => {
self.version += 1;
self.walks.push(Walk {
fn_id,
arg_types: key.1.clone(),
cells: vec![],
ret: Type::Unknown,
walking: false,
result: None,
});
self.keys.insert(key, self.walks.len() - 1);
self.walks.len() - 1
}
};
match &self.walks[w] {
Walk { walking: true, ret, .. } => return ret.clone(),
Walk { result: Some(res), .. } => return res.ret_type.clone(),
_ => {}
}
self.variants[v].walking = true;
let res = walk_fn(v, fns, self);
let ret = res.ret_type;
self.walks[w].walking = true;
let res = walk_fn(w, fns, self);
let ret = res.ret_type.clone();
let last = self.variants[v].ret;
let joined = self.site_table.join(&last, &ret);
self.changes += (joined != last) as usize;
let last = &self.walks[w].ret;
let joined = join(last, &ret);
let grew = joined != *last;
self.changed(grew);
let variant = &mut self.variants[v];
variant.walking = false;
variant.ret = joined;
variant.result = Some(res);
let walk = &mut self.walks[w];
walk.walking = false;
walk.ret = joined;
walk.result = Some(res);
ret
}
}
/// The type of each of fn `fn_id`'s params, for a call with args of these
/// types, before known values are dropped (see `TypeCache::variant()`)
fn params<'a>(fn_id: usize, arg_types: &'a [Type], fns: &[RegFn]) -> impl Iterator<Item = Type> + 'a {
(0..fns[fn_id].arg_count).map(|i| arg_types.get(i).copied().unwrap_or(Type::Undefined))
/// The join of `f` over the members of `ty`
fn map(ty: &Type, mut f: impl FnMut(&Type) -> Type) -> Type {
ty.members().iter().fold(Type::Unknown, |all, member| join(&all, &f(member)))
}
/// What `f` gives for every member of `ty`, if all members give the same
fn agree<T: PartialEq>(ty: &Type, f: impl Fn(&Type) -> Option<T>) -> Option<T> {
let mut members = ty.members().iter().map(f);
let first = members.next()??;
members.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// What `f` gives for every pair of members of `a` and `b`, if all pairs give
/// the same
fn agree2<T: PartialEq>(a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option<T>) -> Option<T> {
let pairs = a.members().iter().flat_map(|x| b.members().iter().map(move |y| (x, y)));
let mut results = pairs.map(|(x, y)| f(x, y));
let first = results.next()??;
results.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// A call of `code` with operands of these types (see the call rule in
@@ -169,104 +192,77 @@ fn call_code(cache: &mut TypeCache, code: Code, arg_types: &[Type], fns: &[RegFn
match code {
Code::Fn(fn_id) => cache.call(fn_id, arg_types, fns),
Code::Native(id) => native::ret_type(id, args, &cache.site_table),
Code::Native(id) => native::ret_type(id, args),
Code::ProtoLink(kind) => {
let linked = cache.protos[kind as usize];
let joined = cache.site_table.join(&linked, args.first().unwrap_or(&Type::Undefined));
cache.changes += (joined != linked) as usize;
let linked = &cache.protos[kind as usize];
let joined = join(linked, args.first().unwrap_or(&Type::Undefined));
cache.changed(joined != *linked);
cache.protos[kind as usize] = joined;
Type::Undefined
}
}
}
/// A call of a callee of type `Any`, with operands of these types: a call of
/// any fn object in an `Any`, with every operand an `Any`, except that the
/// callee is its own env. That is the variant that emit's dispatch on `Any`
/// calls (see `call_any_variant()`). Gives an `Any`
fn call_any(cache: &mut TypeCache, arg_types: &mut [Type], fns: &[RegFn]) -> Type {
for ty in arg_types.iter_mut() {
*ty = cache.site_table.to_any(ty);
}
for site in 0..cache.site_table.sites.len() as SiteId {
let Some(code) = cache.site_table.site(site).code().filter(|_| cache.site_table.site(site).in_any) else {
continue;
};
arg_types[0] = Type::Obj(site);
let ret = call_code(cache, code, arg_types, fns);
cache.site_table.to_any(&ret);
}
Type::Any
}
/// The site that `place` makes in context `ctx`, made if it is new
fn site_at(cache: &mut TypeCache, place: Place, ctx: CtxId) -> SiteId {
let place = cache.site_table.place_id(place);
cache.site_table.site_at(place, ctx)
}
/// An array index must be a number. Any other key is a computed field name,
/// which is not supported
fn check_index(cache: &TypeCache, index: &Type) {
for member in cache.site_table.members(index) {
fn check_index(index: &Type) {
for member in index.members() {
if !matches!(member, Type::Int | Type::Float) {
unimplemented!("`a[k]` with a key of type {member:?}: computed field names");
}
}
}
/// The operand types of the variant that a call on `Any`, with `n_args`
/// args, runs for a fn object of `site`
pub fn call_any_variant(site: SiteId, n_args: usize) -> Vec<Type> {
let mut arg_types = vec![Type::Any; FIRST_ARG as usize + n_args];
arg_types[0] = Type::Obj(site);
arg_types
}
/// Walk every variant from the entry, in rounds, until a round changes
/// nothing. A variant reads the shapes, the protos and the returns of other
/// variants, which variants walked after it can still add to. All of them only
/// grow, so the rounds end. The results are those of the last round
/// Walk the program from the entry, in rounds, until a round changes
/// nothing. A walk reads cells, protos and the returns of other walks, which
/// walks after it can still add to. All of them only grow, so the rounds end.
/// The results are those of the last round
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
let mut cache = TypeCache {
variants: vec![],
by_args: fns.iter().map(|_| HashMap::new()).collect(),
key: vec![],
site_table: SiteTable::new(fns),
protos: [Type::Unknown; ProtoOf::ALL.len()],
changes: 0,
walks: vec![],
keys: HashMap::new(),
builtins: vec![],
protos: std::array::from_fn(|_| Type::Unknown),
version: 0,
};
let codes = (0..NATIVES.len()).map(Code::Native).chain(ProtoOf::ALL.map(Code::ProtoLink));
for code in codes {
let cell = cache.new_cell(ObjKind::Fn(code), None);
cache.builtins.push(cell);
}
loop {
let before = (cache.site_table.version, cache.changes);
cache.variants.iter_mut().for_each(|v| v.result = None);
let version = cache.version;
cache.walks.iter_mut().for_each(|w| w.result = None);
// The synthetic program entry is appended last by `flatten`. It is
// called by the host with no args
cache.call(fns.len() - 1, &[], fns);
if before == (cache.site_table.version, cache.changes) {
if cache.version == version {
return cache;
}
}
}
fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
fn walk_fn(w: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
use OpCode::*;
let (fn_id, ctx) = (cache.variants[variant].fn_id, cache.variants[variant].ctx);
let fn_id = cache.walks[w].fn_id;
let f = &reg_fns[fn_id];
let mut val_types = vec![Type::Unknown; f.val_count];
let mut instr_types = vec![Type::Unknown; f.code.len()];
let mut ret_type = Type::Unknown;
// The cells that this walk made in the rounds before. A recursive call of
// the walk does not walk it, so it does not need them
let mut cells = std::mem::take(&mut cache.walks[w].cells);
// arg `i` is value id `var_count + i` (see regify)
// Init them with the arg types passed in
for (i, ty) in cache.variants[variant].arg_types.iter().enumerate() {
val_types[f.var_count + i] = *ty;
for (i, ty) in cache.walks[w].arg_types.iter().enumerate() {
val_types[f.var_count + i] = ty.clone();
}
// The blocks that can run, given what the types say about each condition
@@ -277,11 +273,10 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
let mut arg_types: Vec<Type> = vec![];
// Walk instrs till nothing changes. A return only changes `ret_type` once
// its block is reachable, and a field read only gives more once the site
// table changes, so the values, the blocks and the site table are enough
// to watch
// its block is reachable, and a field read only gives more once a cell
// changes, so the values, the blocks and the version are enough to watch
loop {
let version = cache.site_table.version;
let version = cache.version;
let mut changed = false;
for (block_idx, block) in f.blocks.iter().enumerate() {
@@ -293,19 +288,34 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
let code = code.iter().chain(&block.postfix);
for (offset, instr) in code.enumerate() {
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
// Jumps are the block's to walk (see below). The others give
// nothing
if matches!(instr.op, Nop | Drop | Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8) {
continue;
}
// A rule with an `Unknown` operand gives `Unknown`: no value has
// reached the instr yet
let waits = instr.args.iter().any(|&v| val_types[v] == Type::Unknown);
if (instr.dsts.is_empty() && !is_ret) || waits {
if instr.args.iter().any(|&v| val_types[v] == Type::Unknown) {
continue;
}
// The instr's index in the code (postfix copies have none)
let idx = block.start + offset;
let arg = |i: usize| val_types[instr.args[i]];
let arg = |i: usize| val_types[instr.args[i]].clone();
// The cell of the objects that this instr makes, made the
// first time, unless the key already holds one (see
// `made_before()`)
let mut cell_here = |cache: &mut TypeCache, kind: ObjKind| {
let found = cells.iter().find(|(i, _)| *i == idx).map(|(_, cell)| cell.clone());
found.or_else(|| cache.made_before(w, idx)).unwrap_or_else(|| {
let cell = cache.new_cell(kind, Some((w, idx)));
cells.push((idx, cell.clone()));
cell
})
};
let ty = match instr.op {
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
@@ -324,8 +334,8 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
JsValue::BoolTrue => Type::Bool(Some(true)),
JsValue::BoolFalse => Type::Bool(Some(false)),
JsValue::Undefined => Type::Undefined,
JsValue::NativeFn(id) => cache.site_table.create(cache.site_table.builtin(Place::Native(id))),
JsValue::ProtoLink(kind) => cache.site_table.create(cache.site_table.builtin(Place::ProtoLink(kind))),
JsValue::NativeFn(id) => Type::Obj(cache.builtin(Code::Native(id)).clone()),
JsValue::ProtoLink(kind) => Type::Obj(cache.builtin(Code::ProtoLink(kind)).clone()),
_ => unreachable!(),
},
@@ -335,18 +345,25 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
// `+` concatenates if either side becomes a string, which
// a string or an object does. Else it adds numbers
Add => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) {
(Type::Any, _) | (_, Type::Any) => Type::Any,
(l, r) if [l, r].iter().any(|t| matches!(t, Type::Str(_) | Type::Obj(_))) => Type::Str(None),
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}),
Add => {
let r = arg(0);
map(&arg(1), |l| map(&r, |r| match (l, r) {
(l, r) if [l, r].iter().any(|t| matches!(t, Type::Str(_) | Type::Obj(_))) => Type::Str(None),
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}))
}
// Any other operator converts its operands to numbers
Sub | Mul | Mod | Pow => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) {
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}),
Neg | Plus | Inc | Dec | PostInc | PostDec => cache.map(arg(0), |_, t| match t {
Sub | Mul | Mod | Pow => {
let r = arg(0);
map(&arg(1), |l| map(&r, |r| match (l, r) {
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}))
}
Neg | Plus | Inc | Dec | PostInc | PostDec => map(&arg(0), |t| match t {
Type::Int => Type::Int,
_ => Type::Float,
}),
@@ -360,56 +377,45 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
// A known value lets infer skip the branch that cannot run
// (see `reachable` below)
StrictEq | StrictNeq | Eq | Neq => {
let (a, b) = (arg(0), arg(1));
// An identity test of an object with what can be an
// object (see `docs/model.md`, section 5)
let members = |t: &Type| cache.site_table.members(t).to_vec();
let (a_members, b_members) = (members(&a), members(&b));
let can_be_object = |ms: &[Type]| ms.iter().any(|t| matches!(t, Type::Obj(_) | Type::Any));
if can_be_object(&a_members) && can_be_object(&b_members) {
for t in a_members.iter().chain(&b_members) {
if let Type::Obj(site) = t {
cache.site_table.mark(*site, |shape| &mut shape.compared);
}
}
}
let eq = match instr.op {
StrictEq | StrictNeq => cache.agree2(&a, &b, strict_eq),
_ => cache.agree2(&a, &b, loose_eq),
StrictEq | StrictNeq => agree2(&arg(0), &arg(1), strict_eq),
_ => agree2(&arg(0), &arg(1), loose_eq),
};
Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq)))
}
Lnot => Type::Bool(cache.agree(&arg(0), truthiness).map(|t| !t)),
Typeof => match cache.agree(&arg(0), |t| typeof_name(t, &cache.site_table)) {
Lnot => Type::Bool(agree(&arg(0), truthiness).map(|t| !t)),
Typeof => match agree(&arg(0), typeof_name) {
Some(name) => Type::Str(Some(AtomRef::Predefined(name))),
None => Type::Str(None),
},
IsUndefined | TypeofIsUndefined => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined]))),
IsNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Null]))),
IsUndefinedOrNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))),
TypeofIsFunction => Type::Bool(cache.agree(&arg(0), |t| is_function(t, &cache.site_table))),
IsUndefined | TypeofIsUndefined => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Undefined]))),
IsNull => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Null]))),
IsUndefinedOrNull => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))),
TypeofIsFunction => Type::Bool(agree(&arg(0), is_function)),
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
SetLoc | SetName => arg(0),
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
Object | Fclosure | Fclosure8 => {
let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx);
cache.site_table.create(site)
Object => Type::Obj(cell_here(cache, ObjKind::Plain)),
Fclosure | Fclosure8 => {
let JsValue::FnById(child) = f.cpool[instr.imm.const_idx()] else {
unreachable!()
};
Type::Obj(cell_here(cache, ObjKind::Fn(Code::Fn(child))))
}
// The elements are its operands, last first
// The elements are its operands
ArrayFrom => {
let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx);
let cell = cell_here(cache, ObjKind::Array { elems: Type::Unknown });
for &v in &instr.args {
cache.site_table.write_elem(site, &val_types[v]);
let grew = cell.write_elem(&val_types[v]);
cache.changed(grew);
}
cache.site_table.create(site)
Type::Obj(cell)
}
// `a[i]` and `a[i] = x`, on arrays, and `s[i]` on strings:
// an index is a number. Other objects need computed field
@@ -419,14 +425,14 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
GetArrayEl => {
let index = arg(0);
cache.map(arg(1), |cache, obj| match obj {
Type::Obj(site) if cache.site_table.site(site).is_array() => {
check_index(cache, &index);
cache.site_table.read_elem(site)
map(&arg(1), |obj| match obj {
Type::Obj(cell) if cell.is_array() => {
check_index(&index);
cell.read_elem()
}
Type::Str(_) => {
check_index(cache, &index);
cache.site_table.join(&Type::Str(None), &Type::Undefined)
check_index(&index);
join(&Type::Str(None), &Type::Undefined)
}
Type::Undefined | Type::Null => Type::Unknown,
obj => unimplemented!("`o[k]` on {obj:?}: computed field names"),
@@ -435,98 +441,71 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
PutArrayEl => {
let (value, index) = (arg(0), arg(1));
cache.map(arg(2), |cache, obj| {
for obj in arg(2).members() {
match obj {
Type::Obj(site) if cache.site_table.site(site).is_array() => {
check_index(cache, &index);
cache.site_table.write_elem(site, &value);
cache.site_table.mark(site, |s| &mut s.written);
Type::Obj(cell) if cell.is_array() => {
check_index(&index);
let grew = cell.write_elem(&value);
cache.changed(grew);
}
Type::Undefined | Type::Null | Type::Str(_) => {}
obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"),
}
}
Type::Unknown
});
arg(2)
Type::Unknown
}
// A key stays as it is: a number is an index
ToPropkey => arg(0),
// The write goes into the fields of the object's site, which
// every value that holds an object of the site sees. A write
// on `Any` can reach any object in an `Any`. A primitive has
// no fields to write, and undefined and null throw
// The write goes into the object's cell, which every holder
// of the object sees. A primitive has no fields to write,
// and undefined and null throw
DefineField | PutField => {
let (field, value) = (instr.imm.atom(), arg(0));
// A define_field builds the object before it goes
// anywhere (see regify)
let creation = instr.op == DefineField;
cache.map(arg(1), |cache, obj| {
match obj {
Type::Obj(site) => {
cache.site_table.write(site, field, &value);
if !creation {
cache.site_table.mark(site, |shape| &mut shape.written);
}
}
Type::Any => {
for site in 0..cache.site_table.sites.len() as SiteId {
if cache.site_table.site(site).in_any {
cache.site_table.write(site, field, &value);
cache.site_table.mark(site, |shape| &mut shape.written);
}
}
}
_ => {}
for obj in arg(1).members() {
if let Type::Obj(cell) = obj {
let grew = cell.write(field, &value, instr.op == DefineField);
cache.changed(grew);
}
}
Type::Unknown
});
arg(1)
Type::Unknown
}
GetField => cache.site_table.read_value(&arg(0), instr.imm.atom(), &cache.protos),
GetField => read_value(&arg(0), instr.imm.atom(), &cache.protos),
// A string's length is its own. Anything else reads a
// `length` field
GetLength => cache.map(arg(0), |cache, obj| match obj {
GetLength => map(&arg(0), |obj| match obj {
Type::Str(_) => Type::Int,
obj => cache.site_table.read_value(&obj, LENGTH_FIELD, &cache.protos),
obj => read_value(obj, LENGTH_FIELD, &cache.protos),
}),
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
// Operands are the callee's params: the callee (its env),
// `this`, then the args (see regify). A callee that is not
// a fn throws, so it gives nothing
// The callee's env is the fn object itself, so a call of
// each member of a sum passes that member as the env
// a fn throws, so it gives nothing. A call of each member
// of a sum passes that member as the env
Call | CallMethod | CallConstructor => {
let this = arg(THIS_ARG as usize);
let ret = map(&arg(0), |callee| {
let Type::Obj(cell) = callee else {
return Type::Unknown;
};
let Some(code) = cell.code() else {
return Type::Unknown;
};
let ret = cache.map(arg(0), |cache, callee| {
arg_types.clear();
arg_types.extend(instr.args.iter().map(|&v| val_types[v]));
arg_types[0] = callee;
match callee {
Type::Obj(site) => match cache.site_table.site(site).code() {
Some(code) => call_code(cache, code, &arg_types, reg_fns),
None => Type::Unknown,
},
Type::Any => call_any(cache, &mut arg_types, reg_fns),
_ => Type::Unknown,
}
arg_types.extend(instr.args.iter().map(|&v| val_types[v].clone()));
arg_types[0] = callee.clone();
call_code(cache, code, &arg_types, reg_fns)
});
// `new` gives what the constructor returns if that is an
// object, else the new object itself, `this`
match instr.op {
CallConstructor => cache.map(ret, |cache, ret| match ret {
Type::Obj(_) => ret,
Type::Any => cache.site_table.join(&ret, &this),
_ => this,
CallConstructor => map(&ret, |ret| match ret {
Type::Obj(_) => ret.clone(),
_ => arg(THIS_ARG as usize),
}),
_ => ret,
}
@@ -542,16 +521,16 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
// Postfix copies come after the block's own instrs, and have no
// entry: a copy gives the type of what it copies
if idx < block.end {
instr_types[idx] = cache.site_table.join(&instr_types[idx], &ty);
instr_types[idx] = join(&instr_types[idx], &ty);
}
if is_ret {
ret_type = cache.site_table.join(&ret_type, &ty);
if matches!(instr.op, Return | ReturnAsync | ReturnUndef) {
ret_type = join(&ret_type, &ty);
continue;
}
for &dst in &instr.dsts {
let joined = cache.site_table.join(&val_types[dst], &ty);
let joined = join(&val_types[dst], &ty);
changed |= joined != val_types[dst];
val_types[dst] = joined;
}
@@ -563,9 +542,9 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
// (A block that only carries copies on an edge has no instrs)
let jumps = match f.code[block.start..block.end].last() {
Some(last) if matches!(last.op, IfFalse | IfFalse8 | IfTrue | IfTrue8) => {
match val_types[last.args[0]] {
match &val_types[last.args[0]] {
Type::Unknown => continue,
cond => cache.agree(&cond, truthiness).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
cond => agree(cond, truthiness).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
}
}
_ => None,
@@ -580,11 +559,13 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
}
} // block
if !changed && cache.site_table.version == version {
if !changed && cache.version == version {
break;
}
} // loop
cache.walks[w].cells = cells;
InferResult {
val_types,
instr_types,
@@ -592,4 +573,3 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
reachable,
}
}