Broken site based impl

This commit is contained in:
2026-10-03 14:06:24 +00:00
parent 23a2c3bc46
commit 44498b32a9
39 changed files with 4250 additions and 1750 deletions
+1
View File
@@ -4,5 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
cc = "1.5.1"
nom = "8.0.0"
num_enum = "0.7.6"
+4
View File
@@ -1,6 +1,10 @@
const QUICKJS: &str = "../third_party/quickjs/build";
fn main() {
// The compiler compiles the C it emits for the target it is built for (see
// main.rs)
println!("cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap());
println!("cargo:rustc-link-search=native={QUICKJS}");
println!("cargo:rustc-link-lib=static=qjs");
+6 -4
View File
@@ -12,13 +12,13 @@ pub enum Escape {
Escapes,
}
/// The escape of each value that `object` or `fclosure` gives. Every other
/// 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) {
if matches!(instr.op, Object | Fclosure | ArrayFrom) {
let cell = instr.dsts[0];
flow.new_walk();
@@ -34,7 +34,8 @@ pub fn analyze(f: &RegFn, flow: &mut Flow) -> Vec<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, a callee and a return can keep it beyond the frame
/// 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);
@@ -43,7 +44,8 @@ fn escapes(flow: &mut Flow, v: usize, looped: bool) -> bool {
let out = match u.op {
Call | CallMethod | CallConstructor | Return | ReturnAsync => true,
DefineField | PutField if u.operand == 0 => 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;
+719 -359
View File
File diff suppressed because it is too large Load Diff
+318 -262
View File
@@ -1,28 +1,30 @@
//! Infers a type for every value (see `docs/model.md`, section 3)
pub mod sites;
pub mod types;
use std::collections::HashMap;
use crate::{
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native,
native::{self, ProtoOf},
regify::RegFn,
transform::{FIRST_ARG, THIS_ARG},
};
use types::{Code, Heap, Number, Site, Type};
use sites::{Code, CtxId, LENGTH_FIELD, Place, Protos, SiteId, SiteTable};
use types::{BaseType, EMPTY_STRING, Type, is_function, loose_eq, strict_eq, test, truthiness, typeof_name};
const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
/// One walk of a fn with one tuple of arg types (see `docs/model.md`,
/// section 3.4)
/// section 3.5)
#[derive(Debug)]
pub struct Variant {
pub fn_id: usize,
/// One for each of the fn's params (see `TypeCache::variant()`)
pub arg_types: Vec<Type>,
/// The context of the sites that it makes (see `SiteTable::context()`)
pub ctx: CtxId,
/// The walk in the current round, once it is done
pub result: Option<InferResult>,
/// What it returned in the rounds so far, which is what a call of it gives
@@ -48,9 +50,9 @@ pub struct TypeCache {
by_args: Vec<HashMap<Vec<Type>, usize>>,
/// Scratch for a call's arg types
key: Vec<Type>,
pub heap: Heap,
pub site_table: SiteTable,
/// The prototype of each primitive kind, as the stdlib links it, by kind
pub protos: [Type; 3],
pub protos: Protos,
/// Goes up when a variant's return or a proto changes (see `infer_types()`)
changes: usize,
}
@@ -62,8 +64,9 @@ impl TypeCache {
/// arg's kind crosses a call, not a known value, else every literal passed
/// would make a new variant
pub fn variant(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
let site_table = &mut self.site_table;
self.key.clear();
self.key.extend(params(fn_id, arg_types, fns));
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;
@@ -74,8 +77,9 @@ impl TypeCache {
self.variants.push(Variant {
fn_id,
arg_types: self.key.clone(),
ctx: self.site_table.context(&self.key),
result: None,
ret: Type::EMPTY,
ret: Type::Unknown,
walking: false,
});
@@ -84,10 +88,47 @@ impl TypeCache {
/// The variant for a call that infer walked
pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
let key: Vec<Type> = params(fn_id, arg_types, fns).collect();
let site_table = &self.site_table;
let key: Vec<Type> = params(fn_id, arg_types, fns).map(|ty| site_table.widened(&ty)).collect();
self.by_args[fn_id][&key]
}
/// The join of `f` over the members of `ty` (see `SiteTable::members()`).
/// The lists of sums only grow, so a member stays where it is while `f`
/// adds sums
fn map(&mut self, ty: Type, mut f: impl FnMut(&mut Self, Type) -> Type) -> Type {
let mut out = Type::Unknown;
for i in 0..self.site_table.members(&ty).len() {
let member = self.site_table.members(&ty)[i];
let ty = f(self, member);
out = self.site_table.join(&out, &ty);
}
out
}
/// The join of `f` over each pair of members of `a` and `b`
fn map2(&mut self, a: Type, b: Type, mut f: impl FnMut(&mut Self, Type, Type) -> Type) -> Type {
self.map(a, |cache, a| cache.map(b, |cache, b| f(cache, a, b)))
}
/// What `f` gives for every member of `ty`, if all members give the same
fn agree<T: PartialEq>(&self, ty: &Type, f: impl Fn(&Type) -> Option<T>) -> Option<T> {
let mut members = self.site_table.members(ty).iter().map(f);
let first = members.next()??;
members.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// What `f` gives for every pair of members of `a` and `b`, if all pairs
/// give the same
fn agree2<T: PartialEq>(&self, a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option<T>) -> Option<T> {
let members = |t| self.site_table.members(t).iter();
let mut pairs = members(a).flat_map(|x| members(b).map(move |y| (x, y))).map(|(x, y)| f(x, y));
let first = pairs.next()??;
pairs.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// Infer a call of `fn_id`, walking it unless an identical call was
/// already walked in this round. Gives what it returns
fn call(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> Type {
@@ -104,7 +145,7 @@ impl TypeCache {
let ret = res.ret_type;
let last = self.variants[v].ret;
let joined = self.heap.join(&last, &ret);
let joined = self.site_table.join(&last, &ret);
self.changes += (joined != last) as usize;
let variant = &mut self.variants[v];
@@ -116,9 +157,73 @@ impl TypeCache {
}
/// The type of each of fn `fn_id`'s params, for a call with args of these
/// types (see `TypeCache::variant()`)
/// types, before known values are dropped (see `TypeCache::variant()`)
fn params<'a>(fn_id: usize, arg_types: &'a [Type], fns: &[RegFn]) -> impl Iterator<Item = Type> + 'a {
(0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::UNDEFINED, Type::widen))
(0..fns[fn_id].arg_count).map(|i| arg_types.get(i).copied().unwrap_or(Type::Undefined))
}
/// 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 {
let args = &arg_types[FIRST_ARG as usize..];
match code {
Code::Fn(fn_id) => cache.call(fn_id, arg_types, fns),
Code::Native(id) => native::ret_type(id, args, &cache.site_table),
Code::ProtoLink(kind) => {
let linked = cache.protos[kind as usize];
let joined = cache.site_table.join(&linked, args.first().unwrap_or(&Type::Undefined));
cache.changes += (joined != linked) as usize;
cache.protos[kind as usize] = joined;
Type::Undefined
}
}
}
/// A call of a callee of type `Any`, with operands of these types: a call of
/// any fn object in an `Any`, with every operand an `Any`, except that the
/// callee is its own env. That is the variant that emit's dispatch on `Any`
/// calls (see `call_any_variant()`). Gives an `Any`
fn call_any(cache: &mut TypeCache, arg_types: &mut [Type], fns: &[RegFn]) -> Type {
for ty in arg_types.iter_mut() {
*ty = cache.site_table.to_any(ty);
}
for site in 0..cache.site_table.sites.len() as SiteId {
let Some(code) = cache.site_table.site(site).code().filter(|_| cache.site_table.site(site).in_any) else {
continue;
};
arg_types[0] = Type::Obj(site);
let ret = call_code(cache, code, arg_types, fns);
cache.site_table.to_any(&ret);
}
Type::Any
}
/// The site that `place` makes in context `ctx`, made if it is new
fn site_at(cache: &mut TypeCache, place: Place, ctx: CtxId) -> SiteId {
let place = cache.site_table.place_id(place);
cache.site_table.site_at(place, ctx)
}
/// An array index must be a number. Any other key is a computed field name,
/// which is not supported
fn check_index(cache: &TypeCache, index: &Type) {
for member in cache.site_table.members(index) {
if !matches!(member, Type::Int | Type::Float) {
unimplemented!("`a[k]` with a key of type {member:?}: computed field names");
}
}
}
/// The operand types of the variant that a call on `Any`, with `n_args`
/// args, runs for a fn object of `site`
pub fn call_any_variant(site: SiteId, n_args: usize) -> Vec<Type> {
let mut arg_types = vec![Type::Any; FIRST_ARG as usize + n_args];
arg_types[0] = Type::Obj(site);
arg_types
}
/// Walk every variant from the entry, in rounds, until a round changes
@@ -130,20 +235,20 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache {
variants: vec![],
by_args: fns.iter().map(|_| HashMap::new()).collect(),
key: vec![],
heap: Heap::new(fns),
protos: [Type::EMPTY; 3],
site_table: SiteTable::new(fns),
protos: [Type::Unknown; ProtoOf::ALL.len()],
changes: 0,
};
loop {
let before = (cache.heap.version, cache.changes);
let before = (cache.site_table.version, cache.changes);
cache.variants.iter_mut().for_each(|v| v.result = None);
// The synthetic program entry is appended last by `flatten`. It is
// called by the host with no args
cache.call(fns.len() - 1, &[], fns);
if before == (cache.heap.version, cache.changes) {
if before == (cache.site_table.version, cache.changes) {
return cache;
}
}
@@ -152,11 +257,11 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache {
fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
use OpCode::*;
let fn_id = cache.variants[variant].fn_id;
let (fn_id, ctx) = (cache.variants[variant].fn_id, cache.variants[variant].ctx);
let f = &reg_fns[fn_id];
let mut val_types = vec![Type::EMPTY; f.val_count];
let mut instr_types = vec![Type::EMPTY; f.code.len()];
let mut ret_type = Type::EMPTY;
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;
// arg `i` is value id `var_count + i` (see regify)
// Init them with the arg types passed in
@@ -170,13 +275,13 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
// Scratch for calls, kept across instrs
let mut arg_types: Vec<Type> = vec![];
let mut codes: Vec<Code> = vec![];
// Walk instrs till nothing changes. A return only changes `ret_type` once
// its block is reachable, and a field read only gives more once the heap
// changes, so the values, the blocks and the heap are enough to watch
// its block is reachable, and a field read only gives more once the site
// table changes, so the values, the blocks and the site table are enough
// to watch
loop {
let version = cache.heap.version;
let version = cache.site_table.version;
let mut changed = false;
for (block_idx, block) in f.blocks.iter().enumerate() {
@@ -190,9 +295,9 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
for (offset, instr) in code.enumerate() {
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
// A rule with an empty operand gives an empty result: no value
// has reached the instr yet
let waits = instr.args.iter().any(|&v| val_types[v].is_empty());
// A rule with an `Unknown` operand gives `Unknown`: no value has
// reached the instr yet
let waits = instr.args.iter().any(|&v| val_types[v] == Type::Unknown);
if (instr.dsts.is_empty() && !is_ret) || waits {
continue;
@@ -205,162 +310,231 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
let ty = match instr.op {
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7
| PushI8 | PushI16 | PushI32 => Type::number(Number::Int),
| PushI8 | PushI16 | PushI32 => Type::Int,
PushTrue => Type::bool(Some(true)),
PushFalse => Type::bool(Some(false)),
PushAtomValue => Type::string(Some(instr.imm.atom())),
PushEmptyString => Type::string(Some(EMPTY_STRING)),
PushTrue => Type::Bool(Some(true)),
PushFalse => Type::Bool(Some(false)),
PushAtomValue => Type::Str(Some(instr.imm.atom())),
PushEmptyString => Type::Str(Some(EMPTY_STRING)),
PushConst | PushConst8 => match f.cpool[instr.imm.const_idx()] {
JsValue::Int(_) => Type::number(Number::Int),
JsValue::Float(_) => Type::number(Number::Float),
JsValue::String(_) => Type::string(None),
JsValue::BoolTrue => Type::bool(Some(true)),
JsValue::BoolFalse => Type::bool(Some(false)),
JsValue::Undefined => Type::UNDEFINED,
JsValue::NativeFn(id) => cache.heap.create(cache.heap.builtin(Site::Native(id))),
JsValue::ProtoLink(kind) => cache.heap.create(cache.heap.builtin(Site::ProtoLink(kind))),
JsValue::Int(_) => Type::Int,
JsValue::Float(_) => Type::Float,
JsValue::String(_) => Type::Str(None),
JsValue::BoolTrue => Type::Bool(Some(true)),
JsValue::BoolFalse => Type::Bool(Some(false)),
JsValue::Undefined => Type::Undefined,
JsValue::NativeFn(id) => cache.site_table.create(cache.site_table.builtin(Place::Native(id))),
JsValue::ProtoLink(kind) => cache.site_table.create(cache.site_table.builtin(Place::ProtoLink(kind))),
_ => unreachable!(),
},
Undefined => Type::UNDEFINED,
Null => Type::NULL,
Undefined => Type::Undefined,
Null => Type::Null,
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
// `+` concatenates if either side becomes a string, which
// only a string or an object can
Add => {
let (l, r) = (arg(1), arg(0));
let is_string = |t: Type| t.is_only(Type::string(None));
let can_be_string = |t: Type| t.string.is_some() || t.object.is_some();
if is_number(l) && is_number(r) {
Type::number(l.number.max(r.number).unwrap())
} else if is_string(l) || is_string(r) {
Type::string(None)
} else if can_be_string(l) || can_be_string(r) {
Type { string: Some(None), ..Type::number(Number::Float) }
} else {
Type::number(Number::Float)
}
}
// a string or an object does. Else it adds numbers
Add => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) {
(Type::Any, _) | (_, Type::Any) => Type::Any,
(l, r) if [l, r].iter().any(|t| matches!(t, Type::Str(_) | Type::Obj(_))) => Type::Str(None),
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}),
// Any other operator converts its operands to numbers
Sub | Mul | Mod | Pow => match (arg(1), arg(0)) {
(l, r) if is_int(l) && is_int(r) => Type::number(Number::Int),
_ => Type::number(Number::Float),
},
Neg | Plus | Inc | Dec | PostInc | PostDec => match arg(0) {
t if is_int(t) => Type::number(Number::Int),
_ => Type::number(Number::Float),
},
Div => Type::number(Number::Float),
Sub | Mul | Mod | Pow => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) {
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}),
Neg | Plus | Inc | Dec | PostInc | PostDec => cache.map(arg(0), |_, t| match t {
Type::Int => Type::Int,
_ => Type::Float,
}),
Div => Type::Float,
// Bitwise NOT gives an int32
Not => Type::number(Number::Int),
Not => Type::Int,
Lt | Lte | Gt | Gte => Type::bool(None),
Lt | Lte | Gt | Gte => Type::Bool(None),
// ─────────────────────────────── Conditions the types may decide ──────────────────────────────────────────
// A known value lets infer skip the branch that cannot run
// (see `reachable` below)
// An identity test of two objects (see `docs/model.md`, section 5)
StrictEq | StrictNeq | Eq | Neq if arg(0).object.is_some() && arg(1).object.is_some() => {
for obj in [arg(0).object, arg(1).object].into_iter().flatten() {
cache.heap.mark(obj, |shape| &mut shape.compared);
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);
}
}
}
Type::bool(None)
let eq = match instr.op {
StrictEq | StrictNeq => cache.agree2(&a, &b, strict_eq),
_ => cache.agree2(&a, &b, loose_eq),
};
Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq)))
}
StrictEq => Type::bool(strict_eq(&arg(0), &arg(1))),
StrictNeq => Type::bool(strict_eq(&arg(0), &arg(1)).map(|eq| !eq)),
Eq => Type::bool(loose_eq(&arg(0), &arg(1))),
Neq => Type::bool(loose_eq(&arg(0), &arg(1)).map(|eq| !eq)),
Lnot => Type::bool(truthiness(&arg(0)).map(|t| !t)),
Typeof => typeof_of(&arg(0), &cache.heap),
IsUndefined | TypeofIsUndefined => Type::bool(test(&arg(0), Type::UNDEFINED)),
IsNull => Type::bool(test(&arg(0), Type::NULL)),
IsUndefinedOrNull => Type::bool(test(&arg(0), Type { null: true, ..Type::UNDEFINED })),
TypeofIsFunction => Type::bool(is_function(&arg(0), &cache.heap)),
Lnot => Type::Bool(cache.agree(&arg(0), truthiness).map(|t| !t)),
Typeof => match cache.agree(&arg(0), |t| typeof_name(t, &cache.site_table)) {
Some(name) => Type::Str(Some(AtomRef::Predefined(name))),
None => Type::Str(None),
},
IsUndefined | TypeofIsUndefined => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined]))),
IsNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Null]))),
IsUndefinedOrNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))),
TypeofIsFunction => Type::Bool(cache.agree(&arg(0), |t| is_function(t, &cache.site_table))),
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
SetLoc | SetName => arg(0),
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
Object | Fclosure | Fclosure8 => cache.heap.create(cache.heap.site_at(fn_id, idx).unwrap()),
// The write goes into the object's shape, which every value
// that holds the object sees. A primitive has no fields to
// write, and undefined and null throw
DefineField | PutField => {
if let Some(obj) = arg(1).object {
cache.heap.write(obj, instr.imm.atom(), &arg(0));
Object | Fclosure | Fclosure8 => {
let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx);
cache.site_table.create(site)
}
// The elements are its operands, last first
ArrayFrom => {
let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx);
if !cache.heap.is_creation_write(fn_id, idx) {
cache.heap.mark(obj, |shape| &mut shape.written);
}
for &v in &instr.args {
cache.site_table.write_elem(site, &val_types[v]);
}
cache.heap.canon(&arg(1))
cache.site_table.create(site)
}
GetField => cache.heap.read_value(&arg(0), instr.imm.atom(), &cache.protos),
// `a[i]` and `a[i] = x`, on arrays, and `s[i]` on strings:
// an index is a number. Other objects need computed field
// names, which are not supported. A string cannot be
// written (module code is strict), and undefined and null
// throw
GetArrayEl => {
let index = arg(0);
cache.map(arg(1), |cache, obj| match obj {
Type::Obj(site) if cache.site_table.site(site).is_array() => {
check_index(cache, &index);
cache.site_table.read_elem(site)
}
Type::Str(_) => {
check_index(cache, &index);
cache.site_table.join(&Type::Str(None), &Type::Undefined)
}
Type::Undefined | Type::Null => Type::Unknown,
obj => unimplemented!("`o[k]` on {obj:?}: computed field names"),
})
}
PutArrayEl => {
let (value, index) = (arg(0), arg(1));
cache.map(arg(2), |cache, obj| {
match obj {
Type::Obj(site) if cache.site_table.site(site).is_array() => {
check_index(cache, &index);
cache.site_table.write_elem(site, &value);
cache.site_table.mark(site, |s| &mut s.written);
}
Type::Undefined | Type::Null | Type::Str(_) => {}
obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"),
}
Type::Unknown
});
arg(2)
}
// A key stays as it is: a number is an index
ToPropkey => arg(0),
// The write goes into the fields of the object's site, which
// every value that holds an object of the site sees. A write
// on `Any` can reach any object in an `Any`. A primitive has
// no fields to write, and undefined and null throw
DefineField | PutField => {
let (field, value) = (instr.imm.atom(), arg(0));
// A define_field builds the object before it goes
// anywhere (see regify)
let creation = instr.op == DefineField;
cache.map(arg(1), |cache, obj| {
match obj {
Type::Obj(site) => {
cache.site_table.write(site, field, &value);
if !creation {
cache.site_table.mark(site, |shape| &mut shape.written);
}
}
Type::Any => {
for site in 0..cache.site_table.sites.len() as SiteId {
if cache.site_table.site(site).in_any {
cache.site_table.write(site, field, &value);
cache.site_table.mark(site, |shape| &mut shape.written);
}
}
}
_ => {}
}
Type::Unknown
});
arg(1)
}
GetField => cache.site_table.read_value(&arg(0), instr.imm.atom(), &cache.protos),
// A string's length is its own. Anything else reads a
// `length` field
GetLength => {
let others = Type { string: None, ..arg(0) };
let read = cache.heap.read_value(&others, LENGTH_FIELD, &cache.protos);
let own = Type { number: arg(0).string.map(|_| Number::Int), ..Type::EMPTY };
cache.heap.join(&read, &own)
}
GetLength => cache.map(arg(0), |cache, obj| match obj {
Type::Str(_) => Type::Int,
obj => cache.site_table.read_value(&obj, LENGTH_FIELD, &cache.protos),
}),
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
// Operands are the callee's params: the callee (its env),
// `this`, then the args (see regify). The call runs one of
// the callee's fns. A callee that is not a fn throws, so it
// gives nothing
// `this`, then the args (see regify). A callee that is not
// a fn throws, so it gives nothing
// The callee's env is the fn object itself, so a call of
// each member of a sum passes that member as the env
Call | CallMethod | CallConstructor => {
arg_types.clear();
arg_types.extend(instr.args.iter().map(|&v| val_types[v]));
let args = &arg_types[FIRST_ARG as usize..];
let this = arg(THIS_ARG as usize);
codes.clear();
let ret = cache.map(arg(0), |cache, callee| {
arg_types.clear();
arg_types.extend(instr.args.iter().map(|&v| val_types[v]));
arg_types[0] = callee;
if let Some(callee) = arg_types[0].object {
codes.extend(&cache.heap.shape(callee).code);
}
let mut ret = Type::EMPTY;
for &code in &codes {
let ty = match code {
Code::Fn(fn_id) => cache.call(fn_id, &arg_types, reg_fns),
Code::Native(id) => native::ret_type(id, args),
Code::ProtoLink(kind) => {
let linked = cache.protos[kind as usize];
let joined = cache.heap.join(&linked, &args[0]);
cache.changes += (joined != linked) as usize;
cache.protos[kind as usize] = joined;
Type::UNDEFINED
}
};
ret = cache.heap.join(&ret, &ty);
}
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
// object, else the new object itself, `this`
let others = Type { object: None, ..ret };
match instr.op {
CallConstructor if !others.is_empty() => {
let this = val_types[instr.args[THIS_ARG as usize]];
cache.heap.join(&Type { object: ret.object, ..Type::EMPTY }, &this)
}
CallConstructor => cache.map(ret, |cache, ret| match ret {
Type::Obj(_) => ret,
Type::Any => cache.site_table.join(&ret, &this),
_ => this,
}),
_ => ret,
}
}
// ──────────────────────────────────────── Returns ──────────────────────────────────────────────────────────
Return | ReturnAsync => arg(0),
ReturnUndef => Type::UNDEFINED,
ReturnUndef => Type::Undefined,
_ => unreachable!("{instr:?}"),
};
@@ -368,16 +542,16 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
// Postfix copies come after the block's own instrs, and have no
// entry: a copy gives the type of what it copies
if idx < block.end {
instr_types[idx] = cache.heap.join(&instr_types[idx], &ty);
instr_types[idx] = cache.site_table.join(&instr_types[idx], &ty);
}
if is_ret {
ret_type = cache.heap.join(&ret_type, &ty);
ret_type = cache.site_table.join(&ret_type, &ty);
continue;
}
for &dst in &instr.dsts {
let joined = cache.heap.join(&val_types[dst], &ty);
let joined = cache.site_table.join(&val_types[dst], &ty);
changed |= joined != val_types[dst];
val_types[dst] = joined;
}
@@ -390,8 +564,8 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
let jumps = match f.code[block.start..block.end].last() {
Some(last) if matches!(last.op, IfFalse | IfFalse8 | IfTrue | IfTrue8) => {
match val_types[last.args[0]] {
cond if cond.is_empty() => continue,
cond => truthiness(&cond).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
Type::Unknown => continue,
cond => cache.agree(&cond, truthiness).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
}
}
_ => None,
@@ -406,17 +580,11 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
}
} // block
if !changed && cache.heap.version == version {
if !changed && cache.site_table.version == version {
break;
}
} // loop
// A type names the site its shape was kept under at the time: make it the
// one now
for ty in val_types.iter_mut().chain(&mut instr_types).chain([&mut ret_type]) {
*ty = cache.heap.canon(ty);
}
InferResult {
val_types,
instr_types,
@@ -425,115 +593,3 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
}
}
/// Whether every value of type `ty` is a number
fn is_number(ty: Type) -> bool {
ty.is_only(Type::number(Number::Int))
}
/// Whether every value of type `ty` is an int32
fn is_int(ty: Type) -> bool {
ty == Type::number(Number::Int)
}
/// The type, if every value of it is one value that its kind and refinement
/// tell
fn single(ty: &Type) -> Option<Type> {
let one = match ty.kinds() {
[true, false, false, false, false, false] | [false, true, false, false, false, false] => true,
[false, false, true, false, false, false] => ty.bool.flatten().is_some(),
[false, false, false, false, true, false] => ty.string.flatten().is_some(),
_ => false,
};
one.then_some(*ty)
}
/// Whether `a === b`, if the types decide it
fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
let disjoint = !a.kinds().iter().zip(b.kinds()).any(|(x, y)| *x && y);
match (single(a), single(b)) {
_ if disjoint => Some(false),
(Some(x), Some(y)) => Some(x == y),
_ => None,
}
}
/// Whether `a == b`, if the types decide it. Undefined and null are equal to
/// each other and to nothing else. Two values of one kind compare as `===`
/// does
fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
let nullish = |t: &Type| t.has_nullish() && t.non_nullish().is_empty();
match (a, b) {
_ if nullish(a) && nullish(b) => Some(true),
_ if (nullish(a) && !b.has_nullish()) || (!a.has_nullish() && nullish(b)) => Some(false),
_ if a.kinds() == b.kinds() && single(a).is_some() => strict_eq(a, b),
_ => None,
}
}
/// Whether every value of type `ty` is of a kind in `kinds`: true if every
/// value is, false if none is, `None` if the type does not decide it
fn test(ty: &Type, kinds: Type) -> Option<bool> {
let [inside, outside] = [true, false].map(|want| {
ty.kinds().iter().zip(kinds.kinds()).any(|(has, of)| *has && of == want)
});
match (inside, outside) {
(true, false) => Some(true),
(false, true) => Some(false),
_ => None,
}
}
/// Whether every value of type `ty` is a fn, if the type decides it
fn is_function(ty: &Type, heap: &Heap) -> Option<bool> {
let shape = ty.object.map(|site| heap.shape(site));
let fns = shape.is_some_and(|shape| !shape.code.is_empty());
let others = ty.kinds()[..5].contains(&true) || shape.is_some_and(|shape| shape.plain);
match (fns, others) {
(true, false) => Some(true),
(false, _) => Some(false),
_ => None,
}
}
/// Whether a value of type `ty` is truthy, if the type decides it
fn truthiness(ty: &Type) -> Option<bool> {
let truthy = [
ty.undefined.then_some(Some(false)),
ty.null.then_some(Some(false)),
ty.bool,
ty.number.map(|_| None),
ty.string.map(|s| s.map(|atom| atom != EMPTY_STRING)),
ty.object.map(|_| Some(true)),
];
let mut kinds = truthy.into_iter().flatten();
let first = kinds.next()?;
kinds.all(|t| t == first).then_some(first).flatten()
}
/// What `typeof` gives for a value of type `ty`
fn typeof_of(ty: &Type, heap: &Heap) -> Type {
let shape = ty.object.map(|site| heap.shape(site));
let names = [
ty.undefined.then_some(73), // "undefined"
ty.null.then_some(77), // "object"
ty.bool.map(|_| 75), // "boolean"
ty.number.map(|_| 74), // "number"
ty.string.map(|_| 76), // "string"
shape.filter(|shape| shape.plain).map(|_| 77), // "object"
shape.filter(|shape| !shape.code.is_empty()).map(|_| 27), // "function"
];
let mut names = names.into_iter().flatten();
match names.next() {
Some(name) if names.all(|n| n == name) => Type::string(Some(AtomRef::Predefined(name))),
_ => Type::string(None),
}
}
+498
View File
@@ -0,0 +1,498 @@
//! 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),
},
}
}
}
+223 -441
View File
@@ -1,493 +1,275 @@
//! Types and shapes (see `docs/model.md`, section 3)
//! 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)
use std::cell::Cell;
use crate::libqjs::parse::AtomRef;
use crate::{
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native::{NATIVES, Primitive},
regify::RegFn,
transform::{BOX_FIELD, PROTO_FIELD},
};
use super::sites::{SiteId, SiteTable};
/// Where an object comes from: the op that creates it, or a built-in fn
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Site {
Op { fn_id: usize, instr: usize },
Native(usize),
ProtoLink(Primitive),
}
pub(super) const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
/// The number of a site: its index in `Heap::sites`
pub type SiteId = u32;
/// The number of a sum (see `SiteTable::members()`)
pub type SumId = u32;
/// The code a fn object runs
/// A type is one of these. Join rules:
/// Unknown * T = T
/// Any * T = Any
/// T * T = T
/// Bool(a) * Bool(b) = Bool, the value no longer known. The same for Str
/// Int * Float = Float
/// T * U = T | U, a sum
///
/// 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
/// variants below. It has at most one member of each base type, except
/// objects: one `Obj` for each site
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Code {
Fn(usize),
Native(usize),
ProtoLink(Primitive),
}
/// A number is an int32 or a double. An int32 is also a double, so `Int`
/// joins with `Float` to give `Float`
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Number {
pub enum Type {
Unknown, // no value has reached it yet
Undefined,
Null,
Bool(Option<bool>), // the value, if it is known
Int,
Float,
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
Sum(SumId), // see above
Any, // any value. No rule gives it now: a join gives a sum instead
}
/// The set of values that a place can hold: a set of kinds, with a refinement
/// for each kind in the set. The default is the empty set
#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
pub struct Type {
pub undefined: bool,
pub null: bool,
/// The value, if it is known
pub bool: Option<Option<bool>>,
pub number: Option<Number>,
/// The value, if it is known. Atoms are interned, so two equal atoms are
/// two equal strings
pub string: Option<Option<AtomRef>>,
/// The shape, by a site in it (see `Heap`)
pub object: Option<SiteId>,
/// A `Type` without its detail (a known value, a site): the kinds of value
/// that JS tells apart
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BaseType {
Undefined,
Null,
Bool,
Number,
String,
Object,
}
impl Type {
pub const UNDEFINED: Type = Type { undefined: true, ..Type::EMPTY };
pub const NULL: Type = Type { null: true, ..Type::EMPTY };
pub const EMPTY: Type = Type {
undefined: false,
null: false,
bool: None,
number: None,
string: None,
object: None,
};
pub const fn bool(value: Option<bool>) -> Type {
Type { bool: Some(value), ..Type::EMPTY }
}
pub const fn number(number: Number) -> Type {
Type { number: Some(number), ..Type::EMPTY }
}
pub const fn string(value: Option<AtomRef>) -> Type {
Type { string: Some(value), ..Type::EMPTY }
}
pub const fn object(site: SiteId) -> Type {
Type { object: Some(site), ..Type::EMPTY }
}
pub fn is_empty(&self) -> bool {
*self == Type::EMPTY
}
/// The type with the known values dropped
/// The type with the known value dropped
pub fn widen(&self) -> Type {
Type {
bool: self.bool.map(|_| None),
string: self.string.map(|_| None),
..*self
match self {
Type::Bool(_) => Type::Bool(None),
Type::Str(_) => Type::Str(None),
ty => *ty,
}
}
/// The type without undefined and null
pub fn non_nullish(&self) -> Type {
Type {
undefined: false,
null: false,
..*self
/// 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
pub fn base(&self) -> Option<BaseType> {
match self {
Type::Unknown | Type::Sum(_) | Type::Any => None,
Type::Undefined => Some(BaseType::Undefined),
Type::Null => Some(BaseType::Null),
Type::Bool(_) => Some(BaseType::Bool),
Type::Int | Type::Float => Some(BaseType::Number),
Type::Str(_) => Some(BaseType::String),
Type::Obj(_) => Some(BaseType::Object),
}
}
pub fn has_nullish(&self) -> bool {
self.undefined || self.null
}
/// The kinds in the type, in the order undefined, null, bool, number,
/// string, object
pub fn kinds(&self) -> [bool; 6] {
[
self.undefined,
self.null,
self.bool.is_some(),
self.number.is_some(),
self.string.is_some(),
self.object.is_some(),
]
}
/// Whether the type has exactly the kinds of `kind`
pub fn is_only(&self, kind: Type) -> bool {
self.kinds() == kind.kinds()
pub fn is_nullish(&self) -> bool {
matches!(self, Type::Undefined | Type::Null)
}
}
/// The join of two refinements that are a value if it is known
fn known<T: PartialEq>(a: Option<Option<T>>, b: Option<Option<T>>) -> Option<Option<T>> {
/// The join of two types of one base type, neither of them `Obj`
fn join_kind(a: Type, b: Type) -> Type {
match (a, b) {
(Some(a), Some(b)) => Some(if a == b { a } else { None }),
(a, b) => a.or(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,
}
}
/// A field of a shape
#[derive(Debug, Clone, Copy)]
pub struct Field {
pub ty: Type,
/// Every object of the shape gets the field when it is created, so the
/// field is never absent
pub defined: bool,
}
/// The type of a set of objects: what `docs/model.md` calls a shape
#[derive(Debug, Clone, Default)]
pub struct Shape {
/// Sorted by name
pub fields: Vec<(AtomRef, Field)>,
/// The code that its objects can run, sorted
pub code: Vec<Code>,
/// Some of its objects are not fns
pub plain: bool,
/// A field of one of its objects is written after the object is created
pub written: bool,
/// One of its objects is in an identity test with an object
pub compared: bool,
/// An op that creates one of its objects has run (see infer)
pub live: bool,
}
impl Shape {
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)
/// 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::Sum(id) => {
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),
}
}
/// 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)
pub fn is_value(&self) -> bool {
!self.written && !self.compared
/// The members of every sum
pub fn sums(&self) -> impl Iterator<Item = &[Type]> {
self.sum_starts.windows(2).map(|w| &self.sum_members[w[0] as usize..w[1] as usize])
}
}
/// A site, and the code that its objects run, if they are fns
#[derive(Debug, Clone, Copy)]
pub struct SiteInfo {
pub site: Site,
pub code: Option<Code>,
}
/// The type whose members are the members of `a` and of `b`
fn union(&mut self, a: &Type, b: &Type) -> Type {
let start = self.sum_members.len();
/// What `Heap::new` finds about one instr
#[derive(Debug, Clone, Copy, Default)]
struct InstrInfo {
/// The site of the object it creates
site: Option<SiteId>,
/// A `define_field` into the value that a creating op gives, before the
/// object goes anywhere. Every other field write is a write after creation
creation_write: bool,
}
for ty in [a, b] {
for i in 0..self.members(ty).len() {
let member = self.members(ty)[i];
self.sum_members.push(member);
}
}
/// Every shape. Objects that flow into one place have one shape: their shapes
/// are merged. A merged site points to the site it was merged into, and only
/// a site that points to itself keeps a shape
#[derive(Debug, Clone)]
pub struct Heap {
/// Every site, by number: the creating ops of every fn in order, then the
/// natives, then the proto links
pub sites: Vec<SiteInfo>,
shapes: Vec<Shape>,
parent: Vec<Cell<SiteId>>,
/// Each fn's first entry in `instrs`, which has one entry for each instr
fn_starts: Vec<usize>,
instrs: Vec<InstrInfo>,
/// The number of the first native's site
natives: SiteId,
/// Goes up on every change, so a walk can tell that a read may give more
pub version: usize,
}
self.sum_of_new(start)
}
impl Heap {
/// Number every site, and give each one a shape with the fields its
/// creation writes write
pub fn new(fns: &[RegFn]) -> Self {
let mut heap = Self {
sites: vec![],
shapes: vec![],
parent: vec![],
fn_starts: vec![],
instrs: vec![],
natives: 0,
version: 0,
/// `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
/// `start` on. They are sorted, and two of one kind join. If they are one
/// type, or a sum that exists, they are removed again
fn sum_of_new(&mut self, start: usize) -> Type {
let list = &mut self.sum_members[start..];
list.sort_unstable();
let mut n = 0;
for i in 0..list.len() {
let ty = list[i];
let joins = n > 0 && (list[n - 1] == ty || (ty.base() == list[n - 1].base() && ty.base() != Some(BaseType::Object)));
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)),
};
// The site that each value of the fn holds, if a creating op gave it
let mut created: Vec<Option<SiteId>> = vec![];
for (fn_id, f) in fns.iter().enumerate() {
heap.fn_starts.push(heap.instrs.len());
created.clear();
created.resize(f.val_count, None);
for (instr, op) in f.code.iter().enumerate() {
let mut info = InstrInfo::default();
match op.op {
OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8 => {
let code = match op.op {
OpCode::Object => None,
_ => match f.cpool[op.imm.const_idx()] {
JsValue::FnById(id) => Some(Code::Fn(id)),
_ => unreachable!(),
},
};
let site = heap.add(Site::Op { fn_id, instr }, code);
created[op.dsts[0]] = Some(site);
info.site = Some(site);
}
OpCode::DefineField => {
if let Some(site) = created[op.args[1]] {
let field = Field { ty: Type::EMPTY, defined: true };
let fields = &mut heap.shapes[site as usize].fields;
if let Err(i) = fields.binary_search_by_key(&op.imm.atom(), |(n, _)| *n) {
fields.insert(i, (op.imm.atom(), field));
}
info.creation_write = true;
}
}
_ => {}
}
heap.instrs.push(info);
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)
}
}
heap.natives = heap.sites.len() as SiteId;
for id in 0..NATIVES.len() {
heap.add(Site::Native(id), Some(Code::Native(id)));
}
for kind in Primitive::ALL {
heap.add(Site::ProtoLink(kind), Some(Code::ProtoLink(kind)));
}
heap
}
fn add(&mut self, site: Site, code: Option<Code>) -> SiteId {
let id = self.sites.len() as SiteId;
self.sites.push(SiteInfo { site, code });
self.parent.push(Cell::new(id));
self.shapes.push(Shape {
code: code.into_iter().collect(),
plain: code.is_none(),
..Shape::default()
});
id
}
/// The site that instr `instr` of fn `fn_id` creates, if it is a creating
/// op
pub fn site_at(&self, fn_id: usize, instr: usize) -> Option<SiteId> {
self.instrs[self.fn_starts[fn_id] + instr].site
}
pub fn is_creation_write(&self, fn_id: usize, instr: usize) -> bool {
self.instrs[self.fn_starts[fn_id] + instr].creation_write
}
/// The site of a built-in fn
pub fn builtin(&self, site: Site) -> SiteId {
self.natives
+ match site {
Site::Native(id) => id as SiteId,
Site::ProtoLink(kind) => (NATIVES.len() + kind as usize) as SiteId,
Site::Op { .. } => unreachable!(),
}
}
/// The site that `site` is merged into, or `site` itself
pub fn find(&self, mut site: SiteId) -> SiteId {
loop {
let parent = self.parent[site as usize].get();
if parent == site {
return site;
}
// Path halving: skip a step for the next find
let grandparent = self.parent[parent as usize].get();
self.parent[site as usize].set(grandparent);
site = grandparent;
}
}
/// `ty` with its object's site the one that keeps its shape
pub fn canon(&self, ty: &Type) -> Type {
Type {
object: ty.object.map(|site| self.find(site)),
..*ty
}
}
pub fn shape(&self, site: SiteId) -> &Shape {
&self.shapes[self.find(site) as usize]
}
/// Every site that keeps a shape, and that a creating op has reached
pub fn live_shapes(&self) -> impl Iterator<Item = SiteId> + '_ {
(0..self.sites.len() as SiteId).filter(|&site| self.find(site) == site && self.shapes[site as usize].live)
}
/// An object of the site, which a creating op gives
pub fn create(&mut self, site: SiteId) -> Type {
let root = self.find(site);
self.mark(root, |shape| &mut shape.live);
Type::object(root)
}
/// Set a flag of the shape of `site`
pub fn mark(&mut self, site: SiteId, flag: fn(&mut Shape) -> &mut bool) {
let root = self.find(site);
let flag = flag(&mut self.shapes[root as usize]);
if !*flag {
*flag = true;
self.version += 1;
}
}
/// Whether an object of the shape 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.shape(site).field(field).is_some_and(|f| f.defined)
}
/// Type lattice join: the union, kind by kind. Two different known values
/// give an unknown one, and two shapes are merged
/// The join of two types (see `Type`)
pub fn join(&mut self, a: &Type, b: &Type) -> Type {
Type {
undefined: a.undefined || b.undefined,
null: a.null || b.null,
bool: known(a.bool, b.bool),
number: a.number.max(b.number),
string: known(a.string, b.string),
object: match (a.object, b.object) {
(Some(x), Some(y)) => Some(self.merge(x, y)),
(x, y) => x.or(y).map(|site| self.find(site)),
},
use Type::*;
match (*a, *b) {
(Unknown, t) | (t, Unknown) => t,
(a, b) if a == b => a,
(Any, t) | (t, Any) => self.to_any(&t),
(a, b) => self.union(&a, &b),
}
}
}
/// Merge the shape of `from` into the shape of `into`. A field that only
/// one of them has can be absent in the merged shape
fn merge(&mut self, into: SiteId, from: SiteId) -> SiteId {
let (into, from) = (self.find(into), self.find(from));
if into == from {
return into;
}
let from_shape = std::mem::take(&mut self.shapes[from as usize]);
self.parent[from as usize].set(into);
self.version += 1;
let shape = &mut self.shapes[into as usize];
shape.code.extend(&from_shape.code);
shape.code.sort_unstable();
shape.code.dedup();
shape.plain |= from_shape.plain;
shape.written |= from_shape.written;
shape.compared |= from_shape.compared;
shape.live |= from_shape.live;
for (name, field) in &mut shape.fields {
field.defined &= from_shape.field(*name).is_some_and(|f| f.defined);
}
/// The type, if every value of it is one value that the type tells
pub(super) fn single(ty: &Type) -> Option<Type> {
match ty {
Type::Undefined | Type::Null | Type::Bool(Some(_)) | Type::Str(Some(_)) => Some(*ty),
_ => None,
}
}
for (name, field) in &from_shape.fields {
let defined = self.shape(into).field(*name).is_some_and(|f| f.defined) && field.defined;
self.write(into, *name, &field.ty);
// The tests below take one member of a type each (see `TypeCache::agree()`)
let root = self.find(into) as usize;
let i = self.shapes[root].fields.binary_search_by_key(name, |(n, _)| *n).unwrap();
self.shapes[root].fields[i].1.defined = defined;
}
/// Whether `a === b`, if the types decide it
pub(super) fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
let disjoint = matches!((a.base(), b.base()), (Some(x), Some(y)) if x != y);
self.find(into)
match (single(a), single(b)) {
_ if disjoint => Some(false),
(Some(x), Some(y)) => Some(x == y),
_ => None,
}
/// Join `ty` into `field` of the shape of `site`. A field that the shape
/// did not have can be absent
pub fn write(&mut self, site: SiteId, field: AtomRef, ty: &Type) {
let old = self.shape(site).field(field).map_or(Type::EMPTY, |f| f.ty);
let new = self.join(&old, ty);
// The join may have merged the shape into another one
let root = self.find(site) as usize;
let fields = &mut self.shapes[root].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;
}
/// Whether `a == b`, if the types decide it. Undefined and null are equal to
/// each other and to nothing else. Two values of one kind compare as `===`
/// does
pub(super) fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
let other = |t: &Type| !t.is_nullish() && *t != Type::Any;
match (a, b) {
_ if a.is_nullish() && b.is_nullish() => Some(true),
_ if (a.is_nullish() && other(b)) || (other(a) && b.is_nullish()) => Some(false),
_ if a.base().is_some() && a.base() == b.base() => strict_eq(a, b),
_ => None,
}
/// A read of `field` on an object of the shape 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
///
/// Merged shapes can make a chain loop, which a real object cannot. A chain
/// longer than the number of sites has looped, and stops
pub fn read(&mut self, site: SiteId, field: AtomRef) -> Type {
let mut site = self.find(site);
let mut ty = Type::EMPTY;
for _ in 0..self.sites.len() {
let own = self.shape(site).field(field).map_or(Type::EMPTY, |f| f.ty);
ty = self.join(&ty, &own);
if !self.can_be_absent(site, field) {
return ty;
}
// The chain ends at a prototype that is null or absent
let proto = self.shape(site).field(PROTO_FIELD).map_or(Type::EMPTY, |f| f.ty);
ty.undefined |= proto.null || self.can_be_absent(site, PROTO_FIELD);
match proto.object {
Some(proto) => site = self.find(proto),
None => break,
}
}
ty
}
/// Whether a value of type `ty` has one of the base types `bases`, if the type decides it
pub(super) fn test(ty: &Type, bases: &[BaseType]) -> Option<bool> {
ty.base().map(|base| bases.contains(&base))
}
/// Whether a value of type `ty` is a fn, if the type decides it
pub(super) fn is_function(ty: &Type, site_table: &SiteTable) -> Option<bool> {
match ty {
Type::Obj(site) => Some(site_table.site(*site).code().is_some()),
Type::Any => None,
_ => Some(false),
}
/// 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 they give nothing
pub fn read_value(&mut self, ty: &Type, field: AtomRef, protos: &[Type; 3]) -> Type {
let proto = |kind: Primitive| protos[kind as usize].object.filter(|_| kind.is_in(ty));
let mut read = Type::EMPTY;
for site in Primitive::ALL.map(proto).into_iter().chain([ty.object]).flatten() {
let ty = self.read(site, field);
read = self.join(&read, &ty);
}
read
}
/// Whether a value of type `ty` is truthy, if the type decides it
pub(super) fn truthiness(ty: &Type) -> Option<bool> {
match ty {
Type::Undefined | Type::Null => Some(false),
Type::Bool(b) => *b,
Type::Str(s) => s.map(|atom| atom != EMPTY_STRING),
Type::Obj(_) => Some(true),
_ => None,
}
}
/// The atom of what `typeof` gives for a value of type `ty`, if the type
/// decides it
pub(super) fn typeof_name(ty: &Type, site_table: &SiteTable) -> Option<u32> {
Some(match ty {
Type::Undefined => 73, // "undefined"
Type::Null => 77, // "object"
Type::Bool(_) => 75, // "boolean"
Type::Int | Type::Float => 74, // "number"
Type::Str(_) => 76, // "string"
Type::Obj(site) if site_table.site(*site).code().is_some() => 27, // "function"
Type::Obj(_) => 77, // "object"
_ => return None,
})
}
+3 -2
View File
@@ -72,7 +72,8 @@ impl Atom {
let (bin, header) = leb128(bin)?;
let wide_char = header & 1 == 1;
let len = header >> 1;
let (bin, data) = take(len)(bin)?;
let byte_len = if wide_char { len * 2 } else { len };
let (bin, data) = take(byte_len)(bin)?;
let data = data.to_vec();
let string = if wide_char {
@@ -171,7 +172,7 @@ pub enum JsValue {
/// A C function, by its index in `native::NATIVES`
NativeFn(usize),
/// Makes its arg the prototype of a primitive kind (see `native`)
ProtoLink(crate::native::Primitive),
ProtoLink(crate::native::ProtoOf),
Module(Module),
TypedArray,
ArrayBuffer,
+66 -5
View File
@@ -17,7 +17,34 @@ use libqjs::compile;
use regify::regify;
use transform::transform;
const STDLIB: &[u8] = include_bytes!("../../lib/stdlib.js");
/// The stdlib, in the order that it runs in, in front of the program
const STDLIB: &[&[u8]] = &[
include_bytes!("../../lib/stdlib/globals.js"),
include_bytes!("../../lib/stdlib/object.js"),
include_bytes!("../../lib/stdlib/string.js"),
include_bytes!("../../lib/stdlib/number.js"),
include_bytes!("../../lib/stdlib/boolean.js"),
include_bytes!("../../lib/stdlib/array.js"),
include_bytes!("../../lib/stdlib/loop.js"),
include_bytes!("../../lib/stdlib/promise.js"),
include_bytes!("../../lib/stdlib/timers.js"),
include_bytes!("../../lib/stdlib/io.js"),
include_bytes!("../../lib/stdlib/console.js"),
];
/// Runs after the program
const STDLIB_END: &[u8] = include_bytes!("../../lib/stdlib/end.js");
/// The runtime. The emitted C includes `jsrt.h`, which includes the rest
const RUNTIME: &[(&str, &[u8])] = &[
("jsrt.h", include_bytes!("../../lib/rt/jsrt.h")),
("core.h", include_bytes!("../../lib/rt/core.h")),
("str.h", include_bytes!("../../lib/rt/str.h")),
("number.h", include_bytes!("../../lib/rt/number.h")),
("any.h", include_bytes!("../../lib/rt/any.h")),
("io.h", include_bytes!("../../lib/rt/io.h")),
("uring.h", include_bytes!("../../lib/rt/uring.h")),
];
fn main() {
// Load source file
@@ -28,10 +55,16 @@ fn main() {
let metadata = std::fs::metadata(filename).unwrap();
assert!(metadata.is_file());
// The stdlib is compiled with the program, in front of it
let mut src = STDLIB.to_vec();
src.reserve(metadata.len() as usize);
// The stdlib is compiled with the program, in front of it. The program is
// in a fn of its own, so that its names can shadow the stdlib's, as a
// script's names shadow JS's globals (node does the same with a script).
// `})` is on a line of its own, in case the program ends in a comment
let mut src = STDLIB.concat();
src.reserve(metadata.len() as usize + STDLIB_END.len() + 32);
src.extend_from_slice(b"(function () {\n");
file.read_to_end(&mut src).unwrap();
src.extend_from_slice(b"\n})();\n");
src.extend_from_slice(STDLIB_END);
// Compile to bytecode
let binary = compile(&src, filename);
@@ -43,5 +76,33 @@ fn main() {
let ty_cache = infer_types(&reg_fns);
let analysis = analyze(&reg_fns);
print!("{}", emit::emit(&reg_fns, &ty_cache, &analysis, &atoms));
let c = emit::emit(&reg_fns, &ty_cache, &analysis, &atoms);
// Compile the C, with the runtime, to `a.out`
let dir = std::env::temp_dir().join(format!("jsrt-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
for (name, src) in RUNTIME {
std::fs::write(dir.join(name), src).unwrap();
}
std::fs::write(dir.join("program.c"), c).unwrap();
let cc = cc::Build::new()
.target(env!("TARGET"))
.host(env!("TARGET"))
.opt_level(2)
.cargo_metadata(false)
.get_compiler();
let status = cc
.to_command()
.args(["-std=c11", "-w"])
.arg(dir.join("program.c"))
.args(["-o", "a.out", "-lm"])
.status()
.unwrap();
std::fs::remove_dir_all(&dir).unwrap();
std::process::exit(status.code().unwrap_or(1));
}
+51 -26
View File
@@ -1,4 +1,4 @@
//! What the stdlib (`lib/stdlib.js`) can reach below JS: the globals it may
//! What the stdlib (`lib/stdlib/`) can reach below JS: the globals it may
//! read, and nothing else may
//!
//! Natives are the common layer of operations on primitive values. The stdlib
@@ -10,7 +10,10 @@
//! kind. A field read on a primitive borrows its kind's prototype
use crate::{
infer::types::{Number, Type},
infer::{
sites::SiteTable,
types::{BaseType, Type},
},
libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS},
};
@@ -37,10 +40,10 @@ macro_rules! native {
/// A native's param or return type: any value of its kind
macro_rules! ty {
(Undefined) => { Type::UNDEFINED };
(Int) => { Type::number(Number::Int) };
(Float) => { Type::number(Number::Float) };
(String) => { Type::string(None) };
(Undefined) => { Type::Undefined };
(Int) => { Type::Int };
(Float) => { Type::Float };
(String) => { Type::Str(None) };
}
#[rustfmt::skip]
@@ -48,7 +51,23 @@ pub static NATIVES: &[NativeFn] = &[
// IO
native!("__native_print_string"(String) -> Undefined),
// Numbers. JS has one number type, see `accepts()`
// Async IO (see `lib/rt/io.h`). A queue gives the op's handle. A wait gives
// the handle of an op that is done, or -1 if none is left. The getters
// read that op's result, until the next wait
native!("__native_io_queue_timeout"(Float) -> Int),
native!("__native_io_queue_open"(String, Int, Int) -> Int),
native!("__native_io_queue_read"(Int, Int, Float) -> Int),
native!("__native_io_queue_write"(Int, String, Float) -> Int),
native!("__native_io_queue_close"(Int) -> Int),
native!("__native_io_queue_stat"(String) -> Int),
native!("__native_io_queue_cancel"(Int) -> Int),
native!("__native_io_wait"() -> Int),
native!("__native_io_status"(Int) -> Int),
native!("__native_io_result_string"(Int) -> String),
native!("__native_io_result_number"(Int) -> Float),
// Numbers. JS has one number type, so an Int and a Float arg both pass
// (see `ret_type()`)
native!("__native_number_to_string"(Float) -> String),
native!("__native_number_to_fixed"(Float, Int) -> String),
native!("__native_string_to_number"(String) -> Float),
@@ -60,33 +79,39 @@ pub static NATIVES: &[NativeFn] = &[
native!("__native_string_from_char_code"(Int) -> String),
];
/// A primitive kind whose field reads borrow a prototype from the stdlib
/// A kind of value whose prototype the stdlib makes and links (see
/// `PROTO_LINKS`): a primitive kind, or arrays. A field read that its own
/// fields do not answer goes on to that prototype
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Primitive {
pub enum ProtoOf {
String,
Number,
Boolean,
Array,
}
impl Primitive {
pub const ALL: [Primitive; 3] = [Primitive::String, Primitive::Number, Primitive::Boolean];
impl ProtoOf {
pub const ALL: [ProtoOf; 4] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean, ProtoOf::Array];
pub const PRIMITIVES: [ProtoOf; 3] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean];
/// Whether `ty` can hold a value of this kind
pub fn is_in(self, ty: &Type) -> bool {
match self {
Primitive::String => ty.string.is_some(),
Primitive::Number => ty.number.is_some(),
Primitive::Boolean => ty.bool.is_some(),
/// The primitive kind of every value of type `ty`, if it has a prototype
pub fn of(ty: &Type) -> Option<ProtoOf> {
match ty.base()? {
BaseType::String => Some(ProtoOf::String),
BaseType::Number => Some(ProtoOf::Number),
BaseType::Bool => Some(ProtoOf::Boolean),
_ => None,
}
}
}
/// `__link_proto_<kind>(proto)` makes `proto` the prototype of `<kind>`. It
/// does nothing at runtime
static PROTO_LINKS: &[(&str, Primitive)] = &[
("__link_proto_string", Primitive::String),
("__link_proto_number", Primitive::Number),
("__link_proto_boolean", Primitive::Boolean),
static PROTO_LINKS: &[(&str, ProtoOf)] = &[
("__link_proto_string", ProtoOf::String),
("__link_proto_number", ProtoOf::Number),
("__link_proto_boolean", ProtoOf::Boolean),
("__link_proto_array", ProtoOf::Array),
];
/// The value of the global `name`. The only globals are the ones the stdlib
@@ -111,17 +136,17 @@ pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue {
/// 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
/// the call gives nothing
pub fn ret_type(idx: usize, args: &[Type]) -> Type {
/// the call gives nothing. An arg of a sum or `Any` is checked at runtime
pub fn ret_type(idx: usize, args: &[Type], site_table: &SiteTable) -> Type {
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)| {
param.kinds().iter().zip(arg.kinds()).any(|(p, a)| *p && a)
*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) {
true => native.ret,
false => Type::EMPTY,
false => Type::Unknown,
}
}
+46 -9
View File
@@ -2,7 +2,7 @@ mod basic_block;
use crate::{
flatten::FlatFn,
libqjs::parse::{AtomRef, ImmOperand, Instr, JsValue, opcodes::OpCode},
libqjs::parse::{ImmOperand, Instr, JsValue, opcodes::OpCode},
};
pub use basic_block::BasicBlock;
@@ -29,7 +29,6 @@ impl RegInstr {
#[derive(Debug)]
pub struct RegFn {
pub name: AtomRef,
pub arg_count: usize,
pub var_count: usize,
/// Locals and args are not renamed: local `k` is value id `k`, and arg `k`
@@ -119,8 +118,25 @@ impl RegFn {
}
}
// A define_field into an object that no creating op of this fn gave
// (a class field on `this`, a static field on the class) is a write
// like any other: a put_field. So a define_field is always a write
// that builds an object before it goes anywhere (see infer)
let mut created = vec![false; next_val];
for instr in &out {
if matches!(instr.op, OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8) {
created[instr.dsts[0]] = true;
}
}
for instr in &mut out {
if instr.op == OpCode::DefineField && !created[instr.args[1]] {
instr.op = OpCode::PutField;
}
}
Self {
name: f.name,
arg_count: f.arg_count,
var_count: f.var_count,
val_count: next_val,
@@ -209,7 +225,7 @@ fn walk_block(
continue;
}
// Handle `Dup` and `Swap`, which just push values
// Handle `Dup`, `Dup2`, `Swap` and `ToPropkey2`, which just push values
if instr.op == OpCode::Dup {
stack.push(out_instr.args[0]);
stack.push(out_instr.args[0]);
@@ -219,6 +235,27 @@ fn walk_block(
continue;
}
if instr.op == OpCode::Dup2 {
let (a, b) = (out_instr.args[1], out_instr.args[0]);
stack.extend([a, b, a, b]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
// to_propkey2 makes the key on top a property key, and keeps the
// object under it. A key is a number, which it keeps as it is (see
// infer), so it only gives both back
if instr.op == OpCode::ToPropkey2 {
stack.push(out_instr.args[1]);
stack.push(out_instr.args[0]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
if instr.op == OpCode::Swap {
stack.push(out_instr.args[0]);
stack.push(out_instr.args[1]);
@@ -228,11 +265,11 @@ fn walk_block(
continue;
}
// A field write lands back in the object's own value: the object with
// the field is the same object. define_field also leaves it on the
// stack, put_field does not
if matches!(instr.op, OpCode::PutField | OpCode::DefineField) {
let obj = out_instr.args[1];
// A field or element write lands back in the object's own value: the
// object with the field is the same object. define_field also leaves
// it on the stack, put_field and put_array_el do not
if matches!(instr.op, OpCode::PutField | OpCode::DefineField | OpCode::PutArrayEl) {
let obj = *out_instr.args.last().unwrap();
if n_push == 1 {
stack.push(obj);
+29
View File
@@ -0,0 +1,29 @@
use super::{FIRST_ARG, THIS_ARG, pusher};
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`,
/// and `this` with a read of its param
pub fn replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let (op, arg) = match (instr.op, instr.imm) {
(PushThis, _) => (GetArg, THIS_ARG),
(GetArg0, _) => (GetArg, FIRST_ARG),
(GetArg1, _) => (GetArg, FIRST_ARG + 1),
(GetArg2, _) => (GetArg, FIRST_ARG + 2),
(GetArg3, _) => (GetArg, FIRST_ARG + 3),
(PutArg0, _) => (PutArg, FIRST_ARG),
(PutArg1, _) => (PutArg, FIRST_ARG + 1),
(PutArg2, _) => (PutArg, FIRST_ARG + 2),
(PutArg3, _) => (PutArg, FIRST_ARG + 3),
(SetArg0, _) => (SetArg, FIRST_ARG),
(SetArg1, _) => (SetArg, FIRST_ARG + 1),
(SetArg2, _) => (SetArg, FIRST_ARG + 2),
(SetArg3, _) => (SetArg, FIRST_ARG + 3),
(GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k),
_ => return false,
};
pusher(instr.pc, out)(op, ImmOperand::Arg(arg));
true
}
+216
View File
@@ -0,0 +1,216 @@
use std::collections::HashMap;
use super::{BOX_FIELD, ENV_ARG, FIRST_ARG, pusher};
use crate::{
libqjs::parse::{
AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode,
},
regify::{is_var_read, is_var_write},
};
/// Put every binding that a closure captures in a box: an object whose
/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The
/// owner and the closures read and write the box. A closure keeps the boxes
/// it captured in its own fields, one for each of its `closure_vars`: field `i`
/// holds the box of capture `i`
///
/// The fn makes a box for each binding that its children capture, at entry.
/// A captured local holds its box in its own slot. A captured arg keeps its
/// box in a new local, and the box starts with the arg's value. A `var` starts
/// as undefined. A `let` or `const` has no value until it is written (its TDZ):
/// object; undefined; define_field <var>; put_loc k each captured `var`
/// object; put_loc k each captured `let`
/// object; get_arg a; define_field <var>; put_loc box each captured arg
///
/// A scope that is entered again is a loop body. QuickJS closes each captured
/// binding of the scope (`close_loc`) at every way out of it, so a new box
/// there gives each iteration its own binding. `set_loc_uninitialized` only
/// marks the TDZ, which is not checked, and hoisted fns are made before it, so
/// it does not make a box
///
/// These instrs take a pc that no jump goes to, so a jump to the fn's first
/// instr does not make them again
///
/// Then it replaces, for a captured binding:
/// get_loc k get_loc box; get_field <var>
/// put_loc k get_loc box; swap; put_field <var>
/// set_loc k dup; get_loc box; swap; put_field <var>
/// close_loc k object; get_loc k; get_field <var>;
/// define_field <var>; put_loc k
/// a way out of a loop body: a new binding, with
/// the current value
///
/// For any binding:
/// get_var_ref i get_arg env; get_field i; get_field <var>
/// put_var_ref i get_arg env; get_field i; swap; put_field <var>
/// set_var_ref i dup; get_arg env; get_field i; swap; put_field <var>
/// fclosure f fclosure f; (push_box; define_field i)*
///
/// where `push_box` is the box of capture `i` of `f`: the local that holds it,
/// or field `j` of this fn's env for capture `j` of this fn. A binding with no
/// frame (a module binding) gets its box here, from `object`
///
/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure
/// captures, do nothing. Gives the code and the new number of locals
pub fn replace_bindings(
code: &[Instr],
cpool: &[JsValue],
captures: &[Vec<ClosureVar>],
var_count: usize,
) -> (Vec<Instr>, usize) {
use OpCode::*;
let children = cpool.iter().filter_map(|c| match c {
JsValue::FnById(child) => Some(*child),
_ => None,
});
let mut locals = vec![];
let mut args = vec![];
for c in children.flat_map(|child| &captures[child]) {
match c.flags.closure_kind {
ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)),
ClosureKind::Arg => args.push(c.var_idx),
_ => {}
}
}
locals.sort_unstable();
locals.dedup();
args.sort_unstable();
args.dedup();
// Value ids of bindings, as `is_var_read` gives them: args after every local
let new_var_count = var_count + args.len();
let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k;
// The local that holds the box of each captured binding, by value id
let boxes: HashMap<usize, usize> = (locals.iter().map(|&(k, _)| (k, k)))
.chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i)))
.collect();
let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32));
let value = ImmOperand::Atom(BOX_FIELD);
let none = ImmOperand::None;
let loc = |k: usize| ImmOperand::Loc(k as u16);
let mut out = Vec::with_capacity(code.len());
{
let mut push = pusher(usize::MAX, &mut out);
for &(k, lexical) in &locals {
push(Object, none);
if !lexical {
push(Undefined, none);
push(DefineField, value);
}
push(PutLoc, loc(k));
}
for (i, &k) in args.iter().enumerate() {
push(Object, none);
push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16));
push(DefineField, value);
push(PutLoc, loc(var_count + i));
}
}
for instr in code {
let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v));
let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v));
let own_box = |k: ImmOperand| match k {
ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(),
_ => None,
};
let mut push = pusher(instr.pc, &mut out);
if let Some(&b) = read {
push(GetLoc, loc(b));
push(GetField, value);
} else if let Some(&b) = write {
if instr.op.info().n_push == 1 {
push(Dup, none);
}
push(GetLoc, loc(b));
push(Swap, none);
push(PutField, value);
} else if let Some((i, writes, leaves)) = var_ref_access(instr) {
if leaves {
push(Dup, none);
}
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(i));
if writes {
push(Swap, none);
push(PutField, value);
} else {
push(GetField, value);
}
} else if instr.op == SetLocUninitialized {
push(Nop, none);
} else if instr.op == CloseLoc {
match own_box(instr.imm) {
Some(b) => {
push(Object, none);
push(GetLoc, loc(b));
push(GetField, value);
push(DefineField, value);
push(PutLoc, loc(b));
}
None => push(Nop, none),
}
} else if matches!(instr.op, Fclosure8 | Fclosure) {
let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else {
unreachable!()
};
push(Fclosure, instr.imm);
for (i, c) in captures[child].iter().enumerate() {
match c.flags.closure_kind {
ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])),
ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])),
ClosureKind::Ref => {
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(c.var_idx));
}
_ => push(Object, none),
}
push(DefineField, slot(i));
}
} else {
push(instr.op, instr.imm);
}
}
(out, new_var_count)
}
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes,
/// and whether a write also leaves the value on the stack
fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
use OpCode::*;
let i = match instr.imm {
ImmOperand::VarRef(i) => i as usize,
_ => 0,
};
match instr.op {
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)),
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)),
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)),
GetVarRef | GetVarRefCheck => Some((i, false, false)),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)),
SetVarRef => Some((i, true, true)),
_ => None,
}
}
+71
View File
@@ -0,0 +1,71 @@
use super::{ENV_ARG, PROTO_FIELD, pusher};
use crate::{
libqjs::parse::{Atom, ImmOperand, Instr, JsValue, opcodes::OpCode},
native,
};
/// Replace ops that are a fixed sequence of simpler ones
///
/// A sequence is never empty: jumps find their target by its pc (see regify),
/// so every instr must keep at least one instr at its pc
pub fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let none = ImmOperand::None;
let imm = instr.imm;
// A plain call has no `this`, so it passes `undefined`. It is pushed last,
// on top of the args, and the call pops it as one more arg (see regify)
let plain_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1))];
let tail_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1)), (Return, none)];
let seq: &[(OpCode, ImmOperand)] = match (instr.op, imm) {
(Call0, _) => &plain_call(0),
(Call1, _) => &plain_call(1),
(Call2, _) => &plain_call(2),
(Call3, _) => &plain_call(3),
(Call, ImmOperand::Npop(n)) => &plain_call(n),
(TailCall, ImmOperand::Npop(n)) => &tail_call(n),
(TailCallMethod, _) => &[(CallMethod, imm), (Return, none)],
(GetField2, _) => &[(Dup, none), (GetField, imm)],
(GetLoc0Loc1, _) => &[(GetLoc0, none), (GetLoc1, none)],
(IncLoc, _) => &[(GetLoc, imm), (Inc, none), (PutLoc, imm)],
(DecLoc, _) => &[(GetLoc, imm), (Dec, none), (PutLoc, imm)],
// The added value is on the stack, the local is the left operand
(AddLoc, _) => &[(GetLoc, imm), (Swap, none), (Add, none), (PutLoc, imm)],
(SetProto, _) => &[(DefineField, ImmOperand::Atom(PROTO_FIELD))],
// The low two bits of `arg`: 0 method, 1 getter, 2 setter
(DefineMethod, ImmOperand::AtomU8 { atom, arg }) => {
assert!(arg & 3 == 0, "getters and setters are not supported");
&[(DefineField, ImmOperand::Atom(atom))]
}
// The fn itself, which is its env
(SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))],
// [[HomeObject]], which only `super` reads. `super` is not supported
// (see set_home_object in regify)
(SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)],
// arguments, new.target, import.meta
(SpecialObject, _) => unimplemented!("special_object {imm:?}"),
_ => return false,
};
let mut push = pusher(instr.pc, out);
seq.iter().for_each(|&(op, imm)| push(op, imm));
true
}
/// Replace a read of a global with the constant it is (see `native`)
pub fn replace_get_var(
instr: &Instr,
atoms: &[Atom],
cpool: &mut Vec<JsValue>,
out: &mut Vec<Instr>,
) -> bool {
if instr.op != OpCode::GetVar {
return false;
}
cpool.push(native::global(instr.imm.atom(), atoms));
pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1));
true
}
+9 -365
View File
@@ -1,16 +1,18 @@
mod args;
mod bindings;
mod declass;
mod expand;
mod new;
use std::collections::HashMap;
use args::replace_arg_access;
use bindings::replace_bindings;
use declass::declass;
use expand::{replace_expansion, replace_get_var};
use new::{is_new, replace_new};
use crate::{
flatten::FlatFn,
libqjs::parse::{
Atom, AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode,
},
native,
regify::{is_var_read, is_var_write},
libqjs::parse::{Atom, AtomRef, ClosureVar, ImmOperand, Instr, opcodes::OpCode},
};
/// The field an object's prototype is kept in. In JS, `o.__proto__` is the
@@ -88,361 +90,3 @@ fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], atoms: &[Atom]) ->
fn pusher(pc: usize, out: &mut Vec<Instr>) -> impl FnMut(OpCode, ImmOperand) + '_ {
move |op, imm| out.push(Instr { pc, op, imm })
}
// ───────────────────────────────────────────────── New Processing ──────────────────────────────────────────────────────────────
/// If `instr` is a `new`, the number of args it passes
fn is_new(instr: &Instr) -> Option<usize> {
match (instr.op, instr.imm) {
(OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize),
_ => None,
}
}
/// Replace:
/// call_constructor n [F, new_target, a0 .. an-1]
///
/// With:
/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top
/// drop new_target, always a copy of F here
/// dup [F, F]
/// get_field prototype [F, proto]
/// object [F, proto, obj]
/// swap [F, obj, proto]
/// define_field __proto__ [F, obj], obj is { __proto__: F.prototype }
/// get_loc t0 .. get_loc t(n-1) the args back
/// call_constructor n [F, obj, a0 .. an-1], a method call of F on obj
///
/// Whether `new` gives the call's result or `obj` depends on types (see infer)
fn replace_new(instr: &Instr, temps: usize, out: &mut Vec<Instr>) -> bool {
let Some(n) = is_new(instr) else {
return false;
};
let mut push = pusher(instr.pc, out);
let temp = |i: usize| ImmOperand::Loc((temps + i) as u16);
for i in (0..n).rev() {
push(OpCode::PutLoc, temp(i));
}
push(OpCode::Drop, ImmOperand::None);
push(OpCode::Dup, ImmOperand::None);
push(OpCode::GetField, ImmOperand::Atom(PROTOTYPE_FIELD));
push(OpCode::Object, ImmOperand::None);
push(OpCode::Swap, ImmOperand::None);
push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD));
for i in 0..n {
push(OpCode::GetLoc, temp(i));
}
push(OpCode::CallConstructor, instr.imm);
true
}
// ─────────────────────────────────────────────── Binding Processing ────────────────────────────────────────────────────────────
/// Put every binding that a closure captures in a box: an object whose
/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The
/// owner and the closures read and write the box. A closure keeps the boxes
/// it captured in its own fields, one for each of its `closure_vars`: field `i`
/// holds the box of capture `i`
///
/// The fn makes a box for each binding that its children capture, at entry.
/// A captured local holds its box in its own slot. A captured arg keeps its
/// box in a new local, and the box starts with the arg's value. A `var` starts
/// as undefined. A `let` or `const` has no value until it is written (its TDZ):
/// object; undefined; define_field <var>; put_loc k each captured `var`
/// object; put_loc k each captured `let`
/// object; get_arg a; define_field <var>; put_loc box each captured arg
///
/// A scope that is entered again is a loop body. QuickJS closes each captured
/// binding of the scope (`close_loc`) at every way out of it, so a new box
/// there gives each iteration its own binding. `set_loc_uninitialized` only
/// marks the TDZ, which is not checked, and hoisted fns are made before it, so
/// it does not make a box
///
/// These instrs take a pc that no jump goes to, so a jump to the fn's first
/// instr does not make them again
///
/// Then it replaces, for a captured binding:
/// get_loc k get_loc box; get_field <var>
/// put_loc k get_loc box; swap; put_field <var>
/// set_loc k dup; get_loc box; swap; put_field <var>
/// close_loc k object; get_loc k; get_field <var>;
/// define_field <var>; put_loc k
/// a way out of a loop body: a new binding, with
/// the current value
///
/// For any binding:
/// get_var_ref i get_arg env; get_field i; get_field <var>
/// put_var_ref i get_arg env; get_field i; swap; put_field <var>
/// set_var_ref i dup; get_arg env; get_field i; swap; put_field <var>
/// fclosure f fclosure f; (push_box; define_field i)*
///
/// where `push_box` is the box of capture `i` of `f`: the local that holds it,
/// or field `j` of this fn's env for capture `j` of this fn. A binding with no
/// frame (a module binding) gets its box here, from `object`
///
/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure
/// captures, do nothing. Gives the code and the new number of locals
fn replace_bindings(
code: &[Instr],
cpool: &[JsValue],
captures: &[Vec<ClosureVar>],
var_count: usize,
) -> (Vec<Instr>, usize) {
use OpCode::*;
let children = cpool.iter().filter_map(|c| match c {
JsValue::FnById(child) => Some(*child),
_ => None,
});
let mut locals = vec![];
let mut args = vec![];
for c in children.flat_map(|child| &captures[child]) {
match c.flags.closure_kind {
ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)),
ClosureKind::Arg => args.push(c.var_idx),
_ => {}
}
}
locals.sort_unstable();
locals.dedup();
args.sort_unstable();
args.dedup();
// Value ids of bindings, as `is_var_read` gives them: args after every local
let new_var_count = var_count + args.len();
let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k;
// The local that holds the box of each captured binding, by value id
let boxes: HashMap<usize, usize> = (locals.iter().map(|&(k, _)| (k, k)))
.chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i)))
.collect();
let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32));
let value = ImmOperand::Atom(BOX_FIELD);
let none = ImmOperand::None;
let loc = |k: usize| ImmOperand::Loc(k as u16);
let mut out = Vec::with_capacity(code.len());
{
let mut push = pusher(usize::MAX, &mut out);
for &(k, lexical) in &locals {
push(Object, none);
if !lexical {
push(Undefined, none);
push(DefineField, value);
}
push(PutLoc, loc(k));
}
for (i, &k) in args.iter().enumerate() {
push(Object, none);
push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16));
push(DefineField, value);
push(PutLoc, loc(var_count + i));
}
}
for instr in code {
let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v));
let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v));
let own_box = |k: ImmOperand| match k {
ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(),
_ => None,
};
let mut push = pusher(instr.pc, &mut out);
if let Some(&b) = read {
push(GetLoc, loc(b));
push(GetField, value);
} else if let Some(&b) = write {
if instr.op.info().n_push == 1 {
push(Dup, none);
}
push(GetLoc, loc(b));
push(Swap, none);
push(PutField, value);
} else if let Some((i, writes, leaves)) = var_ref_access(instr) {
if leaves {
push(Dup, none);
}
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(i));
if writes {
push(Swap, none);
push(PutField, value);
} else {
push(GetField, value);
}
} else if instr.op == SetLocUninitialized {
push(Nop, none);
} else if instr.op == CloseLoc {
match own_box(instr.imm) {
Some(b) => {
push(Object, none);
push(GetLoc, loc(b));
push(GetField, value);
push(DefineField, value);
push(PutLoc, loc(b));
}
None => push(Nop, none),
}
} else if matches!(instr.op, Fclosure8 | Fclosure) {
let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else {
unreachable!()
};
push(Fclosure, instr.imm);
for (i, c) in captures[child].iter().enumerate() {
match c.flags.closure_kind {
ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])),
ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])),
ClosureKind::Ref => {
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(c.var_idx));
}
_ => push(Object, none),
}
push(DefineField, slot(i));
}
} else {
push(instr.op, instr.imm);
}
}
(out, new_var_count)
}
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes,
/// and whether a write also leaves the value on the stack
fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
use OpCode::*;
let i = match instr.imm {
ImmOperand::VarRef(i) => i as usize,
_ => 0,
};
match instr.op {
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)),
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)),
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)),
GetVarRef | GetVarRefCheck => Some((i, false, false)),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)),
SetVarRef => Some((i, true, true)),
_ => None,
}
}
// ──────────────────────────────────────────────── Arg Access Processing ────────────────────────────────────────────────────────
/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`,
/// and `this` with a read of its param
fn replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let (op, arg) = match (instr.op, instr.imm) {
(PushThis, _) => (GetArg, THIS_ARG),
(GetArg0, _) => (GetArg, FIRST_ARG),
(GetArg1, _) => (GetArg, FIRST_ARG + 1),
(GetArg2, _) => (GetArg, FIRST_ARG + 2),
(GetArg3, _) => (GetArg, FIRST_ARG + 3),
(PutArg0, _) => (PutArg, FIRST_ARG),
(PutArg1, _) => (PutArg, FIRST_ARG + 1),
(PutArg2, _) => (PutArg, FIRST_ARG + 2),
(PutArg3, _) => (PutArg, FIRST_ARG + 3),
(SetArg0, _) => (SetArg, FIRST_ARG),
(SetArg1, _) => (SetArg, FIRST_ARG + 1),
(SetArg2, _) => (SetArg, FIRST_ARG + 2),
(SetArg3, _) => (SetArg, FIRST_ARG + 3),
(GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k),
_ => return false,
};
pusher(instr.pc, out)(op, ImmOperand::Arg(arg));
true
}
// ───────────────────────────────────────────────── Misc Expansion ──────────────────────────────────────────────────────────────
/// Replace ops that are a fixed sequence of simpler ones
///
/// A sequence is never empty: jumps find their target by its pc (see regify),
/// so every instr must keep at least one instr at its pc
fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let none = ImmOperand::None;
let imm = instr.imm;
// A plain call has no `this`, so it passes `undefined`. It is pushed last,
// on top of the args, and the call pops it as one more arg (see regify)
let plain_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1))];
let tail_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1)), (Return, none)];
let seq: &[(OpCode, ImmOperand)] = match (instr.op, imm) {
(Call0, _) => &plain_call(0),
(Call1, _) => &plain_call(1),
(Call2, _) => &plain_call(2),
(Call3, _) => &plain_call(3),
(Call, ImmOperand::Npop(n)) => &plain_call(n),
(TailCall, ImmOperand::Npop(n)) => &tail_call(n),
(TailCallMethod, _) => &[(CallMethod, imm), (Return, none)],
(GetField2, _) => &[(Dup, none), (GetField, imm)],
(GetLoc0Loc1, _) => &[(GetLoc0, none), (GetLoc1, none)],
(IncLoc, _) => &[(GetLoc, imm), (Inc, none), (PutLoc, imm)],
(DecLoc, _) => &[(GetLoc, imm), (Dec, none), (PutLoc, imm)],
// The added value is on the stack, the local is the left operand
(AddLoc, _) => &[(GetLoc, imm), (Swap, none), (Add, none), (PutLoc, imm)],
(SetProto, _) => &[(DefineField, ImmOperand::Atom(PROTO_FIELD))],
// The low two bits of `arg`: 0 method, 1 getter, 2 setter
(DefineMethod, ImmOperand::AtomU8 { atom, arg }) => {
assert!(arg & 3 == 0, "getters and setters are not supported");
&[(DefineField, ImmOperand::Atom(atom))]
}
// The fn itself, which is its env
(SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))],
// [[HomeObject]], which only `super` reads. `super` is not supported
// (see set_home_object in regify)
(SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)],
// arguments, new.target, import.meta
(SpecialObject, _) => unimplemented!("special_object {imm:?}"),
_ => return false,
};
let mut push = pusher(instr.pc, out);
seq.iter().for_each(|&(op, imm)| push(op, imm));
true
}
/// Replace a read of a global with the constant it is (see `native`)
fn replace_get_var(
instr: &Instr,
atoms: &[Atom],
cpool: &mut Vec<JsValue>,
out: &mut Vec<Instr>,
) -> bool {
if instr.op != OpCode::GetVar {
return false;
}
cpool.push(native::global(instr.imm.atom(), atoms));
pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1));
true
}
+52
View File
@@ -0,0 +1,52 @@
use super::{PROTO_FIELD, PROTOTYPE_FIELD, pusher};
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
/// If `instr` is a `new`, the number of args it passes
pub fn is_new(instr: &Instr) -> Option<usize> {
match (instr.op, instr.imm) {
(OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize),
_ => None,
}
}
/// Replace:
/// call_constructor n [F, new_target, a0 .. an-1]
///
/// With:
/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top
/// drop new_target, always a copy of F here
/// dup [F, F]
/// get_field prototype [F, proto]
/// object [F, proto, obj]
/// swap [F, obj, proto]
/// define_field __proto__ [F, obj], obj is { __proto__: F.prototype }
/// get_loc t0 .. get_loc t(n-1) the args back
/// call_constructor n [F, obj, a0 .. an-1], a method call of F on obj
///
/// Whether `new` gives the call's result or `obj` depends on types (see infer)
pub fn replace_new(instr: &Instr, temps: usize, out: &mut Vec<Instr>) -> bool {
let Some(n) = is_new(instr) else {
return false;
};
let mut push = pusher(instr.pc, out);
let temp = |i: usize| ImmOperand::Loc((temps + i) as u16);
for i in (0..n).rev() {
push(OpCode::PutLoc, temp(i));
}
push(OpCode::Drop, ImmOperand::None);
push(OpCode::Dup, ImmOperand::None);
push(OpCode::GetField, ImmOperand::Atom(PROTOTYPE_FIELD));
push(OpCode::Object, ImmOperand::None);
push(OpCode::Swap, ImmOperand::None);
push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD));
for i in 0..n {
push(OpCode::GetLoc, temp(i));
}
push(OpCode::CallConstructor, instr.imm);
true
}