Initial ES modules support
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
use super::classes::{CType, ClassKind, Member};
|
||||
use super::program::Program;
|
||||
use super::{js, objects};
|
||||
use crate::{infer::Type, libqjs::parse::AtomRef, regify::Operand};
|
||||
use crate::{flatten::ENTRY_FN, infer::Type, libqjs::parse::AtomRef, regify::Operand};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Function {
|
||||
@@ -65,7 +65,7 @@ impl Function {
|
||||
|
||||
/// Call the entry, with undefined args
|
||||
fn start_body(program: &Program) -> String {
|
||||
let arg_count = program.fns[program.fns.len() - 1].arg_count;
|
||||
let arg_count = program.fns[ENTRY_FN].arg_count;
|
||||
let args = vec![js::UNDEFINED.to_string(); arg_count];
|
||||
format!(" {};\n", host_call(program, program.entry, &args))
|
||||
}
|
||||
@@ -81,7 +81,7 @@ fn complete_body(program: &Program) -> String {
|
||||
/// The call of `__complete`, with the closure as its own env and `this` undefined. Infer
|
||||
/// made the same call (see `TypeCache::call_host_complete`)
|
||||
fn complete_call(program: &Program) -> Option<String> {
|
||||
let (callee, ctype) = program.binding("__complete")?;
|
||||
let (callee, ctype) = program.binding("jsrt:loop", "__complete")?;
|
||||
let CType::One(Member::Obj(class)) = ctype else {
|
||||
return None;
|
||||
};
|
||||
|
||||
+13
-6
@@ -120,10 +120,8 @@ impl JsFunction<'_> {
|
||||
self.program.slot_type(self.walk, slot)
|
||||
}
|
||||
|
||||
/// A C local for each local and reg, and a cell for each shared local and arg. A module
|
||||
/// binding, which is a local of the entry, also puts its cell in its C global
|
||||
/// A C local for each local and reg, and a cell for each shared local and arg
|
||||
fn declarations(&self) -> String {
|
||||
let entry = self.walk == self.program.entry;
|
||||
let mut c = String::new();
|
||||
|
||||
for slot in slots(self.f) {
|
||||
@@ -134,9 +132,6 @@ impl JsFunction<'_> {
|
||||
c += &match (slot, self.shared.contains(&slot)) {
|
||||
(Operand::Arg(_), false) => continue,
|
||||
(Operand::Arg(k), true) => format!("{ctype} *{name} = {cell}; *{name} = p{k};\n"),
|
||||
(Operand::Local(k), true) if entry && self.program.globals.contains_key(&k) => {
|
||||
format!("{ctype} *{name} = g{k} = {cell};\n")
|
||||
}
|
||||
(_, true) => format!("{ctype} *{name} = {cell};\n"),
|
||||
(_, false) => format!("{ctype} {name} = {{0}};\n"),
|
||||
};
|
||||
@@ -220,6 +215,10 @@ impl JsFunction<'_> {
|
||||
// Copies. A key stays as it is: a number is an index
|
||||
SetLoc | SetName | ToPropkey => self.set(instr.dsts[0], &get(0)),
|
||||
|
||||
// The bindings at the top of a module, which are C globals (see link.rs)
|
||||
GetVar => self.set(instr.dsts[0], &format!("g{}", global(instr))),
|
||||
PutVar => format!("SET(g{}, {});\n", global(instr), get(0)),
|
||||
|
||||
PostInc | PostDec => {
|
||||
let name = if instr.op == PostInc {
|
||||
"POST_INC"
|
||||
@@ -298,6 +297,14 @@ impl JsFunction<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The global that `get_var` or `put_var` reads or writes (see transform/modules.rs)
|
||||
fn global(instr: &RegInstr) -> u32 {
|
||||
match instr.imm {
|
||||
ImmOperand::Global(global) => global,
|
||||
imm => unreachable!("a global name that is not a global: {imm:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// An int, as C
|
||||
fn int(n: i64) -> String {
|
||||
match n == i32::MIN as i64 {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! in an order that C can compile:
|
||||
//!
|
||||
//! - **Types:** a struct for each class of objects (see `classes`).
|
||||
//! - **Statics:** each string literal, and a C global for each module binding.
|
||||
//! - **Statics:** each string literal, and a C global for each binding at the top of a module.
|
||||
//! - **Functions:** one for each walk of a JS function, one for each lookup, and the host's
|
||||
//! two entry points (see `functions`).
|
||||
//!
|
||||
@@ -19,7 +19,7 @@ mod js;
|
||||
mod objects;
|
||||
mod program;
|
||||
|
||||
use crate::{flatten::ModuleBindings, infer::TypeCache, libqjs::parse::Atom, regify::RegFn};
|
||||
use crate::{infer::TypeCache, libqjs::parse::Atom, link::Link, regify::RegFn};
|
||||
|
||||
use classes::ClassKind;
|
||||
use program::Program;
|
||||
@@ -27,8 +27,8 @@ use program::Program;
|
||||
const HEADER: &str = "// Generated by jsrt\n#define JSRT_IMPLEMENTATION\n#include \"jsrt.h\"\n\n";
|
||||
|
||||
/// The C of the program, which includes the runtime's `jsrt.h`
|
||||
pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom], bindings: &ModuleBindings) -> String {
|
||||
let program = Program::new(fns, cache, atoms, bindings);
|
||||
pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom], link: &Link) -> String {
|
||||
let program = Program::new(fns, cache, atoms, link);
|
||||
let classes = 0..program.classes.classes.len();
|
||||
let mut c = String::from(HEADER);
|
||||
|
||||
@@ -48,8 +48,8 @@ pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom], bindings: &ModuleB
|
||||
c += &string_definition(id, text);
|
||||
}
|
||||
|
||||
for (&local, &ctype) in &program.globals {
|
||||
c += &format!("static {} *g{local};\n", ctype.c_name());
|
||||
for (g, ctype) in program.globals.iter().enumerate() {
|
||||
c += &format!("static {} g{g};\n", ctype.c_name());
|
||||
}
|
||||
|
||||
// Functions. Each is declared first, because they call each other
|
||||
|
||||
@@ -51,10 +51,7 @@ impl JsFunction<'_> {
|
||||
/// `[a, b, ...]`. The operands are the elements, last first
|
||||
fn array_from(&self, idx: usize, instr: &RegInstr) -> String {
|
||||
let class = self.class_here(idx);
|
||||
let mut c = format!(
|
||||
"struct c{class} *a;\nARRAY_NEW(a, {});\n",
|
||||
instr.args.len()
|
||||
);
|
||||
let mut c = format!("struct c{class} *a;\nARRAY_NEW(a, {});\n", instr.args.len());
|
||||
|
||||
for (i, &elem) in instr.args.iter().rev().enumerate() {
|
||||
c += &format!("SET(a->items[{i}], {});\n", self.get(elem));
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
//! `Program::new` finds all of them before any C is written. Writing a function only reads
|
||||
//! the program, so the functions can be written in any order.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::classes::{CType, ClassField, ClassKind, Classes, Member};
|
||||
use super::functions::Function;
|
||||
use super::js::indent;
|
||||
use crate::{
|
||||
flatten::ModuleBindings,
|
||||
infer::{LENGTH_FIELD, Type, TypeCache, join, module_binding},
|
||||
flatten::ENTRY_FN,
|
||||
infer::{Addr, LENGTH_FIELD, Type, TypeCache, join},
|
||||
libqjs::parse::{Atom, AtomRef, JsValue, opcodes::OpCode},
|
||||
link::{Link, PRELUDE, Target},
|
||||
regify::{Operand, RegFn, RegInstr},
|
||||
transform::{PROTO_FIELD, PROTOTYPE_FIELD},
|
||||
};
|
||||
@@ -21,32 +22,27 @@ pub struct Program<'a> {
|
||||
pub fns: &'a [RegFn],
|
||||
pub cache: &'a TypeCache,
|
||||
pub atoms: &'a [Atom],
|
||||
bindings: &'a ModuleBindings,
|
||||
pub link: &'a Link,
|
||||
pub entry: usize, // the walk of the entry: the JS function that runs the program
|
||||
|
||||
// The output
|
||||
pub classes: Classes, // types: a struct for each class of objects
|
||||
pub strings: Vec<String>, // statics: the text of each string literal
|
||||
pub globals: BTreeMap<usize, CType>, // statics: the module bindings, which are locals of the entry, and their C types
|
||||
pub globals: Vec<CType>, // statics: the C type of each binding at the top of a module (see link.rs)
|
||||
pub functions: Vec<Function>,
|
||||
}
|
||||
|
||||
impl<'a> Program<'a> {
|
||||
pub fn new(
|
||||
fns: &'a [RegFn],
|
||||
cache: &'a TypeCache,
|
||||
atoms: &'a [Atom],
|
||||
bindings: &'a ModuleBindings,
|
||||
) -> Self {
|
||||
pub fn new(fns: &'a [RegFn], cache: &'a TypeCache, atoms: &'a [Atom], link: &'a Link) -> Self {
|
||||
let mut program = Program {
|
||||
fns,
|
||||
cache,
|
||||
atoms,
|
||||
bindings,
|
||||
entry: cache.find(fns.len() - 1, None, &[], fns),
|
||||
link,
|
||||
entry: cache.find(ENTRY_FN, None, &[], fns),
|
||||
classes: Classes::new(&cache.memory),
|
||||
strings: vec![],
|
||||
globals: BTreeMap::new(),
|
||||
globals: vec![],
|
||||
functions: vec![],
|
||||
};
|
||||
|
||||
@@ -142,12 +138,8 @@ impl<'a> Program<'a> {
|
||||
|
||||
/// Each module binding is a C global, so that any function can use the stdlib's
|
||||
fn add_globals(&mut self) {
|
||||
let entry = self.fns.len() - 1;
|
||||
|
||||
for &local in self.bindings.values() {
|
||||
let ty = self.cache.memory.get(module_binding(entry, local));
|
||||
self.globals.insert(local, self.classes.ctype(ty));
|
||||
}
|
||||
let types = (0..self.link.globals).map(|g| self.cache.memory.get(Addr::Global(g)));
|
||||
self.globals = types.map(|ty| self.classes.ctype(ty)).collect();
|
||||
}
|
||||
|
||||
/// The instrs of a JS function that can run in a walk
|
||||
@@ -200,10 +192,12 @@ impl Program<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of the stdlib's module binding `name`, as C, and its C type
|
||||
pub fn binding(&self, name: &str) -> Option<(String, CType)> {
|
||||
let local = *self.bindings.get(name)?;
|
||||
Some((format!("(*g{local})"), self.globals[&local]))
|
||||
/// The value of the export `name` of the stdlib's module `module`, as C, and its C type
|
||||
pub fn binding(&self, module: &str, name: &str) -> Option<(String, CType)> {
|
||||
match self.link.export(module, name)? {
|
||||
Target::Global(g) => Some((format!("g{g}"), self.globals[g])),
|
||||
Target::Native(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +337,7 @@ impl Program<'_> {
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let (stdlib_class, CType::One(Member::Obj(class))) = self.binding(name)? else {
|
||||
let (stdlib_class, CType::One(Member::Obj(class))) = self.binding(PRELUDE, name)? else {
|
||||
return None;
|
||||
};
|
||||
let proto = self.classes.field(class, PROTOTYPE_FIELD)?;
|
||||
|
||||
+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![]);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ pub struct ObjAddr {
|
||||
/// A location in the program, which memory holds a type for
|
||||
///
|
||||
/// Every location is named by its context and a place in its function, except the ones that
|
||||
/// only one walk can see, which are named by the walk. A type can hold an object's address,
|
||||
/// only one walk can see, which are named by the walk, and the bindings at the top of a
|
||||
/// module, of which there is one each for the whole program. A type can hold an object's address,
|
||||
/// and a walk's arg types can hold that type, so objects are never named by a walk: that
|
||||
/// could make new walks without end. Every part of an address comes from the program text or
|
||||
/// from a walk, so there are finitely many, and infer ends
|
||||
@@ -50,6 +51,7 @@ pub enum Addr {
|
||||
Shared { context: Context, slot: Operand }, // a binding that closures share (see transform/closure.rs). The closures are objects, so it is named like them
|
||||
Private { walk: usize, slot: Operand }, // a binding that only its walk sees
|
||||
Return(usize), // what a walk returns
|
||||
Global(usize), // a binding at the top of a module: one for the whole program (see link.rs)
|
||||
}
|
||||
|
||||
/// What each location holds, by its address. A location that nothing has reached holds
|
||||
@@ -64,7 +66,7 @@ pub struct Memory {
|
||||
}
|
||||
|
||||
/// The stdlib's classes, whose prototypes the primitives and arrays read: the address of each
|
||||
/// one's module binding (see `flatten::ModuleBindings`). A program without the stdlib has none
|
||||
/// one's binding, which the prelude exports (see link.rs)
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Builtins {
|
||||
pub string: Option<Addr>,
|
||||
|
||||
+17
-30
@@ -26,7 +26,8 @@ mod walk;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
flatten::ModuleBindings,
|
||||
flatten::ENTRY_FN,
|
||||
link::{Link, Target},
|
||||
regify::{Operand, RegFn},
|
||||
};
|
||||
|
||||
@@ -170,12 +171,15 @@ fn binding_addr(w: usize, context: Context, shared: &[Operand], slot: Operand) -
|
||||
|
||||
/// Walk the program from the entry, in rounds, until a round changes nothing
|
||||
///
|
||||
/// The host makes two kinds of call (see lib/rt/jsrt.h): the entry, with no args, which
|
||||
/// `flatten` appends last, and the stdlib's `__complete`, with an op's handle, when the op
|
||||
/// completes. Both come from the host, which has no call site
|
||||
pub fn infer_types(fns: &[RegFn], bindings: &ModuleBindings) -> TypeCache {
|
||||
let entry = fns.len() - 1;
|
||||
let binding = |name: &str| bindings.get(name).map(|&k| module_binding(entry, k));
|
||||
/// The host makes two kinds of call (see lib/rt/jsrt.h): the entry, with no args, and the
|
||||
/// stdlib's `__complete`, with an op's handle, when the op completes. Both come from the
|
||||
/// host, which has no call site
|
||||
pub fn infer_types(fns: &[RegFn], link: &Link) -> TypeCache {
|
||||
let global = |name: &str| match link.global(name) {
|
||||
Some(Target::Global(global)) => Some(Addr::Global(global)),
|
||||
_ => None,
|
||||
};
|
||||
let complete = Addr::Global(link.stdlib("jsrt:loop", "__complete"));
|
||||
|
||||
let mut cache = TypeCache {
|
||||
walks: vec![],
|
||||
@@ -185,37 +189,20 @@ pub fn infer_types(fns: &[RegFn], bindings: &ModuleBindings) -> TypeCache {
|
||||
};
|
||||
|
||||
cache.memory.builtins = Builtins {
|
||||
string: binding("String"),
|
||||
number: binding("Number"),
|
||||
boolean: binding("Boolean"),
|
||||
array: binding("Array"),
|
||||
string: global("String"),
|
||||
number: global("Number"),
|
||||
boolean: global("Boolean"),
|
||||
array: global("Array"),
|
||||
};
|
||||
|
||||
loop {
|
||||
let before = cache.progress();
|
||||
cache.round += 1;
|
||||
cache.call(entry, None, &[], fns);
|
||||
|
||||
if let Some(complete) = binding("__complete") {
|
||||
cache.call_host_complete(complete, fns);
|
||||
}
|
||||
cache.call(ENTRY_FN, None, &[], fns);
|
||||
cache.call_host_complete(complete, fns);
|
||||
|
||||
if cache.progress() == before {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The address of module binding `local` (see `flatten::ModuleBindings`): a local of the
|
||||
/// entry, which the module's function shares with its closures
|
||||
pub fn module_binding(entry: usize, local: usize) -> Addr {
|
||||
let context = Context {
|
||||
fn_idx: entry,
|
||||
call_site: None,
|
||||
};
|
||||
|
||||
Addr::Shared {
|
||||
context,
|
||||
slot: Operand::Local(local),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use super::{
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
libqjs::parse::{JsValue, opcodes::OpCode},
|
||||
libqjs::parse::{ImmOperand, JsValue, opcodes::OpCode},
|
||||
native,
|
||||
regify::{BasicBlock, Operand, RegFn, RegInstr},
|
||||
transform::{FIRST_ARG, THIS_ARG},
|
||||
@@ -217,6 +217,15 @@ impl Walker<'_> {
|
||||
// Moves. A key stays as it is: a number is an index
|
||||
SetLoc | SetName | ToPropkey => args[0].clone(),
|
||||
|
||||
// The bindings at the top of a module, which are globals (see link.rs)
|
||||
GetVar => memory.get(Addr::Global(global(instr))).clone(),
|
||||
PutVar => {
|
||||
self.cache
|
||||
.memory
|
||||
.join(Addr::Global(global(instr)), &args[0]);
|
||||
Type::Unknown
|
||||
}
|
||||
|
||||
// Objects
|
||||
Object => self.make(idx, Type::Shape(Shape::Obj(vec![]))),
|
||||
Fclosure => {
|
||||
@@ -452,3 +461,11 @@ fn check_index(index: &Type) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The global that `get_var` or `put_var` reads or writes (see transform/modules.rs)
|
||||
fn global(instr: &RegInstr) -> usize {
|
||||
match instr.imm {
|
||||
ImmOperand::Global(global) => global as usize,
|
||||
imm => unreachable!("a global name that is not a global: {imm:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::ffi::{c_int, c_void};
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
pub const JS_TAG_EXCEPTION: i64 = 6;
|
||||
pub const JS_TAG_MODULE: i64 = -3;
|
||||
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;
|
||||
@@ -12,6 +13,13 @@ pub struct JSValue {
|
||||
pub tag: i64,
|
||||
}
|
||||
|
||||
impl JSValue {
|
||||
/// The pointer of a value that has one, such as a module
|
||||
pub fn ptr(self) -> *mut c_void {
|
||||
unsafe { self.u.ptr }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
union JSValueUnion {
|
||||
@@ -22,6 +30,15 @@ union JSValueUnion {
|
||||
|
||||
pub type JSRuntime = c_void;
|
||||
pub type JSContext = c_void;
|
||||
pub type JSModuleDef = c_void;
|
||||
|
||||
pub type JSModuleLoaderFunc = unsafe extern "C" fn(
|
||||
ctx: *mut JSContext,
|
||||
module_name: *const c_char,
|
||||
opaque: *mut c_void,
|
||||
) -> *mut JSModuleDef;
|
||||
|
||||
pub type JSModuleInitFunc = unsafe extern "C" fn(ctx: *mut JSContext, m: *mut JSModuleDef) -> c_int;
|
||||
|
||||
unsafe extern "C" {
|
||||
pub fn JS_NewRuntime() -> *mut JSRuntime;
|
||||
@@ -48,6 +65,32 @@ unsafe extern "C" {
|
||||
|
||||
pub fn JS_GetException(ctx: *mut JSContext) -> JSValue;
|
||||
|
||||
pub fn JS_ThrowReferenceError(ctx: *mut JSContext, fmt: *const c_char, ...) -> JSValue;
|
||||
|
||||
pub fn JS_SetModuleLoaderFunc(
|
||||
rt: *mut JSRuntime,
|
||||
module_normalize: *const c_void,
|
||||
module_loader: JSModuleLoaderFunc,
|
||||
opaque: *mut c_void,
|
||||
);
|
||||
|
||||
pub fn JS_NewCModule(
|
||||
ctx: *mut JSContext,
|
||||
name: *const c_char,
|
||||
func: JSModuleInitFunc,
|
||||
) -> *mut JSModuleDef;
|
||||
|
||||
pub fn JS_AddModuleExport(
|
||||
ctx: *mut JSContext,
|
||||
m: *mut JSModuleDef,
|
||||
name: *const c_char,
|
||||
) -> c_int;
|
||||
|
||||
pub fn JS_NewArray(ctx: *mut JSContext) -> JSValue;
|
||||
|
||||
pub fn JS_SetPropertyUint32(ctx: *mut JSContext, obj: JSValue, idx: u32, val: JSValue)
|
||||
-> c_int;
|
||||
|
||||
pub fn JS_ToCStringLen2(
|
||||
ctx: *mut JSContext,
|
||||
plen: *mut usize,
|
||||
|
||||
+123
-20
@@ -1,38 +1,60 @@
|
||||
mod ffi;
|
||||
pub mod parse;
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ffi::{CStr, CString, c_char, c_int, c_void};
|
||||
|
||||
use parse::QuickJSBinary;
|
||||
|
||||
pub fn compile(src: &[u8], filename: &str) -> QuickJSBinary {
|
||||
unsafe {
|
||||
let filename = CString::new(filename).unwrap();
|
||||
/// Where the compiler gets the modules that QuickJS asks for
|
||||
pub struct Sources {
|
||||
pub source: fn(&str) -> Option<Vec<u8>>, // the source of a module, by its name
|
||||
pub native: &'static str, // the module of the natives, which has no source
|
||||
pub native_exports: Vec<&'static str>, // its exports
|
||||
}
|
||||
|
||||
/// Compiles `roots`, in order, and every module that they import. QuickJS finds the imports:
|
||||
/// it names each one from the specifier and the importing module's name, and asks `sources`
|
||||
/// for any module that it has not loaded (see `load_module`)
|
||||
///
|
||||
/// Every module is written in one go, as one array, so all of them share one atom table. The
|
||||
/// module of the natives is a C module: it has exports but no code, so it is not written
|
||||
pub fn compile(roots: &[String], sources: Sources) -> QuickJSBinary {
|
||||
unsafe {
|
||||
let rt = ffi::JS_NewRuntime();
|
||||
let ctx = ffi::JS_NewContext(rt);
|
||||
// QuickJS calls `load_module` while it compiles a module, so the loader is only ever
|
||||
// reached through this pointer, and never held across a compile
|
||||
let loader = Box::into_raw(Box::new(Loader {
|
||||
sources,
|
||||
loaded: vec![],
|
||||
}));
|
||||
ffi::JS_SetModuleLoaderFunc(rt, std::ptr::null(), load_module, loader.cast());
|
||||
|
||||
// JS_Eval requires a '\0' after the last byte of the source.
|
||||
let src_z = [src, &[0]].concat();
|
||||
for root in roots {
|
||||
if (*loader).loaded.iter().any(|(name, _)| name == root) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let obj = ffi::JS_Eval(
|
||||
ctx,
|
||||
src_z.as_ptr().cast(),
|
||||
src.len(),
|
||||
filename.as_ptr().cast(),
|
||||
ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE,
|
||||
);
|
||||
let source = ((*loader).sources.source)(root);
|
||||
let source = source.unwrap_or_else(|| panic!("cannot read `{root}`"));
|
||||
let module = eval_module(ctx, &source, root);
|
||||
assert!(module.tag != ffi::JS_TAG_EXCEPTION, "{}", exception(ctx));
|
||||
(*loader).loaded.push((root.clone(), module));
|
||||
}
|
||||
|
||||
assert!(obj.tag != ffi::JS_TAG_EXCEPTION, "{}", get_exception(ctx));
|
||||
let loader = Box::from_raw(loader);
|
||||
let modules = ffi::JS_NewArray(ctx);
|
||||
|
||||
for (i, (_, module)) in loader.loaded.into_iter().enumerate() {
|
||||
ffi::JS_SetPropertyUint32(ctx, modules, i as u32, module);
|
||||
}
|
||||
|
||||
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 buf = ffi::JS_WriteObject(ctx, &mut len, modules, ffi::JS_WRITE_OBJ_BYTECODE);
|
||||
assert!(!buf.is_null(), "{}", exception(ctx));
|
||||
let bytes = std::slice::from_raw_parts(buf, len).to_vec();
|
||||
|
||||
// TODO: Are we sure other cleanup is not necessary?
|
||||
ffi::JS_FreeValue(ctx, modules);
|
||||
ffi::JS_FreeContext(ctx);
|
||||
ffi::JS_FreeRuntime(rt);
|
||||
|
||||
@@ -40,7 +62,88 @@ pub fn compile(src: &[u8], filename: &str) -> QuickJSBinary {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_exception(ctx: *mut ffi::JSContext) -> String {
|
||||
/// The loader's state, which QuickJS hands back to `load_module`
|
||||
struct Loader {
|
||||
sources: Sources,
|
||||
loaded: Vec<(String, ffi::JSValue)>, // each module that it compiled, by name, in order
|
||||
}
|
||||
|
||||
/// What QuickJS calls for a module that it has not loaded: `name` is already made from the
|
||||
/// specifier and the importing module's name. The module is compiled, not run. A failure
|
||||
/// sets an exception and gives null, and QuickJS passes it on
|
||||
unsafe extern "C" fn load_module(
|
||||
ctx: *mut ffi::JSContext,
|
||||
name: *const c_char,
|
||||
opaque: *mut c_void,
|
||||
) -> *mut ffi::JSModuleDef {
|
||||
unsafe {
|
||||
let loader = opaque.cast::<Loader>();
|
||||
let text = CStr::from_ptr(name).to_string_lossy().into_owned();
|
||||
|
||||
if text == (*loader).sources.native {
|
||||
return native_module(ctx, name, &(*loader).sources.native_exports);
|
||||
}
|
||||
|
||||
let Some(source) = ((*loader).sources.source)(&text) else {
|
||||
let fmt = c"cannot find module `%s`".as_ptr();
|
||||
ffi::JS_ThrowReferenceError(ctx, fmt, name);
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
|
||||
let module = eval_module(ctx, &source, &text);
|
||||
|
||||
if module.tag == ffi::JS_TAG_EXCEPTION {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
(*loader).loaded.push((text, module));
|
||||
module.ptr()
|
||||
}
|
||||
}
|
||||
|
||||
/// The module of the natives: a C module, with one export for each native
|
||||
unsafe fn native_module(
|
||||
ctx: *mut ffi::JSContext,
|
||||
name: *const c_char,
|
||||
exports: &[&str],
|
||||
) -> *mut ffi::JSModuleDef {
|
||||
unsafe extern "C" fn init(_: *mut ffi::JSContext, _: *mut ffi::JSModuleDef) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let module = ffi::JS_NewCModule(ctx, name, init);
|
||||
|
||||
for export in exports {
|
||||
let export = CString::new(*export).unwrap();
|
||||
ffi::JS_AddModuleExport(ctx, module, export.as_ptr());
|
||||
}
|
||||
|
||||
module
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile `source` as the module `name`, without running it
|
||||
unsafe fn eval_module(ctx: *mut ffi::JSContext, source: &[u8], name: &str) -> ffi::JSValue {
|
||||
// JS_Eval requires a '\0' after the last byte of the source
|
||||
let source_z = [source, &[0]].concat();
|
||||
let name = CString::new(name).unwrap();
|
||||
let flags = ffi::JS_EVAL_FLAG_COMPILE_ONLY | ffi::JS_EVAL_TYPE_MODULE;
|
||||
|
||||
unsafe {
|
||||
let module = ffi::JS_Eval(
|
||||
ctx,
|
||||
source_z.as_ptr(),
|
||||
source.len(),
|
||||
name.as_ptr().cast(),
|
||||
flags,
|
||||
);
|
||||
debug_assert!(module.tag == ffi::JS_TAG_MODULE || module.tag == ffi::JS_TAG_EXCEPTION);
|
||||
module
|
||||
}
|
||||
}
|
||||
|
||||
fn 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);
|
||||
|
||||
@@ -27,8 +27,8 @@ pub mod opcodes {
|
||||
pub struct QuickJSBinary {
|
||||
pub _version: u8,
|
||||
pub _checksum: u32,
|
||||
pub atoms: Vec<Atom>,
|
||||
pub root_mod: Module,
|
||||
pub atoms: Vec<Atom>, // one table for every module (see `libqjs::compile`)
|
||||
pub modules: Vec<Module>, // in the order that they were loaded
|
||||
}
|
||||
|
||||
impl QuickJSBinary {
|
||||
@@ -39,9 +39,16 @@ impl QuickJSBinary {
|
||||
let (bin, atoms) = count(Atom::parse, num_atoms).parse(bin)?;
|
||||
let (bin, value) = JsValue::parse(bin)?;
|
||||
|
||||
let JsValue::Module(root_mod) = value else {
|
||||
panic!()
|
||||
// An array of the program's modules (see `libqjs::compile`)
|
||||
let JsValue::Array(items) = value else {
|
||||
panic!("the binary is not an array of modules")
|
||||
};
|
||||
let modules = (items.into_iter())
|
||||
.map(|item| match item {
|
||||
JsValue::Module(module) => module,
|
||||
_ => panic!("the binary holds a value that is not a module"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((
|
||||
bin,
|
||||
@@ -49,7 +56,7 @@ impl QuickJSBinary {
|
||||
_version: version,
|
||||
_checksum: checksum,
|
||||
atoms,
|
||||
root_mod,
|
||||
modules,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -533,6 +540,9 @@ pub enum ImmOperand {
|
||||
_target: usize,
|
||||
_arg: u16,
|
||||
},
|
||||
/// A module binding, by its index in the program (see link.rs). Only the compiler makes
|
||||
/// it, for `get_var` and `put_var`
|
||||
Global(u32),
|
||||
}
|
||||
|
||||
impl ImmOperand {
|
||||
@@ -763,13 +773,13 @@ impl Function {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Module {
|
||||
pub _name: AtomRef,
|
||||
pub _import_modules: Vec<AtomRef>,
|
||||
pub _exports: Vec<ExportInfo>,
|
||||
pub _star_exports: Vec<usize>,
|
||||
pub _imports: Vec<ImportInfo>,
|
||||
pub _has_top_level_await: bool,
|
||||
pub root_fn: Function,
|
||||
pub name: AtomRef, // the name that it was loaded by (see `libqjs::compile`)
|
||||
pub requests: Vec<AtomRef>, // 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 root_fn: Function, // the module's body
|
||||
}
|
||||
|
||||
impl Module {
|
||||
@@ -796,69 +806,76 @@ impl Module {
|
||||
Ok((
|
||||
bin,
|
||||
Self {
|
||||
_name: name,
|
||||
_import_modules: import_modules,
|
||||
_exports: exports,
|
||||
_star_exports: star_exports,
|
||||
_imports: imports,
|
||||
_has_top_level_await: has_tla == 1,
|
||||
name,
|
||||
requests: import_modules,
|
||||
exports,
|
||||
star_exports,
|
||||
imports,
|
||||
has_top_level_await: has_tla == 1,
|
||||
root_fn,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExportInfo {
|
||||
/// All exports except aliases and star exports
|
||||
Local { _var_idx: usize, _name: AtomRef },
|
||||
/// Alias exports, has `from`, eg: export { a } from "./dep.js";
|
||||
Alias,
|
||||
Local {
|
||||
var_idx: usize, // a captured var of the module's body: its own binding, or an import
|
||||
name: AtomRef,
|
||||
}, // `export { x }`, `export let x`, `export function x`, ...
|
||||
Indirect {
|
||||
request: usize, // an index into the module's `requests`
|
||||
local: AtomRef, // the name that the other module exports
|
||||
name: AtomRef,
|
||||
}, // `export { x as y } from "..."`
|
||||
}
|
||||
|
||||
impl ExportInfo {
|
||||
fn parse(bin: &[u8]) -> IResult<&[u8], Self> {
|
||||
let (bin, kind) = u8(bin)?;
|
||||
|
||||
let ret = match kind {
|
||||
match kind {
|
||||
0 => {
|
||||
let (bin, var_idx) = leb128(bin)?;
|
||||
let (bin, name) = AtomRef::parse(bin)?;
|
||||
(
|
||||
bin,
|
||||
Self::Local {
|
||||
_var_idx: var_idx,
|
||||
_name: name,
|
||||
},
|
||||
)
|
||||
Ok((bin, Self::Local { var_idx, name }))
|
||||
}
|
||||
1 => (bin, Self::Alias),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(ret)
|
||||
_ => {
|
||||
let (bin, request) = leb128(bin)?;
|
||||
let (bin, local) = AtomRef::parse(bin)?;
|
||||
let (bin, name) = AtomRef::parse(bin)?;
|
||||
Ok((
|
||||
bin,
|
||||
Self::Indirect {
|
||||
request,
|
||||
local,
|
||||
name,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImportInfo {
|
||||
pub _var_idx: usize,
|
||||
pub _name: AtomRef,
|
||||
pub _req_module_idx: usize,
|
||||
pub var_idx: usize, // a captured var of the module's body
|
||||
pub name: AtomRef, // the name that the other module exports, or `*` for a namespace
|
||||
pub request: usize, // an index into the module's `requests`
|
||||
}
|
||||
|
||||
impl ImportInfo {
|
||||
fn parse(bin: &[u8]) -> IResult<&[u8], Self> {
|
||||
let (bin, var_idx) = leb128(bin)?;
|
||||
let (bin, name) = AtomRef::parse(bin)?;
|
||||
let (bin, req_module_idx) = leb128(bin)?;
|
||||
|
||||
let (bin, request) = leb128(bin)?;
|
||||
Ok((
|
||||
bin,
|
||||
Self {
|
||||
_var_idx: var_idx,
|
||||
_name: name,
|
||||
_req_module_idx: req_module_idx,
|
||||
var_idx,
|
||||
name,
|
||||
request,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
//! 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 <entry>() {
|
||||
//! // 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<usize, (String, Vec<Target>)>, // 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<String, HashMap<String, Target>>, // 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<Target> {
|
||||
self.export(PRELUDE, name)
|
||||
}
|
||||
|
||||
/// The export `name` of the module `module`
|
||||
pub fn export(&self, module: &str, name: &str) -> Option<Target> {
|
||||
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<ClosureVar>],
|
||||
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<String, Target> {
|
||||
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<ClosureVar>],
|
||||
atoms: &'a [Atom],
|
||||
index: HashMap<String, usize>, // each module, by name
|
||||
decls: Vec<HashMap<usize, usize>>, // 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<ClosureVar>], 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<usize, (String, Vec<Target>)> {
|
||||
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<String, HashMap<String, Target>> {
|
||||
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<Target> {
|
||||
(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<Target> {
|
||||
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<usize>) -> HashMap<String, Target> {
|
||||
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<usize> {
|
||||
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<usize>) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
+72
-43
@@ -2,33 +2,59 @@ mod emit;
|
||||
mod flatten;
|
||||
mod infer;
|
||||
mod libqjs;
|
||||
mod link;
|
||||
mod native;
|
||||
mod regify;
|
||||
mod transform;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use libqjs::{Sources, compile};
|
||||
use link::{NATIVE, PRELUDE};
|
||||
use native::NATIVES;
|
||||
|
||||
use libqjs::compile;
|
||||
|
||||
/// The stdlib, in the order that it runs in, in front of the program
|
||||
const STDLIB: &[&[u8]] = &[
|
||||
include_bytes!("../../lib/stdlib/globals.js"),
|
||||
include_bytes!("../../lib/stdlib/object.js"),
|
||||
include_bytes!("../../lib/stdlib/string.js"),
|
||||
include_bytes!("../../lib/stdlib/number.js"),
|
||||
include_bytes!("../../lib/stdlib/boolean.js"),
|
||||
include_bytes!("../../lib/stdlib/array.js"),
|
||||
include_bytes!("../../lib/stdlib/loop.js"),
|
||||
include_bytes!("../../lib/stdlib/promise.js"),
|
||||
include_bytes!("../../lib/stdlib/coroutine.js"),
|
||||
include_bytes!("../../lib/stdlib/timers.js"),
|
||||
include_bytes!("../../lib/stdlib/io.js"),
|
||||
include_bytes!("../../lib/stdlib/console.js"),
|
||||
/// The stdlib's modules, by name. The prelude's exports are every module's global names (see
|
||||
/// link.rs)
|
||||
const STDLIB: &[(&str, &[u8])] = &[
|
||||
(
|
||||
"jsrt:prelude",
|
||||
include_bytes!("../../lib/stdlib/prelude.js"),
|
||||
),
|
||||
(
|
||||
"jsrt:globals",
|
||||
include_bytes!("../../lib/stdlib/globals.js"),
|
||||
),
|
||||
("jsrt:object", include_bytes!("../../lib/stdlib/object.js")),
|
||||
("jsrt:string", include_bytes!("../../lib/stdlib/string.js")),
|
||||
("jsrt:number", include_bytes!("../../lib/stdlib/number.js")),
|
||||
(
|
||||
"jsrt:boolean",
|
||||
include_bytes!("../../lib/stdlib/boolean.js"),
|
||||
),
|
||||
("jsrt:array", include_bytes!("../../lib/stdlib/array.js")),
|
||||
("jsrt:loop", include_bytes!("../../lib/stdlib/loop.js")),
|
||||
(
|
||||
"jsrt:promise",
|
||||
include_bytes!("../../lib/stdlib/promise.js"),
|
||||
),
|
||||
(
|
||||
"jsrt:coroutine",
|
||||
include_bytes!("../../lib/stdlib/coroutine.js"),
|
||||
),
|
||||
("jsrt:timers", include_bytes!("../../lib/stdlib/timers.js")),
|
||||
("jsrt:io", include_bytes!("../../lib/stdlib/io.js")),
|
||||
(
|
||||
"jsrt:console",
|
||||
include_bytes!("../../lib/stdlib/console.js"),
|
||||
),
|
||||
];
|
||||
|
||||
/// Runs after the program's top level, or after its first top-level `await`
|
||||
const STDLIB_END: &[u8] = include_bytes!("../../lib/stdlib/end.js");
|
||||
/// The source of a module: a stdlib module by its name, else a file by its path
|
||||
fn source(name: &str) -> Option<Vec<u8>> {
|
||||
match STDLIB.iter().find(|(n, _)| *n == name) {
|
||||
Some((_, source)) => Some(source.to_vec()),
|
||||
None if name.starts_with("jsrt:") => None,
|
||||
None => std::fs::read(name).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The runtime. The emitted C includes `jsrt.h`, which includes the rest
|
||||
const RUNTIME: &[(&str, &[u8])] = &[
|
||||
@@ -41,34 +67,37 @@ const RUNTIME: &[(&str, &[u8])] = &[
|
||||
];
|
||||
|
||||
fn main() {
|
||||
// Load source file
|
||||
let args = std::env::args().collect::<Vec<String>>();
|
||||
let filename = args.get(1).unwrap();
|
||||
// `compiler <file.js>...`: each file is a module, and they run in order. A file's imports
|
||||
// are found from it. Each is named by its full path, so that its relative imports name
|
||||
// their files the same way from any folder
|
||||
let files: Vec<String> = (std::env::args().skip(1))
|
||||
.map(|file| match std::fs::canonicalize(&file) {
|
||||
Ok(path) => path.to_string_lossy().into_owned(),
|
||||
Err(err) => panic!("cannot read `{file}`: {err}"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut file = File::open(filename).unwrap();
|
||||
let metadata = std::fs::metadata(filename).unwrap();
|
||||
assert!(metadata.is_file());
|
||||
assert!(!files.is_empty(), "usage: compiler <file.js>...");
|
||||
|
||||
// The stdlib is compiled with the program, in front of it. The program is the body of an
|
||||
// async fn of its own, so that its names can shadow the stdlib's, as a script's names
|
||||
// shadow JS's globals, and so that it can `await` at its top level. `})` is on a line of
|
||||
// its own, in case the program ends in a comment
|
||||
let mut src = STDLIB.concat();
|
||||
src.reserve(metadata.len() as usize + STDLIB_END.len() + 64);
|
||||
src.extend_from_slice(b"(async function () {\n");
|
||||
file.read_to_end(&mut src).unwrap();
|
||||
src.extend_from_slice(b"\n})();\n");
|
||||
src.extend_from_slice(STDLIB_END);
|
||||
|
||||
// Compile to bytecode
|
||||
let bin = compile(&src, filename);
|
||||
// Compile every module to bytecode: the prelude first, then the files
|
||||
let roots: Vec<String> = [PRELUDE.to_string()]
|
||||
.into_iter()
|
||||
.chain(files.clone())
|
||||
.collect();
|
||||
let sources = Sources {
|
||||
source,
|
||||
native: NATIVE,
|
||||
native_exports: NATIVES.iter().map(|native| native.name).collect(),
|
||||
};
|
||||
let bin = compile(&roots, sources);
|
||||
|
||||
// Process it
|
||||
let (flat_fns, captures, atoms, bindings) = flatten::flatten(bin);
|
||||
let flat_fns = transform::transform(flat_fns, captures, &atoms);
|
||||
let (mut flat_fns, captures, atoms, modules) = flatten::flatten(bin);
|
||||
let link = link::link(&modules, &files, &mut flat_fns, &captures, &atoms);
|
||||
let flat_fns = transform::transform(flat_fns, captures, &atoms, &link);
|
||||
let fns = regify::regify(flat_fns);
|
||||
let cache = infer::infer_types(&fns, &bindings);
|
||||
let c = emit::emit(&fns, &cache, &atoms, &bindings);
|
||||
let cache = infer::infer_types(&fns, &link);
|
||||
let c = emit::emit(&fns, &cache, &atoms, &link);
|
||||
|
||||
// Compile the C, with the runtime, to `a.out`
|
||||
let dir = std::env::temp_dir().join(format!("jsrt-{}", std::process::id()));
|
||||
|
||||
+5
-28
@@ -1,18 +1,12 @@
|
||||
//! The C fns that JS can call, and the globals
|
||||
//! The C fns that JS can call
|
||||
//!
|
||||
//! A native is a C fn of the runtime (lib/rt), which the stdlib calls by name, as a global. Its
|
||||
//! prefix tells where it comes from (see lib/rt/jsrt.h):
|
||||
//! A native is a C fn of the runtime (lib/rt). The natives are the exports of the module
|
||||
//! `jsrt:native`, which the stdlib imports them from (see link.rs). Its prefix tells where it
|
||||
//! comes from (see lib/rt/jsrt.h):
|
||||
//! - `__native_*`: the platform's IO, which another platform's runtime replaces
|
||||
//! - `__rt_*`: the same on every platform: strings and numbers
|
||||
//!
|
||||
//! The only globals are the natives and `undefined`. Everything else that the stdlib defines
|
||||
//! is a module binding, which the compiler reads by name where it needs one (see
|
||||
//! `flatten::flatten`)
|
||||
|
||||
use crate::{
|
||||
infer::Type,
|
||||
libqjs::parse::{Atom, AtomRef, JsValue},
|
||||
};
|
||||
use crate::infer::Type;
|
||||
|
||||
/// A native: one set of param types, and one return type
|
||||
pub struct NativeFn {
|
||||
@@ -80,23 +74,6 @@ pub const NATIVES: &[NativeFn] = natives! {
|
||||
fn __rt_string_from_char_code(Int) -> String;
|
||||
};
|
||||
|
||||
/// The value of the global `name`
|
||||
pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue {
|
||||
let name = name.text(atoms);
|
||||
|
||||
// A global property in JS, but a constant
|
||||
if name == "undefined" {
|
||||
return JsValue::Undefined;
|
||||
}
|
||||
|
||||
match NATIVES.iter().position(|n| name == n.name) {
|
||||
Some(idx) => JsValue::NativeFn(idx),
|
||||
None => panic!(
|
||||
"unknown global `{name}`: the globals are the natives in compiler/src/native.rs, and `undefined`"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a value of type `arg` can be passed for a param of type `param`. JS has one number
|
||||
/// type, so an int and a float both pass for a number
|
||||
pub fn can_pass(arg: &Type, param: &Type) -> bool {
|
||||
|
||||
@@ -40,8 +40,10 @@
|
||||
//! Every kind of fn that suspends lowers the same way: only the driver differs, which is
|
||||
//! stdlib JS (lib/stdlib/coroutine.js). `co` holds what a suspension hands out to it, each
|
||||
//! in a field of its own so that their types stay apart: `value` for a yield, `awaiting` for
|
||||
//! an await, `result` for the return. The constructor reaches its kind's driver by name, as
|
||||
//! if the source named it
|
||||
//! an await, `result` for the return. The constructor reads its kind's driver, an export of
|
||||
//! `jsrt:coroutine`, as a global (see link.rs)
|
||||
//!
|
||||
//! A module's body that awaits at its top is an async fn too (see flatten.rs)
|
||||
//!
|
||||
//! In the step:
|
||||
//! a local, arg or capture the step's capture of it
|
||||
@@ -55,8 +57,7 @@
|
||||
//! Its label, where the step's first instrs jump for that state, pushes them back. `yield`
|
||||
//! pushes a flag after its value, which is true for `return()`: here it is always false
|
||||
//!
|
||||
//! Not supported: `yield*`, which QuickJS expands to a loop of iterator ops, and top-level
|
||||
//! `await`. The module fn is async to QuickJS, but runs as a normal fn (see flatten.rs)
|
||||
//! Not supported: `yield*`, which QuickJS expands to a loop of iterator ops
|
||||
|
||||
use super::PROTO_FIELD;
|
||||
use crate::{
|
||||
@@ -65,19 +66,19 @@ use crate::{
|
||||
Atom, AtomRef, ClosureKind, ClosureVar, ClosureVarFlags, FuncKind, ImmOperand as Imm,
|
||||
Instr, JsValue, VarKind, atom_names::PREDEFINED_ATOMS, opcodes::OpCode,
|
||||
},
|
||||
link::Link,
|
||||
};
|
||||
|
||||
/// The module fn, `<eval>`: flatten's first fn
|
||||
const MODULE_FN: usize = 0;
|
||||
|
||||
// ─────────────────────────────────────── The spine ───────────────────────────────────────
|
||||
|
||||
pub fn deasync(fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>, atoms: &[Atom]) {
|
||||
let top_level_await = fns[MODULE_FN].code.iter().any(|i| i.op == OpCode::Await);
|
||||
assert!(!top_level_await, "top-level `await` is not supported");
|
||||
|
||||
pub fn deasync(
|
||||
fns: &mut Vec<FlatFn>,
|
||||
captures: &mut Vec<Vec<ClosureVar>>,
|
||||
atoms: &[Atom],
|
||||
link: &Link,
|
||||
) {
|
||||
let coroutines: Vec<usize> = (0..fns.len())
|
||||
.filter(|&f| is_coroutine(f, &fns[f]))
|
||||
.filter(|&f| fns[f].kind != FuncKind::Normal)
|
||||
.collect();
|
||||
let mut steps = vec![];
|
||||
|
||||
@@ -95,9 +96,8 @@ pub fn deasync(fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>, atoms
|
||||
let step_captures = step.captures(&own);
|
||||
retarget_children(&cpool, &step, &own, fns, captures);
|
||||
|
||||
// 3. Add the step just before the synthetic entry, which stays last (see flatten.rs).
|
||||
// No fn refers to the entry, so moving it changes no reference
|
||||
let s = fns.len() - 1;
|
||||
// 3. Add the step after every other fn
|
||||
let s = fns.len();
|
||||
let step_fn = FlatFn {
|
||||
name: fns[f].name,
|
||||
kind: FuncKind::Normal,
|
||||
@@ -106,30 +106,21 @@ pub fn deasync(fns: &mut Vec<FlatFn>, captures: &mut Vec<Vec<ClosureVar>>, atoms
|
||||
cpool,
|
||||
code: step.code(),
|
||||
};
|
||||
fns.insert(s, step_fn);
|
||||
captures.insert(s, step_captures);
|
||||
fns.push(step_fn);
|
||||
captures.push(step_captures);
|
||||
|
||||
fns[f].local_count = step.locals;
|
||||
fns[f].cpool = vec![JsValue::FnByIdx(s)];
|
||||
steps.push(step);
|
||||
}
|
||||
|
||||
// 4. Each constructor makes `co`, linked to its kind's driver, and starts it. A link goes
|
||||
// through the fn's parents, so it waits until every fn is in place
|
||||
let parents = parents(fns);
|
||||
|
||||
// 4. Each constructor makes `co`, linked to its kind's driver, and starts it
|
||||
for (&f, step) in coroutines.iter().zip(&steps) {
|
||||
let proto = module_binding(f, driver(fns[f].kind), &parents, captures, atoms);
|
||||
fns[f].code = constructor(step, proto, atoms);
|
||||
let driver = link.stdlib("jsrt:coroutine", driver(fns[f].kind));
|
||||
fns[f].code = constructor(step, driver, atoms);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `f` is an async fn or a generator. The module fn is async to QuickJS, but it is
|
||||
/// not lowered
|
||||
fn is_coroutine(f: usize, fun: &FlatFn) -> bool {
|
||||
f != MODULE_FN && fun.kind != FuncKind::Normal
|
||||
}
|
||||
|
||||
/// The stdlib's driver of a kind of fn (see lib/stdlib/coroutine.js)
|
||||
fn driver(kind: FuncKind) -> &'static str {
|
||||
match kind {
|
||||
@@ -531,9 +522,10 @@ fn rename_forwards(
|
||||
|
||||
// ──────────────────────────────────── The constructor ────────────────────────────────────
|
||||
|
||||
/// The constructor's code: keep `this` and the fn itself, make `co` with the step, set the
|
||||
/// state to 0, and return what the driver's `start` gives
|
||||
fn constructor(step: &Step, proto: usize, atoms: &[Atom]) -> Vec<Instr> {
|
||||
/// The constructor's code: keep `this` and the fn itself, make `co` with the step and the
|
||||
/// driver, which is the global `driver`, set the state to 0, and return what the driver's
|
||||
/// `start` gives
|
||||
fn constructor(step: &Step, driver: usize, atoms: &[Atom]) -> Vec<Instr> {
|
||||
use OpCode::*;
|
||||
|
||||
let field = |text: &str| Imm::Atom(find_atom(text, atoms));
|
||||
@@ -545,7 +537,7 @@ fn constructor(step: &Step, proto: usize, atoms: &[Atom]) -> Vec<Instr> {
|
||||
(SpecialObject, Imm::U8(2)),
|
||||
(PutLoc, loc(step.callee)),
|
||||
(Object, Imm::None),
|
||||
(GetVarRef, Imm::VarRef(proto as u16)),
|
||||
(GetVar, Imm::Global(driver as u32)),
|
||||
(DefineField, Imm::Atom(PROTO_FIELD)),
|
||||
(Fclosure, Imm::Const(0)),
|
||||
(DefineField, field("step")),
|
||||
@@ -690,53 +682,3 @@ fn successors(code: &[Instr], i: usize) -> Vec<usize> {
|
||||
|
||||
next
|
||||
}
|
||||
|
||||
// ───────────────────────────────── Links to the drivers ──────────────────────────────────
|
||||
|
||||
/// The capture of `f` that reads the module binding `name`, made if needed, with a forward in
|
||||
/// each fn between the module fn and `f`
|
||||
fn module_binding(
|
||||
f: usize,
|
||||
name: &str,
|
||||
parents: &[usize],
|
||||
captures: &mut [Vec<ClosureVar>],
|
||||
atoms: &[Atom],
|
||||
) -> usize {
|
||||
if f == MODULE_FN {
|
||||
for (i, c) in captures[MODULE_FN].iter().enumerate() {
|
||||
// Every capture of the module fn is a module binding (see flatten.rs)
|
||||
if c.name.text(atoms) == name {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("async fns and generators need `{name}` from the stdlib (lib/stdlib/coroutine.js)");
|
||||
}
|
||||
|
||||
let i = module_binding(parents[f], name, parents, captures, atoms);
|
||||
|
||||
for (j, c) in captures[f].iter().enumerate() {
|
||||
if c.flags.closure_kind == ClosureKind::Ref && c.var_idx == i {
|
||||
return j;
|
||||
}
|
||||
}
|
||||
|
||||
let atom = captures[parents[f]][i].name;
|
||||
captures[f].push(closure_var(atom, i, ClosureKind::Ref));
|
||||
captures[f].len() - 1
|
||||
}
|
||||
|
||||
/// The parent of each fn: the fn whose constant pool holds it
|
||||
fn parents(fns: &[FlatFn]) -> Vec<usize> {
|
||||
let mut parents = vec![usize::MAX; fns.len()];
|
||||
|
||||
for (p, f) in fns.iter().enumerate() {
|
||||
for c in &f.cpool {
|
||||
if let &JsValue::FnByIdx(child) = c {
|
||||
parents[child] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parents
|
||||
}
|
||||
|
||||
@@ -93,8 +93,7 @@
|
||||
//! a capture: transform/async.rs makes it, when a binding that it moves out of a fn's frame
|
||||
//! needs a new location for each loop iteration
|
||||
//!
|
||||
//! A module binding is a local of the synthetic entry fn, which makes `<eval>` once (see
|
||||
//! flatten.rs), so there is one location for each module binding
|
||||
//! A module binding is not a capture: modules.rs made it a global first
|
||||
//!
|
||||
//! In the emitted C, every underlying "plain" function is called as:
|
||||
//! `fn(env, this, arg_0, arg_1, ... arg_n)`
|
||||
@@ -192,7 +191,7 @@ pub fn try_replace_closure(
|
||||
|
||||
/// If `instr` reads or writes capture `i` of its fn: `i`, whether it writes, and whether a
|
||||
/// write also leaves the value on the stack
|
||||
fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
|
||||
pub(super) fn var_ref_access(instr: &Instr) -> Option<(usize, bool, bool)> {
|
||||
use OpCode::*;
|
||||
|
||||
let i = match instr.imm {
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
|
||||
use super::{ENV_ARG, PROTO_FIELD};
|
||||
|
||||
use crate::{
|
||||
libqjs::parse::{Atom, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode},
|
||||
native,
|
||||
};
|
||||
use crate::libqjs::parse::{ImmOperand as Imm, Instr, opcodes::OpCode};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn try_replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
|
||||
@@ -147,25 +144,3 @@ pub fn try_replace_expansion(instr: &Instr, out: &mut Vec<Instr>) -> bool {
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Replace a read of a global with the constant it is (see `native`)
|
||||
pub fn replace_get_var(
|
||||
instr: &Instr,
|
||||
atoms: &[Atom],
|
||||
cpool: &mut Vec<JsValue>,
|
||||
out: &mut Vec<Instr>,
|
||||
) -> bool {
|
||||
if instr.op != OpCode::GetVar {
|
||||
return false;
|
||||
}
|
||||
|
||||
cpool.push(native::global(instr.imm.atom(), atoms));
|
||||
|
||||
out.push(Instr {
|
||||
pc: instr.pc,
|
||||
op: OpCode::PushConst,
|
||||
imm: Imm::Const(cpool.len() as u32 - 1),
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! - define_method
|
||||
//! - check_ctor
|
||||
//! - special_object
|
||||
//! - get_var
|
||||
//! - get_var_undef, put_var_init
|
||||
//! - push_this
|
||||
//! - get_arg{0, 1, 2, 3}
|
||||
//! - put_arg{0, 1, 2, 3}
|
||||
@@ -26,6 +26,8 @@
|
||||
//!
|
||||
//!
|
||||
//! The following instructions take on a different meaning:
|
||||
//! - get_var, put_var: a read or write of a binding at the top of a module, which is a
|
||||
//! global (see modules.rs)
|
||||
//! - call_constructor (see new.rs)
|
||||
//! - close_loc (see closure.rs)
|
||||
//!
|
||||
@@ -33,7 +35,10 @@
|
||||
//! - define_field_ref (see closure.rs)
|
||||
//! - close_field (see closure.rs)
|
||||
//!
|
||||
//! Async fns and generators become a constructor and a step closure first (see async.rs),
|
||||
//! Module bindings become globals first (see modules.rs): `get_var` and `put_var` then read
|
||||
//! and write one, by an `ImmOperand::Global`
|
||||
//!
|
||||
//! Async fns and generators become a constructor and a step closure next (see async.rs),
|
||||
//! so no instr of theirs is left: initial_yield, yield, await, return_async in them, and
|
||||
//! close_var_ref, which deasync makes and closure.rs lowers
|
||||
|
||||
@@ -42,18 +47,21 @@ mod r#async;
|
||||
mod class;
|
||||
mod closure;
|
||||
mod expand;
|
||||
mod modules;
|
||||
mod new;
|
||||
|
||||
use crate::{
|
||||
flatten::FlatFn,
|
||||
libqjs::parse::{Atom, AtomRef, ClosureVar},
|
||||
link::Link,
|
||||
};
|
||||
|
||||
use args::try_replace_arg_access;
|
||||
use r#async::deasync;
|
||||
use class::declass;
|
||||
use closure::try_replace_closure;
|
||||
use expand::{replace_get_var, try_replace_expansion};
|
||||
use expand::try_replace_expansion;
|
||||
use modules::globalize;
|
||||
use new::try_replace_new;
|
||||
|
||||
// Object Prototype Rules:
|
||||
@@ -85,25 +93,24 @@ pub fn transform(
|
||||
mut flat_fns: Vec<FlatFn>,
|
||||
mut captures: Vec<Vec<ClosureVar>>,
|
||||
atoms: &[Atom],
|
||||
link: &Link,
|
||||
) -> Vec<FlatFn> {
|
||||
// Async fns and generators become state machines first: this adds fns, and changes the
|
||||
// Module bindings become globals first: this drops captured vars
|
||||
globalize(&mut flat_fns, &mut captures, link, atoms);
|
||||
|
||||
// Async fns and generators become state machines next: this adds fns, and changes the
|
||||
// captures that the other transforms read
|
||||
deasync(&mut flat_fns, &mut captures, atoms);
|
||||
deasync(&mut flat_fns, &mut captures, atoms, link);
|
||||
|
||||
flat_fns
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, f)| transform_one(f, &captures[idx], &captures, atoms))
|
||||
.map(|(idx, f)| transform_one(f, &captures[idx], &captures))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `own` is the captures of `f`
|
||||
fn transform_one(
|
||||
mut f: FlatFn,
|
||||
own: &[ClosureVar],
|
||||
captures: &[Vec<ClosureVar>],
|
||||
atoms: &[Atom],
|
||||
) -> FlatFn {
|
||||
fn transform_one(f: FlatFn, own: &[ClosureVar], captures: &[Vec<ClosureVar>]) -> FlatFn {
|
||||
// Transform class declarations
|
||||
let code = declass(&f.code);
|
||||
|
||||
@@ -132,10 +139,6 @@ fn transform_one(
|
||||
continue;
|
||||
}
|
||||
|
||||
if replace_get_var(instr, atoms, &mut f.cpool, &mut out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if try_replace_closure(instr, own, captures, &f.cpool, &mut out) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Module bindings become globals (see link.rs)
|
||||
//!
|
||||
//! A module's body reaches its bindings and imports as captured vars, and so does each fn in
|
||||
//! it, through its parents. Link found what each of the body's captured vars refers to. Here
|
||||
//! each read and write of one becomes a read or write of a global, with `get_var` and
|
||||
//! `put_var` on an `ImmOperand::Global`. A native is a constant
|
||||
//!
|
||||
//! get_var_ref k get_var g (or push_const of a native)
|
||||
//! put_var_ref k put_var g
|
||||
//! set_var_ref k dup; put_var g
|
||||
//!
|
||||
//! The `_check` forms are the same: the TDZ is not checked. Those captured vars are then
|
||||
//! dropped, so a closure no longer holds module bindings, and the captured vars that are
|
||||
//! left are numbered again
|
||||
//!
|
||||
//! A global name, such as `String`, is a `get_var` of its name. It becomes the global or the
|
||||
//! native that the prelude exports by that name. `typeof` of a name that nothing declares is
|
||||
//! a `get_var_undef`, which gives undefined. A write of a global name is not allowed
|
||||
|
||||
use super::closure::var_ref_access;
|
||||
use crate::{
|
||||
flatten::{ENTRY_FN, FlatFn},
|
||||
libqjs::parse::{
|
||||
Atom, ClosureKind, ClosureVar, ImmOperand as Imm, Instr, JsValue, opcodes::OpCode,
|
||||
},
|
||||
link::{Link, Target},
|
||||
};
|
||||
|
||||
pub fn globalize(
|
||||
fns: &mut [FlatFn],
|
||||
captures: &mut [Vec<ClosureVar>],
|
||||
link: &Link,
|
||||
atoms: &[Atom],
|
||||
) {
|
||||
// Each fn's parent goes first, because a capture names a captured var of its parent. Each
|
||||
// fn also gets the name of its module, for errors
|
||||
let mut stack = vec![(ENTRY_FN, vec![], String::new())];
|
||||
|
||||
while let Some((f, parent, module)) = stack.pop() {
|
||||
let module = link.body(f).map_or(module, |(name, _)| name.to_string());
|
||||
let targets = targets(f, &captures[f], &parent, link);
|
||||
let renumber = renumber(&targets);
|
||||
|
||||
captures[f] = keep(&captures[f], &targets, &parent);
|
||||
fns[f].code = rewrite(&mut fns[f], &targets, &renumber, link, atoms, &module);
|
||||
|
||||
let children = fns[f].cpool.iter().filter_map(|c| match c {
|
||||
JsValue::FnByIdx(child) => Some(*child),
|
||||
_ => None,
|
||||
});
|
||||
let own = targets.into_iter().zip(renumber).map(|(t, r)| Slot {
|
||||
target: t,
|
||||
index: r,
|
||||
});
|
||||
let own: Vec<Slot> = own.collect();
|
||||
|
||||
for child in children.collect::<Vec<_>>() {
|
||||
stack.push((child, own.clone(), module.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A captured var of a fn, after this pass: the binding that it refers to, if that is a
|
||||
/// module binding, else its new index
|
||||
#[derive(Clone, Copy)]
|
||||
struct Slot {
|
||||
target: Option<Target>,
|
||||
index: Option<usize>,
|
||||
}
|
||||
|
||||
/// What each captured var of `f` refers to, if a module binding. A module's body's captured
|
||||
/// vars are all module bindings. Any other fn's capture refers to one if the captured var of
|
||||
/// its parent does
|
||||
fn targets(f: usize, own: &[ClosureVar], parent: &[Slot], link: &Link) -> Vec<Option<Target>> {
|
||||
if let Some((_, vars)) = link.body(f) {
|
||||
return vars.iter().map(|&target| Some(target)).collect();
|
||||
}
|
||||
|
||||
(own.iter())
|
||||
.map(|var| match var.flags.closure_kind {
|
||||
ClosureKind::Ref => parent[var.var_idx].target,
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The new index of each captured var that stays
|
||||
fn renumber(targets: &[Option<Target>]) -> Vec<Option<usize>> {
|
||||
let mut next = 0;
|
||||
|
||||
(targets.iter())
|
||||
.map(|target| match target {
|
||||
Some(_) => None,
|
||||
None => {
|
||||
next += 1;
|
||||
Some(next - 1)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The captured vars that stay. A capture of the parent's captured var gets its new index
|
||||
fn keep(own: &[ClosureVar], targets: &[Option<Target>], parent: &[Slot]) -> Vec<ClosureVar> {
|
||||
let stay = own
|
||||
.iter()
|
||||
.zip(targets)
|
||||
.filter(|(_, target)| target.is_none());
|
||||
|
||||
stay.map(|(var, _)| {
|
||||
let mut var = var.clone();
|
||||
|
||||
if var.flags.closure_kind == ClosureKind::Ref {
|
||||
var.var_idx = parent[var.var_idx].index.unwrap();
|
||||
}
|
||||
|
||||
var
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The code of `f`, with module bindings as globals, and the captured vars that stay at their
|
||||
/// new indices
|
||||
fn rewrite(
|
||||
f: &mut FlatFn,
|
||||
targets: &[Option<Target>],
|
||||
renumber: &[Option<usize>],
|
||||
link: &Link,
|
||||
atoms: &[Atom],
|
||||
module: &str,
|
||||
) -> Vec<Instr> {
|
||||
use OpCode::*;
|
||||
|
||||
let mut out = vec![];
|
||||
let code = std::mem::take(&mut f.code);
|
||||
|
||||
for instr in code {
|
||||
let pc = instr.pc;
|
||||
let mut push = |op, imm| out.push(Instr { pc, op, imm });
|
||||
|
||||
if let Some((i, writes, leaves)) = var_ref_access(&instr) {
|
||||
match (targets[i], writes) {
|
||||
(Some(target), false) => read(target, &mut f.cpool, &mut push),
|
||||
(Some(Target::Global(global)), true) => {
|
||||
if leaves {
|
||||
push(Dup, Imm::None);
|
||||
}
|
||||
|
||||
push(PutVar, Imm::Global(global as u32));
|
||||
}
|
||||
(Some(Target::Native(_)), true) => panic!("a native cannot be assigned to"),
|
||||
(None, _) => {
|
||||
let op = match (writes, leaves) {
|
||||
(false, _) => GetVarRef,
|
||||
(true, false) => PutVarRef,
|
||||
(true, true) => SetVarRef,
|
||||
};
|
||||
push(op, Imm::VarRef(renumber[i].unwrap() as u16));
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
match (instr.op, instr.imm) {
|
||||
(GetVar | GetVarUndef, Imm::Atom(name)) => {
|
||||
let text = name.text(atoms);
|
||||
|
||||
match (link.global(&text), instr.op) {
|
||||
(Some(target), _) => read(target, &mut f.cpool, &mut push),
|
||||
(None, GetVarUndef) => push(Undefined, Imm::None),
|
||||
(None, _) => {
|
||||
panic!("{module}: `{text}` is not declared, and is not a global name")
|
||||
}
|
||||
}
|
||||
}
|
||||
(PutVar | PutVarInit, Imm::Atom(name)) => {
|
||||
panic!(
|
||||
"{module}: `{}` is a global name, which cannot be assigned to",
|
||||
name.text(atoms)
|
||||
)
|
||||
}
|
||||
_ => out.push(instr),
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// A read of what a module binding refers to: a global, or a native as a constant
|
||||
fn read(target: Target, cpool: &mut Vec<JsValue>, push: &mut impl FnMut(OpCode, Imm)) {
|
||||
match target {
|
||||
Target::Global(global) => push(OpCode::GetVar, Imm::Global(global as u32)),
|
||||
Target::Native(idx) => {
|
||||
cpool.push(JsValue::NativeFn(idx));
|
||||
push(OpCode::PushConst, Imm::Const(cpool.len() as u32 - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user