New partly working cell model
This commit is contained in:
@@ -1,63 +0,0 @@
|
|||||||
//! Escape (see `docs/model.md`, section 6)
|
|
||||||
|
|
||||||
use super::{Flow, instrs};
|
|
||||||
use crate::{libqjs::parse::opcodes::OpCode::*, regify::RegFn};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Escape {
|
|
||||||
/// No holder outlives the frame, so the cell can be on the stack
|
|
||||||
Local,
|
|
||||||
/// The cell must be on the heap. `docs/model.md` also has `returned`, which
|
|
||||||
/// this analysis does not prove yet, so such a cell escapes
|
|
||||||
Escapes,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The escape of each value that `object`, `fclosure` or `array_from` gives. Every other
|
|
||||||
/// value gives `Escapes`, which nothing reads
|
|
||||||
pub fn analyze(f: &RegFn, flow: &mut Flow) -> Vec<Escape> {
|
|
||||||
let mut escape = vec![Escape::Escapes; f.val_count];
|
|
||||||
|
|
||||||
for (block, instr) in instrs(f) {
|
|
||||||
if matches!(instr.op, Object | Fclosure | ArrayFrom) {
|
|
||||||
let cell = instr.dsts[0];
|
|
||||||
flow.new_walk();
|
|
||||||
|
|
||||||
if !escapes(flow, cell, flow.in_loop[block]) {
|
|
||||||
escape[cell] = Escape::Local;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
escape
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the cell that `v` holds can be reached after the frame returns, or
|
|
||||||
/// when its creating op runs again (`looped`: the op is in a loop). A var or a
|
|
||||||
/// join slot can keep the cell until the next run, so in a loop any copy
|
|
||||||
/// escapes. A field, an element, a callee and a return can keep it beyond the
|
|
||||||
/// frame
|
|
||||||
fn escapes(flow: &mut Flow, v: usize, looped: bool) -> bool {
|
|
||||||
flow.visit(v);
|
|
||||||
|
|
||||||
for i in flow.use_range(v) {
|
|
||||||
let u = flow.uses[i];
|
|
||||||
|
|
||||||
let out = match u.op {
|
|
||||||
Call | CallMethod | CallConstructor | Return | ReturnAsync => true,
|
|
||||||
DefineField | PutField | PutArrayEl if u.operand == 0 => true,
|
|
||||||
ArrayFrom => true,
|
|
||||||
SetLoc | SetName if looped => true,
|
|
||||||
SetLoc | SetName => {
|
|
||||||
let dst = u.dst.unwrap() as usize;
|
|
||||||
flow.visit(dst) && escapes(flow, dst, looped)
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if out {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
//! The analyses that narrow the representation (see `docs/model.md`, section 6).
|
|
||||||
//! They run after infer, on each fn's code
|
|
||||||
|
|
||||||
mod escape;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
libqjs::parse::opcodes::OpCode,
|
|
||||||
regify::{RegFn, RegInstr},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use escape::Escape;
|
|
||||||
|
|
||||||
pub struct Analysis {
|
|
||||||
/// Where the cell can be reached, for each value that a creating op gives
|
|
||||||
pub escape: Vec<Escape>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Analysis {
|
|
||||||
fn of(f: &RegFn) -> Self {
|
|
||||||
let mut flow = Flow::of(f);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
escape: escape::analyze(f, &mut flow),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn analyze(fns: &[RegFn]) -> Vec<Analysis> {
|
|
||||||
fns.iter().map(Analysis::of).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One use of a value: an operand of an instr
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct Use {
|
|
||||||
op: OpCode,
|
|
||||||
/// Which operand the value is
|
|
||||||
operand: u32,
|
|
||||||
/// The instr's first dst, if it has one
|
|
||||||
dst: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What the analyses need about a fn: the uses of each value, in one array
|
|
||||||
/// sorted by value (see `ranges()`), and which blocks are in a loop
|
|
||||||
struct Flow {
|
|
||||||
uses: Vec<Use>,
|
|
||||||
use_starts: Vec<u32>,
|
|
||||||
/// Whether a path leads from each block back to it
|
|
||||||
in_loop: Vec<bool>,
|
|
||||||
/// Scratch for walks over values and over blocks. A mark is current if it
|
|
||||||
/// equals its walk's stamp, so the arrays are never cleared
|
|
||||||
val_stamp: u32,
|
|
||||||
seen_vals: Vec<u32>,
|
|
||||||
block_stamp: u32,
|
|
||||||
seen_blocks: Vec<u32>,
|
|
||||||
stack: Vec<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Flow {
|
|
||||||
fn of(f: &RegFn) -> Self {
|
|
||||||
let uses = instrs(f).flat_map(|(_, instr)| {
|
|
||||||
(instr.args.iter().enumerate()).map(move |(operand, &v)| {
|
|
||||||
let dst = instr.dsts.first().map(|&d| d as u32);
|
|
||||||
(v, Use { op: instr.op, operand: operand as u32, dst })
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let (uses, use_starts) = ranges(f.val_count, uses);
|
|
||||||
|
|
||||||
let mut flow = Self {
|
|
||||||
uses,
|
|
||||||
use_starts,
|
|
||||||
in_loop: vec![],
|
|
||||||
val_stamp: 0,
|
|
||||||
seen_vals: vec![0; f.val_count],
|
|
||||||
block_stamp: 0,
|
|
||||||
seen_blocks: vec![0; f.blocks.len()],
|
|
||||||
stack: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
flow.in_loop = (0..f.blocks.len()).map(|b| flow.returns_to(f, b)).collect();
|
|
||||||
flow
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The uses of value `v`, by index into `uses`
|
|
||||||
fn use_range(&self, v: usize) -> std::ops::Range<usize> {
|
|
||||||
self.use_starts[v] as usize..self.use_starts[v + 1] as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start a new walk over values: every mark of the walks before is now
|
|
||||||
/// stale
|
|
||||||
fn new_walk(&mut self) {
|
|
||||||
self.val_stamp += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark value `v` seen in this walk. Whether it was not seen before
|
|
||||||
fn visit(&mut self, v: usize) -> bool {
|
|
||||||
let fresh = self.seen_vals[v] != self.val_stamp;
|
|
||||||
self.seen_vals[v] = self.val_stamp;
|
|
||||||
fresh
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a path leads from `block` back to it
|
|
||||||
fn returns_to(&mut self, f: &RegFn, block: usize) -> bool {
|
|
||||||
self.block_stamp += 1;
|
|
||||||
let stamp = self.block_stamp;
|
|
||||||
|
|
||||||
self.stack.clear();
|
|
||||||
self.stack.extend(f.blocks[block].succ.iter().map(|&s| s as u32));
|
|
||||||
|
|
||||||
while let Some(b) = self.stack.pop() {
|
|
||||||
let b = b as usize;
|
|
||||||
|
|
||||||
if self.seen_blocks[b] == stamp {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if b == block {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.seen_blocks[b] = stamp;
|
|
||||||
self.stack.extend(f.blocks[b].succ.iter().map(|&s| s as u32));
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `(value, item)` pairs, as one array grouped by value: the items of
|
|
||||||
/// value `v` are `items[starts[v]..starts[v + 1]]`
|
|
||||||
fn ranges<T>(n: usize, pairs: impl Iterator<Item = (usize, T)>) -> (Vec<T>, Vec<u32>) {
|
|
||||||
let mut pairs: Vec<(usize, T)> = pairs.collect();
|
|
||||||
pairs.sort_by_key(|(v, _)| *v);
|
|
||||||
|
|
||||||
let mut starts = vec![0u32; n + 1];
|
|
||||||
|
|
||||||
for (v, _) in &pairs {
|
|
||||||
starts[v + 1] += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for v in 0..n {
|
|
||||||
starts[v + 1] += starts[v];
|
|
||||||
}
|
|
||||||
|
|
||||||
(pairs.into_iter().map(|(_, item)| item).collect(), starts)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every instr of `f`, with its block: a block's own instrs, then its postfix
|
|
||||||
/// copies
|
|
||||||
fn instrs(f: &RegFn) -> impl Iterator<Item = (usize, &RegInstr)> {
|
|
||||||
f.blocks.iter().enumerate().flat_map(move |(b, block)| {
|
|
||||||
let code = f.code[block.start..block.end].iter();
|
|
||||||
code.chain(&block.postfix).map(move |instr| (b, instr))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+258
-538
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,256 @@
|
|||||||
|
//! Cells: the types of objects, which every holder of an object shares (see
|
||||||
|
//! `docs/model.md`, section 3)
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cell::{Ref, RefCell},
|
||||||
|
fmt,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
libqjs::parse::AtomRef,
|
||||||
|
native::ProtoOf,
|
||||||
|
transform::{BOX_FIELD, PROTO_FIELD},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::types::{Type, join};
|
||||||
|
|
||||||
|
pub const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
|
||||||
|
|
||||||
|
/// The prototype of each `ProtoOf`, as the stdlib links it
|
||||||
|
pub type Protos = [Type; ProtoOf::ALL.len()];
|
||||||
|
|
||||||
|
/// The code a fn object runs
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum Code {
|
||||||
|
Fn(usize),
|
||||||
|
Native(usize),
|
||||||
|
ProtoLink(ProtoOf),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the objects of a cell are, besides objects with named fields
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ObjKind {
|
||||||
|
Plain,
|
||||||
|
Fn(Code), // a fn, which runs the code
|
||||||
|
Array { elems: Type }, // an array, whose elements are the values written to them, or holes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A field of the objects of a cell
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Field {
|
||||||
|
pub ty: Type,
|
||||||
|
pub defined: bool, // every object of the cell has it from the start (see `write()`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a cell knows about its objects
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CellInfo {
|
||||||
|
pub kind: ObjKind,
|
||||||
|
pub fields: Vec<(AtomRef, Field)>, // sorted by name
|
||||||
|
pub made: Option<(usize, usize)>, // the walk and the instr that made it. `None` for a built-in
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of an object. Every copy of an `Obj` type holds the same cell, so
|
||||||
|
/// a write through one holder is seen by all. A cell is its `Rc`: two cells
|
||||||
|
/// are equal only if they are one cell, and they are ordered by address
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Cell(Rc<RefCell<CellInfo>>);
|
||||||
|
|
||||||
|
impl Cell {
|
||||||
|
pub fn new(kind: ObjKind, made: Option<(usize, usize)>) -> Self {
|
||||||
|
Cell(Rc::new(RefCell::new(CellInfo { kind, fields: vec![], made })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(&self) -> Ref<'_, CellInfo> {
|
||||||
|
self.0.borrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The address of the cell, which is its identity
|
||||||
|
fn addr(&self) -> usize {
|
||||||
|
Rc::as_ptr(&self.0) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The code that its objects run, if they are fns
|
||||||
|
pub fn code(&self) -> Option<Code> {
|
||||||
|
match self.info().kind {
|
||||||
|
ObjKind::Fn(code) => Some(code),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_array(&self) -> bool {
|
||||||
|
matches!(self.info().kind, ObjKind::Array { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of its elements, for an array
|
||||||
|
pub fn elems(&self) -> Type {
|
||||||
|
match &self.info().kind {
|
||||||
|
ObjKind::Array { elems } => elems.clone(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn field(&self, name: AtomRef) -> Option<Field> {
|
||||||
|
let info = self.info();
|
||||||
|
let i = info.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?;
|
||||||
|
Some(info.fields[i].1.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether one of its objects can lack its own `field`. A box's value
|
||||||
|
/// never can: a read of a binding before its first write throws (its
|
||||||
|
/// TDZ), so a read that gives a value comes after a write (see transform)
|
||||||
|
pub fn can_be_absent(&self, field: AtomRef) -> bool {
|
||||||
|
field != BOX_FIELD && !self.field(field).is_some_and(|f| f.defined)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join `ty` into its `field`. A field is defined if a `define_field`
|
||||||
|
/// makes it: that is a write into an object before it goes anywhere (see
|
||||||
|
/// regify). A field that another write makes can be absent. An array's
|
||||||
|
/// `length` is not a field: a write of it sets the array's length.
|
||||||
|
/// Whether the cell changed
|
||||||
|
pub fn write(&self, field: AtomRef, ty: &Type, defined: bool) -> bool {
|
||||||
|
if field == LENGTH_FIELD && self.is_array() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The join is done before the borrow: `ty` can hold this cell
|
||||||
|
let old = self.field(field);
|
||||||
|
let new = join(old.as_ref().map_or(&Type::Unknown, |f| &f.ty), ty);
|
||||||
|
let fields = &mut self.0.borrow_mut().fields;
|
||||||
|
|
||||||
|
match fields.binary_search_by_key(&field, |(n, _)| *n) {
|
||||||
|
Ok(i) if fields[i].1.ty == new => false,
|
||||||
|
Ok(i) => {
|
||||||
|
fields[i].1.ty = new;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(i) => {
|
||||||
|
fields.insert(i, (field, Field { ty: new, defined }));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join `ty` into the type of its elements. Whether the cell changed
|
||||||
|
pub fn write_elem(&self, ty: &Type) -> bool {
|
||||||
|
let new = join(&self.elems(), ty);
|
||||||
|
|
||||||
|
match &mut self.0.borrow_mut().kind {
|
||||||
|
ObjKind::Array { elems } if *elems != new => {
|
||||||
|
*elems = new;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A read of an element of one of its arrays. An index can be a hole, or
|
||||||
|
/// past the end, which gives undefined
|
||||||
|
pub fn read_elem(&self) -> Type {
|
||||||
|
join(&self.elems(), &Type::Undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A read of `field` on one of its objects: its own field, then, where
|
||||||
|
/// that can be absent, the read on its prototype. At the end of the
|
||||||
|
/// chain, an absent field is undefined. An array's prototype is the one
|
||||||
|
/// that the stdlib links for arrays, in `protos`, and its `length` is not a
|
||||||
|
/// field
|
||||||
|
pub fn read(&self, field: AtomRef, protos: &Protos) -> Type {
|
||||||
|
self.read_chain(field, protos, &mut vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `read()`, on a chain that came through the cells `chain`. Objects of
|
||||||
|
/// one cell can be each other's prototypes, so a chain of cells can come
|
||||||
|
/// back to a cell, which a chain of objects cannot. The read on that cell
|
||||||
|
/// is already in the chain, so it gives nothing more
|
||||||
|
fn read_chain(&self, field: AtomRef, protos: &Protos, chain: &mut Vec<Cell>) -> Type {
|
||||||
|
let array = self.is_array();
|
||||||
|
|
||||||
|
if array && field == LENGTH_FIELD {
|
||||||
|
return Type::Int;
|
||||||
|
}
|
||||||
|
|
||||||
|
let own = self.field(field).map_or(Type::Unknown, |f| f.ty);
|
||||||
|
|
||||||
|
if !self.can_be_absent(field) || chain.contains(self) {
|
||||||
|
return own;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The chain ends at a prototype that is absent or not an object
|
||||||
|
let (proto, proto_absent) = match array {
|
||||||
|
true => (protos[ProtoOf::Array as usize].clone(), false),
|
||||||
|
false => (self.field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty), self.can_be_absent(PROTO_FIELD)),
|
||||||
|
};
|
||||||
|
let mut ty = own;
|
||||||
|
|
||||||
|
if proto_absent || proto == Type::Unknown {
|
||||||
|
ty = join(&ty, &Type::Undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.push(self.clone());
|
||||||
|
|
||||||
|
for member in proto.members() {
|
||||||
|
let read = match member {
|
||||||
|
Type::Obj(proto) => proto.read_chain(field, protos, chain),
|
||||||
|
_ => Type::Undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
ty = join(&ty, &read);
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.pop();
|
||||||
|
ty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A read of `field` on a value of type `ty`. A primitive reads its kind's
|
||||||
|
/// prototype in `protos`. Undefined and null have no fields: the read throws,
|
||||||
|
/// so it gives nothing. A sum reads on each of its members
|
||||||
|
pub fn read_value(ty: &Type, field: AtomRef, protos: &Protos) -> Type {
|
||||||
|
match ty {
|
||||||
|
Type::Obj(cell) => cell.read(field, protos),
|
||||||
|
Type::Sum(members) => {
|
||||||
|
let reads = members.iter().map(|member| read_value(member, field, protos));
|
||||||
|
reads.fold(Type::Unknown, |all, read| join(&all, &read))
|
||||||
|
}
|
||||||
|
ty => match ProtoOf::of(ty).map(|kind| &protos[kind as usize]) {
|
||||||
|
Some(Type::Unknown) | None => Type::Unknown,
|
||||||
|
Some(proto) => read_value(proto, field, protos),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Cell {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.addr() == other.addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Cell {}
|
||||||
|
|
||||||
|
impl PartialOrd for Cell {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Cell {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.addr().cmp(&other.addr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Cell {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.addr().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cell prints as its address only: its fields can hold the cell itself
|
||||||
|
impl fmt::Debug for Cell {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "cell {:#x}", self.addr())
|
||||||
|
}
|
||||||
|
}
|
||||||
+279
-299
@@ -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;
|
pub mod types;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
|
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
|
||||||
native::{self, ProtoOf},
|
native::{self, NATIVES, ProtoOf},
|
||||||
regify::RegFn,
|
regify::RegFn,
|
||||||
transform::{FIRST_ARG, THIS_ARG},
|
transform::{FIRST_ARG, THIS_ARG},
|
||||||
};
|
};
|
||||||
|
|
||||||
use sites::{Code, CtxId, LENGTH_FIELD, Place, Protos, SiteId, SiteTable};
|
use cells::{Cell, Code, LENGTH_FIELD, ObjKind, Protos, read_value};
|
||||||
use types::{BaseType, EMPTY_STRING, Type, is_function, loose_eq, strict_eq, test, truthiness, typeof_name};
|
use types::{BaseType, EMPTY_STRING, Type, is_function, join, loose_eq, strict_eq, test, truthiness, typeof_name};
|
||||||
|
|
||||||
|
/// The walk of one fn with one list of arg types
|
||||||
/// One walk of a fn with one tuple of arg types (see `docs/model.md`,
|
|
||||||
/// section 3.5)
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Variant {
|
pub struct Walk {
|
||||||
pub fn_id: usize,
|
pub fn_id: usize,
|
||||||
/// One for each of the fn's params (see `TypeCache::variant()`)
|
pub arg_types: Vec<Type>, // one for each param
|
||||||
pub arg_types: Vec<Type>,
|
cells: Vec<(usize, Cell)>, // the cells it made, by the instr that makes their objects
|
||||||
/// The context of the sites that it makes (see `SiteTable::context()`)
|
ret: Type, // what it returned in the rounds so far: a recursive call gives that
|
||||||
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,
|
|
||||||
walking: bool,
|
walking: bool,
|
||||||
|
pub result: Option<InferResult>, // the walk in the current round, once it is done
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InferResult {
|
pub struct InferResult {
|
||||||
pub val_types: Vec<Type>,
|
pub val_types: Vec<Type>,
|
||||||
/// The type each instr gives, before it is joined into its dst
|
pub instr_types: Vec<Type>, // the type each instr gives, before it is joined into its dst
|
||||||
pub instr_types: Vec<Type>,
|
|
||||||
pub ret_type: Type,
|
pub ret_type: Type,
|
||||||
/// The blocks that can run
|
pub reachable: Vec<bool>, // the blocks that can run
|
||||||
pub reachable: Vec<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The walks of the program, by their keys
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TypeCache {
|
pub struct TypeCache {
|
||||||
pub variants: Vec<Variant>,
|
pub walks: Vec<Walk>,
|
||||||
/// Each fn's variants, by their arg types
|
keys: HashMap<(usize, Vec<Type>), usize>, // the walk of each fn and list of arg types
|
||||||
by_args: Vec<HashMap<Vec<Type>, usize>>,
|
builtins: Vec<Cell>, // the cells of the natives, then of the proto links
|
||||||
/// Scratch for a call's arg types
|
pub protos: Protos, // the prototype of each `ProtoOf`, as the stdlib links it
|
||||||
key: Vec<Type>,
|
version: usize, // goes up on every change of a cell, a walk's return or a proto, and on every new cell or walk
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypeCache {
|
impl TypeCache {
|
||||||
/// The variant for a call of `fn_id` with args of these types, made if it
|
/// The key of a call of `fn_id` with args of these types: the fn, and the
|
||||||
/// is new. It is keyed by the fn's params: an arg it does not pass is
|
/// type of each of its params. An arg it does not pass is undefined, and
|
||||||
/// undefined, and an arg it passes beyond them is never read. Only an
|
/// an arg it passes beyond them is never read. Only an arg's kind crosses
|
||||||
/// arg's kind crosses a call, not a known value, else every literal passed
|
/// a call, not a known value, else every literal passed would make a new
|
||||||
/// would make a new variant
|
/// walk. An object is its cell
|
||||||
pub fn variant(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
|
fn key(fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> (usize, Vec<Type>) {
|
||||||
let site_table = &mut self.site_table;
|
let params = (0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::Undefined, Type::widen));
|
||||||
self.key.clear();
|
(fn_id, params.collect())
|
||||||
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 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 {
|
pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
|
||||||
let site_table = &self.site_table;
|
self.keys[&Self::key(fn_id, arg_types, fns)]
|
||||||
let key: Vec<Type> = params(fn_id, arg_types, fns).map(|ty| site_table.widened(&ty)).collect();
|
|
||||||
self.by_args[fn_id][&key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The join of `f` over the members of `ty` (see `SiteTable::members()`).
|
/// The cell of a built-in fn
|
||||||
/// The lists of sums only grow, so a member stays where it is while `f`
|
pub fn builtin(&self, code: Code) -> &Cell {
|
||||||
/// adds sums
|
match code {
|
||||||
fn map(&mut self, ty: Type, mut f: impl FnMut(&mut Self, Type) -> Type) -> Type {
|
Code::Native(id) => &self.builtins[id],
|
||||||
let mut out = Type::Unknown;
|
Code::ProtoLink(kind) => &self.builtins[NATIVES.len() + kind as usize],
|
||||||
|
Code::Fn(_) => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i in 0..self.site_table.members(&ty).len() {
|
/// The cells of the built-in fns
|
||||||
let member = self.site_table.members(&ty)[i];
|
pub fn builtins(&self) -> &[Cell] {
|
||||||
let ty = f(self, member);
|
&self.builtins
|
||||||
out = self.site_table.join(&out, &ty);
|
}
|
||||||
|
|
||||||
|
/// 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`
|
/// Count a change, if there was one
|
||||||
fn map2(&mut self, a: Type, b: Type, mut f: impl FnMut(&mut Self, Type, Type) -> Type) -> Type {
|
fn changed(&mut self, changed: bool) {
|
||||||
self.map(a, |cache, a| cache.map(b, |cache, b| f(cache, a, b)))
|
self.version += changed as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What `f` gives for every member of `ty`, if all members give the same
|
/// Infer a call of `fn_id`, walking it unless the walk of its key is done
|
||||||
fn agree<T: PartialEq>(&self, ty: &Type, f: impl Fn(&Type) -> Option<T>) -> Option<T> {
|
/// in this round. Gives what it returns
|
||||||
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
|
|
||||||
fn call(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> Type {
|
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] {
|
let w = match self.keys.get(&key) {
|
||||||
Variant { result: Some(res), .. } => return res.ret_type,
|
Some(&w) => w,
|
||||||
Variant { walking: true, ret, .. } => return *ret,
|
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;
|
self.walks[w].walking = true;
|
||||||
let res = walk_fn(v, fns, self);
|
let res = walk_fn(w, fns, self);
|
||||||
let ret = res.ret_type;
|
let ret = res.ret_type.clone();
|
||||||
|
|
||||||
let last = self.variants[v].ret;
|
let last = &self.walks[w].ret;
|
||||||
let joined = self.site_table.join(&last, &ret);
|
let joined = join(last, &ret);
|
||||||
self.changes += (joined != last) as usize;
|
let grew = joined != *last;
|
||||||
|
self.changed(grew);
|
||||||
|
|
||||||
let variant = &mut self.variants[v];
|
let walk = &mut self.walks[w];
|
||||||
variant.walking = false;
|
walk.walking = false;
|
||||||
variant.ret = joined;
|
walk.ret = joined;
|
||||||
variant.result = Some(res);
|
walk.result = Some(res);
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of each of fn `fn_id`'s params, for a call with args of these
|
/// The join of `f` over the members of `ty`
|
||||||
/// types, before known values are dropped (see `TypeCache::variant()`)
|
fn map(ty: &Type, mut f: impl FnMut(&Type) -> Type) -> Type {
|
||||||
fn params<'a>(fn_id: usize, arg_types: &'a [Type], fns: &[RegFn]) -> impl Iterator<Item = Type> + 'a {
|
ty.members().iter().fold(Type::Unknown, |all, member| join(&all, &f(member)))
|
||||||
(0..fns[fn_id].arg_count).map(|i| arg_types.get(i).copied().unwrap_or(Type::Undefined))
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// 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 {
|
match code {
|
||||||
Code::Fn(fn_id) => cache.call(fn_id, arg_types, fns),
|
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) => {
|
Code::ProtoLink(kind) => {
|
||||||
let linked = cache.protos[kind as usize];
|
let linked = &cache.protos[kind as usize];
|
||||||
let joined = cache.site_table.join(&linked, args.first().unwrap_or(&Type::Undefined));
|
let joined = join(linked, args.first().unwrap_or(&Type::Undefined));
|
||||||
cache.changes += (joined != linked) as usize;
|
cache.changed(joined != *linked);
|
||||||
cache.protos[kind as usize] = joined;
|
cache.protos[kind as usize] = joined;
|
||||||
Type::Undefined
|
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,
|
/// An array index must be a number. Any other key is a computed field name,
|
||||||
/// which is not supported
|
/// which is not supported
|
||||||
fn check_index(cache: &TypeCache, index: &Type) {
|
fn check_index(index: &Type) {
|
||||||
for member in cache.site_table.members(index) {
|
for member in index.members() {
|
||||||
if !matches!(member, Type::Int | Type::Float) {
|
if !matches!(member, Type::Int | Type::Float) {
|
||||||
unimplemented!("`a[k]` with a key of type {member:?}: computed field names");
|
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`
|
/// Walk the program from the entry, in rounds, until a round changes
|
||||||
/// args, runs for a fn object of `site`
|
/// nothing. A walk reads cells, protos and the returns of other walks, which
|
||||||
pub fn call_any_variant(site: SiteId, n_args: usize) -> Vec<Type> {
|
/// walks after it can still add to. All of them only grow, so the rounds end.
|
||||||
let mut arg_types = vec![Type::Any; FIRST_ARG as usize + n_args];
|
/// The results are those of the last round
|
||||||
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
|
|
||||||
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
|
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
|
||||||
let mut cache = TypeCache {
|
let mut cache = TypeCache {
|
||||||
variants: vec![],
|
walks: vec![],
|
||||||
by_args: fns.iter().map(|_| HashMap::new()).collect(),
|
keys: HashMap::new(),
|
||||||
key: vec![],
|
builtins: vec![],
|
||||||
site_table: SiteTable::new(fns),
|
protos: std::array::from_fn(|_| Type::Unknown),
|
||||||
protos: [Type::Unknown; ProtoOf::ALL.len()],
|
version: 0,
|
||||||
changes: 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 {
|
loop {
|
||||||
let before = (cache.site_table.version, cache.changes);
|
let version = cache.version;
|
||||||
cache.variants.iter_mut().for_each(|v| v.result = None);
|
cache.walks.iter_mut().for_each(|w| w.result = None);
|
||||||
|
|
||||||
// The synthetic program entry is appended last by `flatten`. It is
|
// The synthetic program entry is appended last by `flatten`. It is
|
||||||
// called by the host with no args
|
// called by the host with no args
|
||||||
cache.call(fns.len() - 1, &[], fns);
|
cache.call(fns.len() - 1, &[], fns);
|
||||||
|
|
||||||
if before == (cache.site_table.version, cache.changes) {
|
if cache.version == version {
|
||||||
return cache;
|
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::*;
|
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 = ®_fns[fn_id];
|
let f = ®_fns[fn_id];
|
||||||
let mut val_types = vec![Type::Unknown; f.val_count];
|
let mut val_types = vec![Type::Unknown; f.val_count];
|
||||||
let mut instr_types = vec![Type::Unknown; f.code.len()];
|
let mut instr_types = vec![Type::Unknown; f.code.len()];
|
||||||
let mut ret_type = Type::Unknown;
|
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)
|
// arg `i` is value id `var_count + i` (see regify)
|
||||||
// Init them with the arg types passed in
|
for (i, ty) in cache.walks[w].arg_types.iter().enumerate() {
|
||||||
for (i, ty) in cache.variants[variant].arg_types.iter().enumerate() {
|
val_types[f.var_count + i] = ty.clone();
|
||||||
val_types[f.var_count + i] = *ty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The blocks that can run, given what the types say about each condition
|
// 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![];
|
let mut arg_types: Vec<Type> = vec![];
|
||||||
|
|
||||||
// Walk instrs till nothing changes. A return only changes `ret_type` once
|
// 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
|
// its block is reachable, and a field read only gives more once a cell
|
||||||
// table changes, so the values, the blocks and the site table are enough
|
// changes, so the values, the blocks and the version are enough to watch
|
||||||
// to watch
|
|
||||||
loop {
|
loop {
|
||||||
let version = cache.site_table.version;
|
let version = cache.version;
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
|
|
||||||
for (block_idx, block) in f.blocks.iter().enumerate() {
|
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);
|
let code = code.iter().chain(&block.postfix);
|
||||||
|
|
||||||
for (offset, instr) in code.enumerate() {
|
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
|
// A rule with an `Unknown` operand gives `Unknown`: no value has
|
||||||
// reached the instr yet
|
// reached the instr yet
|
||||||
let waits = instr.args.iter().any(|&v| val_types[v] == Type::Unknown);
|
if instr.args.iter().any(|&v| val_types[v] == Type::Unknown) {
|
||||||
|
|
||||||
if (instr.dsts.is_empty() && !is_ret) || waits {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The instr's index in the code (postfix copies have none)
|
// The instr's index in the code (postfix copies have none)
|
||||||
let idx = block.start + offset;
|
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 {
|
let ty = match instr.op {
|
||||||
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
|
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
|
||||||
@@ -324,8 +334,8 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
|
|||||||
JsValue::BoolTrue => Type::Bool(Some(true)),
|
JsValue::BoolTrue => Type::Bool(Some(true)),
|
||||||
JsValue::BoolFalse => Type::Bool(Some(false)),
|
JsValue::BoolFalse => Type::Bool(Some(false)),
|
||||||
JsValue::Undefined => Type::Undefined,
|
JsValue::Undefined => Type::Undefined,
|
||||||
JsValue::NativeFn(id) => cache.site_table.create(cache.site_table.builtin(Place::Native(id))),
|
JsValue::NativeFn(id) => Type::Obj(cache.builtin(Code::Native(id)).clone()),
|
||||||
JsValue::ProtoLink(kind) => cache.site_table.create(cache.site_table.builtin(Place::ProtoLink(kind))),
|
JsValue::ProtoLink(kind) => Type::Obj(cache.builtin(Code::ProtoLink(kind)).clone()),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -335,18 +345,25 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
|
|||||||
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
|
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
|
||||||
// `+` concatenates if either side becomes a string, which
|
// `+` concatenates if either side becomes a string, which
|
||||||
// a string or an object does. Else it adds numbers
|
// a string or an object does. Else it adds numbers
|
||||||
Add => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) {
|
Add => {
|
||||||
(Type::Any, _) | (_, Type::Any) => Type::Any,
|
let r = arg(0);
|
||||||
(l, r) if [l, r].iter().any(|t| matches!(t, Type::Str(_) | Type::Obj(_))) => Type::Str(None),
|
|
||||||
(Type::Int, Type::Int) => Type::Int,
|
map(&arg(1), |l| map(&r, |r| match (l, r) {
|
||||||
_ => Type::Float,
|
(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
|
// Any other operator converts its operands to numbers
|
||||||
Sub | Mul | Mod | Pow => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) {
|
Sub | Mul | Mod | Pow => {
|
||||||
(Type::Int, Type::Int) => Type::Int,
|
let r = arg(0);
|
||||||
_ => Type::Float,
|
|
||||||
}),
|
map(&arg(1), |l| map(&r, |r| match (l, r) {
|
||||||
Neg | Plus | Inc | Dec | PostInc | PostDec => cache.map(arg(0), |_, t| match t {
|
(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::Int => Type::Int,
|
||||||
_ => Type::Float,
|
_ => 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
|
// A known value lets infer skip the branch that cannot run
|
||||||
// (see `reachable` below)
|
// (see `reachable` below)
|
||||||
StrictEq | StrictNeq | Eq | Neq => {
|
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 {
|
let eq = match instr.op {
|
||||||
StrictEq | StrictNeq => cache.agree2(&a, &b, strict_eq),
|
StrictEq | StrictNeq => agree2(&arg(0), &arg(1), strict_eq),
|
||||||
_ => cache.agree2(&a, &b, loose_eq),
|
_ => agree2(&arg(0), &arg(1), loose_eq),
|
||||||
};
|
};
|
||||||
|
|
||||||
Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq)))
|
Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq)))
|
||||||
}
|
}
|
||||||
Lnot => Type::Bool(cache.agree(&arg(0), truthiness).map(|t| !t)),
|
Lnot => Type::Bool(agree(&arg(0), truthiness).map(|t| !t)),
|
||||||
Typeof => match cache.agree(&arg(0), |t| typeof_name(t, &cache.site_table)) {
|
Typeof => match agree(&arg(0), typeof_name) {
|
||||||
Some(name) => Type::Str(Some(AtomRef::Predefined(name))),
|
Some(name) => Type::Str(Some(AtomRef::Predefined(name))),
|
||||||
None => Type::Str(None),
|
None => Type::Str(None),
|
||||||
},
|
},
|
||||||
IsUndefined | TypeofIsUndefined => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined]))),
|
IsUndefined | TypeofIsUndefined => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Undefined]))),
|
||||||
IsNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Null]))),
|
IsNull => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Null]))),
|
||||||
IsUndefinedOrNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))),
|
IsUndefinedOrNull => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))),
|
||||||
TypeofIsFunction => Type::Bool(cache.agree(&arg(0), |t| is_function(t, &cache.site_table))),
|
TypeofIsFunction => Type::Bool(agree(&arg(0), is_function)),
|
||||||
|
|
||||||
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
|
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
|
||||||
SetLoc | SetName => arg(0),
|
SetLoc | SetName => arg(0),
|
||||||
|
|
||||||
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
|
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
|
||||||
Object | Fclosure | Fclosure8 => {
|
Object => Type::Obj(cell_here(cache, ObjKind::Plain)),
|
||||||
let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx);
|
Fclosure | Fclosure8 => {
|
||||||
cache.site_table.create(site)
|
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 => {
|
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 {
|
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:
|
// `a[i]` and `a[i] = x`, on arrays, and `s[i]` on strings:
|
||||||
// an index is a number. Other objects need computed field
|
// 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 => {
|
GetArrayEl => {
|
||||||
let index = arg(0);
|
let index = arg(0);
|
||||||
|
|
||||||
cache.map(arg(1), |cache, obj| match obj {
|
map(&arg(1), |obj| match obj {
|
||||||
Type::Obj(site) if cache.site_table.site(site).is_array() => {
|
Type::Obj(cell) if cell.is_array() => {
|
||||||
check_index(cache, &index);
|
check_index(&index);
|
||||||
cache.site_table.read_elem(site)
|
cell.read_elem()
|
||||||
}
|
}
|
||||||
Type::Str(_) => {
|
Type::Str(_) => {
|
||||||
check_index(cache, &index);
|
check_index(&index);
|
||||||
cache.site_table.join(&Type::Str(None), &Type::Undefined)
|
join(&Type::Str(None), &Type::Undefined)
|
||||||
}
|
}
|
||||||
Type::Undefined | Type::Null => Type::Unknown,
|
Type::Undefined | Type::Null => Type::Unknown,
|
||||||
obj => unimplemented!("`o[k]` on {obj:?}: computed field names"),
|
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 => {
|
PutArrayEl => {
|
||||||
let (value, index) = (arg(0), arg(1));
|
let (value, index) = (arg(0), arg(1));
|
||||||
|
|
||||||
cache.map(arg(2), |cache, obj| {
|
for obj in arg(2).members() {
|
||||||
match obj {
|
match obj {
|
||||||
Type::Obj(site) if cache.site_table.site(site).is_array() => {
|
Type::Obj(cell) if cell.is_array() => {
|
||||||
check_index(cache, &index);
|
check_index(&index);
|
||||||
cache.site_table.write_elem(site, &value);
|
let grew = cell.write_elem(&value);
|
||||||
cache.site_table.mark(site, |s| &mut s.written);
|
cache.changed(grew);
|
||||||
}
|
}
|
||||||
Type::Undefined | Type::Null | Type::Str(_) => {}
|
Type::Undefined | Type::Null | Type::Str(_) => {}
|
||||||
obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"),
|
obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Type::Unknown
|
Type::Unknown
|
||||||
});
|
|
||||||
|
|
||||||
arg(2)
|
|
||||||
}
|
}
|
||||||
// A key stays as it is: a number is an index
|
// A key stays as it is: a number is an index
|
||||||
ToPropkey => arg(0),
|
ToPropkey => arg(0),
|
||||||
// The write goes into the fields of the object's site, which
|
// The write goes into the object's cell, which every holder
|
||||||
// every value that holds an object of the site sees. A write
|
// of the object sees. A primitive has no fields to write,
|
||||||
// on `Any` can reach any object in an `Any`. A primitive has
|
// and undefined and null throw
|
||||||
// no fields to write, and undefined and null throw
|
|
||||||
DefineField | PutField => {
|
DefineField | PutField => {
|
||||||
let (field, value) = (instr.imm.atom(), arg(0));
|
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| {
|
for obj in arg(1).members() {
|
||||||
match obj {
|
if let Type::Obj(cell) = obj {
|
||||||
Type::Obj(site) => {
|
let grew = cell.write(field, &value, instr.op == DefineField);
|
||||||
cache.site_table.write(site, field, &value);
|
cache.changed(grew);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Type::Unknown
|
Type::Unknown
|
||||||
});
|
|
||||||
|
|
||||||
arg(1)
|
|
||||||
}
|
}
|
||||||
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
|
// A string's length is its own. Anything else reads a
|
||||||
// `length` field
|
// `length` field
|
||||||
GetLength => cache.map(arg(0), |cache, obj| match obj {
|
GetLength => map(&arg(0), |obj| match obj {
|
||||||
Type::Str(_) => Type::Int,
|
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 ──────────────────────────────────────────────────────
|
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
|
||||||
// Operands are the callee's params: the callee (its env),
|
// Operands are the callee's params: the callee (its env),
|
||||||
// `this`, then the args (see regify). A callee that is not
|
// `this`, then the args (see regify). A callee that is not
|
||||||
// a fn throws, so it gives nothing
|
// a fn throws, so it gives nothing. A call of each member
|
||||||
// The callee's env is the fn object itself, so a call of
|
// of a sum passes that member as the env
|
||||||
// each member of a sum passes that member as the env
|
|
||||||
Call | CallMethod | CallConstructor => {
|
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.clear();
|
||||||
arg_types.extend(instr.args.iter().map(|&v| val_types[v]));
|
arg_types.extend(instr.args.iter().map(|&v| val_types[v].clone()));
|
||||||
arg_types[0] = callee;
|
arg_types[0] = callee.clone();
|
||||||
|
call_code(cache, code, &arg_types, reg_fns)
|
||||||
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,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// `new` gives what the constructor returns if that is an
|
// `new` gives what the constructor returns if that is an
|
||||||
// object, else the new object itself, `this`
|
// object, else the new object itself, `this`
|
||||||
match instr.op {
|
match instr.op {
|
||||||
CallConstructor => cache.map(ret, |cache, ret| match ret {
|
CallConstructor => map(&ret, |ret| match ret {
|
||||||
Type::Obj(_) => ret,
|
Type::Obj(_) => ret.clone(),
|
||||||
Type::Any => cache.site_table.join(&ret, &this),
|
_ => arg(THIS_ARG as usize),
|
||||||
_ => this,
|
|
||||||
}),
|
}),
|
||||||
_ => ret,
|
_ => 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
|
// Postfix copies come after the block's own instrs, and have no
|
||||||
// entry: a copy gives the type of what it copies
|
// entry: a copy gives the type of what it copies
|
||||||
if idx < block.end {
|
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 {
|
if matches!(instr.op, Return | ReturnAsync | ReturnUndef) {
|
||||||
ret_type = cache.site_table.join(&ret_type, &ty);
|
ret_type = join(&ret_type, &ty);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for &dst in &instr.dsts {
|
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];
|
changed |= joined != val_types[dst];
|
||||||
val_types[dst] = joined;
|
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)
|
// (A block that only carries copies on an edge has no instrs)
|
||||||
let jumps = match f.code[block.start..block.end].last() {
|
let jumps = match f.code[block.start..block.end].last() {
|
||||||
Some(last) if matches!(last.op, IfFalse | IfFalse8 | IfTrue | IfTrue8) => {
|
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,
|
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,
|
_ => None,
|
||||||
@@ -580,11 +559,13 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
|
|||||||
}
|
}
|
||||||
} // block
|
} // block
|
||||||
|
|
||||||
if !changed && cache.site_table.version == version {
|
if !changed && cache.version == version {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} // loop
|
} // loop
|
||||||
|
|
||||||
|
cache.walks[w].cells = cells;
|
||||||
|
|
||||||
InferResult {
|
InferResult {
|
||||||
val_types,
|
val_types,
|
||||||
instr_types,
|
instr_types,
|
||||||
@@ -592,4 +573,3 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
|
|||||||
reachable,
|
reachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,498 +0,0 @@
|
|||||||
//! What infer knows about the objects of each site: the type of each field,
|
|
||||||
//! and flags (see `docs/model.md`, section 3.3)
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
|
|
||||||
native::{NATIVES, ProtoOf},
|
|
||||||
regify::RegFn,
|
|
||||||
transform::{BOX_FIELD, PROTO_FIELD},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::types::Type;
|
|
||||||
|
|
||||||
pub const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
|
|
||||||
|
|
||||||
/// The prototype of each `ProtoOf`, as the stdlib links it
|
|
||||||
pub type Protos = [Type; ProtoOf::ALL.len()];
|
|
||||||
|
|
||||||
/// Where an object is made: the op that creates it, or a built-in fn. Places
|
|
||||||
/// are numbered in this order (see `SiteTable::new()`)
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum Place {
|
|
||||||
Op { fn_id: usize, instr: usize },
|
|
||||||
Native(usize),
|
|
||||||
ProtoLink(ProtoOf),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of a place: its index in `SiteTable::places`
|
|
||||||
pub type PlaceId = u32;
|
|
||||||
|
|
||||||
/// The number of a context: its index in `SiteTable::contexts`
|
|
||||||
pub type CtxId = u32;
|
|
||||||
|
|
||||||
/// The number of a site: its index in `SiteTable::sites`
|
|
||||||
pub type SiteId = u32;
|
|
||||||
|
|
||||||
/// The code a fn object runs
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum Code {
|
|
||||||
Fn(usize),
|
|
||||||
Native(usize),
|
|
||||||
ProtoLink(ProtoOf),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A field of the objects of a site
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Field {
|
|
||||||
pub ty: Type,
|
|
||||||
pub defined: bool, // every object of the site gets it when it is made, so it is never absent
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What the objects of a site are, besides objects with named fields
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum ObjKind {
|
|
||||||
Plain,
|
|
||||||
Fn(Code), // a fn, which runs the code
|
|
||||||
Array { elems: Type }, // an array, whose elements are the values written to them, or holes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A place, and what every site of it starts with
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PlaceInfo {
|
|
||||||
pub place: Place,
|
|
||||||
kind: ObjKind, // the kind of its sites, before any element is written
|
|
||||||
defined: Vec<AtomRef>, // the fields that its creation writes write, sorted
|
|
||||||
sites: Vec<(CtxId, SiteId)>, // its sites so far, by context
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A part of a context (see `SiteTable::context()`)
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum CtxPart {
|
|
||||||
Member(Type), // a member of a param's type that is not an object
|
|
||||||
Place(PlaceId), // an object member, by the place that makes it
|
|
||||||
End, // the end of a param's type
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What infer knows about the objects of one site
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SiteInfo {
|
|
||||||
pub place: PlaceId,
|
|
||||||
pub ctx: CtxId,
|
|
||||||
pub kind: ObjKind,
|
|
||||||
pub fields: Vec<(AtomRef, Field)>, // sorted by name
|
|
||||||
pub written: bool, // a field of one of its objects is written after the object is made
|
|
||||||
pub compared: bool, // one of its objects is in an identity test with an object
|
|
||||||
pub in_any: bool, // one of its objects went into an `Any` (see `SiteTable::to_any()`)
|
|
||||||
pub live: bool, // an op that makes one of its objects has run (see infer)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SiteInfo {
|
|
||||||
/// The code that its objects run, if they are fns
|
|
||||||
pub fn code(&self) -> Option<Code> {
|
|
||||||
match self.kind {
|
|
||||||
ObjKind::Fn(code) => Some(code),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_array(&self) -> bool {
|
|
||||||
matches!(self.kind, ObjKind::Array { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn field(&self, name: AtomRef) -> Option<&Field> {
|
|
||||||
let i = self.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?;
|
|
||||||
Some(&self.fields[i].1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a copy of one of its objects cannot be told from the object:
|
|
||||||
/// no write after creation can show the copy, and no identity test can
|
|
||||||
/// (see `docs/model.md`, section 5). An object in an `Any` value can be
|
|
||||||
/// written and compared by code that does not know its site
|
|
||||||
pub fn is_value(&self) -> bool {
|
|
||||||
!self.written && !self.compared && !self.in_any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every place, every site, and every sum
|
|
||||||
///
|
|
||||||
/// A site is a place and a context: the objects that one place makes in the
|
|
||||||
/// variants that have one context. So the objects follow the monomorphization
|
|
||||||
/// of the fns that make them (see `docs/model.md`, section 3.3)
|
|
||||||
///
|
|
||||||
/// The sums are stored flat: the members of sum `i` are
|
|
||||||
/// `sum_members[sum_starts[i]..sum_starts[i + 1]]`. So a search over all sums
|
|
||||||
/// reads one array in order. The contexts are stored the same way
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SiteTable {
|
|
||||||
pub places: Vec<PlaceInfo>, // place id -> where it is, sorted by `place`
|
|
||||||
pub sites: Vec<SiteInfo>, // site id -> what infer knows about its objects
|
|
||||||
ctx_parts: Vec<CtxPart>, // context id -> its parts, see above
|
|
||||||
ctx_starts: Vec<u32>,
|
|
||||||
pub(super) sum_members: Vec<Type>, // sum id -> its members, see above
|
|
||||||
pub(super) sum_starts: Vec<u32>,
|
|
||||||
pub version: usize, // goes up on every change, so a walk can tell that a read may give more
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SiteTable {
|
|
||||||
/// One place for each creating op, in the order of fns and instrs, then
|
|
||||||
/// the natives, then the proto links. A place starts its sites with the
|
|
||||||
/// fields that its creation writes write: every `define_field` writes into
|
|
||||||
/// the object that a creating op gives (see regify). The built-in fns have
|
|
||||||
/// one site each, in the empty context (context 0)
|
|
||||||
pub fn new(fns: &[RegFn]) -> Self {
|
|
||||||
let mut places: Vec<PlaceInfo> = vec![];
|
|
||||||
let place = |place, kind| PlaceInfo { place, kind, defined: vec![], sites: vec![] };
|
|
||||||
|
|
||||||
// The place that made each value of the fn, if a creating op gave it
|
|
||||||
let mut created: Vec<Option<usize>> = vec![];
|
|
||||||
|
|
||||||
for (fn_id, f) in fns.iter().enumerate() {
|
|
||||||
created.clear();
|
|
||||||
created.resize(f.val_count, None);
|
|
||||||
|
|
||||||
for (instr, op) in f.code.iter().enumerate() {
|
|
||||||
match op.op {
|
|
||||||
OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8 | OpCode::ArrayFrom => {
|
|
||||||
let kind = match op.op {
|
|
||||||
OpCode::Object => ObjKind::Plain,
|
|
||||||
OpCode::ArrayFrom => ObjKind::Array { elems: Type::Unknown },
|
|
||||||
_ => match f.cpool[op.imm.const_idx()] {
|
|
||||||
JsValue::FnById(id) => ObjKind::Fn(Code::Fn(id)),
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
created[op.dsts[0]] = Some(places.len());
|
|
||||||
places.push(place(Place::Op { fn_id, instr }, kind));
|
|
||||||
}
|
|
||||||
OpCode::DefineField => {
|
|
||||||
let defined = &mut places[created[op.args[1]].unwrap()].defined;
|
|
||||||
|
|
||||||
if let Err(i) = defined.binary_search(&op.imm.atom()) {
|
|
||||||
defined.insert(i, op.imm.atom());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
places.extend((0..NATIVES.len()).map(|id| place(Place::Native(id), ObjKind::Fn(Code::Native(id)))));
|
|
||||||
places.extend(ProtoOf::ALL.map(|kind| place(Place::ProtoLink(kind), ObjKind::Fn(Code::ProtoLink(kind)))));
|
|
||||||
|
|
||||||
let mut table = Self {
|
|
||||||
places,
|
|
||||||
sites: vec![],
|
|
||||||
ctx_parts: vec![],
|
|
||||||
ctx_starts: vec![0],
|
|
||||||
sum_members: vec![],
|
|
||||||
sum_starts: vec![0],
|
|
||||||
version: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let empty = table.context(&[]);
|
|
||||||
|
|
||||||
for at in table.places.len() - NATIVES.len() - ProtoOf::ALL.len()..table.places.len() {
|
|
||||||
table.site_at(at as PlaceId, empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
table
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The place `place`
|
|
||||||
pub fn place_id(&self, place: Place) -> PlaceId {
|
|
||||||
self.places.binary_search_by_key(&place, |p| p.place).unwrap() as PlaceId
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The site of a built-in fn
|
|
||||||
pub fn builtin(&self, place: Place) -> SiteId {
|
|
||||||
self.places[self.place_id(place) as usize].sites[0].1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The context of a variant whose params have these types: the types,
|
|
||||||
/// where each object member is named only by its place. So there are
|
|
||||||
/// finitely many contexts, although a site's objects can be in a param of
|
|
||||||
/// a variant whose own sites are new (recursion)
|
|
||||||
pub fn context(&mut self, params: &[Type]) -> CtxId {
|
|
||||||
let start = self.ctx_parts.len();
|
|
||||||
|
|
||||||
for ty in params {
|
|
||||||
let members_start = self.ctx_parts.len();
|
|
||||||
|
|
||||||
for i in 0..self.members(ty).len() {
|
|
||||||
let part = match self.members(ty)[i] {
|
|
||||||
Type::Obj(site) => CtxPart::Place(self.sites[site as usize].place),
|
|
||||||
member => CtxPart::Member(member),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self.ctx_parts[members_start..].contains(&part) {
|
|
||||||
self.ctx_parts.push(part);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.ctx_parts.push(CtxPart::End);
|
|
||||||
}
|
|
||||||
|
|
||||||
let new = &self.ctx_parts[start..];
|
|
||||||
let starts = self.ctx_starts.windows(2);
|
|
||||||
|
|
||||||
match starts.map(|w| &self.ctx_parts[w[0] as usize..w[1] as usize]).position(|ctx| ctx == new) {
|
|
||||||
Some(id) => {
|
|
||||||
self.ctx_parts.truncate(start);
|
|
||||||
id as CtxId
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.ctx_starts.push(self.ctx_parts.len() as u32);
|
|
||||||
(self.ctx_starts.len() - 2) as CtxId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The site of `place` in context `ctx`, made if it is new
|
|
||||||
pub fn site_at(&mut self, place: PlaceId, ctx: CtxId) -> SiteId {
|
|
||||||
if let Some(site) = self.find_site(place, ctx) {
|
|
||||||
return site;
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = self.sites.len() as SiteId;
|
|
||||||
let info = &mut self.places[place as usize];
|
|
||||||
let fields = info.defined.iter().map(|&n| (n, Field { ty: Type::Unknown, defined: true })).collect();
|
|
||||||
|
|
||||||
info.sites.push((ctx, id));
|
|
||||||
self.sites.push(SiteInfo {
|
|
||||||
place,
|
|
||||||
ctx,
|
|
||||||
kind: info.kind,
|
|
||||||
fields,
|
|
||||||
written: false,
|
|
||||||
compared: false,
|
|
||||||
in_any: false,
|
|
||||||
live: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
self.version += 1;
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The site of `place` in context `ctx`, if infer made it
|
|
||||||
pub fn find_site(&self, place: PlaceId, ctx: CtxId) -> Option<SiteId> {
|
|
||||||
let sites = &self.places[place as usize].sites;
|
|
||||||
sites.iter().find(|(c, _)| *c == ctx).map(|(_, site)| *site)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn site(&self, site: SiteId) -> &SiteInfo {
|
|
||||||
&self.sites[site as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An object of the site, which a creating op gives
|
|
||||||
pub fn create(&mut self, site: SiteId) -> Type {
|
|
||||||
self.mark(site, |s| &mut s.live);
|
|
||||||
Type::Obj(site)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a flag of `site`. Whether it was not set before
|
|
||||||
pub fn mark(&mut self, site: SiteId, flag: fn(&mut SiteInfo) -> &mut bool) -> bool {
|
|
||||||
let flag = flag(&mut self.sites[site as usize]);
|
|
||||||
|
|
||||||
if *flag {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*flag = true;
|
|
||||||
self.version += 1;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether an object of `site` can lack its own `field`. A box's value
|
|
||||||
/// never can: JS gives a binding a value before any read of it that does
|
|
||||||
/// not throw (see transform)
|
|
||||||
pub fn can_be_absent(&self, site: SiteId, field: AtomRef) -> bool {
|
|
||||||
field != BOX_FIELD && !self.site(site).field(field).is_some_and(|f| f.defined)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A value of type `ty` goes into an `Any` value. Code that holds the
|
|
||||||
/// `Any` does not know the site of an object in it, so the object's site
|
|
||||||
/// is marked, and so are the sites of the objects in its fields, which
|
|
||||||
/// that code can read
|
|
||||||
pub fn to_any(&mut self, ty: &Type) -> Type {
|
|
||||||
let mut stack = vec![];
|
|
||||||
self.push_sites(ty, &mut stack);
|
|
||||||
|
|
||||||
while let Some(site) = stack.pop() {
|
|
||||||
if self.mark(site, |s| &mut s.in_any) {
|
|
||||||
for i in 0..self.site(site).fields.len() {
|
|
||||||
let field = self.site(site).fields[i].1.ty;
|
|
||||||
self.push_sites(&field, &mut stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ObjKind::Array { elems } = self.site(site).kind {
|
|
||||||
self.push_sites(&elems, &mut stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Type::Any
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push the site of each object member of `ty`
|
|
||||||
fn push_sites(&self, ty: &Type, stack: &mut Vec<SiteId>) {
|
|
||||||
stack.extend(self.members(ty).iter().filter_map(|m| match m {
|
|
||||||
Type::Obj(site) => Some(*site),
|
|
||||||
_ => None,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────── Fields ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Join `ty` into `field` of the objects of `site`. A field that the site
|
|
||||||
/// did not have can be absent. An array's `length` is not a field: a write
|
|
||||||
/// of it sets the array's length (see `docs/model.md`, section 3.3)
|
|
||||||
pub fn write(&mut self, site: SiteId, field: AtomRef, ty: &Type) {
|
|
||||||
if field == LENGTH_FIELD && self.site(site).is_array() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let old = self.site(site).field(field).map_or(Type::Unknown, |f| f.ty);
|
|
||||||
let new = self.join(&old, ty);
|
|
||||||
|
|
||||||
// Code that holds one of its objects in an `Any` can read the field
|
|
||||||
if self.site(site).in_any {
|
|
||||||
self.to_any(&new);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fields = &mut self.sites[site as usize].fields;
|
|
||||||
|
|
||||||
match fields.binary_search_by_key(&field, |(n, _)| *n) {
|
|
||||||
Ok(i) if fields[i].1.ty == new => return,
|
|
||||||
Ok(i) => fields[i].1.ty = new,
|
|
||||||
Err(i) => fields.insert(i, (field, Field { ty: new, defined: false })),
|
|
||||||
}
|
|
||||||
|
|
||||||
self.version += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Join `ty` into the element type of the arrays of `site`
|
|
||||||
pub fn write_elem(&mut self, site: SiteId, ty: &Type) {
|
|
||||||
let ObjKind::Array { elems } = self.site(site).kind else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
|
|
||||||
let new = self.join(&elems, ty);
|
|
||||||
|
|
||||||
// Code that holds one of its arrays in an `Any` can read the elements
|
|
||||||
if self.site(site).in_any {
|
|
||||||
self.to_any(&new);
|
|
||||||
}
|
|
||||||
|
|
||||||
if new != elems {
|
|
||||||
self.sites[site as usize].kind = ObjKind::Array { elems: new };
|
|
||||||
self.version += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A read of an element of an array of `site`. An index can be a hole, or
|
|
||||||
/// past the end, which gives undefined
|
|
||||||
pub fn read_elem(&mut self, site: SiteId) -> Type {
|
|
||||||
let ObjKind::Array { elems } = self.site(site).kind else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.join(&elems, &Type::Undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A read of `field` on an object of `site`: its own field, then, where
|
|
||||||
/// that can be absent, the read on its prototype. At the end of the chain,
|
|
||||||
/// an absent field is undefined. An array's prototype is the one that the
|
|
||||||
/// stdlib links for arrays, in `protos`, and its `length` is not a field
|
|
||||||
pub fn read(&mut self, site: SiteId, field: AtomRef, protos: &Protos) -> Type {
|
|
||||||
self.read_chain(site, field, protos, self.sites.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `read()`, with at most `depth` more prototypes. Objects of one site can
|
|
||||||
/// be each other's prototypes, so the chain of sites can loop, which a
|
|
||||||
/// chain of objects cannot. A chain longer than the number of sites has
|
|
||||||
/// looped, and stops
|
|
||||||
fn read_chain(&mut self, site: SiteId, field: AtomRef, protos: &Protos, depth: usize) -> Type {
|
|
||||||
let array = self.site(site).is_array();
|
|
||||||
|
|
||||||
if array && field == LENGTH_FIELD {
|
|
||||||
return Type::Int;
|
|
||||||
}
|
|
||||||
|
|
||||||
let own = self.site(site).field(field).map_or(Type::Unknown, |f| f.ty);
|
|
||||||
|
|
||||||
if !self.can_be_absent(site, field) || depth == 0 {
|
|
||||||
return own;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The chain ends at a prototype that is absent or not an object
|
|
||||||
let (proto, proto_absent) = match array {
|
|
||||||
true => (protos[ProtoOf::Array as usize], false),
|
|
||||||
false => {
|
|
||||||
let proto = self.site(site).field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty);
|
|
||||||
(proto, self.can_be_absent(site, PROTO_FIELD))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut ty = own;
|
|
||||||
|
|
||||||
if proto_absent || proto == Type::Unknown {
|
|
||||||
ty = self.join(&ty, &Type::Undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..self.members(&proto).len() {
|
|
||||||
let read = match self.members(&proto)[i] {
|
|
||||||
Type::Obj(proto) => self.read_chain(proto, field, protos, depth - 1),
|
|
||||||
Type::Any => Type::Any,
|
|
||||||
_ => Type::Undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
ty = self.join(&ty, &read);
|
|
||||||
}
|
|
||||||
|
|
||||||
ty
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A read of `field` on a value of type `ty`. A primitive reads its kind's
|
|
||||||
/// prototype in `protos`. Undefined and null have no fields: the read
|
|
||||||
/// throws, so it gives nothing. A sum reads on each of its members
|
|
||||||
///
|
|
||||||
/// A read on `Any` can read any object in an `Any`, or any primitive. What
|
|
||||||
/// it reads goes into the `Any` that it gives
|
|
||||||
pub fn read_value(&mut self, ty: &Type, field: AtomRef, protos: &Protos) -> Type {
|
|
||||||
match *ty {
|
|
||||||
Type::Obj(site) => self.read(site, field, protos),
|
|
||||||
Type::Sum(_) => {
|
|
||||||
let mut read = Type::Unknown;
|
|
||||||
|
|
||||||
for i in 0..self.members(ty).len() {
|
|
||||||
let member = self.members(ty)[i];
|
|
||||||
let ty = self.read_value(&member, field, protos);
|
|
||||||
read = self.join(&read, &ty);
|
|
||||||
}
|
|
||||||
|
|
||||||
read
|
|
||||||
}
|
|
||||||
Type::Any => {
|
|
||||||
for site in 0..self.sites.len() as SiteId {
|
|
||||||
if self.site(site).in_any {
|
|
||||||
let read = self.read(site, field, protos);
|
|
||||||
self.to_any(&read);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for kind in ProtoOf::ALL {
|
|
||||||
if let Type::Obj(proto) = protos[kind as usize] {
|
|
||||||
let read = self.read(proto, field, protos);
|
|
||||||
self.to_any(&read);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Type::Any
|
|
||||||
}
|
|
||||||
ty => match ProtoOf::of(&ty).map(|kind| protos[kind as usize]) {
|
|
||||||
Some(Type::Unknown) | None => Type::Unknown,
|
|
||||||
Some(proto) => self.read_value(&proto, field, protos),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+79
-145
@@ -1,15 +1,14 @@
|
|||||||
//! Types: the values that a place can hold, their joins, and what a type
|
//! Types: the values that a place can hold, their joins, and what a type
|
||||||
//! decides about the values in it (see `docs/model.md`, section 3.1)
|
//! decides about the values in it (see `docs/model.md`, section 2)
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::libqjs::parse::AtomRef;
|
use crate::libqjs::parse::AtomRef;
|
||||||
|
|
||||||
use super::sites::{SiteId, SiteTable};
|
use super::cells::Cell;
|
||||||
|
|
||||||
pub(super) const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
|
pub(super) const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
|
||||||
|
|
||||||
/// The number of a sum (see `SiteTable::members()`)
|
|
||||||
pub type SumId = u32;
|
|
||||||
|
|
||||||
/// A type is one of these. Join rules:
|
/// A type is one of these. Join rules:
|
||||||
/// Unknown * T = T
|
/// Unknown * T = T
|
||||||
/// Any * T = Any
|
/// Any * T = Any
|
||||||
@@ -21,8 +20,8 @@ pub type SumId = u32;
|
|||||||
/// A sum is a value of one of a list of types, its members. It has two or more
|
/// A sum is a value of one of a list of types, its members. It has two or more
|
||||||
/// members, none of them `Unknown`, `Sum` or `Any`, sorted in the order of the
|
/// members, none of them `Unknown`, `Sum` or `Any`, sorted in the order of the
|
||||||
/// variants below. It has at most one member of each base type, except
|
/// variants below. It has at most one member of each base type, except
|
||||||
/// objects: one `Obj` for each site
|
/// objects: one `Obj` for each cell
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum Type {
|
pub enum Type {
|
||||||
Unknown, // no value has reached it yet
|
Unknown, // no value has reached it yet
|
||||||
Undefined,
|
Undefined,
|
||||||
@@ -31,12 +30,12 @@ pub enum Type {
|
|||||||
Int,
|
Int,
|
||||||
Float,
|
Float,
|
||||||
Str(Option<AtomRef>), // the value, if it is known. Atoms are interned: equal atoms are equal strings
|
Str(Option<AtomRef>), // the value, if it is known. Atoms are interned: equal atoms are equal strings
|
||||||
Obj(SiteId), // an object that the site makes. A fn is an object whose site has code
|
Obj(Cell), // an object, which the cell describes. A fn is an object whose cell has code
|
||||||
Sum(SumId), // see above
|
Sum(Rc<[Type]>), // see above
|
||||||
Any, // any value. No rule gives it now: a join gives a sum instead
|
Any, // any value. No rule gives it now: a join gives a sum instead
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Type` without its detail (a known value, a site): the kinds of value
|
/// A `Type` without its detail (a known value, a cell): the kinds of value
|
||||||
/// that JS tells apart
|
/// that JS tells apart
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum BaseType {
|
pub enum BaseType {
|
||||||
@@ -49,15 +48,6 @@ pub enum BaseType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Type {
|
impl Type {
|
||||||
/// The type with the known value dropped
|
|
||||||
pub fn widen(&self) -> Type {
|
|
||||||
match self {
|
|
||||||
Type::Bool(_) => Type::Bool(None),
|
|
||||||
Type::Str(_) => Type::Str(None),
|
|
||||||
ty => *ty,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The base type of every value of the type. `None` for `Unknown`, which
|
/// The base type of every value of the type. `None` for `Unknown`, which
|
||||||
/// has no values, and for `Sum` and `Any`, which can have more than one
|
/// has no values, and for `Sum` and `Any`, which can have more than one
|
||||||
pub fn base(&self) -> Option<BaseType> {
|
pub fn base(&self) -> Option<BaseType> {
|
||||||
@@ -75,139 +65,87 @@ impl Type {
|
|||||||
pub fn is_nullish(&self) -> bool {
|
pub fn is_nullish(&self) -> bool {
|
||||||
matches!(self, Type::Undefined | Type::Null)
|
matches!(self, Type::Undefined | Type::Null)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// The join of two types of one base type, neither of them `Obj`
|
/// The types that a value of the type can have: the members of a sum,
|
||||||
fn join_kind(a: Type, b: Type) -> Type {
|
/// else the type itself. `Unknown` has none
|
||||||
match (a, b) {
|
pub fn members(&self) -> &[Type] {
|
||||||
(Type::Bool(x), Type::Bool(y)) => Type::Bool(if x == y { x } else { None }),
|
match self {
|
||||||
(Type::Str(x), Type::Str(y)) => Type::Str(if x == y { x } else { None }),
|
|
||||||
(Type::Int, Type::Int) => Type::Int,
|
|
||||||
(Type::Int | Type::Float, Type::Int | Type::Float) => Type::Float,
|
|
||||||
(a, _) => a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The sums, and the join, which makes them. The sums are stored in the site
|
|
||||||
/// table, because a join also marks sites (see `SiteTable::to_any()`)
|
|
||||||
impl SiteTable {
|
|
||||||
/// The types that a value of type `ty` can have: the members of a sum,
|
|
||||||
/// else `ty` itself. `Unknown` has none
|
|
||||||
pub fn members<'a>(&'a self, ty: &'a Type) -> &'a [Type] {
|
|
||||||
match ty {
|
|
||||||
Type::Unknown => &[],
|
Type::Unknown => &[],
|
||||||
Type::Sum(id) => {
|
Type::Sum(members) => members,
|
||||||
let id = *id as usize;
|
|
||||||
&self.sum_members[self.sum_starts[id] as usize..self.sum_starts[id + 1] as usize]
|
|
||||||
}
|
|
||||||
ty => std::slice::from_ref(ty),
|
ty => std::slice::from_ref(ty),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The members of every sum
|
/// The type with every known value dropped
|
||||||
pub fn sums(&self) -> impl Iterator<Item = &[Type]> {
|
pub fn widen(&self) -> Type {
|
||||||
self.sum_starts.windows(2).map(|w| &self.sum_members[w[0] as usize..w[1] as usize])
|
match self {
|
||||||
}
|
Type::Bool(_) => Type::Bool(None),
|
||||||
|
Type::Str(_) => Type::Str(None),
|
||||||
/// The type whose members are the members of `a` and of `b`
|
Type::Sum(members) => sum_of(members.iter().map(Type::widen).collect()),
|
||||||
fn union(&mut self, a: &Type, b: &Type) -> Type {
|
ty => ty.clone(),
|
||||||
let start = self.sum_members.len();
|
|
||||||
|
|
||||||
for ty in [a, b] {
|
|
||||||
for i in 0..self.members(ty).len() {
|
|
||||||
let member = self.members(ty)[i];
|
|
||||||
self.sum_members.push(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sum_of_new(start)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `ty` with every known value dropped
|
|
||||||
pub fn widen(&mut self, ty: &Type) -> Type {
|
|
||||||
let start = self.sum_members.len();
|
|
||||||
|
|
||||||
for i in 0..self.members(ty).len() {
|
|
||||||
let member = self.members(ty)[i].widen();
|
|
||||||
self.sum_members.push(member);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sum_of_new(start)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `widen()` for a type that `widen()` was already given
|
|
||||||
pub fn widened(&self, ty: &Type) -> Type {
|
|
||||||
let list: Vec<Type> = self.members(ty).iter().map(Type::widen).collect();
|
|
||||||
|
|
||||||
match list[..] {
|
|
||||||
[one] => one,
|
|
||||||
_ => Type::Sum(self.sums().position(|sum| sum == list).unwrap() as SumId),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The type whose members are the types at the end of `sum_members`, from
|
/// The join of two types (see `Type`)
|
||||||
/// `start` on. They are sorted, and two of one kind join. If they are one
|
pub fn join(a: &Type, b: &Type) -> Type {
|
||||||
/// type, or a sum that exists, they are removed again
|
match (a, b) {
|
||||||
fn sum_of_new(&mut self, start: usize) -> Type {
|
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
|
||||||
let list = &mut self.sum_members[start..];
|
(a, b) if a == b => a.clone(),
|
||||||
list.sort_unstable();
|
(Type::Any, _) | (_, Type::Any) => Type::Any,
|
||||||
|
_ => {
|
||||||
|
let joined = sum_of(a.members().iter().chain(b.members()).cloned().collect());
|
||||||
|
|
||||||
let mut n = 0;
|
// Keep the sum that exists, so that its members are not copied
|
||||||
|
match () {
|
||||||
for i in 0..list.len() {
|
_ if joined == *a => a.clone(),
|
||||||
let ty = list[i];
|
_ if joined == *b => b.clone(),
|
||||||
let joins = n > 0 && (list[n - 1] == ty || (ty.base() == list[n - 1].base() && ty.base() != Some(BaseType::Object)));
|
_ => joined,
|
||||||
|
|
||||||
if joins {
|
|
||||||
list[n - 1] = join_kind(list[n - 1], ty);
|
|
||||||
} else {
|
|
||||||
list[n] = ty;
|
|
||||||
n += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sum_members.truncate(start + n);
|
|
||||||
|
|
||||||
let new = &self.sum_members[start..];
|
|
||||||
let found = match new {
|
|
||||||
[one] => Some(*one),
|
|
||||||
_ => self.sums().position(|sum| sum == new).map(|id| Type::Sum(id as SumId)),
|
|
||||||
};
|
|
||||||
|
|
||||||
match found {
|
|
||||||
Some(ty) => {
|
|
||||||
self.sum_members.truncate(start);
|
|
||||||
ty
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.sum_starts.push(self.sum_members.len() as u32);
|
|
||||||
Type::Sum((self.sum_starts.len() - 2) as SumId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The join of two types (see `Type`)
|
/// The type whose members are `list`: sorted, and two of one base type
|
||||||
pub fn join(&mut self, a: &Type, b: &Type) -> Type {
|
/// joined
|
||||||
use Type::*;
|
fn sum_of(mut list: Vec<Type>) -> Type {
|
||||||
|
list.sort_unstable();
|
||||||
|
list.dedup_by(|b, a| {
|
||||||
|
let joins = a.base() == b.base() && a.base() != Some(BaseType::Object);
|
||||||
|
|
||||||
match (*a, *b) {
|
if joins {
|
||||||
(Unknown, t) | (t, Unknown) => t,
|
*a = join_base(a, b);
|
||||||
(a, b) if a == b => a,
|
|
||||||
(Any, t) | (t, Any) => self.to_any(&t),
|
|
||||||
(a, b) => self.union(&a, &b),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
joins || a == b
|
||||||
|
});
|
||||||
|
|
||||||
|
match list.len() {
|
||||||
|
1 => list.pop().unwrap(),
|
||||||
|
_ => Type::Sum(list.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The join of two types of one base type, neither of them `Obj`
|
||||||
|
fn join_base(a: &Type, b: &Type) -> Type {
|
||||||
|
match (a, b) {
|
||||||
|
(Type::Bool(x), Type::Bool(y)) => Type::Bool(if x == y { *x } else { None }),
|
||||||
|
(Type::Str(x), Type::Str(y)) => Type::Str(if x == y { *x } else { None }),
|
||||||
|
(Type::Int, Type::Int) => Type::Int,
|
||||||
|
(Type::Int | Type::Float, Type::Int | Type::Float) => Type::Float,
|
||||||
|
(a, _) => a.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type, if every value of it is one value that the type tells
|
/// The type, if every value of it is one value that the type tells
|
||||||
pub(super) fn single(ty: &Type) -> Option<Type> {
|
pub(super) fn single(ty: &Type) -> Option<Type> {
|
||||||
match ty {
|
match ty {
|
||||||
Type::Undefined | Type::Null | Type::Bool(Some(_)) | Type::Str(Some(_)) => Some(*ty),
|
Type::Undefined | Type::Null | Type::Bool(Some(_)) | Type::Str(Some(_)) => Some(ty.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tests below take one member of a type each (see `TypeCache::agree()`)
|
// The tests below take one member of a type each (see `agree()` in infer)
|
||||||
|
|
||||||
/// Whether `a === b`, if the types decide it
|
/// Whether `a === b`, if the types decide it
|
||||||
pub(super) fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
|
pub(super) fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
|
||||||
@@ -224,26 +162,23 @@ pub(super) fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
|
|||||||
/// each other and to nothing else. Two values of one kind compare as `===`
|
/// each other and to nothing else. Two values of one kind compare as `===`
|
||||||
/// does
|
/// does
|
||||||
pub(super) fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
|
pub(super) fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
|
||||||
let other = |t: &Type| !t.is_nullish() && *t != Type::Any;
|
|
||||||
|
|
||||||
match (a, b) {
|
match (a, b) {
|
||||||
_ if a.is_nullish() && b.is_nullish() => Some(true),
|
_ if a.is_nullish() && b.is_nullish() => Some(true),
|
||||||
_ if (a.is_nullish() && other(b)) || (other(a) && b.is_nullish()) => Some(false),
|
_ if a.is_nullish() || b.is_nullish() => Some(false),
|
||||||
_ if a.base().is_some() && a.base() == b.base() => strict_eq(a, b),
|
_ if a.base() == b.base() => strict_eq(a, b),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a value of type `ty` has one of the base types `bases`, if the type decides it
|
/// Whether a value of type `ty` has one of the base types `bases`
|
||||||
pub(super) fn test(ty: &Type, bases: &[BaseType]) -> Option<bool> {
|
pub(super) fn test(ty: &Type, bases: &[BaseType]) -> Option<bool> {
|
||||||
ty.base().map(|base| bases.contains(&base))
|
ty.base().map(|base| bases.contains(&base))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a value of type `ty` is a fn, if the type decides it
|
/// Whether a value of type `ty` is a fn
|
||||||
pub(super) fn is_function(ty: &Type, site_table: &SiteTable) -> Option<bool> {
|
pub(super) fn is_function(ty: &Type) -> Option<bool> {
|
||||||
match ty {
|
match ty {
|
||||||
Type::Obj(site) => Some(site_table.site(*site).code().is_some()),
|
Type::Obj(cell) => Some(cell.code().is_some()),
|
||||||
Type::Any => None,
|
|
||||||
_ => Some(false),
|
_ => Some(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,17 +194,16 @@ pub(super) fn truthiness(ty: &Type) -> Option<bool> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The atom of what `typeof` gives for a value of type `ty`, if the type
|
/// The atom of what `typeof` gives for a value of type `ty`
|
||||||
/// decides it
|
pub(super) fn typeof_name(ty: &Type) -> Option<u32> {
|
||||||
pub(super) fn typeof_name(ty: &Type, site_table: &SiteTable) -> Option<u32> {
|
|
||||||
Some(match ty {
|
Some(match ty {
|
||||||
Type::Undefined => 73, // "undefined"
|
Type::Undefined => 73, // "undefined"
|
||||||
Type::Null => 77, // "object"
|
Type::Null => 77, // "object"
|
||||||
Type::Bool(_) => 75, // "boolean"
|
Type::Bool(_) => 75, // "boolean"
|
||||||
Type::Int | Type::Float => 74, // "number"
|
Type::Int | Type::Float => 74, // "number"
|
||||||
Type::Str(_) => 76, // "string"
|
Type::Str(_) => 76, // "string"
|
||||||
Type::Obj(site) if site_table.site(*site).code().is_some() => 27, // "function"
|
Type::Obj(cell) if cell.code().is_some() => 27, // "function"
|
||||||
Type::Obj(_) => 77, // "object"
|
Type::Obj(_) => 77, // "object"
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
mod analyze;
|
|
||||||
mod emit;
|
mod emit;
|
||||||
mod flatten;
|
mod flatten;
|
||||||
mod infer;
|
mod infer;
|
||||||
@@ -10,7 +9,6 @@ mod transform;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use analyze::analyze;
|
|
||||||
use flatten::flatten;
|
use flatten::flatten;
|
||||||
use infer::infer_types;
|
use infer::infer_types;
|
||||||
use libqjs::compile;
|
use libqjs::compile;
|
||||||
@@ -74,9 +72,8 @@ fn main() {
|
|||||||
let flat_fns = transform(flat_fns, &captures, &atoms);
|
let flat_fns = transform(flat_fns, &captures, &atoms);
|
||||||
let reg_fns = regify(flat_fns);
|
let reg_fns = regify(flat_fns);
|
||||||
let ty_cache = infer_types(®_fns);
|
let ty_cache = infer_types(®_fns);
|
||||||
let analysis = analyze(®_fns);
|
|
||||||
|
|
||||||
let c = emit::emit(®_fns, &ty_cache, &analysis, &atoms);
|
let c = emit::emit(®_fns, &ty_cache, &atoms);
|
||||||
|
|
||||||
// Compile the C, with the runtime, to `a.out`
|
// Compile the C, with the runtime, to `a.out`
|
||||||
let dir = std::env::temp_dir().join(format!("jsrt-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("jsrt-{}", std::process::id()));
|
||||||
|
|||||||
+6
-12
@@ -10,10 +10,7 @@
|
|||||||
//! kind. A field read on a primitive borrows its kind's prototype
|
//! kind. A field read on a primitive borrows its kind's prototype
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
infer::{
|
infer::types::{BaseType, Type},
|
||||||
sites::SiteTable,
|
|
||||||
types::{BaseType, Type},
|
|
||||||
},
|
|
||||||
libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS},
|
libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,7 +44,7 @@ macro_rules! ty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub static NATIVES: &[NativeFn] = &[
|
pub const NATIVES: &[NativeFn] = &[
|
||||||
// IO
|
// IO
|
||||||
native!("__native_print_string"(String) -> Undefined),
|
native!("__native_print_string"(String) -> Undefined),
|
||||||
|
|
||||||
@@ -92,7 +89,6 @@ pub enum ProtoOf {
|
|||||||
|
|
||||||
impl ProtoOf {
|
impl ProtoOf {
|
||||||
pub const ALL: [ProtoOf; 4] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean, ProtoOf::Array];
|
pub const ALL: [ProtoOf; 4] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean, ProtoOf::Array];
|
||||||
pub const PRIMITIVES: [ProtoOf; 3] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean];
|
|
||||||
|
|
||||||
/// The primitive kind of every value of type `ty`, if it has a prototype
|
/// The primitive kind of every value of type `ty`, if it has a prototype
|
||||||
pub fn of(ty: &Type) -> Option<ProtoOf> {
|
pub fn of(ty: &Type) -> Option<ProtoOf> {
|
||||||
@@ -136,16 +132,14 @@ pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue {
|
|||||||
|
|
||||||
/// What a call of native `idx` returns, with args of these types, in source
|
/// What a call of native `idx` returns, with args of these types, in source
|
||||||
/// order. An arg that cannot be of its param's kind makes the call throw, so
|
/// order. An arg that cannot be of its param's kind makes the call throw, so
|
||||||
/// the call gives nothing. An arg of a sum or `Any` is checked at runtime
|
/// the call gives nothing. An arg of a sum is checked at runtime
|
||||||
pub fn ret_type(idx: usize, args: &[Type], site_table: &SiteTable) -> Type {
|
pub fn ret_type(idx: usize, args: &[Type]) -> Type {
|
||||||
let native = &NATIVES[idx];
|
let native = &NATIVES[idx];
|
||||||
let missing = std::iter::repeat(&Type::Undefined);
|
let missing = std::iter::repeat(&Type::Undefined);
|
||||||
let can_pass = |(param, arg): (&Type, &Type)| {
|
let can_pass = |(param, arg): (&Type, &Type)| arg.members().iter().any(|a| a.base() == param.base());
|
||||||
*arg == Type::Any || site_table.members(arg).iter().any(|a| a.base() == param.base())
|
|
||||||
};
|
|
||||||
|
|
||||||
match native.args.iter().zip(args.iter().chain(missing)).all(can_pass) {
|
match native.args.iter().zip(args.iter().chain(missing)).all(can_pass) {
|
||||||
true => native.ret,
|
true => native.ret.clone(),
|
||||||
false => Type::Unknown,
|
false => Type::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,10 +118,11 @@ impl RegFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A define_field into an object that no creating op of this fn gave
|
// A define_field into an object that no op of this fn made (a class
|
||||||
// (a class field on `this`, a static field on the class) is a write
|
// field on `this`, a static field on the class) is a write like any
|
||||||
// like any other: a put_field. So a define_field is always a write
|
// other: a put_field. So a define_field is always a write into an
|
||||||
// that builds an object before it goes anywhere (see infer)
|
// object before it goes anywhere, and its field is never absent (see
|
||||||
|
// `Cell::write()` in infer)
|
||||||
let mut created = vec![false; next_val];
|
let mut created = vec![false; next_val];
|
||||||
|
|
||||||
for instr in &out {
|
for instr in &out {
|
||||||
@@ -265,17 +266,10 @@ fn walk_block(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A field or element write lands back in the object's own value: the
|
// define_field leaves the object it wrote on the stack: the same value,
|
||||||
// object with the field is the same object. define_field also leaves
|
// not a new one
|
||||||
// it on the stack, put_field and put_array_el do not
|
if instr.op == OpCode::DefineField {
|
||||||
if matches!(instr.op, OpCode::PutField | OpCode::DefineField | OpCode::PutArrayEl) {
|
stack.push(out_instr.args[1]);
|
||||||
let obj = *out_instr.args.last().unwrap();
|
|
||||||
|
|
||||||
if n_push == 1 {
|
|
||||||
stack.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
out_instr.dsts.push(obj);
|
|
||||||
out[instr_idx] = out_instr;
|
out[instr_idx] = out_instr;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ pub fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
|
|||||||
// [[HomeObject]], which only `super` reads. `super` is not supported
|
// [[HomeObject]], which only `super` reads. `super` is not supported
|
||||||
// (see set_home_object in regify)
|
// (see set_home_object in regify)
|
||||||
(SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)],
|
(SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)],
|
||||||
|
// A class constructor called without `new` throws. That is not
|
||||||
|
// checked
|
||||||
|
(CheckCtor, _) => &[(Nop, none)],
|
||||||
// arguments, new.target, import.meta
|
// arguments, new.target, import.meta
|
||||||
(SpecialObject, _) => unimplemented!("special_object {imm:?}"),
|
(SpecialObject, _) => unimplemented!("special_object {imm:?}"),
|
||||||
_ => return false,
|
_ => return false,
|
||||||
|
|||||||
+82
-159
@@ -1,59 +1,26 @@
|
|||||||
# The core model
|
# The core model
|
||||||
|
|
||||||
This file is the spec for how the compiler represents JS values, objects, bindings and functions in C. Each part of the code must agree with it. A difference between the code and this file is a bug in one of them.
|
This file is the spec for how the compiler gives a type to every value of a JS program, and how it represents those values in C. The code must agree with it. A difference between the code and this file is a bug in one of them.
|
||||||
|
|
||||||
The method:
|
Everything comes from four simple things and how they act on each other:
|
||||||
|
|
||||||
1. State the JS semantics that the C code must keep (section 1).
|
1. **the types**, an enum, and the rules that join two of them (section 2)
|
||||||
2. Define a default representation that keeps all of it with no analysis (section 2).
|
2. **objects**, which are types that every holder shares (section 3)
|
||||||
3. Let types (sections 3 and 4), the value rule (section 5) and analyses (sections 6 and 7) narrow the representation. Each narrowing has one condition. If the condition is not proved, the default stays.
|
3. **`walk_fn`**, which walks one fn with the types of its args, and gives the type of each of its values (section 4)
|
||||||
|
4. **the type cache**, which keeps one walk for each fn and each list of arg types that the program reaches (section 4)
|
||||||
So a hole in an analysis costs speed. It never makes wrong code. Only a wrong rule can make wrong code.
|
|
||||||
|
|
||||||
## 1. Semantics
|
## 1. Semantics
|
||||||
|
|
||||||
JS has four core concepts.
|
The C code must keep these JS semantics.
|
||||||
|
|
||||||
**Value.** A value is one of: undefined, null, bool, number, string, object. A value cannot change. A value is always copied: an assignment, an argument, a return and a field write each copy it. An object value is a reference to an object, so a copy of it copies the reference.
|
- **A value** is one of: undefined, null, a bool, a number, a string, an object. A value cannot change. An assignment, an arg, a return and a field write each copy the value. An object value is a reference to the object, so a copy of it refers to the same object.
|
||||||
|
- **An object** has identity, own fields, and a prototype (an object or null). A read of a field that the object lacks goes on to the prototype, and at the end of the chain it gives undefined. A field that is absent and a field that holds undefined are different.
|
||||||
|
- **A binding** is the storage of a variable. A closure captures bindings, not values: a write to a captured binding, by its owner or by any closure, is seen by all of them. Each iteration of a loop body has its own bindings.
|
||||||
|
- **A function** is an object that also has code, and the bindings that it captured.
|
||||||
|
|
||||||
**Object.** An object is a cell with identity. It has own fields and a prototype (an object or null). A field read looks at the own fields. If the field is absent, the read goes on to the prototype, and at the end of the chain it gives undefined. An absent field and a field that holds undefined are different. Two object values are equal (`===`, `==`) if and only if they refer to the same object.
|
## 2. Types
|
||||||
|
|
||||||
**Binding.** A binding is the storage of a variable. Each entry into a scope makes new bindings. A loop body is entered once for each iteration. `for (let …)` copies the values of the loop vars into the new bindings of the next iteration. A closure captures bindings, not values. So a write to a captured binding, by the owner or by any closure, is seen by all of them.
|
A type tells which values a place can hold at runtime. It is one of these:
|
||||||
|
|
||||||
**Function.** A function is an object that also has code, and the bindings that it captured.
|
|
||||||
|
|
||||||
### When the representation is free
|
|
||||||
|
|
||||||
A representation is correct if no program can see a difference from these semantics.
|
|
||||||
|
|
||||||
- **A primitive** cannot change and has no identity. A copy of it and a shared pointer to it look the same. Either is correct.
|
|
||||||
- **An object** can be copied instead of shared only if no program can see the copy. There are two ways to see a copy:
|
|
||||||
- a write through one holder, which another holder reads later
|
|
||||||
- an identity test between two holders
|
|
||||||
- **A binding** must be shared if a closure captures it and a write reaches it after the capture.
|
|
||||||
|
|
||||||
## 2. The default representation
|
|
||||||
|
|
||||||
With no types and no analysis, this representation is correct for every program that uses only supported ops.
|
|
||||||
|
|
||||||
- **Every value is an `any_t`:** a tagged union of the kinds. An object in an `any_t` is a pointer to its cell, so it keeps its identity.
|
|
||||||
- **Every object is a cell on the heap.** Each object starts with a header: the site that created it. The site tells the object's shape and, for a fn, its code. Each field starts as absent.
|
|
||||||
- **Every captured binding is a box:** a cell on the heap with one slot.
|
|
||||||
- The box is made when the fn is entered.
|
|
||||||
- The owner and every closure that captured the binding read and write the slot.
|
|
||||||
- A scope that is entered again is a loop body. QuickJS closes each captured binding of such a scope (`close_loc k`) at every way out of it. `close_loc k` puts a new box in `k`, which holds the current value. Closures made before keep the old box. So each iteration has its own bindings.
|
|
||||||
- A box's slot is never absent: JS gives a binding a value before any read of it that does not throw. A `var` starts as undefined. A `let` or `const` has no value before its declaration, where a read throws (the TDZ, section 8).
|
|
||||||
- A binding that no closure captures is a C local. Transform knows this from `closure_vars`, so no analysis decides it.
|
|
||||||
- A module binding has no frame. Its box is made where the fn that declares it is made.
|
|
||||||
- **A function object holds a pointer to each box that it captured.** The fn that makes the closure gives each box: from its own local if the binding is its own, else from its own env. So every capture is one copy of a pointer.
|
|
||||||
|
|
||||||
## 3. Types
|
|
||||||
|
|
||||||
A type tells which values a place can hold at runtime. Infer computes one type for each value id in each variant (section 3.5).
|
|
||||||
|
|
||||||
### 3.1 The types
|
|
||||||
|
|
||||||
A type is one of these:
|
|
||||||
|
|
||||||
| type | values |
|
| type | values |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -64,11 +31,11 @@ A type is one of these:
|
|||||||
| `Int` | an int32 (provisional: the number type is not decided) |
|
| `Int` | an int32 (provisional: the number type is not decided) |
|
||||||
| `Float` | a number |
|
| `Float` | a number |
|
||||||
| `Str(v)` | a string. `v` is the value, if it is known |
|
| `Str(v)` | a string. `v` is the value, if it is known |
|
||||||
| `Obj(site)` | an object that `site` creates. A fn is an object whose site has code |
|
| `Obj(cell)` | an object, described by a cell (section 3) |
|
||||||
| `Sum(id)` | a value of one of the types in a list: `Int \| Str`, `Obj(A) \| Null`, `Obj(A) \| Obj(B)` |
|
| `Sum(…)` | one of a list of the types above |
|
||||||
| `Any` | any value. No rule gives it now (section 3.4) |
|
| `Any` | any value. No rule gives it now |
|
||||||
|
|
||||||
**Join:**
|
**Join** gives the type of a place that gets the values of two types:
|
||||||
|
|
||||||
```
|
```
|
||||||
Unknown * T = T
|
Unknown * T = T
|
||||||
@@ -79,55 +46,71 @@ A type is one of these:
|
|||||||
T * U = T | U
|
T * U = T | U
|
||||||
```
|
```
|
||||||
|
|
||||||
**Sums.** A sum is a list of the types above, except `Unknown`, `Sum` and `Any`. It has at most one type of each kind, except objects, which have one `Obj` for each site. A join of two sums is the union of their lists, where two types of one kind join by the rules above. So `Int | Str` joined with `Float` is `Float | Str`. Each list is kept once, by number, in the site table (`SiteTable`), which also holds a row for each site. There are finitely many kinds and sites, so there are finitely many sums.
|
A sum has at most one member of each kind, except objects: it has one `Obj` for each cell. Two `Obj` types are the same type only if they have the same cell.
|
||||||
|
|
||||||
**Members.** The members of a sum are the types in its list. The only member of any other type is the type itself, and `Unknown` has none. Every rule in infer works member by member, and joins the results (section 3.3).
|
## 3. Objects
|
||||||
|
|
||||||
### 3.2 The invariant
|
**The type of an object is a cell:** a description of the object that every holder of the object shares. In the compiler it is `Obj(Rc<RefCell<…>>)`. The cell holds:
|
||||||
|
|
||||||
At every program point, every runtime value of a place is in the place's type.
|
- the type of each field, and whether every object of the cell has the field from the start (else it can be absent)
|
||||||
|
- what the object is: a plain object, a fn (with its code), or an array (with the type of its elements)
|
||||||
|
|
||||||
Every rule in infer must keep this invariant. Every rule is monotone, and a rule with an `Unknown` operand gives `Unknown`. Infer starts from `Unknown` everywhere and computes a fixed point of the rules.
|
**A copy of the value copies the `Rc`,** so all holders have the same cell. Everything about aliasing comes from this:
|
||||||
|
|
||||||
### 3.3 Objects
|
- **A write** `o.f = v` joins the type of `v` into the cell's field `f`. Every holder of the object sees it at once: through calls, returns, other objects' fields, closures and array elements.
|
||||||
|
- **A read** `o.f` gives the cell's field `f`. If the field can be absent, it also gives the read on the prototype. At the end of the chain, it gives `Undefined`.
|
||||||
|
- **A cell can hold itself,** for example as the type of the field `next` in a list. So an object that reaches itself needs nothing more.
|
||||||
|
|
||||||
- **A site** is an op that creates an object (`object`, `fclosure`), or a built-in fn. All objects of a site share one type for each field, for the whole program. Two sites are never merged: objects of two sites in one place give `Any`.
|
**An array** is a cell whose elements have one type `T`, the join of every value written to an element. `a[i]` gives `T | Undefined`, because an index can be a hole or past the end. `a.length` is an `Int`.
|
||||||
- **A field's type** is the join of every value written to it. A field can be absent, unless every object of the site gets it when it is created (section 7). A field that no write reaches is only absent.
|
|
||||||
- **A field read** on `Obj(site)` gives the own field's type. If the field can be absent, it also gives the read on the prototype (the `__proto__` field). The chain ends at a prototype that is absent or not an object, where the read gives `Undefined`. So a field that can be absent reads as its type `| Undefined`.
|
|
||||||
- **A field write** on `Obj(site)` joins the value's type into the site's field. So every value that holds an object of the site sees the write.
|
|
||||||
- **A primitive** has no own fields. A read on it reads its kind's prototype, which the stdlib links with `__link_proto_*`.
|
|
||||||
- **A field read or write on undefined or null** throws (section 8), so it gives `Unknown`.
|
|
||||||
- **An array** is an object too, of a site whose kind is "array": `array_from` makes it. Besides its fields, the site has an **element type**: the join of every value written to an element. So the type of an array is `Obj(site)`, and its site says what its elements can be (written `[T]` for short).
|
|
||||||
- `a[i]` gives `T | Undefined`: an index can be a hole, or past the end.
|
|
||||||
- `a[i] = x` joins the type of `x` into `T`. An index past the end makes the array longer, and the indexes between are holes.
|
|
||||||
- `a.length` is an `Int`, and it is not a field. A write of it truncates the array, or makes it longer with holes.
|
|
||||||
- An index is a number. Any other key is a computed field name (section 8).
|
|
||||||
- An array's prototype is the one that the stdlib links for arrays (`__link_proto_array`), as for a primitive kind. Its methods are JS in the stdlib, on top of the three ops above.
|
|
||||||
- **On a sum,** a read, a write or a call works on each member, and joins the results. So a read on `Obj(A) | Null` gives the read on `A`: the `Null` member throws, and gives nothing.
|
|
||||||
|
|
||||||
### 3.4 `Any`
|
**A captured binding is a box:** a cell with one field, which holds the binding's value. The fn that owns the binding makes the box when it is entered, and again for each iteration of a loop body. The owner and every closure that captured the binding read and write the box's field. A closure is a fn cell whose fields hold its boxes.
|
||||||
|
|
||||||
Since sums, no join gives `Any`, so no value has the type `Any`. The rules for it stay, so that it can be used again (for example, as a limit on the size of a sum).
|
## 4. Inference
|
||||||
|
|
||||||
Code that holds an `Any` does not know the site of an object in it. So infer tracks which objects can be in an `Any`:
|
**`walk_fn`** walks one fn with one list of arg types.
|
||||||
|
|
||||||
- **The mark.** When a value of type `Obj(site)` goes into an `Any`, the site is marked `in_any`. A value goes into an `Any` in a join that gives `Any`, and when it is an operand or a result of a read, write or call on `Any`. The mark spreads to the sites of the objects in the site's fields, because code that holds the `Any` can read them.
|
- It starts with `val_types = vec![Type::Unknown; n_val]`, and puts the arg types into the args' values.
|
||||||
- **A read on `Any`** reads the field on every `in_any` site and on every primitive prototype. What it reads goes into the `Any` that it gives.
|
- It walks the instrs. Each instr gives a type from the types of its operands, and that type is joined into its dst. An instr whose operand is still `Unknown` waits.
|
||||||
- **A write on `Any`** writes the field on every `in_any` site, and sets their `written` flag (section 5).
|
- It walks again until nothing changes. The result is the type of each value, and the return type.
|
||||||
- **A call on `Any`** calls the code of every `in_any` site that has code, in the variant whose args are all `Any`, except the env, which is the fn object itself (`Obj(site)`). It gives `Any`.
|
|
||||||
|
|
||||||
So every object that code with an `Any` can reach is of an `in_any` site, and the C can choose the code by the site in the object's header.
|
**The cells of a walk.** When a walk reaches an instr that makes an object, the first time, it makes a new cell for that instr. On every later pass, it uses the same cell again. So the objects that a walk makes belong to that walk: a fn walked with `Int` and with `Str` makes different cells.
|
||||||
|
|
||||||
### 3.5 Calls
|
**The type cache** is a map from a fn and a list of arg types to the walk of that fn with those types.
|
||||||
|
|
||||||
- **A call** of `Obj(site)` runs the site's code, with the fn object itself as its env. A call of a sum calls each `Obj` member with its own env, and joins the results. A call of a value that is not a fn throws (section 8). A call of `Any` is in section 3.4.
|
- A call looks up its callee with the types of its args. If there is no walk yet, it walks the callee. The call gives the walk's return type.
|
||||||
- **The program is closed.** Every fn comes from an `fclosure`, or it is a native. So the code of every site is known.
|
- **Only what the program reaches is walked,** as in Rust: there is one walk for each list of arg types that a call really passes.
|
||||||
- **A variant** is one walk of a fn with one tuple of arg types. A known value does not cross a call (`Bool(v)` becomes `Bool`), so there are finitely many variants.
|
- **In the key, an object is its cell.** Known values are dropped (`Bool(true)` is `Bool`), so a literal does not make a new walk.
|
||||||
- **Rounds:** infer walks every variant from the entry again, until a round changes nothing in the fields, the flags, the prototypes or the returns. A call of a variant that is still being walked (recursion) gives what that variant returned in the round before. When a round changes nothing, these returns are the fixed point.
|
|
||||||
|
|
||||||
## 4. Representation
|
**Rounds.** A walk can depend on cells and returns that other walks are still adding to. So infer walks the whole program again, from the entry, until a round changes no cell, no return and no proto, and makes no new walk or cell. A recursive call gives what its walk returned in the round before.
|
||||||
|
|
||||||
The C representation of a value is a function of its type and of the value rule (section 5) only. Each type has one:
|
**The open problem: infer does not always end.** A walk makes cells, and a key holds cells. So when a cell that a walk made gets into the key of a later call, that call gets a new walk, which makes new cells, which can make another new key. Rust does not have this problem, because every type in a Rust key is written in the program (a struct and its type args), and a call cannot change the type of its args. These programs end at once in node, but infer does not end on them:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 1. `this` holds the list of nodes. The arrow fn never reads `this`, but it
|
||||||
|
// is in the key, and each new walk of it leads to a new node
|
||||||
|
let list = null;
|
||||||
|
function add(f) { list = { f: f, next: list }; }
|
||||||
|
add(() => add(() => 0));
|
||||||
|
list.f();
|
||||||
|
|
||||||
|
// 2. A cell is one type for the object's whole life: the call sees the type
|
||||||
|
// of `o.v` after the write, so each round sees one cell more
|
||||||
|
const o = { v: 1 };
|
||||||
|
function f(x) { o.v = { inner: x }; }
|
||||||
|
f(o.v);
|
||||||
|
|
||||||
|
// 3. A new object goes into a recursive call. The exact types are
|
||||||
|
// `{next: Int}`, `{next: {next: Int}}`, ...: as many as `n`, which only the
|
||||||
|
// runtime knows. Rust rejects this program too
|
||||||
|
function g(n, x) { if (n > 0) g(n - 1, { next: x }); }
|
||||||
|
g(3, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
The same thing happens on a large scale with partial types. Inside a walk, a call first sees the types of an early pass, then wider ones. Each key gets its own walk, and its cells stay in the types for good. The stdlib's promises reach thousands of walks from a 7-line program. The same loops without fns (`for (…) x = { next: x }`) end, because one instr in one walk is one cell.
|
||||||
|
|
||||||
|
## 5. Representation in C
|
||||||
|
|
||||||
|
The C type of a value comes from its type only:
|
||||||
|
|
||||||
| type | C |
|
| type | C |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -137,82 +120,22 @@ The C representation of a value is a function of its type and of the value rule
|
|||||||
| `Int` | `int32_t` |
|
| `Int` | `int32_t` |
|
||||||
| `Float` | `double` |
|
| `Float` | `double` |
|
||||||
| `Str` | `string_t` |
|
| `Str` | `string_t` |
|
||||||
| `Obj(site)`, a value shape (section 5) | `S`, the struct of the site |
|
| `Obj(cell)` | a pointer to the struct of the cell. The struct starts with a header that holds the cell's number |
|
||||||
| `Obj(site)`, any other shape | `S *`, a pointer to the struct of the site |
|
| `Str` or one `Obj`, plus `Undefined` and/or `Null` | that pointer. `NULL` is null, and `JSRT_UNDEFINED` is undefined |
|
||||||
| `Str`, plus `Undefined` and/or `Null` | `string_t`. `NULL` is null, and `JSRT_UNDEFINED` is undefined |
|
| `Obj` of several cells, maybe with `Undefined` and `Null` | a pointer to the header |
|
||||||
| `Obj(site)`, plus `Undefined` and/or `Null` | `S *`, with the same two sentinels |
|
| any other sum, and `Any` | `any_t`: a tagged union of every kind |
|
||||||
| `Obj` of several sites, with or without `Undefined` and `Null` | `jsrt_header *`, a pointer to the object's header, with the same two sentinels |
|
|
||||||
| any other sum | `any_t`. It is the tagged union of every kind, so it holds every sum |
|
|
||||||
| `Any` | `any_t`, a tagged union. An object in it is the pointer to its cell |
|
|
||||||
|
|
||||||
- **An array** is a pointer to the struct of its site, whose header is followed by `length`, `cap` and `items`: an array of `cap` elements, of the representation of `T | Undefined`. A hole holds undefined.
|
- **A field that can be absent** holds a sentinel for absent: `JSRT_ABSENT` in a pointer, a tag in an `any_t`.
|
||||||
- **Absent** is a field state, not a value. A field that can be absent needs one more immediate for absent: a sentinel address for `string_t` and `S *`, a header tag for `S`, and an `any_t` tag for every other type.
|
- **An op on a sum** tests which member the value is (by kind, by a sentinel, or by the cell's number in the header), and then works on that member.
|
||||||
- **Conversions.** A type grows in a join, so most conversions go to a representation that holds more: a member into its sum, or `int32_t` into `double`. These cannot fail. A conversion to a representation that holds less checks the value, and a failed check is a `TypeError` (section 8). For example, a `string_t` that can be undefined, passed to a native's string param.
|
- **An array's struct** has `length`, `cap` and `items` after its header. An element holds undefined for a hole.
|
||||||
- **An op on a sum** tests which member the value is: a kind test on an `any_t`, a sentinel test for null and undefined, and a `switch` on the site in the header for objects. Each case then works on one member, as for a single type. A member on which the op throws (a field read on null) has no case, so it throws.
|
|
||||||
- **A read, a write or a call on `any_t`** of type `Any` is a helper that emit writes: a `switch` on the site in the object's header, with one case for each `in_any` site (section 3.4). Every field name is known at compile time, so no hash map is needed. One comes with computed field names (section 8).
|
|
||||||
|
|
||||||
## 5. Values: immutable shapes
|
## 6. Not in this model yet
|
||||||
|
|
||||||
**The rule.** Section 1 gives the two ways to see a copy of an object: a write through one holder that another holder reads later, and an identity test. An object that is never written after its creation, and never in an identity test, has neither. So it behaves like a primitive: a copy of it and a shared pointer to it cannot be told apart. It can be a C struct value, copied freely, with any number of holders.
|
- a bound on the walks: infer must end on every program (section 4)
|
||||||
|
- definite initialization: a field that a constructor writes before `this` goes anywhere is never absent
|
||||||
**The flags.** Infer sets three flags on each site. Each flag only becomes true.
|
- narrowing in branches (`if (x !== null)`)
|
||||||
|
- objects as struct values, and objects on the stack (escape analysis)
|
||||||
- **written:** a field write that is not a creation write reaches an object of the site. A creation write is a `define_field` into the value that a creating op gives, before the object goes anywhere: an object literal's fields, a closure's captured boxes. Regify turns a `define_field` into anything else (a class field on `this`, a static field on the class) into a `put_field`. So every `define_field` is a creation write, and every `put_field` is a write after creation.
|
- fields with computed names (`o[k]` on an object that is not an array), `delete`, `in`
|
||||||
- **compared:** an object of the site is an operand of `===`, `!==`, `==` or `!=`, and the other operand can also be an object (`Obj` or `Any`).
|
- typed arrays, `ArrayBuffer`, `DataView`, and iterators
|
||||||
- **in_any:** an object of the site can be in an `Any` (section 3.4). Code that does not know the site can write it or compare it.
|
- `throw`, `try`, `catch`, `finally`, and the TDZ
|
||||||
|
|
||||||
**Value shapes.** A site has a value shape if no flag is set, it is not an array, it is not a member of a sum (an element of an array is in the sum `T | Undefined`), and it does not contain itself through value fields. A sum is one C type for all its members, so its object members are pointers. A struct cannot contain itself, so a site that does stays a pointer. An object of an immutable site cannot hold itself, but objects of one site can hold each other, as in a list.
|
|
||||||
|
|
||||||
**Representation.** An object of a value shape is a struct value (`S`), in its holder: a C local, a param, a return value, or the struct of the object whose field holds it. In a field that can be absent, its header's site is the tag for absent (`JSRT_SITE_ABSENT`). It is not in a sum and not `in_any`, so it is never an `any_t` value. A runtime operation that only reads it (ToString, `<`) gets a heap copy.
|
|
||||||
|
|
||||||
**Default.** An object of any other shape is a pointer to a cell (`S *`).
|
|
||||||
|
|
||||||
**Soundness.** No write after creation can show a copy, and no identity test can. A creation write happens before the object goes anywhere, so no copy exists yet.
|
|
||||||
|
|
||||||
## 6. Escape
|
|
||||||
|
|
||||||
**Question.** Where can a cell still be reached: after its frame returns, or when the op that made it runs again?
|
|
||||||
|
|
||||||
**Result.** One of three values for each value that a creating op gives:
|
|
||||||
|
|
||||||
- **`escapes`** if one of these is true:
|
|
||||||
- After the frame of its fn returns, the cell can be reached in a way other than as the return value itself. For example, it is stored in a field of a cell that is not `local`, stored in a box that is not `local`, or given to a callee. Every call operand counts as kept, because the analysis does not know the callee.
|
|
||||||
- A value that can reach the cell is still used when the creating op runs again: the op is in a loop, and the cell reaches a var or a join slot, which can keep it until the next run.
|
|
||||||
- **`returned`** if it does not escape, the return value is the only way to reach it after the return, it is the only object that a return of the fn can give, and its creating op runs at most once in each call.
|
|
||||||
- **`local`** otherwise: no holder outlives the frame.
|
|
||||||
|
|
||||||
A box is a cell, so the same rules apply to boxes.
|
|
||||||
|
|
||||||
**Narrowing.**
|
|
||||||
|
|
||||||
- **`local`:** the cell is on the stack of its frame. The value is still a pointer (`S *`) to it, so only the creating op changes. A C compiler can also split such a cell into registers if its address goes nowhere.
|
|
||||||
- **`returned`:** the caller allocates the cell. The variant gets one more param: a pointer to the cell to build into. The callee builds the object there and does not allocate. In the caller, the call is the creating op of the cell. So the caller's escape result for the call's dst chooses the stack or the heap, and the decision goes up through any number of fns that make and return objects.
|
|
||||||
|
|
||||||
**Default.** A cell that escapes is on the heap (`jsrt_alloc`).
|
|
||||||
|
|
||||||
**Soundness.**
|
|
||||||
|
|
||||||
- A stack cell lives until its frame returns, and its creating op uses the same memory again each time it runs. `local` excludes both cases.
|
|
||||||
- A `returned` object is in the caller's cell from the start and never moves. So every pointer to it stays valid, including a pointer from the object to itself.
|
|
||||||
|
|
||||||
**Where it is used.** Emit chooses the allocation of every cell in one function, `alloc()`. A creating op calls it, and so does a call of a variant whose result is `returned`.
|
|
||||||
|
|
||||||
## 7. Other narrowings
|
|
||||||
|
|
||||||
| narrowing | condition | if not proved |
|
|
||||||
|---|---|---|
|
|
||||||
| no absent in a field | a write to the field comes before every read of it (definite initialization). Object-literal fields have this: QuickJS fills them with `define_field` before the object goes anywhere | the field can be absent (section 3.3) |
|
|
||||||
| a captured binding as a value | no write reaches the binding after its first capture. Most `const` bindings have this. A `const` that a hoisted fn captures before its declaration does not | a box |
|
|
||||||
| a direct call | the callee's type is `Obj(site)`: its code is known | a call on `Any`: a `switch` on the site (section 3.4) |
|
|
||||||
|
|
||||||
## 8. Out of scope
|
|
||||||
|
|
||||||
These are not part of this model, and they need their own design:
|
|
||||||
|
|
||||||
- narrowing in branches: in `if (x !== null) { x.f }`, `x` keeps its whole type. This is correct, because the `Null` member of the read throws, but the C still tests for null
|
|
||||||
- `throw`, `try`, `catch` and `finally`. These include the `TypeError`s above: a call of a value that is not a fn, and a failed check of a conversion from `any_t`.
|
|
||||||
- the TDZ (a read of a `let` before its declaration)
|
|
||||||
- fields with computed names (`o[k]`), `delete`, `in`. The absent state is the base that `in` and `delete` need.
|
|
||||||
- typed arrays, `ArrayBuffer` and `DataView`
|
|
||||||
- iterators: `for…of`, spread and destructuring
|
|
||||||
- getters and setters
|
- getters and setters
|
||||||
|
|||||||
+1
-1
@@ -150,7 +150,7 @@ bool __any_is_string(any_t a) { return a.kind == JSRT_KIND_STRING; }
|
|||||||
bool __any_is_object(any_t a) { return a.kind == JSRT_KIND_OBJECT; }
|
bool __any_is_object(any_t a) { return a.kind == JSRT_KIND_OBJECT; }
|
||||||
|
|
||||||
bool __any_is_function(any_t a) {
|
bool __any_is_function(any_t a) {
|
||||||
return a.kind == JSRT_KIND_OBJECT && jsrt_fn_sites[((jsrt_header *)a.as.o)->site];
|
return a.kind == JSRT_KIND_OBJECT && jsrt_fn_cells[((jsrt_header *)a.as.o)->cell];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── any: abstract operations ──
|
// ── any: abstract operations ──
|
||||||
|
|||||||
+7
-13
@@ -12,23 +12,18 @@ typedef struct { char _; } null_t;
|
|||||||
// A string: UTF-16 code units, as JS counts them
|
// A string: UTF-16 code units, as JS counts them
|
||||||
typedef struct jsrt_string *string_t;
|
typedef struct jsrt_string *string_t;
|
||||||
|
|
||||||
// The start of every object: the site that created it, which tells its shape
|
// The start of every object: the number of its cell, which tells its struct
|
||||||
// and, for a fn, its code
|
// and, for a fn, its code
|
||||||
typedef struct { uint32_t site; } jsrt_header;
|
typedef struct { uint32_t cell; } jsrt_header;
|
||||||
|
|
||||||
// The sites below 3 are tags. An object of a value shape is a struct, not a
|
// A pointer to an object or a string can also be one of these. NULL is null.
|
||||||
// pointer, so its header tells if it is undefined, null or absent instead
|
// They are told apart by their address only
|
||||||
#define JSRT_SITE_UNDEFINED 0
|
|
||||||
#define JSRT_SITE_NULL 1
|
|
||||||
#define JSRT_SITE_ABSENT 2
|
|
||||||
|
|
||||||
// A pointer to an object or a string can also be one of these. NULL is null
|
|
||||||
extern jsrt_header jsrt_undefined_cell, jsrt_absent_cell;
|
extern jsrt_header jsrt_undefined_cell, jsrt_absent_cell;
|
||||||
#define JSRT_UNDEFINED ((void *)&jsrt_undefined_cell)
|
#define JSRT_UNDEFINED ((void *)&jsrt_undefined_cell)
|
||||||
#define JSRT_ABSENT ((void *)&jsrt_absent_cell) // a field that an object lacks
|
#define JSRT_ABSENT ((void *)&jsrt_absent_cell) // a field that an object lacks
|
||||||
|
|
||||||
// Whether the objects of each site are fns, by site. The emitted C defines it
|
// Whether the objects of each cell are fns, by cell. The emitted C defines it
|
||||||
extern const bool jsrt_fn_sites[];
|
extern const bool jsrt_fn_cells[];
|
||||||
|
|
||||||
// A new heap cell of `size` bytes, all zero
|
// A new heap cell of `size` bytes, all zero
|
||||||
void *jsrt_alloc(size_t size);
|
void *jsrt_alloc(size_t size);
|
||||||
@@ -48,8 +43,7 @@ void *jsrt_some(void *p);
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
jsrt_header jsrt_undefined_cell = { JSRT_SITE_UNDEFINED };
|
jsrt_header jsrt_undefined_cell, jsrt_absent_cell;
|
||||||
jsrt_header jsrt_absent_cell = { JSRT_SITE_ABSENT };
|
|
||||||
|
|
||||||
void *jsrt_alloc(size_t size) {
|
void *jsrt_alloc(size_t size) {
|
||||||
void *p = calloc(1, size);
|
void *p = calloc(1, size);
|
||||||
|
|||||||
Reference in New Issue
Block a user