Compare commits

...
20 Commits
Author SHA1 Message Date
uditdey 1e12e7be2e Initial ES modules support 2026-10-26 12:39:58 +00:00
uditdey f7c5b2c80f Make stdlib classes uniform 2026-10-24 16:04:01 +00:00
uditdey 95e5c1225e New macro based emit 2026-10-24 15:58:51 +00:00
uditdey cf455b88a8 Modified infer 2026-10-23 12:07:23 +00:00
uditdey a80bf6f19a New emit 2026-10-22 13:05:04 +00:00
uditdey 1e4647347f Rewrite with working async 2026-10-16 08:00:06 +00:00
UditDey d0c2f291e9 Mid rewrite 2026-10-13 11:52:02 +00:00
uditdey 2f21347088 Invert event loop 2026-10-08 08:53:11 +00:00
uditdey c4d0bab3d9 Silence many warnings 2026-10-08 07:34:56 +00:00
uditdey 2e6e4693aa Merge remote-tracking branch 'origin/main' 2026-10-08 07:29:36 +00:00
uditdey 316a2fcd75 Locked in type model 2026-10-08 07:28:51 +00:00
uditdey 2b612d19ed New partly working cell model 2026-10-07 16:41:33 +00:00
uditdey 44498b32a9 Broken site based impl 2026-10-03 14:06:24 +00:00
uditdey 23a2c3bc46 New model 2026-10-01 13:58:50 +00:00
uditdey caa57fa8b9 Add README 2026-09-29 14:46:51 +05:30
UditDey a4d295fbd9 Misc cleanups 2026-09-28 07:47:01 +00:00
uditdey e93ce94ac7 Big progress by Claude 2026-09-25 19:31:15 +00:00
uditdey 183c4e35c9 Claude's declass improvements 2026-09-17 13:00:22 +00:00
UditDey 1a36873812 Improve infer and transforms 2026-09-16 07:32:04 +00:00
UditDey 5d687e308e Improve infer 2026-09-13 16:49:36 +00:00
57 changed files with 8604 additions and 797 deletions
+3
View File
@@ -1,3 +1,6 @@
[submodule "third_party/quickjs"]
path = third_party/quickjs
url = https://github.com/quickjs-ng/quickjs.git
[submodule "third_party/stb"]
path = third_party/stb
url = https://github.com/nothings/stb.git
+8
View File
@@ -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
View File
@@ -126,6 +126,22 @@ class OpDef(NamedTuple):
temporary: bool # written as lowercase `def()`: a compiler-only opcode
# Opcodes that this compiler adds. QuickJS never emits them, so they come after
# every real opcode and `short_opcode_info` never gives them for a byte of
# bytecode. Only the transforms emit them.
EXTRA_OPS = [
# [obj, value] -> [obj]: like `define_field`, but the field is the location
# that `value` was read from, not a copy of it (see transform/closure.rs)
OpDef("define_field_ref", 5, 2, 1, "atom", False),
# [] -> []: like `close_loc`, for a capture: the binding that capture `i` shares gets a
# new location, with the current value (see transform/async.rs)
OpDef("close_var_ref", 3, 0, 0, "var_ref", False),
# [obj] -> []: field `x` of `obj`, which shares a location, gets a new location, with
# the current value. What `close_var_ref` lowers to (see transform/closure.rs)
OpDef("close_field", 5, 1, 0, "atom", False),
]
def parse_fmt_list(text: str) -> list[str]:
"""The `FMT(name)` list, in order."""
out = []
@@ -284,6 +300,8 @@ def gen_opcodes(text: str, tag: str) -> str:
# no short-opcode shift, unlike the C `short_opcode_info` macro whose table
# still holds them.
real = [o for o in ops if not o.temporary]
real_count = len(real)
real += EXTRA_OPS
s = [header(tag, "quickjs-opcode.h")]
@@ -351,8 +369,10 @@ def gen_opcodes(text: str, tag: str) -> str:
"///\n"
"/// The table already excludes the compiler's temporary opcodes, so the\n"
"/// byte indexes it directly; there is no short-opcode shift to apply.\n"
"/// The opcodes that this compiler adds are not in bytecode, so they\n"
"/// are never given.\n"
"pub fn short_opcode_info(op: u8) -> Option<&'static OpInfo> {\n"
" OPCODE_INFO.get(op as usize)\n"
f" OPCODE_INFO[..{real_count}].get(op as usize)\n"
"}\n"
)
return "".join(s)
+1
View File
@@ -4,5 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
cc = "1.5.1"
nom = "8.0.0"
num_enum = "0.7.6"
+7
View File
@@ -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");
-204
View File
@@ -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
}
+110
View File
@@ -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(", ")),
)
}
}
+279
View File
@@ -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()
}
+109
View File
@@ -0,0 +1,109 @@
//! The C functions of the program
//!
//! There are four kinds:
//!
//! - **A JS function**, as infer walked it. Infer walks a JS function once for each context,
//! which is the function in one call site, and each set of arg types. Each walk is one C
//! function (see `js`).
//! - **A lookup**, which gets a field that some objects of a class do not have. It gives the
//! object's own field if the object has it, else it looks on the object's prototype, and so
//! on along the prototype chain. At the end of the chain, the field is undefined (see
//! `objects`).
//! - **`jsrt_start`**, which the host calls to run the program (see lib/rt/jsrt.h).
//! - **`jsrt_complete`**, which the host calls when an IO op completes.
use super::classes::{CType, ClassKind, Member};
use super::program::Program;
use super::{js, objects};
use crate::{flatten::ENTRY_FN, infer::Type, libqjs::parse::AtomRef, regify::Operand};
#[derive(Clone, Copy)]
pub enum Function {
Js(usize), // a JS function, as one walk: the walk's index
Lookup(usize, AtomRef), // a lookup of a field on the objects of a class: the class, and the field
Start,
Complete,
}
impl Function {
pub fn name(&self, program: &Program) -> String {
match self {
Function::Js(walk) => format!("js{walk}"),
Function::Lookup(class, field) => {
format!("lookup_c{class}_{}", program.field_name(*field))
}
Function::Start => "jsrt_start".into(),
Function::Complete => "jsrt_complete".into(),
}
}
pub fn signature(&self, program: &Program) -> String {
let name = self.name(program);
match self {
Function::Js(walk) => js::signature(program, *walk),
Function::Lookup(class, field) => {
let ctype = program.lookup_ctype(*class, *field).c_name();
format!("static {ctype} {name}(struct c{class} *o)")
}
Function::Start => format!("void {name}(void)"),
Function::Complete => format!("void {name}(int32_t h)"),
}
}
pub fn definition(&self, program: &Program) -> String {
let body = match self {
Function::Js(walk) => js::body(program, *walk),
Function::Lookup(class, field) => objects::lookup_body(program, *class, *field),
Function::Start => start_body(program),
Function::Complete => complete_body(program),
};
format!("{} {{\n{body}}}\n\n", self.signature(program))
}
}
/// Call the entry, with undefined args
fn start_body(program: &Program) -> String {
let arg_count = program.fns[ENTRY_FN].arg_count;
let args = vec![js::UNDEFINED.to_string(); arg_count];
format!(" {};\n", host_call(program, program.entry, &args))
}
/// Call the stdlib's `__complete` with `h`, the handle of the op, if the program has it
fn complete_body(program: &Program) -> String {
match complete_call(program) {
Some(call) => format!(" {call};\n"),
None => " (void)h;\n".into(),
}
}
/// The call of `__complete`, with the closure as its own env and `this` undefined. Infer
/// made the same call (see `TypeCache::call_host_complete`)
fn complete_call(program: &Program) -> Option<String> {
let (callee, ctype) = program.binding("jsrt:loop", "__complete")?;
let CType::One(Member::Obj(class)) = ctype else {
return None;
};
let ClassKind::Closure(fn_idx) = program.classes.classes[class].kind else {
return None;
};
let obj = program.classes.classes[class].objs[0];
let types = [Type::Ref(obj), Type::Undefined, Type::Int];
let walk = program.cache.find(fn_idx, None, &types, program.fns);
let args = [callee, js::UNDEFINED.to_string(), "h".to_string()];
Some(host_call(program, walk, &args))
}
/// A call of a walk's C function, with each arg converted to its param's C type
fn host_call(program: &Program, walk: usize, args: &[String]) -> String {
let args: Vec<String> = (args.iter().enumerate())
.map(|(i, arg)| {
let param = program.slot_ctype(walk, Operand::Arg(i)).c_name();
format!("CAST({param}, {arg})")
})
.collect();
format!("js{walk}({})", args.join(", "))
}
+354
View File
@@ -0,0 +1,354 @@
//! A JS function as a C function
//!
//! Infer walks a JS function once for each context, which is the function in one call site,
//! and each set of arg types. Each walk is one C function, `js<walk>`, in which every value
//! has the type that infer found in that walk.
//!
//! Each local, arg and reg is a C local, of the C type of its type. A local or arg that
//! closures share is a pointer to a cell (see transform/closure.rs). The cell is made when the
//! function starts, and again at each `close_loc`. A shared arg is copied into its cell when
//! the function starts.
//!
//! Each block that can run is a C label. Its instrs come in order, then its postfix copies,
//! then its jump. If infer found that a condition is always true or always false, the jump
//! goes directly to the one block that can run.
//!
//! Most instrs are one line: `SET(dst, JS_<op>(operands))`. The runtime's macros convert
//! each operand and the result, by their C types (see lib/rt/core.h).
use super::program::{Program, slots};
use crate::{
infer::{Context, Type},
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
regify::{BasicBlock, Operand, RegFn, RegInstr},
};
/// The undefined value, as C
pub const UNDEFINED: &str = "(undefined_t){0}";
/// A walk of a JS function, as its C function is written
pub struct JsFunction<'a> {
pub program: &'a Program<'a>,
pub walk: usize,
pub context: Context, // the walk's function, and the call site that it is called from
pub f: &'a RegFn,
shared: Vec<Operand>, // the locals and args that closures share: each is a pointer to a cell
}
/// `static <ret> js<walk>(<params>)`. A shared arg's param is named `p<k>`, because its value
/// goes into a cell named `a<k>`
pub fn signature(program: &Program, walk: usize) -> String {
let f = program.js_fn(walk);
let shared = f.captured_slots();
let params: Vec<String> = (0..f.arg_count)
.map(|k| {
let ctype = program.slot_ctype(walk, Operand::Arg(k)).c_name();
let prefix = if shared.contains(&Operand::Arg(k)) {
"p"
} else {
"a"
};
format!("{ctype} {prefix}{k}")
})
.collect();
let params = if params.is_empty() {
"void".into()
} else {
params.join(", ")
};
let ret = program.ret_ctype(walk).c_name();
format!("static {ret} js{walk}({params})")
}
/// The body of `js<walk>`: its locals, args and regs, then each block that can run
pub fn body(program: &Program, walk: usize) -> String {
let f = program.js_fn(walk);
let reachable = &program.cache.walks[walk].reachable;
let js = JsFunction {
program,
walk,
context: program.cache.walks[walk].context,
f,
shared: f.captured_slots(),
};
let mut c = indent(&js.declarations());
for (i, block) in f.blocks.iter().enumerate() {
if reachable[i] {
c += &format!("b{i}:;\n");
c += &indent(&js.block(block, reachable));
}
}
c
}
/// Each line of `code`, indented once
pub fn indent(code: &str) -> String {
code.lines().map(|line| format!(" {line}\n")).collect()
}
impl JsFunction<'_> {
/// The C name of a local, arg or reg. A shared one names the pointer to its cell
pub fn name(&self, slot: Operand) -> String {
match slot {
Operand::Local(k) => format!("l{k}"),
Operand::Arg(k) => format!("a{k}"),
Operand::Reg(k) => format!("r{k}"),
}
}
/// The value of a local, arg or reg, as C. A shared one is the value in its cell
pub fn get(&self, slot: Operand) -> String {
match self.shared.contains(&slot) {
true => format!("(*{})", self.name(slot)),
false => self.name(slot),
}
}
/// `dst = value`, converted to the C type of `dst`
pub fn set(&self, dst: Operand, value: &str) -> String {
format!("SET({}, {value});\n", self.get(dst))
}
/// The type of a local, arg or reg
pub fn ty(&self, slot: Operand) -> Type {
self.program.slot_type(self.walk, slot)
}
/// A C local for each local and reg, and a cell for each shared local and arg
fn declarations(&self) -> String {
let mut c = String::new();
for slot in slots(self.f) {
let ctype = self.program.slot_ctype(self.walk, slot).c_name();
let name = self.name(slot);
let cell = format!("__rt_alloc(sizeof({ctype}))");
c += &match (slot, self.shared.contains(&slot)) {
(Operand::Arg(_), false) => continue,
(Operand::Arg(k), true) => format!("{ctype} *{name} = {cell}; *{name} = p{k};\n"),
(_, true) => format!("{ctype} *{name} = {cell};\n"),
(_, false) => format!("{ctype} {name} = {{0}};\n"),
};
}
c
}
/// A block: its instrs, its postfix copies, then its jump. If an instr cannot run, nor
/// can the rest of the block
fn block(&self, block: &BasicBlock, reachable: &[bool]) -> String {
use OpCode::*;
let last = &self.f.code[block.end - 1];
let jumps = matches!(
last.op,
Goto | Goto8 | Goto16 | IfFalse | IfFalse8 | IfTrue | IfTrue8
);
let end = if jumps { block.end - 1 } else { block.end };
let mut c = String::new();
for idx in block.start..end {
match self.instr(idx) {
Some(code) => c += &code,
None => return c + "abort(); // not reached\n",
}
}
for copy in &block.postfix {
c += &self.set(copy.dsts[0], &self.get(copy.args[0]));
}
// succ[0] is the jump's target, and succ[1] is the next block
let can_go = |i: usize| block.succ.get(i).filter(|&&s| reachable[s]).copied();
c + &match last.op {
IfFalse | IfFalse8 | IfTrue | IfTrue8 => match (can_go(0), can_go(1)) {
(Some(target), Some(next)) => {
let not = if matches!(last.op, IfTrue | IfTrue8) {
""
} else {
"!"
};
let cond = self.get(last.args[0]);
format!("if ({not}TRUTHY({cond})) goto b{target};\ngoto b{next};\n")
}
(Some(only), None) | (None, Some(only)) => format!("goto b{only};\n"),
(None, None) => "abort(); // not reached\n".into(),
},
Return | ReturnAsync | ReturnUndef => String::new(),
_ => can_go(0).map_or(String::new(), |next| format!("goto b{next};\n")),
}
}
/// The C of instr `idx`. `None` if an operand has no value: then the instr cannot run
fn instr(&self, idx: usize) -> Option<String> {
use OpCode::*;
let instr = &self.f.code[idx];
// The first operand of `define_field_ref` is a location, not a value
let values = match instr.op {
DefineFieldRef => &instr.args[1..],
_ => &instr.args[..],
};
if values.iter().any(|&slot| self.ty(slot) == Type::Unknown) {
return None;
}
let get = |i: usize| self.get(instr.args[i]);
Some(match instr.op {
Nop => String::new(),
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8
| PushI16 | PushI32 | PushTrue | PushFalse | PushAtomValue | PushEmptyString
| PushConst | PushConst8 | Undefined | Null => {
self.set(instr.dsts[0], &self.literal(instr))
}
// Copies. A key stays as it is: a number is an index
SetLoc | SetName | ToPropkey => self.set(instr.dsts[0], &get(0)),
// The bindings at the top of a module, which are C globals (see link.rs)
GetVar => self.set(instr.dsts[0], &format!("g{}", global(instr))),
PutVar => format!("SET(g{}, {});\n", global(instr), get(0)),
PostInc | PostDec => {
let name = if instr.op == PostInc {
"POST_INC"
} else {
"POST_DEC"
};
let (old, new) = (self.get(instr.dsts[0]), self.get(instr.dsts[1]));
format!("{name}({old}, {new}, {});\n", get(0))
}
Return | ReturnAsync => self.ret(&get(0)),
ReturnUndef => self.ret(UNDEFINED),
// A new cell for a shared local (see transform/closure.rs). A local that is not
// shared has no cell, so nothing changes
CloseLoc => match instr.imm {
ImmOperand::Loc(k) if self.shared.contains(&Operand::Local(k as usize)) => {
format!("NEW_CELL({});\n", self.name(Operand::Local(k as usize)))
}
_ => String::new(),
},
Call | CallMethod | CallConstructor => self.call(idx, instr),
// An operator or a test: the runtime's macro of the same name. The operands that
// an instr pops come top first (see regify), so they are reversed
op => match operator(op) {
Some(name) => {
let operands: Vec<String> = (0..instr.args.len()).rev().map(get).collect();
self.set(instr.dsts[0], &format!("{name}({})", operands.join(", ")))
}
None => self.object_instr(idx, instr),
},
})
}
/// `return value`, converted to the C type that the walk returns
fn ret(&self, value: &str) -> String {
let ctype = self.program.ret_ctype(self.walk).c_name();
format!("return CAST({ctype}, {value});\n")
}
/// A literal, as C. Each has a cast, so that its C type is the one that its member has
fn literal(&self, instr: &RegInstr) -> String {
use OpCode::*;
match (instr.op, instr.imm) {
(PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7, _) => {
int(instr.op as i64 - Push0 as i64)
}
(_, ImmOperand::I8(n)) => int(n as i64),
(_, ImmOperand::I16(n)) => int(n as i64),
(_, ImmOperand::I32(n)) => int(n as i64),
(PushTrue, _) => "(bool)1".into(),
(PushFalse, _) => "(bool)0".into(),
(PushAtomValue, _) => self
.program
.string(&instr.imm.atom().text(self.program.atoms)),
(PushEmptyString, _) => self.program.string(""),
(Undefined, _) => UNDEFINED.into(),
(Null, _) => "(null_t){0}".into(),
_ => match &self.f.cpool[instr.imm.const_idx()] {
JsValue::Int(n) => int(*n as i64),
JsValue::Float(x) if x.is_nan() => "((double)NAN)".into(),
JsValue::Float(x) if x.is_infinite() && *x > 0.0 => "((double)INFINITY)".into(),
JsValue::Float(x) if x.is_infinite() => "(-(double)INFINITY)".into(),
JsValue::Float(x) => format!("{x:?}"),
JsValue::String(s) => self.program.string(&s.text()),
JsValue::BoolTrue => "(bool)1".into(),
JsValue::BoolFalse => "(bool)0".into(),
JsValue::Undefined => UNDEFINED.into(),
JsValue::NativeFn(idx) => format!("(native_t){{{idx}}}"),
c => unimplemented!("the constant {c:?}"),
},
}
}
}
/// The global that `get_var` or `put_var` reads or writes (see transform/modules.rs)
fn global(instr: &RegInstr) -> u32 {
match instr.imm {
ImmOperand::Global(global) => global,
imm => unreachable!("a global name that is not a global: {imm:?}"),
}
}
/// An int, as C
fn int(n: i64) -> String {
match n == i32::MIN as i64 {
true => "(int32_t)INT32_MIN".into(),
false => format!("(int32_t){n}"),
}
}
/// The runtime's macro for an operator or a test (see lib/rt/core.h)
fn operator(op: OpCode) -> Option<&'static str> {
use OpCode::*;
Some(match op {
Add => "ADD",
Sub => "SUB",
Mul => "MUL",
Div => "DIV",
Mod => "MOD",
Pow => "POW",
Neg => "NEG",
Plus => "PLUS",
Inc => "INC",
Dec => "DEC",
Not => "NOT",
And => "AND",
Or => "OR",
Xor => "XOR",
Shl => "SHL",
Sar => "SAR",
Shr => "SHR",
Lt => "LT",
Lte => "LTE",
Gt => "GT",
Gte => "GTE",
StrictEq => "STRICT_EQ",
StrictNeq => "STRICT_NEQ",
Eq => "EQ",
Neq => "NEQ",
Lnot => "LNOT",
Typeof => "TYPEOF",
IsUndefined | TypeofIsUndefined => "IS_UNDEFINED",
IsNull => "IS_NULL",
IsUndefinedOrNull => "IS_NULLISH",
TypeofIsFunction => "IS_FUNCTION",
_ => return None,
})
}
+125
View File
@@ -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")
}
+230
View File
@@ -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)
}
+378
View File
@@ -0,0 +1,378 @@
//! The program, as C: its types, its statics and its functions
//!
//! `Program::new` finds all of them before any C is written. Writing a function only reads
//! the program, so the functions can be written in any order.
use std::collections::BTreeSet;
use super::classes::{CType, ClassField, ClassKind, Classes, Member};
use super::functions::Function;
use super::js::indent;
use crate::{
flatten::ENTRY_FN,
infer::{Addr, LENGTH_FIELD, Type, TypeCache, join},
libqjs::parse::{Atom, AtomRef, JsValue, opcodes::OpCode},
link::{Link, PRELUDE, Target},
regify::{Operand, RegFn, RegInstr},
transform::{PROTO_FIELD, PROTOTYPE_FIELD},
};
pub struct Program<'a> {
// The input: the JS functions, and what infer found
pub fns: &'a [RegFn],
pub cache: &'a TypeCache,
pub atoms: &'a [Atom],
pub link: &'a Link,
pub entry: usize, // the walk of the entry: the JS function that runs the program
// The output
pub classes: Classes, // types: a struct for each class of objects
pub strings: Vec<String>, // statics: the text of each string literal
pub globals: Vec<CType>, // statics: the C type of each binding at the top of a module (see link.rs)
pub functions: Vec<Function>,
}
impl<'a> Program<'a> {
pub fn new(fns: &'a [RegFn], cache: &'a TypeCache, atoms: &'a [Atom], link: &'a Link) -> Self {
let mut program = Program {
fns,
cache,
atoms,
link,
entry: cache.find(ENTRY_FN, None, &[], fns),
classes: Classes::new(&cache.memory),
strings: vec![],
globals: vec![],
functions: vec![],
};
program.add_globals();
program.add_functions();
program.add_strings();
program
}
/// One C function for each walk in infer's last round, one for each lookup, and the
/// host's two entry points
fn add_functions(&mut self) {
let walks = (0..self.cache.walks.len()).filter(|&walk| self.cache.is_live(walk));
let mut lookups = BTreeSet::new();
for walk in walks.clone() {
self.add_lookups(walk, &mut lookups);
}
let lookups = lookups
.into_iter()
.map(|(class, field)| Function::Lookup(class, field));
self.functions.extend(walks.map(Function::Js));
self.functions.extend(lookups);
self.functions.extend([Function::Start, Function::Complete]);
}
/// The lookups that a walk needs: one for each field that it gets, on each class that can
/// lack the field
fn add_lookups(&self, walk: usize, lookups: &mut BTreeSet<(usize, AtomRef)>) {
for instr in self.instrs(walk) {
let field = match instr.op {
OpCode::GetField => instr.imm.atom(),
OpCode::GetLength => LENGTH_FIELD,
_ => continue,
};
let ty = self.slot_type(walk, instr.args[0]);
for member in self.classes.members(&ty) {
if let Some(class) = self.field_class(member, field) {
self.add_lookup(class, field, lookups);
}
}
}
}
/// The lookup of `field` on `class`, if its objects can lack the field. If they can, the
/// lookup goes on to their prototypes, so those need lookups too
fn add_lookup(&self, class: usize, field: AtomRef, lookups: &mut BTreeSet<(usize, AtomRef)>) {
let defined = self.classes.field(class, field).is_some_and(|f| f.defined);
if defined || !lookups.insert((class, field)) {
return;
}
for proto_class in self.proto_classes(class) {
self.add_lookup(proto_class, field, lookups);
}
}
/// The text of every string literal in the walks
fn add_strings(&mut self) {
let walks = self.functions.iter().filter_map(|function| match function {
Function::Js(walk) => Some(*walk),
_ => None,
});
let mut strings = vec![];
for walk in walks {
for instr in self.instrs(walk) {
let text = match instr.op {
OpCode::PushAtomValue => instr.imm.atom().text(self.atoms),
OpCode::PushEmptyString => String::new(),
OpCode::PushConst | OpCode::PushConst8 => {
match &self.js_fn(walk).cpool[instr.imm.const_idx()] {
JsValue::String(s) => s.text(),
_ => continue,
}
}
_ => continue,
};
if !strings.contains(&text) {
strings.push(text);
}
}
}
self.strings = strings;
}
/// Each module binding is a C global, so that any function can use the stdlib's
fn add_globals(&mut self) {
let types = (0..self.link.globals).map(|g| self.cache.memory.get(Addr::Global(g)));
self.globals = types.map(|ty| self.classes.ctype(ty)).collect();
}
/// The instrs of a JS function that can run in a walk
fn instrs(&self, walk: usize) -> impl Iterator<Item = &'a RegInstr> + use<'a> {
let (cache, fns) = (self.cache, self.fns);
let walk = &cache.walks[walk];
let f = &fns[walk.context.fn_idx];
let blocks = f.blocks.iter().zip(&walk.reachable);
let blocks = blocks.filter(|(_, reachable)| **reachable);
blocks.flat_map(|(block, _)| &f.code[block.start..block.end])
}
/// The JS function of a walk
pub fn js_fn(&self, walk: usize) -> &'a RegFn {
&self.fns[self.cache.walks[walk].context.fn_idx]
}
}
// ─────────────────────────────────────── Values ────────────────────────────────────────
impl Program<'_> {
/// The type of a local, arg or reg of a JS function, in one walk
pub fn slot_type(&self, walk: usize, slot: Operand) -> Type {
let addr = self.cache.addr(walk, slot, self.fns);
self.cache.memory.get(addr).clone()
}
/// The C type of a local, arg or reg of a JS function, in one walk
pub fn slot_ctype(&self, walk: usize, slot: Operand) -> CType {
self.classes.ctype(&self.slot_type(walk, slot))
}
/// The C type of what a walk returns
pub fn ret_ctype(&self, walk: usize) -> CType {
self.classes.ctype(self.cache.ret(walk))
}
pub fn class_kind(&self, member: Member) -> ClassKind {
match member {
Member::Obj(class) => self.classes.classes[class].kind,
_ => ClassKind::Obj,
}
}
/// A string literal, as C: a pointer to its static
pub fn string(&self, text: &str) -> String {
match self.strings.iter().position(|s| s == text) {
Some(id) => format!("(&str{id})"),
None => panic!("emit: no static for the string {text:?}"),
}
}
/// The value of the export `name` of the stdlib's module `module`, as C, and its C type
pub fn binding(&self, module: &str, name: &str) -> Option<(String, CType)> {
match self.link.export(module, name)? {
Target::Global(g) => Some((format!("g{g}"), self.globals[g])),
Target::Native(_) => None,
}
}
}
// ─────────────────────────────────────── Members ───────────────────────────────────────
impl Program<'_> {
/// The C of `code` for each member that `value`, of type `ty`, can be. `code` gets the
/// value as C of that member's C type. A value with one member is that member, so this
/// is its code. Else the value is a `jsrt_value`, and a `switch` on its tag has a case
/// for each member. A number in a `jsrt_value` is a double, so an int is a float there
pub fn split(&self, value: &str, ty: &Type, code: impl Fn(&str, Member) -> String) -> String {
let members = self.classes.members(ty);
if let [member] = members[..] {
return code(value, member);
}
let float = members.contains(&Member::Float);
let natives = members.iter().filter(|m| matches!(m, Member::Native(_)));
if natives.count() > 1 {
unimplemented!("a value that can be one of several natives");
}
let mut c = format!("switch ({value}.tag) {{\n");
for member in members {
let member = match member {
Member::Int if float => continue,
Member::Int => Member::Float,
member => member,
};
// An object comes out as a `void *`, so it is cast to its struct
let ctype = CType::One(member).c_name();
let as_member = match member {
Member::Obj(_) => format!("(({ctype})CAST({ctype}, {value}))"),
_ => format!("CAST({ctype}, {value})"),
};
let body = indent(&code(&as_member, member));
c += &format!("case {}: {{\n{body} break;\n}}\n", tag(member));
}
c + "default: abort();\n}\n"
}
}
/// The tag of a member in a `jsrt_value` (see lib/rt/core.h)
fn tag(member: Member) -> String {
match member {
Member::Undefined => "JSRT_UNDEFINED".into(),
Member::Null => "JSRT_NULL".into(),
Member::Bool => "JSRT_BOOL".into(),
Member::Int | Member::Float => "JSRT_NUMBER".into(),
Member::Str => "JSRT_STRING".into(),
Member::Native(_) => "JSRT_NATIVE".into(),
Member::Obj(class) => format!("JSRT_OBJECT + {class}"),
}
}
/// A throw of a TypeError that says `what` (see `__rt_type_error`)
pub fn throw(what: &str) -> String {
format!("__rt_type_error(\"{what}\");\n")
}
// ─────────────────────────────────────── Fields ────────────────────────────────────────
impl Program<'_> {
/// The C name of a field
pub fn field_name(&self, field: AtomRef) -> String {
let text: String = (field.text(self.atoms).chars().take(24))
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
match field {
AtomRef::TaggedInt(n) => format!("f_{text}_i{n}"),
AtomRef::Predefined(n) => format!("f_{text}_p{n}"),
AtomRef::FileLocal(n) => format!("f_{text}_a{n}"),
}
}
/// A field of `obj`, as C to get or set it. A field that shares a location holds a
/// pointer to it
pub fn field_access(&self, obj: &str, field: &ClassField) -> String {
let name = self.field_name(field.name);
match field.shared {
true => format!("(*{obj}->{name})"),
false => format!("{obj}->{name}"),
}
}
/// The value of `field` on `obj`, an object of `class`, as C. If every object of the
/// class has the field, that is the field itself. Else it is a call of the field's lookup
pub fn field_value(&self, class: usize, field: AtomRef, obj: &str) -> String {
match self.classes.field(class, field) {
Some(f) if f.defined => self.field_access(obj, f),
_ => format!("{}({obj})", Function::Lookup(class, field).name(self)),
}
}
/// The class of the object that has `field`, for a value of `member`. That is the value
/// itself if it is an object, or the stdlib's prototype of its kind if it is a primitive
/// (see `builtin_proto`). A string's length and an array's are not fields, so they have
/// none
pub fn field_class(&self, member: Member, field: AtomRef) -> Option<usize> {
let length = field == LENGTH_FIELD;
match (member, self.class_kind(member)) {
(Member::Str, _) | (Member::Obj(_), ClassKind::Array) if length => None,
(Member::Obj(class), _) => Some(class),
(Member::Undefined | Member::Null | Member::Native(_), _) => None,
_ => self.builtin_proto(member).map(|(_, class)| class),
}
}
/// The C type that the lookup of `field` on `class` gives: of the join of the field's
/// types on the class's objects, and on their prototypes (see `Memory::read_field`)
pub fn lookup_ctype(&self, class: usize, field: AtomRef) -> CType {
let objs = &self.classes.classes[class].objs;
let memory = &self.cache.memory;
let types = objs
.iter()
.map(|&obj| memory.read_field(&Type::Ref(obj), field));
self.classes
.ctype(&types.fold(Type::Unknown, |all, ty| join(&all, &ty)))
}
/// The prototype of a primitive or an array, as C, and its class: the `prototype` field of
/// the stdlib's String, Number, Boolean or Array (see `Memory::builtin_proto`)
pub fn builtin_proto(&self, member: Member) -> Option<(String, usize)> {
let name = match (member, self.class_kind(member)) {
(Member::Str, _) => "String",
(Member::Int | Member::Float, _) => "Number",
(Member::Bool, _) => "Boolean",
(_, ClassKind::Array) => "Array",
_ => return None,
};
let (stdlib_class, CType::One(Member::Obj(class))) = self.binding(PRELUDE, name)? else {
return None;
};
let proto = self.classes.field(class, PROTOTYPE_FIELD)?;
let CType::One(Member::Obj(proto_class)) = proto.ctype else {
return None;
};
Some((self.field_access(&stdlib_class, proto), proto_class))
}
/// The classes that the prototype of an object of `class` can be. An array's prototype
/// is the stdlib's. Another object's is its `__proto__` field, if it has one
pub fn proto_classes(&self, class: usize) -> Vec<usize> {
if self.classes.classes[class].kind == ClassKind::Array {
let proto = self.builtin_proto(Member::Obj(class));
return proto.map(|(_, class)| class).into_iter().collect();
}
let Some(proto) = self.classes.field(class, PROTO_FIELD) else {
return vec![];
};
let members = self.classes.members(&proto.ty).into_iter();
let classes = members.map(|member| match member {
Member::Obj(class) => Some(class),
_ => None,
});
classes.flatten().collect()
}
}
/// Every local, arg and reg of a JS function
pub fn slots(f: &RegFn) -> Vec<Operand> {
let locals = (0..f.local_count).map(Operand::Local);
let args = (0..f.arg_count).map(Operand::Arg);
let regs = (0..f.reg_count).map(Operand::Reg);
locals.chain(args).chain(regs).collect()
}
+84 -9
View File
@@ -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();
-232
View File
@@ -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 = &reg_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)
}
}
}
+362
View File
@@ -0,0 +1,362 @@
//! Memory: what each location in the program holds, by its address, and the reads and
//! writes of objects' fields and elements
use std::collections::HashMap;
use super::types::{Field, Fields, LENGTH_FIELD, Shape, Type, join, join_all};
use crate::{
libqjs::parse::AtomRef,
regify::Operand,
transform::{PROTO_FIELD, PROTOTYPE_FIELD},
};
/// A point in the program: one instr of one fn
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct InstrRef {
pub fn_idx: usize,
pub instr_idx: usize,
}
/// The call instr that a function is called from. `None` for a call from the host
pub type CallSite = Option<InstrRef>;
/// A function, in one call site. Without polymorphism, infer would walk each context once.
/// Objects, and the bindings that closures share, are named by their context, so that every
/// walk of a context sees the same ones (see `TypeCache`)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Context {
pub fn_idx: usize,
pub call_site: CallSite,
}
/// The objects that one instr makes, in one context
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ObjAddr {
pub context: Context,
pub instr: usize,
}
/// A location in the program, which memory holds a type for
///
/// Every location is named by its context and a place in its function, except the ones that
/// only one walk can see, which are named by the walk, and the bindings at the top of a
/// module, of which there is one each for the whole program. A type can hold an object's address,
/// and a walk's arg types can hold that type, so objects are never named by a walk: that
/// could make new walks without end. Every part of an address comes from the program text or
/// from a walk, so there are finitely many, and infer ends
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Addr {
Object(ObjAddr), // an object
Field(ObjAddr, AtomRef), // a field of an object, unless the field shares another location
Shared { context: Context, slot: Operand }, // a binding that closures share (see transform/closure.rs). The closures are objects, so it is named like them
Private { walk: usize, slot: Operand }, // a binding that only its walk sees
Return(usize), // what a walk returns
Global(usize), // a binding at the top of a module: one for the whole program (see link.rs)
}
/// What each location holds, by its address. A location that nothing has reached holds
/// `Unknown`, and has no entry. Types only grow, so the count of changes tells whether
/// anything changed
#[derive(Debug, Default)]
pub struct Memory {
types: HashMap<Addr, Type>,
merged: HashMap<Addr, Addr>, // a location that is one with another (see `share_field`): the other
changes: usize,
pub builtins: Builtins,
}
/// The stdlib's classes, whose prototypes the primitives and arrays read: the address of each
/// one's binding, which the prelude exports (see link.rs)
#[derive(Debug, Default)]
pub struct Builtins {
pub string: Option<Addr>,
pub number: Option<Addr>,
pub boolean: Option<Addr>,
pub array: Option<Addr>,
}
const UNKNOWN: &Type = &Type::Unknown;
impl Memory {
pub fn get(&self, addr: Addr) -> &Type {
self.types.get(&self.find(addr)).unwrap_or(UNKNOWN)
}
/// The address that stands for `addr`: itself, unless it was merged into another
fn find(&self, mut addr: Addr) -> Addr {
while let Some(&other) = self.merged.get(&addr) {
addr = other;
}
addr
}
/// Make `b` one location with `a`: each holds what either held
fn merge(&mut self, a: Addr, b: Addr) {
let (a, b) = (self.find(a), self.find(b));
if a != b {
let ty = self.types.remove(&b).unwrap_or(Type::Unknown);
self.merged.insert(b, a);
self.join(a, &ty);
self.changes += 1;
}
}
/// Every object, with its entry
pub fn objects(&self) -> impl Iterator<Item = (ObjAddr, &Type)> {
self.types.iter().filter_map(|(addr, ty)| match addr {
Addr::Object(obj) => Some((*obj, ty)),
_ => None,
})
}
/// How many times memory changed
pub fn changes(&self) -> usize {
self.changes
}
/// Join `ty` into the location
pub fn join(&mut self, addr: Addr, ty: &Type) {
let addr = self.find(addr);
let old = self.types.entry(addr).or_insert(Type::Unknown);
let new = join(old, ty);
if new != *old {
*old = new;
self.changes += 1;
}
}
// Objects
/// Make the entry of an object, the first time
pub fn make(&mut self, obj: ObjAddr, entry: Type) {
if *self.get(Addr::Object(obj)) == Type::Unknown {
self.types.insert(Addr::Object(obj), entry);
self.changes += 1;
}
}
/// The fn that the object runs, if it is a closure
pub fn closure_fn(&self, obj: ObjAddr) -> Option<usize> {
match self.get(Addr::Object(obj)) {
Type::Closure { fn_idx, .. } => Some(*fn_idx),
_ => None,
}
}
pub fn is_array(&self, obj: ObjAddr) -> bool {
matches!(self.get(Addr::Object(obj)), Type::Shape(Shape::Array(_)))
}
/// Whether a value of type `ty` is a fn
pub fn is_function(&self, ty: &Type) -> Option<bool> {
match ty {
Type::Ref(obj) => Some(self.closure_fn(*obj).is_some()),
Type::Native(_) => Some(true),
_ => Some(false),
}
}
/// What `typeof` gives for a value of type `ty`
pub fn typeof_name(&self, ty: &Type) -> Option<AtomRef> {
Some(AtomRef::Predefined(match ty {
Type::Undefined => 73, // "undefined"
Type::Null => 77, // "object"
Type::Bool(_) => 75, // "boolean"
Type::Int | Type::Float => 74, // "number"
Type::Str(_) => 76, // "string"
Type::Ref(obj) if self.closure_fn(*obj).is_some() => 27, // "function"
Type::Native(_) => 27, // "function"
Type::Ref(_) => 77, // "object"
_ => return None,
}))
}
// Fields
pub fn fields(&self, obj: ObjAddr) -> &Fields {
match self.get(Addr::Object(obj)) {
Type::Shape(Shape::Obj(fields)) | Type::Closure { fields, .. } => fields,
ty => unimplemented!("named fields on {ty:?}"),
}
}
pub fn field(&self, obj: ObjAddr, name: AtomRef) -> Option<Field> {
let fields = self.fields(obj);
let i = fields.binary_search_by_key(&name, |(n, _)| *n).ok()?;
Some(fields[i].1)
}
/// Give the object `field`, unless it has one. Gives its field
fn add_field(&mut self, obj: ObjAddr, name: AtomRef, field: Field) -> Field {
let Some(Type::Shape(Shape::Obj(fields)) | Type::Closure { fields, .. }) =
self.types.get_mut(&Addr::Object(obj))
else {
unreachable!()
};
match fields.binary_search_by_key(&name, |(n, _)| *n) {
Ok(i) => fields[i].1,
Err(i) => {
fields.insert(i, (name, field));
self.changes += 1;
field
}
}
}
/// Join `ty` into its field `name`. A field is defined if a `define_field` makes it: that
/// is a write into an object before it goes anywhere (see regify). A field that another
/// write makes can be absent. An array's `length` is not a field: a write of it sets the
/// array's length, and a longer array has holes at its end, which hold undefined
pub fn write_field(&mut self, obj: ObjAddr, name: AtomRef, ty: &Type, defined: bool) {
if name == LENGTH_FIELD && self.is_array(obj) {
self.write_elem(obj, &Type::Undefined);
return;
}
let field = self.add_field(
obj,
name,
Field {
addr: Addr::Field(obj, name),
defined,
},
);
self.join(field.addr, ty);
}
/// Make its field `name` the location `addr` (see `define_field_ref`). Such a field is
/// always made when the object is made, so it is defined
///
/// An address stands for all the objects that one instr makes in one context. Two walks
/// of that context can share two locations into the field: walks with different
/// closures as their env, for example. A field is one location, so those
/// two are one location too: they are merged, as the types of all its objects' fields are
/// joined. Their C types are then one type as well
pub fn share_field(&mut self, obj: ObjAddr, name: AtomRef, addr: Addr) {
let field = Field {
addr,
defined: true,
};
let field = self.add_field(obj, name, field);
self.merge(field.addr, addr);
}
/// A read of field `name` on a value of type `ty`:
/// - an object reads its own field, then, where that can be absent, the field on its
/// prototype. At the end of the chain, an absent field is undefined
/// - a primitive reads its kind's prototype
/// - undefined and null have no fields: the read throws, so it gives nothing
/// - a sum reads on each of its members
pub fn read_field(&self, ty: &Type, name: AtomRef) -> Type {
join_all(ty.members().iter().map(|member| match member {
Type::Ref(obj) => self.read_chain(*obj, name, &mut vec![]),
Type::Undefined | Type::Null => Type::Unknown,
primitive => self.read_field(&self.builtin_proto(primitive), name),
}))
}
/// The read of field `name` on an object, on a chain that came through the objects
/// `chain`. Objects of one address can be each other's prototypes, so a chain of
/// addresses can come back to an address, which a chain of objects cannot. The read on
/// that address is already in the chain, so it gives nothing more
///
/// An array's prototype is the one that the stdlib links for arrays, and its `length` is
/// not a field
fn read_chain(&self, obj: ObjAddr, name: AtomRef, chain: &mut Vec<ObjAddr>) -> Type {
let array = self.is_array(obj);
if array && name == LENGTH_FIELD {
return Type::Int;
}
let own = if array { None } else { self.field(obj, name) };
let own_ty = own.map_or(Type::Unknown, |f| self.get(f.addr).clone());
if own.is_some_and(|f| f.defined) || chain.contains(&obj) {
return own_ty;
}
let proto_field = if array {
None
} else {
self.field(obj, PROTO_FIELD)
};
let proto = match proto_field {
Some(f) => self.get(f.addr).clone(),
None if array => self.builtin_proto(&Type::Ref(obj)),
None => Type::Unknown,
};
// The chain ends at a prototype that can be absent or is not an object
let ends = (!array && !proto_field.is_some_and(|f| f.defined)) || proto == Type::Unknown;
chain.push(obj);
let from_proto = join_all(proto.members().iter().map(|member| match member {
Type::Ref(proto) => self.read_chain(*proto, name, chain),
_ => Type::Undefined,
}));
chain.pop();
let end = if ends { Type::Undefined } else { Type::Unknown };
join_all([own_ty, from_proto, end])
}
// Elements
pub fn elems(&self, obj: ObjAddr) -> &Type {
match self.get(Addr::Object(obj)) {
Type::Shape(Shape::Array(elems)) => elems,
_ => unreachable!(),
}
}
/// Join `ty` into the type of its elements
pub fn write_elem(&mut self, obj: ObjAddr, ty: &Type) {
let new = join(self.elems(obj), ty);
if new != *self.elems(obj) {
self.types
.insert(Addr::Object(obj), Type::Shape(Shape::Array(Box::new(new))));
self.changes += 1;
}
}
/// A read of an element of one of its arrays. An index can be a hole, or past the end,
/// which gives undefined
pub fn read_elem(&self, obj: ObjAddr) -> Type {
join(self.elems(obj), &Type::Undefined)
}
}
impl Memory {
/// The prototype of a primitive or an array: the `prototype` of the stdlib's `String`,
/// `Number`, `Boolean` or `Array`. The stdlib sets each one before any other code runs,
/// so a read takes the field's own type, and never its absence
pub fn builtin_proto(&self, ty: &Type) -> Type {
let class = match ty {
Type::Str(_) => self.builtins.string,
Type::Int | Type::Float => self.builtins.number,
Type::Bool(_) => self.builtins.boolean,
Type::Ref(obj) if self.is_array(*obj) => self.builtins.array,
_ => None,
};
let Some(class) = class else {
return Type::Unknown;
};
let protos = self.get(class).members().iter().map(|class| match class {
Type::Ref(class) => match self.field(*class, PROTOTYPE_FIELD) {
Some(field) => self.get(field.addr).clone(),
None => Type::Unknown,
},
_ => Type::Unknown,
});
join_all(protos)
}
}
+208
View File
@@ -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;
}
}
}
+234
View File
@@ -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,
}
}
+471
View File
@@ -0,0 +1,471 @@
//! One walk: the types that one context and its arg types give the locals, args and regs of
//! a function
//!
//! The walk goes over the reachable blocks until nothing changes. Each instr has a rule,
//! which gives the type of its result from the types of its operands. The result joins into
//! the instr's dsts in memory. A rule with an `Unknown` operand gives nothing: no value has
//! reached the instr yet
use super::{
TypeCache, binding_addr,
memory::{Addr, Context, InstrRef, ObjAddr},
types::{
BaseType, EMPTY_STRING, LENGTH_FIELD, Shape, Type, decide_pair, is_base, join, join_all,
loose_eq, pairs, strict_eq, truthiness,
},
};
use crate::{
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
native,
regify::{BasicBlock, Operand, RegFn, RegInstr},
transform::{FIRST_ARG, THIS_ARG},
};
/// Walk `w`. Gives the blocks that can run
pub fn walk_fn(w: usize, fns: &[RegFn], cache: &mut TypeCache) -> Vec<bool> {
let context = cache.walks[w].context;
let f = &fns[context.fn_idx];
// The args get their types from the walk's key
for (i, ty) in cache.walks[w].arg_types.clone().iter().enumerate() {
let addr = binding_addr(w, context, &f.captured_slots(), Operand::Arg(i));
cache.memory.join(addr, ty);
}
let walker = Walker {
w,
context,
f,
fns,
shared: f.captured_slots(),
cache,
reachable: vec![false; f.blocks.len()],
};
walker.run()
}
struct Walker<'a> {
w: usize,
context: Context,
f: &'a RegFn,
fns: &'a [RegFn],
shared: Vec<Operand>, // the slots of `f` that closures share
cache: &'a mut TypeCache,
reachable: Vec<bool>,
}
impl Walker<'_> {
fn addr(&self, slot: Operand) -> Addr {
binding_addr(self.w, self.context, &self.shared, slot)
}
/// The address of the objects that the instr at `idx` makes, in this context
fn here(&self, idx: usize) -> ObjAddr {
ObjAddr {
context: self.context,
instr: idx,
}
}
fn run(mut self) -> Vec<bool> {
self.reachable[0] = true;
// A return only changes once its block is reachable, and a read only gives more
// once memory changes, so the blocks and the count of changes are enough to watch
loop {
let before = (self.cache.progress(), self.reachable.clone());
let f = self.f;
for (i, block) in f.blocks.iter().enumerate() {
if self.reachable[i] {
self.walk_block(block);
}
}
if (self.cache.progress(), &self.reachable) == (before.0, &before.1) {
return self.reachable;
}
}
}
fn walk_block(&mut self, block: &BasicBlock) {
let f = self.f;
let code = f.code[block.start..block.end].iter().chain(&block.postfix);
// A postfix copy has no index in the code, but its rule never needs one
for (offset, instr) in code.enumerate() {
self.step(instr, block.start + offset);
}
self.mark_successors(block);
}
fn step(&mut self, instr: &RegInstr, idx: usize) {
use OpCode::*;
// Jumps are the block's to walk (see `mark_successors()`). A `close_loc` or a
// `close_field` gives a binding a new location, which has the same address (see
// transform/closure.rs)
if matches!(
instr.op,
Nop | CloseLoc
| CloseField
| Goto
| Goto8
| Goto16
| IfFalse
| IfFalse8
| IfTrue
| IfTrue8
) {
return;
}
let args: Vec<Type> = instr
.args
.iter()
.map(|&slot| self.cache.memory.get(self.addr(slot)).clone())
.collect();
// The first operand of `define_field_ref` is a location, not a value, and a binding
// has no value before its first write
let values = if instr.op == DefineFieldRef {
&args[1..]
} else {
&args[..]
};
if values.contains(&Type::Unknown) {
return;
}
let ty = self.rule(instr, idx, &args);
if matches!(instr.op, Return | ReturnAsync | ReturnUndef) {
self.cache.memory.join(Addr::Return(self.w), &ty);
}
for &dst in &instr.dsts {
self.cache.memory.join(self.addr(dst), &ty);
}
}
/// The type that `instr` gives, from the types of its operands `args`. An instr that
/// gives nothing gives `Unknown`. Operands that an instr pops come top first (see
/// regify), so a binary operator has its right side in `args[0]`
fn rule(&mut self, instr: &RegInstr, idx: usize, args: &[Type]) -> Type {
use OpCode::*;
let memory = &self.cache.memory;
match instr.op {
// Literals
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8
| PushI16 | PushI32 => Type::Int,
PushTrue => Type::Bool(Some(true)),
PushFalse => Type::Bool(Some(false)),
PushAtomValue => Type::Str(Some(instr.imm.atom())),
PushEmptyString => Type::Str(Some(EMPTY_STRING)),
PushConst | PushConst8 => self.constant(instr),
Undefined => Type::Undefined,
Null => Type::Null,
// Arithmetic. Every number is a double, and an int is a double that is known to
// fit in an int32 (see emit). So an operator that can leave that range, or give -0
// or NaN, gives a float. `+` concatenates if either side becomes a string, which a
// string or an object does
Add => join_all(pairs(&args[1], &args[0]).map(|(l, r)| {
let text = |t: &Type| matches!(t, Type::Str(_) | Type::Ref(_) | Type::Native(_));
match text(l) || text(r) {
true => Type::Str(None),
false => Type::Float,
}
})),
Sub | Mul | Div | Mod | Pow | Neg | Inc | Dec | PostInc | PostDec => Type::Float,
// `+x` is x as a number: an int stays an int
Plus => join_all(args[0].members().iter().map(|t| match t {
Type::Int => Type::Int,
_ => Type::Float,
})),
// The bitwise operators give an int32, but `>>>` gives a uint32
Not | And | Or | Xor | Shl | Sar => Type::Int,
Shr => Type::Float,
Lt | Lte | Gt | Gte => Type::Bool(None),
// Tests. A known result lets the walk skip a branch that cannot run
StrictEq | StrictNeq | Eq | Neq => {
let eq = match instr.op {
StrictEq | StrictNeq => decide_pair(&args[0], &args[1], strict_eq),
_ => decide_pair(&args[0], &args[1], loose_eq),
};
Type::Bool(eq.map(|eq| eq == matches!(instr.op, StrictEq | Eq)))
}
Lnot => Type::Bool(args[0].decide(truthiness).map(|t| !t)),
Typeof => Type::Str(args[0].decide(|t| memory.typeof_name(t))),
IsUndefined | TypeofIsUndefined => {
Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Undefined])))
}
IsNull => Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Null]))),
IsUndefinedOrNull => {
Type::Bool(args[0].decide(|t| is_base(t, &[BaseType::Undefined, BaseType::Null])))
}
TypeofIsFunction => Type::Bool(args[0].decide(|t| memory.is_function(t))),
// Moves. A key stays as it is: a number is an index
SetLoc | SetName | ToPropkey => args[0].clone(),
// The bindings at the top of a module, which are globals (see link.rs)
GetVar => memory.get(Addr::Global(global(instr))).clone(),
PutVar => {
self.cache
.memory
.join(Addr::Global(global(instr)), &args[0]);
Type::Unknown
}
// Objects
Object => self.make(idx, Type::Shape(Shape::Obj(vec![]))),
Fclosure => {
let JsValue::FnByIdx(fn_idx) = self.f.cpool[instr.imm.const_idx()] else {
unreachable!()
};
self.make(
idx,
Type::Closure {
fn_idx,
fields: vec![],
},
)
}
ArrayFrom => {
let array = self.make(idx, Type::Shape(Shape::Array(Box::new(Type::Unknown))));
for value in args {
self.cache.memory.write_elem(self.here(idx), value);
}
array
}
GetArrayEl => self.get_array_el(&args[1], &args[0]),
PutArrayEl => self.put_array_el(&args[2], &args[1], &args[0]),
DefineField | PutField => self.write_field(instr, &args[1], &args[0]),
DefineFieldRef => self.share_field(instr, args),
GetField => memory.read_field(&args[0], instr.imm.atom()),
// A string's length is its own. Anything else reads a `length` field
GetLength => join_all(args[0].members().iter().map(|t| match t {
Type::Str(_) => Type::Int,
t => memory.read_field(t, LENGTH_FIELD),
})),
// Calls
Call | CallMethod | CallConstructor => self.call(instr, idx, args),
// Returns
Return | ReturnAsync => args[0].clone(),
ReturnUndef => Type::Undefined,
_ => unreachable!("{instr:?}"),
}
}
/// The object that the instr at `idx` makes in this walk, made with `entry` the first time
fn make(&mut self, idx: usize, entry: Type) -> Type {
self.cache.memory.make(self.here(idx), entry);
Type::Ref(self.here(idx))
}
/// A constant from the fn's constant pool
fn constant(&self, instr: &RegInstr) -> Type {
match &self.f.cpool[instr.imm.const_idx()] {
JsValue::Int(_) => Type::Int,
JsValue::Float(_) => Type::Float,
JsValue::String(_) => Type::Str(None),
JsValue::BoolTrue => Type::Bool(Some(true)),
JsValue::BoolFalse => Type::Bool(Some(false)),
JsValue::Undefined => Type::Undefined,
JsValue::NativeFn(idx) => Type::Native(*idx),
c => unimplemented!("the constant {c:?}"),
}
}
/// `a[i]` on an array, or `s[i]` on a string: an index is a number. Other objects need
/// computed field names, which are not supported. Undefined and null throw
fn get_array_el(&self, obj: &Type, index: &Type) -> Type {
join_all(obj.members().iter().map(|obj| match obj {
Type::Ref(obj) if self.cache.memory.is_array(*obj) => {
check_index(index);
self.cache.memory.read_elem(*obj)
}
Type::Str(_) => {
check_index(index);
join(&Type::Str(None), &Type::Undefined)
}
Type::Undefined | Type::Null => Type::Unknown,
obj => unimplemented!("`o[k]` on {obj:?}: computed field names"),
}))
}
/// `a[i] = v` on an array. A string cannot be written (module code is strict), and
/// undefined and null throw
fn put_array_el(&mut self, obj: &Type, index: &Type, value: &Type) -> Type {
for obj in obj.members() {
match obj {
// A write past the end leaves holes before it, which hold undefined
Type::Ref(obj) if self.cache.memory.is_array(*obj) => {
check_index(index);
self.cache.memory.write_elem(*obj, value);
self.cache.memory.write_elem(*obj, &Type::Undefined);
}
Type::Undefined | Type::Null | Type::Str(_) => {}
obj => unimplemented!("`o[k] = v` on {obj:?}: computed field names"),
}
}
Type::Unknown
}
/// `define_field` and `put_field`. The write goes into the object's field, which every
/// holder of the object sees. A primitive has no fields to write, and undefined and null
/// throw
fn write_field(&mut self, instr: &RegInstr, obj: &Type, value: &Type) -> Type {
for obj in obj.members() {
if let Type::Ref(obj) = obj {
let defined = instr.op == OpCode::DefineField;
self.cache
.memory
.write_field(*obj, instr.imm.atom(), value, defined);
}
}
Type::Unknown
}
/// `define_field_ref`: the field becomes a location, which regify gives as the first
/// operand. That is a binding, or the reg of an object whose field of the same name is
/// the location (see regify/walk.rs)
fn share_field(&mut self, instr: &RegInstr, args: &[Type]) -> Type {
let name = instr.imm.atom();
let location = match instr.args[0] {
Operand::Reg(_) => {
let holders = args[0].members().iter().map(|holder| match holder {
Type::Ref(holder) => self.cache.memory.field(*holder, name).map(|f| f.addr),
holder => unreachable!("a capture held by {holder:?}"),
});
let mut locations: Vec<Addr> = holders.flatten().collect();
locations.dedup();
assert!(
locations.len() <= 1,
"a capture of one of several locations"
);
locations.pop()
}
slot => Some(self.addr(slot)),
};
// A holder's field can be missing until the walk that makes it gets there
if let Some(location) = location {
for obj in args[1].members() {
if let Type::Ref(obj) = obj {
self.cache.memory.share_field(*obj, name, location);
}
}
}
Type::Unknown
}
/// Operands are the callee's params: the callee (its env), `this`, then the args (see
/// regify). A call of each member of a sum passes that member as the env. A callee that
/// is not a fn throws, so it gives nothing
///
/// `new` gives what the constructor returns if that is an object, else the new object
/// itself, `this`
fn call(&mut self, instr: &RegInstr, idx: usize, args: &[Type]) -> Type {
let call_site = Some(InstrRef {
fn_idx: self.context.fn_idx,
instr_idx: idx,
});
let rets: Vec<Type> = (args[0].members().iter())
.map(|callee| {
// A native takes the args only, without its env and `this`
if let Type::Native(idx) = callee {
return native::ret_type(*idx, &args[FIRST_ARG as usize..]);
}
let Type::Ref(obj) = callee else {
return Type::Unknown;
};
let Some(fn_idx) = self.cache.memory.closure_fn(*obj) else {
return Type::Unknown;
};
let mut call_args = args.to_vec();
call_args[0] = callee.clone();
self.cache.call(fn_idx, call_site, &call_args, self.fns)
})
.collect();
let ret = join_all(rets);
if instr.op != OpCode::CallConstructor {
return ret;
}
join_all(ret.members().iter().map(|ret| match ret {
Type::Ref(_) => ret.clone(),
_ => args[THIS_ARG as usize].clone(),
}))
}
/// Mark the blocks that `block` can go to. A conditional jump whose condition the types
/// decide goes one way only. One whose condition nothing has reached yet goes nowhere yet
fn mark_successors(&mut self, block: &BasicBlock) {
use OpCode::*;
let last = &self.f.code[block.end - 1];
let jumps = match last.op {
IfFalse | IfFalse8 | IfTrue | IfTrue8 => {
match self.cache.memory.get(self.addr(last.args[0])) {
Type::Unknown => return,
cond => cond
.decide(truthiness)
.map(|t| t == matches!(last.op, IfTrue | IfTrue8)),
}
}
_ => None,
};
// succ[0] is the jump target, succ[1] the fall-through
for (i, &succ) in block.succ.iter().enumerate() {
if jumps.is_none_or(|jumps| jumps == (i == 0)) {
self.reachable[succ] = true;
}
}
}
}
/// An array index must be a number. Any other key is a computed field name, which is not
/// supported
fn check_index(index: &Type) {
for member in index.members() {
if !matches!(member, Type::Int | Type::Float) {
unimplemented!("`a[k]` with a key of type {member:?}: computed field names");
}
}
}
/// The global that `get_var` or `put_var` reads or writes (see transform/modules.rs)
fn global(instr: &RegInstr) -> usize {
match instr.imm {
ImmOperand::Global(global) => global as usize,
imm => unreachable!("a global name that is not a global: {imm:?}"),
}
}
+44 -1
View File
@@ -1,6 +1,7 @@
use std::ffi::{c_int, c_void};
use std::ffi::{c_char, c_int, c_void};
pub const JS_TAG_EXCEPTION: i64 = 6;
pub const JS_TAG_MODULE: i64 = -3;
pub const JS_EVAL_TYPE_MODULE: c_int = 1 << 0;
pub const JS_EVAL_FLAG_COMPILE_ONLY: c_int = 1 << 5;
pub const JS_WRITE_OBJ_BYTECODE: c_int = 1 << 0;
@@ -12,6 +13,13 @@ pub struct JSValue {
pub tag: i64,
}
impl JSValue {
/// The pointer of a value that has one, such as a module
pub fn ptr(self) -> *mut c_void {
unsafe { self.u.ptr }
}
}
#[repr(C)]
#[derive(Clone, Copy)]
union JSValueUnion {
@@ -22,6 +30,15 @@ union JSValueUnion {
pub type JSRuntime = c_void;
pub type JSContext = c_void;
pub type JSModuleDef = c_void;
pub type JSModuleLoaderFunc = unsafe extern "C" fn(
ctx: *mut JSContext,
module_name: *const c_char,
opaque: *mut c_void,
) -> *mut JSModuleDef;
pub type JSModuleInitFunc = unsafe extern "C" fn(ctx: *mut JSContext, m: *mut JSModuleDef) -> c_int;
unsafe extern "C" {
pub fn JS_NewRuntime() -> *mut JSRuntime;
@@ -48,6 +65,32 @@ unsafe extern "C" {
pub fn JS_GetException(ctx: *mut JSContext) -> JSValue;
pub fn JS_ThrowReferenceError(ctx: *mut JSContext, fmt: *const c_char, ...) -> JSValue;
pub fn JS_SetModuleLoaderFunc(
rt: *mut JSRuntime,
module_normalize: *const c_void,
module_loader: JSModuleLoaderFunc,
opaque: *mut c_void,
);
pub fn JS_NewCModule(
ctx: *mut JSContext,
name: *const c_char,
func: JSModuleInitFunc,
) -> *mut JSModuleDef;
pub fn JS_AddModuleExport(
ctx: *mut JSContext,
m: *mut JSModuleDef,
name: *const c_char,
) -> c_int;
pub fn JS_NewArray(ctx: *mut JSContext) -> JSValue;
pub fn JS_SetPropertyUint32(ctx: *mut JSContext, obj: JSValue, idx: u32, val: JSValue)
-> c_int;
pub fn JS_ToCStringLen2(
ctx: *mut JSContext,
plen: *mut usize,
+126 -22
View File
@@ -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
View File
@@ -1,3 +1,7 @@
// The parser reads every part of the bytecode format, including parts that the compiler does not
// use yet
#![allow(dead_code)]
use std::fmt::Debug;
use std::fmt::Write;
@@ -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 }))
}
_ => {
let (bin, request) = leb128(bin)?;
let (bin, local) = AtomRef::parse(bin)?;
let (bin, name) = AtomRef::parse(bin)?;
Ok((
bin,
Self::Indirect {
request,
local,
name,
},
))
}
}
1 => (bin, Self::Alias),
_ => unreachable!(),
};
Ok(ret)
}
}
#[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('"')?;
+452
View File
@@ -0,0 +1,452 @@
//! Link: what each name in each module refers to, and the program's entry fn
//!
//! Every binding at the top of a module (`let`, `const`, `function`, `class`, ...) is a
//! global: there is one of each for the whole program. A module's body reaches its own
//! bindings and its imports as captured vars, which QuickJS calls module decls and module
//! imports. Link gives each decl a global, and follows each import to the global of the
//! binding that it names (see `Link::resolve`). transform/modules.rs then rewrites the code
//!
//! A name that no module declares, such as `String` or `console`, is a global name: it
//! refers to the export of that name of the prelude, `jsrt:prelude`, as names in JS refer
//! to the global object. A module can declare a binding of the same name, which hides it
//!
//! The natives are the exports of `jsrt:native`, which QuickJS sees as a C module (see
//! `libqjs::compile`)
//!
//! # The entry fn
//!
//! ```js
//! function <entry>() {
//! // Phase 1 (`this` is true): each body only makes its top-level function declarations.
//! // Phase 2: each body runs. Every module's phase 1 comes first, so that modules that
//! // import each other see each other's functions
//! let phase = true;
//! while (true) {
//! for (const body of bodies) body.call(phase);
//! if (!phase) break;
//! phase = false;
//! }
//! __drain_microtasks();
//! }
//! ```
//!
//! Both phases call each body from one call instr. Infer names the objects that a walk makes
//! by its call site, so with two call instrs, each phase would make its own copy of every
//! object at the top of a module
//!
//! The bodies run in ES's order: each module after the modules that it imports, from the
//! prelude, then each module that the command line names. A body that awaits at its top runs
//! until its first await, so only the last body may await (see transform/async.rs)
use std::collections::HashMap;
use crate::{
flatten::{ENTRY_FN, FlatFn, FlatModule},
libqjs::parse::{
Atom, AtomRef, ClosureKind, ClosureVar, ExportInfo, FuncKind, ImmOperand as Imm, Instr,
JsValue, opcodes::OpCode,
},
native::NATIVES,
};
/// The module whose exports are every module's global names
pub const PRELUDE: &str = "jsrt:prelude";
/// The module of the natives
pub const NATIVE: &str = "jsrt:native";
/// What a name refers to
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Target {
Global(usize), // a binding at the top of a module
Native(usize), // a native, by its index in `native::NATIVES`
}
pub struct Link {
pub globals: usize, // how many there are
bodies: HashMap<usize, (String, Vec<Target>)>, // for the fn of each module's body: the module's name, and what each of the body's captured vars refers to
exports: HashMap<String, HashMap<String, Target>>, // for each module, by name: each export, by name
}
impl Link {
/// The name of the module whose body is `f`, if it is a body, and what each captured var
/// of the body refers to
pub fn body(&self, f: usize) -> Option<(&str, &[Target])> {
self.bodies
.get(&f)
.map(|(name, vars)| (&name[..], &vars[..]))
}
/// What the global name `name` refers to: the prelude's export of that name
pub fn global(&self, name: &str) -> Option<Target> {
self.export(PRELUDE, name)
}
/// The export `name` of the module `module`
pub fn export(&self, module: &str, name: &str) -> Option<Target> {
self.exports.get(module)?.get(name).copied()
}
/// The global of an export that the compiler needs from the stdlib
pub fn stdlib(&self, module: &str, name: &str) -> usize {
match self.export(module, name) {
Some(Target::Global(global)) => global,
_ => panic!("the compiler needs `{name}` from `{module}`"),
}
}
}
/// Link the modules, and write the entry fn. `roots` are the modules that the command line
/// names, in order
pub fn link(
modules: &[FlatModule],
roots: &[String],
fns: &mut [FlatFn],
captures: &[Vec<ClosureVar>],
atoms: &[Atom],
) -> Link {
let linker = Linker::new(modules, captures, atoms);
let link = Link {
globals: linker.globals,
bodies: linker.bodies(),
exports: linker.all_exports(),
};
let order = linker.order(roots);
check_top_level_await(&order, modules);
let drain = link.stdlib("jsrt:loop", "__drain_microtasks");
fns[ENTRY_FN] = entry(&order, modules, drain);
link
}
/// A body that awaits returns at its first await, so the bodies after it would run too early.
/// Only the last one may await, until the entry waits for each body's promise
fn check_top_level_await(order: &[usize], modules: &[FlatModule]) {
for &m in &order[..order.len() - 1] {
if modules[m].has_top_level_await {
let name = &modules[m].name;
unimplemented!("{name}: top-level `await` in a module that is not the last to run");
}
}
}
/// The natives, as the exports of `jsrt:native`
fn natives() -> HashMap<String, Target> {
let natives = NATIVES.iter().enumerate();
natives
.map(|(idx, native)| (native.name.to_string(), Target::Native(idx)))
.collect()
}
struct Linker<'a> {
modules: &'a [FlatModule],
captures: &'a [Vec<ClosureVar>],
atoms: &'a [Atom],
index: HashMap<String, usize>, // each module, by name
decls: Vec<HashMap<usize, usize>>, // for each module: the global of each of its body's decls, by captured var
globals: usize,
}
impl<'a> Linker<'a> {
/// The linker, with a global for each decl of each module's body
fn new(modules: &'a [FlatModule], captures: &'a [Vec<ClosureVar>], atoms: &'a [Atom]) -> Self {
let index = modules.iter().enumerate();
let mut linker = Linker {
modules,
captures,
atoms,
index: index.map(|(m, module)| (module.name.clone(), m)).collect(),
decls: vec![],
globals: 0,
};
linker.add_globals();
linker
}
/// For each module's body: the module's name, and what each captured var refers to
fn bodies(&self) -> HashMap<usize, (String, Vec<Target>)> {
let bodies = self.modules.iter().enumerate();
bodies
.map(|(m, module)| (module.body, (module.name.clone(), self.body_vars(m))))
.collect()
}
/// Every module's exports, by the module's name, with the natives' module
fn all_exports(&self) -> HashMap<String, HashMap<String, Target>> {
let modules = self.modules.iter().enumerate();
let exports =
modules.map(|(m, module)| (module.name.clone(), self.exports(m, &mut vec![])));
exports.chain([(NATIVE.to_string(), natives())]).collect()
}
}
impl Linker<'_> {
/// A global for each decl of each module's body. QuickJS makes a namespace import a decl
/// too, whose object its own linker makes at run time, so that is rejected here
fn add_globals(&mut self) {
for module in self.modules {
let atoms = self.atoms;
let namespace = module
.imports
.iter()
.find(|import| import.name.text(atoms) == "*");
if namespace.is_some() {
unimplemented!("{}: `import * as` needs namespace objects", module.name);
}
let vars = self.captures[module.body].iter().enumerate();
let decls = vars.filter(|(_, var)| var.flags.closure_kind == ClosureKind::ModuleDecl);
let mut globals = HashMap::new();
for (i, _) in decls {
globals.insert(i, self.globals);
self.globals += 1;
}
self.decls.push(globals);
}
}
/// What each captured var of module `m`'s body refers to
fn body_vars(&self, m: usize) -> Vec<Target> {
(0..self.captures[self.modules[m].body].len())
.map(|i| self.var(m, i, &mut vec![]))
.collect()
}
/// What captured var `i` of module `m`'s body refers to: its global if it is a decl, else
/// the export that it imports. `seen` guards against imports that go round in a circle
fn var(&self, m: usize, i: usize, seen: &mut Vec<(usize, usize)>) -> Target {
let module = &self.modules[m];
if let Some(&global) = self.decls[m].get(&i) {
return Target::Global(global);
}
assert!(
!seen.contains(&(m, i)),
"{}: an import refers back to itself",
module.name
);
seen.push((m, i));
let import = module.imports.iter().find(|import| import.var_idx == i);
let import =
import.unwrap_or_else(|| panic!("{}: a module var with no import", module.name));
let name = import.name.text(self.atoms);
let from = self.request(m, import.request);
match self.resolve(&from, &name, seen) {
Some(target) => target,
None => panic!("{}: `{from}` has no export `{name}`", module.name),
}
}
/// ES's ResolveExport: what export `name` of module `module` refers to
fn resolve(&self, module: &str, name: &str, seen: &mut Vec<(usize, usize)>) -> Option<Target> {
if module == NATIVE {
return natives().get(name).copied();
}
let Some(&m) = self.index.get(module) else {
panic!("no module `{module}`");
};
for export in &self.modules[m].exports {
match export {
ExportInfo::Local { var_idx, name: n } if n.text(self.atoms) == name => {
return Some(self.var(m, *var_idx, seen));
}
ExportInfo::Indirect {
request,
local,
name: n,
} if n.text(self.atoms) == name => {
return self.resolve(&self.request(m, *request), &local.text(self.atoms), seen);
}
_ => {}
}
}
// `export *` passes on every export but `default`
if name == "default" {
return None;
}
let mut stars = self.modules[m].star_exports.iter();
stars.find_map(|&request| self.resolve(&self.request(m, request), name, seen))
}
/// Every export of module `m`, by name. `seen` guards against `export *` in a circle
fn exports(&self, m: usize, seen: &mut Vec<usize>) -> HashMap<String, Target> {
let module = &self.modules[m];
let mut exports = HashMap::new();
if seen.contains(&m) {
return exports;
}
seen.push(m);
for &request in &module.star_exports {
let from = self.request(m, request);
let passed = match self.index.get(&from) {
Some(&other) => self.exports(other, seen),
None if from == NATIVE => natives(),
None => panic!("no module `{from}`"),
};
for (name, target) in passed {
if name != "default" {
exports.insert(name, target);
}
}
}
for export in &module.exports {
let (ExportInfo::Local { name, .. } | ExportInfo::Indirect { name, .. }) = export;
let name = name.text(self.atoms);
let target = self.resolve(&module.name, &name, &mut vec![]);
exports.insert(name, target.unwrap());
}
exports
}
/// The name of request `r` of module `m`. QuickJS made the same name when it loaded it
/// (see `normalize`)
fn request(&self, m: usize, r: usize) -> String {
let module = &self.modules[m];
normalize(&module.name, &module.requests[r])
}
/// The order that the bodies run in: each module after the modules that it imports, from
/// the prelude, then each root
fn order(&self, roots: &[String]) -> Vec<usize> {
let mut order = vec![];
for root in [PRELUDE.to_string()].iter().chain(roots) {
self.visit(self.index[root], &mut order);
}
order
}
fn visit(&self, m: usize, order: &mut Vec<usize>) {
if order.contains(&m) {
return;
}
order.push(m);
let at = order.len() - 1;
for r in 0..self.modules[m].requests.len() {
if let Some(&other) = self.index.get(&self.request(m, r)) {
self.visit(other, order);
}
}
// After its imports. It was marked first, so that a circle of imports ends
let m = order.remove(at);
order.push(m);
}
}
/// The name of the module that `specifier` names, in the module `base`. This is QuickJS's
/// default rule, which QuickJS used to load it: a name that does not start with `.` is as it
/// is. Else it is relative to the folder of `base`, and its leading `./` and `../` are taken
/// out
fn normalize(base: &str, specifier: &str) -> String {
if !specifier.starts_with('.') {
return specifier.to_string();
}
let mut folder = match base.rfind('/') {
Some(slash) => base[..slash].to_string(),
None => String::new(),
};
let mut rest = specifier;
loop {
if let Some(after) = rest.strip_prefix("./") {
rest = after;
} else if let Some(after) = rest.strip_prefix("../") {
let last = folder.rfind('/').map_or(0, |slash| slash + 1);
if folder.is_empty() || matches!(&folder[last..], "." | "..") {
break;
}
folder.truncate(last.saturating_sub(1));
rest = after;
} else {
break;
}
}
match folder.is_empty() {
true => rest.to_string(),
false => format!("{folder}/{rest}"),
}
}
/// The entry fn (see the module doc). Local `k` holds the closure of the `k`th body to run,
/// and the last local is the phase
fn entry(order: &[usize], modules: &[FlatModule], drain: usize) -> FlatFn {
use OpCode::*;
let n = order.len();
let phase = Imm::Loc(n as u16);
let body = |k: usize| Imm::Loc(k as u16);
// The loop starts after the closures and the phase, and ends after a call of each body
// and the phase's test
let head = 2 * n + 2;
let end = head + 4 * n + 5;
let make = (0..n).flat_map(|k| [(Fclosure, Imm::Const(k as u32)), (PutLoc, body(k))]);
let call = (0..n).flat_map(|k| {
[
(GetLoc, phase),
(GetLoc, body(k)),
(CallMethod, Imm::Npop(0)),
(Drop, Imm::None),
]
});
let code: Vec<(OpCode, Imm)> = make
.chain([(PushTrue, Imm::None), (PutLoc, phase)])
.chain(call)
.chain([
(GetLoc, phase),
(IfFalse, Imm::Label { target: end }),
(PushFalse, Imm::None),
(PutLoc, phase),
(Goto, Imm::Label { target: head }),
(GetVar, Imm::Global(drain as u32)),
(Call0, Imm::None),
(Drop, Imm::None),
(ReturnUndef, Imm::None),
])
.collect();
let code = code.into_iter().enumerate();
let code = code.map(|(pc, (op, imm))| Instr { pc, op, imm }).collect();
let bodies = order
.iter()
.map(|&m| JsValue::FnByIdx(modules[m].body))
.collect();
FlatFn {
name: AtomRef::Predefined(0),
kind: FuncKind::Normal,
arg_count: 0,
local_count: n + 1,
cpool: bodies,
code,
}
}
+125 -22
View File
@@ -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(&reg_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));
}
+98
View File
@@ -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()
}
+3 -1
View File
@@ -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!(),
};
+107 -190
View File
@@ -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
};
for instr_idx in block.start..block.end {
let instr = &code[instr_idx];
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);
}
// 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 fresh_reg(next_reg: &mut usize) -> Operand {
let r = *next_reg;
*next_reg += 1;
Operand::Reg(r)
}
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,
},
/// 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();
for instr in code {
if instr.op == OpCode::DefineField && !made.contains(&instr.args[1]) {
instr.op = OpCode::PutField;
}
}
}
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()
}
+322
View File
@@ -0,0 +1,322 @@
//! This module converts one basic block's instrs from stack to register form using a simulated stack
//!
//! The output reg form instrs are structured as:
//! `dsts = op(imm, args)`
//!
//! Every instr's pushes allocates new register operands which are pushed on the stack and form the
//! output instr's `dsts`, the instr's pops pop operands from the stack and form the output instr's
//! `args`
//!
//! Each instr's `op` and `imm` is preserved except for some special cases
//!
//! `set_loc` is henceforth used to move data from one operand (local, arg, reg) to another
#![allow(non_snake_case)]
use super::{BasicBlock, Operand, RegInstr};
use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode};
/// Walk a basic block's instructions, converting each instr to register form
pub fn walk_block(
block: &BasicBlock,
code: &[Instr],
stack: &mut Vec<Operand>,
next_reg: &mut usize,
out: &mut [RegInstr],
) {
let mut fresh_reg = || {
let r = *next_reg;
*next_reg += 1;
Operand::Reg(r)
};
for instr_idx in block.start..block.end {
let instr = &code[instr_idx];
let out_instr = &mut out[instr_idx];
if try_handle_named_read(instr, stack, &mut fresh_reg, out_instr) {
continue;
}
if try_handle_named_write(instr, stack, out_instr) {
continue;
}
if try_handle_shuffle(instr, stack, out_instr) {
continue;
}
handle_normal_case(instr, stack, &mut fresh_reg, out_instr);
if instr.op == OpCode::DefineFieldRef {
let prev = std::mem::replace(&mut out[instr_idx - 1], RegInstr::nop());
resolve_location(&prev, &mut out[instr_idx]);
}
}
}
/// Handles a "named read" i.e. a read of a local or arg (and not a stack value)
///
/// We simulate pushing the operand to stack, and replace the instr with `SetLoc`, which
/// is our catch-all for a data-move instr
fn try_handle_named_read(
instr: &Instr,
stack: &mut Vec<Operand>,
mut fresh_reg: impl FnMut() -> Operand,
out: &mut RegInstr,
) -> bool {
use OpCode::*;
let arg = match instr.op {
GetLoc0 => Operand::Local(0),
GetLoc1 => Operand::Local(1),
GetLoc2 => Operand::Local(2),
GetLoc3 => Operand::Local(3),
GetLoc | GetLoc8 | GetLocCheck => match instr.imm {
ImmOperand::Loc(k) => Operand::Local(k as usize),
_ => unreachable!(),
},
GetArg0 => Operand::Arg(0),
GetArg1 => Operand::Arg(1),
GetArg2 => Operand::Arg(2),
GetArg3 => Operand::Arg(3),
GetArg => match instr.imm {
ImmOperand::Arg(k) => Operand::Arg(k as usize),
_ => unreachable!(),
},
_ => return false,
};
let dst = fresh_reg();
// Converted SetLoc, reading from arg, writing to dst
*out = RegInstr {
dsts: vec![dst],
op: SetLoc,
imm: ImmOperand::None,
args: vec![arg],
};
// Simulate stack push
stack.push(dst);
true
}
/// Handles a "named write" i.e. a write of a local or arg (and not a stack value)
///
/// We simulate popping the operand off stack, and replace the instr with `SetLoc`, which
/// is our catch-all for a data-move instr
///
/// A distinction needs to be made between `Set*` and `Put*` since `Set*` instrs leave
/// the value-to-write on the stack and `Put*` pops it off
fn try_handle_named_write(instr: &Instr, stack: &mut Vec<Operand>, out: &mut RegInstr) -> bool {
use OpCode::*;
let (dst, preserved) = match instr.op {
PutLoc0 => (Operand::Local(0), false),
PutLoc1 => (Operand::Local(1), false),
PutLoc2 => (Operand::Local(2), false),
PutLoc3 => (Operand::Local(3), false),
PutLoc | PutLoc8 | PutLocCheck | PutLocCheckInit => match instr.imm {
ImmOperand::Loc(k) => (Operand::Local(k as usize), false),
_ => unreachable!(),
},
PutArg0 => (Operand::Arg(0), false),
PutArg1 => (Operand::Arg(1), false),
PutArg2 => (Operand::Arg(2), false),
PutArg3 => (Operand::Arg(3), false),
SetLoc0 => (Operand::Local(0), true),
SetLoc1 => (Operand::Local(1), true),
SetLoc2 => (Operand::Local(2), true),
SetLoc3 => (Operand::Local(3), true),
SetLoc | SetLoc8 => match instr.imm {
ImmOperand::Loc(k) => (Operand::Local(k as usize), true),
_ => unreachable!(),
},
SetArg0 => (Operand::Arg(0), true),
SetArg1 => (Operand::Arg(1), true),
SetArg2 => (Operand::Arg(2), true),
SetArg3 => (Operand::Arg(3), true),
_ => return false,
};
// Simulate stack pop or peek, depending on instr
let arg = if preserved {
stack.last().unwrap().clone()
} else {
stack.pop().unwrap()
};
// Converted SetLoc, reading from arg, writing to dst
*out = RegInstr {
dsts: vec![dst],
op: SetLoc,
imm: ImmOperand::None,
args: vec![arg],
};
true
}
/// Handles an instr that only moves stack values around: it pops `n` values, and pushes some
/// of them again. No value is made, so `out_instr` becomes `nop`
fn try_handle_shuffle(instr: &Instr, stack: &mut Vec<Operand>, out_instr: &mut RegInstr) -> bool {
let Some((n, pushes)) = shuffle(instr.op) else {
return false;
};
let popped = stack.split_off(stack.len() - n);
stack.extend(pushes.iter().map(|&i| popped[i]));
*out_instr = RegInstr::nop();
true
}
/// The shuffles: how many values an instr pops, and which of them it pushes, in stack order
/// (bottom first). The numbers index the popped values, bottom first
fn shuffle(op: OpCode) -> Option<(usize, &'static [usize])> {
use OpCode::*;
Some(match op {
Drop => (1, &[]), // a ->
Nip => (2, &[1]), // a b -> b
Nip1 => (3, &[1, 2]), // a b c -> b c
Dup => (1, &[0, 0]), // a -> a a
Dup1 => (2, &[0, 0, 1]), // a b -> a a b
Dup2 => (2, &[0, 1, 0, 1]), // a b -> a b a b
Dup3 => (3, &[0, 1, 2, 0, 1, 2]), // a b c -> a b c a b c
Insert2 => (2, &[1, 0, 1]), // obj a -> a obj a
Insert3 => (3, &[2, 0, 1, 2]), // obj prop a -> a obj prop a
Insert4 => (4, &[3, 0, 1, 2, 3]), // this obj prop a -> a this obj prop a
Perm3 => (3, &[1, 0, 2]), // obj a b -> a obj b
Perm4 => (4, &[2, 0, 1, 3]), // obj prop a b -> a obj prop b
Perm5 => (5, &[3, 0, 1, 2, 4]), // this obj prop a b -> a this obj prop b
Swap => (2, &[1, 0]), // a b -> b a
Swap2 => (4, &[2, 3, 0, 1]), // a b c d -> c d a b
Rot3l => (3, &[1, 2, 0]), // x a b -> a b x
Rot3r => (3, &[2, 0, 1]), // a b x -> x a b
Rot4l => (4, &[1, 2, 3, 0]), // x a b c -> a b c x
Rot5l => (5, &[1, 2, 3, 4, 0]), // x a b c d -> a b c d x
// Binds [[HomeObject]], which only `super` reads. `super` is not supported
SetHomeObject => (2, &[0, 1]),
// Makes the key a property key. A key is kept as it is (see infer)
ToPropkey2 => (2, &[0, 1]),
_ => return None,
})
}
/// Handle a normal instruction with simulated stack pushes/pops
fn handle_normal_case(
instr: &Instr,
stack: &mut Vec<Operand>,
mut fresh_reg: impl FnMut() -> Operand,
out_instr: &mut RegInstr,
) {
use OpCode::*;
// op and imm carried as-is
*out_instr = RegInstr {
dsts: vec![],
op: instr.op,
imm: instr.imm,
args: vec![],
};
// Some instrs have extra pops not reflected in info.n_pop
let extra_pops = match instr.imm {
ImmOperand::Npop(npop) => npop as usize,
ImmOperand::NpopU16 { npop, .. } => npop as usize,
_ => 0,
};
let info = instr.op.info();
let n_pop = info.n_pop as usize + extra_pops;
let n_push = info.n_push as usize;
// Simulate this instr's pops, top first
for _ in 0..n_pop {
let x = stack.pop().unwrap();
out_instr.args.push(x);
}
if matches!(instr.op, Call | CallMethod | CallConstructor) {
out_instr.args = call_operands(instr.op, std::mem::take(&mut out_instr.args));
}
// These leave the object they wrote on the stack: the same value, not a new one
if matches!(instr.op, DefineField | DefineFieldRef) {
stack.push(out_instr.args[1]);
return;
}
// Simulate this instr's pushes
for _ in 0..n_push {
let x = fresh_reg();
stack.push(x);
out_instr.dsts.push(x);
}
}
/// Put a call's popped operands in the order of the callee's params: the callee (which is its
/// env), `this`, then the args in source order
///
/// `call_method` has `this` at the bottom of its frame, below the callee. `call_constructor`
/// has the callee at the bottom and `this` above it (see transform/new.rs). `call` has `this`
/// on top, where transform pushed it
fn call_operands(op: OpCode, mut popped: Vec<Operand>) -> Vec<Operand> {
let (callee, this) = match op {
OpCode::CallMethod => {
let this = popped.pop().unwrap();
(popped.pop().unwrap(), this)
}
OpCode::CallConstructor => {
let callee = popped.pop().unwrap();
(callee, popped.pop().unwrap())
}
_ => {
let callee = popped.pop().unwrap();
(callee, popped.remove(0))
}
};
// What remains are the args, last arg first
popped.reverse();
[callee, this].into_iter().chain(popped).collect()
}
/// `define_field_ref x` defines `x` as the location that its value was read from (see
/// transform/closure.rs), and the instr before it is that read. Its value arg becomes the
/// location:
/// - a local or arg: the field is that binding
/// - the reg of an object `o`, for a read `get_field x` of `o`: the field is `o.x`, which is
/// a location itself (a capture that the owner gives to its own child)
///
/// The read itself is then not used, so it becomes `nop`
fn resolve_location(prev: &RegInstr, out_instr: &mut RegInstr) {
let value = out_instr.args[0];
assert_eq!(
prev.dsts,
[value],
"define_field_ref must follow the read of its value"
);
out_instr.args[0] = match prev.op {
OpCode::SetLoc => prev.args[0],
OpCode::GetField => {
assert!(prev.imm.atom() == out_instr.imm.atom());
prev.args[0]
}
op => panic!("define_field_ref after {op:?}, which reads no location"),
};
}
+37
View File
@@ -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
}
+684
View File
@@ -0,0 +1,684 @@
//! Lowers async fns and generators to state machines: a constructor, and a step closure
//!
//! Replace:
//! ```js
//! async function f(a) {
//! const x = await g(a);
//! return x + 1;
//! }
//! ```
//!
//! With:
//! ```js
//! function f(a) {
//! let x, state = 0, await1, co; // the frame: the body's locals, and the state
//!
//! const step = (input) => { // the body, as transitions
//! switch (state) {
//! case 0:
//! await1 = co.toPromise(g(a));
//! co.awaiting = await1;
//! state = 1;
//! return; // suspend
//! case 1:
//! x = await1.value; // resume: read the value from this site's promise
//! co.result = x + 1;
//! co.done = true;
//! return;
//! }
//! };
//!
//! co = { __proto__: __AsyncProto, step, value: undefined, awaiting: undefined, result: undefined, done: false };
//! return co.start();
//! }
//! ```
//!
//! The fn keeps its index and becomes the constructor. Its locals and args stay its own, and
//! the step captures them, so they live across every suspension as cells (see closure.rs). A
//! closure made in the body shares them, as any nested closure does
//!
//! Every kind of fn that suspends lowers the same way: only the driver differs, which is
//! stdlib JS (lib/stdlib/coroutine.js). `co` holds what a suspension hands out to it, each
//! in a field of its own so that their types stay apart: `value` for a yield, `awaiting` for
//! an await, `result` for the return. The constructor reads its kind's driver, an export of
//! `jsrt:coroutine`, as a global (see link.rs)
//!
//! A module's body that awaits at its top is an async fn too (see flatten.rs)
//!
//! In the step:
//! a local, arg or capture the step's capture of it
//! initial_yield suspend
//! yield x co.value = x; suspend; at the label: push input, push false
//! await x p = co.toPromise(x); co.awaiting = p; suspend; at the label: push p.value
//! return x co.result = x; co.done = true; return
//! close_loc k close_var_ref k (see closure.rs)
//!
//! Suspend stores the values on the stack under it into new locals, sets `state` and returns.
//! Its label, where the step's first instrs jump for that state, pushes them back. `yield`
//! pushes a flag after its value, which is true for `return()`: here it is always false
//!
//! Not supported: `yield*`, which QuickJS expands to a loop of iterator ops
use super::PROTO_FIELD;
use crate::{
flatten::FlatFn,
libqjs::parse::{
Atom, AtomRef, ClosureKind, ClosureVar, ClosureVarFlags, FuncKind, ImmOperand as Imm,
Instr, JsValue, VarKind, atom_names::PREDEFINED_ATOMS, opcodes::OpCode,
},
link::Link,
};
// ─────────────────────────────────────── The spine ───────────────────────────────────────
pub fn deasync(
fns: &mut Vec<FlatFn>,
captures: &mut Vec<Vec<ClosureVar>>,
atoms: &[Atom],
link: &Link,
) {
let coroutines: Vec<usize> = (0..fns.len())
.filter(|&f| fns[f].kind != FuncKind::Normal)
.collect();
let mut steps = vec![];
for &f in &coroutines {
// 1. Rewrite the body into the step's code, which reads every local, arg and capture
// of `f` through a capture. This gives `f` the new locals that the step needs
let mut step = Step::new(&fns[f], captures[f].len(), atoms);
let body = std::mem::take(&mut fns[f].code);
step.lower(&body);
// 2. The step captures all of `f`'s args, captures and locals. The closures made in
// the body now sit in the step, so they capture from it
let own = captures[f].clone();
let cpool = std::mem::take(&mut fns[f].cpool);
let step_captures = step.captures(&own);
retarget_children(&cpool, &step, &own, fns, captures);
// 3. Add the step after every other fn
let s = fns.len();
let step_fn = FlatFn {
name: fns[f].name,
kind: FuncKind::Normal,
arg_count: 1, // input
local_count: 0,
cpool,
code: step.code(),
};
fns.push(step_fn);
captures.push(step_captures);
fns[f].local_count = step.locals;
fns[f].cpool = vec![JsValue::FnByIdx(s)];
steps.push(step);
}
// 4. Each constructor makes `co`, linked to its kind's driver, and starts it
for (&f, step) in coroutines.iter().zip(&steps) {
let driver = link.stdlib("jsrt:coroutine", driver(fns[f].kind));
fns[f].code = constructor(step, driver, atoms);
}
}
/// The stdlib's driver of a kind of fn (see lib/stdlib/coroutine.js)
fn driver(kind: FuncKind) -> &'static str {
match kind {
FuncKind::Generator => "__GeneratorProto",
FuncKind::Async => "__AsyncProto",
_ => "__AsyncGeneratorProto",
}
}
// ─────────────────────────────────── Lowering the body ───────────────────────────────────
/// A local, arg or capture of the constructor
#[derive(Clone, Copy)]
enum Slot {
Local(usize),
Arg(usize),
Capture(usize),
}
/// What a suspension's label pushes, after the stack values that it kept
enum Resume {
Nothing, // initial_yield
Input, // yield: what `next(input)` sent, and the flag that it is not a `return()`
ValueOf(usize), // await: the value of the promise in this local
}
/// The step's code as it is written, and the constructor's locals
struct Step<'a> {
atoms: &'a [Atom],
args: usize, // the constructor's args
own_captures: usize, // the constructor's captures. The step captures its args, these, then its locals
locals: usize, // the constructor's locals so far
this: usize, // the local that holds `this`, which the step reads in place of its own
callee: usize, // the local that holds the fn itself, for `special_object 2`
co: usize,
state: usize,
code: Vec<Instr>,
labels: Vec<usize>, // the pc of each state's label: state k at k - 1
next_pc: usize, // pcs past the body's, for labels
}
impl<'a> Step<'a> {
fn new(f: &FlatFn, own_captures: usize, atoms: &'a [Atom]) -> Self {
let l = f.local_count;
let next_pc = f.code.last().map_or(0, |i| i.pc + 1);
Step {
atoms,
args: f.arg_count,
own_captures,
locals: l + 4,
this: l,
callee: l + 1,
co: l + 2,
state: l + 3,
code: vec![],
labels: vec![],
next_pc,
}
}
/// Rewrite the body, one instr at a time
fn lower(&mut self, body: &[Instr]) {
use OpCode::*;
for (instr, depth) in body.iter().zip(stack_depths(body)) {
let pc = instr.pc;
let depth = depth.unwrap_or(0);
if let Some((slot, op)) = slot_access(instr) {
self.var(pc, op, slot);
continue;
}
match (instr.op, instr.imm) {
(GetLoc0Loc1, _) => {
self.var(pc, GetVarRef, Slot::Local(0));
self.var(pc, GetVarRef, Slot::Local(1));
}
(IncLoc | DecLoc | AddLoc, Imm::Loc(k)) => {
self.update_local(pc, instr.op, k as usize)
}
(SetLocUninitialized, _) => self.push(pc, Nop, Imm::None), // the TDZ is not checked
(CloseLoc, Imm::Loc(k)) => self.var(pc, CloseVarRef, Slot::Local(k as usize)),
(PushThis, _) => self.var(pc, GetVarRef, Slot::Local(self.this)),
(SpecialObject, Imm::U8(2)) => self.var(pc, GetVarRef, Slot::Local(self.callee)),
(InitialYield, _) => self.suspend(pc, depth, Resume::Nothing),
(Yield, _) => {
self.set_co(pc, "value");
self.suspend(pc, depth.saturating_sub(1), Resume::Input);
}
(Await, _) => {
let promise = self.to_promise(pc);
self.suspend(pc, depth.saturating_sub(1), Resume::ValueOf(promise));
}
(Return | ReturnAsync, _) => self.finish(pc),
(ReturnUndef, _) => {
self.push(pc, Undefined, Imm::None);
self.finish(pc);
}
(YieldStar | AsyncYieldStar, _) => {
unimplemented!("`yield*`: needs the iterator ops")
}
_ => self.push(pc, instr.op, instr.imm),
}
}
}
/// The step's code: a jump on `state` to the label of each state, then the body. State 0
/// falls into the body
fn code(&mut self) -> Vec<Instr> {
use OpCode::*;
let body = std::mem::take(&mut self.code);
for (k, label) in self.labels.clone().into_iter().enumerate() {
let pc = self.new_pc();
self.var(pc, GetVarRef, Slot::Local(self.state));
self.push(pc, PushI32, Imm::I32(k as i32 + 1));
self.push(pc, StrictEq, Imm::None);
self.push(pc, IfTrue, Imm::Label { target: label });
}
let mut code = std::mem::take(&mut self.code);
code.extend(body);
code
}
/// The step's captures: the constructor's args, its captures, then its locals
fn captures(&self, own: &[ClosureVar]) -> Vec<ClosureVar> {
let mut list = vec![];
for k in 0..self.args {
list.push(closure_var(
self.name(Slot::Arg(k), own),
k,
ClosureKind::Arg,
));
}
for i in 0..self.own_captures {
list.push(closure_var(
self.name(Slot::Capture(i), own),
i,
ClosureKind::Ref,
));
}
for k in 0..self.locals {
list.push(closure_var(
self.name(Slot::Local(k), own),
k,
ClosureKind::Local,
));
}
list
}
/// The step's capture of a slot
fn index(&self, slot: Slot) -> usize {
match slot {
Slot::Arg(k) => k,
Slot::Capture(i) => self.args + i,
Slot::Local(k) => self.args + self.own_captures + k,
}
}
/// The name of the step's capture of a slot. A forward of a capture keeps its name, which
/// closure.rs needs (see `define_field_ref`). Any other gets a name that no variable has:
/// two locals can share a name, as in two blocks
fn name(&self, slot: Slot, own: &[ClosureVar]) -> AtomRef {
match slot {
Slot::Capture(i) => own[i].name,
_ => AtomRef::TaggedInt(self.index(slot) as u32),
}
}
}
// ──────────────────────────────────── Writing instrs ─────────────────────────────────────
impl Step<'_> {
fn push(&mut self, pc: usize, op: OpCode, imm: Imm) {
self.code.push(Instr { pc, op, imm });
}
/// `op` on the step's capture of `slot`: a get, put, set or close of a var ref
fn var(&mut self, pc: usize, op: OpCode, slot: Slot) {
let i = self.index(slot) as u16;
self.push(pc, op, Imm::VarRef(i));
}
fn field(&self, text: &str) -> Imm {
Imm::Atom(find_atom(text, self.atoms))
}
fn new_local(&mut self) -> usize {
self.locals += 1;
self.locals - 1
}
fn new_pc(&mut self) -> usize {
self.next_pc += 1;
self.next_pc - 1
}
/// `inc_loc`, `dec_loc` and `add_loc`. For `add_loc`, the added value is on the stack, and
/// the local is the left operand
fn update_local(&mut self, pc: usize, op: OpCode, k: usize) {
use OpCode::*;
self.var(pc, GetVarRef, Slot::Local(k));
match op {
IncLoc => self.push(pc, Inc, Imm::None),
DecLoc => self.push(pc, Dec, Imm::None),
_ => {
self.push(pc, Swap, Imm::None);
self.push(pc, Add, Imm::None);
}
}
self.var(pc, PutVarRef, Slot::Local(k));
}
/// `co.<field> = x`, with x on the stack
fn set_co(&mut self, pc: usize, field: &str) {
self.var(pc, OpCode::GetVarRef, Slot::Local(self.co));
self.push(pc, OpCode::Swap, Imm::None);
self.push(pc, OpCode::PutField, self.field(field));
}
/// `p = co.toPromise(x); co.awaiting = p`, with x on the stack. Gives the local of `p`
fn to_promise(&mut self, pc: usize) -> usize {
use OpCode::*;
let promise = self.new_local();
// [x] -> [co, toPromise, x] -> [p]
self.var(pc, GetVarRef, Slot::Local(self.co));
self.push(pc, GetField2, self.field("toPromise"));
self.push(pc, Rot3l, Imm::None);
self.push(pc, CallMethod, Imm::Npop(1));
self.var(pc, PutVarRef, Slot::Local(promise));
self.var(pc, GetVarRef, Slot::Local(self.co));
self.var(pc, GetVarRef, Slot::Local(promise));
self.push(pc, PutField, self.field("awaiting"));
promise
}
/// Store the `depth` values on the stack into new locals, set the state, and return. Then
/// the new state's label: push the values back, then what the suspension resumes with
fn suspend(&mut self, pc: usize, depth: usize, resume: Resume) {
use OpCode::*;
let kept: Vec<usize> = (0..depth).map(|_| self.new_local()).collect();
for &k in &kept {
self.var(pc, PutVarRef, Slot::Local(k));
}
let state = self.labels.len() as i32 + 1;
self.push(pc, PushI32, Imm::I32(state));
self.var(pc, PutVarRef, Slot::Local(self.state));
self.push(pc, ReturnUndef, Imm::None);
let label = self.new_pc();
self.labels.push(label);
self.push(label, Nop, Imm::None);
for &k in kept.iter().rev() {
self.var(label, GetVarRef, Slot::Local(k));
}
match resume {
Resume::Nothing => {}
Resume::Input => {
self.push(label, GetArg, Imm::Arg(0));
self.push(label, PushFalse, Imm::None);
}
Resume::ValueOf(promise) => {
self.var(label, GetVarRef, Slot::Local(promise));
self.push(label, GetField, self.field("value"));
}
}
}
/// The return of the body, with its value on the stack: `co.result = x; co.done = true`
fn finish(&mut self, pc: usize) {
self.set_co(pc, "result");
self.var(pc, OpCode::GetVarRef, Slot::Local(self.co));
self.push(pc, OpCode::PushTrue, Imm::None);
self.push(pc, OpCode::PutField, self.field("done"));
self.push(pc, OpCode::ReturnUndef, Imm::None);
}
}
/// The local, arg or capture that `instr` reads or writes, and the op that does that through a
/// capture
fn slot_access(instr: &Instr) -> Option<(Slot, OpCode)> {
use OpCode::*;
let nth = |base: OpCode| instr.op as usize - base as usize;
let imm = match instr.imm {
Imm::Loc(k) | Imm::Arg(k) | Imm::VarRef(k) => k as usize,
_ => 0,
};
let access = match instr.op {
GetLoc0 | GetLoc1 | GetLoc2 | GetLoc3 => (Slot::Local(nth(GetLoc0)), GetVarRef),
PutLoc0 | PutLoc1 | PutLoc2 | PutLoc3 => (Slot::Local(nth(PutLoc0)), PutVarRef),
SetLoc0 | SetLoc1 | SetLoc2 | SetLoc3 => (Slot::Local(nth(SetLoc0)), SetVarRef),
GetLoc | GetLoc8 | GetLocCheck => (Slot::Local(imm), GetVarRef),
PutLoc | PutLoc8 | PutLocCheck | PutLocCheckInit => (Slot::Local(imm), PutVarRef),
SetLoc | SetLoc8 => (Slot::Local(imm), SetVarRef),
GetArg0 | GetArg1 | GetArg2 | GetArg3 => (Slot::Arg(nth(GetArg0)), GetVarRef),
PutArg0 | PutArg1 | PutArg2 | PutArg3 => (Slot::Arg(nth(PutArg0)), PutVarRef),
SetArg0 | SetArg1 | SetArg2 | SetArg3 => (Slot::Arg(nth(SetArg0)), SetVarRef),
GetArg => (Slot::Arg(imm), GetVarRef),
PutArg => (Slot::Arg(imm), PutVarRef),
SetArg => (Slot::Arg(imm), SetVarRef),
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => {
(Slot::Capture(nth(GetVarRef0)), GetVarRef)
}
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => {
(Slot::Capture(nth(PutVarRef0)), PutVarRef)
}
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => {
(Slot::Capture(nth(SetVarRef0)), SetVarRef)
}
GetVarRef | GetVarRefCheck => (Slot::Capture(imm), GetVarRef),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => (Slot::Capture(imm), PutVarRef),
SetVarRef => (Slot::Capture(imm), SetVarRef),
_ => return None,
};
Some(access)
}
/// A closure made in the body now sits in the step: what it captured from the constructor, it
/// now captures from the step, as a forward
fn retarget_children(
cpool: &[JsValue],
step: &Step,
own: &[ClosureVar],
fns: &[FlatFn],
captures: &mut [Vec<ClosureVar>],
) {
for c in cpool {
let &JsValue::FnByIdx(child) = c else {
continue;
};
for i in 0..captures[child].len() {
let slot = match captures[child][i].flags.closure_kind {
ClosureKind::Local => Slot::Local(captures[child][i].var_idx),
ClosureKind::Arg => Slot::Arg(captures[child][i].var_idx),
ClosureKind::Ref => Slot::Capture(captures[child][i].var_idx),
_ => continue,
};
let name = step.name(slot, own);
captures[child][i] = closure_var(name, step.index(slot), ClosureKind::Ref);
rename_forwards(child, i, name, fns, captures);
}
}
}
/// Give every forward of capture `i` of `f`, in `f`'s children and in theirs, the name `name`:
/// a forward keeps its parent's name (see closure.rs)
fn rename_forwards(
f: usize,
i: usize,
name: AtomRef,
fns: &[FlatFn],
captures: &mut [Vec<ClosureVar>],
) {
for c in &fns[f].cpool {
let &JsValue::FnByIdx(child) = c else {
continue;
};
for j in 0..captures[child].len() {
let cv = &mut captures[child][j];
if cv.flags.closure_kind == ClosureKind::Ref && cv.var_idx == i {
cv.name = name;
rename_forwards(child, j, name, fns, captures);
}
}
}
}
// ──────────────────────────────────── The constructor ────────────────────────────────────
/// The constructor's code: keep `this` and the fn itself, make `co` with the step and the
/// driver, which is the global `driver`, set the state to 0, and return what the driver's
/// `start` gives
fn constructor(step: &Step, driver: usize, atoms: &[Atom]) -> Vec<Instr> {
use OpCode::*;
let field = |text: &str| Imm::Atom(find_atom(text, atoms));
let loc = |k: usize| Imm::Loc(k as u16);
let ops = [
(PushThis, Imm::None),
(PutLoc, loc(step.this)),
(SpecialObject, Imm::U8(2)),
(PutLoc, loc(step.callee)),
(Object, Imm::None),
(GetVar, Imm::Global(driver as u32)),
(DefineField, Imm::Atom(PROTO_FIELD)),
(Fclosure, Imm::Const(0)),
(DefineField, field("step")),
(Undefined, Imm::None),
(DefineField, field("value")),
(Undefined, Imm::None),
(DefineField, field("awaiting")),
(Undefined, Imm::None),
(DefineField, field("result")),
(PushFalse, Imm::None),
(DefineField, field("done")),
(PutLoc, loc(step.co)),
(Push0, Imm::None),
(PutLoc, loc(step.state)),
(GetLoc, loc(step.co)),
(GetField2, field("start")),
(CallMethod, Imm::Npop(0)),
(Return, Imm::None),
];
let mut code = vec![];
for (pc, (op, imm)) in ops.into_iter().enumerate() {
code.push(Instr { pc, op, imm });
}
code
}
fn closure_var(name: AtomRef, var_idx: usize, closure_kind: ClosureKind) -> ClosureVar {
let flags = ClosureVarFlags {
closure_kind,
_is_const: false,
is_lexical: false,
_var_kind: VarKind::Normal,
};
ClosureVar {
name,
var_idx,
flags,
}
}
/// The atom of a field name that the drivers use: a predefined one, or one of the program's
fn find_atom(text: &str, atoms: &[Atom]) -> AtomRef {
if let Some(n) = PREDEFINED_ATOMS.iter().position(|&a| a == text) {
return AtomRef::Predefined(n as u32);
}
for (i, atom) in atoms.iter().enumerate() {
if let Atom::String(s) = atom
&& s.text() == text
{
return AtomRef::FileLocal(i);
}
}
panic!(
"async fns and generators need the stdlib (lib/stdlib/coroutine.js), which names `{text}`"
)
}
// ───────────────────────────────────── Stack depths ──────────────────────────────────────
/// The stack depth before each instr. `None` for an instr that no path reaches
fn stack_depths(code: &[Instr]) -> Vec<Option<usize>> {
let mut depths = vec![None; code.len()];
let mut work = vec![0];
depths[0] = Some(0);
while let Some(i) = work.pop() {
let instr = &code[i];
let info = instr.op.info();
let after =
depths[i].unwrap() + info.n_push as usize - info.n_pop as usize - call_args(instr);
for next in successors(code, i) {
if depths[next].is_none() {
depths[next] = Some(after);
work.push(next);
}
}
}
depths
}
/// The args that a call pops, besides the fn: in the operand, or, for `call0` to `call3`, in
/// the opcode
fn call_args(instr: &Instr) -> usize {
use OpCode::*;
match (instr.op, instr.imm) {
(Call0 | Call1 | Call2 | Call3, _) => instr.op as usize - Call0 as usize,
(_, Imm::Npop(n)) => n as usize,
(_, Imm::NpopU16 { npop, .. }) => npop as usize,
_ => 0,
}
}
/// The ops that jump to their label
const JUMPS: &[OpCode] = &[
OpCode::Goto,
OpCode::Goto8,
OpCode::Goto16,
OpCode::IfFalse,
OpCode::IfFalse8,
OpCode::IfTrue,
OpCode::IfTrue8,
];
/// The ops after which the next instr does not run
const ENDS: &[OpCode] = &[
OpCode::Goto,
OpCode::Goto8,
OpCode::Goto16,
OpCode::Return,
OpCode::ReturnUndef,
OpCode::ReturnAsync,
OpCode::Throw,
OpCode::ThrowError,
OpCode::TailCall,
OpCode::TailCallMethod,
OpCode::Ret,
];
/// The instrs that can run after instr `i`: its jump's target, and the next instr unless it
/// ends the flow
fn successors(code: &[Instr], i: usize) -> Vec<usize> {
let instr = &code[i];
let mut next = vec![];
if let Imm::Label { target } = instr.imm
&& JUMPS.contains(&instr.op)
{
next.push(code.iter().position(|j| j.pc == target).unwrap());
}
if !ENDS.contains(&instr.op) && i + 1 < code.len() {
next.push(i + 1);
}
next
}
+169
View File
@@ -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)
}
+217
View File
@@ -0,0 +1,217 @@
//! This module lowers closures by making implicit environment captures explicit
//!
//! Replace:
//! ```js
//! let x = 0;
//! let y = true;
//!
//! function fn() {
//! f(x)
//! f(y)
//! }
//! ```
//!
//! With:
//! ```js
//! let x = 0;
//! let y = true;
//!
//! function fn() {
//! f(fn.<var>.x);
//! f(fn.<var>.y);
//! }
//!
//! fn.<var> = {};
//! fn.<var>.x = x; // by-ref
//! fn.<var>.y = y; // by-ref
//! ```
//!
//! i.e. captured bindings are turned into fields on the function. A closure is an object, so
//! it can have fields of its own. The captures are kept apart from them in one field, `<var>`,
//! so that a write to a field of the closure cannot clobber a capture
//!
//! # Locations
//!
//! Every binding (a local or an arg) is a location. A capture must share the location of the
//! binding it captures: a write by the owner is seen by the closure, and a write by the
//! closure is seen by the owner. Normally a field holds a copy of a value, so a special
//! `define_field_ref` instr is introduced by `c_parse.py`:
//!
//! define_field_ref x [obj, value] -> [obj]
//!
//! It defines `obj.x` as the location that `value` was read from, not as a copy of `value`.
//! So the instr right before it is always the read of a location. This module only emits:
//! - get_loc k a local of the owner
//! - get_arg k an arg of the owner
//! - get_field y a capture of the owner, to give to its own child. The owner's `y` is
//! the location of a binding further out, so the child shares that
//!
//! Where a location lives (the stack or the heap) is not decided here. `escape.rs` decides
//! that from these instrs. This module only states which fields share which locations
//!
//! # `close_loc`: a new binding for each loop iteration
//!
//! In JS, each iteration of a loop gets its own copy of the bindings that are declared in
//! the loop:
//! ```js
//! for (let i = 0; i < 3; i++) fns.push(() => i);
//! // the closures give 0, 1, 2, not 3, 3, 3
//! ```
//!
//! QuickJS uses one local for `i`, and emits `close_loc i` at each way out of the loop body.
//! After this module, `close_loc k` means: the binding of local `k` moves to a new location,
//! with the value of the old location. The closures that were made before the `close_loc`
//! keep the old location. So in the example, `i++` writes the new location of `i`, and the
//! closure of each iteration keeps the `i` of its iteration
//!
//! The owner's code does not change for this: `get_loc k` and `put_loc k` always use the
//! current location of `k`. This module keeps `close_loc` as it is. Then:
//! - infer can ignore it. The old and new locations of a binding have the same address
//! (`Addr::Shared`), so their types are joined
//! - `escape.rs` must give each iteration a new location if a closure can see the old one
//! after the `close_loc`
//!
//! # The replacements
//!
//! In the owner, for `fclosure f`:
//! fclosure f [fn]
//! object [fn, vars]
//! <read of the location> [fn, vars, value] for each capture `x` of `f`
//! define_field_ref x [fn, vars]
//! define_field <var> [fn]
//!
//! A closure that captures nothing gets no `<var>`: only the `fclosure` stays
//!
//! In the closure, with `env` as the closure itself (arg `ENV_ARG`):
//! get_var_ref x get_arg env; get_field <var>; get_field x
//! put_var_ref x get_arg env; get_field <var>; swap; put_field x
//! set_var_ref x dup; get_arg env; get_field <var>; swap; put_field x
//!
//! close_var_ref x get_arg env; get_field <var>; close_field x
//!
//! The `_check` forms are the same: the TDZ is not checked. `close_var_ref` is `close_loc` for
//! a capture: transform/async.rs makes it, when a binding that it moves out of a fn's frame
//! needs a new location for each loop iteration
//!
//! A module binding is not a capture: modules.rs made it a global first
//!
//! In the emitted C, every underlying "plain" function is called as:
//! `fn(env, this, arg_0, arg_1, ... arg_n)`
//! where `env` is the closure itself
use super::{ENV_ARG, FIRST_ARG};
use crate::libqjs::parse::{
AtomRef, ClosureKind, ClosureVar, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode,
};
/// `<var>`: the field of a closure that holds its captures
pub const VAR_FIELD: AtomRef = AtomRef::Predefined(86);
/// Replace a read or write of a capture, or the making of a closure
///
/// `own` is the captures of the fn that holds `instr`
pub fn try_replace_closure(
instr: &Instr,
own: &[ClosureVar],
captures: &[Vec<ClosureVar>],
cpool: &[JsValue],
out: &mut Vec<Instr>,
) -> bool {
use OpCode::*;
let mut push = |op, imm| {
out.push(Instr {
pc: instr.pc,
op,
imm,
})
};
if let Some((i, writes, leaves)) = var_ref_access(instr) {
if leaves {
push(Dup, Imm::None);
}
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
if writes {
push(Swap, Imm::None);
push(PutField, Imm::Atom(own[i].name));
} else {
push(GetField, Imm::Atom(own[i].name));
}
return true;
}
// A new location for the binding that a capture shares (see `close_loc` above)
if let (CloseVarRef, Imm::VarRef(i)) = (instr.op, instr.imm) {
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
push(CloseField, Imm::Atom(own[i as usize].name));
return true;
}
if !matches!(instr.op, Fclosure | Fclosure8) {
return false;
}
let JsValue::FnByIdx(child) = cpool[instr.imm.const_idx()] else {
unreachable!()
};
push(Fclosure, instr.imm);
// A closure with no captures needs no `<var>`
if captures[child].is_empty() {
return true;
}
push(Object, Imm::None);
for c in &captures[child] {
match c.flags.closure_kind {
ClosureKind::Local => push(GetLoc, Imm::Loc(c.var_idx as u16)),
ClosureKind::Arg => push(GetArg, Imm::Arg(FIRST_ARG + c.var_idx as u16)),
ClosureKind::Ref => {
push(GetArg, Imm::Arg(ENV_ARG));
push(GetField, Imm::Atom(VAR_FIELD));
push(GetField, Imm::Atom(own[c.var_idx].name));
}
_ => unimplemented!("a capture of kind {:?}", c.flags.closure_kind),
}
push(DefineFieldRef, Imm::Atom(c.name));
}
push(DefineField, Imm::Atom(VAR_FIELD));
true
}
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes, and whether a
/// write also leaves the value on the stack
pub(super) fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
use OpCode::*;
let i = match instr.imm {
Imm::VarRef(i) => i as usize,
_ => 0,
};
match instr.op {
GetVarRef0 | GetVarRef1 | GetVarRef2 | GetVarRef3 => {
Some((instr.op as usize - GetVarRef0 as usize, false, false))
}
PutVarRef0 | PutVarRef1 | PutVarRef2 | PutVarRef3 => {
Some((instr.op as usize - PutVarRef0 as usize, true, false))
}
SetVarRef0 | SetVarRef1 | SetVarRef2 | SetVarRef3 => {
Some((instr.op as usize - SetVarRef0 as usize, true, true))
}
GetVarRef | GetVarRefCheck => Some((i, false, false)),
PutVarRef | PutVarRefCheck | PutVarRefCheckInit => Some((i, true, false)),
SetVarRef => Some((i, true, true)),
_ => None,
}
}
+146
View File
@@ -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
}
+157
View File
@@ -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,
}
}
+198
View File
@@ -0,0 +1,198 @@
//! Module bindings become globals (see link.rs)
//!
//! A module's body reaches its bindings and imports as captured vars, and so does each fn in
//! it, through its parents. Link found what each of the body's captured vars refers to. Here
//! each read and write of one becomes a read or write of a global, with `get_var` and
//! `put_var` on an `ImmOperand::Global`. A native is a constant
//!
//! get_var_ref k get_var g (or push_const of a native)
//! put_var_ref k put_var g
//! set_var_ref k dup; put_var g
//!
//! The `_check` forms are the same: the TDZ is not checked. Those captured vars are then
//! dropped, so a closure no longer holds module bindings, and the captured vars that are
//! left are numbered again
//!
//! A global name, such as `String`, is a `get_var` of its name. It becomes the global or the
//! native that the prelude exports by that name. `typeof` of a name that nothing declares is
//! a `get_var_undef`, which gives undefined. A write of a global name is not allowed
use super::closure::var_ref_access;
use crate::{
flatten::{ENTRY_FN, FlatFn},
libqjs::parse::{
Atom, ClosureKind, ClosureVar, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode,
},
link::{Link, Target},
};
pub fn globalize(
fns: &mut [FlatFn],
captures: &mut [Vec<ClosureVar>],
link: &Link,
atoms: &[Atom],
) {
// Each fn's parent goes first, because a capture names a captured var of its parent. Each
// fn also gets the name of its module, for errors
let mut stack = vec![(ENTRY_FN, vec![], String::new())];
while let Some((f, parent, module)) = stack.pop() {
let module = link.body(f).map_or(module, |(name, _)| name.to_string());
let targets = targets(f, &captures[f], &parent, link);
let renumber = renumber(&targets);
captures[f] = keep(&captures[f], &targets, &parent);
fns[f].code = rewrite(&mut fns[f], &targets, &renumber, link, atoms, &module);
let children = fns[f].cpool.iter().filter_map(|c| match c {
JsValue::FnByIdx(child) => Some(*child),
_ => None,
});
let own = targets.into_iter().zip(renumber).map(|(t, r)| Slot {
target: t,
index: r,
});
let own: Vec<Slot> = own.collect();
for child in children.collect::<Vec<_>>() {
stack.push((child, own.clone(), module.clone()));
}
}
}
/// A captured var of a fn, after this pass: the binding that it refers to, if that is a
/// module binding, else its new index
#[derive(Clone, Copy)]
struct Slot {
target: Option<Target>,
index: Option<usize>,
}
/// What each captured var of `f` refers to, if a module binding. A module's body's captured
/// vars are all module bindings. Any other fn's capture refers to one if the captured var of
/// its parent does
fn targets(f: usize, own: &[ClosureVar], parent: &[Slot], link: &Link) -> Vec<Option<Target>> {
if let Some((_, vars)) = link.body(f) {
return vars.iter().map(|&target| Some(target)).collect();
}
(own.iter())
.map(|var| match var.flags.closure_kind {
ClosureKind::Ref => parent[var.var_idx].target,
_ => None,
})
.collect()
}
/// The new index of each captured var that stays
fn renumber(targets: &[Option<Target>]) -> Vec<Option<usize>> {
let mut next = 0;
(targets.iter())
.map(|target| match target {
Some(_) => None,
None => {
next += 1;
Some(next - 1)
}
})
.collect()
}
/// The captured vars that stay. A capture of the parent's captured var gets its new index
fn keep(own: &[ClosureVar], targets: &[Option<Target>], parent: &[Slot]) -> Vec<ClosureVar> {
let stay = own
.iter()
.zip(targets)
.filter(|(_, target)| target.is_none());
stay.map(|(var, _)| {
let mut var = var.clone();
if var.flags.closure_kind == ClosureKind::Ref {
var.var_idx = parent[var.var_idx].index.unwrap();
}
var
})
.collect()
}
/// The code of `f`, with module bindings as globals, and the captured vars that stay at their
/// new indices
fn rewrite(
f: &mut FlatFn,
targets: &[Option<Target>],
renumber: &[Option<usize>],
link: &Link,
atoms: &[Atom],
module: &str,
) -> Vec<Instr> {
use OpCode::*;
let mut out = vec![];
let code = std::mem::take(&mut f.code);
for instr in code {
let pc = instr.pc;
let mut push = |op, imm| out.push(Instr { pc, op, imm });
if let Some((i, writes, leaves)) = var_ref_access(&instr) {
match (targets[i], writes) {
(Some(target), false) => read(target, &mut f.cpool, &mut push),
(Some(Target::Global(global)), true) => {
if leaves {
push(Dup, Imm::None);
}
push(PutVar, Imm::Global(global as u32));
}
(Some(Target::Native(_)), true) => panic!("a native cannot be assigned to"),
(None, _) => {
let op = match (writes, leaves) {
(false, _) => GetVarRef,
(true, false) => PutVarRef,
(true, true) => SetVarRef,
};
push(op, Imm::VarRef(renumber[i].unwrap() as u16));
}
}
continue;
}
match (instr.op, instr.imm) {
(GetVar | GetVarUndef, Imm::Atom(name)) => {
let text = name.text(atoms);
match (link.global(&text), instr.op) {
(Some(target), _) => read(target, &mut f.cpool, &mut push),
(None, GetVarUndef) => push(Undefined, Imm::None),
(None, _) => {
panic!("{module}: `{text}` is not declared, and is not a global name")
}
}
}
(PutVar | PutVarInit, Imm::Atom(name)) => {
panic!(
"{module}: `{}` is a global name, which cannot be assigned to",
name.text(atoms)
)
}
_ => out.push(instr),
}
}
out
}
/// A read of what a module binding refers to: a global, or a native as a constant
fn read(target: Target, cpool: &mut Vec<JsValue>, push: &mut impl FnMut(OpCode, Imm)) {
match target {
Target::Global(global) => push(OpCode::GetVar, Imm::Global(global as u32)),
Target::Native(idx) => {
cpool.push(JsValue::NativeFn(idx));
push(OpCode::PushConst, Imm::Const(cpool.len() as u32 - 1));
}
}
}
+64
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+43
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,158 @@
// A minimal io_uring ring, on the raw syscalls. Only io.h uses it
//
// We write SQEs into the SQ and the kernel takes them when we submit. The
// kernel writes CQEs into the CQ and we take them. Each side moves only its
// own index, and reads the other's with acquire, so it sees the entries that
// came before
#pragma once
#ifdef JSRT_IMPLEMENTATION
#include <errno.h>
#include <linux/io_uring.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
typedef struct {
int fd;
// The SQ: we own the tail, the kernel owns the head. `array` maps each
// slot of the ring to an SQE, and we keep it the identity
unsigned *sq_head, *sq_tail, *sq_array;
unsigned sq_mask, sq_entries;
struct io_uring_sqe *sqes;
unsigned tail; // our copy of the SQ tail, ahead of it until submit
unsigned to_submit; // SQEs written that the kernel did not take yet
// The CQ: the kernel owns the tail, we own the head
unsigned *cq_head, *cq_tail;
unsigned cq_mask, cq_entries;
struct io_uring_cqe *cqes;
} jsrt_uring;
_Noreturn static void jsrt_uring_fail(const char *what) {
fprintf(stderr, "jsrt: %s: %s\n", what, strerror(errno));
exit(1);
}
// The features that io.h depends on, all from Linux 5.5:
// SINGLE_MMAP the SQ and the CQ rings are one mapping
// NODROP the kernel keeps CQEs that do not fit the CQ, never drops them
// SUBMIT_STABLE the kernel copies what an SQE points to (a timespec, a path)
// before submit returns, so it needs to stay only until then
static void jsrt_uring_init(jsrt_uring *r, unsigned entries) {
struct io_uring_params p;
memset(&p, 0, sizeof p);
int fd = (int)syscall(__NR_io_uring_setup, entries, &p);
if (fd < 0) {
jsrt_uring_fail("io_uring_setup");
}
unsigned need = IORING_FEAT_SINGLE_MMAP | IORING_FEAT_NODROP | IORING_FEAT_SUBMIT_STABLE;
if ((p.features & need) != need) {
fputs("jsrt: io_uring: the kernel is older than Linux 5.5\n", stderr);
exit(1);
}
size_t sq_size = p.sq_off.array + p.sq_entries * sizeof(unsigned);
size_t cq_size = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe);
size_t size = sq_size > cq_size ? sq_size : cq_size;
int prot = PROT_READ | PROT_WRITE, flags = MAP_SHARED | MAP_POPULATE;
char *ring = mmap(NULL, size, prot, flags, fd, IORING_OFF_SQ_RING);
void *sqes = mmap(NULL, p.sq_entries * sizeof(struct io_uring_sqe), prot, flags, fd, IORING_OFF_SQES);
if (ring == MAP_FAILED || sqes == MAP_FAILED) {
jsrt_uring_fail("io_uring mmap");
}
*r = (jsrt_uring){
.fd = fd,
.sq_head = (unsigned *)(ring + p.sq_off.head),
.sq_tail = (unsigned *)(ring + p.sq_off.tail),
.sq_array = (unsigned *)(ring + p.sq_off.array),
.sq_mask = *(unsigned *)(ring + p.sq_off.ring_mask),
.sq_entries = p.sq_entries,
.sqes = sqes,
.tail = *(unsigned *)(ring + p.sq_off.tail),
.cq_head = (unsigned *)(ring + p.cq_off.head),
.cq_tail = (unsigned *)(ring + p.cq_off.tail),
.cq_mask = *(unsigned *)(ring + p.cq_off.ring_mask),
.cq_entries = p.cq_entries,
.cqes = (struct io_uring_cqe *)(ring + p.cq_off.cqes),
};
}
// A new SQE, all zero, or NULL if the SQ is full. The kernel sees it at the
// next submit
static struct io_uring_sqe *jsrt_uring_sqe(jsrt_uring *r) {
unsigned head = __atomic_load_n(r->sq_head, __ATOMIC_ACQUIRE);
if (r->tail - head == r->sq_entries) {
return NULL;
}
unsigned i = r->tail & r->sq_mask;
r->sq_array[i] = i;
r->tail++;
r->to_submit++;
memset(&r->sqes[i], 0, sizeof r->sqes[i]);
return &r->sqes[i];
}
static bool jsrt_uring_cq_empty(jsrt_uring *r) {
return *r->cq_head == __atomic_load_n(r->cq_tail, __ATOMIC_ACQUIRE);
}
// Submits every SQE written, and if `wait`, waits until the CQ has a CQE
//
// The kernel can take fewer SQEs than we give, for example when one fails
// before it starts. That one still gets a CQE, and we give the rest again
static void jsrt_uring_submit(jsrt_uring *r, bool wait) {
__atomic_store_n(r->sq_tail, r->tail, __ATOMIC_RELEASE);
for (;;) {
bool get = wait && jsrt_uring_cq_empty(r);
if (!r->to_submit && !get) {
return;
}
unsigned flags = get ? IORING_ENTER_GETEVENTS : 0;
int n = (int)syscall(__NR_io_uring_enter, r->fd, r->to_submit, get ? 1 : 0, flags, NULL, 0);
if (n < 0) {
if (errno == EINTR) {
continue;
}
jsrt_uring_fail("io_uring_enter");
}
r->to_submit -= (unsigned)n;
}
}
// Takes the next CQE, if there is one
static bool jsrt_uring_cqe(jsrt_uring *r, uint64_t *user_data, int32_t *res) {
if (jsrt_uring_cq_empty(r)) {
return false;
}
unsigned head = *r->cq_head;
struct io_uring_cqe *cqe = &r->cqes[head & r->cq_mask];
*user_data = cqe->user_data;
*res = cqe->res;
__atomic_store_n(r->cq_head, head + 1, __ATOMIC_RELEASE);
return true;
}
#endif // JSRT_IMPLEMENTATION
+153
View File
@@ -0,0 +1,153 @@
// Arrays. The compiler gives only `a[i]`, `a[i] = x` and `a.length`, which can
// also be written. Every method here is built on those
//
// Not as the spec says:
// - A method takes one value where the spec takes any number (`push(x)`,
// `unshift(x)`, `concat(other)`), because more need rest params
// - A callback gets the element and its index, not the array too
// - Holes are not skipped: a callback sees undefined for a hole
// `Array(n)` and `new Array(n)` make an array of `n` holes
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;
},
};
+10
View File
@@ -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; },
};
+11
View File
@@ -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));
},
};
+64
View File
@@ -0,0 +1,64 @@
// The drivers of async fns and generators
//
// The compiler lowers each async fn and generator to a coroutine (see
// compiler/src/transform/async.rs): a step closure, which runs the body until
// it suspends or returns, and an object `co`:
//
// { __proto__: <a proto below>, step, value, awaiting, result, done }
//
// A yield puts its value in `value`, an await puts its promise in `awaiting`,
// and a return puts its value in `result` and sets `done`. The fn returns
// `co.start()`. Only these drivers differ between the kinds
//
// Not supported yet: `throw()` on a generator, and `finally` blocks in
// `return()`, which need exceptions
import { __is_promise, __reject, __resolve } from "jsrt:promise";
// A generator's object is `co` itself
export const __GeneratorProto = {
// The body runs up to its `initial_yield`, after its params
start() {
this.step(undefined);
return this;
},
next(input) {
if (this.done) return { value: undefined, done: true };
this.step(input);
if (this.done) return { value: this.result, done: true };
return { value: this.value, done: false };
},
return(value) {
this.done = true;
return { value: value, done: true };
},
};
// An async fn returns a promise, which its return fulfils. A rejected await
// rejects it, and the rest of the body does not run, as an uncaught throw at the
// await would
export const __AsyncProto = {
// The body runs up to its first await, in the call
start() {
const promise = new Promise(undefined);
this.run(promise);
return promise;
},
run(promise) {
this.step(undefined);
if (this.done) __resolve(promise, this.result);
else this.awaiting.then(() => this.run(promise), (reason) => __reject(promise, reason));
},
// What an await waits on: a promise as it is, any other value as a fulfilled one.
// The step reads the value from it when it resumes
toPromise(x) {
return __is_promise(x) ? x : Promise.resolve(x);
},
};
+7
View File
@@ -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;
+84
View File
@@ -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);
},
};
+42
View File
@@ -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();
}
+55
View File
@@ -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;
}
+10
View File
@@ -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 };
};
+42
View File
@@ -0,0 +1,42 @@
// The prelude: its exports are the global names of every module, such as
// `String` or `console`. A module that does not declare a name, and does not
// import it, refers to the prelude's export of that name. A module can declare
// a binding of the same name, which hides it (see compiler/src/link.rs)
//
// The stdlib is built on the natives in compiler/src/native.rs, the common
// layer of operations on primitive values, which `jsrt:native` exports. The
// compiler's own code for operators (`+` on strings and so on) uses the runtime
// directly, not these functions.
//
// Restrictions:
// - The prototypes of `String`, `Number`, `Boolean` and `Array` are fixed:
// the compiler reads them as the prelude's exports, as the prototypes of the
// primitives and of arrays. User code may not change them (no monkey-patching)
// - `new String(x)`, `new Number(x)` and `new Boolean(x)` (wrapper objects) are
// not supported. These functions only convert
// - A global name cannot be assigned to
//
// `String.prototype.concat` is not here, because it needs rest params, which
// need arrays. `console.log` prints one arg only, for the same reason
//
// The natives are global names too, as the runtime's own fns, with `__pending`,
// where an IO op's callback waits for the host (see loop.js). The compiler
// needs `jsrt:coroutine` for async fns and generators, so the prelude loads it.
// It finds the host's `__complete` in `jsrt:loop`
//
// The modules run in the order of these imports, before any other module
export { undefined, NaN, Infinity } from "jsrt:globals";
export { Object } from "jsrt:object";
export { String } from "jsrt:string";
export { Number } from "jsrt:number";
export { Boolean } from "jsrt:boolean";
export { Array } from "jsrt:array";
export { Promise, queueMicrotask } from "jsrt:promise";
export { setTimeout, clearTimeout } from "jsrt:timers";
export { fs } from "jsrt:io";
export { console } from "jsrt:console";
export * from "jsrt:native";
export { __pending } from "jsrt:loop";
import "jsrt:coroutine";
+201
View File
@@ -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);
}
+149
View File
@@ -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);
}
+28
View File
@@ -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);
}
Vendored Submodule
+1
Submodule third_party/stb added at 2c980bb598