Skip to content

[compiler][be] Make program traversal more readable #33147

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

Merged
merged 1 commit into from
May 9, 2025
Merged

[compiler][be] Make program traversal more readable #33147

merged 1 commit into from
May 9, 2025

Conversation

mofeiZ
Copy link
Contributor

@mofeiZ mofeiZ commented May 7, 2025

React Compiler's program traversal logic is pretty lengthy and complex as we've added a lot of features piecemeal. compileProgram is 300+ lines long and has confusing control flow (defining helpers inline, invoking visitors, mutating-asts-while-iterating, mutating global ALREADY_COMPILED state).

  • Moved more stuff to ProgramContext
  • Separated compileProgram into a bunch of helpers

Tested by syncing this stack to a Meta codebase and observing no compilation output changes (D74487851, P1806855669, P1806855379)

Stack created with Sapling. Best reviewed with ReviewStack.

Copy link
Member

@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.

Exciting! I agree this has all become a bit of a mess. I need to do a proper pass through but at a glance it looks great

* @returns the compiled function or null if the function was skipped (due to
* config settings and/or outputs)
*/
function processFn(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

processFn now has compilation control flow logic. This is mainly:

  • retry logic
  • determining whether a compiled function should be emitted, according to compiler options and directive opt-ins / opt-outs

): Array<t.Directive> {
return directives.filter(directive =>
OPT_OUT_DIRECTIVES.has(directive.value.value),
): t.Directive | null {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We were only checking if the returned array had entries. Note that we do use loc from this directive sometimes, which is why this isn't refactored to just return a boolean

Comment on lines -242 to -248
/*
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
* consistently respect the `skip()` function to avoid revisiting a node within
* a pass, so we use this set to track nodes that we have compiled.
*/
const ALREADY_COMPILED: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();

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 is now moved to ProgramContext

Comment on lines 285 to +288
export function compileProgram(
program: NodePath<t.Program>,
pass: CompilerPass,
): CompileProgramResult | null {
): CompileProgramMetadata | null {
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 is now much shorter. There are no semantically meaningful changes (most lines were copied and pasted)

@mofeiZ mofeiZ force-pushed the pr33147 branch 2 times, most recently from 23c45ff to 6002392 Compare May 8, 2025 18:54
@mofeiZ mofeiZ marked this pull request as ready for review May 8, 2025 18:55
Comment on lines 484 to 491
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compileResult = {
kind: 'compile',
compiledFn: retryResult,
};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a retry-no-emit fixture to test that we're preserving behavior

const referencedBeforeDeclared =
pass.opts.gating != null
? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
: null;
for (const result of compiledFns) {
const {kind, originalFn, compiledFn} = result;
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
programContext.alreadyCompiled.add(transformedFn);
Copy link
Contributor Author

@mofeiZ mofeiZ May 8, 2025

Choose a reason for hiding this comment

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

This is really a no-op since we only use alreadyCompiled when making an initial pass over the program to collect react functions for compilation

@mofeiZ mofeiZ force-pushed the pr33147 branch 2 times, most recently from 1c521f5 to d80c6a2 Compare May 8, 2025 23:09
Copy link
Member

@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.

Nice! See comments for a couple things but overall the refactoring definitely makes this easier to follow (the git diff is hard to follow bc of how much is just moving and formatting differences, but just looking at the new version in its entirety, the improvement is clear)

Comment on lines +370 to +373
// Avoid modifying the program if we find a program level opt-out
if (findDirectiveDisablingMemoization(program.node.directives) != null) {
return null;
}
Copy link
Member

Choose a reason for hiding this comment

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

hmm, insertNewOutlinedFunctionNode() mutates the program and can have been called already. Shouldn't we check this first before processing the queue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept this PR as refactoring only, #33148 makes that correction (see PR and its changed test fixtures)

}

// Insert React Compiler generated functions into the Babel AST
applyCompiledFunctions(program, compiledFns, pass, programContext);
Copy link
Member

Choose a reason for hiding this comment

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

alternatively we could change insertNewOutlinedFunctionNode() to not mutate and do the mutation portion of it here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The tricky part is that our pipeline needs to operate on a NodePath (primarily for identifier scope resolution). We can only get a NodePath by mutating the existing program AST or creating a placeholder wrapper program (which isn't ideal since we'd lose variable scope analysis, module imports, etc)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd also greatly prefer to apply all compiled functions after all transformations, as transformations may throw / fail or we may re-introduce program-level bailouts. Would love any ideas on how to achieve that!

return null;
}
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(fn, fnType, programContext);
Copy link
Member

Choose a reason for hiding this comment

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

doesn't this mean we'll always compile even if a file is opted out of memoization (even if we're not in ignoreUseNoForget=true mode?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah. This is just a refactoring PR, happy to change this in a follow up

React Compiler's program traversal logic is pretty lengthy and complex as we've added a lot of features piecemeal. `compileProgram` is 300+ lines long and has confusing control flow (defining helpers inline, invoking visitors, mutating-asts-while-iterating, mutating global `ALREADY_COMPILED` state).

- Moved more stuff to `ProgramContext`
- Separated `compileProgram` into a bunch of helpers

Tested by syncing this stack to a Meta codebase and observing no compilation output changes (D74487851, P1806855669, P1806855379)
@mofeiZ mofeiZ merged commit 5069e18 into main May 9, 2025
28 of 29 checks passed
github-actions bot pushed a commit that referenced this pull request May 9, 2025
React Compiler's program traversal logic is pretty lengthy and complex
as we've added a lot of features piecemeal. `compileProgram` is 300+
lines long and has confusing control flow (defining helpers inline,
invoking visitors, mutating-asts-while-iterating, mutating global
`ALREADY_COMPILED` state).

- Moved more stuff to `ProgramContext`
- Separated `compileProgram` into a bunch of helpers

Tested by syncing this stack to a Meta codebase and observing no
compilation output changes (D74487851, P1806855669, P1806855379)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33147).
* #33149
* #33148
* __->__ #33147

DiffTrain build for [5069e18](5069e18)
github-actions bot pushed a commit that referenced this pull request May 9, 2025
React Compiler's program traversal logic is pretty lengthy and complex
as we've added a lot of features piecemeal. `compileProgram` is 300+
lines long and has confusing control flow (defining helpers inline,
invoking visitors, mutating-asts-while-iterating, mutating global
`ALREADY_COMPILED` state).

- Moved more stuff to `ProgramContext`
- Separated `compileProgram` into a bunch of helpers

Tested by syncing this stack to a Meta codebase and observing no
compilation output changes (D74487851, P1806855669, P1806855379)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33147).
* #33149
* #33148
* __->__ #33147

DiffTrain build for [5069e18](5069e18)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants