Initial ES modules support

This commit is contained in:
2026-10-26 12:39:58 +00:00
parent f7c5b2c80f
commit 1e12e7be2e
35 changed files with 1266 additions and 505 deletions
+61 -107
View File
@@ -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![]);
}