Skip to content

BridgeJS: normalize keyword-escaped names in generated code - #797

Open
kateinoigakukun wants to merge 1 commit into
mainfrom
yt/bjs-keyword-escapes
Open

BridgeJS: normalize keyword-escaped names in generated code#797
kateinoigakukun wants to merge 1 commit into
mainfrom
yt/bjs-keyword-escapes

Conversation

@kateinoigakukun

Copy link
Copy Markdown
Member

Summary

@JS func default() {} produced bjs_default`` as the WebAssembly export symbol — an invalid @_expose/`@_cdecl` name — because the BridgeJSTool codegenerator read `TokenSyntax.text` (which keeps backticks for keyword-escaped identifiers) and never normalized them. The backticks leaked into ABI names, generated Swift identifiers, and JS/d.ts names.

The macro plugin (@JSFunction/@JSGetter/@JSSetter) was already correct via stripBackticks; only the codegenerator needed fixing.

Root cause

TokenSyntax.text for `default` returns the string `default` including the backticks. The export-side collector in SwiftToSkeleton.swift extracted names with raw .text and stored them with backticks intact, so they flowed unchanged into:

  • ABI/wasm export names → bjs_default``
  • generated function names → _bjs_`default`
  • JS/d.ts export names → `default`

Fix

Split name escaping into three context-aware helpers (in ImportTS.swift), grounded in swift-syntax's grammar (SwiftParser.IsValidIdentifier / isValidSwiftIdentifier(for:)):

Position Helper Bare valid?
Declaration (var/let/property/let-binding) backtickIfNeeded() non-keywords only — var self: Int is invalid
Member access (.name) backtickIfNeededForMemberAccess() all except selfobj.self is the identity expr, not a member access
Local reference (body) backtickIfNeededForLocalReference() self + non-keywords — self refers to the parameter
Parameter declaration (_ name: Type) (none — stays bare) all keywords except inout

All helpers are idempotent (strip existing backticks first → no double-wrapping) and escape dotted paths per-component. Names are normalized (backticks stripped) at extraction in SwiftToSkeleton so ABI/JSON/JS names are clean, then re-escaped with the correct context helper at every Swift emission site.

The non-obvious finding: obj.self compiles but returns obj (identity expression), silently accessing the wrong thing — so self is the only keyword that must be escaped in member access, while class/where/in/case/etc. are valid bare there.

Tests

  • IdentifierEscapingTests.swift (12 tests): pins each context with explicit keyword lists, plus a parametric test asserting the helpers agree with isValidSwiftIdentifier(for:) for 23 names — so the rules track swift-syntax's grammar rather than a hand-maintained keyword list. Covers idempotency, double-wrap avoidance, and dotted-path per-component escaping.
  • KeywordNames.swift codegen input + snapshots (.swift/.json/.js/.d.ts) covering all positions with keywords: default, Parser (in/self/class/where), Record (case/continue/as), Token (break/return/switch), Observer (do/for/if/else). The generated Swift snapshot parses cleanly under swiftc -parse.
  • Macro tests for keyword names under @JSFunction/@JSGetter/@JSSetter.

200 tests pass; no existing snapshots changed.

`@JS func `default`() {}` produced `bjs_`default`` as the WebAssembly
export symbol — an invalid `@_expose`/`@_cdecl` name — because the
codegenerator read `TokenSyntax.text` (which keeps backticks for
keyword-escaped identifiers) and never normalized it. The backticks
leaked into ABI names, generated Swift identifiers, and JS/d.ts names.

The fix splits name escaping into three context-aware helpers, grounded
in swift-syntax's grammar (`isValidSwiftIdentifier(for:)`):

- `backtickIfNeeded()` — declaration position (`var`/`let`/property/
  `let`-binding). Keywords are not valid bare here; escapes all
  keywords including `self` (`var self: Int` is invalid).
- `backtickIfNeededForMemberAccess()` — `.name` position. Most keywords
  are valid bare (`obj.class`, `.break`); `self` is the exception
  because `obj.self` is the identity expression, not a member access,
  so a property named `self` must be written as `` obj.`self` ``.
- `backtickIfNeededForLocalReference()` — body references. `self` is
  valid bare (refers to the parameter); other keywords still need
  escaping. Parameter *declarations* (`_ name: Type`) stay bare for
  every keyword except `inout`.

All helpers are idempotent (no double-wrapping) and escape dotted
paths per-component. Names are normalized (backticks stripped) at
extraction in `SwiftToSkeleton` so ABI/JSON/JS names are clean.

Adds `IdentifierEscapingTests` pinning the rules against
`isValidSwiftIdentifier(for:)`, a `KeywordNames` codegen snapshot
covering all positions, and macro tests for keyword names under
`@JSFunction`/`@JSGetter`/`@JSSetter`. 200 tests pass; no existing
snapshots changed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant