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:
- 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.).
- 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.
Followup to #462 / #455 DX thread.
#462 closed the load-bearing case:
undefined.fooandnull.foonow throw a node-shapedTypeError: Cannot read properties of {undefined|null} (reading '<prop>')and stop execution. The v0.5.526 changelog explicitly carved out one case as deferred:This is the remaining footgun.
(42).fooshould throw (or auto-box toNumberand resolveNumber.prototype.foo→ undefined, matching Node) — instead it returnsundefinedsilently and execution continues.Repro
Node:
…wait — Node actually allows
(42).fooand returnsundefinedbecause of auto-boxing intoNumber. The footgun is typo'd method calls: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:
.propread → look up on the auto-boxed prototype (Number/Boolean/String/BigInt). Unknown →undefined. Same as Node..method()call where the resolved value isundefinedor non-callable → throwTypeError: <expr> is not a function.Where this hooks in
crates/perry-codegen/src/codegen.rs'sPropertyGetcodegen got therecv_badsplit 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:String.prototype.length, etc.).Expr::Callcodegen), if the resolved callee isundefinedor non-callable, emitjs_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.