diff --git a/compiler/src/analysis/by_ref.rs b/compiler/src/analysis/by_ref.rs deleted file mode 100644 index 5b4a4e8..0000000 --- a/compiler/src/analysis/by_ref.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Decide, for each local and arg, whether emit writes it as a value or as a -//! pointer. -//! -//! A var used only by the function that owns it is a value. A var that also -//! goes into a sink is shared, and a shared var is a pointer. The sinks are a -//! call (callee, `this` or an arg), a field write (the value written, which -//! includes a capture written into a closure's environment), and a copy into -//! another value. A read is a copy, so an object var that is read is shared: -//! JS objects are references. -//! -//! A primitive cannot be changed in place, so sharing one is harmless and it -//! stays a value. The exception is a closure's environment: there the variable -//! itself is shared, and a later assignment must be seen through it. -//! -//! The result is one flag per var of each function: its locals, then its args. -//! A var's index is its value id. - -use crate::{ - infer::{InferResult, TypeCache, types::Type}, - libqjs::parse::opcodes::OpCode, - regify::{RegFn, RegInstr}, -}; - -pub fn find_by_ref(reg_fns: &[RegFn], cache: &TypeCache) -> Vec> { - let mut by_ref: Vec> = reg_fns - .iter() - .map(|f| vec![false; f.var_count + f.arg_count]) - .collect(); - - // A var is a pointer if it needs to be one in any of its function's - // variants: emit gives all the variants the same answer. - for (req, res) in cache.results() { - mark(®_fns[req.fn_id], res, &mut by_ref[req.fn_id]); - } - - by_ref -} - -fn mark(f: &RegFn, res: &InferResult, by_ref: &mut [bool]) { - let n_vars = by_ref.len(); - let code = || { - f.blocks - .iter() - .flat_map(|b| f.code[b.start..b.end].iter().chain(&b.postfix)) - }; - - // A read copies a var into a new value. Map each value to the var it is. - let mut var_of: Vec> = (0..f.val_count) - .map(|v| (v < n_vars).then_some(v)) - .collect(); - - for instr in code() { - if instr.op == OpCode::SetLoc && instr.args[0] < n_vars && instr.dsts[0] >= n_vars { - var_of[instr.dsts[0]] = Some(instr.args[0]); - } - } - - for instr in code() { - let into_env = instr.op == OpCode::DefineField - && matches!(res.val_types[instr.args[1]], Type::Closure { .. }); - - for &v in sinks(instr) { - if let Some(var) = var_of[v] { - if into_env || !is_primitive(&res.val_types[var]) { - by_ref[var] = true; - } - } - } - } -} - -/// The operands of `instr` that go somewhere other than the function itself. -fn sinks(instr: &RegInstr) -> &[usize] { - use OpCode::*; - - match instr.op { - Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod | TailCallMethod - | CallConstructor => &instr.args, - - // args are [value, object]: only the value goes into the object. - DefineField => &instr.args[..1], - - // A copy into another value: a read, a copy into another var, or a phi. - SetLoc if instr.dsts[0] != instr.args[0] => &instr.args, - - _ => &[], - } -} - -fn is_primitive(ty: &Type) -> bool { - matches!( - ty, - Type::Unknown | Type::Undefined | Type::Bool | Type::Int | Type::Float | Type::Native(_) - ) -} diff --git a/compiler/src/analysis/mod.rs b/compiler/src/analysis/mod.rs deleted file mode 100644 index 54701c4..0000000 --- a/compiler/src/analysis/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Analyses over the flattened program. They do not change the code; their -//! results are read by the later stages. - -pub mod by_ref; diff --git a/compiler/src/analyze/by_ref.rs b/compiler/src/analyze/by_ref.rs new file mode 100644 index 0000000..4a6b644 --- /dev/null +++ b/compiler/src/analyze/by_ref.rs @@ -0,0 +1,8 @@ +use crate::{infer::InferResult, regify::RegFn}; + +// TODO: new by_ref that is value centric in one loop +pub fn analyze_by_ref(f: &RegFn, res: &InferResult, by_ref: &mut [bool]) { + for v in 0..f.val_count { + todo!() + } +} diff --git a/compiler/src/analyze/mod.rs b/compiler/src/analyze/mod.rs new file mode 100644 index 0000000..3247bcd --- /dev/null +++ b/compiler/src/analyze/mod.rs @@ -0,0 +1,18 @@ +mod by_ref; + +use crate::{infer::TypeCache, regify::RegFn}; +use by_ref::analyze_by_ref; + +pub struct Analysis { + pub val_by_ref: Vec, +} + +impl Analysis { + fn of(reg_fn: &RegFn) -> Self { + todo!() + } +} + +pub fn analyze(fns: &[RegFn], cache: &TypeCache) -> Vec { + fns.iter().map(Analysis::of).collect() +} diff --git a/compiler/src/builtins.rs b/compiler/src/builtins.rs deleted file mode 100644 index 40c9f27..0000000 --- a/compiler/src/builtins.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Every builtin, in one place. -//! -//! A builtin is ordinary JS in [`PRELUDE`] (`lib/prelude.js`), which is -//! compiled together with the program, in front of it. Each builtin is then a -//! module binding like the program's own, and every stage handles it as user -//! code: infer walks only the ones that are called, and emit compiles them -//! like any other function. -//! -//! Where a builtin needs something JS cannot say, such as IO, its body calls a -//! C function. That is written as a call to a global named `__c_`, and -//! [`NATIVES`] declares each one: what it is called in C, and the type of what -//! it returns. Such a global is the only kind the program may read. - -use crate::infer::types::Type; - -pub const PRELUDE: &[u8] = include_bytes!("../../lib/prelude.js"); - -/// A C function that builtins call. -pub struct Native { - /// The global a builtin calls it by. - pub js_name: &'static str, - /// The function emit calls. - pub c_name: &'static str, - /// What the call returns, from the types of its args in source order. A - /// native does not write its args. - pub ret: fn(&[Type]) -> Type, -} - -pub static NATIVES: &[Native] = &[Native { - js_name: "__c_print", - c_name: "jsrt_print", - ret: |_| Type::Undefined, -}]; - -/// The native a global of this name calls, if it is one. -pub fn native(js_name: &str) -> Option { - NATIVES.iter().position(|n| n.js_name == js_name) -} diff --git a/compiler/src/emit.rs b/compiler/src/emit.rs deleted file mode 100644 index c94a8ad..0000000 --- a/compiler/src/emit.rs +++ /dev/null @@ -1,501 +0,0 @@ -//! C code generation. -//! -//! One struct per object shape, and one function per inferred -//! function-and-argument pair. The body is walked the same way inference walks -//! it — block by block, instruction by instruction — with one difference: -//! inference skips an instruction that defines nothing, but jumps and returns -//! define nothing and still have to be emitted. -//! -//! A function value is compile-time only: every call site knows the callee, so -//! calls go straight to the monomorphized function by name. The function is -//! never stored, so a closure's struct is its captured environment alone. - -use std::collections::HashMap; -use std::fmt::Write; - -use crate::infer::types::Type; -use crate::infer::{InferRequest, InferResult, TypeCache}; -use crate::libqjs::parse::{Atom, AtomRef, ImmOperand, JsValue, atom_to_str, opcodes::OpCode}; -use crate::model::{EXTRA_ARGS, FIELD_PROTO}; -use crate::regify::{BasicBlock, RegFn, RegInstr}; - -/// The C name of each monomorphized variant, keyed by its function and argument -/// types, so a call site and a definition agree. -type Names = HashMap<(usize, Vec), String>; - -pub fn emit(cache: &TypeCache, reg_fns: &[RegFn], atoms: &[Atom]) -> String { - let mut out = String::new(); - - out.push_str("// Generated by the jsrt compiler. Do not edit.\n\n"); - out.push_str("#include \n#include \n\n"); - out.push_str("// Placeholders, until the lattice can say more than this.\n"); - out.push_str("typedef struct { char _; } undefined_t;\n"); - out.push_str("typedef struct { char _; } any_t;\n"); - out.push_str("typedef struct { char _; } empty_t;\n\n"); - - let names = mangle(cache, reg_fns, atoms); - - emit_shapes(cache, atoms, &mut out); - - out.push_str("// ── prototypes ──\n"); - for (req, res) in sorted(cache) { - let _ = writeln!(out, "{};", signature(req, res, cache, &names)); - } - out.push('\n'); - - for (req, res) in sorted(cache) { - emit_fn(req, res, reg_fns, cache, atoms, &names, &mut out); - } - - out -} - -/// Results in a stable order, so the same program emits the same file. -fn sorted(cache: &TypeCache) -> Vec<(&InferRequest, &InferResult)> { - let mut rs: Vec<_> = cache.results().collect(); - rs.sort_by_key(|(req, _)| (req.fn_id, format!("{:?}", req.arg_types))); - rs -} - -/// One C name per function-and-argument pair: the name, its id, and a per- -/// function variant index. -fn mangle(cache: &TypeCache, reg_fns: &[RegFn], atoms: &[Atom]) -> Names { - let mut names = HashMap::new(); - let mut seen: HashMap = HashMap::new(); - - for (req, _) in sorted(cache) { - let variant = seen.entry(req.fn_id).or_insert(0); - let base = sanitize(&atom_to_str(®_fns[req.fn_id].name, atoms)); - let key = (req.fn_id, req.arg_types.clone()); - names.insert(key, format!("{}_{}_{}", base, req.fn_id, variant)); - *variant += 1; - } - - names -} - -fn name_of(fn_id: usize, arg_types: &[Type], names: &Names) -> String { - names[&(fn_id, arg_types.to_vec())].clone() -} - -fn signature(req: &InferRequest, res: &InferResult, cache: &TypeCache, names: &Names) -> String { - let params: Vec = req - .arg_types - .iter() - .enumerate() - .map(|(i, ty)| format!("{} a{i}", c_type(ty, cache))) - .collect(); - - format!( - "{} {}({})", - c_type(&res.ret_type, cache), - name_of(req.fn_id, &req.arg_types, names), - params.join(", ") - ) -} - -fn emit_fn( - req: &InferRequest, - res: &InferResult, - reg_fns: &[RegFn], - cache: &TypeCache, - atoms: &[Atom], - names: &Names, - out: &mut String, -) { - let f = ®_fns[req.fn_id]; - - let _ = writeln!(out, "// {}", atom_to_str(&f.name, atoms)); - let _ = writeln!(out, "{} {{", signature(req, res, cache, names)); - - // Every value gets a slot. Locals are not renamed, so a local and the - // values flowing through it share one. - for (v, ty) in res.val_types.iter().enumerate() { - let _ = writeln!(out, " {} v{v};", c_type(ty, cache)); - } - if !res.val_types.is_empty() { - out.push('\n'); - } - - for (bi, block) in f.blocks.iter().enumerate() { - let _ = writeln!(out, "L{bi}:;"); - - // Stack values that outlive the block are copied into the successor's - // slots, and that has to happen before control leaves the block — so - // the terminator is held back until the copies are out. - let body = &f.code[block.start..block.end]; - let (body, terminator) = match body.last() { - Some(last) if is_terminator(last.op) => (&body[..body.len() - 1], Some(last)), - _ => (body, None), - }; - - for instr in body { - emit_instr(instr, block, res, f, reg_fns, cache, atoms, names, out); - } - - for instr in &block.postfix { - emit_instr(instr, block, res, f, reg_fns, cache, atoms, names, out); - } - - if let Some(instr) = terminator { - emit_instr(instr, block, res, f, reg_fns, cache, atoms, names, out); - } - } - - out.push_str("}\n\n"); -} - -#[allow(clippy::too_many_arguments)] -fn emit_instr( - instr: &RegInstr, - block: &BasicBlock, - res: &InferResult, - f: &RegFn, - reg_fns: &[RegFn], - cache: &TypeCache, - atoms: &[Atom], - names: &Names, - out: &mut String, -) { - use OpCode::*; - - let a = |i: usize| format!("v{}", instr.args[i]); - - // A binary operand sits under its partner on the stack, so the left-hand - // side of the source expression is the *later* arg. - let bin = |op: &str| format!("{} {} {}", a(1), op, a(0)); - - let set = |expr: String, out: &mut String| { - for d in &instr.dsts { - let _ = writeln!(out, " v{d} = {expr};"); - } - }; - - match instr.op { - Nop | Drop => {} - - PushMinus1 => set("-1".into(), out), - Push0 => set("0".into(), out), - Push1 => set("1".into(), out), - Push2 => set("2".into(), out), - Push3 => set("3".into(), out), - Push4 => set("4".into(), out), - Push5 => set("5".into(), out), - Push6 => set("6".into(), out), - Push7 => set("7".into(), out), - PushI8 | PushI16 | PushI32 => { - let v = match instr.imm { - ImmOperand::I8(n) => n as i64, - ImmOperand::I16(n) => n as i64, - ImmOperand::I32(n) => n as i64, - _ => unreachable!(), - }; - set(v.to_string(), out); - } - PushTrue => set("true".into(), out), - PushFalse => set("false".into(), out), - - PushConst | PushConst8 => { - let ImmOperand::Const(k) = instr.imm else { - unreachable!() - }; - let expr = match &f.cpool[k as usize] { - JsValue::Int(n) => n.to_string(), - JsValue::Float(x) => format!("{x:?}"), - JsValue::BoolTrue => "true".into(), - JsValue::BoolFalse => "false".into(), - _ => unreachable!(), - }; - set(expr, out); - } - - Add => set(bin("+"), out), - Sub => set(bin("-"), out), - Mul => set(bin("*"), out), - Mod => set(bin("%"), out), - Div => set(format!("(double){} / (double){}", a(1), a(0)), out), - Neg => set(format!("-{}", a(0)), out), - Plus => set(a(0), out), - Inc => set(format!("{} + 1", a(0)), out), - Dec => set(format!("{} - 1", a(0)), out), - - Lt => set(bin("<"), out), - Lte => set(bin("<="), out), - Gt => set(bin(">"), out), - Gte => set(bin(">="), out), - Eq | StrictEq => set(bin("=="), out), - Neq | StrictNeq => set(bin("!="), out), - Not => set(format!("!{}", a(0)), out), - - Dup | SetLoc | SetName => { - let src = instr.args[0]; - for d in &instr.dsts { - emit_copy(*d, &res.val_types[*d], src, &res.val_types[src], atoms, out); - } - } - Undefined => set("(undefined_t){0}".into(), out), - - GetArg0 => set("a0".into(), out), - GetArg1 => set("a1".into(), out), - GetArg2 => set("a2".into(), out), - GetArg3 => set("a3".into(), out), - GetArg => { - let ImmOperand::Arg(k) = instr.imm else { - unreachable!() - }; - set(format!("a{k}"), out); - } - - // A new object, or a new closure: both start with no fields. A - // closure's function is compile-time only, so it leaves no trace here. - Object | Fclosure | Fclosure8 => { - let d = instr.dsts[0]; - let _ = writeln!( - out, - " v{d} = ({}){{0}};", - c_type(&res.val_types[d], cache) - ); - } - - DefineField => { - let ImmOperand::Atom(atom) = instr.imm else { - unreachable!() - }; - let obj = instr.args[1]; - let d = instr.dsts[0]; - - // A write-back names the object itself, so there is nothing to - // carry over. Otherwise the result is a different shape, and C will - // not assign one struct type to another — so the fields that - // survive are copied one at a time. - if d != obj { - for (carried, _) in res.val_types[obj].fields() { - if carried == atom { - continue; - } - let cn = field_name(carried, atoms); - let _ = writeln!(out, " v{d}.{cn} = v{obj}.{cn};"); - } - } - - let _ = writeln!(out, " v{d}.{} = {};", field_name(atom, atoms), a(0)); - } - - GetField => { - let ImmOperand::Atom(atom) = instr.imm else { - unreachable!() - }; - set(format!("{}.{}", a(0), field_name(atom, atoms)), out); - } - - Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod | TailCallMethod => { - emit_call(instr, res, reg_fns, names, out); - } - - Return | ReturnAsync => { - let _ = writeln!(out, " return {};", a(0)); - } - ReturnUndef => { - let _ = writeln!(out, " return ({}){{0}};", c_type(&res.ret_type, cache)); - } - - Goto | Goto8 | Goto16 => { - let _ = writeln!(out, " goto L{};", block.succ[0]); - } - IfFalse | IfFalse8 => { - let _ = writeln!(out, " if (!{}) goto L{};", a(0), block.succ[0]); - if let Some(fallthrough) = block.succ.get(1) { - let _ = writeln!(out, " goto L{fallthrough};"); - } - } - IfTrue | IfTrue8 => { - let _ = writeln!(out, " if ({}) goto L{};", a(0), block.succ[0]); - if let Some(fallthrough) = block.succ.get(1) { - let _ = writeln!(out, " goto L{fallthrough};"); - } - } - - other => { - let _ = writeln!(out, " /* TODO {other:?} */"); - } - } -} - -/// Move one value into another's slot. -/// -/// A local's slot is declared with the type it ends up holding, so an earlier -/// assignment to it can be a different shape. C will not assign one struct type -/// to another, so the fields the two shapes share are carried over one at a -/// time. Fields the destination does not have yet are simply not set. -fn emit_copy( - dst: usize, - dst_ty: &Type, - src: usize, - src_ty: &Type, - atoms: &[Atom], - out: &mut String, -) { - if dst_ty == src_ty || !is_object(dst_ty) || !is_object(src_ty) { - let _ = writeln!(out, " v{dst} = v{src};"); - return; - } - - for (name, _) in src_ty.fields() { - if !dst_ty.fields().any(|(n, _)| n == name) { - continue; - } - let n = field_name(name, atoms); - let _ = writeln!(out, " v{dst}.{n} = v{src}.{n};"); - } -} - -/// Control leaves the block at these, so nothing may be emitted after one. -fn is_terminator(op: OpCode) -> bool { - use OpCode::*; - matches!( - op, - Goto | Goto8 - | Goto16 - | IfFalse - | IfFalse8 - | IfTrue - | IfTrue8 - | Return - | ReturnUndef - | ReturnAsync - ) -} - -/// Something with a struct representation whose fields can be copied. A closure -/// counts: its representation is its environment. -fn is_object(ty: &Type) -> bool { - matches!( - ty, - Type::Empty | Type::WithField { .. } | Type::Closure { .. } - ) -} - -/// The frame is the one inference reads: `[this,] callee, arg0 .. argN`, with -/// `args` in pop order. The request is rebuilt exactly as inference built it, so -/// the variant is an exact lookup, and the callee is emitted by name. -fn emit_call( - instr: &RegInstr, - res: &InferResult, - reg_fns: &[RegFn], - names: &Names, - out: &mut String, -) { - use OpCode::*; - - let is_method = matches!(instr.op, CallMethod | TailCallMethod); - let n = instr.args.len(); - let (callee_idx, this, arg_end) = if is_method { - (instr.args[n - 2], Some(instr.args[n - 1]), n - 2) - } else { - (instr.args[n - 1], None, n - 1) - }; - - let Type::Closure { fn_id, .. } = res.val_types[callee_idx] else { - unreachable!() - }; - - let declared = reg_fns[fn_id].arg_count - EXTRA_ARGS; - let mut arg_types = Vec::with_capacity(declared + EXTRA_ARGS); - let mut args = Vec::with_capacity(declared + EXTRA_ARGS); - - let mut push = |ty: Type, expr: String| { - arg_types.push(ty); - args.push(expr); - }; - - for i in 0..declared { - match arg_end.checked_sub(i + 1) { - Some(k) if i < arg_end => { - let v = instr.args[k]; - push(res.val_types[v].clone(), format!("v{v}")); - } - _ => push(Type::Undefined, "(undefined_t){0}".to_string()), - } - } - - match this { - Some(v) => push(res.val_types[v].clone(), format!("v{v}")), - None => push(Type::Undefined, "(undefined_t){0}".to_string()), - } - - push(res.val_types[callee_idx].clone(), format!("v{callee_idx}")); - - let call = format!("{}({})", name_of(fn_id, &arg_types, names), args.join(", ")); - - if instr.dsts.is_empty() { - let _ = writeln!(out, " {call};"); - } else { - for d in &instr.dsts { - let _ = writeln!(out, " v{d} = {call};"); - } - } -} - -fn emit_shapes(cache: &TypeCache, atoms: &[Atom], out: &mut String) { - let mut shapes: Vec<_> = cache.shapes().collect(); - shapes.sort_by_key(|(_, id)| id.0); - - for (ty, id) in shapes { - let _ = writeln!(out, "typedef struct {{"); - - for (name, value) in ty.fields() { - let _ = writeln!( - out, - " {} {};", - c_type(value, cache), - field_name(name, atoms) - ); - } - - let _ = writeln!(out, "}} Shape{};\n", id.0); - } -} - -fn c_type(ty: &Type, cache: &TypeCache) -> String { - match ty { - Type::Int => "int32_t".to_string(), - Type::Float => "double".to_string(), - Type::Bool => "bool".to_string(), - Type::Undefined => "undefined_t".to_string(), - // Unknown means the value was never inferred: dead code, or a known - // gap. It has no real type, so it renders as undefined. - Type::Unknown => "undefined_t".to_string(), - Type::Any => "any_t".to_string(), - // A closure is its environment; the function is compile-time only. - Type::Closure { env, .. } => c_type(env, cache), - // An object with no fields carries nothing, so it needs no struct. - Type::Empty => "empty_t".to_string(), - // A native is called by name, so its value carries nothing. - Type::Native(_) => "empty_t".to_string(), - Type::WithField { .. } => format!("Shape{}", cache.shape_id(ty).unwrap().0), - } -} - -fn field_name(name: AtomRef, atoms: &[Atom]) -> String { - if name == FIELD_PROTO { - return "proto".to_string(); - } - - match name { - AtomRef::TaggedInt(n) => format!("i{n}"), - _ => sanitize(&atom_to_str(&name, atoms)), - } -} - -fn sanitize(s: &str) -> String { - let mut out: String = s - .chars() - .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) - .collect(); - - if !out.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') { - out.insert(0, '_'); - } - - out -} diff --git a/compiler/src/flatten.rs b/compiler/src/flatten.rs index bd5e6da..d57c7d2 100644 --- a/compiler/src/flatten.rs +++ b/compiler/src/flatten.rs @@ -14,50 +14,13 @@ pub struct FlatFn { pub fn flatten(bin: QuickJSBinary) -> (Vec, Vec>, Vec) { let mut fns = vec![]; let mut captures = vec![]; + walk(bin.root_mod.root_fn, &mut fns, &mut captures); - append_entry(&mut fns, &mut captures); + add_entry_point(&mut fns, &mut captures); + (fns, captures, bin.atoms) } -/// Append the synthetic program entry, the host the runtime would provide, -/// written in the IR so the ordinary passes handle it. It drives the module -/// body `` (fn 0): build its environment record, run the link phase -/// (`this = true`), then the evaluate phase (`this = false`). -/// -/// `fclosure8` of fn 0 lets `declosure` build the record from ``'s own -/// captures — the module bindings — so no stage needs bespoke module knowledge. -/// The closure is made once and kept in local 0, so both phases run on the one -/// record and the link phase's writes reach the evaluate phase. `call_method` -/// carries the phase flag as the receiver (`this`); a plain call could not set -/// it. -fn append_entry(fns: &mut Vec, captures: &mut Vec>) { - let mk = |pc, op, imm| Instr { pc, op, imm }; - let code = vec![ - mk(0, OpCode::Fclosure8, ImmOperand::Const(0)), - mk(1, OpCode::PutLoc, ImmOperand::Loc(0)), - mk(2, OpCode::PushTrue, ImmOperand::None), - mk(3, OpCode::GetLoc, ImmOperand::Loc(0)), - mk(4, OpCode::CallMethod, ImmOperand::Npop(0)), - mk(5, OpCode::Drop, ImmOperand::None), - mk(6, OpCode::PushFalse, ImmOperand::None), - mk(7, OpCode::GetLoc, ImmOperand::Loc(0)), - mk(8, OpCode::CallMethod, ImmOperand::Npop(0)), - mk(9, OpCode::Drop, ImmOperand::None), - mk(10, OpCode::ReturnUndef, ImmOperand::None), - ]; - - fns.push(FlatFn { - name: AtomRef::Predefined(0), - arg_count: 0, - // Local 0 holds the closure of ``. - var_count: 1, - cpool: vec![JsValue::FnById(0)], - code, - }); - - captures.push(vec![]); -} - fn walk(func: Function, flat_fns: &mut Vec, captures: &mut Vec>) -> usize { let id = flat_fns.len(); @@ -83,3 +46,46 @@ fn walk(func: Function, flat_fns: &mut Vec, captures: &mut Vec() { +/// // Module top level created by quickjs +/// // All other top level decls are inside +/// function () { +/// ... +/// } +/// +/// .call(true); // Link phase (`this = true`) +/// .call(false); // Run phase (`this = false`) +/// } +/// ``` +/// +/// 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 }; + + let code = vec![ + instr(0, OpCode::Fclosure8, ImmOperand::Const(0)), + instr(1, OpCode::PutLoc, ImmOperand::Loc(0)), + instr(2, OpCode::PushTrue, ImmOperand::None), + instr(3, OpCode::GetLoc, ImmOperand::Loc(0)), + instr(4, OpCode::CallMethod, ImmOperand::Npop(0)), + instr(5, OpCode::Drop, ImmOperand::None), + instr(6, OpCode::PushFalse, ImmOperand::None), + instr(7, OpCode::GetLoc, ImmOperand::Loc(0)), + instr(8, OpCode::CallMethod, ImmOperand::Npop(0)), + instr(9, OpCode::Drop, ImmOperand::None), + instr(10, OpCode::ReturnUndef, ImmOperand::None), + ]; + + fns.push(FlatFn { + name: AtomRef::Predefined(0), + arg_count: 0, + var_count: 1, + cpool: vec![JsValue::FnById(0)], + code, + }); + + captures.push(vec![]); +} diff --git a/compiler/src/infer/mod.rs b/compiler/src/infer/mod.rs index bc3de2d..c0aa883 100644 --- a/compiler/src/infer/mod.rs +++ b/compiler/src/infer/mod.rs @@ -1,18 +1,15 @@ -#![allow(non_snake_case)] +pub mod types; use std::collections::HashMap; -pub mod types; - -use types::{Type, get_field, join_ty, set_field}; - use crate::{ - builtins, libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode}, - model::EXTRA_ARGS, + native, regify::RegFn, }; +use types::{Type, get_field, join_ty, set_field}; + #[derive(Debug, Hash, PartialEq, Eq)] pub struct InferRequest { pub fn_id: usize, @@ -25,73 +22,47 @@ pub struct InferResult { pub ret_type: Type, } -/// Names one object shape. Emit turns each into a struct. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ShapeId(pub usize); - -/// What inference accumulated: a result per function-and-argument pair, and a -/// number for every distinct object shape. Shapes are kept exactly as built, so -/// two objects with the same fields in a different order are two shapes. #[derive(Debug, Default)] pub struct TypeCache { results: HashMap, - shapes: HashMap, - next_shape: usize, + shapes: HashMap, + next_shape_id: usize, } impl TypeCache { - pub fn get(&self, req: &InferRequest) -> Option<&InferResult> { + pub fn get_result(&self, req: &InferRequest) -> Option<&InferResult> { self.results.get(req) } - pub fn insert(&mut self, req: InferRequest, res: InferResult) { + pub fn insert_result(&mut self, req: InferRequest, res: InferResult) { self.results.insert(req, res); } - /// Number this shape, reusing the number if it has been seen before. - pub fn shape_of(&mut self, ty: &Type) -> ShapeId { - if let Some(id) = self.shapes.get(ty) { - return *id; + pub fn insert_shape(&mut self, ty: &Type) { + if !self.shapes.contains_key(ty) { + self.shapes.insert(ty.clone(), self.next_shape_id); + self.next_shape_id += 1; } - - let id = ShapeId(self.next_shape); - self.next_shape += 1; - self.shapes.insert(ty.clone(), id); - id - } - - /// The number already given to this shape, if it has one. - pub fn shape_id(&self, ty: &Type) -> Option { - self.shapes.get(ty).copied() - } - - pub fn results(&self) -> impl Iterator { - self.results.iter() - } - - pub fn shapes(&self) -> impl Iterator { - self.shapes.iter().map(|(ty, id)| (ty, *id)) } } -pub fn infer_types(reg_fns: &[RegFn]) -> TypeCache { - // The synthetic program entry is appended last by `flatten`. It is called - // by the host with nothing, so its own parameters are undefined. - let entry = reg_fns.len() - 1; - +pub fn infer_types(fns: &[RegFn]) -> TypeCache { let mut cache = TypeCache::default(); + // The synthetic program entry is appended last by `flatten`. It is called + // by the host with no args let req = InferRequest { - fn_id: entry, - arg_types: vec![Type::Undefined; reg_fns[entry].arg_count], + fn_id: fns.len() - 1, + arg_types: vec![], }; - let res = walk_fn(&req, reg_fns, &mut cache); - cache.insert(req, res); + let res = walk_fn(&req, fns, &mut cache); + cache.insert_result(req, res); cache } +#[allow(non_snake_case)] fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult { use OpCode::*; @@ -99,25 +70,23 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe let mut val_types = vec![Type::Unknown; f.val_count]; let mut ret_type = Type::Unknown; - // Args are not renamed: arg `k` is value id `var_count + k`, and holds what - // the caller passed until the function writes it. - for (k, ty) in req.arg_types.iter().enumerate() { - val_types[f.var_count + k] = ty.clone(); + let mut old_types = val_types.clone(); + + // arg `i` is value id `var_count + i` (see regify) + // Init them with the arg types passed in + for (i, ty) in req.arg_types.iter().enumerate() { + val_types[f.var_count + i] = ty.clone(); } - // Types only grow, so walk until they stop changing. Then every - // instruction has seen the final type of what it reads, including what a - // later write or a loop back edge adds. For that, every rule must be - // monotone: `Unknown` means nothing has reached a value yet, so a rule - // that needs it gives `Unknown` too, never a guess that it later outgrows. + // Walk instrs till types stop changing loop { - let prev = (val_types.clone(), ret_type.clone()); + old_types.clone_from_slice(&val_types); // TODO: We only compare val types not ret type for block in &f.blocks { let code = &f.code[block.start..block.end]; let code = code.iter().chain(&block.postfix); - 'instrs: for instr in code { + for instr in code { let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef); if instr.dsts.is_empty() && !is_ret { @@ -126,7 +95,7 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe // TODO: Some of these ops may not every show up by this point let ty = match instr.op { - // ──────────────────────────────────── Literals ──────────────────────────────────── + // ────────────────────────────────────────── Literals ─────────────────────────────────────────────────────── PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8 | PushI16 | PushI32 => Type::Int, @@ -137,13 +106,13 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe Some(JsValue::Int(_)) => Type::Int, Some(JsValue::Float(_)) => Type::Float, Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool, - Some(JsValue::Native(id)) => Type::Native(*id), + Some(JsValue::NativeFn(id)) => Type::NativeFn(*id), _ => unreachable!(), }, _ => unreachable!(), }, - // ─────────────────────────────────── Arithmetic ─────────────────────────────────── + // ──────────────────────────────────────── Arithmetic ─────────────────────────────────────────────────────── Add | Sub | Mul | Mod | Pow => { match (&val_types[instr.args[0]], &val_types[instr.args[1]]) { (Type::Unknown, _) | (_, Type::Unknown) => Type::Unknown, @@ -164,24 +133,27 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool, - // ────────────────────────────────── Value Movers ────────────────────────────────── + // ─────────────────────────────────────── Value Movers ────────────────────────────────────────────────────── SetLoc | SetName => val_types[instr.args[0]].clone(), Undefined => Type::Undefined, - // ──────────────────────────────────── Objects ───────────────────────────────────── - Object => Type::Empty, - // A closure of the child function, with nothing captured yet. - // The captures that follow are written into its environment. - Fclosure | Fclosure8 => match instr.imm { - ImmOperand::Const(k) => match f.cpool[k as usize] { - JsValue::FnById(fn_id) => Type::Closure { - fn_id, - env: Box::new(Type::Empty), - }, + // ───────────────────────────────────────── Objects ───────────────────────────────────────────────────────── + Fclosure | Fclosure8 => { + let cpool_idx = match instr.imm { + ImmOperand::Const(idx) => idx as usize, _ => unreachable!(), - }, - _ => unreachable!(), - }, + }; + + let fn_id = match f.cpool[cpool_idx] { + JsValue::FnById(id) => id, + _ => unreachable!(), + }; + + Type::Closure { + fn_id, + env: Box::new(Type::Empty), + } + } DefineField => { let field = match instr.imm { ImmOperand::Atom(atom) => atom, @@ -189,125 +161,23 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe }; let obj = &val_types[instr.args[1]]; - - if *obj == Type::Unknown { - continue; - } - let ty = set_field(obj, field, &val_types[instr.args[0]]); - // Register the shape that will be emitted. A closure's is - // its environment; an empty environment needs no struct. let shape = match &ty { Type::Closure { env, .. } => env.as_ref(), other => other, }; - if matches!(shape, Type::WithField { .. }) { - cache.shape_of(shape); - } + cache.insert_shape(shape); ty } - GetField => { - let field = match instr.imm { - ImmOperand::Atom(atom) => atom, - _ => unreachable!(), - }; + Object => Type::Empty, - let ty = val_types[instr.args[0]].clone(); - get_field(&ty, field) - } - - // ──────────────────────────────── Function Calls ────────────────────────────────── + // ───────────────────────────────────── Function Calls ────────────────────────────────────────────────────── Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod - | TailCallMethod => 'call: { - // args are in pop order: [this,] callee, arg0 .. arg_n. A - // method call carries a receiver beneath the callee; a plain - // call has none, so its `this` is undefined. - let is_method = matches!(instr.op, CallMethod | TailCallMethod); - let n = instr.args.len(); + | TailCallMethod => todo!(), - let (callee_idx, this, arg_end) = if is_method { - ( - instr.args[n - 2], - val_types[instr.args[n - 1]].clone(), - n - 2, - ) - } else { - (instr.args[n - 1], Type::Undefined, n - 1) - }; - - let fn_id = match val_types[callee_idx] { - Type::Closure { fn_id, .. } => fn_id, - - // A C function has no body to walk: its return type - // is declared, from the args in source order. - Type::Native(id) => { - let args: Vec = instr.args[..arg_end] - .iter() - .rev() - .map(|&v| val_types[v].clone()) - .collect(); - - break 'call (builtins::NATIVES[id].ret)(&args); - } - - // Nothing has reached the callee yet, so nothing - // reaches the result either. - Type::Unknown => continue 'instrs, - _ => unreachable!(), - }; - - // The callee sees its own params, then this, then the - // environment. The actuals are padded or truncated to the - // declared arity, so the last two land at a fixed index - // whatever the caller passed. - let callee = ®_fns[fn_id]; - let declared = callee.arg_count - EXTRA_ARGS; - let mut arg_types = Vec::with_capacity(callee.arg_count); - - // The value each param was passed from, if any. - let mut operands = Vec::with_capacity(callee.arg_count); - - for i in 0..declared { - let v = (i < arg_end).then(|| instr.args[arg_end - 1 - i]); - arg_types.push(v.map_or(Type::Undefined, |v| val_types[v].clone())); - operands.push(v); - } - - arg_types.push(this); - operands.push(is_method.then(|| instr.args[n - 1])); - - arg_types.push(val_types[callee_idx].clone()); - operands.push(Some(callee_idx)); - - let req = InferRequest { fn_id, arg_types }; - - // What the call returns, and what its params hold at the - // end: the callee's writes to them. - let params = callee.var_count..callee.var_count + callee.arg_count; - let (ret_ty, outs) = match cache.get(&req) { - Some(res) => (res.ret_type.clone(), res.val_types[params].to_vec()), - None => { - let res = walk_fn(&req, reg_fns, cache); - let summary = (res.ret_type.clone(), res.val_types[params].to_vec()); - cache.insert(req, res); - summary - } - }; - - // The caller passed these values, so it sees the callee's - // writes to them. - for (v, out) in operands.into_iter().zip(&outs) { - if let Some(v) = v { - val_types[v] = join_ty(&val_types[v], out); - } - } - - ret_ty - } - - // ────────────────────────────────────── Returns ─────────────────────────────────── + // ──────────────────────────────────────── Returns ────────────────────────────────────────────────────────── Return | ReturnAsync => val_types[instr.args[0]].clone(), ReturnUndef => Type::Undefined, @@ -322,26 +192,29 @@ fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> Infe } } - // A copy of an object is the same object, so what is done - // through the copy is done to the original as well. - if instr.op == SetLoc && is_object(&val_types[instr.dsts[0]]) { - let (src, dst) = (instr.args[0], instr.dsts[0]); - val_types[src] = join_ty(&val_types[src], &val_types[dst]); - } - } - } + // A copy of an object created by SetLoc is the same object, (see regify) + // so what is done through the copy is done to the original as well + if instr.op == SetLoc { + let is_object = matches!( + val_types[instr.dsts[0]], + Type::Empty | Type::WithField { .. } | Type::Closure { .. } + ); - if (&val_types, &ret_type) == (&prev.0, &prev.1) { + if is_object { + let (src, dst) = (instr.args[0], instr.dsts[0]); + val_types[src] = join_ty(&val_types[src], &val_types[dst]); + } + } + } // instr + } // block + + if old_types == val_types { break; } - } + } // loop InferResult { val_types, ret_type, } } - -fn is_object(ty: &Type) -> bool { - matches!(ty, Type::Empty | Type::WithField { .. } | Type::Closure { .. }) -} diff --git a/compiler/src/infer/types.rs b/compiler/src/infer/types.rs index 732356f..aff0a06 100644 --- a/compiler/src/infer/types.rs +++ b/compiler/src/infer/types.rs @@ -1,11 +1,4 @@ -//! The type lattice, and how an object type is read and written. -//! -//! An object is a chain of `WithField` links. Everything that reads or writes -//! one goes through this file: `Fields` walks it, `get_field` looks one field -//! up, `set_field` writes one. - -use crate::libqjs::parse::AtomRef; -use crate::model::{FIELD_PROTO, MAX_PROTO_DEPTH}; +use crate::{libqjs::parse::AtomRef, transform::PROTO_FIELD}; #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Type { @@ -15,52 +8,31 @@ pub enum Type { Int, Float, Empty, - /// A closure: a function together with its captured environment. `fn_id` - /// is set by the `fclosure` that creates it and selects the code; it is - /// compile-time only, never a field, never emitted. `env` is the record of - /// captures, written into as ordinary fields, and that is what becomes a - /// struct. Two closures over the same captures share one env shape. Closure { fn_id: usize, env: Box, }, - /// A C function, by its index in `builtins::NATIVES`. Only a builtin's - /// body reads one, and only to call it. - Native(usize), - /// An object with field `name` of type `value`, over the object `rest`. - /// A chain of these is how a field bag builds up. + NativeFn(usize), WithField { name: AtomRef, - value: Box, + ty: Box, rest: Box, }, Any, } -/// Stood in for a chain that ran deeper than [`MAX_PROTO_DEPTH`]. Pointing the -/// cursor here makes [`Fields::tail`] answer `Any`, so an over-deep chain gives -/// up conservatively instead of reporting a field as absent. -static TOO_DEEP: Type = Type::Any; - -/// Walks the fields of an object type. +/// Iterator over a type's fields /// -/// In prototype mode the own fields come first and the prototype's after, so a -/// name the object defines itself shadows the prototype's simply by arriving -/// first. That also means a name can be yielded more than once in this mode: -/// the first occurrence is the one that counts. Own mode yields each name once. +/// `follow_proto` sets whether we iterate within the type's prototype field too. +/// The type's own fields come first, so a field the object defines itself shadows +/// the prototype's +/// +/// That also means a field name can be yielded more than once, and the first occurrence +/// is the one that counts pub struct Fields<'a> { current: &'a Type, proto: Option<&'a Type>, - follow: bool, - depth: usize, -} - -impl<'a> Fields<'a> { - /// What the chain ended on: `Empty` for an object whose fields are fully - /// known, `Any` when the shape is not, anything else for a non-object. - pub fn tail(&self) -> &'a Type { - self.current - } + follow_proto: bool, } impl<'a> Iterator for Fields<'a> { @@ -69,144 +41,55 @@ impl<'a> Iterator for Fields<'a> { fn next(&mut self) -> Option { loop { match self.current { - Type::WithField { name, value, rest } => { - if *name == FIELD_PROTO && self.follow { - self.proto = Some(value); + Type::WithField { name, ty, rest } => { + if *name == PROTO_FIELD && self.follow_proto { + self.proto = Some(ty); } self.current = rest; - return Some((*name, value)); + return Some((*name, ty)); } - // A closure's fields are its environment's. The function itself - // is compile-time only, so it is never enumerated. Type::Closure { env, .. } => self.current = env, - // Own fields are exhausted. Step out to the prototype, if we - // follow them and there is one left to step to. - _ => match self.proto.take() { - Some(proto) if self.depth < MAX_PROTO_DEPTH => { - self.depth += 1; - self.current = proto; - } - Some(_) => { - self.current = &TOO_DEEP; - return None; - } - None => return None, - }, + // Own fields are exhausted, try step out to the prototype + _ => self.current = self.proto.take()?, } } } } impl Type { - /// The object's own fields, not those of its prototype. + /// The object's own fields, not those of its prototype pub fn fields(&self) -> Fields<'_> { Fields { current: self, proto: None, - follow: false, - depth: 0, + follow_proto: false, } } - /// The object's own fields, then its prototype's, and so on outward. - pub fn fields_through_proto(&self) -> Fields<'_> { + /// The object's own fields, then its prototype's, and so on + pub fn fields_via_proto(&self) -> Fields<'_> { Fields { current: self, proto: None, - follow: true, - depth: 0, + follow_proto: true, } } } -/// Write `field` onto `obj`. A field the object already has is joined with the -/// new value where it sits, so a name appears at most once and a chain is -/// bounded by the number of distinct fields rather than the number of writes. -/// Writing a type the field already covers leaves the object alone. -pub fn set_field(obj: &Type, field: AtomRef, value: &Type) -> Type { - match obj { - // A field written onto a closure goes into its environment. - Type::Closure { fn_id, env } => Type::Closure { - fn_id: *fn_id, - env: Box::new(set_field(env, field, value)), - }, - - Type::WithField { - name, - value: v, - rest, - } if *name == field => { - let joined = join_ty(v, value); - - if joined == **v { - return obj.clone(); - } - - Type::WithField { - name: field, - value: Box::new(joined), - rest: rest.clone(), - } - } - - Type::WithField { - name, - value: v, - rest, - } => { - let new_rest = set_field(rest, field, value); - - // Nothing changed deeper down, so nothing changes here either. - if new_rest == **rest { - return obj.clone(); - } - - Type::WithField { - name: *name, - value: v.clone(), - rest: Box::new(new_rest), - } - } - - _ => Type::WithField { - name: field, - value: Box::new(value.clone()), - rest: Box::new(obj.clone()), - }, - } -} - -/// Look `field` up on `ty`, own fields first and the prototype's after, which -/// is how a method defined on a prototype is found from an instance. A field -/// no write has reached is `Unknown`, not `Undefined`: inference is still -/// growing the object, and a later write may add it. Emit reads it as -/// undefined. -pub fn get_field(ty: &Type, field: AtomRef) -> Type { - let mut it = ty.fields_through_proto(); - let found = it.find(|(name, _)| *name == field); - - match found { - Some((_, value)) => value.clone(), - // Nothing named it, and the shape was not fully known, so it might - // still have been there. - None if matches!(it.tail(), Type::Any) => Type::Any, - None => Type::Unknown, - } -} - /// Type lattice join rules: -/// Unknown * T = T -/// Any * T = Any -/// T * T = T -/// Closure(f, e) * Closure(f, e') = Closure(f, e * e') -/// object A * object B = every field of A and B, each field joined -/// T * U = Any +/// Unknown * T = T +/// Any * T = Any +/// T * T = T +/// T * U = Any /// -/// Objects join field by field, so an object that gains a field only grows, -/// and a field written with two different types becomes `Any`. +/// Composite objects can be joined too: +/// Closure(f, e) * Closure(f, e') = Closure(f, e * e') +/// object A * object B = every field of A and B, each field joined +/// +/// TODO: Where are composite object joins required? pub fn join_ty(a: &Type, b: &Type) -> Type { match (a, b) { (Type::Unknown, t) | (t, Type::Unknown) => t.clone(), @@ -227,3 +110,63 @@ pub fn join_ty(a: &Type, b: &Type) -> Type { _ => Type::Any, } } + +/// Do a look-up of `field` on `obj`, going through own fields then prototype +/// chain's fields +pub fn get_field(obj: &Type, field: AtomRef) -> &Type { + obj.fields_via_proto() + .find(|(name, _)| *name == field) + .map(|(_, ty)| ty) + .unwrap_or(&Type::Undefined) +} + +/// Write `field` into `obj` +/// +/// If the object type already has a field by that name, the types are joined, +/// else a new type with `field` is created +pub fn set_field(obj: &Type, field: AtomRef, field_ty: &Type) -> Type { + match obj { + // A field written onto a closure goes into its environment. + Type::Closure { fn_id, env } => Type::Closure { + fn_id: *fn_id, + env: Box::new(set_field(env, field, field_ty)), + }, + + // Write into existing field, join types + Type::WithField { name, ty, rest } if *name == field => { + let joined = join_ty(ty, field_ty); + + if joined == **ty { + return obj.clone(); + } + + Type::WithField { + name: field, + ty: Box::new(joined), + rest: rest.clone(), + } + } + + Type::WithField { name, ty, rest } => { + let new_rest = set_field(rest, field, field_ty); + + // Nothing changed deeper down, so nothing changes here either. + // TODO: Avoid this recursion and deep eq checking + if new_rest == **rest { + return obj.clone(); + } + + Type::WithField { + name: *name, + ty: ty.clone(), + rest: Box::new(new_rest), + } + } + + _ => Type::WithField { + name: field, + ty: Box::new(field_ty.clone()), + rest: Box::new(obj.clone()), + }, + } +} diff --git a/compiler/src/libqjs/parse.rs b/compiler/src/libqjs/parse.rs index ba78cb2..2fcbf1e 100644 --- a/compiler/src/libqjs/parse.rs +++ b/compiler/src/libqjs/parse.rs @@ -168,9 +168,8 @@ pub enum JsValue { }, Function(Function), FnById(usize), - /// A C function, by its index in `builtins::NATIVES`. Transform puts it in - /// place of the global a builtin calls it by. - Native(usize), + /// A C function, by its index in `native::NATIVES` + NativeFn(usize), Module(Module), TypedArray, ArrayBuffer, @@ -897,39 +896,3 @@ impl Debug for JsString { f.write_char('"') } } - -/// Resolve an atom reference to its text, for names and diagnostics. -pub fn atom_to_str(a: &AtomRef, atoms: &[Atom]) -> String { - match a { - AtomRef::TaggedInt(n) => n.to_string(), - AtomRef::Predefined(i) => atom_names::PREDEFINED_ATOMS - .get(*i as usize) - .copied() - .unwrap_or("") - .to_string(), - AtomRef::FileLocal(i) => atoms - .get(*i) - .map(atom_text) - .unwrap_or_else(|| format!("")), - } -} - -pub fn atom_text(a: &Atom) -> String { - match a { - Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s), - Atom::Predefined(n) => format!(""), - } -} - -pub fn js_string(s: &JsString) -> String { - match s { - JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(), - JsString::UTF16(b) => { - let u: Vec = b - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) - .collect(); - String::from_utf16_lossy(&u) - } - } -} diff --git a/compiler/src/main.rs b/compiler/src/main.rs index b119c8e..f2e2081 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -1,6 +1,8 @@ +mod analyze; mod flatten; mod infer; mod libqjs; +mod native; mod regify; mod transform; @@ -30,7 +32,7 @@ fn main() { // Process it let (flat_fns, captures, atoms) = flatten(binary); - let flat_fns = transform(flat_fns, &captures); + let flat_fns = transform(flat_fns, &captures, &atoms); let reg_fns = regify(flat_fns); - let type_cache = infer_types(®_fns, &atoms); + let ty_cache = infer_types(®_fns); } diff --git a/compiler/src/model.rs b/compiler/src/model.rs deleted file mode 100644 index 9eadc94..0000000 --- a/compiler/src/model.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Reserved field names of the lowered object model. -//! -//! Every lowered object is a plain bag of fields. Two names are reserved for -//! the compiler. Both are quickjs internal atoms, spelled `<...>`, which -//! JavaScript source cannot produce, so neither can collide with a user -//! property. - -use crate::libqjs::parse::AtomRef; - -/// An instance's prototype object. A field lookup that misses on an object -/// itself follows this link and retries. -pub const FIELD_PROTO: AtomRef = AtomRef::Predefined(124); // "" - -/// How many prototype links one lookup follows before giving up and -/// answering `Any`. Guards against a chain that loops. -pub const MAX_PROTO_DEPTH: usize = 16; - -/// Parameters a lowered function gains beyond its source arity: `this`, -/// then the environment. They are the last two, in that order. -pub const EXTRA_ARGS: usize = 2; diff --git a/compiler/src/native.rs b/compiler/src/native.rs new file mode 100644 index 0000000..cf278cc --- /dev/null +++ b/compiler/src/native.rs @@ -0,0 +1,42 @@ +use crate::{ + infer::types::Type, + libqjs::parse::{Atom, AtomRef, JsString}, +}; + +/// A native C function callable by JS +pub struct NativeFn { + pub name: &'static str, + pub ret: fn(&[Type]) -> Type, // TODO: switch to strictly monomorphic +} + +static NATIVES: &[NativeFn] = &[NativeFn { + name: "__native_print", + ret: |_| Type::Undefined, +}]; + +pub fn native_fn_idx(name: AtomRef, atoms: &[Atom]) -> Option { + let AtomRef::FileLocal(idx) = name else { + return None; + }; + + let atom = atoms.get(idx)?; + + let js_string = match atom { + Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => s, + _ => return None, + }; + + // TODO: Avoid string alloc + let 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) + } + }; + + NATIVES.iter().position(|n| n.name == string) +} diff --git a/compiler/src/regify/mod.rs b/compiler/src/regify/mod.rs index 4153dd7..73b8197 100644 --- a/compiler/src/regify/mod.rs +++ b/compiler/src/regify/mod.rs @@ -8,7 +8,7 @@ use crate::{ pub use basic_block::BasicBlock; use basic_block::find_basic_blocks; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct RegInstr { pub dsts: Vec, pub op: OpCode, @@ -19,7 +19,7 @@ pub struct RegInstr { impl RegInstr { fn nop() -> Self { Self { - dsts: vec![], + dsts: vec![], // TODO: do we ever have more than one dst? op: OpCode::Nop, imm: ImmOperand::None, args: vec![], @@ -27,23 +27,13 @@ impl RegInstr { } } -impl std::fmt::Debug for RegInstr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{:?} = {:?} {:?} {:?}", - self.dsts, self.op, self.imm, self.args - ) - } -} - #[derive(Debug)] pub struct RegFn { pub name: AtomRef, pub arg_count: usize, + pub var_count: usize, /// Locals and args are not renamed: local `k` is value id `k`, and arg `k` /// is value id `var_count + k`. - pub var_count: usize, pub val_count: usize, pub cpool: Vec, pub code: Vec, @@ -51,32 +41,41 @@ pub struct RegFn { } impl RegFn { - fn from_flat(flat_fn: FlatFn) -> Self { - let mut blocks = find_basic_blocks(&flat_fn.code); - let mut out = vec![RegInstr::nop(); flat_fn.code.len()]; + fn from_flat(f: FlatFn) -> Self { + let mut blocks = find_basic_blocks(&f.code); + let mut out = vec![RegInstr::nop(); f.code.len()]; // Reserve values for locals, then args - let mut next_val = flat_fn.var_count + flat_fn.arg_count; + let mut next_val = f.var_count + f.arg_count; + // The stack each block recieves at entry from its predecessors let mut entry_stacks: Vec>> = vec![None; blocks.len()]; - let mut worklist = vec![]; + // Walk blocks in CFG order, starting from block 0 + let mut worklist = vec![]; entry_stacks[0] = Some(vec![]); worklist.push(0); while let Some(block_idx) = worklist.pop() { let block = &mut blocks[block_idx]; - let mut stack: Vec = entry_stacks[block_idx].clone().unwrap_or_default(); + let mut entry_stack = entry_stacks[block_idx].clone().unwrap_or_default(); - walk_block(block, &flat_fn.code, flat_fn.var_count, &mut stack, &mut next_val, &mut out); + walk_block( + block, + &f.code, + f.var_count, + &mut entry_stack, + &mut next_val, + &mut out, + ); for succ_idx in block.succ.clone() { let succ_stack = &mut entry_stacks[succ_idx]; match succ_stack { - Some(succ_stack) => add_block_postfix(block, &stack, succ_stack), + Some(succ_stack) => add_block_postfix(block, &entry_stack, succ_stack), None => { - *succ_stack = Some(stack.clone()); + *succ_stack = Some(entry_stack.clone()); worklist.push(succ_idx); } } @@ -84,17 +83,18 @@ impl RegFn { } Self { - name: flat_fn.name, - arg_count: flat_fn.arg_count, - var_count: flat_fn.var_count, + name: f.name, + arg_count: f.arg_count, + var_count: f.var_count, val_count: next_val, - cpool: flat_fn.cpool, + cpool: f.cpool, code: out, blocks, } } } +/// Walk a basic block's instructions, converting each instr to register form fn walk_block( block: &BasicBlock, code: &[Instr], @@ -129,51 +129,37 @@ fn walk_block( out_instr.args.push(v); } - // Locals and args are not renamed, their read/writes need special handling - // Handle read of a local or arg value - match is_var_read(instr, var_count) { - // A read takes the var's value at this point. The stack holds a - // copy, never the var itself. - Some(var) => { - let v = fresh_val(); - stack.push(v); - out_instr.op = OpCode::SetLoc; - out_instr.imm = ImmOperand::None; - out_instr.dsts.push(v); - out_instr.args.push(var); - out[instr_idx] = out_instr; - continue; - } - None => (), + // Locals and args are not renamed, so their read/writes need special handling + // Handle read of a local or arg + if let Some(var) = is_var_read(instr, var_count) { + let v = fresh_val(); + stack.push(v); + out_instr.op = OpCode::SetLoc; + out_instr.imm = ImmOperand::None; + out_instr.dsts.push(v); + out_instr.args.push(var); + out[instr_idx] = out_instr; + continue; } // Handle write of a local or arg value - match is_var_write(instr, var_count) { - Some(loc) => { - out_instr.op = OpCode::SetLoc; - out_instr.imm = ImmOperand::None; - out_instr.dsts.push(loc); + if let Some(var) = is_var_write(instr, var_count) { + out_instr.op = OpCode::SetLoc; + out_instr.imm = ImmOperand::None; + out_instr.dsts.push(var); - // set_loc also leaves the value it stored on the stack. - if n_push == 1 { - stack.push(out_instr.args[0]); - } + // set_loc also leaves the value it stored on the stack. + if n_push == 1 { + stack.push(out_instr.args[0]); // args set by previos pops loop + } - out[instr_idx] = out_instr; - continue; - } - None if instr.op == OpCode::SetLocUninitialized => { - out_instr.op = OpCode::Nop; - out_instr.imm = ImmOperand::None; - out[instr_idx] = out_instr; - continue; - } - None => (), + out[instr_idx] = out_instr; + continue; } - // set_home_object binds [[HomeObject]], which only `super` reads, and - // super is not supported. It returns both its operands unchanged, so on - // values it is nothing at all: hand them straight back. + // Some more special cases: + // set_home_object binds [[HomeObject]], which only `super` reads, + // `super` is unsupported currently, It returns both its operands unchanged if instr.op == OpCode::SetHomeObject { stack.push(out_instr.args[1]); stack.push(out_instr.args[0]); @@ -183,8 +169,7 @@ fn walk_block( continue; } - // dup only copies a stack slot; it computes nothing. The copy is the - // same value, so whatever is done through it is done to the original. + // Handle `Dup` and `Swap`, which just push values if instr.op == OpCode::Dup { stack.push(out_instr.args[0]); stack.push(out_instr.args[0]); @@ -194,7 +179,6 @@ fn walk_block( continue; } - // swap only reorders the stack; it computes nothing. if instr.op == OpCode::Swap { stack.push(out_instr.args[0]); stack.push(out_instr.args[1]); @@ -204,10 +188,7 @@ fn walk_block( continue; } - // A field write is a define whose result lands back in the object's own - // value, so later reads of that object see the field. This is what lets - // a chain of writes to one local build a single object type, and it is - // why no later stage needs to know PutField at all. + // A field write is a define whose result lands back in the object's own value if instr.op == OpCode::PutField { out_instr.op = OpCode::DefineField; out_instr.dsts.push(out_instr.args[1]); @@ -215,7 +196,7 @@ fn walk_block( continue; } - // Handle this instr's pushes + // Normal case: handle this instr's pushes for _ in 0..n_push { let v = fresh_val(); stack.push(v); @@ -226,6 +207,7 @@ fn walk_block( } } +/// Some instrs have extra pops not reflected by instr.op.info().n_pop fn extra_pops(instr: &Instr) -> usize { match instr.op { OpCode::Call0 => 0, @@ -303,6 +285,11 @@ fn is_var_write(instr: &Instr, var_count: usize) -> Option { } } +/// Adds a postfix to a block to join values with a successor's entry stack +/// +/// This does the job of phi nodes in typical SSA, Currently only relevant to +/// ternary operators because ternaries are the only place where JS becomes +/// "expression oriented" fn add_block_postfix(block: &mut BasicBlock, stack: &[usize], succ_stack: &[usize]) { let overlap = stack.iter().rev().zip(succ_stack.iter().rev()); diff --git a/compiler/src/transform/declass.rs b/compiler/src/transform/declass.rs index ed7f513..4863cd5 100644 --- a/compiler/src/transform/declass.rs +++ b/compiler/src/transform/declass.rs @@ -14,46 +14,54 @@ pub struct ClassSequence { } impl ClassSequence { + fn emit_len(&self) -> usize { + 3 + (2 * self.methods.len()) + } + pub fn emit_prototype_form(&self, out: &mut Vec) { - out.push(Instr { - pc: self.start_pc, - op: OpCode::Fclosure8, - imm: ImmOperand::Const(self.ctor_fn_cpool_idx), - }); + let mut push = |op, imm| { + out.push(Instr { + pc: self.start_pc, + op, + imm, + }) + }; - // define_class left the constructor with its prototype above it, and the - // code after the class body stores that constructor into the class - // variable. get_field consumes what it reads, so duplicate first. - out.push(Instr { - pc: self.start_pc, - op: OpCode::Dup, - imm: ImmOperand::None, - }); - out.push(Instr { - pc: self.start_pc, - op: OpCode::GetField, - imm: ImmOperand::Atom(AtomRef::Predefined(63)), // .prototype - }); + push(OpCode::Fclosure8, ImmOperand::Const(self.ctor_fn_cpool_idx)); + push(OpCode::Dup, ImmOperand::None); + push(OpCode::GetField, ImmOperand::Atom(AtomRef::Predefined(63))); // .prototype - // define_field hands the prototype back, so every method defines onto - // that one object and the lookup is not repeated. for m in &self.methods { - out.push(Instr { - pc: self.start_pc, - op: OpCode::Fclosure8, - imm: ImmOperand::Const(m.fn_cpool_idx), - }); - out.push(Instr { - pc: self.start_pc, - op: OpCode::DefineField, - imm: ImmOperand::Atom(m.name), - }); + push(OpCode::Fclosure8, ImmOperand::Const(m.fn_cpool_idx)); + push(OpCode::DefineField, ImmOperand::Atom(m.name)); } } } pub fn declass(mut code: &[Instr]) -> Vec { - let mut out = Vec::with_capacity(code.len()); + let orig = code; + + // Calculate size of output + let mut len = 0; + + while !code.is_empty() { + match match_class_seq(code) { + Ok((rest, seq)) => { + len += seq.emit_len(); + code = rest; + } + Err(rest) => { + let consumed = code.len() - rest.len(); // Handle partial consumption + let consumed = consumed.max(1); + len += consumed; + code = &code[consumed..]; + } + } + } + + // Emit output + code = orig; + let mut out = Vec::with_capacity(len); while !code.is_empty() { match match_class_seq(code) { @@ -70,6 +78,7 @@ pub fn declass(mut code: &[Instr]) -> Vec { } } + debug_assert!(out.len() == len); out } diff --git a/compiler/src/transform/mod.rs b/compiler/src/transform/mod.rs index 3845fa9..aa86fc9 100644 --- a/compiler/src/transform/mod.rs +++ b/compiler/src/transform/mod.rs @@ -1,15 +1,16 @@ mod declass; +use declass::declass; + use crate::{ - builtins, flatten::FlatFn, libqjs::parse::{ - Atom, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, atom_to_str, opcodes::OpCode, + Atom, AtomRef, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode, }, - model::{EXTRA_ARGS, FIELD_PROTO}, + native::native_fn_idx, }; -use declass::declass; +pub const PROTO_FIELD: AtomRef = AtomRef::Predefined(124); // TODO: Should this be or __proto__ or what? pub fn transform( flat_fns: Vec, @@ -29,53 +30,43 @@ fn transform_one( captures: &[Vec], atoms: &[Atom], ) -> FlatFn { - let declassed = declass(&f.code); + let code = declass(&f.code); - // Every function gains two parameters after its own, in this order. The - // indices are static, so neither needs a sentinel. - let this_idx = f.arg_count as u16; - let env_idx = f.arg_count as u16 + 1; + // `this` and `env` are passed as the last 2 args to every fn + let _this_arg = f.arg_count; + let env_arg = f.arg_count + 1; - let mut out_len = 0; + // Calculate transformed out len + let mut len = 0; - for instr in &declassed { - if let Some(info) = is_fclosure(instr, &f, captures) { - // fclosure; (load; define_field)* - out_len += 1 + info.child_captures.iter().map(capture_len).sum::(); + for instr in &code { + if let Some(child_captures) = is_fclosure(instr, &f, captures) { + len += 1 + child_captures.iter().map(capture_len).sum::(); // fclosure; (load; define_field)* continue; } - // get_arg env; swap; put_field - if let Some(CaptureAccess::Write(_)) = is_capture_acc(instr) { - out_len += 3; + if let Some(acc) = is_capture_acc(instr) { + len += acc.out_len(); continue; } - if is_capture_acc(instr).is_some() { - out_len += 2; + if let Some(seq) = is_expansion(instr) { + len += seq.len(); continue; } - if let Some(seq) = expansion(instr.op) { - out_len += seq.len(); - continue; - } - - out_len += 1; + len += 1; } - let mut out = Vec::with_capacity(out_len); + // Emit transformed output + let mut out = Vec::with_capacity(len); - for instr in &declassed { - if replace_fclosure(instr, &f, captures, &captures[fn_id], env_idx, &mut out) { + for instr in &code { + if replace_fclosure(instr, &f, captures, &captures[fn_id], env_arg, &mut out) { continue; } - if replace_capture_accs(instr, &captures[fn_id], env_idx, &mut out) { - continue; - } - - if replace_push_this(instr, this_idx, &mut out) { + if replace_capture_acc(instr, &captures[fn_id], env_arg, &mut out) { continue; } @@ -87,128 +78,98 @@ fn transform_one( continue; } - out.push(renamed(instr).unwrap_or_else(|| instr.clone())); + out.push(instr.clone()); } - debug_assert!(out.len() == out_len); + debug_assert!(out.len() == len); FlatFn { name: f.name, - arg_count: f.arg_count + EXTRA_ARGS, + arg_count: f.arg_count + 2, // 2 extra args, `this` and `env` var_count: f.var_count, cpool: f.cpool, code: out, } } -/// `this` is an ordinary parameter now, so reading it is an ordinary arg read. -fn replace_push_this(instr: &Instr, this_idx: u16, out: &mut Vec) -> bool { - if instr.op != OpCode::PushThis { - return false; - } - - out.push(Instr { - pc: instr.pc, - op: OpCode::GetArg, - imm: ImmOperand::Arg(this_idx), - }); - - true -} - -struct FClosureInfo<'a> { - child_captures: &'a [ClosureVar], -} +// ─────────────────────────────────────────────── Closure Processing ──────────────────────────────────────────────────────────── +/// If `instr` is a closure decl, return the child closure's captures fn is_fclosure<'a>( instr: &Instr, f: &FlatFn, captures: &'a [Vec], -) -> Option> { +) -> Option<&'a [ClosureVar]> { if !matches!(instr.op, OpCode::Fclosure8 | OpCode::Fclosure) { return None; } let cpool_idx = match instr.imm { - ImmOperand::Const(idx) => idx, + ImmOperand::Const(idx) => idx as usize, _ => unreachable!(), }; - let child_fn_id = match f.cpool[cpool_idx as usize] { + let child_fn_id = match f.cpool[cpool_idx] { JsValue::FnById(id) => id, _ => unreachable!(), }; - Some(FClosureInfo { - child_captures: &captures[child_fn_id], - }) + Some(&captures[child_fn_id]) } +/// Replace: +/// fclosure fn_const +/// +/// With: +/// fclosure fn_const +/// (push_capture; define_field capture_name)* +/// +/// ie the implicit closures are turned into explicit fields on the +/// fclosure object which serves as the environment record fn replace_fclosure( instr: &Instr, f: &FlatFn, captures: &[Vec], my_captures: &[ClosureVar], - env_idx: u16, + env_arg: usize, out: &mut Vec, ) -> bool { - let info = match is_fclosure(instr, f, captures) { - Some(info) => info, + let child_captures = match is_fclosure(instr, f, captures) { + Some(c) => c, None => return false, }; - // The closure keeps the op that makes it: `fclosure` creates a closure of the - // child with an empty environment, and each capture is written into that - // environment as an ordinary field. - out.push(instr.clone()); - - for c in info.child_captures { - if !load_capture(c, my_captures, env_idx, instr.pc, out) { - continue; - } - + let mut push = |op, imm| { out.push(Instr { pc: instr.pc, - op: OpCode::DefineField, - imm: ImmOperand::Atom(c.name), - }); - } + op, + imm, + }) + }; - true -} + push(OpCode::Fclosure, instr.imm); -/// Load the value a child closure captures, from where it lives in this frame. -/// `var_idx` means something different per kind: a local slot, an argument, or -/// one of this function's own captures — which, after lowering, is a field of -/// its env. A module binding has no frame at all, so nothing is loaded and the -/// capture is not written: its field first appears when `` writes it, -/// and a read before that finds no field and gives undefined. -fn load_capture( - c: &ClosureVar, - my_captures: &[ClosureVar], - env_idx: u16, - pc: usize, - out: &mut Vec, -) -> bool { - let mut push = |op, imm| out.push(Instr { pc, op, imm }); - - match c.flags.closure_kind { - ClosureKind::Local => push(OpCode::GetLoc, ImmOperand::Loc(c.var_idx as u16)), - ClosureKind::Arg => push(OpCode::GetArg, ImmOperand::Arg(c.var_idx as u16)), - ClosureKind::Ref => { - push(OpCode::GetArg, ImmOperand::Arg(env_idx)); - push( - OpCode::GetField, - ImmOperand::Atom(my_captures[c.var_idx].name), - ); + for c in child_captures { + match c.flags.closure_kind { + ClosureKind::Local => push(OpCode::GetLoc, ImmOperand::Loc(c.var_idx as u16)), + ClosureKind::Arg => push(OpCode::GetArg, ImmOperand::Arg(c.var_idx as u16)), + ClosureKind::Ref => { + push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16)); + push( + OpCode::GetField, + ImmOperand::Atom(my_captures[c.var_idx].name), + ); + } + _ => return false, } - _ => return false, + + push(OpCode::DefineField, ImmOperand::Atom(c.name)); } true } -/// How many instructions one capture takes: its load, then its define_field. +/// How many instructions one capture takes: its load, then its define_field fn capture_len(c: &ClosureVar) -> usize { match c.flags.closure_kind { ClosureKind::Local | ClosureKind::Arg => 2, @@ -217,11 +178,22 @@ fn capture_len(c: &ClosureVar) -> usize { } } +// ──────────────────────────────────────────── Capture Access Processing ──────────────────────────────────────────────────────── enum CaptureAccess { Read(u16), Write(u16), } +impl CaptureAccess { + fn out_len(&self) -> usize { + match self { + Self::Read(_) => 2, // See: replace_capture_acc() + Self::Write(_) => 3, + } + } +} + +/// Is `instr` an access of a captured variable? fn is_capture_acc(instr: &Instr) -> Option { let acc = match instr.op { OpCode::GetVarRef0 => CaptureAccess::Read(0), @@ -248,80 +220,120 @@ fn is_capture_acc(instr: &Instr) -> Option { Some(acc) } -fn replace_capture_accs( +/// Replace: +/// read/write_captured_var +/// +/// With: +/// get_env_arg +/// read_write_env_arg +fn replace_capture_acc( instr: &Instr, my_captures: &[ClosureVar], - env_idx: u16, + env_arg: usize, out: &mut Vec, ) -> bool { - let acc = is_capture_acc(instr); - - let acc = match acc { + let acc = match is_capture_acc(instr) { Some(acc) => acc, None => return false, }; - let pc = instr.pc; + let mut push = |op, imm| { + out.push(Instr { + pc: instr.pc, + op, + imm, + }) + }; match acc { CaptureAccess::Read(read_var) => { let atom = my_captures[read_var as usize].name; - - out.push(Instr { - pc, - op: OpCode::GetArg, - imm: ImmOperand::Arg(env_idx), - }); - - out.push(Instr { - pc, - op: OpCode::GetField, - imm: ImmOperand::Atom(atom), - }); + push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16)); + push(OpCode::GetField, ImmOperand::Atom(atom)); } CaptureAccess::Write(write_var) => { let atom = my_captures[write_var as usize].name; - // The value is already on the stack, so env lands above it, and - // put_field wants the object below the value. - out.push(Instr { - pc, - op: OpCode::GetArg, - imm: ImmOperand::Arg(env_idx), - }); - - out.push(Instr { - pc, - op: OpCode::Swap, - imm: ImmOperand::None, - }); - - out.push(Instr { - pc, - op: OpCode::PutField, - imm: ImmOperand::Atom(atom), - }); + // The value to write is already on the stack, so env lands above it, + // and put_field wants the object below the value + push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16)); + push(OpCode::Swap, ImmOperand::None); + push(OpCode::PutField, ImmOperand::Atom(atom)); } } true } -/// Ops that are a fixed sequence of simpler ones. `true` passes the original -/// immediate on to that op. -fn expansion(op: OpCode) -> Option<&'static [(OpCode, bool)]> { +// ───────────────────────────────────────────────── Misc Expansion ────────────────────────────────────────────────────────────── +#[allow(non_snake_case)] +fn is_expansion(instr: &Instr) -> Option<[Instr; 4]> { use OpCode::*; - let seq: &'static [(OpCode, bool)] = match op { - TailCall => &[(Call, true), (Return, false)], - TailCallMethod => &[(CallMethod, true), (Return, false)], - GetField2 => &[(Dup, false), (GetField, true)], - GetLoc0Loc1 => &[(GetLoc0, false), (GetLoc1, false)], - IncLoc => &[(GetLoc, true), (Inc, false), (PutLoc, true)], - DecLoc => &[(GetLoc, true), (Dec, false), (PutLoc, true)], - // The added value is on the stack; the local is the left operand. - AddLoc => &[(GetLoc, true), (Swap, false), (Add, false), (PutLoc, true)], + let ins = |op, imm| Instr { + pc: instr.pc, + op, + imm, + }; + + let nop = || Instr { + pc: instr.pc, + op: Nop, + imm: ImmOperand::None, + }; + + let seq = match instr.op { + TailCall => [ + nop(), + nop(), + ins(Call, instr.imm), + ins(Return, ImmOperand::None), + ], + TailCallMethod => [ + nop(), + nop(), + ins(CallMethod, instr.imm), + ins(Return, ImmOperand::None), + ], + GetField2 => [ + nop(), + nop(), + ins(Dup, ImmOperand::None), + ins(GetField, instr.imm), + ], + GetLoc0Loc1 => [ + nop(), + nop(), + ins(GetLoc0, ImmOperand::None), + ins(GetLoc1, ImmOperand::None), + ], + IncLoc => [ + nop(), + ins(GetLoc, instr.imm), + ins(Inc, ImmOperand::None), + ins(PutLoc, instr.imm), + ], + DecLoc => [ + nop(), + ins(GetLoc, instr.imm), + ins(Dec, ImmOperand::None), + ins(PutLoc, instr.imm), + ], + AddLoc => [ + ins(GetLoc, instr.imm), + ins(Swap, ImmOperand::None), // The added value is on the stack, the local is the left operand + ins(Add, ImmOperand::None), + ins(PutLoc, instr.imm), + ], + SetProto => [ + nop(), + nop(), + nop(), + ins(DefineField, ImmOperand::Atom(PROTO_FIELD)), + ], + DefineMethod => [nop(), nop(), nop(), ins(DefineField, instr.imm)], // TODO: should we discriminate on instr.imm? + SetLocUninitialized => [nop(), nop(), nop(), nop()], _ => return None, }; @@ -329,39 +341,31 @@ fn expansion(op: OpCode) -> Option<&'static [(OpCode, bool)]> { } fn replace_expansion(instr: &Instr, out: &mut Vec) -> bool { - let Some(seq) = expansion(instr.op) else { + let Some(seq) = is_expansion(instr) else { return false; }; - for &(op, keep_imm) in seq { - let imm = if keep_imm { instr.imm } else { ImmOperand::None }; - out.push(Instr { pc: instr.pc, op, imm }); - } - + out.extend_from_slice(&seq); true } -/// The only globals a program reads are the C functions builtins call. Each -/// becomes a constant naming it. fn replace_get_var( instr: &Instr, atoms: &[Atom], cpool: &mut Vec, out: &mut Vec, ) -> bool { - let ImmOperand::Atom(atom) = instr.imm else { - return false; - }; - if instr.op != OpCode::GetVar { return false; } - let name = atom_to_str(&atom, atoms); - let native = builtins::native(&name).unwrap_or_else(|| panic!("unknown global `{name}`")); + let ImmOperand::Atom(name) = instr.imm else { + return false; + }; - cpool.push(JsValue::Native(native)); + let native_idx = native_fn_idx(name, atoms).unwrap(); + cpool.push(JsValue::NativeFn(native_idx)); out.push(Instr { pc: instr.pc, op: OpCode::PushConst, @@ -370,27 +374,3 @@ fn replace_get_var( true } - -/// Ops that are another op with a different immediate. -fn renamed(instr: &Instr) -> Option { - let define_field = |atom| Instr { - pc: instr.pc, - op: OpCode::DefineField, - imm: ImmOperand::Atom(atom), - }; - - match (instr.op, instr.imm) { - // `{ __proto__: p }`: the prototype is an ordinary field. - (OpCode::SetProto, _) => Some(define_field(FIELD_PROTO)), - - // A method in an object literal is a field holding a closure. The low - // two bits of `arg` say method, getter or setter; only methods are - // supported. - (OpCode::DefineMethod, ImmOperand::AtomU8 { atom, arg }) => { - assert!(arg & 3 == 0, "getters and setters are not supported"); - Some(define_field(atom)) - } - - _ => None, - } -} diff --git a/lib/prelude.js b/lib/prelude.js deleted file mode 100644 index 9d4a373..0000000 --- a/lib/prelude.js +++ /dev/null @@ -1,7 +0,0 @@ -const Object = { - create(proto) { return { __proto__: proto }; }, -}; - -const console = { - log(x) { __c_print(x); }, -}; diff --git a/lib/stdlib.js b/lib/stdlib.js new file mode 100644 index 0000000..2efb233 --- /dev/null +++ b/lib/stdlib.js @@ -0,0 +1,9 @@ +const Object = { + create(proto) { return { __proto__: proto }; }, +}; + +const console = { + // TODO: all __native_* fns should be monomorphic, console.log() should act a bit like Rust's write!/print! + // and break down complex printing in JS-land + log(x) { __native_print(x); }, +};