From 465d9095bb5de88bc66e26698374dbf1a466260d Mon Sep 17 00:00:00 2001 From: uditdey Date: Fri, 11 Sep 2026 13:36:57 +0000 Subject: [PATCH] Big Rewrite --- compiler/src/declass/mod.rs | 30 -- compiler/src/declass/step_1.rs | 283 --------------- compiler/src/declass/step_2.rs | 126 ------- compiler/src/declosure.rs | 326 ----------------- compiler/src/demutate.rs | 235 ------------- compiler/src/flatten.rs | 123 +------ compiler/src/infer.rs | 540 ++++++++++------------------- compiler/src/libqjs.rs | 141 -------- compiler/src/libqjs/ffi.rs | 57 +++ compiler/src/libqjs/mod.rs | 53 +++ compiler/src/{ => libqjs}/parse.rs | 7 +- compiler/src/main.rs | 57 +-- compiler/src/regify/basic_block.rs | 145 ++++---- compiler/src/regify/mod.rs | 326 ++++++++--------- docs/class_form.txt | 139 ++++++++ docs/proto_form.txt | 87 +++++ 16 files changed, 773 insertions(+), 1902 deletions(-) delete mode 100644 compiler/src/declass/mod.rs delete mode 100644 compiler/src/declass/step_1.rs delete mode 100644 compiler/src/declass/step_2.rs delete mode 100644 compiler/src/declosure.rs delete mode 100644 compiler/src/demutate.rs delete mode 100644 compiler/src/libqjs.rs create mode 100644 compiler/src/libqjs/ffi.rs create mode 100644 compiler/src/libqjs/mod.rs rename compiler/src/{ => libqjs}/parse.rs (99%) create mode 100644 docs/class_form.txt create mode 100644 docs/proto_form.txt diff --git a/compiler/src/declass/mod.rs b/compiler/src/declass/mod.rs deleted file mode 100644 index 675e5ec..0000000 --- a/compiler/src/declass/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! declass: ES classes become prototype form, one algo step per file. -//! -//! The steps follow declass_algo.txt: -//! -//! step_1 rewrite how a class is built (per function). -//! step_2 splice the field initialiser into the constructor. -//! step_3 fix the constructor header: rename, drop the capture (to come). - -mod step_1; -mod step_2; - -use crate::{declass::step_1::ClassSequence, regify::RegFn}; - -use step_1::{find_class_seqs, rewrite_class_to_proto}; -use step_2::splice_ctors; - -pub fn declass(mut reg_fns: Vec) -> Vec { - let class_seqs = reg_fns - .iter() - .map(find_class_seqs) - .collect::>>(); - - reg_fns - .iter_mut() - .zip(&class_seqs) - .for_each(|(f, s)| rewrite_class_to_proto(f, s)); - - splice_ctors(&mut reg_fns, &class_seqs); - reg_fns -} diff --git a/compiler/src/declass/step_1.rs b/compiler/src/declass/step_1.rs deleted file mode 100644 index 074b805..0000000 --- a/compiler/src/declass/step_1.rs +++ /dev/null @@ -1,283 +0,0 @@ -use std::range::Range; - -use crate::{ - parse::{AtomRef, ImmOperand, JsValue, atom_names::PREDEFINED_ATOMS, opcodes::OpCode}, - regify::{RegFn, RegInstr}, -}; - -pub fn find_class_seqs(reg_fn: &RegFn) -> Vec { - let mut ins = reg_fn.code.as_slice(); - let mut seqs = vec![]; - let mut offset = 0; - - while !ins.is_empty() { - let seq = match_class_seq(ins); // Try to match a class decl sequence - - match seq { - // Found class sequence, store it and advance `ins` - Ok((rest, num_methods)) => { - let consumed = ins.len() - rest.len(); // How many instrs were consumed - let seq_instr = &ins[..consumed]; // The class seq's instrs - - seqs.push(ClassSequence { - range: Range { - start: offset, - end: offset + consumed, - }, - num_methods, - ctor_fn: fn_id(®_fn.cpool, as_const(seq_instr[0].imm)), - field_init_fn: find_field_init_fn(seq_instr, num_methods, ®_fn.cpool), - }); - - offset += consumed; - ins = rest; - } - // Did not find sequence, advance `ins` by 1 and try again - Err(rest) => { - let consumed = ins.len() - rest.len(); // Handle partial consumption - let consumed = consumed.max(1); - offset += consumed; - ins = &ins[consumed..]; - } - } - } - - seqs -} - -pub fn rewrite_class_to_proto(reg_fn: &mut RegFn, class_seqs: &[ClassSequence]) { - // The prototype form is never longer than the class build it replaces, so - // it drops straight over it and every instr outside a class seq stays put. - // The code moves out only so fresh_val() can borrow reg_fn beside it. - let mut code = std::mem::take(&mut reg_fn.code); - let mut buf = vec![]; // Reused across seqs, so it allocates once - - for seq in class_seqs { - seq.emit_prototype(&code[seq.range], &mut || reg_fn.fresh_val(), &mut buf); - buf[0].block_start = code[seq.range.start].block_start; - code[seq.range].swap_with_slice(&mut buf); - } - - reg_fn.code = code; -} - -pub struct ClassSequence { - pub range: Range, - pub num_methods: usize, - pub ctor_fn: usize, // Fn id of the class ctor - pub field_init_fn: Option, // Fn id of the field init, if any -} - -/// The field initialiser's fn id, if the class has a field-initialiser tail. -fn find_field_init_fn( - matched: &[RegInstr], - num_methods: usize, - cpool: &[JsValue], -) -> Option { - let tail = 2 + 2 * num_methods; // The ctor and its methods, before the tail - - if matched.len() == tail { - return None; // No field-initialiser tail - } - - Some(fn_id(cpool, as_const(matched[tail].imm))) -} - -impl ClassSequence { - /// Emit the prototype form into the scratchpad `buf`, reading operands - /// straight out of `matched` -- this class sequence's own instructions. - /// `buf` comes back padded with nops to exactly the length of the class - /// build, so the caller can drop it over the original in place. - fn emit_prototype(&self, matched: &[RegInstr], fresh_val: &mut F, buf: &mut Vec) - where - F: FnMut() -> usize, - { - buf.clear(); - - // push_const8 #ctor ; define_class -> fclosure8 #ctor (constructor kept) - let (ctor_const, ctor_val) = self.ctor(matched); - buf.push(fclosure8(ctor_const, ctor_val)); - - // A class with no methods has no prototype to read - if self.num_methods > 0 { - // The prototype cannot change between the put_fields below, so read - // it once for the whole class instead of once per method - let proto = fresh_val(); - buf.push(get_field(proto_atom(), proto, ctor_val)); - - // each ( fclosure8 #m ; define_method @meth ) -> fclosure8 + put_field - for k in 0..self.num_methods { - let (method_const, method_val, meth) = self.method(matched, k); - buf.push(fclosure8(method_const, method_val)); - buf.push(put_field(ImmOperand::Atom(meth), method_val, proto)); - } - } - // the fclosure8 #fields ; set_home_object ; put_loc ; drop tail is dropped. - - // 2 + 2n emitted against the 2 + 2n matched, less the 4-instr field - // initialiser tail, so the emission always fits. Nop pads the slack; it - // pops and pushes nothing, so it cannot disturb the value flow. - let len = self.range.end - self.range.start; - debug_assert!(buf.len() <= len, "class rewrite outgrew its range"); - buf.resize_with(len, nop); - } - - /// The constructor's constant and register ID it was written to - /// push_const8 #ctor ; define_class. - fn ctor(&self, matched: &[RegInstr]) -> (u32, usize) { - (as_const(matched[0].imm), matched[1].dsts[0]) - } - - /// The k-th method's constant, closure value, and name; each method pair - /// sits after the two constructor instructions. - fn method(&self, matched: &[RegInstr], k: usize) -> (u32, usize, AtomRef) { - let fclosure = &matched[2 + 2 * k]; - let define_method = &matched[3 + 2 * k]; - ( - as_const(fclosure.imm), - fclosure.dsts[0], - atom_of(define_method.imm), - ) - } -} - -// ─────────────────────────── Class Sequence Grammar ───────────────────────────────────── -// push_const8 #ctor ; define_class @Name <-- Class decl header -// ( fclosure8 #method ; define_method @meth )* <-- Methods -// ( fclosure8 #fields ; set_home_object ; put_loc ; drop )? <-- Field initializer stuff -// -// match fns return Ok(advanced_instrs) or Err(original_instrs) - -fn match_class_seq(ins: &[RegInstr]) -> Result<(&[RegInstr], usize), &[RegInstr]> { - let ins = match_one(ins, |i| i.op == OpCode::PushConst8)?; - - if let Some(i) = ins.first() { - let is_derived_class = matches!(i.imm, ImmOperand::AtomU8 { arg, .. } if arg & 1 == 1); - - if i.op == OpCode::DefineClass && is_derived_class { - panic!("declass: derived classes (extends) are not supported"); - } - } - - let ins = match_one(ins, |i| i.op == OpCode::DefineClass)?; - let (ins, num_methods) = match_many(ins, match_method_seq); - let ins = match_opt(ins, match_field_init_seq); - - Ok((ins, num_methods)) -} - -// TODO: accessors are not handled -fn match_method_seq(ins: &[RegInstr]) -> Result<&[RegInstr], &[RegInstr]> { - let ins = match_one(ins, |i| i.op == OpCode::Fclosure8)?; - let ins = match_one(ins, |i| i.op == OpCode::DefineMethod)?; - Ok(ins) -} - -fn match_field_init_seq(ins: &[RegInstr]) -> Result<&[RegInstr], &[RegInstr]> { - let is_put_loc = |op| { - matches!( - op, - OpCode::PutLoc - | OpCode::PutLoc8 - | OpCode::PutLoc0 - | OpCode::PutLoc1 - | OpCode::PutLoc2 - | OpCode::PutLoc3 - ) - }; - - let ins = match_one(ins, |i| i.op == OpCode::Fclosure8)?; - let ins = match_one(ins, |i| i.op == OpCode::SetHomeObject)?; - let ins = match_one(ins, |i| is_put_loc(i.op))?; - let ins = match_one(ins, |i| i.op == OpCode::Drop)?; - Ok(ins) -} - -// ───────────────────────────── Matcher Primitives ─────────────────────────────────────── - -fn match_one(i: &[RegInstr], f: impl Fn(&RegInstr) -> bool) -> Result<&[RegInstr], &[RegInstr]> { - match i.first() { - Some(instr) if f(instr) => Ok(&i[1..]), - _ => Err(i), - } -} - -fn match_many( - mut i: &[RegInstr], - f: impl Fn(&[RegInstr]) -> Result<&[RegInstr], &[RegInstr]>, -) -> (&[RegInstr], usize) { - let mut n = 0; - - while let Ok(rest) = f(i) { - n += 1; - i = rest; - } - - (i, n) -} - -fn match_opt<'a>( - i: &'a [RegInstr], - f: impl Fn(&'a [RegInstr]) -> Result<&'a [RegInstr], &'a [RegInstr]>, -) -> &'a [RegInstr] { - match f(i) { - Ok(rest) => rest, - Err(_) => i, - } -} - -// ── emitted instructions ─────────────────────────────────────────────────── - -fn nop() -> RegInstr { - RegInstr::new(OpCode::Nop, ImmOperand::None, vec![], vec![]) -} - -fn fclosure8(const_index: u32, dst: usize) -> RegInstr { - RegInstr::new( - OpCode::Fclosure8, - ImmOperand::Const(const_index), - vec![dst], - vec![], - ) -} - -fn get_field(field: ImmOperand, dst: usize, obj: usize) -> RegInstr { - RegInstr::new(OpCode::GetField, field, vec![dst], vec![obj]) -} - -fn put_field(field: ImmOperand, value: usize, obj: usize) -> RegInstr { - RegInstr::new(OpCode::PutField, field, vec![], vec![value, obj]) -} - -// ── operand helpers ──────────────────────────────────────────────────────── - -fn atom_of(imm: ImmOperand) -> AtomRef { - match imm { - ImmOperand::AtomU8 { atom, .. } => atom, - _ => unreachable!("expected an atom operand"), - } -} - -fn as_const(imm: ImmOperand) -> u32 { - match imm { - ImmOperand::Const(c) => c, - _ => unreachable!("expected a constant operand"), - } -} - -/// The function id a constant-pool entry points at. -fn fn_id(cpool: &[JsValue], const_index: u32) -> usize { - match cpool[const_index as usize] { - JsValue::FnById(id) => id, - _ => unreachable!("class constant is not a function"), - } -} - -/// The `"prototype"` field name as a predefined-atom operand. -fn proto_atom() -> ImmOperand { - let i = PREDEFINED_ATOMS - .iter() - .position(|&n| n == "prototype") - .unwrap(); - ImmOperand::Atom(AtomRef::Predefined(i as u32)) -} diff --git a/compiler/src/declass/step_2.rs b/compiler/src/declass/step_2.rs deleted file mode 100644 index 2245cfc..0000000 --- a/compiler/src/declass/step_2.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Step 2 of declass: splice the field initialiser into the constructor. -//! -//! A class constructor does not set its own fields; quickjs hoisted them into a -//! separate initialiser function that the constructor reaches through the -//! captured and runs with a call (declass_algo.txt, step 2): -//! -//! get_var_ref_check ; dup ; if_false -//! ; get_loc ; swap ; call_method -//! -//! This step takes the initialiser's field writes -- its body minus the -//! this/home-object prologue and the trailing return -- and splices them into -//! the constructor in place of that call, renumbered onto fresh value ids. The -//! initialiser is left with an empty body. It cannot be dropped from the -//! function list, because a function id is an index into that list. -//! -//! Deficiency, left for step 3: a class with NO instance fields compiles -//! `` to `Undefined` rather than a closure, so there is no -//! initialiser to splice and `field_init_links` records no link. Its -//! constructor keeps the call block above, which the `if_false` skips at run -//! time -- a harmless no-op. Step 3 must strip that block when it drops the -//! `` capture, or the `get_var_ref_check` would dangle. - -use super::step_1::ClassSequence; - -use crate::{ - parse::{ImmOperand, opcodes::OpCode}, - regify::{RegFn, RegInstr}, -}; - -/// The name quickjs gives the hoisted field initialiser. -const FIELDS_INIT: &str = ""; - -pub fn splice_ctors(fns: &mut [RegFn], class_seqs: &[Vec]) { - // Only a class with instance fields has an initialiser to splice. - for seq in class_seqs.iter().flatten() { - if let Some(field_init_fn) = seq.field_init_fn { - splice_one(fns, seq.ctor_fn, field_init_fn); - } - } -} - -fn splice_one(fns: &mut [RegFn], ctor: usize, fields: usize) { - // Find the call block first. If the constructor does not have one there is - // nothing to splice into, and the initialiser has to keep working. - let Some((start, end)) = call_span(&fns[ctor]) else { - return; - }; - - // Take the field writes out of the initialiser (a separate function), then - // hand them to the constructor. - let writes = take_field_writes(&mut fns[fields]); - - let ctor_fn = &mut fns[ctor]; - let writes = renumber(writes, ctor_fn); - ctor_fn.code.splice(start..=end, writes); -} - -/// The initialiser's field writes: its body minus the this/home-object prologue -/// and the trailing return. They reach `this` through get_loc0, which is `this` -/// in the constructor too, so they carry over unchanged. -/// -/// This empties the initialiser, which is dead once its writes move into the -/// constructor. The function itself has to stay: a function id is an index into -/// the function list, so removing one would renumber every function after it. -fn take_field_writes(init: &mut RegFn) -> Vec { - init.code.retain(|i| !is_prologue(i.op)); - std::mem::take(&mut init.code) -} - -/// Renumber the writes' value ids onto fresh ids in the constructor. Each write -/// only uses values it or an earlier write produced, so a flat map suffices. -fn renumber(mut writes: Vec, ctor_fn: &mut RegFn) -> Vec { - let hi = writes - .iter() - .flat_map(|i| i.dsts.iter().chain(&i.args)) - .copied() - .max() - .unwrap_or(0); - - let mut map = vec![usize::MAX; hi + 1]; - for instr in &mut writes { - for arg in &mut instr.args { - *arg = map[*arg]; - } - for dst in &mut instr.dsts { - let fresh = ctor_fn.fresh_val(); - map[*dst] = fresh; - *dst = fresh; - } - } - writes -} - -/// The call block `get_var_ref_check .. call_method`, as an -/// inclusive `start..=end` into the constructor's code. -fn call_span(ctor: &RegFn) -> Option<(usize, usize)> { - let start = ctor.code.iter().position(|i| reads_fields_init(i, ctor))?; - let end = ctor.code[start..] - .iter() - .position(|i| matches!(i.op, OpCode::CallMethod | OpCode::TailCallMethod))?; - Some((start, start + end)) -} - -/// True for `get_var_ref_check` on the `` capture. -fn reads_fields_init(i: &RegInstr, ctor: &RegFn) -> bool { - let ImmOperand::VarRef(k) = i.imm else { - return false; - }; - i.op == OpCode::GetVarRefCheck && ctor.captures[k as usize].name == FIELDS_INIT -} - -/// The initialiser prologue and terminator, dropped when splicing. -fn is_prologue(op: OpCode) -> bool { - matches!( - op, - OpCode::SpecialObject - | OpCode::PushThis - | OpCode::PutLoc0 - | OpCode::PutLoc1 - | OpCode::PutLoc2 - | OpCode::PutLoc3 - | OpCode::Goto8 - | OpCode::Goto - | OpCode::ReturnUndef - ) -} diff --git a/compiler/src/declosure.rs b/compiler/src/declosure.rs deleted file mode 100644 index 7384003..0000000 --- a/compiler/src/declosure.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! declosure: closures become plain objects. -//! -//! A closure becomes an object holding its function plus its captured -//! variables (docs/declosure_algo.txt). Field 0 is the function; fields 1.. -//! are the captures, in capture order. The object doubles as the environment, -//! so there is no second env value to track. Field names are plain integers, -//! so a field operand reads `Atom(int(k))`. -//! -//! Three steps, one property each: -//! -//! 1. `take_env_param` -- every function takes the env as its -//! LAST parameter and reads its captures out of it: `get_var_ref` / -//! `put_var_ref` become field access on the env. -//! 2. `build_closure_objects` -- every `fclosure` builds the object: the -//! function in field 0, then one field per capture, loaded from the -//! parent frame. -//! 3. `route_calls_through_object` -- every plain call reads field 0 and -//! passes the object itself as the trailing env argument. -//! -//! Every function takes an env whether it captures anything or not, so step 3 -//! can append the object at a uniform call site. -//! -//! Two facts about the register form this leans on: -//! -//! * A function is a plain constant -- an index into the cpool. `fclosure` -//! becomes `push_const`, so it loads like any other const, and storing it -//! in a field is no different from storing a number. -//! * The callee of a call is its LAST argument (the deepest stack slot, -//! popped last), so step 3 reads `args.last()`. -//! -//! Known gaps, in rough order of how likely they are to bite: -//! -//! * A captured variable the parent later writes. The write produces a new -//! value instead of storing through the alias, so the closure never sees -//! it. A capture aliases the value a slot held at capture time. -//! * Non-local capture sources. Step 2 can materialise a capture that comes -//! from a parent local or arg. A capture that itself comes from the -//! parent's captures, a module binding, or a global is left as an -//! `undefined` placeholder -- it needs the parent to forward its own env. -//! * Foreign callees. Step 3 rewrites every plain call into a field-0 read. -//! That is only valid for functions we produced; a host builtin such as -//! `Math.sqrt` has no such field. -//! * Entry points. Step 1 gives every function an env parameter, but the -//! module root and any host-invoked function have no caller of ours to -//! supply one. -//! * `call_method` and `call_constructor` carry a receiver, so they belong -//! to the class passes; step 3 leaves them untouched. Same for `apply`. - -use crate::{ - flatten::{Capture, CaptureFrom}, - parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode}, - regify::{RegFn, RegInstr}, -}; - -pub fn declosure(mut reg_fns: Vec) -> Vec { - // Every function's captures, per fn id. Step 2 runs on a parent but needs - // its CHILD's captures, and `declosure_one` only ever holds one function, - // so the cross-function view has to be gathered here. Taking the lists - // moves them -- no per-function allocation -- and leaves every function - // with the empty capture list declosure ends with anyway. - let captures: Vec> = reg_fns - .iter_mut() - .map(|f| std::mem::take(&mut f.captures)) - .collect(); - - reg_fns - .into_iter() - .map(|f| declosure_one(f, &captures)) - .collect() -} - -fn declosure_one(reg_fn: RegFn, captures: &[Vec]) -> RegFn { - let reg_fn = take_env_param(reg_fn); - let reg_fn = build_closure_objects(reg_fn, captures); - route_calls_through_object(reg_fn) -} - -// ── 1. every function takes an env ───────────────────────────────────────── - -/// Append `env` as the last parameter -- existing arguments keep their numbers -/// -- and turn every capture access into a field access on it. -fn take_env_param(mut reg_fn: RegFn) -> RegFn { - let env_arg = reg_fn.arg_count; - reg_fn.arg_count += 1; - - let code = std::mem::take(&mut reg_fn.code); - - // Nothing reads a capture, so there is nothing to rewrite. The parameter - // is still added, to keep step 3's call sites uniform. - if !code.iter().any(|i| is_capture_access(i).is_some()) { - reg_fn.code = code; - return reg_fn; - } - - // Load the env once at entry. - let mut out = Vec::with_capacity(code.len() + 1); - let env = reg_fn.fresh_val(); - out.push(get_arg(env, env_arg)); - - for instr in code { - match is_capture_access(&instr) { - // A read keeps its destination, so downstream uses do not change. - Some(CaptureAccess::Read(k)) => out - .push(get_field(int_field(k + 1), instr.dsts[0], env).starting(instr.block_start)), - Some(CaptureAccess::Write(k)) => out - .push(put_field(int_field(k + 1), instr.args[0], env).starting(instr.block_start)), - None => out.push(instr), - } - } - - reg_fn.code = out; - reg_fn -} - -/// Which capture an instruction reads or writes, if any. -enum CaptureAccess { - Read(u32), - Write(u32), -} - -fn is_capture_access(i: &RegInstr) -> Option { - // The numbered forms fold the slot into the opcode; the rest carry it. - let slot = || match i.imm { - ImmOperand::VarRef(k) => k as u32, - _ => unreachable!("a var_ref opcode without a var_ref operand"), - }; - Some(match i.op { - OpCode::GetVarRef | OpCode::GetVarRefCheck => CaptureAccess::Read(slot()), - OpCode::GetVarRef0 => CaptureAccess::Read(0), - OpCode::GetVarRef1 => CaptureAccess::Read(1), - OpCode::GetVarRef2 => CaptureAccess::Read(2), - OpCode::GetVarRef3 => CaptureAccess::Read(3), - OpCode::PutVarRef | OpCode::PutVarRefCheck | OpCode::PutVarRefCheckInit => { - CaptureAccess::Write(slot()) - } - OpCode::PutVarRef0 => CaptureAccess::Write(0), - OpCode::PutVarRef1 => CaptureAccess::Write(1), - OpCode::PutVarRef2 => CaptureAccess::Write(2), - OpCode::PutVarRef3 => CaptureAccess::Write(3), - _ => return None, - }) -} - -// ── 2. every fclosure builds the object ──────────────────────────────────── - -/// Replace each `fclosure` with the object that stands for it: the function in -/// field 0, then one field per capture, each loaded from the parent frame. -fn build_closure_objects(mut reg_fn: RegFn, captures: &[Vec]) -> RegFn { - let code = std::mem::take(&mut reg_fn.code); - let mut out = Vec::with_capacity(code.len()); - - for instr in code { - let Some(child) = closure_child(&instr, ®_fn.cpool) else { - out.push(instr); - continue; - }; - // The value the rest of the code already uses for this closure. - let closure = instr.dsts[0]; - let child_captures = &captures[child]; - - // The function is a plain constant, so it loads with `push_const`. - let load = match instr.op { - OpCode::Fclosure8 => OpCode::PushConst8, - _ => OpCode::PushConst, - }; - let func = reg_fn.fresh_val(); - out.push(RegInstr::new(load, instr.imm, vec![func], vec![]).starting(instr.block_start)); - - let mut obj = reg_fn.fresh_val(); - out.push(object(obj)); - - // Field 0 is the function, fields 1.. are the captures. Each - // define_field returns the object, so the stores chain -- and the LAST - // must land in `closure`. - for k in 0..=child_captures.len() { - let value = if k == 0 { - func - } else { - let value = reg_fn.fresh_val(); - out.push(load_capture(child_captures[k - 1].from, value)); - value - }; - - let dst = if k == child_captures.len() { - closure - } else { - reg_fn.fresh_val() - }; - - out.push(define_field(int_field(k as u32), dst, value, obj)); - obj = dst; - } - } - - reg_fn.code = out; - reg_fn -} - -/// The nested function an `fclosure` instantiates, as a fn id. -fn closure_child(i: &RegInstr, cpool: &[JsValue]) -> Option { - if !matches!(i.op, OpCode::Fclosure | OpCode::Fclosure8) { - return None; - } - let ImmOperand::Const(index) = i.imm else { - return None; - }; - match cpool.get(index as usize) { - Some(JsValue::FnById(id)) => Some(*id), - _ => None, - } -} - -/// Load one captured value out of the parent frame into `into`. Only parent -/// locals and args are supported; any other source is a preserved gap and -/// loads `undefined`. -fn load_capture(source: CaptureFrom, into: usize) -> RegInstr { - match source { - CaptureFrom::ParentLocal(slot) => get_loc(into, slot), - CaptureFrom::ParentArg(slot) => get_arg(into, slot), - _ => RegInstr::new(OpCode::Undefined, ImmOperand::None, vec![into], vec![]), - } -} - -// ── 3. every call goes through the object ────────────────────────────────── - -/// A call names a closure object now, not a function. Read the function out of -/// field 0, and pass the object itself as the trailing env argument. -fn route_calls_through_object(mut reg_fn: RegFn) -> RegFn { - let code = std::mem::take(&mut reg_fn.code); - let mut out = Vec::with_capacity(code.len()); - - for instr in code { - let Some((op, argc)) = plain_call(&instr) else { - out.push(instr); - continue; - }; - - // The callee is the deepest stack slot, so it is popped last. - let closure = *instr.args.last().unwrap(); - let func = reg_fn.fresh_val(); - out.push(get_field(int_field(0), func, closure).starting(instr.block_start)); - - // Args become [ env, , func ]: the env is pushed last so - // it pops first, and the callee stays last. - let mut args = Vec::with_capacity(instr.args.len() + 1); - args.push(closure); - args.extend_from_slice(&instr.args[..instr.args.len() - 1]); - args.push(func); - - out.push(RegInstr::new( - op, - ImmOperand::Npop(argc + 1), - instr.dsts, - args, - )); - } - - reg_fn.code = out; - reg_fn -} - -/// The opcode to emit and the argument count, for a plain call. `None` if this -/// is not one: the receiver-carrying calls (`call_method`, `call_constructor`, -/// `apply`) are deliberately excluded. -/// -/// A tail call stays a tail call -- it pushes nothing and returns, so turning -/// it into an ordinary `call` would leave a value behind and drop the return. -fn plain_call(i: &RegInstr) -> Option<(OpCode, u16)> { - if i.args.is_empty() { - return None; - } - let npop = || match i.imm { - ImmOperand::Npop(n) => n, - _ => unreachable!("a call opcode without an npop operand"), - }; - Some(match i.op { - OpCode::Call0 => (OpCode::Call, 0), - OpCode::Call1 => (OpCode::Call, 1), - OpCode::Call2 => (OpCode::Call, 2), - OpCode::Call3 => (OpCode::Call, 3), - OpCode::Call => (OpCode::Call, npop()), - OpCode::TailCall => (OpCode::TailCall, npop()), - _ => return None, - }) -} - -// ── emitted instructions ─────────────────────────────────────────────────── - -fn get_arg(dst: usize, arg: usize) -> RegInstr { - RegInstr::new( - OpCode::GetArg, - ImmOperand::Arg(arg as u16), - vec![dst], - vec![], - ) -} - -fn get_loc(dst: usize, loc: usize) -> RegInstr { - RegInstr::new( - OpCode::GetLoc, - ImmOperand::Loc(loc as u16), - vec![dst], - vec![], - ) -} - -fn object(dst: usize) -> RegInstr { - RegInstr::new(OpCode::Object, ImmOperand::None, vec![dst], vec![]) -} - -fn get_field(field: ImmOperand, dst: usize, obj: usize) -> RegInstr { - RegInstr::new(OpCode::GetField, field, vec![dst], vec![obj]) -} - -fn put_field(field: ImmOperand, value: usize, obj: usize) -> RegInstr { - RegInstr::new(OpCode::PutField, field, vec![], vec![value, obj]) -} - -fn define_field(field: ImmOperand, dst: usize, value: usize, obj: usize) -> RegInstr { - RegInstr::new(OpCode::DefineField, field, vec![dst], vec![value, obj]) -} - -/// A plain integer field name, as an operand. -fn int_field(n: u32) -> ImmOperand { - ImmOperand::Atom(AtomRef::TaggedInt(n)) -} diff --git a/compiler/src/demutate.rs b/compiler/src/demutate.rs deleted file mode 100644 index 7c0283e..0000000 --- a/compiler/src/demutate.rs +++ /dev/null @@ -1,235 +0,0 @@ -//! demutate: every static-key field write becomes a call to a function that -//! RETURNS a fresh object. -//! -//! There is no grouping and no analysis of what surrounds a write. Each one -//! stands alone, because its two arguments are the call's two arguments and -//! the enclosing code already holds both: -//! -//! define_field @F [v, o] -> [n] n = (o, v) -//! put_field @F [v, o] n = (o, v) ; store n back -//! -//! `object` is left as it is. It already makes a fresh object, so it needs no -//! function; infer reads it as `Type::Empty`, the base every shape builds on. -//! -//! The lifted functions have no body. `CodeKind::DefineField` / `PutField` -//! name the operation and the field, and infer gives each a type rule, so ONE -//! function serves every write of that field in the program. Their ids run on -//! after the real functions. - -use crate::{ - infer::{CodeKind, InferFn, Type}, - parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode}, - regify::{RegFn, RegInstr}, -}; - -pub fn demutate(reg_fns: Vec) -> Vec { - // The lifted functions take the ids after the real ones, so a write can - // name the one it needs before the list of them is finished - let first_lifted = reg_fns.len(); - let mut lifted = vec![]; - - let mut fns = reg_fns - .into_iter() - .map(|f| rewrite_fn(f, first_lifted, &mut lifted)) - .collect::>(); - - fns.extend(lifted.into_iter().map(|code| InferFn { - name: lifted_name(&code), - cpool: vec![], - code, - sigs: vec![], - types: vec![], - blocks: vec![], - })); - - fns -} - -/// One scan of the code. Everything that is not a static-key write is moved -/// across untouched; a write becomes a call, and a `put_field` a call and a -/// store. -fn rewrite_fn(mut reg_fn: RegFn, first_lifted: usize, lifted: &mut Vec) -> InferFn { - let code = std::mem::take(&mut reg_fn.code); - let mut new = Vec::with_capacity(code.len()); - - for instr in code { - let Some(field) = static_key(&instr) else { - new.push(instr); // Not a field write, so it passes through - continue; - }; - - let define = instr.op == OpCode::DefineField; - - // Where the object lives says where its replacement goes. The - // instruction that made it is already in `new`. - let object = instr.args[1]; - let slot = (!define).then(|| frame_slot(&new, object)).flatten(); - - let callee = intern_const( - &mut reg_fn.cpool, - first_lifted + intern_fn(lifted, instr.op, field), - ); - - let dst = match slot { - // define_field hands the object back, so the call keeps its dst - _ if define => instr.dsts[0], - - // regify does not rename a local, so the local IS the object's - // value: defining it again is the store, and costs no instruction - Some(Source::Loc(_)) => object, - - // Anything else needs a value of its own to be written back from, - // and with no slot at all the write is lost -- `f().x = 1` does that - _ => reg_fn.fresh_val(), - }; - - new.push(call(callee, dst, instr.args).starting(instr.block_start)); - - if let Some(slot) = slot.filter(|s| !matches!(s, Source::Loc(_))) { - new.push(store(slot, dst)); - } - } - - InferFn { - types: vec![Type::Unknown; reg_fn.num_vals()], - blocks: reg_fn.blocks, - name: reg_fn.name, - cpool: reg_fn.cpool, - code: CodeKind::Code(new), - sigs: vec![], - } -} - -/// The field a write names, when the name is fixed at compile time. A computed -/// key is `define_array_el` / `put_array_el`, and is left alone. -fn static_key(i: &RegInstr) -> Option { - let is_write = matches!(i.op, OpCode::DefineField | OpCode::PutField) && i.args.len() == 2; - - match i.imm { - ImmOperand::Atom(a) if is_write => Some(a), - _ => None, - } -} - -/// The index of the function for this write, adding it if it is the first of -/// its kind. One function serves every write of one field by one opcode. -fn intern_fn(lifted: &mut Vec, op: OpCode, field: AtomRef) -> usize { - let wanted = match op { - OpCode::DefineField => CodeKind::DefineField(field), - _ => CodeKind::PutField(field), - }; - - let same = |c: &CodeKind| match (c, &wanted) { - (CodeKind::DefineField(a), CodeKind::DefineField(b)) => a == b, - (CodeKind::PutField(a), CodeKind::PutField(b)) => a == b, - _ => false, - }; - - lifted.iter().position(same).unwrap_or_else(|| { - lifted.push(wanted); - lifted.len() - 1 - }) -} - -/// The cpool slot holding function `id`, adding it if it is not there yet. A -/// call names its function the way every other reference to one does. -fn intern_const(cpool: &mut Vec, id: usize) -> u32 { - let at = cpool - .iter() - .position(|c| matches!(c, JsValue::FnById(f) if *f == id)) - .unwrap_or_else(|| { - cpool.push(JsValue::FnById(id)); - cpool.len() - 1 - }); - - at as u32 -} - -/// The frame slot an object lives in, from whichever instruction defined it. -fn frame_slot(code: &[RegInstr], object: usize) -> Option { - code.iter() - .find(|i| i.dsts.contains(&object)) - .and_then(frame_load) -} - -/// A frame slot, and the two instructions that read and write it. -#[derive(Clone, Copy, PartialEq, Eq)] -enum Source { - Arg(u16), - Loc(u16), - VarRef(u16), -} - -// ── emitted instructions ─────────────────────────────────────────────────── - -/// A call to a function fixed at rewrite time. The callee is named by the -/// immediate rather than pushed, so the call fits where the write stood. -fn call(callee: u32, dst: usize, args: Vec) -> RegInstr { - RegInstr::new(OpCode::Call, ImmOperand::Const(callee), vec![dst], args) -} - -fn store(slot: Source, value: usize) -> RegInstr { - let (op, imm) = match slot { - Source::Arg(k) => (OpCode::PutArg, ImmOperand::Arg(k)), - Source::Loc(k) => (OpCode::PutLoc, ImmOperand::Loc(k)), - Source::VarRef(k) => (OpCode::PutVarRef, ImmOperand::VarRef(k)), - }; - - RegInstr::new(op, imm, vec![], vec![value]) -} - -fn lifted_name(code: &CodeKind) -> String { - match code { - CodeKind::DefineField(a) => format!(""), - CodeKind::PutField(a) => format!(""), - CodeKind::Code(_) => unreachable!("a lifted function has no body"), - } -} - -// ── operand helpers ──────────────────────────────────────────────────────── - -/// The frame slot an instruction reads. Only plain one-value loads count; -/// `get_loc0_loc1` pushes two. -fn frame_load(i: &RegInstr) -> Option { - let slot = |imm| match imm { - ImmOperand::Loc(k) | ImmOperand::Arg(k) | ImmOperand::VarRef(k) => k, - _ => unreachable!("a frame load without a slot operand"), - }; - - match i.op { - OpCode::GetArg0 => Some(Source::Arg(0)), - OpCode::GetArg1 => Some(Source::Arg(1)), - OpCode::GetArg2 => Some(Source::Arg(2)), - OpCode::GetArg3 => Some(Source::Arg(3)), - OpCode::GetArg => Some(Source::Arg(slot(i.imm))), - - OpCode::GetLoc0 => Some(Source::Loc(0)), - OpCode::GetLoc1 => Some(Source::Loc(1)), - OpCode::GetLoc2 => Some(Source::Loc(2)), - OpCode::GetLoc3 => Some(Source::Loc(3)), - // Every local op names the same value, so any of them identifies it - OpCode::GetLoc - | OpCode::GetLoc8 - | OpCode::GetLocCheck - | OpCode::PutLoc - | OpCode::PutLoc8 - | OpCode::PutLocCheck - | OpCode::PutLocCheckInit - | OpCode::SetLoc - | OpCode::SetLoc8 - | OpCode::SetLocUninitialized => Some(Source::Loc(slot(i.imm))), - - OpCode::PutLoc0 | OpCode::SetLoc0 => Some(Source::Loc(0)), - OpCode::PutLoc1 | OpCode::SetLoc1 => Some(Source::Loc(1)), - OpCode::PutLoc2 | OpCode::SetLoc2 => Some(Source::Loc(2)), - OpCode::PutLoc3 | OpCode::SetLoc3 => Some(Source::Loc(3)), - - OpCode::GetVarRef0 => Some(Source::VarRef(0)), - OpCode::GetVarRef1 => Some(Source::VarRef(1)), - OpCode::GetVarRef2 => Some(Source::VarRef(2)), - OpCode::GetVarRef3 => Some(Source::VarRef(3)), - OpCode::GetVarRef | OpCode::GetVarRefCheck => Some(Source::VarRef(slot(i.imm))), - - _ => None, - } -} diff --git a/compiler/src/flatten.rs b/compiler/src/flatten.rs index 4f20a41..de714e5 100644 --- a/compiler/src/flatten.rs +++ b/compiler/src/flatten.rs @@ -1,131 +1,42 @@ -//! This module takes the nested QuickJS binary structure and separates the functions into a flat indexed namespace - -use crate::parse::{ - Atom, AtomRef, ClosureKind, Function, Instr, JsString, JsValue, QuickJSBinary, atom_names, -}; - -#[derive(Debug, Clone, Copy)] -pub enum CaptureFrom { - /// `parent.var_defs[i]` - ParentArg(usize), - /// `parent.var_defs[parent.arg_count + i]` - ParentLocal(usize), - /// `parent.captures[i]`, itself resolved against the grandparent. - ParentCapture(usize), - /// A module-level binding of the module body. - ModuleBinding(usize), - /// Global or module import; not frame-relative. - NotFrameRelative(&'static str), -} - -#[derive(Debug)] -pub struct Capture { - pub name: String, - /// The name as it was on the wire. A field cannot be given a name that is - /// not already an atom, so the operand has to be kept, not rebuilt. - pub atom: AtomRef, - pub from: CaptureFrom, -} +use crate::libqjs::parse::{Atom, AtomRef, ClosureVar, Function, Instr, JsValue, QuickJSBinary}; #[derive(Debug)] pub struct FlatFn { - pub name: String, + pub name: AtomRef, pub arg_count: usize, - pub var_count: usize, // num locals - pub value_names: Vec, // adds names to args and locals, one for each arg, then one for each local, in one vec - pub captures: Vec, + pub var_count: usize, pub cpool: Vec, + pub captures: Vec, pub code: Vec, } pub fn flatten(bin: QuickJSBinary) -> (Vec, Vec) { let mut fns = vec![]; - walk(&mut fns, bin.root_mod.root_fn, &bin.atoms); + walk(&mut fns, bin.root_mod.root_fn); (fns, bin.atoms) } -fn walk(flat_fns: &mut Vec, func: Function, atoms: &[Atom]) -> usize { +fn walk(flat_fns: &mut Vec, func: Function) -> usize { let id = flat_fns.len(); - let name = atom_str(&func.name, atoms); - // Find vars func captures from its environment - let captures = func - .closure_vars - .iter() - .map(|cv| Capture { - name: atom_str(&cv.name, atoms), - atom: cv.name, - from: match cv.flags.closure_kind { - ClosureKind::Arg => CaptureFrom::ParentArg(cv.var_idx), - ClosureKind::Local => CaptureFrom::ParentLocal(cv.var_idx), - ClosureKind::Ref => CaptureFrom::ParentCapture(cv.var_idx), - ClosureKind::ModuleDecl => CaptureFrom::ModuleBinding(cv.var_idx), - ClosureKind::ModuleImport => todo!(), // CaptureFrom::NotFrameRelative("module_import"), - ClosureKind::Global | ClosureKind::GlobalDecl | ClosureKind::GlobalRef => todo!(), // CaptureFrom::NotFrameRelative("global") - }, - }) - .collect(); - - // Reserve this function's slot before recursing into nested functions, - // so it keeps `id` while its children take the ids after it flat_fns.push(FlatFn { - name, + name: func.name, arg_count: func.arg_count, var_count: func.var_count, - value_names: func.var_defs.iter().map(|v| v.name).collect(), - captures, - cpool: Vec::new(), + cpool: vec![], + captures: func.closure_vars, code: func.byte_code, }); - // Nested functions live in the cpool; replace each with its flat id. - let mut cpool = Vec::with_capacity(func.cpool.len()); - for const_entry in func.cpool { - let entry = match const_entry { - JsValue::Function(child) => JsValue::FnById(walk(flat_fns, child, atoms)), + let cpool = func + .cpool + .into_iter() + .map(|c| match c { + JsValue::Function(child) => JsValue::FnById(walk(flat_fns, child)), other => other, - }; - cpool.push(entry); - } - flat_fns[id].cpool = cpool; + }) + .collect(); + flat_fns[id].cpool = cpool; id } - -// LLM GENERATED -// ─── atom rendering ────────────────────────────────────────────────────── - -pub fn atom_str(a: &AtomRef, atoms: &[Atom]) -> String { - match a { - AtomRef::TaggedInt(n) => n.to_string(), - AtomRef::Predefined(i) => atom_names::PREDEFINED_ATOMS - .get(*i as usize) - .copied() - .unwrap_or("") - .to_string(), - AtomRef::FileLocal(i) => atoms - .get(*i) - .map(atom_text) - .unwrap_or_else(|| format!("")), - } -} - -fn atom_text(a: &Atom) -> String { - match a { - Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s), - Atom::Predefined(n) => format!(""), - } -} - -fn js_string(s: &JsString) -> String { - match s { - JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(), - JsString::UTF16(b) => { - let u: Vec = b - .chunks_exact(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) - .collect(); - String::from_utf16_lossy(&u) - } - } -} diff --git a/compiler/src/infer.rs b/compiler/src/infer.rs index c9cb8d1..04b4341 100644 --- a/compiler/src/infer.rs +++ b/compiler/src/infer.rs @@ -1,52 +1,37 @@ +#![allow(non_snake_case)] + +use std::collections::HashMap; + use crate::{ - parse::{AtomRef, ImmOperand, JsValue, opcodes::OpCode}, - regify::{Block, RegInstr}, + libqjs::parse::{ + Atom, AtomRef, ClosureVar, ImmOperand, JsString, JsValue, atom_names, opcodes::OpCode, + }, + regify::{BasicBlock, RegFn, RegInstr}, }; -#[derive(Debug, Clone, PartialEq)] -pub enum Type { - Unknown, - Undefined, - Bool, - Int, - Float, - Empty, - /// A named function, by flat fn id. - Fn(usize), - ReturnOf((usize, Vec)), - Any, -} - -#[derive(Debug)] -pub enum CodeKind { - Code(Vec), +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FnKind { + Id(usize), DefineField(AtomRef), PutField(AtomRef), } -#[derive(Debug)] -pub struct Signature { - pub args: Vec, - pub ret: Type, +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum Type { + Unknown, + Bool, + Int, + Float, + Fn(FnKind), + Any, } -#[derive(Debug)] -pub struct InferFn { - pub name: String, - pub cpool: Vec, - pub code: CodeKind, - pub sigs: Vec, - /// One entry per value id, so a value's type is `types[v]`. regify keeps - /// the first `var_count` ids for the locals, so a local's type lives there - /// too and `put_loc` / `get_loc` just copy between the two. - pub types: Vec, - pub blocks: Vec, -} - -/// The lattice, joining what a value already holds with what it is being -/// given. Unknown is the bottom and gives way to anything; Any is the top and -/// swallows everything; two types that disagree can only be described by Any. -pub fn join(a: &Type, b: &Type) -> Type { +/// Type lattice join rules: +/// Unknown * T = T +/// Any * T = Any +/// T * T = T +/// T * U = Any +fn join_ty(a: &Type, b: &Type) -> Type { match (a, b) { (Type::Unknown, t) | (t, Type::Unknown) => t.clone(), (Type::Any, _) | (_, Type::Any) => Type::Any, @@ -55,347 +40,192 @@ pub fn join(a: &Type, b: &Type) -> Type { } } -/// Walk a function's code once, giving every value a type, and hand back what -/// the function returns. A call is followed down into its callee. -pub fn infer(fn_id: usize, fns: &mut Vec) -> Type { - // The caller left this function's argument types here before recursing. - let args = match fns[fn_id].sigs.first() { - Some(sig) => sig.args.clone(), - None => vec![], - }; +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct InferRequest { + fn_id: usize, + arg_types: Vec, +} - // The two field writers have no body: their result IS the shape term that - // names them, built from the types they were handed. - let code = match &fns[fn_id].code { - CodeKind::DefineField(_) | CodeKind::PutField(_) => return Type::ReturnOf((fn_id, args)), +#[derive(Debug)] +pub struct InferResult { + val_types: Vec, + ret_type: Type, +} - // An empty body means this function's code is out on the stack, so we - // have come back round a cycle. Unknown is the bottom, and a later - // round raises it. - CodeKind::Code(c) if c.is_empty() => return Type::Unknown, +pub type TypeCache = HashMap; - CodeKind::Code(_) => { - let taken = std::mem::replace(&mut fns[fn_id].code, CodeKind::Code(vec![])); - let CodeKind::Code(code) = taken else { - unreachable!() - }; - code +pub fn infer_types(reg_fns: &[RegFn], atoms: &[Atom]) -> TypeCache { + // Find main fn + let main_fn_id = reg_fns + .iter() + .position(|f| atom_to_str(&f.name, atoms) == "main") + .unwrap(); + + let mut cache = TypeCache::with_capacity(reg_fns.len()); + let mut worklist = vec![]; + + worklist.push(InferRequest { + fn_id: main_fn_id, + arg_types: vec![], + }); + + while let Some(req) = worklist.pop() { + match walk_fn(&req, reg_fns, &mut cache) { + Ok(res) => { + cache.insert(req, res); + } + Err(new_req) => { + worklist.push(new_req); + worklist.push(req); + } } - }; - - if fns[fn_id].sigs.is_empty() { - fns[fn_id].sigs.push(Signature { - args: vec![], - ret: Type::Unknown, - }); } - // The types move out too, so a callee can be inferred through `fns` while - // this function is still being walked. Both go back at the end. - let mut types = std::mem::take(&mut fns[fn_id].types); - let blocks = std::mem::take(&mut fns[fn_id].blocks); + cache +} - // The blocks, read back off the instructions. Passes have moved them, but - // none of them adds or removes a jump, so this is the partition regify - // found and `succ` still lines up with it. Anything inserted above the - // first boundary belongs to the first block. - let mut starts = (code.iter().enumerate()) - .filter(|(_, i)| i.block_start) - .map(|(at, _)| at) - .collect::>(); +fn walk_fn( + req: &InferRequest, + reg_fns: &[RegFn], + cache: &mut TypeCache, +) -> Result { + let f = ®_fns[req.fn_id]; + let mut val_types = vec![Type::Unknown; f.val_count]; + let mut ret_type = Type::Unknown; - if let Some(first) = starts.first_mut() { - *first = 0; + let mut worklist = vec![0]; + + while let Some(block_id) = worklist.pop() { + let block = &f.blocks[block_id]; + walk_block(&block, &req.arg_types, f, &mut val_types, &mut ret_type)?; + + worklist.extend(block.succ.iter()); } - // One flag in, one block out. A pass may move a boundary or insert around - // it, but it must carry the flag when it REPLACES the instruction holding - // one -- that is what `RegInstr::starting` is for. Miss it and the blocks - // silently stop lining up with the edges regify recorded. - debug_assert_eq!( - starts.len(), - blocks.len(), - "{}: block starts lost or gained since regify", - fns[fn_id].name - ); - let mut ret = Type::Unknown; + Ok(InferResult { + val_types, + ret_type, + }) +} - // What each block is entered with, joined from the blocks that reach it. - let mut entry = vec![vec![Type::Unknown; types.len()]; starts.len()]; +fn walk_block( + block: &BasicBlock, + arg_types: &[Type], + f: &RegFn, + val_types: &mut [Type], + ret_type: &mut Type, +) -> Result<(), InferRequest> { + use OpCode::*; - for (b, &start) in starts.iter().enumerate() { - let end = starts.get(b + 1).copied().unwrap_or(code.len()); + for instr in &f.code[block.start..block.end] { + let ty = match instr.op { + // ──────────────────────────────────── Literals ──────────────────────────────────── + PushMinus1 | Push0 | Push1 | Push2 | Push3 | Push4 | Push5 | Push6 | Push7 | PushI8 + | PushI16 | PushI32 => Type::Int, - // Fold in what the blocks reaching this one left behind. Two paths - // meeting is the ONLY place the algebra applies -- everything below - // runs in a straight line. - for (t, e) in types.iter_mut().zip(&entry[b]) { - *t = join(t, e); - } + PushTrue | PushFalse => Type::Bool, - // A slot at a merge keeps the ids the first path in left, so every - // other path's value has to be folded into them. Without this the - // merge reads one arm and believes it. - for &(slot, incoming) in &blocks[b].phi { - types[slot] = join(&types[slot], &types[incoming]); - } + PushConst | PushConst8 | Fclosure | Fclosure8 => match instr.imm { + ImmOperand::Const(k) => match f.cpool.get(k as usize) { + Some(JsValue::Int(_)) => Type::Int, + Some(JsValue::Float(_)) => Type::Float, + Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool, + Some(&JsValue::FnById(id)) => Type::Fn(FnKind::Id(id)), + _ => panic!(), + }, + _ => panic!(), + }, - for instr in &code[start..end] { - // regify does not rename locals, so a load just names the local's value - // again. It must not write to it, or it would wipe what is stored there. - let load = matches!( - instr.op, - OpCode::GetLoc0 - | OpCode::GetLoc1 - | OpCode::GetLoc2 - | OpCode::GetLoc3 - | OpCode::GetLoc0Loc1 - | OpCode::GetLoc - | OpCode::GetLoc8 - | OpCode::GetLocCheck - ); - - let ty = match instr.op { - // ── literals ──────────────────────────────────────────────── - OpCode::PushMinus1 - | OpCode::Push0 - | OpCode::Push1 - | OpCode::Push2 - | OpCode::Push3 - | OpCode::Push4 - | OpCode::Push5 - | OpCode::Push6 - | OpCode::Push7 - | OpCode::PushI8 - | OpCode::PushI16 - | OpCode::PushI32 => Type::Int, - - OpCode::PushTrue | OpCode::PushFalse => Type::Bool, - OpCode::Undefined => Type::Undefined, - OpCode::Object => Type::Empty, - - OpCode::PushConst | OpCode::PushConst8 | OpCode::Fclosure | OpCode::Fclosure8 => { - match instr.imm { - ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) { - Some(JsValue::Int(_)) => Type::Int, - Some(JsValue::Float(_)) => Type::Float, - Some(JsValue::BoolTrue) | Some(JsValue::BoolFalse) => Type::Bool, - Some(JsValue::Undefined) => Type::Undefined, - Some(&JsValue::FnById(id)) => Type::Fn(id), - _ => Type::Any, - }, - _ => Type::Any, - } - } - - // ── field reads ───────────────────────────────────────────── - // Walk the shape term from the outside in: the newest write of the - // field wins, which is what makes a later write shadow an earlier. - OpCode::GetField => { - let field = match instr.imm { - ImmOperand::Atom(a) => a, - _ => AtomRef::TaggedInt(u32::MAX), - }; - - let mut ty = types[instr.args[0]].clone(); - - loop { - let (id, args) = match ty { - Type::ReturnOf((id, args)) if args.len() == 2 => (id, args), - Type::Empty => break Type::Undefined, // No fields at all - _ => break Type::Any, - }; - - match &fns[id].code { - CodeKind::DefineField(a) | CodeKind::PutField(a) if *a == field => { - break args[1].clone(); - } - CodeKind::DefineField(_) | CodeKind::PutField(_) => { - ty = args[0].clone() - } - CodeKind::Code(_) => break Type::Any, // Not a shape we built - } - } - } - - // ── arithmetic ────────────────────────────────────────────── - // A float anywhere makes the result a float; anything else can be - // a string concatenation or an object coercion, so it is Any. - OpCode::Add - | OpCode::Sub - | OpCode::Mul - | OpCode::Div - | OpCode::Mod - | OpCode::Pow => match (&types[instr.args[0]], &types[instr.args[1]]) { + // ─────────────────────────────────── Arithmetic ─────────────────────────────────── + Add | Sub | Mul | Div | Mod | Pow => { + match (&val_types[instr.args[0]], &val_types[instr.args[1]]) { (Type::Int, Type::Int) => Type::Int, (Type::Float, Type::Float) | (Type::Int, Type::Float) | (Type::Float, Type::Int) => Type::Float, _ => Type::Any, - }, - - OpCode::Neg | OpCode::Plus | OpCode::Inc | OpCode::Dec => { - match types[instr.args[0]] { - Type::Int => Type::Int, - Type::Float => Type::Float, - _ => Type::Any, - } - } - - OpCode::Lt - | OpCode::Lte - | OpCode::Gt - | OpCode::Gte - | OpCode::Eq - | OpCode::Neq - | OpCode::StrictEq - | OpCode::StrictNeq - | OpCode::Not => Type::Bool, - - // ── calls ─────────────────────────────────────────────────── - // demutate names a lifted callee in the immediate. Anything else - // is a callee off the stack, which this does not resolve yet. - OpCode::Call - | OpCode::Call0 - | OpCode::Call1 - | OpCode::Call2 - | OpCode::Call3 - | OpCode::TailCall => { - // demutate names its callee in the immediate; every other call - // takes it off the stack, where it is deepest and so comes last. - // Reversing the rest puts them back in written order. - let (callee, args) = match instr.imm { - ImmOperand::Const(k) => match fns[fn_id].cpool.get(k as usize) { - Some(&JsValue::FnById(id)) => (Some(id), &instr.args[..]), - _ => (None, &instr.args[..]), - }, - _ => match instr.args.last().map(|a| &types[*a]) { - Some(&Type::Fn(id)) => (Some(id), &instr.args[..instr.args.len() - 1]), - _ => (None, &instr.args[..]), - }, - }; - - let result = match callee { - Some(callee) => { - let args = args.iter().rev().map(|a| types[*a].clone()).collect(); - fns[callee].sigs = vec![Signature { - args, - ret: Type::Unknown, - }]; - infer(callee, fns) - } - None => Type::Any, - }; - - // A tail call hands its result straight back, so it ends - // the function as much as a `return` does. - if instr.op == OpCode::TailCall { - ret = join(&ret, &result); - } - - result - } - - // A method call keeps its receiver under the callee, so the - // callee is not the last argument and the lookup above does not - // find it. Resolving that is not done yet. - OpCode::CallMethod | OpCode::TailCallMethod => { - if instr.op == OpCode::TailCallMethod { - ret = join(&ret, &Type::Any); - } - - Type::Any - } - - // ── globals ───────────────────────────────────────────────── - // A free name is a property of the global object, which nothing - // here models -- every built-in reaches this way too. Writing - // one (`put_var`) records nothing, because a global is not a - // value and has no id to record it against. - OpCode::GetVar | OpCode::GetVarUndef => Type::Any, - - // ── arguments ─────────────────────────────────────────────── - OpCode::GetArg0 => args.first().cloned().unwrap_or(Type::Undefined), - OpCode::GetArg1 => args.get(1).cloned().unwrap_or(Type::Undefined), - OpCode::GetArg2 => args.get(2).cloned().unwrap_or(Type::Undefined), - OpCode::GetArg3 => args.get(3).cloned().unwrap_or(Type::Undefined), - OpCode::GetArg => match instr.imm { - ImmOperand::Arg(k) => args.get(k as usize).cloned().unwrap_or(Type::Undefined), - _ => Type::Any, - }, - - // ── value movers ──────────────────────────────────────────── - // These hand the value straight back, so the type goes through. - // dup pushes it twice, which the assignment below spreads over - // both destinations. - // A store into a local is one of these too: the local IS the - // destination value, so the type simply moves across. - OpCode::SetName - | OpCode::SetNameComputed - | OpCode::SetHomeObject - | OpCode::Dup - | OpCode::PutLoc0 - | OpCode::PutLoc1 - | OpCode::PutLoc2 - | OpCode::PutLoc3 - | OpCode::PutLoc - | OpCode::PutLoc8 - | OpCode::PutLocCheck - | OpCode::PutLocCheckInit - | OpCode::SetLoc0 - | OpCode::SetLoc1 - | OpCode::SetLoc2 - | OpCode::SetLoc3 - | OpCode::SetLoc - | OpCode::SetLoc8 => types[instr.args[0]].clone(), - - OpCode::SetLocUninitialized => Type::Unknown, - - // ── returns ───────────────────────────────────────────────── - // Every way out has to feed `ret`. Missing one leaves it at - // Unknown, the BOTTOM, which a caller folds away and believes - // -- where Any would have been distrusted. - OpCode::Return | OpCode::ReturnAsync => { - ret = join(&ret, &types[instr.args[0]]); - Type::Unknown - } - OpCode::ReturnUndef => { - ret = join(&ret, &Type::Undefined); - Type::Unknown - } - - // ── everything else ───────────────────────────────────────── - _ => Type::Any, - }; - - // A write joins with what the value already holds, so - // reassigning a local a different type widens it to Any. Almost - // every reassignment keeps the type, and join(T, T) = T, so the - // common path costs nothing. - // - // Nothing branches inside a block, so plain `types[dst] = ty` - // would be sound here and would keep `let x = 1; x = 2.5` as - // Float. That is a one-line swap; the blocks do their own job - // either way, which is to join where paths MEET. - if !load { - for &dst in &instr.dsts { - types[dst] = join(&types[dst], &ty); } } - } - // Hand the state on to every block this one reaches. - for &s in &blocks[b].succ { - for (e, t) in entry[s].iter_mut().zip(&types) { - *e = join(e, t); + Neg | Plus | Inc | Dec => match val_types[instr.args[0]] { + Type::Int => Type::Int, + Type::Float => Type::Float, + _ => Type::Any, + }, + + Lt | Lte | Gt | Gte | Eq | Neq | StrictEq | StrictNeq | Not => Type::Bool, + + // ────────────────────────────────── Value Movers ────────────────────────────────── + Dup | PutLoc0 | PutLoc1 | PutLoc2 | PutLoc3 | PutLoc | PutLoc8 | PutLocCheck + | PutLocCheckInit | SetLoc0 | SetLoc1 | SetLoc2 | SetLoc3 | SetLoc | SetLoc8 => { + val_types[instr.args[0]].clone() + } + + SetLocUninitialized => Type::Unknown, + + // ────────────────────────────────────── Returns ─────────────────────────────────── + Return | ReturnAsync | ReturnUndef => val_types[instr.args[0]].clone(), + + _ => Type::Unknown, + }; + + let is_ret = matches!(instr.op, Return | ReturnAsync | ReturnUndef); + let is_loc_read = matches!( + instr.op, + OpCode::GetLoc0 + | OpCode::GetLoc1 + | OpCode::GetLoc2 + | OpCode::GetLoc3 + | OpCode::GetLoc0Loc1 + | OpCode::GetLoc + | OpCode::GetLoc8 + | OpCode::GetLocCheck + ); + + // TODO: is_loc_read logic is ungraceful + if is_ret { + *ret_type = join_ty(ret_type, &ty); + } else if !is_loc_read { + for &dst in &instr.dsts { + val_types[dst] = join_ty(&val_types[dst], &ty); } } } - fns[fn_id].types = types; - fns[fn_id].blocks = blocks; - fns[fn_id].code = CodeKind::Code(code); - fns[fn_id].sigs[0].ret = ret.clone(); + Ok(()) +} - ret +pub fn atom_to_str(a: &AtomRef, atoms: &[Atom]) -> String { + match a { + AtomRef::TaggedInt(n) => n.to_string(), + AtomRef::Predefined(i) => atom_names::PREDEFINED_ATOMS + .get(*i as usize) + .copied() + .unwrap_or("") + .to_string(), + AtomRef::FileLocal(i) => atoms + .get(*i) + .map(atom_text) + .unwrap_or_else(|| format!("")), + } +} + +fn atom_text(a: &Atom) -> String { + match a { + Atom::String(s) | Atom::GlobalSymbol(s) | Atom::Symbol(s) => js_string(s), + Atom::Predefined(n) => format!(""), + } +} + +fn js_string(s: &JsString) -> String { + match s { + JsString::Latin1(b) => b.iter().map(|&c| c as char).collect(), + JsString::UTF16(b) => { + let u: Vec = b + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + String::from_utf16_lossy(&u) + } + } } diff --git a/compiler/src/libqjs.rs b/compiler/src/libqjs.rs deleted file mode 100644 index 1ddb265..0000000 --- a/compiler/src/libqjs.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::ffi::{CStr, CString, c_char, c_int, c_void}; - -/// Compile `source` to quickjs bytecode, as `qjsc -b` does. -/// -/// `name` is the module name quickjs records and reports errors against; it -/// does not have to be a path that exists. -pub fn compile(source: &str, name: &str) -> Result, String> { - // A runtime and a context per call: compiling holds no state worth reusing, - // and this way nothing outlives the returned bytes. - unsafe { - let rt = JS_NewRuntime(); - let ctx = JS_NewContext(rt); - - let result = compile_in(ctx, source, name); - - JS_FreeContext(ctx); - JS_FreeRuntime(rt); - - result - } -} - -/// The body of `compile`, so the runtime is freed on either outcome. -unsafe fn compile_in(ctx: *mut JSContext, source: &str, name: &str) -> Result, String> { - // JS_Eval requires `input[input_len] == 0`, which a &str does not promise: - // the byte past the end is whatever the allocator left there. Read as a - // token, that byte decides whether the source parses. - let source = CString::new(source).map_err(|_| "source has a nul byte".to_string())?; - let name = CString::new(name).map_err(|_| "module name has a nul byte".to_string())?; - - unsafe { - // COMPILE_ONLY stops at the bytecode, so nothing in the source runs. - let obj = JS_Eval( - ctx, - source.as_ptr(), - source.as_bytes().len(), - name.as_ptr(), - JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY, - ); - - if obj.tag == JS_TAG_EXCEPTION { - return Err(exception(ctx)); - } - - let mut len = 0; - let buf = JS_WriteObject(ctx, &mut len, obj, JS_WRITE_OBJ_BYTECODE); - JS_FreeValue(ctx, obj); - - if buf.is_null() { - return Err(exception(ctx)); - } - - let bytes = std::slice::from_raw_parts(buf, len).to_vec(); - js_free(ctx, buf.cast::()); - - Ok(bytes) - } -} - -/// The pending exception, as text. Taking it also clears it. -unsafe fn exception(ctx: *mut JSContext) -> String { - unsafe { - let err = JS_GetException(ctx); - let text = JS_ToCStringLen2(ctx, std::ptr::null_mut(), err, false); - - let message = if text.is_null() { - "quickjs raised an exception it could not describe".to_string() - } else { - CStr::from_ptr(text).to_string_lossy().into_owned() - }; - - if !text.is_null() { - JS_FreeCString(ctx, text); - } - - JS_FreeValue(ctx, err); - message - } -} - -// ── the C side ───────────────────────────────────────────────────────────── - -enum JSRuntime {} -enum JSContext {} - -/// 16 bytes passed and returned BY VALUE. quickjs only nan-boxes on 32-bit -/// targets (`JS_NAN_BOXING`, quickjs.h), where this layout would be wrong. -#[repr(C)] -#[derive(Clone, Copy)] -struct JSValue { - u: JSValueUnion, - tag: i64, -} - -#[repr(C)] -#[derive(Clone, Copy)] -union JSValueUnion { - int32: i32, - float64: f64, - ptr: *mut c_void, -} - -// `JS_IsException` and `JS_ToCString` are `static inline` in the header, so -// they are not symbols to link against. These are the constants they read. -const JS_TAG_EXCEPTION: i64 = 6; -const JS_EVAL_TYPE_MODULE: c_int = 1 << 0; -const JS_EVAL_FLAG_COMPILE_ONLY: c_int = 1 << 5; -const JS_WRITE_OBJ_BYTECODE: c_int = 1 << 0; - -unsafe extern "C" { - fn JS_NewRuntime() -> *mut JSRuntime; - fn JS_FreeRuntime(rt: *mut JSRuntime); - fn JS_NewContext(rt: *mut JSRuntime) -> *mut JSContext; - fn JS_FreeContext(ctx: *mut JSContext); - - fn JS_Eval( - ctx: *mut JSContext, - input: *const c_char, - input_len: usize, - filename: *const c_char, - eval_flags: c_int, - ) -> JSValue; - - fn JS_WriteObject( - ctx: *mut JSContext, - psize: *mut usize, - obj: JSValue, - flags: c_int, - ) -> *mut u8; - - fn JS_GetException(ctx: *mut JSContext) -> JSValue; - fn JS_ToCStringLen2( - ctx: *mut JSContext, - plen: *mut usize, - val: JSValue, - cesu8: bool, - ) -> *const c_char; - fn JS_FreeCString(ctx: *mut JSContext, ptr: *const c_char); - fn JS_FreeValue(ctx: *mut JSContext, v: JSValue); - fn js_free(ctx: *mut JSContext, ptr: *mut c_void); -} diff --git a/compiler/src/libqjs/ffi.rs b/compiler/src/libqjs/ffi.rs new file mode 100644 index 0000000..1f05f0f --- /dev/null +++ b/compiler/src/libqjs/ffi.rs @@ -0,0 +1,57 @@ +use std::ffi::{c_int, c_void}; + +pub const JS_TAG_EXCEPTION: i64 = 6; +pub const JS_EVAL_TYPE_MODULE: c_int = 1 << 0; +pub const JS_EVAL_FLAG_COMPILE_ONLY: c_int = 1 << 5; +pub const JS_WRITE_OBJ_BYTECODE: c_int = 1 << 0; + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct JSValue { + u: JSValueUnion, + pub tag: i64, +} + +#[repr(C)] +#[derive(Clone, Copy)] +union JSValueUnion { + int32: i32, + float64: f64, + ptr: *mut c_void, +} + +pub type JSRuntime = c_void; +pub type JSContext = c_void; + +unsafe extern "C" { + pub fn JS_NewRuntime() -> *mut JSRuntime; + pub fn JS_FreeRuntime(rt: *mut JSRuntime); + pub fn JS_NewContext(rt: *mut JSRuntime) -> *mut JSContext; + pub fn JS_FreeContext(ctx: *mut JSContext); + + pub fn JS_Eval( + ctx: *mut JSContext, + input: *const u8, + input_len: usize, + filename: *const u8, + eval_flags: c_int, + ) -> JSValue; + + pub fn JS_FreeValue(ctx: *mut JSContext, v: JSValue); + + pub fn JS_WriteObject( + ctx: *mut JSContext, + psize: *mut usize, + obj: JSValue, + flags: c_int, + ) -> *mut u8; + + pub fn JS_GetException(ctx: *mut JSContext) -> JSValue; + + pub fn JS_ToCStringLen2( + ctx: *mut JSContext, + plen: *mut usize, + val: JSValue, + cesu8: bool, + ) -> *const i8; +} diff --git a/compiler/src/libqjs/mod.rs b/compiler/src/libqjs/mod.rs new file mode 100644 index 0000000..3f731c8 --- /dev/null +++ b/compiler/src/libqjs/mod.rs @@ -0,0 +1,53 @@ +mod ffi; +pub mod parse; + +use std::ffi::{CStr, CString}; + +use parse::QuickJSBinary; + +pub fn compile(src: &[u8], filename: &str) -> QuickJSBinary { + unsafe { + let filename = CString::new(filename).unwrap(); + + let rt = ffi::JS_NewRuntime(); + let ctx = ffi::JS_NewContext(rt); + + let obj = ffi::JS_Eval( + ctx, + src.as_ptr(), + src.len(), + filename.as_ptr().cast(), + ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE, + ); + + assert!(obj.tag != ffi::JS_TAG_EXCEPTION, "{}", get_exception(ctx)); + + let mut len = 0; + let buf = ffi::JS_WriteObject(ctx, &mut len, obj, ffi::JS_WRITE_OBJ_BYTECODE); + ffi::JS_FreeValue(ctx, obj); + + assert!(!buf.is_null()); + let bytes = std::slice::from_raw_parts(buf, len).to_vec(); + + // TODO: Are we sure other cleanup is not necessary? + ffi::JS_FreeContext(ctx); + ffi::JS_FreeRuntime(rt); + + QuickJSBinary::parse(&bytes).unwrap().1 + } +} + +fn get_exception(ctx: *mut ffi::JSContext) -> String { + unsafe { + let err = ffi::JS_GetException(ctx); + let text = ffi::JS_ToCStringLen2(ctx, std::ptr::null_mut(), err, false); + + let message = if text.is_null() { + "quickjs raised an exception it could not describe".to_string() + } else { + CStr::from_ptr(text).to_string_lossy().into_owned() + }; + + message + } +} diff --git a/compiler/src/parse.rs b/compiler/src/libqjs/parse.rs similarity index 99% rename from compiler/src/parse.rs rename to compiler/src/libqjs/parse.rs index 4a6aa71..041e389 100644 --- a/compiler/src/parse.rs +++ b/compiler/src/libqjs/parse.rs @@ -1,5 +1,3 @@ -//! This module takes the QuickJS produced binary and parses it into an identical structured represention - use std::fmt::Debug; use std::fmt::Write; @@ -14,11 +12,11 @@ use nom::{ use num_enum::TryFromPrimitive; pub mod atom_names { - include!("../../c_parse/atom_names.rs"); + include!("../../../c_parse/atom_names.rs"); } pub mod opcodes { - include!("../../c_parse/opcodes.rs"); + include!("../../../c_parse/opcodes.rs"); } #[derive(Debug)] @@ -652,7 +650,6 @@ pub struct Function { pub flags: FunctionFlags, pub is_strict_mode: bool, pub name: AtomRef, - /// Regular declared params pub arg_count: usize, /// Num locals pub var_count: usize, diff --git a/compiler/src/main.rs b/compiler/src/main.rs index 2e75a1b..bdd7aa3 100644 --- a/compiler/src/main.rs +++ b/compiler/src/main.rs @@ -1,58 +1,37 @@ -mod declass; -mod declosure; -mod demutate; mod flatten; mod infer; mod libqjs; -mod parse; mod regify; -use declass::declass; -use declosure::declosure; -use demutate::demutate; +use std::fs::File; +use std::io::Read; + use flatten::flatten; -use infer::infer; -use parse::QuickJSBinary; +use infer::infer_types; use regify::regify; fn main() { + // Load source file let args = std::env::args().collect::>(); let filename = args.get(1).unwrap(); - // Compile source file - let source = std::fs::read_to_string(filename).unwrap(); - let bin = libqjs::compile(&source, filename).unwrap(); + let mut file = File::open(filename).unwrap(); + let metadata = std::fs::metadata(filename).unwrap(); + assert!(metadata.is_file()); - // Parse, then flatten - let (_, parsed) = QuickJSBinary::parse(&bin).unwrap(); - let (flat_fns, atoms) = flatten(parsed); + let mut src = Vec::with_capacity(metadata.len() as usize); + file.read_to_end(&mut src).unwrap(); - println!("{flat_fns:#?}"); + // Compile to bytecode + let binary = libqjs::compile(&src, filename); - println!("=== after regify ==="); + // Process it + let (flat_fns, atoms) = flatten(binary); let reg_fns = regify(flat_fns); println!("{reg_fns:#?}"); - - println!("=== after declass ==="); - let declassed = declass(reg_fns); - println!("{declassed:#?}"); - - println!("=== after declosure ==="); - let declosured = declosure(declassed); - println!("{declosured:#?}"); - - println!("=== after demutate ==="); - let mut infer_fns = demutate(declosured); - println!("{infer_fns:#?}"); - - println!("=== after infer ==="); - if let Some(main_id) = infer_fns.iter().position(|f| f.name == "main") { - let ret = infer(main_id, &mut infer_fns); - println!("main returns {ret:?}"); - println!("{infer_fns:#?}"); - } else { - println!("no main"); - } - println!("{atoms:#?}"); + + let type_cache = infer_types(®_fns, &atoms); + + println!("{type_cache:#?}"); } diff --git a/compiler/src/regify/basic_block.rs b/compiler/src/regify/basic_block.rs index 927d1f0..fbeacee 100644 --- a/compiler/src/regify/basic_block.rs +++ b/compiler/src/regify/basic_block.rs @@ -1,91 +1,106 @@ -//! A block is a run of instructions control enters at the top and leaves at the -//! bottom. Blocks are all regify needs: they say what stack a block is entered -//! with, and where two paths meet -- the only thing a linear walk gets wrong. +use crate::libqjs::parse::{ImmOperand, Instr, opcodes::OpCode}; -use crate::parse::{ImmOperand, Instr, opcodes::OpCode}; +type BlockId = usize; #[derive(Debug)] -pub struct Block { - /// The blocks control can reach from this one. - pub succ: Vec, - - /// `(slot value, incoming value)` for every path in AFTER the first. The - /// slot keeps the id the first path gave it, so a later path's value has to - /// be joined into it. That is the phi, and it stays data because nothing - /// here has to run it -- infer folds the types together instead. - pub phi: Vec<(usize, usize)>, +pub struct BasicBlock { + pub start: usize, + pub end: usize, + pub succ: Vec, } -pub fn find_basic_blocks(code: &[Instr], pcs: &[usize]) -> (Vec, Vec) { - let mut starts = vec![0]; +impl BasicBlock { + fn new(start: usize) -> Self { + Self { + start, + end: 0, + succ: vec![], + } + } +} - for (at, instr) in code.iter().enumerate() { - if let Some(target) = jump_target(instr.imm_operands, pcs) { - starts.push(target); +pub fn find_basic_blocks(code: &[Instr]) -> Vec { + // Find block starts: the first instruction, every jump target, + // and whatever follows a jump or a return + let mut blocks = vec![BasicBlock::new(0)]; + + for (i, instr) in code.iter().enumerate() { + // If cond/uncond jump, jump target is a start, next instr (if exists) is another start + if let Some(target) = is_jump_instr(instr, code) { + blocks.push(BasicBlock::new(target)); + + if i + 1 < code.len() { + blocks.push(BasicBlock::new(i + 1)); + } } - if ends_block(instr, pcs) && at + 1 < code.len() { - starts.push(at + 1); + // If terminal, next instr (if exists) is a start + if is_terminal_instr(instr) { + if i + 1 < code.len() { + blocks.push(BasicBlock::new(i + 1)); + } } } - starts.sort_unstable(); - starts.dedup(); + blocks.sort_unstable_by_key(|b| b.start); + blocks.dedup_by_key(|b| b.start); - let ends = |i: usize| starts.get(i + 1).copied().unwrap_or(code.len()); + // Populate block end indices + for i in 0..blocks.len() { + blocks[i].end = blocks.get(i + 1).map(|b| b.start).unwrap_or(code.len()); + } - // A block reaches wherever its last instruction goes: the jump's target, - // and the block below whenever control can run off the end. - let blocks = (0..starts.len()) - .map(|i| { - let last = &code[ends(i) - 1]; - let mut succ = vec![]; + // Find block successors + for i in 0..blocks.len() { + let end = blocks[i].end; + let last_instr = &code[end - 1]; - if let Some(t) = jump_target(last.imm_operands, pcs) { - succ.extend(starts.iter().position(|&s| s == t)); - } + // Jump targets are a successor + if let Some(target) = is_jump_instr(last_instr, code) { + let succ_block = blocks.iter().position(|b| b.start == target).unwrap(); + blocks[i].succ.push(succ_block); + } - if falls_through(last.op) && i + 1 < starts.len() { - succ.push(i + 1); - } + // Fall through blocks are a successor + if falls_through(last_instr) && i + 1 < blocks.len() { + blocks[i].succ.push(i + 1); + } + } - Block { succ, phi: vec![] } - }) - .collect(); - - (starts, blocks) + blocks } -/// True when a block cannot carry on past this instruction, because it jumps -/// or because it leaves the function. -fn ends_block(instr: &Instr, pcs: &[usize]) -> bool { - !falls_through(instr.op) || jump_target(instr.imm_operands, pcs).is_some() +fn falls_through(instr: &Instr) -> bool { + use OpCode::*; + let uncond_jump = matches!(instr.op, Goto | Goto8 | Goto16); + + !uncond_jump && !is_terminal_instr(instr) } -/// The instruction a jump lands on. A target arrives as a byte offset, so it is -/// looked up in the pcs; one past the end lands nowhere and is dropped. -fn jump_target(imm: ImmOperand, pcs: &[usize]) -> Option { - let target = match imm { - ImmOperand::Label { target } - | ImmOperand::LabelU16 { target, .. } - | ImmOperand::AtomLabelU8 { target, .. } - | ImmOperand::AtomLabelU16 { target, .. } => target, - _ => return None, +fn is_jump_instr(instr: &Instr, code: &[Instr]) -> Option { + use OpCode::*; + + let is_jump = matches!( + instr.op, + Goto | Goto8 | Goto16 | IfFalse | IfTrue | IfFalse8 | IfTrue8 + ); + + if !is_jump { + return None; + } + + let target = match instr.imm_operands { + ImmOperand::Label { target } => code.iter().position(|x| x.pc == target).unwrap(), + _ => unreachable!(), }; - pcs.iter().position(|&pc| pc == target) + Some(target) } -/// True when control can leave a block by running off the end of it. -fn falls_through(op: OpCode) -> bool { - !matches!( - op, - OpCode::Goto - | OpCode::Goto8 - | OpCode::Goto16 - | OpCode::Return - | OpCode::ReturnUndef - | OpCode::ReturnAsync - | OpCode::Throw +fn is_terminal_instr(instr: &Instr) -> bool { + use OpCode::*; + matches!( + instr.op, + Return | ReturnUndef | ReturnAsync | Throw | TailCall | TailCallMethod | Ret ) } diff --git a/compiler/src/regify/mod.rs b/compiler/src/regify/mod.rs index fe62ec1..3dbc901 100644 --- a/compiler/src/regify/mod.rs +++ b/compiler/src/regify/mod.rs @@ -1,21 +1,15 @@ mod basic_block; -use std::fmt::Debug; - use crate::{ - flatten::{Capture, FlatFn}, - parse::{ImmOperand, JsValue, opcodes::OpCode}, + flatten::FlatFn, + libqjs::parse::{AtomRef, ClosureVar, ImmOperand, Instr, JsValue, opcodes::OpCode}, }; -pub use basic_block::Block; +pub use basic_block::BasicBlock; use basic_block::find_basic_blocks; #[derive(Clone)] pub struct RegInstr { - /// True on the first instruction of a basic block. It rides with the - /// instruction, so a pass that inserts around it does not disturb it, and - /// anything inserted is correctly NOT a boundary. - pub block_start: bool, pub dsts: Vec, pub op: OpCode, pub imm: ImmOperand, @@ -23,231 +17,181 @@ pub struct RegInstr { } impl RegInstr { - pub fn new(op: OpCode, imm: ImmOperand, dsts: Vec, args: Vec) -> Self { + fn nop() -> Self { Self { - block_start: false, - dsts, - op, - imm, - args, + dsts: vec![], + op: OpCode::Nop, + imm: ImmOperand::None, + args: vec![], } } - - /// Carry a boundary onto a replacement. A rewrite may turn one instruction - /// into several, but it never moves where a block begins, so the flag goes - /// on the first one out. - pub fn starting(mut self, block_start: bool) -> Self { - self.block_start = block_start; - self - } } -impl Debug for RegInstr { +impl std::fmt::Debug for RegInstr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{} {:?} = {:?} {:?} {:?}", - if self.block_start { "|" } else { " " }, - self.dsts, - self.op, - self.imm, - self.args + "{:?} = {:?} {:?} {:?}", + self.dsts, self.op, self.imm, self.args ) } } #[derive(Debug)] pub struct RegFn { - pub name: String, + pub name: AtomRef, pub arg_count: usize, - pub captures: Vec, + pub val_count: usize, pub cpool: Vec, + pub captures: Vec, pub code: Vec, - /// The basic blocks, in listing order. No pass adds or removes a jump, so - /// these outlive every rewrite. Where a block STARTS is not here: that is - /// an instruction index and it moves, so `RegInstr::block_start` holds it. - pub blocks: Vec, - next_val: usize, + pub blocks: Vec, } impl RegFn { fn from_flat(flat_fn: FlatFn) -> Self { - let pcs = flat_fn.code.iter().map(|i| i.pc).collect::>(); - let (starts, mut blocks) = find_basic_blocks(&flat_fn.code, &pcs); + let blocks = find_basic_blocks(&flat_fn.code); + let mut out = vec![RegInstr::nop(); flat_fn.code.len()]; let mut next_val = flat_fn.var_count; // Reserve values for locals - // One instruction in, one out, so a block's output lands exactly where - // its input was and blocks can be walked in any order. A block nothing - // reaches keeps its nops, which is what unreachable code should leave. - let mut output = - vec![RegInstr::new(OpCode::Nop, ImmOperand::None, vec![], vec![]); flat_fn.code.len()]; - - // A worklist, because listing order does not reach every block: a - // for-of puts its body BEFORE the test, so the body is entered only by - // the back edge, from a block further down. A block joins the list the - // once, when the first path arrives with a stack for it. The function - // itself is entered at block 0, holding nothing. - let mut entry_stack = vec![None; blocks.len()]; + let mut entry_stacks: Vec>> = vec![None; blocks.len()]; let mut worklist = vec![]; - entry_stack[0] = Some(vec![]); + entry_stacks[0] = Some(vec![]); worklist.push(0); - while let Some(b) = worklist.pop() { - // A block starts with the stack it is ENTERED with, not with - // whatever the block above it in the listing left behind. This one - // line is what stops the walk running off a `return` into a - // jump-only path, or through a `goto` into the arm it jumps over. - let mut stack = entry_stack[b].clone().unwrap_or_default(); + while let Some(block_idx) = worklist.pop() { + let mut stack: Vec = entry_stacks[block_idx].clone().unwrap_or_default(); - let mut fresh_val = || { - let v = next_val; - next_val += 1; - v - }; + walk_block( + &blocks[block_idx], + &flat_fn.code, + &mut stack, + &mut next_val, + &mut out, + ); - let end = starts.get(b + 1).copied().unwrap_or(flat_fn.code.len()); - - for at in starts[b]..end { - let instr = &flat_fn.code[at]; - - let mut reg_instr = RegInstr { - block_start: false, - dsts: vec![], - op: instr.op, - imm: instr.imm_operands, - args: vec![], - }; - - let extra_pop = match instr.op { - OpCode::Call0 => 0, - OpCode::Call1 => 1, - OpCode::Call2 => 2, - OpCode::Call3 => 3, - _ => match instr.imm_operands { - ImmOperand::Npop(npop) => npop as usize, - ImmOperand::NpopU16 { npop, .. } => npop as usize, - _ => 0, - }, - }; - - let info = instr.op.info(); - let n_pop = info.n_pop as usize + extra_pop; - let n_push = info.n_push as usize; - - for _ in 0..n_pop { - let v = stack.pop().unwrap(); - reg_instr.args.push(v); - } - - // Locals are NOT renamed: local k is value k for the whole function, - // the id reserved above. A load pushes it, a store defines it again, - // and neither makes a new value -- so a local keeps one identity - // however many times it is read and written. - let local = match instr.op { - OpCode::GetLoc0 | OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0), - OpCode::GetLoc1 | OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1), - OpCode::GetLoc2 | OpCode::PutLoc2 | OpCode::SetLoc2 => Some(2), - OpCode::GetLoc3 | OpCode::PutLoc3 | OpCode::SetLoc3 => Some(3), - - OpCode::GetLoc - | OpCode::GetLoc8 - | OpCode::GetLocCheck - | OpCode::PutLoc - | OpCode::PutLoc8 - | OpCode::PutLocCheck - | OpCode::PutLocCheckInit - | OpCode::SetLoc - | OpCode::SetLoc8 - | OpCode::SetLocUninitialized => match reg_instr.imm { - ImmOperand::Loc(k) => Some(k as usize), - _ => None, - }, - - _ => None, - }; - - match local { - Some(k) => { - reg_instr.dsts.push(k); - - if n_push == 1 { - stack.push(k); - } - } - - // The one op that reads two locals at once - None if instr.op == OpCode::GetLoc0Loc1 => { - for k in [0, 1] { - stack.push(k); - reg_instr.dsts.push(k); - } - } - - None => { - for _ in 0..n_push { - let v = fresh_val(); - stack.push(v); - reg_instr.dsts.push(v); - } - } - } - - output[at] = reg_instr; - } - - // Every edge out of here arrives at its target with this stack. - for i in 0..blocks[b].succ.len() { - let to = blocks[b].succ[i]; - - // A later path is reconciled against the names the first one - // left, and that pairing is the phi. - if let Some(slots) = &entry_stack[to] { - blocks[to] - .phi - .extend(slots.iter().copied().zip(stack.iter().copied())); - } - // The first path names the block's slots, and is the one that - // puts it on the worklist. - else { - entry_stack[to] = Some(stack.clone()); - worklist.push(to); + for &succ_idx in &blocks[block_idx].succ { + if entry_stacks[succ_idx].is_none() { + entry_stacks[succ_idx] = Some(stack.clone()); + worklist.push(succ_idx); } } } - // Every block begins where its first instruction sits, walked or not. - for &start in &starts { - output[start].block_start = true; - } - Self { name: flat_fn.name, arg_count: flat_fn.arg_count, - captures: flat_fn.captures, + val_count: next_val, cpool: flat_fn.cpool, - code: output, + captures: flat_fn.captures, + code: out, blocks, - next_val, } } - - pub fn fresh_val(&mut self) -> usize { - let v = self.next_val; - self.next_val += 1; - v - } - - pub fn num_vals(&self) -> usize { - self.next_val - } } -/// Cut the code into blocks. A block begins at the first instruction, at every -/// jump target, and after anything a block cannot continue past. -/// -/// Two things come back: where each block starts, which is only good while the +fn walk_block( + block: &BasicBlock, + code: &[Instr], + stack: &mut Vec, + next_val: &mut usize, + out: &mut [RegInstr], +) { + let mut fresh_val = || { + let v = *next_val; + *next_val += 1; + v + }; + + for instr_idx in block.start..block.end { + let instr = &code[instr_idx]; + + let mut out_instr = RegInstr { + dsts: vec![], + op: instr.op, + imm: instr.imm_operands, + args: vec![], + }; + + // Some instrs have extra pops not reflected in info.n_pop + let extra_pop = match instr.op { + OpCode::Call0 => 0, + OpCode::Call1 => 1, + OpCode::Call2 => 2, + OpCode::Call3 => 3, + _ => match instr.imm_operands { + ImmOperand::Npop(npop) => npop as usize, + ImmOperand::NpopU16 { npop, .. } => npop as usize, + _ => 0, + }, + }; + + let info = instr.op.info(); + let n_pop = info.n_pop as usize + extra_pop; + let n_push = info.n_push as usize; + + // Handle this instr's pops + for _ in 0..n_pop { + let v = stack.pop().unwrap(); + out_instr.args.push(v); + } + + // Locals are not renamed, needs special handling + let local_access = match instr.op { + OpCode::GetLoc0 | OpCode::PutLoc0 | OpCode::SetLoc0 => Some(0), + OpCode::GetLoc1 | OpCode::PutLoc1 | OpCode::SetLoc1 => Some(1), + OpCode::GetLoc2 | OpCode::PutLoc2 | OpCode::SetLoc2 => Some(2), + OpCode::GetLoc3 | OpCode::PutLoc3 | OpCode::SetLoc3 => Some(3), + + OpCode::GetLoc + | OpCode::GetLoc8 + | OpCode::GetLocCheck + | OpCode::PutLoc + | OpCode::PutLoc8 + | OpCode::PutLocCheck + | OpCode::PutLocCheckInit + | OpCode::SetLoc + | OpCode::SetLoc8 + | OpCode::SetLocUninitialized => match instr.imm_operands { + ImmOperand::Loc(k) => Some(k as usize), + _ => None, + }, + + _ => None, + }; + + // Handle this instr's pushes, handling local accesses separately + match local_access { + Some(loc) => { + out_instr.dsts.push(loc); + + if n_push == 1 { + stack.push(loc); + } + } + + // The one op that reads two locals at once + None if instr.op == OpCode::GetLoc0Loc1 => { + for k in [0, 1] { + stack.push(k); + out_instr.dsts.push(k); + } + } + + None => { + for _ in 0..n_push { + let v = fresh_val(); + stack.push(v); + out_instr.dsts.push(v); + } + } + } + + out[instr_idx] = out_instr; + } +} pub fn regify(flat_fns: Vec) -> Vec { flat_fns.into_iter().map(RegFn::from_flat).collect() diff --git a/docs/class_form.txt b/docs/class_form.txt new file mode 100644 index 0000000..453976d --- /dev/null +++ b/docs/class_form.txt @@ -0,0 +1,139 @@ +=== after regify === +[ + RegFn { + name: "test", + arg_count: 0, + var_count: 4, + value_names: [ + file_atom[2], + atom(""), + file_atom[2], + file_atom[3], + ], + captures: [], + cpool: [ + FnById( + 2, + ), + FnById( + 3, + ), + FnById( + 4, + ), + ], + code: [ + [] = SetLocUninitialized Loc(3) [], + [] = SetLocUninitialized Loc(2) [], + [] = SetLocUninitialized Loc(0) [], + [4] = Undefined None [], + [] = SetLocUninitialized Loc(1) [], + + // This is the pattern to be replaced by prototype form + [5] = PushConst8 Const(1) [], + [6, 7] = DefineClass AtomU8 { atom: file_atom[2], arg: 0 } [5, 4], + [8] = Fclosure8 Const(0) [], + [9] = DefineMethod AtomU8 { atom: file_atom[5], arg: 0 } [8, 7], + [10] = Fclosure8 Const(2) [], + [11, 12] = SetHomeObject None [10, 9], + [] = PutLoc1 None [12], + [] = Drop None [11], + + [13] = SetLoc0 None [6], + [] = CloseLoc Loc(1) [], + [] = PutLoc2 None [13], + [14] = GetLocCheck Loc(2) [], + [15, 16] = Dup None [14], + [17] = CallConstructor Npop(0) [16, 15], + [] = PutLoc3 None [17], + [] = ReturnUndef None [], + ], + next_val: 18, + }, + RegFn { + name: "", // method, unchanged + arg_count: 0, + var_count: 0, + value_names: [], + captures: [], + cpool: [], + code: [ + [] = ReturnUndef None [], + ], + next_val: 0, + }, + RegFn { + name: "", // constructor, needs to be renamed to "Counter" + arg_count: 0, + var_count: 1, + value_names: [ + atom("this"), + ], + captures: [ + Capture { + name: "", + atom: atom(""), + from: ParentLocal( + 1, + ), + }, + ], + cpool: [], + code: [ + [1] = PushThis None [], + [] = PutLoc0 None [1], + [] = CheckCtor None [], + [2] = GetVarRefCheck VarRef(0) [], + [3, 4] = Dup None [2], + [] = IfFalse8 Label { target: 14 } [4], + [5] = GetLoc0 None [], + [6, 7] = Swap None [5, 3], + [8] = CallMethod Npop(0) [7, 6], + [] = ReturnUndef None [], + ], + next_val: 9, + }, + RegFn { + name: "", // instance fields initializer, needs thinking + arg_count: 0, + var_count: 2, + value_names: [ + atom("this"), + atom(""), + ], + captures: [], + cpool: [], + code: [ + [2] = SpecialObject U8(4) [], + [] = PutLoc1 None [2], + [3] = PushThis None [], + [] = PutLoc0 None [3], + [] = Goto8 Label { target: 7 } [], + [4] = GetLoc0 None [], + [5] = Push1 None [], + [6] = DefineField Atom(file_atom[4]) [5, 4], + [] = ReturnUndef None [], + ], + next_val: 7, + }, +] +[ + String( + "../test.js", + ), + String( + "test", + ), + String( + "Counter", + ), + String( + "c", + ), + String( + "x", + ), + String( + "m1", + ), +] diff --git a/docs/proto_form.txt b/docs/proto_form.txt new file mode 100644 index 0000000..729d8ab --- /dev/null +++ b/docs/proto_form.txt @@ -0,0 +1,87 @@ +=== after regify === +[ + RegFn { + name: "test", + arg_count: 0, + var_count: 2, + value_names: [ + file_atom[2], + file_atom[3], + ], + captures: [], + cpool: [ + FnById( + 2, + ), + FnById( + 3, + ), + ], + code: [ + [2] = Fclosure8 Const(0) [], + [] = PutLoc0 None [2], + [] = SetLocUninitialized Loc(1) [], + [3] = GetLoc0 None [], + [4] = GetField Atom(atom("prototype")) [3], + [5] = Fclosure8 Const(1) [], + [] = PutField Atom(file_atom[5]) [5, 4], + [6] = GetLoc0 None [], + [7, 8] = Dup None [6], + [9] = CallConstructor Npop(0) [8, 7], + [] = PutLoc1 None [9], + [] = ReturnUndef None [], + ], + next_val: 10, + }, + RegFn { + name: "Counter", + arg_count: 0, + var_count: 1, + value_names: [ + atom("this"), + ], + captures: [], + cpool: [], + code: [ + [1] = PushThis None [], + [] = PutLoc0 None [1], + [2] = GetLoc0 None [], + [3] = Push1 None [], + [] = PutField Atom(file_atom[4]) [3, 2], + [] = ReturnUndef None [], + ], + next_val: 4, + }, + RegFn { + name: "", + arg_count: 0, + var_count: 0, + value_names: [], + captures: [], + cpool: [], + code: [ + [] = ReturnUndef None [], + ], + next_val: 0, + }, +] +[ + String( + "../test.js", + ), + String( + "test", + ), + String( + "Counter", + ), + String( + "c", + ), + String( + "x", + ), + String( + "m1", + ), +]