Skip to content

BridgeJS: Same-named @JS types across modules collide silently in generated glue #785

Description

@krodak

Overview

Since #730, @JS types defined in one module can be referenced from @JS APIs in other modules within the same package. However, type identity in the skeleton and in the generated glue is the unqualified type name — abiName carries the namespace but not the module. When two linked modules define @JS types with the same name (e.g. @JS struct Point in both ModuleA and ModuleB), several generated-name surfaces collide:

Stack ABI intrinsics (silent data corruption): Both modules' generated Swift declares

@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_Point")

WebAssembly imports with the same module + name unify, and the linker assigns bjs["swift_js_struct_lower_Point"] twice — last one wins. Both modules end up calling the same handler, so one module's structs are encoded/decoded with the other's field layout. No diagnostic at build time or runtime.

JS helper tables: structHelpers / enum helper factories are keyed by unqualified name inside createInstantiator — same last-wins behavior.

Exposed thunks: @_expose(wasm, "bjs_Point_init") (and method/deinit thunks) are emitted by both modules, producing duplicate export names.

TypeScript declarations: two interface Point declarations in one bridge-js.d.ts — TypeScript declaration-merges them, which is silent when the field sets are compatible and produces confusing errors when they are not.

Reference resolution: independent of naming, a skeleton reference like .swiftStruct("Point") is unqualified, so the linker resolves it by name-matching across all skeletons — with two candidates the choice is arbitrary. Renaming the ABI surfaces alone is not sufficient; the reference side needs the defining module too.

Public JS declarations (fails loudly, related): even when the colliding types live under different @JS(namespace:) paths — where no public-surface conflict exists — the generated JS still declares classes and enum value objects with flat top-level identifiers (class Point { … }, const PointValues = …) before wiring them into their namespace objects, and bridge-js.d.ts emits the corresponding declarations unscoped. Two same-named types in different namespaces therefore produce duplicate declarations in one scope: a load-time SyntaxError on the JS side and duplicate-identifier errors in the .d.ts. Unlike the ABI surfaces above this fails loudly rather than corrupting data, but it makes same-named types unusable across modules even when their public paths don't overlap.

Possible direction

  • Make type identity in the skeleton module-aware (defining module + name). BridgeJS: Use @JS types from other modules in the same package #730 already established that the skeleton format does not need to stay backward compatible.
  • Module-qualify the internal ABI names (swift_js_struct_lower_<Module>_<Name>, bjs_<Module>_…). Newer name families already follow this pattern — closure invokes (invoke_js_callback_<module>_<mangleName>) and promise settlement helpers (promise_resolve_<module>_<mangled>) — it is only the older core names that predate it.
  • Keep the public JS access paths flat (exports.Point, namespace objects, TS types) and emit a build-time linker diagnostic when two modules expose the identical public name at the same namespace path, mirroring how BridgeJS: Use @JS types from other modules in the same package #730 handles Swift-side ambiguity with a diagnostic that asks for explicit qualification.
  • For the declaration surface: declare classes and enum value objects under unique internal identifiers (the module-qualified ABI name is a natural choice), bind public names only through namespace objects / exports, and scope .d.ts declarations for namespaced types inside declare namespace blocks. Public access paths stay exactly as today; only the internal declaration identifiers change, so same-named types in different namespaces become fully usable.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions