New model
This commit is contained in:
+445
-126
@@ -1,220 +1,539 @@
|
||||
//! Infers a type for every value (see `docs/model.md`, section 3)
|
||||
|
||||
pub mod types;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
|
||||
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
|
||||
native,
|
||||
regify::RegFn,
|
||||
transform::{FIRST_ARG, THIS_ARG},
|
||||
};
|
||||
|
||||
use types::{Type, get_field, join_ty, set_field};
|
||||
use types::{Code, Heap, Number, Site, Type};
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub struct InferRequest {
|
||||
const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
|
||||
const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
|
||||
|
||||
/// One walk of a fn with one tuple of arg types (see `docs/model.md`,
|
||||
/// section 3.4)
|
||||
#[derive(Debug)]
|
||||
pub struct Variant {
|
||||
pub fn_id: usize,
|
||||
/// One for each of the fn's params (see `TypeCache::variant()`)
|
||||
pub arg_types: Vec<Type>,
|
||||
/// 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,
|
||||
walking: bool,
|
||||
}
|
||||
|
||||
#[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 ret_type: Type,
|
||||
/// The blocks that can run
|
||||
pub reachable: Vec<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub struct TypeCache {
|
||||
results: HashMap<InferRequest, InferResult>,
|
||||
shapes: HashMap<Type, usize>,
|
||||
next_shape_id: usize,
|
||||
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 heap: Heap,
|
||||
/// The prototype of each primitive kind, as the stdlib links it, by kind
|
||||
pub protos: [Type; 3],
|
||||
/// Goes up when a variant's return or a proto changes (see `infer_types()`)
|
||||
changes: usize,
|
||||
}
|
||||
|
||||
impl TypeCache {
|
||||
pub fn get_result(&self, req: &InferRequest) -> Option<&InferResult> {
|
||||
self.results.get(req)
|
||||
/// 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 {
|
||||
self.key.clear();
|
||||
self.key.extend(params(fn_id, arg_types, fns));
|
||||
|
||||
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(),
|
||||
result: None,
|
||||
ret: Type::EMPTY,
|
||||
walking: false,
|
||||
});
|
||||
|
||||
v
|
||||
}
|
||||
|
||||
pub fn insert_result(&mut self, req: InferRequest, res: InferResult) {
|
||||
self.results.insert(req, res);
|
||||
/// The variant for a call that infer walked
|
||||
pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
|
||||
let key: Vec<Type> = params(fn_id, arg_types, fns).collect();
|
||||
self.by_args[fn_id][&key]
|
||||
}
|
||||
|
||||
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;
|
||||
/// Infer a call of `fn_id`, walking it unless an identical call was
|
||||
/// already walked 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);
|
||||
|
||||
match &self.variants[v] {
|
||||
Variant { result: Some(res), .. } => return res.ret_type,
|
||||
Variant { walking: true, ret, .. } => return *ret,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.variants[v].walking = true;
|
||||
let res = walk_fn(v, fns, self);
|
||||
let ret = res.ret_type;
|
||||
|
||||
let last = self.variants[v].ret;
|
||||
let joined = self.heap.join(&last, &ret);
|
||||
self.changes += (joined != last) as usize;
|
||||
|
||||
let variant = &mut self.variants[v];
|
||||
variant.walking = false;
|
||||
variant.ret = joined;
|
||||
variant.result = Some(res);
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of each of fn `fn_id`'s params, for a call with args of these
|
||||
/// types (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).map_or(Type::UNDEFINED, Type::widen))
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
|
||||
let mut cache = TypeCache {
|
||||
variants: vec![],
|
||||
by_args: fns.iter().map(|_| HashMap::new()).collect(),
|
||||
key: vec![],
|
||||
heap: Heap::new(fns),
|
||||
protos: [Type::EMPTY; 3],
|
||||
changes: 0,
|
||||
};
|
||||
|
||||
loop {
|
||||
let before = (cache.heap.version, cache.changes);
|
||||
cache.variants.iter_mut().for_each(|v| v.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.heap.version, cache.changes) {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: fns.len() - 1,
|
||||
arg_types: vec![],
|
||||
};
|
||||
|
||||
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 {
|
||||
fn walk_fn(variant: usize, 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;
|
||||
|
||||
let mut old_types = val_types.clone();
|
||||
let fn_id = cache.variants[variant].fn_id;
|
||||
let f = ®_fns[fn_id];
|
||||
let mut val_types = vec![Type::EMPTY; f.val_count];
|
||||
let mut instr_types = vec![Type::EMPTY; f.code.len()];
|
||||
let mut ret_type = Type::EMPTY;
|
||||
|
||||
// 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();
|
||||
for (i, ty) in cache.variants[variant].arg_types.iter().enumerate() {
|
||||
val_types[f.var_count + i] = *ty;
|
||||
}
|
||||
|
||||
// Walk instrs till types stop changing
|
||||
// The blocks that can run, given what the types say about each condition
|
||||
let mut reachable = vec![false; f.blocks.len()];
|
||||
reachable[0] = true;
|
||||
|
||||
// Scratch for calls, kept across instrs
|
||||
let mut arg_types: Vec<Type> = vec![];
|
||||
let mut codes: Vec<Code> = 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 heap
|
||||
// changes, so the values, the blocks and the heap are enough to watch
|
||||
loop {
|
||||
old_types.clone_from_slice(&val_types); // TODO: We only compare val types not ret type
|
||||
let version = cache.heap.version;
|
||||
let mut changed = false;
|
||||
|
||||
for (block_idx, block) in f.blocks.iter().enumerate() {
|
||||
if !reachable[block_idx] {
|
||||
continue;
|
||||
}
|
||||
|
||||
for block in &f.blocks {
|
||||
let code = &f.code[block.start..block.end];
|
||||
let code = code.iter().chain(&block.postfix);
|
||||
|
||||
for instr in code {
|
||||
for (offset, instr) in code.enumerate() {
|
||||
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
|
||||
|
||||
if instr.dsts.is_empty() && !is_ret {
|
||||
// A rule with an empty operand gives an empty result: no value
|
||||
// has reached the instr yet
|
||||
let waits = instr.args.iter().any(|&v| val_types[v].is_empty());
|
||||
|
||||
if (instr.dsts.is_empty() && !is_ret) || waits {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Some of these ops may not every show up by this point
|
||||
// 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 ty = match instr.op {
|
||||
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
|
||||
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7
|
||||
| PushI8 | PushI16 | PushI32 => Type::Int,
|
||||
| PushI8 | PushI16 | PushI32 => Type::number(Number::Int),
|
||||
|
||||
PushTrue | PushFalse => Type::Bool,
|
||||
PushTrue => Type::bool(Some(true)),
|
||||
PushFalse => Type::bool(Some(false)),
|
||||
PushAtomValue => Type::string(Some(instr.imm.atom())),
|
||||
PushEmptyString => Type::string(Some(EMPTY_STRING)),
|
||||
|
||||
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::NativeFn(id)) => Type::NativeFn(*id),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
PushConst | PushConst8 => match f.cpool[instr.imm.const_idx()] {
|
||||
JsValue::Int(_) => Type::number(Number::Int),
|
||||
JsValue::Float(_) => Type::number(Number::Float),
|
||||
JsValue::String(_) => Type::string(None),
|
||||
JsValue::BoolTrue => Type::bool(Some(true)),
|
||||
JsValue::BoolFalse => Type::bool(Some(false)),
|
||||
JsValue::Undefined => Type::UNDEFINED,
|
||||
JsValue::NativeFn(id) => cache.heap.create(cache.heap.builtin(Site::Native(id))),
|
||||
JsValue::ProtoLink(kind) => cache.heap.create(cache.heap.builtin(Site::ProtoLink(kind))),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
|
||||
Undefined => Type::UNDEFINED,
|
||||
Null => Type::NULL,
|
||||
|
||||
// ──────────────────────────────────────── 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,
|
||||
// `+` concatenates if either side becomes a string, which
|
||||
// only a string or an object can
|
||||
Add => {
|
||||
let (l, r) = (arg(1), arg(0));
|
||||
let is_string = |t: Type| t.is_only(Type::string(None));
|
||||
let can_be_string = |t: Type| t.string.is_some() || t.object.is_some();
|
||||
|
||||
if is_number(l) && is_number(r) {
|
||||
Type::number(l.number.max(r.number).unwrap())
|
||||
} else if is_string(l) || is_string(r) {
|
||||
Type::string(None)
|
||||
} else if can_be_string(l) || can_be_string(r) {
|
||||
Type { string: Some(None), ..Type::number(Number::Float) }
|
||||
} else {
|
||||
Type::number(Number::Float)
|
||||
}
|
||||
}
|
||||
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,
|
||||
// Any other operator converts its operands to numbers
|
||||
Sub | Mul | Mod | Pow => match (arg(1), arg(0)) {
|
||||
(l, r) if is_int(l) && is_int(r) => Type::number(Number::Int),
|
||||
_ => Type::number(Number::Float),
|
||||
},
|
||||
Div => Type::Float,
|
||||
Neg | Plus | Inc | Dec | PostInc | PostDec => match arg(0) {
|
||||
t if is_int(t) => Type::number(Number::Int),
|
||||
_ => Type::number(Number::Float),
|
||||
},
|
||||
Div => Type::number(Number::Float),
|
||||
// Bitwise NOT gives an int32
|
||||
Not => Type::number(Number::Int),
|
||||
|
||||
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
|
||||
Lt | Lte | Gt | Gte => Type::bool(None),
|
||||
|
||||
// ─────────────────────────────── Conditions the types may decide ──────────────────────────────────────────
|
||||
// A known value lets infer skip the branch that cannot run
|
||||
// (see `reachable` below)
|
||||
// An identity test of two objects (see `docs/model.md`, section 5)
|
||||
StrictEq | StrictNeq | Eq | Neq if arg(0).object.is_some() && arg(1).object.is_some() => {
|
||||
for obj in [arg(0).object, arg(1).object].into_iter().flatten() {
|
||||
cache.heap.mark(obj, |shape| &mut shape.compared);
|
||||
}
|
||||
|
||||
Type::bool(None)
|
||||
}
|
||||
StrictEq => Type::bool(strict_eq(&arg(0), &arg(1))),
|
||||
StrictNeq => Type::bool(strict_eq(&arg(0), &arg(1)).map(|eq| !eq)),
|
||||
Eq => Type::bool(loose_eq(&arg(0), &arg(1))),
|
||||
Neq => Type::bool(loose_eq(&arg(0), &arg(1)).map(|eq| !eq)),
|
||||
Lnot => Type::bool(truthiness(&arg(0)).map(|t| !t)),
|
||||
Typeof => typeof_of(&arg(0), &cache.heap),
|
||||
IsUndefined | TypeofIsUndefined => Type::bool(test(&arg(0), Type::UNDEFINED)),
|
||||
IsNull => Type::bool(test(&arg(0), Type::NULL)),
|
||||
IsUndefinedOrNull => Type::bool(test(&arg(0), Type { null: true, ..Type::UNDEFINED })),
|
||||
TypeofIsFunction => Type::bool(is_function(&arg(0), &cache.heap)),
|
||||
|
||||
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
|
||||
SetLoc | SetName => val_types[instr.args[0]].clone(),
|
||||
Undefined => Type::Undefined,
|
||||
SetLoc | SetName => arg(0),
|
||||
|
||||
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
|
||||
Fclosure | Fclosure8 => {
|
||||
let cpool_idx = match instr.imm {
|
||||
ImmOperand::Const(idx) => idx as usize,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Object | Fclosure | Fclosure8 => cache.heap.create(cache.heap.site_at(fn_id, idx).unwrap()),
|
||||
// The write goes into the object's shape, which every value
|
||||
// that holds the object sees. A primitive has no fields to
|
||||
// write, and undefined and null throw
|
||||
DefineField | PutField => {
|
||||
if let Some(obj) = arg(1).object {
|
||||
cache.heap.write(obj, instr.imm.atom(), &arg(0));
|
||||
|
||||
let fn_id = match f.cpool[cpool_idx] {
|
||||
JsValue::FnById(id) => id,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Type::Closure {
|
||||
fn_id,
|
||||
env: Box::new(Type::Empty),
|
||||
if !cache.heap.is_creation_write(fn_id, idx) {
|
||||
cache.heap.mark(obj, |shape| &mut shape.written);
|
||||
}
|
||||
}
|
||||
|
||||
cache.heap.canon(&arg(1))
|
||||
}
|
||||
DefineField => {
|
||||
let field = match instr.imm {
|
||||
ImmOperand::Atom(atom) => atom,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let obj = &val_types[instr.args[1]];
|
||||
let ty = set_field(obj, field, &val_types[instr.args[0]]);
|
||||
|
||||
let shape = match &ty {
|
||||
Type::Closure { env, .. } => env.as_ref(),
|
||||
other => other,
|
||||
};
|
||||
|
||||
cache.insert_shape(shape);
|
||||
ty
|
||||
GetField => cache.heap.read_value(&arg(0), instr.imm.atom(), &cache.protos),
|
||||
// A string's length is its own. Anything else reads a
|
||||
// `length` field
|
||||
GetLength => {
|
||||
let others = Type { string: None, ..arg(0) };
|
||||
let read = cache.heap.read_value(&others, LENGTH_FIELD, &cache.protos);
|
||||
let own = Type { number: arg(0).string.map(|_| Number::Int), ..Type::EMPTY };
|
||||
cache.heap.join(&read, &own)
|
||||
}
|
||||
Object => Type::Empty,
|
||||
|
||||
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
|
||||
Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod
|
||||
| TailCallMethod => todo!(),
|
||||
// Operands are the callee's params: the callee (its env),
|
||||
// `this`, then the args (see regify). The call runs one of
|
||||
// the callee's fns. A callee that is not a fn throws, so it
|
||||
// gives nothing
|
||||
Call | CallMethod | CallConstructor => {
|
||||
arg_types.clear();
|
||||
arg_types.extend(instr.args.iter().map(|&v| val_types[v]));
|
||||
let args = &arg_types[FIRST_ARG as usize..];
|
||||
|
||||
codes.clear();
|
||||
|
||||
if let Some(callee) = arg_types[0].object {
|
||||
codes.extend(&cache.heap.shape(callee).code);
|
||||
}
|
||||
|
||||
let mut ret = Type::EMPTY;
|
||||
|
||||
for &code in &codes {
|
||||
let ty = match code {
|
||||
Code::Fn(fn_id) => cache.call(fn_id, &arg_types, reg_fns),
|
||||
Code::Native(id) => native::ret_type(id, args),
|
||||
Code::ProtoLink(kind) => {
|
||||
let linked = cache.protos[kind as usize];
|
||||
let joined = cache.heap.join(&linked, &args[0]);
|
||||
cache.changes += (joined != linked) as usize;
|
||||
cache.protos[kind as usize] = joined;
|
||||
Type::UNDEFINED
|
||||
}
|
||||
};
|
||||
|
||||
ret = cache.heap.join(&ret, &ty);
|
||||
}
|
||||
|
||||
// `new` gives what the constructor returns if that is an
|
||||
// object, else the new object itself, `this`
|
||||
let others = Type { object: None, ..ret };
|
||||
|
||||
match instr.op {
|
||||
CallConstructor if !others.is_empty() => {
|
||||
let this = val_types[instr.args[THIS_ARG as usize]];
|
||||
cache.heap.join(&Type { object: ret.object, ..Type::EMPTY }, &this)
|
||||
}
|
||||
_ => ret,
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────── Returns ──────────────────────────────────────────────────────────
|
||||
Return | ReturnAsync => val_types[instr.args[0]].clone(),
|
||||
ReturnUndef => Type::Undefined,
|
||||
Return | ReturnAsync => arg(0),
|
||||
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);
|
||||
}
|
||||
// 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.heap.join(&instr_types[idx], &ty);
|
||||
}
|
||||
|
||||
// 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 is_ret {
|
||||
ret_type = cache.heap.join(&ret_type, &ty);
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_object {
|
||||
let (src, dst) = (instr.args[0], instr.dsts[0]);
|
||||
val_types[src] = join_ty(&val_types[src], &val_types[dst]);
|
||||
}
|
||||
for &dst in &instr.dsts {
|
||||
let joined = cache.heap.join(&val_types[dst], &ty);
|
||||
changed |= joined != val_types[dst];
|
||||
val_types[dst] = joined;
|
||||
}
|
||||
} // instr
|
||||
|
||||
// Mark the blocks this one can go to. A conditional jump whose
|
||||
// condition the types decide goes one way only. One whose
|
||||
// condition nothing has reached yet goes nowhere yet
|
||||
// (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]] {
|
||||
cond if cond.is_empty() => continue,
|
||||
cond => truthiness(&cond).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// succ[0] is the jump target, succ[1] the fall-through
|
||||
for (i, &succ) in block.succ.iter().enumerate() {
|
||||
if jumps.is_none_or(|jumps| jumps == (i == 0)) && !reachable[succ] {
|
||||
reachable[succ] = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} // block
|
||||
|
||||
if old_types == val_types {
|
||||
if !changed && cache.heap.version == version {
|
||||
break;
|
||||
}
|
||||
} // loop
|
||||
|
||||
// A type names the site its shape was kept under at the time: make it the
|
||||
// one now
|
||||
for ty in val_types.iter_mut().chain(&mut instr_types).chain([&mut ret_type]) {
|
||||
*ty = cache.heap.canon(ty);
|
||||
}
|
||||
|
||||
InferResult {
|
||||
val_types,
|
||||
instr_types,
|
||||
ret_type,
|
||||
reachable,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every value of type `ty` is a number
|
||||
fn is_number(ty: Type) -> bool {
|
||||
ty.is_only(Type::number(Number::Int))
|
||||
}
|
||||
|
||||
/// Whether every value of type `ty` is an int32
|
||||
fn is_int(ty: Type) -> bool {
|
||||
ty == Type::number(Number::Int)
|
||||
}
|
||||
|
||||
/// The type, if every value of it is one value that its kind and refinement
|
||||
/// tell
|
||||
fn single(ty: &Type) -> Option<Type> {
|
||||
let one = match ty.kinds() {
|
||||
[true, false, false, false, false, false] | [false, true, false, false, false, false] => true,
|
||||
[false, false, true, false, false, false] => ty.bool.flatten().is_some(),
|
||||
[false, false, false, false, true, false] => ty.string.flatten().is_some(),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
one.then_some(*ty)
|
||||
}
|
||||
|
||||
/// Whether `a === b`, if the types decide it
|
||||
fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
|
||||
let disjoint = !a.kinds().iter().zip(b.kinds()).any(|(x, y)| *x && y);
|
||||
|
||||
match (single(a), single(b)) {
|
||||
_ if disjoint => Some(false),
|
||||
(Some(x), Some(y)) => Some(x == y),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `a == b`, if the types decide it. Undefined and null are equal to
|
||||
/// each other and to nothing else. Two values of one kind compare as `===`
|
||||
/// does
|
||||
fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
|
||||
let nullish = |t: &Type| t.has_nullish() && t.non_nullish().is_empty();
|
||||
|
||||
match (a, b) {
|
||||
_ if nullish(a) && nullish(b) => Some(true),
|
||||
_ if (nullish(a) && !b.has_nullish()) || (!a.has_nullish() && nullish(b)) => Some(false),
|
||||
_ if a.kinds() == b.kinds() && single(a).is_some() => strict_eq(a, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every value of type `ty` is of a kind in `kinds`: true if every
|
||||
/// value is, false if none is, `None` if the type does not decide it
|
||||
fn test(ty: &Type, kinds: Type) -> Option<bool> {
|
||||
let [inside, outside] = [true, false].map(|want| {
|
||||
ty.kinds().iter().zip(kinds.kinds()).any(|(has, of)| *has && of == want)
|
||||
});
|
||||
|
||||
match (inside, outside) {
|
||||
(true, false) => Some(true),
|
||||
(false, true) => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every value of type `ty` is a fn, if the type decides it
|
||||
fn is_function(ty: &Type, heap: &Heap) -> Option<bool> {
|
||||
let shape = ty.object.map(|site| heap.shape(site));
|
||||
let fns = shape.is_some_and(|shape| !shape.code.is_empty());
|
||||
let others = ty.kinds()[..5].contains(&true) || shape.is_some_and(|shape| shape.plain);
|
||||
|
||||
match (fns, others) {
|
||||
(true, false) => Some(true),
|
||||
(false, _) => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a value of type `ty` is truthy, if the type decides it
|
||||
fn truthiness(ty: &Type) -> Option<bool> {
|
||||
let truthy = [
|
||||
ty.undefined.then_some(Some(false)),
|
||||
ty.null.then_some(Some(false)),
|
||||
ty.bool,
|
||||
ty.number.map(|_| None),
|
||||
ty.string.map(|s| s.map(|atom| atom != EMPTY_STRING)),
|
||||
ty.object.map(|_| Some(true)),
|
||||
];
|
||||
|
||||
let mut kinds = truthy.into_iter().flatten();
|
||||
let first = kinds.next()?;
|
||||
kinds.all(|t| t == first).then_some(first).flatten()
|
||||
}
|
||||
|
||||
/// What `typeof` gives for a value of type `ty`
|
||||
fn typeof_of(ty: &Type, heap: &Heap) -> Type {
|
||||
let shape = ty.object.map(|site| heap.shape(site));
|
||||
|
||||
let names = [
|
||||
ty.undefined.then_some(73), // "undefined"
|
||||
ty.null.then_some(77), // "object"
|
||||
ty.bool.map(|_| 75), // "boolean"
|
||||
ty.number.map(|_| 74), // "number"
|
||||
ty.string.map(|_| 76), // "string"
|
||||
shape.filter(|shape| shape.plain).map(|_| 77), // "object"
|
||||
shape.filter(|shape| !shape.code.is_empty()).map(|_| 27), // "function"
|
||||
];
|
||||
|
||||
let mut names = names.into_iter().flatten();
|
||||
|
||||
match names.next() {
|
||||
Some(name) if names.all(|n| n == name) => Type::string(Some(AtomRef::Predefined(name))),
|
||||
_ => Type::string(None),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user