fix(hir): a zero-arg call to a global builtin is runtime semantics, not a compile error (#6366)#6368
Conversation
…ot a compile error (PerryTS#6366) `globals.rs` rejected a zero-argument call to several JS globals at COMPILE time: Error: isNaN requires one argument But a missing argument is not an error in JS — the parameter is simply `undefined`, and each of these has well-defined behavior for it. Perry was refusing to compile legal JS. Verified against Node: isNaN() -> true isFinite() -> false encodeURI() -> "undefined" decodeURI() -> "undefined" encodeURIComponent() -> "undefined" decodeURIComponent() -> "undefined" Lower the omitted argument to `Expr::Undefined` and let the existing intrinsic produce Node's answer. This is the padding idiom `globals.rs` already uses for `parseInt` / `parseFloat` / `BigInt` / `Object`. NOT extended to `atob` / `btoa` / `structuredClone`: those are WebIDL required-argument throws, and they cannot be modelled by undefined-padding because `f()` and `f(undefined)` genuinely differ — `atob()` throws TypeError but `atob(undefined)` throws InvalidCharacterError; `btoa()` throws TypeError but `btoa(undefined)` returns "dW5kZWZpbmVk". Distinguishing them needs real arity plumbing (the global thunks have fixed arity and no arg count; `Expr::Atob` has no room for "argument omitted"). Tracked in PerryTS#6366; they keep the current diagnostic rather than silently returning a wrong value. `unshadowed_global_isfinite_zero_arg_call_keeps_builtin_arity_error` asserted the old diagnostic, using it as a proxy for "the unshadowed global took the BUILTIN path". That intent is preserved and now asserted directly: the call must lower to the `IsFinite` intrinsic with an `Expr::Undefined` argument. Renamed accordingly. Found by running Bun's own test suite through perry: `test/js/web/util/atob.test.js` fails at the `compile` tier on `try { atob(); } catch { ... }` — a legal, catchable runtime TypeError. Tests: `test-files/test_gap_6366_global_zero_arg_coercion.ts` (parity harness, byte-for-byte vs node), plus the rewritten shadowing test.
📝 WalkthroughWalkthroughGlobal numeric and URI built-ins now treat omitted arguments as ChangesGlobal built-in argument handling
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/perry-hir/src/lower/expr_call/globals.rs (1)
954-963: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider applying
arg_or_undefinedto existing inline patterns for DRY consistency.The new helper is clean and well-documented. Several existing arms in this function (
parseIntstring arg at L31-35,parseFloatat L47-51,BigIntat L62-70,Objectat L104-108,queueMicrotaskat L205-209) use the sameif args.is_empty() { Expr::Undefined } else { args.remove(0) }inline pattern. Applyingarg_or_undefinedto those sites would reduce duplication and centralize the padding behavior. This is purely optional and can be deferred.♻️ Example refactor for
parseFloat(representative)"parseFloat" => { - if !args.is_empty() { - return Ok(Ok(Expr::ParseFloat(Box::new(args.remove(0))))); - } else { - return Ok(Ok(Expr::ParseFloat(Box::new(Expr::Undefined)))); - } + let arg = arg_or_undefined(&mut args); + return Ok(Ok(Expr::ParseFloat(Box::new(arg)))); }
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 70ea600c-ec3e-4449-b7dd-af4a389cd8b3
📒 Files selected for processing (3)
crates/perry-hir/src/lower/expr_call/globals.rscrates/perry-hir/tests/global_builtin_shadowing.rstest-files/test_gap_6366_global_zero_arg_coercion.ts
|
The shard fails on five tests:
This PR changes exactly one thing: how a zero-argument call to None of them contain one, so the changed lowering is a no-op for every one of them — the diff cannot affect these tests. ( Per CLAUDE.md (" The targeted gates for this change are all green: |
Part 1 of #6366.
Problem
globals.rsrejects a zero-argument call to several JS globals at compile time:A missing argument isn't an error in JS — the parameter is simply
undefined, and each of these has well-defined behavior for it. Perry was refusing to compile legal JS. Verified against Node:isNaN()trueisFinite()falseencodeURI()"undefined"decodeURI()"undefined"encodeURIComponent()"undefined"decodeURIComponent()"undefined"Fix
Lower the omitted argument to
Expr::Undefinedand let the existing intrinsic produce Node's answer — the padding idiomglobals.rsalready uses forparseInt/parseFloat/BigInt/Object. Output is byte-for-byte identical to Node.Deliberately NOT included:
atob/btoa/structuredCloneThese are WebIDL required-argument throws and cannot be modelled by undefined-padding, because
f()andf(undefined)genuinely differ:The runtime can't tell them apart today (global thunks have fixed arity and no arg count;
Expr::Atob(Box<Expr>)has no room for "omitted"). Distinguishing them needs real arity plumbing — analysed in #6366 and left as follow-up. They keep the current (incorrect) diagnostic rather than silently returning a wrong value.Test change
unshadowed_global_isfinite_zero_arg_call_keeps_builtin_arity_errorasserted the old diagnostic, using it as a proxy for "the unshadowed global took the BUILTIN path". That intent is preserved and now asserted directly: the call must lower to theIsFiniteintrinsic with anExpr::Undefinedargument. Renamed to..._lowers_to_builtin_with_undefined. Its shadowed sibling is untouched and still passes.Found by
Running Bun's own test suite through perry —
test/js/web/util/atob.test.jsdies at thecompiletier ontry { atob(); } catch (e) { ... }, a legal catchable runtime TypeError.Testing
test-files/test_gap_6366_global_zero_arg_coercion.ts— passes the parity harness byte-for-byte vsnode --experimental-strip-types(Parity Pass: 1).global_builtin_shadowing2/2,node_named_export_hygiene9/9.Summary by CodeRabbit
undefinedforisNaN,isFinite, and URI encoding/decoding functions.undefinedarguments.