diff --git a/c_parse.py b/c_parse.py index 668ebf5..bebfc39 100644 --- a/c_parse.py +++ b/c_parse.py @@ -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) diff --git a/compiler/build.rs b/compiler/build.rs index 3daeef4..b2495b1 100644 --- a/compiler/build.rs +++ b/compiler/build.rs @@ -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"); diff --git a/compiler/src/emit/func.rs b/compiler/src/emit/func.rs new file mode 100644 index 0000000..616dc3e --- /dev/null +++ b/compiler/src/emit/func.rs @@ -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) { + 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 { + 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, + pub reps: HashMap, // the C type of each local, arg and reg + pub types: HashMap, // 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` +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 w()`. 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 + } +} diff --git a/compiler/src/emit/instr.rs b/compiler/src/emit/instr.rs new file mode 100644 index 0000000..ba562a6 --- /dev/null +++ b/compiler/src/emit/instr.rs @@ -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 = 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 = (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 { + 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!(), + } +} diff --git a/compiler/src/emit/layout.rs b/compiler/src/emit/layout.rs new file mode 100644 index 0000000..ca251ac --- /dev/null +++ b/compiler/src/emit/layout.rs @@ -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, + pub fields: Vec, // sorted by name + pub elems: Rep, // for an array: its elements. Else unused +} + +#[derive(Debug)] +pub struct Layout { + pub classes: Vec, + class_of: HashMap, + pub sums: Vec>, // the members of each sum, sorted. A member's tag is its index + sum_ids: HashMap, 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 = 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![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) -> 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 { + 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 { + 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) -> Vec { + if *ty == Type::Unknown { + return vec![Member::Undefined]; + } + + let mut members: Vec = (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 { + let mut objs: Vec = 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, 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>) { + let types: Vec = 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( + objs: &[ObjAddr], + key: impl Fn(ObjAddr) -> K, +) -> HashMap { + let mut ids: HashMap = HashMap::new(); + + objs.iter() + .map(|&obj| { + let next = ids.len(); + (obj, *ids.entry(key(obj)).or_insert(next)) + }) + .collect() +} diff --git a/compiler/src/emit/mod.rs b/compiler/src/emit/mod.rs new file mode 100644 index 0000000..3c18b54 --- /dev/null +++ b/compiler/src/emit/mod.rs @@ -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, // 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, + walks_wanted: HashSet, + 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, // the text of each string literal + string_ids: HashMap, + 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 { + 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 { + 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 { + 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 { + 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 \n#include \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 = 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)); + } + } +} diff --git a/compiler/src/emit/objects.rs b/compiler/src/emit/objects.rs new file mode 100644 index 0000000..db28319 --- /dev/null +++ b/compiler/src/emit/objects.rs @@ -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"); +} diff --git a/compiler/src/flatten.rs b/compiler/src/flatten.rs index d9090aa..e87110c 100644 --- a/compiler/src/flatten.rs +++ b/compiler/src/flatten.rs @@ -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; + #[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, @@ -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, Vec>, Vec) { +/// - the module's bindings, as locals of the entry fn +pub fn flatten( + bin: QuickJSBinary, +) -> (Vec, Vec>, Vec, 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 +/// (``, 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], + 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, captures: &mut Vec>) -> usize { @@ -42,6 +83,7 @@ fn walk(func: Function, flat_fns: &mut Vec, captures: &mut Vec, captures: &mut Vec.call(true); // Link phase (`this = true`) -/// .call(false); // Run phase (`this = false`) +/// // Link phase (`this = true`), then run phase (`this = false`) +/// let phase = true; +/// while (true) { +/// .call(phase); +/// if (!phase) break; +/// phase = false; +/// } /// } /// ``` /// +/// Both phases call `` 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, captures: &mut Vec>) { let instr = |pc, op, imm| Instr { pc, op, imm }; @@ -86,20 +137,25 @@ fn add_entry_point(fns: &mut Vec, captures: &mut Vec>) { instr(0, OpCode::Fclosure8, ImmOperand::Const(0)), instr(1, OpCode::PutLoc, ImmOperand::Loc(0)), instr(2, OpCode::PushTrue, ImmOperand::None), - instr(3, OpCode::GetLoc, ImmOperand::Loc(0)), - instr(4, OpCode::CallMethod, ImmOperand::Npop(0)), - instr(5, OpCode::Drop, ImmOperand::None), - instr(6, OpCode::PushFalse, ImmOperand::None), - instr(7, OpCode::GetLoc, ImmOperand::Loc(0)), - instr(8, OpCode::CallMethod, ImmOperand::Npop(0)), - instr(9, OpCode::Drop, ImmOperand::None), - instr(10, OpCode::ReturnUndef, ImmOperand::None), + 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, // , and the phase cpool: vec![JsValue::FnByIdx(0)], // First fn is the top level code, }); diff --git a/compiler/src/infer/memory.rs b/compiler/src/infer/memory.rs new file mode 100644 index 0000000..b390203 --- /dev/null +++ b/compiler/src/infer/memory.rs @@ -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; + +/// 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, + merged: HashMap, // 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, + pub number: Option, + pub boolean: Option, + pub array: Option, +} + +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 { + 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 { + 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 { + 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 { + 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 { + 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) -> 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) + } +} diff --git a/compiler/src/infer/mod.rs b/compiler/src/infer/mod.rs index eea30f0..63878a3 100644 --- a/compiler/src/infer/mod.rs +++ b/compiler/src/infer/mod.rs @@ -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>); +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(&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, + pub in_call: CallSite, + pub arg_types: Vec, // 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, // 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, - pub reg_types: Vec, - pub ret_type: TypeCell, +pub struct TypeCache { + pub walks: Vec, + ids: HashMap, // 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, +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, + } } diff --git a/compiler/src/infer/shape.rs b/compiler/src/infer/shape.rs deleted file mode 100644 index f527664..0000000 --- a/compiler/src/infer/shape.rs +++ /dev/null @@ -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), - Array(Box), -} diff --git a/compiler/src/infer/types.rs b/compiler/src/infer/types.rs index 7fc462a..e0578a9 100644 --- a/compiler/src/infer/types.rs +++ b/compiler/src/infer/types.rs @@ -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(Option), // the value, if it is known Int, Float, - Str(Option), + Str(Option), // 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), // 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), // 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 { + 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(&self, test: impl Fn(&Type) -> Option) -> Option { + 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( + a: &Type, + b: &Type, + test: impl Fn(&Type, &Type) -> Option, +) -> Option { + same(pairs(a, b).map(|(x, y)| test(x, y))) +} + +/// `Some(x)` if every item is `Some(x)`, for one `x` +fn same(mut items: impl Iterator>) -> Option { + 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 { + 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) -> 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, + } } diff --git a/compiler/src/infer/walk.rs b/compiler/src/infer/walk.rs new file mode 100644 index 0000000..8404c18 --- /dev/null +++ b/compiler/src/infer/walk.rs @@ -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) { + 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, // the slots of `f` that a closure shares + cache: &'a mut TypeCache, + ret: Type, + reachable: Vec, +} + +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) { + // 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 = 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 = 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 = (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"); + } + } +} diff --git a/compiler/src/libqjs/parse.rs b/compiler/src/libqjs/parse.rs index c05efe6..7f39ceb 100644 --- a/compiler/src/libqjs/parse.rs +++ b/compiler/src/libqjs/parse.rs @@ -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> { #[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), } +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 = 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('"')?; diff --git a/compiler/src/main.rs b/compiler/src/main.rs index 8e08d2b..bbf7704 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -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::>(); @@ -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)); } diff --git a/compiler/src/native.rs b/compiler/src/native.rs index b47165b..9a4e7e4 100644 --- a/compiler/src/native.rs +++ b/compiler/src/native.rs @@ -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 = 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() } diff --git a/compiler/src/regify/mod.rs b/compiler/src/regify/mod.rs index 74096b2..d72c317 100644 --- a/compiler/src/regify/mod.rs +++ b/compiler/src/regify/mod.rs @@ -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, } -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) -> Vec { + flat_fns.into_iter().map(RegFn::from_flat).collect() +} - // The stack each block recieves at entry from its predecessors - let mut entry_stacks: Vec>> = 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 { + let refs = self.code.iter().filter(|i| i.op == OpCode::DefineFieldRef); + let mut slots: Vec = 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>> = 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" + ); + + let copies = stack + .iter() + .zip(entry.iter()) + .filter(|(src, dst)| src != dst); + + for (&src, &dst) in copies { + blocks[b].postfix.push(RegInstr { + dsts: vec![dst], + op: OpCode::SetLoc, + imm: ImmOperand::None, + args: vec![src], + }); + } + } + + // The copies run one after the other, so no copy may write a reg that a later + // copy reads. A block's stack only holds its own entry regs and the regs it made, + // so that could only be a block that jumps to itself and moves its stack around + let postfix = &blocks[b].postfix; + assert!( + postfix + .iter() + .all(|c| postfix.iter().all(|d| c.args[0] != d.dsts[0])), + "the copies of a join overlap" ); - - // TODO: handle sucessors } - todo!() + 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 = (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; + } } } diff --git a/compiler/src/regify/walk.rs b/compiler/src/regify/walk.rs index 42ef8b9..568777d 100644 --- a/compiler/src/regify/walk.rs +++ b/compiler/src/regify/walk.rs @@ -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, out: &mut Reg true } -fn try_handle_misc(instr: &Instr, stack: &mut Vec, 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, 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) -> Vec { + 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"), + }; +} diff --git a/compiler/src/transform/async.rs b/compiler/src/transform/async.rs new file mode 100644 index 0000000..5d80a33 --- /dev/null +++ b/compiler/src/transform/async.rs @@ -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, ``: flatten's first fn +const MODULE_FN: usize = 0; + +// ─────────────────────────────────────── The spine ─────────────────────────────────────── + +pub fn deasync(fns: &mut Vec, captures: &mut Vec>, 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 = (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, + labels: Vec, // 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 { + 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 { + 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. = 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 = (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], +) { + 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], +) { + 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 { + 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> { + 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 { + 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], + 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 { + 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 +} diff --git a/compiler/src/transform/closure.rs b/compiler/src/transform/closure.rs index 4447c27..fa29e0e 100644 --- a/compiler/src/transform/closure.rs +++ b/compiler/src/transform/closure.rs @@ -21,16 +21,198 @@ //! f(fn..y); //! } //! +//! fn. = {}; //! fn..x = x; // by-ref //! fn..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, ``, +//! 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] +//! [fn, vars, value] for each capture `x` of `f` +//! define_field_ref x [fn, vars] +//! define_field [fn] +//! +//! A closure that captures nothing gets no ``: 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 ; get_field x +//! put_var_ref x get_arg env; get_field ; swap; put_field x +//! set_var_ref x dup; get_arg env; get_field ; swap; put_field x +//! +//! close_var_ref x get_arg env; get_field ; 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 `` 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, +}; + +/// ``: 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], + cpool: &[JsValue], + out: &mut Vec, +) -> 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 `` + 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, + } +} diff --git a/compiler/src/transform/expand.rs b/compiler/src/transform/expand.rs index 7e3d03a..bf208f4 100644 --- a/compiler/src/transform/expand.rs +++ b/compiler/src/transform/expand.rs @@ -62,6 +62,13 @@ pub fn try_replace_expansion(instr: &Instr, out: &mut Vec) -> 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) -> 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), diff --git a/compiler/src/transform/mod.rs b/compiler/src/transform/mod.rs index 9fe53b7..95deca8 100644 --- a/compiler/src/transform/mod.rs +++ b/compiler/src/transform/mod.rs @@ -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, - captures: &[Vec], + mut flat_fns: Vec, + mut captures: Vec>, atoms: &[Atom], ) -> Vec { + // 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], atoms: &[Atom]) -> FlatFn { +/// `own` is the captures of `f` +fn transform_one( + mut f: FlatFn, + own: &[ClosureVar], + captures: &[Vec], + atoms: &[Atom], +) -> FlatFn { // Transform class declarations let code = declass(&f.code); @@ -113,16 +136,19 @@ fn transform_one(mut f: FlatFn, captures: &[Vec], 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, } } diff --git a/lib/rt/any.h b/lib/rt/any.h deleted file mode 100644 index 1692a37..0000000 --- a/lib/rt/any.h +++ /dev/null @@ -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 - -// ── 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 diff --git a/lib/rt/core.h b/lib/rt/core.h index 23298ae..420a24b 100644 --- a/lib/rt/core.h +++ b/lib/rt/core.h @@ -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 @@ -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 #include +#include -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 diff --git a/lib/rt/io.h b/lib/rt/io.h index 2bdea66..085df3f 100644 --- a/lib/rt/io.h +++ b/lib/rt/io.h @@ -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 diff --git a/lib/rt/jsrt.h b/lib/rt/jsrt.h index 4597d33..2dbe7ea 100644 --- a/lib/rt/jsrt.h +++ b/lib/rt/jsrt.h @@ -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" diff --git a/lib/rt/number.h b/lib/rt/number.h index baa3b6b..176c1f8 100644 --- a/lib/rt/number.h +++ b/lib/rt/number.h @@ -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 + +// 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 #include @@ -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++; diff --git a/lib/rt/str.h b/lib/rt/str.h index 482dfa3..28c8284 100644 --- a/lib/rt/str.h +++ b/lib/rt/str.h @@ -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 -#include - +// 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 +#include + +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; } diff --git a/lib/stdlib/array.js b/lib/stdlib/array.js index 84d9d4f..4bddbda 100644 --- a/lib/stdlib/array.js +++ b/lib/stdlib/array.js @@ -149,5 +149,3 @@ Array.prototype = { return true; }, }; - -__link_proto_array(Array.prototype); diff --git a/lib/stdlib/boolean.js b/lib/stdlib/boolean.js index c41f12e..d5576eb 100644 --- a/lib/stdlib/boolean.js +++ b/lib/stdlib/boolean.js @@ -8,5 +8,3 @@ Boolean.prototype = { toString() { return this ? "true" : "false"; }, valueOf() { return this; }, }; - -__link_proto_boolean(Boolean.prototype); diff --git a/lib/stdlib/console.js b/lib/stdlib/console.js index dd7221a..fdcf2a7 100644 --- a/lib/stdlib/console.js +++ b/lib/stdlib/console.js @@ -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)); }, }; diff --git a/lib/stdlib/coroutine.js b/lib/stdlib/coroutine.js new file mode 100644 index 0000000..4f57413 --- /dev/null +++ b/lib/stdlib/coroutine.js @@ -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__: , 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); + }, +}; diff --git a/lib/stdlib/globals.js b/lib/stdlib/globals.js index 1931f9d..ccf1c64 100644 --- a/lib/stdlib/globals.js +++ b/lib/stdlib/globals.js @@ -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 // diff --git a/lib/stdlib/loop.js b/lib/stdlib/loop.js index 399e2f2..defb2d4 100644 --- a/lib/stdlib/loop.js +++ b/lib/stdlib/loop.js @@ -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); diff --git a/lib/stdlib/number.js b/lib/stdlib/number.js index d0070e8..ceb912d 100644 --- a/lib/stdlib/number.js +++ b/lib/stdlib/number.js @@ -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; } diff --git a/lib/stdlib/string.js b/lib/stdlib/string.js index 47f7b17..03be20e 100644 --- a/lib/stdlib/string.js +++ b/lib/stdlib/string.js @@ -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); }