Skip to content

Sloppy f.caller = v on a plain function throws: the closure poison pill ignores the assignment Throw flag #9525

Description

@proggeramlug
// .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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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