Compare commits

...
7 Commits
Author SHA1 Message Date
uditdey 1e12e7be2e Initial ES modules support 2026-10-26 12:39:58 +00:00
uditdey f7c5b2c80f Make stdlib classes uniform 2026-10-24 16:04:01 +00:00
uditdey 95e5c1225e New macro based emit 2026-10-24 15:58:51 +00:00
uditdey cf455b88a8 Modified infer 2026-10-23 12:07:23 +00:00
uditdey a80bf6f19a New emit 2026-10-22 13:05:04 +00:00
uditdey 1e4647347f Rewrite with working async 2026-10-16 08:00:06 +00:00
UditDey d0c2f291e9 Mid rewrite 2026-10-13 11:52:02 +00:00
56 changed files with 6276 additions and 3943 deletions
+21 -1
View File
@@ -126,6 +126,22 @@ class OpDef(NamedTuple):
temporary: bool # written as lowercase `def()`: a compiler-only opcode
# Opcodes that this compiler adds. QuickJS never emits them, so they come after
# every real opcode and `short_opcode_info` never gives them for a byte of
# bytecode. Only the transforms emit them.
EXTRA_OPS = [
# [obj, value] -> [obj]: like `define_field`, but the field is the location
# that `value` was read from, not a copy of it (see transform/closure.rs)
OpDef("define_field_ref", 5, 2, 1, "atom", False),
# [] -> []: like `close_loc`, for a capture: the binding that capture `i` shares gets a
# new location, with the current value (see transform/async.rs)
OpDef("close_var_ref", 3, 0, 0, "var_ref", False),
# [obj] -> []: field `x` of `obj`, which shares a location, gets a new location, with
# the current value. What `close_var_ref` lowers to (see transform/closure.rs)
OpDef("close_field", 5, 1, 0, "atom", False),
]
def parse_fmt_list(text: str) -> list[str]:
"""The `FMT(name)` list, in order."""
out = []
@@ -284,6 +300,8 @@ def gen_opcodes(text: str, tag: str) -> str:
# no short-opcode shift, unlike the C `short_opcode_info` macro whose table
# still holds them.
real = [o for o in ops if not o.temporary]
real_count = len(real)
real += EXTRA_OPS
s = [header(tag, "quickjs-opcode.h")]
@@ -351,8 +369,10 @@ def gen_opcodes(text: str, tag: str) -> str:
"///\n"
"/// The table already excludes the compiler's temporary opcodes, so the\n"
"/// byte indexes it directly; there is no short-opcode shift to apply.\n"
"/// The opcodes that this compiler adds are not in bytecode, so they\n"
"/// are never given.\n"
"pub fn short_opcode_info(op: u8) -> Option<&'static OpInfo> {\n"
" OPCODE_INFO.get(op as usize)\n"
f" OPCODE_INFO[..{real_count}].get(op as usize)\n"
"}\n"
)
return "".join(s)
+4 -1
View File
@@ -3,7 +3,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-env=TARGET={}",
std::env::var("TARGET").unwrap()
);
println!("cargo:rustc-link-search=native={QUICKJS}");
println!("cargo:rustc-link-lib=static=qjs");
+92 -114
View File
@@ -1,132 +1,110 @@
//! Calls of fns, natives and proto links
//! The C of calls
//!
//! A call's operands are the callee's params: the callee itself, `this`, then the args (see
//! regify). Each closure that the callee can be is a case of its own (see `Program::split`),
//! and a callee that is not a function throws.
use super::*;
use super::classes::{ClassKind, Member};
use super::js::{JsFunction, UNDEFINED, indent};
use super::program::throw;
use crate::{
infer::{InstrRef, Type},
libqjs::parse::opcodes::OpCode,
native::NATIVES,
regify::{Operand, RegInstr},
transform::{FIRST_ARG, THIS_ARG},
};
impl<'a> Emitter<'a> {
/// A call from `place`. The callee's cell tells which code runs, and the
/// callee is its own env. A callee that is not a fn throws
pub(super) fn call(&mut self, res: &InferResult, place: Place, instr: &RegInstr, out: &mut String) {
let arg_types: Vec<Type> = instr.args.iter().map(|&a| res.val_types[a].clone()).collect();
let operands: Vec<(String, Rep)> = (instr.args.iter().zip(&arg_types))
.map(|(a, ty)| (format!("v{a}"), self.rep(ty)))
.collect();
let dst = (instr.dsts[0], self.rep(&res.val_types[instr.dsts[0]]));
self.dispatch(&operands[0].0, &arg_types[0], "jsrt_type_error();", out, |e, member, callee, rep| {
let Type::Obj(cell) = member else {
return None;
};
let mut arg_types = arg_types.clone();
let mut operands = operands.clone();
arg_types[0] = member.clone();
operands[0] = (callee.into(), rep);
let (call, ret) = e.call_code(cell.code()?, place, &arg_types, &operands);
Some(e.call_result(instr.op, &call, &ret, &operands[THIS_ARG as usize], dst))
});
}
/// The statement that runs `call`, which gives a value of type `ret`, and
/// puts the call's result in `dst` (its value, and its rep). `new` gives
/// what the constructor returns if that is an object, else the new object
/// itself, `this` (see infer)
pub(super) fn call_result(&self, op: OpCode, call: &str, ret: &Type, this: &(String, Rep), dst: (usize, Rep)) -> String {
let (dst, dst_rep) = (format!("v{}", dst.0), dst.1);
let ret_rep = self.rep(ret);
let members = ret.members();
let objects = members.iter().filter(|m| matches!(m, Type::Obj(_))).count();
let this = self.conv(&this.0, this.1, dst_rep);
match op {
_ if *ret == Type::Unknown => format!("{call};"),
OpCode::CallConstructor if objects == 0 => format!("{call}; {dst} = {this};"),
OpCode::CallConstructor if objects < members.len() => {
let is_object = match self.tagged("r", ret_rep) {
Some(a) => format!("__any_is_object({a})"),
None => format!("!{}", self.nullish("r", ret)),
};
let value = self.conv("r", ret_rep, dst_rep);
format!("{{ {} r = {call}; {dst} = {is_object} ? {value} : {this}; }}", self.c_type(ret_rep))
impl JsFunction<'_> {
pub(super) fn call(&self, idx: usize, instr: &RegInstr) -> String {
self.split(instr.args[0], |callee, member| {
match (member, self.program.class_kind(member)) {
(Member::Native(native), _) => self.native_call(native, instr),
(Member::Obj(class), ClassKind::Closure(_)) => {
self.closure_call(idx, instr, callee, class)
}
_ => throw("not a function"),
}
_ => format!("{dst} = {};", self.conv(call, ret_rep, dst_rep)),
}
})
}
/// The C call of `code` with these operands (the callee, `this`, then the
/// args, each with its rep) whose types are `arg_types`, and the type it
/// returns
pub(super) fn call_code(&mut self, code: Code, place: Place, arg_types: &[Type], operands: &[(String, Rep)]) -> (String, Type) {
// An operand as a value of rep `to`. One that the call does not pass is
// undefined
let operand = |e: &Self, i: usize, to: Rep| match operands.get(i) {
Some((value, rep)) => e.conv(value, *rep, to),
None => e.conv("(undefined_t){0}", Rep::Undefined, to),
/// A call of a closure of `class`, which is `callee`. Infer walked the closure's JS
/// function for the types of this call's args, and that walk is the C function to call.
/// An arg that the call does not pass is undefined. `new` gives what the constructor
/// returns if that is an object, else the new object itself, which is `this`
fn closure_call(&self, idx: usize, instr: &RegInstr, callee: &str, class: usize) -> String {
let program = self.program;
let ClassKind::Closure(fn_idx) = program.classes.classes[class].kind else {
unreachable!()
};
match code {
Code::Fn(fn_id) => {
let w = self.cache.find(fn_id, place, arg_types, self.fns);
self.use_walk(w);
let mut types: Vec<Type> = instr.args.iter().map(|&slot| self.ty(slot)).collect();
types[0] = Type::Ref(program.classes.classes[class].objs[0]);
let call_site = Some(InstrRef {
fn_idx: self.context.fn_idx,
instr_idx: idx,
});
let walk = program.cache.find(fn_idx, call_site, &types, program.fns);
let walk = &self.cache.walks[w];
let args: Vec<String> = (walk.req.arg_types.iter().enumerate())
.map(|(i, param)| operand(self, i, self.rep(param)))
.collect();
let args: Vec<String> = (0..program.fns[fn_idx].arg_count)
.map(|i| {
let param = program.slot_ctype(walk, Operand::Arg(i)).c_name();
let arg = match instr.args.get(i) {
_ if i == 0 => callee.to_string(),
Some(&slot) => self.get(slot),
None => UNDEFINED.into(),
};
let ret = walk.result.as_ref().unwrap().ret_type.clone();
(format!("{}({})", self.fn_name(w), args.join(", ")), ret)
}
format!("CAST({param}, {arg})")
})
.collect();
// A native whose args cannot be of its params' kinds throws
Code::Native(id) if native::ret_type(id, &arg_types[FIRST_ARG as usize..]) == Type::Unknown => {
("jsrt_type_error()".into(), Type::Unknown)
}
Code::Native(id) => {
let native = &NATIVES[id];
let call = format!("js{walk}({})", args.join(", "));
let ret = program.cache.ret(walk);
let dst = instr.dsts[0];
let args: Vec<String> = (native.args.iter().enumerate())
.map(|(i, param)| operand(self, FIRST_ARG as usize + i, self.rep(param)))
.collect();
(format!("{}({})", native.name, args.join(", ")), native.ret.clone())
}
// Keeps the prototype where field reads on primitives find it
Code::ProtoLink(kind) => {
let value = operand(self, FIRST_ARG as usize, self.rep(&self.cache.protos[kind as usize]));
(format!("({} = {value}, (undefined_t){{0}})", proto_global(kind)), Type::Undefined)
}
// Keeps the fn where `jsrt_complete` finds it
Code::HostLink => {
let value = operand(self, FIRST_ARG as usize, self.rep(&self.cache.host_complete));
(format!("({HOST_COMPLETE_GLOBAL} = {value}, (undefined_t){{0}})"), Type::Undefined)
}
}
}
/// The body of `jsrt_complete(h)`: a call of the fn that the stdlib links
/// for the host, with op `h` (see `infer::host_calls()`)
pub(super) fn host_complete(&mut self) -> String {
let ty = self.cache.host_complete.clone();
let mut out = String::new();
if ty == Type::Unknown {
return out;
// A function that never returns gives no value
if *ret == Type::Unknown {
return format!("{call};\nabort(); // does not return\n");
}
let rep = self.rep(&ty);
let operands = [(HOST_COMPLETE_GLOBAL.to_string(), rep), ("(undefined_t){0}".into(), Rep::Undefined), ("h".into(), Rep::Int)];
if instr.op != OpCode::CallConstructor {
return self.set(dst, &call);
}
self.dispatch(HOST_COMPLETE_GLOBAL, &ty, "jsrt_type_error();", &mut out, |e, member, callee, rep| {
let Type::Obj(cell) = member else { return None };
let mut operands = operands.clone();
operands[0] = (callee.into(), rep);
let arg_types = [member.clone(), Type::Undefined, Type::Int];
Some(format!("{};", e.call_code(cell.code()?, HOST_COMPLETE, &arg_types, &operands).0))
let this = self.get(instr.args[THIS_ARG as usize]);
let result = program.split("result", ret, |result, member| match member {
Member::Obj(_) => self.set(dst, result),
_ => self.set(dst, &this),
});
out
let ctype = program.ret_ctype(walk).c_name();
format!("{{\n {ctype} result = {call};\n{}}}\n", indent(&result))
}
/// A call of a native: a C call, with only the args. Each arg is converted to its
/// param's C type, and an arg of the wrong kind throws (see `ARG_INT`). An arg that
/// the call does not pass is undefined
fn native_call(&self, native: usize, instr: &RegInstr) -> String {
let native = &NATIVES[native];
let args: Vec<String> = (native.arg_types.iter().enumerate())
.map(|(i, param)| {
let arg = instr.args.get(FIRST_ARG as usize + i);
let arg = arg.map_or(UNDEFINED.into(), |&slot| self.get(slot));
match param {
Type::Int => format!("ARG_INT({arg})"),
Type::Float => format!("ARG_NUMBER({arg})"),
Type::Str(_) => format!("ARG_STRING({arg})"),
_ => format!("ARG_UNDEFINED({arg})"),
}
})
.collect();
self.set(
instr.dsts[0],
&format!("{}({})", native.name, args.join(", ")),
)
}
}
+257 -159
View File
@@ -1,181 +1,279 @@
//! Which cells and walks C cannot tell apart
//! The C types of values, and the classes of objects
//!
//! Infer gives each value a type, which can have several members: for example, a number or
//! undefined. In C, each member has a C type of its own (see `Member`). A value whose type
//! has one member is a plain C value of that member's C type. Any other value is a
//! `jsrt_value`, which holds a tag and the value (see lib/rt/core.h).
//!
//! Objects are put in classes, and each class is one C struct. Two objects are in the same
//! class if they have the same fields, and each field has the same C type. A field can point
//! to an object of another class, so the classes are found in rounds: first group objects by
//! their field names, then split each class whose objects disagree on the C type of a field,
//! until no class splits.
//!
//! A closure is always a class of its own. A call picks the C function to call by the
//! closure's address (see `TypeCache::find`), so two closures must never look alike in C.
use super::*;
use std::collections::HashMap;
/// Groups the cells and the walks that C cannot tell apart. Two cells are the
/// same in C if their kinds, code and fields are, with each cell in a field's
/// type replaced by its class. Two walks are the same if their fns, the types
/// of their values and instrs, their reachable blocks and the classes of the
/// walks they call are. Both start in one class each, which splits until no
/// class splits. Gives the class of each cell, a cell of each class, and for
/// each walk, the first walk of its class
pub(super) fn classes(fns: &[RegFn], cache: &TypeCache) -> (HashMap<Cell, CellNum>, Vec<Cell>, Vec<usize>) {
let cells: Vec<Cell> = cache.cells().cloned().collect();
let mut class: HashMap<Cell, CellNum> = cells.iter().map(|c| (c.clone(), 0)).collect();
let mut sig = vec![];
use crate::infer::{Addr, Memory, ObjAddr, Shape, Type, join};
use crate::libqjs::parse::AtomRef;
loop {
let mut ids: HashMap<Vec<u32>, CellNum> = HashMap::new();
let next: Vec<CellNum> = cells
.iter()
.map(|cell| {
sig.clear();
sig.push(class[cell]);
cell_sig(cell, &class, &mut sig);
let n = ids.len() as CellNum;
*ids.entry(sig.clone()).or_insert(n)
})
.collect();
let done = ids.len() == class.values().collect::<HashSet<_>>().len();
cells.iter().zip(next).for_each(|(cell, n)| *class.get_mut(cell).unwrap() = n);
if done {
break;
}
}
let mut reps: Vec<Option<Cell>> = vec![None; class.values().max().map_or(0, |&m| m as usize + 1)];
cells.iter().for_each(|cell| _ = reps[class[cell] as usize].get_or_insert_with(|| cell.clone()));
// Walks. A walk that no round reached has no fn, and its own class
let walks = &cache.walks;
let mut wclass: Vec<u32> = (0..walks.len()).map(|w| if walks[w].result.is_some() { 0 } else { w as u32 + 1 }).collect();
let callees: Vec<Vec<usize>> = (0..walks.len()).map(|w| callees_of(fns, cache, w)).collect();
let mut count = 0;
loop {
let mut ids: HashMap<Vec<u32>, u32> = HashMap::new();
let next: Vec<u32> = (0..walks.len())
.map(|w| {
sig.clear();
sig.push(wclass[w]);
if let Some(res) = &walks[w].result {
sig.push(walks[w].req.fn_id as u32);
walks[w].req.arg_types.iter().chain(&res.val_types).chain(&res.instr_types).for_each(|t| type_sig(t, &class, &mut sig));
type_sig(&res.ret_type, &class, &mut sig);
sig.extend(res.reachable.iter().map(|&r| r as u32));
sig.extend(callees[w].iter().map(|&c| wclass[c]));
}
let n = ids.len() as u32;
*ids.entry(sig.clone()).or_insert(n)
})
.collect();
wclass = next;
if ids.len() == count {
break;
}
count = ids.len();
}
let mut first: HashMap<u32, usize> = HashMap::new();
let walk_rep = (0..walks.len()).map(|w| *first.entry(wclass[w]).or_insert(w)).collect();
(class, reps.into_iter().map(Option::unwrap).collect(), walk_rep)
/// One member of a type, which has a C type of its own
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Member {
Undefined,
Null,
Bool,
Int,
Float,
Str,
Obj(usize), // a pointer to an object of this class
Native(usize), // a native function, by its index in `native::NATIVES`
}
/// The walks that walk `w` calls, in the order that emit writes the calls
pub(super) fn callees_of(fns: &[RegFn], cache: &TypeCache, w: usize) -> Vec<usize> {
let Some(res) = &cache.walks[w].result else { return vec![] };
let fn_id = cache.walks[w].req.fn_id;
let f = &fns[fn_id];
let mut out = vec![];
/// The C type of a value: one member's, or a `jsrt_value`
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CType {
One(Member),
Value,
}
let blocks = f.blocks.iter().enumerate().filter(|(b, _)| res.reachable[*b]);
let code = blocks.flat_map(|(_, block): (usize, &BasicBlock)| (block.start..block.end).map(|idx| (idx, &f.code[idx])));
impl CType {
pub fn c_name(self) -> String {
match self {
CType::One(Member::Undefined) => "undefined_t".into(),
CType::One(Member::Null) => "null_t".into(),
CType::One(Member::Bool) => "bool".into(),
CType::One(Member::Int) => "int32_t".into(),
CType::One(Member::Float) => "double".into(),
CType::One(Member::Str) => "string_t".into(),
CType::One(Member::Obj(class)) => format!("struct c{class} *"),
CType::One(Member::Native(_)) => "native_t".into(),
CType::Value => "jsrt_value".into(),
}
}
}
for (idx, instr) in code {
if !matches!(instr.op, OpCode::Call | OpCode::CallMethod | OpCode::CallConstructor)
|| instr.args.iter().any(|&a| res.val_types[a] == Type::Unknown)
{
continue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClassKind {
Obj,
Array,
Closure(usize), // a closure of this JS function
}
/// A field of the objects of a class
#[derive(Debug, Clone)]
pub struct ClassField {
pub name: AtomRef,
pub ty: Type, // the join of the field's types in the class's objects
pub ctype: CType,
pub shared: bool, // the field shares another location, so the struct holds a pointer to it
pub defined: bool, // every object has the field, so the struct needs no `has_` flag for it
}
/// A group of objects that are one C struct
#[derive(Debug)]
pub struct Class {
pub kind: ClassKind,
pub objs: Vec<ObjAddr>,
pub fields: Vec<ClassField>, // sorted by name
pub elems: CType, // the C type of an array's elements. Not used for other objects
}
/// The classes of all objects
#[derive(Debug)]
pub struct Classes {
pub classes: Vec<Class>,
class_of: HashMap<ObjAddr, usize>,
}
impl Classes {
pub fn new(memory: &Memory) -> Self {
let class_of = refine(memory);
let count = class_of.values().max().map_or(0, |&n| n + 1);
let mut objs_of_class: Vec<Vec<ObjAddr>> = vec![vec![]; count];
let mut objs: Vec<ObjAddr> = memory.objects().map(|(obj, _)| obj).collect();
objs.sort_unstable();
for obj in objs {
objs_of_class[class_of[&obj]].push(obj);
}
let mut args: Vec<Type> = instr.args.iter().map(|&a| res.val_types[a].clone()).collect();
let classes = objs_of_class.into_iter();
let classes = classes
.map(|objs| make_class(memory, &class_of, objs))
.collect();
Classes { classes, class_of }
}
for member in res.val_types[instr.args[0]].members() {
if let Type::Obj(cell) = member
&& let Some(Code::Fn(callee)) = cell.code()
{
args[0] = member.clone();
out.push(cache.find(callee, (fn_id, idx), &args, fns));
pub fn class(&self, obj: ObjAddr) -> usize {
self.class_of[&obj]
}
pub fn field(&self, class: usize, name: AtomRef) -> Option<&ClassField> {
self.classes[class].fields.iter().find(|f| f.name == name)
}
/// The members of a value of type `ty`. A location that no value reached holds undefined
pub fn members(&self, ty: &Type) -> Vec<Member> {
members_with(ty, &self.class_of)
}
/// The C type of a value of type `ty`
pub fn ctype(&self, ty: &Type) -> CType {
ctype_with(ty, &self.class_of)
}
}
/// The class of the objects in `objs`, which refinement put together. The type of a field is
/// the join of its types in all the objects of the class, so that its C type holds each one
fn make_class(memory: &Memory, class_of: &HashMap<ObjAddr, usize>, objs: Vec<ObjAddr>) -> Class {
let first = objs[0];
let join_over = |ty_of: &dyn Fn(ObjAddr) -> Type| {
objs.iter()
.fold(Type::Unknown, |all, &obj| join(&all, &ty_of(obj)))
};
let kind = match memory.get(Addr::Object(first)) {
Type::Shape(Shape::Array(_)) => ClassKind::Array,
Type::Closure { fn_idx, .. } => ClassKind::Closure(*fn_idx),
_ => ClassKind::Obj,
};
let fields = match kind {
ClassKind::Array => vec![],
_ => (memory.fields(first).iter())
.map(|&(name, field)| {
let ty =
join_over(&|obj| memory.get(memory.field(obj, name).unwrap().addr).clone());
ClassField {
name,
ctype: ctype_with(&ty, class_of),
ty,
shared: field.addr != Addr::Field(first, name),
defined: field.defined,
}
})
.collect(),
};
let elems = match kind {
ClassKind::Array => ctype_with(&join_over(&|obj| memory.elems(obj).clone()), class_of),
_ => CType::One(Member::Undefined),
};
Class {
kind,
objs,
fields,
elems,
}
}
/// The members of `ty`, given the class of each object
fn members_with(ty: &Type, class_of: &HashMap<ObjAddr, usize>) -> Vec<Member> {
if *ty == Type::Unknown {
return vec![Member::Undefined];
}
let mut members: Vec<Member> = (ty.members().iter())
.map(|member| match member {
Type::Undefined => Member::Undefined,
Type::Null => Member::Null,
Type::Bool(_) => Member::Bool,
Type::Int => Member::Int,
Type::Float => Member::Float,
Type::Str(_) => Member::Str,
Type::Ref(obj) => Member::Obj(class_of[obj]),
Type::Native(idx) => Member::Native(*idx),
ty => unreachable!("{ty:?} is not a value"),
})
.collect();
members.sort_unstable();
members.dedup();
members
}
/// The C type of `ty`, given the class of each object
fn ctype_with(ty: &Type, class_of: &HashMap<ObjAddr, usize>) -> CType {
match members_with(ty, class_of)[..] {
[member] => CType::One(member),
_ => CType::Value,
}
}
/// The class of every object. At first, objects with the same kind and field names share a
/// class. Then each round splits a class whose objects have fields of different C types,
/// until a round splits nothing
fn refine(memory: &Memory) -> HashMap<ObjAddr, usize> {
let mut objs: Vec<ObjAddr> = memory.objects().map(|(obj, _)| obj).collect();
objs.sort_unstable();
// The first key: the kind, then each field's name and how it is held. A closure is
// its own class
let first_key = |obj: ObjAddr| -> (u8, Option<ObjAddr>, Vec<(AtomRef, bool, bool)>) {
match memory.get(Addr::Object(obj)) {
Type::Shape(Shape::Array(_)) => (0, None, vec![]),
entry => {
let fields = (memory.fields(obj).iter())
.map(|&(name, f)| (name, f.addr != Addr::Field(obj, name), f.defined))
.collect();
match entry {
Type::Closure { .. } => (1, Some(obj), fields),
_ => (2, None, fields),
}
}
}
}
};
out
}
let mut class_of = number(&objs, first_key);
/// Append what C sees of `cell` to `sig`
pub(super) fn cell_sig(cell: &Cell, class: &HashMap<Cell, CellNum>, sig: &mut Vec<u32>) {
let info = cell.info();
loop {
let count = class_of.values().max().map_or(0, |&n| n + 1);
match &info.kind {
ObjKind::Plain => sig.push(0),
ObjKind::Fn(code) => sig.extend(match *code {
Code::Fn(id) => [1, id as u32],
Code::Native(id) => [2, id as u32],
Code::ProtoLink(kind) => [3, kind as u32],
Code::HostLink => [5, 0],
}),
ObjKind::Array { elems } => {
sig.push(4);
type_sig(elems, class, sig);
// The C type of each field, or of an array's elements, with the classes so far
let key = |obj: ObjAddr| -> (usize, Vec<CType>) {
let types: Vec<Type> = match memory.get(Addr::Object(obj)) {
Type::Shape(Shape::Array(elems)) => vec![(**elems).clone()],
_ => memory
.fields(obj)
.iter()
.map(|(_, f)| memory.get(f.addr).clone())
.collect(),
};
(
class_of[&obj],
types.iter().map(|ty| ctype_with(ty, &class_of)).collect(),
)
};
let next = number(&objs, key);
if next.values().max().map_or(0, |&n| n + 1) == count {
return class_of;
}
}
sig.push(info.fields.len() as u32);
for (name, field) in &info.fields {
atom_sig(*name, sig);
sig.push(field.defined as u32);
type_sig(&field.ty, class, sig);
class_of = next;
}
}
/// Append `ty` to `sig`, with each cell as its class. No encoding is a prefix
/// of another, so a list of them reads back one way
pub(super) fn type_sig(ty: &Type, class: &HashMap<Cell, CellNum>, sig: &mut Vec<u32>) {
match ty {
Type::Unknown => sig.push(0),
Type::Undefined => sig.push(1),
Type::Null => sig.push(2),
Type::Bool(b) => sig.push(3 + b.map_or(0, |b| 1 + b as u32)),
Type::Int => sig.push(6),
Type::Float => sig.push(7),
Type::Str(None) => sig.extend([8, u32::MAX]),
Type::Str(Some(atom)) => {
sig.push(8);
atom_sig(*atom, sig);
}
Type::Obj(cell) => sig.extend([10, class[cell]]),
Type::Sum(members) => {
let mut parts: Vec<Vec<u32>> = members
.iter()
.map(|m| {
let mut part = vec![];
type_sig(m, class, &mut part);
part
})
.collect();
parts.sort_unstable();
parts.dedup();
sig.extend([11, parts.len() as u32]);
parts.iter().for_each(|p| sig.extend(p));
}
Type::Any => sig.push(12),
}
}
/// A number for each object, the same for objects with the same key, in the order that the
/// keys first come
fn number<K: std::hash::Hash + Eq>(
objs: &[ObjAddr],
key: impl Fn(ObjAddr) -> K,
) -> HashMap<ObjAddr, usize> {
let mut ids: HashMap<K, usize> = HashMap::new();
pub(super) fn atom_sig(atom: AtomRef, sig: &mut Vec<u32>) {
sig.extend(match atom {
AtomRef::TaggedInt(n) => [0, n],
AtomRef::Predefined(n) => [1, n],
AtomRef::FileLocal(n) => [2, n as u32],
});
objs.iter()
.map(|&obj| {
let next = ids.len();
(obj, *ids.entry(key(obj)).or_insert(next))
})
.collect()
}
-282
View File
@@ -1,282 +0,0 @@
//! Conversions between representations, `dispatch` over the members of a
//! sum, and the tests on values that C needs
use super::*;
impl<'a> Emitter<'a> {
/// `e`, a value of rep `from`, as a value of rep `to`. `e` is evaluated
/// once, even where its value is not needed, because it can be a call
///
/// Types grow in a join, so most conversions go to a rep that holds more:
/// to a sum, or from `Int` to `Double`. A conversion to a rep that holds
/// less checks the kind, and a failed check is a `TypeError`: for example
/// a string that can be undefined, passed to a native's string param. A
/// value of a param that the caller does not pass is undefined
pub(super) fn conv(&self, e: &str, from: Rep, to: Rep) -> String {
if from == to {
return e.into();
}
// A tagged union is made in place from a value of one kind that it
// has. Else it goes through `any_t`, which the C compiler removes
match (from, to) {
(Rep::Sum(_), _) => return self.conv(&self.tagged(e, from).unwrap(), Rep::Any, to),
(_, Rep::Sum(k)) if let Some(made) = self.make_sum(e, from, k) => return made,
(_, Rep::Sum(_)) => return format!("any_{}({})", self.c_type(to), self.conv(e, from, Rep::Any)),
_ => {}
}
let c_to = self.c_type(to);
let after_e = |value: String| format!("((void)({e}), {value})");
match (from, to) {
(_, Rep::Any) => match from {
Rep::Undefined => after_e("__any_undefined()".into()),
Rep::Null => after_e("__any_null()".into()),
Rep::Bool => format!("__any_from_bool({e})"),
Rep::Int => format!("__any_from_int({e})"),
Rep::Double => format!("__any_from_float({e})"),
Rep::Str | Rep::OptStr => format!("__any_from_string({e})"),
Rep::Obj(_) | Rep::OptObj(_) | Rep::Objs => format!("__any_from_object({e})"),
Rep::Sum(_) | Rep::Any => unreachable!(),
},
(Rep::Any, _) => match to {
Rep::Undefined => after_e("(undefined_t){0}".into()),
Rep::Null => after_e("(null_t){0}".into()),
Rep::Bool => format!("__any_as_bool({e})"),
Rep::Int => format!("__any_as_int({e})"),
Rep::Double => format!("__any_as_float({e})"),
Rep::Str => format!("(({c_to})jsrt_some(__any_as_string({e})))"),
Rep::Obj(_) => format!("(({c_to})jsrt_some(__any_as_object({e})))"),
Rep::OptStr => format!("__any_as_string({e})"),
Rep::OptObj(_) | Rep::Objs => format!("(({c_to})__any_as_object({e}))"),
Rep::Sum(_) | Rep::Any => unreachable!(),
},
(Rep::Int, Rep::Double) => format!("(double){e}"),
(Rep::Double, Rep::Int) => format!("(int32_t){e}"),
(Rep::Str, Rep::OptStr) | (Rep::Obj(_), Rep::OptObj(_)) => e.into(),
(Rep::OptStr, Rep::Str) | (Rep::OptObj(_), Rep::Obj(_)) => format!("(({c_to})jsrt_some({e}))"),
(Rep::Obj(_) | Rep::OptObj(_), Rep::Objs) => format!("((jsrt_header *){e})"),
(Rep::Undefined, Rep::OptStr | Rep::OptObj(_) | Rep::Objs) => after_e(format!("(({c_to})JSRT_UNDEFINED)")),
(Rep::Null, Rep::OptStr | Rep::OptObj(_) | Rep::Objs) => after_e(format!("(({c_to})NULL)")),
// Reps that correct types never make meet
_ => after_e(format!("({c_to}){{0}} /* mismatch */")),
}
}
/// `dst = expr`, where `expr` is a value of rep `from`
pub(super) fn assign(&self, out: &mut String, dst: &str, expr: &str, from: Rep, to: Rep) {
let _ = writeln!(out, " {dst} = {};", self.conv(expr, from, to));
}
/// `e`, a value of rep `from`, as a tagged union of kinds `k`, if `from`
/// has one kind and `k` has it, with no sentinels to tell apart
pub(super) fn make_sum(&self, e: &str, from: Rep, k: Kinds) -> Option<String> {
let (kind, member) = match from {
Rep::Undefined => (1, None),
Rep::Null => (2, None),
Rep::Bool => (3, Some("b")),
Rep::Int => (4, Some("i")),
Rep::Double => (5, Some("f")),
Rep::Str => (6, Some("s")),
Rep::Obj(_) => (7, Some("o")),
_ => return None,
};
if k & 1 << kind == 0 {
return None;
}
let t = self.c_type(Rep::Sum(k));
let kind = KINDS[kind].0;
Some(match member {
Some(m) => format!("(({t}){{ .kind = JSRT_KIND_{kind}, .as.{m} = {e} }})"),
None => format!("((void)({e}), ({t}){{ .kind = JSRT_KIND_{kind} }})"),
})
}
/// `e`, a value of rep `rep`, as an `any_t`, if `rep` is a tagged value
pub(super) fn tagged(&self, e: &str, rep: Rep) -> Option<String> {
match rep {
Rep::Any => Some(e.into()),
Rep::Sum(_) => Some(format!("{}_any({e})", self.c_type(rep))),
_ => None,
}
}
/// The absent value of a field of rep `rep`
pub(super) fn absent(&self, rep: Rep) -> String {
match rep {
Rep::Any | Rep::Sum(_) => self.conv("__any_absent()", Rep::Any, rep),
_ => format!("(({})JSRT_ABSENT)", self.c_type(rep)),
}
}
/// A C bool that is true if `e`, a field of rep `rep`, is not absent
pub(super) fn present(&self, e: &str, rep: Rep) -> String {
match self.tagged(e, rep) {
Some(a) => format!("!__any_is_absent({a})"),
None => format!("((void *){e} != JSRT_ABSENT)"),
}
}
/// `e`, an `any_t` that holds an object of cell `id`, as a pointer to its
/// struct
pub(super) fn any_pointer(&self, e: &str, id: CellNum) -> String {
format!("(({} *)__any_as_object({e}))", struct_name(id))
}
/// A C bool that is true if `e`, a value of type `ty`, is null or
/// undefined
pub(super) fn nullish(&self, e: &str, ty: &Type) -> String {
let can_be = ty.members().iter().any(Type::is_nullish);
match self.rep(ty) {
Rep::Undefined | Rep::Null => "true".into(),
rep @ (Rep::Any | Rep::Sum(_)) => format!("__any_is_nullish({})", self.tagged(e, rep).unwrap()),
Rep::OptStr | Rep::OptObj(_) | Rep::Objs if can_be => {
format!("((void *){e} == NULL || (void *){e} == JSRT_UNDEFINED)")
}
_ => "false".into(),
}
}
/// Writes `body`'s statement for each member of `ty`, the type of `e`. A
/// statement gets the member, and `e` as a value of that member and its
/// rep. At runtime, a member is told apart by its kind, and an object by
/// the cell in its header. `body` gives `None` for a member on which the
/// op throws. A value that no statement takes runs `otherwise`
pub(super) fn dispatch(
&mut self,
e: &str,
ty: &Type,
otherwise: &str,
out: &mut String,
mut body: impl FnMut(&mut Self, &Type, &str, Rep) -> Option<String>,
) {
let rep = self.rep(ty);
if let [member] = ty.members() {
let stmt = body(self, member, e, rep);
let _ = writeln!(out, " {}", stmt.as_deref().unwrap_or(otherwise));
return;
}
let _ = write!(out, " ");
let mut objects = vec![];
for member in ty.members() {
let (value, member_rep) = self.member_value(e, rep, member);
match (member, body(self, member, &value, member_rep)) {
(_, None) => {}
// Cells of one class do the same
(Type::Obj(cell), Some(_)) if objects.iter().any(|(id, _)| *id == self.num(cell)) => {}
(Type::Obj(cell), Some(stmt)) => objects.push((self.num(cell), stmt)),
(_, Some(stmt)) => {
let _ = write!(out, "if ({}) {{ {stmt} }} else ", self.is_member(e, rep, member));
}
}
}
// The objects, by cell. A pointer to the objects of one cell needs no
// switch
let is_object = match self.tagged(e, rep) {
Some(a) => format!("__any_is_object({a})"),
None => format!("!{}", self.nullish(e, ty)),
};
match (rep, &objects[..]) {
(_, []) => {}
(Rep::OptObj(_), [(_, stmt)]) => {
let _ = write!(out, "if ({is_object}) {{ {stmt} }} else ");
}
_ => {
let cell = match self.tagged(e, rep) {
Some(a) => format!("((jsrt_header *)__any_as_object({a}))->cell"),
None => format!("{e}->cell"),
};
let _ = writeln!(out, "if ({is_object}) switch ({cell}) {{");
for (id, stmt) in &objects {
let _ = writeln!(out, " case {id}: {{ {stmt} }} break;");
}
let _ = write!(out, " default: {otherwise}\n }} else ");
}
}
let _ = writeln!(out, "{otherwise}");
}
/// `e`, a value of rep `rep`, as a value of `member`, a member of its type,
/// and that value's rep
pub(super) fn member_value(&self, e: &str, rep: Rep, member: &Type) -> (String, Rep) {
let member_rep = self.rep(member);
let value = match (rep, member) {
(Rep::Any | Rep::Sum(_), Type::Obj(cell)) => self.any_pointer(&self.tagged(e, rep).unwrap(), self.num(cell)),
(Rep::Any | Rep::Sum(_), _) => self.conv(e, rep, member_rep),
(Rep::Objs, Type::Obj(cell)) => format!("(({} *){e})", struct_name(self.num(cell))),
(_, Type::Undefined | Type::Null) => format!("({}){{0}}", self.c_type(member_rep)),
_ => e.into(),
};
(value, member_rep)
}
/// A C bool that is true if `e`, a value of rep `rep`, is of `member`, a
/// member of its type that is not an object
pub(super) fn is_member(&self, e: &str, rep: Rep, member: &Type) -> String {
let kind = match member {
Type::Undefined => "undefined",
Type::Null => "null",
Type::Bool(_) => "boolean",
Type::Int | Type::Float => "number",
_ => "string",
};
match (rep, member) {
(Rep::Any | Rep::Sum(_), _) => format!("__any_is_{kind}({})", self.tagged(e, rep).unwrap()),
(_, Type::Undefined) => format!("((void *){e} == JSRT_UNDEFINED)"),
(_, Type::Null) => format!("((void *){e} == NULL)"),
_ => format!("((void *){e} != NULL && (void *){e} != JSRT_UNDEFINED)"),
}
}
/// A C bool that is JS's truthiness of `e`, a value of type `ty`
pub(super) fn truthy(&self, e: &str, ty: &Type) -> String {
match self.rep(ty) {
Rep::Bool => e.into(),
Rep::Int => format!("({e} != 0)"),
Rep::Double => format!("({e} != 0 && {e} == {e})"),
Rep::Str => format!("(jsrt_string_length({e}) != 0)"),
Rep::OptStr => format!("(!{} && jsrt_string_length({e}) != 0)", self.nullish(e, ty)),
Rep::Obj(_) => "true".into(),
Rep::OptObj(_) | Rep::Objs => format!("!{}", self.nullish(e, ty)),
Rep::Undefined | Rep::Null => "false".into(),
rep @ (Rep::Any | Rep::Sum(_)) => format!("__any_truthy({})", self.tagged(e, rep).unwrap()),
}
}
/// `e`, a value of type `ty`, as a string, like the spec's ToString
pub(super) fn to_string(&self, e: &str, ty: &Type) -> String {
match self.rep(ty) {
Rep::Str => e.into(),
Rep::Int | Rep::Double => format!("__native_number_to_string((double){e})"),
Rep::Bool => {
let (t, f) = (self.string_lit("true"), self.string_lit("false"));
format!("({e} ? {t} : {f})")
}
Rep::Undefined => self.string_lit("undefined"),
Rep::Null => self.string_lit("null"),
rep => format!("__any_to_string({})", self.conv(e, rep, Rep::Any)),
}
}
pub(super) fn string_lit(&self, s: &str) -> String {
format!("jsrt_string_lit({})", c_str(s))
}
}
+109
View File
@@ -0,0 +1,109 @@
//! The C functions of the program
//!
//! There are four kinds:
//!
//! - **A JS function**, as infer walked it. Infer walks a JS function once for each context,
//! which is the function in one call site, and each set of arg types. Each walk is one C
//! function (see `js`).
//! - **A lookup**, which gets a field that some objects of a class do not have. It gives the
//! object's own field if the object has it, else it looks on the object's prototype, and so
//! on along the prototype chain. At the end of the chain, the field is undefined (see
//! `objects`).
//! - **`jsrt_start`**, which the host calls to run the program (see lib/rt/jsrt.h).
//! - **`jsrt_complete`**, which the host calls when an IO op completes.
use super::classes::{CType, ClassKind, Member};
use super::program::Program;
use super::{js, objects};
use crate::{flatten::ENTRY_FN, infer::Type, libqjs::parse::AtomRef, regify::Operand};
#[derive(Clone, Copy)]
pub enum Function {
Js(usize), // a JS function, as one walk: the walk's index
Lookup(usize, AtomRef), // a lookup of a field on the objects of a class: the class, and the field
Start,
Complete,
}
impl Function {
pub fn name(&self, program: &Program) -> String {
match self {
Function::Js(walk) => format!("js{walk}"),
Function::Lookup(class, field) => {
format!("lookup_c{class}_{}", program.field_name(*field))
}
Function::Start => "jsrt_start".into(),
Function::Complete => "jsrt_complete".into(),
}
}
pub fn signature(&self, program: &Program) -> String {
let name = self.name(program);
match self {
Function::Js(walk) => js::signature(program, *walk),
Function::Lookup(class, field) => {
let ctype = program.lookup_ctype(*class, *field).c_name();
format!("static {ctype} {name}(struct c{class} *o)")
}
Function::Start => format!("void {name}(void)"),
Function::Complete => format!("void {name}(int32_t h)"),
}
}
pub fn definition(&self, program: &Program) -> String {
let body = match self {
Function::Js(walk) => js::body(program, *walk),
Function::Lookup(class, field) => objects::lookup_body(program, *class, *field),
Function::Start => start_body(program),
Function::Complete => complete_body(program),
};
format!("{} {{\n{body}}}\n\n", self.signature(program))
}
}
/// Call the entry, with undefined args
fn start_body(program: &Program) -> String {
let arg_count = program.fns[ENTRY_FN].arg_count;
let args = vec![js::UNDEFINED.to_string(); arg_count];
format!(" {};\n", host_call(program, program.entry, &args))
}
/// Call the stdlib's `__complete` with `h`, the handle of the op, if the program has it
fn complete_body(program: &Program) -> String {
match complete_call(program) {
Some(call) => format!(" {call};\n"),
None => " (void)h;\n".into(),
}
}
/// The call of `__complete`, with the closure as its own env and `this` undefined. Infer
/// made the same call (see `TypeCache::call_host_complete`)
fn complete_call(program: &Program) -> Option<String> {
let (callee, ctype) = program.binding("jsrt:loop", "__complete")?;
let CType::One(Member::Obj(class)) = ctype else {
return None;
};
let ClassKind::Closure(fn_idx) = program.classes.classes[class].kind else {
return None;
};
let obj = program.classes.classes[class].objs[0];
let types = [Type::Ref(obj), Type::Undefined, Type::Int];
let walk = program.cache.find(fn_idx, None, &types, program.fns);
let args = [callee, js::UNDEFINED.to_string(), "h".to_string()];
Some(host_call(program, walk, &args))
}
/// A call of a walk's C function, with each arg converted to its param's C type
fn host_call(program: &Program, walk: usize, args: &[String]) -> String {
let args: Vec<String> = (args.iter().enumerate())
.map(|(i, arg)| {
let param = program.slot_ctype(walk, Operand::Arg(i)).c_name();
format!("CAST({param}, {arg})")
})
.collect();
format!("js{walk}({})", args.join(", "))
}
-221
View File
@@ -1,221 +0,0 @@
//! The C of each instr
use super::*;
impl<'a> Emitter<'a> {
pub(super) fn emit_instr(&mut self, fn_id: usize, res: &InferResult, idx: usize, instr: &RegInstr, out: &mut String) {
use OpCode::*;
if matches!(instr.op, Nop | Drop) {
return;
}
// No value reaches the instr, so it cannot run. An instr that values
// reach but that gives no value always throws, or is a call that
// never returns: its op's code below does that
if instr.args.iter().any(|&a| res.val_types[a] == Type::Unknown) {
let _ = writeln!(out, " abort(); /* not reached */");
return;
}
let ty = &res.instr_types[idx];
let v = |i: usize| format!("v{}", instr.args[i]);
let vt = |i: usize| &res.val_types[instr.args[i]];
let vr = |i: usize| self.rep(&res.val_types[instr.args[i]]);
let dst = instr.dsts.first().map(|d| format!("v{d}")).unwrap_or_default();
let dst_rep = instr.dsts.first().map_or(Rep::Undefined, |&d| self.rep(&res.val_types[d]));
let rep = self.rep(ty);
// A known value is its own literal. A call still runs, for what it
// does
let known = match ty {
Type::Bool(Some(b)) => Some(b.to_string()),
Type::Str(Some(atom)) => Some(self.string_lit(&atom_name(*atom, self.atoms))),
_ => None,
};
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
let is_call = matches!(instr.op, Call | CallMethod | CallConstructor);
if let Some(lit) = known.filter(|_| !is_ret && !is_call) {
return self.assign(out, &dst, &lit, rep, dst_rep);
}
// Binary ops: the lhs was pushed first, so it is popped last
let (l, r) = (instr.args.len() == 2).then(|| (v(1), v(0))).unwrap_or_default();
let numbers = instr.args.len() == 2 && is_num(vr(0)) && is_num(vr(1));
let expr = match instr.op {
// ── Literals ──
PushMinus1 => "-1".into(),
Push0 => "0".into(),
Push1 => "1".into(),
Push2 => "2".into(),
Push3 => "3".into(),
Push4 => "4".into(),
Push5 => "5".into(),
Push6 => "6".into(),
Push7 => "7".into(),
PushI8 | PushI16 | PushI32 => match instr.imm {
ImmOperand::I8(n) => n.to_string(),
ImmOperand::I16(n) => n.to_string(),
ImmOperand::I32(n) => n.to_string(),
_ => unreachable!(),
},
PushConst | PushConst8 => match &self.fns[fn_id].cpool[instr.imm.const_idx()] {
JsValue::Int(n) => n.to_string(),
JsValue::Float(x) if x.is_finite() => format!("{x:?}"),
JsValue::Float(x) if x.is_nan() => "NAN".into(),
JsValue::Float(x) => format!("{}INFINITY", if *x < 0.0 { "-" } else { "" }),
JsValue::String(s) => self.string_lit(&js_string_text(s)),
JsValue::NativeFn(id) => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::Native(*id))))),
JsValue::ProtoLink(kind) => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::ProtoLink(*kind))))),
JsValue::HostLink => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::HostLink)))),
_ => format!("({}){{0}}", self.c_type(rep)),
},
Undefined | Null => format!("({}){{0}}", self.c_type(rep)),
Object | Fclosure | Fclosure8 => self.alloc(self.num_of(ty)),
// The elements are the operands, last first
ArrayFrom => {
let id = self.num_of(ty);
let (name, elem) = (struct_name(id), self.elem_rep(id));
let _ = writeln!(out, " {dst} = {};\n set_length_{name}({dst}, {});", self.alloc(id), instr.args.len());
for (i, &v) in instr.args.iter().rev().enumerate() {
let value = self.conv(&format!("v{v}"), self.rep(&res.val_types[v]), elem);
let _ = writeln!(out, " {dst}->items[{i}] = {value};");
}
return;
}
GetArrayEl | PutArrayEl => return self.element(res, instr, out),
// ── Arithmetic ──
Add if matches!(ty, Type::Str(_)) => {
let (ls, rs) = (self.to_string(&l, vt(1)), self.to_string(&r, vt(0)));
format!("__native_string_concat({ls}, {rs})")
}
Add | Sub | Mul if numbers => {
let op = match instr.op { Add => "+", Sub => "-", _ => "*" };
format!("{l} {op} {r}")
}
Mod if numbers && rep == Rep::Int => format!("{l} % {r}"),
Mod if numbers => format!("fmod({l}, {r})"),
Pow if numbers => format!("pow({l}, {r})"),
Div if numbers => format!("(double){l} / {r}"),
// An operand that is not a number: JS's operator, at runtime
Add | Sub | Mul | Div | Mod | Pow => {
let name = match instr.op {
Add => "add", Sub => "sub", Mul => "mul", Div => "div", Mod => "mod", _ => "pow",
};
let (la, ra) = (self.conv(&l, vr(1), Rep::Any), self.conv(&r, vr(0), Rep::Any));
return self.assign(out, &dst, &format!("__any_{name}({la}, {ra})"), Rep::Any, dst_rep);
}
Neg | Plus | Inc | Dec | PostInc | PostDec if is_num(vr(0)) => {
let new = match instr.op {
Neg => format!("-{}", v(0)),
Plus => v(0),
Inc | PostInc => format!("{} + 1", v(0)),
_ => format!("{} - 1", v(0)),
};
// Pushes the old value, then the new one
if matches!(instr.op, PostInc | PostDec) {
let new_dst = format!("v{}", instr.dsts[1]);
self.assign(out, &new_dst, &new, rep, self.rep(&res.val_types[instr.dsts[1]]));
v(0)
} else {
new
}
}
Neg | Plus | Inc | Dec | PostInc | PostDec => {
let name = match instr.op {
Neg => "neg", Plus => "plus", Inc | PostInc => "inc", _ => "dec",
};
let operand = self.conv(&v(0), vr(0), Rep::Any);
// Pushes the old value as a number, then the new one
if matches!(instr.op, PostInc | PostDec) {
let new_dst = format!("v{}", instr.dsts[1]);
let new = format!("__any_{name}({operand})");
self.assign(out, &new_dst, &new, Rep::Any, self.rep(&res.val_types[instr.dsts[1]]));
return self.assign(out, &dst, &format!("__any_plus({operand})"), Rep::Any, dst_rep);
}
return self.assign(out, &dst, &format!("__any_{name}({operand})"), Rep::Any, dst_rep);
}
Not => match vr(0) {
Rep::Int => format!("~{}", v(0)),
r => format!("~__any_to_int32({})", self.conv(&v(0), r, Rep::Any)),
},
// ── Comparisons ──
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq => {
let op = match instr.op {
Lt => "<", Lte => "<=", Gt => ">", Gte => ">=",
Eq | StrictEq => "==", _ => "!=",
};
let eq = matches!(instr.op, Eq | Neq | StrictEq | StrictNeq);
match (vr(1), vr(0)) {
_ if numbers => format!("{l} {op} {r}"),
(Rep::Bool, Rep::Bool) => format!("{l} {op} {r}"),
(Rep::Str, Rep::Str) => match instr.op {
Eq | StrictEq => format!("jsrt_string_eq({l}, {r})"),
Neq | StrictNeq => format!("!jsrt_string_eq({l}, {r})"),
_ => format!("jsrt_string_cmp({l}, {r}) {op} 0"),
},
// Two objects are equal if they are one object
(Rep::Obj(_), Rep::Obj(_)) if eq => format!("(void *){l} {op} (void *){r}"),
(lr, rr) => {
let name = match instr.op {
Lt => "lt", Lte => "lte", Gt => "gt", Gte => "gte", Eq => "eq",
Neq => "neq", StrictEq => "strict_eq", _ => "strict_neq",
};
let (la, ra) = (self.conv(&l, lr, Rep::Any), self.conv(&r, rr, Rep::Any));
format!("__any_{name}({la}, {ra})")
}
}
}
Lnot => format!("!{}", self.truthy(&v(0), vt(0))),
// ── Types of values only known at runtime ──
Typeof => format!("__any_typeof({})", self.conv(&v(0), vr(0), Rep::Any)),
TypeofIsUndefined | TypeofIsFunction | IsUndefined | IsNull | IsUndefinedOrNull => {
let name = match instr.op {
TypeofIsFunction => "is_function",
IsNull => "is_null",
IsUndefinedOrNull => "is_nullish",
_ => "is_undefined",
};
format!("__any_{name}({})", self.conv(&v(0), vr(0), Rep::Any))
}
// ── Movers ──
SetLoc | SetName | ToPropkey => v(0),
// ── Objects ──
DefineField | PutField => return self.write_field(res, instr, out),
GetField | GetLength => return self.read_field(res, instr, out),
// ── Calls ──
Call | CallMethod | CallConstructor => return self.call(res, (fn_id, idx), instr, out),
// ── Returns ──
Return | ReturnAsync => {
let value = self.conv(&v(0), vr(0), self.rep(&res.ret_type));
let _ = writeln!(out, " return {value};");
return;
}
ReturnUndef => {
let value = self.conv("(undefined_t){0}", Rep::Undefined, self.rep(&res.ret_type));
let _ = writeln!(out, " return {value};");
return;
}
_ => unimplemented!("emit {instr:?}"),
};
self.assign(out, &dst, &expr, rep, dst_rep);
}
}
+354
View File
@@ -0,0 +1,354 @@
//! A JS function as a C function
//!
//! Infer walks a JS function once for each context, which is the function in one call site,
//! and each set of arg types. Each walk is one C function, `js<walk>`, in which every value
//! has the type that infer found in that walk.
//!
//! Each local, arg and reg is a C local, of the C type of its type. A local or arg that
//! closures share is a pointer to a cell (see transform/closure.rs). The cell is made when the
//! function starts, and again at each `close_loc`. A shared arg is copied into its cell when
//! the function starts.
//!
//! Each block that can run is a C label. Its instrs come in order, then its postfix copies,
//! then its jump. If infer found that a condition is always true or always false, the jump
//! goes directly to the one block that can run.
//!
//! Most instrs are one line: `SET(dst, JS_<op>(operands))`. The runtime's macros convert
//! each operand and the result, by their C types (see lib/rt/core.h).
use super::program::{Program, slots};
use crate::{
infer::{Context, Type},
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
regify::{BasicBlock, Operand, RegFn, RegInstr},
};
/// The undefined value, as C
pub const UNDEFINED: &str = "(undefined_t){0}";
/// A walk of a JS function, as its C function is written
pub struct JsFunction<'a> {
pub program: &'a Program<'a>,
pub walk: usize,
pub context: Context, // the walk's function, and the call site that it is called from
pub f: &'a RegFn,
shared: Vec<Operand>, // the locals and args that closures share: each is a pointer to a cell
}
/// `static <ret> js<walk>(<params>)`. A shared arg's param is named `p<k>`, because its value
/// goes into a cell named `a<k>`
pub fn signature(program: &Program, walk: usize) -> String {
let f = program.js_fn(walk);
let shared = f.captured_slots();
let params: Vec<String> = (0..f.arg_count)
.map(|k| {
let ctype = program.slot_ctype(walk, Operand::Arg(k)).c_name();
let prefix = if shared.contains(&Operand::Arg(k)) {
"p"
} else {
"a"
};
format!("{ctype} {prefix}{k}")
})
.collect();
let params = if params.is_empty() {
"void".into()
} else {
params.join(", ")
};
let ret = program.ret_ctype(walk).c_name();
format!("static {ret} js{walk}({params})")
}
/// The body of `js<walk>`: its locals, args and regs, then each block that can run
pub fn body(program: &Program, walk: usize) -> String {
let f = program.js_fn(walk);
let reachable = &program.cache.walks[walk].reachable;
let js = JsFunction {
program,
walk,
context: program.cache.walks[walk].context,
f,
shared: f.captured_slots(),
};
let mut c = indent(&js.declarations());
for (i, block) in f.blocks.iter().enumerate() {
if reachable[i] {
c += &format!("b{i}:;\n");
c += &indent(&js.block(block, reachable));
}
}
c
}
/// Each line of `code`, indented once
pub fn indent(code: &str) -> String {
code.lines().map(|line| format!(" {line}\n")).collect()
}
impl JsFunction<'_> {
/// The C name of a local, arg or reg. A shared one names the pointer to its cell
pub fn name(&self, slot: Operand) -> String {
match slot {
Operand::Local(k) => format!("l{k}"),
Operand::Arg(k) => format!("a{k}"),
Operand::Reg(k) => format!("r{k}"),
}
}
/// The value of a local, arg or reg, as C. A shared one is the value in its cell
pub fn get(&self, slot: Operand) -> String {
match self.shared.contains(&slot) {
true => format!("(*{})", self.name(slot)),
false => self.name(slot),
}
}
/// `dst = value`, converted to the C type of `dst`
pub fn set(&self, dst: Operand, value: &str) -> String {
format!("SET({}, {value});\n", self.get(dst))
}
/// The type of a local, arg or reg
pub fn ty(&self, slot: Operand) -> Type {
self.program.slot_type(self.walk, slot)
}
/// A C local for each local and reg, and a cell for each shared local and arg
fn declarations(&self) -> String {
let mut c = String::new();
for slot in slots(self.f) {
let ctype = self.program.slot_ctype(self.walk, slot).c_name();
let name = self.name(slot);
let cell = format!("__rt_alloc(sizeof({ctype}))");
c += &match (slot, self.shared.contains(&slot)) {
(Operand::Arg(_), false) => continue,
(Operand::Arg(k), true) => format!("{ctype} *{name} = {cell}; *{name} = p{k};\n"),
(_, true) => format!("{ctype} *{name} = {cell};\n"),
(_, false) => format!("{ctype} {name} = {{0}};\n"),
};
}
c
}
/// A block: its instrs, its postfix copies, then its jump. If an instr cannot run, nor
/// can the rest of the block
fn block(&self, block: &BasicBlock, reachable: &[bool]) -> String {
use OpCode::*;
let last = &self.f.code[block.end - 1];
let jumps = matches!(
last.op,
Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8
);
let end = if jumps { block.end - 1 } else { block.end };
let mut c = String::new();
for idx in block.start..end {
match self.instr(idx) {
Some(code) => c += &code,
None => return c + "abort(); // not reached\n",
}
}
for copy in &block.postfix {
c += &self.set(copy.dsts[0], &self.get(copy.args[0]));
}
// succ[0] is the jump's target, and succ[1] is the next block
let can_go = |i: usize| block.succ.get(i).filter(|&&s| reachable[s]).copied();
c + &match last.op {
IfFalse | IfFalse8 | IfTrue | IfTrue8 => match (can_go(0), can_go(1)) {
(Some(target), Some(next)) => {
let not = if matches!(last.op, IfTrue | IfTrue8) {
""
} else {
"!"
};
let cond = self.get(last.args[0]);
format!("if ({not}TRUTHY({cond})) goto b{target};\ngoto b{next};\n")
}
(Some(only), None) | (None, Some(only)) => format!("goto b{only};\n"),
(None, None) => "abort(); // not reached\n".into(),
},
Return | ReturnAsync | ReturnUndef => String::new(),
_ => can_go(0).map_or(String::new(), |next| format!("goto b{next};\n")),
}
}
/// The C of instr `idx`. `None` if an operand has no value: then the instr cannot run
fn instr(&self, idx: usize) -> Option<String> {
use OpCode::*;
let instr = &self.f.code[idx];
// The first operand of `define_field_ref` is a location, not a value
let values = match instr.op {
DefineFieldRef => &instr.args[1..],
_ => &instr.args[..],
};
if values.iter().any(|&slot| self.ty(slot) == Type::Unknown) {
return None;
}
let get = |i: usize| self.get(instr.args[i]);
Some(match instr.op {
Nop => String::new(),
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8
| PushI16 | PushI32 | PushTrue | PushFalse | PushAtomValue | PushEmptyString
| PushConst | PushConst8 | Undefined | Null => {
self.set(instr.dsts[0], &self.literal(instr))
}
// Copies. A key stays as it is: a number is an index
SetLoc | SetName | ToPropkey => self.set(instr.dsts[0], &get(0)),
// The bindings at the top of a module, which are C globals (see link.rs)
GetVar => self.set(instr.dsts[0], &format!("g{}", global(instr))),
PutVar => format!("SET(g{}, {});\n", global(instr), get(0)),
PostInc | PostDec => {
let name = if instr.op == PostInc {
"POST_INC"
} else {
"POST_DEC"
};
let (old, new) = (self.get(instr.dsts[0]), self.get(instr.dsts[1]));
format!("{name}({old}, {new}, {});\n", get(0))
}
Return | ReturnAsync => self.ret(&get(0)),
ReturnUndef => self.ret(UNDEFINED),
// A new cell for a shared local (see transform/closure.rs). A local that is not
// shared has no cell, so nothing changes
CloseLoc => match instr.imm {
ImmOperand::Loc(k) if self.shared.contains(&Operand::Local(k as usize)) => {
format!("NEW_CELL({});\n", self.name(Operand::Local(k as usize)))
}
_ => String::new(),
},
Call | CallMethod | CallConstructor => self.call(idx, instr),
// An operator or a test: the runtime's macro of the same name. The operands that
// an instr pops come top first (see regify), so they are reversed
op => match operator(op) {
Some(name) => {
let operands: Vec<String> = (0..instr.args.len()).rev().map(get).collect();
self.set(instr.dsts[0], &format!("{name}({})", operands.join(", ")))
}
None => self.object_instr(idx, instr),
},
})
}
/// `return value`, converted to the C type that the walk returns
fn ret(&self, value: &str) -> String {
let ctype = self.program.ret_ctype(self.walk).c_name();
format!("return CAST({ctype}, {value});\n")
}
/// A literal, as C. Each has a cast, so that its C type is the one that its member has
fn literal(&self, instr: &RegInstr) -> String {
use OpCode::*;
match (instr.op, instr.imm) {
(PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7, _) => {
int(instr.op as i64 - Push0 as i64)
}
(_, ImmOperand::I8(n)) => int(n as i64),
(_, ImmOperand::I16(n)) => int(n as i64),
(_, ImmOperand::I32(n)) => int(n as i64),
(PushTrue, _) => "(bool)1".into(),
(PushFalse, _) => "(bool)0".into(),
(PushAtomValue, _) => self
.program
.string(&instr.imm.atom().text(self.program.atoms)),
(PushEmptyString, _) => self.program.string(""),
(Undefined, _) => UNDEFINED.into(),
(Null, _) => "(null_t){0}".into(),
_ => match &self.f.cpool[instr.imm.const_idx()] {
JsValue::Int(n) => int(*n as i64),
JsValue::Float(x) if x.is_nan() => "((double)NAN)".into(),
JsValue::Float(x) if x.is_infinite() && *x > 0.0 => "((double)INFINITY)".into(),
JsValue::Float(x) if x.is_infinite() => "(-(double)INFINITY)".into(),
JsValue::Float(x) => format!("{x:?}"),
JsValue::String(s) => self.program.string(&s.text()),
JsValue::BoolTrue => "(bool)1".into(),
JsValue::BoolFalse => "(bool)0".into(),
JsValue::Undefined => UNDEFINED.into(),
JsValue::NativeFn(idx) => format!("(native_t){{{idx}}}"),
c => unimplemented!("the constant {c:?}"),
},
}
}
}
/// The global that `get_var` or `put_var` reads or writes (see transform/modules.rs)
fn global(instr: &RegInstr) -> u32 {
match instr.imm {
ImmOperand::Global(global) => global,
imm => unreachable!("a global name that is not a global: {imm:?}"),
}
}
/// An int, as C
fn int(n: i64) -> String {
match n == i32::MIN as i64 {
true => "(int32_t)INT32_MIN".into(),
false => format!("(int32_t){n}"),
}
}
/// The runtime's macro for an operator or a test (see lib/rt/core.h)
fn operator(op: OpCode) -> Option<&'static str> {
use OpCode::*;
Some(match op {
Add => "ADD",
Sub => "SUB",
Mul => "MUL",
Div => "DIV",
Mod => "MOD",
Pow => "POW",
Neg => "NEG",
Plus => "PLUS",
Inc => "INC",
Dec => "DEC",
Not => "NOT",
And => "AND",
Or => "OR",
Xor => "XOR",
Shl => "SHL",
Sar => "SAR",
Shr => "SHR",
Lt => "LT",
Lte => "LTE",
Gt => "GT",
Gte => "GTE",
StrictEq => "STRICT_EQ",
StrictNeq => "STRICT_NEQ",
Eq => "EQ",
Neq => "NEQ",
Lnot => "LNOT",
Typeof => "TYPEOF",
IsUndefined | TypeofIsUndefined => "IS_UNDEFINED",
IsNull => "IS_NULL",
IsUndefinedOrNull => "IS_NULLISH",
TypeofIsFunction => "IS_FUNCTION",
_ => return None,
})
}
+99 -284
View File
@@ -1,310 +1,125 @@
//! Emits C: one fn for each walk that the program calls, one struct for each
//! cell (see `docs/model.md`, section 5). Cells and walks that C cannot tell
//! apart share one struct and one fn (see `classes()`)
//! Emits the program as one C file
//!
//! A value's C representation is a function of its type (`rep()`), one for
//! each kind of type. An object is a pointer to a struct of its cell. Each
//! object starts with a header that holds the number of its cell, and the
//! fields that can be absent start as absent
//! The C file holds only three things: types, statics and functions. `Program::new` finds
//! all of them, from the JS functions and what infer found, and `emit` writes each of them,
//! in an order that C can compile:
//!
//! The runtime (`lib/rt/`) gives the primitive types, the string ops, the
//! `any_t` ops and the allocator
use std::{
cell::RefCell,
collections::{BTreeSet, HashMap, HashSet},
fmt::Write,
};
use crate::{
infer::{
HOST, HOST_COMPLETE, InferResult, Place, TypeCache,
cells::{Cell, Code, LENGTH_FIELD, ObjKind, read_value},
types::Type,
},
libqjs::parse::{Atom, AtomRef, ImmOperand, JsValue, opcodes::OpCode},
native::{self, NATIVES, ProtoOf, atom_name, js_string_text},
regify::{BasicBlock, RegFn, RegInstr},
transform::{FIRST_ARG, PROTO_FIELD, THIS_ARG},
};
//! - **Types:** a struct for each class of objects (see `classes`).
//! - **Statics:** each string literal, and a C global for each binding at the top of a module.
//! - **Functions:** one for each walk of a JS function, one for each lookup, and the host's
//! two entry points (see `functions`).
//!
//! The C of a JS function is mostly macro calls. The runtime's macros know how each kind of
//! value behaves (see lib/rt/core.h), so emit only says which operator to use.
mod calls;
mod classes;
mod conv;
mod instrs;
mod functions;
mod js;
mod objects;
mod reps;
mod program;
use classes::*;
use reps::*;
use crate::{infer::TypeCache, libqjs::parse::Atom, link::Link, regify::RegFn};
/// The number of a class of cells in C: the number in the header of their
/// objects, and the name of their struct
type CellNum = u32;
use classes::ClassKind;
use program::Program;
pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom]) -> String {
let (nums, cells, walk_rep) = classes(fns, cache);
const HEADER: &str = "// Generated by jsrt\n#define JSRT_IMPLEMENTATION\n#include \"jsrt.h\"\n\n";
let mut e = Emitter {
fns,
cache,
atoms,
cells,
nums,
walk_rep,
queued: vec![false; cache.walks.len()],
queue: vec![],
helpers: HashSet::new(),
sums: RefCell::new(BTreeSet::new()),
helper_protos: String::new(),
helper_defs: String::new(),
};
/// The C of the program, which includes the runtime's `jsrt.h`
pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom], link: &Link) -> String {
let program = Program::new(fns, cache, atoms, link);
let classes = 0..program.classes.classes.len();
let mut c = String::from(HEADER);
// Every struct is declared first, so that a field can point to any of
// them
let mut structs = String::new();
for id in 0..e.cells.len() as CellNum {
let _ = writeln!(structs, "typedef struct {0} {0};", struct_name(id));
// Types. Each struct is declared first, because a struct can point to another
for class in classes.clone() {
c += &format!("struct c{class};\n");
}
for id in 0..e.cells.len() as CellNum {
e.struct_def(id, &mut structs);
c += &class_table(&program);
for class in classes {
c += &struct_definition(&program, class);
}
// Which cells are fns, by number, for the runtime's `typeof`
let fn_cells: Vec<&str> = e.cells.iter().map(|cell| if cell.code().is_some() { "1" } else { "0" }).collect();
let mut statics = format!("const bool jsrt_fn_cells[] = {{ {} }};\n\n", fn_cells.join(", "));
// A built-in fn is one static object
let mut init = String::new();
for cell in cache.builtins() {
let id = e.num(cell);
let name = struct_name(id);
let _ = writeln!(statics, "static {name} {};", builtin_name(id));
let _ = writeln!(init, " init_{name}(&{}, {id});", builtin_name(id));
// Statics
for (id, text) in program.strings.iter().enumerate() {
c += &string_definition(id, text);
}
for kind in ProtoOf::ALL {
let ty = &cache.protos[kind as usize];
for (g, ctype) in program.globals.iter().enumerate() {
c += &format!("static {} g{g};\n", ctype.c_name());
}
if *ty != Type::Unknown {
let _ = writeln!(statics, "static {} {};", e.c_type(e.rep(ty)), proto_global(kind));
// Functions. Each is declared first, because they call each other
c += "\n";
for function in &program.functions {
c += &format!("{};\n", function.signature(&program));
}
c += "\n";
for function in &program.functions {
c += &function.definition(&program);
}
c
}
/// What the runtime needs to know of the classes (see lib/rt/core.h): the class of a pointer
/// to an object, and whether a class is closures. The table ends in a 0, because C has no
/// empty arrays
fn class_table(program: &Program) -> String {
let classes = &program.classes.classes;
let tags: String = (0..classes.len())
.map(|id| format!(" struct c{id} *: {id}u, \\\n"))
.collect();
let closures: String = (classes.iter())
.map(|class| matches!(class.kind, ClassKind::Closure(_)) as u8)
.map(|is_function| format!("{is_function}, "))
.collect();
let class = format!("#define JSRT_CLASS(x) _Generic((x), \\\n{tags} default: 0u)\n");
let is_function = format!("const bool jsrt_class_is_function[] = {{{closures}0}};\n");
format!("\n{class}\n{is_function}\n")
}
/// The struct of a class: an array's length and elements, then the fields. A field that
/// shares a location is a pointer to it. A field that some objects lack has a `has_` flag
fn struct_definition(program: &Program, id: usize) -> String {
let class = &program.classes.classes[id];
let mut c = format!("struct c{id} {{\n");
if class.kind == ClassKind::Array {
c += &format!(
" int32_t length, cap;\n {} *items;\n",
class.elems.c_name()
);
}
for field in &class.fields {
let name = program.field_name(field.name);
let pointer = if field.shared { "*" } else { "" };
c += &format!(" {} {pointer}{name};\n", field.ctype.c_name());
if !field.defined {
c += &format!(" bool has_{name};\n");
}
}
if cache.host_complete != Type::Unknown {
let _ = writeln!(statics, "static {} {HOST_COMPLETE_GLOBAL};", e.c_type(e.rep(&cache.host_complete)));
// C has no empty structs
if class.kind != ClassKind::Array && class.fields.is_empty() {
c += " char _;\n";
}
// The walks that the code calls, from the host's calls. The host calls the
// entry with nothing: its env and `this` are undefined
let entry = cache.find(fns.len() - 1, HOST, &[], fns);
let entry_args = vec!["(undefined_t){0}"; cache.walks[entry].req.arg_types.len()].join(", ");
e.use_walk(entry);
let complete = e.host_complete();
let mut fn_protos = String::new();
let mut bodies = String::new();
while let Some(w) = e.queue.pop() {
let res = cache.walks[w].result.as_ref().unwrap();
let _ = writeln!(fn_protos, "{};", e.signature(w, res));
e.emit_fn(w, res, &mut bodies);
}
// The program is one file, so it also holds the runtime. The runtime goes
// first (see `lib/rt/jsrt.h`)
let mut out = String::from(
"#define JSRT_IMPLEMENTATION\n#include \"jsrt.h\"\n#include <math.h>\n#include <stdlib.h>\n\n// Natives\n",
);
for n in NATIVES {
let params: Vec<String> = n.args.iter().map(|t| e.c_type(e.rep(t))).collect();
let _ = writeln!(out, "{} {}({});", e.c_type(e.rep(&n.ret)), n.name, params.join(", "));
}
let _ = write!(
out,
"\n{}\n{structs}\n{statics}\n{fn_protos}\n{}\n{}{bodies}",
sum_defs(&e.sums.borrow()),
e.helper_protos,
e.helper_defs
);
// What the host calls (see `lib/rt/jsrt.h`)
let _ = writeln!(out, "void jsrt_start(void) {{\n{init} {}({entry_args});\n}}\n", e.fn_name(entry));
let _ = writeln!(out, "void jsrt_complete(int32_t h) {{\n{complete}}}");
out
c + "};\n\n"
}
struct Emitter<'a> {
fns: &'a [RegFn],
cache: &'a TypeCache,
atoms: &'a [Atom],
cells: Vec<Cell>, // a cell of each class, by number
nums: HashMap<Cell, CellNum>, // the class of each cell
walk_rep: Vec<usize>, // by walk: the walk whose fn it uses
queued: Vec<bool>, // the walks that the code calls, by walk
queue: Vec<usize>, // the walks that the code calls, which are not written yet
helpers: HashSet<String>, // the helpers made so far, by name
sums: RefCell<BTreeSet<Kinds>>, // the tagged unions that the C uses
helper_protos: String,
helper_defs: String,
}
impl<'a> Emitter<'a> {
fn num(&self, cell: &Cell) -> CellNum {
self.nums[cell]
}
/// The number of the cell of an `Obj` type
fn num_of(&self, ty: &Type) -> CellNum {
match ty {
Type::Obj(cell) => self.num(cell),
_ => unreachable!(),
}
}
/// Walk `w` is called, so the walk whose fn it uses is written
fn use_walk(&mut self, w: usize) {
let w = self.walk_rep[w];
if !self.queued[w] {
self.queued[w] = true;
self.queue.push(w);
}
}
fn signature(&self, w: usize, res: &InferResult) -> String {
let params: Vec<String> = (self.cache.walks[w].req.arg_types.iter().enumerate())
.map(|(i, ty)| format!("{} p{i}", self.c_type(self.rep(ty))))
.collect();
let ret = self.c_type(self.rep(&res.ret_type));
format!("static {ret} {}({})", self.fn_name(w), params.join(", "))
}
fn emit_fn(&mut self, w: usize, res: &InferResult, out: &mut String) {
let fn_id = self.cache.walks[w].req.fn_id;
let f = &self.fns[fn_id];
let mut body = String::new();
// Args are values `var_count + i` (see regify)
for (i, ty) in self.cache.walks[w].req.arg_types.iter().enumerate() {
let arg = f.var_count + i;
self.assign(&mut body, &format!("v{arg}"), &format!("p{i}"), self.rep(ty), self.rep(&res.val_types[arg]));
}
for (b, block) in f.blocks.iter().enumerate() {
let _ = writeln!(body, "L{b}:;");
if !res.reachable[b] {
let _ = writeln!(body, " abort();");
continue;
}
let code = &f.code[block.start..block.end];
let (instrs, last) = match code.split_last() {
Some((last, rest)) if is_terminator(last.op) => (rest, Some(last)),
_ => (code, None),
};
for (i, instr) in instrs.iter().enumerate() {
self.emit_instr(fn_id, res, block.start + i, instr, &mut body);
}
// Postfix copies carry values into the next block's stack slots
for copy in &block.postfix {
let (src, dst) = (copy.args[0], copy.dsts[0]);
let (from, to) = (self.rep(&res.val_types[src]), self.rep(&res.val_types[dst]));
self.assign(&mut body, &format!("v{dst}"), &format!("v{src}"), from, to);
}
let goto = |i: usize| format!("goto L{};", block.succ[i]);
match last.map(|l| l.op) {
Some(OpCode::IfFalse | OpCode::IfFalse8 | OpCode::IfTrue | OpCode::IfTrue8) => {
let last = last.unwrap();
let cond = self.truthy(&format!("v{}", last.args[0]), &res.val_types[last.args[0]]);
let not = if matches!(last.op, OpCode::IfFalse | OpCode::IfFalse8) { "!" } else { "" };
let _ = writeln!(body, " if ({not}{cond}) {} else {}", goto(0), goto(1));
}
Some(OpCode::Goto | OpCode::Goto8 | OpCode::Goto16) => {
let _ = writeln!(body, " {}", goto(0));
}
Some(_) => self.emit_instr(fn_id, res, block.end - 1, last.unwrap(), &mut body),
None if !block.succ.is_empty() => {
let _ = writeln!(body, " {}", goto(0));
}
None => {}
}
}
let _ = writeln!(out, "{} {{", self.signature(w, res));
for (v, ty) in res.val_types.iter().enumerate() {
let _ = writeln!(out, " {} v{v};", self.c_type(self.rep(ty)));
}
let _ = writeln!(out, "{body}}}\n");
}
/// A new object of cell `id`, on the heap
fn alloc(&self, id: CellNum) -> String {
let name = struct_name(id);
format!("init_{name}(jsrt_alloc(sizeof({name})), {id})")
}
/// The C fn of walk `w`: the fn of the walk of its class
fn fn_name(&self, w: usize) -> String {
let w = self.walk_rep[w];
format!("f{}_{w}", self.cache.walks[w].req.fn_id)
}
}
/// `s` as a C string literal. Every byte that is not plain printable ASCII is
/// an octal escape, so UTF-8 passes through unchanged
fn c_str(s: &str) -> String {
let mut lit = String::from("\"");
for b in s.bytes() {
match b {
b'"' | b'\\' => lit.extend(['\\', b as char]),
0x20..0x7f => lit.push(b as char),
_ => lit += &format!("\\{b:03o}"),
}
}
lit + "\""
}
fn is_num(rep: Rep) -> bool {
matches!(rep, Rep::Int | Rep::Double)
}
/// The struct of the objects of cell `id`
fn struct_name(id: CellNum) -> String {
format!("S{id}")
}
/// The static object of the built-in fn of cell `id`
fn builtin_name(id: CellNum) -> String {
format!("b{id}")
}
/// The global that holds what the host calls when an IO op completes
const HOST_COMPLETE_GLOBAL: &str = "jsrt_host_complete";
fn proto_global(kind: ProtoOf) -> String {
format!("proto_{kind:?}").to_lowercase()
}
fn is_terminator(op: OpCode) -> bool {
use OpCode::*;
matches!(
op,
Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8 | Return | ReturnAsync | ReturnUndef
)
/// A string literal, with its UTF-16 units
fn string_definition(id: usize, text: &str) -> String {
let units: Vec<String> = text.encode_utf16().map(|u| u.to_string()).collect();
let (length, units) = (units.len(), units.join(", "));
format!("static struct jsrt_string str{id} = {{{length}, {{{units}}}}};\n")
}
+202 -203
View File
@@ -1,231 +1,230 @@
//! Field reads and writes, array elements, and prototype chains
//! The C of the instrs on objects, and of lookups
//!
//! A field is a member of its class's struct. If every object of the class has the field from
//! the start, the field is used directly. Else the struct has a `has_` flag for the field, and
//! getting the field calls its lookup. A field that shares a location (see
//! `define_field_ref`) holds a pointer to it.
//!
//! An instr on a value that can be one of several members has C for each member (see
//! `Program::split`).
use super::*;
use super::classes::{ClassKind, Member};
use super::js::{JsFunction, UNDEFINED, indent};
use super::program::{Program, throw};
use crate::{
infer::{LENGTH_FIELD, ObjAddr},
libqjs::parse::{AtomRef, opcodes::OpCode},
regify::{Operand, RegInstr},
transform::PROTO_FIELD,
};
impl<'a> Emitter<'a> {
/// `obj.field = value`. A primitive has no fields to write, and undefined
/// and null throw
pub(super) fn write_field(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) {
let (value, obj) = (instr.args[0], instr.args[1]);
let value_rep = self.rep(&res.val_types[value]);
let field = instr.imm.atom();
let value = format!("v{value}");
impl JsFunction<'_> {
pub(super) fn object_instr(&self, idx: usize, instr: &RegInstr) -> String {
use OpCode::*;
self.dispatch(&format!("v{obj}"), &res.val_types[obj], "jsrt_type_error();", out, |e, member, obj, _| {
match member {
Type::Obj(cell) if field == LENGTH_FIELD && cell.is_array() => {
let name = struct_name(e.num(cell));
Some(format!("set_length_{name}({obj}, {});", e.conv(&value, value_rep, Rep::Int)))
}
Type::Obj(cell) => {
let slot = e.slot_rep(&cell.field(field).unwrap().ty, cell.can_be_absent(field));
Some(format!("{obj}->{} = {};", e.field_name(field), e.conv(&value, value_rep, slot)))
}
Type::Undefined | Type::Null => None,
_ => Some(";".into()),
match instr.op {
Object | Fclosure => {
let class = self.class_here(idx);
let alloc = format!("(struct c{class} *)__rt_alloc(sizeof(struct c{class}))");
self.set(instr.dsts[0], &alloc)
}
});
ArrayFrom => self.array_from(idx, instr),
GetField | GetLength => self.get_field(instr),
DefineField | PutField => self.put_field(instr),
DefineFieldRef => self.share_field(instr),
CloseField => self.close_field(instr),
GetArrayEl => self.get_elem(instr),
PutArrayEl => self.put_elem(instr),
_ => unimplemented!("emit {instr:?}"),
}
}
/// `dst = obj.field`: on an object, the read through its prototype chain;
/// on a primitive, the read on its kind's prototype, except that a string's
/// `length` is its own. Undefined and null throw
pub(super) fn read_field(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) {
let (obj, dst) = (instr.args[0], instr.dsts[0]);
let dst_rep = self.rep(&res.val_types[dst]);
/// The class of the objects that instr `idx` makes, in this walk's context
fn class_here(&self, idx: usize) -> usize {
let context = self.context;
self.program.classes.class(ObjAddr {
context,
instr: idx,
})
}
/// `[a, b, ...]`. The operands are the elements, last first
fn array_from(&self, idx: usize, instr: &RegInstr) -> String {
let class = self.class_here(idx);
let mut c = format!("struct c{class} *a;\nARRAY_NEW(a, {});\n", instr.args.len());
for (i, &elem) in instr.args.iter().rev().enumerate() {
c += &format!("SET(a->items[{i}], {});\n", self.get(elem));
}
c += &self.set(instr.dsts[0], "a");
format!("{{\n{}}}\n", indent(&c))
}
/// `o.f` and `o.length`. A string has its own length, and so does an array. A primitive
/// gets the field from the stdlib's prototype of its kind (see `Program::field_class`).
/// Undefined and null throw
fn get_field(&self, instr: &RegInstr) -> String {
let program = self.program;
let dst = instr.dsts[0];
let field = match instr.op {
OpCode::GetLength => LENGTH_FIELD,
_ => instr.imm.atom(),
};
self.dispatch(&format!("v{obj}"), &res.val_types[obj], "jsrt_type_error();", out, |e, member, obj, _| {
let (read, rep) = match member {
Type::Obj(cell) => e.read_on(cell, field, obj),
_ => e.read_primitive(member, obj, field)?,
};
Some(format!("v{dst} = {};", e.conv(&read, rep, dst_rep)))
});
self.split(instr.args[0], |obj, member| {
match (member, program.field_class(member, field)) {
(Member::Obj(_), Some(class)) => {
self.set(dst, &program.field_value(class, field, obj))
}
(_, Some(class)) => {
let (proto, _) = program.builtin_proto(member).unwrap();
self.set(dst, &program.field_value(class, field, &proto))
}
(Member::Str, None) if field == LENGTH_FIELD => {
self.set(dst, &format!("__rt_string_length({obj})"))
}
(Member::Obj(_), None) => self.set(dst, &format!("{obj}->length")),
(Member::Undefined | Member::Null, _) => {
throw("cannot read a field of undefined or null")
}
(Member::Native(_), _) => throw("a native function has no fields"),
_ => throw("fields of primitives need the stdlib"),
}
})
}
/// `dst = a[i]` or `a[i] = value`, on an array, and `dst = s[i]` on a
/// string. A string cannot be written, and undefined and null throw
pub(super) fn element(&mut self, res: &InferResult, instr: &RegInstr, out: &mut String) {
let put = instr.op == OpCode::PutArrayEl;
let (index, obj) = (instr.args[put as usize], instr.args[put as usize + 1]);
let index = self.conv(&format!("v{index}"), self.rep(&res.val_types[index]), Rep::Int);
/// `o.f = v`, and a field in an object literal. Setting an array's `length` sets its
/// length. A primitive has no fields to set, and undefined and null throw
fn put_field(&self, instr: &RegInstr) -> String {
let program = self.program;
let field = instr.imm.atom();
let value = self.get(instr.args[0]);
// A put has the value, a get the dst
let (v, v_rep) = match put {
true => (instr.args[0], self.rep(&res.val_types[instr.args[0]])),
false => (instr.dsts[0], self.rep(&res.val_types[instr.dsts[0]])),
self.split(instr.args[1], |obj, member| {
match (member, program.class_kind(member)) {
(Member::Obj(_), ClassKind::Array) if field == LENGTH_FIELD => {
format!("ARRAY_SET_LENGTH({obj}, {value});\n")
}
(Member::Obj(class), _) => {
let f = program.classes.field(class, field).unwrap();
let set = format!("SET({}, {value});\n", program.field_access(obj, f));
match f.defined {
true => set,
false => {
set + &format!("{obj}->has_{} = true;\n", program.field_name(field))
}
}
}
_ => throw("cannot set a field of a primitive, undefined or null"),
}
})
}
/// `define_field_ref`: the field becomes a pointer to the cell of the location in the
/// first operand. That location is a shared local or arg, whose C local points to its
/// cell, or a reg that holds an object whose field of the same name points to the cell
/// (see regify/walk.rs)
fn share_field(&self, instr: &RegInstr) -> String {
let name = self.program.field_name(instr.imm.atom());
let cell = match instr.args[0] {
Operand::Reg(_) => format!("{}->{name}", self.get(instr.args[0])),
slot => self.name(slot),
};
self.dispatch(&format!("v{obj}"), &res.val_types[obj], "jsrt_type_error();", out, |e, member, obj, _| {
let id = match member {
Type::Obj(cell) => e.num(cell),
Type::Str(_) if !put => {
let c = format!("jsrt_string_at({obj}, {index})");
return Some(format!("v{v} = {};", e.conv(&c, Rep::OptStr, v_rep)));
}
_ => return None,
};
let (name, elem) = (struct_name(id), e.elem_rep(id));
Some(match put {
true => format!("set_elem_{name}({obj}, {index}, {});", e.conv(&format!("v{v}"), v_rep, elem)),
false => format!("v{v} = {};", e.conv(&format!("elem_{name}({obj}, {index})"), elem, v_rep)),
})
});
self.split(instr.args[1], |obj, _| format!("{obj}->{name} = {cell};\n"))
}
/// A read of `field` on the object of `cell` at `ptr`, and its rep. An own
/// field that is never absent is read in place. Any other read goes
/// through the prototype chain
pub(super) fn read_on(&mut self, cell: &Cell, field: AtomRef, ptr: &str) -> (String, Rep) {
if field == LENGTH_FIELD && cell.is_array() {
return (format!("{ptr}->length"), Rep::Int);
}
/// `close_field x`: the field, which points to a cell, gets a new cell with the same
/// value. Closures that point to the old cell keep it (see transform/closure.rs)
fn close_field(&self, instr: &RegInstr) -> String {
let name = self.program.field_name(instr.imm.atom());
self.split(instr.args[0], |obj, _| {
format!("NEW_CELL({obj}->{name});\n")
})
}
match cell.field(field) {
Some(f) if !cell.can_be_absent(field) => (format!("{ptr}->{}", self.field_name(field)), self.rep(&f.ty)),
_ => {
let (reader, rep) = self.field_reader(cell, field);
(format!("{reader}({ptr})"), rep)
/// `a[i]` on an array, and `s[i]` on a string. An index out of range gives undefined.
/// Undefined and null throw
fn get_elem(&self, instr: &RegInstr) -> String {
let (dst, index) = (self.get(instr.dsts[0]), self.get(instr.args[0]));
self.split(instr.args[1], |obj, member| {
match (member, self.program.class_kind(member)) {
(Member::Obj(_), ClassKind::Array) => {
format!("ARRAY_GET({dst}, {obj}, {index});\n")
}
(Member::Str, _) => format!("STRING_GET({dst}, {obj}, {index});\n"),
(Member::Undefined | Member::Null, _) => {
throw("cannot read an element of undefined or null")
}
_ => unreachable!("infer gets elements only of arrays and strings"),
}
}
})
}
/// A read of `field` on `e`, a primitive of type `ty`, and its rep: a
/// string's length, or a read on its kind's prototype. `None` if it throws
pub(super) fn read_primitive(&mut self, ty: &Type, e: &str, field: AtomRef) -> Option<(String, Rep)> {
let kind = ProtoOf::of(ty)?;
/// `a[i] = v` on an array. Anything else throws
fn put_elem(&self, instr: &RegInstr) -> String {
let (value, index) = (self.get(instr.args[0]), self.get(instr.args[1]));
if kind == ProtoOf::String && field == LENGTH_FIELD {
return Some((format!("jsrt_string_length({e})"), Rep::Int));
}
Some(self.read_proto(kind, field))
}
/// A read of `field` on the prototype that the stdlib links for `kind`,
/// and its rep
pub(super) fn read_proto(&mut self, kind: ProtoOf, field: AtomRef) -> (String, Rep) {
match &self.cache.protos[kind as usize] {
Type::Unknown => ("(undefined_t){0}".into(), Rep::Undefined),
Type::Obj(cell) => self.read_on(&cell.clone(), field, &proto_global(kind)),
_ => self.proto_reader(kind, field),
}
}
/// The helper that reads `field` on the prototype that the stdlib links
/// for `kind`, where the prototype's type is a sum, and the rep it gives.
/// It reads on each member of the type
pub(super) fn proto_reader(&mut self, kind: ProtoOf, field: AtomRef) -> (String, Rep) {
let name = format!("get_{}_{}", proto_global(kind), self.field_name(field));
let proto = &self.cache.protos[kind as usize];
let ret = self.rep(&read_value(proto, field, &self.cache.protos));
if !self.helpers.insert(name.clone()) {
return (format!("{name}()"), ret);
}
let mut body = String::new();
self.dispatch(&proto_global(kind), proto, "jsrt_type_error();", &mut body, |e, member, proto, _| {
let Type::Obj(cell) = member else {
return None;
};
let (read, rep) = e.read_on(cell, field, proto);
Some(format!("return {};", e.conv(&read, rep, ret)))
});
self.helper(format!("static {} {name}(void)", self.c_type(ret)), body);
(format!("{name}()"), ret)
}
/// The helper that reads `field` on an object of `cell`, and the rep it
/// gives. It reads the own field, and where that can be absent, goes on to
/// the prototype. At the end of the chain, the field is undefined
pub(super) fn field_reader(&mut self, cell: &Cell, field: AtomRef) -> (String, Rep) {
let name = format!("get_{}_{}", struct_name(self.num(cell)), self.field_name(field));
let read = cell.read(field, &self.cache.protos);
let ret = self.rep(&read);
if !self.helpers.insert(name.clone()) {
return (name, ret);
}
let own = cell.field(field);
let absent = cell.can_be_absent(field);
let mut body = String::new();
if let Some(own) = &own {
let slot = self.slot_rep(&own.ty, absent);
let member = format!("o->{}", self.field_name(field));
let value = self.conv(&member, slot, ret);
match absent {
false => {
let _ = writeln!(body, " return {value};");
}
true => {
let _ = writeln!(body, " if ({}) return {value};", self.present(&member, slot));
}
self.split(instr.args[2], |obj, member| {
match self.program.class_kind(member) {
ClassKind::Array => format!("ARRAY_PUT({obj}, {index}, {value});\n"),
_ => throw("cannot set an element of a value that is not an array"),
}
}
})
}
// An array's prototype is the one that the stdlib links for arrays
if absent && cell.is_array() {
let (read, rep) = self.read_proto(ProtoOf::Array, field);
let _ = writeln!(body, " return {};", self.conv(&read, rep, ret));
} else if absent {
let proto_ty = cell.field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty);
let proto_absent = cell.can_be_absent(PROTO_FIELD);
let slot = self.slot_rep(&proto_ty, proto_absent);
let member = format!("o->{}", self.field_name(PROTO_FIELD));
let mut chain = String::new();
// On a prototype that is not an object, the chain ends
self.dispatch(&member, &proto_ty, "{}", &mut chain, |e, member, proto, _| {
let Type::Obj(proto_cell) = member else {
return None;
};
let (reader, rep) = e.field_reader(proto_cell, field);
Some(format!("return {};", e.conv(&format!("{reader}({proto})"), rep, ret)))
});
match (&proto_ty, proto_absent) {
(Type::Unknown, _) => {}
(_, true) => {
let _ = writeln!(body, " if ({}) {{\n{chain} }}", self.present(&member, slot));
}
(_, false) => body += &chain,
}
// The end of the chain. If the read cannot give undefined, the chain
// always finds the field before it
match read.members().contains(&Type::Undefined) {
true => {
let undefined = self.conv("(undefined_t){0}", Rep::Undefined, ret);
let _ = writeln!(body, " return {undefined};");
}
false => body += " abort();\n",
}
}
// A field that is defined, but that no value reaches: the read cannot
// run
if own.is_none() && !absent {
let _ = writeln!(body, " abort();");
}
let sig = format!("static {} {name}({} *o)", self.c_type(ret), struct_name(self.num(cell)));
self.helper(sig, body);
(name, ret)
/// The C of `code` for each member that a local, arg or reg can be (see `Program::split`)
pub fn split(&self, slot: Operand, code: impl Fn(&str, Member) -> String) -> String {
self.program.split(&self.get(slot), &self.ty(slot), code)
}
}
/// The lookup of `field` on `o`, an object of `class`: its own field if it has it, else the
/// field on its prototype. An array's prototype is the stdlib's. Another object's is its
/// `__proto__` field, which it can lack. At the end of the chain, the field is undefined
pub fn lookup_body(program: &Program, class: usize, field: AtomRef) -> String {
let ctype = program.lookup_ctype(class, field).c_name();
let ret = |value: &str| format!("return CAST({ctype}, {value});\n");
let unless_lacks = |field: AtomRef| format!("if (o->has_{}) ", program.field_name(field));
let if_lacks = |field: AtomRef| format!("if (!o->has_{}) ", program.field_name(field));
let mut c = String::new();
if let Some(own) = program.classes.field(class, field) {
let value = program.field_access("o", own);
match own.defined {
true => c += &ret(&value),
false => c += &(unless_lacks(field) + &ret(&value)),
}
}
let proto = program.classes.field(class, PROTO_FIELD);
c += &match (program.classes.classes[class].kind, proto) {
(ClassKind::Array, _) => match program.builtin_proto(Member::Obj(class)) {
Some((proto, proto_class)) => ret(&program.field_value(proto_class, field, &proto)),
None => ret(UNDEFINED),
},
(_, None) => ret(UNDEFINED),
(_, Some(proto)) => {
let lacks = match proto.defined {
true => String::new(),
false => if_lacks(PROTO_FIELD) + &ret(UNDEFINED),
};
let value = program.field_access("o", proto);
let on_proto = program.split(&value, &proto.ty, |proto, member| match member {
Member::Obj(proto_class) => ret(&program.field_value(proto_class, field, proto)),
_ => ret(UNDEFINED),
});
lacks + &on_proto
}
};
indent(&c)
}
+378
View File
@@ -0,0 +1,378 @@
//! The program, as C: its types, its statics and its functions
//!
//! `Program::new` finds all of them before any C is written. Writing a function only reads
//! the program, so the functions can be written in any order.
use std::collections::BTreeSet;
use super::classes::{CType, ClassField, ClassKind, Classes, Member};
use super::functions::Function;
use super::js::indent;
use crate::{
flatten::ENTRY_FN,
infer::{Addr, LENGTH_FIELD, Type, TypeCache, join},
libqjs::parse::{Atom, AtomRef, JsValue, opcodes::OpCode},
link::{Link, PRELUDE, Target},
regify::{Operand, RegFn, RegInstr},
transform::{PROTO_FIELD, PROTOTYPE_FIELD},
};
pub struct Program<'a> {
// The input: the JS functions, and what infer found
pub fns: &'a [RegFn],
pub cache: &'a TypeCache,
pub atoms: &'a [Atom],
pub link: &'a Link,
pub entry: usize, // the walk of the entry: the JS function that runs the program
// The output
pub classes: Classes, // types: a struct for each class of objects
pub strings: Vec<String>, // statics: the text of each string literal
pub globals: Vec<CType>, // statics: the C type of each binding at the top of a module (see link.rs)
pub functions: Vec<Function>,
}
impl<'a> Program<'a> {
pub fn new(fns: &'a [RegFn], cache: &'a TypeCache, atoms: &'a [Atom], link: &'a Link) -> Self {
let mut program = Program {
fns,
cache,
atoms,
link,
entry: cache.find(ENTRY_FN, None, &[], fns),
classes: Classes::new(&cache.memory),
strings: vec![],
globals: vec![],
functions: vec![],
};
program.add_globals();
program.add_functions();
program.add_strings();
program
}
/// One C function for each walk in infer's last round, one for each lookup, and the
/// host's two entry points
fn add_functions(&mut self) {
let walks = (0..self.cache.walks.len()).filter(|&walk| self.cache.is_live(walk));
let mut lookups = BTreeSet::new();
for walk in walks.clone() {
self.add_lookups(walk, &mut lookups);
}
let lookups = lookups
.into_iter()
.map(|(class, field)| Function::Lookup(class, field));
self.functions.extend(walks.map(Function::Js));
self.functions.extend(lookups);
self.functions.extend([Function::Start, Function::Complete]);
}
/// The lookups that a walk needs: one for each field that it gets, on each class that can
/// lack the field
fn add_lookups(&self, walk: usize, lookups: &mut BTreeSet<(usize, AtomRef)>) {
for instr in self.instrs(walk) {
let field = match instr.op {
OpCode::GetField => instr.imm.atom(),
OpCode::GetLength => LENGTH_FIELD,
_ => continue,
};
let ty = self.slot_type(walk, instr.args[0]);
for member in self.classes.members(&ty) {
if let Some(class) = self.field_class(member, field) {
self.add_lookup(class, field, lookups);
}
}
}
}
/// The lookup of `field` on `class`, if its objects can lack the field. If they can, the
/// lookup goes on to their prototypes, so those need lookups too
fn add_lookup(&self, class: usize, field: AtomRef, lookups: &mut BTreeSet<(usize, AtomRef)>) {
let defined = self.classes.field(class, field).is_some_and(|f| f.defined);
if defined || !lookups.insert((class, field)) {
return;
}
for proto_class in self.proto_classes(class) {
self.add_lookup(proto_class, field, lookups);
}
}
/// The text of every string literal in the walks
fn add_strings(&mut self) {
let walks = self.functions.iter().filter_map(|function| match function {
Function::Js(walk) => Some(*walk),
_ => None,
});
let mut strings = vec![];
for walk in walks {
for instr in self.instrs(walk) {
let text = match instr.op {
OpCode::PushAtomValue => instr.imm.atom().text(self.atoms),
OpCode::PushEmptyString => String::new(),
OpCode::PushConst | OpCode::PushConst8 => {
match &self.js_fn(walk).cpool[instr.imm.const_idx()] {
JsValue::String(s) => s.text(),
_ => continue,
}
}
_ => continue,
};
if !strings.contains(&text) {
strings.push(text);
}
}
}
self.strings = strings;
}
/// Each module binding is a C global, so that any function can use the stdlib's
fn add_globals(&mut self) {
let types = (0..self.link.globals).map(|g| self.cache.memory.get(Addr::Global(g)));
self.globals = types.map(|ty| self.classes.ctype(ty)).collect();
}
/// The instrs of a JS function that can run in a walk
fn instrs(&self, walk: usize) -> impl Iterator<Item = &'a RegInstr> + use<'a> {
let (cache, fns) = (self.cache, self.fns);
let walk = &cache.walks[walk];
let f = &fns[walk.context.fn_idx];
let blocks = f.blocks.iter().zip(&walk.reachable);
let blocks = blocks.filter(|(_, reachable)| **reachable);
blocks.flat_map(|(block, _)| &f.code[block.start..block.end])
}
/// The JS function of a walk
pub fn js_fn(&self, walk: usize) -> &'a RegFn {
&self.fns[self.cache.walks[walk].context.fn_idx]
}
}
// ─────────────────────────────────────── Values ────────────────────────────────────────
impl Program<'_> {
/// The type of a local, arg or reg of a JS function, in one walk
pub fn slot_type(&self, walk: usize, slot: Operand) -> Type {
let addr = self.cache.addr(walk, slot, self.fns);
self.cache.memory.get(addr).clone()
}
/// The C type of a local, arg or reg of a JS function, in one walk
pub fn slot_ctype(&self, walk: usize, slot: Operand) -> CType {
self.classes.ctype(&self.slot_type(walk, slot))
}
/// The C type of what a walk returns
pub fn ret_ctype(&self, walk: usize) -> CType {
self.classes.ctype(self.cache.ret(walk))
}
pub fn class_kind(&self, member: Member) -> ClassKind {
match member {
Member::Obj(class) => self.classes.classes[class].kind,
_ => ClassKind::Obj,
}
}
/// A string literal, as C: a pointer to its static
pub fn string(&self, text: &str) -> String {
match self.strings.iter().position(|s| s == text) {
Some(id) => format!("(&str{id})"),
None => panic!("emit: no static for the string {text:?}"),
}
}
/// The value of the export `name` of the stdlib's module `module`, as C, and its C type
pub fn binding(&self, module: &str, name: &str) -> Option<(String, CType)> {
match self.link.export(module, name)? {
Target::Global(g) => Some((format!("g{g}"), self.globals[g])),
Target::Native(_) => None,
}
}
}
// ─────────────────────────────────────── Members ───────────────────────────────────────
impl Program<'_> {
/// The C of `code` for each member that `value`, of type `ty`, can be. `code` gets the
/// value as C of that member's C type. A value with one member is that member, so this
/// is its code. Else the value is a `jsrt_value`, and a `switch` on its tag has a case
/// for each member. A number in a `jsrt_value` is a double, so an int is a float there
pub fn split(&self, value: &str, ty: &Type, code: impl Fn(&str, Member) -> String) -> String {
let members = self.classes.members(ty);
if let [member] = members[..] {
return code(value, member);
}
let float = members.contains(&Member::Float);
let natives = members.iter().filter(|m| matches!(m, Member::Native(_)));
if natives.count() > 1 {
unimplemented!("a value that can be one of several natives");
}
let mut c = format!("switch ({value}.tag) {{\n");
for member in members {
let member = match member {
Member::Int if float => continue,
Member::Int => Member::Float,
member => member,
};
// An object comes out as a `void *`, so it is cast to its struct
let ctype = CType::One(member).c_name();
let as_member = match member {
Member::Obj(_) => format!("(({ctype})CAST({ctype}, {value}))"),
_ => format!("CAST({ctype}, {value})"),
};
let body = indent(&code(&as_member, member));
c += &format!("case {}: {{\n{body} break;\n}}\n", tag(member));
}
c + "default: abort();\n}\n"
}
}
/// The tag of a member in a `jsrt_value` (see lib/rt/core.h)
fn tag(member: Member) -> String {
match member {
Member::Undefined => "JSRT_UNDEFINED".into(),
Member::Null => "JSRT_NULL".into(),
Member::Bool => "JSRT_BOOL".into(),
Member::Int | Member::Float => "JSRT_NUMBER".into(),
Member::Str => "JSRT_STRING".into(),
Member::Native(_) => "JSRT_NATIVE".into(),
Member::Obj(class) => format!("JSRT_OBJECT + {class}"),
}
}
/// A throw of a TypeError that says `what` (see `__rt_type_error`)
pub fn throw(what: &str) -> String {
format!("__rt_type_error(\"{what}\");\n")
}
// ─────────────────────────────────────── Fields ────────────────────────────────────────
impl Program<'_> {
/// The C name of a field
pub fn field_name(&self, field: AtomRef) -> String {
let text: String = (field.text(self.atoms).chars().take(24))
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
match field {
AtomRef::TaggedInt(n) => format!("f_{text}_i{n}"),
AtomRef::Predefined(n) => format!("f_{text}_p{n}"),
AtomRef::FileLocal(n) => format!("f_{text}_a{n}"),
}
}
/// A field of `obj`, as C to get or set it. A field that shares a location holds a
/// pointer to it
pub fn field_access(&self, obj: &str, field: &ClassField) -> String {
let name = self.field_name(field.name);
match field.shared {
true => format!("(*{obj}->{name})"),
false => format!("{obj}->{name}"),
}
}
/// The value of `field` on `obj`, an object of `class`, as C. If every object of the
/// class has the field, that is the field itself. Else it is a call of the field's lookup
pub fn field_value(&self, class: usize, field: AtomRef, obj: &str) -> String {
match self.classes.field(class, field) {
Some(f) if f.defined => self.field_access(obj, f),
_ => format!("{}({obj})", Function::Lookup(class, field).name(self)),
}
}
/// The class of the object that has `field`, for a value of `member`. That is the value
/// itself if it is an object, or the stdlib's prototype of its kind if it is a primitive
/// (see `builtin_proto`). A string's length and an array's are not fields, so they have
/// none
pub fn field_class(&self, member: Member, field: AtomRef) -> Option<usize> {
let length = field == LENGTH_FIELD;
match (member, self.class_kind(member)) {
(Member::Str, _) | (Member::Obj(_), ClassKind::Array) if length => None,
(Member::Obj(class), _) => Some(class),
(Member::Undefined | Member::Null | Member::Native(_), _) => None,
_ => self.builtin_proto(member).map(|(_, class)| class),
}
}
/// The C type that the lookup of `field` on `class` gives: of the join of the field's
/// types on the class's objects, and on their prototypes (see `Memory::read_field`)
pub fn lookup_ctype(&self, class: usize, field: AtomRef) -> CType {
let objs = &self.classes.classes[class].objs;
let memory = &self.cache.memory;
let types = objs
.iter()
.map(|&obj| memory.read_field(&Type::Ref(obj), field));
self.classes
.ctype(&types.fold(Type::Unknown, |all, ty| join(&all, &ty)))
}
/// The prototype of a primitive or an array, as C, and its class: the `prototype` field of
/// the stdlib's String, Number, Boolean or Array (see `Memory::builtin_proto`)
pub fn builtin_proto(&self, member: Member) -> Option<(String, usize)> {
let name = match (member, self.class_kind(member)) {
(Member::Str, _) => "String",
(Member::Int | Member::Float, _) => "Number",
(Member::Bool, _) => "Boolean",
(_, ClassKind::Array) => "Array",
_ => return None,
};
let (stdlib_class, CType::One(Member::Obj(class))) = self.binding(PRELUDE, name)? else {
return None;
};
let proto = self.classes.field(class, PROTOTYPE_FIELD)?;
let CType::One(Member::Obj(proto_class)) = proto.ctype else {
return None;
};
Some((self.field_access(&stdlib_class, proto), proto_class))
}
/// The classes that the prototype of an object of `class` can be. An array's prototype
/// is the stdlib's. Another object's is its `__proto__` field, if it has one
pub fn proto_classes(&self, class: usize) -> Vec<usize> {
if self.classes.classes[class].kind == ClassKind::Array {
let proto = self.builtin_proto(Member::Obj(class));
return proto.map(|(_, class)| class).into_iter().collect();
}
let Some(proto) = self.classes.field(class, PROTO_FIELD) else {
return vec![];
};
let members = self.classes.members(&proto.ty).into_iter();
let classes = members.map(|member| match member {
Member::Obj(class) => Some(class),
_ => None,
});
classes.flatten().collect()
}
}
/// Every local, arg and reg of a JS function
pub fn slots(f: &RegFn) -> Vec<Operand> {
let locals = (0..f.local_count).map(Operand::Local);
let args = (0..f.arg_count).map(Operand::Arg);
let regs = (0..f.reg_count).map(Operand::Reg);
locals.chain(args).chain(regs).collect()
}
-234
View File
@@ -1,234 +0,0 @@
//! How each type looks in C: its representation, the C type of that,
//! the struct of each class of cells, and the tagged union of each sum
use super::*;
/// A C representation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Rep {
Undefined,
Null,
Bool,
Int,
Double,
Str, // a string, or in a field, the sentinel for absent
Obj(CellNum), // a pointer to an object of the cell, or in a field, the sentinel for absent
OptStr, // a sum of a string and null or undefined: `NULL` is null, and `JSRT_UNDEFINED` is undefined
OptObj(CellNum), // a sum of the objects of one cell and null or undefined: a pointer, with the same sentinels
Objs, // a sum of the objects of several cells, maybe with null or undefined: a pointer to the header
Sum(Kinds), // any other sum: a tagged union of its kinds (see `sum_defs()`)
Any, // any value: an `any_t`. The `Any` type, and the args of the runtime's ops on sums
}
/// A set of kinds of value, one bit for each `jsrt_kind` (see `lib/rt/any.h`)
pub(super) type Kinds = u8;
pub(super) const ABSENT: Kinds = 1 << 0;
/// The `jsrt_kind` of each kind, and its member in a tagged union, if it has a
/// value besides its kind
pub(super) const KINDS: [(&str, Option<(&str, &str)>); 8] = [
("ABSENT", None),
("UNDEFINED", None),
("NULL", None),
("BOOL", Some(("bool", "b"))),
("INT", Some(("int32_t", "i"))),
("FLOAT", Some(("double", "f"))),
("STRING", Some(("string_t", "s"))),
("OBJECT", Some(("void *", "o"))),
];
/// The kinds of the values of type `ty`
pub(super) fn kinds(ty: &Type) -> Kinds {
ty.members().iter().fold(0, |all, member| {
all | 1 << match member {
Type::Undefined => 1,
Type::Null => 2,
Type::Bool(_) => 3,
Type::Int => 4,
Type::Float => 5,
Type::Str(_) => 6,
_ => 7,
}
})
}
/// The C of the tagged union of each set of kinds in `sums`, and its
/// conversions to and from `any_t`. Every member of a union is at its start,
/// so a conversion copies the payload's bytes. A union has one kind of number
/// (see `join()`): the other converts. A kind that the union lacks is a
/// `TypeError`
pub(super) fn sum_defs(sums: &BTreeSet<Kinds>) -> String {
let mut out = String::new();
for &k in sums {
let has = |i: usize| k & 1 << i != 0;
let union: String = (0..8).filter(|&i| has(i)).filter_map(|i| KINDS[i].1).map(|(t, m)| format!(" {t} {m};")).collect();
let copy = |to: &str, from: &str| match union.is_empty() {
true => String::new(),
false => format!(" __builtin_memcpy(&{to}.as, &{from}.as, sizeof {to}.as);"),
};
let number = match (has(4), has(5)) {
(false, true) => " if (a.kind == JSRT_KIND_INT) { v.kind = JSRT_KIND_FLOAT; v.as.f = a.as.i; return v; }",
(true, false) => " if (a.kind == JSRT_KIND_FLOAT) { v.kind = JSRT_KIND_INT; v.as.i = (int32_t)a.as.f; return v; }",
_ => "",
};
let union = if union.is_empty() { String::new() } else { format!(" union {{{union} }} as;") };
let _ = writeln!(out, "typedef struct {{ jsrt_kind kind;{union} }} sum_{k};");
let _ = writeln!(out, "static inline any_t sum_{k}_any(sum_{k} v) {{ any_t a = {{ .kind = v.kind }};{} return a; }}", copy("a", "v"));
let _ = writeln!(
out,
"static inline sum_{k} any_sum_{k}(any_t a) {{ sum_{k} v = {{ .kind = a.kind }};{number} if (!({k}u >> a.kind & 1)) jsrt_type_error();{} return v; }}",
copy("v", "a")
);
}
out
}
impl<'a> Emitter<'a> {
/// The representation of a value of type `ty`
pub(super) fn rep(&self, ty: &Type) -> Rep {
match ty {
Type::Unknown | Type::Undefined => Rep::Undefined,
Type::Null => Rep::Null,
Type::Bool(_) => Rep::Bool,
Type::Int => Rep::Int,
Type::Float => Rep::Double,
Type::Str(_) => Rep::Str,
Type::Obj(cell) => Rep::Obj(self.num(cell)),
Type::Sum(_) => self.sum_rep(ty),
Type::Any => Rep::Any,
}
}
/// The representation of a sum. A string or the objects of one cell, with
/// null or undefined, is that pointer with sentinels. The objects of
/// several cells are a pointer to their header. Any other sum is an
/// `any_t`, which holds every kind
pub(super) fn sum_rep(&self, ty: &Type) -> Rep {
let others: Vec<&Type> = ty.members().iter().filter(|m| !m.is_nullish()).collect();
let mut classes: Vec<CellNum> = others.iter().filter_map(|m| self.cell_num(m)).collect();
classes.dedup();
match (&others[..], &classes[..]) {
([], _) => Rep::Sum(kinds(ty)),
([Type::Str(_)], _) => Rep::OptStr,
(_, [id]) if classes.len() == 1 && others.iter().all(|m| matches!(m, Type::Obj(_))) => Rep::OptObj(*id),
_ if others.iter().all(|m| matches!(m, Type::Obj(_))) => Rep::Objs,
_ => Rep::Sum(kinds(ty)),
}
}
/// The class of an `Obj` type
pub(super) fn cell_num(&self, ty: &Type) -> Option<CellNum> {
match ty {
Type::Obj(cell) => Some(self.num(cell)),
_ => None,
}
}
/// The representation of a field of type `ty`. A field that can be absent
/// needs a sentinel for absent: a pointer has one, and any other rep is a
/// tagged union that can also be absent
pub(super) fn slot_rep(&self, ty: &Type, absent: bool) -> Rep {
match self.rep(ty) {
rep @ (Rep::Str | Rep::Obj(_) | Rep::OptStr | Rep::OptObj(_) | Rep::Objs) => rep,
_ if absent => Rep::Sum(kinds(ty) | ABSENT),
rep => rep,
}
}
/// The representation of an element of an array of cell `id`: its type,
/// or undefined for a hole
pub(super) fn elem_rep(&self, id: CellNum) -> Rep {
self.rep(&self.cells[id as usize].read_elem())
}
pub(super) fn c_type(&self, rep: Rep) -> String {
match rep {
Rep::Undefined => "undefined_t".into(),
Rep::Null => "null_t".into(),
Rep::Bool => "bool".into(),
Rep::Int => "int32_t".into(),
Rep::Double => "double".into(),
Rep::Str | Rep::OptStr => "string_t".into(),
Rep::Obj(id) | Rep::OptObj(id) => format!("{} *", struct_name(id)),
Rep::Objs => "jsrt_header *".into(),
Rep::Sum(k) => {
self.sums.borrow_mut().insert(k);
format!("sum_{k}")
}
Rep::Any => "any_t".into(),
}
}
pub(super) fn field_name(&self, name: AtomRef) -> String {
let text = atom_name(name, self.atoms);
let text = text.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '_' });
format!("f_{}", text.collect::<String>())
}
/// The struct of cell `id`, and its helpers: `init_S`, which sets the
/// header and makes each field that can be absent absent, and for an
/// array, the helpers of its elements
pub(super) fn struct_def(&mut self, id: CellNum, out: &mut String) {
let cell = self.cells[id as usize].clone();
let name = struct_name(id);
let mut members = String::from("jsrt_header h;");
let mut body = String::from(" o->h.cell = cell;\n");
// An array's elements come after its header. `items` has room for
// `cap` of them. An element holds undefined for a hole
let elem = cell.is_array().then(|| self.elem_rep(id));
if let Some(elem) = elem {
let _ = write!(members, " int32_t length; int32_t cap; {} *items;", self.c_type(elem));
}
for (field, f) in &cell.info().fields {
let absent = cell.can_be_absent(*field);
let rep = self.slot_rep(&f.ty, absent);
let member = self.field_name(*field);
let _ = write!(members, " {} {member};", self.c_type(rep));
if absent {
let _ = writeln!(body, " o->{member} = {};", self.absent(rep));
}
}
let _ = writeln!(out, "struct {name} {{ {members} }};");
self.helper(
format!("static {name} *init_{name}({name} *o, uint32_t cell)"),
format!("{body} return o;\n"),
);
if let Some(elem) = elem {
let (t, hole) = (self.c_type(elem), self.conv("(undefined_t){0}", Rep::Undefined, elem));
self.helper(
format!("static void set_length_{name}({name} *a, int32_t n)"),
format!(
" if (n < 0) jsrt_type_error();\n\
\x20 a->items = jsrt_grow(a->items, &a->cap, n, sizeof *a->items);\n\
\x20 for (int32_t i = a->length; i < n; i++) a->items[i] = {hole};\n\
\x20 a->length = n;\n"
),
);
self.helper(
format!("static {t} elem_{name}({name} *a, int32_t i)"),
format!(" return i >= 0 && i < a->length ? a->items[i] : {hole};\n"),
);
self.helper(
format!("static void set_elem_{name}({name} *a, int32_t i, {t} v)"),
" if (i < 0) jsrt_type_error();\n if (i >= a->length) set_length_{NAME}(a, i + 1);\n a->items[i] = v;\n"
.replace("{NAME}", &name),
);
}
}
pub(super) fn helper(&mut self, sig: String, body: String) {
let _ = writeln!(self.helper_protos, "{sig};");
let _ = writeln!(self.helper_defs, "{sig} {{\n{body}}}\n");
}
}
+80 -53
View File
@@ -1,24 +1,92 @@
//! QuickJS bytecode represents each module as a hierarchy. Each function has a set of child
//! functions that reference captured variables in their parent
//!
//! This module:
//! - flattens each module's hierarchy into one list for the program
//! - assigns an integer idx to each function
//! - converts child ownership to a reference by-idx
//! - extracts closure captures into a separate vec
//!
//! Fn 0 is the program's entry, which link.rs writes: it runs every module's body
use crate::libqjs::parse::{
Atom, AtomRef, ClosureVar, Function, ImmOperand, Instr, JsValue, QuickJSBinary, opcodes::OpCode,
Atom, AtomRef, ClosureVar, ExportInfo, FuncKind, Function, ImportInfo, Instr, JsValue,
QuickJSBinary,
};
/// The entry fn (see link.rs)
pub const ENTRY_FN: usize = 0;
#[derive(Debug)]
pub struct FlatFn {
pub name: AtomRef,
pub kind: FuncKind, // a normal fn, or one that suspends: async, a generator, or both (see transform/async.rs)
pub arg_count: usize,
pub var_count: usize,
pub local_count: usize,
pub cpool: Vec<JsValue>,
pub code: Vec<Instr>,
}
pub fn flatten(bin: QuickJSBinary) -> (Vec<FlatFn>, Vec<Vec<ClosureVar>>, Vec<Atom>) {
let mut fns = vec![];
let mut captures = vec![];
/// A module, as QuickJS gives it, with its body as a flat fn
#[derive(Debug)]
pub struct FlatModule {
pub name: String, // the name that it was loaded by (see `libqjs::compile`)
pub requests: Vec<String>, // the modules that it imports from, as the source writes them
pub exports: Vec<ExportInfo>,
pub star_exports: Vec<usize>, // `export * from`: each an index into `requests`
pub imports: Vec<ImportInfo>,
pub has_top_level_await: bool,
pub body: usize, // the fn of its body. Its captures are the module's bindings
}
walk(bin.root_mod.root_fn, &mut fns, &mut captures);
add_entry_point(&mut fns, &mut captures);
/// Flattens every module of the binary into one list of fns. Gives the fns, each fn's captures
/// from its parent, the atoms, and the modules
pub fn flatten(
bin: QuickJSBinary,
) -> (
Vec<FlatFn>,
Vec<Vec<ClosureVar>>,
Vec<Atom>,
Vec<FlatModule>,
) {
let text = |atom: AtomRef| atom.text(&bin.atoms);
let mut fns = vec![entry_placeholder()];
let mut captures = vec![vec![]];
let mut modules = vec![];
(fns, captures, bin.atoms)
for module in bin.modules {
let has_top_level_await = module.has_top_level_await;
let body = walk(module.root_fn, &mut fns, &mut captures);
// QuickJS makes every module's body async. It only suspends if it awaits at its top
if !has_top_level_await {
fns[body].kind = FuncKind::Normal;
}
modules.push(FlatModule {
name: text(module.name),
requests: module.requests.iter().map(|&r| text(r)).collect(),
exports: module.exports,
star_exports: module.star_exports,
imports: module.imports,
has_top_level_await,
body,
});
}
(fns, captures, bin.atoms, modules)
}
/// Fn 0, before link.rs writes it
fn entry_placeholder() -> FlatFn {
FlatFn {
name: AtomRef::Predefined(0),
kind: FuncKind::Normal,
arg_count: 0,
local_count: 0,
cpool: vec![],
code: vec![],
}
}
fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) -> usize {
@@ -26,19 +94,21 @@ fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<Closu
flat_fns.push(FlatFn {
name: func.name,
kind: func.flags.func_kind,
arg_count: func.arg_count,
var_count: func.var_count,
local_count: func.var_count,
cpool: vec![],
code: func.byte_code,
});
captures.push(func.closure_vars);
// Every direct child ownership is replaced by a by-idx reference
let cpool = func
.cpool
.into_iter()
.map(|c| match c {
JsValue::Function(child) => JsValue::FnById(walk(child, flat_fns, captures)),
JsValue::Function(child) => JsValue::FnByIdx(walk(child, flat_fns, captures)),
other => other,
})
.collect();
@@ -46,46 +116,3 @@ fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<Closu
flat_fns[id].cpool = cpool;
id
}
/// Add our synthetic entry point, equivalent to:
/// ```js
/// function <null>() {
/// // Module top level <eval> created by quickjs
/// // All other top level decls are inside <eval>
/// function <eval>() {
/// ...
/// }
///
/// <eval>.call(true); // Link phase (`this = true`)
/// <eval>.call(false); // Run phase (`this = false`)
/// }
/// ```
///
/// This gets called by C main()
fn add_entry_point(fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) {
let instr = |pc, op, imm| Instr { pc, op, imm };
let code = vec![
instr(0, OpCode::Fclosure8, ImmOperand::Const(0)),
instr(1, OpCode::PutLoc, ImmOperand::Loc(0)),
instr(2, OpCode::PushTrue, ImmOperand::None),
instr(3, OpCode::GetLoc, ImmOperand::Loc(0)),
instr(4, OpCode::CallMethod, ImmOperand::Npop(0)),
instr(5, OpCode::Drop, ImmOperand::None),
instr(6, OpCode::PushFalse, ImmOperand::None),
instr(7, OpCode::GetLoc, ImmOperand::Loc(0)),
instr(8, OpCode::CallMethod, ImmOperand::Npop(0)),
instr(9, OpCode::Drop, ImmOperand::None),
instr(10, OpCode::ReturnUndef, ImmOperand::None),
];
fns.push(FlatFn {
name: AtomRef::Predefined(0),
arg_count: 0,
var_count: 1,
cpool: vec![JsValue::FnById(0)],
code,
});
captures.push(vec![]);
}
-256
View File
@@ -1,256 +0,0 @@
//! Cells: the types of objects, which every holder of an object shares (see
//! `docs/model.md`, section 3)
use std::{
cell::{Ref, RefCell},
fmt,
hash::{Hash, Hasher},
rc::Rc,
};
use crate::{
libqjs::parse::AtomRef,
native::ProtoOf,
transform::{BOX_FIELD, PROTO_FIELD},
};
use super::types::{Type, join};
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()];
/// The code a fn object runs
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Code {
Fn(usize),
Native(usize),
ProtoLink(ProtoOf),
HostLink,
}
/// What the objects of a cell are, besides objects with named fields
#[derive(Debug, Clone, 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 field of the objects of a cell
#[derive(Debug, Clone)]
pub struct Field {
pub ty: Type,
pub defined: bool, // every object of the cell has it from the start (see `write()`)
}
/// What a cell knows about its objects
#[derive(Debug)]
pub struct CellInfo {
pub kind: ObjKind,
pub fields: Vec<(AtomRef, Field)>, // sorted by name
}
/// The type of an object. Every copy of an `Obj` type holds the same cell, so
/// a write through one holder is seen by all. A cell is its `Rc`: two cells
/// are equal only if they are one cell, and they are ordered by address
#[derive(Clone)]
pub struct Cell(Rc<RefCell<CellInfo>>);
impl Cell {
pub fn new(kind: ObjKind) -> Self {
Cell(Rc::new(RefCell::new(CellInfo { kind, fields: vec![] })))
}
pub fn info(&self) -> Ref<'_, CellInfo> {
self.0.borrow()
}
/// The address of the cell, which is its identity
fn addr(&self) -> usize {
Rc::as_ptr(&self.0) as usize
}
/// The code that its objects run, if they are fns
pub fn code(&self) -> Option<Code> {
match self.info().kind {
ObjKind::Fn(code) => Some(code),
_ => None,
}
}
pub fn is_array(&self) -> bool {
matches!(self.info().kind, ObjKind::Array { .. })
}
/// The type of its elements, for an array
pub fn elems(&self) -> Type {
match &self.info().kind {
ObjKind::Array { elems } => elems.clone(),
_ => unreachable!(),
}
}
pub fn field(&self, name: AtomRef) -> Option<Field> {
let info = self.info();
let i = info.fields.binary_search_by_key(&name, |(n, _)| *n).ok()?;
Some(info.fields[i].1.clone())
}
/// Whether one of its objects can lack its own `field`. A box's value
/// never can: a read of a binding before its first write throws (its
/// TDZ), so a read that gives a value comes after a write (see transform)
pub fn can_be_absent(&self, field: AtomRef) -> bool {
field != BOX_FIELD && !self.field(field).is_some_and(|f| f.defined)
}
/// Join `ty` into its `field`. A field is defined if a `define_field`
/// makes it: that is a write into an object before it goes anywhere (see
/// regify). A field that another write makes can be absent. An array's
/// `length` is not a field: a write of it sets the array's length.
/// Whether the cell changed
pub fn write(&self, field: AtomRef, ty: &Type, defined: bool) -> bool {
if field == LENGTH_FIELD && self.is_array() {
return false;
}
// The join is done before the borrow: `ty` can hold this cell
let old = self.field(field);
let new = join(old.as_ref().map_or(&Type::Unknown, |f| &f.ty), ty);
let fields = &mut self.0.borrow_mut().fields;
match fields.binary_search_by_key(&field, |(n, _)| *n) {
Ok(i) if fields[i].1.ty == new => false,
Ok(i) => {
fields[i].1.ty = new;
true
}
Err(i) => {
fields.insert(i, (field, Field { ty: new, defined }));
true
}
}
}
/// Join `ty` into the type of its elements. Whether the cell changed
pub fn write_elem(&self, ty: &Type) -> bool {
let new = join(&self.elems(), ty);
match &mut self.0.borrow_mut().kind {
ObjKind::Array { elems } if *elems != new => {
*elems = new;
true
}
_ => false,
}
}
/// A read of an element of one of its arrays. An index can be a hole, or
/// past the end, which gives undefined
pub fn read_elem(&self) -> Type {
join(&self.elems(), &Type::Undefined)
}
/// A read of `field` on one of its objects: 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(&self, field: AtomRef, protos: &Protos) -> Type {
self.read_chain(field, protos, &mut vec![])
}
/// `read()`, on a chain that came through the cells `chain`. Objects of
/// one cell can be each other's prototypes, so a chain of cells can come
/// back to a cell, which a chain of objects cannot. The read on that cell
/// is already in the chain, so it gives nothing more
fn read_chain(&self, field: AtomRef, protos: &Protos, chain: &mut Vec<Cell>) -> Type {
let array = self.is_array();
if array && field == LENGTH_FIELD {
return Type::Int;
}
let own = self.field(field).map_or(Type::Unknown, |f| f.ty);
if !self.can_be_absent(field) || chain.contains(self) {
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].clone(), false),
false => (self.field(PROTO_FIELD).map_or(Type::Unknown, |f| f.ty), self.can_be_absent(PROTO_FIELD)),
};
let mut ty = own;
if proto_absent || proto == Type::Unknown {
ty = join(&ty, &Type::Undefined);
}
chain.push(self.clone());
for member in proto.members() {
let read = match member {
Type::Obj(proto) => proto.read_chain(field, protos, chain),
_ => Type::Undefined,
};
ty = join(&ty, &read);
}
chain.pop();
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
pub fn read_value(ty: &Type, field: AtomRef, protos: &Protos) -> Type {
match ty {
Type::Obj(cell) => cell.read(field, protos),
Type::Sum(members) => {
let reads = members.iter().map(|member| read_value(member, field, protos));
reads.fold(Type::Unknown, |all, read| join(&all, &read))
}
ty => match ProtoOf::of(ty).map(|kind| &protos[kind as usize]) {
Some(Type::Unknown) | None => Type::Unknown,
Some(proto) => read_value(proto, field, protos),
},
}
}
impl PartialEq for Cell {
fn eq(&self, other: &Self) -> bool {
self.addr() == other.addr()
}
}
impl Eq for Cell {}
impl PartialOrd for Cell {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Cell {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.addr().cmp(&other.addr())
}
}
impl Hash for Cell {
fn hash<H: Hasher>(&self, state: &mut H) {
self.addr().hash(state);
}
}
/// A cell prints as its address only: its fields can hold the cell itself
impl fmt::Debug for Cell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cell {:#x}", self.addr())
}
}
+362
View File
@@ -0,0 +1,362 @@
//! Memory: what each location in the program holds, by its address, and the reads and
//! writes of objects' fields and elements
use std::collections::HashMap;
use super::types::{Field, Fields, LENGTH_FIELD, Shape, Type, join, join_all};
use crate::{
libqjs::parse::AtomRef,
regify::Operand,
transform::{PROTO_FIELD, PROTOTYPE_FIELD},
};
/// A point in the program: one instr of one fn
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct InstrRef {
pub fn_idx: usize,
pub instr_idx: usize,
}
/// The call instr that a function is called from. `None` for a call from the host
pub type CallSite = Option<InstrRef>;
/// A function, in one call site. Without polymorphism, infer would walk each context once.
/// Objects, and the bindings that closures share, are named by their context, so that every
/// walk of a context sees the same ones (see `TypeCache`)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Context {
pub fn_idx: usize,
pub call_site: CallSite,
}
/// The objects that one instr makes, in one context
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ObjAddr {
pub context: Context,
pub instr: usize,
}
/// A location in the program, which memory holds a type for
///
/// Every location is named by its context and a place in its function, except the ones that
/// only one walk can see, which are named by the walk, and the bindings at the top of a
/// module, of which there is one each for the whole program. A type can hold an object's address,
/// and a walk's arg types can hold that type, so objects are never named by a walk: that
/// could make new walks without end. Every part of an address comes from the program text or
/// from a walk, so there are finitely many, and infer ends
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Addr {
Object(ObjAddr), // an object
Field(ObjAddr, AtomRef), // a field of an object, unless the field shares another location
Shared { context: Context, slot: Operand }, // a binding that closures share (see transform/closure.rs). The closures are objects, so it is named like them
Private { walk: usize, slot: Operand }, // a binding that only its walk sees
Return(usize), // what a walk returns
Global(usize), // a binding at the top of a module: one for the whole program (see link.rs)
}
/// What each location holds, by its address. A location that nothing has reached holds
/// `Unknown`, and has no entry. Types only grow, so the count of changes tells whether
/// anything changed
#[derive(Debug, Default)]
pub struct Memory {
types: HashMap<Addr, Type>,
merged: HashMap<Addr, Addr>, // a location that is one with another (see `share_field`): the other
changes: usize,
pub builtins: Builtins,
}
/// The stdlib's classes, whose prototypes the primitives and arrays read: the address of each
/// one's binding, which the prelude exports (see link.rs)
#[derive(Debug, Default)]
pub struct Builtins {
pub string: Option<Addr>,
pub number: Option<Addr>,
pub boolean: Option<Addr>,
pub array: Option<Addr>,
}
const UNKNOWN: &Type = &Type::Unknown;
impl Memory {
pub fn get(&self, addr: Addr) -> &Type {
self.types.get(&self.find(addr)).unwrap_or(UNKNOWN)
}
/// The address that stands for `addr`: itself, unless it was merged into another
fn find(&self, mut addr: Addr) -> Addr {
while let Some(&other) = self.merged.get(&addr) {
addr = other;
}
addr
}
/// Make `b` one location with `a`: each holds what either held
fn merge(&mut self, a: Addr, b: Addr) {
let (a, b) = (self.find(a), self.find(b));
if a != b {
let ty = self.types.remove(&b).unwrap_or(Type::Unknown);
self.merged.insert(b, a);
self.join(a, &ty);
self.changes += 1;
}
}
/// Every object, with its entry
pub fn objects(&self) -> impl Iterator<Item = (ObjAddr, &Type)> {
self.types.iter().filter_map(|(addr, ty)| match addr {
Addr::Object(obj) => Some((*obj, ty)),
_ => None,
})
}
/// How many times memory changed
pub fn changes(&self) -> usize {
self.changes
}
/// Join `ty` into the location
pub fn join(&mut self, addr: Addr, ty: &Type) {
let addr = self.find(addr);
let old = self.types.entry(addr).or_insert(Type::Unknown);
let new = join(old, ty);
if new != *old {
*old = new;
self.changes += 1;
}
}
// Objects
/// Make the entry of an object, the first time
pub fn make(&mut self, obj: ObjAddr, entry: Type) {
if *self.get(Addr::Object(obj)) == Type::Unknown {
self.types.insert(Addr::Object(obj), entry);
self.changes += 1;
}
}
/// The fn that the object runs, if it is a closure
pub fn closure_fn(&self, obj: ObjAddr) -> Option<usize> {
match self.get(Addr::Object(obj)) {
Type::Closure { fn_idx, .. } => Some(*fn_idx),
_ => None,
}
}
pub fn is_array(&self, obj: ObjAddr) -> bool {
matches!(self.get(Addr::Object(obj)), Type::Shape(Shape::Array(_)))
}
/// Whether a value of type `ty` is a fn
pub fn is_function(&self, ty: &Type) -> Option<bool> {
match ty {
Type::Ref(obj) => Some(self.closure_fn(*obj).is_some()),
Type::Native(_) => Some(true),
_ => Some(false),
}
}
/// What `typeof` gives for a value of type `ty`
pub fn typeof_name(&self, ty: &Type) -> Option<AtomRef> {
Some(AtomRef::Predefined(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::Ref(obj) if self.closure_fn(*obj).is_some() => 27, // "function"
Type::Native(_) => 27, // "function"
Type::Ref(_) => 77, // "object"
_ => return None,
}))
}
// Fields
pub fn fields(&self, obj: ObjAddr) -> &Fields {
match self.get(Addr::Object(obj)) {
Type::Shape(Shape::Obj(fields)) | Type::Closure { fields, .. } => fields,
ty => unimplemented!("named fields on {ty:?}"),
}
}
pub fn field(&self, obj: ObjAddr, name: AtomRef) -> Option<Field> {
let fields = self.fields(obj);
let i = fields.binary_search_by_key(&name, |(n, _)| *n).ok()?;
Some(fields[i].1)
}
/// Give the object `field`, unless it has one. Gives its field
fn add_field(&mut self, obj: ObjAddr, name: AtomRef, field: Field) -> Field {
let Some(Type::Shape(Shape::Obj(fields)) | Type::Closure { fields, .. }) =
self.types.get_mut(&Addr::Object(obj))
else {
unreachable!()
};
match fields.binary_search_by_key(&name, |(n, _)| *n) {
Ok(i) => fields[i].1,
Err(i) => {
fields.insert(i, (name, field));
self.changes += 1;
field
}
}
}
/// Join `ty` into its field `name`. A field is defined if a `define_field` makes it: that
/// is a write into an object before it goes anywhere (see regify). A field that another
/// write makes can be absent. An array's `length` is not a field: a write of it sets the
/// array's length, and a longer array has holes at its end, which hold undefined
pub fn write_field(&mut self, obj: ObjAddr, name: AtomRef, ty: &Type, defined: bool) {
if name == LENGTH_FIELD && self.is_array(obj) {
self.write_elem(obj, &Type::Undefined);
return;
}
let field = self.add_field(
obj,
name,
Field {
addr: Addr::Field(obj, name),
defined,
},
);
self.join(field.addr, ty);
}
/// Make its field `name` the location `addr` (see `define_field_ref`). Such a field is
/// always made when the object is made, so it is defined
///
/// An address stands for all the objects that one instr makes in one context. Two walks
/// of that context can share two locations into the field: walks with different
/// closures as their env, for example. A field is one location, so those
/// two are one location too: they are merged, as the types of all its objects' fields are
/// joined. Their C types are then one type as well
pub fn share_field(&mut self, obj: ObjAddr, name: AtomRef, addr: Addr) {
let field = Field {
addr,
defined: true,
};
let field = self.add_field(obj, name, field);
self.merge(field.addr, addr);
}
/// A read of field `name` on a value of type `ty`:
/// - an object reads its own field, then, where that can be absent, the field on its
/// prototype. At the end of the chain, an absent field is undefined
/// - a primitive reads its kind's prototype
/// - undefined and null have no fields: the read throws, so it gives nothing
/// - a sum reads on each of its members
pub fn read_field(&self, ty: &Type, name: AtomRef) -> Type {
join_all(ty.members().iter().map(|member| match member {
Type::Ref(obj) => self.read_chain(*obj, name, &mut vec![]),
Type::Undefined | Type::Null => Type::Unknown,
primitive => self.read_field(&self.builtin_proto(primitive), name),
}))
}
/// The read of field `name` on an object, on a chain that came through the objects
/// `chain`. Objects of one address can be each other's prototypes, so a chain of
/// addresses can come back to an address, which a chain of objects cannot. The read on
/// that address is already in the chain, so it gives nothing more
///
/// An array's prototype is the one that the stdlib links for arrays, and its `length` is
/// not a field
fn read_chain(&self, obj: ObjAddr, name: AtomRef, chain: &mut Vec<ObjAddr>) -> Type {
let array = self.is_array(obj);
if array && name == LENGTH_FIELD {
return Type::Int;
}
let own = if array { None } else { self.field(obj, name) };
let own_ty = own.map_or(Type::Unknown, |f| self.get(f.addr).clone());
if own.is_some_and(|f| f.defined) || chain.contains(&obj) {
return own_ty;
}
let proto_field = if array {
None
} else {
self.field(obj, PROTO_FIELD)
};
let proto = match proto_field {
Some(f) => self.get(f.addr).clone(),
None if array => self.builtin_proto(&Type::Ref(obj)),
None => Type::Unknown,
};
// The chain ends at a prototype that can be absent or is not an object
let ends = (!array && !proto_field.is_some_and(|f| f.defined)) || proto == Type::Unknown;
chain.push(obj);
let from_proto = join_all(proto.members().iter().map(|member| match member {
Type::Ref(proto) => self.read_chain(*proto, name, chain),
_ => Type::Undefined,
}));
chain.pop();
let end = if ends { Type::Undefined } else { Type::Unknown };
join_all([own_ty, from_proto, end])
}
// Elements
pub fn elems(&self, obj: ObjAddr) -> &Type {
match self.get(Addr::Object(obj)) {
Type::Shape(Shape::Array(elems)) => elems,
_ => unreachable!(),
}
}
/// Join `ty` into the type of its elements
pub fn write_elem(&mut self, obj: ObjAddr, ty: &Type) {
let new = join(self.elems(obj), ty);
if new != *self.elems(obj) {
self.types
.insert(Addr::Object(obj), Type::Shape(Shape::Array(Box::new(new))));
self.changes += 1;
}
}
/// A read of an element of one of its arrays. An index can be a hole, or past the end,
/// which gives undefined
pub fn read_elem(&self, obj: ObjAddr) -> Type {
join(self.elems(obj), &Type::Undefined)
}
}
impl Memory {
/// The prototype of a primitive or an array: the `prototype` of the stdlib's `String`,
/// `Number`, `Boolean` or `Array`. The stdlib sets each one before any other code runs,
/// so a read takes the field's own type, and never its absence
pub fn builtin_proto(&self, ty: &Type) -> Type {
let class = match ty {
Type::Str(_) => self.builtins.string,
Type::Int | Type::Float => self.builtins.number,
Type::Bool(_) => self.builtins.boolean,
Type::Ref(obj) if self.is_array(*obj) => self.builtins.array,
_ => None,
};
let Some(class) = class else {
return Type::Unknown;
};
let protos = self.get(class).members().iter().map(|class| match class {
Type::Ref(class) => match self.field(*class, PROTOTYPE_FIELD) {
Some(field) => self.get(field.addr).clone(),
None => Type::Unknown,
},
_ => Type::Unknown,
});
join_all(protos)
}
}
+156 -515
View File
@@ -1,567 +1,208 @@
//! Infers a type for every value (see `docs/model.md`, section 4)
//! Infers a type for every location in the program
//!
//! A request is a fn, the call place that calls it, and the types of its args.
//! Its result is the type of each of its values. An object is named by the
//! instr that makes it and the call place of the request that runs the instr,
//! and each name has one cell for the whole program. Both come from the
//! program text, so there are finitely many requests and names, and infer
//! ends. A walk reads cells, protos and the returns of other requests, which
//! only grow, so infer walks the program in rounds until one changes nothing
//! **The base model.** A context is a function in one call site. Infer walks each context:
//! it goes over the function's instrs, and joins the type of each result into memory, which
//! holds one type for each location (see `Addr`). An object is named by the instr that makes
//! it and its context. A call walks the callee in its own context, and what the callee
//! returns is a location too. A walk reads memory, and memory only grows, so infer walks the
//! program in rounds, until a round changes nothing.
//!
//! **Polymorphism.** A function can be called with args of different types. Each set of arg
//! types gets a walk of its own, so a walk is a context and the types of its args. This adds
//! three things to the base model:
//!
//! 1. A walk's arg types are part of its key. An arg passes only its kind, not a known value,
//! so that there are finitely many keys (see `key`).
//! 2. A binding that no closure shares is named by its walk, so that each walk has its own
//! types (see `binding_addr`). Objects and shared bindings stay named by their context.
//! 3. A walk can go stale. When an arg's type grows, its call has a new key, so the call
//! makes a new walk. Only the walks of the last round are part of the program (see
//! `TypeCache::is_live`).
pub mod cells;
pub mod types;
mod memory;
mod types;
mod walk;
use std::collections::{BTreeMap, HashMap};
use std::collections::HashMap;
use crate::{
libqjs::parse::{AtomRef, JsValue, opcodes::OpCode},
native::{self, NATIVES, ProtoOf},
regify::RegFn,
transform::{FIRST_ARG, THIS_ARG},
flatten::ENTRY_FN,
link::{Link, Target},
regify::{Operand, RegFn},
};
use cells::{Cell, Code, LENGTH_FIELD, ObjKind, Protos, read_value};
use types::{BaseType, EMPTY_STRING, Type, is_function, join, loose_eq, strict_eq, test, truthiness, typeof_name};
pub use memory::{Addr, Builtins, CallSite, Context, InstrRef, Memory, ObjAddr};
pub use types::{LENGTH_FIELD, Shape, Type, join};
/// A call place: a fn and the index of its call instr
pub type Place = (usize, usize);
/// The call place of the entry fn, which the host calls
pub const HOST: Place = (usize::MAX, 0);
/// The call place of the fn that the host calls when an IO op completes (see
/// `Code::HostLink`)
pub const HOST_COMPLETE: Place = (usize::MAX, 1);
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct InferRequest {
pub fn_id: usize,
pub place: Place,
pub arg_types: Vec<Type>, // one for each param
}
#[derive(Debug)]
pub struct InferResult {
pub val_types: Vec<Type>,
pub instr_types: Vec<Type>, // the type each instr gives, before it is joined into its dst
pub ret_type: Type,
pub reachable: Vec<bool>, // the blocks that can run
}
/// A request and its result
/// A context, and the types of its args: what a walk walks. What the walk found is in memory
#[derive(Debug)]
pub struct Walk {
pub req: InferRequest,
ret: Type, // what it returned in the rounds so far: a recursive call gives that
walking: bool,
pub result: Option<InferResult>, // in the current round, once it is done
pub context: Context,
pub arg_types: Vec<Type>, // one for each param
pub reachable: Vec<bool>, // the blocks that can run
round: usize, // the last round that walked it. A walk that the last round did not reach is stale
walking: bool, // it is on the call stack: a recursive call gives what it returned so far
}
#[derive(Debug)]
pub struct TypeCache {
pub walks: Vec<Walk>,
ids: HashMap<InferRequest, usize>, // the walk of each request
objects: BTreeMap<(usize, usize, Place), Cell>, // the cell of each name: the fn and instr that make the objects, and the call place
builtins: Vec<Cell>, // the cells of the natives, then of the proto links
pub protos: Protos, // the prototype of each `ProtoOf`, as the stdlib links it
pub host_complete: Type, // what the host calls when an IO op completes, as the stdlib links it
version: usize, // goes up on every change of a cell, a return or a proto, and on every new cell or walk
ids: HashMap<(Context, Vec<Type>), usize>, // the walk of each context and arg types
pub memory: Memory,
round: usize,
}
/// The key of a call of `fn_idx` from `call_site` with args of these types. An arg that the
/// call does not pass is undefined, and an arg that it passes beyond the params is never
/// read. Only an arg's kind passes, not a known value, else every literal that a call passes
/// would make a new walk
fn key(
fn_idx: usize,
call_site: CallSite,
arg_types: &[Type],
fns: &[RegFn],
) -> (Context, Vec<Type>) {
let params =
(0..fns[fn_idx].arg_count).map(|i| arg_types.get(i).map_or(Type::Undefined, Type::widen));
(Context { fn_idx, call_site }, params.collect())
}
impl TypeCache {
/// The request of a call of `fn_id` from `place` with args of these types.
/// An arg it does not pass is undefined, and an arg it passes beyond its
/// params is never read. Only an arg's kind crosses a call, not a known
/// value, else every literal passed would make a new walk
fn request(fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> InferRequest {
let params = (0..fns[fn_id].arg_count).map(|i| arg_types.get(i).map_or(Type::Undefined, Type::widen));
InferRequest { fn_id, place, arg_types: params.collect() }
}
/// The walk of a call that infer walked
pub fn find(&self, fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> usize {
self.ids[&Self::request(fn_id, place, arg_types, fns)]
pub fn find(
&self,
fn_idx: usize,
call_site: CallSite,
arg_types: &[Type],
fns: &[RegFn],
) -> usize {
self.ids[&key(fn_idx, call_site, arg_types, fns)]
}
/// The cell of a built-in fn
pub fn builtin(&self, code: Code) -> &Cell {
match code {
Code::Native(id) => &self.builtins[id],
Code::ProtoLink(kind) => &self.builtins[NATIVES.len() + kind as usize],
Code::HostLink => &self.builtins[NATIVES.len() + ProtoOf::ALL.len()],
Code::Fn(_) => unreachable!(),
/// Whether the last round walked `w`. Only such walks are part of the program
pub fn is_live(&self, w: usize) -> bool {
self.walks[w].round == self.round
}
/// What walk `w` returns
pub fn ret(&self, w: usize) -> &Type {
self.memory.get(Addr::Return(w))
}
/// The address of a local, arg or reg of walk `w`
pub fn addr(&self, w: usize, slot: Operand, fns: &[RegFn]) -> Addr {
let context = self.walks[w].context;
binding_addr(w, context, &fns[context.fn_idx].captured_slots(), slot)
}
/// Everything that can change, counted: memory, and the walks. Infer is done when a round
/// leaves it the same
fn progress(&self) -> usize {
self.memory.changes() + self.walks.len()
}
/// The host's call of `__complete` with an op's handle: each closure that the binding can
/// hold, as its own env, with `this` undefined
fn call_host_complete(&mut self, complete: Addr, fns: &[RegFn]) {
for member in self.memory.get(complete).clone().members() {
if let Type::Ref(obj) = member
&& let Some(fn_idx) = self.memory.closure_fn(*obj)
{
self.call(
fn_idx,
None,
&[member.clone(), Type::Undefined, Type::Int],
fns,
);
}
}
}
/// The cells of the built-in fns
pub fn builtins(&self) -> &[Cell] {
&self.builtins
}
/// Infer a call of `fn_idx` from `call_site`, walking it unless this round walked it
/// already. Gives what it returns
fn call(
&mut self,
fn_idx: usize,
call_site: CallSite,
arg_types: &[Type],
fns: &[RegFn],
) -> Type {
let (context, arg_types) = key(fn_idx, call_site, arg_types, fns);
/// Every cell: the built-in ones, then one for each name
pub fn cells(&self) -> impl Iterator<Item = &Cell> {
self.builtins.iter().chain(self.objects.values())
}
/// The cell of the objects of `name`, made the first time
fn object(&mut self, name: (usize, usize, Place), kind: ObjKind) -> Cell {
if let Some(cell) = self.objects.get(&name) {
return cell.clone();
}
self.version += 1;
self.objects.entry(name).or_insert(Cell::new(kind)).clone()
}
/// Count a change, if there was one
fn changed(&mut self, changed: bool) {
self.version += changed as usize;
}
/// Infer a call of `fn_id` from `place`, walking it unless its request is
/// done in this round. Gives what it returns
fn call(&mut self, fn_id: usize, place: Place, arg_types: &[Type], fns: &[RegFn]) -> Type {
let req = Self::request(fn_id, place, arg_types, fns);
let w = match self.ids.get(&req) {
let w = match self.ids.get(&(context, arg_types.clone())) {
Some(&w) => w,
None => {
self.version += 1;
self.ids.insert(req.clone(), self.walks.len());
self.walks.push(Walk { req, ret: Type::Unknown, walking: false, result: None });
self.ids
.insert((context, arg_types.clone()), self.walks.len());
self.walks.push(Walk {
context,
arg_types,
reachable: vec![],
round: 0,
walking: false,
});
self.walks.len() - 1
}
};
match &self.walks[w] {
Walk { walking: true, ret, .. } => return ret.clone(),
Walk { result: Some(res), .. } => return res.ret_type.clone(),
_ => {}
if !self.walks[w].walking && !self.is_live(w) {
self.walks[w].walking = true;
let reachable = walk::walk_fn(w, fns, self);
let walk = &mut self.walks[w];
walk.walking = false;
walk.round = self.round;
walk.reachable = reachable;
}
self.walks[w].walking = true;
let res = walk_fn(w, fns, self);
let ret = res.ret_type.clone();
let walk = &mut self.walks[w];
let joined = join(&walk.ret, &ret);
let grew = joined != walk.ret;
walk.walking = false;
walk.ret = joined;
walk.result = Some(res);
self.changed(grew);
ret
self.ret(w).clone()
}
}
/// The join of `f` over the members of `ty`
fn map(ty: &Type, mut f: impl FnMut(&Type) -> Type) -> Type {
ty.members().iter().fold(Type::Unknown, |all, member| join(&all, &f(member)))
}
/// What `f` gives for every member of `ty`, if all members give the same
fn agree<T: PartialEq>(ty: &Type, f: impl Fn(&Type) -> Option<T>) -> Option<T> {
let mut members = ty.members().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>(a: &Type, b: &Type, f: impl Fn(&Type, &Type) -> Option<T>) -> Option<T> {
let pairs = a.members().iter().flat_map(|x| b.members().iter().map(move |y| (x, y)));
let mut results = pairs.map(|(x, y)| f(x, y));
let first = results.next()??;
results.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// A call of `code` from `place` 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], place: Place) -> Type {
let args = &arg_types[FIRST_ARG as usize..];
match code {
Code::Fn(fn_id) => cache.call(fn_id, place, arg_types, fns),
Code::Native(id) => native::ret_type(id, args),
Code::ProtoLink(kind) => {
let linked = &cache.protos[kind as usize];
let joined = join(linked, args.first().unwrap_or(&Type::Undefined));
cache.changed(joined != *linked);
cache.protos[kind as usize] = joined;
Type::Undefined
}
Code::HostLink => {
let joined = join(&cache.host_complete, args.first().unwrap_or(&Type::Undefined));
cache.changed(joined != cache.host_complete);
cache.host_complete = joined;
Type::Undefined
}
/// The address of a local, arg or reg of walk `w` of `context`, given the slots of its
/// function that closures share. A shared binding is named by its context, like the closures
/// that share it. Any other binding is named by its walk
fn binding_addr(w: usize, context: Context, shared: &[Operand], slot: Operand) -> Addr {
match shared.contains(&slot) {
true => Addr::Shared { context, slot },
false => Addr::Private { walk: w, slot },
}
}
/// The calls that the host makes: the entry, with no args, then the fn that
/// it calls when an IO op completes, with the op's handle. Each member of its
/// type is its own env, and `this` is undefined
fn host_calls(cache: &mut TypeCache, fns: &[RegFn]) {
// The synthetic program entry is appended last by `flatten`
cache.call(fns.len() - 1, HOST, &[], fns);
/// Walk the program from the entry, in rounds, until a round changes nothing
///
/// The host makes two kinds of call (see lib/rt/jsrt.h): the entry, with no args, and the
/// stdlib's `__complete`, with an op's handle, when the op completes. Both come from the
/// host, which has no call site
pub fn infer_types(fns: &[RegFn], link: &Link) -> TypeCache {
let global = |name: &str| match link.global(name) {
Some(Target::Global(global)) => Some(Addr::Global(global)),
_ => None,
};
let complete = Addr::Global(link.stdlib("jsrt:loop", "__complete"));
for member in cache.host_complete.clone().members() {
if let Type::Obj(cell) = member
&& let Some(Code::Fn(fn_id)) = cell.code()
{
cache.call(fn_id, HOST_COMPLETE, &[member.clone(), Type::Undefined, Type::Int], fns);
}
}
}
/// An array index must be a number. Any other key is a computed field name,
/// which is not supported
fn check_index(index: &Type) {
for member in index.members() {
if !matches!(member, Type::Int | Type::Float) {
unimplemented!("`a[k]` with a key of type {member:?}: computed field names");
}
}
}
/// Walk the program from the entry, in rounds, until a round changes
/// nothing. The results are those of the last round
pub fn infer_types(fns: &[RegFn]) -> TypeCache {
let mut cache = TypeCache {
walks: vec![],
ids: HashMap::new(),
objects: BTreeMap::new(),
builtins: vec![],
protos: std::array::from_fn(|_| Type::Unknown),
host_complete: Type::Unknown,
version: 0,
memory: Memory::default(),
round: 0,
};
let codes = (0..NATIVES.len()).map(Code::Native).chain(ProtoOf::ALL.map(Code::ProtoLink)).chain([Code::HostLink]);
cache.builtins = codes.map(|code| Cell::new(ObjKind::Fn(code))).collect();
cache.memory.builtins = Builtins {
string: global("String"),
number: global("Number"),
boolean: global("Boolean"),
array: global("Array"),
};
loop {
let version = cache.version;
cache.walks.iter_mut().for_each(|w| w.result = None);
let before = cache.progress();
cache.round += 1;
cache.call(ENTRY_FN, None, &[], fns);
cache.call_host_complete(complete, fns);
host_calls(&mut cache, fns);
if cache.version == version {
if cache.progress() == before {
return cache;
}
}
}
fn walk_fn(w: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult {
use OpCode::*;
let (fn_id, place) = (cache.walks[w].req.fn_id, cache.walks[w].req.place);
let f = &reg_fns[fn_id];
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)
for (i, ty) in cache.walks[w].req.arg_types.iter().enumerate() {
val_types[f.var_count + i] = ty.clone();
}
// The blocks that can run, given what the types say about each condition
let mut reachable = vec![false; f.blocks.len()];
reachable[0] = true;
// Scratch for calls, kept across instrs
let mut arg_types: Vec<Type> = vec![];
// Walk instrs till nothing changes. A return only changes `ret_type` once
// its block is reachable, and a field read only gives more once a cell
// changes, so the values, the blocks and the version are enough to watch
loop {
let version = cache.version;
let mut changed = false;
for (block_idx, block) in f.blocks.iter().enumerate() {
if !reachable[block_idx] {
continue;
}
let code = &f.code[block.start..block.end];
let code = code.iter().chain(&block.postfix);
for (offset, instr) in code.enumerate() {
// Jumps are the block's to walk (see below). The others give
// nothing
if matches!(instr.op, Nop | Drop | Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8) {
continue;
}
// A rule with an `Unknown` operand gives `Unknown`: no value has
// reached the instr yet
if instr.args.iter().any(|&v| val_types[v] == Type::Unknown) {
continue;
}
// The instr's index in the code (postfix copies have none)
let idx = block.start + offset;
let arg = |i: usize| val_types[instr.args[i]].clone();
// The cell of the objects that this instr makes in this
// request: the one of its name
let cell_here = |cache: &mut TypeCache, kind: ObjKind| cache.object((fn_id, idx, place), kind);
let ty = match instr.op {
// ────────────────────────────────────────── Literals ───────────────────────────────────────────────────────
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7
| PushI8 | PushI16 | PushI32 => Type::Int,
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::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) => Type::Obj(cache.builtin(Code::Native(id)).clone()),
JsValue::ProtoLink(kind) => Type::Obj(cache.builtin(Code::ProtoLink(kind)).clone()),
JsValue::HostLink => Type::Obj(cache.builtin(Code::HostLink).clone()),
_ => unreachable!(),
},
Undefined => Type::Undefined,
Null => Type::Null,
// ──────────────────────────────────────── Arithmetic ───────────────────────────────────────────────────────
// `+` concatenates if either side becomes a string, which
// a string or an object does. Else it adds numbers
Add => {
let r = arg(0);
map(&arg(1), |l| map(&r, |r| match (l, r) {
(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 => {
let r = arg(0);
map(&arg(1), |l| map(&r, |r| match (l, r) {
(Type::Int, Type::Int) => Type::Int,
_ => Type::Float,
}))
}
Neg | Plus | Inc | Dec | PostInc | PostDec => map(&arg(0), |t| match t {
Type::Int => Type::Int,
_ => Type::Float,
}),
Div => Type::Float,
// Bitwise NOT gives an int32
Not => Type::Int,
Lt | Lte | Gt | Gte => Type::Bool(None),
// ─────────────────────────────── Conditions the types may decide ──────────────────────────────────────────
// A known value lets infer skip the branch that cannot run
// (see `reachable` below)
StrictEq | StrictNeq | Eq | Neq => {
let eq = match instr.op {
StrictEq | StrictNeq => agree2(&arg(0), &arg(1), strict_eq),
_ => agree2(&arg(0), &arg(1), loose_eq),
};
Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq)))
}
Lnot => Type::Bool(agree(&arg(0), truthiness).map(|t| !t)),
Typeof => match agree(&arg(0), typeof_name) {
Some(name) => Type::Str(Some(AtomRef::Predefined(name))),
None => Type::Str(None),
},
IsUndefined | TypeofIsUndefined => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Undefined]))),
IsNull => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Null]))),
IsUndefinedOrNull => Type::Bool(agree(&arg(0), |t| test(t, &[BaseType::Undefined, BaseType::Null]))),
TypeofIsFunction => Type::Bool(agree(&arg(0), is_function)),
// ─────────────────────────────────────── Value Movers ──────────────────────────────────────────────────────
SetLoc | SetName => arg(0),
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
Object => Type::Obj(cell_here(cache, ObjKind::Plain)),
Fclosure | Fclosure8 => {
let JsValue::FnById(child) = f.cpool[instr.imm.const_idx()] else {
unreachable!()
};
Type::Obj(cell_here(cache, ObjKind::Fn(Code::Fn(child))))
}
// The elements are its operands
ArrayFrom => {
let cell = cell_here(cache, ObjKind::Array { elems: Type::Unknown });
for &v in &instr.args {
let grew = cell.write_elem(&val_types[v]);
cache.changed(grew);
}
Type::Obj(cell)
}
// `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);
map(&arg(1), |obj| match obj {
Type::Obj(cell) if cell.is_array() => {
check_index(&index);
cell.read_elem()
}
Type::Str(_) => {
check_index(&index);
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));
for obj in arg(2).members() {
match obj {
Type::Obj(cell) if cell.is_array() => {
check_index(&index);
let grew = cell.write_elem(&value);
cache.changed(grew);
}
Type::Undefined | Type::Null | Type::Str(_) => {}
obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"),
}
}
Type::Unknown
}
// A key stays as it is: a number is an index
ToPropkey => arg(0),
// The write goes into the object's cell, which every holder
// of the object sees. A primitive has no fields to write,
// and undefined and null throw
DefineField | PutField => {
let (field, value) = (instr.imm.atom(), arg(0));
for obj in arg(1).members() {
if let Type::Obj(cell) = obj {
let grew = cell.write(field, &value, instr.op == DefineField);
cache.changed(grew);
}
}
Type::Unknown
}
GetField => read_value(&arg(0), instr.imm.atom(), &cache.protos),
// A string's length is its own. Anything else reads a
// `length` field
GetLength => map(&arg(0), |obj| match obj {
Type::Str(_) => Type::Int,
obj => 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). A callee that is not
// a fn throws, so it gives nothing. A call of each member
// of a sum passes that member as the env
Call | CallMethod | CallConstructor => {
let ret = map(&arg(0), |callee| {
let Type::Obj(cell) = callee else {
return Type::Unknown;
};
let Some(code) = cell.code() else {
return Type::Unknown;
};
arg_types.clear();
arg_types.extend(instr.args.iter().map(|&v| val_types[v].clone()));
arg_types[0] = callee.clone();
call_code(cache, code, &arg_types, reg_fns, (fn_id, idx))
});
// `new` gives what the constructor returns if that is an
// object, else the new object itself, `this`
match instr.op {
CallConstructor => map(&ret, |ret| match ret {
Type::Obj(_) => ret.clone(),
_ => arg(THIS_ARG as usize),
}),
_ => ret,
}
}
// ──────────────────────────────────────── Returns ──────────────────────────────────────────────────────────
Return | ReturnAsync => arg(0),
ReturnUndef => Type::Undefined,
_ => unreachable!("{instr:?}"),
};
// Postfix copies come after the block's own instrs, and have no
// entry: a copy gives the type of what it copies
if idx < block.end {
instr_types[idx] = join(&instr_types[idx], &ty);
}
if matches!(instr.op, Return | ReturnAsync | ReturnUndef) {
ret_type = join(&ret_type, &ty);
continue;
}
for &dst in &instr.dsts {
let joined = join(&val_types[dst], &ty);
changed |= joined != val_types[dst];
val_types[dst] = joined;
}
} // instr
// Mark the blocks this one can go to. A conditional jump whose
// condition the types decide goes one way only. One whose
// condition nothing has reached yet goes nowhere yet
// (A block that only carries copies on an edge has no instrs)
let jumps = match f.code[block.start..block.end].last() {
Some(last) if matches!(last.op, IfFalse | IfFalse8 | IfTrue | IfTrue8) => {
match &val_types[last.args[0]] {
Type::Unknown => continue,
cond => agree(cond, truthiness).map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
}
}
_ => None,
};
// succ[0] is the jump target, succ[1] the fall-through
for (i, &succ) in block.succ.iter().enumerate() {
if jumps.is_none_or(|jumps| jumps == (i == 0)) && !reachable[succ] {
reachable[succ] = true;
changed = true;
}
}
} // block
if !changed && cache.version == version {
break;
}
} // loop
InferResult {
val_types,
instr_types,
ret_type,
reachable,
}
}
+98 -73
View File
@@ -1,26 +1,25 @@
//! 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 2)
use std::rc::Rc;
//! Types: the values that a location can hold, how they join, and what a type decides
//! about the values in it
use super::memory::{Addr, ObjAddr};
use crate::libqjs::parse::AtomRef;
use super::cells::Cell;
pub const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
pub const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
pub(super) const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
/// A type is one of these. Join rules:
/// The type of a value. A value has no identity: two values compare by their types. An
/// object value is the address of the object, so it compares by that address
///
/// 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 cell
/// A sum is a value of one of a list of types, its members. It has two or more members,
/// none of them `Unknown` or `Sum`, sorted in the order of the variants below. It has at
/// most one member of each base type, except objects: one `Ref` for each address
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Type {
Unknown, // no value has reached it yet
@@ -30,13 +29,35 @@ pub enum Type {
Int,
Float,
Str(Option<AtomRef>), // the value, if it is known. Atoms are interned: equal atoms are equal strings
Obj(Cell), // an object, which the cell describes. A fn is an object whose cell has code
Sum(Rc<[Type]>), // see above
Any, // any value. No rule gives it now: a join gives a sum instead
Ref(ObjAddr), // an object: the address of its entry in memory
Native(usize), // a C fn that JS can call, by its index in `native::NATIVES`. A fn, but no object
Sum(Vec<Type>), // see above
// Only in an object's own entry
Shape(Shape),
Closure { fn_idx: usize, fields: Fields }, // a fn object, which runs fn `fn_idx`
}
/// A `Type` without its detail (a known value, a cell): the kinds of value
/// that JS tells apart
/// What an object's entry holds, besides a closure's
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Shape {
Obj(Fields),
Array(Box<Type>), // the values written to its elements. Nothing shares an element, so they are values
}
/// The fields of an object, sorted by name
pub type Fields = Vec<(AtomRef, Field)>;
/// A field of an object: a location. It is the field's own location (`Addr::Field`), or a
/// location that it shares (see `define_field_ref`)
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Field {
pub addr: Addr,
pub defined: bool, // every object of the address has it from the start, so a read never goes to the prototype
}
/// A `Type` without its detail (a known value, an address): the kinds of value that JS
/// tells apart
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BaseType {
Undefined,
@@ -48,17 +69,20 @@ pub enum BaseType {
}
impl Type {
/// 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
/// The base type of every value of the type. `None` for `Unknown`, which has no
/// values, and for `Sum`, which can have more than one
pub fn base(&self) -> Option<BaseType> {
match self {
Type::Unknown | Type::Sum(_) | Type::Any => None,
Type::Unknown | Type::Sum(_) => 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),
Type::Ref(_) | Type::Native(_) => Some(BaseType::Object),
Type::Shape(_) | Type::Closure { .. } => {
unreachable!("an object's entry is not a value")
}
}
}
@@ -66,8 +90,8 @@ impl Type {
matches!(self, Type::Undefined | Type::Null)
}
/// The types that a value of the type can have: the members of a sum,
/// else the type itself. `Unknown` has none
/// The types that a value of the type can have: the members of a sum, else the type
/// itself. `Unknown` has none
pub fn members(&self) -> &[Type] {
match self {
Type::Unknown => &[],
@@ -85,29 +109,53 @@ impl Type {
ty => ty.clone(),
}
}
/// What `test` gives for every value of the type, if all its members give the same
pub fn decide<T: PartialEq>(&self, test: impl Fn(&Type) -> Option<T>) -> Option<T> {
same(self.members().iter().map(test))
}
}
/// The join of two types (see `Type`)
/// What `test` gives for every pair of values of `a` and `b`, if all pairs of members give
/// the same
pub fn decide_pair<T: PartialEq>(
a: &Type,
b: &Type,
test: impl Fn(&Type, &Type) -> Option<T>,
) -> Option<T> {
same(pairs(a, b).map(|(x, y)| test(x, y)))
}
/// `Some(x)` if every item is `Some(x)`, for one `x`
fn same<T: PartialEq>(mut items: impl Iterator<Item = Option<T>>) -> Option<T> {
let first = items.next()??;
items.all(|t| t.as_ref() == Some(&first)).then_some(first)
}
/// Every pair of a member of `a` and a member of `b`
pub fn pairs<'a>(a: &'a Type, b: &'a Type) -> impl Iterator<Item = (&'a Type, &'a Type)> {
a.members()
.iter()
.flat_map(|x| b.members().iter().map(move |y| (x, y)))
}
/// The join of two values (see `Type`)
pub fn join(a: &Type, b: &Type) -> Type {
match (a, b) {
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
(a, b) if a == b => a.clone(),
(Type::Any, _) | (_, Type::Any) => Type::Any,
_ => {
let joined = sum_of(a.members().iter().chain(b.members()).cloned().collect());
// Keep the sum that exists, so that its members are not copied
match () {
_ if joined == *a => a.clone(),
_ if joined == *b => b.clone(),
_ => joined,
}
}
_ => sum_of(a.members().iter().chain(b.members()).cloned().collect()),
}
}
/// The type whose members are `list`: sorted, and two of one base type
/// joined
/// The join of all the values
pub fn join_all(types: impl IntoIterator<Item = Type>) -> Type {
types
.into_iter()
.fold(Type::Unknown, |all, ty| join(&all, &ty))
}
/// The type whose members are `list`: sorted, and two of one base type joined
fn sum_of(mut list: Vec<Type>) -> Type {
list.sort_unstable();
list.dedup_by(|b, a| {
@@ -122,11 +170,11 @@ fn sum_of(mut list: Vec<Type>) -> Type {
match list.len() {
1 => list.pop().unwrap(),
_ => Type::Sum(list.into()),
_ => Type::Sum(list),
}
}
/// The join of two types of one base type, neither of them `Obj`
/// The join of two types of one base type, neither of them `Ref`
fn join_base(a: &Type, b: &Type) -> Type {
match (a, b) {
(Type::Bool(x), Type::Bool(y)) => Type::Bool(if x == y { *x } else { None }),
@@ -137,18 +185,18 @@ fn join_base(a: &Type, b: &Type) -> Type {
}
}
// The tests below take one member of a type each (see `Type::decide()`)
/// The type, if every value of it is one value that the type tells
pub(super) fn single(ty: &Type) -> Option<Type> {
fn single(ty: &Type) -> Option<Type> {
match ty {
Type::Undefined | Type::Null | Type::Bool(Some(_)) | Type::Str(Some(_)) => Some(ty.clone()),
_ => None,
}
}
// The tests below take one member of a type each (see `agree()` in infer)
/// Whether `a === b`, if the types decide it
pub(super) fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
pub 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)) {
@@ -158,10 +206,9 @@ pub(super) fn strict_eq(a: &Type, b: &Type) -> Option<bool> {
}
}
/// 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> {
/// 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 fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
match (a, b) {
_ if a.is_nullish() && b.is_nullish() => Some(true),
_ if a.is_nullish() || b.is_nullish() => Some(false),
@@ -171,39 +218,17 @@ pub(super) fn loose_eq(a: &Type, b: &Type) -> Option<bool> {
}
/// Whether a value of type `ty` has one of the base types `bases`
pub(super) fn test(ty: &Type, bases: &[BaseType]) -> Option<bool> {
pub fn is_base(ty: &Type, bases: &[BaseType]) -> Option<bool> {
ty.base().map(|base| bases.contains(&base))
}
/// Whether a value of type `ty` is a fn
pub(super) fn is_function(ty: &Type) -> Option<bool> {
match ty {
Type::Obj(cell) => Some(cell.code().is_some()),
_ => Some(false),
}
}
/// Whether a value of type `ty` is truthy, if the type decides it
pub(super) fn truthiness(ty: &Type) -> Option<bool> {
pub 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),
Type::Ref(_) | Type::Native(_) => Some(true),
_ => None,
}
}
/// The atom of what `typeof` gives for a value of type `ty`
pub(super) fn typeof_name(ty: &Type) -> 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(cell) if cell.code().is_some() => 27, // "function"
Type::Obj(_) => 77, // "object"
_ => return None,
})
}
+471
View File
@@ -0,0 +1,471 @@
//! One walk: the types that one context and its arg types give the locals, args and regs of
//! a function
//!
//! The walk goes over the reachable blocks until nothing changes. Each instr has a rule,
//! which gives the type of its result from the types of its operands. The result joins into
//! the instr's dsts in memory. A rule with an `Unknown` operand gives nothing: no value has
//! reached the instr yet
use super::{
TypeCache, binding_addr,
memory::{Addr, Context, InstrRef, ObjAddr},
types::{
BaseType, EMPTY_STRING, LENGTH_FIELD, Shape, Type, decide_pair, is_base, join, join_all,
loose_eq, pairs, strict_eq, truthiness,
},
};
use crate::{
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
native,
regify::{BasicBlock, Operand, RegFn, RegInstr},
transform::{FIRST_ARG, THIS_ARG},
};
/// Walk `w`. Gives the blocks that can run
pub fn walk_fn(w: usize, fns: &[RegFn], cache: &mut TypeCache) -> Vec<bool> {
let context = cache.walks[w].context;
let f = &fns[context.fn_idx];
// The args get their types from the walk's key
for (i, ty) in cache.walks[w].arg_types.clone().iter().enumerate() {
let addr = binding_addr(w, context, &f.captured_slots(), Operand::Arg(i));
cache.memory.join(addr, ty);
}
let walker = Walker {
w,
context,
f,
fns,
shared: f.captured_slots(),
cache,
reachable: vec![false; f.blocks.len()],
};
walker.run()
}
struct Walker<'a> {
w: usize,
context: Context,
f: &'a RegFn,
fns: &'a [RegFn],
shared: Vec<Operand>, // the slots of `f` that closures share
cache: &'a mut TypeCache,
reachable: Vec<bool>,
}
impl Walker<'_> {
fn addr(&self, slot: Operand) -> Addr {
binding_addr(self.w, self.context, &self.shared, slot)
}
/// The address of the objects that the instr at `idx` makes, in this context
fn here(&self, idx: usize) -> ObjAddr {
ObjAddr {
context: self.context,
instr: idx,
}
}
fn run(mut self) -> Vec<bool> {
self.reachable[0] = true;
// A return only changes once its block is reachable, and a read only gives more
// once memory changes, so the blocks and the count of changes are enough to watch
loop {
let before = (self.cache.progress(), self.reachable.clone());
let f = self.f;
for (i, block) in f.blocks.iter().enumerate() {
if self.reachable[i] {
self.walk_block(block);
}
}
if (self.cache.progress(), &self.reachable) == (before.0, &before.1) {
return self.reachable;
}
}
}
fn walk_block(&mut self, block: &BasicBlock) {
let f = self.f;
let code = f.code[block.start..block.end].iter().chain(&block.postfix);
// A postfix copy has no index in the code, but its rule never needs one
for (offset, instr) in code.enumerate() {
self.step(instr, block.start + offset);
}
self.mark_successors(block);
}
fn step(&mut self, instr: &RegInstr, idx: usize) {
use OpCode::*;
// Jumps are the block's to walk (see `mark_successors()`). A `close_loc` or a
// `close_field` gives a binding a new location, which has the same address (see
// transform/closure.rs)
if matches!(
instr.op,
Nop | CloseLoc
| CloseField
| Goto
| Goto8
| Goto16
| IfFalse
| IfFalse8
| IfTrue
| IfTrue8
) {
return;
}
let args: Vec<Type> = instr
.args
.iter()
.map(|&slot| self.cache.memory.get(self.addr(slot)).clone())
.collect();
// The first operand of `define_field_ref` is a location, not a value, and a binding
// has no value before its first write
let values = if instr.op == DefineFieldRef {
&args[1..]
} else {
&args[..]
};
if values.contains(&Type::Unknown) {
return;
}
let ty = self.rule(instr, idx, &args);
if matches!(instr.op, Return | ReturnAsync | ReturnUndef) {
self.cache.memory.join(Addr::Return(self.w), &ty);
}
for &dst in &instr.dsts {
self.cache.memory.join(self.addr(dst), &ty);
}
}
/// The type that `instr` gives, from the types of its operands `args`. An instr that
/// gives nothing gives `Unknown`. Operands that an instr pops come top first (see
/// regify), so a binary operator has its right side in `args[0]`
fn rule(&mut self, instr: &RegInstr, idx: usize, args: &[Type]) -> Type {
use OpCode::*;
let memory = &self.cache.memory;
match instr.op {
// Literals
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8
| PushI16 | PushI32 => Type::Int,
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 => self.constant(instr),
Undefined => Type::Undefined,
Null => Type::Null,
// Arithmetic. Every number is a double, and an int is a double that is known to
// fit in an int32 (see emit). So an operator that can leave that range, or give -0
// or NaN, gives a float. `+` concatenates if either side becomes a string, which a
// string or an object does
Add => join_all(pairs(&args[1], &args[0]).map(|(l, r)| {
let text = |t: &Type| matches!(t, Type::Str(_) | Type::Ref(_) | Type::Native(_));
match text(l) || text(r) {
true => Type::Str(None),
false => Type::Float,
}
})),
Sub | Mul | Div | Mod | Pow | Neg | Inc | Dec | PostInc | PostDec => Type::Float,
// `+x` is x as a number: an int stays an int
Plus => join_all(args[0].members().iter().map(|t| match t {
Type::Int => Type::Int,
_ => Type::Float,
})),
// The bitwise operators give an int32, but `>>>` gives a uint32
Not | And | Or | Xor | Shl | Sar => Type::Int,
Shr => Type::Float,
Lt | Lte | Gt | Gte => Type::Bool(None),
// Tests. A known result lets the walk skip a branch that cannot run
StrictEq | StrictNeq | Eq | Neq => {
let eq = match instr.op {
StrictEq | StrictNeq => decide_pair(&args[0], &args[1], strict_eq),
_ => decide_pair(&args[0], &args[1], loose_eq),
};
Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq)))
}
Lnot => Type::Bool(args[0].decide(truthiness).map(|t| !t)),
Typeof => Type::Str(args[0].decide(|t| memory.typeof_name(t))),
IsUndefined | TypeofIsUndefined => {
Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Undefined])))
}
IsNull => Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Null]))),
IsUndefinedOrNull => {
Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Undefined, BaseType::Null])))
}
TypeofIsFunction => Type::Bool(args[0].decide(|t| memory.is_function(t))),
// Moves. A key stays as it is: a number is an index
SetLoc | SetName | ToPropkey => args[0].clone(),
// The bindings at the top of a module, which are globals (see link.rs)
GetVar => memory.get(Addr::Global(global(instr))).clone(),
PutVar => {
self.cache
.memory
.join(Addr::Global(global(instr)), &args[0]);
Type::Unknown
}
// Objects
Object => self.make(idx, Type::Shape(Shape::Obj(vec![]))),
Fclosure => {
let JsValue::FnByIdx(fn_idx) = self.f.cpool[instr.imm.const_idx()] else {
unreachable!()
};
self.make(
idx,
Type::Closure {
fn_idx,
fields: vec![],
},
)
}
ArrayFrom => {
let array = self.make(idx, Type::Shape(Shape::Array(Box::new(Type::Unknown))));
for value in args {
self.cache.memory.write_elem(self.here(idx), value);
}
array
}
GetArrayEl => self.get_array_el(&args[1], &args[0]),
PutArrayEl => self.put_array_el(&args[2], &args[1], &args[0]),
DefineField | PutField => self.write_field(instr, &args[1], &args[0]),
DefineFieldRef => self.share_field(instr, args),
GetField => memory.read_field(&args[0], instr.imm.atom()),
// A string's length is its own. Anything else reads a `length` field
GetLength => join_all(args[0].members().iter().map(|t| match t {
Type::Str(_) => Type::Int,
t => memory.read_field(t, LENGTH_FIELD),
})),
// Calls
Call | CallMethod | CallConstructor => self.call(instr, idx, args),
// Returns
Return | ReturnAsync => args[0].clone(),
ReturnUndef => Type::Undefined,
_ => unreachable!("{instr:?}"),
}
}
/// The object that the instr at `idx` makes in this walk, made with `entry` the first time
fn make(&mut self, idx: usize, entry: Type) -> Type {
self.cache.memory.make(self.here(idx), entry);
Type::Ref(self.here(idx))
}
/// A constant from the fn's constant pool
fn constant(&self, instr: &RegInstr) -> Type {
match &self.f.cpool[instr.imm.const_idx()] {
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(idx) => Type::Native(*idx),
c => unimplemented!("the constant {c:?}"),
}
}
/// `a[i]` on an array, or `s[i]` on a string: an index is a number. Other objects need
/// computed field names, which are not supported. Undefined and null throw
fn get_array_el(&self, obj: &Type, index: &Type) -> Type {
join_all(obj.members().iter().map(|obj| match obj {
Type::Ref(obj) if self.cache.memory.is_array(*obj) => {
check_index(index);
self.cache.memory.read_elem(*obj)
}
Type::Str(_) => {
check_index(index);
join(&Type::Str(None), &Type::Undefined)
}
Type::Undefined | Type::Null => Type::Unknown,
obj => unimplemented!("`o[k]` on {obj:?}: computed field names"),
}))
}
/// `a[i] = v` on an array. A string cannot be written (module code is strict), and
/// undefined and null throw
fn put_array_el(&mut self, obj: &Type, index: &Type, value: &Type) -> Type {
for obj in obj.members() {
match obj {
// A write past the end leaves holes before it, which hold undefined
Type::Ref(obj) if self.cache.memory.is_array(*obj) => {
check_index(index);
self.cache.memory.write_elem(*obj, value);
self.cache.memory.write_elem(*obj, &Type::Undefined);
}
Type::Undefined | Type::Null | Type::Str(_) => {}
obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"),
}
}
Type::Unknown
}
/// `define_field` and `put_field`. The write goes into the object's field, which every
/// holder of the object sees. A primitive has no fields to write, and undefined and null
/// throw
fn write_field(&mut self, instr: &RegInstr, obj: &Type, value: &Type) -> Type {
for obj in obj.members() {
if let Type::Ref(obj) = obj {
let defined = instr.op == OpCode::DefineField;
self.cache
.memory
.write_field(*obj, instr.imm.atom(), value, defined);
}
}
Type::Unknown
}
/// `define_field_ref`: the field becomes a location, which regify gives as the first
/// operand. That is a binding, or the reg of an object whose field of the same name is
/// the location (see regify/walk.rs)
fn share_field(&mut self, instr: &RegInstr, args: &[Type]) -> Type {
let name = instr.imm.atom();
let location = match instr.args[0] {
Operand::Reg(_) => {
let holders = args[0].members().iter().map(|holder| match holder {
Type::Ref(holder) => self.cache.memory.field(*holder, name).map(|f| f.addr),
holder => unreachable!("a capture held by {holder:?}"),
});
let mut locations: Vec<Addr> = holders.flatten().collect();
locations.dedup();
assert!(
locations.len() <= 1,
"a capture of one of several locations"
);
locations.pop()
}
slot => Some(self.addr(slot)),
};
// A holder's field can be missing until the walk that makes it gets there
if let Some(location) = location {
for obj in args[1].members() {
if let Type::Ref(obj) = obj {
self.cache.memory.share_field(*obj, name, location);
}
}
}
Type::Unknown
}
/// Operands are the callee's params: the callee (its env), `this`, then the args (see
/// regify). A call of each member of a sum passes that member as the env. A callee that
/// is not a fn throws, so it gives nothing
///
/// `new` gives what the constructor returns if that is an object, else the new object
/// itself, `this`
fn call(&mut self, instr: &RegInstr, idx: usize, args: &[Type]) -> Type {
let call_site = Some(InstrRef {
fn_idx: self.context.fn_idx,
instr_idx: idx,
});
let rets: Vec<Type> = (args[0].members().iter())
.map(|callee| {
// A native takes the args only, without its env and `this`
if let Type::Native(idx) = callee {
return native::ret_type(*idx, &args[FIRST_ARG as usize..]);
}
let Type::Ref(obj) = callee else {
return Type::Unknown;
};
let Some(fn_idx) = self.cache.memory.closure_fn(*obj) else {
return Type::Unknown;
};
let mut call_args = args.to_vec();
call_args[0] = callee.clone();
self.cache.call(fn_idx, call_site, &call_args, self.fns)
})
.collect();
let ret = join_all(rets);
if instr.op != OpCode::CallConstructor {
return ret;
}
join_all(ret.members().iter().map(|ret| match ret {
Type::Ref(_) => ret.clone(),
_ => args[THIS_ARG as usize].clone(),
}))
}
/// Mark the blocks that `block` can go to. A conditional jump whose condition the types
/// decide goes one way only. One whose condition nothing has reached yet goes nowhere yet
fn mark_successors(&mut self, block: &BasicBlock) {
use OpCode::*;
let last = &self.f.code[block.end - 1];
let jumps = match last.op {
IfFalse | IfFalse8 | IfTrue | IfTrue8 => {
match self.cache.memory.get(self.addr(last.args[0])) {
Type::Unknown => return,
cond => cond
.decide(truthiness)
.map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
}
}
_ => None,
};
// succ[0] is the jump target, succ[1] the fall-through
for (i, &succ) in block.succ.iter().enumerate() {
if jumps.is_none_or(|jumps| jumps == (i == 0)) {
self.reachable[succ] = true;
}
}
}
}
/// An array index must be a number. Any other key is a computed field name, which is not
/// supported
fn check_index(index: &Type) {
for member in index.members() {
if !matches!(member, Type::Int | Type::Float) {
unimplemented!("`a[k]` with a key of type {member:?}: computed field names");
}
}
}
/// The global that `get_var` or `put_var` reads or writes (see transform/modules.rs)
fn global(instr: &RegInstr) -> usize {
match instr.imm {
ImmOperand::Global(global) => global as usize,
imm => unreachable!("a global name that is not a global: {imm:?}"),
}
}
+44 -1
View File
@@ -1,6 +1,7 @@
use std::ffi::{c_int, c_void};
use std::ffi::{c_char, c_int, c_void};
pub const JS_TAG_EXCEPTION: i64 = 6;
pub const JS_TAG_MODULE: i64 = -3;
pub const JS_EVAL_TYPE_MODULE: c_int = 1 << 0;
pub const JS_EVAL_FLAG_COMPILE_ONLY: c_int = 1 << 5;
pub const JS_WRITE_OBJ_BYTECODE: c_int = 1 << 0;
@@ -12,6 +13,13 @@ pub struct JSValue {
pub tag: i64,
}
impl JSValue {
/// The pointer of a value that has one, such as a module
pub fn ptr(self) -> *mut c_void {
unsafe { self.u.ptr }
}
}
#[repr(C)]
#[derive(Clone, Copy)]
union JSValueUnion {
@@ -22,6 +30,15 @@ union JSValueUnion {
pub type JSRuntime = c_void;
pub type JSContext = c_void;
pub type JSModuleDef = c_void;
pub type JSModuleLoaderFunc = unsafe extern "C" fn(
ctx: *mut JSContext,
module_name: *const c_char,
opaque: *mut c_void,
) -> *mut JSModuleDef;
pub type JSModuleInitFunc = unsafe extern "C" fn(ctx: *mut JSContext, m: *mut JSModuleDef) -> c_int;
unsafe extern "C" {
pub fn JS_NewRuntime() -> *mut JSRuntime;
@@ -48,6 +65,32 @@ unsafe extern "C" {
pub fn JS_GetException(ctx: *mut JSContext) -> JSValue;
pub fn JS_ThrowReferenceError(ctx: *mut JSContext, fmt: *const c_char, ...) -> JSValue;
pub fn JS_SetModuleLoaderFunc(
rt: *mut JSRuntime,
module_normalize: *const c_void,
module_loader: JSModuleLoaderFunc,
opaque: *mut c_void,
);
pub fn JS_NewCModule(
ctx: *mut JSContext,
name: *const c_char,
func: JSModuleInitFunc,
) -> *mut JSModuleDef;
pub fn JS_AddModuleExport(
ctx: *mut JSContext,
m: *mut JSModuleDef,
name: *const c_char,
) -> c_int;
pub fn JS_NewArray(ctx: *mut JSContext) -> JSValue;
pub fn JS_SetPropertyUint32(ctx: *mut JSContext, obj: JSValue, idx: u32, val: JSValue)
-> c_int;
pub fn JS_ToCStringLen2(
ctx: *mut JSContext,
plen: *mut usize,
+123 -20
View File
@@ -1,38 +1,60 @@
mod ffi;
pub mod parse;
use std::ffi::{CStr, CString};
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use parse::QuickJSBinary;
pub fn compile(src: &[u8], filename: &str) -> QuickJSBinary {
unsafe {
let filename = CString::new(filename).unwrap();
/// Where the compiler gets the modules that QuickJS asks for
pub struct Sources {
pub source: fn(&str) -> Option<Vec<u8>>, // the source of a module, by its name
pub native: &'static str, // the module of the natives, which has no source
pub native_exports: Vec<&'static str>, // its exports
}
/// Compiles `roots`, in order, and every module that they import. QuickJS finds the imports:
/// it names each one from the specifier and the importing module's name, and asks `sources`
/// for any module that it has not loaded (see `load_module`)
///
/// Every module is written in one go, as one array, so all of them share one atom table. The
/// module of the natives is a C module: it has exports but no code, so it is not written
pub fn compile(roots: &[String], sources: Sources) -> QuickJSBinary {
unsafe {
let rt = ffi::JS_NewRuntime();
let ctx = ffi::JS_NewContext(rt);
// QuickJS calls `load_module` while it compiles a module, so the loader is only ever
// reached through this pointer, and never held across a compile
let loader = Box::into_raw(Box::new(Loader {
sources,
loaded: vec![],
}));
ffi::JS_SetModuleLoaderFunc(rt, std::ptr::null(), load_module, loader.cast());
// JS_Eval requires a '\0' after the last byte of the source.
let src_z = [src, &[0]].concat();
for root in roots {
if (*loader).loaded.iter().any(|(name, _)| name == root) {
continue;
}
let obj = ffi::JS_Eval(
ctx,
src_z.as_ptr().cast(),
src.len(),
filename.as_ptr().cast(),
ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE,
);
let source = ((*loader).sources.source)(root);
let source = source.unwrap_or_else(|| panic!("cannot read `{root}`"));
let module = eval_module(ctx, &source, root);
assert!(module.tag != ffi::JS_TAG_EXCEPTION, "{}", exception(ctx));
(*loader).loaded.push((root.clone(), module));
}
assert!(obj.tag != ffi::JS_TAG_EXCEPTION, "{}", get_exception(ctx));
let loader = Box::from_raw(loader);
let modules = ffi::JS_NewArray(ctx);
for (i, (_, module)) in loader.loaded.into_iter().enumerate() {
ffi::JS_SetPropertyUint32(ctx, modules, i as u32, module);
}
let mut len = 0;
let buf = ffi::JS_WriteObject(ctx, &mut len, obj, ffi::JS_WRITE_OBJ_BYTECODE);
ffi::JS_FreeValue(ctx, obj);
assert!(!buf.is_null());
let buf = ffi::JS_WriteObject(ctx, &mut len, modules, ffi::JS_WRITE_OBJ_BYTECODE);
assert!(!buf.is_null(), "{}", exception(ctx));
let bytes = std::slice::from_raw_parts(buf, len).to_vec();
// TODO: Are we sure other cleanup is not necessary?
ffi::JS_FreeValue(ctx, modules);
ffi::JS_FreeContext(ctx);
ffi::JS_FreeRuntime(rt);
@@ -40,7 +62,88 @@ pub fn compile(src: &[u8], filename: &str) -> QuickJSBinary {
}
}
fn get_exception(ctx: *mut ffi::JSContext) -> String {
/// The loader's state, which QuickJS hands back to `load_module`
struct Loader {
sources: Sources,
loaded: Vec<(String, ffi::JSValue)>, // each module that it compiled, by name, in order
}
/// What QuickJS calls for a module that it has not loaded: `name` is already made from the
/// specifier and the importing module's name. The module is compiled, not run. A failure
/// sets an exception and gives null, and QuickJS passes it on
unsafe extern "C" fn load_module(
ctx: *mut ffi::JSContext,
name: *const c_char,
opaque: *mut c_void,
) -> *mut ffi::JSModuleDef {
unsafe {
let loader = opaque.cast::<Loader>();
let text = CStr::from_ptr(name).to_string_lossy().into_owned();
if text == (*loader).sources.native {
return native_module(ctx, name, &(*loader).sources.native_exports);
}
let Some(source) = ((*loader).sources.source)(&text) else {
let fmt = c"cannot find module `%s`".as_ptr();
ffi::JS_ThrowReferenceError(ctx, fmt, name);
return std::ptr::null_mut();
};
let module = eval_module(ctx, &source, &text);
if module.tag == ffi::JS_TAG_EXCEPTION {
return std::ptr::null_mut();
}
(*loader).loaded.push((text, module));
module.ptr()
}
}
/// The module of the natives: a C module, with one export for each native
unsafe fn native_module(
ctx: *mut ffi::JSContext,
name: *const c_char,
exports: &[&str],
) -> *mut ffi::JSModuleDef {
unsafe extern "C" fn init(_: *mut ffi::JSContext, _: *mut ffi::JSModuleDef) -> c_int {
0
}
unsafe {
let module = ffi::JS_NewCModule(ctx, name, init);
for export in exports {
let export = CString::new(*export).unwrap();
ffi::JS_AddModuleExport(ctx, module, export.as_ptr());
}
module
}
}
/// Compile `source` as the module `name`, without running it
unsafe fn eval_module(ctx: *mut ffi::JSContext, source: &[u8], name: &str) -> ffi::JSValue {
// JS_Eval requires a '\0' after the last byte of the source
let source_z = [source, &[0]].concat();
let name = CString::new(name).unwrap();
let flags = ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE;
unsafe {
let module = ffi::JS_Eval(
ctx,
source_z.as_ptr(),
source.len(),
name.as_ptr().cast(),
flags,
);
debug_assert!(module.tag == ffi::JS_TAG_MODULE || module.tag == ffi::JS_TAG_EXCEPTION);
module
}
}
fn exception(ctx: *mut ffi::JSContext) -> String {
unsafe {
let err = ffi::JS_GetException(ctx);
let text = ffi::JS_ToCStringLen2(ctx, std::ptr::null_mut(), err, false);
+109 -56
View File
@@ -1,3 +1,7 @@
// The parser reads every part of the bytecode format, including parts that the compiler does not
// use yet
#![allow(dead_code)]
use std::fmt::Debug;
use std::fmt::Write;
@@ -23,8 +27,8 @@ pub mod opcodes {
pub struct QuickJSBinary {
pub _version: u8,
pub _checksum: u32,
pub atoms: Vec<Atom>,
pub root_mod: Module,
pub atoms: Vec<Atom>, // one table for every module (see `libqjs::compile`)
pub modules: Vec<Module>, // in the order that they were loaded
}
impl QuickJSBinary {
@@ -35,9 +39,16 @@ impl QuickJSBinary {
let (bin, atoms) = count(Atom::parse, num_atoms).parse(bin)?;
let (bin, value) = JsValue::parse(bin)?;
let JsValue::Module(root_mod) = value else {
panic!()
// An array of the program's modules (see `libqjs::compile`)
let JsValue::Array(items) = value else {
panic!("the binary is not an array of modules")
};
let modules = (items.into_iter())
.map(|item| match item {
JsValue::Module(module) => module,
_ => panic!("the binary holds a value that is not a module"),
})
.collect();
Ok((
bin,
@@ -45,7 +56,7 @@ impl QuickJSBinary {
_version: version,
_checksum: checksum,
atoms,
root_mod,
modules,
},
))
}
@@ -134,6 +145,20 @@ impl AtomRef {
}
}
impl AtomRef {
/// The text of the atom. A file's atoms are in `atoms`
pub fn text(self, atoms: &[Atom]) -> String {
match self {
Self::TaggedInt(n) => n.to_string(),
Self::Predefined(n) => atom_names::PREDEFINED_ATOMS[n as usize].to_string(),
Self::FileLocal(i) => match &atoms[i] {
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s.text(),
Atom::Predefined(n) => atom_names::PREDEFINED_ATOMS[*n as usize].to_string(),
},
}
}
}
impl Debug for AtomRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@@ -168,13 +193,9 @@ pub enum JsValue {
_raw: Box<JsValue>,
},
Function(Function),
FnById(usize),
/// A C function, by its index in `native::NATIVES`
FnByIdx(usize),
/// A C function that JS can call, by its index in `native::NATIVES`
NativeFn(usize),
/// Makes its arg the prototype of a primitive kind (see `native`)
ProtoLink(crate::native::ProtoOf),
/// `__link_host_complete`: its arg is what the host calls when an op completes
HostLink,
Module(Module),
TypedArray,
ArrayBuffer,
@@ -262,7 +283,7 @@ impl JsValue {
}
}
#[derive(Debug, TryFromPrimitive)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u16)]
pub enum FuncKind {
Normal = 0,
@@ -277,7 +298,7 @@ pub struct FunctionFlags {
pub _has_simple_param_list: bool,
pub _is_derived_class_constructor: bool,
pub _need_home_object: bool,
pub _func_kind: FuncKind,
pub func_kind: FuncKind,
pub _new_target_allowed: bool,
pub _super_call_allowed: bool,
pub _super_allowed: bool,
@@ -297,7 +318,7 @@ impl FunctionFlags {
_has_simple_param_list: raw & (1 << 1) != 0,
_is_derived_class_constructor: raw & (1 << 2) != 0,
_need_home_object: raw & (1 << 3) != 0,
_func_kind: FuncKind::try_from_primitive((raw >> 4) & 0b11).unwrap(),
func_kind: FuncKind::try_from_primitive((raw >> 4) & 0b11).unwrap(),
_new_target_allowed: raw & (1 << 6) != 0,
_super_call_allowed: raw & (1 << 7) != 0,
_super_allowed: raw & (1 << 8) != 0,
@@ -309,7 +330,7 @@ impl FunctionFlags {
}
}
#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub enum VarKind {
Normal = 0,
@@ -391,7 +412,7 @@ impl VarDef {
}
}
#[derive(Debug, TryFromPrimitive)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub enum ClosureKind {
Local = 0,
@@ -404,7 +425,7 @@ pub enum ClosureKind {
ModuleImport = 7,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ClosureVarFlags {
pub closure_kind: ClosureKind,
pub _is_const: bool,
@@ -434,9 +455,9 @@ impl ClosureVarFlags {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ClosureVar {
pub _name: AtomRef,
pub name: AtomRef,
pub var_idx: usize,
pub flags: ClosureVarFlags,
}
@@ -450,7 +471,7 @@ impl ClosureVar {
Ok((
bin,
Self {
_name: name,
name,
var_idx,
flags,
},
@@ -519,6 +540,9 @@ pub enum ImmOperand {
_target: usize,
_arg: u16,
},
/// A module binding, by its index in the program (see link.rs). Only the compiler makes
/// it, for `get_var` and `put_var`
Global(u32),
}
impl ImmOperand {
@@ -673,7 +697,7 @@ fn parse_bytecode(code: &[u8]) -> IResult<&[u8], Vec<Instr>> {
#[derive(Debug)]
pub struct Function {
pub _flags: FunctionFlags,
pub flags: FunctionFlags,
pub _is_strict_mode: bool,
pub name: AtomRef,
pub arg_count: usize,
@@ -730,7 +754,7 @@ impl Function {
Ok((
bin,
Self {
_flags: flags,
flags,
_is_strict_mode: strict_mode == 1,
name,
arg_count,
@@ -749,13 +773,13 @@ impl Function {
#[derive(Debug)]
pub struct Module {
pub _name: AtomRef,
pub _import_modules: Vec<AtomRef>,
pub _exports: Vec<ExportInfo>,
pub _star_exports: Vec<usize>,
pub _imports: Vec<ImportInfo>,
pub _has_top_level_await: bool,
pub root_fn: Function,
pub name: AtomRef, // the name that it was loaded by (see `libqjs::compile`)
pub requests: Vec<AtomRef>, // the modules that it imports from, as the source writes them
pub exports: Vec<ExportInfo>,
pub star_exports: Vec<usize>, // `export * from`: each an index into `requests`
pub imports: Vec<ImportInfo>,
pub has_top_level_await: bool,
pub root_fn: Function, // the module's body
}
impl Module {
@@ -782,63 +806,76 @@ impl Module {
Ok((
bin,
Self {
_name: name,
_import_modules: import_modules,
_exports: exports,
_star_exports: star_exports,
_imports: imports,
_has_top_level_await: has_tla == 1,
name,
requests: import_modules,
exports,
star_exports,
imports,
has_top_level_await: has_tla == 1,
root_fn,
},
))
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum ExportInfo {
/// All exports except aliases and star exports
Local { _var_idx: usize, _name: AtomRef },
/// Alias exports, has `from`, eg: export { a } from "./dep.js";
Alias,
Local {
var_idx: usize, // a captured var of the module's body: its own binding, or an import
name: AtomRef,
}, // `export { x }`, `export let x`, `export function x`, ...
Indirect {
request: usize, // an index into the module's `requests`
local: AtomRef, // the name that the other module exports
name: AtomRef,
}, // `export { x as y } from "..."`
}
impl ExportInfo {
fn parse(bin: &[u8]) -> IResult<&[u8], Self> {
let (bin, kind) = u8(bin)?;
let ret = match kind {
match kind {
0 => {
let (bin, var_idx) = leb128(bin)?;
let (bin, name) = AtomRef::parse(bin)?;
(bin, Self::Local { _var_idx: var_idx, _name: name })
Ok((bin, Self::Local { var_idx, name }))
}
1 => (bin, Self::Alias),
_ => unreachable!(),
};
Ok(ret)
_ => {
let (bin, request) = leb128(bin)?;
let (bin, local) = AtomRef::parse(bin)?;
let (bin, name) = AtomRef::parse(bin)?;
Ok((
bin,
Self::Indirect {
request,
local,
name,
},
))
}
}
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ImportInfo {
pub _var_idx: usize,
pub _name: AtomRef,
pub _req_module_idx: usize,
pub var_idx: usize, // a captured var of the module's body
pub name: AtomRef, // the name that the other module exports, or `*` for a namespace
pub request: usize, // an index into the module's `requests`
}
impl ImportInfo {
fn parse(bin: &[u8]) -> IResult<&[u8], Self> {
let (bin, var_idx) = leb128(bin)?;
let (bin, name) = AtomRef::parse(bin)?;
let (bin, req_module_idx) = leb128(bin)?;
let (bin, request) = leb128(bin)?;
Ok((
bin,
Self {
_var_idx: var_idx,
_name: name,
_req_module_idx: req_module_idx,
var_idx,
name,
request,
},
))
}
@@ -885,6 +922,22 @@ pub enum JsString {
UTF16(Vec<u8>),
}
impl JsString {
/// The text of the string. A lone surrogate becomes U+FFFD
pub fn text(&self) -> String {
match self {
Self::Latin1(b) => b.iter().map(|&c| c as char).collect(),
Self::UTF16(b) => {
let units: Vec<u16> = b
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&units)
}
}
}
}
impl Debug for JsString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_char('"')?;
+452
View File
@@ -0,0 +1,452 @@
//! Link: what each name in each module refers to, and the program's entry fn
//!
//! Every binding at the top of a module (`let`, `const`, `function`, `class`, ...) is a
//! global: there is one of each for the whole program. A module's body reaches its own
//! bindings and its imports as captured vars, which QuickJS calls module decls and module
//! imports. Link gives each decl a global, and follows each import to the global of the
//! binding that it names (see `Link::resolve`). transform/modules.rs then rewrites the code
//!
//! A name that no module declares, such as `String` or `console`, is a global name: it
//! refers to the export of that name of the prelude, `jsrt:prelude`, as names in JS refer
//! to the global object. A module can declare a binding of the same name, which hides it
//!
//! The natives are the exports of `jsrt:native`, which QuickJS sees as a C module (see
//! `libqjs::compile`)
//!
//! # The entry fn
//!
//! ```js
//! function <entry>() {
//! // Phase 1 (`this` is true): each body only makes its top-level function declarations.
//! // Phase 2: each body runs. Every module's phase 1 comes first, so that modules that
//! // import each other see each other's functions
//! let phase = true;
//! while (true) {
//! for (const body of bodies) body.call(phase);
//! if (!phase) break;
//! phase = false;
//! }
//! __drain_microtasks();
//! }
//! ```
//!
//! Both phases call each body from one call instr. Infer names the objects that a walk makes
//! by its call site, so with two call instrs, each phase would make its own copy of every
//! object at the top of a module
//!
//! The bodies run in ES's order: each module after the modules that it imports, from the
//! prelude, then each module that the command line names. A body that awaits at its top runs
//! until its first await, so only the last body may await (see transform/async.rs)
use std::collections::HashMap;
use crate::{
flatten::{ENTRY_FN, FlatFn, FlatModule},
libqjs::parse::{
Atom, AtomRef, ClosureKind, ClosureVar, ExportInfo, FuncKind, ImmOperand as Imm, Instr,
JsValue, opcodes::OpCode,
},
native::NATIVES,
};
/// The module whose exports are every module's global names
pub const PRELUDE: &str = "jsrt:prelude";
/// The module of the natives
pub const NATIVE: &str = "jsrt:native";
/// What a name refers to
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Target {
Global(usize), // a binding at the top of a module
Native(usize), // a native, by its index in `native::NATIVES`
}
pub struct Link {
pub globals: usize, // how many there are
bodies: HashMap<usize, (String, Vec<Target>)>, // for the fn of each module's body: the module's name, and what each of the body's captured vars refers to
exports: HashMap<String, HashMap<String, Target>>, // for each module, by name: each export, by name
}
impl Link {
/// The name of the module whose body is `f`, if it is a body, and what each captured var
/// of the body refers to
pub fn body(&self, f: usize) -> Option<(&str, &[Target])> {
self.bodies
.get(&f)
.map(|(name, vars)| (&name[..], &vars[..]))
}
/// What the global name `name` refers to: the prelude's export of that name
pub fn global(&self, name: &str) -> Option<Target> {
self.export(PRELUDE, name)
}
/// The export `name` of the module `module`
pub fn export(&self, module: &str, name: &str) -> Option<Target> {
self.exports.get(module)?.get(name).copied()
}
/// The global of an export that the compiler needs from the stdlib
pub fn stdlib(&self, module: &str, name: &str) -> usize {
match self.export(module, name) {
Some(Target::Global(global)) => global,
_ => panic!("the compiler needs `{name}` from `{module}`"),
}
}
}
/// Link the modules, and write the entry fn. `roots` are the modules that the command line
/// names, in order
pub fn link(
modules: &[FlatModule],
roots: &[String],
fns: &mut [FlatFn],
captures: &[Vec<ClosureVar>],
atoms: &[Atom],
) -> Link {
let linker = Linker::new(modules, captures, atoms);
let link = Link {
globals: linker.globals,
bodies: linker.bodies(),
exports: linker.all_exports(),
};
let order = linker.order(roots);
check_top_level_await(&order, modules);
let drain = link.stdlib("jsrt:loop", "__drain_microtasks");
fns[ENTRY_FN] = entry(&order, modules, drain);
link
}
/// A body that awaits returns at its first await, so the bodies after it would run too early.
/// Only the last one may await, until the entry waits for each body's promise
fn check_top_level_await(order: &[usize], modules: &[FlatModule]) {
for &m in &order[..order.len() - 1] {
if modules[m].has_top_level_await {
let name = &modules[m].name;
unimplemented!("{name}: top-level `await` in a module that is not the last to run");
}
}
}
/// The natives, as the exports of `jsrt:native`
fn natives() -> HashMap<String, Target> {
let natives = NATIVES.iter().enumerate();
natives
.map(|(idx, native)| (native.name.to_string(), Target::Native(idx)))
.collect()
}
struct Linker<'a> {
modules: &'a [FlatModule],
captures: &'a [Vec<ClosureVar>],
atoms: &'a [Atom],
index: HashMap<String, usize>, // each module, by name
decls: Vec<HashMap<usize, usize>>, // for each module: the global of each of its body's decls, by captured var
globals: usize,
}
impl<'a> Linker<'a> {
/// The linker, with a global for each decl of each module's body
fn new(modules: &'a [FlatModule], captures: &'a [Vec<ClosureVar>], atoms: &'a [Atom]) -> Self {
let index = modules.iter().enumerate();
let mut linker = Linker {
modules,
captures,
atoms,
index: index.map(|(m, module)| (module.name.clone(), m)).collect(),
decls: vec![],
globals: 0,
};
linker.add_globals();
linker
}
/// For each module's body: the module's name, and what each captured var refers to
fn bodies(&self) -> HashMap<usize, (String, Vec<Target>)> {
let bodies = self.modules.iter().enumerate();
bodies
.map(|(m, module)| (module.body, (module.name.clone(), self.body_vars(m))))
.collect()
}
/// Every module's exports, by the module's name, with the natives' module
fn all_exports(&self) -> HashMap<String, HashMap<String, Target>> {
let modules = self.modules.iter().enumerate();
let exports =
modules.map(|(m, module)| (module.name.clone(), self.exports(m, &mut vec![])));
exports.chain([(NATIVE.to_string(), natives())]).collect()
}
}
impl Linker<'_> {
/// A global for each decl of each module's body. QuickJS makes a namespace import a decl
/// too, whose object its own linker makes at run time, so that is rejected here
fn add_globals(&mut self) {
for module in self.modules {
let atoms = self.atoms;
let namespace = module
.imports
.iter()
.find(|import| import.name.text(atoms) == "*");
if namespace.is_some() {
unimplemented!("{}: `import * as` needs namespace objects", module.name);
}
let vars = self.captures[module.body].iter().enumerate();
let decls = vars.filter(|(_, var)| var.flags.closure_kind == ClosureKind::ModuleDecl);
let mut globals = HashMap::new();
for (i, _) in decls {
globals.insert(i, self.globals);
self.globals += 1;
}
self.decls.push(globals);
}
}
/// What each captured var of module `m`'s body refers to
fn body_vars(&self, m: usize) -> Vec<Target> {
(0..self.captures[self.modules[m].body].len())
.map(|i| self.var(m, i, &mut vec![]))
.collect()
}
/// What captured var `i` of module `m`'s body refers to: its global if it is a decl, else
/// the export that it imports. `seen` guards against imports that go round in a circle
fn var(&self, m: usize, i: usize, seen: &mut Vec<(usize, usize)>) -> Target {
let module = &self.modules[m];
if let Some(&global) = self.decls[m].get(&i) {
return Target::Global(global);
}
assert!(
!seen.contains(&(m, i)),
"{}: an import refers back to itself",
module.name
);
seen.push((m, i));
let import = module.imports.iter().find(|import| import.var_idx == i);
let import =
import.unwrap_or_else(|| panic!("{}: a module var with no import", module.name));
let name = import.name.text(self.atoms);
let from = self.request(m, import.request);
match self.resolve(&from, &name, seen) {
Some(target) => target,
None => panic!("{}: `{from}` has no export `{name}`", module.name),
}
}
/// ES's ResolveExport: what export `name` of module `module` refers to
fn resolve(&self, module: &str, name: &str, seen: &mut Vec<(usize, usize)>) -> Option<Target> {
if module == NATIVE {
return natives().get(name).copied();
}
let Some(&m) = self.index.get(module) else {
panic!("no module `{module}`");
};
for export in &self.modules[m].exports {
match export {
ExportInfo::Local { var_idx, name: n } if n.text(self.atoms) == name => {
return Some(self.var(m, *var_idx, seen));
}
ExportInfo::Indirect {
request,
local,
name: n,
} if n.text(self.atoms) == name => {
return self.resolve(&self.request(m, *request), &local.text(self.atoms), seen);
}
_ => {}
}
}
// `export *` passes on every export but `default`
if name == "default" {
return None;
}
let mut stars = self.modules[m].star_exports.iter();
stars.find_map(|&request| self.resolve(&self.request(m, request), name, seen))
}
/// Every export of module `m`, by name. `seen` guards against `export *` in a circle
fn exports(&self, m: usize, seen: &mut Vec<usize>) -> HashMap<String, Target> {
let module = &self.modules[m];
let mut exports = HashMap::new();
if seen.contains(&m) {
return exports;
}
seen.push(m);
for &request in &module.star_exports {
let from = self.request(m, request);
let passed = match self.index.get(&from) {
Some(&other) => self.exports(other, seen),
None if from == NATIVE => natives(),
None => panic!("no module `{from}`"),
};
for (name, target) in passed {
if name != "default" {
exports.insert(name, target);
}
}
}
for export in &module.exports {
let (ExportInfo::Local { name, .. } | ExportInfo::Indirect { name, .. }) = export;
let name = name.text(self.atoms);
let target = self.resolve(&module.name, &name, &mut vec![]);
exports.insert(name, target.unwrap());
}
exports
}
/// The name of request `r` of module `m`. QuickJS made the same name when it loaded it
/// (see `normalize`)
fn request(&self, m: usize, r: usize) -> String {
let module = &self.modules[m];
normalize(&module.name, &module.requests[r])
}
/// The order that the bodies run in: each module after the modules that it imports, from
/// the prelude, then each root
fn order(&self, roots: &[String]) -> Vec<usize> {
let mut order = vec![];
for root in [PRELUDE.to_string()].iter().chain(roots) {
self.visit(self.index[root], &mut order);
}
order
}
fn visit(&self, m: usize, order: &mut Vec<usize>) {
if order.contains(&m) {
return;
}
order.push(m);
let at = order.len() - 1;
for r in 0..self.modules[m].requests.len() {
if let Some(&other) = self.index.get(&self.request(m, r)) {
self.visit(other, order);
}
}
// After its imports. It was marked first, so that a circle of imports ends
let m = order.remove(at);
order.push(m);
}
}
/// The name of the module that `specifier` names, in the module `base`. This is QuickJS's
/// default rule, which QuickJS used to load it: a name that does not start with `.` is as it
/// is. Else it is relative to the folder of `base`, and its leading `./` and `../` are taken
/// out
fn normalize(base: &str, specifier: &str) -> String {
if !specifier.starts_with('.') {
return specifier.to_string();
}
let mut folder = match base.rfind('/') {
Some(slash) => base[..slash].to_string(),
None => String::new(),
};
let mut rest = specifier;
loop {
if let Some(after) = rest.strip_prefix("./") {
rest = after;
} else if let Some(after) = rest.strip_prefix("../") {
let last = folder.rfind('/').map_or(0, |slash| slash + 1);
if folder.is_empty() || matches!(&folder[last..], "." | "..") {
break;
}
folder.truncate(last.saturating_sub(1));
rest = after;
} else {
break;
}
}
match folder.is_empty() {
true => rest.to_string(),
false => format!("{folder}/{rest}"),
}
}
/// The entry fn (see the module doc). Local `k` holds the closure of the `k`th body to run,
/// and the last local is the phase
fn entry(order: &[usize], modules: &[FlatModule], drain: usize) -> FlatFn {
use OpCode::*;
let n = order.len();
let phase = Imm::Loc(n as u16);
let body = |k: usize| Imm::Loc(k as u16);
// The loop starts after the closures and the phase, and ends after a call of each body
// and the phase's test
let head = 2 * n + 2;
let end = head + 4 * n + 5;
let make = (0..n).flat_map(|k| [(Fclosure, Imm::Const(k as u32)), (PutLoc, body(k))]);
let call = (0..n).flat_map(|k| {
[
(GetLoc, phase),
(GetLoc, body(k)),
(CallMethod, Imm::Npop(0)),
(Drop, Imm::None),
]
});
let code: Vec<(OpCode, Imm)> = make
.chain([(PushTrue, Imm::None), (PutLoc, phase)])
.chain(call)
.chain([
(GetLoc, phase),
(IfFalse, Imm::Label { target: end }),
(PushFalse, Imm::None),
(PutLoc, phase),
(Goto, Imm::Label { target: head }),
(GetVar, Imm::Global(drain as u32)),
(Call0, Imm::None),
(Drop, Imm::None),
(ReturnUndef, Imm::None),
])
.collect();
let code = code.into_iter().enumerate();
let code = code.map(|(pc, (op, imm))| Instr { pc, op, imm }).collect();
let bodies = order
.iter()
.map(|&m| JsValue::FnByIdx(modules[m].body))
.collect();
FlatFn {
name: AtomRef::Predefined(0),
kind: FuncKind::Normal,
arg_count: 0,
local_count: n + 1,
cpool: bodies,
code,
}
}
+83 -52
View File
@@ -2,36 +2,59 @@ mod emit;
mod flatten;
mod infer;
mod libqjs;
mod link;
mod native;
mod regify;
mod transform;
use std::fs::File;
use std::io::Read;
use libqjs::{Sources, compile};
use link::{NATIVE, PRELUDE};
use native::NATIVES;
use flatten::flatten;
use infer::infer_types;
use libqjs::compile;
use regify::regify;
use transform::transform;
/// 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"),
/// The stdlib's modules, by name. The prelude's exports are every module's global names (see
/// link.rs)
const STDLIB: &[(&str, &[u8])] = &[
(
"jsrt:prelude",
include_bytes!("../../lib/stdlib/prelude.js"),
),
(
"jsrt:globals",
include_bytes!("../../lib/stdlib/globals.js"),
),
("jsrt:object", include_bytes!("../../lib/stdlib/object.js")),
("jsrt:string", include_bytes!("../../lib/stdlib/string.js")),
("jsrt:number", include_bytes!("../../lib/stdlib/number.js")),
(
"jsrt:boolean",
include_bytes!("../../lib/stdlib/boolean.js"),
),
("jsrt:array", include_bytes!("../../lib/stdlib/array.js")),
("jsrt:loop", include_bytes!("../../lib/stdlib/loop.js")),
(
"jsrt:promise",
include_bytes!("../../lib/stdlib/promise.js"),
),
(
"jsrt:coroutine",
include_bytes!("../../lib/stdlib/coroutine.js"),
),
("jsrt:timers", include_bytes!("../../lib/stdlib/timers.js")),
("jsrt:io", include_bytes!("../../lib/stdlib/io.js")),
(
"jsrt:console",
include_bytes!("../../lib/stdlib/console.js"),
),
];
/// Runs after the program
const STDLIB_END: &[u8] = include_bytes!("../../lib/stdlib/end.js");
/// The source of a module: a stdlib module by its name, else a file by its path
fn source(name: &str) -> Option<Vec<u8>> {
match STDLIB.iter().find(|(n, _)| *n == name) {
Some((_, source)) => Some(source.to_vec()),
None if name.starts_with("jsrt:") => None,
None => std::fs::read(name).ok(),
}
}
/// The runtime. The emitted C includes `jsrt.h`, which includes the rest
const RUNTIME: &[(&str, &[u8])] = &[
@@ -39,41 +62,42 @@ const RUNTIME: &[(&str, &[u8])] = &[
("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
let args = std::env::args().collect::<Vec<String>>();
let filename = args.get(1).unwrap();
// `compiler <file.js>...`: each file is a module, and they run in order. A file's imports
// are found from it. Each is named by its full path, so that its relative imports name
// their files the same way from any folder
let files: Vec<String> = (std::env::args().skip(1))
.map(|file| match std::fs::canonicalize(&file) {
Ok(path) => path.to_string_lossy().into_owned(),
Err(err) => panic!("cannot read `{file}`: {err}"),
})
.collect();
let mut file = File::open(filename).unwrap();
let metadata = std::fs::metadata(filename).unwrap();
assert!(metadata.is_file());
assert!(!files.is_empty(), "usage: compiler <file.js>...");
// 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);
// Compile every module to bytecode: the prelude first, then the files
let roots: Vec<String> = [PRELUDE.to_string()]
.into_iter()
.chain(files.clone())
.collect();
let sources = Sources {
source,
native: NATIVE,
native_exports: NATIVES.iter().map(|native| native.name).collect(),
};
let bin = compile(&roots, sources);
// Process it
let (flat_fns, captures, atoms) = flatten(binary);
let flat_fns = transform(flat_fns, &captures, &atoms);
let reg_fns = regify(flat_fns);
let ty_cache = infer_types(&reg_fns);
let c = emit::emit(&reg_fns, &ty_cache, &atoms);
let (mut flat_fns, captures, atoms, modules) = flatten::flatten(bin);
let link = link::link(&modules, &files, &mut flat_fns, &captures, &atoms);
let flat_fns = transform::transform(flat_fns, captures, &atoms, &link);
let fns = regify::regify(flat_fns);
let cache = infer::infer_types(&fns, &link);
let c = emit::emit(&fns, &cache, &atoms, &link);
// Compile the C, with the runtime, to `a.out`
let dir = std::env::temp_dir().join(format!("jsrt-{}", std::process::id()));
@@ -92,11 +116,18 @@ fn main() {
.cargo_metadata(false)
.get_compiler();
// Each fn and global in a section of its own, so that the linker drops the
// runtime's that the program never uses
// Each fn and global in a section of its own, so that the linker drops the runtime's
// that the program never uses. Int math wraps on overflow (`-fwrapv`), not undefined
let status = cc
.to_command()
.args(["-std=c11", "-w", "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections"])
.args([
"-std=c11",
"-w",
"-fwrapv",
"-ffunction-sections",
"-fdata-sections",
"-Wl,--gc-sections",
])
.arg(dir.join("program.c"))
.args(["-o", "a.out", "-lm"])
.status()
+73 -156
View File
@@ -1,181 +1,98 @@
//! What the stdlib (`lib/stdlib/`) can reach below JS: the globals it may
//! read, and nothing else may
//! The C fns that JS can call
//!
//! Natives are the common layer of operations on primitive values. The stdlib
//! builds the JS-visible functions and methods on top of them, and the
//! compiler's own code for operators (`+` on strings and so on) uses them
//! directly
//!
//! Proto links tell the compiler which object is the prototype of a primitive
//! kind. A field read on a primitive borrows its kind's prototype
//! A native is a C fn of the runtime (lib/rt). The natives are the exports of the module
//! `jsrt:native`, which the stdlib imports them from (see link.rs). Its prefix tells where it
//! comes from (see lib/rt/jsrt.h):
//! - `__native_*`: the platform's IO, which another platform's runtime replaces
//! - `__rt_*`: the same on every platform: strings and numbers
use crate::{
infer::types::{BaseType, Type},
libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS},
};
use crate::infer::Type;
/// A native C function callable by JS
///
/// A native is monomorphic: it takes one kind for each arg, and returns one
/// fixed type. Anything more general is written in JS, in the stdlib, on top
/// of natives
/// A native: one set of param types, and one return type
pub struct NativeFn {
pub name: &'static str,
pub args: &'static [Type],
pub ret: Type,
pub arg_types: &'static [Type],
pub ret_type: Type,
}
macro_rules! native {
($name:literal ( $($arg:ident),* ) -> $ret:ident) => {
NativeFn {
name: $name,
args: &[$(ty!($arg)),*],
ret: ty!($ret),
}
macro_rules! ty {
(Undefined) => {
Type::Undefined
};
(Int) => {
Type::Int
};
(Float) => {
Type::Float
};
(String) => {
Type::Str(None)
};
}
/// A native's param or return type: any value of its kind
macro_rules! ty {
(Undefined) => { Type::Undefined };
(Int) => { Type::Int };
(Float) => { Type::Float };
(String) => { Type::Str(None) };
macro_rules! natives {
($(fn $name:ident( $($arg:ident),* ) -> $ret:ident;)*) => {
&[
$(
NativeFn {
name: stringify!($name),
arg_types: &[$(ty!($arg)),*],
ret_type: ty!($ret),
}
),*
]
};
}
#[rustfmt::skip]
pub const NATIVES: &[NativeFn] = &[
// IO
native!("__native_print_string"(String) -> Undefined),
/// All native functions accessible to JS
pub const NATIVES: &[NativeFn] = natives! {
// Output, at once (see lib/rt/io.h)
fn __native_print_string(String) -> Undefined;
// 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_status"(Int) -> Int),
native!("__native_io_result_string"(Int) -> String),
native!("__native_io_result_number"(Int) -> Float),
// Async IO (see lib/rt/io.h). A queue gives the op's handle. The getters read the result
// of the op that the host gives to `__complete`, while that runs
fn __native_io_queue_timeout(Float) -> Int;
fn __native_io_queue_open(String, Int, Int) -> Int;
fn __native_io_queue_read(Int, Int, Float) -> Int;
fn __native_io_queue_write(Int, String, Float) -> Int;
fn __native_io_queue_close(Int) -> Int;
fn __native_io_queue_stat(String) -> Int;
fn __native_io_queue_cancel(Int) -> Int;
fn __native_io_status(Int) -> Int;
fn __native_io_result_string(Int) -> String;
fn __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),
// Numbers (see lib/rt/number.h)
fn __rt_number_to_string(Float) -> String;
fn __rt_number_to_fixed(Float, Int) -> String;
fn __rt_string_to_number(String) -> Float;
// Strings. Indices and lengths count UTF-16 code units, as in JS
native!("__native_string_concat"(String, String) -> String),
native!("__native_string_slice"(String, Int, Int) -> String),
native!("__native_string_char_code_at"(String, Int) -> Int),
native!("__native_string_from_char_code"(Int) -> String),
];
// Strings (see lib/rt/str.h). Indices and lengths count UTF-16 code units, as in JS
fn __rt_string_concat(String, String) -> String;
fn __rt_string_slice(String, Int, Int) -> String;
fn __rt_string_char_code_at(String, Int) -> Int;
fn __rt_string_from_char_code(Int) -> String;
};
/// 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 ProtoOf {
String,
Number,
Boolean,
Array,
/// Whether a value of type `arg` can be passed for a param of type `param`. JS has one number
/// type, so an int and a float both pass for a number
pub fn can_pass(arg: &Type, param: &Type) -> bool {
arg.base() == param.base()
}
impl ProtoOf {
pub const ALL: [ProtoOf; 4] = [ProtoOf::String, ProtoOf::Number, ProtoOf::Boolean, ProtoOf::Array];
/// 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, ProtoOf)] = &[
("__link_proto_string", ProtoOf::String),
("__link_proto_number", ProtoOf::Number),
("__link_proto_boolean", ProtoOf::Boolean),
("__link_proto_array", ProtoOf::Array),
];
/// `__link_host_complete(f)` makes `f` what the host calls when an IO op
/// completes, with the op's handle (see `jsrt_complete` in `lib/rt/jsrt.h`).
/// The host owns the event loop: a flavor of the runtime runs it, and the
/// program only queues ops and handles their completions
pub const HOST_LINK: &str = "__link_host_complete";
/// The value of the global `name`. The only globals are the ones the stdlib
/// may read, anything else is an error
pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue {
let name = atom_name(name, atoms);
// A global property in JS, but a constant
if name == "undefined" {
return JsValue::Undefined;
}
if let Some(idx) = NATIVES.iter().position(|n| name == n.name) {
return JsValue::NativeFn(idx);
}
if name == HOST_LINK {
return JsValue::HostLink;
}
match PROTO_LINKS.iter().find(|(link, _)| name == *link) {
Some(&(_, kind)) => JsValue::ProtoLink(kind),
None => panic!("unknown global `{name}`"),
}
}
/// What a call of native `idx` returns, with args of these types, in source
/// order. An arg that cannot be of its param's kind makes the call throw, so
/// the call gives nothing. An arg of a sum is checked at runtime
/// What a call of native `idx` gives, with args of these types, in source order. An arg that
/// cannot be of its param's type makes the call throw, so the call gives nothing. A sum is
/// checked at runtime
pub fn ret_type(idx: usize, args: &[Type]) -> Type {
let native = &NATIVES[idx];
let missing = std::iter::repeat(&Type::Undefined);
let can_pass = |(param, arg): (&Type, &Type)| arg.members().iter().any(|a| a.base() == param.base());
match native.args.iter().zip(args.iter().chain(missing)).all(can_pass) {
true => native.ret.clone(),
false => Type::Unknown,
}
}
for (i, param) in native.arg_types.iter().enumerate() {
let arg = args.get(i).unwrap_or(&Type::Undefined);
pub fn atom_name(name: AtomRef, atoms: &[Atom]) -> String {
let js_string = match name {
AtomRef::FileLocal(idx) => match &atoms[idx] {
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s,
Atom::Predefined(n) => return PREDEFINED_ATOMS[*n as usize].to_string(),
},
AtomRef::Predefined(n) => return PREDEFINED_ATOMS[n as usize].to_string(),
AtomRef::TaggedInt(n) => return n.to_string(),
};
js_string_text(js_string)
}
pub fn js_string_text(js_string: &JsString) -> String {
// TODO: Avoid string alloc
match js_string {
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
JsString::UTF16(b) => {
let u: Vec<u16> = b
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&u)
if !arg.members().iter().any(|a| can_pass(a, param)) {
return Type::Unknown;
}
}
native.ret_type.clone()
}
+99 -347
View File
@@ -1,25 +1,34 @@
mod basic_block;
mod walk;
use crate::{
flatten::FlatFn,
libqjs::parse::{ImmOperand, Instr, JsValue, opcodes::OpCode},
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
};
pub use basic_block::BasicBlock;
use basic_block::find_basic_blocks;
use walk::walk_block;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Operand {
Local(usize),
Arg(usize),
Reg(usize),
}
#[derive(Debug, Clone)]
pub struct RegInstr {
pub dsts: Vec<usize>,
pub dsts: Vec<Operand>,
pub op: OpCode,
pub imm: ImmOperand,
pub args: Vec<usize>,
pub args: Vec<Operand>,
}
impl RegInstr {
fn nop() -> Self {
Self {
dsts: vec![], // TODO: do we ever have more than one dst?
dsts: vec![],
op: OpCode::Nop,
imm: ImmOperand::None,
args: vec![],
@@ -30,117 +39,101 @@ impl RegInstr {
#[derive(Debug)]
pub struct RegFn {
pub arg_count: usize,
pub var_count: usize,
/// Locals and args are not renamed: local `k` is value id `k`, and arg `k`
/// is value id `var_count + k`.
pub val_count: usize,
pub local_count: usize,
pub reg_count: usize,
pub cpool: Vec<JsValue>,
pub code: Vec<RegInstr>,
pub blocks: Vec<BasicBlock>,
}
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
flat_fns.into_iter().map(RegFn::from_flat).collect()
}
impl RegFn {
/// The locals and args that a closure shares: the locations of its `define_field_ref`s
/// (see transform/closure.rs)
pub fn captured_slots(&self) -> Vec<Operand> {
let refs = self.code.iter().filter(|i| i.op == OpCode::DefineFieldRef);
let mut slots: Vec<Operand> = refs
.map(|i| i.args[0])
.filter(|slot| !matches!(slot, Operand::Reg(_)))
.collect();
slots.sort_unstable();
slots.dedup();
slots
}
/// Joins: the stack that a block gets at entry is new regs, made when the walk first gets
/// to the block. Each predecessor copies its own stack into them, in its postfix. So every
/// block has one entry stack, whichever way control comes in (this does the job of phi
/// nodes in SSA)
///
/// A block with two successors copies into the entry regs of both, before its jump. The
/// copies for the edge that is not taken do no harm: a block's entry regs are only read
/// in that block, and every way into it writes them first
fn from_flat(f: FlatFn) -> Self {
let mut blocks = find_basic_blocks(&f.code);
let mut out = vec![RegInstr::nop(); f.code.len()];
let mut next_reg = 0;
// Reserve values for locals, then args
let mut next_val = f.var_count + f.arg_count;
// The stack each block recieves at entry from its predecessors
let mut entry_stacks: Vec<Option<Vec<usize>>> = vec![None; blocks.len()];
// How many blocks lead to each block
let mut n_preds = vec![0; blocks.len()];
blocks.iter().flat_map(|b| &b.succ).for_each(|&s| n_preds[s] += 1);
// Walk blocks in CFG order, starting from block 0
let mut worklist = vec![];
let mut entry_stacks: Vec<Option<Vec<Operand>>> = vec![None; blocks.len()];
entry_stacks[0] = Some(vec![]);
worklist.push(0);
while let Some(block_idx) = worklist.pop() {
let mut stack = entry_stacks[block_idx].clone().unwrap_or_default();
// Walk blocks in CFG order, starting from block 0. A block that the walk never
// gets to is dead code, and stays `nop`
let mut worklist = vec![0];
walk_block(
&blocks[block_idx],
&f.code,
f.var_count,
&mut stack,
&mut next_val,
&mut out,
);
while let Some(b) = worklist.pop() {
let mut stack = entry_stacks[b].clone().unwrap();
walk_block(&blocks[b], &f.code, &mut stack, &mut next_reg, &mut out);
for i in 0..blocks[block_idx].succ.len() {
let succ_idx = blocks[block_idx].succ[i];
for s in blocks[b].succ.clone() {
let entry = entry_stacks[s].get_or_insert_with(|| {
worklist.push(s);
stack.iter().map(|_| fresh_reg(&mut next_reg)).collect()
});
// A join's stack slots are new values, which every block that
// leads to it copies its own values into. Naming a slot after
// one predecessor's value would give that value a second
// meaning (this does the job of phi nodes in SSA)
let succ_stack = match &entry_stacks[succ_idx] {
Some(succ_stack) => succ_stack.clone(),
None => {
let succ_stack: Vec<usize> = match n_preds[succ_idx] {
1 => stack.clone(),
_ => stack.iter().map(|_| fresh_val(&mut next_val)).collect(),
};
assert_eq!(
entry.len(),
stack.len(),
"a join of stacks of different depths"
);
entry_stacks[succ_idx] = Some(succ_stack.clone());
worklist.push(succ_idx);
succ_stack
}
};
let copies = stack
.iter()
.zip(entry.iter())
.filter(|(src, dst)| src != dst);
let copies = edge_copies(&stack, &succ_stack);
if copies.is_empty() {
continue;
}
// The copies belong to this one edge. A block that can go
// elsewhere too gets them on a block of their own on this
// edge, so the other edge does not run them
if blocks[block_idx].succ.len() == 1 {
blocks[block_idx].postfix.extend(copies);
} else {
let at = blocks[succ_idx].start;
blocks.push(BasicBlock {
start: at,
end: at,
succ: vec![succ_idx],
postfix: copies,
for (&src, &dst) in copies {
blocks[b].postfix.push(RegInstr {
dsts: vec![dst],
op: OpCode::SetLoc,
imm: ImmOperand::None,
args: vec![src],
});
blocks[block_idx].succ[i] = blocks.len() - 1;
}
}
// The copies run one after the other, so no copy may write a reg that a later
// copy reads. A block's stack only holds its own entry regs and the regs it made,
// so that could only be a block that jumps to itself and moves its stack around
let postfix = &blocks[b].postfix;
assert!(
postfix
.iter()
.all(|c| postfix.iter().all(|d| c.args[0] != d.dsts[0])),
"the copies of a join overlap"
);
}
// A define_field into an object that no op of this fn made (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 into an
// object before it goes anywhere, and its field is never absent (see
// `Cell::write()` in 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;
}
}
mark_puts(&mut out);
Self {
arg_count: f.arg_count,
var_count: f.var_count,
val_count: next_val,
local_count: f.local_count,
reg_count: next_reg,
cpool: f.cpool,
code: out,
blocks,
@@ -148,265 +141,24 @@ impl RegFn {
}
}
/// Walk a basic block's instructions, converting each instr to register form
fn walk_block(
block: &BasicBlock,
code: &[Instr],
var_count: usize,
stack: &mut Vec<usize>,
next_val: &mut usize,
out: &mut [RegInstr],
) {
let mut fresh_val = || fresh_val(next_val);
fn fresh_reg(next_reg: &mut usize) -> Operand {
let r = *next_reg;
*next_reg += 1;
Operand::Reg(r)
}
for instr_idx in block.start..block.end {
let instr = &code[instr_idx];
/// A define_field into an object that no instr of this fn made (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 into an object before it goes anywhere, and its field is never absent
fn mark_puts(code: &mut [RegInstr]) {
let made: Vec<Operand> = (code.iter())
.filter(|i| matches!(i.op, OpCode::Object | OpCode::Fclosure | OpCode::Fclosure8))
.map(|i| i.dsts[0])
.collect();
let mut out_instr = RegInstr {
dsts: vec![],
op: instr.op,
imm: instr.imm,
args: vec![],
};
let info = instr.op.info();
let n_pop = info.n_pop as usize + extra_pops(instr);
let n_push = info.n_push as usize;
// Handle this instr's pops
for _ in 0..n_pop {
let v = stack.pop().unwrap();
out_instr.args.push(v);
for instr in code {
if instr.op == OpCode::DefineField && !made.contains(&instr.args[1]) {
instr.op = OpCode::PutField;
}
if matches!(
instr.op,
OpCode::Call | OpCode::CallMethod | OpCode::CallConstructor
) {
out_instr.args = call_operands(instr.op, out_instr.args);
}
// Locals and args are not renamed, so their read/writes need special handling
// Handle read of a local or arg
if let Some(var) = is_var_read(instr, var_count) {
let v = fresh_val();
stack.push(v);
out_instr.op = OpCode::SetLoc;
out_instr.imm = ImmOperand::None;
out_instr.dsts.push(v);
out_instr.args.push(var);
out[instr_idx] = out_instr;
continue;
}
// Handle write of a local or arg value
if let Some(var) = is_var_write(instr, var_count) {
out_instr.op = OpCode::SetLoc;
out_instr.imm = ImmOperand::None;
out_instr.dsts.push(var);
// set_loc also leaves the value it stored on the stack.
if n_push == 1 {
stack.push(out_instr.args[0]); // args set by previos pops loop
}
out[instr_idx] = out_instr;
continue;
}
// Some more special cases:
// set_home_object binds [[HomeObject]], which only `super` reads,
// `super` is unsupported currently, It returns both its operands unchanged
if instr.op == OpCode::SetHomeObject {
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;
}
// 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]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
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]);
out_instr.op = OpCode::Nop;
out_instr.imm = ImmOperand::None;
out[instr_idx] = out_instr;
continue;
}
// define_field leaves the object it wrote on the stack: the same value,
// not a new one
if instr.op == OpCode::DefineField {
stack.push(out_instr.args[1]);
out[instr_idx] = out_instr;
continue;
}
// Normal case: handle this instr's pushes
for _ in 0..n_push {
let v = fresh_val();
stack.push(v);
out_instr.dsts.push(v);
}
out[instr_idx] = out_instr;
}
}
/// Some instrs have extra pops not reflected by instr.op.info().n_pop
fn extra_pops(instr: &Instr) -> usize {
match instr.imm {
ImmOperand::Npop(npop) => npop as usize,
ImmOperand::NpopU16 { npop, .. } => npop as usize,
_ => 0,
}
}
/// Put a call's popped operands in the order of the callee's params: the
/// callee (which is its env), `this`, then the args in source order
///
/// `call_method` has `this` at the bottom of its frame, below the callee.
/// `call_constructor` has the callee at the bottom and `this` above it (see
/// transform). `call` has `this` on top, where transform pushed it
fn call_operands(op: OpCode, mut popped: Vec<usize>) -> Vec<usize> {
let (callee, this) = match op {
OpCode::CallMethod => {
let this = popped.pop().unwrap();
(popped.pop().unwrap(), this)
}
OpCode::CallConstructor => {
let callee = popped.pop().unwrap();
(callee, popped.pop().unwrap())
}
_ => {
let callee = popped.pop().unwrap();
(callee, popped.remove(0))
}
};
// What remains are the args, last arg first
popped.reverse();
[callee, this].into_iter().chain(popped).collect()
}
/// The value id of the local or arg that `instr` reads, if any.
pub fn is_var_read(instr: &Instr, var_count: usize) -> Option<usize> {
let arg = |k: usize| Some(var_count + k);
match instr.op {
OpCode::GetLoc0 => Some(0),
OpCode::GetLoc1 => Some(1),
OpCode::GetLoc2 => Some(2),
OpCode::GetLoc3 => Some(3),
OpCode::GetLoc | OpCode::GetLoc8 | OpCode::GetLocCheck => match instr.imm {
ImmOperand::Loc(k) => Some(k as usize),
_ => unreachable!(),
},
OpCode::GetArg0 => arg(0),
OpCode::GetArg1 => arg(1),
OpCode::GetArg2 => arg(2),
OpCode::GetArg3 => arg(3),
OpCode::GetArg => match instr.imm {
ImmOperand::Arg(k) => arg(k as usize),
_ => unreachable!(),
},
_ => None,
}
}
/// The value id of the local or arg that `instr` writes, if any.
pub fn is_var_write(instr: &Instr, var_count: usize) -> Option<usize> {
let arg = |k: usize| Some(var_count + k);
match instr.op {
OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0),
OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1),
OpCode::PutLoc2 | OpCode::SetLoc2 => Some(2),
OpCode::PutLoc3 | OpCode::SetLoc3 => Some(3),
OpCode::PutLoc
| OpCode::PutLoc8
| OpCode::PutLocCheck
| OpCode::PutLocCheckInit
| OpCode::SetLoc
| OpCode::SetLoc8 => match instr.imm {
ImmOperand::Loc(k) => Some(k as usize),
_ => unreachable!(),
},
OpCode::PutArg0 | OpCode::SetArg0 => arg(0),
OpCode::PutArg1 | OpCode::SetArg1 => arg(1),
OpCode::PutArg2 | OpCode::SetArg2 => arg(2),
OpCode::PutArg3 | OpCode::SetArg3 => arg(3),
OpCode::PutArg | OpCode::SetArg => match instr.imm {
ImmOperand::Arg(k) => arg(k as usize),
_ => unreachable!(),
},
_ => None,
}
}
/// The copies that carry the values on a block's stack into its successor's
/// entry stack
fn edge_copies(stack: &[usize], succ_stack: &[usize]) -> Vec<RegInstr> {
let overlap = stack.iter().rev().zip(succ_stack.iter().rev());
overlap
.filter(|(val, succ_val)| val != succ_val)
.map(|(&val, &succ_val)| RegInstr {
dsts: vec![succ_val],
op: OpCode::SetLoc,
imm: ImmOperand::None,
args: vec![val],
})
.collect()
}
fn fresh_val(next_val: &mut usize) -> usize {
let v = *next_val;
*next_val += 1;
v
}
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
flat_fns.into_iter().map(RegFn::from_flat).collect()
}
+322
View File
@@ -0,0 +1,322 @@
//! This module converts one basic block's instrs from stack to register form using a simulated stack
//!
//! The output reg form instrs are structured as:
//! `dsts = op(imm, args)`
//!
//! Every instr's pushes allocates new register operands which are pushed on the stack and form the
//! output instr's `dsts`, the instr's pops pop operands from the stack and form the output instr's
//! `args`
//!
//! Each instr's `op` and `imm` is preserved except for some special cases
//!
//! `set_loc` is henceforth used to move data from one operand (local, arg, reg) to another
#![allow(non_snake_case)]
use super::{BasicBlock, Operand, RegInstr};
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
/// Walk a basic block's instructions, converting each instr to register form
pub fn walk_block(
block: &BasicBlock,
code: &[Instr],
stack: &mut Vec<Operand>,
next_reg: &mut usize,
out: &mut [RegInstr],
) {
let mut fresh_reg = || {
let r = *next_reg;
*next_reg += 1;
Operand::Reg(r)
};
for instr_idx in block.start..block.end {
let instr = &code[instr_idx];
let out_instr = &mut out[instr_idx];
if try_handle_named_read(instr, stack, &mut fresh_reg, out_instr) {
continue;
}
if try_handle_named_write(instr, stack, out_instr) {
continue;
}
if try_handle_shuffle(instr, stack, out_instr) {
continue;
}
handle_normal_case(instr, stack, &mut fresh_reg, out_instr);
if instr.op == OpCode::DefineFieldRef {
let prev = std::mem::replace(&mut out[instr_idx - 1], RegInstr::nop());
resolve_location(&prev, &mut out[instr_idx]);
}
}
}
/// Handles a "named read" i.e. a read of a local or arg (and not a stack value)
///
/// We simulate pushing the operand to stack, and replace the instr with `SetLoc`, which
/// is our catch-all for a data-move instr
fn try_handle_named_read(
instr: &Instr,
stack: &mut Vec<Operand>,
mut fresh_reg: impl FnMut() -> Operand,
out: &mut RegInstr,
) -> bool {
use OpCode::*;
let arg = match instr.op {
GetLoc0 => Operand::Local(0),
GetLoc1 => Operand::Local(1),
GetLoc2 => Operand::Local(2),
GetLoc3 => Operand::Local(3),
GetLoc | GetLoc8 | GetLocCheck => match instr.imm {
ImmOperand::Loc(k) => Operand::Local(k as usize),
_ => unreachable!(),
},
GetArg0 => Operand::Arg(0),
GetArg1 => Operand::Arg(1),
GetArg2 => Operand::Arg(2),
GetArg3 => Operand::Arg(3),
GetArg => match instr.imm {
ImmOperand::Arg(k) => Operand::Arg(k as usize),
_ => unreachable!(),
},
_ => return false,
};
let dst = fresh_reg();
// Converted SetLoc, reading from arg, writing to dst
*out = RegInstr {
dsts: vec![dst],
op: SetLoc,
imm: ImmOperand::None,
args: vec![arg],
};
// Simulate stack push
stack.push(dst);
true
}
/// Handles a "named write" i.e. a write of a local or arg (and not a stack value)
///
/// We simulate popping the operand off stack, and replace the instr with `SetLoc`, which
/// is our catch-all for a data-move instr
///
/// A distinction needs to be made between `Set*` and `Put*` since `Set*` instrs leave
/// the value-to-write on the stack and `Put*` pops it off
fn try_handle_named_write(instr: &Instr, stack: &mut Vec<Operand>, out: &mut RegInstr) -> bool {
use OpCode::*;
let (dst, preserved) = match instr.op {
PutLoc0 => (Operand::Local(0), false),
PutLoc1 => (Operand::Local(1), false),
PutLoc2 => (Operand::Local(2), false),
PutLoc3 => (Operand::Local(3), false),
PutLoc | PutLoc8 | PutLocCheck | PutLocCheckInit => match instr.imm {
ImmOperand::Loc(k) => (Operand::Local(k as usize), false),
_ => unreachable!(),
},
PutArg0 => (Operand::Arg(0), false),
PutArg1 => (Operand::Arg(1), false),
PutArg2 => (Operand::Arg(2), false),
PutArg3 => (Operand::Arg(3), false),
SetLoc0 => (Operand::Local(0), true),
SetLoc1 => (Operand::Local(1), true),
SetLoc2 => (Operand::Local(2), true),
SetLoc3 => (Operand::Local(3), true),
SetLoc | SetLoc8 => match instr.imm {
ImmOperand::Loc(k) => (Operand::Local(k as usize), true),
_ => unreachable!(),
},
SetArg0 => (Operand::Arg(0), true),
SetArg1 => (Operand::Arg(1), true),
SetArg2 => (Operand::Arg(2), true),
SetArg3 => (Operand::Arg(3), true),
_ => return false,
};
// Simulate stack pop or peek, depending on instr
let arg = if preserved {
stack.last().unwrap().clone()
} else {
stack.pop().unwrap()
};
// Converted SetLoc, reading from arg, writing to dst
*out = RegInstr {
dsts: vec![dst],
op: SetLoc,
imm: ImmOperand::None,
args: vec![arg],
};
true
}
/// Handles an instr that only moves stack values around: it pops `n` values, and pushes some
/// of them again. No value is made, so `out_instr` becomes `nop`
fn try_handle_shuffle(instr: &Instr, stack: &mut Vec<Operand>, out_instr: &mut RegInstr) -> bool {
let Some((n, pushes)) = shuffle(instr.op) else {
return false;
};
let popped = stack.split_off(stack.len() - n);
stack.extend(pushes.iter().map(|&i| popped[i]));
*out_instr = RegInstr::nop();
true
}
/// The shuffles: how many values an instr pops, and which of them it pushes, in stack order
/// (bottom first). The numbers index the popped values, bottom first
fn shuffle(op: OpCode) -> Option<(usize, &'static [usize])> {
use OpCode::*;
Some(match op {
Drop => (1, &[]), // a ->
Nip => (2, &[1]), // a b -> b
Nip1 => (3, &[1, 2]), // a b c -> b c
Dup => (1, &[0, 0]), // a -> a a
Dup1 => (2, &[0, 0, 1]), // a b -> a a b
Dup2 => (2, &[0, 1, 0, 1]), // a b -> a b a b
Dup3 => (3, &[0, 1, 2, 0, 1, 2]), // a b c -> a b c a b c
Insert2 => (2, &[1, 0, 1]), // obj a -> a obj a
Insert3 => (3, &[2, 0, 1, 2]), // obj prop a -> a obj prop a
Insert4 => (4, &[3, 0, 1, 2, 3]), // this obj prop a -> a this obj prop a
Perm3 => (3, &[1, 0, 2]), // obj a b -> a obj b
Perm4 => (4, &[2, 0, 1, 3]), // obj prop a b -> a obj prop b
Perm5 => (5, &[3, 0, 1, 2, 4]), // this obj prop a b -> a this obj prop b
Swap => (2, &[1, 0]), // a b -> b a
Swap2 => (4, &[2, 3, 0, 1]), // a b c d -> c d a b
Rot3l => (3, &[1, 2, 0]), // x a b -> a b x
Rot3r => (3, &[2, 0, 1]), // a b x -> x a b
Rot4l => (4, &[1, 2, 3, 0]), // x a b c -> a b c x
Rot5l => (5, &[1, 2, 3, 4, 0]), // x a b c d -> a b c d x
// Binds [[HomeObject]], which only `super` reads. `super` is not supported
SetHomeObject => (2, &[0, 1]),
// Makes the key a property key. A key is kept as it is (see infer)
ToPropkey2 => (2, &[0, 1]),
_ => return None,
})
}
/// Handle a normal instruction with simulated stack pushes/pops
fn handle_normal_case(
instr: &Instr,
stack: &mut Vec<Operand>,
mut fresh_reg: impl FnMut() -> Operand,
out_instr: &mut RegInstr,
) {
use OpCode::*;
// op and imm carried as-is
*out_instr = RegInstr {
dsts: vec![],
op: instr.op,
imm: instr.imm,
args: vec![],
};
// Some instrs have extra pops not reflected in info.n_pop
let extra_pops = match instr.imm {
ImmOperand::Npop(npop) => npop as usize,
ImmOperand::NpopU16 { npop, .. } => npop as usize,
_ => 0,
};
let info = instr.op.info();
let n_pop = info.n_pop as usize + extra_pops;
let n_push = info.n_push as usize;
// Simulate this instr's pops, top first
for _ in 0..n_pop {
let x = stack.pop().unwrap();
out_instr.args.push(x);
}
if matches!(instr.op, Call | CallMethod | CallConstructor) {
out_instr.args = call_operands(instr.op, std::mem::take(&mut out_instr.args));
}
// These leave the object they wrote on the stack: the same value, not a new one
if matches!(instr.op, DefineField | DefineFieldRef) {
stack.push(out_instr.args[1]);
return;
}
// Simulate this instr's pushes
for _ in 0..n_push {
let x = fresh_reg();
stack.push(x);
out_instr.dsts.push(x);
}
}
/// Put a call's popped operands in the order of the callee's params: the callee (which is its
/// env), `this`, then the args in source order
///
/// `call_method` has `this` at the bottom of its frame, below the callee. `call_constructor`
/// has the callee at the bottom and `this` above it (see transform/new.rs). `call` has `this`
/// on top, where transform pushed it
fn call_operands(op: OpCode, mut popped: Vec<Operand>) -> Vec<Operand> {
let (callee, this) = match op {
OpCode::CallMethod => {
let this = popped.pop().unwrap();
(popped.pop().unwrap(), this)
}
OpCode::CallConstructor => {
let callee = popped.pop().unwrap();
(callee, popped.pop().unwrap())
}
_ => {
let callee = popped.pop().unwrap();
(callee, popped.remove(0))
}
};
// What remains are the args, last arg first
popped.reverse();
[callee, this].into_iter().chain(popped).collect()
}
/// `define_field_ref x` defines `x` as the location that its value was read from (see
/// transform/closure.rs), and the instr before it is that read. Its value arg becomes the
/// location:
/// - a local or arg: the field is that binding
/// - the reg of an object `o`, for a read `get_field x` of `o`: the field is `o.x`, which is
/// a location itself (a capture that the owner gives to its own child)
///
/// The read itself is then not used, so it becomes `nop`
fn resolve_location(prev: &RegInstr, out_instr: &mut RegInstr) {
let value = out_instr.args[0];
assert_eq!(
prev.dsts,
[value],
"define_field_ref must follow the read of its value"
);
out_instr.args[0] = match prev.op {
OpCode::SetLoc => prev.args[0],
OpCode::GetField => {
assert!(prev.imm.atom() == out_instr.imm.atom());
prev.args[0]
}
op => panic!("define_field_ref after {op:?}, which reads no location"),
};
}
+13 -5
View File
@@ -1,9 +1,12 @@
use super::{FIRST_ARG, THIS_ARG, pusher};
//! Replace all accesses to args, accounting for the added env and `this` args
//!
//! After this only `get_arg` and `set_arg` are used
use super::{FIRST_ARG, THIS_ARG};
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 {
#[allow(non_snake_case)]
pub fn try_replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> bool {
use OpCode::*;
let (op, arg) = match (instr.op, instr.imm) {
@@ -24,6 +27,11 @@ pub fn replace_arg_access(instr: &Instr, out: &mut Vec<Instr>) -> bool {
_ => return false,
};
pusher(instr.pc, out)(op, ImmOperand::Arg(arg));
out.push(Instr {
pc: instr.pc,
op,
imm: ImmOperand::Arg(arg),
});
true
}
+684
View File
@@ -0,0 +1,684 @@
//! Lowers async fns and generators to state machines: a constructor, and a step closure
//!
//! Replace:
//! ```js
//! async function f(a) {
//! const x = await g(a);
//! return x + 1;
//! }
//! ```
//!
//! With:
//! ```js
//! function f(a) {
//! let x, state = 0, await1, co; // the frame: the body's locals, and the state
//!
//! const step = (input) => { // the body, as transitions
//! switch (state) {
//! case 0:
//! await1 = co.toPromise(g(a));
//! co.awaiting = await1;
//! state = 1;
//! return; // suspend
//! case 1:
//! x = await1.value; // resume: read the value from this site's promise
//! co.result = x + 1;
//! co.done = true;
//! return;
//! }
//! };
//!
//! co = { __proto__: __AsyncProto, step, value: undefined, awaiting: undefined, result: undefined, done: false };
//! return co.start();
//! }
//! ```
//!
//! The fn keeps its index and becomes the constructor. Its locals and args stay its own, and
//! the step captures them, so they live across every suspension as cells (see closure.rs). A
//! closure made in the body shares them, as any nested closure does
//!
//! Every kind of fn that suspends lowers the same way: only the driver differs, which is
//! stdlib JS (lib/stdlib/coroutine.js). `co` holds what a suspension hands out to it, each
//! in a field of its own so that their types stay apart: `value` for a yield, `awaiting` for
//! an await, `result` for the return. The constructor reads its kind's driver, an export of
//! `jsrt:coroutine`, as a global (see link.rs)
//!
//! A module's body that awaits at its top is an async fn too (see flatten.rs)
//!
//! In the step:
//! a local, arg or capture the step's capture of it
//! initial_yield suspend
//! yield x co.value = x; suspend; at the label: push input, push false
//! await x p = co.toPromise(x); co.awaiting = p; suspend; at the label: push p.value
//! return x co.result = x; co.done = true; return
//! close_loc k close_var_ref k (see closure.rs)
//!
//! Suspend stores the values on the stack under it into new locals, sets `state` and returns.
//! Its label, where the step's first instrs jump for that state, pushes them back. `yield`
//! pushes a flag after its value, which is true for `return()`: here it is always false
//!
//! Not supported: `yield*`, which QuickJS expands to a loop of iterator ops
use super::PROTO_FIELD;
use crate::{
flatten::FlatFn,
libqjs::parse::{
Atom, AtomRef, ClosureKind, ClosureVar, ClosureVarFlags, FuncKind, ImmOperand as Imm,
Instr, JsValue, VarKind, atom_names::PREDEFINED_ATOMS, opcodes::OpCode,
},
link::Link,
};
// ─────────────────────────────────────── The spine ───────────────────────────────────────
pub fn deasync(
fns: &mut Vec<FlatFn>,
captures: &mut Vec<Vec<ClosureVar>>,
atoms: &[Atom],
link: &Link,
) {
let coroutines: Vec<usize> = (0..fns.len())
.filter(|&f| fns[f].kind != FuncKind::Normal)
.collect();
let mut steps = vec![];
for &f in &coroutines {
// 1. Rewrite the body into the step's code, which reads every local, arg and capture
// of `f` through a capture. This gives `f` the new locals that the step needs
let mut step = Step::new(&fns[f], captures[f].len(), atoms);
let body = std::mem::take(&mut fns[f].code);
step.lower(&body);
// 2. The step captures all of `f`'s args, captures and locals. The closures made in
// the body now sit in the step, so they capture from it
let own = captures[f].clone();
let cpool = std::mem::take(&mut fns[f].cpool);
let step_captures = step.captures(&own);
retarget_children(&cpool, &step, &own, fns, captures);
// 3. Add the step after every other fn
let s = fns.len();
let step_fn = FlatFn {
name: fns[f].name,
kind: FuncKind::Normal,
arg_count: 1, // input
local_count: 0,
cpool,
code: step.code(),
};
fns.push(step_fn);
captures.push(step_captures);
fns[f].local_count = step.locals;
fns[f].cpool = vec![JsValue::FnByIdx(s)];
steps.push(step);
}
// 4. Each constructor makes `co`, linked to its kind's driver, and starts it
for (&f, step) in coroutines.iter().zip(&steps) {
let driver = link.stdlib("jsrt:coroutine", driver(fns[f].kind));
fns[f].code = constructor(step, driver, atoms);
}
}
/// The stdlib's driver of a kind of fn (see lib/stdlib/coroutine.js)
fn driver(kind: FuncKind) -> &'static str {
match kind {
FuncKind::Generator => "__GeneratorProto",
FuncKind::Async => "__AsyncProto",
_ => "__AsyncGeneratorProto",
}
}
// ─────────────────────────────────── Lowering the body ───────────────────────────────────
/// A local, arg or capture of the constructor
#[derive(Clone, Copy)]
enum Slot {
Local(usize),
Arg(usize),
Capture(usize),
}
/// What a suspension's label pushes, after the stack values that it kept
enum Resume {
Nothing, // initial_yield
Input, // yield: what `next(input)` sent, and the flag that it is not a `return()`
ValueOf(usize), // await: the value of the promise in this local
}
/// The step's code as it is written, and the constructor's locals
struct Step<'a> {
atoms: &'a [Atom],
args: usize, // the constructor's args
own_captures: usize, // the constructor's captures. The step captures its args, these, then its locals
locals: usize, // the constructor's locals so far
this: usize, // the local that holds `this`, which the step reads in place of its own
callee: usize, // the local that holds the fn itself, for `special_object 2`
co: usize,
state: usize,
code: Vec<Instr>,
labels: Vec<usize>, // the pc of each state's label: state k at k - 1
next_pc: usize, // pcs past the body's, for labels
}
impl<'a> Step<'a> {
fn new(f: &FlatFn, own_captures: usize, atoms: &'a [Atom]) -> Self {
let l = f.local_count;
let next_pc = f.code.last().map_or(0, |i| i.pc + 1);
Step {
atoms,
args: f.arg_count,
own_captures,
locals: l + 4,
this: l,
callee: l + 1,
co: l + 2,
state: l + 3,
code: vec![],
labels: vec![],
next_pc,
}
}
/// Rewrite the body, one instr at a time
fn lower(&mut self, body: &[Instr]) {
use OpCode::*;
for (instr, depth) in body.iter().zip(stack_depths(body)) {
let pc = instr.pc;
let depth = depth.unwrap_or(0);
if let Some((slot, op)) = slot_access(instr) {
self.var(pc, op, slot);
continue;
}
match (instr.op, instr.imm) {
(GetLoc0Loc1, _) => {
self.var(pc, GetVarRef, Slot::Local(0));
self.var(pc, GetVarRef, Slot::Local(1));
}
(IncLoc | DecLoc | AddLoc, Imm::Loc(k)) => {
self.update_local(pc, instr.op, k as usize)
}
(SetLocUninitialized, _) => self.push(pc, Nop, Imm::None), // the TDZ is not checked
(CloseLoc, Imm::Loc(k)) => self.var(pc, CloseVarRef, Slot::Local(k as usize)),
(PushThis, _) => self.var(pc, GetVarRef, Slot::Local(self.this)),
(SpecialObject, Imm::U8(2)) => self.var(pc, GetVarRef, Slot::Local(self.callee)),
(InitialYield, _) => self.suspend(pc, depth, Resume::Nothing),
(Yield, _) => {
self.set_co(pc, "value");
self.suspend(pc, depth.saturating_sub(1), Resume::Input);
}
(Await, _) => {
let promise = self.to_promise(pc);
self.suspend(pc, depth.saturating_sub(1), Resume::ValueOf(promise));
}
(Return | ReturnAsync, _) => self.finish(pc),
(ReturnUndef, _) => {
self.push(pc, Undefined, Imm::None);
self.finish(pc);
}
(YieldStar | AsyncYieldStar, _) => {
unimplemented!("`yield*`: needs the iterator ops")
}
_ => self.push(pc, instr.op, instr.imm),
}
}
}
/// The step's code: a jump on `state` to the label of each state, then the body. State 0
/// falls into the body
fn code(&mut self) -> Vec<Instr> {
use OpCode::*;
let body = std::mem::take(&mut self.code);
for (k, label) in self.labels.clone().into_iter().enumerate() {
let pc = self.new_pc();
self.var(pc, GetVarRef, Slot::Local(self.state));
self.push(pc, PushI32, Imm::I32(k as i32 + 1));
self.push(pc, StrictEq, Imm::None);
self.push(pc, IfTrue, Imm::Label { target: label });
}
let mut code = std::mem::take(&mut self.code);
code.extend(body);
code
}
/// The step's captures: the constructor's args, its captures, then its locals
fn captures(&self, own: &[ClosureVar]) -> Vec<ClosureVar> {
let mut list = vec![];
for k in 0..self.args {
list.push(closure_var(
self.name(Slot::Arg(k), own),
k,
ClosureKind::Arg,
));
}
for i in 0..self.own_captures {
list.push(closure_var(
self.name(Slot::Capture(i), own),
i,
ClosureKind::Ref,
));
}
for k in 0..self.locals {
list.push(closure_var(
self.name(Slot::Local(k), own),
k,
ClosureKind::Local,
));
}
list
}
/// The step's capture of a slot
fn index(&self, slot: Slot) -> usize {
match slot {
Slot::Arg(k) => k,
Slot::Capture(i) => self.args + i,
Slot::Local(k) => self.args + self.own_captures + k,
}
}
/// The name of the step's capture of a slot. A forward of a capture keeps its name, which
/// closure.rs needs (see `define_field_ref`). Any other gets a name that no variable has:
/// two locals can share a name, as in two blocks
fn name(&self, slot: Slot, own: &[ClosureVar]) -> AtomRef {
match slot {
Slot::Capture(i) => own[i].name,
_ => AtomRef::TaggedInt(self.index(slot) as u32),
}
}
}
// ──────────────────────────────────── Writing instrs ─────────────────────────────────────
impl Step<'_> {
fn push(&mut self, pc: usize, op: OpCode, imm: Imm) {
self.code.push(Instr { pc, op, imm });
}
/// `op` on the step's capture of `slot`: a get, put, set or close of a var ref
fn var(&mut self, pc: usize, op: OpCode, slot: Slot) {
let i = self.index(slot) as u16;
self.push(pc, op, Imm::VarRef(i));
}
fn field(&self, text: &str) -> Imm {
Imm::Atom(find_atom(text, self.atoms))
}
fn new_local(&mut self) -> usize {
self.locals += 1;
self.locals - 1
}
fn new_pc(&mut self) -> usize {
self.next_pc += 1;
self.next_pc - 1
}
/// `inc_loc`, `dec_loc` and `add_loc`. For `add_loc`, the added value is on the stack, and
/// the local is the left operand
fn update_local(&mut self, pc: usize, op: OpCode, k: usize) {
use OpCode::*;
self.var(pc, GetVarRef, Slot::Local(k));
match op {
IncLoc => self.push(pc, Inc, Imm::None),
DecLoc => self.push(pc, Dec, Imm::None),
_ => {
self.push(pc, Swap, Imm::None);
self.push(pc, Add, Imm::None);
}
}
self.var(pc, PutVarRef, Slot::Local(k));
}
/// `co.<field> = x`, with x on the stack
fn set_co(&mut self, pc: usize, field: &str) {
self.var(pc, OpCode::GetVarRef, Slot::Local(self.co));
self.push(pc, OpCode::Swap, Imm::None);
self.push(pc, OpCode::PutField, self.field(field));
}
/// `p = co.toPromise(x); co.awaiting = p`, with x on the stack. Gives the local of `p`
fn to_promise(&mut self, pc: usize) -> usize {
use OpCode::*;
let promise = self.new_local();
// [x] -> [co, toPromise, x] -> [p]
self.var(pc, GetVarRef, Slot::Local(self.co));
self.push(pc, GetField2, self.field("toPromise"));
self.push(pc, Rot3l, Imm::None);
self.push(pc, CallMethod, Imm::Npop(1));
self.var(pc, PutVarRef, Slot::Local(promise));
self.var(pc, GetVarRef, Slot::Local(self.co));
self.var(pc, GetVarRef, Slot::Local(promise));
self.push(pc, PutField, self.field("awaiting"));
promise
}
/// Store the `depth` values on the stack into new locals, set the state, and return. Then
/// the new state's label: push the values back, then what the suspension resumes with
fn suspend(&mut self, pc: usize, depth: usize, resume: Resume) {
use OpCode::*;
let kept: Vec<usize> = (0..depth).map(|_| self.new_local()).collect();
for &k in &kept {
self.var(pc, PutVarRef, Slot::Local(k));
}
let state = self.labels.len() as i32 + 1;
self.push(pc, PushI32, Imm::I32(state));
self.var(pc, PutVarRef, Slot::Local(self.state));
self.push(pc, ReturnUndef, Imm::None);
let label = self.new_pc();
self.labels.push(label);
self.push(label, Nop, Imm::None);
for &k in kept.iter().rev() {
self.var(label, GetVarRef, Slot::Local(k));
}
match resume {
Resume::Nothing => {}
Resume::Input => {
self.push(label, GetArg, Imm::Arg(0));
self.push(label, PushFalse, Imm::None);
}
Resume::ValueOf(promise) => {
self.var(label, GetVarRef, Slot::Local(promise));
self.push(label, GetField, self.field("value"));
}
}
}
/// The return of the body, with its value on the stack: `co.result = x; co.done = true`
fn finish(&mut self, pc: usize) {
self.set_co(pc, "result");
self.var(pc, OpCode::GetVarRef, Slot::Local(self.co));
self.push(pc, OpCode::PushTrue, Imm::None);
self.push(pc, OpCode::PutField, self.field("done"));
self.push(pc, OpCode::ReturnUndef, Imm::None);
}
}
/// The local, arg or capture that `instr` reads or writes, and the op that does that through a
/// capture
fn slot_access(instr: &Instr) -> Option<(Slot, OpCode)> {
use OpCode::*;
let nth = |base: OpCode| instr.op as usize - base as usize;
let imm = match instr.imm {
Imm::Loc(k) | Imm::Arg(k) | Imm::VarRef(k) => k as usize,
_ => 0,
};
let access = match instr.op {
GetLoc0 | GetLoc1 | GetLoc2 | GetLoc3 => (Slot::Local(nth(GetLoc0)), GetVarRef),
PutLoc0 | PutLoc1 | PutLoc2 | PutLoc3 => (Slot::Local(nth(PutLoc0)), PutVarRef),
SetLoc0 | SetLoc1 | SetLoc2 | SetLoc3 => (Slot::Local(nth(SetLoc0)), SetVarRef),
GetLoc | GetLoc8 | GetLocCheck => (Slot::Local(imm), GetVarRef),
PutLoc | PutLoc8 | PutLocCheck | PutLocCheckInit => (Slot::Local(imm), PutVarRef),
SetLoc | SetLoc8 => (Slot::Local(imm), SetVarRef),
GetArg0 | GetArg1 | GetArg2 | GetArg3 => (Slot::Arg(nth(GetArg0)), GetVarRef),
PutArg0 | PutArg1 | PutArg2 | PutArg3 => (Slot::Arg(nth(PutArg0)), PutVarRef),
SetArg0 | SetArg1 | SetArg2 | SetArg3 => (Slot::Arg(nth(SetArg0)), SetVarRef),
GetArg => (Slot::Arg(imm), GetVarRef),
PutArg => (Slot::Arg(imm), PutVarRef),
SetArg => (Slot::Arg(imm), SetVarRef),
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => {
(Slot::Capture(nth(GetVarRef0)), GetVarRef)
}
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => {
(Slot::Capture(nth(PutVarRef0)), PutVarRef)
}
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => {
(Slot::Capture(nth(SetVarRef0)), SetVarRef)
}
GetVarRef | GetVarRefCheck => (Slot::Capture(imm), GetVarRef),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => (Slot::Capture(imm), PutVarRef),
SetVarRef => (Slot::Capture(imm), SetVarRef),
_ => return None,
};
Some(access)
}
/// A closure made in the body now sits in the step: what it captured from the constructor, it
/// now captures from the step, as a forward
fn retarget_children(
cpool: &[JsValue],
step: &Step,
own: &[ClosureVar],
fns: &[FlatFn],
captures: &mut [Vec<ClosureVar>],
) {
for c in cpool {
let &JsValue::FnByIdx(child) = c else {
continue;
};
for i in 0..captures[child].len() {
let slot = match captures[child][i].flags.closure_kind {
ClosureKind::Local => Slot::Local(captures[child][i].var_idx),
ClosureKind::Arg => Slot::Arg(captures[child][i].var_idx),
ClosureKind::Ref => Slot::Capture(captures[child][i].var_idx),
_ => continue,
};
let name = step.name(slot, own);
captures[child][i] = closure_var(name, step.index(slot), ClosureKind::Ref);
rename_forwards(child, i, name, fns, captures);
}
}
}
/// Give every forward of capture `i` of `f`, in `f`'s children and in theirs, the name `name`:
/// a forward keeps its parent's name (see closure.rs)
fn rename_forwards(
f: usize,
i: usize,
name: AtomRef,
fns: &[FlatFn],
captures: &mut [Vec<ClosureVar>],
) {
for c in &fns[f].cpool {
let &JsValue::FnByIdx(child) = c else {
continue;
};
for j in 0..captures[child].len() {
let cv = &mut captures[child][j];
if cv.flags.closure_kind == ClosureKind::Ref && cv.var_idx == i {
cv.name = name;
rename_forwards(child, j, name, fns, captures);
}
}
}
}
// ──────────────────────────────────── The constructor ────────────────────────────────────
/// The constructor's code: keep `this` and the fn itself, make `co` with the step and the
/// driver, which is the global `driver`, set the state to 0, and return what the driver's
/// `start` gives
fn constructor(step: &Step, driver: usize, atoms: &[Atom]) -> Vec<Instr> {
use OpCode::*;
let field = |text: &str| Imm::Atom(find_atom(text, atoms));
let loc = |k: usize| Imm::Loc(k as u16);
let ops = [
(PushThis, Imm::None),
(PutLoc, loc(step.this)),
(SpecialObject, Imm::U8(2)),
(PutLoc, loc(step.callee)),
(Object, Imm::None),
(GetVar, Imm::Global(driver as u32)),
(DefineField, Imm::Atom(PROTO_FIELD)),
(Fclosure, Imm::Const(0)),
(DefineField, field("step")),
(Undefined, Imm::None),
(DefineField, field("value")),
(Undefined, Imm::None),
(DefineField, field("awaiting")),
(Undefined, Imm::None),
(DefineField, field("result")),
(PushFalse, Imm::None),
(DefineField, field("done")),
(PutLoc, loc(step.co)),
(Push0, Imm::None),
(PutLoc, loc(step.state)),
(GetLoc, loc(step.co)),
(GetField2, field("start")),
(CallMethod, Imm::Npop(0)),
(Return, Imm::None),
];
let mut code = vec![];
for (pc, (op, imm)) in ops.into_iter().enumerate() {
code.push(Instr { pc, op, imm });
}
code
}
fn closure_var(name: AtomRef, var_idx: usize, closure_kind: ClosureKind) -> ClosureVar {
let flags = ClosureVarFlags {
closure_kind,
_is_const: false,
is_lexical: false,
_var_kind: VarKind::Normal,
};
ClosureVar {
name,
var_idx,
flags,
}
}
/// The atom of a field name that the drivers use: a predefined one, or one of the program's
fn find_atom(text: &str, atoms: &[Atom]) -> AtomRef {
if let Some(n) = PREDEFINED_ATOMS.iter().position(|&a| a == text) {
return AtomRef::Predefined(n as u32);
}
for (i, atom) in atoms.iter().enumerate() {
if let Atom::String(s) = atom
&& s.text() == text
{
return AtomRef::FileLocal(i);
}
}
panic!(
"async fns and generators need the stdlib (lib/stdlib/coroutine.js), which names `{text}`"
)
}
// ───────────────────────────────────── Stack depths ──────────────────────────────────────
/// The stack depth before each instr. `None` for an instr that no path reaches
fn stack_depths(code: &[Instr]) -> Vec<Option<usize>> {
let mut depths = vec![None; code.len()];
let mut work = vec![0];
depths[0] = Some(0);
while let Some(i) = work.pop() {
let instr = &code[i];
let info = instr.op.info();
let after =
depths[i].unwrap() + info.n_push as usize - info.n_pop as usize - call_args(instr);
for next in successors(code, i) {
if depths[next].is_none() {
depths[next] = Some(after);
work.push(next);
}
}
}
depths
}
/// The args that a call pops, besides the fn: in the operand, or, for `call0` to `call3`, in
/// the opcode
fn call_args(instr: &Instr) -> usize {
use OpCode::*;
match (instr.op, instr.imm) {
(Call0 | Call1 | Call2 | Call3, _) => instr.op as usize - Call0 as usize,
(_, Imm::Npop(n)) => n as usize,
(_, Imm::NpopU16 { npop, .. }) => npop as usize,
_ => 0,
}
}
/// The ops that jump to their label
const JUMPS: &[OpCode] = &[
OpCode::Goto,
OpCode::Goto8,
OpCode::Goto16,
OpCode::IfFalse,
OpCode::IfFalse8,
OpCode::IfTrue,
OpCode::IfTrue8,
];
/// The ops after which the next instr does not run
const ENDS: &[OpCode] = &[
OpCode::Goto,
OpCode::Goto8,
OpCode::Goto16,
OpCode::Return,
OpCode::ReturnUndef,
OpCode::ReturnAsync,
OpCode::Throw,
OpCode::ThrowError,
OpCode::TailCall,
OpCode::TailCallMethod,
OpCode::Ret,
];
/// The instrs that can run after instr `i`: its jump's target, and the next instr unless it
/// ends the flow
fn successors(code: &[Instr], i: usize) -> Vec<usize> {
let instr = &code[i];
let mut next = vec![];
if let Imm::Label { target } = instr.imm
&& JUMPS.contains(&instr.op)
{
next.push(code.iter().position(|j| j.pc == target).unwrap());
}
if !ENDS.contains(&instr.op) && i + 1 < code.len() {
next.push(i + 1);
}
next
}
-216
View File
@@ -1,216 +0,0 @@
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,
}
}
@@ -1,4 +1,25 @@
use super::{PROTOTYPE_FIELD, pusher};
//! Convert ES6 classes to old-school prototype form
//!
//! Class form:
//! ```js
//! class Class {
//! constructor() { }
//! m1() { }
//! }
//! ```
//!
//! Becomes prototype form:
//! ```js
//! function Class() { }
//! Class.prototype.m1 = () => { };
//! ```
//!
//! We first pattern-match a class declaration sequence in the input bytecode,
//! and replace it with equivalent prototype form in the emitted output
//!
//! TODO: field initializer unhandled
use super::PROTOTYPE_FIELD;
use crate::libqjs::parse::{AtomRef, ImmOperand, Instr, opcodes::OpCode};
#[derive(Debug)]
@@ -15,19 +36,34 @@ pub struct ClassSequence {
}
impl ClassSequence {
/// fclosure ctor [C]
/// dup [C, C]
/// object [C, C, P]
/// Bytecode for prototype form looks like:
/// ```
/// fclosure ctor
/// dup
/// object
/// (fclosure method; define_field name)*
/// put_field prototype [C], C.prototype = P
/// dup [C, C]
/// get_field prototype [C, P]
/// put_field prototype
/// dup
/// get_field prototype
/// ```
///
/// JS gives every fn a `prototype` object, but a closure here starts with
/// no fields, so the prototype is created explicitly. `define_class` left
/// [C, P] on the stack, and the code after it expects that
pub fn emit_prototype_form(&self, out: &mut Vec<Instr>) {
let mut push = pusher(self.start_pc, out);
/// i.e.
/// - make `ctor` function
/// - make prototype obj
/// - assign methods to prototype
/// - assign prototype to `ctor`
///
/// The original class sequence leaves `[class, prototype]` on top of the stack,
/// the last `get_field` ensures that stack condition is identical in this form
pub fn emit_proto_form(&self, out: &mut Vec<Instr>) {
let mut push = move |op, imm| {
out.push(Instr {
pc: self.start_pc,
op,
imm,
})
};
let prototype = ImmOperand::Atom(PROTOTYPE_FIELD);
push(OpCode::Fclosure8, self.ctor_const);
@@ -45,13 +81,17 @@ impl ClassSequence {
}
}
/// Declass one function's bytecode
///
/// Try to pattern-match a class decl, emitting prototype form in its place
/// and advancing the slice as we go
pub fn declass(mut code: &[Instr]) -> Vec<Instr> {
let mut out = Vec::with_capacity(code.len());
while !code.is_empty() {
match match_class_seq(code) {
Ok((rest, seq)) => {
seq.emit_prototype_form(&mut out);
seq.emit_proto_form(&mut out);
code = rest;
}
Err(rest) => {
@@ -82,7 +122,7 @@ fn match_class_seq(ins: &[Instr]) -> PResult<'_, ClassSequence> {
// Bit 0 of `arg` is set when the class has a superclass
let is_derived = matches!(define_class.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1);
assert!(!is_derived, "derived classes are not supported");
assert!(!is_derived, "TODO: derived classes");
let seq = ClassSequence {
start_pc: pc,
@@ -93,7 +133,7 @@ fn match_class_seq(ins: &[Instr]) -> PResult<'_, ClassSequence> {
Ok((ins, seq))
}
// TODO: accessors are not handled
// TODO: accessor fns are not handled
fn match_method_seq(ins: &[Instr]) -> PResult<'_, MethodSequence> {
let (ins, fclosure) = match_instr(ins, |i| i.op == OpCode::Fclosure8)?;
let (ins, define_method) = match_instr(ins, |i| i.op == OpCode::DefineMethod)?;
+217
View File
@@ -0,0 +1,217 @@
//! This module lowers closures by making implicit environment captures explicit
//!
//! Replace:
//! ```js
//! let x = 0;
//! let y = true;
//!
//! function fn() {
//! f(x)
//! f(y)
//! }
//! ```
//!
//! With:
//! ```js
//! let x = 0;
//! let y = true;
//!
//! function fn() {
//! f(fn.<var>.x);
//! f(fn.<var>.y);
//! }
//!
//! fn.<var> = {};
//! fn.<var>.x = x; // by-ref
//! fn.<var>.y = y; // by-ref
//! ```
//!
//! i.e. captured bindings are turned into fields on the function. A closure is an object, so
//! it can have fields of its own. The captures are kept apart from them in one field, `<var>`,
//! so that a write to a field of the closure cannot clobber a capture
//!
//! # Locations
//!
//! Every binding (a local or an arg) is a location. A capture must share the location of the
//! binding it captures: a write by the owner is seen by the closure, and a write by the
//! closure is seen by the owner. Normally a field holds a copy of a value, so a special
//! `define_field_ref` instr is introduced by `c_parse.py`:
//!
//! define_field_ref x [obj, value] -> [obj]
//!
//! It defines `obj.x` as the location that `value` was read from, not as a copy of `value`.
//! So the instr right before it is always the read of a location. This module only emits:
//! - get_loc k a local of the owner
//! - get_arg k an arg of the owner
//! - get_field y a capture of the owner, to give to its own child. The owner's `y` is
//! the location of a binding further out, so the child shares that
//!
//! Where a location lives (the stack or the heap) is not decided here. `escape.rs` decides
//! that from these instrs. This module only states which fields share which locations
//!
//! # `close_loc`: a new binding for each loop iteration
//!
//! In JS, each iteration of a loop gets its own copy of the bindings that are declared in
//! the loop:
//! ```js
//! for (let i = 0; i < 3; i++) fns.push(() => i);
//! // the closures give 0, 1, 2, not 3, 3, 3
//! ```
//!
//! QuickJS uses one local for `i`, and emits `close_loc i` at each way out of the loop body.
//! After this module, `close_loc k` means: the binding of local `k` moves to a new location,
//! with the value of the old location. The closures that were made before the `close_loc`
//! keep the old location. So in the example, `i++` writes the new location of `i`, and the
//! closure of each iteration keeps the `i` of its iteration
//!
//! The owner's code does not change for this: `get_loc k` and `put_loc k` always use the
//! current location of `k`. This module keeps `close_loc` as it is. Then:
//! - infer can ignore it. The old and new locations of a binding have the same address
//! (`Addr::Shared`), so their types are joined
//! - `escape.rs` must give each iteration a new location if a closure can see the old one
//! after the `close_loc`
//!
//! # The replacements
//!
//! In the owner, for `fclosure f`:
//! fclosure f [fn]
//! object [fn, vars]
//! <read of the location> [fn, vars, value] for each capture `x` of `f`
//! define_field_ref x [fn, vars]
//! define_field <var> [fn]
//!
//! A closure that captures nothing gets no `<var>`: only the `fclosure` stays
//!
//! In the closure, with `env` as the closure itself (arg `ENV_ARG`):
//! get_var_ref x get_arg env; get_field <var>; get_field x
//! put_var_ref x get_arg env; get_field <var>; swap; put_field x
//! set_var_ref x dup; get_arg env; get_field <var>; swap; put_field x
//!
//! close_var_ref x get_arg env; get_field <var>; close_field x
//!
//! The `_check` forms are the same: the TDZ is not checked. `close_var_ref` is `close_loc` for
//! a capture: transform/async.rs makes it, when a binding that it moves out of a fn's frame
//! needs a new location for each loop iteration
//!
//! A module binding is not a capture: modules.rs made it a global first
//!
//! In the emitted C, every underlying "plain" function is called as:
//! `fn(env, this, arg_0, arg_1, ... arg_n)`
//! where `env` is the closure itself
use super::{ENV_ARG, FIRST_ARG};
use crate::libqjs::parse::{
AtomRef, ClosureKind, ClosureVar, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode,
};
/// `<var>`: the field of a closure that holds its captures
pub const VAR_FIELD: AtomRef = AtomRef::Predefined(86);
/// Replace a read or write of a capture, or the making of a closure
///
/// `own` is the captures of the fn that holds `instr`
pub fn try_replace_closure(
instr: &Instr,
own: &[ClosureVar],
captures: &[Vec<ClosureVar>],
cpool: &[JsValue],
out: &mut Vec<Instr>,
) -> bool {
use OpCode::*;
let mut push = |op, imm| {
out.push(Instr {
pc: instr.pc,
op,
imm,
})
};
if let Some((i, writes, leaves)) = var_ref_access(instr) {
if leaves {
push(Dup, Imm::None);
}
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
if writes {
push(Swap, Imm::None);
push(PutField, Imm::Atom(own[i].name));
} else {
push(GetField, Imm::Atom(own[i].name));
}
return true;
}
// A new location for the binding that a capture shares (see `close_loc` above)
if let (CloseVarRef, Imm::VarRef(i)) = (instr.op, instr.imm) {
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
push(CloseField, Imm::Atom(own[i as usize].name));
return true;
}
if !matches!(instr.op, Fclosure | Fclosure8) {
return false;
}
let JsValue::FnByIdx(child) = cpool[instr.imm.const_idx()] else {
unreachable!()
};
push(Fclosure, instr.imm);
// A closure with no captures needs no `<var>`
if captures[child].is_empty() {
return true;
}
push(Object, Imm::None);
for c in &captures[child] {
match c.flags.closure_kind {
ClosureKind::Local => push(GetLoc, Imm::Loc(c.var_idx as u16)),
ClosureKind::Arg => push(GetArg, Imm::Arg(FIRST_ARG + c.var_idx as u16)),
ClosureKind::Ref => {
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
push(GetField, Imm::Atom(own[c.var_idx].name));
}
_ => unimplemented!("a capture of kind {:?}", c.flags.closure_kind),
}
push(DefineFieldRef, Imm::Atom(c.name));
}
push(DefineField, Imm::Atom(VAR_FIELD));
true
}
/// 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
pub(super) fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
use OpCode::*;
let i = match instr.imm {
Imm::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,
}
}
+130 -58
View File
@@ -1,74 +1,146 @@
use super::{ENV_ARG, PROTO_FIELD, pusher};
use crate::{
libqjs::parse::{Atom, ImmOperand, Instr, JsValue, opcodes::OpCode},
native,
};
//! Process instrs that are a simple sequence of other instrs
//!
//! Useless instrs are replaced with `nop` since simply dropping instrs can break jump targets
/// 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 super::{ENV_ARG, PROTO_FIELD};
use crate::libqjs::parse::{ImmOperand as Imm, Instr, opcodes::OpCode};
#[allow(non_snake_case)]
pub fn try_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)],
let seq = match (instr.op, instr.imm) {
(Call0, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Undefined, Imm::None),
(Call, Imm::Npop(1)),
],
(Call1, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Undefined, Imm::None),
(Call, Imm::Npop(2)),
],
(Call2, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Undefined, Imm::None),
(Call, Imm::Npop(3)),
],
(Call3, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Undefined, Imm::None),
(Call, Imm::Npop(4)),
],
(Call, Imm::Npop(n)) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Undefined, Imm::None),
(Call, Imm::Npop(n + 1)),
],
(TailCall, Imm::Npop(n)) => [
(Nop, Imm::None),
(Undefined, Imm::None),
(Call, Imm::Npop(n + 1)),
(Return, Imm::None),
],
(TailCallMethod, imm) => [
(Nop, Imm::None),
(Nop, Imm::None),
(CallMethod, imm),
(Return, Imm::None),
],
(GetField2, imm) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Dup, Imm::None),
(GetField, imm),
],
// obj prop -> obj value: the object stays for a method call
(GetArrayEl2, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Dup1, Imm::None),
(GetArrayEl, Imm::None),
],
(GetLoc0Loc1, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(GetLoc0, Imm::None),
(GetLoc1, Imm::None),
],
(IncLoc, imm) => [
(Nop, Imm::None),
(GetLoc, imm),
(Inc, Imm::None),
(PutLoc, imm),
],
(DecLoc, imm) => [
(Nop, Imm::None),
(GetLoc, imm),
(Dec, Imm::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 }) => {
(AddLoc, imm) => [
(GetLoc, imm),
(Swap, Imm::None),
(Add, Imm::None),
(PutLoc, imm),
],
(SetProto, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Nop, Imm::None),
(DefineField, Imm::Atom(PROTO_FIELD)),
],
(DefineMethod, Imm::AtomU8 { atom, arg }) => {
assert!(arg & 3 == 0, "getters and setters are not supported");
&[(DefineField, ImmOperand::Atom(atom))]
[
(Nop, Imm::None),
(Nop, Imm::None),
(Nop, Imm::None),
(DefineField, Imm::Atom(atom)),
]
}
// The fn itself, which is its env
(SpecialObject, ImmOperand::U8(2)) => &[(GetArg, ImmOperand::Arg(ENV_ARG))],
(SpecialObject, Imm::U8(2)) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Nop, Imm::None),
(GetArg, Imm::Arg(ENV_ARG)),
],
// [[HomeObject]], which only `super` reads. `super` is not supported
// (see set_home_object in regify)
(SpecialObject, ImmOperand::U8(4)) => &[(Undefined, none)],
// A class constructor called without `new` throws. That is not
// checked
(CheckCtor, _) => &[(Nop, none)],
(SpecialObject, Imm::U8(4)) => unimplemented!(),
// Marks a binding as having no value yet (its TDZ), which is not checked
(SetLocUninitialized, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Nop, Imm::None),
(Nop, Imm::None),
],
// A class constructor called without `new` throws. That is not checked for the moment
(CheckCtor, _) => [
(Nop, Imm::None),
(Nop, Imm::None),
(Nop, Imm::None),
(Nop, Imm::None),
],
// arguments, new.target, import.meta
(SpecialObject, _) => unimplemented!("special_object {imm:?}"),
(SpecialObject, _) => unimplemented!(),
_ => 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;
for (op, imm) in seq {
out.push(Instr {
pc: instr.pc,
op,
imm,
});
}
cpool.push(native::global(instr.imm.atom(), atoms));
pusher(instr.pc, out)(OpCode::PushConst, ImmOperand::Const(cpool.len() as u32 - 1));
true
}
+110 -49
View File
@@ -1,96 +1,157 @@
mod args;
mod bindings;
mod declass;
mod expand;
mod new;
//! This module applies a set of transforms on the bytecode to lower them to a simpler
//! form more suitable for type inference and emission
//!
//! After transformation, the following instructions are eliminated:
//! - call{0, 1, 2, 3}
//! - tail_call, tail_call_method
//! - get_field2
//! - get_array_el2
//! - get_loc0_loc1
//! - inc_loc, dec_loc, add_loc
//! - set_proto
//! - define_method
//! - check_ctor
//! - special_object
//! - get_var_undef, put_var_init
//! - push_this
//! - get_arg{0, 1, 2, 3}
//! - put_arg{0, 1, 2, 3}
//! - set_arg{0, 1, 2, 3}
//! - get_var_ref{0, 1, 2, 3}, get_var_ref_check
//! - put_var_ref{0, 1, 2, 3}, put_var_ref_check, put_var_ref_check_init
//! - set_var_ref{0, 1, 2, 3}, set_var_ref
//! - set_loc_uninitialized
//! - fclosure8
//! - define_class
//!
//!
//! The following instructions take on a different meaning:
//! - get_var, put_var: a read or write of a binding at the top of a module, which is a
//! global (see modules.rs)
//! - call_constructor (see new.rs)
//! - close_loc (see closure.rs)
//!
//! And the following instructions are added:
//! - define_field_ref (see closure.rs)
//! - close_field (see closure.rs)
//!
//! Module bindings become globals first (see modules.rs): `get_var` and `put_var` then read
//! and write one, by an `ImmOperand::Global`
//!
//! Async fns and generators become a constructor and a step closure next (see async.rs),
//! so no instr of theirs is left: initial_yield, yield, await, return_async in them, and
//! close_var_ref, which deasync makes and closure.rs lowers
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};
mod args;
mod r#async;
mod class;
mod closure;
mod expand;
mod modules;
mod new;
use crate::{
flatten::FlatFn,
libqjs::parse::{Atom, AtomRef, ClosureVar, ImmOperand, Instr, opcodes::OpCode},
libqjs::parse::{Atom, AtomRef, ClosureVar},
link::Link,
};
/// The field an object's prototype is kept in. In JS, `o.__proto__` is the
/// prototype of an ordinary object, so user code that reads or writes it
/// gets the right behaviour with no extra work.
pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(72); // __proto__
use args::try_replace_arg_access;
use r#async::deasync;
use class::declass;
use closure::try_replace_closure;
use expand::try_replace_expansion;
use modules::globalize;
use new::try_replace_new;
/// The field a constructor keeps the prototype of its instances in
pub const PROTOTYPE_FIELD: AtomRef = AtomRef::Predefined(63); // prototype
// Object Prototype Rules:
// - The `__proto__` field is the JS spec's `[[Prototype]]`, ie the link between an object and
// its parent prototype
//
// - The `prototype` field is the normal `.prototype` field used by classes, ie it holds the
// object that `new F()` will link
//
// So objects are created by:
// ```
// Object.create = function (proto) {
// return { __proto__: proto };
// };
// ```
/// `__proto__`
pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(72);
/// `prototype`
pub const PROTOTYPE_FIELD: AtomRef = AtomRef::Predefined(63);
/// Every fn takes its env and `this` as its first two params, and its declared
/// params after them. So the params of a call do not depend on how many args
/// the callee declares: a missing arg is not passed, and an extra one is not
/// read.
// Arg Rules:
// After transformation, every fn takes its capture env and `this` as the first two args
// and all the declared args after them
pub const ENV_ARG: u16 = 0;
pub const THIS_ARG: u16 = 1;
pub const FIRST_ARG: u16 = 2;
/// The field a box keeps its binding's value in (see replace_bindings())
pub const BOX_FIELD: AtomRef = AtomRef::Predefined(86); // <var>
pub fn transform(
flat_fns: Vec<FlatFn>,
captures: &[Vec<ClosureVar>],
mut flat_fns: Vec<FlatFn>,
mut captures: Vec<Vec<ClosureVar>>,
atoms: &[Atom],
link: &Link,
) -> Vec<FlatFn> {
// Module bindings become globals first: this drops captured vars
globalize(&mut flat_fns, &mut captures, link, atoms);
// Async fns and generators become state machines next: this adds fns, and changes the
// captures that the other transforms read
deasync(&mut flat_fns, &mut captures, atoms, link);
flat_fns
.into_iter()
.map(|f| transform_one(f, captures, atoms))
.enumerate()
.map(|(idx, f)| transform_one(f, &captures[idx], &captures))
.collect()
}
fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], atoms: &[Atom]) -> FlatFn {
/// `own` is the captures of `f`
fn transform_one(f: FlatFn, own: &[ClosureVar], captures: &[Vec<ClosureVar>]) -> FlatFn {
// Transform class declarations
let code = declass(&f.code);
// Temp locals, after the fn's own, for the args of a `new` (see
// replace_new()). Each `new` has its own: a local has one type for its
// whole fn, so shared temps would join the args of every `new` in it
let temps = f.var_count;
let n_temps: usize = code.iter().filter_map(is_new).sum();
let mut next_temp = temps;
// Transforms may need to allocate new local vars
let mut next_local = f.local_count;
let mut new_local = || {
let n = next_local;
next_local += 1;
n
};
// Apply transformations
let mut out = Vec::with_capacity(code.len());
for instr in &code {
if replace_new(instr, next_temp, &mut out) {
next_temp += is_new(instr).unwrap();
if try_replace_new(instr, &mut new_local, &mut out) {
continue;
}
if replace_arg_access(instr, &mut out) {
if try_replace_arg_access(instr, &mut out) {
continue;
}
if replace_expansion(instr, &mut out) {
if try_replace_expansion(instr, &mut out) {
continue;
}
if replace_get_var(instr, atoms, &mut f.cpool, &mut out) {
if try_replace_closure(instr, own, captures, &f.cpool, &mut out) {
continue;
}
out.push(instr.clone());
}
// Last, so that it also sees the var accesses that the expansions make
let (code, var_count) = replace_bindings(&out, &f.cpool, captures, temps + n_temps);
FlatFn {
name: f.name,
kind: f.kind,
arg_count: f.arg_count + FIRST_ARG as usize,
var_count,
local_count: next_local,
cpool: f.cpool,
code,
code: out,
}
}
/// Pushes instrs at the pc of the instr they replace
fn pusher(pc: usize, out: &mut Vec<Instr>) -> impl FnMut(OpCode, ImmOperand) + '_ {
move |op, imm| out.push(Instr { pc, op, imm })
}
+198
View File
@@ -0,0 +1,198 @@
//! Module bindings become globals (see link.rs)
//!
//! A module's body reaches its bindings and imports as captured vars, and so does each fn in
//! it, through its parents. Link found what each of the body's captured vars refers to. Here
//! each read and write of one becomes a read or write of a global, with `get_var` and
//! `put_var` on an `ImmOperand::Global`. A native is a constant
//!
//! get_var_ref k get_var g (or push_const of a native)
//! put_var_ref k put_var g
//! set_var_ref k dup; put_var g
//!
//! The `_check` forms are the same: the TDZ is not checked. Those captured vars are then
//! dropped, so a closure no longer holds module bindings, and the captured vars that are
//! left are numbered again
//!
//! A global name, such as `String`, is a `get_var` of its name. It becomes the global or the
//! native that the prelude exports by that name. `typeof` of a name that nothing declares is
//! a `get_var_undef`, which gives undefined. A write of a global name is not allowed
use super::closure::var_ref_access;
use crate::{
flatten::{ENTRY_FN, FlatFn},
libqjs::parse::{
Atom, ClosureKind, ClosureVar, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode,
},
link::{Link, Target},
};
pub fn globalize(
fns: &mut [FlatFn],
captures: &mut [Vec<ClosureVar>],
link: &Link,
atoms: &[Atom],
) {
// Each fn's parent goes first, because a capture names a captured var of its parent. Each
// fn also gets the name of its module, for errors
let mut stack = vec![(ENTRY_FN, vec![], String::new())];
while let Some((f, parent, module)) = stack.pop() {
let module = link.body(f).map_or(module, |(name, _)| name.to_string());
let targets = targets(f, &captures[f], &parent, link);
let renumber = renumber(&targets);
captures[f] = keep(&captures[f], &targets, &parent);
fns[f].code = rewrite(&mut fns[f], &targets, &renumber, link, atoms, &module);
let children = fns[f].cpool.iter().filter_map(|c| match c {
JsValue::FnByIdx(child) => Some(*child),
_ => None,
});
let own = targets.into_iter().zip(renumber).map(|(t, r)| Slot {
target: t,
index: r,
});
let own: Vec<Slot> = own.collect();
for child in children.collect::<Vec<_>>() {
stack.push((child, own.clone(), module.clone()));
}
}
}
/// A captured var of a fn, after this pass: the binding that it refers to, if that is a
/// module binding, else its new index
#[derive(Clone, Copy)]
struct Slot {
target: Option<Target>,
index: Option<usize>,
}
/// What each captured var of `f` refers to, if a module binding. A module's body's captured
/// vars are all module bindings. Any other fn's capture refers to one if the captured var of
/// its parent does
fn targets(f: usize, own: &[ClosureVar], parent: &[Slot], link: &Link) -> Vec<Option<Target>> {
if let Some((_, vars)) = link.body(f) {
return vars.iter().map(|&target| Some(target)).collect();
}
(own.iter())
.map(|var| match var.flags.closure_kind {
ClosureKind::Ref => parent[var.var_idx].target,
_ => None,
})
.collect()
}
/// The new index of each captured var that stays
fn renumber(targets: &[Option<Target>]) -> Vec<Option<usize>> {
let mut next = 0;
(targets.iter())
.map(|target| match target {
Some(_) => None,
None => {
next += 1;
Some(next - 1)
}
})
.collect()
}
/// The captured vars that stay. A capture of the parent's captured var gets its new index
fn keep(own: &[ClosureVar], targets: &[Option<Target>], parent: &[Slot]) -> Vec<ClosureVar> {
let stay = own
.iter()
.zip(targets)
.filter(|(_, target)| target.is_none());
stay.map(|(var, _)| {
let mut var = var.clone();
if var.flags.closure_kind == ClosureKind::Ref {
var.var_idx = parent[var.var_idx].index.unwrap();
}
var
})
.collect()
}
/// The code of `f`, with module bindings as globals, and the captured vars that stay at their
/// new indices
fn rewrite(
f: &mut FlatFn,
targets: &[Option<Target>],
renumber: &[Option<usize>],
link: &Link,
atoms: &[Atom],
module: &str,
) -> Vec<Instr> {
use OpCode::*;
let mut out = vec![];
let code = std::mem::take(&mut f.code);
for instr in code {
let pc = instr.pc;
let mut push = |op, imm| out.push(Instr { pc, op, imm });
if let Some((i, writes, leaves)) = var_ref_access(&instr) {
match (targets[i], writes) {
(Some(target), false) => read(target, &mut f.cpool, &mut push),
(Some(Target::Global(global)), true) => {
if leaves {
push(Dup, Imm::None);
}
push(PutVar, Imm::Global(global as u32));
}
(Some(Target::Native(_)), true) => panic!("a native cannot be assigned to"),
(None, _) => {
let op = match (writes, leaves) {
(false, _) => GetVarRef,
(true, false) => PutVarRef,
(true, true) => SetVarRef,
};
push(op, Imm::VarRef(renumber[i].unwrap() as u16));
}
}
continue;
}
match (instr.op, instr.imm) {
(GetVar | GetVarUndef, Imm::Atom(name)) => {
let text = name.text(atoms);
match (link.global(&text), instr.op) {
(Some(target), _) => read(target, &mut f.cpool, &mut push),
(None, GetVarUndef) => push(Undefined, Imm::None),
(None, _) => {
panic!("{module}: `{text}` is not declared, and is not a global name")
}
}
}
(PutVar | PutVarInit, Imm::Atom(name)) => {
panic!(
"{module}: `{}` is a global name, which cannot be assigned to",
name.text(atoms)
)
}
_ => out.push(instr),
}
}
out
}
/// A read of what a module binding refers to: a global, or a native as a constant
fn read(target: Target, cpool: &mut Vec<JsValue>, push: &mut impl FnMut(OpCode, Imm)) {
match target {
Target::Global(global) => push(OpCode::GetVar, Imm::Global(global as u32)),
Target::Native(idx) => {
cpool.push(JsValue::NativeFn(idx));
push(OpCode::PushConst, Imm::Const(cpool.len() as u32 - 1));
}
}
}
+45 -33
View File
@@ -1,39 +1,51 @@
use super::{PROTO_FIELD, PROTOTYPE_FIELD, pusher};
//! QuickJS emits `call_constructor` which is a complex instruction so we need
//! to lower it first, and simplify its meaning
//!
//! We 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
//!
//! The temp locals are needed because of where F sits on the stack. At call_constructor,
//! the stack is [F, new_target, a0 .. an-1], with the arguments on top. To make the object,
//! transform needs F on top, so that it can read F.prototype
use super::{PROTO_FIELD, PROTOTYPE_FIELD};
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;
pub fn try_replace_new(
instr: &Instr,
mut new_local: impl FnMut() -> usize,
out: &mut Vec<Instr>,
) -> bool {
let num_args = match (instr.op, instr.imm) {
(OpCode::CallConstructor, ImmOperand::Npop(n)) => n,
_ => return false,
};
let mut push = pusher(instr.pc, out);
let temp = |i: usize| ImmOperand::Loc((temps + i) as u16);
let mut push = move |op, imm| {
out.push(Instr {
pc: instr.pc,
op,
imm,
})
};
for i in (0..n).rev() {
push(OpCode::PutLoc, temp(i));
let mut new_locals = vec![];
for _ in 0..num_args {
let n = new_local() as u16;
new_locals.push(n);
push(OpCode::PutLoc, ImmOperand::Loc(n));
}
push(OpCode::Drop, ImmOperand::None);
@@ -43,8 +55,8 @@ pub fn replace_new(instr: &Instr, temps: usize, out: &mut Vec<Instr>) -> bool {
push(OpCode::Swap, ImmOperand::None);
push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD));
for i in 0..n {
push(OpCode::GetLoc, temp(i));
for &n in new_locals.iter().rev() {
push(OpCode::GetLoc, ImmOperand::Loc(n));
}
push(OpCode::CallConstructor, instr.imm);
+10 -10
View File
@@ -110,24 +110,24 @@ The same thing happens on a large scale with partial types. Inside a walk, a cal
## 5. Representation in C
The C type of a value comes from its type only:
The C type of a value comes from its type only. A type with one member is that member's C type:
| type | C |
| member | C |
|---|---|
| `Unknown` | the code cannot run |
| `Undefined`, `Null` | `undefined_t`, `null_t`: unit types |
| `Bool` | `bool` |
| `Int` | `int32_t` |
| `Float` | `double` |
| `Str` | `string_t` |
| `Obj(cell)` | a pointer to the struct of the cell. The struct starts with a header that holds the cell's number |
| `Str` or one `Obj`, plus `Undefined` and/or `Null` | that pointer. `NULL` is null, and `JSRT_UNDEFINED` is undefined |
| `Obj` of several cells, maybe with `Undefined` and `Null` | a pointer to the header |
| any other sum, and `Any` | `any_t`: a tagged union of every kind |
| `Native` | `native_t`: the native's index |
| `Obj` | a pointer to the struct of the object's class |
- **A field that can be absent** holds a sentinel for absent: `JSRT_ABSENT` in a pointer, a tag in an `any_t`.
- **An op on a sum** tests which member the value is (by kind, by a sentinel, or by the cell's number in the header), and then works on that member.
- **An array's struct** has `length`, `cap` and `items` after its header. An element holds undefined for a hole.
A type with several members is a `jsrt_value`: a tag, which says the kind, and the value. A number in a `jsrt_value` is always a double, and an object's tag also says its class. `Unknown` means that the code cannot run.
- **The C of an op** is a macro of the runtime (lib/rt/core.h), which picks its code by the C types of its operands. A plain C value becomes a `jsrt_value` with a constant tag, so the C compiler removes the tag and keeps only the code for that kind.
- **A field that can be absent** has a `has_` flag beside it. Getting it calls a lookup, which goes along the prototype chain.
- **An op that depends on an object's class**, such as getting a field or calling a closure, tests the tag of a `jsrt_value` with a `switch`, and has code for each class.
- **An array's struct** has `length`, `cap` and `items`. An element holds undefined for a hole.
## 6. Not in this model yet
-299
View File
@@ -1,299 +0,0 @@
// 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_cells[((jsrt_header *)a.as.o)->cell];
}
// ── 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
+471 -36
View File
@@ -1,5 +1,7 @@
// Values and objects: the types that every other part uses, the object
// header and its tags, the allocator and errors. See docs/model.md
// Values: the types that every other part uses, the allocator, errors, and
// JS's operators on every kind of value.
// An object is a struct of its class, which the emitted C defines, and every
// pointer to one is a real object: undefined and null are values of their own
#pragma once
#include <stdbool.h>
@@ -9,43 +11,30 @@
typedef struct { char _; } undefined_t;
typedef struct { char _; } null_t;
// A native fn as a value: its index in the compiler's list (see native.rs)
typedef struct { int32_t idx; } native_t;
// A string: UTF-16 code units, as JS counts them
typedef struct jsrt_string *string_t;
// The start of every object: the number of its cell, which tells its struct
// and, for a fn, its code
typedef struct { uint32_t cell; } jsrt_header;
// A pointer to an object or a string can also be one of these. NULL is null.
// They are told apart by their address only
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 cell are fns, by cell. The emitted C defines it
extern const bool jsrt_fn_cells[];
// A new heap cell of `size` bytes, all zero
void *jsrt_alloc(size_t size);
void *__rt_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);
// `need` of them. `*cap` is the room that it has, and is set to the new room.
// The new room is all zero
void *__rt_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);
// Throws a TypeError, which says `what`. Exceptions are not supported yet, so
// it stops the program
_Noreturn void __rt_type_error(const char *what);
#ifdef JSRT_IMPLEMENTATION
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
jsrt_header jsrt_undefined_cell, jsrt_absent_cell;
void *jsrt_alloc(size_t size) {
void *__rt_alloc(size_t size) {
void *p = calloc(1, size);
if (!p) {
@@ -56,7 +45,7 @@ void *jsrt_alloc(size_t size) {
return p;
}
void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size) {
void *__rt_grow(void *items, int32_t *cap, int32_t need, size_t size) {
if (need <= *cap) {
return items;
}
@@ -69,21 +58,467 @@ void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size) {
abort();
}
memset((char *)items + (size_t)*cap * size, 0, (size_t)(room - *cap) * size);
*cap = room;
return items;
}
_Noreturn void jsrt_type_error(void) {
fputs("TypeError\n", stderr);
_Noreturn void __rt_type_error(const char *what) {
fprintf(stderr, "TypeError: %s\n", what);
exit(1);
}
void *jsrt_some(void *p) {
if (p == NULL || p == JSRT_UNDEFINED || p == JSRT_ABSENT) {
jsrt_type_error();
}
#endif // JSRT_IMPLEMENTATION
return p;
// ─────────────────────────────────── Values and operators ────────────────────────────────
//
// Infer gives each value a type. A value whose type has one member is a plain
// C value of that member's C type: undefined_t, null_t, bool, int32_t, double,
// string_t, native_t, or a pointer to an object's struct. A value whose type
// has several members is a jsrt_value: a tag, which says which kind it is, and
// the value. A number in a jsrt_value is always a double
//
// Each macro gives a result of one exact C type, because SET picks the
// conversion by it: a comparison is cast to bool, and a bitwise result to int32_t
//
// The macros here take a value of any of these C types. VALUE(x) makes a
// jsrt_value of any value, and each operator is one fn on jsrt_values. With a
// plain C value, the tag is a constant, so the C compiler removes the tag and
// the switch, and only the code for that kind is left
//
// The emitted C defines two things that the macros use:
// JSRT_CLASS(x) the class of `x`, a pointer to an object
// jsrt_class_is_function[k] whether the objects of class `k` are closures
//
// The array macros use their args more than once, so an arg must be a name
//
// The operators call str.h's and number.h's fns, so this part comes after them
#include <math.h>
#include <string.h>
#include "str.h"
#include "number.h"
enum {
JSRT_UNDEFINED,
JSRT_NULL,
JSRT_BOOL,
JSRT_NUMBER,
JSRT_STRING,
JSRT_NATIVE,
JSRT_OBJECT, // an object of class k has the tag JSRT_OBJECT + k
};
typedef struct {
uint32_t tag;
union {
bool b;
double n;
string_t s;
int32_t native;
void *o;
} as;
} jsrt_value;
extern const bool jsrt_class_is_function[];
#define JSRT_INLINE static inline __attribute__((always_inline))
#define JSRT_NO_CONVERSION "converting an object to a string or a number is not supported"
// ─────────────────────────────── Making and reading values ───────────────────────────────
// Any value, as a jsrt_value
#define VALUE(x) \
_Generic((x), \
undefined_t: jsrt_of_undefined, \
null_t: jsrt_of_null, \
bool: jsrt_of_bool, \
int32_t: jsrt_of_int, \
double: jsrt_of_number, \
string_t: jsrt_of_string, \
native_t: jsrt_of_native, \
jsrt_value: jsrt_of_value, \
default: jsrt_of_object)((x), JSRT_CLASS(x))
// A jsrt_value, as a value of the C type of `like`. Its kind must be one that
// the C type holds: infer makes sure of it
#define FROM_VALUE(like, v) \
_Generic((like), \
undefined_t: jsrt_to_undefined, \
null_t: jsrt_to_null, \
bool: jsrt_to_bool, \
int32_t: jsrt_to_int, \
double: jsrt_to_number, \
string_t: jsrt_to_string, \
native_t: jsrt_to_native, \
jsrt_value: jsrt_to_value, \
default: jsrt_to_object)(v)
// `dst = x`, with `x` converted to the C type of `dst`
#define SET(dst, x) ((dst) = FROM_VALUE(dst, VALUE(x)))
// `x`, converted to the C type `T`
#define CAST(T, x) FROM_VALUE((T){0}, VALUE(x))
JSRT_INLINE jsrt_value jsrt_of_undefined(undefined_t x, uint32_t class) {
(void)x, (void)class;
return (jsrt_value){.tag = JSRT_UNDEFINED};
}
#endif // JSRT_IMPLEMENTATION
JSRT_INLINE jsrt_value jsrt_of_null(null_t x, uint32_t class) {
(void)x, (void)class;
return (jsrt_value){.tag = JSRT_NULL};
}
JSRT_INLINE jsrt_value jsrt_of_bool(bool x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_BOOL, .as.b = x};
}
JSRT_INLINE jsrt_value jsrt_of_int(int32_t x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_NUMBER, .as.n = x};
}
JSRT_INLINE jsrt_value jsrt_of_number(double x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_NUMBER, .as.n = x};
}
JSRT_INLINE jsrt_value jsrt_of_string(string_t x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_STRING, .as.s = x};
}
JSRT_INLINE jsrt_value jsrt_of_native(native_t x, uint32_t class) {
(void)class;
return (jsrt_value){.tag = JSRT_NATIVE, .as.native = x.idx};
}
JSRT_INLINE jsrt_value jsrt_of_value(jsrt_value x, uint32_t class) {
(void)class;
return x;
}
JSRT_INLINE jsrt_value jsrt_of_object(const void *x, uint32_t class) {
return (jsrt_value){.tag = JSRT_OBJECT + class, .as.o = (void *)x};
}
// A read of a value as a kind that it is not is a bug in the compiler. The tag
// is a constant wherever the kind is known, so the check costs nothing there
JSRT_INLINE void jsrt_check(bool ok) {
if (!ok) __rt_type_error("jsrt: a value was read as a kind that it is not");
}
JSRT_INLINE undefined_t jsrt_to_undefined(jsrt_value v) {
jsrt_check(v.tag == JSRT_UNDEFINED);
return (undefined_t){0};
}
JSRT_INLINE null_t jsrt_to_null(jsrt_value v) {
jsrt_check(v.tag == JSRT_NULL);
return (null_t){0};
}
JSRT_INLINE bool jsrt_to_bool(jsrt_value v) {
jsrt_check(v.tag == JSRT_BOOL);
return v.as.b;
}
JSRT_INLINE int32_t jsrt_to_int(jsrt_value v) {
jsrt_check(v.tag == JSRT_NUMBER);
return (int32_t)v.as.n;
}
JSRT_INLINE double jsrt_to_number(jsrt_value v) {
jsrt_check(v.tag == JSRT_NUMBER);
return v.as.n;
}
JSRT_INLINE string_t jsrt_to_string(jsrt_value v) {
jsrt_check(v.tag == JSRT_STRING);
return v.as.s;
}
JSRT_INLINE native_t jsrt_to_native(jsrt_value v) {
jsrt_check(v.tag == JSRT_NATIVE);
return (native_t){v.as.native};
}
JSRT_INLINE jsrt_value jsrt_to_value(jsrt_value v) { return v; }
JSRT_INLINE void *jsrt_to_object(jsrt_value v) {
jsrt_check(v.tag >= JSRT_OBJECT);
return v.as.o;
}
// ────────────────────────────────────── Conversions ──────────────────────────────────────
// The strings that conversions and `typeof` give, made once
enum { JSRT_S_UNDEFINED, JSRT_S_NULL, JSRT_S_TRUE, JSRT_S_FALSE, JSRT_S_BOOLEAN,
JSRT_S_NUMBER, JSRT_S_STRING, JSRT_S_FUNCTION, JSRT_S_OBJECT };
static inline string_t jsrt_name(int name) {
static const char *const texts[] = {"undefined", "null", "true", "false", "boolean",
"number", "string", "function", "object"};
static string_t made[sizeof texts / sizeof *texts];
return made[name] ? made[name] : (made[name] = __rt_string_lit(texts[name]));
}
// JS's ToNumber. An object's conversion would call its methods, which is not supported
JSRT_INLINE double jsrt_number(jsrt_value v) {
switch (v.tag) {
case JSRT_UNDEFINED: return NAN;
case JSRT_NULL: return 0;
case JSRT_BOOL: return v.as.b;
case JSRT_NUMBER: return v.as.n;
case JSRT_STRING: return __rt_string_to_number(v.as.s);
default: __rt_type_error(JSRT_NO_CONVERSION);
}
}
// JS's ToString
JSRT_INLINE string_t jsrt_string(jsrt_value v) {
switch (v.tag) {
case JSRT_UNDEFINED: return jsrt_name(JSRT_S_UNDEFINED);
case JSRT_NULL: return jsrt_name(JSRT_S_NULL);
case JSRT_BOOL: return jsrt_name(v.as.b ? JSRT_S_TRUE : JSRT_S_FALSE);
case JSRT_NUMBER: return __rt_number_to_string(v.as.n);
case JSRT_STRING: return v.as.s;
default: __rt_type_error(JSRT_NO_CONVERSION);
}
}
// JS's ToBoolean
JSRT_INLINE bool jsrt_truthy(jsrt_value v) {
switch (v.tag) {
case JSRT_UNDEFINED: case JSRT_NULL: return false;
case JSRT_BOOL: return v.as.b;
case JSRT_NUMBER: return v.as.n == v.as.n && v.as.n != 0;
case JSRT_STRING: return __rt_string_length(v.as.s) != 0;
default: return true;
}
}
JSRT_INLINE bool jsrt_is_function(jsrt_value v) {
if (v.tag == JSRT_NATIVE) return true;
return v.tag >= JSRT_OBJECT && jsrt_class_is_function[v.tag - JSRT_OBJECT];
}
JSRT_INLINE string_t jsrt_typeof(jsrt_value v) {
switch (v.tag) {
case JSRT_UNDEFINED: return jsrt_name(JSRT_S_UNDEFINED);
case JSRT_BOOL: return jsrt_name(JSRT_S_BOOLEAN);
case JSRT_NUMBER: return jsrt_name(JSRT_S_NUMBER);
case JSRT_STRING: return jsrt_name(JSRT_S_STRING);
default: return jsrt_name(jsrt_is_function(v) ? JSRT_S_FUNCTION : JSRT_S_OBJECT);
}
}
// ─────────────────────────────────────── Operators ───────────────────────────────────────
// `a + b`: a concatenation if either side is a string or an object, else a sum
JSRT_INLINE jsrt_value jsrt_add(jsrt_value a, jsrt_value b) {
if (a.tag >= JSRT_STRING || b.tag >= JSRT_STRING) {
string_t s = __rt_string_concat(jsrt_string(a), jsrt_string(b));
return (jsrt_value){.tag = JSRT_STRING, .as.s = s};
}
return (jsrt_value){.tag = JSRT_NUMBER, .as.n = jsrt_number(a) + jsrt_number(b)};
}
// `a < b`, or `a <= b` if `or_equal`. Two strings compare as strings, anything else
// as numbers
JSRT_INLINE bool jsrt_less(jsrt_value a, jsrt_value b, bool or_equal) {
if (a.tag == JSRT_STRING && b.tag == JSRT_STRING) {
int32_t cmp = __rt_string_cmp(a.as.s, b.as.s);
return or_equal ? cmp <= 0 : cmp < 0;
}
double x = jsrt_number(a), y = jsrt_number(b);
return or_equal ? x <= y : x < y;
}
// `a === b`. Values of different kinds are never equal, and objects of
// different classes are different objects
JSRT_INLINE bool jsrt_strict_eq(jsrt_value a, jsrt_value b) {
if (a.tag != b.tag) return false;
switch (a.tag) {
case JSRT_UNDEFINED: case JSRT_NULL: return true;
case JSRT_BOOL: return a.as.b == b.as.b;
case JSRT_NUMBER: return a.as.n == b.as.n;
case JSRT_STRING: return __rt_string_eq(a.as.s, b.as.s);
case JSRT_NATIVE: return a.as.native == b.as.native;
default: return a.as.o == b.as.o;
}
}
// `a == b`. Undefined and null are equal to each other and to nothing else. Two
// objects compare as `===` does, and so do two values of one kind. Anything
// else compares as numbers
JSRT_INLINE bool jsrt_loose_eq(jsrt_value a, jsrt_value b) {
bool a_nullish = a.tag <= JSRT_NULL, b_nullish = b.tag <= JSRT_NULL;
bool objects = a.tag >= JSRT_NATIVE && b.tag >= JSRT_NATIVE;
if (a_nullish || b_nullish) return a_nullish && b_nullish;
if (objects || a.tag == b.tag) return jsrt_strict_eq(a, b);
return jsrt_number(a) == jsrt_number(b);
}
#define NUMBER(x) _Generic((x), double: (x), default: jsrt_number(VALUE(x)))
#define INT32(x) _Generic((x), int32_t: (x), default: __rt_to_int32(NUMBER(x)))
#define STRING(x) jsrt_string(VALUE(x))
#define TRUTHY(x) jsrt_truthy(VALUE(x))
// Arithmetic. Every number is a double, so every result is a double, except
// `+x` of an int
#define ADD(a, b) jsrt_add(VALUE(a), VALUE(b))
#define SUB(a, b) (NUMBER(a) - NUMBER(b))
#define MUL(a, b) (NUMBER(a) * NUMBER(b))
#define DIV(a, b) (NUMBER(a) / NUMBER(b))
#define MOD(a, b) fmod(NUMBER(a), NUMBER(b))
#define POW(a, b) __rt_pow(NUMBER(a), NUMBER(b))
#define NEG(x) (-NUMBER(x))
#define PLUS(x) _Generic((x), int32_t: (x), default: NUMBER(x))
#define INC(x) (NUMBER(x) + 1)
#define DEC(x) (NUMBER(x) - 1)
// `x++` and `x--`: `old` gets x as a number, and `new` gets the next number
#define POST_INC(old, new, x) \
do { \
double n = NUMBER(x); \
SET(old, n); \
SET(new, n + 1); \
} while (0)
#define POST_DEC(old, new, x) \
do { \
double n = NUMBER(x); \
SET(old, n); \
SET(new, n - 1); \
} while (0)
// Bitwise operators: on int32s, and a shift counts the low 5 bits only. `>>>`
// gives a uint32, which is a double
#define NOT(x) ((int32_t)~INT32(x))
#define AND(a, b) ((int32_t)(INT32(a) & INT32(b)))
#define OR(a, b) ((int32_t)(INT32(a) | INT32(b)))
#define XOR(a, b) ((int32_t)(INT32(a) ^ INT32(b)))
#define SHL(a, b) ((int32_t)((uint32_t)INT32(a) << (INT32(b) & 31)))
#define SAR(a, b) ((int32_t)(INT32(a) >> (INT32(b) & 31)))
#define SHR(a, b) ((double)((uint32_t)INT32(a) >> (INT32(b) & 31)))
// Comparisons. `a > b` is `b < a`, and `a >= b` is `b <= a`
#define LT(a, b) jsrt_less(VALUE(a), VALUE(b), false)
#define LTE(a, b) jsrt_less(VALUE(a), VALUE(b), true)
#define GT(a, b) jsrt_less(VALUE(b), VALUE(a), false)
#define GTE(a, b) jsrt_less(VALUE(b), VALUE(a), true)
#define STRICT_EQ(a, b) jsrt_strict_eq(VALUE(a), VALUE(b))
#define STRICT_NEQ(a, b) ((bool)!STRICT_EQ(a, b))
#define EQ(a, b) jsrt_loose_eq(VALUE(a), VALUE(b))
#define NEQ(a, b) ((bool)!EQ(a, b))
// Tests of one value
#define LNOT(x) ((bool)!TRUTHY(x))
#define TYPEOF(x) jsrt_typeof(VALUE(x))
#define IS_UNDEFINED(x) ((bool)(VALUE(x).tag == JSRT_UNDEFINED))
#define IS_NULL(x) ((bool)(VALUE(x).tag == JSRT_NULL))
#define IS_NULLISH(x) ((bool)(VALUE(x).tag <= JSRT_NULL))
#define IS_FUNCTION(x) jsrt_is_function(VALUE(x))
// ──────────────────────────────────── Natives' args ──────────────────────────────────────
// An arg of a native, as its param's C type. An int and a float both pass for a
// number, and an arg of any other wrong kind throws
#define ARG_INT(x) \
_Generic((x), int32_t: (x), default: __rt_to_int32(jsrt_arg_number(VALUE(x))))
#define ARG_NUMBER(x) jsrt_arg_number(VALUE(x))
#define ARG_STRING(x) jsrt_arg_string(VALUE(x))
#define ARG_UNDEFINED(x) jsrt_arg_undefined(VALUE(x))
#define JSRT_WRONG_ARG "a native got an arg of the wrong kind"
JSRT_INLINE double jsrt_arg_number(jsrt_value v) {
if (v.tag != JSRT_NUMBER) __rt_type_error(JSRT_WRONG_ARG);
return v.as.n;
}
JSRT_INLINE string_t jsrt_arg_string(jsrt_value v) {
if (v.tag != JSRT_STRING) __rt_type_error(JSRT_WRONG_ARG);
return v.as.s;
}
JSRT_INLINE undefined_t jsrt_arg_undefined(jsrt_value v) {
if (v.tag != JSRT_UNDEFINED) __rt_type_error(JSRT_WRONG_ARG);
return (undefined_t){0};
}
// ──────────────────────────────────── Arrays and cells ───────────────────────────────────
//
// Every array's struct has `length`, `cap` and `items`, so these work on any of them
// Whether `i`, a number, is an index below `length`
#define JSRT_IN_RANGE(i, length) ((i) >= 0 && (i) < (length) && (i) == (double)(int32_t)(i))
#define JSRT_BAD_INDEX "an array index must be a whole number from 0"
#define JSRT_BAD_LENGTH "an array's length must be a whole number from 0"
// `a = [...]` with room for `n` elements, which the caller sets
#define ARRAY_NEW(a, n) \
do { \
(a) = __rt_alloc(sizeof *(a)); \
(a)->items = __rt_grow(NULL, &(a)->cap, (n), sizeof *(a)->items); \
(a)->length = (n); \
} while (0)
// `dst = a[i]`: undefined if `i` is not an index in the array
#define ARRAY_GET(dst, a, i) \
do { \
double index = NUMBER(i); \
if (JSRT_IN_RANGE(index, (a)->length)) SET(dst, (a)->items[(int32_t)index]); \
else SET(dst, (undefined_t){0}); \
} while (0)
// `dst = s[i]`: undefined if `i` is not an index in the string
#define STRING_GET(dst, s, i) \
do { \
double index = NUMBER(i); \
if (!JSRT_IN_RANGE(index, __rt_string_length(s))) SET(dst, (undefined_t){0}); \
else SET(dst, __rt_string_at((s), (int32_t)index)); \
} while (0)
// `a[i] = v`. The array grows to hold `i`, and the elements between are holes.
// An `i` that is not an index would be a named field, which is not supported
#define ARRAY_PUT(a, i, v) \
do { \
double index = NUMBER(i); \
if (!JSRT_IN_RANGE(index, INT32_MAX)) __rt_type_error(JSRT_BAD_INDEX); \
int32_t at = (int32_t)index; \
if (at >= (a)->length) { \
(a)->items = __rt_grow((a)->items, &(a)->cap, at + 1, sizeof *(a)->items); \
(a)->length = at + 1; \
} \
SET((a)->items[at], v); \
} while (0)
// `a.length = n`: the array gets `n` elements, and the new ones are holes. `n`
// must be a whole number from 0, as JS's RangeError says
#define ARRAY_SET_LENGTH(a, n) \
do { \
double length = NUMBER(n); \
if (!JSRT_IN_RANGE(length, INT32_MAX)) __rt_type_error(JSRT_BAD_LENGTH); \
(a)->items = __rt_grow((a)->items, &(a)->cap, (int32_t)length, sizeof *(a)->items); \
(a)->length = (int32_t)length; \
} while (0)
// A new cell for `p`, a pointer to a cell, with the same value. Closures that
// point to the old cell keep it (see transform/closure.rs)
#define NEW_CELL(p) \
do { \
void *cell = __rt_alloc(sizeof *(p)); \
memcpy(cell, (p), sizeof *(p)); \
(p) = cell; \
} while (0)
+9 -9
View File
@@ -31,7 +31,7 @@
// ── sync ──
undefined_t __native_print_string(string_t s) {
char *utf8 = jsrt_string_utf8(s);
char *utf8 = __rt_string_utf8(s);
puts(utf8);
free(utf8);
return (undefined_t){0};
@@ -178,7 +178,7 @@ static void jsrt_io_release(int32_t 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();
__rt_type_error("an IO result read outside its op's callback");
}
return &jsrt_io.ops[h];
@@ -320,7 +320,7 @@ int32_t __native_io_queue_timeout(double ms) {
// `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].buf = __rt_string_utf8(path);
jsrt_io.ops[h].flags = flags | O_CLOEXEC;
jsrt_io.ops[h].mode = mode;
return jsrt_io_queue(h);
@@ -334,7 +334,7 @@ int32_t __native_io_queue_read(int32_t fd, int32_t len, double offset) {
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);
op->buf = __rt_alloc(op->len ? op->len : 1);
return jsrt_io_queue(h);
}
@@ -344,7 +344,7 @@ 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->buf = __rt_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);
@@ -359,9 +359,9 @@ int32_t __native_io_queue_close(int32_t fd) {
// 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);
char *utf8 = __rt_string_utf8(path);
size_t size = strlen(utf8) + 1;
char *buf = jsrt_alloc(sizeof(struct statx) + size);
char *buf = __rt_alloc(sizeof(struct statx) + size);
memcpy(buf + sizeof(struct statx), utf8, size);
free(utf8);
jsrt_io.ops[h].buf = buf;
@@ -373,7 +373,7 @@ int32_t __native_io_queue_stat(string_t path) {
// -EALREADY
int32_t __native_io_queue_cancel(int32_t target) {
if (target < 0 || target >= jsrt_io.cap) {
jsrt_type_error();
__rt_type_error("a cancel of an op that does not exist");
}
int32_t h = jsrt_io_new(JSRT_IO_CANCEL);
@@ -439,7 +439,7 @@ int32_t __native_io_status(int32_t h) {
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);
return __rt_string_from_utf8(op->buf, data ? (size_t)op->res : 0);
}
// The size of a stat's file. The status for any other op
+10 -7
View File
@@ -3,13 +3,17 @@
//
// 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
// that, and includes this before any other header
//
// 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
// The fns that the program can call have one of two prefixes:
// __rt_* the same on every platform: JS's operators, strings, numbers,
// the allocator and errors
// __native_* the platform's IO, which another platform's runtime replaces
//
// core.h values, the allocator, errors, and JS's operators on every
// kind of value
// str.h strings
// number.h numbers, and their conversions to and from strings
// io.h IO natives, async on io_uring (uring.h), and the host's loop
//
// The host owns the event loop. The program exports two fns, which the host
@@ -36,5 +40,4 @@ void jsrt_complete(int32_t h);
#include "str.h"
#include "number.h"
#include "any.h"
#include "io.h"
+33 -10
View File
@@ -1,8 +1,31 @@
// The number natives: conversions between numbers and strings
// Numbers: conversions between numbers and strings
#pragma once
#include "str.h"
// JS's Number::toString
string_t __rt_number_to_string(double x);
// JS's ToNumber on a string
double __rt_string_to_number(string_t s);
#include <math.h>
// JS's `x ** y`: C's pow, except that a NaN exponent always gives NaN, and so does
// an infinite exponent when |x| is 1. C gives 1 for both
static inline double __rt_pow(double x, double y) {
if (isnan(y) || (isinf(y) && fabs(x) == 1)) return NAN;
return pow(x, y);
}
// JS's ToInt32: the number modulo 2^32, as a signed int. NaN and the infinities are 0
static inline int32_t __rt_to_int32(double x) {
if (!isfinite(x)) return 0;
double m = fmod(trunc(x), 4294967296.0);
if (m < 0) m += 4294967296.0;
return (int32_t)(uint32_t)m;
}
#ifdef JSRT_IMPLEMENTATION
#include <math.h>
#include <stdio.h>
@@ -11,10 +34,10 @@
// 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");
string_t __rt_number_to_string(double x) {
if (isnan(x)) return __rt_string_lit("NaN");
if (isinf(x)) return __rt_string_lit(x < 0 ? "-Infinity" : "Infinity");
if (x == 0) return __rt_string_lit("0");
// The fewest digits: buf is [-]d[.ddd]e±xx
char buf[64];
@@ -61,19 +84,19 @@ string_t __native_number_to_string(double x) {
sprintf(o, "%c%s%se%c%d", digits[0], n > 1 ? "." : "", digits + 1, e < 0 ? '-' : '+', abs(e));
}
return jsrt_string_lit(out);
return __rt_string_lit(out);
}
string_t __native_number_to_fixed(double x, int32_t digits) {
string_t __rt_number_to_fixed(double x, int32_t digits) {
char buf[400];
snprintf(buf, sizeof buf, "%.*f", digits, x);
return jsrt_string_lit(buf);
return __rt_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);
double __rt_string_to_number(string_t s) {
char *utf8 = __rt_string_utf8(s);
char *start = utf8, *end;
while (*start == ' ' || (*start >= '\t' && *start <= '\r')) start++;
+57 -43
View File
@@ -1,40 +1,44 @@
// Strings: UTF-16 code units, as JS counts them, and the string natives
// Strings: UTF-16 code units, as JS counts them
#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>
// A string. The emitted C makes its literals as static strings of this
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));
string_t __rt_string_lit(const char *utf8);
string_t __rt_string_from_utf8(const char *utf8, size_t size);
int32_t __rt_string_length(string_t s);
bool __rt_string_eq(string_t a, string_t b);
int32_t __rt_string_cmp(string_t a, string_t b);
// `a + b`
string_t __rt_string_concat(string_t a, string_t b);
// `s[i]`: the code unit at `i`, as a string. `i` must be in the string
string_t __rt_string_at(string_t s, int32_t i);
#ifdef JSRT_IMPLEMENTATION
#include <stdlib.h>
#include <string.h>
static string_t __rt_string_new(int32_t length) {
string_t s = __rt_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));
string_t __rt_string_lit(const char *utf8) {
return __rt_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) {
string_t __rt_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);
string_t s = __rt_string_new((int32_t)size * 2);
int32_t n = 0;
while (p < end) {
@@ -59,15 +63,15 @@ string_t jsrt_string_from_utf8(const char *utf8, size_t size) {
return s;
}
int32_t jsrt_string_length(string_t s) {
int32_t __rt_string_length(string_t s) {
return s->length;
}
bool jsrt_string_eq(string_t a, string_t b) {
bool __rt_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 __rt_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++) {
@@ -79,19 +83,36 @@ int32_t jsrt_string_cmp(string_t a, string_t b) {
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 __rt_string_concat(string_t a, string_t b) {
string_t s = __rt_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;
}
// The strings of one ASCII unit, made once each, the first time they are needed
static string_t __rt_ascii_strings[128];
string_t __rt_string_at(string_t s, int32_t i) {
uint16_t unit = s->units[i];
if (unit < 128 && __rt_ascii_strings[unit]) {
return __rt_ascii_strings[unit];
}
string_t c = __rt_string_new(1);
c->units[0] = unit;
if (unit < 128) {
__rt_ascii_strings[unit] = c;
}
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);
static char *__rt_string_utf8(string_t s) {
char *utf8 = __rt_alloc((size_t)s->length * 3 + 1);
char *out = utf8;
for (int32_t i = 0; i < s->length; i++) {
@@ -124,30 +145,23 @@ static char *jsrt_string_utf8(string_t s) {
return utf8;
}
// ── natives ──
// ── for the stdlib ──
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) {
string_t __rt_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);
string_t r = __rt_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) {
int32_t __rt_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);
string_t __rt_string_from_char_code(int32_t code) {
string_t s = __rt_string_new(1);
s->units[0] = (uint16_t)code;
return s;
}
+3 -3
View File
@@ -8,7 +8,9 @@
// - 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) {
import { relativeIndex } from "jsrt:string";
export function Array(n) {
const a = [];
if (n !== undefined) a.length = n;
return a;
@@ -149,5 +151,3 @@ Array.prototype = {
return true;
},
};
__link_proto_array(Array.prototype);
+1 -3
View File
@@ -1,5 +1,5 @@
// Converts any value to a boolean, like the spec's ToBoolean
function Boolean(x) {
export function Boolean(x) {
return !!x;
}
@@ -8,5 +8,3 @@ Boolean.prototype = {
toString() { return this ? "true" : "false"; },
valueOf() { return this; },
};
__link_proto_boolean(Boolean.prototype);
+6 -2
View File
@@ -1,7 +1,11 @@
// Not as the spec says: it prints its first arg only, because more args need
// rest params, which need arrays
const console = {
import { __native_print_string } from "jsrt:native";
export const console = {
log(x) {
__native_print_string(String(x));
// As node does: `-0` prints as `-0`, though String(-0) is "0"
const negativeZero = typeof x === "number" && x === 0 && 1 / x < 0;
__native_print_string(negativeZero ? "-0" : String(x));
},
};
+64
View File
@@ -0,0 +1,64 @@
// The drivers of async fns and generators
//
// The compiler lowers each async fn and generator to a coroutine (see
// compiler/src/transform/async.rs): a step closure, which runs the body until
// it suspends or returns, and an object `co`:
//
// { __proto__: <a proto below>, step, value, awaiting, result, done }
//
// A yield puts its value in `value`, an await puts its promise in `awaiting`,
// and a return puts its value in `result` and sets `done`. The fn returns
// `co.start()`. Only these drivers differ between the kinds
//
// Not supported yet: `throw()` on a generator, and `finally` blocks in
// `return()`, which need exceptions
import { __is_promise, __reject, __resolve } from "jsrt:promise";
// A generator's object is `co` itself
export const __GeneratorProto = {
// The body runs up to its `initial_yield`, after its params
start() {
this.step(undefined);
return this;
},
next(input) {
if (this.done) return { value: undefined, done: true };
this.step(input);
if (this.done) return { value: this.result, done: true };
return { value: this.value, done: false };
},
return(value) {
this.done = true;
return { value: value, done: true };
},
};
// An async fn returns a promise, which its return fulfils. A rejected await
// rejects it, and the rest of the body does not run, as an uncaught throw at the
// await would
export const __AsyncProto = {
// The body runs up to its first await, in the call
start() {
const promise = new Promise(undefined);
this.run(promise);
return promise;
},
run(promise) {
this.step(undefined);
if (this.done) __resolve(promise, this.result);
else this.awaiting.then(() => this.run(promise), (reason) => __reject(promise, reason));
},
// What an await waits on: a promise as it is, any other value as a fulfilled one.
// The step reads the value from it when it resumes
toPromise(x) {
return __is_promise(x) ? x : Promise.resolve(x);
},
};
-3
View File
@@ -1,3 +0,0 @@
// Runs after the program's top level: its microtasks. The host then runs
// the event loop (see loop.js)
__drain_microtasks();
+6 -18
View File
@@ -1,19 +1,7 @@
// 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
// The global values that are not functions. `undefined` is a binding like any
// other: a global name refers to the prelude's export of that name (see
// prelude.js)
const NaN = 0 / 0;
const Infinity = 1 / 0;
export const undefined = void 0;
export const NaN = 0 / 0;
export const Infinity = 1 / 0;
+13 -1
View File
@@ -5,6 +5,18 @@
// 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
import {
__native_io_queue_close,
__native_io_queue_open,
__native_io_queue_read,
__native_io_queue_stat,
__native_io_queue_write,
__native_io_result_number,
__native_io_result_string,
__native_io_status,
} from "jsrt:native";
import { __pending } from "jsrt:loop";
// 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
@@ -39,7 +51,7 @@ function __io_ok(h) {
return __io_status(h).then((status) => (status < 0 ? Promise.reject(status) : status));
}
const fs = {
export const fs = {
readFile(path) {
let fd = -1;
let text = "";
+8 -9
View File
@@ -1,12 +1,12 @@
// The event loop's JS side. The host owns the loop (see lib/rt/jsrt.h). A
// macrotask is an IO op or a timer, which the host runs: it calls
// `__complete(h)` when op `h` is done. A microtask is a promise job, in
// `__microtasks`. After the program's top level, end.js runs all microtasks,
// and after each macrotask, `__complete` does
// `__microtasks`. After every module's body, the entry runs all microtasks
// (see compiler/src/link.rs), and after each macrotask, `__complete` does
// 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 = [];
export 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
@@ -14,11 +14,11 @@ const __pending = [];
// values of its own promise (see `docs/model.md`, section 3.3)
const __microtasks = { jobs: [], head: 0 };
function __queue_job(job) {
export function __queue_job(job) {
__microtasks.jobs.push(job);
}
function __drain_microtasks() {
export function __drain_microtasks() {
const q = __microtasks;
while (q.head < q.jobs.length) {
@@ -32,12 +32,11 @@ function __drain_microtasks() {
q.head = 0;
}
// What the host calls when op `h` is done: its callback, then the microtasks
function __complete(h) {
// What the host calls when op `h` is done: its callback, then the microtasks.
// The compiler finds it as this module's export
export function __complete(h) {
const done = __pending[h];
__pending[h] = undefined;
if (done !== undefined) done(h);
__drain_microtasks();
}
__link_host_complete(__complete);
+14 -8
View File
@@ -1,8 +1,16 @@
import {
__rt_number_to_fixed,
__rt_number_to_string,
__rt_string_concat,
__rt_string_from_char_code,
__rt_string_to_number,
} from "jsrt:native";
// Converts any value to a number, like the spec's ToNumber
function Number(x) {
export function Number(x) {
switch (typeof x) {
case "number": return x;
case "string": return __native_string_to_number(x);
case "string": return __rt_string_to_number(x);
case "boolean": return x ? 1 : 0;
case "undefined": return NaN;
case "object": return x === null ? 0 : NaN;
@@ -23,15 +31,13 @@ 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);
if (radix === undefined || radix === 10) return __rt_number_to_string(this);
return integerToString(this, radix);
},
toFixed(digits) { return __native_number_to_fixed(this, digits === undefined ? 0 : digits); },
toFixed(digits) { return __rt_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;
@@ -41,9 +47,9 @@ function integerToString(n, radix) {
let out = "";
do {
let digit = n % radix;
out = __native_string_concat(__native_string_from_char_code(digit < 10 ? 48 + digit : 87 + digit), out);
out = __rt_string_concat(__rt_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;
return negative ? __rt_string_concat("-", out) : out;
}
+1 -1
View File
@@ -1,5 +1,5 @@
// `new Object()` makes an empty object. Called without `new`, it does the same
function Object() {
export function Object() {
return {};
}
+42
View File
@@ -0,0 +1,42 @@
// The prelude: its exports are the global names of every module, such as
// `String` or `console`. A module that does not declare a name, and does not
// import it, refers to the prelude's export of that name. A module can declare
// a binding of the same name, which hides it (see compiler/src/link.rs)
//
// The stdlib is built on the natives in compiler/src/native.rs, the common
// layer of operations on primitive values, which `jsrt:native` exports. The
// compiler's own code for operators (`+` on strings and so on) uses the runtime
// directly, not these functions.
//
// Restrictions:
// - The prototypes of `String`, `Number`, `Boolean` and `Array` are fixed:
// the compiler reads them as the prelude's exports, as the prototypes of the
// primitives and of arrays. 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
// - A global name cannot be assigned to
//
// `String.prototype.concat` is not here, because it needs rest params, which
// need arrays. `console.log` prints one arg only, for the same reason
//
// The natives are global names too, as the runtime's own fns, with `__pending`,
// where an IO op's callback waits for the host (see loop.js). The compiler
// needs `jsrt:coroutine` for async fns and generators, so the prelude loads it.
// It finds the host's `__complete` in `jsrt:loop`
//
// The modules run in the order of these imports, before any other module
export { undefined, NaN, Infinity } from "jsrt:globals";
export { Object } from "jsrt:object";
export { String } from "jsrt:string";
export { Number } from "jsrt:number";
export { Boolean } from "jsrt:boolean";
export { Array } from "jsrt:array";
export { Promise, queueMicrotask } from "jsrt:promise";
export { setTimeout, clearTimeout } from "jsrt:timers";
export { fs } from "jsrt:io";
export { console } from "jsrt:console";
export * from "jsrt:native";
export { __pending } from "jsrt:loop";
import "jsrt:coroutine";
+52 -50
View File
@@ -15,22 +15,58 @@
// - A fn that makes a closure keeps out of the common path: a captured arg is
// in a box, which every call from one place shares
import { __queue_job } from "jsrt:loop";
// The states of a promise
const __PENDING = 0;
const __FULFILLED = 1;
const __REJECTED = 2;
class Promise {
constructor(executor) {
const p = { __proto__: this.__proto__, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
export function Promise(executor) {
const p = { __proto__: this.__proto__, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
if (executor !== undefined) {
executor((value) => __resolve(p, value), (reason) => __reject(p, reason));
}
return p;
if (executor !== undefined) {
executor((value) => __resolve(p, value), (reason) => __reject(p, reason));
}
return p;
}
Promise.resolve = function (value) {
if (__is_promise(value)) return value;
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
__resolve(p, value);
return p;
};
Promise.reject = function (reason) {
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
__reject(p, reason);
return p;
};
Promise.all = function (promises) {
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
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;
};
Promise.prototype = {
then(onFulfilled, onRejected) {
const derived = { __proto__: this.__proto__, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
let value;
@@ -68,11 +104,11 @@ class Promise {
}
return derived;
}
},
catch(onRejected) {
return this.then(undefined, onRejected);
}
},
finally(f) {
return this.then(
@@ -85,56 +121,22 @@ class Promise {
return Promise.reject(reason);
},
);
}
},
};
static resolve(value) {
if (__is_promise(value)) return value;
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
__resolve(p, value);
return p;
}
static reject(reason) {
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
__reject(p, reason);
return p;
}
static all(promises) {
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
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) {
export 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) {
export function __resolve(p, value) {
if (p.resolved) return;
p.resolved = true;
__fulfil_or_adopt(p, value);
}
// Reject `p` with `reason`, unless it was resolved or rejected before
function __reject(p, reason) {
export function __reject(p, reason) {
if (p.resolved) return;
p.resolved = true;
__settle_rejected(p, reason);
@@ -194,6 +196,6 @@ function __adopt(p, thenable) {
);
}
function queueMicrotask(f) {
export function queueMicrotask(f) {
__queue_job(f);
}
+31 -25
View File
@@ -1,8 +1,16 @@
import {
__rt_number_to_string,
__rt_string_char_code_at,
__rt_string_concat,
__rt_string_from_char_code,
__rt_string_slice,
} from "jsrt:native";
// Converts any value to a string, like the spec's ToString
function String(x) {
export function String(x) {
switch (typeof x) {
case "string": return x;
case "number": return __native_number_to_string(x);
case "number": return __rt_number_to_string(x);
case "boolean": return x ? "true" : "false";
case "undefined": return "undefined";
case "function": return "function () { [native code] }";
@@ -14,7 +22,7 @@ function String(x) {
}
String.fromCharCode = function (code) {
return __native_string_from_char_code(code);
return __rt_string_from_char_code(code);
};
// In these methods `this` is the primitive string itself
@@ -25,41 +33,41 @@ String.prototype = {
// ── Characters ──
charAt(i) {
if (i < 0 || i >= this.length) return "";
return __native_string_slice(this, i, i + 1);
return __rt_string_slice(this, i, i + 1);
},
charCodeAt(i) {
if (i < 0 || i >= this.length) return NaN;
return __native_string_char_code_at(this, i);
return __rt_string_char_code_at(this, i);
},
at(i) {
if (i < 0) i += this.length;
if (i < 0 || i >= this.length) return undefined;
return __native_string_slice(this, i, i + 1);
return __rt_string_slice(this, i, i + 1);
},
// ── Search ──
indexOf(search, from) {
let start = from === undefined ? 0 : clampIndex(from, this.length);
for (let i = start; i + search.length <= this.length; i++) {
if (__native_string_slice(this, i, i + search.length) === search) return i;
if (__rt_string_slice(this, i, i + search.length) === search) return i;
}
return -1;
},
lastIndexOf(search) {
for (let i = this.length - search.length; i >= 0; i--) {
if (__native_string_slice(this, i, i + search.length) === search) return i;
if (__rt_string_slice(this, i, i + search.length) === search) return i;
}
return -1;
},
includes(search, from) { return this.indexOf(search, from) !== -1; },
startsWith(search, from) {
let start = from === undefined ? 0 : clampIndex(from, this.length);
return __native_string_slice(this, start, start + search.length) === search;
return __rt_string_slice(this, start, start + search.length) === search;
},
endsWith(search, end) {
let stop = end === undefined ? this.length : clampIndex(end, this.length);
let start = stop - search.length;
return start >= 0 && __native_string_slice(this, start, stop) === search;
return start >= 0 && __rt_string_slice(this, start, stop) === search;
},
// ── Substrings ──
@@ -67,13 +75,13 @@ String.prototype = {
let len = this.length;
let s = relativeIndex(start === undefined ? 0 : start, len);
let e = relativeIndex(end === undefined ? len : end, len);
return s < e ? __native_string_slice(this, s, e) : "";
return s < e ? __rt_string_slice(this, s, e) : "";
},
substring(start, end) {
let len = this.length;
let s = clampIndex(start === undefined ? 0 : start, len);
let e = clampIndex(end === undefined ? len : end, len);
return s < e ? __native_string_slice(this, s, e) : __native_string_slice(this, e, s);
return s < e ? __rt_string_slice(this, s, e) : __rt_string_slice(this, e, s);
},
// ── Case ──
@@ -84,34 +92,32 @@ String.prototype = {
trim() { return this.trimStart().trimEnd(); },
trimStart() {
let i = 0;
while (i < this.length && isSpace(__native_string_char_code_at(this, i))) i++;
return __native_string_slice(this, i, this.length);
while (i < this.length && isSpace(__rt_string_char_code_at(this, i))) i++;
return __rt_string_slice(this, i, this.length);
},
trimEnd() {
let i = this.length;
while (i > 0 && isSpace(__native_string_char_code_at(this, i - 1))) i--;
return __native_string_slice(this, 0, i);
while (i > 0 && isSpace(__rt_string_char_code_at(this, i - 1))) i--;
return __rt_string_slice(this, 0, i);
},
// ── Building ──
repeat(count) {
let s = "";
for (let i = 0; i < count; i++) s = __native_string_concat(s, this);
for (let i = 0; i < count; i++) s = __rt_string_concat(s, this);
return s;
},
padStart(length, fill) { return pad(this, length, fill, true); },
padEnd(length, fill) { return pad(this, length, fill, false); },
};
__link_proto_string(String.prototype);
// An index clamped to [0, len]
function clampIndex(i, len) {
return i < 0 ? 0 : i > len ? len : i;
}
// An index where a negative value counts from the end, clamped to [0, len]
function relativeIndex(i, len) {
export function relativeIndex(i, len) {
return clampIndex(i < 0 ? i + len : i, len);
}
@@ -119,9 +125,9 @@ function relativeIndex(i, len) {
function mapCodes(s, lo, hi, shift) {
let out = "";
for (let i = 0; i < s.length; i++) {
let c = __native_string_char_code_at(s, i);
let c = __rt_string_char_code_at(s, i);
if (c >= lo && c <= hi) c += shift;
out = __native_string_concat(out, __native_string_from_char_code(c));
out = __rt_string_concat(out, __rt_string_from_char_code(c));
}
return out;
}
@@ -136,8 +142,8 @@ function pad(s, length, fill, atStart) {
if (s.length >= length || f.length === 0) return s;
let padding = "";
while (padding.length < length - s.length) padding = __native_string_concat(padding, f);
padding = __native_string_slice(padding, 0, length - s.length);
while (padding.length < length - s.length) padding = __rt_string_concat(padding, f);
padding = __rt_string_slice(padding, 0, length - s.length);
return atStart ? __native_string_concat(padding, s) : __native_string_concat(s, padding);
return atStart ? __rt_string_concat(padding, s) : __rt_string_concat(s, padding);
}
+5 -2
View File
@@ -4,7 +4,10 @@
//
// Not as the spec says: `setTimeout` takes no args for the callback
function setTimeout(f, ms) {
import { __native_io_queue_cancel, __native_io_queue_timeout } from "jsrt:native";
import { __pending } from "jsrt:loop";
export 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 };
@@ -18,7 +21,7 @@ function setTimeout(f, ms) {
return timer;
}
function clearTimeout(timer) {
export function clearTimeout(timer) {
if (timer === undefined || timer === null || !timer.active) return;
timer.active = false;
__native_io_queue_cancel(timer.handle);