New model

This commit is contained in:
2026-10-01 13:58:50 +00:00
parent a4d295fbd9
commit 23a2c3bc46
15 changed files with 3431 additions and 689 deletions
-8
View File
@@ -1,8 +0,0 @@
use crate::{infer::InferResult, regify::RegFn};
// TODO: new by_ref that is value centric in one loop
pub fn analyze_by_ref(f: &RegFn, res: &InferResult, by_ref: &mut [bool]) {
for v in 0..f.val_count {
todo!()
}
}
+61
View File
@@ -0,0 +1,61 @@
//! Escape (see `docs/model.md`, section 6)
use super::{Flow, instrs};
use crate::{libqjs::parse::opcodes::OpCode::*, regify::RegFn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Escape {
/// No holder outlives the frame, so the cell can be on the stack
Local,
/// The cell must be on the heap. `docs/model.md` also has `returned`, which
/// this analysis does not prove yet, so such a cell escapes
Escapes,
}
/// The escape of each value that `object` or `fclosure` 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) {
let cell = instr.dsts[0];
flow.new_walk();
if !escapes(flow, cell, flow.in_loop[block]) {
escape[cell] = Escape::Local;
}
}
}
escape
}
/// Whether the cell that `v` holds can be reached after the frame returns, or
/// when its creating op runs again (`looped`: the op is in a loop). A var or a
/// join slot can keep the cell until the next run, so in a loop any copy
/// escapes. A field, a callee and a return can keep it beyond the frame
fn escapes(flow: &mut Flow, v: usize, looped: bool) -> bool {
flow.visit(v);
for i in flow.use_range(v) {
let u = flow.uses[i];
let out = match u.op {
Call | CallMethod | CallConstructor | Return | ReturnAsync => true,
DefineField | PutField if u.operand == 0 => true,
SetLoc | SetName if looped => true,
SetLoc | SetName => {
let dst = u.dst.unwrap() as usize;
flow.visit(dst) && escapes(flow, dst, looped)
}
_ => false,
};
if out {
return true;
}
}
false
}
+144 -7
View File
@@ -1,18 +1,155 @@
mod by_ref;
//! The analyses that narrow the representation (see `docs/model.md`, section 6).
//! They run after infer, on each fn's code
use crate::{infer::TypeCache, regify::RegFn};
use by_ref::analyze_by_ref;
mod escape;
use crate::{
libqjs::parse::opcodes::OpCode,
regify::{RegFn, RegInstr},
};
pub use escape::Escape;
pub struct Analysis {
pub val_by_ref: Vec<bool>,
/// Where the cell can be reached, for each value that a creating op gives
pub escape: Vec<Escape>,
}
impl Analysis {
fn of(reg_fn: &RegFn) -> Self {
todo!()
fn of(f: &RegFn) -> Self {
let mut flow = Flow::of(f);
Self {
escape: escape::analyze(f, &mut flow),
}
}
}
pub fn analyze(fns: &[RegFn], cache: &TypeCache) -> Vec<Analysis> {
pub fn analyze(fns: &[RegFn]) -> Vec<Analysis> {
fns.iter().map(Analysis::of).collect()
}
/// One use of a value: an operand of an instr
#[derive(Clone, Copy)]
struct Use {
op: OpCode,
/// Which operand the value is
operand: u32,
/// The instr's first dst, if it has one
dst: Option<u32>,
}
/// What the analyses need about a fn: the uses of each value, in one array
/// sorted by value (see `ranges()`), and which blocks are in a loop
struct Flow {
uses: Vec<Use>,
use_starts: Vec<u32>,
/// Whether a path leads from each block back to it
in_loop: Vec<bool>,
/// Scratch for walks over values and over blocks. A mark is current if it
/// equals its walk's stamp, so the arrays are never cleared
val_stamp: u32,
seen_vals: Vec<u32>,
block_stamp: u32,
seen_blocks: Vec<u32>,
stack: Vec<u32>,
}
impl Flow {
fn of(f: &RegFn) -> Self {
let uses = instrs(f).flat_map(|(_, instr)| {
(instr.args.iter().enumerate()).map(move |(operand, &v)| {
let dst = instr.dsts.first().map(|&d| d as u32);
(v, Use { op: instr.op, operand: operand as u32, dst })
})
});
let (uses, use_starts) = ranges(f.val_count, uses);
let mut flow = Self {
uses,
use_starts,
in_loop: vec![],
val_stamp: 0,
seen_vals: vec![0; f.val_count],
block_stamp: 0,
seen_blocks: vec![0; f.blocks.len()],
stack: vec![],
};
flow.in_loop = (0..f.blocks.len()).map(|b| flow.returns_to(f, b)).collect();
flow
}
/// The uses of value `v`, by index into `uses`
fn use_range(&self, v: usize) -> std::ops::Range<usize> {
self.use_starts[v] as usize..self.use_starts[v + 1] as usize
}
/// Start a new walk over values: every mark of the walks before is now
/// stale
fn new_walk(&mut self) {
self.val_stamp += 1;
}
/// Mark value `v` seen in this walk. Whether it was not seen before
fn visit(&mut self, v: usize) -> bool {
let fresh = self.seen_vals[v] != self.val_stamp;
self.seen_vals[v] = self.val_stamp;
fresh
}
/// Whether a path leads from `block` back to it
fn returns_to(&mut self, f: &RegFn, block: usize) -> bool {
self.block_stamp += 1;
let stamp = self.block_stamp;
self.stack.clear();
self.stack.extend(f.blocks[block].succ.iter().map(|&s| s as u32));
while let Some(b) = self.stack.pop() {
let b = b as usize;
if self.seen_blocks[b] == stamp {
continue;
}
if b == block {
return true;
}
self.seen_blocks[b] = stamp;
self.stack.extend(f.blocks[b].succ.iter().map(|&s| s as u32));
}
false
}
}
/// The `(value, item)` pairs, as one array grouped by value: the items of
/// value `v` are `items[starts[v]..starts[v + 1]]`
fn ranges<T>(n: usize, pairs: impl Iterator<Item = (usize, T)>) -> (Vec<T>, Vec<u32>) {
let mut pairs: Vec<(usize, T)> = pairs.collect();
pairs.sort_by_key(|(v, _)| *v);
let mut starts = vec![0u32; n + 1];
for (v, _) in &pairs {
starts[v + 1] += 1;
}
for v in 0..n {
starts[v + 1] += starts[v];
}
(pairs.into_iter().map(|(_, item)| item).collect(), starts)
}
/// Every instr of `f`, with its block: a block's own instrs, then its postfix
/// copies
fn instrs(f: &RegFn) -> impl Iterator<Item = (usize, &RegInstr)> {
f.blocks.iter().enumerate().flat_map(move |(b, block)| {
let code = f.code[block.start..block.end].iter();
code.chain(&block.postfix).map(move |instr| (b, instr))
})
}
+1123
View File
File diff suppressed because it is too large Load Diff
+445 -126
View File
@@ -1,220 +1,539 @@
//! Infers a type for every value (see `docs/model.md`, section 3)
pub mod types;
use std::collections::HashMap;
use crate::{
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native,
regify::RegFn,
transform::{FIRST_ARG, THIS_ARG},
};
use types::{Type, get_field, join_ty, set_field};
use types::{Code, Heap, Number, Site, Type};
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct InferRequest {
const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
/// One walk of a fn with one tuple of arg types (see `docs/model.md`,
/// section 3.4)
#[derive(Debug)]
pub struct Variant {
pub fn_id: usize,
/// One for each of the fn's params (see `TypeCache::variant()`)
pub arg_types: Vec<Type>,
/// The walk in the current round, once it is done
pub result: Option<InferResult>,
/// What it returned in the rounds so far, which is what a call of it gives
/// while it is still being walked (a recursive call)
ret: Type,
walking: bool,
}
#[derive(Debug)]
pub struct InferResult {
pub val_types: Vec<Type>,
/// The type each instr gives, before it is joined into its dst
pub instr_types: Vec<Type>,
pub ret_type: Type,
/// The blocks that can run
pub reachable: Vec<bool>,
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct TypeCache {
results: HashMap<InferRequest, InferResult>,
shapes: HashMap<Type, usize>,
next_shape_id: usize,
pub variants: Vec<Variant>,
/// Each fn's variants, by their arg types
by_args: Vec<HashMap<Vec<Type>, usize>>,
/// Scratch for a call's arg types
key: Vec<Type>,
pub heap: Heap,
/// The prototype of each primitive kind, as the stdlib links it, by kind
pub protos: [Type; 3],
/// Goes up when a variant's return or a proto changes (see `infer_types()`)
changes: usize,
}
impl TypeCache {
pub fn get_result(&self, req: &InferRequest) -> Option<&InferResult> {
self.results.get(req)
/// The variant for a call of `fn_id` with args of these types, made if it
/// is new. It is keyed by the fn's params: an arg it does not pass is
/// undefined, and an arg it passes beyond them is never read. Only an
/// arg's kind crosses a call, not a known value, else every literal passed
/// would make a new variant
pub fn variant(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
self.key.clear();
self.key.extend(params(fn_id, arg_types, fns));
if let Some(&v) = self.by_args[fn_id].get(&self.key[..]) {
return v;
}
let v = self.variants.len();
self.by_args[fn_id].insert(self.key.clone(), v);
self.variants.push(Variant {
fn_id,
arg_types: self.key.clone(),
result: None,
ret: Type::EMPTY,
walking: false,
});
v
}
pub fn insert_result(&mut self, req: InferRequest, res: InferResult) {
self.results.insert(req, res);
/// The variant for a call that infer walked
pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
let key: Vec<Type> = params(fn_id, arg_types, fns).collect();
self.by_args[fn_id][&key]
}
pub fn insert_shape(&mut self, ty: &Type) {
if !self.shapes.contains_key(ty) {
self.shapes.insert(ty.clone(), self.next_shape_id);
self.next_shape_id += 1;
/// Infer a call of `fn_id`, walking it unless an identical call was
/// already walked in this round. Gives what it returns
fn call(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> Type {
let v = self.variant(fn_id, arg_types, fns);
match &self.variants[v] {
Variant { result: Some(res), .. } => return res.ret_type,
Variant { walking: true, ret, .. } => return *ret,
_ => {}
}
self.variants[v].walking = true;
let res = walk_fn(v, fns, self);
let ret = res.ret_type;
let last = self.variants[v].ret;
let joined = self.heap.join(&last, &ret);
self.changes += (joined != last) as usize;
let variant = &mut self.variants[v];
variant.walking = false;
variant.ret = joined;
variant.result = Some(res);
ret
}
}
/// The type of each of fn `fn_id`'s params, for a call with args of these
/// types (see `TypeCache::variant()`)
fn params<'a>(fn_id: usize, arg_types: &'a [Type], fns: &[RegFn]) -> impl Iterator<Item = Type> + 'a {
(0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::UNDEFINED, Type::widen))
}
/// Walk every variant from the entry, in rounds, until a round changes
/// nothing. A variant reads the shapes, the protos and the returns of other
/// variants, which variants walked after it can still add to. All of them only
/// grow, so the rounds end. The results are those of the last round
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
let mut cache = TypeCache {
variants: vec![],
by_args: fns.iter().map(|_| HashMap::new()).collect(),
key: vec![],
heap: Heap::new(fns),
protos: [Type::EMPTY; 3],
changes: 0,
};
loop {
let before = (cache.heap.version, cache.changes);
cache.variants.iter_mut().for_each(|v| v.result = None);
// The synthetic program entry is appended last by `flatten`. It is
// called by the host with no args
cache.call(fns.len() - 1, &[], fns);
if before == (cache.heap.version, cache.changes) {
return cache;
}
}
}
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
let mut cache = TypeCache::default();
// The synthetic program entry is appended last by `flatten`. It is called
// by the host with no args
let req = InferRequest {
fn_id: fns.len() - 1,
arg_types: vec![],
};
let res = walk_fn(&req, fns, &mut cache);
cache.insert_result(req, res);
cache
}
#[allow(non_snake_case)]
fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
use OpCode::*;
let f = &reg_fns[req.fn_id];
let mut val_types = vec![Type::Unknown; f.val_count];
let mut ret_type = Type::Unknown;
let mut old_types = val_types.clone();
let fn_id = cache.variants[variant].fn_id;
let f = &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;
// arg `i` is value id `var_count + i` (see regify)
// Init them with the arg types passed in
for (i, ty) in req.arg_types.iter().enumerate() {
val_types[f.var_count + i] = ty.clone();
for (i, ty) in cache.variants[variant].arg_types.iter().enumerate() {
val_types[f.var_count + i] = *ty;
}
// Walk instrs till types stop changing
// The blocks that can run, given what the types say about each condition
let mut reachable = vec![false; f.blocks.len()];
reachable[0] = true;
// Scratch for calls, kept across instrs
let mut arg_types: Vec<Type> = vec![];
let mut codes: Vec<Code> = vec![];
// Walk instrs till nothing changes. A return only changes `ret_type` once
// its block is reachable, and a field read only gives more once the heap
// changes, so the values, the blocks and the heap are enough to watch
loop {
old_types.clone_from_slice(&val_types); // TODO: We only compare val types not ret type
let version = cache.heap.version;
let mut changed = false;
for (block_idx, block) in f.blocks.iter().enumerate() {
if !reachable[block_idx] {
continue;
}
for block in &f.blocks {
let code = &f.code[block.start..block.end];
let code = code.iter().chain(&block.postfix);
for instr in code {
for (offset, instr) in code.enumerate() {
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
if instr.dsts.is_empty() && !is_ret {
// A rule with an empty operand gives an empty result: no value
// has reached the instr yet
let waits = instr.args.iter().any(|&v| val_types[v].is_empty());
if (instr.dsts.is_empty() && !is_ret) || waits {
continue;
}
// TODO: Some of these ops may not every show up by this point
// The instr's index in the code (postfix copies have none)
let idx = block.start + offset;
let arg = |i: usize| val_types[instr.args[i]];
let ty = match instr.op {
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7
| PushI8 | PushI16 | PushI32 => Type::Int,
| PushI8 | PushI16 | PushI32 => Type::number(Number::Int),
PushTrue | PushFalse => Type::Bool,
PushTrue => Type::bool(Some(true)),
PushFalse => Type::bool(Some(false)),
PushAtomValue => Type::string(Some(instr.imm.atom())),
PushEmptyString => Type::string(Some(EMPTY_STRING)),
PushConst | PushConst8 => match instr.imm {
ImmOperand::Const(k) => match f.cpool.get(k as usize) {
Some(JsValue::Int(_)) => Type::Int,
Some(JsValue::Float(_)) => Type::Float,
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
Some(JsValue::NativeFn(id)) => Type::NativeFn(*id),
_ => unreachable!(),
},
PushConst | PushConst8 => match f.cpool[instr.imm.const_idx()] {
JsValue::Int(_) => Type::number(Number::Int),
JsValue::Float(_) => Type::number(Number::Float),
JsValue::String(_) => Type::string(None),
JsValue::BoolTrue => Type::bool(Some(true)),
JsValue::BoolFalse => Type::bool(Some(false)),
JsValue::Undefined => Type::UNDEFINED,
JsValue::NativeFn(id) => cache.heap.create(cache.heap.builtin(Site::Native(id))),
JsValue::ProtoLink(kind) => cache.heap.create(cache.heap.builtin(Site::ProtoLink(kind))),
_ => unreachable!(),
},
Undefined => Type::UNDEFINED,
Null => Type::NULL,
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
Add | Sub | Mul | Mod | Pow => {
match (&val_types[instr.args[0]], &val_types[instr.args[1]]) {
(Type::Unknown, _) | (_, Type::Unknown) => Type::Unknown,
(Type::Int, Type::Int) => Type::Int,
(Type::Float, Type::Float)
| (Type::Int, Type::Float)
| (Type::Float, Type::Int) => Type::Float,
_ => Type::Any,
// `+` concatenates if either side becomes a string, which
// only a string or an object can
Add => {
let (l, r) = (arg(1), arg(0));
let is_string = |t: Type| t.is_only(Type::string(None));
let can_be_string = |t: Type| t.string.is_some() || t.object.is_some();
if is_number(l) && is_number(r) {
Type::number(l.number.max(r.number).unwrap())
} else if is_string(l) || is_string(r) {
Type::string(None)
} else if can_be_string(l) || can_be_string(r) {
Type { string: Some(None), ..Type::number(Number::Float) }
} else {
Type::number(Number::Float)
}
}
Neg | Plus | Inc | Dec | PostInc | PostDec => match val_types[instr.args[0]] {
Type::Unknown => Type::Unknown,
Type::Int => Type::Int,
Type::Float => Type::Float,
_ => Type::Any,
// Any other operator converts its operands to numbers
Sub | Mul | Mod | Pow => match (arg(1), arg(0)) {
(l, r) if is_int(l) && is_int(r) => Type::number(Number::Int),
_ => Type::number(Number::Float),
},
Div => Type::Float,
Neg | Plus | Inc | Dec | PostInc | PostDec => match arg(0) {
t if is_int(t) => Type::number(Number::Int),
_ => Type::number(Number::Float),
},
Div => Type::number(Number::Float),
// Bitwise NOT gives an int32
Not => Type::number(Number::Int),
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
Lt | Lte | Gt | Gte => Type::bool(None),
// ─────────────────────────────── Conditions the types may decide ──────────────────────────────────────────
// A known value lets infer skip the branch that cannot run
// (see `reachable` below)
// An identity test of two objects (see `docs/model.md`, section 5)
StrictEq | StrictNeq | Eq | Neq if arg(0).object.is_some() && arg(1).object.is_some() => {
for obj in [arg(0).object, arg(1).object].into_iter().flatten() {
cache.heap.mark(obj, |shape| &mut shape.compared);
}
Type::bool(None)
}
StrictEq => Type::bool(strict_eq(&arg(0), &arg(1))),
StrictNeq => Type::bool(strict_eq(&arg(0), &arg(1)).map(|eq| !eq)),
Eq => Type::bool(loose_eq(&arg(0), &arg(1))),
Neq => Type::bool(loose_eq(&arg(0), &arg(1)).map(|eq| !eq)),
Lnot => Type::bool(truthiness(&arg(0)).map(|t| !t)),
Typeof => typeof_of(&arg(0), &cache.heap),
IsUndefined | TypeofIsUndefined => Type::bool(test(&arg(0), Type::UNDEFINED)),
IsNull => Type::bool(test(&arg(0), Type::NULL)),
IsUndefinedOrNull => Type::bool(test(&arg(0), Type { null: true, ..Type::UNDEFINED })),
TypeofIsFunction => Type::bool(is_function(&arg(0), &cache.heap)),
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
SetLoc | SetName => val_types[instr.args[0]].clone(),
Undefined => Type::Undefined,
SetLoc | SetName => arg(0),
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
Fclosure | Fclosure8 => {
let cpool_idx = match instr.imm {
ImmOperand::Const(idx) => idx as usize,
_ => unreachable!(),
};
Object | Fclosure | Fclosure8 => cache.heap.create(cache.heap.site_at(fn_id, idx).unwrap()),
// The write goes into the object's shape, which every value
// that holds the object sees. A primitive has no fields to
// write, and undefined and null throw
DefineField | PutField => {
if let Some(obj) = arg(1).object {
cache.heap.write(obj, instr.imm.atom(), &arg(0));
let fn_id = match f.cpool[cpool_idx] {
JsValue::FnById(id) => id,
_ => unreachable!(),
};
Type::Closure {
fn_id,
env: Box::new(Type::Empty),
if !cache.heap.is_creation_write(fn_id, idx) {
cache.heap.mark(obj, |shape| &mut shape.written);
}
}
cache.heap.canon(&arg(1))
}
DefineField => {
let field = match instr.imm {
ImmOperand::Atom(atom) => atom,
_ => unreachable!(),
};
let obj = &val_types[instr.args[1]];
let ty = set_field(obj, field, &val_types[instr.args[0]]);
let shape = match &ty {
Type::Closure { env, .. } => env.as_ref(),
other => other,
};
cache.insert_shape(shape);
ty
GetField => cache.heap.read_value(&arg(0), instr.imm.atom(), &cache.protos),
// A string's length is its own. Anything else reads a
// `length` field
GetLength => {
let others = Type { string: None, ..arg(0) };
let read = cache.heap.read_value(&others, LENGTH_FIELD, &cache.protos);
let own = Type { number: arg(0).string.map(|_| Number::Int), ..Type::EMPTY };
cache.heap.join(&read, &own)
}
Object => Type::Empty,
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod
| TailCallMethod => todo!(),
// Operands are the callee's params: the callee (its env),
// `this`, then the args (see regify). The call runs one of
// the callee's fns. A callee that is not a fn throws, so it
// gives nothing
Call | CallMethod | CallConstructor => {
arg_types.clear();
arg_types.extend(instr.args.iter().map(|&v| val_types[v]));
let args = &arg_types[FIRST_ARG as usize..];
codes.clear();
if let Some(callee) = arg_types[0].object {
codes.extend(&cache.heap.shape(callee).code);
}
let mut ret = Type::EMPTY;
for &code in &codes {
let ty = match code {
Code::Fn(fn_id) => cache.call(fn_id, &arg_types, reg_fns),
Code::Native(id) => native::ret_type(id, args),
Code::ProtoLink(kind) => {
let linked = cache.protos[kind as usize];
let joined = cache.heap.join(&linked, &args[0]);
cache.changes += (joined != linked) as usize;
cache.protos[kind as usize] = joined;
Type::UNDEFINED
}
};
ret = cache.heap.join(&ret, &ty);
}
// `new` gives what the constructor returns if that is an
// object, else the new object itself, `this`
let others = Type { object: None, ..ret };
match instr.op {
CallConstructor if !others.is_empty() => {
let this = val_types[instr.args[THIS_ARG as usize]];
cache.heap.join(&Type { object: ret.object, ..Type::EMPTY }, &this)
}
_ => ret,
}
}
// ──────────────────────────────────────── Returns ──────────────────────────────────────────────────────────
Return | ReturnAsync => val_types[instr.args[0]].clone(),
ReturnUndef => Type::Undefined,
Return | ReturnAsync => arg(0),
ReturnUndef => Type::UNDEFINED,
_ => unreachable!("{instr:?}"),
};
if is_ret {
ret_type = join_ty(&ret_type, &ty);
} else {
for &dst in &instr.dsts {
val_types[dst] = join_ty(&val_types[dst], &ty);
}
// Postfix copies come after the block's own instrs, and have no
// entry: a copy gives the type of what it copies
if idx < block.end {
instr_types[idx] = cache.heap.join(&instr_types[idx], &ty);
}
// A copy of an object created by SetLoc is the same object, (see regify)
// so what is done through the copy is done to the original as well
if instr.op == SetLoc {
let is_object = matches!(
val_types[instr.dsts[0]],
Type::Empty | Type::WithField { .. } | Type::Closure { .. }
);
if is_ret {
ret_type = cache.heap.join(&ret_type, &ty);
continue;
}
if is_object {
let (src, dst) = (instr.args[0], instr.dsts[0]);
val_types[src] = join_ty(&val_types[src], &val_types[dst]);
}
for &dst in &instr.dsts {
let joined = cache.heap.join(&val_types[dst], &ty);
changed |= joined != val_types[dst];
val_types[dst] = joined;
}
} // instr
// Mark the blocks this one can go to. A conditional jump whose
// condition the types decide goes one way only. One whose
// condition nothing has reached yet goes nowhere yet
// (A block that only carries copies on an edge has no instrs)
let jumps = match f.code[block.start..block.end].last() {
Some(last) if matches!(last.op, IfFalse | IfFalse8 | IfTrue | IfTrue8) => {
match val_types[last.args[0]] {
cond if cond.is_empty() => continue,
cond => truthiness(&cond).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
}
}
_ => None,
};
// succ[0] is the jump target, succ[1] the fall-through
for (i, &succ) in block.succ.iter().enumerate() {
if jumps.is_none_or(|jumps| jumps == (i == 0)) && !reachable[succ] {
reachable[succ] = true;
changed = true;
}
}
} // block
if old_types == val_types {
if !changed && cache.heap.version == version {
break;
}
} // loop
// A type names the site its shape was kept under at the time: make it the
// one now
for ty in val_types.iter_mut().chain(&mut instr_types).chain([&mut ret_type]) {
*ty = cache.heap.canon(ty);
}
InferResult {
val_types,
instr_types,
ret_type,
reachable,
}
}
/// Whether every value of type `ty` is a number
fn is_number(ty: Type) -> bool {
ty.is_only(Type::number(Number::Int))
}
/// Whether every value of type `ty` is an int32
fn is_int(ty: Type) -> bool {
ty == Type::number(Number::Int)
}
/// The type, if every value of it is one value that its kind and refinement
/// tell
fn single(ty: &Type) -> Option<Type> {
let one = match ty.kinds() {
[true, false, false, false, false, false] | [false, true, false, false, false, false] => true,
[false, false, true, false, false, false] => ty.bool.flatten().is_some(),
[false, false, false, false, true, false] => ty.string.flatten().is_some(),
_ => false,
};
one.then_some(*ty)
}
/// Whether `a === b`, if the types decide it
fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
let disjoint = !a.kinds().iter().zip(b.kinds()).any(|(x, y)| *x && y);
match (single(a), single(b)) {
_ if disjoint => Some(false),
(Some(x), Some(y)) => Some(x == y),
_ => None,
}
}
/// Whether `a == b`, if the types decide it. Undefined and null are equal to
/// each other and to nothing else. Two values of one kind compare as `===`
/// does
fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
let nullish = |t: &Type| t.has_nullish() && t.non_nullish().is_empty();
match (a, b) {
_ if nullish(a) && nullish(b) => Some(true),
_ if (nullish(a) && !b.has_nullish()) || (!a.has_nullish() && nullish(b)) => Some(false),
_ if a.kinds() == b.kinds() && single(a).is_some() => strict_eq(a, b),
_ => None,
}
}
/// Whether every value of type `ty` is of a kind in `kinds`: true if every
/// value is, false if none is, `None` if the type does not decide it
fn test(ty: &Type, kinds: Type) -> Option<bool> {
let [inside, outside] = [true, false].map(|want| {
ty.kinds().iter().zip(kinds.kinds()).any(|(has, of)| *has && of == want)
});
match (inside, outside) {
(true, false) => Some(true),
(false, true) => Some(false),
_ => None,
}
}
/// Whether every value of type `ty` is a fn, if the type decides it
fn is_function(ty: &Type, heap: &Heap) -> Option<bool> {
let shape = ty.object.map(|site| heap.shape(site));
let fns = shape.is_some_and(|shape| !shape.code.is_empty());
let others = ty.kinds()[..5].contains(&true) || shape.is_some_and(|shape| shape.plain);
match (fns, others) {
(true, false) => Some(true),
(false, _) => Some(false),
_ => None,
}
}
/// Whether a value of type `ty` is truthy, if the type decides it
fn truthiness(ty: &Type) -> Option<bool> {
let truthy = [
ty.undefined.then_some(Some(false)),
ty.null.then_some(Some(false)),
ty.bool,
ty.number.map(|_| None),
ty.string.map(|s| s.map(|atom| atom != EMPTY_STRING)),
ty.object.map(|_| Some(true)),
];
let mut kinds = truthy.into_iter().flatten();
let first = kinds.next()?;
kinds.all(|t| t == first).then_some(first).flatten()
}
/// What `typeof` gives for a value of type `ty`
fn typeof_of(ty: &Type, heap: &Heap) -> Type {
let shape = ty.object.map(|site| heap.shape(site));
let names = [
ty.undefined.then_some(73), // "undefined"
ty.null.then_some(77), // "object"
ty.bool.map(|_| 75), // "boolean"
ty.number.map(|_| 74), // "number"
ty.string.map(|_| 76), // "string"
shape.filter(|shape| shape.plain).map(|_| 77), // "object"
shape.filter(|shape| !shape.code.is_empty()).map(|_| 27), // "function"
];
let mut names = names.into_iter().flatten();
match names.next() {
Some(name) if names.all(|n| n == name) => Type::string(Some(AtomRef::Predefined(name))),
_ => Type::string(None),
}
}
+457 -136
View File
@@ -1,172 +1,493 @@
use crate::{libqjs::parse::AtomRef, transform::PROTO_FIELD};
//! Types and shapes (see `docs/model.md`, section 3)
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Type {
Unknown,
Undefined,
Bool,
use std::cell::Cell;
use crate::{
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native::{NATIVES, Primitive},
regify::RegFn,
transform::{BOX_FIELD, PROTO_FIELD},
};
/// 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),
}
/// The number of a site: its index in `Heap::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(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 {
Int,
Float,
Empty,
Closure {
fn_id: usize,
env: Box<Type>,
},
NativeFn(usize),
WithField {
name: AtomRef,
ty: Box<Type>,
rest: Box<Type>,
},
Any,
}
/// Iterator over a type's fields
///
/// `follow_proto` sets whether we iterate within the type's prototype field too.
/// The type's own fields come first, so a field the object defines itself shadows
/// the prototype's
///
/// That also means a field name can be yielded more than once, and the first occurrence
/// is the one that counts
pub struct Fields<'a> {
current: &'a Type,
proto: Option<&'a Type>,
follow_proto: bool,
}
impl<'a> Iterator for Fields<'a> {
type Item = (AtomRef, &'a Type);
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.current {
Type::WithField { name, ty, rest } => {
if *name == PROTO_FIELD && self.follow_proto {
self.proto = Some(ty);
}
self.current = rest;
return Some((*name, ty));
}
Type::Closure { env, .. } => self.current = env,
// Own fields are exhausted, try step out to the prototype
_ => self.current = self.proto.take()?,
}
}
}
/// 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>,
}
impl Type {
/// The object's own fields, not those of its prototype
pub fn fields(&self) -> Fields<'_> {
Fields {
current: self,
proto: None,
follow_proto: false,
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
pub fn widen(&self) -> Type {
Type {
bool: self.bool.map(|_| None),
string: self.string.map(|_| None),
..*self
}
}
/// The object's own fields, then its prototype's, and so on
pub fn fields_via_proto(&self) -> Fields<'_> {
Fields {
current: self,
proto: None,
follow_proto: true,
/// The type without undefined and null
pub fn non_nullish(&self) -> Type {
Type {
undefined: false,
null: false,
..*self
}
}
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()
}
}
/// Type lattice join rules:
/// Unknown * T = T
/// Any * T = Any
/// T * T = T
/// T * U = Any
///
/// Composite objects can be joined too:
/// Closure(f, e) * Closure(f, e') = Closure(f, e * e')
/// object A * object B = every field of A and B, each field joined
///
/// TODO: Where are composite object joins required?
pub fn join_ty(a: &Type, b: &Type) -> Type {
/// 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>> {
match (a, b) {
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
(Type::Any, _) | (_, Type::Any) => Type::Any,
(a, b) if a == b => a.clone(),
(Type::Closure { fn_id: f, env: e }, Type::Closure { fn_id: g, env: e2 }) if f == g => {
Type::Closure {
fn_id: *f,
env: Box::new(join_ty(e, e2)),
}
}
(Type::Empty | Type::WithField { .. }, Type::Empty | Type::WithField { .. }) => b
.fields()
.fold(a.clone(), |acc, (name, value)| set_field(&acc, name, value)),
_ => Type::Any,
(Some(a), Some(b)) => Some(if a == b { a } else { None }),
(a, b) => a.or(b),
}
}
/// Do a look-up of `field` on `obj`, going through own fields then prototype
/// chain's fields
pub fn get_field(obj: &Type, field: AtomRef) -> &Type {
obj.fields_via_proto()
.find(|(name, _)| *name == field)
.map(|(_, ty)| ty)
.unwrap_or(&Type::Undefined)
/// 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,
}
/// Write `field` into `obj`
///
/// If the object type already has a field by that name, the types are joined,
/// else a new type with `field` is created
pub fn set_field(obj: &Type, field: AtomRef, field_ty: &Type) -> Type {
match obj {
// A field written onto a closure goes into its environment.
Type::Closure { fn_id, env } => Type::Closure {
fn_id: *fn_id,
env: Box::new(set_field(env, field, field_ty)),
},
/// 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,
}
// Write into existing field, join types
Type::WithField { name, ty, rest } if *name == field => {
let joined = join_ty(ty, field_ty);
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)
}
if joined == **ty {
return obj.clone();
/// 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
}
}
/// 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>,
}
/// 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,
}
/// 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,
}
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,
};
// 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);
}
}
heap.natives = heap.sites.len() as SiteId;
for id in 0..NATIVES.len() {
heap.add(Site::Native(id), Some(Code::Native(id)));
}
Type::WithField {
name: field,
ty: Box::new(joined),
rest: rest.clone(),
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]
}
Type::WithField { name, ty, rest } => {
let new_rest = set_field(rest, field, field_ty);
/// 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)
}
// Nothing changed deeper down, so nothing changes here either.
// TODO: Avoid this recursion and deep eq checking
if new_rest == **rest {
return obj.clone();
/// 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
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)),
},
}
}
/// 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);
}
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);
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;
}
self.find(into)
}
/// 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;
}
/// 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;
}
Type::WithField {
name: *name,
ty: ty.clone(),
rest: Box::new(new_rest),
// 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
}
/// 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);
}
_ => Type::WithField {
name: field,
ty: Box::new(field_ty.clone()),
rest: Box::new(obj.clone()),
},
read
}
}
+21 -1
View File
@@ -92,7 +92,7 @@ impl Atom {
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AtomRef {
TaggedInt(u32),
Predefined(u32),
@@ -170,6 +170,8 @@ pub enum JsValue {
FnById(usize),
/// 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),
Module(Module),
TypedArray,
ArrayBuffer,
@@ -516,6 +518,24 @@ pub enum ImmOperand {
},
}
impl ImmOperand {
/// The atom of an instr that names one, such as `get_field`
pub fn atom(self) -> AtomRef {
match self {
Self::Atom(atom) | Self::AtomU8 { atom, .. } | Self::AtomU16 { atom, .. } => atom,
_ => unreachable!("{self:?}"),
}
}
/// The cpool index of an instr that takes a constant, such as `fclosure`
pub fn const_idx(self) -> usize {
match self {
Self::Const(idx) => idx as usize,
_ => unreachable!("{self:?}"),
}
}
}
#[derive(Clone)]
pub struct Instr {
pub pc: usize,
+10 -1
View File
@@ -1,4 +1,5 @@
mod analyze;
mod emit;
mod flatten;
mod infer;
mod libqjs;
@@ -9,12 +10,15 @@ mod transform;
use std::fs::File;
use std::io::Read;
use analyze::analyze;
use flatten::flatten;
use infer::infer_types;
use libqjs::compile;
use regify::regify;
use transform::transform;
const STDLIB: &[u8] = include_bytes!("../../lib/stdlib.js");
fn main() {
// Load source file
let args = std::env::args().collect::<Vec<String>>();
@@ -24,7 +28,9 @@ fn main() {
let metadata = std::fs::metadata(filename).unwrap();
assert!(metadata.is_file());
let mut src = Vec::with_capacity(metadata.len() as usize);
// The stdlib is compiled with the program, in front of it
let mut src = STDLIB.to_vec();
src.reserve(metadata.len() as usize);
file.read_to_end(&mut src).unwrap();
// Compile to bytecode
@@ -35,4 +41,7 @@ fn main() {
let flat_fns = transform(flat_fns, &captures, &atoms);
let reg_fns = regify(flat_fns);
let ty_cache = infer_types(&reg_fns);
let analysis = analyze(&reg_fns);
print!("{}", emit::emit(&reg_fns, &ty_cache, &analysis, &atoms));
}
+129 -18
View File
@@ -1,33 +1,146 @@
//! What the stdlib (`lib/stdlib.js`) 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
//! builds the JS-visible functions and methods on top of them, and the
//! compiler's own code for operators (`+` on strings and so on) uses them
//! directly
//!
//! Proto links tell the compiler which object is the prototype of a primitive
//! kind. A field read on a primitive borrows its kind's prototype
use crate::{
infer::types::Type,
libqjs::parse::{Atom, AtomRef, JsString},
infer::types::{Number, Type},
libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS},
};
/// A native C function callable by JS
///
/// A native is monomorphic: it takes one kind for each arg, and returns one
/// fixed type. Anything more general is written in JS, in the stdlib, on top
/// of natives
pub struct NativeFn {
pub name: &'static str,
pub ret: fn(&[Type]) -> Type, // TODO: switch to strictly monomorphic
pub args: &'static [Type],
pub ret: Type,
}
static NATIVES: &[NativeFn] = &[NativeFn {
name: "__native_print",
ret: |_| Type::Undefined,
}];
macro_rules! native {
($name:literal ( $($arg:ident),* ) -> $ret:ident) => {
NativeFn {
name: $name,
args: &[$(ty!($arg)),*],
ret: ty!($ret),
}
};
}
pub fn native_fn_idx(name: AtomRef, atoms: &[Atom]) -> Option<usize> {
let AtomRef::FileLocal(idx) = name else {
return None;
/// 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) };
}
#[rustfmt::skip]
pub static NATIVES: &[NativeFn] = &[
// IO
native!("__native_print_string"(String) -> Undefined),
// Numbers. JS has one number type, see `accepts()`
native!("__native_number_to_string"(Float) -> String),
native!("__native_number_to_fixed"(Float, Int) -> String),
native!("__native_string_to_number"(String) -> Float),
// Strings. Indices and lengths count UTF-16 code units, as in JS
native!("__native_string_concat"(String, String) -> String),
native!("__native_string_slice"(String, Int, Int) -> String),
native!("__native_string_char_code_at"(String, Int) -> Int),
native!("__native_string_from_char_code"(Int) -> String),
];
/// A primitive kind whose field reads borrow a prototype from the stdlib
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Primitive {
String,
Number,
Boolean,
}
impl Primitive {
pub const ALL: [Primitive; 3] = [Primitive::String, Primitive::Number, Primitive::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(),
}
}
}
/// `__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),
];
/// The value of the global `name`. The only globals are the ones the stdlib
/// may read, anything else is an error
pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue {
let name = atom_name(name, atoms);
// A global property in JS, but a constant
if name == "undefined" {
return JsValue::Undefined;
}
if let Some(idx) = NATIVES.iter().position(|n| name == n.name) {
return JsValue::NativeFn(idx);
}
match PROTO_LINKS.iter().find(|(link, _)| name == *link) {
Some(&(_, kind)) => JsValue::ProtoLink(kind),
None => panic!("unknown global `{name}`"),
}
}
/// 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 {
let native = &NATIVES[idx];
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)
};
let atom = atoms.get(idx)?;
match native.args.iter().zip(args.iter().chain(missing)).all(can_pass) {
true => native.ret,
false => Type::EMPTY,
}
}
let js_string = match atom {
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s,
_ => return None,
pub fn atom_name(name: AtomRef, atoms: &[Atom]) -> String {
let js_string = match name {
AtomRef::FileLocal(idx) => match &atoms[idx] {
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s,
Atom::Predefined(n) => return PREDEFINED_ATOMS[*n as usize].to_string(),
},
AtomRef::Predefined(n) => return PREDEFINED_ATOMS[n as usize].to_string(),
AtomRef::TaggedInt(n) => return n.to_string(),
};
js_string_text(js_string)
}
pub fn js_string_text(js_string: &JsString) -> String {
// TODO: Avoid string alloc
let string = match js_string {
match js_string {
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
JsString::UTF16(b) => {
let u: Vec<u16> = b
@@ -36,7 +149,5 @@ pub fn native_fn_idx(name: AtomRef, atoms: &[Atom]) -> Option<usize> {
.collect();
String::from_utf16_lossy(&u)
}
};
NATIVES.iter().position(|n| n.name == string)
}
}
+115 -42
View File
@@ -51,33 +51,70 @@ impl RegFn {
// The stack each block recieves at entry from its predecessors
let mut entry_stacks: Vec<Option<Vec<usize>>> = vec![None; blocks.len()];
// How many blocks lead to each block
let mut n_preds = vec![0; blocks.len()];
blocks.iter().flat_map(|b| &b.succ).for_each(|&s| n_preds[s] += 1);
// Walk blocks in CFG order, starting from block 0
let mut worklist = vec![];
entry_stacks[0] = Some(vec![]);
worklist.push(0);
while let Some(block_idx) = worklist.pop() {
let block = &mut blocks[block_idx];
let mut entry_stack = entry_stacks[block_idx].clone().unwrap_or_default();
let mut stack = entry_stacks[block_idx].clone().unwrap_or_default();
walk_block(
block,
&blocks[block_idx],
&f.code,
f.var_count,
&mut entry_stack,
&mut stack,
&mut next_val,
&mut out,
);
for succ_idx in block.succ.clone() {
let succ_stack = &mut entry_stacks[succ_idx];
for i in 0..blocks[block_idx].succ.len() {
let succ_idx = blocks[block_idx].succ[i];
match succ_stack {
Some(succ_stack) => add_block_postfix(block, &entry_stack, succ_stack),
// A join's stack slots are new values, which every block that
// leads to it copies its own values into. Naming a slot after
// one predecessor's value would give that value a second
// meaning (this does the job of phi nodes in SSA)
let succ_stack = match &entry_stacks[succ_idx] {
Some(succ_stack) => succ_stack.clone(),
None => {
*succ_stack = Some(entry_stack.clone());
let succ_stack: Vec<usize> = match n_preds[succ_idx] {
1 => stack.clone(),
_ => stack.iter().map(|_| fresh_val(&mut next_val)).collect(),
};
entry_stacks[succ_idx] = Some(succ_stack.clone());
worklist.push(succ_idx);
succ_stack
}
};
let copies = edge_copies(&stack, &succ_stack);
if copies.is_empty() {
continue;
}
// The copies belong to this one edge. A block that can go
// elsewhere too gets them on a block of their own on this
// edge, so the other edge does not run them
if blocks[block_idx].succ.len() == 1 {
blocks[block_idx].postfix.extend(copies);
} else {
let at = blocks[succ_idx].start;
blocks.push(BasicBlock {
start: at,
end: at,
succ: vec![succ_idx],
postfix: copies,
});
blocks[block_idx].succ[i] = blocks.len() - 1;
}
}
}
@@ -103,11 +140,7 @@ fn walk_block(
next_val: &mut usize,
out: &mut [RegInstr],
) {
let mut fresh_val = || {
let v = *next_val;
*next_val += 1;
v
};
let mut fresh_val = || fresh_val(next_val);
for instr_idx in block.start..block.end {
let instr = &code[instr_idx];
@@ -129,6 +162,13 @@ fn walk_block(
out_instr.args.push(v);
}
if matches!(
instr.op,
OpCode::Call | OpCode::CallMethod | OpCode::CallConstructor
) {
out_instr.args = call_operands(instr.op, out_instr.args);
}
// Locals and args are not renamed, so their read/writes need special handling
// Handle read of a local or arg
if let Some(var) = is_var_read(instr, var_count) {
@@ -188,10 +228,17 @@ fn walk_block(
continue;
}
// A field write is a define whose result lands back in the object's own value
if instr.op == OpCode::PutField {
out_instr.op = OpCode::DefineField;
out_instr.dsts.push(out_instr.args[1]);
// 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];
if n_push == 1 {
stack.push(obj);
}
out_instr.dsts.push(obj);
out[instr_idx] = out_instr;
continue;
}
@@ -209,21 +256,43 @@ fn walk_block(
/// Some instrs have extra pops not reflected by instr.op.info().n_pop
fn extra_pops(instr: &Instr) -> usize {
match instr.op {
OpCode::Call0 => 0,
OpCode::Call1 => 1,
OpCode::Call2 => 2,
OpCode::Call3 => 3,
_ => match instr.imm {
ImmOperand::Npop(npop) => npop as usize,
ImmOperand::NpopU16 { npop, .. } => npop as usize,
_ => 0,
},
match instr.imm {
ImmOperand::Npop(npop) => npop as usize,
ImmOperand::NpopU16 { npop, .. } => npop as usize,
_ => 0,
}
}
/// Put a call's popped operands in the order of the callee's params: the
/// callee (which is its env), `this`, then the args in source order
///
/// `call_method` has `this` at the bottom of its frame, below the callee.
/// `call_constructor` has the callee at the bottom and `this` above it (see
/// transform). `call` has `this` on top, where transform pushed it
fn call_operands(op: OpCode, mut popped: Vec<usize>) -> Vec<usize> {
let (callee, this) = match op {
OpCode::CallMethod => {
let this = popped.pop().unwrap();
(popped.pop().unwrap(), this)
}
OpCode::CallConstructor => {
let callee = popped.pop().unwrap();
(callee, popped.pop().unwrap())
}
_ => {
let callee = popped.pop().unwrap();
(callee, popped.remove(0))
}
};
// What remains are the args, last arg first
popped.reverse();
[callee, this].into_iter().chain(popped).collect()
}
/// The value id of the local or arg that `instr` reads, if any.
fn is_var_read(instr: &Instr, var_count: usize) -> Option<usize> {
pub fn is_var_read(instr: &Instr, var_count: usize) -> Option<usize> {
let arg = |k: usize| Some(var_count + k);
match instr.op {
@@ -252,7 +321,7 @@ fn is_var_read(instr: &Instr, var_count: usize) -> Option<usize> {
}
/// The value id of the local or arg that `instr` writes, if any.
fn is_var_write(instr: &Instr, var_count: usize) -> Option<usize> {
pub fn is_var_write(instr: &Instr, var_count: usize) -> Option<usize> {
let arg = |k: usize| Some(var_count + k);
match instr.op {
@@ -285,22 +354,26 @@ fn is_var_write(instr: &Instr, var_count: usize) -> Option<usize> {
}
}
/// Adds a postfix to a block to join values with a successor's entry stack
///
/// This does the job of phi nodes in typical SSA, Currently only relevant to
/// ternary operators because ternaries are the only place where JS becomes
/// "expression oriented"
fn add_block_postfix(block: &mut BasicBlock, stack: &[usize], succ_stack: &[usize]) {
/// The copies that carry the values on a block's stack into its successor's
/// entry stack
fn edge_copies(stack: &[usize], succ_stack: &[usize]) -> Vec<RegInstr> {
let overlap = stack.iter().rev().zip(succ_stack.iter().rev());
for (val, succ_val) in overlap {
block.postfix.push(RegInstr {
dsts: vec![*succ_val],
overlap
.filter(|(val, succ_val)| val != succ_val)
.map(|(&val, &succ_val)| RegInstr {
dsts: vec![succ_val],
op: OpCode::SetLoc,
imm: ImmOperand::None,
args: vec![*val],
});
}
args: vec![val],
})
.collect()
}
fn fresh_val(next_val: &mut usize) -> usize {
let v = *next_val;
*next_val += 1;
v
}
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
+35 -67
View File
@@ -1,67 +1,52 @@
use super::{PROTOTYPE_FIELD, pusher};
use crate::libqjs::parse::{AtomRef, ImmOperand, Instr, opcodes::OpCode};
#[derive(Debug)]
struct MethodSequence {
fn_cpool_idx: u32,
fn_const: ImmOperand,
name: AtomRef,
}
#[derive(Debug)]
pub struct ClassSequence {
start_pc: usize,
ctor_fn_cpool_idx: u32,
ctor_const: ImmOperand,
methods: Vec<MethodSequence>,
}
impl ClassSequence {
fn emit_len(&self) -> usize {
3 + (2 * self.methods.len())
}
/// fclosure ctor [C]
/// dup [C, C]
/// object [C, C, P]
/// (fclosure method; define_field name)*
/// put_field prototype [C], C.prototype = P
/// dup [C, C]
/// get_field prototype [C, P]
///
/// JS gives every fn a `prototype` object, but a closure here starts with
/// no fields, so the prototype is created explicitly. `define_class` left
/// [C, P] on the stack, and the code after it expects that
pub fn emit_prototype_form(&self, out: &mut Vec<Instr>) {
let mut push = |op, imm| {
out.push(Instr {
pc: self.start_pc,
op,
imm,
})
};
let mut push = pusher(self.start_pc, out);
let prototype = ImmOperand::Atom(PROTOTYPE_FIELD);
push(OpCode::Fclosure8, ImmOperand::Const(self.ctor_fn_cpool_idx));
push(OpCode::Fclosure8, self.ctor_const);
push(OpCode::Dup, ImmOperand::None);
push(OpCode::GetField, ImmOperand::Atom(AtomRef::Predefined(63))); // .prototype
push(OpCode::Object, ImmOperand::None);
for m in &self.methods {
push(OpCode::Fclosure8, ImmOperand::Const(m.fn_cpool_idx));
push(OpCode::Fclosure8, m.fn_const);
push(OpCode::DefineField, ImmOperand::Atom(m.name));
}
push(OpCode::PutField, prototype);
push(OpCode::Dup, ImmOperand::None);
push(OpCode::GetField, prototype);
}
}
pub fn declass(mut code: &[Instr]) -> Vec<Instr> {
let orig = code;
// Calculate size of output
let mut len = 0;
while !code.is_empty() {
match match_class_seq(code) {
Ok((rest, seq)) => {
len += seq.emit_len();
code = rest;
}
Err(rest) => {
let consumed = code.len() - rest.len(); // Handle partial consumption
let consumed = consumed.max(1);
len += consumed;
code = &code[consumed..];
}
}
}
// Emit output
code = orig;
let mut out = Vec::with_capacity(len);
let mut out = Vec::with_capacity(code.len());
while !code.is_empty() {
match match_class_seq(code) {
@@ -70,15 +55,14 @@ pub fn declass(mut code: &[Instr]) -> Vec<Instr> {
code = rest;
}
Err(rest) => {
let consumed = code.len() - rest.len(); // Handle partial consumption
let consumed = consumed.max(1);
// Copy what matched before the match failed, at least one instr
let consumed = (code.len() - rest.len()).max(1);
out.extend_from_slice(&code[..consumed]);
code = &code[consumed..];
}
}
}
debug_assert!(out.len() == len);
out
}
@@ -93,21 +77,16 @@ fn match_class_seq(ins: &[Instr]) -> PResult<'_, ClassSequence> {
let pc = ins[0].pc;
let (ins, push_const) = match_instr(ins, |i| i.op == OpCode::PushConst8)?;
let is_derived_class = matches!(push_const.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1);
assert!(!is_derived_class);
let (ins, _) = match_instr(ins, |i| i.op == OpCode::DefineClass)?;
let (ins, define_class) = match_instr(ins, |i| i.op == OpCode::DefineClass)?;
let (ins, methods) = match_many(ins, match_method_seq);
let ctor_fn_id = match push_const.imm {
ImmOperand::Const(id) => id,
_ => unreachable!(),
};
// Bit 0 of `arg` is set when the class has a superclass
let is_derived = matches!(define_class.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1);
assert!(!is_derived, "derived classes are not supported");
let seq = ClassSequence {
start_pc: pc,
ctor_fn_cpool_idx: ctor_fn_id,
ctor_const: push_const.imm,
methods,
};
@@ -119,23 +98,12 @@ fn match_method_seq(ins: &[Instr]) -> PResult<'_, MethodSequence> {
let (ins, fclosure) = match_instr(ins, |i| i.op == OpCode::Fclosure8)?;
let (ins, define_method) = match_instr(ins, |i| i.op == OpCode::DefineMethod)?;
let fn_id = match fclosure.imm {
ImmOperand::Const(id) => id,
_ => unreachable!(),
let seq = MethodSequence {
fn_const: fclosure.imm,
name: define_method.imm.atom(),
};
let name = match define_method.imm {
ImmOperand::AtomU8 { atom, .. } => atom,
_ => unreachable!(),
};
Ok((
ins,
MethodSequence {
fn_cpool_idx: fn_id,
name,
},
))
Ok((ins, seq))
}
// ───────────────────────────── Matcher Primitives ───────────────────────────────────────
+350 -278
View File
@@ -1,5 +1,7 @@
mod declass;
use std::collections::HashMap;
use declass::declass;
use crate::{
@@ -7,10 +9,28 @@ use crate::{
libqjs::parse::{
Atom, AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode,
},
native::native_fn_idx,
native,
regify::{is_var_read, is_var_write},
};
pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(124); // TODO: Should this be <home_object> or __proto__ or what?
/// The field an object's prototype is kept in. In JS, `o.__proto__` is the
/// prototype of an ordinary object, so user code that reads or writes it
/// gets the right behaviour with no extra work.
pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(72); // __proto__
/// The field a constructor keeps the prototype of its instances in
pub const PROTOTYPE_FIELD: AtomRef = AtomRef::Predefined(63); // prototype
/// Every fn takes its env and `this` as its first two params, and its declared
/// params after them. So the params of a call do not depend on how many args
/// the callee declares: a missing arg is not passed, and an extra one is not
/// read.
pub const ENV_ARG: u16 = 0;
pub const THIS_ARG: u16 = 1;
pub const FIRST_ARG: u16 = 2;
/// The field a box keeps its binding's value in (see replace_bindings())
pub const BOX_FIELD: AtomRef = AtomRef::Predefined(86); // <var>
pub fn transform(
flat_fns: Vec<FlatFn>,
@@ -19,54 +39,25 @@ pub fn transform(
) -> Vec<FlatFn> {
flat_fns
.into_iter()
.enumerate()
.map(|(i, f)| transform_one(i, f, captures, atoms))
.map(|f| transform_one(f, captures, atoms))
.collect()
}
fn transform_one(
fn_id: usize,
mut f: FlatFn,
captures: &[Vec<ClosureVar>],
atoms: &[Atom],
) -> FlatFn {
fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], atoms: &[Atom]) -> FlatFn {
let code = declass(&f.code);
// `this` and `env` are passed as the last 2 args to every fn
let _this_arg = f.arg_count;
let env_arg = f.arg_count + 1;
// Temp locals, after the fn's own, for the args of a `new` (see replace_new())
let temps = f.var_count;
let n_temps = code.iter().filter_map(is_new).max().unwrap_or(0);
// Calculate transformed out len
let mut len = 0;
let mut out = Vec::with_capacity(code.len());
for instr in &code {
if let Some(child_captures) = is_fclosure(instr, &f, captures) {
len += 1 + child_captures.iter().map(capture_len).sum::<usize>(); // fclosure; (load; define_field)*
if replace_new(instr, temps, &mut out) {
continue;
}
if let Some(acc) = is_capture_acc(instr) {
len += acc.out_len();
continue;
}
if let Some(seq) = is_expansion(instr) {
len += seq.len();
continue;
}
len += 1;
}
// Emit transformed output
let mut out = Vec::with_capacity(len);
for instr in &code {
if replace_fclosure(instr, &f, captures, &captures[fn_id], env_arg, &mut out) {
continue;
}
if replace_capture_acc(instr, &captures[fn_id], env_arg, &mut out) {
if replace_arg_access(instr, &mut out) {
continue;
}
@@ -81,274 +72,366 @@ fn transform_one(
out.push(instr.clone());
}
debug_assert!(out.len() == len);
// Last, so that it also sees the var accesses that the expansions make
let (code, var_count) = replace_bindings(&out, &f.cpool, captures, temps + n_temps);
FlatFn {
name: f.name,
arg_count: f.arg_count + 2, // 2 extra args, `this` and `env`
var_count: f.var_count,
arg_count: f.arg_count + FIRST_ARG as usize,
var_count,
cpool: f.cpool,
code: out,
code,
}
}
// ─────────────────────────────────────────────── Closure Processing ────────────────────────────────────────────────────────────
/// Pushes instrs at the pc of the instr they replace
fn pusher(pc: usize, out: &mut Vec<Instr>) -> impl FnMut(OpCode, ImmOperand) + '_ {
move |op, imm| out.push(Instr { pc, op, imm })
}
/// If `instr` is a closure decl, return the child closure's captures
fn is_fclosure<'a>(
instr: &Instr,
f: &FlatFn,
captures: &'a [Vec<ClosureVar>],
) -> Option<&'a [ClosureVar]> {
if !matches!(instr.op, OpCode::Fclosure8 | OpCode::Fclosure) {
return None;
// ───────────────────────────────────────────────── 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,
}
let cpool_idx = match instr.imm {
ImmOperand::Const(idx) => idx as usize,
_ => unreachable!(),
};
let child_fn_id = match f.cpool[cpool_idx] {
JsValue::FnById(id) => id,
_ => unreachable!(),
};
Some(&captures[child_fn_id])
}
/// Replace:
/// fclosure fn_const
/// call_constructor n [F, new_target, a0 .. an-1]
///
/// With:
/// fclosure fn_const
/// (push_capture; define_field capture_name)*
/// 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
///
/// ie the implicit closures are turned into explicit fields on the
/// fclosure object which serves as the environment record
fn replace_fclosure(
instr: &Instr,
f: &FlatFn,
captures: &[Vec<ClosureVar>],
my_captures: &[ClosureVar],
env_arg: usize,
out: &mut Vec<Instr>,
) -> bool {
let child_captures = match is_fclosure(instr, f, captures) {
Some(c) => c,
None => return false,
/// 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 = |op, imm| {
out.push(Instr {
pc: instr.pc,
op,
imm,
})
};
let mut push = pusher(instr.pc, out);
let temp = |i: usize| ImmOperand::Loc((temps + i) as u16);
push(OpCode::Fclosure, instr.imm);
for c in child_captures {
match c.flags.closure_kind {
ClosureKind::Local => push(OpCode::GetLoc, ImmOperand::Loc(c.var_idx as u16)),
ClosureKind::Arg => push(OpCode::GetArg, ImmOperand::Arg(c.var_idx as u16)),
ClosureKind::Ref => {
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
push(
OpCode::GetField,
ImmOperand::Atom(my_captures[c.var_idx].name),
);
}
_ => return false,
}
push(OpCode::DefineField, ImmOperand::Atom(c.name));
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
}
/// How many instructions one capture takes: its load, then its define_field
fn capture_len(c: &ClosureVar) -> usize {
match c.flags.closure_kind {
ClosureKind::Local | ClosureKind::Arg => 2,
ClosureKind::Ref => 3,
_ => 0,
}
}
// ─────────────────────────────────────────────── Binding Processing ────────────────────────────────────────────────────────────
// ──────────────────────────────────────────── Capture Access Processing ────────────────────────────────────────────────────────
enum CaptureAccess {
Read(u16),
Write(u16),
}
impl CaptureAccess {
fn out_len(&self) -> usize {
match self {
Self::Read(_) => 2, // See: replace_capture_acc()
Self::Write(_) => 3,
}
}
}
/// Is `instr` an access of a captured variable?
fn is_capture_acc(instr: &Instr) -> Option<CaptureAccess> {
let acc = match instr.op {
OpCode::GetVarRef0 => CaptureAccess::Read(0),
OpCode::GetVarRef1 => CaptureAccess::Read(1),
OpCode::GetVarRef2 => CaptureAccess::Read(2),
OpCode::GetVarRef3 => CaptureAccess::Read(3),
OpCode::GetVarRef | OpCode::GetVarRefCheck => match instr.imm {
ImmOperand::VarRef(var) => CaptureAccess::Read(var),
_ => unreachable!(),
},
OpCode::PutVarRef0 => CaptureAccess::Write(0),
OpCode::PutVarRef1 => CaptureAccess::Write(1),
OpCode::PutVarRef2 => CaptureAccess::Write(2),
OpCode::PutVarRef3 => CaptureAccess::Write(3),
OpCode::PutVarRef | OpCode::PutVarRefCheck => match instr.imm {
ImmOperand::VarRef(var) => CaptureAccess::Write(var),
_ => unreachable!(),
},
_ => return None,
};
Some(acc)
}
/// Replace:
/// read/write_captured_var
/// 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`
///
/// With:
/// get_env_arg
/// read_write_env_arg
fn replace_capture_acc(
instr: &Instr,
my_captures: &[ClosureVar],
env_arg: usize,
out: &mut Vec<Instr>,
) -> bool {
let acc = match is_capture_acc(instr) {
Some(acc) => acc,
None => return false,
};
/// 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 mut push = |op, imm| {
out.push(Instr {
pc: instr.pc,
op,
imm,
})
};
let children = cpool.iter().filter_map(|c| match c {
JsValue::FnById(child) => Some(*child),
_ => None,
});
match acc {
CaptureAccess::Read(read_var) => {
let atom = my_captures[read_var as usize].name;
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
push(OpCode::GetField, ImmOperand::Atom(atom));
}
let mut locals = vec![];
let mut args = vec![];
CaptureAccess::Write(write_var) => {
let atom = my_captures[write_var as usize].name;
// The value to write is already on the stack, so env lands above it,
// and put_field wants the object below the value
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
push(OpCode::Swap, ImmOperand::None);
push(OpCode::PutField, ImmOperand::Atom(atom));
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 ──────────────────────────────────────────────────────────────
#[allow(non_snake_case)]
fn is_expansion(instr: &Instr) -> Option<[Instr; 4]> {
/// 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 ins = |op, imm| Instr {
pc: instr.pc,
op,
imm,
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 nop = || Instr {
pc: instr.pc,
op: Nop,
imm: ImmOperand::None,
};
let seq = match instr.op {
TailCall => [
nop(),
nop(),
ins(Call, instr.imm),
ins(Return, ImmOperand::None),
],
TailCallMethod => [
nop(),
nop(),
ins(CallMethod, instr.imm),
ins(Return, ImmOperand::None),
],
GetField2 => [
nop(),
nop(),
ins(Dup, ImmOperand::None),
ins(GetField, instr.imm),
],
GetLoc0Loc1 => [
nop(),
nop(),
ins(GetLoc0, ImmOperand::None),
ins(GetLoc1, ImmOperand::None),
],
IncLoc => [
nop(),
ins(GetLoc, instr.imm),
ins(Inc, ImmOperand::None),
ins(PutLoc, instr.imm),
],
DecLoc => [
nop(),
ins(GetLoc, instr.imm),
ins(Dec, ImmOperand::None),
ins(PutLoc, instr.imm),
],
AddLoc => [
ins(GetLoc, instr.imm),
ins(Swap, ImmOperand::None), // The added value is on the stack, the local is the left operand
ins(Add, ImmOperand::None),
ins(PutLoc, instr.imm),
],
SetProto => [
nop(),
nop(),
nop(),
ins(DefineField, ImmOperand::Atom(PROTO_FIELD)),
],
DefineMethod => [nop(), nop(), nop(), ins(DefineField, instr.imm)], // TODO: should we discriminate on instr.imm?
SetLocUninitialized => [nop(), nop(), nop(), nop()],
_ => return None,
};
Some(seq)
}
fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
let Some(seq) = is_expansion(instr) else {
return false;
};
out.extend_from_slice(&seq);
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],
@@ -359,18 +442,7 @@ fn replace_get_var(
return false;
}
let ImmOperand::Atom(name) = instr.imm else {
return false;
};
let native_idx = native_fn_idx(name, atoms).unwrap();
cpool.push(JsValue::NativeFn(native_idx));
out.push(Instr {
pc: instr.pc,
op: OpCode::PushConst,
imm: ImmOperand::Const(cpool.len() as u32 - 1),
});
cpool.push(native::global(instr.imm.atom(), atoms));
pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1));
true
}