[compiler] Don't outline functions that reference enclosing-function locals#36736
Open
poteto wants to merge 2 commits into
Open
[compiler] Don't outline functions that reference enclosing-function locals#36736poteto wants to merge 2 commits into
poteto wants to merge 2 commits into
Conversation
…nction locals
In @compilationMode:"infer", a component defined inside a non-compiled
factory function can contain a callback whose only free variable is a
local of that factory. The callback's HIR context is empty (the factory
local is misclassified as ModuleLocal), so outlineFunctions hoists it to
module scope as `function _temp() { return store; }`, where `store` is
not in scope: the compiled fixture throws a ReferenceError at runtime
while the uncompiled version renders fine (react#34901). The
expect file documents today's wrong output; the fixture is added to
SproutTodoFilter until the fix lands.
…locals When the compiled function is itself nested inside a non-compiled function (e.g. a factory, with @compilationMode:"infer"), HIRBuilder resolves free variables against parentFunction.scope.parent, which is the enclosing function's scope rather than the program scope, so the enclosing function's locals are classified as ModuleLocal and lowered as LoadGlobal instead of being captured in the function's context. Such functions looked outlineable (empty context), and outlining hoisted them to module scope where the referenced name is not in scope, throwing a ReferenceError at runtime (react#34901). Fix in the outlining candidate check, in both compilers: reject a candidate if it (or a nested function) references a name that does not resolve to a module-scope binding. The TS pass re-resolves LoadGlobal(ModuleLocal)/StoreGlobal names against the scope HIRBuilder resolved against and requires the binding to be at program scope; the Rust lowering records the misclassified names in env.non_module_scope_names for outline_functions to consult, encoding the same decision. Adopts the approach of community PR react#36539 with a stricter predicate: binding-identity instead of name lookup (catching shadowed module names), recursion into nested functions, StoreGlobal coverage, and the Rust port. Co-authored-by: tejasupmanyu <[email protected]>
9f250f9 to
c871df1
Compare
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.
Function outlining hoisted closures to module scope when their only free variables looked like module-scope names. But in
infercompilation mode, HIRBuilder classifies references to an enclosing non-compiled function's locals as ModuleLocal too:resolveIdentifieronly distinguishes bindings inside the compiled function from bindings above it. A factory's component returning() => storeoutlined tofunction _temp() { return store; }at module level, throwingReferenceError: store is not definedat runtime where the uncompiled source works.The outlining candidate predicate now also requires that every ModuleLocal LoadGlobal (and StoreGlobal) name in the function, recursively through nested functions, resolves at the program scope. Names that resolve to a scope between module level and the compiled function block outlining; names that resolve to nothing (ambient globals, references to already-outlined siblings) remain outlineable. Re-resolution happens against the same scope HIRBuilder resolved against, so a factory local shadowing a module-level name is correctly rejected rather than silently rebinding.
The Rust port reaches the same decisions through its lowering: the branch that classifies these names already knows which case fired, so it records them into the environment and the outline pass consults that set. Mechanisms differ (TS re-resolves at outline time, Rust records at lowering time); decisions verified identical on both fixtures through the Rust e2e CLI.
Builds on the approach in #36539 by @tejasupmanyu, extended with nested-function recursion, StoreGlobal coverage, and binding-identity (rather than name-presence) scope checks; the positive control pins that module-const references still outline.
Verification: TS snap 1806/1806, Rust snap 1806/1806, cargo workspace green, scoped TS-vs-Rust HIR parity harness green; all pre-existing outlining fixtures byte-identical.
Closes #34901