Skip to content

Runtime: throw TypeError on property access against primitive receivers (number, boolean, raw f64) #510

Description

@proggeramlug

Followup to #462 / #455 DX thread.

#462 closed the load-bearing case: undefined.foo and null.foo now throw a node-shaped TypeError: Cannot read properties of {undefined|null} (reading '<prop>') and stop execution. The v0.5.526 changelog explicitly carved out one case as deferred:

Non-nullish invalid receivers (numbers, bools, raw f64) keep the silent-undefined fall-through since Perry has no primitive auto-boxing yet.

This is the remaining footgun. (42).foo should throw (or auto-box to Number and resolve Number.prototype.foo → undefined, matching Node) — instead it returns undefined silently and execution continues.

Repro

const n: any = 42;
console.log("before");
const x = n.foo;
console.log("after:", x);   // Perry: prints "after: undefined"; Node: throws here
const b: any = true;
const y = b.bar;
console.log("bool after:", y);  // same: silently undefined

Node:

TypeError: Cannot read properties of undefined (reading 'foo')

…wait — Node actually allows (42).foo and returns undefined because of auto-boxing into Number. The footgun is typo'd method calls:

const s = "hello";
s.lengt;   // undefined — but Node returns undefined too via Number/String prototype lookup
s.lengt(); // Node: TypeError: s.lengt is not a function
           // Perry: silently returns undefined and proceeds

So the fix is the call site, not just the property access: when calling a method on a primitive whose name doesn't exist on the auto-boxed prototype, throw TypeError: <expr> is not a function. Property reads on primitives matching Node's auto-boxed prototype lookup is the simpler half.

Expected behavior

Match Node:

  • Primitive .prop read → look up on the auto-boxed prototype (Number/Boolean/String/BigInt). Unknown → undefined. Same as Node.
  • Primitive .method() call where the resolved value is undefined or non-callable → throw TypeError: <expr> is not a function.

Where this hooks in

crates/perry-codegen/src/codegen.rs's PropertyGet codegen got the recv_bad split in v0.5.526 — the TAG_UNDEFINED/TAG_NULL → throw path is in place. The non-nullish fall-through stays silent because there's no primitive prototype dispatch yet. Two-step plan:

  1. Wire prototype lookup for STRING_TAG / INT32_TAG / TAG_TRUE / TAG_FALSE / raw f64 receivers — most of these prototypes already have native dispatchers (String.prototype.length, etc.).
  2. At the call site (Expr::Call codegen), if the resolved callee is undefined or non-callable, emit js_throw_type_error_not_a_function (parallel to the v0.5.526 helper).

Refs: #455 (Justin's DX feedback), #462 (closed — null/undefined case), v0.5.526 changelog.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions