Skip to content

fix(globals): legacy escape()/unescape() + Node global alias (#4511) - #4527

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-escape-unescape-4511
Jun 5, 2026
Merged

proggeramlug merged 1 commit into
mainfrom
worktree-fix-escape-unescape-4511

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Closes #4511.

Problem

  • escape() / unescape() (ES Annex B B.2.1, still standard and present in Node) were undefined — bare calls threw ReferenceError. qs (qs/lib/utils.js) uses them for %uXXXX decoding, so any app pulling in qs (e.g. via stripe) hit this.
  • The Node global object was likewise unresolved — (global as any).x emitted Warning: unknown identifier 'global'. (globalThis already worked.)

Approach

Both are routed through Perry's existing reflective globalThis function-value path — no dedicated HIR Expr variant (lighter than the ~20-file encodeURI-style approach; correctness, not a fast path, is what's needed here).

  • runtime (builtins/globals.rs): js_escape / js_unescape implement Annex B encoding over UTF-16 code units — code units <256 → %XX, else %uXXXX; astral scalars emit their surrogate pair (matching Node). unescape reassembles UTF-16 units before decoding so %uD835%uDFD8-style pairs recombine.
  • runtime (global_this*.rs): register escape/unescape thunks in GLOBAL_THIS_BUILTIN_FUNCTIONS; install a self-referential global property on the globalThis singleton (global === globalThis).
  • HIR (analysis/builtins.rs): add escape/unescape/global to is_builtin_global_value_name so bare calls and value reads resolve to globalThis.<name> instead of throwing (also makes them is_known_global_identifier_name).
  • codegen (expr/helpers.rs): add the three to is_global_this_builtin_name; exclude global from the function-typeof set so typeof global === "object".

Verification

Byte-identical to Node on both the default auto-optimize and PERRY_NO_AUTO_OPTIMIZE=1 paths, including non-ASCII (café), BMP (☃), astral (𝟘 → %uD835%uDFD8), malformed/partial escapes (%, %2, %zz, 100%), rebound values (const e = escape), and local shadowing (function escape(){} / const global = {} correctly win). All perry-runtime / perry-hir / perry-codegen tests pass; cargo fmt --check clean. Adds test_gap_escape_unescape_global_4511.ts.

Note: the issue's "Expected" shows a%20b%2Bc, but + is in escape's unescaped set — real Node returns a%20b+c, which this matches.

`escape()` / `unescape()` (ES Annex B B.2.1) were undefined, so bare
calls threw `ReferenceError`. `qs` (pulled in via `stripe`) uses them for
`%uXXXX` decoding, so any app importing `qs` hit this. The Node `global`
object was likewise unresolved — `(global as any).x` emitted the
unknown-identifier warning.

Implementation routes both through the existing reflective globalThis
function-value path (no dedicated HIR variant):

- runtime: `js_escape` / `js_unescape` in builtins/globals.rs implement
  Annex B encoding over UTF-16 code units (code units <256 -> `%XX`,
  else `%uXXXX`; astral scalars emit their surrogate pair, matching
  Node). `unescape` reassembles UTF-16 units before decoding so
  `%uD835%uDFD8`-style pairs recombine.
- runtime: register `escape`/`unescape` thunks in GLOBAL_THIS_BUILTIN_
  FUNCTIONS; install a self-referential `global` property on the
  globalThis singleton (`global === globalThis`).
- HIR: add escape/unescape/global to `is_builtin_global_value_name` so
  bare calls and value reads resolve to `globalThis.<name>` instead of
  throwing (also makes them `is_known_global_identifier_name`).
- codegen: add escape/unescape/global to `is_global_this_builtin_name`;
  exclude `global` from the function-typeof set so `typeof global ===
  "object"`.

Byte-identical to Node on both the default auto-optimize and
PERRY_NO_AUTO_OPTIMIZE paths, including non-ASCII, astral, malformed
escapes, and local shadowing. Adds test_gap_escape_unescape_global_4511.ts.
@proggeramlug
proggeramlug merged commit 953f926 into main Jun 5, 2026
13 of 14 checks passed
@proggeramlug
proggeramlug deleted the worktree-fix-escape-unescape-4511 branch June 5, 2026 06:26
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.

globals: legacy escape()/unescape() throw ReferenceError; bare Node global unresolved

1 participant