New model
This commit is contained in:
@@ -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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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};
|
mod escape;
|
||||||
use by_ref::analyze_by_ref;
|
|
||||||
|
use crate::{
|
||||||
|
libqjs::parse::opcodes::OpCode,
|
||||||
|
regify::{RegFn, RegInstr},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use escape::Escape;
|
||||||
|
|
||||||
pub struct Analysis {
|
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 {
|
impl Analysis {
|
||||||
fn of(reg_fn: &RegFn) -> Self {
|
fn of(f: &RegFn) -> Self {
|
||||||
todo!()
|
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()
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+445
-126
@@ -1,220 +1,539 @@
|
|||||||
|
//! Infers a type for every value (see `docs/model.md`, section 3)
|
||||||
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
|
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
|
||||||
native,
|
native,
|
||||||
regify::RegFn,
|
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)]
|
const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
|
||||||
pub struct InferRequest {
|
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,
|
pub fn_id: usize,
|
||||||
|
/// One for each of the fn's params (see `TypeCache::variant()`)
|
||||||
pub arg_types: Vec<Type>,
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct InferResult {
|
pub struct InferResult {
|
||||||
pub val_types: Vec<Type>,
|
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,
|
pub ret_type: Type,
|
||||||
|
/// The blocks that can run
|
||||||
|
pub reachable: Vec<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub struct TypeCache {
|
pub struct TypeCache {
|
||||||
results: HashMap<InferRequest, InferResult>,
|
pub variants: Vec<Variant>,
|
||||||
shapes: HashMap<Type, usize>,
|
/// Each fn's variants, by their arg types
|
||||||
next_shape_id: usize,
|
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 {
|
impl TypeCache {
|
||||||
pub fn get_result(&self, req: &InferRequest) -> Option<&InferResult> {
|
/// The variant for a call of `fn_id` with args of these types, made if it
|
||||||
self.results.get(req)
|
/// is new. It is keyed by the fn's params: an arg it does not pass is
|
||||||
|
/// undefined, and an arg it passes beyond them is never read. Only an
|
||||||
|
/// arg's kind crosses a call, not a known value, else every literal passed
|
||||||
|
/// would make a new variant
|
||||||
|
pub fn variant(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
|
||||||
|
self.key.clear();
|
||||||
|
self.key.extend(params(fn_id, arg_types, fns));
|
||||||
|
|
||||||
|
if let Some(&v) = self.by_args[fn_id].get(&self.key[..]) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = self.variants.len();
|
||||||
|
self.by_args[fn_id].insert(self.key.clone(), v);
|
||||||
|
self.variants.push(Variant {
|
||||||
|
fn_id,
|
||||||
|
arg_types: self.key.clone(),
|
||||||
|
result: None,
|
||||||
|
ret: Type::EMPTY,
|
||||||
|
walking: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_result(&mut self, req: InferRequest, res: InferResult) {
|
/// The variant for a call that infer walked
|
||||||
self.results.insert(req, res);
|
pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
|
||||||
|
let key: Vec<Type> = params(fn_id, arg_types, fns).collect();
|
||||||
|
self.by_args[fn_id][&key]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_shape(&mut self, ty: &Type) {
|
/// Infer a call of `fn_id`, walking it unless an identical call was
|
||||||
if !self.shapes.contains_key(ty) {
|
/// already walked in this round. Gives what it returns
|
||||||
self.shapes.insert(ty.clone(), self.next_shape_id);
|
fn call(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> Type {
|
||||||
self.next_shape_id += 1;
|
let v = self.variant(fn_id, arg_types, fns);
|
||||||
|
|
||||||
|
match &self.variants[v] {
|
||||||
|
Variant { result: Some(res), .. } => return res.ret_type,
|
||||||
|
Variant { walking: true, ret, .. } => return *ret,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.variants[v].walking = true;
|
||||||
|
let res = walk_fn(v, fns, self);
|
||||||
|
let ret = res.ret_type;
|
||||||
|
|
||||||
|
let last = self.variants[v].ret;
|
||||||
|
let joined = self.heap.join(&last, &ret);
|
||||||
|
self.changes += (joined != last) as usize;
|
||||||
|
|
||||||
|
let variant = &mut self.variants[v];
|
||||||
|
variant.walking = false;
|
||||||
|
variant.ret = joined;
|
||||||
|
variant.result = Some(res);
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of each of fn `fn_id`'s params, for a call with args of these
|
||||||
|
/// types (see `TypeCache::variant()`)
|
||||||
|
fn params<'a>(fn_id: usize, arg_types: &'a [Type], fns: &[RegFn]) -> impl Iterator<Item = Type> + 'a {
|
||||||
|
(0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::UNDEFINED, Type::widen))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk every variant from the entry, in rounds, until a round changes
|
||||||
|
/// nothing. A variant reads the shapes, the protos and the returns of other
|
||||||
|
/// variants, which variants walked after it can still add to. All of them only
|
||||||
|
/// grow, so the rounds end. The results are those of the last round
|
||||||
|
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
|
||||||
|
let mut cache = TypeCache {
|
||||||
|
variants: vec![],
|
||||||
|
by_args: fns.iter().map(|_| HashMap::new()).collect(),
|
||||||
|
key: vec![],
|
||||||
|
heap: Heap::new(fns),
|
||||||
|
protos: [Type::EMPTY; 3],
|
||||||
|
changes: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let before = (cache.heap.version, cache.changes);
|
||||||
|
cache.variants.iter_mut().for_each(|v| v.result = None);
|
||||||
|
|
||||||
|
// The synthetic program entry is appended last by `flatten`. It is
|
||||||
|
// called by the host with no args
|
||||||
|
cache.call(fns.len() - 1, &[], fns);
|
||||||
|
|
||||||
|
if before == (cache.heap.version, cache.changes) {
|
||||||
|
return cache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
|
fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
|
||||||
let mut cache = TypeCache::default();
|
|
||||||
|
|
||||||
// The synthetic program entry is appended last by `flatten`. It is called
|
|
||||||
// by the host with no args
|
|
||||||
let req = InferRequest {
|
|
||||||
fn_id: fns.len() - 1,
|
|
||||||
arg_types: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = walk_fn(&req, fns, &mut cache);
|
|
||||||
cache.insert_result(req, res);
|
|
||||||
|
|
||||||
cache
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
|
|
||||||
use OpCode::*;
|
use OpCode::*;
|
||||||
|
|
||||||
let f = ®_fns[req.fn_id];
|
let fn_id = cache.variants[variant].fn_id;
|
||||||
let mut val_types = vec![Type::Unknown; f.val_count];
|
let f = ®_fns[fn_id];
|
||||||
let mut ret_type = Type::Unknown;
|
let mut val_types = vec![Type::EMPTY; f.val_count];
|
||||||
|
let mut instr_types = vec![Type::EMPTY; f.code.len()];
|
||||||
let mut old_types = val_types.clone();
|
let mut ret_type = Type::EMPTY;
|
||||||
|
|
||||||
// arg `i` is value id `var_count + i` (see regify)
|
// arg `i` is value id `var_count + i` (see regify)
|
||||||
// Init them with the arg types passed in
|
// Init them with the arg types passed in
|
||||||
for (i, ty) in req.arg_types.iter().enumerate() {
|
for (i, ty) in cache.variants[variant].arg_types.iter().enumerate() {
|
||||||
val_types[f.var_count + i] = ty.clone();
|
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 {
|
loop {
|
||||||
old_types.clone_from_slice(&val_types); // TODO: We only compare val types not ret type
|
let version = cache.heap.version;
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
for (block_idx, block) in f.blocks.iter().enumerate() {
|
||||||
|
if !reachable[block_idx] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for block in &f.blocks {
|
|
||||||
let code = &f.code[block.start..block.end];
|
let code = &f.code[block.start..block.end];
|
||||||
let code = code.iter().chain(&block.postfix);
|
let code = code.iter().chain(&block.postfix);
|
||||||
|
|
||||||
for instr in code {
|
for (offset, instr) in code.enumerate() {
|
||||||
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
|
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
|
||||||
|
|
||||||
if instr.dsts.is_empty() && !is_ret {
|
// A rule with an empty operand gives an empty result: no value
|
||||||
|
// has reached the instr yet
|
||||||
|
let waits = instr.args.iter().any(|&v| val_types[v].is_empty());
|
||||||
|
|
||||||
|
if (instr.dsts.is_empty() && !is_ret) || waits {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Some of these ops may not every show up by this point
|
// The instr's index in the code (postfix copies have none)
|
||||||
|
let idx = block.start + offset;
|
||||||
|
let arg = |i: usize| val_types[instr.args[i]];
|
||||||
|
|
||||||
let ty = match instr.op {
|
let ty = match instr.op {
|
||||||
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
|
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
|
||||||
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7
|
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 {
|
PushConst | PushConst8 => match f.cpool[instr.imm.const_idx()] {
|
||||||
ImmOperand::Const(k) => match f.cpool.get(k as usize) {
|
JsValue::Int(_) => Type::number(Number::Int),
|
||||||
Some(JsValue::Int(_)) => Type::Int,
|
JsValue::Float(_) => Type::number(Number::Float),
|
||||||
Some(JsValue::Float(_)) => Type::Float,
|
JsValue::String(_) => Type::string(None),
|
||||||
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
|
JsValue::BoolTrue => Type::bool(Some(true)),
|
||||||
Some(JsValue::NativeFn(id)) => Type::NativeFn(*id),
|
JsValue::BoolFalse => Type::bool(Some(false)),
|
||||||
_ => unreachable!(),
|
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!(),
|
_ => unreachable!(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Undefined => Type::UNDEFINED,
|
||||||
|
Null => Type::NULL,
|
||||||
|
|
||||||
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
|
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
|
||||||
Add | Sub | Mul | Mod | Pow => {
|
// `+` concatenates if either side becomes a string, which
|
||||||
match (&val_types[instr.args[0]], &val_types[instr.args[1]]) {
|
// only a string or an object can
|
||||||
(Type::Unknown, _) | (_, Type::Unknown) => Type::Unknown,
|
Add => {
|
||||||
(Type::Int, Type::Int) => Type::Int,
|
let (l, r) = (arg(1), arg(0));
|
||||||
(Type::Float, Type::Float)
|
let is_string = |t: Type| t.is_only(Type::string(None));
|
||||||
| (Type::Int, Type::Float)
|
let can_be_string = |t: Type| t.string.is_some() || t.object.is_some();
|
||||||
| (Type::Float, Type::Int) => Type::Float,
|
|
||||||
_ => Type::Any,
|
if is_number(l) && is_number(r) {
|
||||||
|
Type::number(l.number.max(r.number).unwrap())
|
||||||
|
} else if is_string(l) || is_string(r) {
|
||||||
|
Type::string(None)
|
||||||
|
} else if can_be_string(l) || can_be_string(r) {
|
||||||
|
Type { string: Some(None), ..Type::number(Number::Float) }
|
||||||
|
} else {
|
||||||
|
Type::number(Number::Float)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Neg | Plus | Inc | Dec | PostInc | PostDec => match val_types[instr.args[0]] {
|
// Any other operator converts its operands to numbers
|
||||||
Type::Unknown => Type::Unknown,
|
Sub | Mul | Mod | Pow => match (arg(1), arg(0)) {
|
||||||
Type::Int => Type::Int,
|
(l, r) if is_int(l) && is_int(r) => Type::number(Number::Int),
|
||||||
Type::Float => Type::Float,
|
_ => Type::number(Number::Float),
|
||||||
_ => Type::Any,
|
|
||||||
},
|
},
|
||||||
Div => Type::Float,
|
Neg | Plus | Inc | Dec | PostInc | PostDec => match arg(0) {
|
||||||
|
t if is_int(t) => Type::number(Number::Int),
|
||||||
|
_ => Type::number(Number::Float),
|
||||||
|
},
|
||||||
|
Div => Type::number(Number::Float),
|
||||||
|
// Bitwise NOT gives an int32
|
||||||
|
Not => Type::number(Number::Int),
|
||||||
|
|
||||||
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
|
Lt | Lte | Gt | Gte => Type::bool(None),
|
||||||
|
|
||||||
|
// ─────────────────────────────── Conditions the types may decide ──────────────────────────────────────────
|
||||||
|
// A known value lets infer skip the branch that cannot run
|
||||||
|
// (see `reachable` below)
|
||||||
|
// An identity test of two objects (see `docs/model.md`, section 5)
|
||||||
|
StrictEq | StrictNeq | Eq | Neq if arg(0).object.is_some() && arg(1).object.is_some() => {
|
||||||
|
for obj in [arg(0).object, arg(1).object].into_iter().flatten() {
|
||||||
|
cache.heap.mark(obj, |shape| &mut shape.compared);
|
||||||
|
}
|
||||||
|
|
||||||
|
Type::bool(None)
|
||||||
|
}
|
||||||
|
StrictEq => Type::bool(strict_eq(&arg(0), &arg(1))),
|
||||||
|
StrictNeq => Type::bool(strict_eq(&arg(0), &arg(1)).map(|eq| !eq)),
|
||||||
|
Eq => Type::bool(loose_eq(&arg(0), &arg(1))),
|
||||||
|
Neq => Type::bool(loose_eq(&arg(0), &arg(1)).map(|eq| !eq)),
|
||||||
|
Lnot => Type::bool(truthiness(&arg(0)).map(|t| !t)),
|
||||||
|
Typeof => typeof_of(&arg(0), &cache.heap),
|
||||||
|
IsUndefined | TypeofIsUndefined => Type::bool(test(&arg(0), Type::UNDEFINED)),
|
||||||
|
IsNull => Type::bool(test(&arg(0), Type::NULL)),
|
||||||
|
IsUndefinedOrNull => Type::bool(test(&arg(0), Type { null: true, ..Type::UNDEFINED })),
|
||||||
|
TypeofIsFunction => Type::bool(is_function(&arg(0), &cache.heap)),
|
||||||
|
|
||||||
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
|
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
|
||||||
SetLoc | SetName => val_types[instr.args[0]].clone(),
|
SetLoc | SetName => arg(0),
|
||||||
Undefined => Type::Undefined,
|
|
||||||
|
|
||||||
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
|
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
|
||||||
Fclosure | Fclosure8 => {
|
Object | Fclosure | Fclosure8 => cache.heap.create(cache.heap.site_at(fn_id, idx).unwrap()),
|
||||||
let cpool_idx = match instr.imm {
|
// The write goes into the object's shape, which every value
|
||||||
ImmOperand::Const(idx) => idx as usize,
|
// that holds the object sees. A primitive has no fields to
|
||||||
_ => unreachable!(),
|
// 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] {
|
if !cache.heap.is_creation_write(fn_id, idx) {
|
||||||
JsValue::FnById(id) => id,
|
cache.heap.mark(obj, |shape| &mut shape.written);
|
||||||
_ => unreachable!(),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Type::Closure {
|
|
||||||
fn_id,
|
|
||||||
env: Box::new(Type::Empty),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache.heap.canon(&arg(1))
|
||||||
}
|
}
|
||||||
DefineField => {
|
GetField => cache.heap.read_value(&arg(0), instr.imm.atom(), &cache.protos),
|
||||||
let field = match instr.imm {
|
// A string's length is its own. Anything else reads a
|
||||||
ImmOperand::Atom(atom) => atom,
|
// `length` field
|
||||||
_ => unreachable!(),
|
GetLength => {
|
||||||
};
|
let others = Type { string: None, ..arg(0) };
|
||||||
|
let read = cache.heap.read_value(&others, LENGTH_FIELD, &cache.protos);
|
||||||
let obj = &val_types[instr.args[1]];
|
let own = Type { number: arg(0).string.map(|_| Number::Int), ..Type::EMPTY };
|
||||||
let ty = set_field(obj, field, &val_types[instr.args[0]]);
|
cache.heap.join(&read, &own)
|
||||||
|
|
||||||
let shape = match &ty {
|
|
||||||
Type::Closure { env, .. } => env.as_ref(),
|
|
||||||
other => other,
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.insert_shape(shape);
|
|
||||||
ty
|
|
||||||
}
|
}
|
||||||
Object => Type::Empty,
|
|
||||||
|
|
||||||
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
|
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
|
||||||
Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod
|
// Operands are the callee's params: the callee (its env),
|
||||||
| TailCallMethod => todo!(),
|
// `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 ──────────────────────────────────────────────────────────
|
// ──────────────────────────────────────── Returns ──────────────────────────────────────────────────────────
|
||||||
Return | ReturnAsync => val_types[instr.args[0]].clone(),
|
Return | ReturnAsync => arg(0),
|
||||||
ReturnUndef => Type::Undefined,
|
ReturnUndef => Type::UNDEFINED,
|
||||||
|
|
||||||
_ => unreachable!("{instr:?}"),
|
_ => unreachable!("{instr:?}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_ret {
|
// Postfix copies come after the block's own instrs, and have no
|
||||||
ret_type = join_ty(&ret_type, &ty);
|
// entry: a copy gives the type of what it copies
|
||||||
} else {
|
if idx < block.end {
|
||||||
for &dst in &instr.dsts {
|
instr_types[idx] = cache.heap.join(&instr_types[idx], &ty);
|
||||||
val_types[dst] = join_ty(&val_types[dst], &ty);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A copy of an object created by SetLoc is the same object, (see regify)
|
if is_ret {
|
||||||
// so what is done through the copy is done to the original as well
|
ret_type = cache.heap.join(&ret_type, &ty);
|
||||||
if instr.op == SetLoc {
|
continue;
|
||||||
let is_object = matches!(
|
}
|
||||||
val_types[instr.dsts[0]],
|
|
||||||
Type::Empty | Type::WithField { .. } | Type::Closure { .. }
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_object {
|
for &dst in &instr.dsts {
|
||||||
let (src, dst) = (instr.args[0], instr.dsts[0]);
|
let joined = cache.heap.join(&val_types[dst], &ty);
|
||||||
val_types[src] = join_ty(&val_types[src], &val_types[dst]);
|
changed |= joined != val_types[dst];
|
||||||
}
|
val_types[dst] = joined;
|
||||||
}
|
}
|
||||||
} // instr
|
} // 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
|
} // block
|
||||||
|
|
||||||
if old_types == val_types {
|
if !changed && cache.heap.version == version {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} // loop
|
} // 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 {
|
InferResult {
|
||||||
val_types,
|
val_types,
|
||||||
|
instr_types,
|
||||||
ret_type,
|
ret_type,
|
||||||
|
reachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether every value of type `ty` is a number
|
||||||
|
fn is_number(ty: Type) -> bool {
|
||||||
|
ty.is_only(Type::number(Number::Int))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether every value of type `ty` is an int32
|
||||||
|
fn is_int(ty: Type) -> bool {
|
||||||
|
ty == Type::number(Number::Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type, if every value of it is one value that its kind and refinement
|
||||||
|
/// tell
|
||||||
|
fn single(ty: &Type) -> Option<Type> {
|
||||||
|
let one = match ty.kinds() {
|
||||||
|
[true, false, false, false, false, false] | [false, true, false, false, false, false] => true,
|
||||||
|
[false, false, true, false, false, false] => ty.bool.flatten().is_some(),
|
||||||
|
[false, false, false, false, true, false] => ty.string.flatten().is_some(),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
one.then_some(*ty)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `a === b`, if the types decide it
|
||||||
|
fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
|
||||||
|
let disjoint = !a.kinds().iter().zip(b.kinds()).any(|(x, y)| *x && y);
|
||||||
|
|
||||||
|
match (single(a), single(b)) {
|
||||||
|
_ if disjoint => Some(false),
|
||||||
|
(Some(x), Some(y)) => Some(x == y),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `a == b`, if the types decide it. Undefined and null are equal to
|
||||||
|
/// each other and to nothing else. Two values of one kind compare as `===`
|
||||||
|
/// does
|
||||||
|
fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
|
||||||
|
let nullish = |t: &Type| t.has_nullish() && t.non_nullish().is_empty();
|
||||||
|
|
||||||
|
match (a, b) {
|
||||||
|
_ if nullish(a) && nullish(b) => Some(true),
|
||||||
|
_ if (nullish(a) && !b.has_nullish()) || (!a.has_nullish() && nullish(b)) => Some(false),
|
||||||
|
_ if a.kinds() == b.kinds() && single(a).is_some() => strict_eq(a, b),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether every value of type `ty` is of a kind in `kinds`: true if every
|
||||||
|
/// value is, false if none is, `None` if the type does not decide it
|
||||||
|
fn test(ty: &Type, kinds: Type) -> Option<bool> {
|
||||||
|
let [inside, outside] = [true, false].map(|want| {
|
||||||
|
ty.kinds().iter().zip(kinds.kinds()).any(|(has, of)| *has && of == want)
|
||||||
|
});
|
||||||
|
|
||||||
|
match (inside, outside) {
|
||||||
|
(true, false) => Some(true),
|
||||||
|
(false, true) => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether every value of type `ty` is a fn, if the type decides it
|
||||||
|
fn is_function(ty: &Type, heap: &Heap) -> Option<bool> {
|
||||||
|
let shape = ty.object.map(|site| heap.shape(site));
|
||||||
|
let fns = shape.is_some_and(|shape| !shape.code.is_empty());
|
||||||
|
let others = ty.kinds()[..5].contains(&true) || shape.is_some_and(|shape| shape.plain);
|
||||||
|
|
||||||
|
match (fns, others) {
|
||||||
|
(true, false) => Some(true),
|
||||||
|
(false, _) => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a value of type `ty` is truthy, if the type decides it
|
||||||
|
fn truthiness(ty: &Type) -> Option<bool> {
|
||||||
|
let truthy = [
|
||||||
|
ty.undefined.then_some(Some(false)),
|
||||||
|
ty.null.then_some(Some(false)),
|
||||||
|
ty.bool,
|
||||||
|
ty.number.map(|_| None),
|
||||||
|
ty.string.map(|s| s.map(|atom| atom != EMPTY_STRING)),
|
||||||
|
ty.object.map(|_| Some(true)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut kinds = truthy.into_iter().flatten();
|
||||||
|
let first = kinds.next()?;
|
||||||
|
kinds.all(|t| t == first).then_some(first).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What `typeof` gives for a value of type `ty`
|
||||||
|
fn typeof_of(ty: &Type, heap: &Heap) -> Type {
|
||||||
|
let shape = ty.object.map(|site| heap.shape(site));
|
||||||
|
|
||||||
|
let names = [
|
||||||
|
ty.undefined.then_some(73), // "undefined"
|
||||||
|
ty.null.then_some(77), // "object"
|
||||||
|
ty.bool.map(|_| 75), // "boolean"
|
||||||
|
ty.number.map(|_| 74), // "number"
|
||||||
|
ty.string.map(|_| 76), // "string"
|
||||||
|
shape.filter(|shape| shape.plain).map(|_| 77), // "object"
|
||||||
|
shape.filter(|shape| !shape.code.is_empty()).map(|_| 27), // "function"
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut names = names.into_iter().flatten();
|
||||||
|
|
||||||
|
match names.next() {
|
||||||
|
Some(name) if names.all(|n| n == name) => Type::string(Some(AtomRef::Predefined(name))),
|
||||||
|
_ => Type::string(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+457
-136
@@ -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)]
|
use std::cell::Cell;
|
||||||
pub enum Type {
|
|
||||||
Unknown,
|
use crate::{
|
||||||
Undefined,
|
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
|
||||||
Bool,
|
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,
|
Int,
|
||||||
Float,
|
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
|
/// 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
|
||||||
/// `follow_proto` sets whether we iterate within the type's prototype field too.
|
#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
|
||||||
/// The type's own fields come first, so a field the object defines itself shadows
|
pub struct Type {
|
||||||
/// the prototype's
|
pub undefined: bool,
|
||||||
///
|
pub null: bool,
|
||||||
/// That also means a field name can be yielded more than once, and the first occurrence
|
/// The value, if it is known
|
||||||
/// is the one that counts
|
pub bool: Option<Option<bool>>,
|
||||||
pub struct Fields<'a> {
|
pub number: Option<Number>,
|
||||||
current: &'a Type,
|
/// The value, if it is known. Atoms are interned, so two equal atoms are
|
||||||
proto: Option<&'a Type>,
|
/// two equal strings
|
||||||
follow_proto: bool,
|
pub string: Option<Option<AtomRef>>,
|
||||||
}
|
/// The shape, by a site in it (see `Heap`)
|
||||||
|
pub object: Option<SiteId>,
|
||||||
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()?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Type {
|
impl Type {
|
||||||
/// The object's own fields, not those of its prototype
|
pub const UNDEFINED: Type = Type { undefined: true, ..Type::EMPTY };
|
||||||
pub fn fields(&self) -> Fields<'_> {
|
pub const NULL: Type = Type { null: true, ..Type::EMPTY };
|
||||||
Fields {
|
pub const EMPTY: Type = Type {
|
||||||
current: self,
|
undefined: false,
|
||||||
proto: None,
|
null: false,
|
||||||
follow_proto: 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
|
/// The type without undefined and null
|
||||||
pub fn fields_via_proto(&self) -> Fields<'_> {
|
pub fn non_nullish(&self) -> Type {
|
||||||
Fields {
|
Type {
|
||||||
current: self,
|
undefined: false,
|
||||||
proto: None,
|
null: false,
|
||||||
follow_proto: true,
|
..*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:
|
/// The join of two refinements that are a value if it is known
|
||||||
/// Unknown * T = T
|
fn known<T: PartialEq>(a: Option<Option<T>>, b: Option<Option<T>>) -> Option<Option<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 {
|
|
||||||
match (a, b) {
|
match (a, b) {
|
||||||
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
|
(Some(a), Some(b)) => Some(if a == b { a } else { None }),
|
||||||
(Type::Any, _) | (_, Type::Any) => Type::Any,
|
(a, b) => a.or(b),
|
||||||
(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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Do a look-up of `field` on `obj`, going through own fields then prototype
|
/// A field of a shape
|
||||||
/// chain's fields
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub fn get_field(obj: &Type, field: AtomRef) -> &Type {
|
pub struct Field {
|
||||||
obj.fields_via_proto()
|
pub ty: Type,
|
||||||
.find(|(name, _)| *name == field)
|
/// Every object of the shape gets the field when it is created, so the
|
||||||
.map(|(_, ty)| ty)
|
/// field is never absent
|
||||||
.unwrap_or(&Type::Undefined)
|
pub defined: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write `field` into `obj`
|
/// The type of a set of objects: what `docs/model.md` calls a shape
|
||||||
///
|
#[derive(Debug, Clone, Default)]
|
||||||
/// If the object type already has a field by that name, the types are joined,
|
pub struct Shape {
|
||||||
/// else a new type with `field` is created
|
/// Sorted by name
|
||||||
pub fn set_field(obj: &Type, field: AtomRef, field_ty: &Type) -> Type {
|
pub fields: Vec<(AtomRef, Field)>,
|
||||||
match obj {
|
/// The code that its objects can run, sorted
|
||||||
// A field written onto a closure goes into its environment.
|
pub code: Vec<Code>,
|
||||||
Type::Closure { fn_id, env } => Type::Closure {
|
/// Some of its objects are not fns
|
||||||
fn_id: *fn_id,
|
pub plain: bool,
|
||||||
env: Box::new(set_field(env, field, field_ty)),
|
/// A field of one of its objects is written after the object is created
|
||||||
},
|
pub written: bool,
|
||||||
|
/// One of its objects is in an identity test with an object
|
||||||
|
pub compared: bool,
|
||||||
|
/// An op that creates one of its objects has run (see infer)
|
||||||
|
pub live: bool,
|
||||||
|
}
|
||||||
|
|
||||||
// Write into existing field, join types
|
impl Shape {
|
||||||
Type::WithField { name, ty, rest } if *name == field => {
|
pub fn field(&self, name: AtomRef) -> Option<&Field> {
|
||||||
let joined = join_ty(ty, field_ty);
|
let i = self.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?;
|
||||||
|
Some(&self.fields[i].1)
|
||||||
|
}
|
||||||
|
|
||||||
if joined == **ty {
|
/// Whether a copy of one of its objects cannot be told from the object:
|
||||||
return obj.clone();
|
/// no write after creation can show the copy, and no identity test can
|
||||||
|
/// (see `docs/model.md`, section 5)
|
||||||
|
pub fn is_value(&self) -> bool {
|
||||||
|
!self.written && !self.compared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A site, and the code that its objects run, if they are fns
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SiteInfo {
|
||||||
|
pub site: Site,
|
||||||
|
pub code: Option<Code>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What `Heap::new` finds about one instr
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
struct InstrInfo {
|
||||||
|
/// The site of the object it creates
|
||||||
|
site: Option<SiteId>,
|
||||||
|
/// A `define_field` into the value that a creating op gives, before the
|
||||||
|
/// object goes anywhere. Every other field write is a write after creation
|
||||||
|
creation_write: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every shape. Objects that flow into one place have one shape: their shapes
|
||||||
|
/// are merged. A merged site points to the site it was merged into, and only
|
||||||
|
/// a site that points to itself keeps a shape
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Heap {
|
||||||
|
/// Every site, by number: the creating ops of every fn in order, then the
|
||||||
|
/// natives, then the proto links
|
||||||
|
pub sites: Vec<SiteInfo>,
|
||||||
|
shapes: Vec<Shape>,
|
||||||
|
parent: Vec<Cell<SiteId>>,
|
||||||
|
/// Each fn's first entry in `instrs`, which has one entry for each instr
|
||||||
|
fn_starts: Vec<usize>,
|
||||||
|
instrs: Vec<InstrInfo>,
|
||||||
|
/// The number of the first native's site
|
||||||
|
natives: SiteId,
|
||||||
|
/// Goes up on every change, so a walk can tell that a read may give more
|
||||||
|
pub version: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Heap {
|
||||||
|
/// Number every site, and give each one a shape with the fields its
|
||||||
|
/// creation writes write
|
||||||
|
pub fn new(fns: &[RegFn]) -> Self {
|
||||||
|
let mut heap = Self {
|
||||||
|
sites: vec![],
|
||||||
|
shapes: vec![],
|
||||||
|
parent: vec![],
|
||||||
|
fn_starts: vec![],
|
||||||
|
instrs: vec![],
|
||||||
|
natives: 0,
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The site that each value of the fn holds, if a creating op gave it
|
||||||
|
let mut created: Vec<Option<SiteId>> = vec![];
|
||||||
|
|
||||||
|
for (fn_id, f) in fns.iter().enumerate() {
|
||||||
|
heap.fn_starts.push(heap.instrs.len());
|
||||||
|
created.clear();
|
||||||
|
created.resize(f.val_count, None);
|
||||||
|
|
||||||
|
for (instr, op) in f.code.iter().enumerate() {
|
||||||
|
let mut info = InstrInfo::default();
|
||||||
|
|
||||||
|
match op.op {
|
||||||
|
OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8 => {
|
||||||
|
let code = match op.op {
|
||||||
|
OpCode::Object => None,
|
||||||
|
_ => match f.cpool[op.imm.const_idx()] {
|
||||||
|
JsValue::FnById(id) => Some(Code::Fn(id)),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let site = heap.add(Site::Op { fn_id, instr }, code);
|
||||||
|
created[op.dsts[0]] = Some(site);
|
||||||
|
info.site = Some(site);
|
||||||
|
}
|
||||||
|
OpCode::DefineField => {
|
||||||
|
if let Some(site) = created[op.args[1]] {
|
||||||
|
let field = Field { ty: Type::EMPTY, defined: true };
|
||||||
|
let fields = &mut heap.shapes[site as usize].fields;
|
||||||
|
|
||||||
|
if let Err(i) = fields.binary_search_by_key(&op.imm.atom(), |(n, _)| *n) {
|
||||||
|
fields.insert(i, (op.imm.atom(), field));
|
||||||
|
}
|
||||||
|
|
||||||
|
info.creation_write = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
heap.instrs.push(info);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heap.natives = heap.sites.len() as SiteId;
|
||||||
|
|
||||||
|
for id in 0..NATIVES.len() {
|
||||||
|
heap.add(Site::Native(id), Some(Code::Native(id)));
|
||||||
|
}
|
||||||
|
|
||||||
Type::WithField {
|
for kind in Primitive::ALL {
|
||||||
name: field,
|
heap.add(Site::ProtoLink(kind), Some(Code::ProtoLink(kind)));
|
||||||
ty: Box::new(joined),
|
}
|
||||||
rest: rest.clone(),
|
|
||||||
|
heap
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, site: Site, code: Option<Code>) -> SiteId {
|
||||||
|
let id = self.sites.len() as SiteId;
|
||||||
|
|
||||||
|
self.sites.push(SiteInfo { site, code });
|
||||||
|
self.parent.push(Cell::new(id));
|
||||||
|
self.shapes.push(Shape {
|
||||||
|
code: code.into_iter().collect(),
|
||||||
|
plain: code.is_none(),
|
||||||
|
..Shape::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The site that instr `instr` of fn `fn_id` creates, if it is a creating
|
||||||
|
/// op
|
||||||
|
pub fn site_at(&self, fn_id: usize, instr: usize) -> Option<SiteId> {
|
||||||
|
self.instrs[self.fn_starts[fn_id] + instr].site
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_creation_write(&self, fn_id: usize, instr: usize) -> bool {
|
||||||
|
self.instrs[self.fn_starts[fn_id] + instr].creation_write
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The site of a built-in fn
|
||||||
|
pub fn builtin(&self, site: Site) -> SiteId {
|
||||||
|
self.natives
|
||||||
|
+ match site {
|
||||||
|
Site::Native(id) => id as SiteId,
|
||||||
|
Site::ProtoLink(kind) => (NATIVES.len() + kind as usize) as SiteId,
|
||||||
|
Site::Op { .. } => unreachable!(),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The site that `site` is merged into, or `site` itself
|
||||||
|
pub fn find(&self, mut site: SiteId) -> SiteId {
|
||||||
|
loop {
|
||||||
|
let parent = self.parent[site as usize].get();
|
||||||
|
|
||||||
|
if parent == site {
|
||||||
|
return site;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path halving: skip a step for the next find
|
||||||
|
let grandparent = self.parent[parent as usize].get();
|
||||||
|
self.parent[site as usize].set(grandparent);
|
||||||
|
site = grandparent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `ty` with its object's site the one that keeps its shape
|
||||||
|
pub fn canon(&self, ty: &Type) -> Type {
|
||||||
|
Type {
|
||||||
|
object: ty.object.map(|site| self.find(site)),
|
||||||
|
..*ty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shape(&self, site: SiteId) -> &Shape {
|
||||||
|
&self.shapes[self.find(site) as usize]
|
||||||
|
}
|
||||||
|
|
||||||
Type::WithField { name, ty, rest } => {
|
/// Every site that keeps a shape, and that a creating op has reached
|
||||||
let new_rest = set_field(rest, field, field_ty);
|
pub fn live_shapes(&self) -> impl Iterator<Item = SiteId> + '_ {
|
||||||
|
(0..self.sites.len() as SiteId).filter(|&site| self.find(site) == site && self.shapes[site as usize].live)
|
||||||
|
}
|
||||||
|
|
||||||
// Nothing changed deeper down, so nothing changes here either.
|
/// An object of the site, which a creating op gives
|
||||||
// TODO: Avoid this recursion and deep eq checking
|
pub fn create(&mut self, site: SiteId) -> Type {
|
||||||
if new_rest == **rest {
|
let root = self.find(site);
|
||||||
return obj.clone();
|
self.mark(root, |shape| &mut shape.live);
|
||||||
|
Type::object(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a flag of the shape of `site`
|
||||||
|
pub fn mark(&mut self, site: SiteId, flag: fn(&mut Shape) -> &mut bool) {
|
||||||
|
let root = self.find(site);
|
||||||
|
let flag = flag(&mut self.shapes[root as usize]);
|
||||||
|
|
||||||
|
if !*flag {
|
||||||
|
*flag = true;
|
||||||
|
self.version += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether an object of the shape of `site` can lack its own `field`. A
|
||||||
|
/// box's value never can: JS gives a binding a value before any read of it
|
||||||
|
/// that does not throw (see transform)
|
||||||
|
pub fn can_be_absent(&self, site: SiteId, field: AtomRef) -> bool {
|
||||||
|
field != BOX_FIELD && !self.shape(site).field(field).is_some_and(|f| f.defined)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type lattice join: the union, kind by kind. Two different known values
|
||||||
|
/// give an unknown one, and two shapes are merged
|
||||||
|
pub fn join(&mut self, a: &Type, b: &Type) -> Type {
|
||||||
|
Type {
|
||||||
|
undefined: a.undefined || b.undefined,
|
||||||
|
null: a.null || b.null,
|
||||||
|
bool: known(a.bool, b.bool),
|
||||||
|
number: a.number.max(b.number),
|
||||||
|
string: known(a.string, b.string),
|
||||||
|
object: match (a.object, b.object) {
|
||||||
|
(Some(x), Some(y)) => Some(self.merge(x, y)),
|
||||||
|
(x, y) => x.or(y).map(|site| self.find(site)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge the shape of `from` into the shape of `into`. A field that only
|
||||||
|
/// one of them has can be absent in the merged shape
|
||||||
|
fn merge(&mut self, into: SiteId, from: SiteId) -> SiteId {
|
||||||
|
let (into, from) = (self.find(into), self.find(from));
|
||||||
|
|
||||||
|
if into == from {
|
||||||
|
return into;
|
||||||
|
}
|
||||||
|
|
||||||
|
let from_shape = std::mem::take(&mut self.shapes[from as usize]);
|
||||||
|
self.parent[from as usize].set(into);
|
||||||
|
self.version += 1;
|
||||||
|
|
||||||
|
let shape = &mut self.shapes[into as usize];
|
||||||
|
shape.code.extend(&from_shape.code);
|
||||||
|
shape.code.sort_unstable();
|
||||||
|
shape.code.dedup();
|
||||||
|
shape.plain |= from_shape.plain;
|
||||||
|
shape.written |= from_shape.written;
|
||||||
|
shape.compared |= from_shape.compared;
|
||||||
|
shape.live |= from_shape.live;
|
||||||
|
|
||||||
|
for (name, field) in &mut shape.fields {
|
||||||
|
field.defined &= from_shape.field(*name).is_some_and(|f| f.defined);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, field) in &from_shape.fields {
|
||||||
|
let defined = self.shape(into).field(*name).is_some_and(|f| f.defined) && field.defined;
|
||||||
|
self.write(into, *name, &field.ty);
|
||||||
|
|
||||||
|
let root = self.find(into) as usize;
|
||||||
|
let i = self.shapes[root].fields.binary_search_by_key(name, |(n, _)| *n).unwrap();
|
||||||
|
self.shapes[root].fields[i].1.defined = defined;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.find(into)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join `ty` into `field` of the shape of `site`. A field that the shape
|
||||||
|
/// did not have can be absent
|
||||||
|
pub fn write(&mut self, site: SiteId, field: AtomRef, ty: &Type) {
|
||||||
|
let old = self.shape(site).field(field).map_or(Type::EMPTY, |f| f.ty);
|
||||||
|
let new = self.join(&old, ty);
|
||||||
|
|
||||||
|
// The join may have merged the shape into another one
|
||||||
|
let root = self.find(site) as usize;
|
||||||
|
let fields = &mut self.shapes[root].fields;
|
||||||
|
|
||||||
|
match fields.binary_search_by_key(&field, |(n, _)| *n) {
|
||||||
|
Ok(i) if fields[i].1.ty == new => return,
|
||||||
|
Ok(i) => fields[i].1.ty = new,
|
||||||
|
Err(i) => fields.insert(i, (field, Field { ty: new, defined: false })),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.version += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A read of `field` on an object of the shape of `site`: its own field,
|
||||||
|
/// then, where that can be absent, the read on its prototype. At the end of
|
||||||
|
/// the chain, an absent field is undefined
|
||||||
|
///
|
||||||
|
/// Merged shapes can make a chain loop, which a real object cannot. A chain
|
||||||
|
/// longer than the number of sites has looped, and stops
|
||||||
|
pub fn read(&mut self, site: SiteId, field: AtomRef) -> Type {
|
||||||
|
let mut site = self.find(site);
|
||||||
|
let mut ty = Type::EMPTY;
|
||||||
|
|
||||||
|
for _ in 0..self.sites.len() {
|
||||||
|
let own = self.shape(site).field(field).map_or(Type::EMPTY, |f| f.ty);
|
||||||
|
ty = self.join(&ty, &own);
|
||||||
|
|
||||||
|
if !self.can_be_absent(site, field) {
|
||||||
|
return ty;
|
||||||
}
|
}
|
||||||
|
|
||||||
Type::WithField {
|
// The chain ends at a prototype that is null or absent
|
||||||
name: *name,
|
let proto = self.shape(site).field(PROTO_FIELD).map_or(Type::EMPTY, |f| f.ty);
|
||||||
ty: ty.clone(),
|
ty.undefined |= proto.null || self.can_be_absent(site, PROTO_FIELD);
|
||||||
rest: Box::new(new_rest),
|
|
||||||
|
match proto.object {
|
||||||
|
Some(proto) => site = self.find(proto),
|
||||||
|
None => break,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A read of `field` on a value of type `ty`. A primitive reads its kind's
|
||||||
|
/// prototype in `protos`. Undefined and null have no fields: the read
|
||||||
|
/// throws, so they give nothing
|
||||||
|
pub fn read_value(&mut self, ty: &Type, field: AtomRef, protos: &[Type; 3]) -> Type {
|
||||||
|
let proto = |kind: Primitive| protos[kind as usize].object.filter(|_| kind.is_in(ty));
|
||||||
|
let mut read = Type::EMPTY;
|
||||||
|
|
||||||
|
for site in Primitive::ALL.map(proto).into_iter().chain([ty.object]).flatten() {
|
||||||
|
let ty = self.read(site, field);
|
||||||
|
read = self.join(&read, &ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => Type::WithField {
|
read
|
||||||
name: field,
|
|
||||||
ty: Box::new(field_ty.clone()),
|
|
||||||
rest: Box::new(obj.clone()),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ impl Atom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum AtomRef {
|
pub enum AtomRef {
|
||||||
TaggedInt(u32),
|
TaggedInt(u32),
|
||||||
Predefined(u32),
|
Predefined(u32),
|
||||||
@@ -170,6 +170,8 @@ pub enum JsValue {
|
|||||||
FnById(usize),
|
FnById(usize),
|
||||||
/// A C function, by its index in `native::NATIVES`
|
/// A C function, by its index in `native::NATIVES`
|
||||||
NativeFn(usize),
|
NativeFn(usize),
|
||||||
|
/// Makes its arg the prototype of a primitive kind (see `native`)
|
||||||
|
ProtoLink(crate::native::Primitive),
|
||||||
Module(Module),
|
Module(Module),
|
||||||
TypedArray,
|
TypedArray,
|
||||||
ArrayBuffer,
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct Instr {
|
pub struct Instr {
|
||||||
pub pc: usize,
|
pub pc: usize,
|
||||||
|
|||||||
+10
-1
@@ -1,4 +1,5 @@
|
|||||||
mod analyze;
|
mod analyze;
|
||||||
|
mod emit;
|
||||||
mod flatten;
|
mod flatten;
|
||||||
mod infer;
|
mod infer;
|
||||||
mod libqjs;
|
mod libqjs;
|
||||||
@@ -9,12 +10,15 @@ mod transform;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
|
use analyze::analyze;
|
||||||
use flatten::flatten;
|
use flatten::flatten;
|
||||||
use infer::infer_types;
|
use infer::infer_types;
|
||||||
use libqjs::compile;
|
use libqjs::compile;
|
||||||
use regify::regify;
|
use regify::regify;
|
||||||
use transform::transform;
|
use transform::transform;
|
||||||
|
|
||||||
|
const STDLIB: &[u8] = include_bytes!("../../lib/stdlib.js");
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Load source file
|
// Load source file
|
||||||
let args = std::env::args().collect::<Vec<String>>();
|
let args = std::env::args().collect::<Vec<String>>();
|
||||||
@@ -24,7 +28,9 @@ fn main() {
|
|||||||
let metadata = std::fs::metadata(filename).unwrap();
|
let metadata = std::fs::metadata(filename).unwrap();
|
||||||
assert!(metadata.is_file());
|
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();
|
file.read_to_end(&mut src).unwrap();
|
||||||
|
|
||||||
// Compile to bytecode
|
// Compile to bytecode
|
||||||
@@ -35,4 +41,7 @@ fn main() {
|
|||||||
let flat_fns = transform(flat_fns, &captures, &atoms);
|
let flat_fns = transform(flat_fns, &captures, &atoms);
|
||||||
let reg_fns = regify(flat_fns);
|
let reg_fns = regify(flat_fns);
|
||||||
let ty_cache = infer_types(®_fns);
|
let ty_cache = infer_types(®_fns);
|
||||||
|
let analysis = analyze(®_fns);
|
||||||
|
|
||||||
|
print!("{}", emit::emit(®_fns, &ty_cache, &analysis, &atoms));
|
||||||
}
|
}
|
||||||
|
|||||||
+129
-18
@@ -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::{
|
use crate::{
|
||||||
infer::types::Type,
|
infer::types::{Number, Type},
|
||||||
libqjs::parse::{Atom, AtomRef, JsString},
|
libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A native C function callable by JS
|
/// 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 struct NativeFn {
|
||||||
pub name: &'static str,
|
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 {
|
macro_rules! native {
|
||||||
name: "__native_print",
|
($name:literal ( $($arg:ident),* ) -> $ret:ident) => {
|
||||||
ret: |_| Type::Undefined,
|
NativeFn {
|
||||||
}];
|
name: $name,
|
||||||
|
args: &[$(ty!($arg)),*],
|
||||||
|
ret: ty!($ret),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn native_fn_idx(name: AtomRef, atoms: &[Atom]) -> Option<usize> {
|
/// A native's param or return type: any value of its kind
|
||||||
let AtomRef::FileLocal(idx) = name else {
|
macro_rules! ty {
|
||||||
return None;
|
(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 {
|
||||||
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s,
|
let js_string = match name {
|
||||||
_ => return None,
|
AtomRef::FileLocal(idx) => match &atoms[idx] {
|
||||||
|
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s,
|
||||||
|
Atom::Predefined(n) => return PREDEFINED_ATOMS[*n as usize].to_string(),
|
||||||
|
},
|
||||||
|
AtomRef::Predefined(n) => return PREDEFINED_ATOMS[n as usize].to_string(),
|
||||||
|
AtomRef::TaggedInt(n) => return n.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
js_string_text(js_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn js_string_text(js_string: &JsString) -> String {
|
||||||
// TODO: Avoid string alloc
|
// TODO: Avoid string alloc
|
||||||
let string = match js_string {
|
match js_string {
|
||||||
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
|
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
|
||||||
JsString::UTF16(b) => {
|
JsString::UTF16(b) => {
|
||||||
let u: Vec<u16> = b
|
let u: Vec<u16> = b
|
||||||
@@ -36,7 +149,5 @@ pub fn native_fn_idx(name: AtomRef, atoms: &[Atom]) -> Option<usize> {
|
|||||||
.collect();
|
.collect();
|
||||||
String::from_utf16_lossy(&u)
|
String::from_utf16_lossy(&u)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
NATIVES.iter().position(|n| n.name == string)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+115
-42
@@ -51,33 +51,70 @@ impl RegFn {
|
|||||||
// The stack each block recieves at entry from its predecessors
|
// The stack each block recieves at entry from its predecessors
|
||||||
let mut entry_stacks: Vec<Option<Vec<usize>>> = vec![None; blocks.len()];
|
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
|
// Walk blocks in CFG order, starting from block 0
|
||||||
let mut worklist = vec![];
|
let mut worklist = vec![];
|
||||||
entry_stacks[0] = Some(vec![]);
|
entry_stacks[0] = Some(vec![]);
|
||||||
worklist.push(0);
|
worklist.push(0);
|
||||||
|
|
||||||
while let Some(block_idx) = worklist.pop() {
|
while let Some(block_idx) = worklist.pop() {
|
||||||
let block = &mut blocks[block_idx];
|
let mut stack = entry_stacks[block_idx].clone().unwrap_or_default();
|
||||||
let mut entry_stack = entry_stacks[block_idx].clone().unwrap_or_default();
|
|
||||||
|
|
||||||
walk_block(
|
walk_block(
|
||||||
block,
|
&blocks[block_idx],
|
||||||
&f.code,
|
&f.code,
|
||||||
f.var_count,
|
f.var_count,
|
||||||
&mut entry_stack,
|
&mut stack,
|
||||||
&mut next_val,
|
&mut next_val,
|
||||||
&mut out,
|
&mut out,
|
||||||
);
|
);
|
||||||
|
|
||||||
for succ_idx in block.succ.clone() {
|
for i in 0..blocks[block_idx].succ.len() {
|
||||||
let succ_stack = &mut entry_stacks[succ_idx];
|
let succ_idx = blocks[block_idx].succ[i];
|
||||||
|
|
||||||
match succ_stack {
|
// A join's stack slots are new values, which every block that
|
||||||
Some(succ_stack) => add_block_postfix(block, &entry_stack, succ_stack),
|
// 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 => {
|
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);
|
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,
|
next_val: &mut usize,
|
||||||
out: &mut [RegInstr],
|
out: &mut [RegInstr],
|
||||||
) {
|
) {
|
||||||
let mut fresh_val = || {
|
let mut fresh_val = || fresh_val(next_val);
|
||||||
let v = *next_val;
|
|
||||||
*next_val += 1;
|
|
||||||
v
|
|
||||||
};
|
|
||||||
|
|
||||||
for instr_idx in block.start..block.end {
|
for instr_idx in block.start..block.end {
|
||||||
let instr = &code[instr_idx];
|
let instr = &code[instr_idx];
|
||||||
@@ -129,6 +162,13 @@ fn walk_block(
|
|||||||
out_instr.args.push(v);
|
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
|
// Locals and args are not renamed, so their read/writes need special handling
|
||||||
// Handle read of a local or arg
|
// Handle read of a local or arg
|
||||||
if let Some(var) = is_var_read(instr, var_count) {
|
if let Some(var) = is_var_read(instr, var_count) {
|
||||||
@@ -188,10 +228,17 @@ fn walk_block(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A field write is a define whose result lands back in the object's own value
|
// A field write lands back in the object's own value: the object with
|
||||||
if instr.op == OpCode::PutField {
|
// the field is the same object. define_field also leaves it on the
|
||||||
out_instr.op = OpCode::DefineField;
|
// stack, put_field does not
|
||||||
out_instr.dsts.push(out_instr.args[1]);
|
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;
|
out[instr_idx] = out_instr;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -209,21 +256,43 @@ fn walk_block(
|
|||||||
|
|
||||||
/// Some instrs have extra pops not reflected by instr.op.info().n_pop
|
/// Some instrs have extra pops not reflected by instr.op.info().n_pop
|
||||||
fn extra_pops(instr: &Instr) -> usize {
|
fn extra_pops(instr: &Instr) -> usize {
|
||||||
match instr.op {
|
match instr.imm {
|
||||||
OpCode::Call0 => 0,
|
ImmOperand::Npop(npop) => npop as usize,
|
||||||
OpCode::Call1 => 1,
|
ImmOperand::NpopU16 { npop, .. } => npop as usize,
|
||||||
OpCode::Call2 => 2,
|
_ => 0,
|
||||||
OpCode::Call3 => 3,
|
|
||||||
_ => 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.
|
/// 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);
|
let arg = |k: usize| Some(var_count + k);
|
||||||
|
|
||||||
match instr.op {
|
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.
|
/// 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);
|
let arg = |k: usize| Some(var_count + k);
|
||||||
|
|
||||||
match instr.op {
|
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
|
/// The copies that carry the values on a block's stack into its successor's
|
||||||
///
|
/// entry stack
|
||||||
/// This does the job of phi nodes in typical SSA, Currently only relevant to
|
fn edge_copies(stack: &[usize], succ_stack: &[usize]) -> Vec<RegInstr> {
|
||||||
/// 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]) {
|
|
||||||
let overlap = stack.iter().rev().zip(succ_stack.iter().rev());
|
let overlap = stack.iter().rev().zip(succ_stack.iter().rev());
|
||||||
|
|
||||||
for (val, succ_val) in overlap {
|
overlap
|
||||||
block.postfix.push(RegInstr {
|
.filter(|(val, succ_val)| val != succ_val)
|
||||||
dsts: vec![*succ_val],
|
.map(|(&val, &succ_val)| RegInstr {
|
||||||
|
dsts: vec![succ_val],
|
||||||
op: OpCode::SetLoc,
|
op: OpCode::SetLoc,
|
||||||
imm: ImmOperand::None,
|
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> {
|
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
|
||||||
|
|||||||
@@ -1,67 +1,52 @@
|
|||||||
|
use super::{PROTOTYPE_FIELD, pusher};
|
||||||
use crate::libqjs::parse::{AtomRef, ImmOperand, Instr, opcodes::OpCode};
|
use crate::libqjs::parse::{AtomRef, ImmOperand, Instr, opcodes::OpCode};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct MethodSequence {
|
struct MethodSequence {
|
||||||
fn_cpool_idx: u32,
|
fn_const: ImmOperand,
|
||||||
name: AtomRef,
|
name: AtomRef,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ClassSequence {
|
pub struct ClassSequence {
|
||||||
start_pc: usize,
|
start_pc: usize,
|
||||||
ctor_fn_cpool_idx: u32,
|
ctor_const: ImmOperand,
|
||||||
methods: Vec<MethodSequence>,
|
methods: Vec<MethodSequence>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClassSequence {
|
impl ClassSequence {
|
||||||
fn emit_len(&self) -> usize {
|
/// fclosure ctor [C]
|
||||||
3 + (2 * self.methods.len())
|
/// 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>) {
|
pub fn emit_prototype_form(&self, out: &mut Vec<Instr>) {
|
||||||
let mut push = |op, imm| {
|
let mut push = pusher(self.start_pc, out);
|
||||||
out.push(Instr {
|
let prototype = ImmOperand::Atom(PROTOTYPE_FIELD);
|
||||||
pc: self.start_pc,
|
|
||||||
op,
|
|
||||||
imm,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
push(OpCode::Fclosure8, ImmOperand::Const(self.ctor_fn_cpool_idx));
|
push(OpCode::Fclosure8, self.ctor_const);
|
||||||
push(OpCode::Dup, ImmOperand::None);
|
push(OpCode::Dup, ImmOperand::None);
|
||||||
push(OpCode::GetField, ImmOperand::Atom(AtomRef::Predefined(63))); // .prototype
|
push(OpCode::Object, ImmOperand::None);
|
||||||
|
|
||||||
for m in &self.methods {
|
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::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> {
|
pub fn declass(mut code: &[Instr]) -> Vec<Instr> {
|
||||||
let orig = code;
|
let mut out = Vec::with_capacity(code.len());
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
while !code.is_empty() {
|
while !code.is_empty() {
|
||||||
match match_class_seq(code) {
|
match match_class_seq(code) {
|
||||||
@@ -70,15 +55,14 @@ pub fn declass(mut code: &[Instr]) -> Vec<Instr> {
|
|||||||
code = rest;
|
code = rest;
|
||||||
}
|
}
|
||||||
Err(rest) => {
|
Err(rest) => {
|
||||||
let consumed = code.len() - rest.len(); // Handle partial consumption
|
// Copy what matched before the match failed, at least one instr
|
||||||
let consumed = consumed.max(1);
|
let consumed = (code.len() - rest.len()).max(1);
|
||||||
out.extend_from_slice(&code[..consumed]);
|
out.extend_from_slice(&code[..consumed]);
|
||||||
code = &code[consumed..];
|
code = &code[consumed..];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug_assert!(out.len() == len);
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,21 +77,16 @@ fn match_class_seq(ins: &[Instr]) -> PResult<'_, ClassSequence> {
|
|||||||
let pc = ins[0].pc;
|
let pc = ins[0].pc;
|
||||||
|
|
||||||
let (ins, push_const) = match_instr(ins, |i| i.op == OpCode::PushConst8)?;
|
let (ins, push_const) = match_instr(ins, |i| i.op == OpCode::PushConst8)?;
|
||||||
|
let (ins, define_class) = match_instr(ins, |i| i.op == OpCode::DefineClass)?;
|
||||||
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, methods) = match_many(ins, match_method_seq);
|
let (ins, methods) = match_many(ins, match_method_seq);
|
||||||
|
|
||||||
let ctor_fn_id = match push_const.imm {
|
// Bit 0 of `arg` is set when the class has a superclass
|
||||||
ImmOperand::Const(id) => id,
|
let is_derived = matches!(define_class.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1);
|
||||||
_ => unreachable!(),
|
assert!(!is_derived, "derived classes are not supported");
|
||||||
};
|
|
||||||
|
|
||||||
let seq = ClassSequence {
|
let seq = ClassSequence {
|
||||||
start_pc: pc,
|
start_pc: pc,
|
||||||
ctor_fn_cpool_idx: ctor_fn_id,
|
ctor_const: push_const.imm,
|
||||||
methods,
|
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, fclosure) = match_instr(ins, |i| i.op == OpCode::Fclosure8)?;
|
||||||
let (ins, define_method) = match_instr(ins, |i| i.op == OpCode::DefineMethod)?;
|
let (ins, define_method) = match_instr(ins, |i| i.op == OpCode::DefineMethod)?;
|
||||||
|
|
||||||
let fn_id = match fclosure.imm {
|
let seq = MethodSequence {
|
||||||
ImmOperand::Const(id) => id,
|
fn_const: fclosure.imm,
|
||||||
_ => unreachable!(),
|
name: define_method.imm.atom(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = match define_method.imm {
|
Ok((ins, seq))
|
||||||
ImmOperand::AtomU8 { atom, .. } => atom,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
ins,
|
|
||||||
MethodSequence {
|
|
||||||
fn_cpool_idx: fn_id,
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────────────────────── Matcher Primitives ───────────────────────────────────────
|
// ───────────────────────────── Matcher Primitives ───────────────────────────────────────
|
||||||
|
|||||||
+350
-278
@@ -1,5 +1,7 @@
|
|||||||
mod declass;
|
mod declass;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use declass::declass;
|
use declass::declass;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -7,10 +9,28 @@ use crate::{
|
|||||||
libqjs::parse::{
|
libqjs::parse::{
|
||||||
Atom, AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode,
|
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(
|
pub fn transform(
|
||||||
flat_fns: Vec<FlatFn>,
|
flat_fns: Vec<FlatFn>,
|
||||||
@@ -19,54 +39,25 @@ pub fn transform(
|
|||||||
) -> Vec<FlatFn> {
|
) -> Vec<FlatFn> {
|
||||||
flat_fns
|
flat_fns
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.map(|f| transform_one(f, captures, atoms))
|
||||||
.map(|(i, f)| transform_one(i, f, captures, atoms))
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transform_one(
|
fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], atoms: &[Atom]) -> FlatFn {
|
||||||
fn_id: usize,
|
|
||||||
mut f: FlatFn,
|
|
||||||
captures: &[Vec<ClosureVar>],
|
|
||||||
atoms: &[Atom],
|
|
||||||
) -> FlatFn {
|
|
||||||
let code = declass(&f.code);
|
let code = declass(&f.code);
|
||||||
|
|
||||||
// `this` and `env` are passed as the last 2 args to every fn
|
// Temp locals, after the fn's own, for the args of a `new` (see replace_new())
|
||||||
let _this_arg = f.arg_count;
|
let temps = f.var_count;
|
||||||
let env_arg = f.arg_count + 1;
|
let n_temps = code.iter().filter_map(is_new).max().unwrap_or(0);
|
||||||
|
|
||||||
// Calculate transformed out len
|
let mut out = Vec::with_capacity(code.len());
|
||||||
let mut len = 0;
|
|
||||||
|
|
||||||
for instr in &code {
|
for instr in &code {
|
||||||
if let Some(child_captures) = is_fclosure(instr, &f, captures) {
|
if replace_new(instr, temps, &mut out) {
|
||||||
len += 1 + child_captures.iter().map(capture_len).sum::<usize>(); // fclosure; (load; define_field)*
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(acc) = is_capture_acc(instr) {
|
if replace_arg_access(instr, &mut out) {
|
||||||
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) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,274 +72,366 @@ fn transform_one(
|
|||||||
out.push(instr.clone());
|
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 {
|
FlatFn {
|
||||||
name: f.name,
|
name: f.name,
|
||||||
arg_count: f.arg_count + 2, // 2 extra args, `this` and `env`
|
arg_count: f.arg_count + FIRST_ARG as usize,
|
||||||
var_count: f.var_count,
|
var_count,
|
||||||
cpool: f.cpool,
|
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
|
// ───────────────────────────────────────────────── New Processing ──────────────────────────────────────────────────────────────
|
||||||
fn is_fclosure<'a>(
|
|
||||||
instr: &Instr,
|
/// If `instr` is a `new`, the number of args it passes
|
||||||
f: &FlatFn,
|
fn is_new(instr: &Instr) -> Option<usize> {
|
||||||
captures: &'a [Vec<ClosureVar>],
|
match (instr.op, instr.imm) {
|
||||||
) -> Option<&'a [ClosureVar]> {
|
(OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize),
|
||||||
if !matches!(instr.op, OpCode::Fclosure8 | OpCode::Fclosure) {
|
_ => None,
|
||||||
return 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:
|
/// Replace:
|
||||||
/// fclosure fn_const
|
/// call_constructor n [F, new_target, a0 .. an-1]
|
||||||
///
|
///
|
||||||
/// With:
|
/// With:
|
||||||
/// fclosure fn_const
|
/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top
|
||||||
/// (push_capture; define_field capture_name)*
|
/// 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
|
/// Whether `new` gives the call's result or `obj` depends on types (see infer)
|
||||||
/// fclosure object which serves as the environment record
|
fn replace_new(instr: &Instr, temps: usize, out: &mut Vec<Instr>) -> bool {
|
||||||
fn replace_fclosure(
|
let Some(n) = is_new(instr) else {
|
||||||
instr: &Instr,
|
return false;
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut push = |op, imm| {
|
let mut push = pusher(instr.pc, out);
|
||||||
out.push(Instr {
|
let temp = |i: usize| ImmOperand::Loc((temps + i) as u16);
|
||||||
pc: instr.pc,
|
|
||||||
op,
|
|
||||||
imm,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
push(OpCode::Fclosure, instr.imm);
|
for i in (0..n).rev() {
|
||||||
|
push(OpCode::PutLoc, temp(i));
|
||||||
for c in child_captures {
|
|
||||||
match c.flags.closure_kind {
|
|
||||||
ClosureKind::Local => push(OpCode::GetLoc, ImmOperand::Loc(c.var_idx as u16)),
|
|
||||||
ClosureKind::Arg => push(OpCode::GetArg, ImmOperand::Arg(c.var_idx as u16)),
|
|
||||||
ClosureKind::Ref => {
|
|
||||||
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
|
|
||||||
push(
|
|
||||||
OpCode::GetField,
|
|
||||||
ImmOperand::Atom(my_captures[c.var_idx].name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
|
|
||||||
push(OpCode::DefineField, ImmOperand::Atom(c.name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many instructions one capture takes: its load, then its define_field
|
// ─────────────────────────────────────────────── Binding Processing ────────────────────────────────────────────────────────────
|
||||||
fn capture_len(c: &ClosureVar) -> usize {
|
|
||||||
match c.flags.closure_kind {
|
|
||||||
ClosureKind::Local | ClosureKind::Arg => 2,
|
|
||||||
ClosureKind::Ref => 3,
|
|
||||||
_ => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────── Capture Access Processing ────────────────────────────────────────────────────────
|
/// Put every binding that a closure captures in a box: an object whose
|
||||||
enum CaptureAccess {
|
/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The
|
||||||
Read(u16),
|
/// owner and the closures read and write the box. A closure keeps the boxes
|
||||||
Write(u16),
|
/// it captured in its own fields, one for each of its `closure_vars`: field `i`
|
||||||
}
|
/// holds the box of capture `i`
|
||||||
|
|
||||||
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
|
|
||||||
///
|
///
|
||||||
/// With:
|
/// The fn makes a box for each binding that its children capture, at entry.
|
||||||
/// get_env_arg
|
/// A captured local holds its box in its own slot. A captured arg keeps its
|
||||||
/// read_write_env_arg
|
/// box in a new local, and the box starts with the arg's value. A `var` starts
|
||||||
fn replace_capture_acc(
|
/// as undefined. A `let` or `const` has no value until it is written (its TDZ):
|
||||||
instr: &Instr,
|
/// object; undefined; define_field <var>; put_loc k each captured `var`
|
||||||
my_captures: &[ClosureVar],
|
/// object; put_loc k each captured `let`
|
||||||
env_arg: usize,
|
/// object; get_arg a; define_field <var>; put_loc box each captured arg
|
||||||
out: &mut Vec<Instr>,
|
///
|
||||||
) -> bool {
|
/// A scope that is entered again is a loop body. QuickJS closes each captured
|
||||||
let acc = match is_capture_acc(instr) {
|
/// binding of the scope (`close_loc`) at every way out of it, so a new box
|
||||||
Some(acc) => acc,
|
/// there gives each iteration its own binding. `set_loc_uninitialized` only
|
||||||
None => return false,
|
/// marks the TDZ, which is not checked, and hoisted fns are made before it, so
|
||||||
};
|
/// it does not make a box
|
||||||
|
///
|
||||||
|
/// These instrs take a pc that no jump goes to, so a jump to the fn's first
|
||||||
|
/// instr does not make them again
|
||||||
|
///
|
||||||
|
/// Then it replaces, for a captured binding:
|
||||||
|
/// get_loc k get_loc box; get_field <var>
|
||||||
|
/// put_loc k get_loc box; swap; put_field <var>
|
||||||
|
/// set_loc k dup; get_loc box; swap; put_field <var>
|
||||||
|
/// close_loc k object; get_loc k; get_field <var>;
|
||||||
|
/// define_field <var>; put_loc k
|
||||||
|
/// a way out of a loop body: a new binding, with
|
||||||
|
/// the current value
|
||||||
|
///
|
||||||
|
/// For any binding:
|
||||||
|
/// get_var_ref i get_arg env; get_field i; get_field <var>
|
||||||
|
/// put_var_ref i get_arg env; get_field i; swap; put_field <var>
|
||||||
|
/// set_var_ref i dup; get_arg env; get_field i; swap; put_field <var>
|
||||||
|
/// fclosure f fclosure f; (push_box; define_field i)*
|
||||||
|
///
|
||||||
|
/// where `push_box` is the box of capture `i` of `f`: the local that holds it,
|
||||||
|
/// or field `j` of this fn's env for capture `j` of this fn. A binding with no
|
||||||
|
/// frame (a module binding) gets its box here, from `object`
|
||||||
|
///
|
||||||
|
/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure
|
||||||
|
/// captures, do nothing. Gives the code and the new number of locals
|
||||||
|
fn replace_bindings(
|
||||||
|
code: &[Instr],
|
||||||
|
cpool: &[JsValue],
|
||||||
|
captures: &[Vec<ClosureVar>],
|
||||||
|
var_count: usize,
|
||||||
|
) -> (Vec<Instr>, usize) {
|
||||||
|
use OpCode::*;
|
||||||
|
|
||||||
let mut push = |op, imm| {
|
let children = cpool.iter().filter_map(|c| match c {
|
||||||
out.push(Instr {
|
JsValue::FnById(child) => Some(*child),
|
||||||
pc: instr.pc,
|
_ => None,
|
||||||
op,
|
});
|
||||||
imm,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
match acc {
|
let mut locals = vec![];
|
||||||
CaptureAccess::Read(read_var) => {
|
let mut args = vec![];
|
||||||
let atom = my_captures[read_var as usize].name;
|
|
||||||
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
|
|
||||||
push(OpCode::GetField, ImmOperand::Atom(atom));
|
|
||||||
}
|
|
||||||
|
|
||||||
CaptureAccess::Write(write_var) => {
|
for c in children.flat_map(|child| &captures[child]) {
|
||||||
let atom = my_captures[write_var as usize].name;
|
match c.flags.closure_kind {
|
||||||
|
ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)),
|
||||||
// The value to write is already on the stack, so env lands above it,
|
ClosureKind::Arg => args.push(c.var_idx),
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
locals.sort_unstable();
|
||||||
|
locals.dedup();
|
||||||
|
args.sort_unstable();
|
||||||
|
args.dedup();
|
||||||
|
|
||||||
|
// Value ids of bindings, as `is_var_read` gives them: args after every local
|
||||||
|
let new_var_count = var_count + args.len();
|
||||||
|
let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k;
|
||||||
|
|
||||||
|
// The local that holds the box of each captured binding, by value id
|
||||||
|
let boxes: HashMap<usize, usize> = (locals.iter().map(|&(k, _)| (k, k)))
|
||||||
|
.chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32));
|
||||||
|
let value = ImmOperand::Atom(BOX_FIELD);
|
||||||
|
let none = ImmOperand::None;
|
||||||
|
let loc = |k: usize| ImmOperand::Loc(k as u16);
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(code.len());
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut push = pusher(usize::MAX, &mut out);
|
||||||
|
|
||||||
|
for &(k, lexical) in &locals {
|
||||||
|
push(Object, none);
|
||||||
|
|
||||||
|
if !lexical {
|
||||||
|
push(Undefined, none);
|
||||||
|
push(DefineField, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
push(PutLoc, loc(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, &k) in args.iter().enumerate() {
|
||||||
|
push(Object, none);
|
||||||
|
push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16));
|
||||||
|
push(DefineField, value);
|
||||||
|
push(PutLoc, loc(var_count + i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for instr in code {
|
||||||
|
let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v));
|
||||||
|
let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v));
|
||||||
|
let own_box = |k: ImmOperand| match k {
|
||||||
|
ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let mut push = pusher(instr.pc, &mut out);
|
||||||
|
|
||||||
|
if let Some(&b) = read {
|
||||||
|
push(GetLoc, loc(b));
|
||||||
|
push(GetField, value);
|
||||||
|
} else if let Some(&b) = write {
|
||||||
|
if instr.op.info().n_push == 1 {
|
||||||
|
push(Dup, none);
|
||||||
|
}
|
||||||
|
|
||||||
|
push(GetLoc, loc(b));
|
||||||
|
push(Swap, none);
|
||||||
|
push(PutField, value);
|
||||||
|
} else if let Some((i, writes, leaves)) = var_ref_access(instr) {
|
||||||
|
if leaves {
|
||||||
|
push(Dup, none);
|
||||||
|
}
|
||||||
|
|
||||||
|
push(GetArg, ImmOperand::Arg(ENV_ARG));
|
||||||
|
push(GetField, slot(i));
|
||||||
|
|
||||||
|
if writes {
|
||||||
|
push(Swap, none);
|
||||||
|
push(PutField, value);
|
||||||
|
} else {
|
||||||
|
push(GetField, value);
|
||||||
|
}
|
||||||
|
} else if instr.op == SetLocUninitialized {
|
||||||
|
push(Nop, none);
|
||||||
|
} else if instr.op == CloseLoc {
|
||||||
|
match own_box(instr.imm) {
|
||||||
|
Some(b) => {
|
||||||
|
push(Object, none);
|
||||||
|
push(GetLoc, loc(b));
|
||||||
|
push(GetField, value);
|
||||||
|
push(DefineField, value);
|
||||||
|
push(PutLoc, loc(b));
|
||||||
|
}
|
||||||
|
None => push(Nop, none),
|
||||||
|
}
|
||||||
|
} else if matches!(instr.op, Fclosure8 | Fclosure) {
|
||||||
|
let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
push(Fclosure, instr.imm);
|
||||||
|
|
||||||
|
for (i, c) in captures[child].iter().enumerate() {
|
||||||
|
match c.flags.closure_kind {
|
||||||
|
ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])),
|
||||||
|
ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])),
|
||||||
|
ClosureKind::Ref => {
|
||||||
|
push(GetArg, ImmOperand::Arg(ENV_ARG));
|
||||||
|
push(GetField, slot(c.var_idx));
|
||||||
|
}
|
||||||
|
_ => push(Object, none),
|
||||||
|
}
|
||||||
|
|
||||||
|
push(DefineField, slot(i));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
push(instr.op, instr.imm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(out, new_var_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes,
|
||||||
|
/// and whether a write also leaves the value on the stack
|
||||||
|
fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
|
||||||
|
use OpCode::*;
|
||||||
|
|
||||||
|
let i = match instr.imm {
|
||||||
|
ImmOperand::VarRef(i) => i as usize,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
match instr.op {
|
||||||
|
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)),
|
||||||
|
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)),
|
||||||
|
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)),
|
||||||
|
GetVarRef | GetVarRefCheck => Some((i, false, false)),
|
||||||
|
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)),
|
||||||
|
SetVarRef => Some((i, true, true)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────── Arg Access Processing ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`,
|
||||||
|
/// and `this` with a read of its param
|
||||||
|
fn replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> bool {
|
||||||
|
use OpCode::*;
|
||||||
|
|
||||||
|
let (op, arg) = match (instr.op, instr.imm) {
|
||||||
|
(PushThis, _) => (GetArg, THIS_ARG),
|
||||||
|
(GetArg0, _) => (GetArg, FIRST_ARG),
|
||||||
|
(GetArg1, _) => (GetArg, FIRST_ARG + 1),
|
||||||
|
(GetArg2, _) => (GetArg, FIRST_ARG + 2),
|
||||||
|
(GetArg3, _) => (GetArg, FIRST_ARG + 3),
|
||||||
|
(PutArg0, _) => (PutArg, FIRST_ARG),
|
||||||
|
(PutArg1, _) => (PutArg, FIRST_ARG + 1),
|
||||||
|
(PutArg2, _) => (PutArg, FIRST_ARG + 2),
|
||||||
|
(PutArg3, _) => (PutArg, FIRST_ARG + 3),
|
||||||
|
(SetArg0, _) => (SetArg, FIRST_ARG),
|
||||||
|
(SetArg1, _) => (SetArg, FIRST_ARG + 1),
|
||||||
|
(SetArg2, _) => (SetArg, FIRST_ARG + 2),
|
||||||
|
(SetArg3, _) => (SetArg, FIRST_ARG + 3),
|
||||||
|
(GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k),
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pusher(instr.pc, out)(op, ImmOperand::Arg(arg));
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────────────────────────────────────────── Misc Expansion ──────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────── 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::*;
|
use OpCode::*;
|
||||||
|
|
||||||
let ins = |op, imm| Instr {
|
let none = ImmOperand::None;
|
||||||
pc: instr.pc,
|
let imm = instr.imm;
|
||||||
op,
|
|
||||||
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 {
|
let mut push = pusher(instr.pc, out);
|
||||||
pc: instr.pc,
|
seq.iter().for_each(|&(op, imm)| push(op, imm));
|
||||||
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);
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace a read of a global with the constant it is (see `native`)
|
||||||
fn replace_get_var(
|
fn replace_get_var(
|
||||||
instr: &Instr,
|
instr: &Instr,
|
||||||
atoms: &[Atom],
|
atoms: &[Atom],
|
||||||
@@ -359,18 +442,7 @@ fn replace_get_var(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ImmOperand::Atom(name) = instr.imm else {
|
cpool.push(native::global(instr.imm.atom(), atoms));
|
||||||
return false;
|
pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1));
|
||||||
};
|
|
||||||
|
|
||||||
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),
|
|
||||||
});
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
+176
@@ -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
@@ -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
@@ -1,9 +1,264 @@
|
|||||||
const Object = {
|
// The stdlib, compiled in front of every program
|
||||||
create(proto) { return { __proto__: proto }; },
|
//
|
||||||
|
// 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 = {
|
const console = {
|
||||||
// TODO: all __native_* fns should be monomorphic, console.log() should act a bit like Rust's write!/print!
|
log(...args) {
|
||||||
// and break down complex printing in JS-land
|
let line = "";
|
||||||
log(x) { __native_print(x); },
|
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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user