// .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).
Function.prototype.caller/.argumentsare the ECMAScript poison-pillaccessors. 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
OrdinarySetreturningfalse: a silent no-opin sloppy code, a
TypeErrorin 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(theclosure store path):
The throw is unconditional. The comment's premise — "Perry compiles everything
strict" — is what #9423/#9458 established is not true of a
.ctsscript, andit is exactly the same shape as #9394/#9422/#9459: a rejected
[[Set]]throwingwithout consulting the assignment's
Throwflag. The class-constructor site(
field_set_by_name.rs, "Writing.caller/.argumentson a classconstructor") 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] = vwithk = "caller"),which never touches the name-keyed
PropertySetlowering. So it is neithercaused nor fixed by #9459, which verified that
js_put_value_setreaches bothpoison-pill sites and left the runtime side alone.
Fix shape
Carry the assignment's
Throwflag to the closure store path and make thepoison-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_setalready hasstrictand iswhat both spellings reach.
Verification bar
test-files/test_gap_9459_property_set_strictness.ctsalready asserts theordinary-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
hasOwnPropertyasserted so a fix that storesinstead 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).