Skip to content

hir: zero-arg calls to global builtins are a COMPILE error instead of JS's runtime semantics #6366

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions