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