Big progress by Claude
This commit is contained in:
@@ -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<Vec<bool>> {
|
||||||
|
let mut by_ref: Vec<Vec<bool>> = 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<Option<usize>> = (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(_)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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_<name>`, 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<usize> {
|
||||||
|
NATIVES.iter().position(|n| n.js_name == js_name)
|
||||||
|
}
|
||||||
@@ -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<Type>), 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 <stdbool.h>\n#include <stdint.h>\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<usize, usize> = 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<String> = 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
|
||||||
|
}
|
||||||
+43
-1
@@ -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)]
|
#[derive(Debug)]
|
||||||
pub struct FlatFn {
|
pub struct FlatFn {
|
||||||
@@ -13,9 +15,49 @@ pub fn flatten(bin: QuickJSBinary) -> (Vec<FlatFn>, Vec<Vec<ClosureVar>>, Vec<At
|
|||||||
let mut fns = vec![];
|
let mut fns = vec![];
|
||||||
let mut captures = vec![];
|
let mut captures = vec![];
|
||||||
walk(bin.root_mod.root_fn, &mut fns, &mut captures);
|
walk(bin.root_mod.root_fn, &mut fns, &mut captures);
|
||||||
|
append_entry(&mut fns, &mut captures);
|
||||||
(fns, captures, bin.atoms)
|
(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 `<eval>` (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 `<eval>`'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<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) {
|
||||||
|
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 `<eval>`.
|
||||||
|
var_count: 1,
|
||||||
|
cpool: vec![JsValue::FnById(0)],
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
|
||||||
|
captures.push(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) -> usize {
|
fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) -> usize {
|
||||||
let id = flat_fns.len();
|
let id = flat_fns.len();
|
||||||
|
|
||||||
|
|||||||
@@ -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<Type>)),
|
|
||||||
Any,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type lattice join rules:
|
|
||||||
/// Unknown * T = T
|
|
||||||
/// Any * T = Any
|
|
||||||
/// T * T = T
|
|
||||||
/// T * U = Any
|
|
||||||
fn join_ty(a: &Type, b: &Type) -> Type {
|
|
||||||
match (a, b) {
|
|
||||||
(Type::Unknown, t) | (t, Type::Unknown) => t.clone(),
|
|
||||||
(Type::Any, _) | (_, Type::Any) => Type::Any,
|
|
||||||
(a, b) if a == b => a.clone(),
|
|
||||||
_ => Type::Any,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<Type>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Type> {
|
|
||||||
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<Type>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct InferResult {
|
|
||||||
pub val_types: Vec<Type>,
|
|
||||||
pub ret_type: Type,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type TypeCache = HashMap<InferRequest, InferResult>;
|
|
||||||
|
|
||||||
pub fn infer_types(reg_fns: &[RegFn], atoms: &[Atom]) -> TypeCache {
|
|
||||||
// Find main fn
|
|
||||||
let main_fn_id = reg_fns
|
|
||||||
.iter()
|
|
||||||
.position(|f| atom_to_str(&f.name, atoms) == "main")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut cache = TypeCache::with_capacity(reg_fns.len());
|
|
||||||
|
|
||||||
// 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("<predef?>")
|
|
||||||
.to_string(),
|
|
||||||
AtomRef::FileLocal(i) => atoms
|
|
||||||
.get(*i)
|
|
||||||
.map(atom_text)
|
|
||||||
.unwrap_or_else(|| format!("<file_atom {i}>")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn atom_text(a: &Atom) -> String {
|
|
||||||
match a {
|
|
||||||
Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s),
|
|
||||||
Atom::Predefined(n) => format!("<predef {n}>"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn js_string(s: &JsString) -> String {
|
|
||||||
match s {
|
|
||||||
JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(),
|
|
||||||
JsString::UTF16(b) => {
|
|
||||||
let u: Vec<u16> = b
|
|
||||||
.chunks_exact(2)
|
|
||||||
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
|
||||||
.collect();
|
|
||||||
String::from_utf16_lossy(&u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Type>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InferResult {
|
||||||
|
pub val_types: Vec<Type>,
|
||||||
|
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<InferRequest, InferResult>,
|
||||||
|
shapes: HashMap<Type, ShapeId>,
|
||||||
|
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<ShapeId> {
|
||||||
|
self.shapes.get(ty).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn results(&self) -> impl Iterator<Item = (&InferRequest, &InferResult)> {
|
||||||
|
self.results.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shapes(&self) -> impl Iterator<Item = (&Type, ShapeId)> {
|
||||||
|
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<Type> = 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 { .. })
|
||||||
|
}
|
||||||
@@ -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<Type>,
|
||||||
|
},
|
||||||
|
/// 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<Type>,
|
||||||
|
rest: Box<Type>,
|
||||||
|
},
|
||||||
|
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<Self::Item> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,12 @@ pub fn compile(src: &[u8], filename: &str) -> QuickJSBinary {
|
|||||||
let rt = ffi::JS_NewRuntime();
|
let rt = ffi::JS_NewRuntime();
|
||||||
let ctx = ffi::JS_NewContext(rt);
|
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(
|
let obj = ffi::JS_Eval(
|
||||||
ctx,
|
ctx,
|
||||||
src.as_ptr(),
|
src_z.as_ptr().cast(),
|
||||||
src.len(),
|
src.len(),
|
||||||
filename.as_ptr().cast(),
|
filename.as_ptr().cast(),
|
||||||
ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE,
|
ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE,
|
||||||
|
|||||||
@@ -168,6 +168,9 @@ pub enum JsValue {
|
|||||||
},
|
},
|
||||||
Function(Function),
|
Function(Function),
|
||||||
FnById(usize),
|
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),
|
Module(Module),
|
||||||
TypedArray,
|
TypedArray,
|
||||||
ArrayBuffer,
|
ArrayBuffer,
|
||||||
@@ -894,3 +897,39 @@ impl Debug for JsString {
|
|||||||
f.write_char('"')
|
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("<predef?>")
|
||||||
|
.to_string(),
|
||||||
|
AtomRef::FileLocal(i) => atoms
|
||||||
|
.get(*i)
|
||||||
|
.map(atom_text)
|
||||||
|
.unwrap_or_else(|| format!("<file_atom {i}>")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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!("<predef {n}>"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<u16> = b
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16_lossy(&u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,6 @@
|
|||||||
|
|
||||||
use crate::libqjs::parse::AtomRef;
|
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); // "<null>"
|
|
||||||
|
|
||||||
/// An instance's prototype object. A field lookup that misses on an object
|
/// An instance's prototype object. A field lookup that misses on an object
|
||||||
/// itself follows this link and retries.
|
/// itself follows this link and retries.
|
||||||
pub const FIELD_PROTO: AtomRef = AtomRef::Predefined(124); // "<home_object>"
|
pub const FIELD_PROTO: AtomRef = AtomRef::Predefined(124); // "<home_object>"
|
||||||
|
|||||||
+85
-22
@@ -41,6 +41,9 @@ impl std::fmt::Debug for RegInstr {
|
|||||||
pub struct RegFn {
|
pub struct RegFn {
|
||||||
pub name: AtomRef,
|
pub name: AtomRef,
|
||||||
pub arg_count: usize,
|
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 val_count: usize,
|
||||||
pub cpool: Vec<JsValue>,
|
pub cpool: Vec<JsValue>,
|
||||||
pub code: Vec<RegInstr>,
|
pub code: Vec<RegInstr>,
|
||||||
@@ -52,7 +55,8 @@ impl RegFn {
|
|||||||
let mut blocks = find_basic_blocks(&flat_fn.code);
|
let mut blocks = find_basic_blocks(&flat_fn.code);
|
||||||
let mut out = vec![RegInstr::nop(); flat_fn.code.len()];
|
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<Option<Vec<usize>>> = vec![None; blocks.len()];
|
let mut entry_stacks: Vec<Option<Vec<usize>>> = vec![None; blocks.len()];
|
||||||
let mut worklist = vec![];
|
let mut worklist = vec![];
|
||||||
@@ -64,7 +68,7 @@ impl RegFn {
|
|||||||
let block = &mut blocks[block_idx];
|
let block = &mut blocks[block_idx];
|
||||||
let mut stack: Vec<usize> = entry_stacks[block_idx].clone().unwrap_or_default();
|
let mut stack: Vec<usize> = 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() {
|
for succ_idx in block.succ.clone() {
|
||||||
let succ_stack = &mut entry_stacks[succ_idx];
|
let succ_stack = &mut entry_stacks[succ_idx];
|
||||||
@@ -82,6 +86,7 @@ impl RegFn {
|
|||||||
Self {
|
Self {
|
||||||
name: flat_fn.name,
|
name: flat_fn.name,
|
||||||
arg_count: flat_fn.arg_count,
|
arg_count: flat_fn.arg_count,
|
||||||
|
var_count: flat_fn.var_count,
|
||||||
val_count: next_val,
|
val_count: next_val,
|
||||||
cpool: flat_fn.cpool,
|
cpool: flat_fn.cpool,
|
||||||
code: out,
|
code: out,
|
||||||
@@ -93,6 +98,7 @@ impl RegFn {
|
|||||||
fn walk_block(
|
fn walk_block(
|
||||||
block: &BasicBlock,
|
block: &BasicBlock,
|
||||||
code: &[Instr],
|
code: &[Instr],
|
||||||
|
var_count: usize,
|
||||||
stack: &mut Vec<usize>,
|
stack: &mut Vec<usize>,
|
||||||
next_val: &mut usize,
|
next_val: &mut usize,
|
||||||
out: &mut [RegInstr],
|
out: &mut [RegInstr],
|
||||||
@@ -123,39 +129,37 @@ fn walk_block(
|
|||||||
out_instr.args.push(v);
|
out_instr.args.push(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locals are not renamed, local read/writes need special handling
|
// Locals and args are not renamed, their read/writes need special handling
|
||||||
// Handle read of a local value
|
// Handle read of a local or arg value
|
||||||
match is_local_read(instr) {
|
match is_var_read(instr, var_count) {
|
||||||
Some(loc) => {
|
// A read takes the var's value at this point. The stack holds a
|
||||||
stack.push(loc);
|
// copy, never the var itself.
|
||||||
out_instr.op = OpCode::Nop;
|
Some(var) => {
|
||||||
out_instr.imm = ImmOperand::None;
|
let v = fresh_val();
|
||||||
out[instr_idx] = out_instr;
|
stack.push(v);
|
||||||
continue;
|
out_instr.op = OpCode::SetLoc;
|
||||||
}
|
|
||||||
None if instr.op == OpCode::GetLoc0Loc1 => {
|
|
||||||
stack.push(0);
|
|
||||||
stack.push(1);
|
|
||||||
out_instr.op = OpCode::Nop;
|
|
||||||
out_instr.imm = ImmOperand::None;
|
out_instr.imm = ImmOperand::None;
|
||||||
|
out_instr.dsts.push(v);
|
||||||
|
out_instr.args.push(var);
|
||||||
out[instr_idx] = out_instr;
|
out[instr_idx] = out_instr;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
None => (),
|
None => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle write of a local value
|
// Handle write of a local or arg value
|
||||||
match is_local_write(instr) {
|
match is_var_write(instr, var_count) {
|
||||||
Some(loc) => {
|
Some(loc) => {
|
||||||
out_instr.op = OpCode::SetLoc;
|
out_instr.op = OpCode::SetLoc;
|
||||||
out_instr.imm = ImmOperand::None;
|
out_instr.imm = ImmOperand::None;
|
||||||
out_instr.dsts.push(loc);
|
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 {
|
if n_push == 1 {
|
||||||
stack.push(loc);
|
stack.push(out_instr.args[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out[instr_idx] = out_instr;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
None if instr.op == OpCode::SetLocUninitialized => {
|
None if instr.op == OpCode::SetLocUninitialized => {
|
||||||
@@ -167,6 +171,39 @@ fn walk_block(
|
|||||||
None => (),
|
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
|
// 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
|
// 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
|
// 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<usize> {
|
/// The value id of the local or arg that `instr` reads, if any.
|
||||||
|
fn is_var_read(instr: &Instr, var_count: usize) -> Option<usize> {
|
||||||
|
let arg = |k: usize| Some(var_count + k);
|
||||||
|
|
||||||
match instr.op {
|
match instr.op {
|
||||||
OpCode::GetLoc0 => Some(0),
|
OpCode::GetLoc0 => Some(0),
|
||||||
OpCode::GetLoc1 => Some(1),
|
OpCode::GetLoc1 => Some(1),
|
||||||
@@ -215,11 +255,24 @@ fn is_local_read(instr: &Instr) -> Option<usize> {
|
|||||||
_ => unreachable!(),
|
_ => 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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_local_write(instr: &Instr) -> Option<usize> {
|
/// The value id of the local or arg that `instr` writes, if any.
|
||||||
|
fn is_var_write(instr: &Instr, var_count: usize) -> Option<usize> {
|
||||||
|
let arg = |k: usize| Some(var_count + k);
|
||||||
|
|
||||||
match instr.op {
|
match instr.op {
|
||||||
OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0),
|
OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0),
|
||||||
OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1),
|
OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1),
|
||||||
@@ -236,6 +289,16 @@ fn is_local_write(instr: &Instr) -> Option<usize> {
|
|||||||
_ => unreachable!(),
|
_ => 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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,23 @@ impl ClassSequence {
|
|||||||
imm: ImmOperand::Const(self.ctor_fn_cpool_idx),
|
imm: ImmOperand::Const(self.ctor_fn_cpool_idx),
|
||||||
});
|
});
|
||||||
|
|
||||||
for m in &self.methods {
|
// 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 {
|
out.push(Instr {
|
||||||
pc: self.start_pc,
|
pc: self.start_pc,
|
||||||
op: OpCode::GetField,
|
op: OpCode::GetField,
|
||||||
imm: ImmOperand::Atom(AtomRef::Predefined(63)), // .prototype
|
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 {
|
out.push(Instr {
|
||||||
pc: self.start_pc,
|
pc: self.start_pc,
|
||||||
op: OpCode::Fclosure8,
|
op: OpCode::Fclosure8,
|
||||||
|
|||||||
+169
-47
@@ -1,22 +1,34 @@
|
|||||||
mod declass;
|
mod declass;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
builtins,
|
||||||
flatten::FlatFn,
|
flatten::FlatFn,
|
||||||
libqjs::parse::{ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode},
|
libqjs::parse::{
|
||||||
model::{EXTRA_ARGS, FIELD_FN},
|
Atom, ClosureKind, ClosureVar, ImmOperand, Instr, JsValue, atom_to_str, opcodes::OpCode,
|
||||||
|
},
|
||||||
|
model::{EXTRA_ARGS, FIELD_PROTO},
|
||||||
};
|
};
|
||||||
|
|
||||||
use declass::declass;
|
use declass::declass;
|
||||||
|
|
||||||
pub fn transform(flat_fns: Vec<FlatFn>, captures: &[Vec<ClosureVar>]) -> Vec<FlatFn> {
|
pub fn transform(
|
||||||
|
flat_fns: Vec<FlatFn>,
|
||||||
|
captures: &[Vec<ClosureVar>],
|
||||||
|
atoms: &[Atom],
|
||||||
|
) -> Vec<FlatFn> {
|
||||||
flat_fns
|
flat_fns
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, f)| transform_one(i, f, captures))
|
.map(|(i, f)| transform_one(i, f, captures, atoms))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transform_one(fn_id: usize, f: FlatFn, captures: &[Vec<ClosureVar>]) -> FlatFn {
|
fn transform_one(
|
||||||
|
fn_id: usize,
|
||||||
|
mut f: FlatFn,
|
||||||
|
captures: &[Vec<ClosureVar>],
|
||||||
|
atoms: &[Atom],
|
||||||
|
) -> FlatFn {
|
||||||
let declassed = declass(&f.code);
|
let declassed = declass(&f.code);
|
||||||
|
|
||||||
// Every function gains two parameters after its own, in this order. The
|
// 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<ClosureVar>]) -> FlatF
|
|||||||
|
|
||||||
for instr in &declassed {
|
for instr in &declassed {
|
||||||
if let Some(info) = is_fclosure(instr, &f, captures) {
|
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::<usize>();
|
||||||
continue;
|
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;
|
out_len += 2;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(seq) = expansion(instr.op) {
|
||||||
|
out_len += seq.len();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
out_len += 1;
|
out_len += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(out_len);
|
let mut out = Vec::with_capacity(out_len);
|
||||||
|
|
||||||
for instr in &declassed {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +79,15 @@ fn transform_one(fn_id: usize, f: FlatFn, captures: &[Vec<ClosureVar>]) -> FlatF
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if replace_tail_call(instr, &mut out) {
|
if replace_expansion(instr, &mut out) {
|
||||||
continue;
|
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);
|
debug_assert!(out.len() == out_len);
|
||||||
@@ -89,7 +117,6 @@ fn replace_push_this(instr: &Instr, this_idx: u16, out: &mut Vec<Instr>) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct FClosureInfo<'a> {
|
struct FClosureInfo<'a> {
|
||||||
cpool_idx: u32,
|
|
||||||
child_captures: &'a [ClosureVar],
|
child_captures: &'a [ClosureVar],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +125,7 @@ fn is_fclosure<'a>(
|
|||||||
f: &FlatFn,
|
f: &FlatFn,
|
||||||
captures: &'a [Vec<ClosureVar>],
|
captures: &'a [Vec<ClosureVar>],
|
||||||
) -> Option<FClosureInfo<'a>> {
|
) -> Option<FClosureInfo<'a>> {
|
||||||
if instr.op != OpCode::Fclosure8 {
|
if !matches!(instr.op, OpCode::Fclosure8 | OpCode::Fclosure) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +140,6 @@ fn is_fclosure<'a>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
Some(FClosureInfo {
|
Some(FClosureInfo {
|
||||||
cpool_idx,
|
|
||||||
child_captures: &captures[child_fn_id],
|
child_captures: &captures[child_fn_id],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -122,6 +148,8 @@ fn replace_fclosure(
|
|||||||
instr: &Instr,
|
instr: &Instr,
|
||||||
f: &FlatFn,
|
f: &FlatFn,
|
||||||
captures: &[Vec<ClosureVar>],
|
captures: &[Vec<ClosureVar>],
|
||||||
|
my_captures: &[ClosureVar],
|
||||||
|
env_idx: u16,
|
||||||
out: &mut Vec<Instr>,
|
out: &mut Vec<Instr>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let info = match is_fclosure(instr, f, captures) {
|
let info = match is_fclosure(instr, f, captures) {
|
||||||
@@ -129,43 +157,66 @@ fn replace_fclosure(
|
|||||||
None => return false,
|
None => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let pc = instr.pc;
|
// 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
|
||||||
out.push(Instr {
|
// environment as an ordinary field.
|
||||||
pc,
|
out.push(instr.clone());
|
||||||
op: OpCode::Object,
|
|
||||||
imm: ImmOperand::None,
|
|
||||||
});
|
|
||||||
|
|
||||||
for c in info.child_captures {
|
for c in info.child_captures {
|
||||||
out.push(Instr {
|
if !load_capture(c, my_captures, env_idx, instr.pc, out) {
|
||||||
pc,
|
continue;
|
||||||
op: OpCode::GetLoc,
|
}
|
||||||
imm: ImmOperand::Loc(c.var_idx as u16),
|
|
||||||
});
|
|
||||||
|
|
||||||
out.push(Instr {
|
out.push(Instr {
|
||||||
pc,
|
pc: instr.pc,
|
||||||
op: OpCode::DefineField,
|
op: OpCode::DefineField,
|
||||||
imm: ImmOperand::Atom(c.name),
|
imm: ImmOperand::Atom(c.name),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push(Instr {
|
true
|
||||||
pc,
|
}
|
||||||
op: OpCode::PushConst,
|
|
||||||
imm: ImmOperand::Const(info.cpool_idx),
|
|
||||||
});
|
|
||||||
|
|
||||||
out.push(Instr {
|
/// Load the value a child closure captures, from where it lives in this frame.
|
||||||
pc,
|
/// `var_idx` means something different per kind: a local slot, an argument, or
|
||||||
op: OpCode::DefineField,
|
/// one of this function's own captures — which, after lowering, is a field of
|
||||||
imm: ImmOperand::Atom(FIELD_FN),
|
/// 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 `<eval>` 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<Instr>,
|
||||||
|
) -> 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
|
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 {
|
enum CaptureAccess {
|
||||||
Read(u16),
|
Read(u16),
|
||||||
Write(u16),
|
Write(u16),
|
||||||
@@ -232,12 +283,20 @@ fn replace_capture_accs(
|
|||||||
CaptureAccess::Write(write_var) => {
|
CaptureAccess::Write(write_var) => {
|
||||||
let atom = my_captures[write_var as usize].name;
|
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 {
|
out.push(Instr {
|
||||||
pc,
|
pc,
|
||||||
op: OpCode::GetArg,
|
op: OpCode::GetArg,
|
||||||
imm: ImmOperand::Arg(env_idx),
|
imm: ImmOperand::Arg(env_idx),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
out.push(Instr {
|
||||||
|
pc,
|
||||||
|
op: OpCode::Swap,
|
||||||
|
imm: ImmOperand::None,
|
||||||
|
});
|
||||||
|
|
||||||
out.push(Instr {
|
out.push(Instr {
|
||||||
pc,
|
pc,
|
||||||
op: OpCode::PutField,
|
op: OpCode::PutField,
|
||||||
@@ -249,26 +308,89 @@ fn replace_capture_accs(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_tail_call(instr: &Instr) -> bool {
|
/// Ops that are a fixed sequence of simpler ones. `true` passes the original
|
||||||
instr.op == OpCode::TailCall
|
/// 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<Instr>) -> bool {
|
fn replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
|
||||||
if !is_tail_call(instr) {
|
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<JsValue>,
|
||||||
|
out: &mut Vec<Instr>,
|
||||||
|
) -> bool {
|
||||||
|
let ImmOperand::Atom(atom) = instr.imm else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if instr.op != OpCode::GetVar {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push(Instr {
|
let name = atom_to_str(&atom, atoms);
|
||||||
pc: instr.pc,
|
let native = builtins::native(&name).unwrap_or_else(|| panic!("unknown global `{name}`"));
|
||||||
op: OpCode::Call,
|
|
||||||
imm: instr.imm,
|
cpool.push(JsValue::Native(native));
|
||||||
});
|
|
||||||
|
|
||||||
out.push(Instr {
|
out.push(Instr {
|
||||||
pc: instr.pc,
|
pc: instr.pc,
|
||||||
op: OpCode::Return,
|
op: OpCode::PushConst,
|
||||||
imm: ImmOperand::None,
|
imm: ImmOperand::Const(cpool.len() as u32 - 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ops that are another op with a different immediate.
|
||||||
|
fn renamed(instr: &Instr) -> Option<Instr> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const Object = {
|
||||||
|
create(proto) { return { __proto__: proto }; },
|
||||||
|
};
|
||||||
|
|
||||||
|
const console = {
|
||||||
|
log(x) { __c_print(x); },
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user