Locked in type model
This commit is contained in:
+72
-108
@@ -1,9 +1,17 @@
|
||||
//! Infers a type for every value (see `docs/model.md`, section 4)
|
||||
//!
|
||||
//! A request is a fn, the call place that calls it, and the types of its args.
|
||||
//! Its result is the type of each of its values. An object is named by the
|
||||
//! instr that makes it and the call place of the request that runs the instr,
|
||||
//! and each name has one cell for the whole program. Both come from the
|
||||
//! program text, so there are finitely many requests and names, and infer
|
||||
//! ends. A walk reads cells, protos and the returns of other requests, which
|
||||
//! only grow, so infer walks the program in rounds until one changes nothing
|
||||
|
||||
pub mod cells;
|
||||
pub mod types;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use crate::{
|
||||
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
|
||||
@@ -15,15 +23,17 @@ use crate::{
|
||||
use cells::{Cell, Code, LENGTH_FIELD, ObjKind, Protos, read_value};
|
||||
use types::{BaseType, EMPTY_STRING, Type, is_function, join, loose_eq, strict_eq, test, truthiness, typeof_name};
|
||||
|
||||
/// The walk of one fn with one list of arg types
|
||||
#[derive(Debug)]
|
||||
pub struct Walk {
|
||||
/// A call place: a fn and the index of its call instr
|
||||
pub type Place = (usize, usize);
|
||||
|
||||
/// The call place of the entry fn, which the host calls
|
||||
pub const HOST: Place = (usize::MAX, 0);
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct InferRequest {
|
||||
pub fn_id: usize,
|
||||
pub arg_types: Vec<Type>, // one for each param
|
||||
cells: Vec<(usize, Cell)>, // the cells it made, by the instr that makes their objects
|
||||
ret: Type, // what it returned in the rounds so far: a recursive call gives that
|
||||
walking: bool,
|
||||
pub result: Option<InferResult>, // the walk in the current round, once it is done
|
||||
pub place: Place,
|
||||
pub arg_types: Vec<Type>, // one for each param
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -34,30 +44,38 @@ pub struct InferResult {
|
||||
pub reachable: Vec<bool>, // the blocks that can run
|
||||
}
|
||||
|
||||
/// The walks of the program, by their keys
|
||||
/// A request and its result
|
||||
#[derive(Debug)]
|
||||
pub struct Walk {
|
||||
pub req: InferRequest,
|
||||
ret: Type, // what it returned in the rounds so far: a recursive call gives that
|
||||
walking: bool,
|
||||
pub result: Option<InferResult>, // in the current round, once it is done
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TypeCache {
|
||||
pub walks: Vec<Walk>,
|
||||
keys: HashMap<(usize, Vec<Type>), usize>, // the walk of each fn and list of arg types
|
||||
ids: HashMap<InferRequest, usize>, // the walk of each request
|
||||
objects: BTreeMap<(usize, usize, Place), Cell>, // the cell of each name: the fn and instr that make the objects, and the call place
|
||||
builtins: Vec<Cell>, // the cells of the natives, then of the proto links
|
||||
pub protos: Protos, // the prototype of each `ProtoOf`, as the stdlib links it
|
||||
version: usize, // goes up on every change of a cell, a walk's return or a proto, and on every new cell or walk
|
||||
version: usize, // goes up on every change of a cell, a return or a proto, and on every new cell or walk
|
||||
}
|
||||
|
||||
impl TypeCache {
|
||||
/// The key of a call of `fn_id` with args of these types: the fn, and the
|
||||
/// type of each of its params. An arg it does not pass is undefined, and
|
||||
/// an arg it passes beyond them is never read. Only an arg's kind crosses
|
||||
/// a call, not a known value, else every literal passed would make a new
|
||||
/// walk. An object is its cell
|
||||
fn key(fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> (usize, Vec<Type>) {
|
||||
/// The request of a call of `fn_id` from `place` with args of these types.
|
||||
/// An arg it does not pass is undefined, and an arg it passes beyond its
|
||||
/// params is never read. Only an arg's kind crosses a call, not a known
|
||||
/// value, else every literal passed would make a new walk
|
||||
fn request(fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> InferRequest {
|
||||
let params = (0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::Undefined, Type::widen));
|
||||
(fn_id, params.collect())
|
||||
InferRequest { fn_id, place, arg_types: params.collect() }
|
||||
}
|
||||
|
||||
/// The walk of a call that infer walked
|
||||
pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
|
||||
self.keys[&Self::key(fn_id, arg_types, fns)]
|
||||
pub fn find(&self, fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> usize {
|
||||
self.ids[&Self::request(fn_id, place, arg_types, fns)]
|
||||
}
|
||||
|
||||
/// The cell of a built-in fn
|
||||
@@ -74,44 +92,19 @@ impl TypeCache {
|
||||
&self.builtins
|
||||
}
|
||||
|
||||
/// Every cell: the built-in ones, then the ones that each walk made
|
||||
/// Every cell: the built-in ones, then one for each name
|
||||
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)
|
||||
self.builtins.iter().chain(self.objects.values())
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
/// The cell of the objects of `name`, made the first time
|
||||
fn object(&mut self, name: (usize, usize, Place), kind: ObjKind) -> Cell {
|
||||
if let Some(cell) = self.objects.get(&name) {
|
||||
return cell.clone();
|
||||
}
|
||||
|
||||
None
|
||||
self.version += 1;
|
||||
self.objects.entry(name).or_insert(Cell::new(kind)).clone()
|
||||
}
|
||||
|
||||
/// Count a change, if there was one
|
||||
@@ -119,24 +112,17 @@ impl TypeCache {
|
||||
self.version += changed as usize;
|
||||
}
|
||||
|
||||
/// Infer a call of `fn_id`, walking it unless the walk of its key is done
|
||||
/// in this round. Gives what it returns
|
||||
fn call(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> Type {
|
||||
let key = Self::key(fn_id, arg_types, fns);
|
||||
/// Infer a call of `fn_id` from `place`, walking it unless its request is
|
||||
/// done in this round. Gives what it returns
|
||||
fn call(&mut self, fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> Type {
|
||||
let req = Self::request(fn_id, place, arg_types, fns);
|
||||
|
||||
let w = match self.keys.get(&key) {
|
||||
let w = match self.ids.get(&req) {
|
||||
Some(&w) => w,
|
||||
None => {
|
||||
self.version += 1;
|
||||
self.walks.push(Walk {
|
||||
fn_id,
|
||||
arg_types: key.1.clone(),
|
||||
cells: vec![],
|
||||
ret: Type::Unknown,
|
||||
walking: false,
|
||||
result: None,
|
||||
});
|
||||
self.keys.insert(key, self.walks.len() - 1);
|
||||
self.ids.insert(req.clone(), self.walks.len());
|
||||
self.walks.push(Walk { req, ret: Type::Unknown, walking: false, result: None });
|
||||
self.walks.len() - 1
|
||||
}
|
||||
};
|
||||
@@ -151,15 +137,13 @@ impl TypeCache {
|
||||
let res = walk_fn(w, fns, self);
|
||||
let ret = res.ret_type.clone();
|
||||
|
||||
let last = &self.walks[w].ret;
|
||||
let joined = join(last, &ret);
|
||||
let grew = joined != *last;
|
||||
self.changed(grew);
|
||||
|
||||
let walk = &mut self.walks[w];
|
||||
let joined = join(&walk.ret, &ret);
|
||||
let grew = joined != walk.ret;
|
||||
walk.walking = false;
|
||||
walk.ret = joined;
|
||||
walk.result = Some(res);
|
||||
self.changed(grew);
|
||||
ret
|
||||
}
|
||||
}
|
||||
@@ -185,13 +169,13 @@ fn agree2<T: PartialEq>(a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option<T
|
||||
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
|
||||
/// `walk_fn()`). Gives what it returns
|
||||
fn call_code(cache: &mut TypeCache, code: Code, arg_types: &[Type], fns: &[RegFn]) -> Type {
|
||||
/// A call of `code` from `place` with operands of these types (see the call
|
||||
/// rule in `walk_fn()`). Gives what it returns
|
||||
fn call_code(cache: &mut TypeCache, code: Code, arg_types: &[Type], fns: &[RegFn], place: Place) -> Type {
|
||||
let args = &arg_types[FIRST_ARG as usize..];
|
||||
|
||||
match code {
|
||||
Code::Fn(fn_id) => cache.call(fn_id, arg_types, fns),
|
||||
Code::Fn(fn_id) => cache.call(fn_id, place, arg_types, fns),
|
||||
Code::Native(id) => native::ret_type(id, args),
|
||||
Code::ProtoLink(kind) => {
|
||||
let linked = &cache.protos[kind as usize];
|
||||
@@ -214,24 +198,19 @@ fn check_index(index: &Type) {
|
||||
}
|
||||
|
||||
/// Walk the program from the entry, in rounds, until a round changes
|
||||
/// nothing. A walk reads cells, protos and the returns of other walks, which
|
||||
/// walks after it can still add to. All of them only grow, so the rounds end.
|
||||
/// The results are those of the last round
|
||||
/// nothing. The results are those of the last round
|
||||
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
|
||||
let mut cache = TypeCache {
|
||||
walks: vec![],
|
||||
keys: HashMap::new(),
|
||||
ids: HashMap::new(),
|
||||
objects: BTreeMap::new(),
|
||||
builtins: vec![],
|
||||
protos: std::array::from_fn(|_| Type::Unknown),
|
||||
version: 0,
|
||||
};
|
||||
|
||||
let codes = (0..NATIVES.len()).map(Code::Native).chain(ProtoOf::ALL.map(Code::ProtoLink));
|
||||
|
||||
for code in codes {
|
||||
let cell = cache.new_cell(ObjKind::Fn(code), None);
|
||||
cache.builtins.push(cell);
|
||||
}
|
||||
cache.builtins = codes.map(|code| Cell::new(ObjKind::Fn(code))).collect();
|
||||
|
||||
loop {
|
||||
let version = cache.version;
|
||||
@@ -239,7 +218,7 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache {
|
||||
|
||||
// The synthetic program entry is appended last by `flatten`. It is
|
||||
// called by the host with no args
|
||||
cache.call(fns.len() - 1, &[], fns);
|
||||
cache.call(fns.len() - 1, HOST, &[], fns);
|
||||
|
||||
if cache.version == version {
|
||||
return cache;
|
||||
@@ -250,18 +229,14 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache {
|
||||
fn walk_fn(w: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
|
||||
use OpCode::*;
|
||||
|
||||
let fn_id = cache.walks[w].fn_id;
|
||||
let (fn_id, place) = (cache.walks[w].req.fn_id, cache.walks[w].req.place);
|
||||
let f = ®_fns[fn_id];
|
||||
let mut val_types = vec![Type::Unknown; f.val_count];
|
||||
let mut instr_types = vec![Type::Unknown; f.code.len()];
|
||||
let mut ret_type = Type::Unknown;
|
||||
|
||||
// The cells that this walk made in the rounds before. A recursive call of
|
||||
// the walk does not walk it, so it does not need them
|
||||
let mut cells = std::mem::take(&mut cache.walks[w].cells);
|
||||
|
||||
// arg `i` is value id `var_count + i` (see regify)
|
||||
for (i, ty) in cache.walks[w].arg_types.iter().enumerate() {
|
||||
for (i, ty) in cache.walks[w].req.arg_types.iter().enumerate() {
|
||||
val_types[f.var_count + i] = ty.clone();
|
||||
}
|
||||
|
||||
@@ -304,18 +279,9 @@ fn walk_fn(w: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
|
||||
let idx = block.start + offset;
|
||||
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
|
||||
})
|
||||
};
|
||||
// The cell of the objects that this instr makes in this
|
||||
// request: the one of its name
|
||||
let cell_here = |cache: &mut TypeCache, kind: ObjKind| cache.object((fn_id, idx, place), kind);
|
||||
|
||||
let ty = match instr.op {
|
||||
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
|
||||
@@ -497,7 +463,7 @@ fn walk_fn(w: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
|
||||
arg_types.clear();
|
||||
arg_types.extend(instr.args.iter().map(|&v| val_types[v].clone()));
|
||||
arg_types[0] = callee.clone();
|
||||
call_code(cache, code, &arg_types, reg_fns)
|
||||
call_code(cache, code, &arg_types, reg_fns, (fn_id, idx))
|
||||
});
|
||||
|
||||
// `new` gives what the constructor returns if that is an
|
||||
@@ -564,8 +530,6 @@ fn walk_fn(w: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
|
||||
}
|
||||
} // loop
|
||||
|
||||
cache.walks[w].cells = cells;
|
||||
|
||||
InferResult {
|
||||
val_types,
|
||||
instr_types,
|
||||
|
||||
Reference in New Issue
Block a user