Skip to content

fix(assert): deepStrictEqual — don't treat object-literal shape ids as prototypes (#4937)#5051

Merged
proggeramlug merged 1 commit into
mainfrom
fix/deepequal-mutated-empty-literal
Jun 13, 2026
Merged

fix(assert): deepStrictEqual — don't treat object-literal shape ids as prototypes (#4937)#5051
proggeramlug merged 1 commit into
mainfrom
fix/deepequal-mutated-empty-literal

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Fixes #4937.

Root cause

Not in the deep-equality body walk at all — the prototype-sensitivity gate from #2934. prototype_token (builtins/formatting/prototype_equality.rs) tokenizes a heap object's prototype as CLASS_PROTO_NAMESPACE | class_id. Codegen stores an unregistered layout-shape id in class_id for object literals (e.g. 4), while a {}-born object populated afterwards keeps class_id 0. Both have Object.prototype as their real [[Prototype]], but the tokens differed (…0000 vs …0004), so prototypes_differ short-circuited every mutated-vs-literal comparison to not equal before the body compare ran.

Fix

Normalize any class_id with no CLASS_NAMES entry to 0 before building the token — the registry's own constructor test (class_decl_prototype_value uses exactly this predicate). Registered class instances, null-prototype objects, and recorded setPrototypeOf values keep their distinct tokens.

Validation

All of these now match Node (node --experimental-strip-types) exactly:

dot / computed-lit / computed-var set then deepStrictEqual vs literal: true (was throw)
incremental multi-key build (matchKnownFields idiom): passes
class instance vs literal:        false (prototype sensitivity preserved)
Object.create(null) vs literal:   false
literal vs literal, mutated vs mutated, unequal bodies: unchanged
  • New unit test unregistered_shape_id_normalizes_to_plain_object_prototype pins the token normalization + end-to-end js_util_is_deep_strict_equal behavior.
  • cargo test -p perry-runtime prototype: the test_array_exotic_descriptors_and_global_prototype_identity flake in that group reproduces on pristine origin/main (parallel-run snapshot flake, passes in isolation) — pre-existing, unrelated.

Code-only PR — version bump + changelog left for merge time.

…s prototypes (#4937)

deepStrictEqual's prototype gate (#2934) tokenized an object's prototype
as CLASS_PROTO_NAMESPACE | class_id. Codegen stores an unregistered
layout-shape id in class_id for object literals, while a {}-born object
mutated afterwards keeps class_id 0 — so 'd = {}; d.simple = v' never
compared equal to '{ simple: v }' despite identical keys, values, and
real [[Prototype]] (both Object.prototype).

Normalize class_ids with no CLASS_NAMES entry (the registry's own
constructor test, per class_decl_prototype_value) to 0 before building
the token. Registered class instances, null-prototype objects, and
recorded setPrototypeOf values keep their distinct tokens.
@proggeramlug proggeramlug merged commit da24be2 into main Jun 13, 2026
13 checks passed
@proggeramlug proggeramlug deleted the fix/deepequal-mutated-empty-literal branch June 13, 2026 04:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

assert.deepStrictEqual: object mutated after empty-literal creation never equals an equivalent literal

1 participant