Problem
crates/perry-hir/src/lower/expr_call/globals.rs rejects a zero-argument call to several JS globals at compile time:
$ perry compile a.js
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 is refusing to compile legal JS.
Verified against Node:
| call |
Node |
perry (before) |
isNaN() |
true |
compile error |
isFinite() |
false |
compile error |
encodeURI() |
"undefined" |
compile error |
decodeURI() |
"undefined" |
compile error |
encodeURIComponent() |
"undefined" |
compile error |
decodeURIComponent() |
"undefined" |
compile error |
atob() |
TypeError |
compile error |
btoa() |
TypeError |
compile error |
structuredClone() |
TypeError |
compile error |
Found by running Bun's own test suite through perry: test/js/web/util/atob.test.js fails at the compile tier (total failure) because it contains try { atob(); } catch (e) { ... } — a legal, catchable runtime TypeError.
Part 1 — fixed (PR to follow)
The six that just coerce undefined are fixed by lowering the omitted argument to Expr::Undefined, which is the padding idiom globals.rs already uses for parseInt / parseFloat / BigInt / Object. Output is now byte-for-byte identical to Node.
Part 2 — still open: atob / btoa / structuredClone
These are WebIDL required-argument throws, and they cannot be modelled by undefined-padding, because f() and f(undefined) genuinely differ:
atob() // TypeError
atob(undefined) // InvalidCharacterError ("The string contains invalid characters.")
btoa() // TypeError
btoa(undefined) // "dW5kZWZpbmVk" <-- doesn't even throw
So the runtime must be able to tell "no argument" from "the argument undefined". Today it can't: the global thunks have a fixed arity and no arg count —
// perry-runtime/src/object/global_this/builtin_thunks.rs
pub(crate) extern "C" fn global_this_atob_thunk(
_closure: *const ClosureHeader,
value: f64, // <-- no way to distinguish absent from undefined
) -> f64 { ... }
and HIR's Expr::Atob(Box<Expr>) / Expr::Btoa(Box<Expr>) likewise have no room for "argument omitted".
A fix needs real arity plumbing — e.g. Expr::Atob(Option<Box<Expr>>) (21 match sites across codegen/HIR) plus a throwing runtime entry point, or a dedicated missing-required-argument throw expression. Left as follow-up; until then these three keep the (incorrect) compile-time diagnostic rather than silently returning a wrong value.
Problem
crates/perry-hir/src/lower/expr_call/globals.rsrejects a zero-argument call to several JS globals at compile time: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 is refusing to compile legal JS.Verified against Node:
isNaN()trueisFinite()falseencodeURI()"undefined"decodeURI()"undefined"encodeURIComponent()"undefined"decodeURIComponent()"undefined"atob()TypeErrorbtoa()TypeErrorstructuredClone()TypeErrorFound by running Bun's own test suite through perry:
test/js/web/util/atob.test.jsfails at thecompiletier (total failure) because it containstry { atob(); } catch (e) { ... }— a legal, catchable runtimeTypeError.Part 1 — fixed (PR to follow)
The six that just coerce
undefinedare fixed by lowering the omitted argument toExpr::Undefined, which is the padding idiomglobals.rsalready uses forparseInt/parseFloat/BigInt/Object. Output is now byte-for-byte identical to Node.Part 2 — still open:
atob/btoa/structuredCloneThese are WebIDL required-argument throws, and they cannot be modelled by undefined-padding, because
f()andf(undefined)genuinely differ:So the runtime must be able to tell "no argument" from "the argument
undefined". Today it can't: the global thunks have a fixed arity and no arg count —and HIR's
Expr::Atob(Box<Expr>)/Expr::Btoa(Box<Expr>)likewise have no room for "argument omitted".A fix needs real arity plumbing — e.g.
Expr::Atob(Option<Box<Expr>>)(21 match sites across codegen/HIR) plus a throwing runtime entry point, or a dedicatedmissing-required-argumentthrow expression. Left as follow-up; until then these three keep the (incorrect) compile-time diagnostic rather than silently returning a wrong value.