Misc cleanups

This commit is contained in:
UditDey
2026-09-28 07:47:01 +00:00
parent e93ce94ac7
commit a4d295fbd9
18 changed files with 563 additions and 1388 deletions
-95
View File
@@ -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(&reg_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(_)
)
}
-4
View File
@@ -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;
+8
View File
@@ -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!()
}
}
+18
View File
@@ -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()
}
-38
View File
@@ -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)
}
-501
View File
@@ -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(&reg_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 = &reg_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
View File
@@ -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![]);
}
+66 -193
View File
@@ -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 {
// ───────────────────────────────────────── Objects ─────────────────────────────────────────────────────────
Fclosure | Fclosure8 => {
let cpool_idx = match instr.imm {
ImmOperand::Const(idx) => idx as usize,
_ => unreachable!(),
};
let fn_id = match f.cpool[cpool_idx] {
JsValue::FnById(id) => id,
_ => unreachable!(),
};
Type::Closure {
fn_id,
env: Box::new(Type::Empty),
},
_ => unreachable!(),
},
_ => unreachable!(),
},
}
}
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 = &reg_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]]) {
// 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 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 (&val_types, &ret_type) == (&prev.0, &prev.1) {
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 { .. })
}
+87 -144
View File
@@ -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
///
/// 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()),
},
}
}
+2 -39
View File
@@ -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)
}
}
}
+4 -2
View File
@@ -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(&reg_fns, &atoms);
let ty_cache = infer_types(&reg_fns);
}
-20
View File
@@ -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;
+42
View File
@@ -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)
}
+44 -57
View File
@@ -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,12 +129,9 @@ 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) => {
// 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;
@@ -144,36 +141,25 @@ fn walk_block(
out[instr_idx] = out_instr;
continue;
}
None => (),
}
// Handle write of a local or arg value
match is_var_write(instr, var_count) {
Some(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(loc);
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]);
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 => (),
}
// 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());
+38 -29
View File
@@ -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>) {
let mut push = |op, imm| {
out.push(Instr {
pc: self.start_pc,
op: OpCode::Fclosure8,
imm: ImmOperand::Const(self.ctor_fn_cpool_idx),
});
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
}
+167 -187
View File
@@ -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();
len += 1;
}
// Emit transformed output
let mut out = Vec::with_capacity(len);
for instr in &code {
if replace_fclosure(instr, &f, captures, &captures[fn_id], env_arg, &mut out) {
continue;
}
out_len += 1;
}
let mut out = Vec::with_capacity(out_len);
for instr in &declassed {
if replace_fclosure(instr, &f, captures, &captures[fn_id], env_idx, &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,116 +78,83 @@ 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
}
/// 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 });
push(OpCode::Fclosure, instr.imm);
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_idx));
push(OpCode::GetArg, ImmOperand::Arg(env_arg as u16));
push(
OpCode::GetField,
ImmOperand::Atom(my_captures[c.var_idx].name),
@@ -205,10 +163,13 @@ fn load_capture(
_ => 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,
}
}
-7
View File
@@ -1,7 +0,0 @@
const Object = {
create(proto) { return { __proto__: proto }; },
};
const console = {
log(x) { __c_print(x); },
};
+9
View File
@@ -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); },
};