Repository navigation
class: later evaluations of a class declaration are fresh classes; first evaluation stays at shared cost (Closes #11759) - #11840
Merged
Conversation
added 4 commits
October 3, 2026 23:40
…classes
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.<static field>` 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
#11759 (c′) guarded `new C()` and `C.<static field>` on the declaration's first evaluation at every use, so a hot loop paid the test per iteration and the guarded `new` lost scalar replacement. - Codegen: a `for`/`while`/`do` loop holding first-evaluation guards on a binding it cannot rebind (not boxed, not a module global, never reassigned in the body, not rebound by the loop) tests the binding once before the loop and lowers the loop twice: the first-evaluation copy holds only the static forms, the later-evaluation copy only the by-value forms. A loop that defines a closure or class is left alone. - Escape analysis: a `let` bound to the guarded `new` is a scalar-replacement candidate (replaced only where the versioned copy lowers it as a plain `new`). A class declaration's evaluation and the first-evaluation test are walked by their operands instead of the catch-all that escaped every candidate of the body. - HIR: `C.<static method>(args)` through a repeatable declaration's binding is guarded like a static field read, keeping the static call on the first evaluation. Closes #11759
#11759 (c′) left two first-evaluation costs behind the loop versioning: `t += c.x` kept `t` boxed, because one local has one representation for the whole function and the later-evaluation copy's `c.x` had no number proof; and an instance built before a loop (`const c0 = new C()`) kept its per-use test and dispatched its method calls through the inline cache. - Ptr<Shape> proof: a local bound to a repeatable declaration's guarded `new` is proven by every rule except the exact class object, and recorded as a template-lineage fact. Every evaluation of one template runs the same constructor chain, initializers and methods on the same arguments, so which fields hold a Number is a template fact: it feeds the Number-by-construction proof and the canonical-f64 predicate for reads of those fields, in both loop copies. It never licenses a bare load or a direct call. - Codegen: the rest of a function body after `const c = new C()` through such a binding tests the first evaluation once and is lowered twice, like a versioned loop, when the binding cannot be rebound, the rest defines no closure or class and holds at most 400 HIR nodes. In the first-evaluation copy (and in a versioned loop's) the instance is the shared class's, which is the lineage fact's missing exactness: its field reads and method calls take the Ptr<Shape> forms. The rest is versioned only when it gains from it: it uses the instance, or tests the binding outside a loop (a loop versions itself). A class with captures, whose first-evaluation `new` is stamped with its evaluation, versions the same way but keeps the guarded instance forms. Closes #11759
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing (60)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
proggeramlug
pushed a commit
that referenced
this pull request
Oct 3, 2026
…its prototype link in separate captures #11840 put the evaluation state in capture 1 of the class function object and #11843 put the prototype link there too; the merge left conflict markers in class_value.rs and main does not compile. The evaluation state keeps capture 1 (codegen writes it at CLASS_EVALUATION_STATE_CAPTURE); the prototype link moves to capture 2, and the object is minted with three captures. Const asserts keep the two slots apart.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11759 with option (c′), owner decision 64: the first evaluation of a class declaration keeps the shared class, and the 2nd and later evaluations create a fresh class, as node does (
function f(){ class K {} return K } f() !== f()).Mechanism
run_onceproves a single run, there is no word and no check..constructorfollow the real chain.__esmwrapper still emits the check, becauserun_oncecan't prove it runs once without a pattern table. The class correctly stays shared at runtime.First-evaluation cost kept at main's level
const c = new C(), the rest of the body tests once and is compiled twice, bounded to 400 HIR nodes.t += c.xstays a plain add.new C()+ field readVerification
.texttsc −37.7 KB.Not addressed here: later evaluations still run #11780's fresh runtime path, about 8× a shared class. That's a separate lane.