use crate::esm_bindgen::EsmBindgen;
use crate::files::Files;
use crate::function_bindgen::{ErrHandling, FunctionBindgen};
use crate::intrinsics::{render_intrinsics, Intrinsic};
use crate::names::{maybe_quote_id, maybe_quote_member, LocalNames};
use crate::source;
use crate::{uwrite, uwriteln};
use base64::{engine::general_purpose, Engine as _};
use heck::*;
use indexmap::IndexMap;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt::Write;
use std::mem;
use wasmtime_environ::component::{
CanonicalOptions, Component, CoreDef, CoreExport, Export, ExportItem, GlobalInitializer,
InstantiateModule, LowerImport, RuntimeInstanceIndex, StaticModuleIndex, Transcoder,
};
use wasmtime_environ::{EntityIndex, ModuleTranslation, PrimaryMap};
use wit_parser::abi::{AbiVariant, LiftLower};
use wit_parser::*;
#[derive(Default, Clone)]
pub struct TranspileOpts {
pub name: String,
pub no_typescript: bool,
pub instantiation: bool,
pub map: Option<HashMap<String, String>>,
pub no_nodejs_compat: bool,
pub base64_cutoff: usize,
pub tla_compat: bool,
pub valid_lifting_optimization: bool,
}
struct JsBindgen<'a> {
local_names: LocalNames,
esm_bindgen: EsmBindgen,
src: Source,
core_module_cnt: usize,
opts: &'a TranspileOpts,
all_intrinsics: BTreeSet<Intrinsic>,
}
pub fn transpile_bindgen(
name: &str,
component: &Component,
modules: &PrimaryMap<StaticModuleIndex, ModuleTranslation<'_>>,
resolve: &Resolve,
id: WorldId,
opts: TranspileOpts,
files: &mut Files,
) -> (Vec<String>, Vec<(String, Export)>) {
let mut bindgen = JsBindgen {
local_names: LocalNames::default(),
src: Source::default(),
esm_bindgen: EsmBindgen::default(),
core_module_cnt: 0,
opts: &opts,
all_intrinsics: BTreeSet::new(),
};
bindgen
.local_names
.exclude_intrinsics(Intrinsic::get_all_names());
bindgen.core_module_cnt = modules.len();
let mut imports = BTreeMap::new();
let mut exports = BTreeMap::new();
for (key, _) in &resolve.worlds[id].imports {
let name = resolve.name_world_key(key);
imports.insert(name, key.clone());
}
for (key, _) in &resolve.worlds[id].exports {
let name = resolve.name_world_key(key);
exports.insert(name, key.clone());
}
let mut instantiator = Instantiator {
src: Source::default(),
sizes: SizeAlign::default(),
gen: &mut bindgen,
modules,
instances: Default::default(),
resolve,
world: id,
component,
imports,
exports,
};
instantiator.sizes.fill(resolve);
instantiator.instantiate();
instantiator.gen.src.js(&instantiator.src.js);
instantiator.gen.src.js_init(&instantiator.src.js_init);
instantiator.gen.finish_component(name, files);
let exports = instantiator
.gen
.esm_bindgen
.exports()
.iter()
.map(|(export_name, canon_export_name)| {
let export = if canon_export_name.contains(':') {
&instantiator.component.exports[*canon_export_name]
} else {
&instantiator.component.exports[&canon_export_name.to_kebab_case()]
};
(export_name.to_string(), export.clone())
})
.collect();
(bindgen.esm_bindgen.import_specifiers(), exports)
}
impl<'a> JsBindgen<'a> {
fn finish_component(&mut self, name: &str, files: &mut Files) {
let mut output = source::Source::default();
let mut compilation_promises = source::Source::default();
let mut removed = BTreeSet::new();
for i in 0..self.core_module_cnt {
let local_name = format!("module{}", i);
let mut name_idx = core_file_name(name, i as u32);
if self.opts.instantiation {
uwriteln!(
compilation_promises,
"const {local_name} = compileCore('{name_idx}');"
);
} else {
if files.get_size(&name_idx).unwrap() < self.opts.base64_cutoff {
assert!(removed.insert(i));
let data = files.remove(&name_idx).unwrap();
uwriteln!(
compilation_promises,
"const {local_name} = {}('{}');",
self.intrinsic(Intrinsic::Base64Compile),
general_purpose::STANDARD_NO_PAD.encode(&data),
);
} else {
if let Some(&replacement) = removed.iter().next() {
assert!(removed.remove(&replacement) && removed.insert(i));
let data = files.remove(&name_idx).unwrap();
name_idx = core_file_name(name, replacement as u32);
files.push(&name_idx, &data);
}
uwriteln!(
compilation_promises,
"const {local_name} = {}(new URL('./{name_idx}', import.meta.url));",
self.intrinsic(Intrinsic::FetchCompile)
);
}
}
}
let js_intrinsics = render_intrinsics(
&mut self.all_intrinsics,
self.opts.no_nodejs_compat,
self.opts.instantiation,
);
if self.opts.instantiation {
uwrite!(
output,
"\
{}
export async function instantiate(compileCore, imports, instantiateCore = WebAssembly.instantiate) {{
{}
",
&js_intrinsics as &str,
&compilation_promises as &str,
);
}
let imports_object = if self.opts.instantiation {
Some("imports")
} else {
None
};
self.esm_bindgen
.render_imports(&mut output, imports_object, &mut self.local_names);
if self.opts.instantiation {
self.esm_bindgen.render_exports(
&mut self.src.js,
self.opts.instantiation,
&mut self.local_names,
);
uwrite!(
output,
"\
{}\
{};
}}
",
&self.src.js_init as &str,
&self.src.js as &str,
);
} else {
let (maybe_init_export, maybe_init) = if self.opts.tla_compat {
uwriteln!(self.src.js_init, "_initialized = true;");
(
"\
let _initialized = false;
export ",
"",
)
} else {
(
"",
"
await $init;
",
)
};
uwrite!(
output,
"\
{}
{}
{maybe_init_export}const $init = (async() => {{
{}\
{}\
}})();
{maybe_init}\
",
&js_intrinsics as &str,
&self.src.js as &str,
&compilation_promises as &str,
&self.src.js_init as &str,
);
self.esm_bindgen.render_exports(
&mut output,
self.opts.instantiation,
&mut self.local_names,
);
}
let mut bytes = output.as_bytes();
if bytes[0] == b'\n' {
bytes = &bytes[1..];
}
files.push(&format!("{name}.js"), bytes);
}
fn intrinsic(&mut self, intrinsic: Intrinsic) -> String {
self.all_intrinsics.insert(intrinsic);
return intrinsic.name().to_string();
}
}
struct Instantiator<'a, 'b> {
src: Source,
gen: &'a mut JsBindgen<'b>,
modules: &'a PrimaryMap<StaticModuleIndex, ModuleTranslation<'a>>,
instances: PrimaryMap<RuntimeInstanceIndex, StaticModuleIndex>,
resolve: &'a Resolve,
world: WorldId,
sizes: SizeAlign,
component: &'a Component,
exports: BTreeMap<String, WorldKey>,
imports: BTreeMap<String, WorldKey>,
}
impl Instantiator<'_, '_> {
fn instantiate(&mut self) {
if self.modules.len() > 1 {
self.src.js_init.push_str("Promise.all([");
for i in 0..self.modules.len() {
if i > 0 {
self.src.js_init.push_str(", ");
}
self.src.js_init.push_str(&format!("module{}", i));
}
uwriteln!(self.src.js_init, "]).catch(() => {{}});");
}
for init in self.component.initializers.iter() {
self.instantiation_global_initializer(init);
}
if self.gen.opts.instantiation {
let js_init = mem::take(&mut self.src.js_init);
self.src.js.push_str(&js_init);
}
self.exports(&self.component.exports)
}
fn instantiation_global_initializer(&mut self, init: &GlobalInitializer) {
match init {
GlobalInitializer::InstantiateModule(m) => match m {
InstantiateModule::Static(idx, args) => self.instantiate_static_module(*idx, args),
InstantiateModule::Import(..) => unimplemented!(),
},
GlobalInitializer::LowerImport(i) => {
self.lower_import(i);
}
GlobalInitializer::ExtractMemory(m) => {
let def = self.core_export(&m.export);
let idx = m.index.as_u32();
uwriteln!(self.src.js, "let memory{idx};");
uwriteln!(self.src.js_init, "memory{idx} = {def};");
}
GlobalInitializer::ExtractRealloc(r) => {
let def = self.core_def(&r.def);
let idx = r.index.as_u32();
uwriteln!(self.src.js, "let realloc{idx};");
uwriteln!(self.src.js_init, "realloc{idx} = {def};",);
}
GlobalInitializer::ExtractPostReturn(p) => {
let def = self.core_def(&p.def);
let idx = p.index.as_u32();
uwriteln!(self.src.js, "let postReturn{idx};");
uwriteln!(self.src.js_init, "postReturn{idx} = {def};");
}
GlobalInitializer::AlwaysTrap(_) => unimplemented!(),
GlobalInitializer::SaveStaticModule(_) => unimplemented!(),
GlobalInitializer::SaveModuleImport(_) => unimplemented!(),
GlobalInitializer::Transcoder(Transcoder {
index: _,
op: _,
from: _,
from64: _,
to: _,
to64: _,
signature: _,
}) => unimplemented!(),
}
}
fn instantiate_static_module(&mut self, idx: StaticModuleIndex, args: &[CoreDef]) {
let module = &self.modules[idx].module;
let mut import_obj = BTreeMap::new();
assert_eq!(module.imports().len(), args.len());
for ((module, name, _), arg) in module.imports().zip(args) {
let def = self.core_def(arg);
let dst = import_obj.entry(module).or_insert(BTreeMap::new());
let prev = dst.insert(name, def);
assert!(prev.is_none());
}
let mut imports = String::new();
if !import_obj.is_empty() {
imports.push_str(", {\n");
for (module, names) in import_obj {
imports.push_str(&maybe_quote_id(module));
imports.push_str(": {\n");
for (name, val) in names {
imports.push_str(&maybe_quote_id(name));
uwriteln!(imports, ": {val},");
}
imports.push_str("},\n");
}
imports.push_str("}");
}
let i = self.instances.push(idx);
let iu32 = i.as_u32();
let instantiate = self.gen.intrinsic(Intrinsic::InstantiateCore);
uwriteln!(self.src.js, "let exports{iu32};");
uwriteln!(
self.src.js_init,
"({{ exports: exports{iu32} }} = await {instantiate}(await module{}{imports}));",
idx.as_u32()
);
}
fn lower_import(&mut self, import: &LowerImport) {
let (import_index, path) = &self.component.imports[import.import];
let (import_name, _) = &self.component.import_types[*import_index];
let world_key = &self.imports[import_name];
let (import_specifier, mut maybe_iface_member) =
map_import(&self.gen.opts.map, &import_name);
let (func, func_name, iface_name) =
match &self.resolve.worlds[self.world].imports[world_key] {
WorldItem::Function(func) => {
assert_eq!(path.len(), 0);
(func, import_name, None)
}
WorldItem::Interface(i) => {
assert_eq!(path.len(), 1);
let iface = &self.resolve.interfaces[*i];
let func = &iface.functions[&path[0]];
(
func,
&path[0],
Some(iface.name.as_ref().unwrap_or_else(|| import_name)),
)
}
WorldItem::Type(_) => unreachable!(),
};
let index = import.index.as_u32();
let callee_name = self
.gen
.local_names
.get_or_create(&format!("import:{}-{}", import_name, func_name), func_name)
.to_string();
uwrite!(self.src.js, "\nfunction lowering{index}");
let nparams = self
.resolve
.wasm_signature(AbiVariant::GuestImport, func)
.params
.len();
self.bindgen(
nparams,
&callee_name,
&import.options,
func,
AbiVariant::GuestImport,
);
uwriteln!(self.src.js, "");
if let Some(_iface_name) = iface_name {
if let Some(iface_member) = maybe_iface_member.take() {
self.gen.esm_bindgen.add_import_binding(
&[
import_specifier,
iface_member.to_lower_camel_case(),
func_name.to_lower_camel_case(),
],
callee_name,
);
} else {
self.gen.esm_bindgen.add_import_binding(
&[import_specifier, func_name.to_lower_camel_case()],
callee_name,
);
}
} else {
self.gen
.esm_bindgen
.add_import_binding(&[import_specifier], callee_name);
}
}
fn bindgen(
&mut self,
nparams: usize,
callee: &str,
opts: &CanonicalOptions,
func: &Function,
abi: AbiVariant,
) {
let memory = match opts.memory {
Some(idx) => Some(format!("memory{}", idx.as_u32())),
None => None,
};
let realloc = match opts.realloc {
Some(idx) => Some(format!("realloc{}", idx.as_u32())),
None => None,
};
let post_return = match opts.post_return {
Some(idx) => Some(format!("postReturn{}", idx.as_u32())),
None => None,
};
self.src.js("(");
let mut params = Vec::new();
for i in 0..nparams {
if i > 0 {
self.src.js(", ");
}
let param = format!("arg{i}");
self.src.js(¶m);
params.push(param);
}
uwriteln!(self.src.js, ") {{");
if self.gen.opts.tla_compat && matches!(abi, AbiVariant::GuestExport) {
let throw_uninitialized = self.gen.intrinsic(Intrinsic::ThrowUninitialized);
uwrite!(
self.src.js,
"\
if (!_initialized) {throw_uninitialized}();
"
);
}
let mut f = FunctionBindgen {
intrinsics: &mut self.gen.all_intrinsics,
valid_lifting_optimization: self.gen.opts.valid_lifting_optimization,
sizes: &self.sizes,
err: if func.results.throws(self.resolve).is_some() {
match abi {
AbiVariant::GuestExport => ErrHandling::ThrowResultErr,
AbiVariant::GuestImport => ErrHandling::ResultCatchHandler,
}
} else {
ErrHandling::None
},
block_storage: Vec::new(),
blocks: Vec::new(),
callee,
memory: memory.as_ref(),
realloc: realloc.as_ref(),
tmp: 0,
params,
post_return: post_return.as_ref(),
encoding: opts.string_encoding,
src: source::Source::default(),
};
self.resolve.call(
abi,
match abi {
AbiVariant::GuestImport => LiftLower::LiftArgsLowerResults,
AbiVariant::GuestExport => LiftLower::LowerArgsLiftResults,
},
func,
&mut f,
);
self.src.js(&f.src);
self.src.js("}");
}
fn core_def(&self, def: &CoreDef) -> String {
match def {
CoreDef::Export(e) => self.core_export(e),
CoreDef::Lowered(i) => format!("lowering{}", i.as_u32()),
CoreDef::AlwaysTrap(_) => unimplemented!(),
CoreDef::InstanceFlags(_) => unimplemented!(),
CoreDef::Transcoder(_) => unimplemented!(),
}
}
fn core_export<T>(&self, export: &CoreExport<T>) -> String
where
T: Into<EntityIndex> + Copy,
{
let name = match &export.item {
ExportItem::Index(idx) => {
let module = &self.modules[self.instances[export.instance]].module;
let idx = (*idx).into();
module
.exports
.iter()
.filter_map(|(name, i)| if *i == idx { Some(name) } else { None })
.next()
.unwrap()
}
ExportItem::Name(s) => s,
};
let i = export.instance.as_u32() as usize;
format!("exports{i}{}", maybe_quote_member(name))
}
fn exports(&mut self, exports: &IndexMap<String, Export>) {
for (export_name, export) in exports.iter() {
let world_key = &self.exports[export_name];
let item = &self.resolve.worlds[self.world].exports[world_key];
match export {
Export::LiftedFunction {
ty: _,
func,
options,
} => {
let local_name = self.gen.local_names.create_once(export_name).to_string();
self.export_bindgen(
&local_name,
func,
options,
match item {
WorldItem::Function(f) => f,
WorldItem::Interface(_) | WorldItem::Type(_) => unreachable!(),
},
);
self.gen.esm_bindgen.add_export_binding(
None,
local_name,
export_name.to_lower_camel_case(),
);
}
Export::Instance(iface) => {
let id = match item {
WorldItem::Interface(id) => *id,
WorldItem::Function(_) | WorldItem::Type(_) => unreachable!(),
};
for (func_name, export) in iface {
let (func, options) = match export {
Export::LiftedFunction { func, options, .. } => (func, options),
Export::Type(_) => continue, _ => unreachable!(),
};
let local_func_name =
self.gen.local_names.create_once(func_name).to_string();
self.export_bindgen(
&local_func_name,
func,
options,
&self.resolve.interfaces[id].functions[func_name],
);
self.gen.esm_bindgen.add_export_binding(
Some(export_name),
local_func_name,
func_name.to_lower_camel_case(),
);
}
}
Export::Type(_) => {}
Export::Module(_) => unimplemented!(),
}
}
self.gen.esm_bindgen.populate_export_aliases();
}
fn export_bindgen(
&mut self,
name: &str,
def: &CoreDef,
options: &CanonicalOptions,
func: &Function,
) {
uwrite!(self.src.js, "\nfunction {name}");
let callee = self.core_def(def);
self.bindgen(
func.params.len(),
&callee,
options,
func,
AbiVariant::GuestExport,
);
self.src.js("\n");
}
}
#[derive(Default)]
pub struct Source {
pub js: source::Source,
pub js_init: source::Source,
}
impl Source {
pub fn js(&mut self, s: &str) {
self.js.push_str(s);
}
pub fn js_init(&mut self, s: &str) {
self.js_init.push_str(s);
}
}
fn map_import(map: &Option<HashMap<String, String>>, impt: &str) -> (String, Option<String>) {
if let Some(map) = map.as_ref() {
if let Some(mapping) = map.get(impt) {
return if let Some(hash_idx) = mapping.find('#') {
(
mapping[0..hash_idx].to_string(),
Some(mapping[hash_idx + 1..].into()),
)
} else {
(mapping.into(), None)
};
}
for (key, mapping) in map {
if let Some(wildcard_idx) = key.find('*') {
let lhs = &key[0..wildcard_idx];
let rhs = &key[wildcard_idx + 1..];
if impt.starts_with(lhs) && impt.ends_with(rhs) {
let matched =
&impt[wildcard_idx..wildcard_idx + impt.len() - lhs.len() - rhs.len()];
let mapping = mapping.replace('*', matched);
return if let Some(hash_idx) = mapping.find('#') {
(
mapping[0..hash_idx].to_string(),
Some(mapping[hash_idx + 1..].into()),
)
} else {
(mapping.into(), None)
};
}
}
}
}
(impt.to_string(), None)
}
pub fn parse_world_key<'a>(name: &'a str) -> Option<(&'a str, &'a str, &'a str)> {
let registry_idx = match name.find(':') {
Some(idx) => idx,
None => return None,
};
let ns = &name[0..registry_idx];
match name.rfind('/') {
Some(sep_idx) => Some((ns, &name[registry_idx + 1..sep_idx], &name[sep_idx + 1..])),
None => Some((ns, &name[registry_idx + 1..], "".as_ref())),
}
}
fn core_file_name(name: &str, idx: u32) -> String {
let i_str = if idx == 0 {
String::from("")
} else {
(idx + 1).to_string()
};
format!("{}.core{i_str}.wasm", name)
}