Compare commits
9
Commits
2e6e4693aa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e12e7be2e | ||
|
|
f7c5b2c80f | ||
|
|
95e5c1225e | ||
|
|
cf455b88a8 | ||
|
|
a80bf6f19a | ||
|
|
1e4647347f | ||
|
|
d0c2f291e9 | ||
|
|
2f21347088 | ||
|
|
c4d0bab3d9 |
+21
-1
@@ -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
@@ -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");
|
||||
|
||||
+96
-88
@@ -1,102 +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)
|
||||
}
|
||||
// A function that never returns gives no value
|
||||
if *ret == Type::Unknown {
|
||||
return format!("{call};\nabort(); // does not return\n");
|
||||
}
|
||||
|
||||
if instr.op != OpCode::CallConstructor {
|
||||
return self.set(dst, &call);
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
|
||||
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
-158
@@ -1,180 +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],
|
||||
}),
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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(", "))
|
||||
}
|
||||
@@ -1,220 +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))))),
|
||||
_ => 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);
|
||||
}
|
||||
}
|
||||
@@ -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
-274
@@ -1,300 +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, 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");
|
||||
}
|
||||
}
|
||||
|
||||
// The walks that the code calls, from the entry. 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 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);
|
||||
// C has no empty structs
|
||||
if class.kind != ClassKind::Array && class.fields.is_empty() {
|
||||
c += " char _;\n";
|
||||
}
|
||||
|
||||
// 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
|
||||
);
|
||||
let _ = writeln!(out, "int main(void) {{\n{init} {}({entry_args});\n return 0;\n}}", e.fn_name(entry));
|
||||
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}")
|
||||
}
|
||||
|
||||
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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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![]);
|
||||
}
|
||||
|
||||
@@ -1,255 +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),
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
@@ -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
-487
@@ -1,539 +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);
|
||||
|
||||
#[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
|
||||
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::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
|
||||
}
|
||||
/// 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 },
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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"));
|
||||
|
||||
/// 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),
|
||||
version: 0,
|
||||
memory: Memory::default(),
|
||||
round: 0,
|
||||
};
|
||||
|
||||
let codes = (0..NATIVES.len()).map(Code::Native).chain(ProtoOf::ALL.map(Code::ProtoLink));
|
||||
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);
|
||||
|
||||
// The synthetic program entry is appended last by `flatten`. It is
|
||||
// called by the host with no args
|
||||
cache.call(fns.len() - 1, HOST, &[], 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 = ®_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()),
|
||||
_ => 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
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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:?}"),
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
+169
-114
@@ -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;
|
||||
|
||||
@@ -21,10 +25,10 @@ pub mod opcodes {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QuickJSBinary {
|
||||
pub version: u8,
|
||||
pub checksum: u32,
|
||||
pub atoms: Vec<Atom>,
|
||||
pub root_mod: Module,
|
||||
pub _version: u8,
|
||||
pub _checksum: u32,
|
||||
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,17 +39,24 @@ 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,
|
||||
QuickJSBinary {
|
||||
version,
|
||||
checksum,
|
||||
_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 {
|
||||
@@ -164,15 +189,13 @@ pub enum JsValue {
|
||||
/// array body; `raw` is the extra trailing value stored as `.raw`
|
||||
/// (`Undefined` when the template has no raw form).
|
||||
TemplateObject {
|
||||
cooked: Vec<JsValue>,
|
||||
raw: Box<JsValue>,
|
||||
_cooked: Vec<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),
|
||||
Module(Module),
|
||||
TypedArray,
|
||||
ArrayBuffer,
|
||||
@@ -240,8 +263,8 @@ impl JsValue {
|
||||
(
|
||||
bin,
|
||||
Self::TemplateObject {
|
||||
cooked,
|
||||
raw: Box::new(raw),
|
||||
_cooked: cooked,
|
||||
_raw: Box::new(raw),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -260,7 +283,7 @@ impl JsValue {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, TryFromPrimitive)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(u16)]
|
||||
pub enum FuncKind {
|
||||
Normal = 0,
|
||||
@@ -271,16 +294,16 @@ pub enum FuncKind {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FunctionFlags {
|
||||
pub has_prototype: bool,
|
||||
pub has_simple_param_list: bool,
|
||||
pub is_derived_class_constructor: bool,
|
||||
pub need_home_object: bool,
|
||||
pub _has_prototype: bool,
|
||||
pub _has_simple_param_list: bool,
|
||||
pub _is_derived_class_constructor: bool,
|
||||
pub _need_home_object: bool,
|
||||
pub func_kind: FuncKind,
|
||||
pub new_target_allowed: bool,
|
||||
pub super_call_allowed: bool,
|
||||
pub super_allowed: bool,
|
||||
pub arguments_allowed: bool,
|
||||
pub backtrace_barrier: bool,
|
||||
pub _new_target_allowed: bool,
|
||||
pub _super_call_allowed: bool,
|
||||
pub _super_allowed: bool,
|
||||
pub _arguments_allowed: bool,
|
||||
pub _backtrace_barrier: bool,
|
||||
pub has_debug: bool,
|
||||
}
|
||||
|
||||
@@ -291,23 +314,23 @@ impl FunctionFlags {
|
||||
Ok((
|
||||
bin,
|
||||
Self {
|
||||
has_prototype: raw & (1 << 0) != 0,
|
||||
has_simple_param_list: raw & (1 << 1) != 0,
|
||||
is_derived_class_constructor: raw & (1 << 2) != 0,
|
||||
need_home_object: raw & (1 << 3) != 0,
|
||||
_has_prototype: raw & (1 << 0) != 0,
|
||||
_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(),
|
||||
new_target_allowed: raw & (1 << 6) != 0,
|
||||
super_call_allowed: raw & (1 << 7) != 0,
|
||||
super_allowed: raw & (1 << 8) != 0,
|
||||
arguments_allowed: raw & (1 << 9) != 0,
|
||||
backtrace_barrier: raw & (1 << 10) != 0,
|
||||
_new_target_allowed: raw & (1 << 6) != 0,
|
||||
_super_call_allowed: raw & (1 << 7) != 0,
|
||||
_super_allowed: raw & (1 << 8) != 0,
|
||||
_arguments_allowed: raw & (1 << 9) != 0,
|
||||
_backtrace_barrier: raw & (1 << 10) != 0,
|
||||
has_debug: raw & (1 << 11) != 0,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum VarKind {
|
||||
Normal = 0,
|
||||
@@ -326,9 +349,9 @@ pub enum VarKind {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VarDefFlags {
|
||||
pub kind: VarKind,
|
||||
pub is_const: bool,
|
||||
pub is_lexical: bool,
|
||||
pub _kind: VarKind,
|
||||
pub _is_const: bool,
|
||||
pub _is_lexical: bool,
|
||||
pub is_captured: bool,
|
||||
}
|
||||
|
||||
@@ -344,9 +367,9 @@ impl VarDefFlags {
|
||||
Ok((
|
||||
bin,
|
||||
Self {
|
||||
kind,
|
||||
is_const,
|
||||
is_lexical,
|
||||
_kind: kind,
|
||||
_is_const: is_const,
|
||||
_is_lexical: is_lexical,
|
||||
is_captured,
|
||||
},
|
||||
))
|
||||
@@ -355,11 +378,11 @@ impl VarDefFlags {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VarDef {
|
||||
pub name: AtomRef,
|
||||
pub scope_level: usize,
|
||||
pub scope_next_plus_1: usize,
|
||||
pub flags: VarDefFlags,
|
||||
pub var_ref_idx: Option<usize>,
|
||||
pub _name: AtomRef,
|
||||
pub _scope_level: usize,
|
||||
pub _scope_next_plus_1: usize,
|
||||
pub _flags: VarDefFlags,
|
||||
pub _var_ref_idx: Option<usize>,
|
||||
}
|
||||
|
||||
impl VarDef {
|
||||
@@ -379,17 +402,17 @@ impl VarDef {
|
||||
Ok((
|
||||
bin,
|
||||
Self {
|
||||
name,
|
||||
scope_level,
|
||||
scope_next_plus_1,
|
||||
flags,
|
||||
var_ref_idx,
|
||||
_name: name,
|
||||
_scope_level: scope_level,
|
||||
_scope_next_plus_1: scope_next_plus_1,
|
||||
_flags: flags,
|
||||
_var_ref_idx: var_ref_idx,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, TryFromPrimitive)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum ClosureKind {
|
||||
Local = 0,
|
||||
@@ -402,12 +425,12 @@ pub enum ClosureKind {
|
||||
ModuleImport = 7,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClosureVarFlags {
|
||||
pub closure_kind: ClosureKind,
|
||||
pub is_const: bool,
|
||||
pub _is_const: bool,
|
||||
pub is_lexical: bool,
|
||||
pub var_kind: VarKind,
|
||||
pub _var_kind: VarKind,
|
||||
}
|
||||
|
||||
impl ClosureVarFlags {
|
||||
@@ -424,15 +447,15 @@ impl ClosureVarFlags {
|
||||
bin,
|
||||
Self {
|
||||
closure_kind,
|
||||
is_const,
|
||||
_is_const: is_const,
|
||||
is_lexical,
|
||||
var_kind,
|
||||
_var_kind: var_kind,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClosureVar {
|
||||
pub name: AtomRef,
|
||||
pub var_idx: usize,
|
||||
@@ -487,7 +510,7 @@ pub enum ImmOperand {
|
||||
/// (the scope chain entry point, FORMAT.md §3.3).
|
||||
NpopU16 {
|
||||
npop: u16,
|
||||
scope: u16,
|
||||
_scope: u16,
|
||||
},
|
||||
/// Jump. The wire value is a signed offset from the operand
|
||||
/// field's own position; `target` is already resolved.
|
||||
@@ -501,22 +524,25 @@ pub enum ImmOperand {
|
||||
},
|
||||
AtomU16 {
|
||||
atom: AtomRef,
|
||||
arg: u16,
|
||||
_arg: u16,
|
||||
},
|
||||
AtomLabelU8 {
|
||||
atom: AtomRef,
|
||||
target: usize,
|
||||
arg: u8,
|
||||
_atom: AtomRef,
|
||||
_target: usize,
|
||||
_arg: u8,
|
||||
},
|
||||
AtomLabelU16 {
|
||||
atom: AtomRef,
|
||||
target: usize,
|
||||
arg: u16,
|
||||
_atom: AtomRef,
|
||||
_target: usize,
|
||||
_arg: u16,
|
||||
},
|
||||
LabelU16 {
|
||||
target: usize,
|
||||
arg: u16,
|
||||
_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 {
|
||||
@@ -618,7 +644,7 @@ fn parse_bytecode(code: &[u8]) -> IResult<&[u8], Vec<Instr>> {
|
||||
opcodes::OpFmt::Npop => ImmOperand::Npop(u16_at(0)),
|
||||
opcodes::OpFmt::NpopU16 => ImmOperand::NpopU16 {
|
||||
npop: u16_at(0),
|
||||
scope: u16_at(2),
|
||||
_scope: u16_at(2),
|
||||
},
|
||||
opcodes::OpFmt::Loc => ImmOperand::Loc(u16_at(0)),
|
||||
opcodes::OpFmt::Arg => ImmOperand::Arg(u16_at(0)),
|
||||
@@ -639,21 +665,21 @@ fn parse_bytecode(code: &[u8]) -> IResult<&[u8], Vec<Instr>> {
|
||||
},
|
||||
opcodes::OpFmt::AtomU16 => ImmOperand::AtomU16 {
|
||||
atom: AtomRef::from_code_u32(u32_at(0)),
|
||||
arg: u16_at(4),
|
||||
_arg: u16_at(4),
|
||||
},
|
||||
opcodes::OpFmt::AtomLabelU8 => ImmOperand::AtomLabelU8 {
|
||||
atom: AtomRef::from_code_u32(u32_at(0)),
|
||||
target: jump(atom_label_base, u32_at(4) as i32 as i64),
|
||||
arg: u8_at(8),
|
||||
_atom: AtomRef::from_code_u32(u32_at(0)),
|
||||
_target: jump(atom_label_base, u32_at(4) as i32 as i64),
|
||||
_arg: u8_at(8),
|
||||
},
|
||||
opcodes::OpFmt::AtomLabelU16 => ImmOperand::AtomLabelU16 {
|
||||
atom: AtomRef::from_code_u32(u32_at(0)),
|
||||
target: jump(atom_label_base, u32_at(4) as i32 as i64),
|
||||
arg: u16_at(8),
|
||||
_atom: AtomRef::from_code_u32(u32_at(0)),
|
||||
_target: jump(atom_label_base, u32_at(4) as i32 as i64),
|
||||
_arg: u16_at(8),
|
||||
},
|
||||
opcodes::OpFmt::LabelU16 => ImmOperand::LabelU16 {
|
||||
target: jump(pc + 1, u32_at(0) as i32 as i64),
|
||||
arg: u16_at(4),
|
||||
_target: jump(pc + 1, u32_at(0) as i32 as i64),
|
||||
_arg: u16_at(4),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -672,17 +698,17 @@ fn parse_bytecode(code: &[u8]) -> IResult<&[u8], Vec<Instr>> {
|
||||
#[derive(Debug)]
|
||||
pub struct Function {
|
||||
pub flags: FunctionFlags,
|
||||
pub is_strict_mode: bool,
|
||||
pub _is_strict_mode: bool,
|
||||
pub name: AtomRef,
|
||||
pub arg_count: usize,
|
||||
/// Num locals
|
||||
pub var_count: usize,
|
||||
/// Parameters before the first default or rest, this is Function.prototype.length
|
||||
pub defined_arg_count: usize,
|
||||
pub stack_size: usize,
|
||||
pub var_ref_count: usize,
|
||||
pub _defined_arg_count: usize,
|
||||
pub _stack_size: usize,
|
||||
pub _var_ref_count: usize,
|
||||
/// arguments first, then locals
|
||||
pub var_defs: Vec<VarDef>,
|
||||
pub _var_defs: Vec<VarDef>,
|
||||
pub closure_vars: Vec<ClosureVar>,
|
||||
pub cpool: Vec<JsValue>,
|
||||
/// LLM GENERATED, TENTATIVE
|
||||
@@ -729,14 +755,14 @@ impl Function {
|
||||
bin,
|
||||
Self {
|
||||
flags,
|
||||
is_strict_mode: strict_mode == 1,
|
||||
_is_strict_mode: strict_mode == 1,
|
||||
name,
|
||||
arg_count,
|
||||
var_count,
|
||||
defined_arg_count,
|
||||
stack_size,
|
||||
var_ref_count,
|
||||
var_defs,
|
||||
_defined_arg_count: defined_arg_count,
|
||||
_stack_size: stack_size,
|
||||
_var_ref_count: var_ref_count,
|
||||
_var_defs: var_defs,
|
||||
closure_vars,
|
||||
cpool,
|
||||
byte_code,
|
||||
@@ -747,13 +773,13 @@ impl Function {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Module {
|
||||
pub name: AtomRef,
|
||||
pub import_modules: Vec<AtomRef>,
|
||||
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>,
|
||||
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,
|
||||
pub root_fn: Function, // the module's body
|
||||
}
|
||||
|
||||
impl Module {
|
||||
@@ -781,7 +807,7 @@ impl Module {
|
||||
bin,
|
||||
Self {
|
||||
name,
|
||||
import_modules,
|
||||
requests: import_modules,
|
||||
exports,
|
||||
star_exports,
|
||||
imports,
|
||||
@@ -792,51 +818,64 @@ impl Module {
|
||||
}
|
||||
}
|
||||
|
||||
#[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, 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,
|
||||
name,
|
||||
req_module_idx,
|
||||
request,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -883,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('"')?;
|
||||
|
||||
@@ -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
@@ -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(®_fns);
|
||||
|
||||
let c = emit::emit(®_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
-147
@@ -1,172 +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_wait"() -> 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),
|
||||
];
|
||||
|
||||
/// The value of the global `name`. The only globals are the ones the stdlib
|
||||
/// may read, anything else is an error
|
||||
pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue {
|
||||
let name = atom_name(name, atoms);
|
||||
|
||||
// A global property in JS, but a constant
|
||||
if name == "undefined" {
|
||||
return JsValue::Undefined;
|
||||
}
|
||||
|
||||
if let Some(idx) = NATIVES.iter().position(|n| name == n.name) {
|
||||
return JsValue::NativeFn(idx);
|
||||
}
|
||||
|
||||
match PROTO_LINKS.iter().find(|(link, _)| name == *link) {
|
||||
Some(&(_, kind)) => JsValue::ProtoLink(kind),
|
||||
None => panic!("unknown global `{name}`"),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a call of native `idx` returns, with args of these types, in source
|
||||
/// order. An arg that cannot be of its param's kind makes the call throw, so
|
||||
/// the call gives nothing. 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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)?;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
+29
-14
@@ -2,9 +2,10 @@
|
||||
// io_uring (see uring.h)
|
||||
//
|
||||
// A `__native_io_queue_<op>` queues an op and gives its handle. It makes no
|
||||
// syscall. `__native_io_wait` puts queued ops into the SQ, submits them,
|
||||
// waits until one op is done, and gives its handle. The getters then read
|
||||
// that op's result, until the next wait
|
||||
// syscall, and no native waits. The host's loop (`main` below) puts queued
|
||||
// ops into the SQ, submits them, waits until one op is done, and gives its
|
||||
// handle to `jsrt_complete`. The getters read that op's result while
|
||||
// `jsrt_complete` runs
|
||||
//
|
||||
// At most `cq_entries` ops are in flight, so the CQ cannot overflow. The rest
|
||||
// stay queued until a later wait. So a queue never fails, and the program
|
||||
@@ -30,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};
|
||||
@@ -177,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];
|
||||
@@ -319,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);
|
||||
@@ -333,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);
|
||||
}
|
||||
|
||||
@@ -343,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);
|
||||
@@ -358,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;
|
||||
@@ -372,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);
|
||||
@@ -392,8 +393,8 @@ int32_t __native_io_queue_cancel(int32_t target) {
|
||||
}
|
||||
|
||||
// The handle of an op that is done, or -1 if no op is queued or in flight.
|
||||
// The op that the last wait gave is freed now
|
||||
int32_t __native_io_wait(void) {
|
||||
// The op that the last call gave is freed now
|
||||
static int32_t jsrt_io_next(void) {
|
||||
if (jsrt_io.given >= 0) {
|
||||
jsrt_io_release(jsrt_io.given);
|
||||
jsrt_io.given = -1;
|
||||
@@ -438,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
|
||||
@@ -452,4 +453,18 @@ double __native_io_result_number(int32_t h) {
|
||||
return op->res;
|
||||
}
|
||||
|
||||
#ifndef JSRT_NO_HOST
|
||||
// The Linux flavor's host: run the program, then give it each op as it
|
||||
// completes, until none is left
|
||||
int main(void) {
|
||||
jsrt_start();
|
||||
|
||||
for (int32_t h; (h = jsrt_io_next()) >= 0;) {
|
||||
jsrt_complete(h);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // JSRT_IMPLEMENTATION
|
||||
|
||||
+25
-8
@@ -3,14 +3,26 @@
|
||||
//
|
||||
// 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
|
||||
// io.h IO natives, async on io_uring (uring.h)
|
||||
// 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
|
||||
// calls, and a flavor of the runtime is the host. This one, for Linux, is the
|
||||
// `main` in io.h: define JSRT_NO_HOST to bring another
|
||||
//
|
||||
// jsrt_start() runs the program's top level, then its microtasks
|
||||
// jsrt_complete(h) op `h` is done: runs its callback, then the microtasks.
|
||||
// The op's result can be read only until this returns
|
||||
#pragma once
|
||||
|
||||
// io.h needs Linux's syscall() and mmap(), which -std=c11 hides. This must
|
||||
@@ -20,7 +32,12 @@
|
||||
#endif
|
||||
|
||||
#include "core.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void jsrt_start(void);
|
||||
void jsrt_complete(int32_t h);
|
||||
|
||||
#include "str.h"
|
||||
#include "number.h"
|
||||
#include "any.h"
|
||||
#include "io.h"
|
||||
|
||||
+33
-10
@@ -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
@@ -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
@@ -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,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);
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
// Runs after the program: the event loop (see loop.js)
|
||||
__run_loop();
|
||||
+6
-18
@@ -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
@@ -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 = "";
|
||||
|
||||
+15
-19
@@ -1,12 +1,12 @@
|
||||
// The event loop. A macrotask is an IO op or a timer, which completes in C
|
||||
// (see lib/rt/io.h): `__native_io_wait()` gives the handle of the next one
|
||||
// that completes. A microtask is a promise job, in `__microtasks`. After the
|
||||
// program, end.js runs the loop: all microtasks, then one macrotask, then all
|
||||
// microtasks again, until nothing is pending
|
||||
// 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 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,15 +32,11 @@ function __drain_microtasks() {
|
||||
q.head = 0;
|
||||
}
|
||||
|
||||
function __run_loop() {
|
||||
for (;;) {
|
||||
__drain_microtasks();
|
||||
|
||||
const h = __native_io_wait();
|
||||
if (h < 0) return;
|
||||
|
||||
const done = __pending[h];
|
||||
__pending[h] = undefined;
|
||||
if (done !== undefined) done(h);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
|
||||
+14
-8
@@ -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,5 +1,5 @@
|
||||
// `new Object()` makes an empty object. Called without `new`, it does the same
|
||||
function Object() {
|
||||
export function Object() {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user