Initial ES modules support
This commit is contained in:
+61
-107
@@ -1,26 +1,21 @@
|
||||
//! QuickJS bytecode represents the program as a hierarchy. Each function has a set of child functions
|
||||
//! that reference captured variables in their parent
|
||||
//! QuickJS bytecode represents each module as a hierarchy. Each function has a set of child
|
||||
//! functions that reference captured variables in their parent
|
||||
//!
|
||||
//! This module:
|
||||
//! - flattens the hierarchy
|
||||
//! - flattens each module's hierarchy into one list for the program
|
||||
//! - assigns an integer idx to each function
|
||||
//! - converts child ownership to a reference by-idx
|
||||
//! - extracts closure captures into a separate vec
|
||||
//!
|
||||
//! Also adds a synthetic entry point function that calls the top level fn generated by QuickJS,
|
||||
//! and makes the module's bindings its locals
|
||||
|
||||
use std::collections::HashMap;
|
||||
//! Fn 0 is the program's entry, which link.rs writes: it runs every module's body
|
||||
|
||||
use crate::libqjs::parse::{
|
||||
Atom, AtomRef, ClosureKind, ClosureVar, FuncKind, Function, ImmOperand, Instr, JsValue,
|
||||
QuickJSBinary, opcodes::OpCode,
|
||||
Atom, AtomRef, ClosureVar, ExportInfo, FuncKind, Function, ImportInfo, Instr, JsValue,
|
||||
QuickJSBinary,
|
||||
};
|
||||
|
||||
/// The module's bindings, which are locals of the entry fn, by name. The compiler reads the
|
||||
/// stdlib's by name where it needs one: the classes whose prototypes the primitives use, and
|
||||
/// what the host calls when an IO op completes
|
||||
pub type ModuleBindings = HashMap<String, usize>;
|
||||
/// The entry fn (see link.rs)
|
||||
pub const ENTRY_FN: usize = 0;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FlatFn {
|
||||
@@ -32,50 +27,66 @@ pub struct FlatFn {
|
||||
pub code: Vec<Instr>,
|
||||
}
|
||||
|
||||
/// Flattens the binary
|
||||
///
|
||||
/// Returns:
|
||||
/// - flat_fns[i] = the i'th flattened function
|
||||
/// - captures[i] = the i'th fn's captures from its parent
|
||||
/// - the module's bindings, as locals of the entry fn
|
||||
pub fn flatten(
|
||||
bin: QuickJSBinary,
|
||||
) -> (Vec<FlatFn>, Vec<Vec<ClosureVar>>, Vec<Atom>, ModuleBindings) {
|
||||
let mut fns = vec![];
|
||||
let mut captures = vec![];
|
||||
|
||||
walk(bin.root_mod.root_fn, &mut fns, &mut captures);
|
||||
add_entry_point(&mut fns, &mut captures);
|
||||
let bindings = bind_module(&mut fns, &mut captures, &bin.atoms);
|
||||
|
||||
(fns, captures, bin.atoms, bindings)
|
||||
/// A module, as QuickJS gives it, with its body as a flat fn
|
||||
#[derive(Debug)]
|
||||
pub struct FlatModule {
|
||||
pub name: String, // the name that it was loaded by (see `libqjs::compile`)
|
||||
pub requests: Vec<String>, // the modules that it imports from, as the source writes them
|
||||
pub exports: Vec<ExportInfo>,
|
||||
pub star_exports: Vec<usize>, // `export * from`: each an index into `requests`
|
||||
pub imports: Vec<ImportInfo>,
|
||||
pub has_top_level_await: bool,
|
||||
pub body: usize, // the fn of its body. Its captures are the module's bindings
|
||||
}
|
||||
|
||||
/// Make each binding of the module a local of the entry fn, after its own two. The module fn
|
||||
/// (`<eval>`, the first fn) captures them, as any fn captures its parent's locals, so they are
|
||||
/// cells (see transform/closure.rs)
|
||||
fn bind_module(
|
||||
fns: &mut [FlatFn],
|
||||
captures: &mut [Vec<ClosureVar>],
|
||||
atoms: &[Atom],
|
||||
) -> ModuleBindings {
|
||||
let entry = fns.len() - 1;
|
||||
let mut bindings = ModuleBindings::new();
|
||||
/// Flattens every module of the binary into one list of fns. Gives the fns, each fn's captures
|
||||
/// from its parent, the atoms, and the modules
|
||||
pub fn flatten(
|
||||
bin: QuickJSBinary,
|
||||
) -> (
|
||||
Vec<FlatFn>,
|
||||
Vec<Vec<ClosureVar>>,
|
||||
Vec<Atom>,
|
||||
Vec<FlatModule>,
|
||||
) {
|
||||
let text = |atom: AtomRef| atom.text(&bin.atoms);
|
||||
let mut fns = vec![entry_placeholder()];
|
||||
let mut captures = vec![vec![]];
|
||||
let mut modules = vec![];
|
||||
|
||||
for capture in &mut captures[0] {
|
||||
if capture.flags.closure_kind != ClosureKind::ModuleDecl {
|
||||
continue;
|
||||
for module in bin.modules {
|
||||
let has_top_level_await = module.has_top_level_await;
|
||||
let body = walk(module.root_fn, &mut fns, &mut captures);
|
||||
|
||||
// QuickJS makes every module's body async. It only suspends if it awaits at its top
|
||||
if !has_top_level_await {
|
||||
fns[body].kind = FuncKind::Normal;
|
||||
}
|
||||
|
||||
let local = fns[entry].local_count;
|
||||
fns[entry].local_count += 1;
|
||||
|
||||
capture.flags.closure_kind = ClosureKind::Local;
|
||||
capture.var_idx = local;
|
||||
bindings.insert(capture.name.text(atoms), local);
|
||||
modules.push(FlatModule {
|
||||
name: text(module.name),
|
||||
requests: module.requests.iter().map(|&r| text(r)).collect(),
|
||||
exports: module.exports,
|
||||
star_exports: module.star_exports,
|
||||
imports: module.imports,
|
||||
has_top_level_await,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
bindings
|
||||
(fns, captures, bin.atoms, modules)
|
||||
}
|
||||
|
||||
/// Fn 0, before link.rs writes it
|
||||
fn entry_placeholder() -> FlatFn {
|
||||
FlatFn {
|
||||
name: AtomRef::Predefined(0),
|
||||
kind: FuncKind::Normal,
|
||||
arg_count: 0,
|
||||
local_count: 0,
|
||||
cpool: vec![],
|
||||
code: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn walk(func: Function, flat_fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>) -> usize {
|
||||
@@ -105,60 +116,3 @@ 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>() {
|
||||
/// ...
|
||||
/// }
|
||||
///
|
||||
/// // Link phase (`this = true`), then run phase (`this = false`)
|
||||
/// let phase = true;
|
||||
/// while (true) {
|
||||
/// <eval>.call(phase);
|
||||
/// if (!phase) break;
|
||||
/// phase = false;
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Both phases call `<eval>` from one call instr. Infer names the objects that a walk makes
|
||||
/// by its call site, so with two call instrs, each phase would make its own copy of every
|
||||
/// module-level object, and each module binding would hold one of two copies
|
||||
///
|
||||
/// 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::PutLoc, ImmOperand::Loc(1)),
|
||||
// Loop head: one call for both phases
|
||||
instr(4, OpCode::GetLoc, ImmOperand::Loc(1)),
|
||||
instr(5, OpCode::GetLoc, ImmOperand::Loc(0)),
|
||||
instr(6, OpCode::CallMethod, ImmOperand::Npop(0)),
|
||||
instr(7, OpCode::Drop, ImmOperand::None),
|
||||
instr(8, OpCode::GetLoc, ImmOperand::Loc(1)),
|
||||
instr(9, OpCode::IfFalse8, ImmOperand::Label { target: 13 }),
|
||||
instr(10, OpCode::PushFalse, ImmOperand::None),
|
||||
instr(11, OpCode::PutLoc, ImmOperand::Loc(1)),
|
||||
instr(12, OpCode::Goto8, ImmOperand::Label { target: 4 }),
|
||||
instr(13, OpCode::ReturnUndef, ImmOperand::None),
|
||||
];
|
||||
|
||||
fns.push(FlatFn {
|
||||
name: AtomRef::Predefined(0),
|
||||
kind: FuncKind::Normal,
|
||||
arg_count: 0,
|
||||
local_count: 2, // <eval>, and the phase
|
||||
cpool: vec![JsValue::FnByIdx(0)], // First fn is the top level <eval>
|
||||
code,
|
||||
});
|
||||
|
||||
captures.push(vec![]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user