From e93ce94ac769740bca971f77f18cf0c7bfea907a Mon Sep 17 00:00:00 2001 From: uditdey Date: Fri, 25 Sep 2026 19:31:15 +0000 Subject: [PATCH] Big progress by Claude --- compiler/src/analysis/by_ref.rs | 95 ++++++ compiler/src/analysis/mod.rs | 4 + compiler/src/builtins.rs | 38 +++ compiler/src/emit.rs | 501 ++++++++++++++++++++++++++++++ compiler/src/flatten.rs | 44 ++- compiler/src/infer.rs | 390 ----------------------- compiler/src/infer/mod.rs | 347 +++++++++++++++++++++ compiler/src/infer/types.rs | 229 ++++++++++++++ compiler/src/libqjs/mod.rs | 5 +- compiler/src/libqjs/parse.rs | 39 +++ compiler/src/model.rs | 4 - compiler/src/regify/mod.rs | 107 +++++-- compiler/src/transform/declass.rs | 21 +- compiler/src/transform/mod.rs | 216 ++++++++++--- lib/prelude.js | 7 + 15 files changed, 1577 insertions(+), 470 deletions(-) create mode 100644 compiler/src/analysis/by_ref.rs create mode 100644 compiler/src/analysis/mod.rs create mode 100644 compiler/src/builtins.rs create mode 100644 compiler/src/emit.rs delete mode 100644 compiler/src/infer.rs create mode 100644 compiler/src/infer/mod.rs create mode 100644 compiler/src/infer/types.rs create mode 100644 lib/prelude.js diff --git a/compiler/src/analysis/by_ref.rs b/compiler/src/analysis/by_ref.rs new file mode 100644 index 0000000..5b4a4e8 --- /dev/null +++ b/compiler/src/analysis/by_ref.rs @@ -0,0 +1,95 @@ +//! 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 new file mode 100644 index 0000000..54701c4 --- /dev/null +++ b/compiler/src/analysis/mod.rs @@ -0,0 +1,4 @@ +//! 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/builtins.rs b/compiler/src/builtins.rs new file mode 100644 index 0000000..40c9f27 --- /dev/null +++ b/compiler/src/builtins.rs @@ -0,0 +1,38 @@ +//! 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 new file mode 100644 index 0000000..c94a8ad --- /dev/null +++ b/compiler/src/emit.rs @@ -0,0 +1,501 @@ +//! 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 5a42040..bd5e6da 100644 --- a/compiler/src/flatten.rs +++ b/compiler/src/flatten.rs @@ -1,4 +1,6 @@ -use crate::libqjs::parse::{Atom, AtomRef, ClosureVar, Function, Instr, JsValue, QuickJSBinary}; +use crate::libqjs::parse::{ + Atom, AtomRef, ClosureVar, Function, ImmOperand, Instr, JsValue, QuickJSBinary, opcodes::OpCode, +}; #[derive(Debug)] pub struct FlatFn { @@ -13,9 +15,49 @@ pub fn flatten(bin: QuickJSBinary) -> (Vec, Vec>, Vec` (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(); diff --git a/compiler/src/infer.rs b/compiler/src/infer.rs deleted file mode 100644 index a9220dc..0000000 --- a/compiler/src/infer.rs +++ /dev/null @@ -1,390 +0,0 @@ -#![allow(non_snake_case)] - -use std::collections::HashMap; - -use crate::{ - infer::Type::ReturnOf, - libqjs::parse::{Atom, AtomRef, ImmOperand, JsString, JsValue, atom_names, opcodes::OpCode}, - model::{EXTRA_ARGS, FIELD_FN, FIELD_PROTO, MAX_PROTO_DEPTH}, - regify::{RegFn, RegInstr}, -}; - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum FnKind { - Id(usize), - DefineField(AtomRef), -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum Type { - Unknown, - Undefined, - Bool, - Int, - Float, - Empty, - Fn(usize), - ReturnOf((FnKind, Vec)), - Any, -} - -/// Type lattice join rules: -/// Unknown * T = T -/// Any * T = Any -/// T * T = T -/// T * U = Any -fn join_ty(a: &Type, b: &Type) -> Type { - match (a, b) { - (Type::Unknown, t) | (t, Type::Unknown) => t.clone(), - (Type::Any, _) | (_, Type::Any) => Type::Any, - (a, b) if a == b => a.clone(), - _ => Type::Any, - } -} - -/// Result of looking a field up on one object, ignoring its prototype. -enum Lookup { - Found(Type), - Missing, - Any, -} - -/// Look `field` up on `ty`. A miss on the object itself follows the prototype -/// link and retries, which is how a method defined on a prototype is found -/// from an instance. -fn get_field(ty: &Type, field: AtomRef, cache: &mut TypeCache) -> Type { - let mut current = ty.clone(); - - for _ in 0..MAX_PROTO_DEPTH { - match own_field(¤t, field, cache) { - Lookup::Found(t) => return t, - Lookup::Any => return Type::Any, - Lookup::Missing => match own_field(¤t, FIELD_PROTO, cache) { - Lookup::Found(proto) => current = proto, - _ => return Type::Undefined, - }, - } - } - - // The chain loops, or is deeper than anything real. Stop guessing. - Type::Any -} - -/// Walk one object's own field chain, without following its prototype. -fn own_field(ty: &Type, field: AtomRef, cache: &mut TypeCache) -> Lookup { - let mut current = ty.clone(); - - loop { - match current { - Type::Any => return Lookup::Any, - - Type::ReturnOf((fn_kind, args)) => match fn_kind { - FnKind::DefineField(atom) => { - debug_assert!(args.len() == 2); - - if atom == field { - return Lookup::Found(args[0].clone()); - } - - current = args[1].clone(); - } - - FnKind::Id(id) => { - match cache.get(&InferRequest { - fn_id: id, - arg_types: args.clone(), - }) { - Some(info) => current = info.ret_type.clone(), - // Not inferred yet, so the shape is unknown rather - // than impossible. - None => return Lookup::Any, - } - } - }, - - _ => return Lookup::Missing, - } - } -} - -/// A call frame, in the order the operands were pushed: -/// -/// [this,] callee, arg0 .. arg_n -/// -/// `instr.args` is in pop order, so the callee is last for a plain call, and -/// second to last for a method call, which carries a receiver beneath it. A -/// plain call has no receiver slot, so its `this` is undefined. -struct CallFrame { - /// The closure object being called. It is also the callee's environment. - callee: Type, - this: Type, - /// The actual arguments, back in source order. - args: Vec, -} - -fn call_frame(instr: &RegInstr, val_types: &[Type]) -> CallFrame { - let is_method = matches!(instr.op, OpCode::CallMethod | OpCode::TailCallMethod); - let n = instr.args.len(); - - let (callee, this, rest) = if is_method { - ( - val_types[instr.args[n - 2]].clone(), - val_types[instr.args[n - 1]].clone(), - &instr.args[..n - 2], - ) - } else { - ( - val_types[instr.args[n - 1]].clone(), - Type::Undefined, - &instr.args[..n - 1], - ) - }; - - CallFrame { - callee, - this, - args: rest.iter().rev().map(|a| val_types[*a].clone()).collect(), - } -} - -/// The parameters a lowered function sees: its own, then `this`, then the -/// environment. The actuals are padded or truncated to the declared arity, so -/// the last two always land at a fixed index whatever the caller passed. -fn arg_types_for(callee: &RegFn, frame: &CallFrame) -> Vec { - let declared = callee.arg_count.saturating_sub(EXTRA_ARGS); - let mut out = Vec::with_capacity(declared + EXTRA_ARGS); - - for i in 0..declared { - out.push(frame.args.get(i).cloned().unwrap_or(Type::Undefined)); - } - - out.push(frame.this.clone()); - out.push(frame.callee.clone()); - out -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub struct InferRequest { - pub fn_id: usize, - pub arg_types: Vec, -} - -#[derive(Debug)] -pub struct InferResult { - pub val_types: Vec, - pub ret_type: Type, -} - -pub type TypeCache = HashMap; - -pub fn infer_types(reg_fns: &[RegFn], atoms: &[Atom]) -> TypeCache { - // Find main fn - let main_fn_id = reg_fns - .iter() - .position(|f| atom_to_str(&f.name, atoms) == "main") - .unwrap(); - - let mut cache = TypeCache::with_capacity(reg_fns.len()); - - // main is called by nobody, so every parameter it has — including this - // and its environment — is undefined. - let req = InferRequest { - fn_id: main_fn_id, - arg_types: vec![Type::Undefined; reg_fns[main_fn_id].arg_count], - }; - - let res = walk_fn(&req, reg_fns, &mut cache); - cache.insert(req, res); - - cache -} - -fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult { - use OpCode::*; - - let f = ®_fns[req.fn_id]; - let mut val_types = vec![Type::Unknown; f.val_count]; - let mut ret_type = Type::Unknown; - - for block in &f.blocks { - let code = &f.code[block.start..block.end]; - let code = code.iter().chain(&block.postfix); - - for instr in code { - let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef); - - if instr.dsts.is_empty() && !is_ret { - continue; - } - - // TODO: Some of these ops may not every show up by this point - let ty = match instr.op { - // ──────────────────────────────────── Literals ──────────────────────────────────── - PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 - | PushI8 | PushI16 | PushI32 => Type::Int, - - PushTrue | PushFalse => Type::Bool, - - PushConst | PushConst8 => match instr.imm { - ImmOperand::Const(k) => match f.cpool.get(k as usize) { - Some(JsValue::Int(_)) => Type::Int, - Some(JsValue::Float(_)) => Type::Float, - Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool, - Some(JsValue::FnById(id)) => Type::Fn(*id), - _ => unreachable!(), - }, - _ => unreachable!(), - }, - - // ─────────────────────────────────── Arithmetic ─────────────────────────────────── - Add | Sub | Mul | Mod | Pow => { - match (&val_types[instr.args[0]], &val_types[instr.args[1]]) { - (Type::Int, Type::Int) => Type::Int, - (Type::Float, Type::Float) - | (Type::Int, Type::Float) - | (Type::Float, Type::Int) => Type::Float, - _ => Type::Any, - } - } - Neg | Plus | Inc | Dec => match val_types[instr.args[0]] { - Type::Int => Type::Int, - Type::Float => Type::Float, - _ => Type::Any, - }, - Div => Type::Float, - - Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool, - - // ────────────────────────────────── Value Movers ────────────────────────────────── - Dup | SetLoc | SetName => val_types[instr.args[0]].clone(), - Undefined => Type::Undefined, - - // ──────────────────────────────────── Arguments ─────────────────────────────────── - GetArg0 => req.arg_types.first().cloned().unwrap_or(Type::Undefined), - GetArg1 => req.arg_types.get(1).cloned().unwrap_or(Type::Undefined), - GetArg2 => req.arg_types.get(2).cloned().unwrap_or(Type::Undefined), - GetArg3 => req.arg_types.get(3).cloned().unwrap_or(Type::Undefined), - GetArg => match instr.imm { - ImmOperand::Arg(k) => req - .arg_types - .get(k as usize) - .cloned() - .unwrap_or(Type::Undefined), - _ => unreachable!(), - }, - - // ──────────────────────────────────── Objects ───────────────────────────────────── - Object => Type::Empty, - DefineField => { - let field = match instr.imm { - ImmOperand::Atom(atom) => atom, - _ => unreachable!(), - }; - - let arg0_ty = val_types[instr.args[0]].clone(); - let arg1_ty = val_types[instr.args[1]].clone(); - - ReturnOf((FnKind::DefineField(field), vec![arg0_ty, arg1_ty])) - } - GetField => { - let field = match instr.imm { - ImmOperand::Atom(atom) => atom, - _ => unreachable!(), - }; - - let ty = val_types[instr.args[0]].clone(); - get_field(&ty, field, cache) - } - - // ──────────────────────────────── Function Calls ────────────────────────────────── - Call | Call0 | Call1 | Call2 | Call3 | TailCall | CallMethod | TailCallMethod => { - let frame = call_frame(instr, &val_types); - - match get_field(&frame.callee, FIELD_FN, cache) { - Type::Fn(fn_id) => { - let req = InferRequest { - fn_id, - arg_types: arg_types_for(®_fns[fn_id], &frame), - }; - - match cache.get(&req) { - Some(res) => res.ret_type.clone(), - None => { - let res = walk_fn(&req, reg_fns, cache); - let ret_ty = res.ret_type.clone(); - cache.insert(req, res); - ret_ty - } - } - } - - // A callee we cannot resolve to one function: a - // builtin, or a value we lost track of. - _ => Type::Any, - } - } - - // ────────────────────────────────────── Returns ─────────────────────────────────── - Return | ReturnAsync => val_types[instr.args[0]].clone(), - ReturnUndef => Type::Empty, - - _ => unreachable!("{instr:?}"), - }; - - if is_ret { - ret_type = join_ty(&ret_type, &ty); - } else if instr.op == DefineField { - // A define states its result completely, from its operands. - // Joining it with the object's previous type would only - // destroy the chain that a write-back is building up. - for &dst in &instr.dsts { - val_types[dst] = ty.clone(); - } - } else { - for &dst in &instr.dsts { - val_types[dst] = join_ty(&val_types[dst], &ty); - } - } - } - } - - InferResult { - val_types, - ret_type, - } -} - -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!("")), - } -} - -fn atom_text(a: &Atom) -> String { - match a { - Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s), - Atom::Predefined(n) => format!(""), - } -} - -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/infer/mod.rs b/compiler/src/infer/mod.rs new file mode 100644 index 0000000..bc3de2d --- /dev/null +++ b/compiler/src/infer/mod.rs @@ -0,0 +1,347 @@ +#![allow(non_snake_case)] + +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, + regify::RegFn, +}; + +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct InferRequest { + pub fn_id: usize, + pub arg_types: Vec, +} + +#[derive(Debug)] +pub struct InferResult { + pub val_types: Vec, + 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, +} + +impl TypeCache { + pub fn get(&self, req: &InferRequest) -> Option<&InferResult> { + self.results.get(req) + } + + pub fn insert(&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; + } + + 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; + + let mut cache = TypeCache::default(); + + let req = InferRequest { + fn_id: entry, + arg_types: vec![Type::Undefined; reg_fns[entry].arg_count], + }; + + let res = walk_fn(&req, reg_fns, &mut cache); + cache.insert(req, res); + + cache +} + +fn walk_fn(req: &InferRequest, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult { + use OpCode::*; + + let f = ®_fns[req.fn_id]; + 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(); + } + + // 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. + loop { + let prev = (val_types.clone(), ret_type.clone()); + + 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 { + let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef); + + if instr.dsts.is_empty() && !is_ret { + continue; + } + + // TODO: Some of these ops may not every show up by this point + let ty = match instr.op { + // ──────────────────────────────────── Literals ──────────────────────────────────── + PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 + | PushI8 | PushI16 | PushI32 => Type::Int, + + PushTrue | PushFalse => Type::Bool, + + PushConst | PushConst8 => match instr.imm { + ImmOperand::Const(k) => match f.cpool.get(k as usize) { + Some(JsValue::Int(_)) => Type::Int, + Some(JsValue::Float(_)) => Type::Float, + Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool, + Some(JsValue::Native(id)) => Type::Native(*id), + _ => unreachable!(), + }, + _ => unreachable!(), + }, + + // ─────────────────────────────────── Arithmetic ─────────────────────────────────── + Add | Sub | Mul | Mod | Pow => { + match (&val_types[instr.args[0]], &val_types[instr.args[1]]) { + (Type::Unknown, _) | (_, Type::Unknown) => Type::Unknown, + (Type::Int, Type::Int) => Type::Int, + (Type::Float, Type::Float) + | (Type::Int, Type::Float) + | (Type::Float, Type::Int) => Type::Float, + _ => Type::Any, + } + } + Neg | Plus | Inc | Dec | PostInc | PostDec => match val_types[instr.args[0]] { + Type::Unknown => Type::Unknown, + Type::Int => Type::Int, + Type::Float => Type::Float, + _ => Type::Any, + }, + Div => Type::Float, + + Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool, + + // ────────────────────────────────── 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), + }, + _ => unreachable!(), + }, + _ => unreachable!(), + }, + DefineField => { + let field = match instr.imm { + ImmOperand::Atom(atom) => atom, + _ => unreachable!(), + }; + + 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); + } + + ty + } + GetField => { + let field = match instr.imm { + ImmOperand::Atom(atom) => atom, + _ => unreachable!(), + }; + + let ty = val_types[instr.args[0]].clone(); + get_field(&ty, field) + } + + // ──────────────────────────────── 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(); + + 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 ─────────────────────────────────── + Return | ReturnAsync => val_types[instr.args[0]].clone(), + ReturnUndef => Type::Undefined, + + _ => unreachable!("{instr:?}"), + }; + + if is_ret { + ret_type = join_ty(&ret_type, &ty); + } else { + for &dst in &instr.dsts { + val_types[dst] = join_ty(&val_types[dst], &ty); + } + } + + // 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]); + } + } + } + + if (&val_types, &ret_type) == (&prev.0, &prev.1) { + break; + } + } + + 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 new file mode 100644 index 0000000..732356f --- /dev/null +++ b/compiler/src/infer/types.rs @@ -0,0 +1,229 @@ +//! 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}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum Type { + Unknown, + Undefined, + Bool, + 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. + WithField { + name: AtomRef, + value: 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. +/// +/// 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. +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 + } +} + +impl<'a> Iterator for Fields<'a> { + type Item = (AtomRef, &'a Type); + + fn next(&mut self) -> Option { + loop { + match self.current { + Type::WithField { name, value, rest } => { + if *name == FIELD_PROTO && self.follow { + self.proto = Some(value); + } + + self.current = rest; + return Some((*name, value)); + } + + // 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, + }, + } + } + } +} + +impl Type { + /// The object's own fields, not those of its prototype. + pub fn fields(&self) -> Fields<'_> { + Fields { + current: self, + proto: None, + follow: false, + depth: 0, + } + } + + /// The object's own fields, then its prototype's, and so on outward. + pub fn fields_through_proto(&self) -> Fields<'_> { + Fields { + current: self, + proto: None, + follow: true, + depth: 0, + } + } +} + +/// 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 +/// +/// Objects join field by field, so an object that gains a field only grows, +/// and a field written with two different types becomes `Any`. +pub fn join_ty(a: &Type, b: &Type) -> Type { + match (a, b) { + (Type::Unknown, t) | (t, Type::Unknown) => t.clone(), + (Type::Any, _) | (_, Type::Any) => Type::Any, + (a, b) if a == b => a.clone(), + + (Type::Closure { fn_id: f, env: e }, Type::Closure { fn_id: g, env: e2 }) if f == g => { + Type::Closure { + fn_id: *f, + env: Box::new(join_ty(e, e2)), + } + } + + (Type::Empty | Type::WithField { .. }, Type::Empty | Type::WithField { .. }) => b + .fields() + .fold(a.clone(), |acc, (name, value)| set_field(&acc, name, value)), + + _ => Type::Any, + } +} diff --git a/compiler/src/libqjs/mod.rs b/compiler/src/libqjs/mod.rs index 140bd81..a9b0796 100644 --- a/compiler/src/libqjs/mod.rs +++ b/compiler/src/libqjs/mod.rs @@ -12,9 +12,12 @@ pub fn compile(src: &[u8], filename: &str) -> QuickJSBinary { let rt = ffi::JS_NewRuntime(); let ctx = ffi::JS_NewContext(rt); + // JS_Eval requires a '\0' after the last byte of the source. + let src_z = [src, &[0]].concat(); + let obj = ffi::JS_Eval( ctx, - src.as_ptr(), + src_z.as_ptr().cast(), src.len(), filename.as_ptr().cast(), ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE, diff --git a/compiler/src/libqjs/parse.rs b/compiler/src/libqjs/parse.rs index 550ff5c..ba78cb2 100644 --- a/compiler/src/libqjs/parse.rs +++ b/compiler/src/libqjs/parse.rs @@ -168,6 +168,9 @@ 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), Module(Module), TypedArray, ArrayBuffer, @@ -894,3 +897,39 @@ 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/model.rs b/compiler/src/model.rs index a3f1c1f..9eadc94 100644 --- a/compiler/src/model.rs +++ b/compiler/src/model.rs @@ -7,10 +7,6 @@ use crate::libqjs::parse::AtomRef; -/// The function held by a closure object. Every closure carries one, and a -/// call reads it to find the function behind the object it is calling. -pub const FIELD_FN: AtomRef = AtomRef::Predefined(0); // "" - /// 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); // "" diff --git a/compiler/src/regify/mod.rs b/compiler/src/regify/mod.rs index 87dd2e9..4153dd7 100644 --- a/compiler/src/regify/mod.rs +++ b/compiler/src/regify/mod.rs @@ -41,6 +41,9 @@ impl std::fmt::Debug for RegInstr { pub struct RegFn { pub name: AtomRef, pub arg_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, @@ -52,7 +55,8 @@ impl RegFn { let mut blocks = find_basic_blocks(&flat_fn.code); let mut out = vec![RegInstr::nop(); flat_fn.code.len()]; - let mut next_val = flat_fn.var_count; // Reserve values for locals + // Reserve values for locals, then args + let mut next_val = flat_fn.var_count + flat_fn.arg_count; let mut entry_stacks: Vec>> = vec![None; blocks.len()]; let mut worklist = vec![]; @@ -64,7 +68,7 @@ impl RegFn { let block = &mut blocks[block_idx]; let mut stack: Vec = entry_stacks[block_idx].clone().unwrap_or_default(); - walk_block(block, &flat_fn.code, &mut stack, &mut next_val, &mut out); + walk_block(block, &flat_fn.code, flat_fn.var_count, &mut stack, &mut next_val, &mut out); for succ_idx in block.succ.clone() { let succ_stack = &mut entry_stacks[succ_idx]; @@ -82,6 +86,7 @@ impl RegFn { Self { name: flat_fn.name, arg_count: flat_fn.arg_count, + var_count: flat_fn.var_count, val_count: next_val, cpool: flat_fn.cpool, code: out, @@ -93,6 +98,7 @@ impl RegFn { fn walk_block( block: &BasicBlock, code: &[Instr], + var_count: usize, stack: &mut Vec, next_val: &mut usize, out: &mut [RegInstr], @@ -123,39 +129,37 @@ fn walk_block( out_instr.args.push(v); } - // Locals are not renamed, local read/writes need special handling - // Handle read of a local value - match is_local_read(instr) { - Some(loc) => { - stack.push(loc); - out_instr.op = OpCode::Nop; - out_instr.imm = ImmOperand::None; - out[instr_idx] = out_instr; - continue; - } - None if instr.op == OpCode::GetLoc0Loc1 => { - stack.push(0); - stack.push(1); - out_instr.op = OpCode::Nop; + // 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 => (), } - // Handle write of a local value - match is_local_write(instr) { + // 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); - out[instr_idx] = out_instr; + // set_loc also leaves the value it stored on the stack. if n_push == 1 { - stack.push(loc); + stack.push(out_instr.args[0]); } + out[instr_idx] = out_instr; continue; } None if instr.op == OpCode::SetLocUninitialized => { @@ -167,6 +171,39 @@ fn walk_block( None => (), } + // 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. + if instr.op == OpCode::SetHomeObject { + stack.push(out_instr.args[1]); + stack.push(out_instr.args[0]); + out_instr.op = OpCode::Nop; + out_instr.imm = ImmOperand::None; + out[instr_idx] = out_instr; + 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. + if instr.op == OpCode::Dup { + stack.push(out_instr.args[0]); + stack.push(out_instr.args[0]); + out_instr.op = OpCode::Nop; + out_instr.imm = ImmOperand::None; + out[instr_idx] = out_instr; + 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]); + out_instr.op = OpCode::Nop; + out_instr.imm = ImmOperand::None; + out[instr_idx] = out_instr; + 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 @@ -203,7 +240,10 @@ fn extra_pops(instr: &Instr) -> usize { } } -fn is_local_read(instr: &Instr) -> Option { +/// The value id of the local or arg that `instr` reads, if any. +fn is_var_read(instr: &Instr, var_count: usize) -> Option { + let arg = |k: usize| Some(var_count + k); + match instr.op { OpCode::GetLoc0 => Some(0), OpCode::GetLoc1 => Some(1), @@ -215,11 +255,24 @@ fn is_local_read(instr: &Instr) -> Option { _ => unreachable!(), }, + OpCode::GetArg0 => arg(0), + OpCode::GetArg1 => arg(1), + OpCode::GetArg2 => arg(2), + OpCode::GetArg3 => arg(3), + + OpCode::GetArg => match instr.imm { + ImmOperand::Arg(k) => arg(k as usize), + _ => unreachable!(), + }, + _ => None, } } -fn is_local_write(instr: &Instr) -> Option { +/// The value id of the local or arg that `instr` writes, if any. +fn is_var_write(instr: &Instr, var_count: usize) -> Option { + let arg = |k: usize| Some(var_count + k); + match instr.op { OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0), OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1), @@ -236,6 +289,16 @@ fn is_local_write(instr: &Instr) -> Option { _ => unreachable!(), }, + OpCode::PutArg0 | OpCode::SetArg0 => arg(0), + OpCode::PutArg1 | OpCode::SetArg1 => arg(1), + OpCode::PutArg2 | OpCode::SetArg2 => arg(2), + OpCode::PutArg3 | OpCode::SetArg3 => arg(3), + + OpCode::PutArg | OpCode::SetArg => match instr.imm { + ImmOperand::Arg(k) => arg(k as usize), + _ => unreachable!(), + }, + _ => None, } } diff --git a/compiler/src/transform/declass.rs b/compiler/src/transform/declass.rs index 380840f..ed7f513 100644 --- a/compiler/src/transform/declass.rs +++ b/compiler/src/transform/declass.rs @@ -21,12 +21,23 @@ impl ClassSequence { imm: ImmOperand::Const(self.ctor_fn_cpool_idx), }); + // 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 + }); + + // 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::GetField, - imm: ImmOperand::Atom(AtomRef::Predefined(63)), // .prototype - }); out.push(Instr { pc: self.start_pc, op: OpCode::Fclosure8, diff --git a/compiler/src/transform/mod.rs b/compiler/src/transform/mod.rs index 36db097..3845fa9 100644 --- a/compiler/src/transform/mod.rs +++ b/compiler/src/transform/mod.rs @@ -1,22 +1,34 @@ mod declass; use crate::{ + builtins, flatten::FlatFn, - libqjs::parse::{ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode}, - model::{EXTRA_ARGS, FIELD_FN}, + libqjs::parse::{ + Atom, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, atom_to_str, opcodes::OpCode, + }, + model::{EXTRA_ARGS, FIELD_PROTO}, }; use declass::declass; -pub fn transform(flat_fns: Vec, captures: &[Vec]) -> Vec { +pub fn transform( + flat_fns: Vec, + captures: &[Vec], + atoms: &[Atom], +) -> Vec { flat_fns .into_iter() .enumerate() - .map(|(i, f)| transform_one(i, f, captures)) + .map(|(i, f)| transform_one(i, f, captures, atoms)) .collect() } -fn transform_one(fn_id: usize, f: FlatFn, captures: &[Vec]) -> FlatFn { +fn transform_one( + fn_id: usize, + mut f: FlatFn, + captures: &[Vec], + atoms: &[Atom], +) -> FlatFn { let declassed = declass(&f.code); // Every function gains two parameters after its own, in this order. The @@ -28,22 +40,34 @@ fn transform_one(fn_id: usize, f: FlatFn, captures: &[Vec]) -> FlatF for instr in &declassed { if let Some(info) = is_fclosure(instr, &f, captures) { - out_len += 1 + (2 * info.child_captures.len()) + 2; // object; (push; define_field)*; push_fn_const; define_field + // fclosure; (load; define_field)* + out_len += 1 + info.child_captures.iter().map(capture_len).sum::(); continue; } - if is_capture_acc(instr).is_some() || is_tail_call(instr) { + // get_arg env; swap; put_field + if let Some(CaptureAccess::Write(_)) = is_capture_acc(instr) { + out_len += 3; + continue; + } + + if is_capture_acc(instr).is_some() { out_len += 2; continue; } + if let Some(seq) = expansion(instr.op) { + out_len += seq.len(); + continue; + } + out_len += 1; } let mut out = Vec::with_capacity(out_len); for instr in &declassed { - if replace_fclosure(instr, &f, captures, &mut out) { + if replace_fclosure(instr, &f, captures, &captures[fn_id], env_idx, &mut out) { continue; } @@ -55,11 +79,15 @@ fn transform_one(fn_id: usize, f: FlatFn, captures: &[Vec]) -> FlatF continue; } - if replace_tail_call(instr, &mut out) { + if replace_expansion(instr, &mut out) { continue; } - out.push(instr.clone()); + if replace_get_var(instr, atoms, &mut f.cpool, &mut out) { + continue; + } + + out.push(renamed(instr).unwrap_or_else(|| instr.clone())); } debug_assert!(out.len() == out_len); @@ -89,7 +117,6 @@ fn replace_push_this(instr: &Instr, this_idx: u16, out: &mut Vec) -> bool } struct FClosureInfo<'a> { - cpool_idx: u32, child_captures: &'a [ClosureVar], } @@ -98,7 +125,7 @@ fn is_fclosure<'a>( f: &FlatFn, captures: &'a [Vec], ) -> Option> { - if instr.op != OpCode::Fclosure8 { + if !matches!(instr.op, OpCode::Fclosure8 | OpCode::Fclosure) { return None; } @@ -113,7 +140,6 @@ fn is_fclosure<'a>( }; Some(FClosureInfo { - cpool_idx, child_captures: &captures[child_fn_id], }) } @@ -122,6 +148,8 @@ fn replace_fclosure( instr: &Instr, f: &FlatFn, captures: &[Vec], + my_captures: &[ClosureVar], + env_idx: u16, out: &mut Vec, ) -> bool { let info = match is_fclosure(instr, f, captures) { @@ -129,43 +157,66 @@ fn replace_fclosure( None => return false, }; - let pc = instr.pc; - - out.push(Instr { - pc, - op: OpCode::Object, - imm: ImmOperand::None, - }); + // 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 { - out.push(Instr { - pc, - op: OpCode::GetLoc, - imm: ImmOperand::Loc(c.var_idx as u16), - }); + if !load_capture(c, my_captures, env_idx, instr.pc, out) { + continue; + } out.push(Instr { - pc, + pc: instr.pc, op: OpCode::DefineField, imm: ImmOperand::Atom(c.name), }); } - out.push(Instr { - pc, - op: OpCode::PushConst, - imm: ImmOperand::Const(info.cpool_idx), - }); + true +} - out.push(Instr { - pc, - op: OpCode::DefineField, - imm: ImmOperand::Atom(FIELD_FN), - }); +/// 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), + ); + } + _ => return false, + } true } +/// 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, + ClosureKind::Ref => 3, + _ => 0, + } +} + enum CaptureAccess { Read(u16), Write(u16), @@ -232,12 +283,20 @@ fn replace_capture_accs( 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, @@ -249,26 +308,89 @@ fn replace_capture_accs( true } -fn is_tail_call(instr: &Instr) -> bool { - instr.op == OpCode::TailCall +/// 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)]> { + 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)], + _ => return None, + }; + + Some(seq) } -fn replace_tail_call(instr: &Instr, out: &mut Vec) -> bool { - if !is_tail_call(instr) { +fn replace_expansion(instr: &Instr, out: &mut Vec) -> bool { + let Some(seq) = expansion(instr.op) 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 }); + } + + 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; } - out.push(Instr { - pc: instr.pc, - op: OpCode::Call, - imm: instr.imm, - }); + let name = atom_to_str(&atom, atoms); + let native = builtins::native(&name).unwrap_or_else(|| panic!("unknown global `{name}`")); + + cpool.push(JsValue::Native(native)); out.push(Instr { pc: instr.pc, - op: OpCode::Return, - imm: ImmOperand::None, + op: OpCode::PushConst, + imm: ImmOperand::Const(cpool.len() as u32 - 1), }); 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 new file mode 100644 index 0000000..9d4a373 --- /dev/null +++ b/lib/prelude.js @@ -0,0 +1,7 @@ +const Object = { + create(proto) { return { __proto__: proto }; }, +}; + +const console = { + log(x) { __c_print(x); }, +};