Locked in type model

This commit is contained in:
2026-10-08 07:28:51 +00:00
parent 2b612d19ed
commit 316a2fcd75
13 changed files with 1707 additions and 1358 deletions
+72 -108
View File
@@ -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 = &reg_fns[fn_id];
let mut val_types = vec![Type::Unknown; f.val_count];
let mut instr_types = vec![Type::Unknown; f.code.len()];
let mut ret_type = Type::Unknown;
// The cells that this walk made in the rounds before. A recursive call of
// the walk does not walk it, so it does not need them
let mut cells = std::mem::take(&mut cache.walks[w].cells);
// arg `i` is value id `var_count + i` (see regify)
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,