Skip to content

A rejected STRICT ordinary-object write never throws — the mirror image of #9394 on the object path #9422

Description

@proggeramlug
"use strict";
const o = { x: 1 };
Object.freeze(o);
o.x = 9;      // node: TypeError   perry: silent

Per ES §6.2.5.7, a failed [[Set]] must throw in strict mode. Perry is silent.

Root cause, already located

Codegen emits js_put_value_set(..., strict = 0) at every property-set site (crates/perry-codegen/src/expr/property_set.rs:348 and :479). The strictness flag exists and is threaded elsewhere — it simply is not passed here.

Relationship to #9394

Exact mirror image. #9394 was the array element path throwing in sloppy mode where node is silent; this is the ordinary-object path staying silent in strict mode where node throws. #9418 fixed the array side by carrying the assignment's own Throw flag, which codegen already had — the same flag is what this needs.

Deliberately kept out of #9418's scope so a correctness fix for one path did not silently change the other; the fixture there asserts the object control in the sloppy arm only, with a comment pointing here.

Test guidance

Assert both arms — strict throws and sloppy stays silent — for frozen, sealed, non-extensible and non-writable-descriptor receivers. Asserting only one arm is precisely what let #9394 through.

Found while fixing #9394.

Activity

  1. proggeramlug commented on Sep 1, 2026

    @proggeramlug
    ContributorAuthor

    This issue does not reproduce as filed. Both its stated symptom and its stated root cause are wrong on current main. Investigated under PR #9458.

    The repro in this issue passes today:

    "use strict"; const o={x:1}; Object.freeze(o); o.x=9;   // node TypeError, perry TypeError

    So do all seven shapes listed here, plus computed-key, class-field, +=, ++ and frozen-array-index — 34 of 36 lines of the verification fixture are byte-identical to node before any change.

    The stated root cause, settled from IR

    I filed this citing js_put_value_set(..., strict = 0) at every property-set site (a line from #9426's changelog). That is wrong. Strict and sloppy twins of the exact program above, dumped with PERRY_SAVE_LL:

    f() [strict]:  invoke void @js_class_field_set_fallback(...)          ← throws
    g() [sloppy]:  invoke double @js_put_value_set(..., i32 0)            ← correct for sloppy
    

    The two strict = 0 literals at expr/property_set.rs:348,479 are inside try_lower_sloppy_class_field_store / …_boxed_store, which expr/proxy_reflect.rs reaches only under if !*strict. 0 is the right constant there, and the strict arm never enters that lane.

    This resolves the contradiction I flagged during review: #9426's commit message is right ("already passes to the ordinary-object [[Set]]") and its changelog line is wrong ("every property-set site"). Two documents by the same author disagreeing is not two witnesses — the IR was the tiebreaker.

    What was actually found and fixed

    One genuine strict under-throw, one lane over: js_array_set_length_strict (array/push_pop.rs:1269) tested OBJ_FLAG_FROZEN only, so Object.defineProperty(arr, "length", {writable:false}) — which records the attribute in the descriptor side table without freezing — fell through to the sloppy body, whose non-writable arm is a silent return annotated "strict-mode throw is handled by the caller's PutValue". That entry is the caller. Fixed in PR #9458 by reusing array_length_is_non_writable, the predicate push/pop/shift/unshift have used since test262's set-length-*-non-writable.

    The real remaining gap is the opposite direction

    Sloppy o.x += 1 on a frozen object over-throws (node silent, perry TypeError), because Expr::PropertySet carries no strictness field at all. Filed as #9459. That is the object-path analogue of #9394, and it is a hard failure rather than a wrong value — worth more than this issue was.

    Closing this as not-reproducing once #9458 lands, unless someone can produce a shape that still under-throws.

  2. added a commit that references this issue on Sep 2, 2026
  3. proggeramlug commented on Sep 2, 2026

    @proggeramlug
    ContributorAuthor

    Closing as not-reproducing-as-filed, per the investigation on PR #9458: the issue's own repro and all seven listed shapes already passed on unfixed main (34/36 fixture lines), and the stated root cause was disproven from IR — the strict = 0 literals live in a lane reached only under if !*strict. The one genuine strict under-throw found nearby (non-writable arr.length via the descriptor side table) is fixed in #9458. test_gap_9422_strict_object_store_strictness.cts. Verified on main 0a1c137d90 with a fresh build: fixture byte-identical to node --experimental-strip-types (stdout, stderr, and exit code).
    The real remaining gap is the OPPOSITE direction and has its own issue: #9459 (sloppy o.x += 1 on a frozen object over-throws; Expr::PropertySet carries no strictness field).

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