Repository navigation
Description
// .cts, sloppy
function f() {}
f.caller = 2; // node: silent no-op, no own property Perry: TypeError
f.arguments = 2; // node: silent no-op Perry: TypeError
// .cts, "use strict"
function g() {}
g.caller = 2; // node: TypeError Perry: TypeError (correct)
class D {}
D.caller = 2; // node: TypeError in BOTH modes Perry: TypeError (correct)Function.prototype.caller / .arguments are the ECMAScript poison-pill
accessors. For a class constructor (a strict function) V8 throws in both
modes, which Perry matches. For a plain non-strict function declaration,
node's observable behaviour is OrdinarySet returning false: a silent no-op
in sloppy code, a TypeError in strict code, and no own property either way.
Perry throws in both.
Root cause
crates/perry-runtime/src/object/field_set_by_name/write_helpers.rs (the
closure store path):
// ECMAScript "poison pill" — assigning `caller`/`arguments` on any
// strict-mode function (Perry compiles everything strict: declarations,
// expressions, bound and built-in closures, arrows) throws via the
// %ThrowTypeError% accessor's missing setter.
if matches!(name_str, "caller" | "arguments")
&& !crate::closure::closure_has_own_dynamic_prop(obj as usize, name_str)
{
crate::fs::validate::throw_type_error_with_code(...);
}The throw is unconditional. The comment's premise — "Perry compiles everything
strict" — is what #9423/#9458 established is not true of a .cts script, and
it is exactly the same shape as #9394/#9422/#9459: a rejected [[Set]] throwing
without consulting the assignment's Throw flag. The class-constructor site
(field_set_by_name.rs, "Writing .caller / .arguments on a class
constructor") is correct as-is and must keep throwing unconditionally.
Scope
This is a RUNTIME store path, not a codegen routing question: it reproduces
identically through the computed-key route (f[k] = v with k = "caller"),
which never touches the name-keyed PropertySet lowering. So it is neither
caused nor fixed by #9459, which verified that js_put_value_set reaches both
poison-pill sites and left the runtime side alone.
Fix shape
Carry the assignment's Throw flag to the closure store path and make the
poison-pill rejection conditional on it, leaving the class-constructor site
unconditional. The entry that needs the flag is the closure branch of
js_object_set_field_by_name; js_put_value_set already has strict and is
what both spellings reach.
Verification bar
test-files/test_gap_9459_property_set_strictness.cts already asserts the
ordinary-object and class-constructor receiver paths in both modes and carries a
comment naming this gap where the plain-function case belongs. Add the function
receiver there, both arms, with hasOwnProperty asserted so a fix that stores
instead of no-opping is caught.
Found while fixing #9459 (a CodeRabbit review of PR #9519 asked whether the
sloppy tail bypassed the poison pill; it does not — this is the residual).
Activity
fix(codegen): the sloppy tail stops excluding `caller`/`arguments` (P…
fix(runtime): honor sloppy function poison-pill stores (PerryTS#9525)
fix(runtime): honor sloppy function poison-pill stores (#9525)