Hi — looking for a sanity check before filing a GitHub issue.
With jco transpile --instantiation async, importing a WIT function named
reject (also resolve, and likely promise / gen / runNext /
maybeSyncReturn) produces JS that fails to load:
SyntaxError: Identifier 'reject' has already been declared
reject / resolve are legal WIT identifiers (not WIT keywords, not JS
keywords). The collision is with locals that the instantiation helper emits
in the same function scope.
package demo:jco;
interface respondent {
reject: func(id: u32);
}
world demo {
import respondent;
export run: func(id: u32);
}
Generated instantiate() contains, in one scope:
const { reject } = imports['demo:jco/respondent'];
// ...
let promise, resolve, reject; // instantiation helper
Either the import or the helper locals are deconflicted (e.g. reject$1), same as other name clashes — module loads cleanly.
Imports go through LocalNames in render_imports, but the helper is a raw string in transpile_bindgen.rs (let promise, resolve, reject; plus gen / runNext / maybeSyncReturn) and those names are never registered via exclude_globals / create_once. So the deconflicter never sees them.
Specific to --instantiation mode (default ESM puts the helper in a $init IIFE, so this is shadowing rather than redeclaration). Still present on bindgen from main last we checked, and on @bytecodealliance/jco@1.10.2.
Related precedent: #1898 / #1899 (exported eval not sanitized) — same “bindings must produce valid JS for any legal WIT name” principle.
No wasm toolchain needed:
npm i @bytecodealliance/jco@1.10.2 # also fails on newer in our checks up to 1.34.0
# demo.wit as above
jco transpile demo.wit --stub --instantiation async --no-typescript -o out
node -e "import('./out/demo.js').then(() => console.log('OK'), e => console.log('FAIL:', e.message))"
# FAIL: Identifier 'reject' has already been declared
Control: rename reject → anything else → OK.
Hey @Mike M thanks for the report -- yeah that is definitely worth filing an issue for! I think basically the list that needs to be updated is here, but definitely this is worth filing an issue for and having the repro will be great to add to Jco.
https://github.com/bytecodealliance/jco/issues/2134
Hey @Mike M Thanks for filing the issue! Do you want to take a stab at it? If not I'm happy to get to it (hopefully today/by monday!)
yeah, sure! I wouldn't have presumed, but I'm happy to. Gimme a few.
One thought I'd like your input on: since this isn't a forbidden JS keyword, it's bindgen’s own helper locals that weren’t reserved. So what do you think about either:
1) Extend global_names (or I could create a sibling call when instantiation/$init is emitted) with the helper locals:
promise, resolve, reject, gen, runNext, maybeSyncReturn, normalizeInstantiationError
Then an imported reject becomes reject$1 only when it actually collides — which is how LocalNames seem to work?
or...
2) perhaps even better (for a future where this chunk of helper code is modified again), leverage LocalNames directly and wrap those keyword in local_names.create_once(...), which seems to be the precedent for preventing collisions in the bindings, e.g.,
let promise = local_names.create_once("promise");
let resolve = local_names.create_once("resolve");
let reject = local_names.create_once("reject");
let run_next = local_names.create_once("runNext");
// ...
uwrite!(output, "
let {promise}, {resolve}, {reject};
function {run_next} (value) {{ ... }}
...
", ...);
This has the benefit of not adding the name to global_names once, only to have another name pop up later that also needs to be added to global_names and leads to this same bug.
Thoughts?
Here's the PR if you want to look at the impl I suggest above. Happy to tweak/pivot as needed. Pls advise when you have a moment.
https://github.com/bytecodealliance/jco/pull/2135
Last updated: Sep 20 2026 at 18:08 UTC