Skip to content

Improved TS type generation from WASM #4229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Nov 14, 2024
149 changes: 73 additions & 76 deletions crates/cli-support/src/wasm2es6js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,97 +62,94 @@ fn args_are_optional(name: &str) -> bool {

pub fn interface(module: &Module) -> Result<String, Error> {
let mut exports = String::new();
module_export_types(module, |name, ty| {
writeln!(exports, " readonly {}: {};", name, ty).unwrap();
});
Ok(exports)
}

pub fn typescript(module: &Module) -> Result<String, Error> {
let mut exports = "/* tslint:disable */\n/* eslint-disable */\n".to_string();
module_export_types(module, |name, ty| {
writeln!(exports, "export const {}: {};", name, ty).unwrap();
});
Ok(exports)
}

/// Iterates over all the exports in a module and generates TypeScript types. All
/// name-type pairs are passed to the `export` function.
fn module_export_types(module: &Module, mut export: impl FnMut(&str, &str)) {
for entry in module.exports.iter() {
let id = match entry.item {
walrus::ExportItem::Function(i) => i,
walrus::ExportItem::Memory(_) => {
exports.push_str(&format!(" readonly {}: WebAssembly.Memory;\n", entry.name,));
continue;
}
walrus::ExportItem::Table(_) => {
exports.push_str(&format!(" readonly {}: WebAssembly.Table;\n", entry.name,));
continue;
match entry.item {
walrus::ExportItem::Function(id) => {
let func = module.funcs.get(id);
let ty = module.types.get(func.ty());
let ts_type = function_type_to_ts(ty, args_are_optional(&entry.name));
export(&entry.name, &ts_type);
}
walrus::ExportItem::Memory(_) => export(&entry.name, "WebAssembly.Memory"),
walrus::ExportItem::Table(_) => export(&entry.name, "WebAssembly.Table"),
walrus::ExportItem::Global(_) => continue,
};
}
}
fn val_type_to_ts(ty: walrus::ValType) -> &'static str {
// see https://webassembly.github.io/spec/js-api/index.html#towebassemblyvalue
// and https://webassembly.github.io/spec/js-api/index.html#tojsvalue
match ty {
walrus::ValType::I32 | walrus::ValType::F32 | walrus::ValType::F64 => "number",
walrus::ValType::I64 => "bigint",
// there could be anything behind a reference
walrus::ValType::Ref(_) => "any",
// V128 currently isn't supported in JS and therefore doesn't have a
// specific type in the spec. When it does get support, this type will
// still be technically correct, but should be updated to something more
// specific.
walrus::ValType::V128 => "any",
}
}
fn function_type_to_ts(function: &walrus::Type, all_args_optional: bool) -> String {
let mut out = String::new();

let func = module.funcs.get(id);
let ty = module.types.get(func.ty());
let mut args = String::new();
for (i, _) in ty.params().iter().enumerate() {
if i > 0 {
args.push_str(", ");
}

push_index_identifier(i, &mut args);
if args_are_optional(&entry.name) {
args.push('?');
}
args.push_str(": number");
// parameters
out.push('(');
for (i, arg_type) in function.params().iter().enumerate() {
if i > 0 {
out.push_str(", ");
}

exports.push_str(&format!(
" readonly {name}: ({args}) => {ret};\n",
name = entry.name,
args = args,
ret = match ty.results().len() {
0 => "void",
1 => "number",
_ => "number[]",
},
));
push_index_identifier(i, &mut out);
if all_args_optional {
out.push('?');
}
out.push_str(": ");
out.push_str(val_type_to_ts(*arg_type));
}
out.push(')');

Ok(exports)
}

pub fn typescript(module: &Module) -> Result<String, Error> {
let mut exports = "/* tslint:disable */\n/* eslint-disable */\n".to_string();
for entry in module.exports.iter() {
let id = match entry.item {
walrus::ExportItem::Function(i) => i,
walrus::ExportItem::Memory(_) => {
exports.push_str(&format!(
"export const {}: WebAssembly.Memory;\n",
entry.name,
));
continue;
}
walrus::ExportItem::Table(_) => {
exports.push_str(&format!(
"export const {}: WebAssembly.Table;\n",
entry.name,
));
continue;
}
walrus::ExportItem::Global(_) => continue,
};
// arrow
out.push_str(" => ");

let func = module.funcs.get(id);
let ty = module.types.get(func.ty());
let mut args = String::new();
for (i, _) in ty.params().iter().enumerate() {
if i > 0 {
args.push_str(", ");
// results
let results = function.results();
// this match follows the spec:
// https://webassembly.github.io/spec/js-api/index.html#exported-function-exotic-objects
match results.len() {
0 => out.push_str("void"),
1 => out.push_str(val_type_to_ts(results[0])),
_ => {
out.push('[');
for (i, result) in results.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(val_type_to_ts(*result));
}
push_index_identifier(i, &mut args);
args.push_str(": number");
out.push(']');
}

exports.push_str(&format!(
"export function {name}({args}): {ret};\n",
name = entry.name,
args = args,
ret = match ty.results().len() {
0 => "void",
1 => "number",
_ => "number[]",
},
));
}

Ok(exports)
out
}

impl Output {
Expand Down
38 changes: 38 additions & 0 deletions crates/cli/tests/reference/wasm-export-types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* tslint:disable */
/* eslint-disable */
export function example(a: number, b: bigint, c: any, d: string): string;
export function example_128(a: bigint): bigint | undefined;

export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;

export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly example: (a: number, b: bigint, c: any, d: number, e: number) => [number, number];
readonly example_128: (a: bigint, b: bigint) => [number, bigint, bigint];
readonly __wbindgen_export_0: WebAssembly.Table;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_start: () => void;
}

export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;

/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
Loading