From 4811ebbb0da8ee90f2a9f03f9445e0549c7473bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 3 Oct 2026 23:40:16 +0200 Subject: [PATCH 1/4] class: later evaluations of a repeatable class declaration are fresh classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A class declaration evaluated more than once returned the same class object on every evaluation (`function f() { class K {} return K } f() === f()`), so its prototype, statics and instanceof were shared across evaluations. Owner decision 64, option (c′): the first evaluation keeps the shared class and every later evaluation creates a fresh class object. - HIR: a function-body or block declaration that `lower::run_once` cannot prove runs once, and that has no private names, computed keys or runtime heritage, lowers to `ClassExprFresh { shared_first_evaluation }`. Its captures, including the self-binding, use the guarded class environment. `new C()` and `C.` through its binding are guarded on the first evaluation (`ClassIsFirstEvaluation`) and keep their static forms there. A subclass in the same body shares its first evaluation only while the parent's binding holds the parent's first evaluation. The template-keyed capture snapshot belongs to the first evaluation. - Codegen: a module-private word per template holds the shared class once the first evaluation has handed it out. An evaluation costs one load, compare and branch; a run-once declaration emits nothing. The first evaluation marks the class function object. - Runtime: a class function object that is its declaration's first evaluation is a class of its own. Later evaluations' static writes are not mirrored into it, it does not stand in for the template, its static parent is kept, and instanceof and `constructor` follow the actual chain. - Transform: the exact-receiver inliner recognizes the guarded `new`. Monomorph's default padding reaches `ClassEnvStamp`. Closes #11759 --- .../PENDING-class-fresh-evaluations.md | 35 + crates/perry-abi/src/lib.rs | 7 + crates/perry-codegen/src/codegen/artifacts.rs | 5 + .../src/codegen/fresh_class_templates.rs | 67 +- crates/perry-codegen/src/collectors/refs.rs | 8 + .../src/expr/class_first_evaluation.rs | 205 ++++++ crates/perry-codegen/src/expr/dispatch.rs | 1 + crates/perry-codegen/src/expr/mod.rs | 1 + .../src/expr/static_field_meta.rs | 606 ++++++++++-------- .../src/type_analysis/numeric.rs | 1 + .../perry-codegen/src/type_analysis/refine.rs | 19 + crates/perry-hir/src/analysis/value_types.rs | 1 + .../src/analysis/value_types_tests.rs | 2 + crates/perry-hir/src/ir/expr.rs | 31 + crates/perry-hir/src/lower/context_new.rs | 3 + crates/perry-hir/src/lower/expr_function.rs | 12 +- crates/perry-hir/src/lower/lower_expr.rs | 10 +- .../src/lower/lower_expr/arm_class.rs | 2 + .../perry-hir/src/lower/lowering_context.rs | 16 + .../src/lower/shared_mutable_capture.rs | 8 + .../src/lower/tests/capture_stash.rs | 6 +- .../lower/tests/class_decl_self_binding.rs | 52 +- .../tests/class_expr_naming_and_shadowing.rs | 51 +- .../tests/fresh_class_extends_renamed.rs | 70 +- .../issue_11157_class_decl_self_statics.rs | 8 +- crates/perry-hir/src/lower_decl/block.rs | 12 +- crates/perry-hir/src/lower_decl/body_stmt.rs | 95 ++- crates/perry-hir/src/lower_decl/class_decl.rs | 71 +- .../class_decl/decl_self_binding.rs | 178 ++++- crates/perry-hir/src/lower_decl/mod.rs | 3 +- crates/perry-hir/src/monomorph/defaults.rs | 11 + crates/perry-hir/src/monomorph/tests.rs | 37 +- crates/perry-hir/src/stable_hash/expr.rs | 3 +- crates/perry-hir/src/walker/expr_mut.rs | 11 + crates/perry-hir/src/walker/expr_ref.rs | 11 + .../tests/class_self_new_shadowing.rs | 38 +- .../src/object/class_registry.rs | 1 + .../object/class_registry/parent_static.rs | 16 +- .../src/object/class_registry/state.rs | 19 + .../perry-runtime/src/object/class_value.rs | 37 +- .../field_get_set/class_object_props.rs | 8 + .../field_get_set/class_object_template.rs | 11 +- .../object/field_set_by_name/write_helpers.rs | 7 +- .../src/object/instanceof/dynamic_dispatch.rs | 25 +- .../src/inline/call_inliner.rs | 5 +- .../src/inline/exact_receivers.rs | 29 +- .../src/inline/factory_specialize.rs | 2 + .../perry-transform/src/inline/substitute.rs | 2 + .../issue_11759_class_first_evaluation.rs | 158 +++++ ...est_gap_11759_class_decl_evaluations_gc.ts | 38 ++ ...p_11759_class_decl_fresh_per_evaluation.ts | 74 +++ ...p_11759_class_decl_loop_extends_wrapper.ts | 75 +++ 52 files changed, 1823 insertions(+), 381 deletions(-) create mode 100644 changelog.d/PENDING-class-fresh-evaluations.md create mode 100644 crates/perry-codegen/src/expr/class_first_evaluation.rs create mode 100644 crates/perry/tests/issue_11759_class_first_evaluation.rs create mode 100644 test-files/test_gap_11759_class_decl_evaluations_gc.ts create mode 100644 test-files/test_gap_11759_class_decl_fresh_per_evaluation.ts create mode 100644 test-files/test_gap_11759_class_decl_loop_extends_wrapper.ts diff --git a/changelog.d/PENDING-class-fresh-evaluations.md b/changelog.d/PENDING-class-fresh-evaluations.md new file mode 100644 index 0000000000..2724a3c74c --- /dev/null +++ b/changelog.d/PENDING-class-fresh-evaluations.md @@ -0,0 +1,35 @@ +**A class declaration evaluated more than once now creates a fresh class on every evaluation after the first, as Node does** (#11759). + +`function f() { class K {} return K }` returned the same class on every call, so `f() === f()` was `true`, a property written to one evaluation's `prototype` showed up on the other's instances, `instanceof` answered `true` across evaluations, and every evaluation shared the same static fields. The same happened for a class declared in a module-level loop body. Classes that capture variables, have private elements, or have a computed key or a runtime `extends` value were already fresh per evaluation. + +The fix follows owner decision 64, option (c′). The first evaluation of a declaration is still the shared class, so code that runs once keeps the static class and its fast paths: bundle wrappers (esbuild `__esm`/`__commonJS` bodies, which only the runtime can see running once), functions called once, and module top level. The second and later evaluations each create a fresh class object, built from #11780's template shapes. + +- **The evaluation check.** Whether the first evaluation has happened is a fact of the class template: a module-private word per template (`@perry_class_first_eval.