Broken site based impl

This commit is contained in:
2026-10-03 14:06:24 +00:00
parent 23a2c3bc46
commit 44498b32a9
39 changed files with 4250 additions and 1750 deletions
+3
View File
@@ -1,3 +1,6 @@
[submodule "third_party/quickjs"]
path = third_party/quickjs
url = https://github.com/quickjs-ng/quickjs.git
[submodule "third_party/stb"]
path = third_party/stb
url = https://github.com/nothings/stb.git
+1
View File
@@ -4,5 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
cc = "1.5.1"
nom = "8.0.0"
num_enum = "0.7.6"
+4
View File
@@ -1,6 +1,10 @@
const QUICKJS: &str = "../third_party/quickjs/build";
fn main() {
// The compiler compiles the C it emits for the target it is built for (see
// main.rs)
println!("cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap());
println!("cargo:rustc-link-search=native={QUICKJS}");
println!("cargo:rustc-link-lib=static=qjs");
+6 -4
View File
@@ -12,13 +12,13 @@ pub enum Escape {
Escapes,
}
/// The escape of each value that `object` or `fclosure` gives. Every other
/// The escape of each value that `object`, `fclosure` or `array_from` gives. Every other
/// value gives `Escapes`, which nothing reads
pub fn analyze(f: &RegFn, flow: &mut Flow) -> Vec<Escape> {
let mut escape = vec![Escape::Escapes; f.val_count];
for (block, instr) in instrs(f) {
if matches!(instr.op, Object | Fclosure) {
if matches!(instr.op, Object | Fclosure | ArrayFrom) {
let cell = instr.dsts[0];
flow.new_walk();
@@ -34,7 +34,8 @@ pub fn analyze(f: &RegFn, flow: &mut Flow) -> Vec<Escape> {
/// Whether the cell that `v` holds can be reached after the frame returns, or
/// when its creating op runs again (`looped`: the op is in a loop). A var or a
/// join slot can keep the cell until the next run, so in a loop any copy
/// escapes. A field, a callee and a return can keep it beyond the frame
/// escapes. A field, an element, a callee and a return can keep it beyond the
/// frame
fn escapes(flow: &mut Flow, v: usize, looped: bool) -> bool {
flow.visit(v);
@@ -43,7 +44,8 @@ fn escapes(flow: &mut Flow, v: usize, looped: bool) -> bool {
let out = match u.op {
Call | CallMethod | CallConstructor | Return | ReturnAsync => true,
DefineField | PutField if u.operand == 0 => true,
DefineField | PutField | PutArrayEl if u.operand == 0 => true,
ArrayFrom => true,
SetLoc | SetName if looped => true,
SetLoc | SetName => {
let dst = u.dst.unwrap() as usize;
+696 -336
View File
File diff suppressed because it is too large Load Diff
+314 -258
View File
@@ -1,28 +1,30 @@
//! Infers a type for every value (see `docs/model.md`, section 3)
pub mod sites;
pub mod types;
use std::collections::HashMap;
use crate::{
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native,
native::{self, ProtoOf},
regify::RegFn,
transform::{FIRST_ARG, THIS_ARG},
};
use types::{Code, Heap, Number, Site, Type};
use sites::{Code, CtxId, LENGTH_FIELD, Place, Protos, SiteId, SiteTable};
use types::{BaseType, EMPTY_STRING, Type, is_function, loose_eq, strict_eq, test, truthiness, typeof_name};
const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
/// One walk of a fn with one tuple of arg types (see `docs/model.md`,
/// section 3.4)
/// section 3.5)
#[derive(Debug)]
pub struct Variant {
pub fn_id: usize,
/// One for each of the fn's params (see `TypeCache::variant()`)
pub arg_types: Vec<Type>,
/// The context of the sites that it makes (see `SiteTable::context()`)
pub ctx: CtxId,
/// The walk in the current round, once it is done
pub result: Option<InferResult>,
/// What it returned in the rounds so far, which is what a call of it gives
@@ -48,9 +50,9 @@ pub struct TypeCache {
by_args: Vec<HashMap<Vec<Type>, usize>>,
/// Scratch for a call's arg types
key: Vec<Type>,
pub heap: Heap,
pub site_table: SiteTable,
/// The prototype of each primitive kind, as the stdlib links it, by kind
pub protos: [Type; 3],
pub protos: Protos,
/// Goes up when a variant's return or a proto changes (see `infer_types()`)
changes: usize,
}
@@ -62,8 +64,9 @@ impl TypeCache {
/// arg's kind crosses a call, not a known value, else every literal passed
/// would make a new variant
pub fn variant(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
let site_table = &mut self.site_table;
self.key.clear();
self.key.extend(params(fn_id, arg_types, fns));
self.key.extend(params(fn_id, arg_types, fns).map(|ty| site_table.widen(&ty)));
if let Some(&v) = self.by_args[fn_id].get(&self.key[..]) {
return v;
@@ -74,8 +77,9 @@ impl TypeCache {
self.variants.push(Variant {
fn_id,
arg_types: self.key.clone(),
ctx: self.site_table.context(&self.key),
result: None,
ret: Type::EMPTY,
ret: Type::Unknown,
walking: false,
});
@@ -84,10 +88,47 @@ impl TypeCache {
/// The variant for a call that infer walked
pub fn find(&self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> usize {
let key: Vec<Type> = params(fn_id, arg_types, fns).collect();
let site_table = &self.site_table;
let key: Vec<Type> = params(fn_id, arg_types, fns).map(|ty| site_table.widened(&ty)).collect();
self.by_args[fn_id][&key]
}
/// The join of `f` over the members of `ty` (see `SiteTable::members()`).
/// The lists of sums only grow, so a member stays where it is while `f`
/// adds sums
fn map(&mut self, ty: Type, mut f: impl FnMut(&mut Self, Type) -> Type) -> Type {
let mut out = Type::Unknown;
for i in 0..self.site_table.members(&ty).len() {
let member = self.site_table.members(&ty)[i];
let ty = f(self, member);
out = self.site_table.join(&out, &ty);
}
out
}
/// The join of `f` over each pair of members of `a` and `b`
fn map2(&mut self, a: Type, b: Type, mut f: impl FnMut(&mut Self, Type, Type) -> Type) -> Type {
self.map(a, |cache, a| cache.map(b, |cache, b| f(cache, a, b)))
}
/// What `f` gives for every member of `ty`, if all members give the same
fn agree<T: PartialEq>(&self, ty: &Type, f: impl Fn(&Type) -> Option<T>) -> Option<T> {
let mut members = self.site_table.members(ty).iter().map(f);
let first = members.next()??;
members.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// What `f` gives for every pair of members of `a` and `b`, if all pairs
/// give the same
fn agree2<T: PartialEq>(&self, a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option<T>) -> Option<T> {
let members = |t| self.site_table.members(t).iter();
let mut pairs = members(a).flat_map(|x| members(b).map(move |y| (x, y))).map(|(x, y)| f(x, y));
let first = pairs.next()??;
pairs.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// Infer a call of `fn_id`, walking it unless an identical call was
/// already walked in this round. Gives what it returns
fn call(&mut self, fn_id: usize, arg_types: &[Type], fns: &[RegFn]) -> Type {
@@ -104,7 +145,7 @@ impl TypeCache {
let ret = res.ret_type;
let last = self.variants[v].ret;
let joined = self.heap.join(&last, &ret);
let joined = self.site_table.join(&last, &ret);
self.changes += (joined != last) as usize;
let variant = &mut self.variants[v];
@@ -116,9 +157,73 @@ impl TypeCache {
}
/// The type of each of fn `fn_id`'s params, for a call with args of these
/// types (see `TypeCache::variant()`)
/// types, before known values are dropped (see `TypeCache::variant()`)
fn params<'a>(fn_id: usize, arg_types: &'a [Type], fns: &[RegFn]) -> impl Iterator<Item = Type> + 'a {
(0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::UNDEFINED, Type::widen))
(0..fns[fn_id].arg_count).map(|i| arg_types.get(i).copied().unwrap_or(Type::Undefined))
}
/// A call of `code` with operands of these types (see the call rule in
/// `walk_fn()`). Gives what it returns
fn call_code(cache: &mut TypeCache, code: Code, arg_types: &[Type], fns: &[RegFn]) -> Type {
let args = &arg_types[FIRST_ARG as usize..];
match code {
Code::Fn(fn_id) => cache.call(fn_id, arg_types, fns),
Code::Native(id) => native::ret_type(id, args, &cache.site_table),
Code::ProtoLink(kind) => {
let linked = cache.protos[kind as usize];
let joined = cache.site_table.join(&linked, args.first().unwrap_or(&Type::Undefined));
cache.changes += (joined != linked) as usize;
cache.protos[kind as usize] = joined;
Type::Undefined
}
}
}
/// A call of a callee of type `Any`, with operands of these types: a call of
/// any fn object in an `Any`, with every operand an `Any`, except that the
/// callee is its own env. That is the variant that emit's dispatch on `Any`
/// calls (see `call_any_variant()`). Gives an `Any`
fn call_any(cache: &mut TypeCache, arg_types: &mut [Type], fns: &[RegFn]) -> Type {
for ty in arg_types.iter_mut() {
*ty = cache.site_table.to_any(ty);
}
for site in 0..cache.site_table.sites.len() as SiteId {
let Some(code) = cache.site_table.site(site).code().filter(|_| cache.site_table.site(site).in_any) else {
continue;
};
arg_types[0] = Type::Obj(site);
let ret = call_code(cache, code, arg_types, fns);
cache.site_table.to_any(&ret);
}
Type::Any
}
/// The site that `place` makes in context `ctx`, made if it is new
fn site_at(cache: &mut TypeCache, place: Place, ctx: CtxId) -> SiteId {
let place = cache.site_table.place_id(place);
cache.site_table.site_at(place, ctx)
}
/// An array index must be a number. Any other key is a computed field name,
/// which is not supported
fn check_index(cache: &TypeCache, index: &Type) {
for member in cache.site_table.members(index) {
if !matches!(member, Type::Int | Type::Float) {
unimplemented!("`a[k]` with a key of type {member:?}: computed field names");
}
}
}
/// The operand types of the variant that a call on `Any`, with `n_args`
/// args, runs for a fn object of `site`
pub fn call_any_variant(site: SiteId, n_args: usize) -> Vec<Type> {
let mut arg_types = vec![Type::Any; FIRST_ARG as usize + n_args];
arg_types[0] = Type::Obj(site);
arg_types
}
/// Walk every variant from the entry, in rounds, until a round changes
@@ -130,20 +235,20 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache {
variants: vec![],
by_args: fns.iter().map(|_| HashMap::new()).collect(),
key: vec![],
heap: Heap::new(fns),
protos: [Type::EMPTY; 3],
site_table: SiteTable::new(fns),
protos: [Type::Unknown; ProtoOf::ALL.len()],
changes: 0,
};
loop {
let before = (cache.heap.version, cache.changes);
let before = (cache.site_table.version, cache.changes);
cache.variants.iter_mut().for_each(|v| v.result = None);
// The synthetic program entry is appended last by `flatten`. It is
// called by the host with no args
cache.call(fns.len() - 1, &[], fns);
if before == (cache.heap.version, cache.changes) {
if before == (cache.site_table.version, cache.changes) {
return cache;
}
}
@@ -152,11 +257,11 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache {
fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
use OpCode::*;
let fn_id = cache.variants[variant].fn_id;
let (fn_id, ctx) = (cache.variants[variant].fn_id, cache.variants[variant].ctx);
let f = &reg_fns[fn_id];
let mut val_types = vec![Type::EMPTY; f.val_count];
let mut instr_types = vec![Type::EMPTY; f.code.len()];
let mut ret_type = Type::EMPTY;
let mut val_types = vec![Type::Unknown; f.val_count];
let mut instr_types = vec![Type::Unknown; f.code.len()];
let mut ret_type = Type::Unknown;
// arg `i` is value id `var_count + i` (see regify)
// Init them with the arg types passed in
@@ -170,13 +275,13 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
// Scratch for calls, kept across instrs
let mut arg_types: Vec<Type> = vec![];
let mut codes: Vec<Code> = vec![];
// Walk instrs till nothing changes. A return only changes `ret_type` once
// its block is reachable, and a field read only gives more once the heap
// changes, so the values, the blocks and the heap are enough to watch
// its block is reachable, and a field read only gives more once the site
// table changes, so the values, the blocks and the site table are enough
// to watch
loop {
let version = cache.heap.version;
let version = cache.site_table.version;
let mut changed = false;
for (block_idx, block) in f.blocks.iter().enumerate() {
@@ -190,9 +295,9 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
for (offset, instr) in code.enumerate() {
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
// A rule with an empty operand gives an empty result: no value
// has reached the instr yet
let waits = instr.args.iter().any(|&v| val_types[v].is_empty());
// A rule with an `Unknown` operand gives `Unknown`: no value has
// reached the instr yet
let waits = instr.args.iter().any(|&v| val_types[v] == Type::Unknown);
if (instr.dsts.is_empty() && !is_ret) || waits {
continue;
@@ -205,162 +310,231 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
let ty = match instr.op {
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7
| PushI8 | PushI16 | PushI32 => Type::number(Number::Int),
| PushI8 | PushI16 | PushI32 => Type::Int,
PushTrue => Type::bool(Some(true)),
PushFalse => Type::bool(Some(false)),
PushAtomValue => Type::string(Some(instr.imm.atom())),
PushEmptyString => Type::string(Some(EMPTY_STRING)),
PushTrue => Type::Bool(Some(true)),
PushFalse => Type::Bool(Some(false)),
PushAtomValue => Type::Str(Some(instr.imm.atom())),
PushEmptyString => Type::Str(Some(EMPTY_STRING)),
PushConst | PushConst8 => match f.cpool[instr.imm.const_idx()] {
JsValue::Int(_) => Type::number(Number::Int),
JsValue::Float(_) => Type::number(Number::Float),
JsValue::String(_) => Type::string(None),
JsValue::BoolTrue => Type::bool(Some(true)),
JsValue::BoolFalse => Type::bool(Some(false)),
JsValue::Undefined => Type::UNDEFINED,
JsValue::NativeFn(id) => cache.heap.create(cache.heap.builtin(Site::Native(id))),
JsValue::ProtoLink(kind) => cache.heap.create(cache.heap.builtin(Site::ProtoLink(kind))),
JsValue::Int(_) => Type::Int,
JsValue::Float(_) => Type::Float,
JsValue::String(_) => Type::Str(None),
JsValue::BoolTrue => Type::Bool(Some(true)),
JsValue::BoolFalse => Type::Bool(Some(false)),
JsValue::Undefined => Type::Undefined,
JsValue::NativeFn(id) => cache.site_table.create(cache.site_table.builtin(Place::Native(id))),
JsValue::ProtoLink(kind) => cache.site_table.create(cache.site_table.builtin(Place::ProtoLink(kind))),
_ => unreachable!(),
},
Undefined => Type::UNDEFINED,
Null => Type::NULL,
Undefined => Type::Undefined,
Null => Type::Null,
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
// `+` concatenates if either side becomes a string, which
// only a string or an object can
Add => {
let (l, r) = (arg(1), arg(0));
let is_string = |t: Type| t.is_only(Type::string(None));
let can_be_string = |t: Type| t.string.is_some() || t.object.is_some();
if is_number(l) && is_number(r) {
Type::number(l.number.max(r.number).unwrap())
} else if is_string(l) || is_string(r) {
Type::string(None)
} else if can_be_string(l) || can_be_string(r) {
Type { string: Some(None), ..Type::number(Number::Float) }
} else {
Type::number(Number::Float)
}
}
// a string or an object does. Else it adds numbers
Add => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) {
(Type::Any, _) | (_, Type::Any) => Type::Any,
(l, r) if [l, r].iter().any(|t| matches!(t, Type::Str(_) | Type::Obj(_))) => Type::Str(None),
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}),
// Any other operator converts its operands to numbers
Sub | Mul | Mod | Pow => match (arg(1), arg(0)) {
(l, r) if is_int(l) && is_int(r) => Type::number(Number::Int),
_ => Type::number(Number::Float),
},
Neg | Plus | Inc | Dec | PostInc | PostDec => match arg(0) {
t if is_int(t) => Type::number(Number::Int),
_ => Type::number(Number::Float),
},
Div => Type::number(Number::Float),
Sub | Mul | Mod | Pow => cache.map2(arg(1), arg(0), |_, l, r| match (l, r) {
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}),
Neg | Plus | Inc | Dec | PostInc | PostDec => cache.map(arg(0), |_, t| match t {
Type::Int => Type::Int,
_ => Type::Float,
}),
Div => Type::Float,
// Bitwise NOT gives an int32
Not => Type::number(Number::Int),
Not => Type::Int,
Lt | Lte | Gt | Gte => Type::bool(None),
Lt | Lte | Gt | Gte => Type::Bool(None),
// ─────────────────────────────── Conditions the types may decide ──────────────────────────────────────────
// A known value lets infer skip the branch that cannot run
// (see `reachable` below)
// An identity test of two objects (see `docs/model.md`, section 5)
StrictEq | StrictNeq | Eq | Neq if arg(0).object.is_some() && arg(1).object.is_some() => {
for obj in [arg(0).object, arg(1).object].into_iter().flatten() {
cache.heap.mark(obj, |shape| &mut shape.compared);
StrictEq | StrictNeq | Eq | Neq => {
let (a, b) = (arg(0), arg(1));
// An identity test of an object with what can be an
// object (see `docs/model.md`, section 5)
let members = |t: &Type| cache.site_table.members(t).to_vec();
let (a_members, b_members) = (members(&a), members(&b));
let can_be_object = |ms: &[Type]| ms.iter().any(|t| matches!(t, Type::Obj(_) | Type::Any));
if can_be_object(&a_members) && can_be_object(&b_members) {
for t in a_members.iter().chain(&b_members) {
if let Type::Obj(site) = t {
cache.site_table.mark(*site, |shape| &mut shape.compared);
}
}
}
Type::bool(None)
let eq = match instr.op {
StrictEq | StrictNeq => cache.agree2(&a, &b, strict_eq),
_ => cache.agree2(&a, &b, loose_eq),
};
Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq)))
}
StrictEq => Type::bool(strict_eq(&arg(0), &arg(1))),
StrictNeq => Type::bool(strict_eq(&arg(0), &arg(1)).map(|eq| !eq)),
Eq => Type::bool(loose_eq(&arg(0), &arg(1))),
Neq => Type::bool(loose_eq(&arg(0), &arg(1)).map(|eq| !eq)),
Lnot => Type::bool(truthiness(&arg(0)).map(|t| !t)),
Typeof => typeof_of(&arg(0), &cache.heap),
IsUndefined | TypeofIsUndefined => Type::bool(test(&arg(0), Type::UNDEFINED)),
IsNull => Type::bool(test(&arg(0), Type::NULL)),
IsUndefinedOrNull => Type::bool(test(&arg(0), Type { null: true, ..Type::UNDEFINED })),
TypeofIsFunction => Type::bool(is_function(&arg(0), &cache.heap)),
Lnot => Type::Bool(cache.agree(&arg(0), truthiness).map(|t| !t)),
Typeof => match cache.agree(&arg(0), |t| typeof_name(t, &cache.site_table)) {
Some(name) => Type::Str(Some(AtomRef::Predefined(name))),
None => Type::Str(None),
},
IsUndefined | TypeofIsUndefined => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined]))),
IsNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Null]))),
IsUndefinedOrNull => Type::Bool(cache.agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))),
TypeofIsFunction => Type::Bool(cache.agree(&arg(0), |t| is_function(t, &cache.site_table))),
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
SetLoc | SetName => arg(0),
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
Object | Fclosure | Fclosure8 => cache.heap.create(cache.heap.site_at(fn_id, idx).unwrap()),
// The write goes into the object's shape, which every value
// that holds the object sees. A primitive has no fields to
// write, and undefined and null throw
Object | Fclosure | Fclosure8 => {
let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx);
cache.site_table.create(site)
}
// The elements are its operands, last first
ArrayFrom => {
let site = site_at(cache, Place::Op { fn_id, instr: idx }, ctx);
for &v in &instr.args {
cache.site_table.write_elem(site, &val_types[v]);
}
cache.site_table.create(site)
}
// `a[i]` and `a[i] = x`, on arrays, and `s[i]` on strings:
// an index is a number. Other objects need computed field
// names, which are not supported. A string cannot be
// written (module code is strict), and undefined and null
// throw
GetArrayEl => {
let index = arg(0);
cache.map(arg(1), |cache, obj| match obj {
Type::Obj(site) if cache.site_table.site(site).is_array() => {
check_index(cache, &index);
cache.site_table.read_elem(site)
}
Type::Str(_) => {
check_index(cache, &index);
cache.site_table.join(&Type::Str(None), &Type::Undefined)
}
Type::Undefined | Type::Null => Type::Unknown,
obj => unimplemented!("`o[k]` on {obj:?}: computed field names"),
})
}
PutArrayEl => {
let (value, index) = (arg(0), arg(1));
cache.map(arg(2), |cache, obj| {
match obj {
Type::Obj(site) if cache.site_table.site(site).is_array() => {
check_index(cache, &index);
cache.site_table.write_elem(site, &value);
cache.site_table.mark(site, |s| &mut s.written);
}
Type::Undefined | Type::Null | Type::Str(_) => {}
obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"),
}
Type::Unknown
});
arg(2)
}
// A key stays as it is: a number is an index
ToPropkey => arg(0),
// The write goes into the fields of the object's site, which
// every value that holds an object of the site sees. A write
// on `Any` can reach any object in an `Any`. A primitive has
// no fields to write, and undefined and null throw
DefineField | PutField => {
if let Some(obj) = arg(1).object {
cache.heap.write(obj, instr.imm.atom(), &arg(0));
let (field, value) = (instr.imm.atom(), arg(0));
// A define_field builds the object before it goes
// anywhere (see regify)
let creation = instr.op == DefineField;
if !cache.heap.is_creation_write(fn_id, idx) {
cache.heap.mark(obj, |shape| &mut shape.written);
cache.map(arg(1), |cache, obj| {
match obj {
Type::Obj(site) => {
cache.site_table.write(site, field, &value);
if !creation {
cache.site_table.mark(site, |shape| &mut shape.written);
}
}
Type::Any => {
for site in 0..cache.site_table.sites.len() as SiteId {
if cache.site_table.site(site).in_any {
cache.site_table.write(site, field, &value);
cache.site_table.mark(site, |shape| &mut shape.written);
}
}
}
_ => {}
}
cache.heap.canon(&arg(1))
Type::Unknown
});
arg(1)
}
GetField => cache.heap.read_value(&arg(0), instr.imm.atom(), &cache.protos),
GetField => cache.site_table.read_value(&arg(0), instr.imm.atom(), &cache.protos),
// A string's length is its own. Anything else reads a
// `length` field
GetLength => {
let others = Type { string: None, ..arg(0) };
let read = cache.heap.read_value(&others, LENGTH_FIELD, &cache.protos);
let own = Type { number: arg(0).string.map(|_| Number::Int), ..Type::EMPTY };
cache.heap.join(&read, &own)
}
GetLength => cache.map(arg(0), |cache, obj| match obj {
Type::Str(_) => Type::Int,
obj => cache.site_table.read_value(&obj, LENGTH_FIELD, &cache.protos),
}),
// ───────────────────────────────────── Function Calls ──────────────────────────────────────────────────────
// Operands are the callee's params: the callee (its env),
// `this`, then the args (see regify). The call runs one of
// the callee's fns. A callee that is not a fn throws, so it
// gives nothing
// `this`, then the args (see regify). A callee that is not
// a fn throws, so it gives nothing
// The callee's env is the fn object itself, so a call of
// each member of a sum passes that member as the env
Call | CallMethod | CallConstructor => {
let this = arg(THIS_ARG as usize);
let ret = cache.map(arg(0), |cache, callee| {
arg_types.clear();
arg_types.extend(instr.args.iter().map(|&v| val_types[v]));
let args = &arg_types[FIRST_ARG as usize..];
arg_types[0] = callee;
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);
match callee {
Type::Obj(site) => match cache.site_table.site(site).code() {
Some(code) => call_code(cache, code, &arg_types, reg_fns),
None => Type::Unknown,
},
Type::Any => call_any(cache, &mut arg_types, reg_fns),
_ => Type::Unknown,
}
});
// `new` gives what the constructor returns if that is an
// object, else the new object itself, `this`
let others = Type { object: None, ..ret };
match instr.op {
CallConstructor if !others.is_empty() => {
let this = val_types[instr.args[THIS_ARG as usize]];
cache.heap.join(&Type { object: ret.object, ..Type::EMPTY }, &this)
}
CallConstructor => cache.map(ret, |cache, ret| match ret {
Type::Obj(_) => ret,
Type::Any => cache.site_table.join(&ret, &this),
_ => this,
}),
_ => ret,
}
}
// ──────────────────────────────────────── Returns ──────────────────────────────────────────────────────────
Return | ReturnAsync => arg(0),
ReturnUndef => Type::UNDEFINED,
ReturnUndef => Type::Undefined,
_ => unreachable!("{instr:?}"),
};
@@ -368,16 +542,16 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
// Postfix copies come after the block's own instrs, and have no
// entry: a copy gives the type of what it copies
if idx < block.end {
instr_types[idx] = cache.heap.join(&instr_types[idx], &ty);
instr_types[idx] = cache.site_table.join(&instr_types[idx], &ty);
}
if is_ret {
ret_type = cache.heap.join(&ret_type, &ty);
ret_type = cache.site_table.join(&ret_type, &ty);
continue;
}
for &dst in &instr.dsts {
let joined = cache.heap.join(&val_types[dst], &ty);
let joined = cache.site_table.join(&val_types[dst], &ty);
changed |= joined != val_types[dst];
val_types[dst] = joined;
}
@@ -390,8 +564,8 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
let jumps = match f.code[block.start..block.end].last() {
Some(last) if matches!(last.op, IfFalse | IfFalse8 | IfTrue | IfTrue8) => {
match val_types[last.args[0]] {
cond if cond.is_empty() => continue,
cond => truthiness(&cond).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
Type::Unknown => continue,
cond => cache.agree(&cond, truthiness).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
}
}
_ => None,
@@ -406,17 +580,11 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
}
} // block
if !changed && cache.heap.version == version {
if !changed && cache.site_table.version == version {
break;
}
} // loop
// A type names the site its shape was kept under at the time: make it the
// one now
for ty in val_types.iter_mut().chain(&mut instr_types).chain([&mut ret_type]) {
*ty = cache.heap.canon(ty);
}
InferResult {
val_types,
instr_types,
@@ -425,115 +593,3 @@ fn walk_fn(variant: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferRes
}
}
/// Whether every value of type `ty` is a number
fn is_number(ty: Type) -> bool {
ty.is_only(Type::number(Number::Int))
}
/// Whether every value of type `ty` is an int32
fn is_int(ty: Type) -> bool {
ty == Type::number(Number::Int)
}
/// The type, if every value of it is one value that its kind and refinement
/// tell
fn single(ty: &Type) -> Option<Type> {
let one = match ty.kinds() {
[true, false, false, false, false, false] | [false, true, false, false, false, false] => true,
[false, false, true, false, false, false] => ty.bool.flatten().is_some(),
[false, false, false, false, true, false] => ty.string.flatten().is_some(),
_ => false,
};
one.then_some(*ty)
}
/// Whether `a === b`, if the types decide it
fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
let disjoint = !a.kinds().iter().zip(b.kinds()).any(|(x, y)| *x && y);
match (single(a), single(b)) {
_ if disjoint => Some(false),
(Some(x), Some(y)) => Some(x == y),
_ => None,
}
}
/// Whether `a == b`, if the types decide it. Undefined and null are equal to
/// each other and to nothing else. Two values of one kind compare as `===`
/// does
fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
let nullish = |t: &Type| t.has_nullish() && t.non_nullish().is_empty();
match (a, b) {
_ if nullish(a) && nullish(b) => Some(true),
_ if (nullish(a) && !b.has_nullish()) || (!a.has_nullish() && nullish(b)) => Some(false),
_ if a.kinds() == b.kinds() && single(a).is_some() => strict_eq(a, b),
_ => None,
}
}
/// Whether every value of type `ty` is of a kind in `kinds`: true if every
/// value is, false if none is, `None` if the type does not decide it
fn test(ty: &Type, kinds: Type) -> Option<bool> {
let [inside, outside] = [true, false].map(|want| {
ty.kinds().iter().zip(kinds.kinds()).any(|(has, of)| *has && of == want)
});
match (inside, outside) {
(true, false) => Some(true),
(false, true) => Some(false),
_ => None,
}
}
/// Whether every value of type `ty` is a fn, if the type decides it
fn is_function(ty: &Type, heap: &Heap) -> Option<bool> {
let shape = ty.object.map(|site| heap.shape(site));
let fns = shape.is_some_and(|shape| !shape.code.is_empty());
let others = ty.kinds()[..5].contains(&true) || shape.is_some_and(|shape| shape.plain);
match (fns, others) {
(true, false) => Some(true),
(false, _) => Some(false),
_ => None,
}
}
/// Whether a value of type `ty` is truthy, if the type decides it
fn truthiness(ty: &Type) -> Option<bool> {
let truthy = [
ty.undefined.then_some(Some(false)),
ty.null.then_some(Some(false)),
ty.bool,
ty.number.map(|_| None),
ty.string.map(|s| s.map(|atom| atom != EMPTY_STRING)),
ty.object.map(|_| Some(true)),
];
let mut kinds = truthy.into_iter().flatten();
let first = kinds.next()?;
kinds.all(|t| t == first).then_some(first).flatten()
}
/// What `typeof` gives for a value of type `ty`
fn typeof_of(ty: &Type, heap: &Heap) -> Type {
let shape = ty.object.map(|site| heap.shape(site));
let names = [
ty.undefined.then_some(73), // "undefined"
ty.null.then_some(77), // "object"
ty.bool.map(|_| 75), // "boolean"
ty.number.map(|_| 74), // "number"
ty.string.map(|_| 76), // "string"
shape.filter(|shape| shape.plain).map(|_| 77), // "object"
shape.filter(|shape| !shape.code.is_empty()).map(|_| 27), // "function"
];
let mut names = names.into_iter().flatten();
match names.next() {
Some(name) if names.all(|n| n == name) => Type::string(Some(AtomRef::Predefined(name))),
_ => Type::string(None),
}
}
+498
View File
@@ -0,0 +1,498 @@
//! What infer knows about the objects of each site: the type of each field,
//! and flags (see `docs/model.md`, section 3.3)
use crate::{
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native::{NATIVES, ProtoOf},
regify::RegFn,
transform::{BOX_FIELD, PROTO_FIELD},
};
use super::types::Type;
pub const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
/// The prototype of each `ProtoOf`, as the stdlib links it
pub type Protos = [Type; ProtoOf::ALL.len()];
/// Where an object is made: the op that creates it, or a built-in fn. Places
/// are numbered in this order (see `SiteTable::new()`)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Place {
Op { fn_id: usize, instr: usize },
Native(usize),
ProtoLink(ProtoOf),
}
/// The number of a place: its index in `SiteTable::places`
pub type PlaceId = u32;
/// The number of a context: its index in `SiteTable::contexts`
pub type CtxId = u32;
/// The number of a site: its index in `SiteTable::sites`
pub type SiteId = u32;
/// The code a fn object runs
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Code {
Fn(usize),
Native(usize),
ProtoLink(ProtoOf),
}
/// A field of the objects of a site
#[derive(Debug, Clone, Copy)]
pub struct Field {
pub ty: Type,
pub defined: bool, // every object of the site gets it when it is made, so it is never absent
}
/// What the objects of a site are, besides objects with named fields
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjKind {
Plain,
Fn(Code), // a fn, which runs the code
Array { elems: Type }, // an array, whose elements are the values written to them, or holes
}
/// A place, and what every site of it starts with
#[derive(Debug, Clone)]
pub struct PlaceInfo {
pub place: Place,
kind: ObjKind, // the kind of its sites, before any element is written
defined: Vec<AtomRef>, // the fields that its creation writes write, sorted
sites: Vec<(CtxId, SiteId)>, // its sites so far, by context
}
/// A part of a context (see `SiteTable::context()`)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CtxPart {
Member(Type), // a member of a param's type that is not an object
Place(PlaceId), // an object member, by the place that makes it
End, // the end of a param's type
}
/// What infer knows about the objects of one site
#[derive(Debug, Clone)]
pub struct SiteInfo {
pub place: PlaceId,
pub ctx: CtxId,
pub kind: ObjKind,
pub fields: Vec<(AtomRef, Field)>, // sorted by name
pub written: bool, // a field of one of its objects is written after the object is made
pub compared: bool, // one of its objects is in an identity test with an object
pub in_any: bool, // one of its objects went into an `Any` (see `SiteTable::to_any()`)
pub live: bool, // an op that makes one of its objects has run (see infer)
}
impl SiteInfo {
/// The code that its objects run, if they are fns
pub fn code(&self) -> Option<Code> {
match self.kind {
ObjKind::Fn(code) => Some(code),
_ => None,
}
}
pub fn is_array(&self) -> bool {
matches!(self.kind, ObjKind::Array { .. })
}
pub fn field(&self, name: AtomRef) -> Option<&Field> {
let i = self.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?;
Some(&self.fields[i].1)
}
/// Whether a copy of one of its objects cannot be told from the object:
/// no write after creation can show the copy, and no identity test can
/// (see `docs/model.md`, section 5). An object in an `Any` value can be
/// written and compared by code that does not know its site
pub fn is_value(&self) -> bool {
!self.written && !self.compared && !self.in_any
}
}
/// Every place, every site, and every sum
///
/// A site is a place and a context: the objects that one place makes in the
/// variants that have one context. So the objects follow the monomorphization
/// of the fns that make them (see `docs/model.md`, section 3.3)
///
/// The sums are stored flat: the members of sum `i` are
/// `sum_members[sum_starts[i]..sum_starts[i + 1]]`. So a search over all sums
/// reads one array in order. The contexts are stored the same way
#[derive(Debug, Clone)]
pub struct SiteTable {
pub places: Vec<PlaceInfo>, // place id -> where it is, sorted by `place`
pub sites: Vec<SiteInfo>, // site id -> what infer knows about its objects
ctx_parts: Vec<CtxPart>, // context id -> its parts, see above
ctx_starts: Vec<u32>,
pub(super) sum_members: Vec<Type>, // sum id -> its members, see above
pub(super) sum_starts: Vec<u32>,
pub version: usize, // goes up on every change, so a walk can tell that a read may give more
}
impl SiteTable {
/// One place for each creating op, in the order of fns and instrs, then
/// the natives, then the proto links. A place starts its sites with the
/// fields that its creation writes write: every `define_field` writes into
/// the object that a creating op gives (see regify). The built-in fns have
/// one site each, in the empty context (context 0)
pub fn new(fns: &[RegFn]) -> Self {
let mut places: Vec<PlaceInfo> = vec![];
let place = |place, kind| PlaceInfo { place, kind, defined: vec![], sites: vec![] };
// The place that made each value of the fn, if a creating op gave it
let mut created: Vec<Option<usize>> = vec![];
for (fn_id, f) in fns.iter().enumerate() {
created.clear();
created.resize(f.val_count, None);
for (instr, op) in f.code.iter().enumerate() {
match op.op {
OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8 | OpCode::ArrayFrom => {
let kind = match op.op {
OpCode::Object => ObjKind::Plain,
OpCode::ArrayFrom => ObjKind::Array { elems: Type::Unknown },
_ => match f.cpool[op.imm.const_idx()] {
JsValue::FnById(id) => ObjKind::Fn(Code::Fn(id)),
_ => unreachable!(),
},
};
created[op.dsts[0]] = Some(places.len());
places.push(place(Place::Op { fn_id, instr }, kind));
}
OpCode::DefineField => {
let defined = &mut places[created[op.args[1]].unwrap()].defined;
if let Err(i) = defined.binary_search(&op.imm.atom()) {
defined.insert(i, op.imm.atom());
}
}
_ => {}
}
}
}
places.extend((0..NATIVES.len()).map(|id| place(Place::Native(id), ObjKind::Fn(Code::Native(id)))));
places.extend(ProtoOf::ALL.map(|kind| place(Place::ProtoLink(kind), ObjKind::Fn(Code::ProtoLink(kind)))));
let mut table = Self {
places,
sites: vec![],
ctx_parts: vec![],
ctx_starts: vec![0],
sum_members: vec![],
sum_starts: vec![0],
version: 0,
};
let empty = table.context(&[]);
for at in table.places.len() - NATIVES.len() - ProtoOf::ALL.len()..table.places.len() {
table.site_at(at as PlaceId, empty);
}
table
}
/// The place `place`
pub fn place_id(&self, place: Place) -> PlaceId {
self.places.binary_search_by_key(&place, |p| p.place).unwrap() as PlaceId
}
/// The site of a built-in fn
pub fn builtin(&self, place: Place) -> SiteId {
self.places[self.place_id(place) as usize].sites[0].1
}
/// The context of a variant whose params have these types: the types,
/// where each object member is named only by its place. So there are
/// finitely many contexts, although a site's objects can be in a param of
/// a variant whose own sites are new (recursion)
pub fn context(&mut self, params: &[Type]) -> CtxId {
let start = self.ctx_parts.len();
for ty in params {
let members_start = self.ctx_parts.len();
for i in 0..self.members(ty).len() {
let part = match self.members(ty)[i] {
Type::Obj(site) => CtxPart::Place(self.sites[site as usize].place),
member => CtxPart::Member(member),
};
if !self.ctx_parts[members_start..].contains(&part) {
self.ctx_parts.push(part);
}
}
self.ctx_parts.push(CtxPart::End);
}
let new = &self.ctx_parts[start..];
let starts = self.ctx_starts.windows(2);
match starts.map(|w| &self.ctx_parts[w[0] as usize..w[1] as usize]).position(|ctx| ctx == new) {
Some(id) => {
self.ctx_parts.truncate(start);
id as CtxId
}
None => {
self.ctx_starts.push(self.ctx_parts.len() as u32);
(self.ctx_starts.len() - 2) as CtxId
}
}
}
/// The site of `place` in context `ctx`, made if it is new
pub fn site_at(&mut self, place: PlaceId, ctx: CtxId) -> SiteId {
if let Some(site) = self.find_site(place, ctx) {
return site;
}
let id = self.sites.len() as SiteId;
let info = &mut self.places[place as usize];
let fields = info.defined.iter().map(|&n| (n, Field { ty: Type::Unknown, defined: true })).collect();
info.sites.push((ctx, id));
self.sites.push(SiteInfo {
place,
ctx,
kind: info.kind,
fields,
written: false,
compared: false,
in_any: false,
live: false,
});
self.version += 1;
id
}
/// The site of `place` in context `ctx`, if infer made it
pub fn find_site(&self, place: PlaceId, ctx: CtxId) -> Option<SiteId> {
let sites = &self.places[place as usize].sites;
sites.iter().find(|(c, _)| *c == ctx).map(|(_, site)| *site)
}
pub fn site(&self, site: SiteId) -> &SiteInfo {
&self.sites[site as usize]
}
/// An object of the site, which a creating op gives
pub fn create(&mut self, site: SiteId) -> Type {
self.mark(site, |s| &mut s.live);
Type::Obj(site)
}
/// Set a flag of `site`. Whether it was not set before
pub fn mark(&mut self, site: SiteId, flag: fn(&mut SiteInfo) -> &mut bool) -> bool {
let flag = flag(&mut self.sites[site as usize]);
if *flag {
return false;
}
*flag = true;
self.version += 1;
true
}
/// Whether an object of `site` can lack its own `field`. A box's value
/// never can: JS gives a binding a value before any read of it that does
/// not throw (see transform)
pub fn can_be_absent(&self, site: SiteId, field: AtomRef) -> bool {
field != BOX_FIELD && !self.site(site).field(field).is_some_and(|f| f.defined)
}
/// A value of type `ty` goes into an `Any` value. Code that holds the
/// `Any` does not know the site of an object in it, so the object's site
/// is marked, and so are the sites of the objects in its fields, which
/// that code can read
pub fn to_any(&mut self, ty: &Type) -> Type {
let mut stack = vec![];
self.push_sites(ty, &mut stack);
while let Some(site) = stack.pop() {
if self.mark(site, |s| &mut s.in_any) {
for i in 0..self.site(site).fields.len() {
let field = self.site(site).fields[i].1.ty;
self.push_sites(&field, &mut stack);
}
if let ObjKind::Array { elems } = self.site(site).kind {
self.push_sites(&elems, &mut stack);
}
}
}
Type::Any
}
/// Push the site of each object member of `ty`
fn push_sites(&self, ty: &Type, stack: &mut Vec<SiteId>) {
stack.extend(self.members(ty).iter().filter_map(|m| match m {
Type::Obj(site) => Some(*site),
_ => None,
}));
}
// ─────────────────────────────────────────── Fields ──────────────────────────────────────────────────
/// Join `ty` into `field` of the objects of `site`. A field that the site
/// did not have can be absent. An array's `length` is not a field: a write
/// of it sets the array's length (see `docs/model.md`, section 3.3)
pub fn write(&mut self, site: SiteId, field: AtomRef, ty: &Type) {
if field == LENGTH_FIELD && self.site(site).is_array() {
return;
}
let old = self.site(site).field(field).map_or(Type::Unknown, |f| f.ty);
let new = self.join(&old, ty);
// Code that holds one of its objects in an `Any` can read the field
if self.site(site).in_any {
self.to_any(&new);
}
let fields = &mut self.sites[site as usize].fields;
match fields.binary_search_by_key(&field, |(n, _)| *n) {
Ok(i) if fields[i].1.ty == new => return,
Ok(i) => fields[i].1.ty = new,
Err(i) => fields.insert(i, (field, Field { ty: new, defined: false })),
}
self.version += 1;
}
/// Join `ty` into the element type of the arrays of `site`
pub fn write_elem(&mut self, site: SiteId, ty: &Type) {
let ObjKind::Array { elems } = self.site(site).kind else {
unreachable!()
};
let new = self.join(&elems, ty);
// Code that holds one of its arrays in an `Any` can read the elements
if self.site(site).in_any {
self.to_any(&new);
}
if new != elems {
self.sites[site as usize].kind = ObjKind::Array { elems: new };
self.version += 1;
}
}
/// A read of an element of an array of `site`. An index can be a hole, or
/// past the end, which gives undefined
pub fn read_elem(&mut self, site: SiteId) -> Type {
let ObjKind::Array { elems } = self.site(site).kind else {
unreachable!()
};
self.join(&elems, &Type::Undefined)
}
/// A read of `field` on an object of `site`: its own field, then, where
/// that can be absent, the read on its prototype. At the end of the chain,
/// an absent field is undefined. An array's prototype is the one that the
/// stdlib links for arrays, in `protos`, and its `length` is not a field
pub fn read(&mut self, site: SiteId, field: AtomRef, protos: &Protos) -> Type {
self.read_chain(site, field, protos, self.sites.len())
}
/// `read()`, with at most `depth` more prototypes. Objects of one site can
/// be each other's prototypes, so the chain of sites can loop, which a
/// chain of objects cannot. A chain longer than the number of sites has
/// looped, and stops
fn read_chain(&mut self, site: SiteId, field: AtomRef, protos: &Protos, depth: usize) -> Type {
let array = self.site(site).is_array();
if array && field == LENGTH_FIELD {
return Type::Int;
}
let own = self.site(site).field(field).map_or(Type::Unknown, |f| f.ty);
if !self.can_be_absent(site, field) || depth == 0 {
return own;
}
// The chain ends at a prototype that is absent or not an object
let (proto, proto_absent) = match array {
true => (protos[ProtoOf::Array as usize], false),
false => {
let proto = self.site(site).field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty);
(proto, self.can_be_absent(site, PROTO_FIELD))
}
};
let mut ty = own;
if proto_absent || proto == Type::Unknown {
ty = self.join(&ty, &Type::Undefined);
}
for i in 0..self.members(&proto).len() {
let read = match self.members(&proto)[i] {
Type::Obj(proto) => self.read_chain(proto, field, protos, depth - 1),
Type::Any => Type::Any,
_ => Type::Undefined,
};
ty = self.join(&ty, &read);
}
ty
}
/// A read of `field` on a value of type `ty`. A primitive reads its kind's
/// prototype in `protos`. Undefined and null have no fields: the read
/// throws, so it gives nothing. A sum reads on each of its members
///
/// A read on `Any` can read any object in an `Any`, or any primitive. What
/// it reads goes into the `Any` that it gives
pub fn read_value(&mut self, ty: &Type, field: AtomRef, protos: &Protos) -> Type {
match *ty {
Type::Obj(site) => self.read(site, field, protos),
Type::Sum(_) => {
let mut read = Type::Unknown;
for i in 0..self.members(ty).len() {
let member = self.members(ty)[i];
let ty = self.read_value(&member, field, protos);
read = self.join(&read, &ty);
}
read
}
Type::Any => {
for site in 0..self.sites.len() as SiteId {
if self.site(site).in_any {
let read = self.read(site, field, protos);
self.to_any(&read);
}
}
for kind in ProtoOf::ALL {
if let Type::Obj(proto) = protos[kind as usize] {
let read = self.read(proto, field, protos);
self.to_any(&read);
}
}
Type::Any
}
ty => match ProtoOf::of(&ty).map(|kind| protos[kind as usize]) {
Some(Type::Unknown) | None => Type::Unknown,
Some(proto) => self.read_value(&proto, field, protos),
},
}
}
}
+233 -451
View File
@@ -1,493 +1,275 @@
//! Types and shapes (see `docs/model.md`, section 3)
//! Types: the values that a place can hold, their joins, and what a type
//! decides about the values in it (see `docs/model.md`, section 3.1)
use std::cell::Cell;
use crate::libqjs::parse::AtomRef;
use crate::{
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native::{NATIVES, Primitive},
regify::RegFn,
transform::{BOX_FIELD, PROTO_FIELD},
};
use super::sites::{SiteId, SiteTable};
/// Where an object comes from: the op that creates it, or a built-in fn
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Site {
Op { fn_id: usize, instr: usize },
Native(usize),
ProtoLink(Primitive),
}
pub(super) const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
/// The number of a site: its index in `Heap::sites`
pub type SiteId = u32;
/// The number of a sum (see `SiteTable::members()`)
pub type SumId = u32;
/// The code a fn object runs
/// A type is one of these. Join rules:
/// Unknown * T = T
/// Any * T = Any
/// T * T = T
/// Bool(a) * Bool(b) = Bool, the value no longer known. The same for Str
/// Int * Float = Float
/// T * U = T | U, a sum
///
/// A sum is a value of one of a list of types, its members. It has two or more
/// members, none of them `Unknown`, `Sum` or `Any`, sorted in the order of the
/// variants below. It has at most one member of each base type, except
/// objects: one `Obj` for each site
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Code {
Fn(usize),
Native(usize),
ProtoLink(Primitive),
}
/// A number is an int32 or a double. An int32 is also a double, so `Int`
/// joins with `Float` to give `Float`
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Number {
pub enum Type {
Unknown, // no value has reached it yet
Undefined,
Null,
Bool(Option<bool>), // the value, if it is known
Int,
Float,
Str(Option<AtomRef>), // the value, if it is known. Atoms are interned: equal atoms are equal strings
Obj(SiteId), // an object that the site makes. A fn is an object whose site has code
Sum(SumId), // see above
Any, // any value. No rule gives it now: a join gives a sum instead
}
/// The set of values that a place can hold: a set of kinds, with a refinement
/// for each kind in the set. The default is the empty set
#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
pub struct Type {
pub undefined: bool,
pub null: bool,
/// The value, if it is known
pub bool: Option<Option<bool>>,
pub number: Option<Number>,
/// The value, if it is known. Atoms are interned, so two equal atoms are
/// two equal strings
pub string: Option<Option<AtomRef>>,
/// The shape, by a site in it (see `Heap`)
pub object: Option<SiteId>,
/// A `Type` without its detail (a known value, a site): the kinds of value
/// that JS tells apart
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BaseType {
Undefined,
Null,
Bool,
Number,
String,
Object,
}
impl Type {
pub const UNDEFINED: Type = Type { undefined: true, ..Type::EMPTY };
pub const NULL: Type = Type { null: true, ..Type::EMPTY };
pub const EMPTY: Type = Type {
undefined: false,
null: false,
bool: None,
number: None,
string: None,
object: None,
};
pub const fn bool(value: Option<bool>) -> Type {
Type { bool: Some(value), ..Type::EMPTY }
}
pub const fn number(number: Number) -> Type {
Type { number: Some(number), ..Type::EMPTY }
}
pub const fn string(value: Option<AtomRef>) -> Type {
Type { string: Some(value), ..Type::EMPTY }
}
pub const fn object(site: SiteId) -> Type {
Type { object: Some(site), ..Type::EMPTY }
}
pub fn is_empty(&self) -> bool {
*self == Type::EMPTY
}
/// The type with the known values dropped
/// The type with the known value dropped
pub fn widen(&self) -> Type {
Type {
bool: self.bool.map(|_| None),
string: self.string.map(|_| None),
..*self
match self {
Type::Bool(_) => Type::Bool(None),
Type::Str(_) => Type::Str(None),
ty => *ty,
}
}
/// The type without undefined and null
pub fn non_nullish(&self) -> Type {
Type {
undefined: false,
null: false,
..*self
/// The base type of every value of the type. `None` for `Unknown`, which
/// has no values, and for `Sum` and `Any`, which can have more than one
pub fn base(&self) -> Option<BaseType> {
match self {
Type::Unknown | Type::Sum(_) | Type::Any => None,
Type::Undefined => Some(BaseType::Undefined),
Type::Null => Some(BaseType::Null),
Type::Bool(_) => Some(BaseType::Bool),
Type::Int | Type::Float => Some(BaseType::Number),
Type::Str(_) => Some(BaseType::String),
Type::Obj(_) => Some(BaseType::Object),
}
}
pub fn has_nullish(&self) -> bool {
self.undefined || self.null
}
/// The kinds in the type, in the order undefined, null, bool, number,
/// string, object
pub fn kinds(&self) -> [bool; 6] {
[
self.undefined,
self.null,
self.bool.is_some(),
self.number.is_some(),
self.string.is_some(),
self.object.is_some(),
]
}
/// Whether the type has exactly the kinds of `kind`
pub fn is_only(&self, kind: Type) -> bool {
self.kinds() == kind.kinds()
pub fn is_nullish(&self) -> bool {
matches!(self, Type::Undefined | Type::Null)
}
}
/// The join of two refinements that are a value if it is known
fn known<T: PartialEq>(a: Option<Option<T>>, b: Option<Option<T>>) -> Option<Option<T>> {
/// The join of two types of one base type, neither of them `Obj`
fn join_kind(a: Type, b: Type) -> Type {
match (a, b) {
(Some(a), Some(b)) => Some(if a == b { a } else { None }),
(a, b) => a.or(b),
(Type::Bool(x), Type::Bool(y)) => Type::Bool(if x == y { x } else { None }),
(Type::Str(x), Type::Str(y)) => Type::Str(if x == y { x } else { None }),
(Type::Int, Type::Int) => Type::Int,
(Type::Int | Type::Float, Type::Int | Type::Float) => Type::Float,
(a, _) => a,
}
}
/// A field of a shape
#[derive(Debug, Clone, Copy)]
pub struct Field {
pub ty: Type,
/// Every object of the shape gets the field when it is created, so the
/// field is never absent
pub defined: bool,
}
/// The type of a set of objects: what `docs/model.md` calls a shape
#[derive(Debug, Clone, Default)]
pub struct Shape {
/// Sorted by name
pub fields: Vec<(AtomRef, Field)>,
/// The code that its objects can run, sorted
pub code: Vec<Code>,
/// Some of its objects are not fns
pub plain: bool,
/// A field of one of its objects is written after the object is created
pub written: bool,
/// One of its objects is in an identity test with an object
pub compared: bool,
/// An op that creates one of its objects has run (see infer)
pub live: bool,
}
impl Shape {
pub fn field(&self, name: AtomRef) -> Option<&Field> {
let i = self.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?;
Some(&self.fields[i].1)
/// The sums, and the join, which makes them. The sums are stored in the site
/// table, because a join also marks sites (see `SiteTable::to_any()`)
impl SiteTable {
/// The types that a value of type `ty` can have: the members of a sum,
/// else `ty` itself. `Unknown` has none
pub fn members<'a>(&'a self, ty: &'a Type) -> &'a [Type] {
match ty {
Type::Unknown => &[],
Type::Sum(id) => {
let id = *id as usize;
&self.sum_members[self.sum_starts[id] as usize..self.sum_starts[id + 1] as usize]
}
ty => std::slice::from_ref(ty),
}
}
/// Whether a copy of one of its objects cannot be told from the object:
/// no write after creation can show the copy, and no identity test can
/// (see `docs/model.md`, section 5)
pub fn is_value(&self) -> bool {
!self.written && !self.compared
/// The members of every sum
pub fn sums(&self) -> impl Iterator<Item = &[Type]> {
self.sum_starts.windows(2).map(|w| &self.sum_members[w[0] as usize..w[1] as usize])
}
}
/// A site, and the code that its objects run, if they are fns
#[derive(Debug, Clone, Copy)]
pub struct SiteInfo {
pub site: Site,
pub code: Option<Code>,
}
/// The type whose members are the members of `a` and of `b`
fn union(&mut self, a: &Type, b: &Type) -> Type {
let start = self.sum_members.len();
/// What `Heap::new` finds about one instr
#[derive(Debug, Clone, Copy, Default)]
struct InstrInfo {
/// The site of the object it creates
site: Option<SiteId>,
/// A `define_field` into the value that a creating op gives, before the
/// object goes anywhere. Every other field write is a write after creation
creation_write: bool,
}
for ty in [a, b] {
for i in 0..self.members(ty).len() {
let member = self.members(ty)[i];
self.sum_members.push(member);
}
}
/// Every shape. Objects that flow into one place have one shape: their shapes
/// are merged. A merged site points to the site it was merged into, and only
/// a site that points to itself keeps a shape
#[derive(Debug, Clone)]
pub struct Heap {
/// Every site, by number: the creating ops of every fn in order, then the
/// natives, then the proto links
pub sites: Vec<SiteInfo>,
shapes: Vec<Shape>,
parent: Vec<Cell<SiteId>>,
/// Each fn's first entry in `instrs`, which has one entry for each instr
fn_starts: Vec<usize>,
instrs: Vec<InstrInfo>,
/// The number of the first native's site
natives: SiteId,
/// Goes up on every change, so a walk can tell that a read may give more
pub version: usize,
}
self.sum_of_new(start)
}
impl Heap {
/// Number every site, and give each one a shape with the fields its
/// creation writes write
pub fn new(fns: &[RegFn]) -> Self {
let mut heap = Self {
sites: vec![],
shapes: vec![],
parent: vec![],
fn_starts: vec![],
instrs: vec![],
natives: 0,
version: 0,
/// `ty` with every known value dropped
pub fn widen(&mut self, ty: &Type) -> Type {
let start = self.sum_members.len();
for i in 0..self.members(ty).len() {
let member = self.members(ty)[i].widen();
self.sum_members.push(member);
}
self.sum_of_new(start)
}
/// `widen()` for a type that `widen()` was already given
pub fn widened(&self, ty: &Type) -> Type {
let list: Vec<Type> = self.members(ty).iter().map(Type::widen).collect();
match list[..] {
[one] => one,
_ => Type::Sum(self.sums().position(|sum| sum == list).unwrap() as SumId),
}
}
/// The type whose members are the types at the end of `sum_members`, from
/// `start` on. They are sorted, and two of one kind join. If they are one
/// type, or a sum that exists, they are removed again
fn sum_of_new(&mut self, start: usize) -> Type {
let list = &mut self.sum_members[start..];
list.sort_unstable();
let mut n = 0;
for i in 0..list.len() {
let ty = list[i];
let joins = n > 0 && (list[n - 1] == ty || (ty.base() == list[n - 1].base() && ty.base() != Some(BaseType::Object)));
if joins {
list[n - 1] = join_kind(list[n - 1], ty);
} else {
list[n] = ty;
n += 1;
}
}
self.sum_members.truncate(start + n);
let new = &self.sum_members[start..];
let found = match new {
[one] => Some(*one),
_ => self.sums().position(|sum| sum == new).map(|id| Type::Sum(id as SumId)),
};
// The site that each value of the fn holds, if a creating op gave it
let mut created: Vec<Option<SiteId>> = vec![];
for (fn_id, f) in fns.iter().enumerate() {
heap.fn_starts.push(heap.instrs.len());
created.clear();
created.resize(f.val_count, None);
for (instr, op) in f.code.iter().enumerate() {
let mut info = InstrInfo::default();
match op.op {
OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8 => {
let code = match op.op {
OpCode::Object => None,
_ => match f.cpool[op.imm.const_idx()] {
JsValue::FnById(id) => Some(Code::Fn(id)),
_ => unreachable!(),
},
};
let site = heap.add(Site::Op { fn_id, instr }, code);
created[op.dsts[0]] = Some(site);
info.site = Some(site);
}
OpCode::DefineField => {
if let Some(site) = created[op.args[1]] {
let field = Field { ty: Type::EMPTY, defined: true };
let fields = &mut heap.shapes[site as usize].fields;
if let Err(i) = fields.binary_search_by_key(&op.imm.atom(), |(n, _)| *n) {
fields.insert(i, (op.imm.atom(), field));
}
info.creation_write = true;
}
}
_ => {}
}
heap.instrs.push(info);
}
}
heap.natives = heap.sites.len() as SiteId;
for id in 0..NATIVES.len() {
heap.add(Site::Native(id), Some(Code::Native(id)));
}
for kind in Primitive::ALL {
heap.add(Site::ProtoLink(kind), Some(Code::ProtoLink(kind)));
}
heap
}
fn add(&mut self, site: Site, code: Option<Code>) -> SiteId {
let id = self.sites.len() as SiteId;
self.sites.push(SiteInfo { site, code });
self.parent.push(Cell::new(id));
self.shapes.push(Shape {
code: code.into_iter().collect(),
plain: code.is_none(),
..Shape::default()
});
id
}
/// The site that instr `instr` of fn `fn_id` creates, if it is a creating
/// op
pub fn site_at(&self, fn_id: usize, instr: usize) -> Option<SiteId> {
self.instrs[self.fn_starts[fn_id] + instr].site
}
pub fn is_creation_write(&self, fn_id: usize, instr: usize) -> bool {
self.instrs[self.fn_starts[fn_id] + instr].creation_write
}
/// The site of a built-in fn
pub fn builtin(&self, site: Site) -> SiteId {
self.natives
+ match site {
Site::Native(id) => id as SiteId,
Site::ProtoLink(kind) => (NATIVES.len() + kind as usize) as SiteId,
Site::Op { .. } => unreachable!(),
}
}
/// The site that `site` is merged into, or `site` itself
pub fn find(&self, mut site: SiteId) -> SiteId {
loop {
let parent = self.parent[site as usize].get();
if parent == site {
return site;
}
// Path halving: skip a step for the next find
let grandparent = self.parent[parent as usize].get();
self.parent[site as usize].set(grandparent);
site = grandparent;
}
}
/// `ty` with its object's site the one that keeps its shape
pub fn canon(&self, ty: &Type) -> Type {
Type {
object: ty.object.map(|site| self.find(site)),
..*ty
}
}
pub fn shape(&self, site: SiteId) -> &Shape {
&self.shapes[self.find(site) as usize]
}
/// Every site that keeps a shape, and that a creating op has reached
pub fn live_shapes(&self) -> impl Iterator<Item = SiteId> + '_ {
(0..self.sites.len() as SiteId).filter(|&site| self.find(site) == site && self.shapes[site as usize].live)
}
/// An object of the site, which a creating op gives
pub fn create(&mut self, site: SiteId) -> Type {
let root = self.find(site);
self.mark(root, |shape| &mut shape.live);
Type::object(root)
}
/// Set a flag of the shape of `site`
pub fn mark(&mut self, site: SiteId, flag: fn(&mut Shape) -> &mut bool) {
let root = self.find(site);
let flag = flag(&mut self.shapes[root as usize]);
if !*flag {
*flag = true;
self.version += 1;
}
}
/// Whether an object of the shape of `site` can lack its own `field`. A
/// box's value never can: JS gives a binding a value before any read of it
/// that does not throw (see transform)
pub fn can_be_absent(&self, site: SiteId, field: AtomRef) -> bool {
field != BOX_FIELD && !self.shape(site).field(field).is_some_and(|f| f.defined)
}
/// Type lattice join: the union, kind by kind. Two different known values
/// give an unknown one, and two shapes are merged
pub fn join(&mut self, a: &Type, b: &Type) -> Type {
Type {
undefined: a.undefined || b.undefined,
null: a.null || b.null,
bool: known(a.bool, b.bool),
number: a.number.max(b.number),
string: known(a.string, b.string),
object: match (a.object, b.object) {
(Some(x), Some(y)) => Some(self.merge(x, y)),
(x, y) => x.or(y).map(|site| self.find(site)),
},
}
}
/// Merge the shape of `from` into the shape of `into`. A field that only
/// one of them has can be absent in the merged shape
fn merge(&mut self, into: SiteId, from: SiteId) -> SiteId {
let (into, from) = (self.find(into), self.find(from));
if into == from {
return into;
}
let from_shape = std::mem::take(&mut self.shapes[from as usize]);
self.parent[from as usize].set(into);
self.version += 1;
let shape = &mut self.shapes[into as usize];
shape.code.extend(&from_shape.code);
shape.code.sort_unstable();
shape.code.dedup();
shape.plain |= from_shape.plain;
shape.written |= from_shape.written;
shape.compared |= from_shape.compared;
shape.live |= from_shape.live;
for (name, field) in &mut shape.fields {
field.defined &= from_shape.field(*name).is_some_and(|f| f.defined);
}
for (name, field) in &from_shape.fields {
let defined = self.shape(into).field(*name).is_some_and(|f| f.defined) && field.defined;
self.write(into, *name, &field.ty);
let root = self.find(into) as usize;
let i = self.shapes[root].fields.binary_search_by_key(name, |(n, _)| *n).unwrap();
self.shapes[root].fields[i].1.defined = defined;
}
self.find(into)
}
/// Join `ty` into `field` of the shape of `site`. A field that the shape
/// did not have can be absent
pub fn write(&mut self, site: SiteId, field: AtomRef, ty: &Type) {
let old = self.shape(site).field(field).map_or(Type::EMPTY, |f| f.ty);
let new = self.join(&old, ty);
// The join may have merged the shape into another one
let root = self.find(site) as usize;
let fields = &mut self.shapes[root].fields;
match fields.binary_search_by_key(&field, |(n, _)| *n) {
Ok(i) if fields[i].1.ty == new => return,
Ok(i) => fields[i].1.ty = new,
Err(i) => fields.insert(i, (field, Field { ty: new, defined: false })),
}
self.version += 1;
}
/// A read of `field` on an object of the shape of `site`: its own field,
/// then, where that can be absent, the read on its prototype. At the end of
/// the chain, an absent field is undefined
///
/// Merged shapes can make a chain loop, which a real object cannot. A chain
/// longer than the number of sites has looped, and stops
pub fn read(&mut self, site: SiteId, field: AtomRef) -> Type {
let mut site = self.find(site);
let mut ty = Type::EMPTY;
for _ in 0..self.sites.len() {
let own = self.shape(site).field(field).map_or(Type::EMPTY, |f| f.ty);
ty = self.join(&ty, &own);
if !self.can_be_absent(site, field) {
return ty;
}
// The chain ends at a prototype that is null or absent
let proto = self.shape(site).field(PROTO_FIELD).map_or(Type::EMPTY, |f| f.ty);
ty.undefined |= proto.null || self.can_be_absent(site, PROTO_FIELD);
match proto.object {
Some(proto) => site = self.find(proto),
None => break,
}
}
match found {
Some(ty) => {
self.sum_members.truncate(start);
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);
None => {
self.sum_starts.push(self.sum_members.len() as u32);
Type::Sum((self.sum_starts.len() - 2) as SumId)
}
}
}
read
/// The join of two types (see `Type`)
pub fn join(&mut self, a: &Type, b: &Type) -> Type {
use Type::*;
match (*a, *b) {
(Unknown, t) | (t, Unknown) => t,
(a, b) if a == b => a,
(Any, t) | (t, Any) => self.to_any(&t),
(a, b) => self.union(&a, &b),
}
}
}
/// The type, if every value of it is one value that the type tells
pub(super) fn single(ty: &Type) -> Option<Type> {
match ty {
Type::Undefined | Type::Null | Type::Bool(Some(_)) | Type::Str(Some(_)) => Some(*ty),
_ => None,
}
}
// The tests below take one member of a type each (see `TypeCache::agree()`)
/// Whether `a === b`, if the types decide it
pub(super) fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
let disjoint = matches!((a.base(), b.base()), (Some(x), Some(y)) if x != y);
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
pub(super) fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
let other = |t: &Type| !t.is_nullish() && *t != Type::Any;
match (a, b) {
_ if a.is_nullish() && b.is_nullish() => Some(true),
_ if (a.is_nullish() && other(b)) || (other(a) && b.is_nullish()) => Some(false),
_ if a.base().is_some() && a.base() == b.base() => strict_eq(a, b),
_ => None,
}
}
/// Whether a value of type `ty` has one of the base types `bases`, if the type decides it
pub(super) fn test(ty: &Type, bases: &[BaseType]) -> Option<bool> {
ty.base().map(|base| bases.contains(&base))
}
/// Whether a value of type `ty` is a fn, if the type decides it
pub(super) fn is_function(ty: &Type, site_table: &SiteTable) -> Option<bool> {
match ty {
Type::Obj(site) => Some(site_table.site(*site).code().is_some()),
Type::Any => None,
_ => Some(false),
}
}
/// Whether a value of type `ty` is truthy, if the type decides it
pub(super) fn truthiness(ty: &Type) -> Option<bool> {
match ty {
Type::Undefined | Type::Null => Some(false),
Type::Bool(b) => *b,
Type::Str(s) => s.map(|atom| atom != EMPTY_STRING),
Type::Obj(_) => Some(true),
_ => None,
}
}
/// The atom of what `typeof` gives for a value of type `ty`, if the type
/// decides it
pub(super) fn typeof_name(ty: &Type, site_table: &SiteTable) -> Option<u32> {
Some(match ty {
Type::Undefined => 73, // "undefined"
Type::Null => 77, // "object"
Type::Bool(_) => 75, // "boolean"
Type::Int | Type::Float => 74, // "number"
Type::Str(_) => 76, // "string"
Type::Obj(site) if site_table.site(*site).code().is_some() => 27, // "function"
Type::Obj(_) => 77, // "object"
_ => return None,
})
}
+3 -2
View File
@@ -72,7 +72,8 @@ impl Atom {
let (bin, header) = leb128(bin)?;
let wide_char = header & 1 == 1;
let len = header >> 1;
let (bin, data) = take(len)(bin)?;
let byte_len = if wide_char { len * 2 } else { len };
let (bin, data) = take(byte_len)(bin)?;
let data = data.to_vec();
let string = if wide_char {
@@ -171,7 +172,7 @@ pub enum JsValue {
/// A C function, by its index in `native::NATIVES`
NativeFn(usize),
/// Makes its arg the prototype of a primitive kind (see `native`)
ProtoLink(crate::native::Primitive),
ProtoLink(crate::native::ProtoOf),
Module(Module),
TypedArray,
ArrayBuffer,
+66 -5
View File
@@ -17,7 +17,34 @@ use libqjs::compile;
use regify::regify;
use transform::transform;
const STDLIB: &[u8] = include_bytes!("../../lib/stdlib.js");
/// The stdlib, in the order that it runs in, in front of the program
const STDLIB: &[&[u8]] = &[
include_bytes!("../../lib/stdlib/globals.js"),
include_bytes!("../../lib/stdlib/object.js"),
include_bytes!("../../lib/stdlib/string.js"),
include_bytes!("../../lib/stdlib/number.js"),
include_bytes!("../../lib/stdlib/boolean.js"),
include_bytes!("../../lib/stdlib/array.js"),
include_bytes!("../../lib/stdlib/loop.js"),
include_bytes!("../../lib/stdlib/promise.js"),
include_bytes!("../../lib/stdlib/timers.js"),
include_bytes!("../../lib/stdlib/io.js"),
include_bytes!("../../lib/stdlib/console.js"),
];
/// Runs after the program
const STDLIB_END: &[u8] = include_bytes!("../../lib/stdlib/end.js");
/// The runtime. The emitted C includes `jsrt.h`, which includes the rest
const RUNTIME: &[(&str, &[u8])] = &[
("jsrt.h", include_bytes!("../../lib/rt/jsrt.h")),
("core.h", include_bytes!("../../lib/rt/core.h")),
("str.h", include_bytes!("../../lib/rt/str.h")),
("number.h", include_bytes!("../../lib/rt/number.h")),
("any.h", include_bytes!("../../lib/rt/any.h")),
("io.h", include_bytes!("../../lib/rt/io.h")),
("uring.h", include_bytes!("../../lib/rt/uring.h")),
];
fn main() {
// Load source file
@@ -28,10 +55,16 @@ fn main() {
let metadata = std::fs::metadata(filename).unwrap();
assert!(metadata.is_file());
// The stdlib is compiled with the program, in front of it
let mut src = STDLIB.to_vec();
src.reserve(metadata.len() as usize);
// The stdlib is compiled with the program, in front of it. The program is
// in a fn of its own, so that its names can shadow the stdlib's, as a
// script's names shadow JS's globals (node does the same with a script).
// `})` is on a line of its own, in case the program ends in a comment
let mut src = STDLIB.concat();
src.reserve(metadata.len() as usize + STDLIB_END.len() + 32);
src.extend_from_slice(b"(function () {\n");
file.read_to_end(&mut src).unwrap();
src.extend_from_slice(b"\n})();\n");
src.extend_from_slice(STDLIB_END);
// Compile to bytecode
let binary = compile(&src, filename);
@@ -43,5 +76,33 @@ fn main() {
let ty_cache = infer_types(&reg_fns);
let analysis = analyze(&reg_fns);
print!("{}", emit::emit(&reg_fns, &ty_cache, &analysis, &atoms));
let c = emit::emit(&reg_fns, &ty_cache, &analysis, &atoms);
// Compile the C, with the runtime, to `a.out`
let dir = std::env::temp_dir().join(format!("jsrt-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
for (name, src) in RUNTIME {
std::fs::write(dir.join(name), src).unwrap();
}
std::fs::write(dir.join("program.c"), c).unwrap();
let cc = cc::Build::new()
.target(env!("TARGET"))
.host(env!("TARGET"))
.opt_level(2)
.cargo_metadata(false)
.get_compiler();
let status = cc
.to_command()
.args(["-std=c11", "-w"])
.arg(dir.join("program.c"))
.args(["-o", "a.out", "-lm"])
.status()
.unwrap();
std::fs::remove_dir_all(&dir).unwrap();
std::process::exit(status.code().unwrap_or(1));
}
+51 -26
View File
@@ -1,4 +1,4 @@
//! What the stdlib (`lib/stdlib.js`) can reach below JS: the globals it may
//! What the stdlib (`lib/stdlib/`) can reach below JS: the globals it may
//! read, and nothing else may
//!
//! Natives are the common layer of operations on primitive values. The stdlib
@@ -10,7 +10,10 @@
//! kind. A field read on a primitive borrows its kind's prototype
use crate::{
infer::types::{Number, Type},
infer::{
sites::SiteTable,
types::{BaseType, Type},
},
libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS},
};
@@ -37,10 +40,10 @@ macro_rules! native {
/// A native's param or return type: any value of its kind
macro_rules! ty {
(Undefined) => { Type::UNDEFINED };
(Int) => { Type::number(Number::Int) };
(Float) => { Type::number(Number::Float) };
(String) => { Type::string(None) };
(Undefined) => { Type::Undefined };
(Int) => { Type::Int };
(Float) => { Type::Float };
(String) => { Type::Str(None) };
}
#[rustfmt::skip]
@@ -48,7 +51,23 @@ pub static NATIVES: &[NativeFn] = &[
// IO
native!("__native_print_string"(String) -> Undefined),
// Numbers. JS has one number type, see `accepts()`
// Async IO (see `lib/rt/io.h`). A queue gives the op's handle. A wait gives
// the handle of an op that is done, or -1 if none is left. The getters
// read that op's result, until the next wait
native!("__native_io_queue_timeout"(Float) -> Int),
native!("__native_io_queue_open"(String, Int, Int) -> Int),
native!("__native_io_queue_read"(Int, Int, Float) -> Int),
native!("__native_io_queue_write"(Int, String, Float) -> Int),
native!("__native_io_queue_close"(Int) -> Int),
native!("__native_io_queue_stat"(String) -> Int),
native!("__native_io_queue_cancel"(Int) -> Int),
native!("__native_io_wait"() -> Int),
native!("__native_io_status"(Int) -> Int),
native!("__native_io_result_string"(Int) -> String),
native!("__native_io_result_number"(Int) -> Float),
// Numbers. JS has one number type, so an Int and a Float arg both pass
// (see `ret_type()`)
native!("__native_number_to_string"(Float) -> String),
native!("__native_number_to_fixed"(Float, Int) -> String),
native!("__native_string_to_number"(String) -> Float),
@@ -60,33 +79,39 @@ pub static NATIVES: &[NativeFn] = &[
native!("__native_string_from_char_code"(Int) -> String),
];
/// A primitive kind whose field reads borrow a prototype from the stdlib
/// A kind of value whose prototype the stdlib makes and links (see
/// `PROTO_LINKS`): a primitive kind, or arrays. A field read that its own
/// fields do not answer goes on to that prototype
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Primitive {
pub enum ProtoOf {
String,
Number,
Boolean,
Array,
}
impl Primitive {
pub const ALL: [Primitive; 3] = [Primitive::String, Primitive::Number, Primitive::Boolean];
impl ProtoOf {
pub const ALL: [ProtoOf; 4] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean, ProtoOf::Array];
pub const PRIMITIVES: [ProtoOf; 3] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean];
/// Whether `ty` can hold a value of this kind
pub fn is_in(self, ty: &Type) -> bool {
match self {
Primitive::String => ty.string.is_some(),
Primitive::Number => ty.number.is_some(),
Primitive::Boolean => ty.bool.is_some(),
/// The primitive kind of every value of type `ty`, if it has a prototype
pub fn of(ty: &Type) -> Option<ProtoOf> {
match ty.base()? {
BaseType::String => Some(ProtoOf::String),
BaseType::Number => Some(ProtoOf::Number),
BaseType::Bool => Some(ProtoOf::Boolean),
_ => None,
}
}
}
/// `__link_proto_<kind>(proto)` makes `proto` the prototype of `<kind>`. It
/// does nothing at runtime
static PROTO_LINKS: &[(&str, Primitive)] = &[
("__link_proto_string", Primitive::String),
("__link_proto_number", Primitive::Number),
("__link_proto_boolean", Primitive::Boolean),
static PROTO_LINKS: &[(&str, ProtoOf)] = &[
("__link_proto_string", ProtoOf::String),
("__link_proto_number", ProtoOf::Number),
("__link_proto_boolean", ProtoOf::Boolean),
("__link_proto_array", ProtoOf::Array),
];
/// The value of the global `name`. The only globals are the ones the stdlib
@@ -111,17 +136,17 @@ pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue {
/// What a call of native `idx` returns, with args of these types, in source
/// order. An arg that cannot be of its param's kind makes the call throw, so
/// the call gives nothing
pub fn ret_type(idx: usize, args: &[Type]) -> Type {
/// the call gives nothing. An arg of a sum or `Any` is checked at runtime
pub fn ret_type(idx: usize, args: &[Type], site_table: &SiteTable) -> Type {
let native = &NATIVES[idx];
let missing = std::iter::repeat(&Type::UNDEFINED);
let missing = std::iter::repeat(&Type::Undefined);
let can_pass = |(param, arg): (&Type, &Type)| {
param.kinds().iter().zip(arg.kinds()).any(|(p, a)| *p && a)
*arg == Type::Any || site_table.members(arg).iter().any(|a| a.base() == param.base())
};
match native.args.iter().zip(args.iter().chain(missing)).all(can_pass) {
true => native.ret,
false => Type::EMPTY,
false => Type::Unknown,
}
}
+46 -9
View File
@@ -2,7 +2,7 @@ mod basic_block;
use crate::{
flatten::FlatFn,
libqjs::parse::{AtomRef, ImmOperand, Instr, JsValue, opcodes::OpCode},
libqjs::parse::{ImmOperand, Instr, JsValue, opcodes::OpCode},
};
pub use basic_block::BasicBlock;
@@ -29,7 +29,6 @@ impl RegInstr {
#[derive(Debug)]
pub struct RegFn {
pub name: AtomRef,
pub arg_count: usize,
pub var_count: usize,
/// Locals and args are not renamed: local `k` is value id `k`, and arg `k`
@@ -119,8 +118,25 @@ impl RegFn {
}
}
// A define_field into an object that no creating op of this fn gave
// (a class field on `this`, a static field on the class) is a write
// like any other: a put_field. So a define_field is always a write
// that builds an object before it goes anywhere (see infer)
let mut created = vec![false; next_val];
for instr in &out {
if matches!(instr.op, OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8) {
created[instr.dsts[0]] = true;
}
}
for instr in &mut out {
if instr.op == OpCode::DefineField && !created[instr.args[1]] {
instr.op = OpCode::PutField;
}
}
Self {
name: f.name,
arg_count: f.arg_count,
var_count: f.var_count,
val_count: next_val,
@@ -209,7 +225,7 @@ fn walk_block(
continue;
}
// Handle `Dup` and `Swap`, which just push values
// Handle `Dup`, `Dup2`, `Swap` and `ToPropkey2`, which just push values
if instr.op == OpCode::Dup {
stack.push(out_instr.args[0]);
stack.push(out_instr.args[0]);
@@ -219,6 +235,27 @@ fn walk_block(
continue;
}
if instr.op == OpCode::Dup2 {
let (a, b) = (out_instr.args[1], out_instr.args[0]);
stack.extend([a, b, a, b]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
// to_propkey2 makes the key on top a property key, and keeps the
// object under it. A key is a number, which it keeps as it is (see
// infer), so it only gives both back
if instr.op == OpCode::ToPropkey2 {
stack.push(out_instr.args[1]);
stack.push(out_instr.args[0]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
if instr.op == OpCode::Swap {
stack.push(out_instr.args[0]);
stack.push(out_instr.args[1]);
@@ -228,11 +265,11 @@ fn walk_block(
continue;
}
// A field write lands back in the object's own value: the object with
// the field is the same object. define_field also leaves it on the
// stack, put_field does not
if matches!(instr.op, OpCode::PutField | OpCode::DefineField) {
let obj = out_instr.args[1];
// A field or element write lands back in the object's own value: the
// object with the field is the same object. define_field also leaves
// it on the stack, put_field and put_array_el do not
if matches!(instr.op, OpCode::PutField | OpCode::DefineField | OpCode::PutArrayEl) {
let obj = *out_instr.args.last().unwrap();
if n_push == 1 {
stack.push(obj);
+29
View File
@@ -0,0 +1,29 @@
use super::{FIRST_ARG, THIS_ARG, pusher};
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`,
/// and `this` with a read of its param
pub fn replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let (op, arg) = match (instr.op, instr.imm) {
(PushThis, _) => (GetArg, THIS_ARG),
(GetArg0, _) => (GetArg, FIRST_ARG),
(GetArg1, _) => (GetArg, FIRST_ARG + 1),
(GetArg2, _) => (GetArg, FIRST_ARG + 2),
(GetArg3, _) => (GetArg, FIRST_ARG + 3),
(PutArg0, _) => (PutArg, FIRST_ARG),
(PutArg1, _) => (PutArg, FIRST_ARG + 1),
(PutArg2, _) => (PutArg, FIRST_ARG + 2),
(PutArg3, _) => (PutArg, FIRST_ARG + 3),
(SetArg0, _) => (SetArg, FIRST_ARG),
(SetArg1, _) => (SetArg, FIRST_ARG + 1),
(SetArg2, _) => (SetArg, FIRST_ARG + 2),
(SetArg3, _) => (SetArg, FIRST_ARG + 3),
(GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k),
_ => return false,
};
pusher(instr.pc, out)(op, ImmOperand::Arg(arg));
true
}
+216
View File
@@ -0,0 +1,216 @@
use std::collections::HashMap;
use super::{BOX_FIELD, ENV_ARG, FIRST_ARG, pusher};
use crate::{
libqjs::parse::{
AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode,
},
regify::{is_var_read, is_var_write},
};
/// Put every binding that a closure captures in a box: an object whose
/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The
/// owner and the closures read and write the box. A closure keeps the boxes
/// it captured in its own fields, one for each of its `closure_vars`: field `i`
/// holds the box of capture `i`
///
/// The fn makes a box for each binding that its children capture, at entry.
/// A captured local holds its box in its own slot. A captured arg keeps its
/// box in a new local, and the box starts with the arg's value. A `var` starts
/// as undefined. A `let` or `const` has no value until it is written (its TDZ):
/// object; undefined; define_field <var>; put_loc k each captured `var`
/// object; put_loc k each captured `let`
/// object; get_arg a; define_field <var>; put_loc box each captured arg
///
/// A scope that is entered again is a loop body. QuickJS closes each captured
/// binding of the scope (`close_loc`) at every way out of it, so a new box
/// there gives each iteration its own binding. `set_loc_uninitialized` only
/// marks the TDZ, which is not checked, and hoisted fns are made before it, so
/// it does not make a box
///
/// These instrs take a pc that no jump goes to, so a jump to the fn's first
/// instr does not make them again
///
/// Then it replaces, for a captured binding:
/// get_loc k get_loc box; get_field <var>
/// put_loc k get_loc box; swap; put_field <var>
/// set_loc k dup; get_loc box; swap; put_field <var>
/// close_loc k object; get_loc k; get_field <var>;
/// define_field <var>; put_loc k
/// a way out of a loop body: a new binding, with
/// the current value
///
/// For any binding:
/// get_var_ref i get_arg env; get_field i; get_field <var>
/// put_var_ref i get_arg env; get_field i; swap; put_field <var>
/// set_var_ref i dup; get_arg env; get_field i; swap; put_field <var>
/// fclosure f fclosure f; (push_box; define_field i)*
///
/// where `push_box` is the box of capture `i` of `f`: the local that holds it,
/// or field `j` of this fn's env for capture `j` of this fn. A binding with no
/// frame (a module binding) gets its box here, from `object`
///
/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure
/// captures, do nothing. Gives the code and the new number of locals
pub fn replace_bindings(
code: &[Instr],
cpool: &[JsValue],
captures: &[Vec<ClosureVar>],
var_count: usize,
) -> (Vec<Instr>, usize) {
use OpCode::*;
let children = cpool.iter().filter_map(|c| match c {
JsValue::FnById(child) => Some(*child),
_ => None,
});
let mut locals = vec![];
let mut args = vec![];
for c in children.flat_map(|child| &captures[child]) {
match c.flags.closure_kind {
ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)),
ClosureKind::Arg => args.push(c.var_idx),
_ => {}
}
}
locals.sort_unstable();
locals.dedup();
args.sort_unstable();
args.dedup();
// Value ids of bindings, as `is_var_read` gives them: args after every local
let new_var_count = var_count + args.len();
let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k;
// The local that holds the box of each captured binding, by value id
let boxes: HashMap<usize, usize> = (locals.iter().map(|&(k, _)| (k, k)))
.chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i)))
.collect();
let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32));
let value = ImmOperand::Atom(BOX_FIELD);
let none = ImmOperand::None;
let loc = |k: usize| ImmOperand::Loc(k as u16);
let mut out = Vec::with_capacity(code.len());
{
let mut push = pusher(usize::MAX, &mut out);
for &(k, lexical) in &locals {
push(Object, none);
if !lexical {
push(Undefined, none);
push(DefineField, value);
}
push(PutLoc, loc(k));
}
for (i, &k) in args.iter().enumerate() {
push(Object, none);
push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16));
push(DefineField, value);
push(PutLoc, loc(var_count + i));
}
}
for instr in code {
let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v));
let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v));
let own_box = |k: ImmOperand| match k {
ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(),
_ => None,
};
let mut push = pusher(instr.pc, &mut out);
if let Some(&b) = read {
push(GetLoc, loc(b));
push(GetField, value);
} else if let Some(&b) = write {
if instr.op.info().n_push == 1 {
push(Dup, none);
}
push(GetLoc, loc(b));
push(Swap, none);
push(PutField, value);
} else if let Some((i, writes, leaves)) = var_ref_access(instr) {
if leaves {
push(Dup, none);
}
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(i));
if writes {
push(Swap, none);
push(PutField, value);
} else {
push(GetField, value);
}
} else if instr.op == SetLocUninitialized {
push(Nop, none);
} else if instr.op == CloseLoc {
match own_box(instr.imm) {
Some(b) => {
push(Object, none);
push(GetLoc, loc(b));
push(GetField, value);
push(DefineField, value);
push(PutLoc, loc(b));
}
None => push(Nop, none),
}
} else if matches!(instr.op, Fclosure8 | Fclosure) {
let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else {
unreachable!()
};
push(Fclosure, instr.imm);
for (i, c) in captures[child].iter().enumerate() {
match c.flags.closure_kind {
ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])),
ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])),
ClosureKind::Ref => {
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(c.var_idx));
}
_ => push(Object, none),
}
push(DefineField, slot(i));
}
} else {
push(instr.op, instr.imm);
}
}
(out, new_var_count)
}
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes,
/// and whether a write also leaves the value on the stack
fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
use OpCode::*;
let i = match instr.imm {
ImmOperand::VarRef(i) => i as usize,
_ => 0,
};
match instr.op {
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)),
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)),
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)),
GetVarRef | GetVarRefCheck => Some((i, false, false)),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)),
SetVarRef => Some((i, true, true)),
_ => None,
}
}
+71
View File
@@ -0,0 +1,71 @@
use super::{ENV_ARG, PROTO_FIELD, pusher};
use crate::{
libqjs::parse::{Atom, ImmOperand, Instr, JsValue, opcodes::OpCode},
native,
};
/// Replace ops that are a fixed sequence of simpler ones
///
/// A sequence is never empty: jumps find their target by its pc (see regify),
/// so every instr must keep at least one instr at its pc
pub fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let none = ImmOperand::None;
let imm = instr.imm;
// A plain call has no `this`, so it passes `undefined`. It is pushed last,
// on top of the args, and the call pops it as one more arg (see regify)
let plain_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1))];
let tail_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1)), (Return, none)];
let seq: &[(OpCode, ImmOperand)] = match (instr.op, imm) {
(Call0, _) => &plain_call(0),
(Call1, _) => &plain_call(1),
(Call2, _) => &plain_call(2),
(Call3, _) => &plain_call(3),
(Call, ImmOperand::Npop(n)) => &plain_call(n),
(TailCall, ImmOperand::Npop(n)) => &tail_call(n),
(TailCallMethod, _) => &[(CallMethod, imm), (Return, none)],
(GetField2, _) => &[(Dup, none), (GetField, imm)],
(GetLoc0Loc1, _) => &[(GetLoc0, none), (GetLoc1, none)],
(IncLoc, _) => &[(GetLoc, imm), (Inc, none), (PutLoc, imm)],
(DecLoc, _) => &[(GetLoc, imm), (Dec, none), (PutLoc, imm)],
// The added value is on the stack, the local is the left operand
(AddLoc, _) => &[(GetLoc, imm), (Swap, none), (Add, none), (PutLoc, imm)],
(SetProto, _) => &[(DefineField, ImmOperand::Atom(PROTO_FIELD))],
// The low two bits of `arg`: 0 method, 1 getter, 2 setter
(DefineMethod, ImmOperand::AtomU8 { atom, arg }) => {
assert!(arg & 3 == 0, "getters and setters are not supported");
&[(DefineField, ImmOperand::Atom(atom))]
}
// The fn itself, which is its env
(SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))],
// [[HomeObject]], which only `super` reads. `super` is not supported
// (see set_home_object in regify)
(SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)],
// arguments, new.target, import.meta
(SpecialObject, _) => unimplemented!("special_object {imm:?}"),
_ => return false,
};
let mut push = pusher(instr.pc, out);
seq.iter().for_each(|&(op, imm)| push(op, imm));
true
}
/// Replace a read of a global with the constant it is (see `native`)
pub fn replace_get_var(
instr: &Instr,
atoms: &[Atom],
cpool: &mut Vec<JsValue>,
out: &mut Vec<Instr>,
) -> bool {
if instr.op != OpCode::GetVar {
return false;
}
cpool.push(native::global(instr.imm.atom(), atoms));
pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1));
true
}
+9 -365
View File
@@ -1,16 +1,18 @@
mod args;
mod bindings;
mod declass;
mod expand;
mod new;
use std::collections::HashMap;
use args::replace_arg_access;
use bindings::replace_bindings;
use declass::declass;
use expand::{replace_expansion, replace_get_var};
use new::{is_new, replace_new};
use crate::{
flatten::FlatFn,
libqjs::parse::{
Atom, AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode,
},
native,
regify::{is_var_read, is_var_write},
libqjs::parse::{Atom, AtomRef, ClosureVar, ImmOperand, Instr, opcodes::OpCode},
};
/// The field an object's prototype is kept in. In JS, `o.__proto__` is the
@@ -88,361 +90,3 @@ fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], atoms: &[Atom]) ->
fn pusher(pc: usize, out: &mut Vec<Instr>) -> impl FnMut(OpCode, ImmOperand) + '_ {
move |op, imm| out.push(Instr { pc, op, imm })
}
// ───────────────────────────────────────────────── New Processing ──────────────────────────────────────────────────────────────
/// If `instr` is a `new`, the number of args it passes
fn is_new(instr: &Instr) -> Option<usize> {
match (instr.op, instr.imm) {
(OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize),
_ => None,
}
}
/// Replace:
/// call_constructor n [F, new_target, a0 .. an-1]
///
/// With:
/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top
/// drop new_target, always a copy of F here
/// dup [F, F]
/// get_field prototype [F, proto]
/// object [F, proto, obj]
/// swap [F, obj, proto]
/// define_field __proto__ [F, obj], obj is { __proto__: F.prototype }
/// get_loc t0 .. get_loc t(n-1) the args back
/// call_constructor n [F, obj, a0 .. an-1], a method call of F on obj
///
/// Whether `new` gives the call's result or `obj` depends on types (see infer)
fn replace_new(instr: &Instr, temps: usize, out: &mut Vec<Instr>) -> bool {
let Some(n) = is_new(instr) else {
return false;
};
let mut push = pusher(instr.pc, out);
let temp = |i: usize| ImmOperand::Loc((temps + i) as u16);
for i in (0..n).rev() {
push(OpCode::PutLoc, temp(i));
}
push(OpCode::Drop, ImmOperand::None);
push(OpCode::Dup, ImmOperand::None);
push(OpCode::GetField, ImmOperand::Atom(PROTOTYPE_FIELD));
push(OpCode::Object, ImmOperand::None);
push(OpCode::Swap, ImmOperand::None);
push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD));
for i in 0..n {
push(OpCode::GetLoc, temp(i));
}
push(OpCode::CallConstructor, instr.imm);
true
}
// ─────────────────────────────────────────────── Binding Processing ────────────────────────────────────────────────────────────
/// Put every binding that a closure captures in a box: an object whose
/// `BOX_FIELD` holds the binding's value (see `docs/model.md`, section 2). The
/// owner and the closures read and write the box. A closure keeps the boxes
/// it captured in its own fields, one for each of its `closure_vars`: field `i`
/// holds the box of capture `i`
///
/// The fn makes a box for each binding that its children capture, at entry.
/// A captured local holds its box in its own slot. A captured arg keeps its
/// box in a new local, and the box starts with the arg's value. A `var` starts
/// as undefined. A `let` or `const` has no value until it is written (its TDZ):
/// object; undefined; define_field <var>; put_loc k each captured `var`
/// object; put_loc k each captured `let`
/// object; get_arg a; define_field <var>; put_loc box each captured arg
///
/// A scope that is entered again is a loop body. QuickJS closes each captured
/// binding of the scope (`close_loc`) at every way out of it, so a new box
/// there gives each iteration its own binding. `set_loc_uninitialized` only
/// marks the TDZ, which is not checked, and hoisted fns are made before it, so
/// it does not make a box
///
/// These instrs take a pc that no jump goes to, so a jump to the fn's first
/// instr does not make them again
///
/// Then it replaces, for a captured binding:
/// get_loc k get_loc box; get_field <var>
/// put_loc k get_loc box; swap; put_field <var>
/// set_loc k dup; get_loc box; swap; put_field <var>
/// close_loc k object; get_loc k; get_field <var>;
/// define_field <var>; put_loc k
/// a way out of a loop body: a new binding, with
/// the current value
///
/// For any binding:
/// get_var_ref i get_arg env; get_field i; get_field <var>
/// put_var_ref i get_arg env; get_field i; swap; put_field <var>
/// set_var_ref i dup; get_arg env; get_field i; swap; put_field <var>
/// fclosure f fclosure f; (push_box; define_field i)*
///
/// where `push_box` is the box of capture `i` of `f`: the local that holds it,
/// or field `j` of this fn's env for capture `j` of this fn. A binding with no
/// frame (a module binding) gets its box here, from `object`
///
/// `set_loc_uninitialized`, and a `close_loc` of a binding no closure
/// captures, do nothing. Gives the code and the new number of locals
fn replace_bindings(
code: &[Instr],
cpool: &[JsValue],
captures: &[Vec<ClosureVar>],
var_count: usize,
) -> (Vec<Instr>, usize) {
use OpCode::*;
let children = cpool.iter().filter_map(|c| match c {
JsValue::FnById(child) => Some(*child),
_ => None,
});
let mut locals = vec![];
let mut args = vec![];
for c in children.flat_map(|child| &captures[child]) {
match c.flags.closure_kind {
ClosureKind::Local => locals.push((c.var_idx, c.flags.is_lexical)),
ClosureKind::Arg => args.push(c.var_idx),
_ => {}
}
}
locals.sort_unstable();
locals.dedup();
args.sort_unstable();
args.dedup();
// Value ids of bindings, as `is_var_read` gives them: args after every local
let new_var_count = var_count + args.len();
let arg_id = |k: usize| new_var_count + FIRST_ARG as usize + k;
// The local that holds the box of each captured binding, by value id
let boxes: HashMap<usize, usize> = (locals.iter().map(|&(k, _)| (k, k)))
.chain(args.iter().enumerate().map(|(i, &k)| (arg_id(k), var_count + i)))
.collect();
let slot = |i: usize| ImmOperand::Atom(AtomRef::TaggedInt(i as u32));
let value = ImmOperand::Atom(BOX_FIELD);
let none = ImmOperand::None;
let loc = |k: usize| ImmOperand::Loc(k as u16);
let mut out = Vec::with_capacity(code.len());
{
let mut push = pusher(usize::MAX, &mut out);
for &(k, lexical) in &locals {
push(Object, none);
if !lexical {
push(Undefined, none);
push(DefineField, value);
}
push(PutLoc, loc(k));
}
for (i, &k) in args.iter().enumerate() {
push(Object, none);
push(GetArg, ImmOperand::Arg(FIRST_ARG + k as u16));
push(DefineField, value);
push(PutLoc, loc(var_count + i));
}
}
for instr in code {
let read = is_var_read(instr, new_var_count).and_then(|v| boxes.get(&v));
let write = is_var_write(instr, new_var_count).and_then(|v| boxes.get(&v));
let own_box = |k: ImmOperand| match k {
ImmOperand::Loc(k) => boxes.get(&(k as usize)).copied(),
_ => None,
};
let mut push = pusher(instr.pc, &mut out);
if let Some(&b) = read {
push(GetLoc, loc(b));
push(GetField, value);
} else if let Some(&b) = write {
if instr.op.info().n_push == 1 {
push(Dup, none);
}
push(GetLoc, loc(b));
push(Swap, none);
push(PutField, value);
} else if let Some((i, writes, leaves)) = var_ref_access(instr) {
if leaves {
push(Dup, none);
}
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(i));
if writes {
push(Swap, none);
push(PutField, value);
} else {
push(GetField, value);
}
} else if instr.op == SetLocUninitialized {
push(Nop, none);
} else if instr.op == CloseLoc {
match own_box(instr.imm) {
Some(b) => {
push(Object, none);
push(GetLoc, loc(b));
push(GetField, value);
push(DefineField, value);
push(PutLoc, loc(b));
}
None => push(Nop, none),
}
} else if matches!(instr.op, Fclosure8 | Fclosure) {
let JsValue::FnById(child) = cpool[instr.imm.const_idx()] else {
unreachable!()
};
push(Fclosure, instr.imm);
for (i, c) in captures[child].iter().enumerate() {
match c.flags.closure_kind {
ClosureKind::Local => push(GetLoc, loc(boxes[&c.var_idx])),
ClosureKind::Arg => push(GetLoc, loc(boxes[&arg_id(c.var_idx)])),
ClosureKind::Ref => {
push(GetArg, ImmOperand::Arg(ENV_ARG));
push(GetField, slot(c.var_idx));
}
_ => push(Object, none),
}
push(DefineField, slot(i));
}
} else {
push(instr.op, instr.imm);
}
}
(out, new_var_count)
}
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes,
/// and whether a write also leaves the value on the stack
fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
use OpCode::*;
let i = match instr.imm {
ImmOperand::VarRef(i) => i as usize,
_ => 0,
};
match instr.op {
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => Some((instr.op as usize - GetVarRef0 as usize, false, false)),
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => Some((instr.op as usize - PutVarRef0 as usize, true, false)),
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => Some((instr.op as usize - SetVarRef0 as usize, true, true)),
GetVarRef | GetVarRefCheck => Some((i, false, false)),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)),
SetVarRef => Some((i, true, true)),
_ => None,
}
}
// ──────────────────────────────────────────────── Arg Access Processing ────────────────────────────────────────────────────────
/// Replace an access of declared param `k` with one of param `FIRST_ARG + k`,
/// and `this` with a read of its param
fn replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let (op, arg) = match (instr.op, instr.imm) {
(PushThis, _) => (GetArg, THIS_ARG),
(GetArg0, _) => (GetArg, FIRST_ARG),
(GetArg1, _) => (GetArg, FIRST_ARG + 1),
(GetArg2, _) => (GetArg, FIRST_ARG + 2),
(GetArg3, _) => (GetArg, FIRST_ARG + 3),
(PutArg0, _) => (PutArg, FIRST_ARG),
(PutArg1, _) => (PutArg, FIRST_ARG + 1),
(PutArg2, _) => (PutArg, FIRST_ARG + 2),
(PutArg3, _) => (PutArg, FIRST_ARG + 3),
(SetArg0, _) => (SetArg, FIRST_ARG),
(SetArg1, _) => (SetArg, FIRST_ARG + 1),
(SetArg2, _) => (SetArg, FIRST_ARG + 2),
(SetArg3, _) => (SetArg, FIRST_ARG + 3),
(GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k),
_ => return false,
};
pusher(instr.pc, out)(op, ImmOperand::Arg(arg));
true
}
// ───────────────────────────────────────────────── Misc Expansion ──────────────────────────────────────────────────────────────
/// Replace ops that are a fixed sequence of simpler ones
///
/// A sequence is never empty: jumps find their target by its pc (see regify),
/// so every instr must keep at least one instr at its pc
fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let none = ImmOperand::None;
let imm = instr.imm;
// A plain call has no `this`, so it passes `undefined`. It is pushed last,
// on top of the args, and the call pops it as one more arg (see regify)
let plain_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1))];
let tail_call = |n: u16| [(Undefined, none), (Call, ImmOperand::Npop(n + 1)), (Return, none)];
let seq: &[(OpCode, ImmOperand)] = match (instr.op, imm) {
(Call0, _) => &plain_call(0),
(Call1, _) => &plain_call(1),
(Call2, _) => &plain_call(2),
(Call3, _) => &plain_call(3),
(Call, ImmOperand::Npop(n)) => &plain_call(n),
(TailCall, ImmOperand::Npop(n)) => &tail_call(n),
(TailCallMethod, _) => &[(CallMethod, imm), (Return, none)],
(GetField2, _) => &[(Dup, none), (GetField, imm)],
(GetLoc0Loc1, _) => &[(GetLoc0, none), (GetLoc1, none)],
(IncLoc, _) => &[(GetLoc, imm), (Inc, none), (PutLoc, imm)],
(DecLoc, _) => &[(GetLoc, imm), (Dec, none), (PutLoc, imm)],
// The added value is on the stack, the local is the left operand
(AddLoc, _) => &[(GetLoc, imm), (Swap, none), (Add, none), (PutLoc, imm)],
(SetProto, _) => &[(DefineField, ImmOperand::Atom(PROTO_FIELD))],
// The low two bits of `arg`: 0 method, 1 getter, 2 setter
(DefineMethod, ImmOperand::AtomU8 { atom, arg }) => {
assert!(arg & 3 == 0, "getters and setters are not supported");
&[(DefineField, ImmOperand::Atom(atom))]
}
// The fn itself, which is its env
(SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))],
// [[HomeObject]], which only `super` reads. `super` is not supported
// (see set_home_object in regify)
(SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)],
// arguments, new.target, import.meta
(SpecialObject, _) => unimplemented!("special_object {imm:?}"),
_ => return false,
};
let mut push = pusher(instr.pc, out);
seq.iter().for_each(|&(op, imm)| push(op, imm));
true
}
/// Replace a read of a global with the constant it is (see `native`)
fn replace_get_var(
instr: &Instr,
atoms: &[Atom],
cpool: &mut Vec<JsValue>,
out: &mut Vec<Instr>,
) -> bool {
if instr.op != OpCode::GetVar {
return false;
}
cpool.push(native::global(instr.imm.atom(), atoms));
pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1));
true
}
+52
View File
@@ -0,0 +1,52 @@
use super::{PROTO_FIELD, PROTOTYPE_FIELD, pusher};
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
/// If `instr` is a `new`, the number of args it passes
pub fn is_new(instr: &Instr) -> Option<usize> {
match (instr.op, instr.imm) {
(OpCode::CallConstructor, ImmOperand::Npop(n)) => Some(n as usize),
_ => None,
}
}
/// Replace:
/// call_constructor n [F, new_target, a0 .. an-1]
///
/// With:
/// put_loc t(n-1) .. put_loc t0 the args, into temp locals, so F is on top
/// drop new_target, always a copy of F here
/// dup [F, F]
/// get_field prototype [F, proto]
/// object [F, proto, obj]
/// swap [F, obj, proto]
/// define_field __proto__ [F, obj], obj is { __proto__: F.prototype }
/// get_loc t0 .. get_loc t(n-1) the args back
/// call_constructor n [F, obj, a0 .. an-1], a method call of F on obj
///
/// Whether `new` gives the call's result or `obj` depends on types (see infer)
pub fn replace_new(instr: &Instr, temps: usize, out: &mut Vec<Instr>) -> bool {
let Some(n) = is_new(instr) else {
return false;
};
let mut push = pusher(instr.pc, out);
let temp = |i: usize| ImmOperand::Loc((temps + i) as u16);
for i in (0..n).rev() {
push(OpCode::PutLoc, temp(i));
}
push(OpCode::Drop, ImmOperand::None);
push(OpCode::Dup, ImmOperand::None);
push(OpCode::GetField, ImmOperand::Atom(PROTOTYPE_FIELD));
push(OpCode::Object, ImmOperand::None);
push(OpCode::Swap, ImmOperand::None);
push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD));
for i in 0..n {
push(OpCode::GetLoc, temp(i));
}
push(OpCode::CallConstructor, instr.imm);
true
}
+93 -51
View File
@@ -36,7 +36,7 @@ A representation is correct if no program can see a difference from these semant
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 value is an `any_t`:** a tagged union of the kinds. 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.
@@ -49,81 +49,121 @@ With no types and no analysis, this representation is correct for every program
## 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).
A type tells which values a place can hold at runtime. Infer computes one type for each value id in each variant (section 3.5).
### 3.1 The lattice
### 3.1 The types
A type is a set of kinds, with a refinement for each kind that is in the set:
A type is one of these:
| kind | refinement |
| type | values |
|---|---|
| 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 |
| `Unknown` | none: no value has reached the place yet |
| `Undefined` | undefined |
| `Null` | null |
| `Bool(v)` | a bool. `v` is the value, if it is known |
| `Int` | an int32 (provisional: the number type is not decided) |
| `Float` | a number |
| `Str(v)` | a string. `v` is the value, if it is known |
| `Obj(site)` | an object that `site` creates. A fn is an object whose site has code |
| `Sum(id)` | a value of one of the types in a list: `Int \| Str`, `Obj(A) \| Null`, `Obj(A) \| Obj(B)` |
| `Any` | any value. No rule gives it now (section 3.4) |
- **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.
**Join:**
```
Unknown * T = T
Any * T = Any
T * T = T
Bool(a) * Bool(b) = Bool, the value not known. The same for Str
Int * Float = Float
T * U = T | U
```
**Sums.** A sum is a list of the types above, except `Unknown`, `Sum` and `Any`. It has at most one type of each kind, except objects, which have one `Obj` for each site. A join of two sums is the union of their lists, where two types of one kind join by the rules above. So `Int | Str` joined with `Float` is `Float | Str`. Each list is kept once, by number, in the site table (`SiteTable`), which also holds a row for each site. There are finitely many kinds and sites, so there are finitely many sums.
**Members.** The members of a sum are the types in its list. The only member of any other type is the type itself, and `Unknown` has none. Every rule in infer works member by member, and joins the results (section 3.3).
### 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.
Every rule in infer must keep this invariant. Every rule is monotone, and a rule with an `Unknown` operand gives `Unknown`. Infer starts from `Unknown` everywhere and computes a fixed point of the rules.
### 3.3 Shapes
### 3.3 Objects
- **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.
- **A site** is an op that creates an object (`object`, `fclosure`), or a built-in fn. All objects of a site share one type for each field, for the whole program. Two sites are never merged: objects of two sites in one place give `Any`.
- **A field's type** is the join of every value written to it. A field can be absent, unless every object of the site gets it when it is created (section 7). A field that no write reaches is only absent.
- **A field read** on `Obj(site)` gives the own field's type. If the field can be absent, it also gives the read on the prototype (the `__proto__` field). The chain ends at a prototype that is absent or not an object, where the read gives `Undefined`. So a field that can be absent reads as its type `| Undefined`.
- **A field write** on `Obj(site)` joins the value's type into the site's field. So every value that holds an object of the site sees the write.
- **A primitive** has no own fields. A read on it reads its kind's prototype, which the stdlib links with `__link_proto_*`.
- **A field read or write on undefined or null** throws (section 8), so it gives `Unknown`.
- **An array** is an object too, of a site whose kind is "array": `array_from` makes it. Besides its fields, the site has an **element type**: the join of every value written to an element. So the type of an array is `Obj(site)`, and its site says what its elements can be (written `[T]` for short).
- `a[i]` gives `T | Undefined`: an index can be a hole, or past the end.
- `a[i] = x` joins the type of `x` into `T`. An index past the end makes the array longer, and the indexes between are holes.
- `a.length` is an `Int`, and it is not a field. A write of it truncates the array, or makes it longer with holes.
- An index is a number. Any other key is a computed field name (section 8).
- An array's prototype is the one that the stdlib links for arrays (`__link_proto_array`), as for a primitive kind. Its methods are JS in the stdlib, on top of the three ops above.
- **On a sum,** a read, a write or a call works on each member, and joins the results. So a read on `Obj(A) | Null` gives the read on `A`: the `Null` member throws, and gives nothing.
### 3.4 Calls
### 3.4 `Any`
- **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.
Since sums, no join gives `Any`, so no value has the type `Any`. The rules for it stay, so that it can be used again (for example, as a limit on the size of a sum).
Code that holds an `Any` does not know the site of an object in it. So infer tracks which objects can be in an `Any`:
- **The mark.** When a value of type `Obj(site)` goes into an `Any`, the site is marked `in_any`. A value goes into an `Any` in a join that gives `Any`, and when it is an operand or a result of a read, write or call on `Any`. The mark spreads to the sites of the objects in the site's fields, because code that holds the `Any` can read them.
- **A read on `Any`** reads the field on every `in_any` site and on every primitive prototype. What it reads goes into the `Any` that it gives.
- **A write on `Any`** writes the field on every `in_any` site, and sets their `written` flag (section 5).
- **A call on `Any`** calls the code of every `in_any` site that has code, in the variant whose args are all `Any`, except the env, which is the fn object itself (`Obj(site)`). It gives `Any`.
So every object that code with an `Any` can reach is of an `in_any` site, and the C can choose the code by the site in the object's header.
### 3.5 Calls
- **A call** of `Obj(site)` runs the site's code, with the fn object itself as its env. A call of a sum calls each `Obj` member with its own env, and joins the results. A call of a value that is not a fn throws (section 8). A call of `Any` is in section 3.4.
- **The program is closed.** Every fn comes from an `fclosure`, or it is a native. So the code of every site is known.
- **A variant** is one walk of a fn with one tuple of arg types. A known value does not cross a call (`Bool(v)` becomes `Bool`), so there are finitely many variants.
- **Rounds:** infer walks every variant from the entry again, until a round changes nothing in the fields, the flags, 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.
The C representation of a value is a function of its type and of the value rule (section 5) only. Each type has one:
| 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` |
| `Unknown` | the code cannot run |
| `Undefined`, `Null` | `undefined_t`, `null_t`: unit types |
| `Bool` | `bool` |
| `Int` | `int32_t` |
| `Float` | `double` |
| `Str` | `string_t` |
| `Obj(site)`, a value shape (section 5) | `S`, the struct of the site |
| `Obj(site)`, any other shape | `S *`, a pointer to the struct of the site |
| `Str`, plus `Undefined` and/or `Null` | `string_t`. `NULL` is null, and `JSRT_UNDEFINED` is undefined |
| `Obj(site)`, plus `Undefined` and/or `Null` | `S *`, with the same two sentinels |
| `Obj` of several sites, with or without `Undefined` and `Null` | `jsrt_header *`, a pointer to the object's header, with the same two sentinels |
| any other sum | `any_t`. It is the tagged union of every kind, so it holds every sum |
| `Any` | `any_t`, a tagged union. An object in it is the pointer to its cell |
- **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.
- **An array** is a pointer to the struct of its site, whose header is followed by `length`, `cap` and `items`: an array of `cap` elements, of the representation of `T | Undefined`. A hole holds undefined.
- **Absent** is a field state, not a value. A field that can be absent needs one more immediate for absent: a sentinel address for `string_t` and `S *`, a header tag for `S`, and an `any_t` tag for every other type.
- **Conversions.** A type grows in a join, so most conversions go to a representation that holds more: a member into its sum, or `int32_t` into `double`. These cannot fail. A conversion to a representation that holds less checks the value, and a failed check is a `TypeError` (section 8). For example, a `string_t` that can be undefined, passed to a native's string param.
- **An op on a sum** tests which member the value is: a kind test on an `any_t`, a sentinel test for null and undefined, and a `switch` on the site in the header for objects. Each case then works on one member, as for a single type. A member on which the op throws (a field read on null) has no case, so it throws.
- **A read, a write or a call on `any_t`** of type `Any` is a helper that emit writes: a `switch` on the site in the object's header, with one case for each `in_any` site (section 3.4). Every field name is known at compile time, so no hash map is needed. One comes with computed field names (section 8).
## 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.
**The flags.** Infer sets three flags on each site. Each flag only becomes true.
- **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.
- **written:** a field write that is not a creation write reaches an object of the site. 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. Regify turns a `define_field` into anything else (a class field on `this`, a static field on the class) into a `put_field`. So every `define_field` is a creation write, and every `put_field` is a write after creation.
- **compared:** an object of the site is an operand of `===`, `!==`, `==` or `!=`, and the other operand can also be an object (`Obj` or `Any`).
- **in_any:** an object of the site can be in an `Any` (section 3.4). Code that does not know the site can write it or compare it.
**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.
**Value shapes.** A site has a value shape if no flag is set, it is not an array, it is not a member of a sum (an element of an array is in the sum `T | Undefined`), and it does not contain itself through value fields. A sum is one C type for all its members, so its object members are pointers. A struct cannot contain itself, so a site that does stays a pointer. An object of an immutable site cannot hold itself, but objects of one site 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.
**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. In a field that can be absent, its header's site is the tag for absent (`JSRT_SITE_ABSENT`). It is not in a sum and not `in_any`, so it is never an `any_t` value. A runtime operation that only reads it (ToString, `<`) gets a heap copy.
**Default.** An object of any other shape is a pointer to a cell (`S *`).
@@ -161,16 +201,18 @@ A box is a cell, so the same rules apply to boxes.
| 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 |
| 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 can be absent (section 3.3) |
| 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 |
| a direct call | the callee's type is `Obj(site)`: its code is known | a call on `Any`: a `switch` on the site (section 3.4) |
## 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.
- narrowing in branches: in `if (x !== null) { x.f }`, `x` keeps its whole type. This is correct, because the `Null` member of the read throws, but the C still tests for null
- `throw`, `try`, `catch` and `finally`. These include the `TypeError`s above: a call of a value that is not a fn, and a failed check of a conversion from `any_t`.
- 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.
- typed arrays, `ArrayBuffer` and `DataView`
- iterators: `for…of`, spread and destructuring
- getters and setters
-105
View File
@@ -1,105 +0,0 @@
// 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);
+299
View File
@@ -0,0 +1,299 @@
// any_t: a value of any kind, and JS's operators on it
#pragma once
#include "number.h"
// A value of any kind. An object in it is the pointer to its cell, so it keeps
// its identity. A field can also hold absent
typedef enum {
JSRT_KIND_ABSENT,
JSRT_KIND_UNDEFINED,
JSRT_KIND_NULL,
JSRT_KIND_BOOL,
JSRT_KIND_INT,
JSRT_KIND_FLOAT,
JSRT_KIND_STRING,
JSRT_KIND_OBJECT,
} jsrt_kind;
typedef struct {
jsrt_kind kind;
union {
bool b;
int32_t i;
double f;
string_t s;
void *o;
} as;
} 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);
#ifdef JSRT_IMPLEMENTATION
#include <math.h>
// ── any: boxing ──
any_t __any_undefined(void) { return (any_t){ .kind = JSRT_KIND_UNDEFINED }; }
any_t __any_null(void) { return (any_t){ .kind = JSRT_KIND_NULL }; }
any_t __any_absent(void) { return (any_t){ .kind = JSRT_KIND_ABSENT }; }
any_t __any_from_bool(bool b) { return (any_t){ .kind = JSRT_KIND_BOOL, .as.b = b }; }
any_t __any_from_int(int32_t n) { return (any_t){ .kind = JSRT_KIND_INT, .as.i = n }; }
any_t __any_from_float(double x) { return (any_t){ .kind = JSRT_KIND_FLOAT, .as.f = x }; }
// A pointer that can be a sentinel, as the kind it stands for, else as `kind`
static any_t jsrt_from_ptr(void *p, jsrt_kind kind) {
if (p == NULL) return __any_null();
if (p == JSRT_UNDEFINED) return __any_undefined();
if (p == JSRT_ABSENT) return __any_absent();
return (any_t){ .kind = kind, .as.o = p };
}
any_t __any_from_string(string_t s) { return jsrt_from_ptr(s, JSRT_KIND_STRING); }
any_t __any_from_object(void *o) { return jsrt_from_ptr(o, JSRT_KIND_OBJECT); }
bool __any_as_bool(any_t a) {
if (a.kind != JSRT_KIND_BOOL) jsrt_type_error();
return a.as.b;
}
double __any_as_float(any_t a) {
if (a.kind == JSRT_KIND_INT) return a.as.i;
if (a.kind != JSRT_KIND_FLOAT) jsrt_type_error();
return a.as.f;
}
int32_t __any_as_int(any_t a) {
return a.kind == JSRT_KIND_INT ? a.as.i : (int32_t)__any_as_float(a);
}
// A pointer of kind `kind`, or the sentinel of a nullish or absent value
static void *jsrt_as_ptr(any_t a, jsrt_kind kind) {
switch (a.kind) {
case JSRT_KIND_NULL: return NULL;
case JSRT_KIND_UNDEFINED: return JSRT_UNDEFINED;
case JSRT_KIND_ABSENT: return JSRT_ABSENT;
default:
if (a.kind != kind) jsrt_type_error();
return a.as.o;
}
}
string_t __any_as_string(any_t a) { return jsrt_as_ptr(a, JSRT_KIND_STRING); }
void *__any_as_object(any_t a) { return jsrt_as_ptr(a, JSRT_KIND_OBJECT); }
// ── any: kinds ──
bool __any_is_absent(any_t a) { return a.kind == JSRT_KIND_ABSENT; }
bool __any_is_undefined(any_t a) { return a.kind == JSRT_KIND_UNDEFINED || a.kind == JSRT_KIND_ABSENT; }
bool __any_is_null(any_t a) { return a.kind == JSRT_KIND_NULL; }
bool __any_is_nullish(any_t a) { return __any_is_undefined(a) || __any_is_null(a); }
bool __any_is_boolean(any_t a) { return a.kind == JSRT_KIND_BOOL; }
bool __any_is_number(any_t a) { return a.kind == JSRT_KIND_INT || a.kind == JSRT_KIND_FLOAT; }
bool __any_is_string(any_t a) { return a.kind == JSRT_KIND_STRING; }
bool __any_is_object(any_t a) { return a.kind == JSRT_KIND_OBJECT; }
bool __any_is_function(any_t a) {
return a.kind == JSRT_KIND_OBJECT && jsrt_fn_sites[((jsrt_header *)a.as.o)->site];
}
// ── any: abstract operations ──
// ToNumber. An object would call its `valueOf` or `toString` first, which the
// runtime cannot do, so it gives NaN
static double jsrt_to_number(any_t a) {
switch (a.kind) {
case JSRT_KIND_NULL: return 0;
case JSRT_KIND_BOOL: return a.as.b;
case JSRT_KIND_INT: return a.as.i;
case JSRT_KIND_FLOAT: return a.as.f;
case JSRT_KIND_STRING: return __native_string_to_number(a.as.s);
default: return NAN;
}
}
bool __any_truthy(any_t a) {
switch (a.kind) {
case JSRT_KIND_BOOL: return a.as.b;
case JSRT_KIND_INT: return a.as.i != 0;
case JSRT_KIND_FLOAT: return a.as.f != 0 && !isnan(a.as.f);
case JSRT_KIND_STRING: return a.as.s->length != 0;
case JSRT_KIND_OBJECT: return true;
default: return false;
}
}
int32_t __any_to_int32(any_t a) {
double x = jsrt_to_number(a);
if (isnan(x) || isinf(x)) {
return 0;
}
x = fmod(trunc(x), 4294967296.0);
return (int32_t)(uint32_t)(int64_t)(x < 0 ? x + 4294967296.0 : x);
}
// ToString. An object would call its `toString`, which the runtime cannot do
string_t __any_to_string(any_t a) {
switch (a.kind) {
case JSRT_KIND_UNDEFINED: case JSRT_KIND_ABSENT: return jsrt_string_lit("undefined");
case JSRT_KIND_NULL: return jsrt_string_lit("null");
case JSRT_KIND_BOOL: return jsrt_string_lit(a.as.b ? "true" : "false");
case JSRT_KIND_INT: return __native_number_to_string(a.as.i);
case JSRT_KIND_FLOAT: return __native_number_to_string(a.as.f);
case JSRT_KIND_STRING: return a.as.s;
default: return jsrt_string_lit(__any_is_function(a) ? "function" : "[object Object]");
}
}
string_t __any_typeof(any_t a) {
switch (a.kind) {
case JSRT_KIND_UNDEFINED: case JSRT_KIND_ABSENT: return jsrt_string_lit("undefined");
case JSRT_KIND_BOOL: return jsrt_string_lit("boolean");
case JSRT_KIND_INT: case JSRT_KIND_FLOAT: return jsrt_string_lit("number");
case JSRT_KIND_STRING: return jsrt_string_lit("string");
case JSRT_KIND_OBJECT: return jsrt_string_lit(__any_is_function(a) ? "function" : "object");
default: return jsrt_string_lit("object");
}
}
// ── any: operators ──
any_t __any_add(any_t a, any_t b) {
// An object becomes a string (see `__any_to_string`), so it concatenates
bool strings = a.kind == JSRT_KIND_STRING || b.kind == JSRT_KIND_STRING
|| a.kind == JSRT_KIND_OBJECT || b.kind == JSRT_KIND_OBJECT;
if (strings) {
return __any_from_string(__native_string_concat(__any_to_string(a), __any_to_string(b)));
}
return __any_from_float(jsrt_to_number(a) + jsrt_to_number(b));
}
any_t __any_sub(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) - jsrt_to_number(b)); }
any_t __any_mul(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) * jsrt_to_number(b)); }
any_t __any_div(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) / jsrt_to_number(b)); }
any_t __any_mod(any_t a, any_t b) { return __any_from_float(fmod(jsrt_to_number(a), jsrt_to_number(b))); }
any_t __any_pow(any_t a, any_t b) { return __any_from_float(pow(jsrt_to_number(a), jsrt_to_number(b))); }
any_t __any_neg(any_t a) { return __any_from_float(-jsrt_to_number(a)); }
any_t __any_plus(any_t a) { return __any_from_float(jsrt_to_number(a)); }
any_t __any_inc(any_t a) { return __any_from_float(jsrt_to_number(a) + 1); }
any_t __any_dec(any_t a) { return __any_from_float(jsrt_to_number(a) - 1); }
// ── any: comparisons ──
// Two strings compare by code units. Anything else compares as numbers, where
// NaN makes every comparison false
bool __any_lt(any_t a, any_t b) {
bool strings = a.kind == JSRT_KIND_STRING && b.kind == JSRT_KIND_STRING;
return strings ? jsrt_string_cmp(a.as.s, b.as.s) < 0 : jsrt_to_number(a) < jsrt_to_number(b);
}
bool __any_lte(any_t a, any_t b) {
bool strings = a.kind == JSRT_KIND_STRING && b.kind == JSRT_KIND_STRING;
return strings ? jsrt_string_cmp(a.as.s, b.as.s) <= 0 : jsrt_to_number(a) <= jsrt_to_number(b);
}
bool __any_gt(any_t a, any_t b) { return __any_lt(b, a); }
bool __any_gte(any_t a, any_t b) { return __any_lte(b, a); }
bool __any_strict_eq(any_t a, any_t b) {
if (__any_is_number(a) && __any_is_number(b)) {
return jsrt_to_number(a) == jsrt_to_number(b);
}
if (__any_is_undefined(a) || __any_is_undefined(b)) {
return __any_is_undefined(a) && __any_is_undefined(b);
}
if (a.kind != b.kind) {
return false;
}
switch (a.kind) {
case JSRT_KIND_NULL: return true;
case JSRT_KIND_BOOL: return a.as.b == b.as.b;
case JSRT_KIND_STRING: return jsrt_string_eq(a.as.s, b.as.s);
default: return a.as.o == b.as.o;
}
}
bool __any_strict_neq(any_t a, any_t b) { return !__any_strict_eq(a, b); }
// Undefined and null equal each other and nothing else. Values of one kind,
// and objects, compare as `===` does. Other primitives compare as numbers
bool __any_eq(any_t a, any_t b) {
if (__any_is_nullish(a) || __any_is_nullish(b)) {
return __any_is_nullish(a) && __any_is_nullish(b);
}
bool same = a.kind == b.kind || (__any_is_number(a) && __any_is_number(b));
if (same || a.kind == JSRT_KIND_OBJECT || b.kind == JSRT_KIND_OBJECT) {
return __any_strict_eq(a, b);
}
return jsrt_to_number(a) == jsrt_to_number(b);
}
bool __any_neq(any_t a, any_t b) { return !__any_eq(a, b); }
#endif // JSRT_IMPLEMENTATION
+95
View File
@@ -0,0 +1,95 @@
// Values and objects: the types that every other part uses, the object
// header and its tags, the allocator and errors. See docs/model.md
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct { char _; } undefined_t;
typedef struct { char _; } null_t;
// A string: UTF-16 code units, as JS counts them
typedef struct jsrt_string *string_t;
// 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
// Whether the objects of each site are fns, by site. The emitted C defines it
extern const bool jsrt_fn_sites[];
// A new heap cell of `size` bytes, all zero
void *jsrt_alloc(size_t size);
// `items`, an array's elements of `size` bytes each, with room for at least
// `need` of them. `*cap` is the room that it has, and is set to the new room
void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size);
// Throws a TypeError. Exceptions are not supported yet, so it stops the program
_Noreturn void jsrt_type_error(void);
// `p`, a pointer to an object or a string. A sentinel for null, undefined or
// absent throws a TypeError
void *jsrt_some(void *p);
#ifdef JSRT_IMPLEMENTATION
#include <stdio.h>
#include <stdlib.h>
jsrt_header jsrt_undefined_cell = { JSRT_SITE_UNDEFINED };
jsrt_header jsrt_absent_cell = { JSRT_SITE_ABSENT };
void *jsrt_alloc(size_t size) {
void *p = calloc(1, size);
if (!p) {
fputs("jsrt: out of memory\n", stderr);
abort();
}
return p;
}
void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size) {
if (need <= *cap) {
return items;
}
int32_t room = *cap * 2 > need ? *cap * 2 : need < 4 ? 4 : need;
items = realloc(items, (size_t)room * size);
if (!items) {
fputs("jsrt: out of memory\n", stderr);
abort();
}
*cap = room;
return items;
}
_Noreturn void jsrt_type_error(void) {
fputs("TypeError\n", stderr);
exit(1);
}
void *jsrt_some(void *p) {
if (p == NULL || p == JSRT_UNDEFINED || p == JSRT_ABSENT) {
jsrt_type_error();
}
return p;
}
#endif // JSRT_IMPLEMENTATION
+455
View File
@@ -0,0 +1,455 @@
// IO natives. `__native_print_string` writes at once. The rest are async, on
// io_uring (see uring.h)
//
// A `__native_io_queue_<op>` queues an op and gives its handle. It makes no
// syscall. `__native_io_wait` puts queued ops into the SQ, submits them,
// waits until one op is done, and gives its handle. The getters then read
// that op's result, until the next wait
//
// At most `cq_entries` ops are in flight, so the CQ cannot overflow. The rest
// stay queued until a later wait. So a queue never fails, and the program
// never sees a limit
//
// The ring is made at the first wait that has ops, so a program that does no
// async IO never makes it
#pragma once
#include "str.h"
#ifdef JSRT_IMPLEMENTATION
#include <errno.h>
#include <fcntl.h>
#include <linux/stat.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "uring.h"
// ── sync ──
undefined_t __native_print_string(string_t s) {
char *utf8 = jsrt_string_utf8(s);
puts(utf8);
free(utf8);
return (undefined_t){0};
}
// ── ops ──
typedef enum {
JSRT_IO_TIMEOUT,
JSRT_IO_OPEN,
JSRT_IO_READ,
JSRT_IO_WRITE,
JSRT_IO_CLOSE,
JSRT_IO_STAT,
JSRT_IO_CANCEL,
} jsrt_io_kind;
typedef enum {
JSRT_IO_FREE, // in the free list
JSRT_IO_QUEUED, // in the backlog, not in the SQ yet
JSRT_IO_FLIGHT, // in the SQ or in the kernel
JSRT_IO_DONE, // in the done list, or the last op a wait gave
} jsrt_io_state;
// An op. Its handle is its index in the table, and its SQE's user_data
//
// The table moves when it grows, which only a queue does. So the kernel
// must not hold a pointer into it after a submit returns: `ts` is read
// before (SUBMIT_STABLE), and everything that the kernel reads or writes
// later is in `buf`, which does not move
typedef struct {
uint8_t kind, state;
bool cancelled; // cancelled while queued: never goes to the SQ
uint32_t gen; // counts reuses of this slot (see CANCEL)
int32_t next; // the next op in its list, or -1
int32_t res; // the CQE's res: >= 0 on success, -errno on failure
int32_t fd; // READ, WRITE, CLOSE
int32_t flags, mode; // OPEN
uint32_t len; // READ, WRITE: the size of `buf`
uint64_t off; // READ, WRITE: -1 is the file's position
int32_t target; // CANCEL: the op to cancel...
uint32_t target_gen; // ...while it is still this use of its slot
struct __kernel_timespec ts; // TIMEOUT
void *buf; // OPEN: path. READ, WRITE: data. STAT: statx, then path
} jsrt_io_op;
// A FIFO of ops, linked through `next`
typedef struct { int32_t head, tail; } jsrt_io_list;
static struct {
jsrt_uring ring;
bool ring_ready;
unsigned in_flight;
jsrt_io_op *ops;
int32_t cap;
int32_t free; // a stack, linked through `next`
jsrt_io_list backlog, done;
int32_t given; // the op the last wait gave, or -1
} jsrt_io = {
.free = -1,
.backlog = { -1, -1 },
.done = { -1, -1 },
.given = -1,
};
static void jsrt_io_push(jsrt_io_list *l, int32_t h) {
jsrt_io.ops[h].next = -1;
if (l->tail < 0) {
l->head = h;
} else {
jsrt_io.ops[l->tail].next = h;
}
l->tail = h;
}
static int32_t jsrt_io_pop(jsrt_io_list *l) {
int32_t h = l->head;
if (h >= 0) {
l->head = jsrt_io.ops[h].next;
if (l->head < 0) {
l->tail = -1;
}
}
return h;
}
// A new op of `kind`, not in any list yet
static int32_t jsrt_io_new(jsrt_io_kind kind) {
if (jsrt_io.free < 0) {
int32_t cap = jsrt_io.cap ? jsrt_io.cap * 2 : 64;
jsrt_io_op *ops = realloc(jsrt_io.ops, (size_t)cap * sizeof *ops);
if (!ops) {
fputs("jsrt: out of memory\n", stderr);
abort();
}
// The new slots go on the free stack, the lowest on top
for (int32_t h = cap - 1; h >= jsrt_io.cap; h--) {
ops[h] = (jsrt_io_op){ .state = JSRT_IO_FREE, .next = jsrt_io.free };
jsrt_io.free = h;
}
jsrt_io.ops = ops;
jsrt_io.cap = cap;
}
int32_t h = jsrt_io.free;
jsrt_io_op *op = &jsrt_io.ops[h];
jsrt_io.free = op->next;
*op = (jsrt_io_op){ .kind = kind, .state = JSRT_IO_QUEUED, .gen = op->gen + 1, .next = -1 };
return h;
}
static int32_t jsrt_io_queue(int32_t h) {
jsrt_io_push(&jsrt_io.backlog, h);
return h;
}
// Ends op `h` with `res`, without the kernel
static int32_t jsrt_io_finish(int32_t h, int32_t res) {
jsrt_io.ops[h].state = JSRT_IO_DONE;
jsrt_io.ops[h].res = res;
jsrt_io_push(&jsrt_io.done, h);
return h;
}
static void jsrt_io_release(int32_t h) {
jsrt_io_op *op = &jsrt_io.ops[h];
free(op->buf);
op->buf = NULL;
op->state = JSRT_IO_FREE;
op->next = jsrt_io.free;
jsrt_io.free = h;
}
// The op that the last wait gave. A handle of any other op is an error
static jsrt_io_op *jsrt_io_given(int32_t h) {
if (h < 0 || h != jsrt_io.given) {
jsrt_type_error();
}
return &jsrt_io.ops[h];
}
// 0 if op `h` still has work for the kernel, else the -errno it ends with
//
// An op that was cancelled while queued has none. Nor has a cancel whose
// target ended, because then the target's slot can hold a new op
static int32_t jsrt_io_dead(int32_t h) {
jsrt_io_op *op = &jsrt_io.ops[h];
if (op->cancelled) {
return -ECANCELED;
}
if (op->kind == JSRT_IO_CANCEL) {
jsrt_io_op *t = &jsrt_io.ops[op->target];
if (t->gen != op->target_gen || t->state != JSRT_IO_FLIGHT) {
return -ENOENT;
}
}
return 0;
}
static void jsrt_io_prep(struct io_uring_sqe *sqe, int32_t h) {
jsrt_io_op *op = &jsrt_io.ops[h];
sqe->user_data = (uint64_t)h;
switch (op->kind) {
case JSRT_IO_TIMEOUT:
// A plain timer: `off` 0 means that no count of CQEs ends it
sqe->opcode = IORING_OP_TIMEOUT;
sqe->fd = -1;
sqe->addr = (uint64_t)(uintptr_t)&op->ts;
sqe->len = 1;
break;
case JSRT_IO_OPEN:
sqe->opcode = IORING_OP_OPENAT;
sqe->fd = AT_FDCWD;
sqe->addr = (uint64_t)(uintptr_t)op->buf;
sqe->len = (uint32_t)op->mode;
sqe->open_flags = (uint32_t)op->flags;
break;
case JSRT_IO_READ:
case JSRT_IO_WRITE:
sqe->opcode = op->kind == JSRT_IO_READ ? IORING_OP_READ : IORING_OP_WRITE;
sqe->fd = op->fd;
sqe->addr = (uint64_t)(uintptr_t)op->buf;
sqe->len = op->len;
sqe->off = op->off;
break;
case JSRT_IO_CLOSE:
sqe->opcode = IORING_OP_CLOSE;
sqe->fd = op->fd;
break;
case JSRT_IO_STAT:
sqe->opcode = IORING_OP_STATX;
sqe->fd = AT_FDCWD;
sqe->addr = (uint64_t)(uintptr_t)((char *)op->buf + sizeof(struct statx));
sqe->len = STATX_SIZE;
sqe->addr2 = (uint64_t)(uintptr_t)op->buf;
break;
case JSRT_IO_CANCEL:
// A timeout is removed with its own op
sqe->opcode = jsrt_io.ops[op->target].kind == JSRT_IO_TIMEOUT
? IORING_OP_TIMEOUT_REMOVE
: IORING_OP_ASYNC_CANCEL;
sqe->fd = -1;
sqe->addr = (uint64_t)op->target;
break;
}
}
// Moves queued ops into the SQ while fewer than `cq_entries` are in flight,
// and submits them
static void jsrt_io_drain(void) {
if (jsrt_io.backlog.head < 0) {
return;
}
jsrt_uring *r = &jsrt_io.ring;
if (!jsrt_io.ring_ready) {
jsrt_uring_init(r, 128);
jsrt_io.ring_ready = true;
}
while (jsrt_io.backlog.head >= 0 && jsrt_io.in_flight < r->cq_entries) {
int32_t h = jsrt_io.backlog.head;
int32_t dead = jsrt_io_dead(h);
if (dead) {
jsrt_io_pop(&jsrt_io.backlog);
jsrt_io_finish(h, dead);
continue;
}
struct io_uring_sqe *sqe = jsrt_uring_sqe(r);
// The SQ is full: a submit empties it
if (!sqe) {
jsrt_uring_submit(r, false);
continue;
}
jsrt_io_pop(&jsrt_io.backlog);
jsrt_io_prep(sqe, h);
jsrt_io.ops[h].state = JSRT_IO_FLIGHT;
jsrt_io.in_flight++;
}
// No SQE may stay unsubmitted when the wait returns, because a queue can
// then move the table (see jsrt_io_op)
jsrt_uring_submit(r, false);
}
// ── natives ──
int32_t __native_io_queue_timeout(double ms) {
int32_t h = jsrt_io_new(JSRT_IO_TIMEOUT);
if (!(ms > 0)) {
ms = 0;
}
jsrt_io.ops[h].ts.tv_sec = (int64_t)(ms / 1000);
jsrt_io.ops[h].ts.tv_nsec = (int64_t)(fmod(ms, 1000) * 1e6);
return jsrt_io_queue(h);
}
// `flags` and `mode` are open(2)'s, as numbers
int32_t __native_io_queue_open(string_t path, int32_t flags, int32_t mode) {
int32_t h = jsrt_io_new(JSRT_IO_OPEN);
jsrt_io.ops[h].buf = jsrt_string_utf8(path);
jsrt_io.ops[h].flags = flags | O_CLOEXEC;
jsrt_io.ops[h].mode = mode;
return jsrt_io_queue(h);
}
// Reads at most `len` bytes at `offset`, or at the file's position if
// `offset` is -1. The status is the number of bytes read
int32_t __native_io_queue_read(int32_t fd, int32_t len, double offset) {
int32_t h = jsrt_io_new(JSRT_IO_READ);
jsrt_io_op *op = &jsrt_io.ops[h];
op->fd = fd;
op->len = len > 0 ? (uint32_t)len : 0;
op->off = offset < 0 ? (uint64_t)-1 : (uint64_t)offset;
op->buf = jsrt_alloc(op->len ? op->len : 1);
return jsrt_io_queue(h);
}
// Writes `data` as UTF-8, at `offset` as for a read. The status is the number
// of bytes written, which can be fewer than all
int32_t __native_io_queue_write(int32_t fd, string_t data, double offset) {
int32_t h = jsrt_io_new(JSRT_IO_WRITE);
jsrt_io_op *op = &jsrt_io.ops[h];
op->fd = fd;
op->buf = jsrt_string_utf8(data);
op->len = (uint32_t)strlen(op->buf);
op->off = offset < 0 ? (uint64_t)-1 : (uint64_t)offset;
return jsrt_io_queue(h);
}
int32_t __native_io_queue_close(int32_t fd) {
int32_t h = jsrt_io_new(JSRT_IO_CLOSE);
jsrt_io.ops[h].fd = fd;
return jsrt_io_queue(h);
}
// Gives the file's size, as `__native_io_result_number`
int32_t __native_io_queue_stat(string_t path) {
int32_t h = jsrt_io_new(JSRT_IO_STAT);
char *utf8 = jsrt_string_utf8(path);
size_t size = strlen(utf8) + 1;
char *buf = jsrt_alloc(sizeof(struct statx) + size);
memcpy(buf + sizeof(struct statx), utf8, size);
free(utf8);
jsrt_io.ops[h].buf = buf;
return jsrt_io_queue(h);
}
// Cancels op `target`, which then ends with -ECANCELED if it did not end
// first. The cancel's own status is 0 if it found the op, else -ENOENT or
// -EALREADY
int32_t __native_io_queue_cancel(int32_t target) {
if (target < 0 || target >= jsrt_io.cap) {
jsrt_type_error();
}
int32_t h = jsrt_io_new(JSRT_IO_CANCEL);
jsrt_io_op *t = &jsrt_io.ops[target];
switch (t->state) {
case JSRT_IO_QUEUED:
t->cancelled = true;
return jsrt_io_finish(h, 0);
case JSRT_IO_FLIGHT:
jsrt_io.ops[h].target = target;
jsrt_io.ops[h].target_gen = t->gen;
return jsrt_io_queue(h);
default:
return jsrt_io_finish(h, -ENOENT);
}
}
// The handle of an op that is done, or -1 if no op is queued or in flight.
// The op that the last wait gave is freed now
int32_t __native_io_wait(void) {
if (jsrt_io.given >= 0) {
jsrt_io_release(jsrt_io.given);
jsrt_io.given = -1;
}
jsrt_io_drain();
int32_t h = jsrt_io_pop(&jsrt_io.done);
while (h < 0) {
if (jsrt_io.in_flight == 0) {
return -1;
}
uint64_t user_data;
int32_t res;
if (!jsrt_uring_cqe(&jsrt_io.ring, &user_data, &res)) {
jsrt_uring_submit(&jsrt_io.ring, true);
continue;
}
jsrt_io.in_flight--;
h = (int32_t)user_data;
// A timer that runs out ends with -ETIME, which is its success
bool timer = jsrt_io.ops[h].kind == JSRT_IO_TIMEOUT;
jsrt_io.ops[h].state = JSRT_IO_DONE;
jsrt_io.ops[h].res = timer && res == -ETIME ? 0 : res;
}
jsrt_io.given = h;
return h;
}
// >= 0 if the op succeeded (a count of bytes, an fd, or 0), else -errno
int32_t __native_io_status(int32_t h) {
return jsrt_io_given(h)->res;
}
// The data of a read, decoded as UTF-8. Empty for any other op, or a failure
string_t __native_io_result_string(int32_t h) {
jsrt_io_op *op = jsrt_io_given(h);
bool data = op->kind == JSRT_IO_READ && op->res > 0;
return jsrt_string_from_utf8(op->buf, data ? (size_t)op->res : 0);
}
// The size of a stat's file. The status for any other op
double __native_io_result_number(int32_t h) {
jsrt_io_op *op = jsrt_io_given(h);
if (op->kind == JSRT_IO_STAT && op->res >= 0) {
return (double)((struct statx *)op->buf)->stx_size;
}
return op->res;
}
#endif // JSRT_IMPLEMENTATION
+26
View File
@@ -0,0 +1,26 @@
// The runtime that emitted C is compiled with. See docs/model.md for the
// representation of values
//
// Each header declares its part, and if JSRT_IMPLEMENTATION is defined before
// this is included, also defines it. The emitted C is one file, which does
// that, and includes this before any other header. The natives are declared
// by the emitted C itself, from compiler/src/native.rs, and defined here
//
// core.h values, the object header, the allocator, errors
// str.h strings and the string natives
// number.h the number natives
// any.h any_t and JS's operators on it
// io.h IO natives, async on io_uring (uring.h)
#pragma once
// io.h needs Linux's syscall() and mmap(), which -std=c11 hides. This must
// come before the first system header
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "core.h"
#include "str.h"
#include "number.h"
#include "any.h"
#include "io.h"
+93
View File
@@ -0,0 +1,93 @@
// The number natives: conversions between numbers and strings
#pragma once
#include "str.h"
#ifdef JSRT_IMPLEMENTATION
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// JS's Number::toString: the fewest digits that give the number back. Plain
// notation while the decimal point is within 21 places, else exponential
string_t __native_number_to_string(double x) {
if (isnan(x)) return jsrt_string_lit("NaN");
if (isinf(x)) return jsrt_string_lit(x < 0 ? "-Infinity" : "Infinity");
if (x == 0) return jsrt_string_lit("0");
// The fewest digits: buf is [-]d[.ddd]e±xx
char buf[64];
for (int precision = 0; precision < 17; precision++) {
snprintf(buf, sizeof buf, "%.*e", precision, x);
if (strtod(buf, NULL) == x) {
break;
}
}
char digits[32], out[64];
char *p = buf, *o = out;
int n = 0;
if (*p == '-') {
*o++ = *p++;
}
for (; *p != 'e'; p++) {
if (*p != '.') {
digits[n++] = *p;
}
}
digits[n] = 0;
// The decimal point goes after the first `point` digits
int point = atoi(p + 1) + 1;
if (n <= point && point <= 21) {
o += sprintf(o, "%s", digits);
for (int i = n; i < point; i++) *o++ = '0';
*o = 0;
} else if (0 < point && point <= 21) {
sprintf(o, "%.*s.%s", point, digits, digits + point);
} else if (-6 < point && point <= 0) {
o += sprintf(o, "0.");
for (int i = point; i < 0; i++) *o++ = '0';
sprintf(o, "%s", digits);
} else {
int e = point - 1;
sprintf(o, "%c%s%se%c%d", digits[0], n > 1 ? "." : "", digits + 1, e < 0 ? '-' : '+', abs(e));
}
return jsrt_string_lit(out);
}
string_t __native_number_to_fixed(double x, int32_t digits) {
char buf[400];
snprintf(buf, sizeof buf, "%.*f", digits, x);
return jsrt_string_lit(buf);
}
// JS's ToNumber on a string: a decimal number with whitespace around it, or
// only whitespace, which is 0. Anything else is NaN
double __native_string_to_number(string_t s) {
char *utf8 = jsrt_string_utf8(s);
char *start = utf8, *end;
while (*start == ' ' || (*start >= '\t' && *start <= '\r')) start++;
double x = 0;
if (*start) {
x = strtod(start, &end);
while (*end == ' ' || (*end >= '\t' && *end <= '\r')) end++;
if (*end || start == end) x = NAN;
}
free(utf8);
return x;
}
#endif // JSRT_IMPLEMENTATION
+155
View File
@@ -0,0 +1,155 @@
// Strings: UTF-16 code units, as JS counts them, and the string natives
#pragma once
#include "core.h"
string_t jsrt_string_lit(const char *utf8);
string_t jsrt_string_from_utf8(const char *utf8, size_t size);
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);
// `s[i]`: the code unit at `i`, as a string, or JSRT_UNDEFINED past the end
string_t jsrt_string_at(string_t s, int32_t i);
#ifdef JSRT_IMPLEMENTATION
#include <stdlib.h>
#include <string.h>
struct jsrt_string {
int32_t length;
uint16_t units[];
};
static string_t jsrt_string_new(int32_t length) {
string_t s = jsrt_alloc(sizeof *s + (size_t)length * sizeof(uint16_t));
s->length = length;
return s;
}
string_t jsrt_string_lit(const char *utf8) {
return jsrt_string_from_utf8(utf8, strlen(utf8));
}
// The `size` bytes at `utf8`, which need not end in a 0
string_t jsrt_string_from_utf8(const char *utf8, size_t size) {
const unsigned char *p = (const unsigned char *)utf8, *end = p + size;
string_t s = jsrt_string_new((int32_t)size * 2);
int32_t n = 0;
while (p < end) {
uint32_t c = *p++;
int extra = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 0;
c &= extra ? 0x3F >> extra : 0x7F;
for (; extra && p < end; extra--) {
c = (c << 6) | (*p++ & 0x3F);
}
if (c >= 0x10000) {
c -= 0x10000;
s->units[n++] = (uint16_t)(0xD800 + (c >> 10));
s->units[n++] = (uint16_t)(0xDC00 + (c & 0x3FF));
} else {
s->units[n++] = (uint16_t)c;
}
}
s->length = n;
return s;
}
int32_t jsrt_string_length(string_t s) {
return s->length;
}
bool jsrt_string_eq(string_t a, string_t b) {
return a->length == b->length && !memcmp(a->units, b->units, (size_t)a->length * sizeof(uint16_t));
}
int32_t jsrt_string_cmp(string_t a, string_t b) {
int32_t n = a->length < b->length ? a->length : b->length;
for (int32_t i = 0; i < n; i++) {
if (a->units[i] != b->units[i]) {
return a->units[i] < b->units[i] ? -1 : 1;
}
}
return a->length < b->length ? -1 : a->length > b->length;
}
string_t jsrt_string_at(string_t s, int32_t i) {
if (i < 0 || i >= s->length) {
return JSRT_UNDEFINED;
}
string_t c = jsrt_string_new(1);
c->units[0] = s->units[i];
return c;
}
// `s` as UTF-8, in a new buffer for the caller to free
static char *jsrt_string_utf8(string_t s) {
char *utf8 = jsrt_alloc((size_t)s->length * 3 + 1);
char *out = utf8;
for (int32_t i = 0; i < s->length; i++) {
uint32_t c = s->units[i];
bool pair = c >= 0xD800 && c < 0xDC00 && i + 1 < s->length
&& s->units[i + 1] >= 0xDC00 && s->units[i + 1] < 0xE000;
if (pair) {
c = 0x10000 + ((c - 0xD800) << 10) + (s->units[++i] - 0xDC00);
}
if (c < 0x80) {
*out++ = (char)c;
} else if (c < 0x800) {
*out++ = (char)(0xC0 | c >> 6);
*out++ = (char)(0x80 | (c & 0x3F));
} else if (c < 0x10000) {
*out++ = (char)(0xE0 | c >> 12);
*out++ = (char)(0x80 | (c >> 6 & 0x3F));
*out++ = (char)(0x80 | (c & 0x3F));
} else {
*out++ = (char)(0xF0 | c >> 18);
*out++ = (char)(0x80 | (c >> 12 & 0x3F));
*out++ = (char)(0x80 | (c >> 6 & 0x3F));
*out++ = (char)(0x80 | (c & 0x3F));
}
}
*out = 0;
return utf8;
}
// ── natives ──
string_t __native_string_concat(string_t a, string_t b) {
string_t s = jsrt_string_new(a->length + b->length);
memcpy(s->units, a->units, (size_t)a->length * sizeof(uint16_t));
memcpy(s->units + a->length, b->units, (size_t)b->length * sizeof(uint16_t));
return s;
}
string_t __native_string_slice(string_t s, int32_t start, int32_t end) {
start = start < 0 ? 0 : start > s->length ? s->length : start;
end = end < start ? start : end > s->length ? s->length : end;
string_t r = jsrt_string_new(end - start);
memcpy(r->units, s->units + start, (size_t)(end - start) * sizeof(uint16_t));
return r;
}
int32_t __native_string_char_code_at(string_t s, int32_t i) {
return s->units[i];
}
string_t __native_string_from_char_code(int32_t code) {
string_t s = jsrt_string_new(1);
s->units[0] = (uint16_t)code;
return s;
}
#endif // JSRT_IMPLEMENTATION
+158
View File
@@ -0,0 +1,158 @@
// A minimal io_uring ring, on the raw syscalls. Only io.h uses it
//
// We write SQEs into the SQ and the kernel takes them when we submit. The
// kernel writes CQEs into the CQ and we take them. Each side moves only its
// own index, and reads the other's with acquire, so it sees the entries that
// came before
#pragma once
#ifdef JSRT_IMPLEMENTATION
#include <errno.h>
#include <linux/io_uring.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
typedef struct {
int fd;
// The SQ: we own the tail, the kernel owns the head. `array` maps each
// slot of the ring to an SQE, and we keep it the identity
unsigned *sq_head, *sq_tail, *sq_array;
unsigned sq_mask, sq_entries;
struct io_uring_sqe *sqes;
unsigned tail; // our copy of the SQ tail, ahead of it until submit
unsigned to_submit; // SQEs written that the kernel did not take yet
// The CQ: the kernel owns the tail, we own the head
unsigned *cq_head, *cq_tail;
unsigned cq_mask, cq_entries;
struct io_uring_cqe *cqes;
} jsrt_uring;
_Noreturn static void jsrt_uring_fail(const char *what) {
fprintf(stderr, "jsrt: %s: %s\n", what, strerror(errno));
exit(1);
}
// The features that io.h depends on, all from Linux 5.5:
// SINGLE_MMAP the SQ and the CQ rings are one mapping
// NODROP the kernel keeps CQEs that do not fit the CQ, never drops them
// SUBMIT_STABLE the kernel copies what an SQE points to (a timespec, a path)
// before submit returns, so it needs to stay only until then
static void jsrt_uring_init(jsrt_uring *r, unsigned entries) {
struct io_uring_params p;
memset(&p, 0, sizeof p);
int fd = (int)syscall(__NR_io_uring_setup, entries, &p);
if (fd < 0) {
jsrt_uring_fail("io_uring_setup");
}
unsigned need = IORING_FEAT_SINGLE_MMAP | IORING_FEAT_NODROP | IORING_FEAT_SUBMIT_STABLE;
if ((p.features & need) != need) {
fputs("jsrt: io_uring: the kernel is older than Linux 5.5\n", stderr);
exit(1);
}
size_t sq_size = p.sq_off.array + p.sq_entries * sizeof(unsigned);
size_t cq_size = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe);
size_t size = sq_size > cq_size ? sq_size : cq_size;
int prot = PROT_READ | PROT_WRITE, flags = MAP_SHARED | MAP_POPULATE;
char *ring = mmap(NULL, size, prot, flags, fd, IORING_OFF_SQ_RING);
void *sqes = mmap(NULL, p.sq_entries * sizeof(struct io_uring_sqe), prot, flags, fd, IORING_OFF_SQES);
if (ring == MAP_FAILED || sqes == MAP_FAILED) {
jsrt_uring_fail("io_uring mmap");
}
*r = (jsrt_uring){
.fd = fd,
.sq_head = (unsigned *)(ring + p.sq_off.head),
.sq_tail = (unsigned *)(ring + p.sq_off.tail),
.sq_array = (unsigned *)(ring + p.sq_off.array),
.sq_mask = *(unsigned *)(ring + p.sq_off.ring_mask),
.sq_entries = p.sq_entries,
.sqes = sqes,
.tail = *(unsigned *)(ring + p.sq_off.tail),
.cq_head = (unsigned *)(ring + p.cq_off.head),
.cq_tail = (unsigned *)(ring + p.cq_off.tail),
.cq_mask = *(unsigned *)(ring + p.cq_off.ring_mask),
.cq_entries = p.cq_entries,
.cqes = (struct io_uring_cqe *)(ring + p.cq_off.cqes),
};
}
// A new SQE, all zero, or NULL if the SQ is full. The kernel sees it at the
// next submit
static struct io_uring_sqe *jsrt_uring_sqe(jsrt_uring *r) {
unsigned head = __atomic_load_n(r->sq_head, __ATOMIC_ACQUIRE);
if (r->tail - head == r->sq_entries) {
return NULL;
}
unsigned i = r->tail & r->sq_mask;
r->sq_array[i] = i;
r->tail++;
r->to_submit++;
memset(&r->sqes[i], 0, sizeof r->sqes[i]);
return &r->sqes[i];
}
static bool jsrt_uring_cq_empty(jsrt_uring *r) {
return *r->cq_head == __atomic_load_n(r->cq_tail, __ATOMIC_ACQUIRE);
}
// Submits every SQE written, and if `wait`, waits until the CQ has a CQE
//
// The kernel can take fewer SQEs than we give, for example when one fails
// before it starts. That one still gets a CQE, and we give the rest again
static void jsrt_uring_submit(jsrt_uring *r, bool wait) {
__atomic_store_n(r->sq_tail, r->tail, __ATOMIC_RELEASE);
for (;;) {
bool get = wait && jsrt_uring_cq_empty(r);
if (!r->to_submit && !get) {
return;
}
unsigned flags = get ? IORING_ENTER_GETEVENTS : 0;
int n = (int)syscall(__NR_io_uring_enter, r->fd, r->to_submit, get ? 1 : 0, flags, NULL, 0);
if (n < 0) {
if (errno == EINTR) {
continue;
}
jsrt_uring_fail("io_uring_enter");
}
r->to_submit -= (unsigned)n;
}
}
// Takes the next CQE, if there is one
static bool jsrt_uring_cqe(jsrt_uring *r, uint64_t *user_data, int32_t *res) {
if (jsrt_uring_cq_empty(r)) {
return false;
}
unsigned head = *r->cq_head;
struct io_uring_cqe *cqe = &r->cqes[head & r->cq_mask];
*user_data = cqe->user_data;
*res = cqe->res;
__atomic_store_n(r->cq_head, head + 1, __ATOMIC_RELEASE);
return true;
}
#endif // JSRT_IMPLEMENTATION
+153
View File
@@ -0,0 +1,153 @@
// Arrays. The compiler gives only `a[i]`, `a[i] = x` and `a.length`, which can
// also be written. Every method here is built on those
//
// Not as the spec says:
// - A method takes one value where the spec takes any number (`push(x)`,
// `unshift(x)`, `concat(other)`), because more need rest params
// - A callback gets the element and its index, not the array too
// - Holes are not skipped: a callback sees undefined for a hole
// `Array(n)` and `new Array(n)` make an array of `n` holes
function Array(n) {
const a = [];
if (n !== undefined) a.length = n;
return a;
}
Array.prototype = {
// ── Change the array ──
push(x) {
this[this.length] = x;
return this.length;
},
pop() {
if (this.length === 0) return undefined;
const x = this[this.length - 1];
this.length = this.length - 1;
return x;
},
shift() {
if (this.length === 0) return undefined;
const x = this[0];
for (let i = 1; i < this.length; i++) this[i - 1] = this[i];
this.length = this.length - 1;
return x;
},
unshift(x) {
for (let i = this.length; i > 0; i--) this[i] = this[i - 1];
this[0] = x;
return this.length;
},
reverse() {
for (let i = 0, j = this.length - 1; i < j; i++, j--) {
const x = this[i];
this[i] = this[j];
this[j] = x;
}
return this;
},
fill(x, start, end) {
const len = this.length;
const s = relativeIndex(start === undefined ? 0 : start, len);
const e = relativeIndex(end === undefined ? len : end, len);
for (let i = s; i < e; i++) this[i] = x;
return this;
},
// ── Read ──
at(i) {
if (i < 0) i += this.length;
return this[i];
},
indexOf(x, from) {
for (let i = from === undefined ? 0 : relativeIndex(from, this.length); i < this.length; i++) {
if (this[i] === x) return i;
}
return -1;
},
lastIndexOf(x) {
for (let i = this.length - 1; i >= 0; i--) {
if (this[i] === x) return i;
}
return -1;
},
includes(x) {
return this.indexOf(x) !== -1;
},
join(sep) {
const s = sep === undefined ? "," : sep;
let out = "";
for (let i = 0; i < this.length; i++) {
if (i > 0) out = out + s;
const x = this[i];
if (x !== undefined && x !== null) out = out + String(x);
}
return out;
},
toString() {
return this.join(",");
},
// ── Make a new array ──
slice(start, end) {
const len = this.length;
const s = relativeIndex(start === undefined ? 0 : start, len);
const e = relativeIndex(end === undefined ? len : end, len);
const out = [];
for (let i = s; i < e; i++) out[i - s] = this[i];
return out;
},
concat(other) {
const out = this.slice();
for (let i = 0; i < other.length; i++) out[out.length] = other[i];
return out;
},
// ── With a callback ──
forEach(f) {
for (let i = 0; i < this.length; i++) f(this[i], i);
},
map(f) {
const out = [];
for (let i = 0; i < this.length; i++) out[i] = f(this[i], i);
return out;
},
filter(f) {
const out = [];
for (let i = 0; i < this.length; i++) {
if (f(this[i], i)) out[out.length] = this[i];
}
return out;
},
reduce(f, init) {
let acc = init;
for (let i = 0; i < this.length; i++) acc = f(acc, this[i], i);
return acc;
},
find(f) {
for (let i = 0; i < this.length; i++) {
if (f(this[i], i)) return this[i];
}
return undefined;
},
findIndex(f) {
for (let i = 0; i < this.length; i++) {
if (f(this[i], i)) return i;
}
return -1;
},
some(f) {
for (let i = 0; i < this.length; i++) {
if (f(this[i], i)) return true;
}
return false;
},
every(f) {
for (let i = 0; i < this.length; i++) {
if (!f(this[i], i)) return false;
}
return true;
},
};
__link_proto_array(Array.prototype);
+12
View File
@@ -0,0 +1,12 @@
// 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);
+7
View File
@@ -0,0 +1,7 @@
// Not as the spec says: it prints its first arg only, because more args need
// rest params, which need arrays
const console = {
log(x) {
__native_print_string(String(x));
},
};
+2
View File
@@ -0,0 +1,2 @@
// Runs after the program: the event loop (see loop.js)
__run_loop();
+19
View File
@@ -0,0 +1,19 @@
// The stdlib, compiled in front of every program. Its files are joined in the
// order of `STDLIB` in compiler/src/main.rs, and this one is first
//
// 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`. `String.prototype.concat` is not here, because it needs rest
// params, which need arrays. `console.log` prints one arg only, for the same
// reason
const NaN = 0 / 0;
const Infinity = 1 / 0;
+72
View File
@@ -0,0 +1,72 @@
// Files, with promises, on the IO natives (see lib/rt/io.h). They are like
// node's `fs.promises`, but only for UTF-8 text:
// - `fs.readFile(path)` gives the text of the file
// - `fs.writeFile(path, text)` makes or replaces the file
// A failure rejects with the negative errno, not an `Error`. A read or a write
// that the kernel does not finish in one op is not continued
// A promise of what op `h` gives when it completes: its status, its data, or
// a number. A result can only be read before the next wait, so these read it
// when the op completes
function __io_status(h) {
return new Promise((resolve) => {
__pending[h] = (h) => resolve(__native_io_status(h));
});
}
function __io_string(h) {
return new Promise((resolve, reject) => {
__pending[h] = (h) => {
const status = __native_io_status(h);
if (status < 0) reject(status);
else resolve(__native_io_result_string(h));
};
});
}
function __io_number(h) {
return new Promise((resolve, reject) => {
__pending[h] = (h) => {
const status = __native_io_status(h);
if (status < 0) reject(status);
else resolve(__native_io_result_number(h));
};
});
}
// A promise of the status of op `h`, rejected if it failed
function __io_ok(h) {
return __io_status(h).then((status) => (status < 0 ? Promise.reject(status) : status));
}
const fs = {
readFile(path) {
let fd = -1;
let text = "";
return __io_ok(__native_io_queue_open(path, 0, 0))
.then((f) => {
fd = f;
return __io_number(__native_io_queue_stat(path));
})
.then((size) => __io_string(__native_io_queue_read(fd, size, 0)))
.then((s) => {
text = s;
return __io_ok(__native_io_queue_close(fd));
})
.then(() => text);
},
writeFile(path, text) {
let fd = -1;
// O_WRONLY | O_CREAT | O_TRUNC, mode 0644
return __io_ok(__native_io_queue_open(path, 577, 420))
.then((f) => {
fd = f;
return __io_ok(__native_io_queue_write(fd, text, 0));
})
.then(() => __io_ok(__native_io_queue_close(fd)))
.then(() => undefined);
},
};
+46
View File
@@ -0,0 +1,46 @@
// The event loop. A macrotask is an IO op or a timer, which completes in C
// (see lib/rt/io.h): `__native_io_wait()` gives the handle of the next one
// that completes. A microtask is a promise job, in `__microtasks`. After the
// program, end.js runs the loop: all microtasks, then one macrotask, then all
// microtasks again, until nothing is pending
// What to do when IO op `h` completes: `__pending[h]`, a fn that gets `h`.
// Handles are small ints, and a handle is used again after its op completes
const __pending = [];
// Promise jobs, in order. `head` is the next one, so that taking a job does
// not move the others. A job is a closure, which keeps its own data: the
// handler and the value that it is called with. So each handler gets only the
// values of its own promise (see `docs/model.md`, section 3.3)
const __microtasks = { jobs: [], head: 0 };
function __queue_job(job) {
__microtasks.jobs.push(job);
}
function __drain_microtasks() {
const q = __microtasks;
while (q.head < q.jobs.length) {
const job = q.jobs[q.head];
q.jobs[q.head] = undefined;
q.head++;
job();
}
q.jobs.length = 0;
q.head = 0;
}
function __run_loop() {
for (;;) {
__drain_microtasks();
const h = __native_io_wait();
if (h < 0) return;
const done = __pending[h];
__pending[h] = undefined;
if (done !== undefined) done(h);
}
}
+49
View File
@@ -0,0 +1,49 @@
// 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;
}
+10
View File
@@ -0,0 +1,10 @@
// `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 };
};
+165
View File
@@ -0,0 +1,165 @@
// Promises, as the spec says, except:
// - A throw in a handler or an executor does not reject, because there is no
// `try`. `reject()` and a returned rejected promise do
// - An unhandled rejection does nothing. Node prints it and stops
// - `Promise.all` takes an array, not any iterable
// The states of a promise
const __PENDING = 0;
const __FULFILLED = 1;
const __REJECTED = 2;
class Promise {
constructor(executor) {
this.state = __PENDING;
this.value = undefined;
this.resolved = false; // `resolve()` or `reject()` was called, maybe with a thenable
this.reactions = [];
if (executor !== undefined) {
executor((value) => __resolve(this, value), (reason) => __reject(this, reason));
}
}
then(onFulfilled, onRejected) {
const derived = new Promise();
const reaction = { onFulfilled: onFulfilled, onRejected: onRejected, derived: derived };
if (this.state === __PENDING) {
this.reactions.push(reaction);
} else {
__queue_reaction(reaction, this.state, this.value);
}
return derived;
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
finally(f) {
return this.then(
(value) => {
f();
return value;
},
(reason) => {
f();
return Promise.reject(reason);
},
);
}
static resolve(value) {
if (__is_promise(value)) return value;
const p = new Promise();
__resolve(p, value);
return p;
}
static reject(reason) {
const p = new Promise();
__reject(p, reason);
return p;
}
static all(promises) {
const p = new Promise();
const values = [];
let left = promises.length;
if (left === 0) __resolve(p, values);
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
(value) => {
values[i] = value;
left--;
if (left === 0) __resolve(p, values);
},
(reason) => __reject(p, reason),
);
}
return p;
}
}
function __is_promise(x) {
return typeof x === "object" && x !== null && x.reactions !== undefined;
}
// Resolve `p` with `value`, unless it was resolved or rejected before
function __resolve(p, value) {
if (p.resolved) return;
p.resolved = true;
__fulfil_or_adopt(p, value);
}
// Fulfil `p` with `value`, or if `value` is a thenable, adopt its state. That
// is in a job of its own, as the spec says, so it takes a tick more
function __fulfil_or_adopt(p, value) {
if ((typeof value === "object" && value !== null) || typeof value === "function") {
const then = value.then;
if (typeof then === "function") {
__queue_job(() => __adopt(p, value));
return;
}
}
__settle(p, __FULFILLED, value);
}
function __reject(p, reason) {
if (p.resolved) return;
p.resolved = true;
__settle(p, __REJECTED, reason);
}
function __settle(p, state, value) {
p.state = state;
p.value = value;
const reactions = p.reactions;
p.reactions = [];
for (let i = 0; i < reactions.length; i++) {
__queue_reaction(reactions[i], state, value);
}
}
function __queue_reaction(reaction, state, value) {
const handler = state === __FULFILLED ? reaction.onFulfilled : reaction.onRejected;
const derived = reaction.derived;
__queue_job(() => __react(handler, value, derived, state));
}
// Call `handler` with the value of a promise that is in `state`, and resolve
// `derived` with the result. With no handler, `derived` gets the same state
function __react(handler, value, derived, state) {
if (typeof handler === "function") {
__resolve(derived, handler(value));
} else if (state === __FULFILLED) {
__resolve(derived, value);
} else {
__reject(derived, value);
}
}
// Make `p` follow the thenable `thenable`
function __adopt(p, thenable) {
thenable.then(
(value) => {
if (p.state === __PENDING) __fulfil_or_adopt(p, value);
},
(reason) => {
if (p.state === __PENDING) __settle(p, __REJECTED, reason);
},
);
}
function queueMicrotask(f) {
__queue_job(f);
}
-121
View File
@@ -1,36 +1,3 @@
// The stdlib, compiled in front of every program
//
// Built on the natives in compiler/src/native.rs, the common layer of
// operations on primitive values. The compiler's own code for operators (`+` on
// strings and so on) uses those natives directly, not these functions.
//
// Restrictions:
// - The prototypes linked with `__link_proto_*` are fixed. User code may not
// change them (no monkey-patching)
// - `new String(x)`, `new Number(x)` and `new Boolean(x)` (wrapper objects) are
// not supported. These functions only convert
//
// Needs `typeof`. `console.log` and `String.prototype.concat` also need rest
// params, which need arrays
const NaN = 0 / 0;
const Infinity = 1 / 0;
// ─────────────────────────────────────────────── Object ───────────────────────────────────────────────
// `new Object()` makes an empty object. Called without `new`, it does the same
function Object() {
return {};
}
Object.prototype = {};
Object.create = function (proto) {
return { __proto__: proto };
};
// ─────────────────────────────────────────────── String ───────────────────────────────────────────────
// Converts any value to a string, like the spec's ToString
function String(x) {
switch (typeof x) {
@@ -55,14 +22,6 @@ 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 "";
@@ -182,83 +141,3 @@ function pad(s, length, fill, atStart) {
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 = {
log(...args) {
let line = "";
for (let i = 0; i < args.length; i++) {
if (i > 0) line = __native_string_concat(line, " ");
line = __native_string_concat(line, String(args[i]));
}
__native_print_string(line);
},
};
+25
View File
@@ -0,0 +1,25 @@
// Timers. `setTimeout` gives a record, as node does, not an int: an IO handle
// is used again after its op completes, so `clearTimeout` of an old int could
// cancel another op. A record stays the timer's own
//
// Not as the spec says: `setTimeout` takes no args for the callback
function setTimeout(f, ms) {
// As in node, a delay below 1 ms is 1 ms
const delay = ms === undefined || !(ms >= 1) ? 1 : ms;
const timer = { handle: __native_io_queue_timeout(delay), active: true };
__pending[timer.handle] = (h) => {
if (!timer.active) return;
timer.active = false;
f();
};
return timer;
}
function clearTimeout(timer) {
if (timer === undefined || timer === null || !timer.active) return;
timer.active = false;
__native_io_queue_cancel(timer.handle);
}
Vendored Submodule
+1
Submodule third_party/stb added at 2c980bb598