Compare commits
20
Commits
61c4971fa6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e12e7be2e | ||
|
|
f7c5b2c80f | ||
|
|
95e5c1225e | ||
|
|
cf455b88a8 | ||
|
|
a80bf6f19a | ||
|
|
1e4647347f | ||
|
|
d0c2f291e9 | ||
|
|
2f21347088 | ||
|
|
c4d0bab3d9 | ||
|
|
2e6e4693aa | ||
|
|
316a2fcd75 | ||
|
|
2b612d19ed | ||
|
|
44498b32a9 | ||
|
|
23a2c3bc46 | ||
|
|
caa57fa8b9 | ||
|
|
a4d295fbd9 | ||
|
|
e93ce94ac7 | ||
|
|
183c4e35c9 | ||
|
|
1a36873812 | ||
|
|
5d687e308e |
@@ -1,3 +1,6 @@
|
||||
[submodule "third_party/quickjs"]
|
||||
path = third_party/quickjs
|
||||
url = https://github.com/quickjs-ng/quickjs.git
|
||||
[submodule "third_party/stb"]
|
||||
path = third_party/stb
|
||||
url = https://github.com/nothings/stb.git
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# ReedJSC
|
||||
|
||||
Reed is a static, ahead-of-time JavaScript compiler with whole-program type inference
|
||||
|
||||
Reed uses [QuickJS-NG](https://github.com/quickjs-ng/quickjs) as a frontend and transforms QuickJS bytecode to a form suitable for C code emission
|
||||
|
||||
> [!WARNING]
|
||||
> Work in progress!
|
||||
+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,5 +4,6 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
cc = "1.5.1"
|
||||
nom = "8.0.0"
|
||||
num_enum = "0.7.6"
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
const QUICKJS: &str = "../third_party/quickjs/build";
|
||||
|
||||
fn main() {
|
||||
// The compiler compiles the C it emits for the target it is built for (see
|
||||
// main.rs)
|
||||
println!(
|
||||
"cargo:rustc-env=TARGET={}",
|
||||
std::env::var("TARGET").unwrap()
|
||||
);
|
||||
|
||||
println!("cargo:rustc-link-search=native={QUICKJS}");
|
||||
println!("cargo:rustc-link-lib=static=qjs");
|
||||
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
use crate::{
|
||||
flatten::FlatFn,
|
||||
libqjs::parse::{AtomRef, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode},
|
||||
};
|
||||
|
||||
pub fn declosure(flat_fns: Vec<FlatFn>, captures: &[Vec<ClosureVar>]) -> Vec<FlatFn> {
|
||||
flat_fns
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| declosure_one(i, f, captures))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn declosure_one(fn_id: usize, f: FlatFn, captures: &[Vec<ClosureVar>]) -> FlatFn {
|
||||
let mut out_len = 0;
|
||||
|
||||
for instr in &f.code {
|
||||
if let Some(info) = is_fclosure(instr, &f, captures) {
|
||||
out_len += 1 + (2 * info.child_captures.len()) + 1 + 1; // object; (push; define_field)*; push_fn_const; define_field
|
||||
} else if is_capture_acc(instr).is_some() {
|
||||
out_len += 2;
|
||||
} else {
|
||||
out_len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(out_len);
|
||||
|
||||
for instr in &f.code {
|
||||
if replace_fclosure(instr, &f, captures, &mut out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if replace_capture_accs(instr, &captures[fn_id], &mut out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push(instr.clone());
|
||||
}
|
||||
|
||||
FlatFn {
|
||||
name: f.name,
|
||||
arg_count: f.arg_count,
|
||||
var_count: f.var_count,
|
||||
cpool: f.cpool,
|
||||
code: out,
|
||||
}
|
||||
}
|
||||
|
||||
struct FClosureInfo<'a> {
|
||||
cpool_idx: u32,
|
||||
child_captures: &'a [ClosureVar],
|
||||
}
|
||||
|
||||
fn is_fclosure<'a>(
|
||||
instr: &Instr,
|
||||
f: &FlatFn,
|
||||
captures: &'a [Vec<ClosureVar>],
|
||||
) -> Option<FClosureInfo<'a>> {
|
||||
if instr.op != OpCode::Fclosure8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cpool_idx = match instr.imm_operands {
|
||||
ImmOperand::Const(idx) => idx,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let child_fn_id = match f.cpool[cpool_idx as usize] {
|
||||
JsValue::FnById(id) => id,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Some(FClosureInfo {
|
||||
cpool_idx,
|
||||
child_captures: &captures[child_fn_id],
|
||||
})
|
||||
}
|
||||
|
||||
fn replace_fclosure(
|
||||
instr: &Instr,
|
||||
f: &FlatFn,
|
||||
captures: &[Vec<ClosureVar>],
|
||||
out: &mut Vec<Instr>,
|
||||
) -> bool {
|
||||
let info = match is_fclosure(instr, f, captures) {
|
||||
Some(info) => info,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let pc = instr.pc;
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::Object,
|
||||
imm_operands: ImmOperand::None,
|
||||
});
|
||||
|
||||
for c in info.child_captures {
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::GetLoc,
|
||||
imm_operands: ImmOperand::Loc(c.var_idx as u16),
|
||||
});
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::DefineField,
|
||||
imm_operands: ImmOperand::Atom(c.name),
|
||||
});
|
||||
}
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::PushConst,
|
||||
imm_operands: ImmOperand::Const(info.cpool_idx),
|
||||
});
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::DefineField,
|
||||
imm_operands: ImmOperand::Atom(AtomRef::Predefined(0)),
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
enum CaptureAccess {
|
||||
Read(u16),
|
||||
Write(u16),
|
||||
}
|
||||
|
||||
fn is_capture_acc(instr: &Instr) -> Option<CaptureAccess> {
|
||||
let acc = match instr.op {
|
||||
OpCode::GetVarRef0 => CaptureAccess::Read(0),
|
||||
OpCode::GetVarRef1 => CaptureAccess::Read(1),
|
||||
OpCode::GetVarRef2 => CaptureAccess::Read(2),
|
||||
OpCode::GetVarRef3 => CaptureAccess::Read(3),
|
||||
OpCode::GetVarRef | OpCode::GetVarRefCheck => match instr.imm_operands {
|
||||
ImmOperand::VarRef(var) => CaptureAccess::Read(var),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
|
||||
OpCode::PutVarRef0 => CaptureAccess::Write(0),
|
||||
OpCode::PutVarRef1 => CaptureAccess::Write(1),
|
||||
OpCode::PutVarRef2 => CaptureAccess::Write(2),
|
||||
OpCode::PutVarRef3 => CaptureAccess::Write(3),
|
||||
OpCode::PutVarRef | OpCode::PutVarRefCheck => match instr.imm_operands {
|
||||
ImmOperand::VarRef(var) => CaptureAccess::Write(var),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(acc)
|
||||
}
|
||||
|
||||
fn replace_capture_accs(instr: &Instr, my_captures: &[ClosureVar], out: &mut Vec<Instr>) -> bool {
|
||||
let acc = is_capture_acc(instr);
|
||||
|
||||
let acc = match acc {
|
||||
Some(acc) => acc,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let pc = instr.pc;
|
||||
|
||||
match acc {
|
||||
CaptureAccess::Read(read_var) => {
|
||||
let atom = my_captures[read_var as usize].name;
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::GetArg,
|
||||
imm_operands: ImmOperand::Arg(u16::MAX),
|
||||
});
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::GetField,
|
||||
imm_operands: ImmOperand::Atom(atom),
|
||||
});
|
||||
}
|
||||
|
||||
CaptureAccess::Write(write_var) => {
|
||||
let atom = my_captures[write_var as usize].name;
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::GetArg,
|
||||
imm_operands: ImmOperand::Arg(u16::MAX),
|
||||
});
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: OpCode::PutField,
|
||||
imm_operands: ImmOperand::Atom(atom),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//! 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::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 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"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 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!()
|
||||
};
|
||||
|
||||
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 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(),
|
||||
};
|
||||
|
||||
format!("CAST({param}, {arg})")
|
||||
})
|
||||
.collect();
|
||||
|
||||
let call = format!("js{walk}({})", args.join(", "));
|
||||
let ret = program.cache.ret(walk);
|
||||
let dst = instr.dsts[0];
|
||||
|
||||
// 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(", ")),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
//! 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 std::collections::HashMap;
|
||||
|
||||
use crate::infer::{Addr, Memory, ObjAddr, Shape, Type, join};
|
||||
use crate::libqjs::parse::AtomRef;
|
||||
|
||||
/// 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 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,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 classes = objs_of_class.into_iter();
|
||||
let classes = classes
|
||||
.map(|objs| make_class(memory, &class_of, objs))
|
||||
.collect();
|
||||
Classes { classes, class_of }
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut class_of = number(&objs, first_key);
|
||||
|
||||
loop {
|
||||
let count = class_of.values().max().map_or(0, |&n| n + 1);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
class_of = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
objs.iter()
|
||||
.map(|&obj| {
|
||||
let next = ids.len();
|
||||
(obj, *ids.entry(key(obj)).or_insert(next))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -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(", "))
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Emits the program as one C file
|
||||
//!
|
||||
//! 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:
|
||||
//!
|
||||
//! - **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 functions;
|
||||
mod js;
|
||||
mod objects;
|
||||
mod program;
|
||||
|
||||
use crate::{infer::TypeCache, libqjs::parse::Atom, link::Link, regify::RegFn};
|
||||
|
||||
use classes::ClassKind;
|
||||
use program::Program;
|
||||
|
||||
const HEADER: &str = "// Generated by jsrt\n#define JSRT_IMPLEMENTATION\n#include \"jsrt.h\"\n\n";
|
||||
|
||||
/// 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);
|
||||
|
||||
// Types. Each struct is declared first, because a struct can point to another
|
||||
for class in classes.clone() {
|
||||
c += &format!("struct c{class};\n");
|
||||
}
|
||||
|
||||
c += &class_table(&program);
|
||||
|
||||
for class in classes {
|
||||
c += &struct_definition(&program, class);
|
||||
}
|
||||
|
||||
// Statics
|
||||
for (id, text) in program.strings.iter().enumerate() {
|
||||
c += &string_definition(id, text);
|
||||
}
|
||||
|
||||
for (g, ctype) in program.globals.iter().enumerate() {
|
||||
c += &format!("static {} g{g};\n", ctype.c_name());
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
// C has no empty structs
|
||||
if class.kind != ClassKind::Array && class.fields.is_empty() {
|
||||
c += " char _;\n";
|
||||
}
|
||||
|
||||
c + "};\n\n"
|
||||
}
|
||||
|
||||
/// 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")
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//! 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::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 JsFunction<'_> {
|
||||
pub(super) fn object_instr(&self, idx: usize, instr: &RegInstr) -> String {
|
||||
use OpCode::*;
|
||||
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.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"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `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]);
|
||||
|
||||
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.split(instr.args[1], |obj, _| format!("{obj}->{name} = {cell};\n"))
|
||||
}
|
||||
|
||||
/// `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")
|
||||
})
|
||||
}
|
||||
|
||||
/// `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[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]));
|
||||
|
||||
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"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
+84
-9
@@ -1,19 +1,92 @@
|
||||
use crate::libqjs::parse::{Atom, AtomRef, ClosureVar, Function, Instr, JsValue, QuickJSBinary};
|
||||
//! 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, 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![];
|
||||
walk(bin.root_mod.root_fn, &mut fns, &mut captures);
|
||||
(fns, captures, bin.atoms)
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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![];
|
||||
|
||||
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 {
|
||||
@@ -21,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();
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
infer::Type::ReturnOf,
|
||||
libqjs::parse::{Atom, AtomRef, ImmOperand, JsString, JsValue, atom_names, opcodes::OpCode},
|
||||
regify::RegFn,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum FnKind {
|
||||
Id(usize),
|
||||
DefineField(AtomRef),
|
||||
PutField(AtomRef),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Unknown,
|
||||
Undefined,
|
||||
Bool,
|
||||
Int,
|
||||
Float,
|
||||
Empty,
|
||||
Fn(FnKind),
|
||||
ReturnOf((FnKind, Vec<Type>)),
|
||||
Any,
|
||||
}
|
||||
|
||||
/// Type lattice join rules:
|
||||
/// Unknown * T = T
|
||||
/// Any * T = Any
|
||||
/// T * T = T
|
||||
/// T * U = Any
|
||||
fn join_ty(a: &Type, b: &Type) -> Type {
|
||||
match (a, b) {
|
||||
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
|
||||
(Type::Any, _) | (_, Type::Any) => Type::Any,
|
||||
(a, b) if a == b => a.clone(),
|
||||
_ => Type::Any,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub struct InferRequest {
|
||||
fn_id: usize,
|
||||
arg_types: Vec<Type>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InferResult {
|
||||
val_types: Vec<Type>,
|
||||
ret_type: Type,
|
||||
}
|
||||
|
||||
pub type TypeCache = HashMap<InferRequest, InferResult>;
|
||||
|
||||
pub fn infer_types(reg_fns: &[RegFn], atoms: &[Atom]) -> TypeCache {
|
||||
// Find main fn
|
||||
let main_fn_id = reg_fns
|
||||
.iter()
|
||||
.position(|f| atom_to_str(&f.name, atoms) == "main")
|
||||
.unwrap();
|
||||
|
||||
let mut cache = TypeCache::with_capacity(reg_fns.len());
|
||||
let mut worklist = vec![];
|
||||
|
||||
worklist.push(InferRequest {
|
||||
fn_id: main_fn_id,
|
||||
arg_types: vec![],
|
||||
});
|
||||
|
||||
while let Some(req) = worklist.pop() {
|
||||
match walk_fn(&req, reg_fns, &mut cache) {
|
||||
Ok(res) => {
|
||||
cache.insert(req, res);
|
||||
}
|
||||
Err(new_req) => {
|
||||
worklist.push(new_req);
|
||||
worklist.push(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache
|
||||
}
|
||||
|
||||
fn walk_fn(
|
||||
req: &InferRequest,
|
||||
reg_fns: &[RegFn],
|
||||
cache: &mut TypeCache,
|
||||
) -> Result<InferResult, InferRequest> {
|
||||
use OpCode::*;
|
||||
|
||||
let f = ®_fns[req.fn_id];
|
||||
let mut val_types = vec![Type::Unknown; f.val_count];
|
||||
let mut ret_type = Type::Unknown;
|
||||
|
||||
for instr in &f.code {
|
||||
let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef);
|
||||
|
||||
if instr.dsts.is_empty() && !is_ret {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ty = match instr.op {
|
||||
// ──────────────────────────────────── Literals ────────────────────────────────────
|
||||
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8
|
||||
| PushI16 | PushI32 => Type::Int,
|
||||
|
||||
PushTrue | PushFalse => Type::Bool,
|
||||
|
||||
// TODO: Fclosure will disappear soon
|
||||
PushConst | PushConst8 | Fclosure | Fclosure8 => match instr.imm {
|
||||
ImmOperand::Const(k) => match f.cpool.get(k as usize) {
|
||||
Some(JsValue::Int(_)) => Type::Int,
|
||||
Some(JsValue::Float(_)) => Type::Float,
|
||||
Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool,
|
||||
Some(&JsValue::FnById(id)) => Type::Fn(FnKind::Id(id)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
},
|
||||
|
||||
// ─────────────────────────────────── Arithmetic ───────────────────────────────────
|
||||
Add | Sub | Mul | Div | Mod | Pow => {
|
||||
match (&val_types[instr.args[0]], &val_types[instr.args[1]]) {
|
||||
(Type::Int, Type::Int) => Type::Int,
|
||||
(Type::Float, Type::Float)
|
||||
| (Type::Int, Type::Float)
|
||||
| (Type::Float, Type::Int) => Type::Float,
|
||||
_ => Type::Any,
|
||||
}
|
||||
}
|
||||
|
||||
Neg | Plus | Inc | Dec => match val_types[instr.args[0]] {
|
||||
Type::Int => Type::Int,
|
||||
Type::Float => Type::Float,
|
||||
_ => Type::Any,
|
||||
},
|
||||
|
||||
Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool,
|
||||
|
||||
// ────────────────────────────────── Value Movers ──────────────────────────────────
|
||||
Dup | SetLoc => val_types[instr.args[0]].clone(),
|
||||
|
||||
// ──────────────────────────────────── Arguments ───────────────────────────────────
|
||||
OpCode::GetArg0 => req.arg_types.first().cloned().unwrap_or(Type::Undefined),
|
||||
OpCode::GetArg1 => req.arg_types.get(1).cloned().unwrap_or(Type::Undefined),
|
||||
OpCode::GetArg2 => req.arg_types.get(2).cloned().unwrap_or(Type::Undefined),
|
||||
OpCode::GetArg3 => req.arg_types.get(3).cloned().unwrap_or(Type::Undefined),
|
||||
|
||||
OpCode::GetArg => match instr.imm {
|
||||
ImmOperand::Arg(k) => req
|
||||
.arg_types
|
||||
.get(k as usize)
|
||||
.cloned()
|
||||
.unwrap_or(Type::Undefined),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
|
||||
// ──────────────────────────────────── Objects ─────────────────────────────────────
|
||||
OpCode::Object => Type::Empty,
|
||||
OpCode::DefineField => {
|
||||
let atom = match instr.imm {
|
||||
ImmOperand::Atom(atom) => atom,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let arg0_ty = val_types[instr.args[0]].clone();
|
||||
let arg1_ty = val_types[instr.args[1]].clone();
|
||||
|
||||
ReturnOf((FnKind::DefineField(atom), vec![arg0_ty, arg1_ty]))
|
||||
}
|
||||
|
||||
// ────────────────────────────────────── Returns ───────────────────────────────────
|
||||
Return | ReturnAsync => val_types[instr.args[0]].clone(),
|
||||
ReturnUndef => Type::Empty,
|
||||
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if is_ret {
|
||||
ret_type = join_ty(&ret_type, &ty);
|
||||
} else {
|
||||
for &dst in &instr.dsts {
|
||||
val_types[dst] = join_ty(&val_types[dst], &ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(InferResult {
|
||||
val_types,
|
||||
ret_type,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn atom_to_str(a: &AtomRef, atoms: &[Atom]) -> String {
|
||||
match a {
|
||||
AtomRef::TaggedInt(n) => n.to_string(),
|
||||
AtomRef::Predefined(i) => atom_names::PREDEFINED_ATOMS
|
||||
.get(*i as usize)
|
||||
.copied()
|
||||
.unwrap_or("<predef?>")
|
||||
.to_string(),
|
||||
AtomRef::FileLocal(i) => atoms
|
||||
.get(*i)
|
||||
.map(atom_text)
|
||||
.unwrap_or_else(|| format!("<file_atom {i}>")),
|
||||
}
|
||||
}
|
||||
|
||||
fn atom_text(a: &Atom) -> String {
|
||||
match a {
|
||||
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s),
|
||||
Atom::Predefined(n) => format!("<predef {n}>"),
|
||||
}
|
||||
}
|
||||
|
||||
fn js_string(s: &JsString) -> String {
|
||||
match s {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//! Infers a type for every location in the program
|
||||
//!
|
||||
//! **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`).
|
||||
|
||||
mod memory;
|
||||
mod types;
|
||||
mod walk;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
flatten::ENTRY_FN,
|
||||
link::{Link, Target},
|
||||
regify::{Operand, RegFn},
|
||||
};
|
||||
|
||||
pub use memory::{Addr, Builtins, CallSite, Context, InstrRef, Memory, ObjAddr};
|
||||
pub use types::{LENGTH_FIELD, Shape, Type, join};
|
||||
|
||||
/// 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 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<(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 walk of a call that infer walked
|
||||
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)]
|
||||
}
|
||||
|
||||
/// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
let w = match self.ids.get(&(context, arg_types.clone())) {
|
||||
Some(&w) => w,
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
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.ret(w).clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 },
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"));
|
||||
|
||||
let mut cache = TypeCache {
|
||||
walks: vec![],
|
||||
ids: HashMap::new(),
|
||||
memory: Memory::default(),
|
||||
round: 0,
|
||||
};
|
||||
|
||||
cache.memory.builtins = Builtins {
|
||||
string: global("String"),
|
||||
number: global("Number"),
|
||||
boolean: global("Boolean"),
|
||||
array: global("Array"),
|
||||
};
|
||||
|
||||
loop {
|
||||
let before = cache.progress();
|
||||
cache.round += 1;
|
||||
cache.call(ENTRY_FN, None, &[], fns);
|
||||
cache.call_host_complete(complete, fns);
|
||||
|
||||
if cache.progress() == before {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
//! 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;
|
||||
|
||||
pub const EMPTY_STRING: AtomRef = AtomRef::Predefined(48); // ""
|
||||
pub const LENGTH_FIELD: AtomRef = AtomRef::Predefined(51); // length
|
||||
|
||||
/// 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
|
||||
/// 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` 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
|
||||
Undefined,
|
||||
Null,
|
||||
Bool(Option<bool>), // the value, if it is known
|
||||
Int,
|
||||
Float,
|
||||
Str(Option<AtomRef>), // the value, if it is known. Atoms are interned: equal atoms are equal strings
|
||||
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`
|
||||
}
|
||||
|
||||
/// 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,
|
||||
Null,
|
||||
Bool,
|
||||
Number,
|
||||
String,
|
||||
Object,
|
||||
}
|
||||
|
||||
impl Type {
|
||||
/// 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(_) => 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::Ref(_) | Type::Native(_) => Some(BaseType::Object),
|
||||
Type::Shape(_) | Type::Closure { .. } => {
|
||||
unreachable!("an object's entry is not a value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_nullish(&self) -> bool {
|
||||
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
|
||||
pub fn members(&self) -> &[Type] {
|
||||
match self {
|
||||
Type::Unknown => &[],
|
||||
Type::Sum(members) => members,
|
||||
ty => std::slice::from_ref(ty),
|
||||
}
|
||||
}
|
||||
|
||||
/// The type with every known value dropped
|
||||
pub fn widen(&self) -> Type {
|
||||
match self {
|
||||
Type::Bool(_) => Type::Bool(None),
|
||||
Type::Str(_) => Type::Str(None),
|
||||
Type::Sum(members) => sum_of(members.iter().map(Type::widen).collect()),
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
_ => sum_of(a.members().iter().chain(b.members()).cloned().collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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| {
|
||||
let joins = a.base() == b.base() && a.base() != Some(BaseType::Object);
|
||||
|
||||
if joins {
|
||||
*a = join_base(a, b);
|
||||
}
|
||||
|
||||
joins || a == b
|
||||
});
|
||||
|
||||
match list.len() {
|
||||
1 => list.pop().unwrap(),
|
||||
_ => Type::Sum(list),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 }),
|
||||
(Type::Str(x), Type::Str(y)) => Type::Str(if x == y { *x } else { None }),
|
||||
(Type::Int, Type::Int) => Type::Int,
|
||||
(Type::Int | Type::Float, Type::Int | Type::Float) => Type::Float,
|
||||
(a, _) => a.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
fn single(ty: &Type) -> Option<Type> {
|
||||
match ty {
|
||||
Type::Undefined | Type::Null | Type::Bool(Some(_)) | Type::Str(Some(_)) => Some(ty.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `a === b`, if the types decide it
|
||||
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)) {
|
||||
_ if disjoint => Some(false),
|
||||
(Some(x), Some(y)) => Some(x == y),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `a == b`, if the types decide it. Undefined and null are equal to each other and
|
||||
/// to nothing else. Two values of one kind compare as `===` does
|
||||
pub 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),
|
||||
_ if a.base() == b.base() => strict_eq(a, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a value of type `ty` has one of the base types `bases`
|
||||
pub fn is_base(ty: &Type, bases: &[BaseType]) -> Option<bool> {
|
||||
ty.base().map(|base| bases.contains(&base))
|
||||
}
|
||||
|
||||
/// Whether a value of type `ty` is truthy, if the type decides it
|
||||
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::Ref(_) | Type::Native(_) => Some(true),
|
||||
_ => 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,
|
||||
|
||||
+126
-22
@@ -1,35 +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());
|
||||
|
||||
let obj = ffi::JS_Eval(
|
||||
ctx,
|
||||
src.as_ptr(),
|
||||
src.len(),
|
||||
filename.as_ptr().cast(),
|
||||
ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE,
|
||||
);
|
||||
for root in roots {
|
||||
if (*loader).loaded.iter().any(|(name, _)| name == root) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(obj.tag != ffi::JS_TAG_EXCEPTION, "{}", get_exception(ctx));
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -37,17 +62,96 @@ 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);
|
||||
|
||||
let message = if text.is_null() {
|
||||
if text.is_null() {
|
||||
"quickjs raised an exception it could not describe".to_string()
|
||||
} else {
|
||||
CStr::from_ptr(text).to_string_lossy().into_owned()
|
||||
};
|
||||
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+194
-116
@@ -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,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -72,7 +83,8 @@ impl Atom {
|
||||
let (bin, header) = leb128(bin)?;
|
||||
let wide_char = header & 1 == 1;
|
||||
let len = header >> 1;
|
||||
let (bin, data) = take(len)(bin)?;
|
||||
let byte_len = if wide_char { len * 2 } else { len };
|
||||
let (bin, data) = take(byte_len)(bin)?;
|
||||
let data = data.to_vec();
|
||||
|
||||
let string = if wide_char {
|
||||
@@ -92,7 +104,7 @@ impl Atom {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum AtomRef {
|
||||
TaggedInt(u32),
|
||||
Predefined(u32),
|
||||
@@ -133,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 {
|
||||
@@ -163,11 +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),
|
||||
FnByIdx(usize),
|
||||
/// A C function that JS can call, by its index in `native::NATIVES`
|
||||
NativeFn(usize),
|
||||
Module(Module),
|
||||
TypedArray,
|
||||
ArrayBuffer,
|
||||
@@ -235,8 +263,8 @@ impl JsValue {
|
||||
(
|
||||
bin,
|
||||
Self::TemplateObject {
|
||||
cooked,
|
||||
raw: Box::new(raw),
|
||||
_cooked: cooked,
|
||||
_raw: Box::new(raw),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -255,7 +283,7 @@ impl JsValue {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, TryFromPrimitive)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(u16)]
|
||||
pub enum FuncKind {
|
||||
Normal = 0,
|
||||
@@ -266,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,
|
||||
}
|
||||
|
||||
@@ -286,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,
|
||||
@@ -321,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,
|
||||
}
|
||||
|
||||
@@ -339,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,
|
||||
},
|
||||
))
|
||||
@@ -350,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 {
|
||||
@@ -374,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,
|
||||
@@ -397,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 {
|
||||
@@ -419,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,
|
||||
@@ -482,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.
|
||||
@@ -496,35 +524,56 @@ 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 {
|
||||
/// The atom of an instr that names one, such as `get_field`
|
||||
pub fn atom(self) -> AtomRef {
|
||||
match self {
|
||||
Self::Atom(atom) | Self::AtomU8 { atom, .. } | Self::AtomU16 { atom, .. } => atom,
|
||||
_ => unreachable!("{self:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The cpool index of an instr that takes a constant, such as `fclosure`
|
||||
pub fn const_idx(self) -> usize {
|
||||
match self {
|
||||
Self::Const(idx) => idx as usize,
|
||||
_ => unreachable!("{self:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Instr {
|
||||
pub pc: usize,
|
||||
pub op: opcodes::OpCode,
|
||||
pub imm_operands: ImmOperand,
|
||||
pub imm: ImmOperand,
|
||||
}
|
||||
|
||||
impl Debug for Instr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:04} {}", self.pc, self.op.name())?;
|
||||
match &self.imm_operands {
|
||||
match &self.imm {
|
||||
ImmOperand::None => Ok(()),
|
||||
other => write!(f, " {other:?}"),
|
||||
}
|
||||
@@ -595,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)),
|
||||
@@ -616,28 +665,28 @@ 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),
|
||||
},
|
||||
};
|
||||
|
||||
out.push(Instr {
|
||||
pc,
|
||||
op: info.op,
|
||||
imm_operands: operands,
|
||||
imm: operands,
|
||||
});
|
||||
|
||||
pc += size;
|
||||
@@ -649,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
|
||||
@@ -706,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,
|
||||
@@ -724,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 {
|
||||
@@ -758,7 +807,7 @@ impl Module {
|
||||
bin,
|
||||
Self {
|
||||
name,
|
||||
import_modules,
|
||||
requests: import_modules,
|
||||
exports,
|
||||
star_exports,
|
||||
imports,
|
||||
@@ -769,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,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -860,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,
|
||||
}
|
||||
}
|
||||
+125
-22
@@ -1,35 +1,138 @@
|
||||
mod declosure;
|
||||
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 declosure::declosure;
|
||||
use flatten::flatten;
|
||||
use infer::infer_types;
|
||||
use regify::regify;
|
||||
/// 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"),
|
||||
),
|
||||
];
|
||||
|
||||
/// 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])] = &[
|
||||
("jsrt.h", include_bytes!("../../lib/rt/jsrt.h")),
|
||||
("core.h", include_bytes!("../../lib/rt/core.h")),
|
||||
("str.h", include_bytes!("../../lib/rt/str.h")),
|
||||
("number.h", include_bytes!("../../lib/rt/number.h")),
|
||||
("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>...");
|
||||
|
||||
let mut src = Vec::with_capacity(metadata.len() as usize);
|
||||
file.read_to_end(&mut src).unwrap();
|
||||
|
||||
// Compile to bytecode
|
||||
let binary = libqjs::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 = declosure(flat_fns, &captures);
|
||||
let reg_fns = regify(flat_fns);
|
||||
let type_cache = infer_types(®_fns, &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()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
for (name, src) in RUNTIME {
|
||||
std::fs::write(dir.join(name), src).unwrap();
|
||||
}
|
||||
|
||||
std::fs::write(dir.join("program.c"), c).unwrap();
|
||||
|
||||
let cc = cc::Build::new()
|
||||
.target(env!("TARGET"))
|
||||
.host(env!("TARGET"))
|
||||
.opt_level(2)
|
||||
.cargo_metadata(false)
|
||||
.get_compiler();
|
||||
|
||||
// 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",
|
||||
"-fwrapv",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-Wl,--gc-sections",
|
||||
])
|
||||
.arg(dir.join("program.c"))
|
||||
.args(["-o", "a.out", "-lm"])
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! The C fns that JS can call
|
||||
//!
|
||||
//! 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::Type;
|
||||
|
||||
/// A native: one set of param types, and one return type
|
||||
pub struct NativeFn {
|
||||
pub name: &'static str,
|
||||
pub arg_types: &'static [Type],
|
||||
pub ret_type: Type,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
),*
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/// 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. 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 (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 (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;
|
||||
};
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// 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];
|
||||
|
||||
for (i, param) in native.arg_types.iter().enumerate() {
|
||||
let arg = args.get(i).unwrap_or(&Type::Undefined);
|
||||
|
||||
if !arg.members().iter().any(|a| can_pass(a, param)) {
|
||||
return Type::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
native.ret_type.clone()
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use super::RegInstr;
|
||||
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
|
||||
|
||||
@@ -90,7 +92,7 @@ fn is_jump_instr(instr: &Instr, code: &[Instr]) -> Option<usize> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let target = match instr.imm_operands {
|
||||
let target = match instr.imm {
|
||||
ImmOperand::Label { target } => code.iter().position(|x| x.pc == target).unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
+106
-189
@@ -1,19 +1,28 @@
|
||||
mod basic_block;
|
||||
mod walk;
|
||||
|
||||
use crate::{
|
||||
flatten::FlatFn,
|
||||
libqjs::parse::{AtomRef, ClosureVar, 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(Clone)]
|
||||
#[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 {
|
||||
@@ -27,221 +36,129 @@ impl RegInstr {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RegInstr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:?} = {:?} {:?} {:?}",
|
||||
self.dsts, self.op, self.imm, self.args
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RegFn {
|
||||
pub name: AtomRef,
|
||||
pub arg_count: usize,
|
||||
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 {
|
||||
fn from_flat(flat_fn: FlatFn) -> Self {
|
||||
let mut blocks = find_basic_blocks(&flat_fn.code);
|
||||
let mut out = vec![RegInstr::nop(); flat_fn.code.len()];
|
||||
/// 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();
|
||||
|
||||
let mut next_val = flat_fn.var_count; // Reserve values for locals
|
||||
slots.sort_unstable();
|
||||
slots.dedup();
|
||||
slots
|
||||
}
|
||||
|
||||
let mut entry_stacks: Vec<Option<Vec<usize>>> = vec![None; blocks.len()];
|
||||
let mut worklist = vec![];
|
||||
/// 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;
|
||||
|
||||
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 block = &mut blocks[block_idx];
|
||||
let mut stack: Vec<usize> = 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(block, &flat_fn.code, &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 succ_idx in block.succ.clone() {
|
||||
let succ_stack = &mut entry_stacks[succ_idx];
|
||||
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()
|
||||
});
|
||||
|
||||
match succ_stack {
|
||||
Some(succ_stack) => add_block_postfix(block, &stack, succ_stack),
|
||||
None => {
|
||||
*succ_stack = Some(stack.clone());
|
||||
worklist.push(succ_idx);
|
||||
}
|
||||
assert_eq!(
|
||||
entry.len(),
|
||||
stack.len(),
|
||||
"a join of stacks of different depths"
|
||||
);
|
||||
|
||||
let copies = stack
|
||||
.iter()
|
||||
.zip(entry.iter())
|
||||
.filter(|(src, dst)| src != dst);
|
||||
|
||||
for (&src, &dst) in copies {
|
||||
blocks[b].postfix.push(RegInstr {
|
||||
dsts: vec![dst],
|
||||
op: OpCode::SetLoc,
|
||||
imm: ImmOperand::None,
|
||||
args: vec![src],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
);
|
||||
}
|
||||
|
||||
mark_puts(&mut out);
|
||||
|
||||
Self {
|
||||
name: flat_fn.name,
|
||||
arg_count: flat_fn.arg_count,
|
||||
val_count: next_val,
|
||||
cpool: flat_fn.cpool,
|
||||
arg_count: f.arg_count,
|
||||
local_count: f.local_count,
|
||||
reg_count: next_reg,
|
||||
cpool: f.cpool,
|
||||
code: out,
|
||||
blocks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_block(
|
||||
block: &BasicBlock,
|
||||
code: &[Instr],
|
||||
stack: &mut Vec<usize>,
|
||||
next_val: &mut usize,
|
||||
out: &mut [RegInstr],
|
||||
) {
|
||||
let mut fresh_val = || {
|
||||
let v = *next_val;
|
||||
*next_val += 1;
|
||||
v
|
||||
};
|
||||
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_operands,
|
||||
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;
|
||||
}
|
||||
|
||||
// Locals are not renamed, local read/writes need special handling
|
||||
// Handle read of a local value
|
||||
match is_local_read(instr) {
|
||||
Some(loc) => {
|
||||
stack.push(loc);
|
||||
out_instr.op = OpCode::Nop;
|
||||
out_instr.imm = ImmOperand::None;
|
||||
out[instr_idx] = out_instr;
|
||||
continue;
|
||||
}
|
||||
None if instr.op == OpCode::GetLoc0Loc1 => {
|
||||
stack.push(0);
|
||||
stack.push(1);
|
||||
out_instr.op = OpCode::Nop;
|
||||
out_instr.imm = ImmOperand::None;
|
||||
out[instr_idx] = out_instr;
|
||||
continue;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
// Handle write of a local value
|
||||
match is_local_write(instr) {
|
||||
Some(loc) => {
|
||||
out_instr.op = OpCode::SetLoc;
|
||||
out_instr.imm = ImmOperand::None;
|
||||
out_instr.dsts.push(loc);
|
||||
out[instr_idx] = out_instr;
|
||||
|
||||
if n_push == 1 {
|
||||
stack.push(loc);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
None if instr.op == OpCode::SetLocUninitialized => {
|
||||
out_instr.op = OpCode::Nop;
|
||||
out_instr.imm = ImmOperand::None;
|
||||
out[instr_idx] = out_instr;
|
||||
continue;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
fn extra_pops(instr: &Instr) -> usize {
|
||||
match instr.op {
|
||||
OpCode::Call0 => 0,
|
||||
OpCode::Call1 => 1,
|
||||
OpCode::Call2 => 2,
|
||||
OpCode::Call3 => 3,
|
||||
_ => match instr.imm_operands {
|
||||
ImmOperand::Npop(npop) => npop as usize,
|
||||
ImmOperand::NpopU16 { npop, .. } => npop as usize,
|
||||
_ => 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn is_local_read(instr: &Instr) -> Option<usize> {
|
||||
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_operands {
|
||||
ImmOperand::Loc(k) => Some(k as usize),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_local_write(instr: &Instr) -> Option<usize> {
|
||||
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_operands {
|
||||
ImmOperand::Loc(k) => Some(k as usize),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_block_postfix(block: &mut BasicBlock, stack: &[usize], succ_stack: &[usize]) {
|
||||
let overlap = stack.iter().rev().zip(succ_stack.iter().rev());
|
||||
|
||||
for (val, succ_val) in overlap {
|
||||
block.postfix.push(RegInstr {
|
||||
dsts: vec![*succ_val],
|
||||
op: OpCode::SetLoc,
|
||||
imm: ImmOperand::None,
|
||||
args: vec![*val],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! 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};
|
||||
|
||||
#[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) {
|
||||
(PushThis, _) => (GetArg, THIS_ARG),
|
||||
(GetArg0, _) => (GetArg, FIRST_ARG),
|
||||
(GetArg1, _) => (GetArg, FIRST_ARG + 1),
|
||||
(GetArg2, _) => (GetArg, FIRST_ARG + 2),
|
||||
(GetArg3, _) => (GetArg, FIRST_ARG + 3),
|
||||
(PutArg0, _) => (PutArg, FIRST_ARG),
|
||||
(PutArg1, _) => (PutArg, FIRST_ARG + 1),
|
||||
(PutArg2, _) => (PutArg, FIRST_ARG + 2),
|
||||
(PutArg3, _) => (PutArg, FIRST_ARG + 3),
|
||||
(SetArg0, _) => (SetArg, FIRST_ARG),
|
||||
(SetArg1, _) => (SetArg, FIRST_ARG + 1),
|
||||
(SetArg2, _) => (SetArg, FIRST_ARG + 2),
|
||||
(SetArg3, _) => (SetArg, FIRST_ARG + 3),
|
||||
(GetArg | PutArg | SetArg, ImmOperand::Arg(k)) => (instr.op, FIRST_ARG + k),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
//! 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)]
|
||||
struct MethodSequence {
|
||||
fn_const: ImmOperand,
|
||||
name: AtomRef,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClassSequence {
|
||||
start_pc: usize,
|
||||
ctor_const: ImmOperand,
|
||||
methods: Vec<MethodSequence>,
|
||||
}
|
||||
|
||||
impl ClassSequence {
|
||||
/// Bytecode for prototype form looks like:
|
||||
/// ```
|
||||
/// fclosure ctor
|
||||
/// dup
|
||||
/// object
|
||||
/// (fclosure method; define_field name)*
|
||||
/// put_field prototype
|
||||
/// dup
|
||||
/// get_field prototype
|
||||
/// ```
|
||||
///
|
||||
/// 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);
|
||||
push(OpCode::Dup, ImmOperand::None);
|
||||
push(OpCode::Object, ImmOperand::None);
|
||||
|
||||
for m in &self.methods {
|
||||
push(OpCode::Fclosure8, m.fn_const);
|
||||
push(OpCode::DefineField, ImmOperand::Atom(m.name));
|
||||
}
|
||||
|
||||
push(OpCode::PutField, prototype);
|
||||
push(OpCode::Dup, ImmOperand::None);
|
||||
push(OpCode::GetField, prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_proto_form(&mut out);
|
||||
code = rest;
|
||||
}
|
||||
Err(rest) => {
|
||||
// Copy what matched before the match failed, at least one instr
|
||||
let consumed = (code.len() - rest.len()).max(1);
|
||||
out.extend_from_slice(&code[..consumed]);
|
||||
code = &code[consumed..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
// ─────────────────────────── Class Sequence Grammar ─────────────────────────────────────
|
||||
// push_const8 #ctor ; define_class @Name <-- Class decl header
|
||||
// ( fclosure8 #method ; define_method @meth )* <-- Methods
|
||||
// ( fclosure8 #fields ; set_home_object ; put_loc ; drop )? <-- Field initializer stuff
|
||||
//
|
||||
// match fns return Ok(advanced_instrs) or Err(original_instrs)
|
||||
|
||||
fn match_class_seq(ins: &[Instr]) -> PResult<'_, ClassSequence> {
|
||||
let pc = ins[0].pc;
|
||||
|
||||
let (ins, push_const) = match_instr(ins, |i| i.op == OpCode::PushConst8)?;
|
||||
let (ins, define_class) = match_instr(ins, |i| i.op == OpCode::DefineClass)?;
|
||||
let (ins, methods) = match_many(ins, match_method_seq);
|
||||
|
||||
// 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, "TODO: derived classes");
|
||||
|
||||
let seq = ClassSequence {
|
||||
start_pc: pc,
|
||||
ctor_const: push_const.imm,
|
||||
methods,
|
||||
};
|
||||
|
||||
Ok((ins, seq))
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
|
||||
let seq = MethodSequence {
|
||||
fn_const: fclosure.imm,
|
||||
name: define_method.imm.atom(),
|
||||
};
|
||||
|
||||
Ok((ins, seq))
|
||||
}
|
||||
|
||||
// ───────────────────────────── Matcher Primitives ───────────────────────────────────────
|
||||
|
||||
type PResult<'a, T> = Result<(&'a [Instr], T), &'a [Instr]>;
|
||||
|
||||
fn match_instr(i: &[Instr], f: impl Fn(&Instr) -> bool) -> PResult<'_, &Instr> {
|
||||
match i.first() {
|
||||
Some(instr) if f(instr) => Ok((&i[1..], instr)),
|
||||
_ => Err(i),
|
||||
}
|
||||
}
|
||||
|
||||
fn match_many<T>(mut i: &[Instr], f: impl Fn(&[Instr]) -> PResult<T>) -> (&[Instr], Vec<T>) {
|
||||
let mut res = vec![];
|
||||
|
||||
while let Ok((rest, r)) = f(i) {
|
||||
res.push(r);
|
||||
i = rest;
|
||||
}
|
||||
|
||||
(i, res)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Process instrs that are a simple sequence of other instrs
|
||||
//!
|
||||
//! Useless instrs are replaced with `nop` since simply dropping instrs can break jump targets
|
||||
|
||||
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 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, 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");
|
||||
[
|
||||
(Nop, Imm::None),
|
||||
(Nop, Imm::None),
|
||||
(Nop, Imm::None),
|
||||
(DefineField, Imm::Atom(atom)),
|
||||
]
|
||||
}
|
||||
// The fn itself, which is its env
|
||||
(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
|
||||
(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!(),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
for (op, imm) in seq {
|
||||
out.push(Instr {
|
||||
pc: instr.pc,
|
||||
op,
|
||||
imm,
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! 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
|
||||
|
||||
mod args;
|
||||
mod r#async;
|
||||
mod class;
|
||||
mod closure;
|
||||
mod expand;
|
||||
mod modules;
|
||||
mod new;
|
||||
|
||||
use crate::{
|
||||
flatten::FlatFn,
|
||||
libqjs::parse::{Atom, AtomRef, ClosureVar},
|
||||
link::Link,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
|
||||
pub fn transform(
|
||||
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()
|
||||
.enumerate()
|
||||
.map(|(idx, f)| transform_one(f, &captures[idx], &captures))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `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);
|
||||
|
||||
// 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 try_replace_new(instr, &mut new_local, &mut out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if try_replace_arg_access(instr, &mut out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if try_replace_expansion(instr, &mut out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if try_replace_closure(instr, own, captures, &f.cpool, &mut out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push(instr.clone());
|
||||
}
|
||||
|
||||
FlatFn {
|
||||
name: f.name,
|
||||
kind: f.kind,
|
||||
arg_count: f.arg_count + FIRST_ARG as usize,
|
||||
local_count: next_local,
|
||||
cpool: f.cpool,
|
||||
code: out,
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//! 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};
|
||||
|
||||
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 = move |op, imm| {
|
||||
out.push(Instr {
|
||||
pc: instr.pc,
|
||||
op,
|
||||
imm,
|
||||
})
|
||||
};
|
||||
|
||||
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);
|
||||
push(OpCode::Dup, ImmOperand::None);
|
||||
push(OpCode::GetField, ImmOperand::Atom(PROTOTYPE_FIELD));
|
||||
push(OpCode::Object, ImmOperand::None);
|
||||
push(OpCode::Swap, ImmOperand::None);
|
||||
push(OpCode::DefineField, ImmOperand::Atom(PROTO_FIELD));
|
||||
|
||||
for &n in new_locals.iter().rev() {
|
||||
push(OpCode::GetLoc, ImmOperand::Loc(n));
|
||||
}
|
||||
|
||||
push(OpCode::CallConstructor, instr.imm);
|
||||
true
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
# The core model
|
||||
|
||||
This file is the spec for how the compiler gives a type to every value of a JS program, and how it represents those values in C. The code must agree with it. A difference between the code and this file is a bug in one of them.
|
||||
|
||||
Everything comes from four simple things and how they act on each other:
|
||||
|
||||
1. **the types**, an enum, and the rules that join two of them (section 2)
|
||||
2. **objects**, which are types that every holder shares (section 3)
|
||||
3. **`walk_fn`**, which walks one fn with the types of its args, and gives the type of each of its values (section 4)
|
||||
4. **the type cache**, which keeps one walk for each fn and each list of arg types that the program reaches (section 4)
|
||||
|
||||
## 1. Semantics
|
||||
|
||||
The C code must keep these JS semantics.
|
||||
|
||||
- **A value** is one of: undefined, null, a bool, a number, a string, an object. A value cannot change. An assignment, an arg, a return and a field write each copy the value. An object value is a reference to the object, so a copy of it refers to the same object.
|
||||
- **An object** has identity, own fields, and a prototype (an object or null). A read of a field that the object lacks goes on to the prototype, and at the end of the chain it gives undefined. A field that is absent and a field that holds undefined are different.
|
||||
- **A binding** is the storage of a variable. A closure captures bindings, not values: a write to a captured binding, by its owner or by any closure, is seen by all of them. Each iteration of a loop body has its own bindings.
|
||||
- **A function** is an object that also has code, and the bindings that it captured.
|
||||
|
||||
## 2. Types
|
||||
|
||||
A type tells which values a place can hold at runtime. It is one of these:
|
||||
|
||||
| type | values |
|
||||
|---|---|
|
||||
| `Unknown` | none: no value has reached the place yet |
|
||||
| `Undefined` | undefined |
|
||||
| `Null` | null |
|
||||
| `Bool(v)` | a bool. `v` is the value, if it is known |
|
||||
| `Int` | an int32 (provisional: the number type is not decided) |
|
||||
| `Float` | a number |
|
||||
| `Str(v)` | a string. `v` is the value, if it is known |
|
||||
| `Obj(cell)` | an object, described by a cell (section 3) |
|
||||
| `Sum(…)` | one of a list of the types above |
|
||||
| `Any` | any value. No rule gives it now |
|
||||
|
||||
**Join** gives the type of a place that gets the values of two types:
|
||||
|
||||
```
|
||||
Unknown * T = T
|
||||
Any * T = Any
|
||||
T * T = T
|
||||
Bool(a) * Bool(b) = Bool, the value not known. The same for Str
|
||||
Int * Float = Float
|
||||
T * U = T | U
|
||||
```
|
||||
|
||||
A sum has at most one member of each kind, except objects: it has one `Obj` for each cell. Two `Obj` types are the same type only if they have the same cell.
|
||||
|
||||
## 3. Objects
|
||||
|
||||
**The type of an object is a cell:** a description of the object that every holder of the object shares. In the compiler it is `Obj(Rc<RefCell<…>>)`. The cell holds:
|
||||
|
||||
- the type of each field, and whether every object of the cell has the field from the start (else it can be absent)
|
||||
- what the object is: a plain object, a fn (with its code), or an array (with the type of its elements)
|
||||
|
||||
**A copy of the value copies the `Rc`,** so all holders have the same cell. Everything about aliasing comes from this:
|
||||
|
||||
- **A write** `o.f = v` joins the type of `v` into the cell's field `f`. Every holder of the object sees it at once: through calls, returns, other objects' fields, closures and array elements.
|
||||
- **A read** `o.f` gives the cell's field `f`. If the field can be absent, it also gives the read on the prototype. At the end of the chain, it gives `Undefined`.
|
||||
- **A cell can hold itself,** for example as the type of the field `next` in a list. So an object that reaches itself needs nothing more.
|
||||
|
||||
**An array** is a cell whose elements have one type `T`, the join of every value written to an element. `a[i]` gives `T | Undefined`, because an index can be a hole or past the end. `a.length` is an `Int`.
|
||||
|
||||
**A captured binding is a box:** a cell with one field, which holds the binding's value. The fn that owns the binding makes the box when it is entered, and again for each iteration of a loop body. The owner and every closure that captured the binding read and write the box's field. A closure is a fn cell whose fields hold its boxes.
|
||||
|
||||
## 4. Inference
|
||||
|
||||
**`walk_fn`** walks one fn with one list of arg types.
|
||||
|
||||
- It starts with `val_types = vec![Type::Unknown; n_val]`, and puts the arg types into the args' values.
|
||||
- It walks the instrs. Each instr gives a type from the types of its operands, and that type is joined into its dst. An instr whose operand is still `Unknown` waits.
|
||||
- It walks again until nothing changes. The result is the type of each value, and the return type.
|
||||
|
||||
**The cells of a walk.** When a walk reaches an instr that makes an object, the first time, it makes a new cell for that instr. On every later pass, it uses the same cell again. So the objects that a walk makes belong to that walk: a fn walked with `Int` and with `Str` makes different cells.
|
||||
|
||||
**The type cache** is a map from a fn and a list of arg types to the walk of that fn with those types.
|
||||
|
||||
- A call looks up its callee with the types of its args. If there is no walk yet, it walks the callee. The call gives the walk's return type.
|
||||
- **Only what the program reaches is walked,** as in Rust: there is one walk for each list of arg types that a call really passes.
|
||||
- **In the key, an object is its cell.** Known values are dropped (`Bool(true)` is `Bool`), so a literal does not make a new walk.
|
||||
|
||||
**Rounds.** A walk can depend on cells and returns that other walks are still adding to. So infer walks the whole program again, from the entry, until a round changes no cell, no return and no proto, and makes no new walk or cell. A recursive call gives what its walk returned in the round before.
|
||||
|
||||
**The open problem: infer does not always end.** A walk makes cells, and a key holds cells. So when a cell that a walk made gets into the key of a later call, that call gets a new walk, which makes new cells, which can make another new key. Rust does not have this problem, because every type in a Rust key is written in the program (a struct and its type args), and a call cannot change the type of its args. These programs end at once in node, but infer does not end on them:
|
||||
|
||||
```js
|
||||
// 1. `this` holds the list of nodes. The arrow fn never reads `this`, but it
|
||||
// is in the key, and each new walk of it leads to a new node
|
||||
let list = null;
|
||||
function add(f) { list = { f: f, next: list }; }
|
||||
add(() => add(() => 0));
|
||||
list.f();
|
||||
|
||||
// 2. A cell is one type for the object's whole life: the call sees the type
|
||||
// of `o.v` after the write, so each round sees one cell more
|
||||
const o = { v: 1 };
|
||||
function f(x) { o.v = { inner: x }; }
|
||||
f(o.v);
|
||||
|
||||
// 3. A new object goes into a recursive call. The exact types are
|
||||
// `{next: Int}`, `{next: {next: Int}}`, ...: as many as `n`, which only the
|
||||
// runtime knows. Rust rejects this program too
|
||||
function g(n, x) { if (n > 0) g(n - 1, { next: x }); }
|
||||
g(3, 0);
|
||||
```
|
||||
|
||||
The same thing happens on a large scale with partial types. Inside a walk, a call first sees the types of an early pass, then wider ones. Each key gets its own walk, and its cells stay in the types for good. The stdlib's promises reach thousands of walks from a 7-line program. The same loops without fns (`for (…) x = { next: x }`) end, because one instr in one walk is one cell.
|
||||
|
||||
## 5. Representation in C
|
||||
|
||||
The C type of a value comes from its type only. A type with one member is that member's C type:
|
||||
|
||||
| member | C |
|
||||
|---|---|
|
||||
| `Undefined`, `Null` | `undefined_t`, `null_t`: unit types |
|
||||
| `Bool` | `bool` |
|
||||
| `Int` | `int32_t` |
|
||||
| `Float` | `double` |
|
||||
| `Str` | `string_t` |
|
||||
| `Native` | `native_t`: the native's index |
|
||||
| `Obj` | a pointer to the struct of the object's class |
|
||||
|
||||
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
|
||||
|
||||
- a bound on the walks: infer must end on every program (section 4)
|
||||
- definite initialization: a field that a constructor writes before `this` goes anywhere is never absent
|
||||
- narrowing in branches (`if (x !== null)`)
|
||||
- objects as struct values, and objects on the stack (escape analysis)
|
||||
- fields with computed names (`o[k]` on an object that is not an array), `delete`, `in`
|
||||
- typed arrays, `ArrayBuffer`, `DataView`, and iterators
|
||||
- `throw`, `try`, `catch`, `finally`, and the TDZ
|
||||
- getters and setters
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
// 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>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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;
|
||||
|
||||
// A new heap cell of `size` bytes, all zero
|
||||
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.
|
||||
// The new room is all zero
|
||||
void *__rt_grow(void *items, int32_t *cap, int32_t need, size_t size);
|
||||
|
||||
// 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>
|
||||
|
||||
void *__rt_alloc(size_t size) {
|
||||
void *p = calloc(1, size);
|
||||
|
||||
if (!p) {
|
||||
fputs("jsrt: out of memory\n", stderr);
|
||||
abort();
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
void *__rt_grow(void *items, int32_t *cap, int32_t need, size_t size) {
|
||||
if (need <= *cap) {
|
||||
return items;
|
||||
}
|
||||
|
||||
int32_t room = *cap * 2 > need ? *cap * 2 : need < 4 ? 4 : need;
|
||||
items = realloc(items, (size_t)room * size);
|
||||
|
||||
if (!items) {
|
||||
fputs("jsrt: out of memory\n", stderr);
|
||||
abort();
|
||||
}
|
||||
|
||||
memset((char *)items + (size_t)*cap * size, 0, (size_t)(room - *cap) * size);
|
||||
*cap = room;
|
||||
return items;
|
||||
}
|
||||
|
||||
_Noreturn void __rt_type_error(const char *what) {
|
||||
fprintf(stderr, "TypeError: %s\n", what);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#endif // JSRT_IMPLEMENTATION
|
||||
|
||||
// ─────────────────────────────────── 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};
|
||||
}
|
||||
|
||||
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)
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
// IO natives. `__native_print_string` writes at once. The rest are async, on
|
||||
// io_uring (see uring.h)
|
||||
//
|
||||
// A `__native_io_queue_<op>` queues an op and gives its handle. It makes no
|
||||
// syscall, 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
|
||||
// never sees a limit
|
||||
//
|
||||
// The ring is made at the first wait that has ops, so a program that does no
|
||||
// async IO never makes it
|
||||
#pragma once
|
||||
|
||||
#include "str.h"
|
||||
|
||||
#ifdef JSRT_IMPLEMENTATION
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/stat.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "uring.h"
|
||||
|
||||
// ── sync ──
|
||||
|
||||
undefined_t __native_print_string(string_t s) {
|
||||
char *utf8 = __rt_string_utf8(s);
|
||||
puts(utf8);
|
||||
free(utf8);
|
||||
return (undefined_t){0};
|
||||
}
|
||||
|
||||
// ── ops ──
|
||||
|
||||
typedef enum {
|
||||
JSRT_IO_TIMEOUT,
|
||||
JSRT_IO_OPEN,
|
||||
JSRT_IO_READ,
|
||||
JSRT_IO_WRITE,
|
||||
JSRT_IO_CLOSE,
|
||||
JSRT_IO_STAT,
|
||||
JSRT_IO_CANCEL,
|
||||
} jsrt_io_kind;
|
||||
|
||||
typedef enum {
|
||||
JSRT_IO_FREE, // in the free list
|
||||
JSRT_IO_QUEUED, // in the backlog, not in the SQ yet
|
||||
JSRT_IO_FLIGHT, // in the SQ or in the kernel
|
||||
JSRT_IO_DONE, // in the done list, or the last op a wait gave
|
||||
} jsrt_io_state;
|
||||
|
||||
// An op. Its handle is its index in the table, and its SQE's user_data
|
||||
//
|
||||
// The table moves when it grows, which only a queue does. So the kernel
|
||||
// must not hold a pointer into it after a submit returns: `ts` is read
|
||||
// before (SUBMIT_STABLE), and everything that the kernel reads or writes
|
||||
// later is in `buf`, which does not move
|
||||
typedef struct {
|
||||
uint8_t kind, state;
|
||||
bool cancelled; // cancelled while queued: never goes to the SQ
|
||||
uint32_t gen; // counts reuses of this slot (see CANCEL)
|
||||
int32_t next; // the next op in its list, or -1
|
||||
int32_t res; // the CQE's res: >= 0 on success, -errno on failure
|
||||
|
||||
int32_t fd; // READ, WRITE, CLOSE
|
||||
int32_t flags, mode; // OPEN
|
||||
uint32_t len; // READ, WRITE: the size of `buf`
|
||||
uint64_t off; // READ, WRITE: -1 is the file's position
|
||||
int32_t target; // CANCEL: the op to cancel...
|
||||
uint32_t target_gen; // ...while it is still this use of its slot
|
||||
struct __kernel_timespec ts; // TIMEOUT
|
||||
void *buf; // OPEN: path. READ, WRITE: data. STAT: statx, then path
|
||||
} jsrt_io_op;
|
||||
|
||||
// A FIFO of ops, linked through `next`
|
||||
typedef struct { int32_t head, tail; } jsrt_io_list;
|
||||
|
||||
static struct {
|
||||
jsrt_uring ring;
|
||||
bool ring_ready;
|
||||
unsigned in_flight;
|
||||
|
||||
jsrt_io_op *ops;
|
||||
int32_t cap;
|
||||
int32_t free; // a stack, linked through `next`
|
||||
jsrt_io_list backlog, done;
|
||||
int32_t given; // the op the last wait gave, or -1
|
||||
} jsrt_io = {
|
||||
.free = -1,
|
||||
.backlog = { -1, -1 },
|
||||
.done = { -1, -1 },
|
||||
.given = -1,
|
||||
};
|
||||
|
||||
static void jsrt_io_push(jsrt_io_list *l, int32_t h) {
|
||||
jsrt_io.ops[h].next = -1;
|
||||
|
||||
if (l->tail < 0) {
|
||||
l->head = h;
|
||||
} else {
|
||||
jsrt_io.ops[l->tail].next = h;
|
||||
}
|
||||
|
||||
l->tail = h;
|
||||
}
|
||||
|
||||
static int32_t jsrt_io_pop(jsrt_io_list *l) {
|
||||
int32_t h = l->head;
|
||||
|
||||
if (h >= 0) {
|
||||
l->head = jsrt_io.ops[h].next;
|
||||
|
||||
if (l->head < 0) {
|
||||
l->tail = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
// A new op of `kind`, not in any list yet
|
||||
static int32_t jsrt_io_new(jsrt_io_kind kind) {
|
||||
if (jsrt_io.free < 0) {
|
||||
int32_t cap = jsrt_io.cap ? jsrt_io.cap * 2 : 64;
|
||||
jsrt_io_op *ops = realloc(jsrt_io.ops, (size_t)cap * sizeof *ops);
|
||||
|
||||
if (!ops) {
|
||||
fputs("jsrt: out of memory\n", stderr);
|
||||
abort();
|
||||
}
|
||||
|
||||
// The new slots go on the free stack, the lowest on top
|
||||
for (int32_t h = cap - 1; h >= jsrt_io.cap; h--) {
|
||||
ops[h] = (jsrt_io_op){ .state = JSRT_IO_FREE, .next = jsrt_io.free };
|
||||
jsrt_io.free = h;
|
||||
}
|
||||
|
||||
jsrt_io.ops = ops;
|
||||
jsrt_io.cap = cap;
|
||||
}
|
||||
|
||||
int32_t h = jsrt_io.free;
|
||||
jsrt_io_op *op = &jsrt_io.ops[h];
|
||||
jsrt_io.free = op->next;
|
||||
*op = (jsrt_io_op){ .kind = kind, .state = JSRT_IO_QUEUED, .gen = op->gen + 1, .next = -1 };
|
||||
return h;
|
||||
}
|
||||
|
||||
static int32_t jsrt_io_queue(int32_t h) {
|
||||
jsrt_io_push(&jsrt_io.backlog, h);
|
||||
return h;
|
||||
}
|
||||
|
||||
// Ends op `h` with `res`, without the kernel
|
||||
static int32_t jsrt_io_finish(int32_t h, int32_t res) {
|
||||
jsrt_io.ops[h].state = JSRT_IO_DONE;
|
||||
jsrt_io.ops[h].res = res;
|
||||
jsrt_io_push(&jsrt_io.done, h);
|
||||
return h;
|
||||
}
|
||||
|
||||
static void jsrt_io_release(int32_t h) {
|
||||
jsrt_io_op *op = &jsrt_io.ops[h];
|
||||
free(op->buf);
|
||||
op->buf = NULL;
|
||||
op->state = JSRT_IO_FREE;
|
||||
op->next = jsrt_io.free;
|
||||
jsrt_io.free = h;
|
||||
}
|
||||
|
||||
// The op that the last wait gave. A handle of any other op is an error
|
||||
static jsrt_io_op *jsrt_io_given(int32_t h) {
|
||||
if (h < 0 || h != jsrt_io.given) {
|
||||
__rt_type_error("an IO result read outside its op's callback");
|
||||
}
|
||||
|
||||
return &jsrt_io.ops[h];
|
||||
}
|
||||
|
||||
// 0 if op `h` still has work for the kernel, else the -errno it ends with
|
||||
//
|
||||
// An op that was cancelled while queued has none. Nor has a cancel whose
|
||||
// target ended, because then the target's slot can hold a new op
|
||||
static int32_t jsrt_io_dead(int32_t h) {
|
||||
jsrt_io_op *op = &jsrt_io.ops[h];
|
||||
|
||||
if (op->cancelled) {
|
||||
return -ECANCELED;
|
||||
}
|
||||
|
||||
if (op->kind == JSRT_IO_CANCEL) {
|
||||
jsrt_io_op *t = &jsrt_io.ops[op->target];
|
||||
|
||||
if (t->gen != op->target_gen || t->state != JSRT_IO_FLIGHT) {
|
||||
return -ENOENT;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void jsrt_io_prep(struct io_uring_sqe *sqe, int32_t h) {
|
||||
jsrt_io_op *op = &jsrt_io.ops[h];
|
||||
sqe->user_data = (uint64_t)h;
|
||||
|
||||
switch (op->kind) {
|
||||
case JSRT_IO_TIMEOUT:
|
||||
// A plain timer: `off` 0 means that no count of CQEs ends it
|
||||
sqe->opcode = IORING_OP_TIMEOUT;
|
||||
sqe->fd = -1;
|
||||
sqe->addr = (uint64_t)(uintptr_t)&op->ts;
|
||||
sqe->len = 1;
|
||||
break;
|
||||
|
||||
case JSRT_IO_OPEN:
|
||||
sqe->opcode = IORING_OP_OPENAT;
|
||||
sqe->fd = AT_FDCWD;
|
||||
sqe->addr = (uint64_t)(uintptr_t)op->buf;
|
||||
sqe->len = (uint32_t)op->mode;
|
||||
sqe->open_flags = (uint32_t)op->flags;
|
||||
break;
|
||||
|
||||
case JSRT_IO_READ:
|
||||
case JSRT_IO_WRITE:
|
||||
sqe->opcode = op->kind == JSRT_IO_READ ? IORING_OP_READ : IORING_OP_WRITE;
|
||||
sqe->fd = op->fd;
|
||||
sqe->addr = (uint64_t)(uintptr_t)op->buf;
|
||||
sqe->len = op->len;
|
||||
sqe->off = op->off;
|
||||
break;
|
||||
|
||||
case JSRT_IO_CLOSE:
|
||||
sqe->opcode = IORING_OP_CLOSE;
|
||||
sqe->fd = op->fd;
|
||||
break;
|
||||
|
||||
case JSRT_IO_STAT:
|
||||
sqe->opcode = IORING_OP_STATX;
|
||||
sqe->fd = AT_FDCWD;
|
||||
sqe->addr = (uint64_t)(uintptr_t)((char *)op->buf + sizeof(struct statx));
|
||||
sqe->len = STATX_SIZE;
|
||||
sqe->addr2 = (uint64_t)(uintptr_t)op->buf;
|
||||
break;
|
||||
|
||||
case JSRT_IO_CANCEL:
|
||||
// A timeout is removed with its own op
|
||||
sqe->opcode = jsrt_io.ops[op->target].kind == JSRT_IO_TIMEOUT
|
||||
? IORING_OP_TIMEOUT_REMOVE
|
||||
: IORING_OP_ASYNC_CANCEL;
|
||||
sqe->fd = -1;
|
||||
sqe->addr = (uint64_t)op->target;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Moves queued ops into the SQ while fewer than `cq_entries` are in flight,
|
||||
// and submits them
|
||||
static void jsrt_io_drain(void) {
|
||||
if (jsrt_io.backlog.head < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
jsrt_uring *r = &jsrt_io.ring;
|
||||
|
||||
if (!jsrt_io.ring_ready) {
|
||||
jsrt_uring_init(r, 128);
|
||||
jsrt_io.ring_ready = true;
|
||||
}
|
||||
|
||||
while (jsrt_io.backlog.head >= 0 && jsrt_io.in_flight < r->cq_entries) {
|
||||
int32_t h = jsrt_io.backlog.head;
|
||||
int32_t dead = jsrt_io_dead(h);
|
||||
|
||||
if (dead) {
|
||||
jsrt_io_pop(&jsrt_io.backlog);
|
||||
jsrt_io_finish(h, dead);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct io_uring_sqe *sqe = jsrt_uring_sqe(r);
|
||||
|
||||
// The SQ is full: a submit empties it
|
||||
if (!sqe) {
|
||||
jsrt_uring_submit(r, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
jsrt_io_pop(&jsrt_io.backlog);
|
||||
jsrt_io_prep(sqe, h);
|
||||
jsrt_io.ops[h].state = JSRT_IO_FLIGHT;
|
||||
jsrt_io.in_flight++;
|
||||
}
|
||||
|
||||
// No SQE may stay unsubmitted when the wait returns, because a queue can
|
||||
// then move the table (see jsrt_io_op)
|
||||
jsrt_uring_submit(r, false);
|
||||
}
|
||||
|
||||
// ── natives ──
|
||||
|
||||
int32_t __native_io_queue_timeout(double ms) {
|
||||
int32_t h = jsrt_io_new(JSRT_IO_TIMEOUT);
|
||||
|
||||
if (!(ms > 0)) {
|
||||
ms = 0;
|
||||
}
|
||||
|
||||
jsrt_io.ops[h].ts.tv_sec = (int64_t)(ms / 1000);
|
||||
jsrt_io.ops[h].ts.tv_nsec = (int64_t)(fmod(ms, 1000) * 1e6);
|
||||
return jsrt_io_queue(h);
|
||||
}
|
||||
|
||||
// `flags` and `mode` are open(2)'s, as numbers
|
||||
int32_t __native_io_queue_open(string_t path, int32_t flags, int32_t mode) {
|
||||
int32_t h = jsrt_io_new(JSRT_IO_OPEN);
|
||||
jsrt_io.ops[h].buf = __rt_string_utf8(path);
|
||||
jsrt_io.ops[h].flags = flags | O_CLOEXEC;
|
||||
jsrt_io.ops[h].mode = mode;
|
||||
return jsrt_io_queue(h);
|
||||
}
|
||||
|
||||
// Reads at most `len` bytes at `offset`, or at the file's position if
|
||||
// `offset` is -1. The status is the number of bytes read
|
||||
int32_t __native_io_queue_read(int32_t fd, int32_t len, double offset) {
|
||||
int32_t h = jsrt_io_new(JSRT_IO_READ);
|
||||
jsrt_io_op *op = &jsrt_io.ops[h];
|
||||
op->fd = fd;
|
||||
op->len = len > 0 ? (uint32_t)len : 0;
|
||||
op->off = offset < 0 ? (uint64_t)-1 : (uint64_t)offset;
|
||||
op->buf = __rt_alloc(op->len ? op->len : 1);
|
||||
return jsrt_io_queue(h);
|
||||
}
|
||||
|
||||
// Writes `data` as UTF-8, at `offset` as for a read. The status is the number
|
||||
// of bytes written, which can be fewer than all
|
||||
int32_t __native_io_queue_write(int32_t fd, string_t data, double offset) {
|
||||
int32_t h = jsrt_io_new(JSRT_IO_WRITE);
|
||||
jsrt_io_op *op = &jsrt_io.ops[h];
|
||||
op->fd = fd;
|
||||
op->buf = __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);
|
||||
}
|
||||
|
||||
int32_t __native_io_queue_close(int32_t fd) {
|
||||
int32_t h = jsrt_io_new(JSRT_IO_CLOSE);
|
||||
jsrt_io.ops[h].fd = fd;
|
||||
return jsrt_io_queue(h);
|
||||
}
|
||||
|
||||
// Gives the file's size, as `__native_io_result_number`
|
||||
int32_t __native_io_queue_stat(string_t path) {
|
||||
int32_t h = jsrt_io_new(JSRT_IO_STAT);
|
||||
char *utf8 = __rt_string_utf8(path);
|
||||
size_t size = strlen(utf8) + 1;
|
||||
char *buf = __rt_alloc(sizeof(struct statx) + size);
|
||||
memcpy(buf + sizeof(struct statx), utf8, size);
|
||||
free(utf8);
|
||||
jsrt_io.ops[h].buf = buf;
|
||||
return jsrt_io_queue(h);
|
||||
}
|
||||
|
||||
// Cancels op `target`, which then ends with -ECANCELED if it did not end
|
||||
// first. The cancel's own status is 0 if it found the op, else -ENOENT or
|
||||
// -EALREADY
|
||||
int32_t __native_io_queue_cancel(int32_t target) {
|
||||
if (target < 0 || target >= jsrt_io.cap) {
|
||||
__rt_type_error("a cancel of an op that does not exist");
|
||||
}
|
||||
|
||||
int32_t h = jsrt_io_new(JSRT_IO_CANCEL);
|
||||
jsrt_io_op *t = &jsrt_io.ops[target];
|
||||
|
||||
switch (t->state) {
|
||||
case JSRT_IO_QUEUED:
|
||||
t->cancelled = true;
|
||||
return jsrt_io_finish(h, 0);
|
||||
case JSRT_IO_FLIGHT:
|
||||
jsrt_io.ops[h].target = target;
|
||||
jsrt_io.ops[h].target_gen = t->gen;
|
||||
return jsrt_io_queue(h);
|
||||
default:
|
||||
return jsrt_io_finish(h, -ENOENT);
|
||||
}
|
||||
}
|
||||
|
||||
// The handle of an op that is done, or -1 if no op is queued or in flight.
|
||||
// The op that the last 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;
|
||||
}
|
||||
|
||||
jsrt_io_drain();
|
||||
|
||||
int32_t h = jsrt_io_pop(&jsrt_io.done);
|
||||
|
||||
while (h < 0) {
|
||||
if (jsrt_io.in_flight == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t user_data;
|
||||
int32_t res;
|
||||
|
||||
if (!jsrt_uring_cqe(&jsrt_io.ring, &user_data, &res)) {
|
||||
jsrt_uring_submit(&jsrt_io.ring, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
jsrt_io.in_flight--;
|
||||
h = (int32_t)user_data;
|
||||
|
||||
// A timer that runs out ends with -ETIME, which is its success
|
||||
bool timer = jsrt_io.ops[h].kind == JSRT_IO_TIMEOUT;
|
||||
jsrt_io.ops[h].state = JSRT_IO_DONE;
|
||||
jsrt_io.ops[h].res = timer && res == -ETIME ? 0 : res;
|
||||
}
|
||||
|
||||
jsrt_io.given = h;
|
||||
return h;
|
||||
}
|
||||
|
||||
// >= 0 if the op succeeded (a count of bytes, an fd, or 0), else -errno
|
||||
int32_t __native_io_status(int32_t h) {
|
||||
return jsrt_io_given(h)->res;
|
||||
}
|
||||
|
||||
// The data of a read, decoded as UTF-8. Empty for any other op, or a failure
|
||||
string_t __native_io_result_string(int32_t h) {
|
||||
jsrt_io_op *op = jsrt_io_given(h);
|
||||
bool data = op->kind == JSRT_IO_READ && op->res > 0;
|
||||
return __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
|
||||
double __native_io_result_number(int32_t h) {
|
||||
jsrt_io_op *op = jsrt_io_given(h);
|
||||
|
||||
if (op->kind == JSRT_IO_STAT && op->res >= 0) {
|
||||
return (double)((struct statx *)op->buf)->stx_size;
|
||||
}
|
||||
|
||||
return op->res;
|
||||
}
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,43 @@
|
||||
// The runtime that emitted C is compiled with. See docs/model.md for the
|
||||
// representation of values
|
||||
//
|
||||
// Each header declares its part, and if JSRT_IMPLEMENTATION is defined before
|
||||
// this is included, also defines it. The emitted C is one file, which does
|
||||
// that, and includes this before any other header
|
||||
//
|
||||
// The 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
|
||||
// come before the first system header
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "core.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void jsrt_start(void);
|
||||
void jsrt_complete(int32_t h);
|
||||
|
||||
#include "str.h"
|
||||
#include "number.h"
|
||||
#include "io.h"
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// 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>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// JS's Number::toString: the fewest digits that give the number back. Plain
|
||||
// notation while the decimal point is within 21 places, else exponential
|
||||
string_t __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];
|
||||
|
||||
for (int precision = 0; precision < 17; precision++) {
|
||||
snprintf(buf, sizeof buf, "%.*e", precision, x);
|
||||
|
||||
if (strtod(buf, NULL) == x) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
char digits[32], out[64];
|
||||
char *p = buf, *o = out;
|
||||
int n = 0;
|
||||
|
||||
if (*p == '-') {
|
||||
*o++ = *p++;
|
||||
}
|
||||
|
||||
for (; *p != 'e'; p++) {
|
||||
if (*p != '.') {
|
||||
digits[n++] = *p;
|
||||
}
|
||||
}
|
||||
|
||||
digits[n] = 0;
|
||||
|
||||
// The decimal point goes after the first `point` digits
|
||||
int point = atoi(p + 1) + 1;
|
||||
|
||||
if (n <= point && point <= 21) {
|
||||
o += sprintf(o, "%s", digits);
|
||||
for (int i = n; i < point; i++) *o++ = '0';
|
||||
*o = 0;
|
||||
} else if (0 < point && point <= 21) {
|
||||
sprintf(o, "%.*s.%s", point, digits, digits + point);
|
||||
} else if (-6 < point && point <= 0) {
|
||||
o += sprintf(o, "0.");
|
||||
for (int i = point; i < 0; i++) *o++ = '0';
|
||||
sprintf(o, "%s", digits);
|
||||
} else {
|
||||
int e = point - 1;
|
||||
sprintf(o, "%c%s%se%c%d", digits[0], n > 1 ? "." : "", digits + 1, e < 0 ? '-' : '+', abs(e));
|
||||
}
|
||||
|
||||
return __rt_string_lit(out);
|
||||
}
|
||||
|
||||
string_t __rt_number_to_fixed(double x, int32_t digits) {
|
||||
char buf[400];
|
||||
snprintf(buf, sizeof buf, "%.*f", digits, x);
|
||||
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 __rt_string_to_number(string_t s) {
|
||||
char *utf8 = __rt_string_utf8(s);
|
||||
char *start = utf8, *end;
|
||||
|
||||
while (*start == ' ' || (*start >= '\t' && *start <= '\r')) start++;
|
||||
|
||||
double x = 0;
|
||||
|
||||
if (*start) {
|
||||
x = strtod(start, &end);
|
||||
while (*end == ' ' || (*end >= '\t' && *end <= '\r')) end++;
|
||||
if (*end || start == end) x = NAN;
|
||||
}
|
||||
|
||||
free(utf8);
|
||||
return x;
|
||||
}
|
||||
|
||||
#endif // JSRT_IMPLEMENTATION
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// Strings: UTF-16 code units, as JS counts them
|
||||
#pragma once
|
||||
|
||||
#include "core.h"
|
||||
|
||||
// A string. The emitted C makes its literals as static strings of this
|
||||
struct jsrt_string {
|
||||
int32_t length;
|
||||
uint16_t units[];
|
||||
};
|
||||
|
||||
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 __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 __rt_string_from_utf8(const char *utf8, size_t size) {
|
||||
const unsigned char *p = (const unsigned char *)utf8, *end = p + size;
|
||||
string_t s = __rt_string_new((int32_t)size * 2);
|
||||
int32_t n = 0;
|
||||
|
||||
while (p < end) {
|
||||
uint32_t c = *p++;
|
||||
int extra = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 0;
|
||||
c &= extra ? 0x3F >> extra : 0x7F;
|
||||
|
||||
for (; extra && p < end; extra--) {
|
||||
c = (c << 6) | (*p++ & 0x3F);
|
||||
}
|
||||
|
||||
if (c >= 0x10000) {
|
||||
c -= 0x10000;
|
||||
s->units[n++] = (uint16_t)(0xD800 + (c >> 10));
|
||||
s->units[n++] = (uint16_t)(0xDC00 + (c & 0x3FF));
|
||||
} else {
|
||||
s->units[n++] = (uint16_t)c;
|
||||
}
|
||||
}
|
||||
|
||||
s->length = n;
|
||||
return s;
|
||||
}
|
||||
|
||||
int32_t __rt_string_length(string_t s) {
|
||||
return s->length;
|
||||
}
|
||||
|
||||
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 __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++) {
|
||||
if (a->units[i] != b->units[i]) {
|
||||
return a->units[i] < b->units[i] ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return a->length < b->length ? -1 : a->length > b->length;
|
||||
}
|
||||
|
||||
string_t __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;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
// `s` as UTF-8, in a new buffer for the caller to free
|
||||
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++) {
|
||||
uint32_t c = s->units[i];
|
||||
bool pair = c >= 0xD800 && c < 0xDC00 && i + 1 < s->length
|
||||
&& s->units[i + 1] >= 0xDC00 && s->units[i + 1] < 0xE000;
|
||||
|
||||
if (pair) {
|
||||
c = 0x10000 + ((c - 0xD800) << 10) + (s->units[++i] - 0xDC00);
|
||||
}
|
||||
|
||||
if (c < 0x80) {
|
||||
*out++ = (char)c;
|
||||
} else if (c < 0x800) {
|
||||
*out++ = (char)(0xC0 | c >> 6);
|
||||
*out++ = (char)(0x80 | (c & 0x3F));
|
||||
} else if (c < 0x10000) {
|
||||
*out++ = (char)(0xE0 | c >> 12);
|
||||
*out++ = (char)(0x80 | (c >> 6 & 0x3F));
|
||||
*out++ = (char)(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
*out++ = (char)(0xF0 | c >> 18);
|
||||
*out++ = (char)(0x80 | (c >> 12 & 0x3F));
|
||||
*out++ = (char)(0x80 | (c >> 6 & 0x3F));
|
||||
*out++ = (char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
|
||||
*out = 0;
|
||||
return utf8;
|
||||
}
|
||||
|
||||
// ── for the stdlib ──
|
||||
|
||||
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 = __rt_string_new(end - start);
|
||||
memcpy(r->units, s->units + start, (size_t)(end - start) * sizeof(uint16_t));
|
||||
return r;
|
||||
}
|
||||
|
||||
int32_t __rt_string_char_code_at(string_t s, int32_t i) {
|
||||
return s->units[i];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#endif // JSRT_IMPLEMENTATION
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
// A minimal io_uring ring, on the raw syscalls. Only io.h uses it
|
||||
//
|
||||
// We write SQEs into the SQ and the kernel takes them when we submit. The
|
||||
// kernel writes CQEs into the CQ and we take them. Each side moves only its
|
||||
// own index, and reads the other's with acquire, so it sees the entries that
|
||||
// came before
|
||||
#pragma once
|
||||
|
||||
#ifdef JSRT_IMPLEMENTATION
|
||||
#include <errno.h>
|
||||
#include <linux/io_uring.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
int fd;
|
||||
|
||||
// The SQ: we own the tail, the kernel owns the head. `array` maps each
|
||||
// slot of the ring to an SQE, and we keep it the identity
|
||||
unsigned *sq_head, *sq_tail, *sq_array;
|
||||
unsigned sq_mask, sq_entries;
|
||||
struct io_uring_sqe *sqes;
|
||||
unsigned tail; // our copy of the SQ tail, ahead of it until submit
|
||||
unsigned to_submit; // SQEs written that the kernel did not take yet
|
||||
|
||||
// The CQ: the kernel owns the tail, we own the head
|
||||
unsigned *cq_head, *cq_tail;
|
||||
unsigned cq_mask, cq_entries;
|
||||
struct io_uring_cqe *cqes;
|
||||
} jsrt_uring;
|
||||
|
||||
_Noreturn static void jsrt_uring_fail(const char *what) {
|
||||
fprintf(stderr, "jsrt: %s: %s\n", what, strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// The features that io.h depends on, all from Linux 5.5:
|
||||
// SINGLE_MMAP the SQ and the CQ rings are one mapping
|
||||
// NODROP the kernel keeps CQEs that do not fit the CQ, never drops them
|
||||
// SUBMIT_STABLE the kernel copies what an SQE points to (a timespec, a path)
|
||||
// before submit returns, so it needs to stay only until then
|
||||
static void jsrt_uring_init(jsrt_uring *r, unsigned entries) {
|
||||
struct io_uring_params p;
|
||||
memset(&p, 0, sizeof p);
|
||||
|
||||
int fd = (int)syscall(__NR_io_uring_setup, entries, &p);
|
||||
|
||||
if (fd < 0) {
|
||||
jsrt_uring_fail("io_uring_setup");
|
||||
}
|
||||
|
||||
unsigned need = IORING_FEAT_SINGLE_MMAP | IORING_FEAT_NODROP | IORING_FEAT_SUBMIT_STABLE;
|
||||
|
||||
if ((p.features & need) != need) {
|
||||
fputs("jsrt: io_uring: the kernel is older than Linux 5.5\n", stderr);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
size_t sq_size = p.sq_off.array + p.sq_entries * sizeof(unsigned);
|
||||
size_t cq_size = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe);
|
||||
size_t size = sq_size > cq_size ? sq_size : cq_size;
|
||||
int prot = PROT_READ | PROT_WRITE, flags = MAP_SHARED | MAP_POPULATE;
|
||||
|
||||
char *ring = mmap(NULL, size, prot, flags, fd, IORING_OFF_SQ_RING);
|
||||
void *sqes = mmap(NULL, p.sq_entries * sizeof(struct io_uring_sqe), prot, flags, fd, IORING_OFF_SQES);
|
||||
|
||||
if (ring == MAP_FAILED || sqes == MAP_FAILED) {
|
||||
jsrt_uring_fail("io_uring mmap");
|
||||
}
|
||||
|
||||
*r = (jsrt_uring){
|
||||
.fd = fd,
|
||||
.sq_head = (unsigned *)(ring + p.sq_off.head),
|
||||
.sq_tail = (unsigned *)(ring + p.sq_off.tail),
|
||||
.sq_array = (unsigned *)(ring + p.sq_off.array),
|
||||
.sq_mask = *(unsigned *)(ring + p.sq_off.ring_mask),
|
||||
.sq_entries = p.sq_entries,
|
||||
.sqes = sqes,
|
||||
.tail = *(unsigned *)(ring + p.sq_off.tail),
|
||||
.cq_head = (unsigned *)(ring + p.cq_off.head),
|
||||
.cq_tail = (unsigned *)(ring + p.cq_off.tail),
|
||||
.cq_mask = *(unsigned *)(ring + p.cq_off.ring_mask),
|
||||
.cq_entries = p.cq_entries,
|
||||
.cqes = (struct io_uring_cqe *)(ring + p.cq_off.cqes),
|
||||
};
|
||||
}
|
||||
|
||||
// A new SQE, all zero, or NULL if the SQ is full. The kernel sees it at the
|
||||
// next submit
|
||||
static struct io_uring_sqe *jsrt_uring_sqe(jsrt_uring *r) {
|
||||
unsigned head = __atomic_load_n(r->sq_head, __ATOMIC_ACQUIRE);
|
||||
|
||||
if (r->tail - head == r->sq_entries) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
unsigned i = r->tail & r->sq_mask;
|
||||
r->sq_array[i] = i;
|
||||
r->tail++;
|
||||
r->to_submit++;
|
||||
|
||||
memset(&r->sqes[i], 0, sizeof r->sqes[i]);
|
||||
return &r->sqes[i];
|
||||
}
|
||||
|
||||
static bool jsrt_uring_cq_empty(jsrt_uring *r) {
|
||||
return *r->cq_head == __atomic_load_n(r->cq_tail, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
// Submits every SQE written, and if `wait`, waits until the CQ has a CQE
|
||||
//
|
||||
// The kernel can take fewer SQEs than we give, for example when one fails
|
||||
// before it starts. That one still gets a CQE, and we give the rest again
|
||||
static void jsrt_uring_submit(jsrt_uring *r, bool wait) {
|
||||
__atomic_store_n(r->sq_tail, r->tail, __ATOMIC_RELEASE);
|
||||
|
||||
for (;;) {
|
||||
bool get = wait && jsrt_uring_cq_empty(r);
|
||||
|
||||
if (!r->to_submit && !get) {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned flags = get ? IORING_ENTER_GETEVENTS : 0;
|
||||
int n = (int)syscall(__NR_io_uring_enter, r->fd, r->to_submit, get ? 1 : 0, flags, NULL, 0);
|
||||
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
jsrt_uring_fail("io_uring_enter");
|
||||
}
|
||||
|
||||
r->to_submit -= (unsigned)n;
|
||||
}
|
||||
}
|
||||
|
||||
// Takes the next CQE, if there is one
|
||||
static bool jsrt_uring_cqe(jsrt_uring *r, uint64_t *user_data, int32_t *res) {
|
||||
if (jsrt_uring_cq_empty(r)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned head = *r->cq_head;
|
||||
struct io_uring_cqe *cqe = &r->cqes[head & r->cq_mask];
|
||||
*user_data = cqe->user_data;
|
||||
*res = cqe->res;
|
||||
|
||||
__atomic_store_n(r->cq_head, head + 1, __ATOMIC_RELEASE);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // JSRT_IMPLEMENTATION
|
||||
@@ -0,0 +1,153 @@
|
||||
// Arrays. The compiler gives only `a[i]`, `a[i] = x` and `a.length`, which can
|
||||
// also be written. Every method here is built on those
|
||||
//
|
||||
// Not as the spec says:
|
||||
// - A method takes one value where the spec takes any number (`push(x)`,
|
||||
// `unshift(x)`, `concat(other)`), because more need rest params
|
||||
// - A callback gets the element and its index, not the array too
|
||||
// - Holes are not skipped: a callback sees undefined for a hole
|
||||
|
||||
// `Array(n)` and `new Array(n)` make an array of `n` holes
|
||||
import { relativeIndex } from "jsrt:string";
|
||||
|
||||
export function Array(n) {
|
||||
const a = [];
|
||||
if (n !== undefined) a.length = n;
|
||||
return a;
|
||||
}
|
||||
|
||||
Array.prototype = {
|
||||
// ── Change the array ──
|
||||
push(x) {
|
||||
this[this.length] = x;
|
||||
return this.length;
|
||||
},
|
||||
pop() {
|
||||
if (this.length === 0) return undefined;
|
||||
const x = this[this.length - 1];
|
||||
this.length = this.length - 1;
|
||||
return x;
|
||||
},
|
||||
shift() {
|
||||
if (this.length === 0) return undefined;
|
||||
const x = this[0];
|
||||
for (let i = 1; i < this.length; i++) this[i - 1] = this[i];
|
||||
this.length = this.length - 1;
|
||||
return x;
|
||||
},
|
||||
unshift(x) {
|
||||
for (let i = this.length; i > 0; i--) this[i] = this[i - 1];
|
||||
this[0] = x;
|
||||
return this.length;
|
||||
},
|
||||
reverse() {
|
||||
for (let i = 0, j = this.length - 1; i < j; i++, j--) {
|
||||
const x = this[i];
|
||||
this[i] = this[j];
|
||||
this[j] = x;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
fill(x, start, end) {
|
||||
const len = this.length;
|
||||
const s = relativeIndex(start === undefined ? 0 : start, len);
|
||||
const e = relativeIndex(end === undefined ? len : end, len);
|
||||
for (let i = s; i < e; i++) this[i] = x;
|
||||
return this;
|
||||
},
|
||||
|
||||
// ── Read ──
|
||||
at(i) {
|
||||
if (i < 0) i += this.length;
|
||||
return this[i];
|
||||
},
|
||||
indexOf(x, from) {
|
||||
for (let i = from === undefined ? 0 : relativeIndex(from, this.length); i < this.length; i++) {
|
||||
if (this[i] === x) return i;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
lastIndexOf(x) {
|
||||
for (let i = this.length - 1; i >= 0; i--) {
|
||||
if (this[i] === x) return i;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
includes(x) {
|
||||
return this.indexOf(x) !== -1;
|
||||
},
|
||||
join(sep) {
|
||||
const s = sep === undefined ? "," : sep;
|
||||
let out = "";
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (i > 0) out = out + s;
|
||||
const x = this[i];
|
||||
if (x !== undefined && x !== null) out = out + String(x);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
toString() {
|
||||
return this.join(",");
|
||||
},
|
||||
|
||||
// ── Make a new array ──
|
||||
slice(start, end) {
|
||||
const len = this.length;
|
||||
const s = relativeIndex(start === undefined ? 0 : start, len);
|
||||
const e = relativeIndex(end === undefined ? len : end, len);
|
||||
const out = [];
|
||||
for (let i = s; i < e; i++) out[i - s] = this[i];
|
||||
return out;
|
||||
},
|
||||
concat(other) {
|
||||
const out = this.slice();
|
||||
for (let i = 0; i < other.length; i++) out[out.length] = other[i];
|
||||
return out;
|
||||
},
|
||||
|
||||
// ── With a callback ──
|
||||
forEach(f) {
|
||||
for (let i = 0; i < this.length; i++) f(this[i], i);
|
||||
},
|
||||
map(f) {
|
||||
const out = [];
|
||||
for (let i = 0; i < this.length; i++) out[i] = f(this[i], i);
|
||||
return out;
|
||||
},
|
||||
filter(f) {
|
||||
const out = [];
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (f(this[i], i)) out[out.length] = this[i];
|
||||
}
|
||||
return out;
|
||||
},
|
||||
reduce(f, init) {
|
||||
let acc = init;
|
||||
for (let i = 0; i < this.length; i++) acc = f(acc, this[i], i);
|
||||
return acc;
|
||||
},
|
||||
find(f) {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (f(this[i], i)) return this[i];
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
findIndex(f) {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (f(this[i], i)) return i;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
some(f) {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (f(this[i], i)) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
every(f) {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (!f(this[i], i)) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
// Converts any value to a boolean, like the spec's ToBoolean
|
||||
export function Boolean(x) {
|
||||
return !!x;
|
||||
}
|
||||
|
||||
// In these methods `this` is the primitive boolean itself
|
||||
Boolean.prototype = {
|
||||
toString() { return this ? "true" : "false"; },
|
||||
valueOf() { return this; },
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
// Not as the spec says: it prints its first arg only, because more args need
|
||||
// rest params, which need arrays
|
||||
import { __native_print_string } from "jsrt:native";
|
||||
|
||||
export const console = {
|
||||
log(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);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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)
|
||||
|
||||
export const undefined = void 0;
|
||||
export const NaN = 0 / 0;
|
||||
export const Infinity = 1 / 0;
|
||||
@@ -0,0 +1,84 @@
|
||||
// Files, with promises, on the IO natives (see lib/rt/io.h). They are like
|
||||
// node's `fs.promises`, but only for UTF-8 text:
|
||||
// - `fs.readFile(path)` gives the text of the file
|
||||
// - `fs.writeFile(path, text)` makes or replaces the file
|
||||
// A failure rejects with the negative errno, not an `Error`. A read or a write
|
||||
// that the kernel does not finish in one op is not continued
|
||||
|
||||
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
|
||||
function __io_status(h) {
|
||||
return new Promise((resolve) => {
|
||||
__pending[h] = (h) => resolve(__native_io_status(h));
|
||||
});
|
||||
}
|
||||
|
||||
function __io_string(h) {
|
||||
return new Promise((resolve, reject) => {
|
||||
__pending[h] = (h) => {
|
||||
const status = __native_io_status(h);
|
||||
if (status < 0) reject(status);
|
||||
else resolve(__native_io_result_string(h));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function __io_number(h) {
|
||||
return new Promise((resolve, reject) => {
|
||||
__pending[h] = (h) => {
|
||||
const status = __native_io_status(h);
|
||||
if (status < 0) reject(status);
|
||||
else resolve(__native_io_result_number(h));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// A promise of the status of op `h`, rejected if it failed
|
||||
function __io_ok(h) {
|
||||
return __io_status(h).then((status) => (status < 0 ? Promise.reject(status) : status));
|
||||
}
|
||||
|
||||
export const fs = {
|
||||
readFile(path) {
|
||||
let fd = -1;
|
||||
let text = "";
|
||||
|
||||
return __io_ok(__native_io_queue_open(path, 0, 0))
|
||||
.then((f) => {
|
||||
fd = f;
|
||||
return __io_number(__native_io_queue_stat(path));
|
||||
})
|
||||
.then((size) => __io_string(__native_io_queue_read(fd, size, 0)))
|
||||
.then((s) => {
|
||||
text = s;
|
||||
return __io_ok(__native_io_queue_close(fd));
|
||||
})
|
||||
.then(() => text);
|
||||
},
|
||||
|
||||
writeFile(path, text) {
|
||||
let fd = -1;
|
||||
|
||||
// O_WRONLY | O_CREAT | O_TRUNC, mode 0644
|
||||
return __io_ok(__native_io_queue_open(path, 577, 420))
|
||||
.then((f) => {
|
||||
fd = f;
|
||||
return __io_ok(__native_io_queue_write(fd, text, 0));
|
||||
})
|
||||
.then(() => __io_ok(__native_io_queue_close(fd)))
|
||||
.then(() => undefined);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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
|
||||
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
|
||||
// handler and the value that it is called with. So each handler gets only the
|
||||
// values of its own promise (see `docs/model.md`, section 3.3)
|
||||
const __microtasks = { jobs: [], head: 0 };
|
||||
|
||||
export function __queue_job(job) {
|
||||
__microtasks.jobs.push(job);
|
||||
}
|
||||
|
||||
export function __drain_microtasks() {
|
||||
const q = __microtasks;
|
||||
|
||||
while (q.head < q.jobs.length) {
|
||||
const job = q.jobs[q.head];
|
||||
q.jobs[q.head] = undefined;
|
||||
q.head++;
|
||||
job();
|
||||
}
|
||||
|
||||
q.jobs.length = 0;
|
||||
q.head = 0;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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
|
||||
export function Number(x) {
|
||||
switch (typeof x) {
|
||||
case "number": return 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;
|
||||
default: return NaN;
|
||||
}
|
||||
}
|
||||
|
||||
Number.NaN = NaN;
|
||||
Number.POSITIVE_INFINITY = Infinity;
|
||||
Number.NEGATIVE_INFINITY = -Infinity;
|
||||
Number.MAX_SAFE_INTEGER = 9007199254740991;
|
||||
Number.MIN_SAFE_INTEGER = -9007199254740991;
|
||||
|
||||
Number.isNaN = function (x) { return typeof x === "number" && x !== x; };
|
||||
Number.isFinite = function (x) { return typeof x === "number" && x === x && x !== Infinity && x !== -Infinity; };
|
||||
Number.isInteger = function (x) { return Number.isFinite(x) && x % 1 === 0; };
|
||||
|
||||
// In these methods `this` is the primitive number itself
|
||||
Number.prototype = {
|
||||
toString(radix) {
|
||||
if (radix === undefined || radix === 10) return __rt_number_to_string(this);
|
||||
return integerToString(this, radix);
|
||||
},
|
||||
toFixed(digits) { return __rt_number_to_fixed(this, digits === undefined ? 0 : digits); },
|
||||
valueOf() { return this; },
|
||||
};
|
||||
|
||||
// An integer in `radix` (2 to 36). Only integers: the fraction is dropped
|
||||
function integerToString(n, radix) {
|
||||
let negative = n < 0;
|
||||
if (negative) n = -n;
|
||||
n = n - (n % 1);
|
||||
|
||||
let out = "";
|
||||
do {
|
||||
let digit = n % radix;
|
||||
out = __rt_string_concat(__rt_string_from_char_code(digit < 10 ? 48 + digit : 87 + digit), out);
|
||||
n = (n - digit) / radix;
|
||||
} while (n > 0);
|
||||
|
||||
return negative ? __rt_string_concat("-", out) : out;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// `new Object()` makes an empty object. Called without `new`, it does the same
|
||||
export function Object() {
|
||||
return {};
|
||||
}
|
||||
|
||||
Object.prototype = {};
|
||||
|
||||
Object.create = function (proto) {
|
||||
return { __proto__: proto };
|
||||
};
|
||||
@@ -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";
|
||||
@@ -0,0 +1,201 @@
|
||||
// Promises, as the spec says, except:
|
||||
// - A throw in a handler or an executor does not reject, because there is no
|
||||
// `try`. `reject()` and a returned rejected promise do
|
||||
// - An unhandled rejection does nothing. Node prints it and stops
|
||||
// - `Promise.all` takes an array, not any iterable
|
||||
//
|
||||
// Written for type inference, which names an object by the instr that makes
|
||||
// it and the call place of the call it is made in (see compiler/src/infer):
|
||||
// - A promise is an object literal, made in the fn that user code calls, not
|
||||
// in a helper. So each place in user code gets its own promise objects, and
|
||||
// their fields are always there
|
||||
// - The closures that run a reaction are made in `then`, for the same reason
|
||||
// - Fulfilment and rejection have their own fns and fields, so that a result
|
||||
// and a reason never share a type
|
||||
// - 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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
let reason;
|
||||
|
||||
const fulfilled = () => {
|
||||
if (typeof onFulfilled === "function") __resolve(derived, onFulfilled(value));
|
||||
else __resolve(derived, value);
|
||||
};
|
||||
|
||||
const rejected = () => {
|
||||
if (typeof onRejected === "function") __resolve(derived, onRejected(reason));
|
||||
else __reject(derived, reason);
|
||||
};
|
||||
|
||||
// What `__fulfil()` and `__settle_rejected()` call: each runs its
|
||||
// handler in a job of its own
|
||||
const reaction = {
|
||||
fulfil: (v) => {
|
||||
value = v;
|
||||
__queue_job(fulfilled);
|
||||
},
|
||||
reject: (r) => {
|
||||
reason = r;
|
||||
__queue_job(rejected);
|
||||
},
|
||||
};
|
||||
|
||||
if (this.state === __PENDING) {
|
||||
this.reactions.push(reaction);
|
||||
} else if (this.state === __FULFILLED) {
|
||||
reaction.fulfil(this.value);
|
||||
} else {
|
||||
reaction.reject(this.reason);
|
||||
}
|
||||
|
||||
return derived;
|
||||
},
|
||||
|
||||
catch(onRejected) {
|
||||
return this.then(undefined, onRejected);
|
||||
},
|
||||
|
||||
finally(f) {
|
||||
return this.then(
|
||||
(value) => {
|
||||
f();
|
||||
return value;
|
||||
},
|
||||
(reason) => {
|
||||
f();
|
||||
return Promise.reject(reason);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
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
|
||||
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
|
||||
export function __reject(p, reason) {
|
||||
if (p.resolved) return;
|
||||
p.resolved = true;
|
||||
__settle_rejected(p, reason);
|
||||
}
|
||||
|
||||
// Fulfil `p` with `value`, or if `value` is a thenable, adopt its state. That
|
||||
// is in a job of its own, as the spec says, so it takes a tick more
|
||||
function __fulfil_or_adopt(p, value) {
|
||||
if ((typeof value === "object" && value !== null) || typeof value === "function") {
|
||||
const then = value.then;
|
||||
|
||||
if (typeof then === "function") {
|
||||
__queue_adopt(p, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
__fulfil(p, value);
|
||||
}
|
||||
|
||||
// A closure boxes the args that it captures, and the box is one for every
|
||||
// call from one place. So only this fn captures `p` and `value`, and only
|
||||
// thenables share its boxes
|
||||
function __queue_adopt(p, thenable) {
|
||||
__queue_job(() => __adopt(p, thenable));
|
||||
}
|
||||
|
||||
// The reactions are cleared in place: a new array here would be one array
|
||||
// for every promise
|
||||
function __fulfil(p, value) {
|
||||
p.state = __FULFILLED;
|
||||
p.value = value;
|
||||
|
||||
const reactions = p.reactions;
|
||||
for (let i = 0; i < reactions.length; i++) reactions[i].fulfil(value);
|
||||
reactions.length = 0;
|
||||
}
|
||||
|
||||
function __settle_rejected(p, reason) {
|
||||
p.state = __REJECTED;
|
||||
p.reason = reason;
|
||||
|
||||
const reactions = p.reactions;
|
||||
for (let i = 0; i < reactions.length; i++) reactions[i].reject(reason);
|
||||
reactions.length = 0;
|
||||
}
|
||||
|
||||
// Make `p` follow the thenable `thenable`
|
||||
function __adopt(p, thenable) {
|
||||
thenable.then(
|
||||
(value) => {
|
||||
if (p.state === __PENDING) __fulfil_or_adopt(p, value);
|
||||
},
|
||||
(reason) => {
|
||||
if (p.state === __PENDING) __settle_rejected(p, reason);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function queueMicrotask(f) {
|
||||
__queue_job(f);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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
|
||||
export function String(x) {
|
||||
switch (typeof x) {
|
||||
case "string": return 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] }";
|
||||
case "object":
|
||||
if (x === null) return "null";
|
||||
if (typeof x.toString === "function") return x.toString();
|
||||
return "[object Object]";
|
||||
}
|
||||
}
|
||||
|
||||
String.fromCharCode = function (code) {
|
||||
return __rt_string_from_char_code(code);
|
||||
};
|
||||
|
||||
// In these methods `this` is the primitive string itself
|
||||
String.prototype = {
|
||||
toString() { return this; },
|
||||
valueOf() { return this; },
|
||||
|
||||
// ── Characters ──
|
||||
charAt(i) {
|
||||
if (i < 0 || i >= this.length) return "";
|
||||
return __rt_string_slice(this, i, i + 1);
|
||||
},
|
||||
charCodeAt(i) {
|
||||
if (i < 0 || i >= this.length) return NaN;
|
||||
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 __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 (__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 (__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 __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 && __rt_string_slice(this, start, stop) === search;
|
||||
},
|
||||
|
||||
// ── Substrings ──
|
||||
slice(start, end) {
|
||||
let len = this.length;
|
||||
let s = relativeIndex(start === undefined ? 0 : start, len);
|
||||
let e = relativeIndex(end === undefined ? len : end, len);
|
||||
return s < e ? __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 ? __rt_string_slice(this, s, e) : __rt_string_slice(this, e, s);
|
||||
},
|
||||
|
||||
// ── Case ──
|
||||
toUpperCase() { return mapCodes(this, 97, 122, -32); }, // a-z to A-Z
|
||||
toLowerCase() { return mapCodes(this, 65, 90, 32); }, // A-Z to a-z
|
||||
|
||||
// ── Whitespace ──
|
||||
trim() { return this.trimStart().trimEnd(); },
|
||||
trimStart() {
|
||||
let i = 0;
|
||||
while (i < this.length && isSpace(__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(__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 = __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); },
|
||||
};
|
||||
|
||||
// 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]
|
||||
export function relativeIndex(i, len) {
|
||||
return clampIndex(i < 0 ? i + len : i, len);
|
||||
}
|
||||
|
||||
// `s` with every char code in [lo, hi] moved by `shift`, for ASCII case mapping
|
||||
function mapCodes(s, lo, hi, shift) {
|
||||
let out = "";
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
let c = __rt_string_char_code_at(s, i);
|
||||
if (c >= lo && c <= hi) c += shift;
|
||||
out = __rt_string_concat(out, __rt_string_from_char_code(c));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isSpace(c) {
|
||||
// tab, line feed, vertical tab, form feed, carriage return, space, nbsp
|
||||
return (c >= 9 && c <= 13) || c === 32 || c === 160;
|
||||
}
|
||||
|
||||
function pad(s, length, fill, atStart) {
|
||||
let f = fill === undefined ? " " : String(fill);
|
||||
if (s.length >= length || f.length === 0) return s;
|
||||
|
||||
let padding = "";
|
||||
while (padding.length < length - s.length) padding = __rt_string_concat(padding, f);
|
||||
padding = __rt_string_slice(padding, 0, length - s.length);
|
||||
|
||||
return atStart ? __rt_string_concat(padding, s) : __rt_string_concat(s, padding);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Timers. `setTimeout` gives a record, as node does, not an int: an IO handle
|
||||
// is used again after its op completes, so `clearTimeout` of an old int could
|
||||
// cancel another op. A record stays the timer's own
|
||||
//
|
||||
// Not as the spec says: `setTimeout` takes no args for the callback
|
||||
|
||||
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 };
|
||||
|
||||
__pending[timer.handle] = (h) => {
|
||||
if (!timer.active) return;
|
||||
timer.active = false;
|
||||
f();
|
||||
};
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
export function clearTimeout(timer) {
|
||||
if (timer === undefined || timer === null || !timer.active) return;
|
||||
timer.active = false;
|
||||
__native_io_queue_cancel(timer.handle);
|
||||
}
|
||||
+1
Submodule third_party/stb added at 2c980bb598
Reference in New Issue
Block a user