Rewrite with working async

This commit is contained in:
2026-10-16 08:00:06 +00:00
parent d0c2f291e9
commit 1e4647347f
36 changed files with 5106 additions and 654 deletions
+21 -1
View File
@@ -126,6 +126,22 @@ class OpDef(NamedTuple):
temporary: bool # written as lowercase `def()`: a compiler-only opcode
# Opcodes that this compiler adds. QuickJS never emits them, so they come after
# every real opcode and `short_opcode_info` never gives them for a byte of
# bytecode. Only the transforms emit them.
EXTRA_OPS = [
# [obj, value] -> [obj]: like `define_field`, but the field is the location
# that `value` was read from, not a copy of it (see transform/closure.rs)
OpDef("define_field_ref", 5, 2, 1, "atom", False),
# [] -> []: like `close_loc`, for a capture: the binding that capture `i` shares gets a
# new location, with the current value (see transform/async.rs)
OpDef("close_var_ref", 3, 0, 0, "var_ref", False),
# [obj] -> []: field `x` of `obj`, which shares a location, gets a new location, with
# the current value. What `close_var_ref` lowers to (see transform/closure.rs)
OpDef("close_field", 5, 1, 0, "atom", False),
]
def parse_fmt_list(text: str) -> list[str]:
"""The `FMT(name)` list, in order."""
out = []
@@ -284,6 +300,8 @@ def gen_opcodes(text: str, tag: str) -> str:
# no short-opcode shift, unlike the C `short_opcode_info` macro whose table
# still holds them.
real = [o for o in ops if not o.temporary]
real_count = len(real)
real += EXTRA_OPS
s = [header(tag, "quickjs-opcode.h")]
@@ -351,8 +369,10 @@ def gen_opcodes(text: str, tag: str) -> str:
"///\n"
"/// The table already excludes the compiler's temporary opcodes, so the\n"
"/// byte indexes it directly; there is no short-opcode shift to apply.\n"
"/// The opcodes that this compiler adds are not in bytecode, so they\n"
"/// are never given.\n"
"pub fn short_opcode_info(op: u8) -> Option<&'static OpInfo> {\n"
" OPCODE_INFO.get(op as usize)\n"
f" OPCODE_INFO[..{real_count}].get(op as usize)\n"
"}\n"
)
return "".join(s)
+4 -1
View File
@@ -3,7 +3,10 @@ const QUICKJS: &str = "../third_party/quickjs/build";
fn main() {
// The compiler compiles the C it emits for the target it is built for (see
// main.rs)
println!("cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap());
println!(
"cargo:rustc-env=TARGET={}",
std::env::var("TARGET").unwrap()
);
println!("cargo:rustc-link-search=native={QUICKJS}");
println!("cargo:rustc-link-lib=static=qjs");
+371
View File
@@ -0,0 +1,371 @@
//! One walk as one C fn
//!
//! Each local, arg and reg is a C local, of the C type of its type in memory. A binding that
//! a closure shares is a pointer to a cell (see transform/closure.rs): its cell is made at
//! entry, and again at each `close_loc`. A captured arg is copied into its cell at entry
//!
//! Each reachable block is a label. Its instrs come in order, then its postfix copies, then
//! its jump. A jump whose condition infer decided goes to the one block that can run
use std::collections::HashMap;
use std::fmt::Write as _;
use super::{Emitter, layout::Member, layout::Rep};
use crate::{
infer::Type,
libqjs::parse::opcodes::OpCode,
regify::{BasicBlock, Operand, RegFn},
};
// ─────────────────────────────────────── Writers ────────────────────────────────────────
/// Lines of C, indented
#[derive(Default)]
pub struct Code {
pub out: String,
depth: usize,
temps: usize,
}
/// What prints C: a walk's fn, or a read fn. Gives the lines, and splitting on sums
pub trait Writer<'a> {
fn code(&mut self) -> &mut Code;
fn emitter(&mut self) -> &mut Emitter<'a>;
fn line(&mut self, text: impl AsRef<str>) {
let code = self.code();
let indent = code.depth * 4;
let _ = writeln!(code.out, "{:indent$}{}", "", text.as_ref());
}
fn indent(&mut self, by: isize) {
let code = self.code();
code.depth = code.depth.saturating_add_signed(by);
}
/// Throw a TypeError that says `what` (see `__rt_type_error`)
fn throw(&mut self, what: &str) {
self.line(format!("__rt_type_error(\"{what}\");"));
}
/// A new C local of C type `rep`. Gives its name
fn temp(&mut self, rep: Rep) -> String {
let ctype = self.emitter().ctype(rep);
let code = self.code();
let name = format!("t{}", code.temps);
code.temps += 1;
self.line(format!("{ctype} {name};"));
name
}
/// Run `body` once for each combination of the members of `ops`, with each operand as
/// a value of that member. An operand that is a sum is split by a `switch` on its tag
fn split(
&mut self,
ops: &[(String, Rep)],
body: &mut dyn FnMut(&mut Self, &[(String, Member)]),
) {
self.split_from(ops, &mut vec![], body);
}
fn split_from(
&mut self,
ops: &[(String, Rep)],
done: &mut Vec<(String, Member)>,
body: &mut dyn FnMut(&mut Self, &[(String, Member)]),
) {
let Some(((expr, rep), rest)) = ops.split_first() else {
return body(self, done);
};
match *rep {
Rep::One(member) => {
done.push((expr.clone(), member));
self.split_from(rest, done, body);
done.pop();
}
Rep::Sum(id) => {
let members = self.emitter().layout.sums[id].clone();
self.line(format!("switch ({expr}.tag) {{"));
for (tag, member) in members.into_iter().enumerate() {
self.line(format!("case {tag}: {{"));
self.indent(1);
done.push((format!("{expr}.as.m{tag}"), member));
self.split_from(rest, done, body);
done.pop();
self.line("break;");
self.indent(-1);
self.line("}");
}
self.line("}");
}
}
}
}
// ───────────────────────────────────── A walk's fn ──────────────────────────────────────
/// The C type of a local, arg or reg of walk `w`. A captured one is a pointer to a cell of
/// this type
pub fn slot_rep(e: &mut Emitter, w: usize, slot: Operand) -> Rep {
let ty = e.cache.memory.get(e.cache.addr(w, slot, e.fns)).clone();
e.layout.rep(&ty)
}
/// Every local, arg and reg of a fn
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()
}
pub struct FnEmitter<'e, 'a> {
pub e: &'e mut Emitter<'a>,
pub w: usize,
pub f: &'a RegFn,
pub captured: Vec<Operand>,
pub reps: HashMap<Operand, Rep>, // the C type of each local, arg and reg
pub types: HashMap<Operand, Type>, // the type of each, in memory
pub ret: Rep,
pub code: Code,
}
impl<'a> Writer<'a> for FnEmitter<'_, 'a> {
fn code(&mut self) -> &mut Code {
&mut self.code
}
fn emitter(&mut self) -> &mut Emitter<'a> {
self.e
}
}
/// Print walk `w` as the C fn `w<w>`
pub fn emit_walk(e: &mut Emitter, w: usize) {
let (fns, cache) = (e.fns, e.cache);
let walk = &cache.walks[w];
let f = &fns[walk.req.fn_idx];
let mut reps = HashMap::new();
let mut types = HashMap::new();
for slot in slots(f) {
reps.insert(slot, slot_rep(e, w, slot));
types.insert(slot, cache.memory.get(cache.addr(w, slot, fns)).clone());
}
let ret = e.layout.rep(&walk.ret);
let code = Code {
depth: 1,
..Code::default()
};
let captured = f.captured_slots();
let mut fe = FnEmitter {
e,
w,
f,
captured,
reps,
types,
ret,
code,
};
// 1. The signature, with one param for each arg
let signature = fe.signature();
let _ = writeln!(fe.e.protos, "{signature};");
// 2. The locals, args and regs, and their cells
fe.declare_slots();
// 3. The blocks that can run
let reachable = &cache.walks[w].reachable;
for (i, block) in f.blocks.iter().enumerate() {
if reachable[i] {
fe.indent(-1);
fe.line(format!("b{i}:;"));
fe.indent(1);
fe.block(block, reachable);
}
}
let body = std::mem::take(&mut fe.code.out);
let _ = write!(fe.e.walk_defs, "{signature} {{\n{body}}}\n\n");
}
impl<'a> FnEmitter<'_, 'a> {
/// The C name of a local, arg or reg. A captured 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}"),
}
}
pub fn is_captured(&self, slot: Operand) -> bool {
self.captured.contains(&slot)
}
/// The value of a local, arg or reg, and its C type
pub fn read(&self, slot: Operand) -> (String, Rep) {
let name = self.name(slot);
match self.is_captured(slot) {
true => (format!("(*{name})"), self.reps[&slot]),
false => (name, self.reps[&slot]),
}
}
/// Write `expr`, a value of C type `from`, into a local, arg or reg
pub fn assign(&mut self, dst: Operand, expr: &str, from: Rep) {
let value = self.e.convert(expr, from, self.reps[&dst]);
let star = if self.is_captured(dst) { "*" } else { "" };
self.line(format!("{star}{} = {value};", self.name(dst)));
}
/// Write a value of one member into a local, arg or reg
pub fn set(&mut self, dst: Operand, expr: &str, member: Member) {
self.assign(dst, expr, Rep::One(member));
}
/// `static <ret> w<w>(<params>)`. A captured arg's param is its value, which goes into its
/// cell
fn signature(&self) -> String {
let mut params = vec![];
for k in 0..self.f.arg_count {
let slot = Operand::Arg(k);
let ctype = self.e.ctype(self.reps[&slot]);
let name = if self.is_captured(slot) {
format!("p{k}")
} else {
self.name(slot)
};
params.push(format!("{ctype} {name}"));
}
let params = if params.is_empty() {
"void".into()
} else {
params.join(", ")
};
format!("static {} w{}({params})", self.e.ctype(self.ret), self.w)
}
/// A C local for each local and reg, and a cell for each captured local and arg. A module
/// binding's cell is also a C global (see `Emitter::binding`)
fn declare_slots(&mut self) {
for slot in slots(self.f) {
let ctype = self.e.ctype(self.reps[&slot]);
let name = self.name(slot);
let cell = match slot {
Operand::Local(k) if self.e.is_global(self.w, slot) => {
format!("g{k} = __rt_alloc(sizeof({ctype}))")
}
_ => format!("__rt_alloc(sizeof({ctype}))"),
};
match (slot, self.is_captured(slot)) {
(Operand::Arg(k), true) => {
self.line(format!("{ctype} *{name} = {cell};"));
self.line(format!("*{name} = p{k};"));
}
(Operand::Arg(_), false) => {}
(_, true) => self.line(format!("{ctype} *{name} = {cell};")),
(_, false) => self.line(format!("{ctype} {name} = {{0}};")),
}
}
}
/// A block: its instrs, its postfix copies, then its jump
fn block(&mut self, block: &BasicBlock, reachable: &[bool]) {
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 };
for idx in block.start..end {
if !self.instr(idx) {
return;
}
}
for copy in &block.postfix {
let (value, rep) = self.read(copy.args[0]);
self.assign(copy.dsts[0], &value, rep);
}
// succ[0] is the jump target, succ[1] the fall-through
let can_go = |i: usize| block.succ.get(i).filter(|&&s| reachable[s]).copied();
match last.op {
IfFalse | IfFalse8 | IfTrue | IfTrue8 => match (can_go(0), can_go(1)) {
(Some(target), Some(next)) => {
let cond = self.truthy(last.args[0]);
let test = if matches!(last.op, IfTrue | IfTrue8) {
cond
} else {
format!("!{cond}")
};
self.line(format!("if ({test}) goto b{target};"));
self.line(format!("goto b{next};"));
}
(Some(only), None) | (None, Some(only)) => self.line(format!("goto b{only};")),
(None, None) => self.line("abort(); // not reached"),
},
Return | ReturnAsync | ReturnUndef => {}
_ => {
if let Some(next) = can_go(0) {
self.line(format!("goto b{next};"));
}
}
}
}
/// Whether a local, arg or reg is truthy, in a new C local
fn truthy(&mut self, slot: Operand) -> String {
let cond = self.temp(Rep::One(Member::Bool));
let (value, rep) = self.read(slot);
self.split(&[(value, rep)], &mut |fe, ms| {
let truthy = fe.e.truthy(&ms[0].0, ms[0].1);
fe.line(format!("{cond} = {truthy};"));
});
cond
}
/// Print instr `idx`. Gives false if no value reaches it: then it cannot run, and nor can
/// the rest of its block
fn instr(&mut self, idx: usize) -> bool {
let instr = &self.f.code[idx];
if matches!(instr.op, OpCode::Nop) {
return true;
}
// The first operand of `define_field_ref` is a location, not a value
let values = if instr.op == OpCode::DefineFieldRef {
&instr.args[1..]
} else {
&instr.args[..]
};
if values.iter().any(|slot| self.types[slot] == Type::Unknown) {
self.line("abort(); // not reached");
return false;
}
self.emit_instr(idx, instr);
true
}
}
+579
View File
@@ -0,0 +1,579 @@
//! The C of each instr: literals, operators, tests, moves, calls and returns. Objects are in
//! `objects`
//!
//! A rule takes its operands' members and gives the C of its result, and the member of that
//! result. Operands that an instr pops come top first (see regify), so a binary operator
//! has its right side in `args[0]`
use super::{
func::{FnEmitter, Writer},
layout::{ClassKind, Member, Rep},
undefined, unit_literal,
};
use crate::{
infer::{InstrRef, ObjAddr, Type},
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
native::NATIVES,
regify::{Operand, RegInstr},
transform::{FIRST_ARG, THIS_ARG},
};
/// What a conversion of an object to a string or a number throws: it would call the object's
/// methods, which is not supported
const NO_CONVERSION: &str = "converting an object to a string or a number is not supported";
/// A JS base type: the kinds of value that `===` tells apart
fn base(member: Member) -> u8 {
match member {
Member::Undefined => 0,
Member::Null => 1,
Member::Bool => 2,
Member::Int | Member::Float => 3,
Member::Str => 4,
Member::Obj(_) | Member::Native(_) => 5,
}
}
impl FnEmitter<'_, '_> {
pub(super) fn emit_instr(&mut self, idx: usize, instr: &RegInstr) {
use OpCode::*;
match instr.op {
PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8
| PushI16 | PushI32 | PushTrue | PushFalse | PushAtomValue | PushEmptyString
| PushConst | PushConst8 | Undefined | Null => {
let (expr, member) = self.literal(instr);
self.set(instr.dsts[0], &expr, member);
}
// Operators
Add | Sub | Mul | Div | Mod | Pow => self.binary(instr),
And | Or | Xor | Shl | Sar | Shr => self.bitwise(instr),
Neg | Plus | Inc | Dec | Not => self.unary(instr),
PostInc | PostDec => self.post(instr),
Lt | Lte | Gt | Gte => self.compare(instr),
StrictEq | StrictNeq | Eq | Neq => self.equality(instr),
// Tests
Lnot | Typeof | IsUndefined | TypeofIsUndefined | IsNull | IsUndefinedOrNull
| TypeofIsFunction => self.test(instr),
// Moves. A key stays as it is: a number is an index
SetLoc | SetName | ToPropkey => {
let (value, rep) = self.read(instr.args[0]);
self.assign(instr.dsts[0], &value, rep);
}
// Objects (see `objects`)
Object | Fclosure => {
let class = self.class_here(idx);
let alloc = format!("__rt_alloc(sizeof(struct c{class}))");
self.set(instr.dsts[0], &alloc, Member::Obj(class));
}
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),
CloseLoc => self.close_loc(instr),
// Calls and returns
Call | CallMethod | CallConstructor => self.call(idx, instr),
Return | ReturnAsync => {
let value = self.read(instr.args[0]);
self.ret(value);
}
ReturnUndef => self.ret(undefined()),
_ => unimplemented!("emit {instr:?}"),
}
}
/// The class of the objects that instr `idx` makes in this walk
pub(super) fn class_here(&self, idx: usize) -> usize {
let req = &self.e.cache.walks[self.w].req;
let made_at = InstrRef {
fn_idx: req.fn_idx,
instr_idx: idx,
};
self.e.layout.class(ObjAddr {
made_at,
in_call: req.in_call,
})
}
/// A new location for a binding that a closure shares (see transform/closure.rs). Any
/// other binding has one location
fn close_loc(&mut self, instr: &RegInstr) {
let ImmOperand::Loc(k) = instr.imm else {
unreachable!()
};
let slot = Operand::Local(k as usize);
if self.is_captured(slot) {
let (ctype, name) = (self.e.ctype(self.reps[&slot]), self.name(slot));
self.line(format!(
"{{ {ctype} *cell = __rt_alloc(sizeof({ctype})); *cell = *{name}; {name} = cell; }}"
));
}
}
/// Return a value, as the fn's return type
fn ret(&mut self, (value, rep): (String, Rep)) {
let value = self.e.convert(&value, rep, self.ret);
self.line(format!("return {value};"));
}
// ───────────────────────────────────── Literals ─────────────────────────────────────
/// A literal, and its member
fn literal(&mut self, instr: &RegInstr) -> (String, Member) {
use OpCode::*;
match (instr.op, instr.imm) {
(PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7, _) => {
let n = instr.op as i64 - Push0 as i64;
(n.to_string(), Member::Int)
}
(_, ImmOperand::I8(n)) => (int(n as i64), Member::Int),
(_, ImmOperand::I16(n)) => (int(n as i64), Member::Int),
(_, ImmOperand::I32(n)) => (int(n as i64), Member::Int),
(PushTrue, _) => ("true".into(), Member::Bool),
(PushFalse, _) => ("false".into(), Member::Bool),
(PushAtomValue, _) => (
self.e.string(&self.e.atom_text(instr.imm.atom())),
Member::Str,
),
(PushEmptyString, _) => (self.e.string(""), Member::Str),
(Undefined, _) => (unit_literal(Member::Undefined).into(), Member::Undefined),
(Null, _) => (unit_literal(Member::Null).into(), Member::Null),
_ => self.constant(instr),
}
}
/// A constant from the fn's constant pool, and its member
fn constant(&mut self, instr: &RegInstr) -> (String, Member) {
match &self.f.cpool[instr.imm.const_idx()] {
JsValue::Int(n) => (int(*n as i64), Member::Int),
JsValue::Float(x) if x.is_nan() => ("NAN".into(), Member::Float),
JsValue::Float(x) if x.is_infinite() => (
if *x < 0.0 { "(-INFINITY)" } else { "INFINITY" }.into(),
Member::Float,
),
JsValue::Float(x) => (format!("{x:?}"), Member::Float),
JsValue::String(s) => (self.e.string(&s.text()), Member::Str),
JsValue::BoolTrue => ("true".into(), Member::Bool),
JsValue::BoolFalse => ("false".into(), Member::Bool),
JsValue::Undefined => (unit_literal(Member::Undefined).into(), Member::Undefined),
JsValue::NativeFn(idx) => (
unit_literal(Member::Native(*idx)).into(),
Member::Native(*idx),
),
c => unimplemented!("the constant {c:?}"),
}
}
// ──────────────────────────────────── Operators ─────────────────────────────────────
/// `l op r`. `+` concatenates if either side is a string or an object. Anything else
/// converts both sides to numbers, which are doubles
fn binary(&mut self, instr: &RegInstr) {
use OpCode::*;
let (dst, op) = (instr.dsts[0], instr.op);
let ops = [self.read(instr.args[1]), self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let ((l, lm), (r, rm)) = (&ms[0], &ms[1]);
let text = |m: Member| matches!(m, Member::Str | Member::Obj(_) | Member::Native(_));
if op == Add && (text(*lm) || text(*rm)) {
let (Some(ls), Some(rs)) = (fe.e.to_string(l, *lm), fe.e.to_string(r, *rm)) else {
return fe.throw(NO_CONVERSION);
};
return fe.set(dst, &format!("__rt_string_concat({ls}, {rs})"), Member::Str);
}
let (Some(ln), Some(rn)) = (fe.e.to_number(l, *lm), fe.e.to_number(r, *rm)) else {
return fe.throw(NO_CONVERSION);
};
// Numbers are doubles: C's operators on doubles are JS's, `%` is C's `fmod`, and
// `**` differs only for NaN and infinite exponents (see `__rt_pow`)
let expr = match op {
Add | Sub | Mul => format!("({ln} {} {rn})", c_op(op)),
Mod => format!("fmod({ln}, {rn})"),
Pow => format!("__rt_pow({ln}, {rn})"),
_ => format!("({ln} / {rn})"),
};
fe.set(dst, &expr, Member::Float);
});
}
/// `&`, `|`, `^`, `<<`, `>>` and `>>>`. Each operand becomes an int32, as JS's ToInt32
/// does, and a shift counts its low 5 bits only. `>>>` gives a uint32, which is a float
fn bitwise(&mut self, instr: &RegInstr) {
use OpCode::*;
let (dst, op) = (instr.dsts[0], instr.op);
let ops = [self.read(instr.args[1]), self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let ((l, lm), (r, rm)) = (&ms[0], &ms[1]);
let (Some(a), Some(b)) = (fe.e.to_int32(l, *lm), fe.e.to_int32(r, *rm)) else {
return fe.throw(NO_CONVERSION);
};
let (expr, member) = match op {
And => (format!("({a} & {b})"), Member::Int),
Or => (format!("({a} | {b})"), Member::Int),
Xor => (format!("({a} ^ {b})"), Member::Int),
Shl => (
format!("(int32_t)((uint32_t){a} << ({b} & 31))"),
Member::Int,
),
Sar => (format!("({a} >> ({b} & 31))"), Member::Int),
_ => (
format!("(double)((uint32_t){a} >> ({b} & 31))"),
Member::Float,
),
};
fe.set(dst, &expr, member);
});
}
/// `-v`, `+v`, `v + 1`, `v - 1` and `~v`, on the value as a number. Only `+v` keeps an int
/// an int: `-0` is a float, and so is anything that can leave the int32 range. `~` gives
/// an int32
fn unary(&mut self, instr: &RegInstr) {
use OpCode::*;
let (dst, op) = (instr.dsts[0], instr.op);
let ops = [self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let (v, m) = &ms[0];
let Some(n) = fe.e.to_number(v, *m) else {
return fe.throw(NO_CONVERSION);
};
let (expr, member) = match (op, *m) {
(Not, _) => (format!("(~{})", fe.e.to_int32(v, *m).unwrap()), Member::Int),
(Plus, Member::Int) => (v.clone(), Member::Int),
(Plus, _) => (n, Member::Float),
(Neg, _) => (format!("(-{n})"), Member::Float),
(Inc, _) => (format!("({n} + 1)"), Member::Float),
_ => (format!("({n} - 1)"), Member::Float),
};
fe.set(dst, &expr, member);
});
}
/// `v++` and `v--`: the old value, as a number, then the new one
fn post(&mut self, instr: &RegInstr) {
let (old, new) = (instr.dsts[0], instr.dsts[1]);
let step = if instr.op == OpCode::PostInc {
"+"
} else {
"-"
};
let ops = [self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let (v, m) = &ms[0];
let Some(n) = fe.e.to_number(v, *m) else {
return fe.throw(NO_CONVERSION);
};
fe.set(old, &n, Member::Float);
fe.set(new, &format!("({n} {step} 1)"), Member::Float);
});
}
/// `<`, `<=`, `>` and `>=`. Two strings compare as strings, anything else as numbers
fn compare(&mut self, instr: &RegInstr) {
let (dst, op) = (instr.dsts[0], c_op(instr.op));
let ops = [self.read(instr.args[1]), self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let ((l, lm), (r, rm)) = (&ms[0], &ms[1]);
if (lm, rm) == (&Member::Str, &Member::Str) {
return fe.set(
dst,
&format!("(__rt_string_cmp({l}, {r}) {op} 0)"),
Member::Bool,
);
}
let (Some(ln), Some(rn)) = (fe.e.to_number(l, *lm), fe.e.to_number(r, *rm)) else {
return fe.throw(NO_CONVERSION);
};
fe.set(dst, &format!("({ln} {op} {rn})"), Member::Bool);
});
}
/// `===`, `!==`, `==` and `!=`
fn equality(&mut self, instr: &RegInstr) {
use OpCode::*;
let (dst, op) = (instr.dsts[0], instr.op);
let ops = [self.read(instr.args[1]), self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let ((l, lm), (r, rm)) = (&ms[0], &ms[1]);
let nullish = |m: Member| matches!(m, Member::Undefined | Member::Null);
let eq = match op {
// Undefined and null are equal to each other and to nothing else. Two values of
// one kind compare as `===` does. A string or a bool against a number compares
// as numbers
Eq | Neq => match (*lm, *rm) {
_ if nullish(*lm) && nullish(*rm) => "true".into(),
_ if nullish(*lm) || nullish(*rm) => "false".into(),
_ if base(*lm) == base(*rm) => strict_eq(l, *lm, r, *rm),
_ => match (fe.e.to_number(l, *lm), fe.e.to_number(r, *rm)) {
(Some(ln), Some(rn)) => format!("({ln} == {rn})"),
_ => return fe.throw(NO_CONVERSION),
},
},
_ => strict_eq(l, *lm, r, *rm),
};
let expr = if matches!(op, Neq | StrictNeq) {
format!("(!{eq})")
} else {
eq
};
fe.set(dst, &expr, Member::Bool);
});
}
/// The tests of one value: `!`, `typeof`, and whether it is undefined, null or a fn
fn test(&mut self, instr: &RegInstr) {
use OpCode::*;
let (dst, op) = (instr.dsts[0], instr.op);
let ops = [self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let (v, m) = (&ms[0].0, ms[0].1);
let known = |b: bool| (b.to_string(), Member::Bool);
let (expr, member) = match op {
Lnot => (format!("(!{})", fe.e.truthy(v, m)), Member::Bool),
Typeof => {
let name = match m {
Member::Undefined => "undefined",
Member::Bool => "boolean",
Member::Int | Member::Float => "number",
Member::Str => "string",
_ if fe.e.is_function(m) => "function",
_ => "object",
};
(fe.e.string(name), Member::Str)
}
IsUndefined | TypeofIsUndefined => known(m == Member::Undefined),
IsNull => known(m == Member::Null),
IsUndefinedOrNull => known(matches!(m, Member::Undefined | Member::Null)),
_ => known(fe.e.is_function(m)),
};
fe.set(dst, &expr, member);
});
}
// ────────────────────────────────────── Calls ───────────────────────────────────────
/// A call. Its operands are the callee's params: the callee (its env), `this`, then the
/// args (see regify). Each closure that the callee can be has its own walk (see
/// `TypeCache::find`), so a callee that is a sum is split. A callee that is not a fn
/// throws
///
/// `new` gives what the constructor returns if that is an object, else the new object
/// itself, `this`
fn call(&mut self, idx: usize, instr: &RegInstr) {
let (cache, fns) = (self.e.cache, self.e.fns);
let in_call = Some(InstrRef {
fn_idx: cache.walks[self.w].req.fn_idx,
instr_idx: idx,
});
let arg_types: Vec<Type> = instr
.args
.iter()
.map(|slot| self.types[slot].clone())
.collect();
let dst = instr.dsts[0];
let ops = [self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let (callee, member) = (&ms[0].0, ms[0].1);
if let Member::Native(idx) = member {
fe.native_call(idx, instr);
return;
}
let (Member::Obj(class), ClassKind::Closure(fn_idx)) = (member, fe.class_kind(member))
else {
fe.throw("not a function");
return;
};
let mut types = arg_types.clone();
types[0] = Type::Ref(fe.e.layout.classes[class].objs[0]);
let w = cache.find(fn_idx, in_call, &types, fns);
fe.e.want(w);
// An arg it does not pass is undefined
let args: Vec<String> = (0..fns[fn_idx].arg_count)
.map(|i| {
let param = super::func::slot_rep(fe.e, w, Operand::Arg(i));
let (value, rep) = match i {
0 => (callee.clone(), Rep::One(member)),
_ if i < instr.args.len() => fe.read(instr.args[i]),
_ => undefined(),
};
fe.e.convert(&value, rep, param)
})
.collect();
let call = format!("w{w}({})", args.join(", "));
// A callee that never returns gives no value
if cache.walks[w].ret == Type::Unknown {
fe.line(format!("{call};"));
fe.line("abort(); // does not return");
return;
}
let ret = fe.e.layout.rep(&cache.walks[w].ret);
if instr.op != OpCode::CallConstructor {
fe.assign(dst, &call, ret);
return;
}
let result = fe.temp(ret);
fe.line(format!("{result} = {call};"));
let this = fe.read(instr.args[THIS_ARG as usize]);
fe.split(&[(result, ret)], &mut |fe, ms| match ms[0].1 {
Member::Obj(_) => fe.set(dst, &ms[0].0, ms[0].1),
_ => fe.assign(dst, &this.0, this.1),
});
});
}
/// A call of a native: a C call, with the args only. An int and a float both pass for a
/// number. Any other arg of the wrong kind throws (see `native::ret_type`)
fn native_call(&mut self, idx: usize, instr: &RegInstr) {
let native = &NATIVES[idx];
let dst = instr.dsts[0];
let mut ops = vec![];
// An arg that the call does not pass is undefined
for i in 0..native.arg_types.len() {
let slot = FIRST_ARG as usize + i;
match instr.args.get(slot) {
Some(&arg) => ops.push(self.read(arg)),
None => ops.push(undefined()),
}
}
self.split(&ops, &mut |fe, ms| {
let mut args = vec![];
for ((value, member), param) in ms.iter().zip(native.arg_types) {
match native_arg(value, *member, param) {
Some(arg) => args.push(arg),
None => return fe.throw("a native got an arg of the wrong kind"),
}
}
let call = format!("{}({})", native.name, args.join(", "));
fe.set(dst, &call, native_member(&native.ret_type));
});
}
/// The kind of class of an object member
pub(super) fn class_kind(&self, member: Member) -> ClassKind {
match member {
Member::Obj(class) => self.e.layout.classes[class].kind,
_ => ClassKind::Obj,
}
}
}
/// The C literal of an int
fn int(n: i64) -> String {
if n == i32::MIN as i64 {
"INT32_MIN".into()
} else {
n.to_string()
}
}
/// An arg of `member` as the native param `param`, if it can pass
fn native_arg(value: &str, member: Member, param: &Type) -> Option<String> {
match (member, param) {
(Member::Int, Type::Float) => Some(format!("(double){value}")),
(Member::Float, Type::Int) => Some(format!("__rt_to_int32({value})")),
(Member::Int, Type::Int) | (Member::Float, Type::Float) => Some(value.into()),
(Member::Str, Type::Str(_)) | (Member::Undefined, Type::Undefined) => Some(value.into()),
_ => None,
}
}
/// The member of a native's return type
fn native_member(ty: &Type) -> Member {
match ty {
Type::Undefined => Member::Undefined,
Type::Int => Member::Int,
Type::Float => Member::Float,
Type::Str(_) => Member::Str,
ty => unreachable!("a native returns {ty:?}"),
}
}
/// `a === b`, for one member of each. Different kinds are never equal. Numbers compare as
/// numbers, strings by their text, and objects by identity
fn strict_eq(l: &str, lm: Member, r: &str, rm: Member) -> String {
match (lm, rm) {
_ if base(lm) != base(rm) => "false".into(),
(Member::Undefined | Member::Null, _) => "true".into(),
(Member::Str, _) => format!("__rt_string_eq({l}, {r})"),
(Member::Obj(_), Member::Obj(_)) => format!("((void *){l} == (void *){r})"),
(Member::Native(a), Member::Native(b)) => (a == b).to_string(),
(Member::Obj(_) | Member::Native(_), _) => "false".into(),
_ => format!("({l} == {r})"),
}
}
/// The C operator of a JS operator
fn c_op(op: OpCode) -> &'static str {
use OpCode::*;
match op {
Add => "+",
Sub => "-",
Mul => "*",
Lt => "<",
Lte => "<=",
Gt => ">",
Gte => ">=",
_ => unreachable!(),
}
}
+284
View File
@@ -0,0 +1,284 @@
//! Layouts: what each type is in C
//!
//! Each member of a type becomes one C type (a `Member`). A type with one member is that C
//! type, and a type with several is a tagged union of them (a sum). Objects are grouped into
//! layout classes, one C struct each: two objects share a 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 by refinement: start from the field names, then split
//! every class whose objects disagree, until nothing splits
//!
//! A closure is a class of its own. A call picks its C function 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, in C
#[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, by its index in `native::NATIVES`: known from the type, so no data
}
/// A type, in C: one member, or a sum of several
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Rep {
One(Member),
Sum(usize), // an index into `Layout::sums`
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClassKind {
Obj,
Array,
Closure(usize), // it runs this fn
}
/// A field of the objects of a class
#[derive(Debug, Clone)]
pub struct ClassField {
pub name: AtomRef,
pub rep: Rep,
pub shared: bool, // it shares another location: the struct holds a pointer to it
pub defined: bool, // every object has it, so it needs no `has_` flag
}
#[derive(Debug)]
pub struct Class {
pub kind: ClassKind,
pub objs: Vec<ObjAddr>,
pub fields: Vec<ClassField>, // sorted by name
pub elems: Rep, // for an array: its elements. Else unused
}
#[derive(Debug)]
pub struct Layout {
pub classes: Vec<Class>,
class_of: HashMap<ObjAddr, usize>,
pub sums: Vec<Vec<Member>>, // the members of each sum, sorted. A member's tag is its index
sum_ids: HashMap<Vec<Member>, usize>,
}
impl Layout {
pub fn new(memory: &Memory) -> Self {
let class_of = refine(memory);
let mut layout = Layout {
classes: vec![],
class_of,
sums: vec![],
sum_ids: HashMap::new(),
};
let mut objs: Vec<ObjAddr> = memory.objects().map(|(obj, _)| obj).collect();
objs.sort_unstable();
let count = layout.class_of.values().max().map_or(0, |&n| n + 1);
let mut members: Vec<Vec<ObjAddr>> = vec![vec![]; count];
for obj in objs {
members[layout.class_of[&obj]].push(obj);
}
for objs in members {
let class = layout.make_class(memory, objs);
layout.classes.push(class);
}
layout
}
/// The class of the objects in `objs`, which refinement put together
fn make_class(&mut self, memory: &Memory, objs: Vec<ObjAddr>) -> Class {
let first = objs[0];
let kind = match memory.get(Addr::Object(first)) {
Type::Shape(Shape::Array(_)) => ClassKind::Array,
Type::Closure { fn_idx, .. } => ClassKind::Closure(*fn_idx),
_ => ClassKind::Obj,
};
// The type of each field is the join over the class, so the C type holds all
let fields = match kind {
ClassKind::Array => vec![],
_ => (memory.fields(first).iter())
.map(|&(name, field)| {
let types = objs.iter().map(|&obj| {
let field = memory.field(obj, name).unwrap();
memory.get(field.addr).clone()
});
let ty = types.fold(Type::Unknown, |all, ty| join(&all, &ty));
ClassField {
name,
rep: self.rep(&ty),
shared: field.addr != Addr::Field(first, name),
defined: field.defined,
}
})
.collect(),
};
let elems = match kind {
ClassKind::Array => {
let ty = objs
.iter()
.fold(Type::Unknown, |all, &obj| join(&all, memory.elems(obj)));
self.rep(&ty)
}
_ => Rep::One(Member::Undefined),
};
Class {
kind,
objs,
fields,
elems,
}
}
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 C 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)
}
/// What a value of type `ty` is in C
pub fn rep(&mut self, ty: &Type) -> Rep {
let members = self.members(ty);
if let [member] = members[..] {
return Rep::One(member);
}
let next = self.sums.len();
let id = *self.sum_ids.entry(members.clone()).or_insert(next);
if id == next {
self.sums.push(members);
}
Rep::Sum(id)
}
/// The members of a value of C type `rep`
pub fn rep_members(&self, rep: Rep) -> Vec<Member> {
match rep {
Rep::One(member) => vec![member],
Rep::Sum(id) => self.sums[id].clone(),
}
}
}
/// 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 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 members,
/// 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);
// A field's members, or an array's elements, with the classes so far
let key = |obj: ObjAddr| -> (usize, Vec<Vec<Member>>) {
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| members_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()
}
+515
View File
@@ -0,0 +1,515 @@
//! Emits the program as one C file
//!
//! Emit plans first, then prints. The plan is the layout: the C type of every type, and a
//! struct for each class of objects (see `layout`). Then each walk that the program can
//! call becomes one C function, which `func` prints from the plan
//!
//! Every instr is one rule, on the members of its operands' types: an operand of one member
//! is used as it is, and an operand that is a sum is split by a `switch` on its tag, with the
//! rule's C in each case. The result is written into the destination, which converts it:
//! into a sum, that is a write of the tag and of the union's member
//!
//! A value that no write reached (a straggler `Unknown`) is undefined
mod func;
mod instr;
mod layout;
mod objects;
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::fmt::Write;
use crate::{
flatten::ModuleBindings,
infer::{Type, TypeCache, module_binding},
libqjs::parse::{Atom, AtomRef},
regify::{Operand, RegFn},
transform::PROTOTYPE_FIELD,
};
use layout::{ClassField, ClassKind, Layout, Member, Rep};
/// The C of the program, which includes the runtime's `jsrt.h`
pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom], bindings: &ModuleBindings) -> String {
let mut e = Emitter::new(fns, cache, atoms, bindings);
// 1. The host's two calls (see lib/rt/jsrt.h): the entry, when the program starts, and the
// stdlib's `__complete`, when an IO op completes
let start = e.host_start();
let complete = e.host_complete();
// 2. Every fn that a call can reach, and the read fns that they use. Printing a fn can
// want more of both
loop {
if let Some(w) = e.walk_queue.pop_front() {
func::emit_walk(&mut e, w);
} else if let Some((class, name)) = e.read_queue.pop_front() {
objects::emit_read(&mut e, class, name);
} else {
break;
}
}
// 3. The file
e.finish(&start, &complete)
}
pub struct Emitter<'a> {
fns: &'a [RegFn],
cache: &'a TypeCache,
atoms: &'a [Atom],
bindings: &'a ModuleBindings,
layout: Layout,
entry: usize, // the entry's walk
globals: BTreeMap<usize, Rep>, // the module bindings, which are C globals: each local of the entry, and its C type
// What printing wants, and has not printed yet
walk_queue: VecDeque<usize>,
walks_wanted: HashSet<usize>,
read_queue: VecDeque<(usize, AtomRef)>,
reads: HashMap<(usize, AtomRef), (usize, Rep)>, // the read fn of a field on a class, and what it gives
strings: Vec<String>, // the text of each string literal
string_ids: HashMap<String, usize>,
convs: HashMap<(Rep, Rep), usize>, // the conversion fn from a sum, by its reps
// What is printed, in the order that the file needs it
protos: String,
conv_defs: String,
read_defs: String,
walk_defs: String,
}
impl<'a> Emitter<'a> {
fn new(
fns: &'a [RegFn],
cache: &'a TypeCache,
atoms: &'a [Atom],
bindings: &'a ModuleBindings,
) -> Self {
let entry_fn = fns.len() - 1;
let mut layout = Layout::new(&cache.memory);
// The module bindings are C globals, so that any fn can read the stdlib's
let mut globals = BTreeMap::new();
for &local in bindings.values() {
let ty = cache.memory.get(module_binding(entry_fn, local));
globals.insert(local, layout.rep(ty));
}
Emitter {
fns,
cache,
atoms,
bindings,
layout,
entry: cache.find(entry_fn, None, &[], fns),
globals,
walk_queue: VecDeque::new(),
walks_wanted: HashSet::new(),
read_queue: VecDeque::new(),
reads: HashMap::new(),
strings: vec![],
string_ids: HashMap::new(),
convs: HashMap::new(),
protos: String::new(),
conv_defs: String::new(),
read_defs: String::new(),
walk_defs: String::new(),
}
}
/// Print walk `w` as a C fn, unless it is printed or wanted already
fn want(&mut self, w: usize) {
if self.walks_wanted.insert(w) {
self.walk_queue.push_back(w);
}
}
}
// ────────────────────────────────────── Host calls ──────────────────────────────────────
impl Emitter<'_> {
/// The call of the entry, with no args: `env` and `this` are undefined
fn host_start(&mut self) -> String {
self.want(self.entry);
let arg_count = self.fns[self.fns.len() - 1].arg_count;
let entry = self.entry;
let args = self.args(entry, vec![undefined(); arg_count]);
format!("w{entry}({})", args.join(", "))
}
/// The call of the stdlib's `__complete` with an op's handle `h`, if the program has it
fn host_complete(&mut self) -> String {
let Some((complete, Rep::One(Member::Obj(class)))) = self.binding("__complete") else {
return String::new();
};
let ClassKind::Closure(fn_idx) = self.layout.classes[class].kind else {
return String::new();
};
let obj = self.layout.classes[class].objs[0];
let w = self.cache.find(
fn_idx,
None,
&[Type::Ref(obj), Type::Undefined, Type::Int],
self.fns,
);
self.want(w);
let args = vec![
(complete, Rep::One(Member::Obj(class))),
undefined(),
("h".into(), Rep::One(Member::Int)),
];
format!("w{w}({});", self.args(w, args).join(", "))
}
/// The args of a call of walk `w`: each value converted to its param's C type
fn args(&mut self, w: usize, values: Vec<(String, Rep)>) -> Vec<String> {
let mut args = vec![];
for (i, (value, rep)) in values.into_iter().enumerate() {
let param = func::slot_rep(self, w, Operand::Arg(i));
args.push(self.convert(&value, rep, param));
}
args
}
/// The value of the stdlib's module binding `name`, as C, and its C type
fn binding(&self, name: &str) -> Option<(String, Rep)> {
let local = *self.bindings.get(name)?;
Some((format!("(*g{local})"), self.globals[&local]))
}
/// Whether a local of walk `w` is a module binding, which is a C global
fn is_global(&self, w: usize, slot: Operand) -> bool {
matches!(slot, Operand::Local(local) if w == self.entry && self.globals.contains_key(&local))
}
/// The prototype of a primitive or an array, as C, and its C type: the `prototype` of the
/// stdlib's class (see `Memory::builtin_proto`)
fn builtin_proto(&self, member: Member) -> Option<(String, Rep)> {
let class_name = match member {
Member::Str => "String",
Member::Int | Member::Float => "Number",
Member::Bool => "Boolean",
Member::Obj(class) if self.layout.classes[class].kind == ClassKind::Array => "Array",
_ => return None,
};
let (class_value, Rep::One(Member::Obj(class))) = self.binding(class_name)? else {
return None;
};
let field = self.layout.field(class, PROTOTYPE_FIELD)?;
Some((self.field_access(&class_value, field), field.rep))
}
}
// ──────────────────────────────────────── Values ────────────────────────────────────────
impl Emitter<'_> {
/// A string literal: a pointer to a static string
fn string(&mut self, text: &str) -> String {
let next = self.strings.len();
let id = *self.string_ids.entry(text.to_string()).or_insert(next);
if id == next {
self.strings.push(text.to_string());
}
format!("(&str{id})")
}
fn atom_text(&self, atom: AtomRef) -> String {
atom.text(self.atoms)
}
fn ctype(&self, rep: Rep) -> String {
match rep {
Rep::One(member) => member_ctype(member),
Rep::Sum(id) => format!("s{id}"),
}
}
/// The C name of a field
fn field_name(&self, atom: AtomRef) -> String {
let text: String = (self.atom_text(atom).chars().take(24))
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
match atom {
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`, to read or write. A field that shares a location holds a pointer to it
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}"),
}
}
/// `expr`, a value of `member`, as a number (a `double`), as JS's ToNumber does. `None`
/// for an object: its conversion calls its methods, which is not supported
fn to_number(&self, expr: &str, member: Member) -> Option<String> {
Some(match member {
Member::Undefined => "NAN".into(),
Member::Null => "0.0".into(),
Member::Bool | Member::Int => format!("(double){expr}"),
Member::Float => expr.into(),
Member::Str => format!("__rt_string_to_number({expr})"),
Member::Obj(_) | Member::Native(_) => return None,
})
}
/// `expr`, a value of `member`, as an int32, as JS's ToInt32 does. `None` for an object
fn to_int32(&self, expr: &str, member: Member) -> Option<String> {
match member {
Member::Int => Some(expr.into()),
_ => Some(format!("__rt_to_int32({})", self.to_number(expr, member)?)),
}
}
/// `expr`, a value of `member`, as a string, as JS's ToString does. `None` for an object:
/// its conversion calls its methods, which is not supported
fn to_string(&mut self, expr: &str, member: Member) -> Option<String> {
Some(match member {
Member::Undefined => self.string("undefined"),
Member::Null => self.string("null"),
Member::Bool => format!(
"({expr} ? {} : {})",
self.string("true"),
self.string("false")
),
Member::Int => format!("__rt_number_to_string((double){expr})"),
Member::Float => format!("__rt_number_to_string({expr})"),
Member::Str => expr.into(),
Member::Obj(_) | Member::Native(_) => return None,
})
}
/// Whether `expr`, a value of `member`, is truthy
fn truthy(&self, expr: &str, member: Member) -> String {
match member {
Member::Undefined | Member::Null => "false".into(),
Member::Bool => expr.into(),
Member::Int => format!("({expr} != 0)"),
Member::Float => format!("({expr} == {expr} && {expr} != 0)"),
Member::Str => format!("(__rt_string_length({expr}) != 0)"),
Member::Obj(_) | Member::Native(_) => "true".into(),
}
}
/// Whether a value of `member` is a fn
fn is_function(&self, member: Member) -> bool {
match member {
Member::Obj(class) => matches!(self.layout.classes[class].kind, ClassKind::Closure(_)),
Member::Native(_) => true,
_ => false,
}
}
}
// ───────────────────────────────────── Conversions ──────────────────────────────────────
impl Emitter<'_> {
/// `expr`, a value of C type `from`, as a value of C type `to`. Types only grow, so
/// `to` holds every member of `from`
fn convert(&mut self, expr: &str, from: Rep, to: Rep) -> String {
if from == to {
return expr.to_string();
}
match (from, to) {
(Rep::One(a), Rep::One(b)) => convert_member(expr, a, b),
(Rep::One(a), Rep::Sum(id)) => {
let (tag, b) = member_in(a, &self.layout.sums[id]);
format!(
"(s{id}){{.tag = {tag}, .as.m{tag} = {}}}",
convert_member(expr, a, b)
)
}
(Rep::Sum(_), _) => {
let next = self.convs.len();
let id = *self.convs.entry((from, to)).or_insert(next);
if id == next {
self.print_conv(id, from, to);
}
format!("cv{id}({expr})")
}
}
}
/// The fn that converts a sum `from` into `to`
fn print_conv(&mut self, id: usize, from: Rep, to: Rep) {
let (from_c, to_c) = (self.ctype(from), self.ctype(to));
let mut body = format!("static {to_c} cv{id}({from_c} v) {{\n switch (v.tag) {{\n");
for (tag, member) in self.layout.rep_members(from).into_iter().enumerate() {
let value = self.convert(&format!("v.as.m{tag}"), Rep::One(member), to);
let _ = writeln!(body, " case {tag}: return {value};");
}
body.push_str(" }\n abort();\n}\n\n");
let _ = writeln!(self.protos, "static {to_c} cv{id}({from_c} v);");
self.conv_defs.push_str(&body);
}
}
/// The undefined value, and its C type
fn undefined() -> (String, Rep) {
(
unit_literal(Member::Undefined).into(),
Rep::One(Member::Undefined),
)
}
fn member_ctype(member: Member) -> String {
match member {
Member::Undefined | Member::Native(_) => "undefined_t".into(),
Member::Null => "null_t".into(),
Member::Bool => "bool".into(),
Member::Int => "int32_t".into(),
Member::Float => "double".into(),
Member::Str => "string_t".into(),
Member::Obj(class) => format!("struct c{class} *"),
}
}
/// The literal of a member that has one value
fn unit_literal(member: Member) -> &'static str {
match member {
Member::Undefined | Member::Native(_) => "(undefined_t){0}",
Member::Null => "(null_t){0}",
_ => unreachable!(),
}
}
/// `expr` of member `a` as member `b`: the same, or an int as a float
fn convert_member(expr: &str, a: Member, b: Member) -> String {
match (a, b) {
_ if a == b => expr.into(),
(Member::Int, Member::Float) => format!("(double){expr}"),
_ => panic!("emit: a value of {a:?} where {b:?} was inferred"),
}
}
/// The tag and member of a sum that holds a value of member `a`: its own, or a float for
/// an int
fn member_in(a: Member, members: &[Member]) -> (usize, Member) {
let find = |m: Member| members.iter().position(|&x| x == m).map(|tag| (tag, m));
find(a)
.or_else(|| (a == Member::Int).then(|| find(Member::Float)).flatten())
.unwrap_or_else(|| panic!("emit: a value of {a:?} where {members:?} was inferred"))
}
// ─────────────────────────────────────── The file ───────────────────────────────────────
const HEADER: &str = "// Generated by jsrt\n#define JSRT_IMPLEMENTATION\n#include \"jsrt.h\"\n\n#include <math.h>\n#include <stdlib.h>\n\n";
impl Emitter<'_> {
/// The C file: the types, the statics, the fns, then the host's entry points
fn finish(self, start: &str, complete: &str) -> String {
let mut c = String::from(HEADER);
self.print_types(&mut c);
self.print_statics(&mut c);
c.push('\n');
c.push_str(&self.protos);
c.push('\n');
c.push_str(&self.conv_defs);
c.push_str(&self.read_defs);
c.push_str(&self.walk_defs);
// The host's entry points (see lib/rt/jsrt.h)
let _ = writeln!(c, "void jsrt_start(void) {{\n {start};\n}}\n");
let _ = write!(
c,
"void jsrt_complete(int32_t h) {{\n (void)h;\n {complete}\n}}\n"
);
c
}
/// The structs, declared first because a sum can point to one, then the sums, then the
/// structs' fields
fn print_types(&self, c: &mut String) {
for id in 0..self.layout.classes.len() {
let _ = writeln!(c, "struct c{id};");
}
c.push('\n');
for (id, members) in self.layout.sums.iter().enumerate() {
let mut fields = String::new();
for (tag, &member) in members.iter().enumerate() {
let _ = write!(fields, " {} m{tag};", member_ctype(member));
}
let _ = writeln!(
c,
"typedef struct {{ uint8_t tag; union {{{fields} }} as; }} s{id};"
);
}
c.push('\n');
for (id, class) in self.layout.classes.iter().enumerate() {
let _ = writeln!(c, "struct c{id} {{");
if class.kind == ClassKind::Array {
let _ = writeln!(
c,
" int32_t length, cap;\n {} *items;",
self.ctype(class.elems)
);
}
for field in &class.fields {
let (ctype, name) = (self.ctype(field.rep), self.field_name(field.name));
let pointer = if field.shared { "*" } else { "" };
let _ = writeln!(c, " {ctype} {pointer}{name};");
if !field.defined {
let _ = writeln!(c, " bool has_{name};");
}
}
// C has no empty structs
if class.kind != ClassKind::Array && class.fields.is_empty() {
c.push_str(" char _;\n");
}
c.push_str("};\n\n");
}
}
/// The string literals, whose UTF-16 units are in the C, and the module bindings
fn print_statics(&self, c: &mut String) {
for (id, text) in self.strings.iter().enumerate() {
let units: Vec<String> = text.encode_utf16().map(|u| u.to_string()).collect();
let _ = writeln!(
c,
"static struct jsrt_string str{id} = {{{}, {{{}}}}};",
units.len(),
units.join(", ")
);
}
for (local, rep) in &self.globals {
let _ = writeln!(c, "static {} *g{local};", self.ctype(*rep));
}
}
}
+397
View File
@@ -0,0 +1,397 @@
//! The C of objects: fields, elements, and the read fns that follow a prototype chain
//!
//! A field is a member of its class's struct. A field that every object has from the start
//! is read directly. Any other field can be absent: it has a `has_` flag, and a read of it
//! is a read fn, which reads its own field if it has one, else reads on its prototype. A
//! field that shares a location (see `define_field_ref`) holds a pointer to it
use std::fmt::Write as _;
use super::{
Emitter,
func::{Code, FnEmitter, Writer},
layout::{ClassKind, Member, Rep},
undefined,
};
use crate::{
infer::{LENGTH_FIELD, Type, join},
libqjs::parse::{AtomRef, opcodes::OpCode},
regify::{Operand, RegInstr},
transform::PROTO_FIELD,
};
// ──────────────────────────────────────── Fields ────────────────────────────────────────
impl FnEmitter<'_, '_> {
/// `[a, b, ...]`: the elements are the operands, last first
pub(super) fn array_from(&mut self, idx: usize, instr: &RegInstr) {
let class = self.class_here(idx);
let elems = self.e.layout.classes[class].elems;
let array = self.temp(Rep::One(Member::Obj(class)));
let n = instr.args.len();
self.line(format!("{array} = __rt_alloc(sizeof(struct c{class}));"));
self.line(format!(
"{array}->items = __rt_grow(NULL, &{array}->cap, {n}, sizeof *{array}->items);"
));
self.line(format!("{array}->length = {n};"));
for (i, &slot) in instr.args.iter().rev().enumerate() {
let (value, rep) = self.read(slot);
let value = self.e.convert(&value, rep, elems);
self.line(format!("{array}->items[{i}] = {value};"));
}
self.set(instr.dsts[0], &array, Member::Obj(class));
}
/// `o.f`, and `o.length`. A string's length is its own, and so is an array's. Undefined
/// and null throw. A primitive reads its kind's prototype (see `Emitter::builtin_proto`)
pub(super) fn get_field(&mut self, instr: &RegInstr) {
let name = match instr.op {
OpCode::GetLength => LENGTH_FIELD,
_ => instr.imm.atom(),
};
let dst = instr.dsts[0];
let ops = [self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let (obj, member) = (&ms[0].0, ms[0].1);
match (member, fe.class_kind(member)) {
(Member::Str, _) if name == LENGTH_FIELD => {
fe.set(dst, &format!("__rt_string_length({obj})"), Member::Int)
}
(Member::Obj(_), ClassKind::Array) if name == LENGTH_FIELD => {
fe.set(dst, &format!("{obj}->length"), Member::Int)
}
(Member::Obj(class), _) => {
let (value, rep) = read_field(fe.e, class, name, obj);
fe.assign(dst, &value, rep);
}
(Member::Undefined | Member::Null, _) => {
fe.throw("cannot read a field of undefined or null")
}
(Member::Native(_), _) => fe.throw("a native fn has no fields"),
_ => match fe.e.builtin_proto(member) {
Some((proto, Rep::One(Member::Obj(proto_class)))) => {
let (value, rep) = read_field(fe.e, proto_class, name, &proto);
fe.assign(dst, &value, rep);
}
_ => fe.throw("fields of primitives need the stdlib"),
},
}
});
}
/// `o.f = v`, and a field made in an object literal. A primitive has no fields to write,
/// and undefined and null throw. A write of an array's `length` sets its length
pub(super) fn put_field(&mut self, instr: &RegInstr) {
let name = instr.imm.atom();
let value = self.read(instr.args[0]);
let ops = [self.read(instr.args[1])];
self.split(&ops, &mut |fe, ms| {
let (obj, member) = (&ms[0].0, ms[0].1);
match (member, fe.class_kind(member)) {
(Member::Obj(_), ClassKind::Array) if name == LENGTH_FIELD => {
fe.set_length(obj, &value);
}
(Member::Obj(class), _) => {
let field = fe.e.layout.field(class, name).unwrap().clone();
let target = fe.e.field_access(obj, &field);
let v = fe.e.convert(&value.0, value.1, field.rep);
fe.line(format!("{target} = {v};"));
if !field.defined {
fe.line(format!("{obj}->has_{} = true;", fe.e.field_name(name)));
}
}
_ => fe.throw("cannot set a field of a primitive, undefined or null"),
}
});
}
/// `a.length = n`: the array gets `n` elements, the new ones holes. `n` must be an array
/// index: a whole number from 0, else it throws, as JS's RangeError
fn set_length(&mut self, obj: &str, value: &(String, Rep)) {
self.split(std::slice::from_ref(value), &mut |fe, ms| {
let (n, member) = (&ms[0].0, ms[0].1);
let length = match member {
Member::Int => n.clone(),
Member::Float => format!("(int32_t){n}"),
_ => return fe.throw("an array's length must be a number"),
};
let check = in_range(n, member, "INT32_MAX");
fe.line(format!("if (!({check})) __rt_type_error(\"an array's length must be a whole number from 0\");"));
fe.line(format!("{obj}->items = __rt_grow({obj}->items, &{obj}->cap, {length}, sizeof *{obj}->items);"));
fe.line(format!("{obj}->length = {length};"));
});
}
/// `define_field_ref`: the field becomes the location that regify gives as the first
/// operand. That is a captured binding, whose C local points to its cell, or the reg of
/// an object whose field of the same name points to one (see regify/walk.rs)
pub(super) fn share_field(&mut self, instr: &RegInstr) {
let name = instr.imm.atom();
let c_name = self.e.field_name(name);
let location = instr.args[0];
let ops = [self.read(instr.args[1])];
self.split(&ops, &mut |fe, ms| {
let obj = &ms[0].0;
match location {
Operand::Reg(_) => {
let holder = fe.read(location);
fe.split(&[holder], &mut |fe, ms| match ms[0].1 {
Member::Obj(_) => {
fe.line(format!("{obj}->{c_name} = {}->{c_name};", ms[0].0))
}
_ => unreachable!(),
});
}
slot => {
let cell = fe.name(slot);
fe.line(format!("{obj}->{c_name} = {cell};"));
}
}
});
}
/// `close_field x`: the field, which shares a location, gets a new cell with its value.
/// Closures that shared the old cell keep it (see transform/closure.rs)
pub(super) fn close_field(&mut self, instr: &RegInstr) {
let name = instr.imm.atom();
let ops = [self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let (obj, member) = (&ms[0].0, ms[0].1);
let Member::Obj(class) = member else { unreachable!("a capture is held by an object") };
let field = fe.e.layout.field(class, name).unwrap().clone();
assert!(field.shared, "close_field of a field that shares no location");
let (ctype, c_name) = (fe.e.ctype(field.rep), fe.e.field_name(name));
fe.line(format!("{{ {ctype} *cell = __rt_alloc(sizeof({ctype})); *cell = *{obj}->{c_name}; {obj}->{c_name} = cell; }}"));
});
}
}
// ─────────────────────────────────────── Elements ───────────────────────────────────────
impl FnEmitter<'_, '_> {
/// `a[i]` on an array, and `s[i]` on a string. An index that is not in it gives
/// undefined. Undefined and null throw
pub(super) fn get_elem(&mut self, instr: &RegInstr) {
let dst = instr.dsts[0];
let ops = [self.read(instr.args[1]), self.read(instr.args[0])];
self.split(&ops, &mut |fe, ms| {
let ((obj, om), (index, im)) = (&ms[0], &ms[1]);
let (length, item, rep) = match (*om, fe.class_kind(*om)) {
(Member::Obj(class), ClassKind::Array) => {
let elems = fe.e.layout.classes[class].elems;
(
format!("{obj}->length"),
format!("{obj}->items[{}]", as_index(index, *im)),
elems,
)
}
(Member::Str, _) => (
format!("__rt_string_length({obj})"),
format!("__rt_string_at({obj}, {})", as_index(index, *im)),
Rep::One(Member::Str),
),
(Member::Undefined | Member::Null, _) => {
fe.throw("cannot read an element of undefined or null");
return;
}
_ => unreachable!("infer reads elements only of arrays and strings"),
};
fe.line(format!("if ({}) {{", in_range(index, *im, &length)));
fe.indent(1);
fe.assign(dst, &item, rep);
fe.indent(-1);
fe.line("} else {");
fe.indent(1);
let (value, rep) = undefined();
fe.assign(dst, &value, rep);
fe.indent(-1);
fe.line("}");
});
}
/// `a[i] = v` on an array, which grows to hold `i`. A write to a string, or to undefined
/// or null, throws, and so does an index that is not an array index, which would be a
/// named field
pub(super) fn put_elem(&mut self, instr: &RegInstr) {
let value = self.read(instr.args[0]);
let ops = [self.read(instr.args[2]), self.read(instr.args[1])];
self.split(&ops, &mut |fe, ms| {
let ((obj, om), (index, im)) = (&ms[0], &ms[1]);
let (Member::Obj(class), ClassKind::Array) = (*om, fe.class_kind(*om)) else {
fe.throw("cannot set an element of a value that is not an array");
return;
};
let elems = fe.e.layout.classes[class].elems;
let i = as_index(index, *im);
let v = fe.e.convert(&value.0, value.1, elems);
fe.line(format!("if (!({})) __rt_type_error(\"an array index must be a whole number from 0\");", in_range(index, *im, "INT32_MAX")));
fe.line(format!("if ({i} >= {obj}->length) {{"));
fe.indent(1);
fe.line(format!("{obj}->items = __rt_grow({obj}->items, &{obj}->cap, {i} + 1, sizeof *{obj}->items);"));
fe.line(format!("{obj}->length = {i} + 1;"));
fe.indent(-1);
fe.line("}");
fe.line(format!("{obj}->items[{i}] = {v};"));
});
}
}
/// An index of member `member` as an `int32_t`
fn as_index(index: &str, member: Member) -> String {
match member {
Member::Int => index.into(),
Member::Float => format!("(int32_t){index}"),
_ => unreachable!("an index is a number (see infer)"),
}
}
/// Whether an index is an array index below `length`. A float must be a whole number
fn in_range(index: &str, member: Member, length: &str) -> String {
match member {
Member::Int => format!("{index} >= 0 && {index} < {length}"),
_ => format!("{index} >= 0 && {index} < {length} && {index} == (double)(int32_t){index}"),
}
}
// ─────────────────────────────────────── Read fns ───────────────────────────────────────
/// The read of field `name` on `obj`, an object of `class`: the field itself if every object
/// has it, else a call of the read fn. Gives the value and its C type
fn read_field(e: &mut Emitter, class: usize, name: AtomRef, obj: &str) -> (String, Rep) {
if let Some(field) = e.layout.field(class, name).filter(|f| f.defined) {
return (e.field_access(obj, field), field.rep);
}
let (id, rep) = read_fn(e, class, name);
(format!("rd{id}({obj})"), rep)
}
/// The read fn of field `name` on `class`, wanted the first time. Gives its number and what
/// it gives
fn read_fn(e: &mut Emitter, class: usize, name: AtomRef) -> (usize, Rep) {
if let Some(&found) = e.reads.get(&(class, name)) {
return found;
}
// What it gives is the join of the reads on the class's objects (see `Memory::read_field`)
let objs = e.layout.classes[class].objs.clone();
let ty = objs.iter().fold(Type::Unknown, |all, &obj| {
join(&all, &e.cache.memory.read_field(&Type::Ref(obj), name))
});
let rep = e.layout.rep(&ty);
let id = e.reads.len();
e.reads.insert((class, name), (id, rep));
e.read_queue.push_back((class, name));
(id, rep)
}
/// Prints read fns
struct ReadEmitter<'e, 'a> {
e: &'e mut Emitter<'a>,
code: Code,
}
impl<'a> Writer<'a> for ReadEmitter<'_, 'a> {
fn code(&mut self) -> &mut Code {
&mut self.code
}
fn emitter(&mut self) -> &mut Emitter<'a> {
self.e
}
}
impl ReadEmitter<'_, '_> {
/// Return a value, as the read's type
fn ret(&mut self, (value, from): (String, Rep), rep: Rep) {
let value = self.e.convert(&value, from, rep);
self.line(format!("return {value};"));
}
}
/// Print the read fn of field `name` on `class`: its own field if it has it, else the read on
/// its prototype. At the end of the chain, the field is undefined. An array's prototype is
/// the stdlib's (see `Emitter::builtin_proto`)
pub fn emit_read(e: &mut Emitter, class: usize, name: AtomRef) {
let (id, rep) = e.reads[&(class, name)];
let signature = format!("static {} rd{id}(struct c{class} *o)", e.ctype(rep));
let _ = writeln!(e.protos, "{signature};");
let code = Code::default();
let mut re = ReadEmitter { e, code };
re.indent(1);
if let Some(field) = re.e.layout.field(class, name).cloned() {
let own = re.e.field_access("o", &field);
let value = re.e.convert(&own, field.rep, rep);
match field.defined {
true => re.line(format!("return {value};")),
false => re.line(format!(
"if (o->has_{}) return {value};",
re.e.field_name(name)
)),
}
}
let proto = re.e.layout.field(class, PROTO_FIELD).cloned();
match (re.e.layout.classes[class].kind, proto) {
// An array's prototype is the stdlib's (see `Emitter::builtin_proto`)
(ClassKind::Array, _) => match re.e.builtin_proto(Member::Obj(class)) {
Some((proto, Rep::One(Member::Obj(proto_class)))) => {
let read = read_field(re.e, proto_class, name, &proto);
re.ret(read, rep);
}
_ => re.ret(undefined(), rep),
},
(_, None) => re.ret(undefined(), rep),
(_, Some(proto)) => {
if !proto.defined {
re.line(format!("if (!o->has_{}) {{", re.e.field_name(PROTO_FIELD)));
re.indent(1);
re.ret(undefined(), rep);
re.indent(-1);
re.line("}");
}
let value = re.e.field_access("o", &proto);
re.split(&[(value, proto.rep)], &mut |re, ms| match ms[0].1 {
Member::Obj(proto_class) => {
let read = read_field(re.e, proto_class, name, &ms[0].0);
re.ret(read, rep);
}
_ => re.ret(undefined(), rep),
});
re.line("abort();");
}
}
let body = std::mem::take(&mut re.code.out);
let _ = write!(re.e.read_defs, "{signature} {{\n{body}}}\n\n");
}
+71 -15
View File
@@ -7,15 +7,25 @@
//! - converts child ownership to a reference by-idx
//! - extracts closure captures into a separate vec
//!
//! Also adds a synthetic entry point function that calls the top level fn generated by QuickJS
//! Also adds a synthetic entry point function that calls the top level fn generated by QuickJS,
//! and makes the module's bindings its locals
use std::collections::HashMap;
use crate::libqjs::parse::{
Atom, AtomRef, ClosureVar, Function, ImmOperand, Instr, JsValue, QuickJSBinary, opcodes::OpCode,
Atom, AtomRef, ClosureKind, ClosureVar, FuncKind, Function, ImmOperand, Instr, JsValue,
QuickJSBinary, opcodes::OpCode,
};
/// The module's bindings, which are locals of the entry fn, by name. The compiler reads the
/// stdlib's by name where it needs one: the classes whose prototypes the primitives use, and
/// what the host calls when an IO op completes
pub type ModuleBindings = HashMap<String, usize>;
#[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 local_count: usize,
pub cpool: Vec<JsValue>,
@@ -27,14 +37,45 @@ pub struct FlatFn {
/// Returns:
/// - flat_fns[i] = the i'th flattened function
/// - captures[i] = the i'th fn's captures from its parent
pub fn flatten(bin: QuickJSBinary) -> (Vec<FlatFn>, Vec<Vec<ClosureVar>>, Vec<Atom>) {
/// - the module's bindings, as locals of the entry fn
pub fn flatten(
bin: QuickJSBinary,
) -> (Vec<FlatFn>, Vec<Vec<ClosureVar>>, Vec<Atom>, ModuleBindings) {
let mut fns = vec![];
let mut captures = vec![];
walk(bin.root_mod.root_fn, &mut fns, &mut captures);
add_entry_point(&mut fns, &mut captures);
let bindings = bind_module(&mut fns, &mut captures, &bin.atoms);
(fns, captures, bin.atoms)
(fns, captures, bin.atoms, bindings)
}
/// Make each binding of the module a local of the entry fn, after its own two. The module fn
/// (`<eval>`, the first fn) captures them, as any fn captures its parent's locals, so they are
/// cells (see transform/closure.rs)
fn bind_module(
fns: &mut [FlatFn],
captures: &mut [Vec<ClosureVar>],
atoms: &[Atom],
) -> ModuleBindings {
let entry = fns.len() - 1;
let mut bindings = ModuleBindings::new();
for capture in &mut captures[0] {
if capture.flags.closure_kind != ClosureKind::ModuleDecl {
continue;
}
let local = fns[entry].local_count;
fns[entry].local_count += 1;
capture.flags.closure_kind = ClosureKind::Local;
capture.var_idx = local;
bindings.insert(capture.name.text(atoms), local);
}
bindings
}
fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) -> usize {
@@ -42,6 +83,7 @@ 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,
local_count: func.var_count,
cpool: vec![],
@@ -73,11 +115,20 @@ fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<Closu
/// ...
/// }
///
/// <eval>.call(true); // Link phase (`this = true`)
/// <eval>.call(false); // Run phase (`this = false`)
/// // Link phase (`this = true`), then run phase (`this = false`)
/// let phase = true;
/// while (true) {
/// <eval>.call(phase);
/// if (!phase) break;
/// phase = false;
/// }
/// }
/// ```
///
/// Both phases call `<eval>` 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
/// module-level object, and each module binding would hold one of two copies
///
/// This gets called by C main()
fn add_entry_point(fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) {
let instr = |pc, op, imm| Instr { pc, op, imm };
@@ -86,20 +137,25 @@ fn add_entry_point(fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) {
instr(0, OpCode::Fclosure8, ImmOperand::Const(0)),
instr(1, OpCode::PutLoc, ImmOperand::Loc(0)),
instr(2, OpCode::PushTrue, ImmOperand::None),
instr(3, OpCode::GetLoc, ImmOperand::Loc(0)),
instr(4, OpCode::CallMethod, ImmOperand::Npop(0)),
instr(5, OpCode::Drop, ImmOperand::None),
instr(6, OpCode::PushFalse, ImmOperand::None),
instr(7, OpCode::GetLoc, ImmOperand::Loc(0)),
instr(8, OpCode::CallMethod, ImmOperand::Npop(0)),
instr(9, OpCode::Drop, ImmOperand::None),
instr(10, OpCode::ReturnUndef, ImmOperand::None),
instr(3, OpCode::PutLoc, ImmOperand::Loc(1)),
// Loop head: one call for both phases
instr(4, OpCode::GetLoc, ImmOperand::Loc(1)),
instr(5, OpCode::GetLoc, ImmOperand::Loc(0)),
instr(6, OpCode::CallMethod, ImmOperand::Npop(0)),
instr(7, OpCode::Drop, ImmOperand::None),
instr(8, OpCode::GetLoc, ImmOperand::Loc(1)),
instr(9, OpCode::IfFalse8, ImmOperand::Label { target: 13 }),
instr(10, OpCode::PushFalse, ImmOperand::None),
instr(11, OpCode::PutLoc, ImmOperand::Loc(1)),
instr(12, OpCode::Goto8, ImmOperand::Label { target: 4 }),
instr(13, OpCode::ReturnUndef, ImmOperand::None),
];
fns.push(FlatFn {
name: AtomRef::Predefined(0),
kind: FuncKind::Normal,
arg_count: 0,
local_count: 1,
local_count: 2, // <eval>, and the phase
cpool: vec![JsValue::FnByIdx(0)], // First fn is the top level <eval>
code,
});
+357
View File
@@ -0,0 +1,357 @@
//! 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 request comes from. `None` for the entry fn, which the host calls
pub type CallSite = Option<InstrRef>;
/// The objects that one instr makes, in the requests from one call site
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ObjAddr {
pub made_at: InstrRef,
pub in_call: CallSite,
}
/// A location in the program, which memory holds a type for. A location that an object
/// can hold is named like an object, by where it is made and the call site of the request.
/// One that only its walk can see is named by the walk
///
/// A request key can hold an address, in a `Type::Ref`. Every part of an address comes from
/// the program text (or the walks, which come from the keys), so there are finitely many
/// addresses and keys, 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
Captured {
fn_idx: usize,
slot: Operand,
in_call: CallSite,
}, // a binding that a closure shares (see transform/closure.rs)
Binding {
walk: usize,
slot: Operand,
}, // a binding that only its walk sees
}
/// 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 module binding (see `flatten::ModuleBindings`). A program without the stdlib has none
#[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 call site. Two
/// walks from that call site can share two locations into the field: walks of one fn
/// 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)
}
}
+211 -39
View File
@@ -1,53 +1,225 @@
mod shape;
//! Infers a type for every location
//!
//! A request is a fn, the call site that calls it, and the types of its args. Every location
//! has an address (see `Addr`), and memory holds one type for each address. An object is
//! named by the instr that makes it and the call site of the request that runs the instr. A
//! binding is named by its walk, or like an object if a closure shares it. A walk reads
//! memory and the returns of other requests, which only grow, so infer walks the program in
//! rounds until one changes nothing
mod memory;
mod types;
mod walk;
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
pub use types::Type;
use crate::{
flatten::ModuleBindings,
regify::{Operand, RegFn},
};
/// TypeCells are owned by the site (value inside a InferResult) where it was created
///
/// Deep clone vs shallow clone is used to model by-ref and by-value semantics when passing
/// to callees or writing into objects
#[derive(Debug)]
pub struct TypeCell(Rc<RefCell<Type>>);
pub use memory::{Addr, Builtins, CallSite, InstrRef, Memory, ObjAddr};
pub use types::{LENGTH_FIELD, Shape, Type, join};
impl TypeCell {
fn shallow_clone(&self) -> Self {
todo!()
}
fn deep_clone(&self) -> Self {
todo!()
}
fn get(&self) -> RefMut<'_, Type> {
self.0.borrow_mut()
}
}
impl Hash for TypeCell {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.as_ptr().hash(state); // TODO: hash by ptr identity is correct right?
}
}
#[derive(Debug, Hash)]
/// A fn, the call it comes from, and the values of its args. Values compare by their
/// types, and an object by its address, so there are finitely many requests. The call site
/// is part of the key because it names the objects that the walk makes
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct InferRequest {
pub fn_idx: usize,
pub arg_types: Vec<TypeCell>,
pub in_call: CallSite,
pub arg_types: Vec<Type>, // one for each param
}
impl InferRequest {
/// The request of a call of `fn_idx` from `in_call` with args of these types. An arg it
/// does not pass is undefined, and an arg it passes beyond its params is never read.
/// Only an arg's kind crosses a call, not a known value, else every literal passed would
/// make a new walk
fn new(fn_idx: usize, in_call: CallSite, arg_types: &[Type], fns: &[RegFn]) -> Self {
let params = (0..fns[fn_idx].arg_count)
.map(|i| arg_types.get(i).map_or(Type::Undefined, Type::widen));
Self {
fn_idx,
in_call,
arg_types: params.collect(),
}
}
}
/// A request and what its walks found. Its index in `TypeCache::walks` names its bindings,
/// whose types are in memory
#[derive(Debug)]
pub struct Walk {
pub req: InferRequest,
pub ret: Type, // what it returns
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 `ret` as it is
}
#[derive(Debug)]
pub struct InferResult {
pub local_types: Vec<TypeCell>,
pub reg_types: Vec<TypeCell>,
pub ret_type: TypeCell,
pub struct TypeCache {
pub walks: Vec<Walk>,
ids: HashMap<InferRequest, usize>, // the walk of each request
pub memory: Memory,
round: usize,
changes: usize, // new walks and growths of a return. With the changes of memory, these tell whether a round found anything new
}
pub struct TypeCache {
results: HashMap<InferRequest, InferResult>,
impl TypeCache {
/// The walk of a call that infer walked
pub fn find(
&self,
fn_idx: usize,
in_call: CallSite,
arg_types: &[Type],
fns: &[RegFn],
) -> usize {
self.ids[&InferRequest::new(fn_idx, in_call, 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
}
/// The address of a local, arg or reg of walk `w`
pub fn addr(&self, w: usize, slot: Operand, fns: &[RegFn]) -> Addr {
let req = &self.walks[w].req;
binding_addr(w, req, &fns[req.fn_idx].captured_slots(), slot)
}
/// Everything that can change, counted. Infer is done when a round leaves it the same
fn progress(&self) -> usize {
self.changes + self.memory.changes()
}
/// 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 `in_call`, walking it unless this round walked it
/// already. Gives what it returns
fn call(
&mut self,
fn_idx: usize,
in_call: CallSite,
arg_types: &[Type],
fns: &[RegFn],
) -> Type {
let req = InferRequest::new(fn_idx, in_call, arg_types, fns);
let w = match self.ids.get(&req) {
Some(&w) => w,
None => {
self.ids.insert(req.clone(), self.walks.len());
self.walks.push(Walk {
req,
ret: Type::Unknown,
reachable: vec![],
round: 0,
walking: false,
});
self.changes += 1;
self.walks.len() - 1
}
};
if self.walks[w].walking || self.is_live(w) {
return self.walks[w].ret.clone();
}
self.walks[w].walking = true;
let (ret, reachable) = walk::walk_fn(w, fns, self);
let walk = &mut self.walks[w];
let joined = types::join(&walk.ret, &ret);
if joined != walk.ret {
walk.ret = joined;
self.changes += 1;
}
walk.walking = false;
walk.round = self.round;
walk.reachable = reachable;
walk.ret.clone()
}
}
/// The address of a local, arg or reg of walk `w` of `req`, given the slots of its fn that a
/// closure shares. A binding that a closure shares is named like an object, any other by
/// the walk
fn binding_addr(w: usize, req: &InferRequest, captured: &[Operand], slot: Operand) -> Addr {
match captured.contains(&slot) {
true => Addr::Captured {
fn_idx: req.fn_idx,
slot,
in_call: req.in_call,
},
false => Addr::Binding { 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, which
/// `flatten` appends last, 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], bindings: &ModuleBindings) -> TypeCache {
let entry = fns.len() - 1;
let binding = |name: &str| bindings.get(name).map(|&k| module_binding(entry, k));
let mut cache = TypeCache {
walks: vec![],
ids: HashMap::new(),
memory: Memory::default(),
round: 0,
changes: 0,
};
cache.memory.builtins = Builtins {
string: binding("String"),
number: binding("Number"),
boolean: binding("Boolean"),
array: binding("Array"),
};
loop {
let before = cache.progress();
cache.round += 1;
cache.call(entry, None, &[], fns);
if let Some(complete) = binding("__complete") {
cache.call_host_complete(complete, fns);
}
if cache.progress() == before {
return cache;
}
}
}
/// The address of module binding `local` (see `flatten::ModuleBindings`): a local of the
/// entry, which the module fn captures
pub fn module_binding(entry: usize, local: usize) -> Addr {
Addr::Captured {
fn_idx: entry,
slot: Operand::Local(local),
in_call: None,
}
}
-9
View File
@@ -1,9 +0,0 @@
use super::types::Type;
use crate::libqjs::parse::AtomRef;
#[derive(Debug, Hash)]
pub enum Shape {
Obj(Vec<(AtomRef, Type)>),
Sum(Vec<Type>),
Array(Box<Type>),
}
+225 -6
View File
@@ -1,15 +1,234 @@
use super::shape::Shape;
//! 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;
#[derive(Debug, Hash)]
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,
Unknown, // no value has reached it yet
Undefined,
Null,
Bool(Option<bool>),
Bool(Option<bool>), // the value, if it is known
Int,
Float,
Str(Option<AtomRef>),
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: 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,
}
}
+457
View File
@@ -0,0 +1,457 @@
//! One walk: the types that one request gives the locals, args and regs of its fn
//!
//! 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::{
InferRequest, TypeCache, binding_addr,
memory::{Addr, 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::{JsValue, opcodes::OpCode},
native,
regify::{BasicBlock, Operand, RegFn, RegInstr},
transform::{FIRST_ARG, THIS_ARG},
};
/// Walk `w`. Gives what it returns, and the blocks that can run
pub fn walk_fn(w: usize, fns: &[RegFn], cache: &mut TypeCache) -> (Type, Vec<bool>) {
let req = cache.walks[w].req.clone();
let f = &fns[req.fn_idx];
let walker = Walker {
w,
f,
fns,
captured: f.captured_slots(),
req,
cache,
ret: Type::Unknown,
reachable: vec![false; f.blocks.len()],
};
walker.run()
}
struct Walker<'a> {
w: usize,
req: InferRequest,
f: &'a RegFn,
fns: &'a [RegFn],
captured: Vec<Operand>, // the slots of `f` that a closure shares
cache: &'a mut TypeCache,
ret: Type,
reachable: Vec<bool>,
}
impl Walker<'_> {
fn addr(&self, slot: Operand) -> Addr {
binding_addr(self.w, &self.req, &self.captured, slot)
}
/// The address of the objects that the instr at `idx` makes in this walk
fn here(&self, idx: usize) -> ObjAddr {
ObjAddr {
made_at: InstrRef {
fn_idx: self.req.fn_idx,
instr_idx: idx,
},
in_call: self.req.in_call,
}
}
fn run(mut self) -> (Type, Vec<bool>) {
// The args get their values from the request
for (i, ty) in self.req.arg_types.iter().enumerate() {
self.cache.memory.join(self.addr(Operand::Arg(i)), ty);
}
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.ret, 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.ret = join(&self.ret, &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(),
// 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 in_call = Some(InstrRef {
fn_idx: self.req.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, in_call, &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");
}
}
}
+46 -16
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;
@@ -134,6 +138,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 {
@@ -169,12 +187,8 @@ pub enum JsValue {
},
Function(Function),
FnByIdx(usize),
/// A C function, by its index in `native::NATIVES`
/// A C function that JS can call, by its index in `native::NATIVES`
NativeFn(usize),
/// Makes its arg the prototype of a primitive kind (see `native`)
ProtoLink(crate::native::ProtoOf),
/// `__link_host_complete`: its arg is what the host calls when an op completes
HostLink,
Module(Module),
TypedArray,
ArrayBuffer,
@@ -262,7 +276,7 @@ impl JsValue {
}
}
#[derive(Debug, TryFromPrimitive)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u16)]
pub enum FuncKind {
Normal = 0,
@@ -277,7 +291,7 @@ pub struct FunctionFlags {
pub _has_simple_param_list: bool,
pub _is_derived_class_constructor: bool,
pub _need_home_object: bool,
pub _func_kind: FuncKind,
pub func_kind: FuncKind,
pub _new_target_allowed: bool,
pub _super_call_allowed: bool,
pub _super_allowed: bool,
@@ -297,7 +311,7 @@ impl FunctionFlags {
_has_simple_param_list: raw & (1 << 1) != 0,
_is_derived_class_constructor: raw & (1 << 2) != 0,
_need_home_object: raw & (1 << 3) != 0,
_func_kind: FuncKind::try_from_primitive((raw >> 4) & 0b11).unwrap(),
func_kind: FuncKind::try_from_primitive((raw >> 4) & 0b11).unwrap(),
_new_target_allowed: raw & (1 << 6) != 0,
_super_call_allowed: raw & (1 << 7) != 0,
_super_allowed: raw & (1 << 8) != 0,
@@ -309,7 +323,7 @@ impl FunctionFlags {
}
}
#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub enum VarKind {
Normal = 0,
@@ -391,7 +405,7 @@ impl VarDef {
}
}
#[derive(Debug, TryFromPrimitive)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub enum ClosureKind {
Local = 0,
@@ -404,7 +418,7 @@ pub enum ClosureKind {
ModuleImport = 7,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ClosureVarFlags {
pub closure_kind: ClosureKind,
pub _is_const: bool,
@@ -434,9 +448,9 @@ impl ClosureVarFlags {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ClosureVar {
pub _name: AtomRef,
pub name: AtomRef,
pub var_idx: usize,
pub flags: ClosureVarFlags,
}
@@ -450,7 +464,7 @@ impl ClosureVar {
Ok((
bin,
Self {
_name: name,
name,
var_idx,
flags,
},
@@ -673,7 +687,7 @@ fn parse_bytecode(code: &[u8]) -> IResult<&[u8], Vec<Instr>> {
#[derive(Debug)]
pub struct Function {
pub _flags: FunctionFlags,
pub flags: FunctionFlags,
pub _is_strict_mode: bool,
pub name: AtomRef,
pub arg_count: usize,
@@ -730,7 +744,7 @@ impl Function {
Ok((
bin,
Self {
_flags: flags,
flags,
_is_strict_mode: strict_mode == 1,
name,
arg_count,
@@ -891,6 +905,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('"')?;
+81 -1
View File
@@ -1,3 +1,4 @@
mod emit;
mod flatten;
mod infer;
mod libqjs;
@@ -10,6 +11,35 @@ use std::io::Read;
use libqjs::compile;
/// The stdlib, in the order that it runs in, in front of the program
const STDLIB: &[&[u8]] = &[
include_bytes!("../../lib/stdlib/globals.js"),
include_bytes!("../../lib/stdlib/object.js"),
include_bytes!("../../lib/stdlib/string.js"),
include_bytes!("../../lib/stdlib/number.js"),
include_bytes!("../../lib/stdlib/boolean.js"),
include_bytes!("../../lib/stdlib/array.js"),
include_bytes!("../../lib/stdlib/loop.js"),
include_bytes!("../../lib/stdlib/promise.js"),
include_bytes!("../../lib/stdlib/coroutine.js"),
include_bytes!("../../lib/stdlib/timers.js"),
include_bytes!("../../lib/stdlib/io.js"),
include_bytes!("../../lib/stdlib/console.js"),
];
/// Runs after the program's top level, or after its first top-level `await`
const STDLIB_END: &[u8] = include_bytes!("../../lib/stdlib/end.js");
/// The runtime. The emitted C includes `jsrt.h`, which includes the rest
const RUNTIME: &[(&str, &[u8])] = &[
("jsrt.h", include_bytes!("../../lib/rt/jsrt.h")),
("core.h", include_bytes!("../../lib/rt/core.h")),
("str.h", include_bytes!("../../lib/rt/str.h")),
("number.h", include_bytes!("../../lib/rt/number.h")),
("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>>();
@@ -19,11 +49,61 @@ fn main() {
let metadata = std::fs::metadata(filename).unwrap();
assert!(metadata.is_file());
let mut src = Vec::with_capacity(metadata.len() as usize);
// The stdlib is compiled with the program, in front of it. The program is the body of an
// async fn of its own, so that its names can shadow the stdlib's, as a script's names
// shadow JS's globals, and so that it can `await` at its top level. `})` is on a line of
// its own, in case the program ends in a comment
let mut src = STDLIB.concat();
src.reserve(metadata.len() as usize + STDLIB_END.len() + 64);
src.extend_from_slice(b"(async function () {\n");
file.read_to_end(&mut src).unwrap();
src.extend_from_slice(b"\n})();\n");
src.extend_from_slice(STDLIB_END);
// Compile to bytecode
let bin = compile(&src, filename);
// Process it
let (flat_fns, captures, atoms, bindings) = flatten::flatten(bin);
let flat_fns = transform::transform(flat_fns, captures, &atoms);
let fns = regify::regify(flat_fns);
let cache = infer::infer_types(&fns, &bindings);
let c = emit::emit(&fns, &cache, &atoms, &bindings);
// 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));
}
+67 -32
View File
@@ -1,12 +1,20 @@
//! The C fns that JS can call, and the globals
//!
//! A native is a C fn of the runtime (lib/rt), which the stdlib calls by name, as a global. 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
//!
//! The only globals are the natives and `undefined`. Everything else that the stdlib defines
//! is a module binding, which the compiler reads by name where it needs one (see
//! `flatten::flatten`)
use crate::{
infer::Type,
libqjs::parse::{Atom, AtomRef, JsString, JsValue, atom_names::PREDEFINED_ATOMS},
libqjs::parse::{Atom, AtomRef, JsValue},
};
/// A native C function callable by JS
///
/// Natives are strictly monomorphic with a single set of expected arg types and fixed
/// return type
/// A native: one set of param types, and one return type
pub struct NativeFn {
pub name: &'static str,
pub arg_types: &'static [Type],
@@ -43,44 +51,71 @@ macro_rules! natives {
}
/// All native functions accessible to JS
const NATIVES: &[NativeFn] = natives! {
// String ops
fn __native_string_concat(String, String) -> String;
fn __native_string_slice(String, Int, Int) -> String;
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;
};
/// The value of the global `name`
pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue {
// TODO: write an Eq between Atom and &str
let name = atom_name(name, atoms);
let name = name.text(atoms);
// A global property in JS, but a constant
if name == "undefined" {
return JsValue::Undefined;
}
match NATIVES.iter().position(|n| name == n.name) {
Some(idx) => JsValue::NativeFn(idx),
None => panic!(),
None => panic!(
"unknown global `{name}`: the globals are the natives in compiler/src/native.rs, and `undefined`"
),
}
}
fn atom_name(name: AtomRef, atoms: &[Atom]) -> String {
let js_string = match name {
AtomRef::FileLocal(idx) => match &atoms[idx] {
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s,
Atom::Predefined(n) => return PREDEFINED_ATOMS[*n as usize].to_string(),
},
AtomRef::Predefined(n) => return PREDEFINED_ATOMS[n as usize].to_string(),
AtomRef::TaggedInt(n) => return n.to_string(),
};
js_string_text(js_string)
/// 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()
}
fn js_string_text(js_string: &JsString) -> String {
match js_string {
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
JsString::UTF16(b) => {
let u: Vec<u16> = b
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&u)
/// 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()
}
+105 -20
View File
@@ -10,7 +10,7 @@ pub use basic_block::BasicBlock;
use basic_block::find_basic_blocks;
use walk::walk_block;
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Operand {
Local(usize),
Arg(usize),
@@ -46,34 +46,119 @@ pub struct RegFn {
pub blocks: Vec<BasicBlock>,
}
impl RegFn {
fn from_flat(f: FlatFn) -> Self {
let blocks = find_basic_blocks(&f.code);
let mut out = vec![RegInstr::nop(); f.code.len()];
pub fn regify(flat_fns: Vec<FlatFn>) -> Vec<RegFn> {
flat_fns.into_iter().map(RegFn::from_flat).collect()
}
// The stack each block recieves at entry from its predecessors
let mut entry_stacks: Vec<Option<Vec<Operand>>> = vec![None; blocks.len()];
impl RegFn {
/// The locals and args that a closure shares: the locations of its `define_field_ref`s
/// (see transform/closure.rs)
pub fn captured_slots(&self) -> Vec<Operand> {
let refs = self.code.iter().filter(|i| i.op == OpCode::DefineFieldRef);
let mut slots: Vec<Operand> = refs
.map(|i| i.args[0])
.filter(|slot| !matches!(slot, Operand::Reg(_)))
.collect();
slots.sort_unstable();
slots.dedup();
slots
}
/// Joins: the stack that a block gets at entry is new regs, made when the walk first gets
/// to the block. Each predecessor copies its own stack into them, in its postfix. So every
/// block has one entry stack, whichever way control comes in (this does the job of phi
/// nodes in SSA)
///
/// A block with two successors copies into the entry regs of both, before its jump. The
/// copies for the edge that is not taken do no harm: a block's entry regs are only read
/// in that block, and every way into it writes them first
fn from_flat(f: FlatFn) -> Self {
let mut blocks = find_basic_blocks(&f.code);
let mut out = vec![RegInstr::nop(); f.code.len()];
let mut next_reg = 0;
// Walk blocks in CFG order, starting from block 0
let mut worklist = vec![];
let mut entry_stacks: Vec<Option<Vec<Operand>>> = vec![None; blocks.len()];
entry_stacks[0] = Some(vec![]);
worklist.push(0);
while let Some(block_idx) = worklist.pop() {
let mut stack = entry_stacks[block_idx].clone().unwrap_or_default();
// Walk blocks in CFG order, starting from block 0. A block that the walk never
// gets to is dead code, and stays `nop`
let mut worklist = vec![0];
walk_block(
&blocks[block_idx],
&f.code,
&mut stack,
&mut next_reg,
&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 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()
});
assert_eq!(
entry.len(),
stack.len(),
"a join of stacks of different depths"
);
// TODO: handle sucessors
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],
});
}
}
todo!()
// 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 {
arg_count: f.arg_count,
local_count: f.local_count,
reg_count: next_reg,
cpool: f.cpool,
code: out,
blocks,
}
}
}
fn fresh_reg(next_reg: &mut usize) -> Operand {
let r = *next_reg;
*next_reg += 1;
Operand::Reg(r)
}
/// 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;
}
}
}
+115 -52
View File
@@ -42,11 +42,16 @@ pub fn walk_block(
continue;
}
if try_handle_misc(instr, stack, out_instr) {
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]);
}
}
}
@@ -163,61 +168,53 @@ fn try_handle_named_write(instr: &Instr, stack: &mut Vec<Operand>, out: &mut Reg
true
}
fn try_handle_misc(instr: &Instr, stack: &mut Vec<Operand>, out_instr: &mut RegInstr) -> bool {
use OpCode::*;
*out_instr = match instr.op {
// Duplicate stack top, out_instr becomes `nop`
Dup => {
let top = stack[stack.len() - 1].clone();
stack.push(top);
RegInstr {
dsts: vec![],
op: Nop,
imm: ImmOperand::None,
args: vec![],
}
}
// Duplicate the top 2 elems in stack, out_instr becomes `nop`
Dup2 => {
let a = stack[stack.len() - 1].clone();
let b = stack[stack.len() - 2].clone();
stack.push(b);
stack.push(a);
RegInstr {
dsts: vec![],
op: Nop,
imm: ImmOperand::None,
args: vec![],
}
}
// Swap top 2 elems in stack, out_instr becomes `nop`
Swap => {
let a = stack.pop().unwrap();
let b = stack.pop().unwrap();
stack.push(a);
stack.push(b);
RegInstr {
dsts: vec![],
op: Nop,
imm: ImmOperand::None,
args: vec![],
}
}
_ => return false,
/// 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,
@@ -225,6 +222,8 @@ fn handle_normal_case(
mut fresh_reg: impl FnMut() -> Operand,
out_instr: &mut RegInstr,
) {
use OpCode::*;
// op and imm carried as-is
*out_instr = RegInstr {
dsts: vec![],
@@ -244,12 +243,22 @@ fn handle_normal_case(
let n_pop = info.n_pop as usize + extra_pops;
let n_push = info.n_push as usize;
// Simulate this instr's pops
// 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();
@@ -257,3 +266,57 @@ fn handle_normal_case(
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"),
};
}
+742
View File
@@ -0,0 +1,742 @@
//! 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 reaches its kind's driver by name, as
//! if the source named it
//!
//! 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, and top-level
//! `await`. The module fn is async to QuickJS, but runs as a normal fn (see flatten.rs)
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,
},
};
/// The module fn, `<eval>`: flatten's first fn
const MODULE_FN: usize = 0;
// ─────────────────────────────────────── The spine ───────────────────────────────────────
pub fn deasync(fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>, atoms: &[Atom]) {
let top_level_await = fns[MODULE_FN].code.iter().any(|i| i.op == OpCode::Await);
assert!(!top_level_await, "top-level `await` is not supported");
let coroutines: Vec<usize> = (0..fns.len())
.filter(|&f| is_coroutine(f, &fns[f]))
.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 just before the synthetic entry, which stays last (see flatten.rs).
// No fn refers to the entry, so moving it changes no reference
let s = fns.len() - 1;
let step_fn = FlatFn {
name: fns[f].name,
kind: FuncKind::Normal,
arg_count: 1, // input
local_count: 0,
cpool,
code: step.code(),
};
fns.insert(s, step_fn);
captures.insert(s, 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. A link goes
// through the fn's parents, so it waits until every fn is in place
let parents = parents(fns);
for (&f, step) in coroutines.iter().zip(&steps) {
let proto = module_binding(f, driver(fns[f].kind), &parents, captures, atoms);
fns[f].code = constructor(step, proto, atoms);
}
}
/// Whether `f` is an async fn or a generator. The module fn is async to QuickJS, but it is
/// not lowered
fn is_coroutine(f: usize, fun: &FlatFn) -> bool {
f != MODULE_FN && fun.kind != FuncKind::Normal
}
/// 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, set the
/// state to 0, and return what the driver's `start` gives
fn constructor(step: &Step, proto: 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),
(GetVarRef, Imm::VarRef(proto as u16)),
(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
}
// ───────────────────────────────── Links to the drivers ──────────────────────────────────
/// The capture of `f` that reads the module binding `name`, made if needed, with a forward in
/// each fn between the module fn and `f`
fn module_binding(
f: usize,
name: &str,
parents: &[usize],
captures: &mut [Vec<ClosureVar>],
atoms: &[Atom],
) -> usize {
if f == MODULE_FN {
for (i, c) in captures[MODULE_FN].iter().enumerate() {
// Every capture of the module fn is a module binding (see flatten.rs)
if c.name.text(atoms) == name {
return i;
}
}
panic!("async fns and generators need `{name}` from the stdlib (lib/stdlib/coroutine.js)");
}
let i = module_binding(parents[f], name, parents, captures, atoms);
for (j, c) in captures[f].iter().enumerate() {
if c.flags.closure_kind == ClosureKind::Ref && c.var_idx == i {
return j;
}
}
let atom = captures[parents[f]][i].name;
captures[f].push(closure_var(atom, i, ClosureKind::Ref));
captures[f].len() - 1
}
/// The parent of each fn: the fn whose constant pool holds it
fn parents(fns: &[FlatFn]) -> Vec<usize> {
let mut parents = vec![usize::MAX; fns.len()];
for (p, f) in fns.iter().enumerate() {
for c in &f.cpool {
if let &JsValue::FnByIdx(child) = c {
parents[child] = p;
}
}
}
parents
}
+188 -6
View File
@@ -21,16 +21,198 @@
//! f(fn.<var>.y);
//! }
//!
//! fn.<var> = {};
//! fn.<var>.x = x; // by-ref
//! fn.<var>.y = y; // by-ref
//! ```
//!
//! i.e. captured fields are turned into fields on the function
//! 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
//!
//! A special `define_field_ref` instr is introduced by `c_parse.py` to define a field on an
//! object specifically by-reference. This signals to `escape.rs` to promote those values to
//! the heap regardless if its a primitive or an object. This is required because normally
//! primitives are only passed by-value, but closure captures must always be by-reference
//! # 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::Captured`), 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 a local of the synthetic entry fn, which makes `<eval>` once (see
//! flatten.rs), so there is one location for each module binding
//!
//! In the emitted C, every underlying "plain" function is called as:
//! `fn(this, fields, arg_0, arg_1, ... arg_n)`
//! `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
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,
}
}
+14
View File
@@ -62,6 +62,13 @@ pub fn try_replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
(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),
@@ -111,6 +118,13 @@ pub fn try_replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
],
// [[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),
+35 -9
View File
@@ -5,6 +5,7 @@
//! - 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
@@ -20,15 +21,24 @@
//! - 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
//! - close_loc
//! - fclosure8
//! - define_class
//!
//!
//! And the following instructions take on a different meaning:
//! The following instructions take on a different meaning:
//! - 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)
//!
//! Async fns and generators become a constructor and a step closure first (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;
@@ -40,7 +50,9 @@ use crate::{
};
use args::try_replace_arg_access;
use r#async::deasync;
use class::declass;
use closure::try_replace_closure;
use expand::{replace_get_var, try_replace_expansion};
use new::try_replace_new;
@@ -70,17 +82,28 @@ pub const THIS_ARG: u16 = 1;
pub const FIRST_ARG: u16 = 2;
pub fn transform(
flat_fns: Vec<FlatFn>,
captures: &[Vec<ClosureVar>],
mut flat_fns: Vec<FlatFn>,
mut captures: Vec<Vec<ClosureVar>>,
atoms: &[Atom],
) -> Vec<FlatFn> {
// Async fns and generators become state machines first: this adds fns, and changes the
// captures that the other transforms read
deasync(&mut flat_fns, &mut captures, atoms);
flat_fns
.into_iter()
.map(|f| transform_one(f, captures, atoms))
.enumerate()
.map(|(idx, f)| transform_one(f, &captures[idx], &captures, atoms))
.collect()
}
fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], atoms: &[Atom]) -> FlatFn {
/// `own` is the captures of `f`
fn transform_one(
mut f: FlatFn,
own: &[ClosureVar],
captures: &[Vec<ClosureVar>],
atoms: &[Atom],
) -> FlatFn {
// Transform class declarations
let code = declass(&f.code);
@@ -113,16 +136,19 @@ fn transform_one(mut f: FlatFn, captures: &[Vec<ClosureVar>], atoms: &[Atom]) ->
continue;
}
// TODO: replace closures/bindings
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: f.local_count,
local_count: next_local,
cpool: f.cpool,
code,
code: out,
}
}
-299
View File
@@ -1,299 +0,0 @@
// any_t: a value of any kind, and JS's operators on it
#pragma once
#include "number.h"
// A value of any kind. An object in it is the pointer to its cell, so it keeps
// its identity. A field can also hold absent
typedef enum {
JSRT_KIND_ABSENT,
JSRT_KIND_UNDEFINED,
JSRT_KIND_NULL,
JSRT_KIND_BOOL,
JSRT_KIND_INT,
JSRT_KIND_FLOAT,
JSRT_KIND_STRING,
JSRT_KIND_OBJECT,
} jsrt_kind;
typedef struct {
jsrt_kind kind;
union {
bool b;
int32_t i;
double f;
string_t s;
void *o;
} as;
} any_t;
// Box a value. A pointer that is a sentinel (NULL, JSRT_UNDEFINED,
// JSRT_ABSENT) boxes as null, undefined or absent
any_t __any_undefined(void);
any_t __any_null(void);
any_t __any_absent(void);
any_t __any_from_bool(bool b);
any_t __any_from_int(int32_t n);
any_t __any_from_float(double x);
any_t __any_from_string(string_t s);
any_t __any_from_object(void *o);
// Unbox a value. A value of another kind throws. A string or an object unboxes
// null, undefined and absent to their sentinels
bool __any_as_bool(any_t a);
int32_t __any_as_int(any_t a);
double __any_as_float(any_t a); // an int too
string_t __any_as_string(any_t a);
void *__any_as_object(any_t a);
// The kind of a value
bool __any_is_absent(any_t a);
bool __any_is_undefined(any_t a);
bool __any_is_null(any_t a);
bool __any_is_nullish(any_t a); // undefined or null
bool __any_is_boolean(any_t a);
bool __any_is_number(any_t a);
bool __any_is_string(any_t a);
bool __any_is_object(any_t a); // a fn too
bool __any_is_function(any_t a);
// JS's operators, with JS's conversions
any_t __any_add(any_t a, any_t b);
any_t __any_sub(any_t a, any_t b);
any_t __any_mul(any_t a, any_t b);
any_t __any_div(any_t a, any_t b);
any_t __any_mod(any_t a, any_t b);
any_t __any_pow(any_t a, any_t b);
any_t __any_neg(any_t a);
any_t __any_plus(any_t a); // ToNumber
any_t __any_inc(any_t a);
any_t __any_dec(any_t a);
bool __any_lt(any_t a, any_t b);
bool __any_lte(any_t a, any_t b);
bool __any_gt(any_t a, any_t b);
bool __any_gte(any_t a, any_t b);
bool __any_eq(any_t a, any_t b);
bool __any_neq(any_t a, any_t b);
bool __any_strict_eq(any_t a, any_t b);
bool __any_strict_neq(any_t a, any_t b);
// JS's abstract operations
bool __any_truthy(any_t a); // ToBoolean
int32_t __any_to_int32(any_t a); // ToInt32
string_t __any_to_string(any_t a); // ToString
string_t __any_typeof(any_t a);
#ifdef JSRT_IMPLEMENTATION
#include <math.h>
// ── any: boxing ──
any_t __any_undefined(void) { return (any_t){ .kind = JSRT_KIND_UNDEFINED }; }
any_t __any_null(void) { return (any_t){ .kind = JSRT_KIND_NULL }; }
any_t __any_absent(void) { return (any_t){ .kind = JSRT_KIND_ABSENT }; }
any_t __any_from_bool(bool b) { return (any_t){ .kind = JSRT_KIND_BOOL, .as.b = b }; }
any_t __any_from_int(int32_t n) { return (any_t){ .kind = JSRT_KIND_INT, .as.i = n }; }
any_t __any_from_float(double x) { return (any_t){ .kind = JSRT_KIND_FLOAT, .as.f = x }; }
// A pointer that can be a sentinel, as the kind it stands for, else as `kind`
static any_t jsrt_from_ptr(void *p, jsrt_kind kind) {
if (p == NULL) return __any_null();
if (p == JSRT_UNDEFINED) return __any_undefined();
if (p == JSRT_ABSENT) return __any_absent();
return (any_t){ .kind = kind, .as.o = p };
}
any_t __any_from_string(string_t s) { return jsrt_from_ptr(s, JSRT_KIND_STRING); }
any_t __any_from_object(void *o) { return jsrt_from_ptr(o, JSRT_KIND_OBJECT); }
bool __any_as_bool(any_t a) {
if (a.kind != JSRT_KIND_BOOL) jsrt_type_error();
return a.as.b;
}
double __any_as_float(any_t a) {
if (a.kind == JSRT_KIND_INT) return a.as.i;
if (a.kind != JSRT_KIND_FLOAT) jsrt_type_error();
return a.as.f;
}
int32_t __any_as_int(any_t a) {
return a.kind == JSRT_KIND_INT ? a.as.i : (int32_t)__any_as_float(a);
}
// A pointer of kind `kind`, or the sentinel of a nullish or absent value
static void *jsrt_as_ptr(any_t a, jsrt_kind kind) {
switch (a.kind) {
case JSRT_KIND_NULL: return NULL;
case JSRT_KIND_UNDEFINED: return JSRT_UNDEFINED;
case JSRT_KIND_ABSENT: return JSRT_ABSENT;
default:
if (a.kind != kind) jsrt_type_error();
return a.as.o;
}
}
string_t __any_as_string(any_t a) { return jsrt_as_ptr(a, JSRT_KIND_STRING); }
void *__any_as_object(any_t a) { return jsrt_as_ptr(a, JSRT_KIND_OBJECT); }
// ── any: kinds ──
bool __any_is_absent(any_t a) { return a.kind == JSRT_KIND_ABSENT; }
bool __any_is_undefined(any_t a) { return a.kind == JSRT_KIND_UNDEFINED || a.kind == JSRT_KIND_ABSENT; }
bool __any_is_null(any_t a) { return a.kind == JSRT_KIND_NULL; }
bool __any_is_nullish(any_t a) { return __any_is_undefined(a) || __any_is_null(a); }
bool __any_is_boolean(any_t a) { return a.kind == JSRT_KIND_BOOL; }
bool __any_is_number(any_t a) { return a.kind == JSRT_KIND_INT || a.kind == JSRT_KIND_FLOAT; }
bool __any_is_string(any_t a) { return a.kind == JSRT_KIND_STRING; }
bool __any_is_object(any_t a) { return a.kind == JSRT_KIND_OBJECT; }
bool __any_is_function(any_t a) {
return a.kind == JSRT_KIND_OBJECT && jsrt_fn_cells[((jsrt_header *)a.as.o)->cell];
}
// ── any: abstract operations ──
// ToNumber. An object would call its `valueOf` or `toString` first, which the
// runtime cannot do, so it gives NaN
static double jsrt_to_number(any_t a) {
switch (a.kind) {
case JSRT_KIND_NULL: return 0;
case JSRT_KIND_BOOL: return a.as.b;
case JSRT_KIND_INT: return a.as.i;
case JSRT_KIND_FLOAT: return a.as.f;
case JSRT_KIND_STRING: return __native_string_to_number(a.as.s);
default: return NAN;
}
}
bool __any_truthy(any_t a) {
switch (a.kind) {
case JSRT_KIND_BOOL: return a.as.b;
case JSRT_KIND_INT: return a.as.i != 0;
case JSRT_KIND_FLOAT: return a.as.f != 0 && !isnan(a.as.f);
case JSRT_KIND_STRING: return a.as.s->length != 0;
case JSRT_KIND_OBJECT: return true;
default: return false;
}
}
int32_t __any_to_int32(any_t a) {
double x = jsrt_to_number(a);
if (isnan(x) || isinf(x)) {
return 0;
}
x = fmod(trunc(x), 4294967296.0);
return (int32_t)(uint32_t)(int64_t)(x < 0 ? x + 4294967296.0 : x);
}
// ToString. An object would call its `toString`, which the runtime cannot do
string_t __any_to_string(any_t a) {
switch (a.kind) {
case JSRT_KIND_UNDEFINED: case JSRT_KIND_ABSENT: return jsrt_string_lit("undefined");
case JSRT_KIND_NULL: return jsrt_string_lit("null");
case JSRT_KIND_BOOL: return jsrt_string_lit(a.as.b ? "true" : "false");
case JSRT_KIND_INT: return __native_number_to_string(a.as.i);
case JSRT_KIND_FLOAT: return __native_number_to_string(a.as.f);
case JSRT_KIND_STRING: return a.as.s;
default: return jsrt_string_lit(__any_is_function(a) ? "function" : "[object Object]");
}
}
string_t __any_typeof(any_t a) {
switch (a.kind) {
case JSRT_KIND_UNDEFINED: case JSRT_KIND_ABSENT: return jsrt_string_lit("undefined");
case JSRT_KIND_BOOL: return jsrt_string_lit("boolean");
case JSRT_KIND_INT: case JSRT_KIND_FLOAT: return jsrt_string_lit("number");
case JSRT_KIND_STRING: return jsrt_string_lit("string");
case JSRT_KIND_OBJECT: return jsrt_string_lit(__any_is_function(a) ? "function" : "object");
default: return jsrt_string_lit("object");
}
}
// ── any: operators ──
any_t __any_add(any_t a, any_t b) {
// An object becomes a string (see `__any_to_string`), so it concatenates
bool strings = a.kind == JSRT_KIND_STRING || b.kind == JSRT_KIND_STRING
|| a.kind == JSRT_KIND_OBJECT || b.kind == JSRT_KIND_OBJECT;
if (strings) {
return __any_from_string(__native_string_concat(__any_to_string(a), __any_to_string(b)));
}
return __any_from_float(jsrt_to_number(a) + jsrt_to_number(b));
}
any_t __any_sub(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) - jsrt_to_number(b)); }
any_t __any_mul(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) * jsrt_to_number(b)); }
any_t __any_div(any_t a, any_t b) { return __any_from_float(jsrt_to_number(a) / jsrt_to_number(b)); }
any_t __any_mod(any_t a, any_t b) { return __any_from_float(fmod(jsrt_to_number(a), jsrt_to_number(b))); }
any_t __any_pow(any_t a, any_t b) { return __any_from_float(pow(jsrt_to_number(a), jsrt_to_number(b))); }
any_t __any_neg(any_t a) { return __any_from_float(-jsrt_to_number(a)); }
any_t __any_plus(any_t a) { return __any_from_float(jsrt_to_number(a)); }
any_t __any_inc(any_t a) { return __any_from_float(jsrt_to_number(a) + 1); }
any_t __any_dec(any_t a) { return __any_from_float(jsrt_to_number(a) - 1); }
// ── any: comparisons ──
// Two strings compare by code units. Anything else compares as numbers, where
// NaN makes every comparison false
bool __any_lt(any_t a, any_t b) {
bool strings = a.kind == JSRT_KIND_STRING && b.kind == JSRT_KIND_STRING;
return strings ? jsrt_string_cmp(a.as.s, b.as.s) < 0 : jsrt_to_number(a) < jsrt_to_number(b);
}
bool __any_lte(any_t a, any_t b) {
bool strings = a.kind == JSRT_KIND_STRING && b.kind == JSRT_KIND_STRING;
return strings ? jsrt_string_cmp(a.as.s, b.as.s) <= 0 : jsrt_to_number(a) <= jsrt_to_number(b);
}
bool __any_gt(any_t a, any_t b) { return __any_lt(b, a); }
bool __any_gte(any_t a, any_t b) { return __any_lte(b, a); }
bool __any_strict_eq(any_t a, any_t b) {
if (__any_is_number(a) && __any_is_number(b)) {
return jsrt_to_number(a) == jsrt_to_number(b);
}
if (__any_is_undefined(a) || __any_is_undefined(b)) {
return __any_is_undefined(a) && __any_is_undefined(b);
}
if (a.kind != b.kind) {
return false;
}
switch (a.kind) {
case JSRT_KIND_NULL: return true;
case JSRT_KIND_BOOL: return a.as.b == b.as.b;
case JSRT_KIND_STRING: return jsrt_string_eq(a.as.s, b.as.s);
default: return a.as.o == b.as.o;
}
}
bool __any_strict_neq(any_t a, any_t b) { return !__any_strict_eq(a, b); }
// Undefined and null equal each other and nothing else. Values of one kind,
// and objects, compare as `===` does. Other primitives compare as numbers
bool __any_eq(any_t a, any_t b) {
if (__any_is_nullish(a) || __any_is_nullish(b)) {
return __any_is_nullish(a) && __any_is_nullish(b);
}
bool same = a.kind == b.kind || (__any_is_number(a) && __any_is_number(b));
if (same || a.kind == JSRT_KIND_OBJECT || b.kind == JSRT_KIND_OBJECT) {
return __any_strict_eq(a, b);
}
return jsrt_to_number(a) == jsrt_to_number(b);
}
bool __any_neq(any_t a, any_t b) { return !__any_eq(a, b); }
#endif // JSRT_IMPLEMENTATION
+16 -38
View File
@@ -1,5 +1,6 @@
// Values and objects: the types that every other part uses, the object
// header and its tags, the allocator and errors. See docs/model.md
// Values: the types that every other part uses, the allocator and errors.
// 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>
@@ -12,40 +13,24 @@ typedef struct { char _; } null_t;
// A string: UTF-16 code units, as JS counts them
typedef struct jsrt_string *string_t;
// The start of every object: the number of its cell, which tells its struct
// and, for a fn, its code
typedef struct { uint32_t cell; } jsrt_header;
// A pointer to an object or a string can also be one of these. NULL is null.
// They are told apart by their address only
extern jsrt_header jsrt_undefined_cell, jsrt_absent_cell;
#define JSRT_UNDEFINED ((void *)&jsrt_undefined_cell)
#define JSRT_ABSENT ((void *)&jsrt_absent_cell) // a field that an object lacks
// Whether the objects of each cell are fns, by cell. The emitted C defines it
extern const bool jsrt_fn_cells[];
// A new heap cell of `size` bytes, all zero
void *jsrt_alloc(size_t size);
void *__rt_alloc(size_t size);
// `items`, an array's elements of `size` bytes each, with room for at least
// `need` of them. `*cap` is the room that it has, and is set to the new room
void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size);
// `need` of them. `*cap` is the room that it has, and is set to the new room.
// The new room is all zero
void *__rt_grow(void *items, int32_t *cap, int32_t need, size_t size);
// Throws a TypeError. Exceptions are not supported yet, so it stops the program
_Noreturn void jsrt_type_error(void);
// `p`, a pointer to an object or a string. A sentinel for null, undefined or
// absent throws a TypeError
void *jsrt_some(void *p);
// Throws a TypeError, which says `what`. Exceptions are not supported yet, so
// it stops the program
_Noreturn void __rt_type_error(const char *what);
#ifdef JSRT_IMPLEMENTATION
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
jsrt_header jsrt_undefined_cell, jsrt_absent_cell;
void *jsrt_alloc(size_t size) {
void *__rt_alloc(size_t size) {
void *p = calloc(1, size);
if (!p) {
@@ -56,7 +41,7 @@ void *jsrt_alloc(size_t size) {
return p;
}
void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size) {
void *__rt_grow(void *items, int32_t *cap, int32_t need, size_t size) {
if (need <= *cap) {
return items;
}
@@ -69,21 +54,14 @@ void *jsrt_grow(void *items, int32_t *cap, int32_t need, size_t size) {
abort();
}
memset((char *)items + (size_t)*cap * size, 0, (size_t)(room - *cap) * size);
*cap = room;
return items;
}
_Noreturn void jsrt_type_error(void) {
fputs("TypeError\n", stderr);
_Noreturn void __rt_type_error(const char *what) {
fprintf(stderr, "TypeError: %s\n", what);
exit(1);
}
void *jsrt_some(void *p) {
if (p == NULL || p == JSRT_UNDEFINED || p == JSRT_ABSENT) {
jsrt_type_error();
}
return p;
}
#endif // JSRT_IMPLEMENTATION
+9 -9
View File
@@ -31,7 +31,7 @@
// ── sync ──
undefined_t __native_print_string(string_t s) {
char *utf8 = jsrt_string_utf8(s);
char *utf8 = __rt_string_utf8(s);
puts(utf8);
free(utf8);
return (undefined_t){0};
@@ -178,7 +178,7 @@ static void jsrt_io_release(int32_t h) {
// The op that the last wait gave. A handle of any other op is an error
static jsrt_io_op *jsrt_io_given(int32_t h) {
if (h < 0 || h != jsrt_io.given) {
jsrt_type_error();
__rt_type_error("an IO result read outside its op's callback");
}
return &jsrt_io.ops[h];
@@ -320,7 +320,7 @@ int32_t __native_io_queue_timeout(double ms) {
// `flags` and `mode` are open(2)'s, as numbers
int32_t __native_io_queue_open(string_t path, int32_t flags, int32_t mode) {
int32_t h = jsrt_io_new(JSRT_IO_OPEN);
jsrt_io.ops[h].buf = jsrt_string_utf8(path);
jsrt_io.ops[h].buf = __rt_string_utf8(path);
jsrt_io.ops[h].flags = flags | O_CLOEXEC;
jsrt_io.ops[h].mode = mode;
return jsrt_io_queue(h);
@@ -334,7 +334,7 @@ int32_t __native_io_queue_read(int32_t fd, int32_t len, double offset) {
op->fd = fd;
op->len = len > 0 ? (uint32_t)len : 0;
op->off = offset < 0 ? (uint64_t)-1 : (uint64_t)offset;
op->buf = jsrt_alloc(op->len ? op->len : 1);
op->buf = __rt_alloc(op->len ? op->len : 1);
return jsrt_io_queue(h);
}
@@ -344,7 +344,7 @@ int32_t __native_io_queue_write(int32_t fd, string_t data, double offset) {
int32_t h = jsrt_io_new(JSRT_IO_WRITE);
jsrt_io_op *op = &jsrt_io.ops[h];
op->fd = fd;
op->buf = jsrt_string_utf8(data);
op->buf = __rt_string_utf8(data);
op->len = (uint32_t)strlen(op->buf);
op->off = offset < 0 ? (uint64_t)-1 : (uint64_t)offset;
return jsrt_io_queue(h);
@@ -359,9 +359,9 @@ int32_t __native_io_queue_close(int32_t fd) {
// Gives the file's size, as `__native_io_result_number`
int32_t __native_io_queue_stat(string_t path) {
int32_t h = jsrt_io_new(JSRT_IO_STAT);
char *utf8 = jsrt_string_utf8(path);
char *utf8 = __rt_string_utf8(path);
size_t size = strlen(utf8) + 1;
char *buf = jsrt_alloc(sizeof(struct statx) + size);
char *buf = __rt_alloc(sizeof(struct statx) + size);
memcpy(buf + sizeof(struct statx), utf8, size);
free(utf8);
jsrt_io.ops[h].buf = buf;
@@ -373,7 +373,7 @@ int32_t __native_io_queue_stat(string_t path) {
// -EALREADY
int32_t __native_io_queue_cancel(int32_t target) {
if (target < 0 || target >= jsrt_io.cap) {
jsrt_type_error();
__rt_type_error("a cancel of an op that does not exist");
}
int32_t h = jsrt_io_new(JSRT_IO_CANCEL);
@@ -439,7 +439,7 @@ int32_t __native_io_status(int32_t h) {
string_t __native_io_result_string(int32_t h) {
jsrt_io_op *op = jsrt_io_given(h);
bool data = op->kind == JSRT_IO_READ && op->res > 0;
return jsrt_string_from_utf8(op->buf, data ? (size_t)op->res : 0);
return __rt_string_from_utf8(op->buf, data ? (size_t)op->res : 0);
}
// The size of a stat's file. The status for any other op
+9 -7
View File
@@ -3,13 +3,16 @@
//
// Each header declares its part, and if JSRT_IMPLEMENTATION is defined before
// this is included, also defines it. The emitted C is one file, which does
// that, and includes this before any other header. The natives are declared
// by the emitted C itself, from compiler/src/native.rs, and defined here
// that, and includes this before any other header
//
// core.h values, the object header, the allocator, errors
// str.h strings and the string natives
// number.h the number natives
// any.h any_t and JS's operators on it
// The fns that the program can call have one of two prefixes:
// __rt_* the same on every platform: JS's operators, strings, numbers,
// the allocator and errors
// __native_* the platform's IO, which another platform's runtime replaces
//
// core.h values, the allocator, errors
// str.h strings
// number.h numbers, and their conversions to and from strings
// io.h IO natives, async on io_uring (uring.h), and the host's loop
//
// The host owns the event loop. The program exports two fns, which the host
@@ -36,5 +39,4 @@ void jsrt_complete(int32_t h);
#include "str.h"
#include "number.h"
#include "any.h"
#include "io.h"
+33 -10
View File
@@ -1,8 +1,31 @@
// The number natives: conversions between numbers and strings
// Numbers: conversions between numbers and strings
#pragma once
#include "str.h"
// JS's Number::toString
string_t __rt_number_to_string(double x);
// JS's ToNumber on a string
double __rt_string_to_number(string_t s);
#include <math.h>
// JS's `x ** y`: C's pow, except that a NaN exponent always gives NaN, and so does
// an infinite exponent when |x| is 1. C gives 1 for both
static inline double __rt_pow(double x, double y) {
if (isnan(y) || (isinf(y) && fabs(x) == 1)) return NAN;
return pow(x, y);
}
// JS's ToInt32: the number modulo 2^32, as a signed int. NaN and the infinities are 0
static inline int32_t __rt_to_int32(double x) {
if (!isfinite(x)) return 0;
double m = fmod(trunc(x), 4294967296.0);
if (m < 0) m += 4294967296.0;
return (int32_t)(uint32_t)m;
}
#ifdef JSRT_IMPLEMENTATION
#include <math.h>
#include <stdio.h>
@@ -11,10 +34,10 @@
// JS's Number::toString: the fewest digits that give the number back. Plain
// notation while the decimal point is within 21 places, else exponential
string_t __native_number_to_string(double x) {
if (isnan(x)) return jsrt_string_lit("NaN");
if (isinf(x)) return jsrt_string_lit(x < 0 ? "-Infinity" : "Infinity");
if (x == 0) return jsrt_string_lit("0");
string_t __rt_number_to_string(double x) {
if (isnan(x)) return __rt_string_lit("NaN");
if (isinf(x)) return __rt_string_lit(x < 0 ? "-Infinity" : "Infinity");
if (x == 0) return __rt_string_lit("0");
// The fewest digits: buf is [-]d[.ddd]e±xx
char buf[64];
@@ -61,19 +84,19 @@ string_t __native_number_to_string(double x) {
sprintf(o, "%c%s%se%c%d", digits[0], n > 1 ? "." : "", digits + 1, e < 0 ? '-' : '+', abs(e));
}
return jsrt_string_lit(out);
return __rt_string_lit(out);
}
string_t __native_number_to_fixed(double x, int32_t digits) {
string_t __rt_number_to_fixed(double x, int32_t digits) {
char buf[400];
snprintf(buf, sizeof buf, "%.*f", digits, x);
return jsrt_string_lit(buf);
return __rt_string_lit(buf);
}
// JS's ToNumber on a string: a decimal number with whitespace around it, or
// only whitespace, which is 0. Anything else is NaN
double __native_string_to_number(string_t s) {
char *utf8 = jsrt_string_utf8(s);
double __rt_string_to_number(string_t s) {
char *utf8 = __rt_string_utf8(s);
char *start = utf8, *end;
while (*start == ' ' || (*start >= '\t' && *start <= '\r')) start++;
+57 -43
View File
@@ -1,40 +1,44 @@
// Strings: UTF-16 code units, as JS counts them, and the string natives
// Strings: UTF-16 code units, as JS counts them
#pragma once
#include "core.h"
string_t jsrt_string_lit(const char *utf8);
string_t jsrt_string_from_utf8(const char *utf8, size_t size);
int32_t jsrt_string_length(string_t s);
bool jsrt_string_eq(string_t a, string_t b);
int32_t jsrt_string_cmp(string_t a, string_t b);
// `s[i]`: the code unit at `i`, as a string, or JSRT_UNDEFINED past the end
string_t jsrt_string_at(string_t s, int32_t i);
#ifdef JSRT_IMPLEMENTATION
#include <stdlib.h>
#include <string.h>
// A string. The emitted C makes its literals as static strings of this
struct jsrt_string {
int32_t length;
uint16_t units[];
};
static string_t jsrt_string_new(int32_t length) {
string_t s = jsrt_alloc(sizeof *s + (size_t)length * sizeof(uint16_t));
string_t __rt_string_lit(const char *utf8);
string_t __rt_string_from_utf8(const char *utf8, size_t size);
int32_t __rt_string_length(string_t s);
bool __rt_string_eq(string_t a, string_t b);
int32_t __rt_string_cmp(string_t a, string_t b);
// `a + b`
string_t __rt_string_concat(string_t a, string_t b);
// `s[i]`: the code unit at `i`, as a string. `i` must be in the string
string_t __rt_string_at(string_t s, int32_t i);
#ifdef JSRT_IMPLEMENTATION
#include <stdlib.h>
#include <string.h>
static string_t __rt_string_new(int32_t length) {
string_t s = __rt_alloc(sizeof *s + (size_t)length * sizeof(uint16_t));
s->length = length;
return s;
}
string_t jsrt_string_lit(const char *utf8) {
return jsrt_string_from_utf8(utf8, strlen(utf8));
string_t __rt_string_lit(const char *utf8) {
return __rt_string_from_utf8(utf8, strlen(utf8));
}
// The `size` bytes at `utf8`, which need not end in a 0
string_t jsrt_string_from_utf8(const char *utf8, size_t size) {
string_t __rt_string_from_utf8(const char *utf8, size_t size) {
const unsigned char *p = (const unsigned char *)utf8, *end = p + size;
string_t s = jsrt_string_new((int32_t)size * 2);
string_t s = __rt_string_new((int32_t)size * 2);
int32_t n = 0;
while (p < end) {
@@ -59,15 +63,15 @@ string_t jsrt_string_from_utf8(const char *utf8, size_t size) {
return s;
}
int32_t jsrt_string_length(string_t s) {
int32_t __rt_string_length(string_t s) {
return s->length;
}
bool jsrt_string_eq(string_t a, string_t b) {
bool __rt_string_eq(string_t a, string_t b) {
return a->length == b->length && !memcmp(a->units, b->units, (size_t)a->length * sizeof(uint16_t));
}
int32_t jsrt_string_cmp(string_t a, string_t b) {
int32_t __rt_string_cmp(string_t a, string_t b) {
int32_t n = a->length < b->length ? a->length : b->length;
for (int32_t i = 0; i < n; i++) {
@@ -79,19 +83,36 @@ int32_t jsrt_string_cmp(string_t a, string_t b) {
return a->length < b->length ? -1 : a->length > b->length;
}
string_t jsrt_string_at(string_t s, int32_t i) {
if (i < 0 || i >= s->length) {
return JSRT_UNDEFINED;
string_t __rt_string_concat(string_t a, string_t b) {
string_t s = __rt_string_new(a->length + b->length);
memcpy(s->units, a->units, (size_t)a->length * sizeof(uint16_t));
memcpy(s->units + a->length, b->units, (size_t)b->length * sizeof(uint16_t));
return s;
}
// The strings of one ASCII unit, made once each, the first time they are needed
static string_t __rt_ascii_strings[128];
string_t __rt_string_at(string_t s, int32_t i) {
uint16_t unit = s->units[i];
if (unit < 128 && __rt_ascii_strings[unit]) {
return __rt_ascii_strings[unit];
}
string_t c = __rt_string_new(1);
c->units[0] = unit;
if (unit < 128) {
__rt_ascii_strings[unit] = c;
}
string_t c = jsrt_string_new(1);
c->units[0] = s->units[i];
return c;
}
// `s` as UTF-8, in a new buffer for the caller to free
static char *jsrt_string_utf8(string_t s) {
char *utf8 = jsrt_alloc((size_t)s->length * 3 + 1);
static char *__rt_string_utf8(string_t s) {
char *utf8 = __rt_alloc((size_t)s->length * 3 + 1);
char *out = utf8;
for (int32_t i = 0; i < s->length; i++) {
@@ -124,30 +145,23 @@ static char *jsrt_string_utf8(string_t s) {
return utf8;
}
// ── natives ──
// ── for the stdlib ──
string_t __native_string_concat(string_t a, string_t b) {
string_t s = jsrt_string_new(a->length + b->length);
memcpy(s->units, a->units, (size_t)a->length * sizeof(uint16_t));
memcpy(s->units + a->length, b->units, (size_t)b->length * sizeof(uint16_t));
return s;
}
string_t __native_string_slice(string_t s, int32_t start, int32_t end) {
string_t __rt_string_slice(string_t s, int32_t start, int32_t end) {
start = start < 0 ? 0 : start > s->length ? s->length : start;
end = end < start ? start : end > s->length ? s->length : end;
string_t r = jsrt_string_new(end - start);
string_t r = __rt_string_new(end - start);
memcpy(r->units, s->units + start, (size_t)(end - start) * sizeof(uint16_t));
return r;
}
int32_t __native_string_char_code_at(string_t s, int32_t i) {
int32_t __rt_string_char_code_at(string_t s, int32_t i) {
return s->units[i];
}
string_t __native_string_from_char_code(int32_t code) {
string_t s = jsrt_string_new(1);
string_t __rt_string_from_char_code(int32_t code) {
string_t s = __rt_string_new(1);
s->units[0] = (uint16_t)code;
return s;
}
-2
View File
@@ -149,5 +149,3 @@ Array.prototype = {
return true;
},
};
__link_proto_array(Array.prototype);
-2
View File
@@ -8,5 +8,3 @@ Boolean.prototype = {
toString() { return this ? "true" : "false"; },
valueOf() { return this; },
};
__link_proto_boolean(Boolean.prototype);
+3 -1
View File
@@ -2,6 +2,8 @@
// rest params, which need arrays
const console = {
log(x) {
__native_print_string(String(x));
// As node does: `-0` prints as `-0`, though String(-0) is "0"
const negativeZero = typeof x === "number" && x === 0 && 1 / x < 0;
__native_print_string(negativeZero ? "-0" : String(x));
},
};
+62
View File
@@ -0,0 +1,62 @@
// 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
// A generator's object is `co` itself
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
const __AsyncProto = {
// The body runs up to its first await, in the call
start() {
const promise = new Promise(undefined);
this.run(promise);
return promise;
},
run(promise) {
this.step(undefined);
if (this.done) __resolve(promise, this.result);
else this.awaiting.then(() => this.run(promise), (reason) => __reject(promise, reason));
},
// What an await waits on: a promise as it is, any other value as a fulfilled one.
// The step reads the value from it when it resumes
toPromise(x) {
return __is_promise(x) ? x : Promise.resolve(x);
},
};
+3 -2
View File
@@ -6,8 +6,9 @@
// strings and so on) uses those natives directly, not these functions.
//
// Restrictions:
// - The prototypes linked with `__link_proto_*` are fixed. User code may not
// change them (no monkey-patching)
// - The prototypes of `String`, `Number`, `Boolean` and `Array` are fixed:
// the compiler reads them by name, 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
//
+2 -3
View File
@@ -32,12 +32,11 @@ function __drain_microtasks() {
q.head = 0;
}
// What the host calls when op `h` is done: its callback, then the microtasks
// What the host calls when op `h` is done: its callback, then the microtasks.
// The compiler finds it by name
function __complete(h) {
const done = __pending[h];
__pending[h] = undefined;
if (done !== undefined) done(h);
__drain_microtasks();
}
__link_host_complete(__complete);
+5 -7
View File
@@ -2,7 +2,7 @@
function Number(x) {
switch (typeof x) {
case "number": return x;
case "string": return __native_string_to_number(x);
case "string": return __rt_string_to_number(x);
case "boolean": return x ? 1 : 0;
case "undefined": return NaN;
case "object": return x === null ? 0 : NaN;
@@ -23,15 +23,13 @@ Number.isInteger = function (x) { return Number.isFinite(x) && x % 1 === 0; };
// In these methods `this` is the primitive number itself
Number.prototype = {
toString(radix) {
if (radix === undefined || radix === 10) return __native_number_to_string(this);
if (radix === undefined || radix === 10) return __rt_number_to_string(this);
return integerToString(this, radix);
},
toFixed(digits) { return __native_number_to_fixed(this, digits === undefined ? 0 : digits); },
toFixed(digits) { return __rt_number_to_fixed(this, digits === undefined ? 0 : digits); },
valueOf() { return this; },
};
__link_proto_number(Number.prototype);
// An integer in `radix` (2 to 36). Only integers: the fraction is dropped
function integerToString(n, radix) {
let negative = n < 0;
@@ -41,9 +39,9 @@ function integerToString(n, radix) {
let out = "";
do {
let digit = n % radix;
out = __native_string_concat(__native_string_from_char_code(digit < 10 ? 48 + digit : 87 + digit), out);
out = __rt_string_concat(__rt_string_from_char_code(digit < 10 ? 48 + digit : 87 + digit), out);
n = (n - digit) / radix;
} while (n > 0);
return negative ? __native_string_concat("-", out) : out;
return negative ? __rt_string_concat("-", out) : out;
}
+21 -23
View File
@@ -2,7 +2,7 @@
function String(x) {
switch (typeof x) {
case "string": return x;
case "number": return __native_number_to_string(x);
case "number": return __rt_number_to_string(x);
case "boolean": return x ? "true" : "false";
case "undefined": return "undefined";
case "function": return "function () { [native code] }";
@@ -14,7 +14,7 @@ function String(x) {
}
String.fromCharCode = function (code) {
return __native_string_from_char_code(code);
return __rt_string_from_char_code(code);
};
// In these methods `this` is the primitive string itself
@@ -25,41 +25,41 @@ String.prototype = {
// ── Characters ──
charAt(i) {
if (i < 0 || i >= this.length) return "";
return __native_string_slice(this, i, i + 1);
return __rt_string_slice(this, i, i + 1);
},
charCodeAt(i) {
if (i < 0 || i >= this.length) return NaN;
return __native_string_char_code_at(this, i);
return __rt_string_char_code_at(this, i);
},
at(i) {
if (i < 0) i += this.length;
if (i < 0 || i >= this.length) return undefined;
return __native_string_slice(this, i, i + 1);
return __rt_string_slice(this, i, i + 1);
},
// ── Search ──
indexOf(search, from) {
let start = from === undefined ? 0 : clampIndex(from, this.length);
for (let i = start; i + search.length <= this.length; i++) {
if (__native_string_slice(this, i, i + search.length) === search) return i;
if (__rt_string_slice(this, i, i + search.length) === search) return i;
}
return -1;
},
lastIndexOf(search) {
for (let i = this.length - search.length; i >= 0; i--) {
if (__native_string_slice(this, i, i + search.length) === search) return i;
if (__rt_string_slice(this, i, i + search.length) === search) return i;
}
return -1;
},
includes(search, from) { return this.indexOf(search, from) !== -1; },
startsWith(search, from) {
let start = from === undefined ? 0 : clampIndex(from, this.length);
return __native_string_slice(this, start, start + search.length) === search;
return __rt_string_slice(this, start, start + search.length) === search;
},
endsWith(search, end) {
let stop = end === undefined ? this.length : clampIndex(end, this.length);
let start = stop - search.length;
return start >= 0 && __native_string_slice(this, start, stop) === search;
return start >= 0 && __rt_string_slice(this, start, stop) === search;
},
// ── Substrings ──
@@ -67,13 +67,13 @@ String.prototype = {
let len = this.length;
let s = relativeIndex(start === undefined ? 0 : start, len);
let e = relativeIndex(end === undefined ? len : end, len);
return s < e ? __native_string_slice(this, s, e) : "";
return s < e ? __rt_string_slice(this, s, e) : "";
},
substring(start, end) {
let len = this.length;
let s = clampIndex(start === undefined ? 0 : start, len);
let e = clampIndex(end === undefined ? len : end, len);
return s < e ? __native_string_slice(this, s, e) : __native_string_slice(this, e, s);
return s < e ? __rt_string_slice(this, s, e) : __rt_string_slice(this, e, s);
},
// ── Case ──
@@ -84,27 +84,25 @@ String.prototype = {
trim() { return this.trimStart().trimEnd(); },
trimStart() {
let i = 0;
while (i < this.length && isSpace(__native_string_char_code_at(this, i))) i++;
return __native_string_slice(this, i, this.length);
while (i < this.length && isSpace(__rt_string_char_code_at(this, i))) i++;
return __rt_string_slice(this, i, this.length);
},
trimEnd() {
let i = this.length;
while (i > 0 && isSpace(__native_string_char_code_at(this, i - 1))) i--;
return __native_string_slice(this, 0, i);
while (i > 0 && isSpace(__rt_string_char_code_at(this, i - 1))) i--;
return __rt_string_slice(this, 0, i);
},
// ── Building ──
repeat(count) {
let s = "";
for (let i = 0; i < count; i++) s = __native_string_concat(s, this);
for (let i = 0; i < count; i++) s = __rt_string_concat(s, this);
return s;
},
padStart(length, fill) { return pad(this, length, fill, true); },
padEnd(length, fill) { return pad(this, length, fill, false); },
};
__link_proto_string(String.prototype);
// An index clamped to [0, len]
function clampIndex(i, len) {
return i < 0 ? 0 : i > len ? len : i;
@@ -119,9 +117,9 @@ function relativeIndex(i, len) {
function mapCodes(s, lo, hi, shift) {
let out = "";
for (let i = 0; i < s.length; i++) {
let c = __native_string_char_code_at(s, i);
let c = __rt_string_char_code_at(s, i);
if (c >= lo && c <= hi) c += shift;
out = __native_string_concat(out, __native_string_from_char_code(c));
out = __rt_string_concat(out, __rt_string_from_char_code(c));
}
return out;
}
@@ -136,8 +134,8 @@ function pad(s, length, fill, atStart) {
if (s.length >= length || f.length === 0) return s;
let padding = "";
while (padding.length < length - s.length) padding = __native_string_concat(padding, f);
padding = __native_string_slice(padding, 0, length - s.length);
while (padding.length < length - s.length) padding = __rt_string_concat(padding, f);
padding = __rt_string_slice(padding, 0, length - s.length);
return atStart ? __native_string_concat(padding, s) : __native_string_concat(s, padding);
return atStart ? __rt_string_concat(padding, s) : __rt_string_concat(s, padding);
}