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
+447 -128
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;
}
pub fn insert_result(&mut self, req: InferRequest, res: InferResult) {
self.results.insert(req, res);
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_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;
/// 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]
}
/// 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
}
}
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
let mut cache = TypeCache::default();
/// 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))
}
// 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![],
/// 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,
};
let res = walk_fn(&req, fns, &mut cache);
cache.insert_result(req, res);
loop {
let before = (cache.heap.version, cache.changes);
cache.variants.iter_mut().for_each(|v| v.result = None);
cache
// 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;
}
}
}
#[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 in &f.blocks {
let code = &f.code[block.start..block.end];
let code = code.iter().chain(&block.postfix);
for instr in code {
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
if instr.dsts.is_empty() && !is_ret {
for (block_idx, block) in f.blocks.iter().enumerate() {
if !reachable[block_idx] {
continue;
}
// TODO: Some of these ops may not every show up by this point
let code = &f.code[block.start..block.end];
let code = code.iter().chain(&block.postfix);
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());
if (instr.dsts.is_empty() && !is_ret) || waits {
continue;
}
// 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,
}
}
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,
},
Div => Type::Float,
// `+` 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();
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
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)
}
}
// 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),
// Bitwise NOT gives an int32
Not => Type::number(Number::Int),
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);
}
}
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
cache.heap.canon(&arg(1))
}
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:?}"),
};
// 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);
}
if is_ret {
ret_type = join_ty(&ret_type, &ty);
} else {
ret_type = cache.heap.join(&ret_type, &ty);
continue;
}
for &dst in &instr.dsts {
val_types[dst] = join_ty(&val_types[dst], &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_object {
let (src, dst) = (instr.args[0], instr.dsts[0]);
val_types[src] = join_ty(&val_types[src], &val_types[dst]);
}
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),
}
}
+455 -134
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,
}
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)
}
/// 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!(),
},
};
// Write into existing field, join types
Type::WithField { name, ty, rest } if *name == field => {
let joined = join_ty(ty, field_ty);
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 joined == **ty {
return obj.clone();
if let Err(i) = fields.binary_search_by_key(&op.imm.atom(), |(n, _)| *n) {
fields.insert(i, (op.imm.atom(), field));
}
Type::WithField {
name: field,
ty: Box::new(joined),
rest: rest.clone(),
info.creation_write = true;
}
}
_ => {}
}
heap.instrs.push(info);
}
}
Type::WithField { name, ty, rest } => {
let new_rest = set_field(rest, field, field_ty);
heap.natives = heap.sites.len() as SiteId;
// Nothing changed deeper down, so nothing changes here either.
// TODO: Avoid this recursion and deep eq checking
if new_rest == **rest {
return obj.clone();
for id in 0..NATIVES.len() {
heap.add(Site::Native(id), Some(Code::Native(id)));
}
Type::WithField {
name: *name,
ty: ty.clone(),
rest: Box::new(new_rest),
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!(),
}
}
_ => Type::WithField {
name: field,
ty: Box::new(field_ty.clone()),
rest: Box::new(obj.clone()),
/// 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
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;
}
// 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);
}
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));
}
+128 -17
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 {
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,
_ => return None,
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)
}
}
+112 -39
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 {
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 ───────────────────────────────────────
+346 -274
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,
for i in (0..n).rev() {
push(OpCode::PutLoc, temp(i));
}
push(OpCode::DefineField, ImmOperand::Atom(c.name));
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 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!()
};
let mut push = |op, imm| {
out.push(Instr {
pc: instr.pc,
op,
imm,
})
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 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));
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,
}
}
CaptureAccess::Write(write_var) => {
let atom = my_captures[write_var as usize].name;
// ──────────────────────────────────────────────── Arg Access Processing ────────────────────────────────────────────────────────
// 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));
}
}
/// 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
}
+176
View File
@@ -0,0 +1,176 @@
# The core model
This file is the spec for how the compiler represents JS values, objects, bindings and functions in C. Each part of the code must agree with it. A difference between the code and this file is a bug in one of them.
The method:
1. State the JS semantics that the C code must keep (section 1).
2. Define a default representation that keeps all of it with no analysis (section 2).
3. Let types (sections 3 and 4), the value rule (section 5) and analyses (sections 6 and 7) narrow the representation. Each narrowing has one condition. If the condition is not proved, the default stays.
So a hole in an analysis costs speed. It never makes wrong code. Only a wrong rule can make wrong code.
## 1. Semantics
JS has four core concepts.
**Value.** A value is one of: undefined, null, bool, number, string, object. A value cannot change. A value is always copied: an assignment, an argument, a return and a field write each copy it. An object value is a reference to an object, so a copy of it copies the reference.
**Object.** An object is a cell with identity. It has own fields and a prototype (an object or null). A field read looks at the own fields. If the field is absent, the read goes on to the prototype, and at the end of the chain it gives undefined. An absent field and a field that holds undefined are different. Two object values are equal (`===`, `==`) if and only if they refer to the same object.
**Binding.** A binding is the storage of a variable. Each entry into a scope makes new bindings. A loop body is entered once for each iteration. `for (let …)` copies the values of the loop vars into the new bindings of the next iteration. A closure captures bindings, not values. So a write to a captured binding, by the owner or by any closure, is seen by all of them.
**Function.** A function is an object that also has code, and the bindings that it captured.
### When the representation is free
A representation is correct if no program can see a difference from these semantics.
- **A primitive** cannot change and has no identity. A copy of it and a shared pointer to it look the same. Either is correct.
- **An object** can be copied instead of shared only if no program can see the copy. There are two ways to see a copy:
- a write through one holder, which another holder reads later
- an identity test between two holders
- **A binding** must be shared if a closure captures it and a write reaches it after the capture.
## 2. The default representation
With no types and no analysis, this representation is correct for every program that uses only supported ops.
- **Every value is an `any_t`:** a 64-bit NaN-boxed word. It holds a double, or a tag and a payload. An object in an `any_t` is a pointer to its cell, so it keeps its identity.
- **Every object is a cell on the heap.** Each object starts with a header: the site that created it. The site tells the object's shape and, for a fn, its code. Each field starts as absent.
- **Every captured binding is a box:** a cell on the heap with one slot.
- The box is made when the fn is entered.
- The owner and every closure that captured the binding read and write the slot.
- A scope that is entered again is a loop body. QuickJS closes each captured binding of such a scope (`close_loc k`) at every way out of it. `close_loc k` puts a new box in `k`, which holds the current value. Closures made before keep the old box. So each iteration has its own bindings.
- A box's slot is never absent: JS gives a binding a value before any read of it that does not throw. A `var` starts as undefined. A `let` or `const` has no value before its declaration, where a read throws (the TDZ, section 8).
- A binding that no closure captures is a C local. Transform knows this from `closure_vars`, so no analysis decides it.
- A module binding has no frame. Its box is made where the fn that declares it is made.
- **A function object holds a pointer to each box that it captured.** The fn that makes the closure gives each box: from its own local if the binding is its own, else from its own env. So every capture is one copy of a pointer.
## 3. Types
A type is the set of values that a place can hold at runtime. Infer computes one type for each value id in each variant (section 3.4).
### 3.1 The lattice
A type is a set of kinds, with a refinement for each kind that is in the set:
| kind | refinement |
|---|---|
| undefined | none (one value) |
| null | none (one value) |
| bool | the value, if it is known |
| number | int32 or double (provisional: the number type is not decided) |
| string | the value, if it is known |
| object | its shape |
- **Join** is the union, kind by kind. Two different known values join to "not known". int32 joins with double to give double. Two shapes join by a merge (section 3.3).
- **Bottom** is the empty set. No value reaches the place, so the code cannot run.
### 3.2 The invariant
At every program point, every runtime value of a place is in the place's type.
Every rule in infer must keep this invariant. Every rule is monotone, and a rule with an empty operand gives an empty result. Infer starts from bottom and computes a fixed point of the rules. The invariant needs a fixed point, not the least one, so a merge (section 3.3), which can give more than the least result, is safe.
### 3.3 Shapes
- **A site** is the op that creates an object. Each site starts with its own shape.
- **A shape** gives, for its objects: the type of each own field, the type of the prototype, and the set of fns that they can run (empty if they are not fns).
- **A field type includes absent,** an internal marker that is not a JS value, unless definite initialization removes it (section 7). A field that no write reaches is only absent.
- **A merge** is the result when two object types join. The two shapes become one. Each field type, the prototypes and the fn sets are joined. A merge costs precision, but it keeps the invariant.
- **A field read** gives the own field's type without absent. If the field can be absent, it also gives the read on the prototype. At the end of the chain, an absent field gives undefined.
- **A field write** joins the value's type into the field's type in the shape. So every value that holds an object of the shape sees the write.
### 3.4 Calls
- **A call** of an object runs one of the fns in its shape's fn set. Infer walks each fn in the set with the arg types, and joins the results. A call of a value that is not a fn throws (section 8).
- **The program is closed.** Every fn comes from an `fclosure`, or it is a native. So the fn set of a call is always known.
- **A variant** is one walk of a fn with one tuple of arg types. The lattice has finite height, so there are finitely many variants.
- **Rounds:** infer walks every variant from the entry again, until a round changes nothing in the shapes, the prototypes or the returns. A call of a variant that is still being walked (recursion) gives what that variant returned in the round before. When a round changes nothing, these returns are the fixed point.
## 4. Representation
The C representation of a value is a function of its type and of the value rule (section 5) only.
| type | C |
|---|---|
| empty | the code cannot run |
| only undefined, or only null | nothing (a unit type) |
| bool | `bool` |
| number, int32 only | `int32_t` |
| number | `double` |
| string | `string_t` |
| object of a value shape (section 5) | `S`, the struct of its shape |
| object of a value shape, plus undefined, null or both | `S`, with a tag in its header for undefined and null |
| object of any other shape | `S *`, a pointer to the struct of its shape |
| object of any other shape, plus undefined, null or both | `S *`. `NULL` is null, and the address of a sentinel is undefined |
| string, plus undefined, null or both | `string_t`, with the same two values |
| any other set of kinds | `any_t` |
- **Undefined and null are kinds in the types, and immediates in the representation.**
- **Absent** is a field state, not a value. A field whose type includes absent has a representation with one more immediate for absent: a second sentinel address, or an `any_t` tag.
- **A widening conversion** (to a representation that holds more kinds) always exists and cannot fail. **A narrowing conversion** checks the kind. A failed check is a `TypeError` (section 8). No conversion copies an object.
## 5. Values: immutable shapes
**The rule.** Section 1 gives the two ways to see a copy of an object: a write through one holder that another holder reads later, and an identity test. An object that is never written after its creation, and never in an identity test, has neither. So it behaves like a primitive: a copy of it and a shared pointer to it cannot be told apart. It can be a C struct value, copied freely, with any number of holders.
**The flags.** Infer sets two flags on each shape. Both only become true, and a merge ORs them.
- **written:** a field write that is not a creation write reaches an object of the shape. A creation write is a `define_field` into the value that a creating op gives, before the object goes anywhere: an object literal's fields, a closure's captured boxes. Every `put_field` is a write after creation, and so is a `define_field` into anything else.
- **compared:** an object of the shape is an operand of `===`, `!==`, `==` or `!=`, and the other operand can also be an object. A test against undefined or null does not compare identity.
**Value shapes.** A shape is a value shape if neither flag is set, and it does not contain itself through value fields. A struct cannot contain itself, so a shape that does stays a pointer. An object of an immutable shape cannot hold itself, but objects of one shape can hold each other, as in a list.
**Representation.** An object of a value shape is a struct value (`S`), in its holder: a C local, a param, a return value, or the struct of the object whose field holds it. Its header's site is a tag for undefined, null or absent (`JSRT_SITE_*`), as the sentinel pointers are for other objects. In an `any_t`, it is a heap copy.
**Default.** An object of any other shape is a pointer to a cell (`S *`).
**Soundness.** No write after creation can show a copy, and no identity test can. A creation write happens before the object goes anywhere, so no copy exists yet.
## 6. Escape
**Question.** Where can a cell still be reached: after its frame returns, or when the op that made it runs again?
**Result.** One of three values for each value that a creating op gives:
- **`escapes`** if one of these is true:
- After the frame of its fn returns, the cell can be reached in a way other than as the return value itself. For example, it is stored in a field of a cell that is not `local`, stored in a box that is not `local`, or given to a callee. Every call operand counts as kept, because the analysis does not know the callee.
- A value that can reach the cell is still used when the creating op runs again: the op is in a loop, and the cell reaches a var or a join slot, which can keep it until the next run.
- **`returned`** if it does not escape, the return value is the only way to reach it after the return, it is the only object that a return of the fn can give, and its creating op runs at most once in each call.
- **`local`** otherwise: no holder outlives the frame.
A box is a cell, so the same rules apply to boxes.
**Narrowing.**
- **`local`:** the cell is on the stack of its frame. The value is still a pointer (`S *`) to it, so only the creating op changes. A C compiler can also split such a cell into registers if its address goes nowhere.
- **`returned`:** the caller allocates the cell. The variant gets one more param: a pointer to the cell to build into. The callee builds the object there and does not allocate. In the caller, the call is the creating op of the cell. So the caller's escape result for the call's dst chooses the stack or the heap, and the decision goes up through any number of fns that make and return objects.
**Default.** A cell that escapes is on the heap (`jsrt_alloc`).
**Soundness.**
- A stack cell lives until its frame returns, and its creating op uses the same memory again each time it runs. `local` excludes both cases.
- A `returned` object is in the caller's cell from the start and never moves. So every pointer to it stays valid, including a pointer from the object to itself.
**Where it is used.** Emit chooses the allocation of every cell in one function, `alloc()`. A creating op calls it, and so does a call of a variant whose result is `returned`.
## 7. Other narrowings
| narrowing | condition | if not proved |
|---|---|---|
| no absent in a field | a write to the field comes before every read of it (definite initialization). Object-literal fields have this: QuickJS fills them with `define_field` before the object goes anywhere | the field's type includes absent |
| a captured binding as a value | no write reaches the binding after its first capture. Most `const` bindings have this. A `const` that a hoisted fn captures before its declaration does not | a box |
| a direct call | the fn set has one fn | a `switch` on the fn |
## 8. Out of scope
These are not part of this model, and they need their own design:
- `throw`, `try`, `catch` and `finally`. These include the `TypeError`s above: a call of a value that is not a fn, and a failed narrowing check.
- the TDZ (a read of a `let` before its declaration)
- fields with computed names (`o[k]`), `delete`, `in`. The absent state is the base that `in` and `delete` need.
- arrays. An array is an object whose `length` follows its integer fields, so it needs computed field names first.
- getters and setters
+105
View File
@@ -0,0 +1,105 @@
// The runtime that emitted C is compiled against. Declarations only for now.
// The natives are declared by the emitted C itself, from compiler/src/native.rs
// See docs/model.md for the representation of values
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct { char _; } undefined_t;
typedef struct { char _; } null_t;
typedef struct jsrt_string *string_t;
// ────────────────────────────────────────────── cells ──────────────────────────────────────────────
// The start of every object: the site that created it, which tells its shape
// and, for a fn, its code
typedef struct { uint32_t site; } jsrt_header;
// The sites below 3 are tags. An object of a value shape is a struct, not a
// pointer, so its header tells if it is undefined, null or absent instead
#define JSRT_SITE_UNDEFINED 0
#define JSRT_SITE_NULL 1
#define JSRT_SITE_ABSENT 2
// A pointer to an object or a string can also be one of these. NULL is null
extern jsrt_header jsrt_undefined_cell, jsrt_absent_cell;
#define JSRT_UNDEFINED ((void *)&jsrt_undefined_cell)
#define JSRT_ABSENT ((void *)&jsrt_absent_cell) // a field that an object lacks
// A new heap cell of `size` bytes
void *jsrt_alloc(size_t size);
// Throws a TypeError. Exceptions are not supported yet, so it stops the program
_Noreturn void jsrt_type_error(void);
// ───────────────────────────────────────────── strings ─────────────────────────────────────────────
string_t jsrt_string_lit(const char *s);
int32_t jsrt_string_length(string_t s);
bool jsrt_string_eq(string_t a, string_t b);
int32_t jsrt_string_cmp(string_t a, string_t b);
// ─────────────────────────────────────────────── any ───────────────────────────────────────────────
// A value of any kind: a NaN-boxed 64-bit word. An object in it is the pointer
// to its cell, so it keeps its identity. A field can also hold absent
typedef uint64_t any_t;
// Box a value. A pointer that is a sentinel (NULL, JSRT_UNDEFINED,
// JSRT_ABSENT) boxes as null, undefined or absent
any_t __any_undefined(void);
any_t __any_null(void);
any_t __any_absent(void);
any_t __any_from_bool(bool b);
any_t __any_from_int(int32_t n);
any_t __any_from_float(double x);
any_t __any_from_string(string_t s);
any_t __any_from_object(void *o);
// Unbox a value. A value of another kind throws. A string or an object unboxes
// null, undefined and absent to their sentinels
bool __any_as_bool(any_t a);
int32_t __any_as_int(any_t a);
double __any_as_float(any_t a); // an int too
string_t __any_as_string(any_t a);
void *__any_as_object(any_t a);
// The kind of a value
bool __any_is_absent(any_t a);
bool __any_is_undefined(any_t a);
bool __any_is_null(any_t a);
bool __any_is_nullish(any_t a); // undefined or null
bool __any_is_boolean(any_t a);
bool __any_is_number(any_t a);
bool __any_is_string(any_t a);
bool __any_is_object(any_t a); // a fn too
bool __any_is_function(any_t a);
// JS's operators, with JS's conversions
any_t __any_add(any_t a, any_t b);
any_t __any_sub(any_t a, any_t b);
any_t __any_mul(any_t a, any_t b);
any_t __any_div(any_t a, any_t b);
any_t __any_mod(any_t a, any_t b);
any_t __any_pow(any_t a, any_t b);
any_t __any_neg(any_t a);
any_t __any_plus(any_t a); // ToNumber
any_t __any_inc(any_t a);
any_t __any_dec(any_t a);
bool __any_lt(any_t a, any_t b);
bool __any_lte(any_t a, any_t b);
bool __any_gt(any_t a, any_t b);
bool __any_gte(any_t a, any_t b);
bool __any_eq(any_t a, any_t b);
bool __any_neq(any_t a, any_t b);
bool __any_strict_eq(any_t a, any_t b);
bool __any_strict_neq(any_t a, any_t b);
// JS's abstract operations
bool __any_truthy(any_t a); // ToBoolean
int32_t __any_to_int32(any_t a); // ToInt32
string_t __any_to_string(any_t a); // ToString
string_t __any_typeof(any_t a);
+260 -5
View File
@@ -1,9 +1,264 @@
const Object = {
create(proto) { return { __proto__: proto }; },
// The stdlib, compiled in front of every program
//
// Built on the natives in compiler/src/native.rs, the common layer of
// operations on primitive values. The compiler's own code for operators (`+` on
// strings and so on) uses those natives directly, not these functions.
//
// Restrictions:
// - The prototypes linked with `__link_proto_*` are fixed. User code may not
// change them (no monkey-patching)
// - `new String(x)`, `new Number(x)` and `new Boolean(x)` (wrapper objects) are
// not supported. These functions only convert
//
// Needs `typeof`. `console.log` and `String.prototype.concat` also need rest
// params, which need arrays
const NaN = 0 / 0;
const Infinity = 1 / 0;
// ─────────────────────────────────────────────── Object ───────────────────────────────────────────────
// `new Object()` makes an empty object. Called without `new`, it does the same
function Object() {
return {};
}
Object.prototype = {};
Object.create = function (proto) {
return { __proto__: proto };
};
// ─────────────────────────────────────────────── String ───────────────────────────────────────────────
// Converts any value to a string, like the spec's ToString
function String(x) {
switch (typeof x) {
case "string": return x;
case "number": return __native_number_to_string(x);
case "boolean": return x ? "true" : "false";
case "undefined": return "undefined";
case "function": return "function () { [native code] }";
case "object":
if (x === null) return "null";
if (typeof x.toString === "function") return x.toString();
return "[object Object]";
}
}
String.fromCharCode = function (code) {
return __native_string_from_char_code(code);
};
// In these methods `this` is the primitive string itself
String.prototype = {
toString() { return this; },
valueOf() { return this; },
concat(...parts) {
let s = this;
for (let i = 0; i < parts.length; i++) {
s = __native_string_concat(s, String(parts[i]));
}
return s;
},
// ── Characters ──
charAt(i) {
if (i < 0 || i >= this.length) return "";
return __native_string_slice(this, i, i + 1);
},
charCodeAt(i) {
if (i < 0 || i >= this.length) return NaN;
return __native_string_char_code_at(this, i);
},
at(i) {
if (i < 0) i += this.length;
if (i < 0 || i >= this.length) return undefined;
return __native_string_slice(this, i, i + 1);
},
// ── Search ──
indexOf(search, from) {
let start = from === undefined ? 0 : clampIndex(from, this.length);
for (let i = start; i + search.length <= this.length; i++) {
if (__native_string_slice(this, i, i + search.length) === search) return i;
}
return -1;
},
lastIndexOf(search) {
for (let i = this.length - search.length; i >= 0; i--) {
if (__native_string_slice(this, i, i + search.length) === search) return i;
}
return -1;
},
includes(search, from) { return this.indexOf(search, from) !== -1; },
startsWith(search, from) {
let start = from === undefined ? 0 : clampIndex(from, this.length);
return __native_string_slice(this, start, start + search.length) === search;
},
endsWith(search, end) {
let stop = end === undefined ? this.length : clampIndex(end, this.length);
let start = stop - search.length;
return start >= 0 && __native_string_slice(this, start, stop) === search;
},
// ── Substrings ──
slice(start, end) {
let len = this.length;
let s = relativeIndex(start === undefined ? 0 : start, len);
let e = relativeIndex(end === undefined ? len : end, len);
return s < e ? __native_string_slice(this, s, e) : "";
},
substring(start, end) {
let len = this.length;
let s = clampIndex(start === undefined ? 0 : start, len);
let e = clampIndex(end === undefined ? len : end, len);
return s < e ? __native_string_slice(this, s, e) : __native_string_slice(this, e, s);
},
// ── Case ──
toUpperCase() { return mapCodes(this, 97, 122, -32); }, // a-z to A-Z
toLowerCase() { return mapCodes(this, 65, 90, 32); }, // A-Z to a-z
// ── Whitespace ──
trim() { return this.trimStart().trimEnd(); },
trimStart() {
let i = 0;
while (i < this.length && isSpace(__native_string_char_code_at(this, i))) i++;
return __native_string_slice(this, i, this.length);
},
trimEnd() {
let i = this.length;
while (i > 0 && isSpace(__native_string_char_code_at(this, i - 1))) i--;
return __native_string_slice(this, 0, i);
},
// ── Building ──
repeat(count) {
let s = "";
for (let i = 0; i < count; i++) s = __native_string_concat(s, this);
return s;
},
padStart(length, fill) { return pad(this, length, fill, true); },
padEnd(length, fill) { return pad(this, length, fill, false); },
};
__link_proto_string(String.prototype);
// An index clamped to [0, len]
function clampIndex(i, len) {
return i < 0 ? 0 : i > len ? len : i;
}
// An index where a negative value counts from the end, clamped to [0, len]
function relativeIndex(i, len) {
return clampIndex(i < 0 ? i + len : i, len);
}
// `s` with every char code in [lo, hi] moved by `shift`, for ASCII case mapping
function mapCodes(s, lo, hi, shift) {
let out = "";
for (let i = 0; i < s.length; i++) {
let c = __native_string_char_code_at(s, i);
if (c >= lo && c <= hi) c += shift;
out = __native_string_concat(out, __native_string_from_char_code(c));
}
return out;
}
function isSpace(c) {
// tab, line feed, vertical tab, form feed, carriage return, space, nbsp
return (c >= 9 && c <= 13) || c === 32 || c === 160;
}
function pad(s, length, fill, atStart) {
let f = fill === undefined ? " " : String(fill);
if (s.length >= length || f.length === 0) return s;
let padding = "";
while (padding.length < length - s.length) padding = __native_string_concat(padding, f);
padding = __native_string_slice(padding, 0, length - s.length);
return atStart ? __native_string_concat(padding, s) : __native_string_concat(s, padding);
}
// ─────────────────────────────────────────────── Number ───────────────────────────────────────────────
// Converts any value to a number, like the spec's ToNumber
function Number(x) {
switch (typeof x) {
case "number": return x;
case "string": return __native_string_to_number(x);
case "boolean": return x ? 1 : 0;
case "undefined": return NaN;
case "object": return x === null ? 0 : NaN;
default: return NaN;
}
}
Number.NaN = NaN;
Number.POSITIVE_INFINITY = Infinity;
Number.NEGATIVE_INFINITY = -Infinity;
Number.MAX_SAFE_INTEGER = 9007199254740991;
Number.MIN_SAFE_INTEGER = -9007199254740991;
Number.isNaN = function (x) { return typeof x === "number" && x !== x; };
Number.isFinite = function (x) { return typeof x === "number" && x === x && x !== Infinity && x !== -Infinity; };
Number.isInteger = function (x) { return Number.isFinite(x) && x % 1 === 0; };
// In these methods `this` is the primitive number itself
Number.prototype = {
toString(radix) {
if (radix === undefined || radix === 10) return __native_number_to_string(this);
return integerToString(this, radix);
},
toFixed(digits) { return __native_number_to_fixed(this, digits === undefined ? 0 : digits); },
valueOf() { return this; },
};
__link_proto_number(Number.prototype);
// An integer in `radix` (2 to 36). Only integers: the fraction is dropped
function integerToString(n, radix) {
let negative = n < 0;
if (negative) n = -n;
n = n - (n % 1);
let out = "";
do {
let digit = n % radix;
out = __native_string_concat(__native_string_from_char_code(digit < 10 ? 48 + digit : 87 + digit), out);
n = (n - digit) / radix;
} while (n > 0);
return negative ? __native_string_concat("-", out) : out;
}
// ─────────────────────────────────────────────── Boolean ──────────────────────────────────────────────
// Converts any value to a boolean, like the spec's ToBoolean
function Boolean(x) {
return !!x;
}
// In these methods `this` is the primitive boolean itself
Boolean.prototype = {
toString() { return this ? "true" : "false"; },
valueOf() { return this; },
};
__link_proto_boolean(Boolean.prototype);
// ─────────────────────────────────────────────── console ──────────────────────────────────────────────
const console = {
// TODO: all __native_* fns should be monomorphic, console.log() should act a bit like Rust's write!/print!
// and break down complex printing in JS-land
log(x) { __native_print(x); },
log(...args) {
let line = "";
for (let i = 0; i < args.length; i++) {
if (i > 0) line = __native_string_concat(line, " ");
line = __native_string_concat(line, String(args[i]));
}
__native_print_string(line);
},
};