Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[compiler] Delete LoweredFunction.dependencies and hoisted instructions #32096

Merged
merged 1 commit into from
Feb 18, 2025

Conversation

mofeiZ
Copy link
Contributor

@mofeiZ mofeiZ commented Jan 16, 2025

LoweredFunction dependencies were exclusively used for dependency extraction (in propagateScopeDeps). Now that we have a propagateScopeDepsHIR that recursively traverses into nested functions, we can delete dependencies and their associated synthetic LoadLocal/PropertyLoad instructions.

Internal snapshot diff for this change shows ~.2% of files changed. I read through ~60 of the changed files

  • most changes are due to better outlining (due to better DCE)
  • a few changes in memo inference are due to changed ordering
// source
arr.map(() => contextVar.inner);

// previous instructions
$0 = LoadLocal arr
$1 = $0.map
// Below instructions are synthetic
$2 = LoadLocal contextVar
$3 = $2.inner
$4 = Function deps=$3 context=contextVar {
  ...
}
  • a few changes are effectively bugfixes (see aliased-nested-scope-fn-expr)

Stack created with Sapling. Best reviewed with ReviewStack.

Copy link
Contributor

@josephsavona josephsavona left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super exciting! A couple questions that i'd really like to understand before we move forward but this is great!

Comment on lines +278 to +292
if (
(instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod') &&
place.identifier.type.kind === 'Primitive'
) {
continue;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you double-check this case a bit more? Every time we have to special case something like this there's a chance that we're overlooking the larger rule. Why do we need to skip primitives, and why only for functionexpr/objectmethod?

If nothing else, this repeatedly checks properties of the instruction value for every single operand, which is inefficient, but i'm more concerned with the check itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! This change special cases function calls that capture identifiers whose type is Primitive but have a scope for other reasons. I added this to reduce the number of files changes by this refactor.

We can remove this check (which leads to more merged scopes) or generalize it in visitPlace in a later PR.

const context = instrValue.loweredFunc.func.context
.map(dep => printPlace(dep))
.map(dep => `${printPlace(dep)}`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the original printPlace(dep) is more clear

Comment on lines 85 to 86
CompilerError.invariant(operand.effect === Effect.Unknown, {
reason: 'Unexpected unknown effect',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we expect the effect to be unknown in the condition, but the error message says the opposite (?)

Comment on lines 407 to 408
operandValues.size === 1 &&
operandValues.values().next().value?.kind === 'DeclareContext'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm why only consider hoisted context variables if there is one operand? could you take the example below and also add some other context reference and break this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.expect.md for an explanation. I think this check is semantically important, but happy to refactor to make it cleaner

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: check if we can just "never freeze declare contexts"

Comment on lines +382 to +389
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
if (operand.identifier.type.kind === 'Primitive') {
continue;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above, i'd like to understand this more. why this exact combination and not primitives of other instruction types, for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +147 to +162
if (DEBUG) {
for (const [, block] of ir.blocks) {
for (const phi of block.phis) {
CompilerError.invariant(!rewrites.has(phi.place.identifier), {
reason: '[EliminateRedundantPhis]: rewrite not complete',
loc: phi.place.loc,
});
for (const [, operand] of phi.operands) {
CompilerError.invariant(!rewrites.has(operand.identifier), {
reason: '[EliminateRedundantPhis]: rewrite not complete',
loc: phi.place.loc,
});
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert this file? or set DEBUG=false

@@ -27,7 +27,6 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
function component(a, b) {
const $ = _c(2);
const y = { b };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered about the exact mechanism that was kicking in to let us prune this. We have always had code that recursively calls DCE on inner functions (its part of AnalyzeFunctions). And DCE prunes unused context variables on the outermost function its called on. So we were pruning the context access for y. But because we didn't prune unused deps, the outer DCE thought that y was still used. Now the inner DCE removes y from context, and then the outer DCE can see that y is unused and prune it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that's completely right. I thought about writing a separate pass to prune unused deps and their source instructions to reduce noise of the snapshot diffing for this PR. Ultimately it didn't seem worth the time

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this doing here?

mofeiZ added a commit that referenced this pull request Jan 22, 2025
Small patch to pass aliased context values into
`Object|ArrayExpression`s
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32093).
* #32099
* #32104
* #32098
* #32097
* #32096
* #32095
* #32094
* __->__ #32093
mofeiZ added a commit that referenced this pull request Jan 22, 2025
See test fixture
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32094).
* #32099
* #32104
* #32098
* #32097
* #32096
* #32095
* __->__ #32094
* #32093
mofeiZ added a commit that referenced this pull request Jan 22, 2025
See test fixture
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32095).
* #32099
* #32104
* #32098
* #32097
* #32096
* __->__ #32095
* #32094
* #32093
@mofeiZ mofeiZ marked this pull request as ready for review February 14, 2025 21:19
...context,
temporaries: actuallyEvaluatedTemporaries,
});
const nodes = collectNonNullsInBlocks(fn, context);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can now be cleaned up 🎉

// Helper class to track indirections such as LoadLocal and PropertyLoad.
export class IdentifierState {
properties: Map<Identifier, Dependency> = new Map();
properties: Map<IdentifierId, Identifier> = new Map();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're no longer tracking dependencies, we also don't need to track a granular Property mapping.

We keep this aliasing logic around in this PR to reduce the number of files changed by the refactor. This is deleted altogether in #32097

Comment on lines +98 to +99
} else if (knownMutated.has(operand)) {
operand.effect = Effect.Mutate;
Copy link
Contributor Author

@mofeiZ mofeiZ Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR (the rewrite) preserves existing behavior, but the knownMutated logic is removed altogether in #32097 (which simplifies this pass)

Copy link
Contributor

@josephsavona josephsavona left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We reviewed in-person, this looks great! Super exciting, nice work as always!

LoweredFunction dependencies were exclusively used for dependency extraction (in `propagateScopeDeps`). Now that we have a `propagateScopeDepsHIR` that recursively traverses into nested functions, we can delete `dependencies` and their associated synthetic `LoadLocal`/`PropertyLoad` instructions.

[Internal snapshot diff](https://www.internalfb.com/phabricator/paste/view/P1716950202) for this change shows ~.2% of files changed. I [read through ~60 of the changed files](https://www.internalfb.com/phabricator/paste/view/P1733074307)
- most changes are due to better outlining (due to better DCE)
- a few changes in memo inference are due to changed ordering
```
// source
arr.map(() => contextVar.inner);

// previous instructions
$0 = LoadLocal arr
$1 = $0.map
// Below instructions are synthetic
$2 = LoadLocal contextVar
$3 = $2.inner
$4 = Function deps=$3 context=contextVar {
  ...
}
```
- a few changes are effectively bugfixes (see `aliased-nested-scope-fn-expr`)
@mofeiZ mofeiZ merged commit d99f8bb into main Feb 18, 2025
22 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants