//! Link: what each name in each module refers to, and the program's entry fn //! //! Every binding at the top of a module (`let`, `const`, `function`, `class`, ...) is a //! global: there is one of each for the whole program. A module's body reaches its own //! bindings and its imports as captured vars, which QuickJS calls module decls and module //! imports. Link gives each decl a global, and follows each import to the global of the //! binding that it names (see `Link::resolve`). transform/modules.rs then rewrites the code //! //! A name that no module declares, such as `String` or `console`, is a global name: it //! refers to the export of that name of the prelude, `jsrt:prelude`, as names in JS refer //! to the global object. A module can declare a binding of the same name, which hides it //! //! The natives are the exports of `jsrt:native`, which QuickJS sees as a C module (see //! `libqjs::compile`) //! //! # The entry fn //! //! ```js //! function () { //! // Phase 1 (`this` is true): each body only makes its top-level function declarations. //! // Phase 2: each body runs. Every module's phase 1 comes first, so that modules that //! // import each other see each other's functions //! let phase = true; //! while (true) { //! for (const body of bodies) body.call(phase); //! if (!phase) break; //! phase = false; //! } //! __drain_microtasks(); //! } //! ``` //! //! Both phases call each body 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 //! object at the top of a module //! //! The bodies run in ES's order: each module after the modules that it imports, from the //! prelude, then each module that the command line names. A body that awaits at its top runs //! until its first await, so only the last body may await (see transform/async.rs) use std::collections::HashMap; use crate::{ flatten::{ENTRY_FN, FlatFn, FlatModule}, libqjs::parse::{ Atom, AtomRef, ClosureKind, ClosureVar, ExportInfo, FuncKind, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode, }, native::NATIVES, }; /// The module whose exports are every module's global names pub const PRELUDE: &str = "jsrt:prelude"; /// The module of the natives pub const NATIVE: &str = "jsrt:native"; /// What a name refers to #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Target { Global(usize), // a binding at the top of a module Native(usize), // a native, by its index in `native::NATIVES` } pub struct Link { pub globals: usize, // how many there are bodies: HashMap)>, // for the fn of each module's body: the module's name, and what each of the body's captured vars refers to exports: HashMap>, // for each module, by name: each export, by name } impl Link { /// The name of the module whose body is `f`, if it is a body, and what each captured var /// of the body refers to pub fn body(&self, f: usize) -> Option<(&str, &[Target])> { self.bodies .get(&f) .map(|(name, vars)| (&name[..], &vars[..])) } /// What the global name `name` refers to: the prelude's export of that name pub fn global(&self, name: &str) -> Option { self.export(PRELUDE, name) } /// The export `name` of the module `module` pub fn export(&self, module: &str, name: &str) -> Option { self.exports.get(module)?.get(name).copied() } /// The global of an export that the compiler needs from the stdlib pub fn stdlib(&self, module: &str, name: &str) -> usize { match self.export(module, name) { Some(Target::Global(global)) => global, _ => panic!("the compiler needs `{name}` from `{module}`"), } } } /// Link the modules, and write the entry fn. `roots` are the modules that the command line /// names, in order pub fn link( modules: &[FlatModule], roots: &[String], fns: &mut [FlatFn], captures: &[Vec], atoms: &[Atom], ) -> Link { let linker = Linker::new(modules, captures, atoms); let link = Link { globals: linker.globals, bodies: linker.bodies(), exports: linker.all_exports(), }; let order = linker.order(roots); check_top_level_await(&order, modules); let drain = link.stdlib("jsrt:loop", "__drain_microtasks"); fns[ENTRY_FN] = entry(&order, modules, drain); link } /// A body that awaits returns at its first await, so the bodies after it would run too early. /// Only the last one may await, until the entry waits for each body's promise fn check_top_level_await(order: &[usize], modules: &[FlatModule]) { for &m in &order[..order.len() - 1] { if modules[m].has_top_level_await { let name = &modules[m].name; unimplemented!("{name}: top-level `await` in a module that is not the last to run"); } } } /// The natives, as the exports of `jsrt:native` fn natives() -> HashMap { let natives = NATIVES.iter().enumerate(); natives .map(|(idx, native)| (native.name.to_string(), Target::Native(idx))) .collect() } struct Linker<'a> { modules: &'a [FlatModule], captures: &'a [Vec], atoms: &'a [Atom], index: HashMap, // each module, by name decls: Vec>, // for each module: the global of each of its body's decls, by captured var globals: usize, } impl<'a> Linker<'a> { /// The linker, with a global for each decl of each module's body fn new(modules: &'a [FlatModule], captures: &'a [Vec], atoms: &'a [Atom]) -> Self { let index = modules.iter().enumerate(); let mut linker = Linker { modules, captures, atoms, index: index.map(|(m, module)| (module.name.clone(), m)).collect(), decls: vec![], globals: 0, }; linker.add_globals(); linker } /// For each module's body: the module's name, and what each captured var refers to fn bodies(&self) -> HashMap)> { let bodies = self.modules.iter().enumerate(); bodies .map(|(m, module)| (module.body, (module.name.clone(), self.body_vars(m)))) .collect() } /// Every module's exports, by the module's name, with the natives' module fn all_exports(&self) -> HashMap> { let modules = self.modules.iter().enumerate(); let exports = modules.map(|(m, module)| (module.name.clone(), self.exports(m, &mut vec![]))); exports.chain([(NATIVE.to_string(), natives())]).collect() } } impl Linker<'_> { /// A global for each decl of each module's body. QuickJS makes a namespace import a decl /// too, whose object its own linker makes at run time, so that is rejected here fn add_globals(&mut self) { for module in self.modules { let atoms = self.atoms; let namespace = module .imports .iter() .find(|import| import.name.text(atoms) == "*"); if namespace.is_some() { unimplemented!("{}: `import * as` needs namespace objects", module.name); } let vars = self.captures[module.body].iter().enumerate(); let decls = vars.filter(|(_, var)| var.flags.closure_kind == ClosureKind::ModuleDecl); let mut globals = HashMap::new(); for (i, _) in decls { globals.insert(i, self.globals); self.globals += 1; } self.decls.push(globals); } } /// What each captured var of module `m`'s body refers to fn body_vars(&self, m: usize) -> Vec { (0..self.captures[self.modules[m].body].len()) .map(|i| self.var(m, i, &mut vec![])) .collect() } /// What captured var `i` of module `m`'s body refers to: its global if it is a decl, else /// the export that it imports. `seen` guards against imports that go round in a circle fn var(&self, m: usize, i: usize, seen: &mut Vec<(usize, usize)>) -> Target { let module = &self.modules[m]; if let Some(&global) = self.decls[m].get(&i) { return Target::Global(global); } assert!( !seen.contains(&(m, i)), "{}: an import refers back to itself", module.name ); seen.push((m, i)); let import = module.imports.iter().find(|import| import.var_idx == i); let import = import.unwrap_or_else(|| panic!("{}: a module var with no import", module.name)); let name = import.name.text(self.atoms); let from = self.request(m, import.request); match self.resolve(&from, &name, seen) { Some(target) => target, None => panic!("{}: `{from}` has no export `{name}`", module.name), } } /// ES's ResolveExport: what export `name` of module `module` refers to fn resolve(&self, module: &str, name: &str, seen: &mut Vec<(usize, usize)>) -> Option { if module == NATIVE { return natives().get(name).copied(); } let Some(&m) = self.index.get(module) else { panic!("no module `{module}`"); }; for export in &self.modules[m].exports { match export { ExportInfo::Local { var_idx, name: n } if n.text(self.atoms) == name => { return Some(self.var(m, *var_idx, seen)); } ExportInfo::Indirect { request, local, name: n, } if n.text(self.atoms) == name => { return self.resolve(&self.request(m, *request), &local.text(self.atoms), seen); } _ => {} } } // `export *` passes on every export but `default` if name == "default" { return None; } let mut stars = self.modules[m].star_exports.iter(); stars.find_map(|&request| self.resolve(&self.request(m, request), name, seen)) } /// Every export of module `m`, by name. `seen` guards against `export *` in a circle fn exports(&self, m: usize, seen: &mut Vec) -> HashMap { let module = &self.modules[m]; let mut exports = HashMap::new(); if seen.contains(&m) { return exports; } seen.push(m); for &request in &module.star_exports { let from = self.request(m, request); let passed = match self.index.get(&from) { Some(&other) => self.exports(other, seen), None if from == NATIVE => natives(), None => panic!("no module `{from}`"), }; for (name, target) in passed { if name != "default" { exports.insert(name, target); } } } for export in &module.exports { let (ExportInfo::Local { name, .. } | ExportInfo::Indirect { name, .. }) = export; let name = name.text(self.atoms); let target = self.resolve(&module.name, &name, &mut vec![]); exports.insert(name, target.unwrap()); } exports } /// The name of request `r` of module `m`. QuickJS made the same name when it loaded it /// (see `normalize`) fn request(&self, m: usize, r: usize) -> String { let module = &self.modules[m]; normalize(&module.name, &module.requests[r]) } /// The order that the bodies run in: each module after the modules that it imports, from /// the prelude, then each root fn order(&self, roots: &[String]) -> Vec { let mut order = vec![]; for root in [PRELUDE.to_string()].iter().chain(roots) { self.visit(self.index[root], &mut order); } order } fn visit(&self, m: usize, order: &mut Vec) { if order.contains(&m) { return; } order.push(m); let at = order.len() - 1; for r in 0..self.modules[m].requests.len() { if let Some(&other) = self.index.get(&self.request(m, r)) { self.visit(other, order); } } // After its imports. It was marked first, so that a circle of imports ends let m = order.remove(at); order.push(m); } } /// The name of the module that `specifier` names, in the module `base`. This is QuickJS's /// default rule, which QuickJS used to load it: a name that does not start with `.` is as it /// is. Else it is relative to the folder of `base`, and its leading `./` and `../` are taken /// out fn normalize(base: &str, specifier: &str) -> String { if !specifier.starts_with('.') { return specifier.to_string(); } let mut folder = match base.rfind('/') { Some(slash) => base[..slash].to_string(), None => String::new(), }; let mut rest = specifier; loop { if let Some(after) = rest.strip_prefix("./") { rest = after; } else if let Some(after) = rest.strip_prefix("../") { let last = folder.rfind('/').map_or(0, |slash| slash + 1); if folder.is_empty() || matches!(&folder[last..], "." | "..") { break; } folder.truncate(last.saturating_sub(1)); rest = after; } else { break; } } match folder.is_empty() { true => rest.to_string(), false => format!("{folder}/{rest}"), } } /// The entry fn (see the module doc). Local `k` holds the closure of the `k`th body to run, /// and the last local is the phase fn entry(order: &[usize], modules: &[FlatModule], drain: usize) -> FlatFn { use OpCode::*; let n = order.len(); let phase = Imm::Loc(n as u16); let body = |k: usize| Imm::Loc(k as u16); // The loop starts after the closures and the phase, and ends after a call of each body // and the phase's test let head = 2 * n + 2; let end = head + 4 * n + 5; let make = (0..n).flat_map(|k| [(Fclosure, Imm::Const(k as u32)), (PutLoc, body(k))]); let call = (0..n).flat_map(|k| { [ (GetLoc, phase), (GetLoc, body(k)), (CallMethod, Imm::Npop(0)), (Drop, Imm::None), ] }); let code: Vec<(OpCode, Imm)> = make .chain([(PushTrue, Imm::None), (PutLoc, phase)]) .chain(call) .chain([ (GetLoc, phase), (IfFalse, Imm::Label { target: end }), (PushFalse, Imm::None), (PutLoc, phase), (Goto, Imm::Label { target: head }), (GetVar, Imm::Global(drain as u32)), (Call0, Imm::None), (Drop, Imm::None), (ReturnUndef, Imm::None), ]) .collect(); let code = code.into_iter().enumerate(); let code = code.map(|(pc, (op, imm))| Instr { pc, op, imm }).collect(); let bodies = order .iter() .map(|&m| JsValue::FnByIdx(modules[m].body)) .collect(); FlatFn { name: AtomRef::Predefined(0), kind: FuncKind::Normal, arg_count: 0, local_count: n + 1, cpool: bodies, code, } }