Compare commits
112 Commits
pr32099
...
component-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca1d5e068b | ||
|
|
ecb1861dd8 | ||
|
|
a5861172c6 | ||
|
|
df0a663a8c | ||
|
|
6c71a7766d | ||
|
|
9275c835c3 | ||
|
|
9cae1cea4e | ||
|
|
183bd4feac | ||
|
|
81c3a5331e | ||
|
|
ab86a5efe8 | ||
|
|
1e4614bf13 | ||
|
|
c5ab27a649 | ||
|
|
049bfbb169 | ||
|
|
a85b0b0bb4 | ||
|
|
789e5f02c5 | ||
|
|
d6d929e2f1 | ||
|
|
26315d64dc | ||
|
|
2852c9d08c | ||
|
|
94718f18b4 | ||
|
|
462d08f9ba | ||
|
|
6060367ef8 | ||
|
|
c250b7d980 | ||
|
|
4448b18760 | ||
|
|
4a45ba92c4 | ||
|
|
08cb2d7ee7 | ||
|
|
203df2c940 | ||
|
|
65b5aae010 | ||
|
|
3f67d0857e | ||
|
|
96eb84e493 | ||
|
|
63d664b220 | ||
|
|
d85f86cf01 | ||
|
|
76dddd1d57 | ||
|
|
3a5b326d81 | ||
|
|
59440424d0 | ||
|
|
b480865db0 | ||
|
|
62d3f36ea7 | ||
|
|
0cac32d60d | ||
|
|
676f0879f3 | ||
|
|
997c7bc930 | ||
|
|
b94603b955 | ||
|
|
2bcf06b692 | ||
|
|
5d04d73274 | ||
|
|
3820740a7f | ||
|
|
5069e18060 | ||
|
|
21fdf308a1 | ||
|
|
4ca97e4891 | ||
|
|
9b79292ae7 | ||
|
|
ac06829246 | ||
|
|
38ef6550a8 | ||
|
|
b629a865fb | ||
|
|
fbe7bc21b9 | ||
|
|
9518f11856 | ||
|
|
557a64795c | ||
|
|
a75932b2ea | ||
|
|
8fa3dfc845 | ||
|
|
8a8df5dbdd | ||
|
|
946da518eb | ||
|
|
a437c99ff7 | ||
|
|
4206fe4982 | ||
|
|
4a702865dd | ||
|
|
0ff1d13b80 | ||
|
|
53c9f81049 | ||
|
|
e5a8de81e5 | ||
|
|
7a2c7045ae | ||
|
|
0e5c79cfea | ||
|
|
845d93742f | ||
|
|
54a50729cc | ||
|
|
587cb8f896 | ||
|
|
79586c7eb6 | ||
|
|
edf550b679 | ||
|
|
b9cfa0d308 | ||
|
|
c129c2424b | ||
|
|
0c1575cee8 | ||
|
|
52ea641449 | ||
|
|
3ec88e797f | ||
|
|
0ca8420f9d | ||
|
|
0db8db178c | ||
|
|
8570116bd1 | ||
|
|
4f1d2ddf95 | ||
|
|
73d7e816b7 | ||
|
|
ac2cae5245 | ||
|
|
66de8e5a99 | ||
|
|
26ecc98a00 | ||
|
|
f0ca53d133 | ||
|
|
e39b380a21 | ||
|
|
9de0304ad7 | ||
|
|
0d695bea10 | ||
|
|
4c4a57c4f9 | ||
|
|
dc2b11817b | ||
|
|
b5450b0738 | ||
|
|
f150c046ec | ||
|
|
12b094d2f6 | ||
|
|
e5f0315efa | ||
|
|
f739642745 | ||
|
|
0ed6ceb9f6 | ||
|
|
ee7fee8f88 | ||
|
|
ee077b6ccd | ||
|
|
bb57fa7351 | ||
|
|
e9db3cc2d4 | ||
|
|
d8074cbc79 | ||
|
|
71797c871b | ||
|
|
9d795d3808 | ||
|
|
12f4cb85c5 | ||
|
|
90a124a980 | ||
|
|
49ea8bf569 | ||
|
|
9a52ad9fd9 | ||
|
|
fa8e3a251e | ||
|
|
408d055a3b | ||
|
|
fbf29ccaa3 | ||
|
|
62960c67c8 | ||
|
|
cd4e4d7599 | ||
|
|
18212ca960 |
@@ -496,6 +496,7 @@ module.exports = {
|
||||
'packages/react-devtools-shared/src/devtools/views/**/*.js',
|
||||
'packages/react-devtools-shared/src/hook.js',
|
||||
'packages/react-devtools-shared/src/backend/console.js',
|
||||
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
|
||||
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
|
||||
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
|
||||
],
|
||||
@@ -504,6 +505,7 @@ module.exports = {
|
||||
__IS_FIREFOX__: 'readonly',
|
||||
__IS_EDGE__: 'readonly',
|
||||
__IS_NATIVE__: 'readonly',
|
||||
__IS_INTERNAL_MCP_BUILD__: 'readonly',
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
chrome: 'readonly',
|
||||
},
|
||||
@@ -579,6 +581,7 @@ module.exports = {
|
||||
JSONValue: 'readonly',
|
||||
JSResourceReference: 'readonly',
|
||||
MouseEventHandler: 'readonly',
|
||||
NavigateEvent: 'readonly',
|
||||
PropagationPhases: 'readonly',
|
||||
PropertyDescriptor: 'readonly',
|
||||
React$AbstractComponent: 'readonly',
|
||||
@@ -634,5 +637,6 @@ module.exports = {
|
||||
AsyncLocalStorage: 'readonly',
|
||||
async_hooks: 'readonly',
|
||||
globalThis: 'readonly',
|
||||
navigation: 'readonly',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ jobs:
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
14
.github/workflows/runtime_commit_artifacts.yml
vendored
14
.github/workflows/runtime_commit_artifacts.yml
vendored
@@ -132,9 +132,9 @@ jobs:
|
||||
mkdir ./compiled/facebook-www/__test_utils__
|
||||
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
|
||||
|
||||
# Move eslint-plugin-react-hooks into eslint-plugin-react-hooks
|
||||
# Copy eslint-plugin-react-hooks
|
||||
mkdir ./compiled/eslint-plugin-react-hooks
|
||||
mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
./compiled/eslint-plugin-react-hooks/index.js
|
||||
|
||||
# Move unstable_server-external-runtime.js into facebook-www
|
||||
@@ -167,6 +167,12 @@ jobs:
|
||||
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
|
||||
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
|
||||
|
||||
# Copy eslint-plugin-react-hooks
|
||||
# NOTE: This is different from www, here we include the full package
|
||||
# including package.json to include dependencies in fbsource.
|
||||
mkdir "$BASE_FOLDER/tools"
|
||||
cp -r build/oss-experimental/eslint-plugin-react-hooks "$BASE_FOLDER/tools"
|
||||
|
||||
# Move React Native version file
|
||||
mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB
|
||||
|
||||
@@ -326,10 +332,10 @@ jobs:
|
||||
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
|
||||
echo "===================="
|
||||
# Ignore REVISION or lines removing @generated headers.
|
||||
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
|
||||
if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
|
||||
echo "Changes detected"
|
||||
echo "===== Changes ====="
|
||||
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
|
||||
git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
|
||||
echo "==================="
|
||||
echo "should_commit=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
|
||||
1
.github/workflows/runtime_discord_notify.yml
vendored
1
.github/workflows/runtime_discord_notify.yml
vendored
@@ -15,6 +15,7 @@ jobs:
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## 19.1.0-rc.2 (May 14, 2025)
|
||||
|
||||
## babel-plugin-react-compiler
|
||||
|
||||
* Fix for string attribute values with emoji [#33096](https://github.com/facebook/react/pull/33096) by [@josephsavona](https://github.com/josephsavona)
|
||||
|
||||
## 19.1.0-rc.1 (April 21, 2025)
|
||||
|
||||
## eslint-plugin-react-hooks
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode(all)
|
||||
@compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
@@ -92,7 +92,7 @@ function useFoo(propVal: {+baz: number}) {
|
||||
},
|
||||
{
|
||||
name: 'compilationMode-infer',
|
||||
input: `// @compilationMode(infer)
|
||||
input: `// @compilationMode:"infer"
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
@@ -101,7 +101,7 @@ function nonReactFn() {
|
||||
},
|
||||
{
|
||||
name: 'compilationMode-all',
|
||||
input: `// @compilationMode(all)
|
||||
input: `// @compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ import {
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {ExternalFunction, isHookName} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {CompilerReactTarget} from './Options';
|
||||
import {getReactCompilerRuntimeModule} from './Program';
|
||||
import {LoggerEvent, PluginOptions} from './Options';
|
||||
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
|
||||
import {SuppressionRange} from './Suppression';
|
||||
|
||||
export function validateRestrictedImports(
|
||||
path: NodePath<t.Program>,
|
||||
@@ -52,32 +53,65 @@ export function validateRestrictedImports(
|
||||
}
|
||||
}
|
||||
|
||||
type ProgramContextOptions = {
|
||||
program: NodePath<t.Program>;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
};
|
||||
export class ProgramContext {
|
||||
/* Program and environment context */
|
||||
/**
|
||||
* Program and environment context
|
||||
*/
|
||||
scope: BabelScope;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
reactRuntimeModule: string;
|
||||
hookPattern: string | null;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
|
||||
/*
|
||||
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
|
||||
* consistently respect the `skip()` function to avoid revisiting a node within
|
||||
* a pass, so we use this set to track nodes that we have compiled.
|
||||
*/
|
||||
alreadyCompiled: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
|
||||
// known generated or referenced identifiers in the program
|
||||
knownReferencedNames: Set<string> = new Set();
|
||||
// generated imports
|
||||
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
|
||||
|
||||
constructor(
|
||||
program: NodePath<t.Program>,
|
||||
reactRuntimeModule: CompilerReactTarget,
|
||||
hookPattern: string | null,
|
||||
) {
|
||||
this.hookPattern = hookPattern;
|
||||
/**
|
||||
* Metadata from compilation
|
||||
*/
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
|
||||
inferredEffectLocations: Set<t.SourceLocation> = new Set();
|
||||
|
||||
constructor({
|
||||
program,
|
||||
suppressions,
|
||||
opts,
|
||||
filename,
|
||||
code,
|
||||
hasModuleScopeOptOut,
|
||||
}: ProgramContextOptions) {
|
||||
this.scope = program.scope;
|
||||
this.reactRuntimeModule = getReactCompilerRuntimeModule(reactRuntimeModule);
|
||||
this.opts = opts;
|
||||
this.filename = filename;
|
||||
this.code = code;
|
||||
this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target);
|
||||
this.suppressions = suppressions;
|
||||
this.hasModuleScopeOptOut = hasModuleScopeOptOut;
|
||||
}
|
||||
|
||||
isHookName(name: string): boolean {
|
||||
if (this.hookPattern == null) {
|
||||
if (this.opts.environment.hookPattern == null) {
|
||||
return isHookName(name);
|
||||
} else {
|
||||
const match = new RegExp(this.hookPattern).exec(name);
|
||||
const match = new RegExp(this.opts.environment.hookPattern).exec(name);
|
||||
return (
|
||||
match != null && typeof match[1] === 'string' && isHookName(match[1])
|
||||
);
|
||||
@@ -179,6 +213,12 @@ export class ProgramContext {
|
||||
});
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
logEvent(event: LoggerEvent): void {
|
||||
if (this.opts.logger != null) {
|
||||
this.opts.logger.logEvent(this.filename, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExistingImports(
|
||||
|
||||
@@ -98,7 +98,7 @@ export type PluginOptions = {
|
||||
* provided rules will skip compilation. To disable this feature (never bailout of compilation
|
||||
* even if the default ESLint is suppressed), pass an empty array.
|
||||
*/
|
||||
eslintSuppressionRules?: Array<string> | null | undefined;
|
||||
eslintSuppressionRules: Array<string> | null | undefined;
|
||||
|
||||
flowSuppressions: boolean;
|
||||
/*
|
||||
@@ -106,7 +106,7 @@ export type PluginOptions = {
|
||||
*/
|
||||
ignoreUseNoForget: boolean;
|
||||
|
||||
sources?: Array<string> | ((filename: string) => boolean) | null;
|
||||
sources: Array<string> | ((filename: string) => boolean) | null;
|
||||
|
||||
/**
|
||||
* The compiler has customized support for react-native-reanimated, intended as a temporary workaround.
|
||||
|
||||
@@ -103,6 +103,7 @@ import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -129,7 +130,6 @@ function run(
|
||||
mode,
|
||||
config,
|
||||
contextIdentifiers,
|
||||
func,
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
@@ -275,6 +275,10 @@ function runWithEnvironment(
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoFreezingKnownMutableFunctions) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CompilerErrorDetail,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {EnvironmentConfig, ReactFunctionType} from '../HIR/Environment';
|
||||
import {ReactFunctionType} from '../HIR/Environment';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
|
||||
import {isHookDeclaration} from '../Utils/HookDeclaration';
|
||||
@@ -43,17 +43,21 @@ export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
|
||||
|
||||
export function findDirectiveEnablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): Array<t.Directive> {
|
||||
return directives.filter(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
): t.Directive | null {
|
||||
return (
|
||||
directives.find(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function findDirectiveDisablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): Array<t.Directive> {
|
||||
return directives.filter(directive =>
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value),
|
||||
): t.Directive | null {
|
||||
return (
|
||||
directives.find(directive =>
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,13 +92,16 @@ export type CompileResult = {
|
||||
|
||||
function logError(
|
||||
err: unknown,
|
||||
pass: CompilerPass,
|
||||
context: {
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
},
|
||||
fnLoc: t.SourceLocation | null,
|
||||
): void {
|
||||
if (pass.opts.logger) {
|
||||
if (context.opts.logger) {
|
||||
if (err instanceof CompilerError) {
|
||||
for (const detail of err.details) {
|
||||
pass.opts.logger.logEvent(pass.filename, {
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc,
|
||||
detail: detail.options,
|
||||
@@ -108,7 +115,7 @@ function logError(
|
||||
stringifiedError = err?.toString() ?? '[ null ]';
|
||||
}
|
||||
|
||||
pass.opts.logger.logEvent(pass.filename, {
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
kind: 'PipelineError',
|
||||
fnLoc,
|
||||
data: stringifiedError,
|
||||
@@ -118,13 +125,17 @@ function logError(
|
||||
}
|
||||
function handleError(
|
||||
err: unknown,
|
||||
pass: CompilerPass,
|
||||
context: {
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
},
|
||||
fnLoc: t.SourceLocation | null,
|
||||
): void {
|
||||
logError(err, pass, fnLoc);
|
||||
logError(err, context, fnLoc);
|
||||
if (
|
||||
pass.opts.panicThreshold === 'all_errors' ||
|
||||
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
|
||||
context.opts.panicThreshold === 'all_errors' ||
|
||||
(context.opts.panicThreshold === 'critical_errors' &&
|
||||
isCriticalError(err)) ||
|
||||
isConfigError(err) // Always throws regardless of panic threshold
|
||||
) {
|
||||
throw err;
|
||||
@@ -187,7 +198,6 @@ export function createNewFunctionNode(
|
||||
}
|
||||
}
|
||||
// Avoid visiting the new transformed version
|
||||
ALREADY_COMPILED.add(transformedFn);
|
||||
return transformedFn;
|
||||
}
|
||||
|
||||
@@ -239,13 +249,6 @@ function insertNewOutlinedFunctionNode(
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
|
||||
* consistently respect the `skip()` function to avoid revisiting a node within
|
||||
* a pass, so we use this set to track nodes that we have compiled.
|
||||
*/
|
||||
const ALREADY_COMPILED: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
|
||||
|
||||
const DEFAULT_ESLINT_SUPPRESSIONS = [
|
||||
'react-hooks/exhaustive-deps',
|
||||
'react-hooks/rules-of-hooks',
|
||||
@@ -268,41 +271,43 @@ function isFilePartOfSources(
|
||||
return false;
|
||||
}
|
||||
|
||||
export type CompileProgramResult = {
|
||||
export type CompileProgramMetadata = {
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
|
||||
inferredEffectLocations: Set<t.SourceLocation>;
|
||||
};
|
||||
/**
|
||||
* `compileProgram` is directly invoked by the react-compiler babel plugin, so
|
||||
* exceptions thrown by this function will fail the babel build.
|
||||
* - call `handleError` if your error is recoverable.
|
||||
* Unless the error is a warning / info diagnostic, compilation of a function
|
||||
* / entire file should also be skipped.
|
||||
* - throw an exception if the error is fatal / not recoverable.
|
||||
* Examples of this are invalid compiler configs or failure to codegen outlined
|
||||
* functions *after* already emitting optimized components / hooks that invoke
|
||||
* the outlined functions.
|
||||
* Main entrypoint for React Compiler.
|
||||
*
|
||||
* @param program The Babel program node to compile
|
||||
* @param pass Compiler configuration and context
|
||||
* @returns Compilation results or null if compilation was skipped
|
||||
*/
|
||||
export function compileProgram(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
): CompileProgramResult | null {
|
||||
): CompileProgramMetadata | null {
|
||||
/**
|
||||
* This is directly invoked by the react-compiler babel plugin, so exceptions
|
||||
* thrown by this function will fail the babel build.
|
||||
* - call `handleError` if your error is recoverable.
|
||||
* Unless the error is a warning / info diagnostic, compilation of a function
|
||||
* / entire file should also be skipped.
|
||||
* - throw an exception if the error is fatal / not recoverable.
|
||||
* Examples of this are invalid compiler configs or failure to codegen outlined
|
||||
* functions *after* already emitting optimized components / hooks that invoke
|
||||
* the outlined functions.
|
||||
*/
|
||||
if (shouldSkipCompilation(program, pass)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const environment = pass.opts.environment;
|
||||
const restrictedImportsErr = validateRestrictedImports(program, environment);
|
||||
const restrictedImportsErr = validateRestrictedImports(
|
||||
program,
|
||||
pass.opts.environment,
|
||||
);
|
||||
if (restrictedImportsErr) {
|
||||
handleError(restrictedImportsErr, pass, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const programContext = new ProgramContext(
|
||||
program,
|
||||
pass.opts.target,
|
||||
environment.hookPattern,
|
||||
);
|
||||
/*
|
||||
* Record lint errors and critical errors as depending on Forget's config,
|
||||
* we may still need to run Forget's analysis on every function (even if we
|
||||
@@ -313,16 +318,102 @@ export function compileProgram(
|
||||
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
|
||||
pass.opts.flowSuppressions,
|
||||
);
|
||||
const queue: Array<{
|
||||
kind: 'original' | 'outlined';
|
||||
fn: BabelFn;
|
||||
fnType: ReactFunctionType;
|
||||
}> = [];
|
||||
|
||||
const programContext = new ProgramContext({
|
||||
program: program,
|
||||
opts: pass.opts,
|
||||
filename: pass.filename,
|
||||
code: pass.code,
|
||||
suppressions,
|
||||
hasModuleScopeOptOut:
|
||||
findDirectiveDisablingMemoization(program.node.directives) != null,
|
||||
});
|
||||
|
||||
const queue: Array<CompileSource> = findFunctionsToCompile(
|
||||
program,
|
||||
pass,
|
||||
programContext,
|
||||
);
|
||||
const compiledFns: Array<CompileResult> = [];
|
||||
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.shift()!;
|
||||
const compiled = processFn(current.fn, current.fnType, programContext);
|
||||
|
||||
if (compiled != null) {
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
loc: outlined.fn.loc,
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
current.fn,
|
||||
outlined.fn,
|
||||
);
|
||||
fn.skip();
|
||||
programContext.alreadyCompiled.add(fn.node);
|
||||
if (outlined.type !== null) {
|
||||
queue.push({
|
||||
kind: 'outlined',
|
||||
fn,
|
||||
fnType: outlined.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
compiledFns.push({
|
||||
kind: current.kind,
|
||||
originalFn: current.fn,
|
||||
compiledFn: compiled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid modifying the program if we find a program level opt-out
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
if (compiledFns.length > 0) {
|
||||
const error = new CompilerError();
|
||||
error.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
reason:
|
||||
'Unexpected compiled functions when module scope opt-out is present',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
handleError(error, programContext, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Insert React Compiler generated functions into the Babel AST
|
||||
applyCompiledFunctions(program, compiledFns, pass, programContext);
|
||||
|
||||
return {
|
||||
retryErrors: programContext.retryErrors,
|
||||
inferredEffectLocations: programContext.inferredEffectLocations,
|
||||
};
|
||||
}
|
||||
|
||||
type CompileSource = {
|
||||
kind: 'original' | 'outlined';
|
||||
fn: BabelFn;
|
||||
fnType: ReactFunctionType;
|
||||
};
|
||||
/**
|
||||
* Find all React components and hooks that need to be compiled
|
||||
*
|
||||
* @returns An array of React functions from @param program to transform
|
||||
*/
|
||||
function findFunctionsToCompile(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): Array<CompileSource> {
|
||||
const queue: Array<CompileSource> = [];
|
||||
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
|
||||
const fnType = getReactFunctionType(fn, pass, environment);
|
||||
if (fnType === null || ALREADY_COMPILED.has(fn.node)) {
|
||||
const fnType = getReactFunctionType(fn, pass);
|
||||
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -331,7 +422,7 @@ export function compileProgram(
|
||||
* traversal will loop infinitely.
|
||||
* Ensure we avoid visiting the original function again.
|
||||
*/
|
||||
ALREADY_COMPILED.add(fn.node);
|
||||
programContext.alreadyCompiled.add(fn.node);
|
||||
fn.skip();
|
||||
|
||||
queue.push({kind: 'original', fn, fnType});
|
||||
@@ -346,7 +437,6 @@ export function compileProgram(
|
||||
* can reference `this` which is unsafe for compilation
|
||||
*/
|
||||
node.skip();
|
||||
return;
|
||||
},
|
||||
|
||||
ClassExpression(node: NodePath<t.ClassExpression>) {
|
||||
@@ -355,7 +445,6 @@ export function compileProgram(
|
||||
* can reference `this` which is unsafe for compilation
|
||||
*/
|
||||
node.skip();
|
||||
return;
|
||||
},
|
||||
|
||||
FunctionDeclaration: traverseFunction,
|
||||
@@ -370,205 +459,206 @@ export function compileProgram(
|
||||
filename: pass.filename ?? null,
|
||||
},
|
||||
);
|
||||
const retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
|
||||
const inferredEffectLocations = new Set<t.SourceLocation>();
|
||||
const processFn = (
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
): null | CodegenFunction => {
|
||||
let optInDirectives: Array<t.Directive> = [];
|
||||
let optOutDirectives: Array<t.Directive> = [];
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
optInDirectives = findDirectiveEnablingMemoization(
|
||||
fn.node.body.directives,
|
||||
);
|
||||
optOutDirectives = findDirectiveDisablingMemoization(
|
||||
fn.node.body.directives,
|
||||
);
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
* applies to this function first.
|
||||
*/
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
|
||||
suppressions,
|
||||
fn,
|
||||
);
|
||||
let compileResult:
|
||||
| {kind: 'compile'; compiledFn: CodegenFunction}
|
||||
| {kind: 'error'; error: unknown};
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
compileResult = {
|
||||
kind: 'error',
|
||||
error: suppressionsToCompilerError(suppressionsInFunction),
|
||||
};
|
||||
/**
|
||||
* Try to compile a source function, taking into account all local suppressions,
|
||||
* opt-ins, and opt-outs.
|
||||
*
|
||||
* Errors encountered during compilation are either logged (if recoverable) or
|
||||
* thrown (if non-recoverable).
|
||||
*
|
||||
* @returns the compiled function or null if the function was skipped (due to
|
||||
* config settings and/or outputs)
|
||||
*/
|
||||
function processFn(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
): null | CodegenFunction {
|
||||
let directives;
|
||||
if (fn.node.body.type !== 'BlockStatement') {
|
||||
directives = {optIn: null, optOut: null};
|
||||
} else {
|
||||
directives = {
|
||||
optIn: findDirectiveEnablingMemoization(fn.node.body.directives),
|
||||
optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
|
||||
};
|
||||
}
|
||||
|
||||
let compiledFn: CodegenFunction;
|
||||
const compileResult = tryCompileFunction(fn, fnType, programContext);
|
||||
if (compileResult.kind === 'error') {
|
||||
if (directives.optOut != null) {
|
||||
logError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
} else {
|
||||
try {
|
||||
compileResult = {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
programContext,
|
||||
pass.opts.logger,
|
||||
pass.filename,
|
||||
pass.code,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
compileResult = {kind: 'error', error: err};
|
||||
}
|
||||
handleError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
}
|
||||
|
||||
if (compileResult.kind === 'error') {
|
||||
/**
|
||||
* If an opt out directive is present, log only instead of throwing and don't mark as
|
||||
* containing a critical error.
|
||||
*/
|
||||
if (optOutDirectives.length > 0) {
|
||||
logError(compileResult.error, pass, fn.node.loc ?? null);
|
||||
} else {
|
||||
handleError(compileResult.error, pass, fn.node.loc ?? null);
|
||||
}
|
||||
// If non-memoization features are enabled, retry regardless of error kind
|
||||
if (
|
||||
!(environment.enableFire || environment.inferEffectDependencies != null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
compileResult = {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
programContext,
|
||||
pass.opts.logger,
|
||||
pass.filename,
|
||||
pass.code,
|
||||
),
|
||||
};
|
||||
if (
|
||||
!compileResult.compiledFn.hasFireRewrite &&
|
||||
!compileResult.compiledFn.hasInferredEffect
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: we might want to log error here, but this will also result in duplicate logging
|
||||
if (err instanceof CompilerError) {
|
||||
retryErrors.push({fn, error: err});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
|
||||
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
|
||||
* unused 'use no forget/memo' directive.
|
||||
*/
|
||||
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
|
||||
for (const directive of optOutDirectives) {
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSkip',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
reason: `Skipped due to '${directive.value.value}' directive.`,
|
||||
loc: directive.loc ?? null,
|
||||
});
|
||||
}
|
||||
const retryResult = retryCompileFunction(fn, fnType, programContext);
|
||||
if (retryResult == null) {
|
||||
return null;
|
||||
}
|
||||
compiledFn = retryResult;
|
||||
} else {
|
||||
compiledFn = compileResult.compiledFn;
|
||||
}
|
||||
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSuccess',
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
fnName: compileResult.compiledFn.id?.name ?? null,
|
||||
memoSlots: compileResult.compiledFn.memoSlotsUsed,
|
||||
memoBlocks: compileResult.compiledFn.memoBlocks,
|
||||
memoValues: compileResult.compiledFn.memoValues,
|
||||
prunedMemoBlocks: compileResult.compiledFn.prunedMemoBlocks,
|
||||
prunedMemoValues: compileResult.compiledFn.prunedMemoValues,
|
||||
/**
|
||||
* If 'use no forget/memo' is present and we still ran the code through the
|
||||
* compiler for validation, log a skip event and don't mutate the babel AST.
|
||||
* This allows us to flag if there is an unused 'use no forget/memo'
|
||||
* directive.
|
||||
*/
|
||||
if (
|
||||
programContext.opts.ignoreUseNoForget === false &&
|
||||
directives.optOut != null
|
||||
) {
|
||||
programContext.logEvent({
|
||||
kind: 'CompileSkip',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
reason: `Skipped due to '${directives.optOut.value}' directive.`,
|
||||
loc: directives.optOut.loc ?? null,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
programContext.logEvent({
|
||||
kind: 'CompileSuccess',
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
fnName: compiledFn.id?.name ?? null,
|
||||
memoSlots: compiledFn.memoSlotsUsed,
|
||||
memoBlocks: compiledFn.memoBlocks,
|
||||
memoValues: compiledFn.memoValues,
|
||||
prunedMemoBlocks: compiledFn.prunedMemoBlocks,
|
||||
prunedMemoValues: compiledFn.prunedMemoValues,
|
||||
});
|
||||
|
||||
/**
|
||||
* Always compile functions with opt in directives.
|
||||
*/
|
||||
if (optInDirectives.length > 0) {
|
||||
return compileResult.compiledFn;
|
||||
} else if (pass.opts.compilationMode === 'annotation') {
|
||||
/**
|
||||
* No opt-in directive in annotation mode, so don't insert the compiled function.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pass.opts.noEmit) {
|
||||
return compileResult.compiledFn;
|
||||
}
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
return null;
|
||||
} else if (programContext.opts.noEmit) {
|
||||
/**
|
||||
* 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 compileResult.compiledFn.inferredEffectLocations) {
|
||||
if (loc !== GeneratedSource) inferredEffectLocations.add(loc);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.shift()!;
|
||||
const compiled = processFn(current.fn, current.fnType);
|
||||
if (compiled === null) {
|
||||
continue;
|
||||
}
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
loc: outlined.fn.loc,
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
current.fn,
|
||||
outlined.fn,
|
||||
);
|
||||
fn.skip();
|
||||
ALREADY_COMPILED.add(fn.node);
|
||||
if (outlined.type !== null) {
|
||||
queue.push({
|
||||
kind: 'outlined',
|
||||
fn,
|
||||
fnType: outlined.type,
|
||||
});
|
||||
for (const loc of compiledFn.inferredEffectLocations) {
|
||||
if (loc !== GeneratedSource) {
|
||||
programContext.inferredEffectLocations.add(loc);
|
||||
}
|
||||
}
|
||||
compiledFns.push({
|
||||
kind: current.kind,
|
||||
compiledFn: compiled,
|
||||
originalFn: current.fn,
|
||||
});
|
||||
return null;
|
||||
} else if (
|
||||
programContext.opts.compilationMode === 'annotation' &&
|
||||
directives.optIn == null
|
||||
) {
|
||||
/**
|
||||
* If no opt-in directive is found and the compiler is configured in
|
||||
* annotation mode, don't insert the compiled function.
|
||||
*/
|
||||
return null;
|
||||
} else {
|
||||
return compiledFn;
|
||||
}
|
||||
}
|
||||
|
||||
function tryCompileFunction(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
):
|
||||
| {kind: 'compile'; compiledFn: CodegenFunction}
|
||||
| {kind: 'error'; error: unknown} {
|
||||
/**
|
||||
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
* applies to this function first.
|
||||
*/
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
|
||||
programContext.suppressions,
|
||||
fn,
|
||||
);
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
return {
|
||||
kind: 'error',
|
||||
error: suppressionsToCompilerError(suppressionsInFunction),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not modify source if there is a module scope level opt out directive.
|
||||
*/
|
||||
const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
|
||||
program.node.directives,
|
||||
);
|
||||
if (moduleScopeOptOutDirectives.length > 0) {
|
||||
try {
|
||||
return {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
programContext.opts.environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
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;
|
||||
}
|
||||
/*
|
||||
* Only insert Forget-ified functions if we have not encountered a critical
|
||||
* error elsewhere in the file, regardless of bailout mode.
|
||||
/**
|
||||
* Note that function suppressions are not checked in the retry pipeline, as
|
||||
* they only affect auto-memoization features.
|
||||
*/
|
||||
try {
|
||||
const retryResult = compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
);
|
||||
|
||||
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
|
||||
return null;
|
||||
}
|
||||
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});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies React Compiler generated functions to the babel AST by replacing
|
||||
* existing functions in place or inserting new declarations.
|
||||
*/
|
||||
function applyCompiledFunctions(
|
||||
program: NodePath<t.Program>,
|
||||
compiledFns: Array<CompileResult>,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const referencedBeforeDeclared =
|
||||
pass.opts.gating != null
|
||||
? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
|
||||
@@ -576,6 +666,7 @@ export function compileProgram(
|
||||
for (const result of compiledFns) {
|
||||
const {kind, originalFn, compiledFn} = result;
|
||||
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
|
||||
programContext.alreadyCompiled.add(transformedFn);
|
||||
|
||||
if (referencedBeforeDeclared != null && kind === 'original') {
|
||||
CompilerError.invariant(pass.opts.gating != null, {
|
||||
@@ -598,7 +689,6 @@ export function compileProgram(
|
||||
if (compiledFns.length > 0) {
|
||||
addImportsToProgram(program, programContext);
|
||||
}
|
||||
return {retryErrors, inferredEffectLocations};
|
||||
}
|
||||
|
||||
function shouldSkipCompilation(
|
||||
@@ -640,14 +730,10 @@ function shouldSkipCompilation(
|
||||
function getReactFunctionType(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
/**
|
||||
* TODO(mofeiZ): remove once we validate PluginOptions with Zod
|
||||
*/
|
||||
environment: EnvironmentConfig,
|
||||
): ReactFunctionType | null {
|
||||
const hookPattern = environment.hookPattern;
|
||||
const hookPattern = pass.opts.environment.hookPattern;
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
|
||||
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null)
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {Environment} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {CompileProgramResult} from './Program';
|
||||
import {CompileProgramMetadata} from './Program';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
@@ -109,7 +109,7 @@ export default function validateNoUntransformedReferences(
|
||||
filename: string | null,
|
||||
logger: Logger | null,
|
||||
env: EnvironmentConfig,
|
||||
compileResult: CompileProgramResult | null,
|
||||
compileResult: CompileProgramMetadata | null,
|
||||
): void {
|
||||
const moduleLoadChecks = new Map<
|
||||
string,
|
||||
@@ -236,7 +236,7 @@ function transformProgram(
|
||||
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
|
||||
filename: string | null,
|
||||
logger: Logger | null,
|
||||
compileResult: CompileProgramResult | null,
|
||||
compileResult: CompileProgramMetadata | null,
|
||||
): void {
|
||||
const traversalState: TraversalState = {
|
||||
shouldInvalidateScopes: true,
|
||||
|
||||
@@ -70,14 +70,12 @@ import {BuiltInArrayId} from './ObjectShape';
|
||||
export function lower(
|
||||
func: NodePath<t.Function>,
|
||||
env: Environment,
|
||||
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
|
||||
bindings: Bindings | null = null,
|
||||
capturedRefs: Array<t.Identifier> = [],
|
||||
// the outermost function being compiled, in case lower() is called recursively (for lambdas)
|
||||
parent: NodePath<t.Function> | null = null,
|
||||
): Result<HIRFunction, CompilerError> {
|
||||
const builder = new HIRBuilder(env, {
|
||||
bindings,
|
||||
context: capturedRefs,
|
||||
});
|
||||
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
|
||||
const context: HIRFunction['context'] = [];
|
||||
|
||||
for (const ref of capturedRefs ?? []) {
|
||||
@@ -217,7 +215,7 @@ export function lower(
|
||||
return Ok({
|
||||
id,
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
fnType: parent == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
body: builder.build(),
|
||||
@@ -3419,7 +3417,7 @@ function lowerFunction(
|
||||
| t.ObjectMethod
|
||||
>,
|
||||
): LoweredFunction | null {
|
||||
const componentScope: Scope = builder.environment.parentFunction.scope;
|
||||
const componentScope: Scope = builder.parentFunction.scope;
|
||||
const capturedContext = gatherCapturedContext(expr, componentScope);
|
||||
|
||||
/*
|
||||
@@ -3430,10 +3428,13 @@ function lowerFunction(
|
||||
* This isn't a problem in practice because use Babel's scope analysis to
|
||||
* identify the correct references.
|
||||
*/
|
||||
const lowering = lower(expr, builder.environment, builder.bindings, [
|
||||
...builder.context,
|
||||
...capturedContext,
|
||||
]);
|
||||
const lowering = lower(
|
||||
expr,
|
||||
builder.environment,
|
||||
builder.bindings,
|
||||
[...builder.context, ...capturedContext],
|
||||
builder.parentFunction,
|
||||
);
|
||||
let loweredFunc: HIRFunction;
|
||||
if (lowering.isErr()) {
|
||||
lowering
|
||||
@@ -3455,7 +3456,7 @@ function lowerExpressionToTemporary(
|
||||
return lowerValueToTemporary(builder, value);
|
||||
}
|
||||
|
||||
export function lowerValueToTemporary(
|
||||
function lowerValueToTemporary(
|
||||
builder: HIRBuilder,
|
||||
value: InstructionValue,
|
||||
): Place {
|
||||
@@ -3608,31 +3609,40 @@ function lowerAssignment(
|
||||
|
||||
let temporary;
|
||||
if (builder.isContextIdentifier(lvalue)) {
|
||||
if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
|
||||
if (kind === InstructionKind.Const) {
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'DeclareContext',
|
||||
lvalue: {
|
||||
kind: InstructionKind.Let,
|
||||
place: {...place},
|
||||
},
|
||||
loc: place.loc,
|
||||
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'StoreContext',
|
||||
lvalue: {place: {...place}, kind: InstructionKind.Reassign},
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
if (
|
||||
kind !== InstructionKind.Const &&
|
||||
kind !== InstructionKind.Reassign &&
|
||||
kind !== InstructionKind.Let &&
|
||||
kind !== InstructionKind.Function
|
||||
) {
|
||||
builder.errors.push({
|
||||
reason: `Unexpected context variable kind`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
node: lvalueNode,
|
||||
loc: lvalueNode.loc ?? GeneratedSource,
|
||||
});
|
||||
} else {
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'StoreContext',
|
||||
lvalue: {place: {...place}, kind},
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const typeAnnotation = lvalue.get('typeAnnotation');
|
||||
let type: t.FlowType | t.TSType | null;
|
||||
|
||||
@@ -191,7 +191,6 @@ type TerminalRewriteInfo =
|
||||
| {
|
||||
kind: 'StartScope';
|
||||
blockId: BlockId;
|
||||
dependencyId: BlockId;
|
||||
fallthroughId: BlockId;
|
||||
instrId: InstructionId;
|
||||
scope: ReactiveScope;
|
||||
@@ -216,14 +215,12 @@ function pushStartScopeTerminal(
|
||||
scope: ReactiveScope,
|
||||
context: ScopeTraversalContext,
|
||||
): void {
|
||||
const dependencyId = context.env.nextBlockId;
|
||||
const blockId = context.env.nextBlockId;
|
||||
const fallthroughId = context.env.nextBlockId;
|
||||
context.rewrites.push({
|
||||
kind: 'StartScope',
|
||||
blockId,
|
||||
fallthroughId,
|
||||
dependencyId,
|
||||
instrId: scope.range.start,
|
||||
scope,
|
||||
});
|
||||
@@ -265,13 +262,10 @@ type RewriteContext = {
|
||||
* instr1, instr2, instr3, instr4, [[ original terminal ]]
|
||||
* Rewritten:
|
||||
* bb0:
|
||||
* instr1, [[ scope start dependencies=bb1 block=bb2]]
|
||||
* instr1, [[ scope start block=bb1]]
|
||||
* bb1:
|
||||
* [[ empty, filled in in PropagateScopeDependenciesHIR ]]
|
||||
* goto bb2
|
||||
* instr2, instr3, [[ scope end goto=bb2 ]]
|
||||
* bb2:
|
||||
* instr2, instr3, [[ scope end goto=bb3 ]]
|
||||
* bb3:
|
||||
* instr4, [[ original terminal ]]
|
||||
*/
|
||||
function handleRewrite(
|
||||
@@ -285,7 +279,6 @@ function handleRewrite(
|
||||
? {
|
||||
kind: 'scope',
|
||||
fallthrough: terminalInfo.fallthroughId,
|
||||
dependencies: terminalInfo.dependencyId,
|
||||
block: terminalInfo.blockId,
|
||||
scope: terminalInfo.scope,
|
||||
id: terminalInfo.instrId,
|
||||
@@ -312,28 +305,7 @@ function handleRewrite(
|
||||
context.nextPreds = new Set([currBlockId]);
|
||||
context.nextBlockId =
|
||||
terminalInfo.kind === 'StartScope'
|
||||
? terminalInfo.dependencyId
|
||||
? terminalInfo.blockId
|
||||
: terminalInfo.fallthroughId;
|
||||
context.instrSliceIdx = idx;
|
||||
|
||||
if (terminalInfo.kind === 'StartScope') {
|
||||
const currBlockId = context.nextBlockId;
|
||||
context.rewrites.push({
|
||||
kind: context.source.kind,
|
||||
id: currBlockId,
|
||||
instructions: [],
|
||||
preds: context.nextPreds,
|
||||
// Only the first rewrite should reuse source block phis
|
||||
phis: new Set(),
|
||||
terminal: {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: terminal.block,
|
||||
id: terminalInfo.instrId,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
});
|
||||
context.nextPreds = new Set([currBlockId]);
|
||||
context.nextBlockId = terminalInfo.blockId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,10 +241,7 @@ type PropertyPathNode =
|
||||
class PropertyPathRegistry {
|
||||
roots: Map<IdentifierId, RootNode> = new Map();
|
||||
|
||||
getOrCreateIdentifier(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
): PropertyPathNode {
|
||||
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
|
||||
/**
|
||||
* Reads from a statically scoped variable are always safe in JS,
|
||||
* with the exception of TDZ (not addressed by this pass).
|
||||
@@ -258,19 +255,12 @@ class PropertyPathRegistry {
|
||||
optionalProperties: new Map(),
|
||||
fullPath: {
|
||||
identifier,
|
||||
reactive,
|
||||
path: [],
|
||||
},
|
||||
hasOptional: false,
|
||||
parent: null,
|
||||
};
|
||||
this.roots.set(identifier.id, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
|
||||
reason:
|
||||
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
|
||||
loc: identifier.loc,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
@@ -288,7 +278,6 @@ class PropertyPathRegistry {
|
||||
parent: parent,
|
||||
fullPath: {
|
||||
identifier: parent.fullPath.identifier,
|
||||
reactive: parent.fullPath.reactive,
|
||||
path: parent.fullPath.path.concat(entry),
|
||||
},
|
||||
hasOptional: parent.hasOptional || entry.optional,
|
||||
@@ -304,7 +293,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);
|
||||
if (n.path.length === 0) {
|
||||
return currNode;
|
||||
}
|
||||
@@ -326,11 +315,10 @@ function getMaybeNonNullInInstruction(
|
||||
instr: InstructionValue,
|
||||
context: CollectHoistablePropertyLoadsContext,
|
||||
): PropertyPathNode | null {
|
||||
let path: ReactiveScopeDependency | null = null;
|
||||
let path = null;
|
||||
if (instr.kind === 'PropertyLoad') {
|
||||
path = context.temporaries.get(instr.object.identifier.id) ?? {
|
||||
identifier: instr.object.identifier,
|
||||
reactive: instr.object.reactive,
|
||||
path: [],
|
||||
};
|
||||
} else if (instr.kind === 'Destructure') {
|
||||
@@ -393,7 +381,7 @@ function collectNonNullsInBlocks(
|
||||
) {
|
||||
const identifier = fn.params[0].identifier;
|
||||
knownNonNullIdentifiers.add(
|
||||
context.registry.getOrCreateIdentifier(identifier, true),
|
||||
context.registry.getOrCreateIdentifier(identifier),
|
||||
);
|
||||
}
|
||||
const nodes = new Map<
|
||||
@@ -628,11 +616,9 @@ function reduceMaybeOptionalChains(
|
||||
changed = false;
|
||||
|
||||
for (const original of optionalChainNodes) {
|
||||
let {identifier, path: origPath, reactive} = original.fullPath;
|
||||
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
reactive,
|
||||
);
|
||||
let {identifier, path: origPath} = original.fullPath;
|
||||
let currNode: PropertyPathNode =
|
||||
registry.getOrCreateIdentifier(identifier);
|
||||
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
|
||||
|
||||
@@ -114,7 +114,7 @@ export type OptionalChainSidemap = {
|
||||
hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
|
||||
};
|
||||
|
||||
export type OptionalTraversalContext = {
|
||||
type OptionalTraversalContext = {
|
||||
currFn: HIRFunction;
|
||||
blocks: ReadonlyMap<BlockId, BasicBlock>;
|
||||
|
||||
@@ -235,7 +235,7 @@ function matchOptionalTestBlock(
|
||||
* property loads. If any part of the optional chain is not hoistable, returns
|
||||
* null.
|
||||
*/
|
||||
export function traverseOptionalBlock(
|
||||
function traverseOptionalBlock(
|
||||
optional: TBasicBlock<OptionalTerminal>,
|
||||
context: OptionalTraversalContext,
|
||||
outerAlternate: BlockId | null,
|
||||
@@ -290,7 +290,6 @@ export function traverseOptionalBlock(
|
||||
);
|
||||
baseObject = {
|
||||
identifier: maybeTest.instructions[0].value.place.identifier,
|
||||
reactive: maybeTest.instructions[0].value.place.reactive,
|
||||
path,
|
||||
};
|
||||
test = maybeTest.terminal;
|
||||
@@ -392,7 +391,6 @@ export function traverseOptionalBlock(
|
||||
);
|
||||
const load = {
|
||||
identifier: baseObject.identifier,
|
||||
reactive: baseObject.reactive,
|
||||
path: [
|
||||
...baseObject.path,
|
||||
{
|
||||
|
||||
@@ -25,9 +25,8 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
|
||||
* evaluate (non-optional) PropertyLoads from.
|
||||
*/
|
||||
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
|
||||
new Map();
|
||||
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
|
||||
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
|
||||
#deps: Map<Identifier, DependencyNode> = new Map();
|
||||
|
||||
/**
|
||||
* @param hoistableObjects a set of paths from which we can safely evaluate
|
||||
@@ -36,10 +35,9 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* duplicates when traversing the CFG.
|
||||
*/
|
||||
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
|
||||
for (const {path, identifier, reactive} of hoistableObjects) {
|
||||
for (const {path, identifier} of hoistableObjects) {
|
||||
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#hoistableObjects,
|
||||
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
|
||||
);
|
||||
@@ -72,8 +70,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
|
||||
static #getOrCreateRoot<T extends string>(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
|
||||
roots: Map<Identifier, TreeNode<T>>,
|
||||
defaultAccessType: T,
|
||||
): TreeNode<T> {
|
||||
// roots can always be accessed unconditionally in JS
|
||||
@@ -82,16 +79,9 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
if (rootNode === undefined) {
|
||||
rootNode = {
|
||||
properties: new Map(),
|
||||
reactive,
|
||||
accessType: defaultAccessType,
|
||||
};
|
||||
roots.set(identifier, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.reactive, {
|
||||
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
|
||||
description: `Identifier ${printIdentifier(identifier)}`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
@@ -102,10 +92,9 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* safe-to-evaluate subpath
|
||||
*/
|
||||
addDependency(dep: ReactiveScopeDependency): void {
|
||||
const {identifier, reactive, path} = dep;
|
||||
const {identifier, path} = dep;
|
||||
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#deps,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
);
|
||||
@@ -183,13 +172,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
|
||||
const results = new Set<ReactiveScopeDependency>();
|
||||
for (const [rootId, rootNode] of this.#deps.entries()) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
rootNode,
|
||||
rootNode.reactive,
|
||||
rootId,
|
||||
[],
|
||||
results,
|
||||
);
|
||||
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -311,24 +294,25 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
|
||||
type DependencyNode = TreeNode<PropertyAccessType>;
|
||||
|
||||
/**
|
||||
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
|
||||
* longer have conditionally accessed nodes, we can simplify
|
||||
*
|
||||
* Recursively calculates minimal dependencies in a subtree.
|
||||
* @param node DependencyNode representing a dependency subtree.
|
||||
* @returns a minimal list of dependencies in this subtree.
|
||||
*/
|
||||
function collectMinimalDependenciesInSubtree(
|
||||
node: DependencyNode,
|
||||
reactive: boolean,
|
||||
rootIdentifier: Identifier,
|
||||
path: Array<DependencyPathEntry>,
|
||||
results: Set<ReactiveScopeDependency>,
|
||||
): void {
|
||||
if (isDependency(node.accessType)) {
|
||||
results.add({identifier: rootIdentifier, reactive, path});
|
||||
results.add({identifier: rootIdentifier, path});
|
||||
} else {
|
||||
for (const [childName, childNode] of node.properties) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
childNode,
|
||||
reactive,
|
||||
rootIdentifier,
|
||||
[
|
||||
...path,
|
||||
|
||||
@@ -9,15 +9,7 @@ import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
defaultOptions,
|
||||
Logger,
|
||||
PanicThresholdOptions,
|
||||
parsePluginOptions,
|
||||
PluginOptions,
|
||||
ProgramContext,
|
||||
} from '../Entrypoint';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
DEFAULT_GLOBALS,
|
||||
@@ -55,7 +47,7 @@ import {
|
||||
ShapeRegistry,
|
||||
addHook,
|
||||
} from './ObjectShape';
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
@@ -158,7 +150,7 @@ export type Hook = z.infer<typeof HookSchema>;
|
||||
* missing some recursive Object / Function shapeIds
|
||||
*/
|
||||
|
||||
const EnvironmentConfigSchema = z.object({
|
||||
export const EnvironmentConfigSchema = z.object({
|
||||
customHooks: z.map(z.string(), HookSchema).default(new Map()),
|
||||
|
||||
/**
|
||||
@@ -367,6 +359,11 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoImpureFunctionsInRender: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validate against passing mutable functions to hooks
|
||||
*/
|
||||
validateNoFreezingKnownMutableFunctions: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* When enabled, the compiler assumes that hooks follow the Rules of React:
|
||||
* - Hooks may memoize computation based on any of their parameters, thus
|
||||
@@ -635,191 +632,6 @@ const EnvironmentConfigSchema = z.object({
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
|
||||
/**
|
||||
* For test fixtures and playground only.
|
||||
*
|
||||
* Pragmas are straightforward to parse for boolean options (`:true` and
|
||||
* `:false`). These are 'enabled' config values for non-boolean configs (i.e.
|
||||
* what is used when parsing `:true`).
|
||||
*/
|
||||
const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
validateNoCapitalizedCalls: [],
|
||||
enableChangeDetectionForDebugging: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: '$structuralCheck',
|
||||
},
|
||||
enableEmitFreeze: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'makeReadOnly',
|
||||
},
|
||||
enableEmitInstrumentForget: {
|
||||
fn: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useRenderCounter',
|
||||
},
|
||||
gating: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'shouldInstrument',
|
||||
},
|
||||
globalGating: 'DEV',
|
||||
},
|
||||
enableEmitHookGuards: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: '$dispatcherGuard',
|
||||
},
|
||||
inlineJsxTransform: {
|
||||
elementSymbol: 'react.transitional.element',
|
||||
globalDevVar: 'DEV',
|
||||
},
|
||||
lowerContextAccess: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useContext_withSelector',
|
||||
},
|
||||
inferEffectDependencies: [
|
||||
{
|
||||
function: {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'shared-runtime',
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
},
|
||||
numRequiredArgs: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'useEffectWrapper',
|
||||
importSpecifierName: 'default',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* For snap test fixtures and playground only.
|
||||
*/
|
||||
function parseConfigPragmaEnvironmentForTest(
|
||||
pragma: string,
|
||||
): EnvironmentConfig {
|
||||
const maybeConfig: any = {};
|
||||
// Get the defaults to programmatically check for boolean properties
|
||||
const defaultConfig = EnvironmentConfigSchema.parse({});
|
||||
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
const keyVal = token.slice(1);
|
||||
let [key, val = undefined] = keyVal.split(':');
|
||||
const isSet = val === undefined || val === 'true';
|
||||
|
||||
if (isSet && key in testComplexConfigDefaults) {
|
||||
maybeConfig[key] =
|
||||
testComplexConfigDefaults[key as keyof PartialEnvironmentConfig];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === 'customMacros' && val) {
|
||||
const valSplit = val.split('.');
|
||||
if (valSplit.length > 0) {
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
props.push({type: 'wildcard'});
|
||||
} else if (elt.length > 0) {
|
||||
props.push({type: 'name', name: elt});
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
key !== 'enableResetCacheOnSourceFileChanges' &&
|
||||
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
|
||||
) {
|
||||
// skip parsing non-boolean properties
|
||||
continue;
|
||||
}
|
||||
if (val === undefined || val === 'true') {
|
||||
maybeConfig[key] = true;
|
||||
} else {
|
||||
maybeConfig[key] = false;
|
||||
}
|
||||
}
|
||||
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
|
||||
if (config.success) {
|
||||
/**
|
||||
* Unless explicitly enabled, do not insert HMR handling code
|
||||
* in test fixtures or playground to reduce visual noise.
|
||||
*/
|
||||
if (config.data.enableResetCacheOnSourceFileChanges == null) {
|
||||
config.data.enableResetCacheOnSourceFileChanges = false;
|
||||
}
|
||||
return config.data;
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Internal error, could not parse config from pragma string',
|
||||
description: `${fromZodError(config.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
},
|
||||
): PluginOptions {
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
let compilationMode: CompilationMode = defaults.compilationMode;
|
||||
let panicThreshold: PanicThresholdOptions = 'all_errors';
|
||||
let noEmit: boolean = defaultOptions.noEmit;
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
switch (token) {
|
||||
case '@compilationMode(annotation)': {
|
||||
compilationMode = 'annotation';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(infer)': {
|
||||
compilationMode = 'infer';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(all)': {
|
||||
compilationMode = 'all';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(syntax)': {
|
||||
compilationMode = 'syntax';
|
||||
break;
|
||||
}
|
||||
case '@panicThreshold(none)': {
|
||||
panicThreshold = 'none';
|
||||
break;
|
||||
}
|
||||
case '@noEmit': {
|
||||
noEmit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsePluginOptions({
|
||||
environment,
|
||||
compilationMode,
|
||||
panicThreshold,
|
||||
noEmit,
|
||||
});
|
||||
}
|
||||
|
||||
export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
|
||||
|
||||
export type ReactFunctionType = 'Component' | 'Hook' | 'Other';
|
||||
@@ -863,7 +675,6 @@ export class Environment {
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
parentFunction: NodePath<t.Function>;
|
||||
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
@@ -871,7 +682,6 @@ export class Environment {
|
||||
compilerMode: CompilerMode,
|
||||
config: EnvironmentConfig,
|
||||
contextIdentifiers: Set<t.Identifier>,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
@@ -930,7 +740,6 @@ export class Environment {
|
||||
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
|
||||
}
|
||||
|
||||
this.parentFunction = parentFunction;
|
||||
this.#contextIdentifiers = contextIdentifiers;
|
||||
this.#hoistedIdentifiers = new Set();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
BuiltInUseRefId,
|
||||
BuiltInUseStateId,
|
||||
BuiltInUseTransitionId,
|
||||
BuiltInWeakMapId,
|
||||
BuiltInWeakSetId,
|
||||
ReanimatedSharedValueId,
|
||||
ShapeRegistry,
|
||||
addFunction,
|
||||
addHook,
|
||||
@@ -491,6 +494,38 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
true,
|
||||
),
|
||||
],
|
||||
[
|
||||
'WeakMap',
|
||||
addFunction(
|
||||
DEFAULT_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [Effect.ConditionallyMutateIterator],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
null,
|
||||
true,
|
||||
),
|
||||
],
|
||||
[
|
||||
'WeakSet',
|
||||
addFunction(
|
||||
DEFAULT_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [Effect.ConditionallyMutateIterator],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
null,
|
||||
true,
|
||||
),
|
||||
],
|
||||
// TODO: rest of Global objects
|
||||
];
|
||||
|
||||
@@ -908,7 +943,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
|
||||
addHook(registry, {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: {kind: 'Poly'},
|
||||
returnType: {kind: 'Object', shapeId: ReanimatedSharedValueId},
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
noAlias: true,
|
||||
calleeEffect: Effect.Read,
|
||||
|
||||
@@ -62,14 +62,12 @@ export type ReactiveFunction = {
|
||||
|
||||
export type ReactiveScopeBlock = {
|
||||
kind: 'scope';
|
||||
dependencyInstructions: Array<ReactiveInstructionStatement>;
|
||||
scope: ReactiveScope;
|
||||
instructions: ReactiveBlock;
|
||||
};
|
||||
|
||||
export type PrunedReactiveScopeBlock = {
|
||||
kind: 'pruned-scope';
|
||||
dependencyInstructions: Array<ReactiveInstructionStatement>;
|
||||
scope: ReactiveScope;
|
||||
instructions: ReactiveBlock;
|
||||
};
|
||||
@@ -616,7 +614,6 @@ export type MaybeThrowTerminal = {
|
||||
export type ReactiveScopeTerminal = {
|
||||
kind: 'scope';
|
||||
fallthrough: BlockId;
|
||||
dependencies: BlockId;
|
||||
block: BlockId;
|
||||
scope: ReactiveScope;
|
||||
id: InstructionId;
|
||||
@@ -626,7 +623,6 @@ export type ReactiveScopeTerminal = {
|
||||
export type PrunedScopeTerminal = {
|
||||
kind: 'pruned-scope';
|
||||
fallthrough: BlockId;
|
||||
dependencies: BlockId;
|
||||
block: BlockId;
|
||||
scope: ReactiveScope;
|
||||
id: InstructionId;
|
||||
@@ -750,6 +746,27 @@ export enum InstructionKind {
|
||||
Function = 'Function',
|
||||
}
|
||||
|
||||
export function convertHoistedLValueKind(
|
||||
kind: InstructionKind,
|
||||
): InstructionKind | null {
|
||||
switch (kind) {
|
||||
case InstructionKind.HoistedLet:
|
||||
return InstructionKind.Let;
|
||||
case InstructionKind.HoistedConst:
|
||||
return InstructionKind.Const;
|
||||
case InstructionKind.HoistedFunction:
|
||||
return InstructionKind.Function;
|
||||
case InstructionKind.Let:
|
||||
case InstructionKind.Const:
|
||||
case InstructionKind.Function:
|
||||
case InstructionKind.Reassign:
|
||||
case InstructionKind.Catch:
|
||||
return null;
|
||||
default:
|
||||
assertExhaustive(kind, 'Unexpected lvalue kind');
|
||||
}
|
||||
}
|
||||
|
||||
function _staticInvariantInstructionValueHasLocation(
|
||||
value: InstructionValue,
|
||||
): SourceLocation {
|
||||
@@ -884,8 +901,20 @@ export type InstructionValue =
|
||||
| StoreLocal
|
||||
| {
|
||||
kind: 'StoreContext';
|
||||
/**
|
||||
* StoreContext kinds:
|
||||
* Reassign: context variable reassignment in source
|
||||
* Const: const declaration + assignment in source
|
||||
* ('const' context vars are ones whose declarations are hoisted)
|
||||
* Let: let declaration + assignment in source
|
||||
* Function: function declaration in source (similar to `const`)
|
||||
*/
|
||||
lvalue: {
|
||||
kind: InstructionKind.Reassign;
|
||||
kind:
|
||||
| InstructionKind.Reassign
|
||||
| InstructionKind.Const
|
||||
| InstructionKind.Let
|
||||
| InstructionKind.Function;
|
||||
place: Place;
|
||||
};
|
||||
value: Place;
|
||||
@@ -1476,10 +1505,9 @@ export type ReactiveScope = {
|
||||
range: MutableRange;
|
||||
|
||||
/**
|
||||
* Note the dependencies of a reactive scope are tracked in HIR and
|
||||
* ReactiveFunction
|
||||
* The inputs to this reactive scope
|
||||
*/
|
||||
dependencies: Array<Place>;
|
||||
dependencies: ReactiveScopeDependencies;
|
||||
|
||||
/**
|
||||
* The set of values produced by this scope. This may be empty
|
||||
@@ -1540,18 +1568,6 @@ export type DependencyPathEntry = {
|
||||
export type DependencyPath = Array<DependencyPathEntry>;
|
||||
export type ReactiveScopeDependency = {
|
||||
identifier: Identifier;
|
||||
/**
|
||||
* Reflects whether the base identifier is reactive. Note that some reactive
|
||||
* objects may have non-reactive properties, but we do not currently track
|
||||
* this.
|
||||
*
|
||||
* ```js
|
||||
* // Technically, result[0] is reactive and result[1] is not.
|
||||
* // Currently, both dependencies would be marked as reactive.
|
||||
* const result = useState();
|
||||
* ```
|
||||
*/
|
||||
reactive: boolean;
|
||||
path: DependencyPath;
|
||||
};
|
||||
|
||||
@@ -1709,6 +1725,18 @@ export function isRefOrRefValue(id: Identifier): boolean {
|
||||
return isUseRefType(id) || isRefValueType(id);
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if the type is a Ref or a custom user type that acts like a ref when it
|
||||
* shouldn't. For now the only other case of this is Reanimated's shared values.
|
||||
*/
|
||||
export function isRefOrRefLikeMutableType(type: Type): boolean {
|
||||
return (
|
||||
type.kind === 'Object' &&
|
||||
(type.shapeId === 'BuiltInUseRefId' ||
|
||||
type.shapeId == 'ReanimatedSharedValueId')
|
||||
);
|
||||
}
|
||||
|
||||
export function isSetStateType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ export default class HIRBuilder {
|
||||
#bindings: Bindings;
|
||||
#env: Environment;
|
||||
#exceptionHandlerStack: Array<BlockId> = [];
|
||||
parentFunction: NodePath<t.Function>;
|
||||
errors: CompilerError = new CompilerError();
|
||||
/**
|
||||
* Traversal context: counts the number of `fbt` tag parents
|
||||
@@ -135,17 +136,16 @@ export default class HIRBuilder {
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
options?: {
|
||||
bindings?: Bindings | null;
|
||||
context?: Array<t.Identifier>;
|
||||
entryBlockKind?: BlockKind;
|
||||
},
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
bindings: Bindings | null = null,
|
||||
context: Array<t.Identifier> | null = null,
|
||||
) {
|
||||
this.#env = env;
|
||||
this.#bindings = options?.bindings ?? new Map();
|
||||
this.#context = options?.context ?? [];
|
||||
this.#bindings = bindings ?? new Map();
|
||||
this.parentFunction = parentFunction;
|
||||
this.#context = context ?? [];
|
||||
this.#entry = makeBlockId(env.nextBlockId);
|
||||
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
|
||||
this.#current = newBlock(this.#entry, 'block');
|
||||
}
|
||||
|
||||
currentBlockKind(): BlockKind {
|
||||
@@ -239,7 +239,7 @@ export default class HIRBuilder {
|
||||
|
||||
// Check if the binding is from module scope
|
||||
const outerBinding =
|
||||
this.#env.parentFunction.scope.parent.getBinding(originalName);
|
||||
this.parentFunction.scope.parent.getBinding(originalName);
|
||||
if (babelBinding === outerBinding) {
|
||||
const path = babelBinding.path;
|
||||
if (path.isImportDefaultSpecifier()) {
|
||||
@@ -293,7 +293,7 @@ export default class HIRBuilder {
|
||||
const binding = this.#resolveBabelBinding(path);
|
||||
if (binding) {
|
||||
// Check if the binding is from module scope, if so return null
|
||||
const outerBinding = this.#env.parentFunction.scope.parent.getBinding(
|
||||
const outerBinding = this.parentFunction.scope.parent.getBinding(
|
||||
path.node.name,
|
||||
);
|
||||
if (binding === outerBinding) {
|
||||
@@ -376,7 +376,7 @@ export default class HIRBuilder {
|
||||
}
|
||||
|
||||
// Terminate the current block w the given terminal, and start a new block
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
|
||||
const {id: blockId, kind, instructions} = this.#current;
|
||||
this.#completed.set(blockId, {
|
||||
kind,
|
||||
@@ -390,7 +390,6 @@ export default class HIRBuilder {
|
||||
const nextId = this.#env.nextBlockId;
|
||||
this.#current = newBlock(nextId, nextBlockKind);
|
||||
}
|
||||
return blockId;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -747,11 +746,6 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
|
||||
* (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
|
||||
*/
|
||||
const block = func.blocks.get(blockId)!;
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: '[HIRBuilder] Unexpected null block',
|
||||
description: `expected block ${blockId} to exist`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
|
||||
const fallthrough = terminalFallthrough(block.terminal);
|
||||
|
||||
|
||||
@@ -203,6 +203,8 @@ export const BuiltInPropsId = 'BuiltInProps';
|
||||
export const BuiltInArrayId = 'BuiltInArray';
|
||||
export const BuiltInSetId = 'BuiltInSet';
|
||||
export const BuiltInMapId = 'BuiltInMap';
|
||||
export const BuiltInWeakSetId = 'BuiltInWeakSet';
|
||||
export const BuiltInWeakMapId = 'BuiltInWeakMap';
|
||||
export const BuiltInFunctionId = 'BuiltInFunction';
|
||||
export const BuiltInJsxId = 'BuiltInJsx';
|
||||
export const BuiltInObjectId = 'BuiltInObject';
|
||||
@@ -225,6 +227,9 @@ export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
|
||||
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
|
||||
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
|
||||
|
||||
// ShapeRegistry with default definitions for built-ins.
|
||||
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
|
||||
|
||||
@@ -764,6 +769,101 @@ addObject(BUILTIN_SHAPES, BuiltInMapId, [
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInWeakSetId, [
|
||||
[
|
||||
/**
|
||||
* add(value)
|
||||
* Parameters
|
||||
* value: the value of the element to add to the Set object.
|
||||
* Returns the Set object with added value.
|
||||
*/
|
||||
'add',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
|
||||
calleeEffect: Effect.Store,
|
||||
// returnValueKind is technically dependent on the ValueKind of the set itself
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* setInstance.delete(value)
|
||||
* Returns true if value was already in Set; otherwise false.
|
||||
*/
|
||||
'delete',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'has',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInWeakMapId, [
|
||||
[
|
||||
'delete',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'get',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'has',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* Params
|
||||
* key: the key of the element to add to the Map object. The key may be
|
||||
* any JavaScript type (any primitive value or any type of JavaScript
|
||||
* object).
|
||||
* value: the value of the element to add to the Map object.
|
||||
* Returns the Map object.
|
||||
*/
|
||||
'set',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture, Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
|
||||
['0', {kind: 'Poly'}],
|
||||
[
|
||||
|
||||
@@ -286,13 +286,13 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
case 'scope': {
|
||||
value = `[${terminal.id}] Scope ${printReactiveScopeSummary(
|
||||
terminal.scope,
|
||||
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
break;
|
||||
}
|
||||
case 'pruned-scope': {
|
||||
value = `[${terminal.id}] <pruned> Scope ${printReactiveScopeSummary(
|
||||
terminal.scope,
|
||||
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
|
||||
break;
|
||||
}
|
||||
case 'try': {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
FunctionExpression,
|
||||
ObjectMethod,
|
||||
PropertyLiteral,
|
||||
convertHoistedLValueKind,
|
||||
} from './HIR';
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
@@ -47,17 +48,6 @@ import {CompilerError} from '../CompilerError';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
|
||||
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
|
||||
import {
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
reversePostorderBlocks,
|
||||
} from './HIRBuilder';
|
||||
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
import {
|
||||
readScopeDependencies,
|
||||
writeScopeDependencies,
|
||||
} from './ScopeDependencyUtils';
|
||||
|
||||
export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
const usedOutsideDeclaringScope =
|
||||
@@ -84,10 +74,8 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
/**
|
||||
* Derive the minimal set of hoistable dependencies for each scope.
|
||||
*/
|
||||
const minimalDeps = new Map<ReactiveScope, Set<ReactiveScopeDependency>>();
|
||||
for (const [scope, deps] of scopeDeps) {
|
||||
if (deps.length === 0) {
|
||||
minimalDeps.set(scope, new Set());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -114,79 +102,17 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
* Step 3: Reduce dependencies to a minimal set.
|
||||
*/
|
||||
const candidates = tree.deriveMinimalDependencies();
|
||||
const dependencies = new Set<ReactiveScopeDependency>();
|
||||
for (const candidateDep of candidates) {
|
||||
if (
|
||||
!Iterable_some(
|
||||
dependencies,
|
||||
scope.dependencies,
|
||||
existingDep =>
|
||||
existingDep.identifier.declarationId ===
|
||||
candidateDep.identifier.declarationId &&
|
||||
areEqualPaths(existingDep.path, candidateDep.path),
|
||||
)
|
||||
)
|
||||
dependencies.add(candidateDep);
|
||||
}
|
||||
minimalDeps.set(scope, dependencies);
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
/**
|
||||
* Step 4: inject dependencies
|
||||
*/
|
||||
for (const [_, {terminal}] of fn.body.blocks) {
|
||||
if (terminal.kind !== 'scope' && terminal.kind !== 'pruned-scope') {
|
||||
continue;
|
||||
}
|
||||
const scope = terminal.scope;
|
||||
const deps = minimalDeps.get(scope);
|
||||
if (deps == null || deps.size === 0) {
|
||||
continue;
|
||||
}
|
||||
writeScopeDependencies(terminal, deps, fn);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 5: fix scope and identifier ranges to account for renumbered
|
||||
* instructions
|
||||
*/
|
||||
if (changed) {
|
||||
reversePostorderBlocks(fn.body);
|
||||
markPredecessors(fn.body);
|
||||
markInstructionIds(fn.body);
|
||||
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
{
|
||||
for (const [scope, deps] of minimalDeps) {
|
||||
const checkedDeps = [...readScopeDependencies(fn, scope.id)];
|
||||
CompilerError.invariant(checkedDeps != null, {
|
||||
reason: '[Rewrite] Cannot find scope dep when reading',
|
||||
loc: scope.loc,
|
||||
});
|
||||
CompilerError.invariant(checkedDeps.length === deps.size, {
|
||||
reason: '[Rewrite] non matching sizes when reading',
|
||||
description: `scopeId=${scope.id} deps=${[...deps].map(printDependency)} checkedDeps=${[...checkedDeps].map(printDependency)}`,
|
||||
loc: scope.loc,
|
||||
});
|
||||
for (const dep of deps) {
|
||||
CompilerError.invariant(
|
||||
checkedDeps.some(
|
||||
checkedDep =>
|
||||
dep.identifier === checkedDep.identifier &&
|
||||
areEqualPaths(dep.path, checkedDep.path),
|
||||
),
|
||||
{
|
||||
reason:
|
||||
'[Rewrite] could not find match for dependency when re-reading',
|
||||
description: `${printDependency(dep)}`,
|
||||
loc: scope.loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
scope.dependencies.add(candidateDep);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,12 +247,18 @@ function isLoadContextMutable(
|
||||
id: InstructionId,
|
||||
): instrValue is LoadContext {
|
||||
if (instrValue.kind === 'LoadContext') {
|
||||
CompilerError.invariant(instrValue.place.identifier.scope != null, {
|
||||
reason:
|
||||
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
|
||||
loc: instrValue.loc,
|
||||
});
|
||||
return id >= instrValue.place.identifier.scope.range.end;
|
||||
/**
|
||||
* Not all context variables currently have scopes due to limitations of
|
||||
* mutability analysis for function expressions.
|
||||
*
|
||||
* Currently, many function expressions references are inferred to be
|
||||
* 'Read' | 'Freeze' effects which don't replay mutable effects of captured
|
||||
* context.
|
||||
*/
|
||||
return (
|
||||
instrValue.place.identifier.scope != null &&
|
||||
id >= instrValue.place.identifier.scope.range.end
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -384,7 +316,6 @@ function collectTemporariesSidemapImpl(
|
||||
) {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
@@ -438,13 +369,11 @@ function getProperty(
|
||||
if (resolvedDependency == null) {
|
||||
property = {
|
||||
identifier: object.identifier,
|
||||
reactive: object.reactive,
|
||||
path: [{property: propertyName, optional}],
|
||||
};
|
||||
} else {
|
||||
property = {
|
||||
identifier: resolvedDependency.identifier,
|
||||
reactive: resolvedDependency.reactive,
|
||||
path: [...resolvedDependency.path, {property: propertyName, optional}],
|
||||
};
|
||||
}
|
||||
@@ -549,6 +478,9 @@ export class DependencyCollectionContext {
|
||||
}
|
||||
this.#reassignments.set(identifier, decl);
|
||||
}
|
||||
hasDeclared(identifier: Identifier): boolean {
|
||||
return this.#declarations.has(identifier.declarationId);
|
||||
}
|
||||
|
||||
// Checks if identifier is a valid dependency in the current scope
|
||||
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
|
||||
@@ -600,7 +532,6 @@ export class DependencyCollectionContext {
|
||||
this.visitDependency(
|
||||
this.#temporaries.get(place.identifier.id) ?? {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
},
|
||||
);
|
||||
@@ -665,7 +596,6 @@ export class DependencyCollectionContext {
|
||||
) {
|
||||
maybeDependency = {
|
||||
identifier: maybeDependency.identifier,
|
||||
reactive: maybeDependency.reactive,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
@@ -687,11 +617,7 @@ export class DependencyCollectionContext {
|
||||
identifier =>
|
||||
identifier.declarationId === place.identifier.declarationId,
|
||||
) &&
|
||||
this.#checkValidDependency({
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
})
|
||||
this.#checkValidDependency({identifier: place.identifier, path: []})
|
||||
) {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
@@ -756,21 +682,21 @@ export function handleInstruction(
|
||||
});
|
||||
} else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
|
||||
/*
|
||||
* Some variables may be declared and never initialized. We need
|
||||
* to retain (and hoist) these declarations if they are included
|
||||
* in a reactive scope. One approach is to simply add all `DeclareLocal`s
|
||||
* as scope declarations.
|
||||
* Some variables may be declared and never initialized. We need to retain
|
||||
* (and hoist) these declarations if they are included in a reactive scope.
|
||||
* One approach is to simply add all `DeclareLocal`s as scope declarations.
|
||||
*
|
||||
* Context variables with hoisted declarations only become live after their
|
||||
* first assignment. We only declare real DeclareLocal / DeclareContext
|
||||
* instructions (not hoisted ones) to avoid generating dependencies on
|
||||
* hoisted declarations.
|
||||
*/
|
||||
|
||||
/*
|
||||
* We add context variable declarations here, not at `StoreContext`, since
|
||||
* context Store / Loads are modeled as reads and mutates to the underlying
|
||||
* variable reference (instead of through intermediate / inlined temporaries)
|
||||
*/
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
if (convertHoistedLValueKind(value.lvalue.kind) === null) {
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
} else if (value.kind === 'Destructure') {
|
||||
context.visitOperand(value.value);
|
||||
for (const place of eachPatternOperand(value.lvalue.pattern)) {
|
||||
@@ -782,6 +708,26 @@ export function handleInstruction(
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
} else if (value.kind === 'StoreContext') {
|
||||
/**
|
||||
* Some StoreContext variables have hoisted declarations. If we're storing
|
||||
* to a context variable that hasn't yet been declared, the StoreContext is
|
||||
* the declaration.
|
||||
* (see corresponding logic in PruneHoistedContext)
|
||||
*/
|
||||
if (
|
||||
!context.hasDeclared(value.lvalue.place.identifier) ||
|
||||
value.lvalue.kind !== InstructionKind.Reassign
|
||||
) {
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
}
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
import {
|
||||
ScopeId,
|
||||
HIRFunction,
|
||||
Place,
|
||||
ReactiveScopeDependency,
|
||||
Identifier,
|
||||
makeInstructionId,
|
||||
InstructionKind,
|
||||
GeneratedSource,
|
||||
IdentifierId,
|
||||
BlockId,
|
||||
makeTemporaryIdentifier,
|
||||
Effect,
|
||||
OptionalTerminal,
|
||||
TBasicBlock,
|
||||
ReactiveScopeTerminal,
|
||||
GotoVariant,
|
||||
PrunedScopeTerminal,
|
||||
ReactiveInstruction,
|
||||
ReactiveValue,
|
||||
ReactiveScopeBlock,
|
||||
PrunedReactiveScopeBlock,
|
||||
} from './HIR';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
OptionalTraversalContext,
|
||||
traverseOptionalBlock,
|
||||
} from './CollectOptionalChainDependencies';
|
||||
import {Environment} from './Environment';
|
||||
import HIRBuilder from './HIRBuilder';
|
||||
import {lowerValueToTemporary} from './BuildHIR';
|
||||
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
import {printPlace} from './PrintHIR';
|
||||
|
||||
function writeNonOptionalDependency(
|
||||
dep: ReactiveScopeDependency,
|
||||
env: Environment,
|
||||
builder: HIRBuilder,
|
||||
): Identifier {
|
||||
const loc = dep.identifier.loc;
|
||||
let last: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
|
||||
builder.push({
|
||||
lvalue: {
|
||||
identifier: last,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {
|
||||
identifier: dep.identifier,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
});
|
||||
|
||||
for (const path of dep.path) {
|
||||
const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
|
||||
builder.push({
|
||||
lvalue: {
|
||||
identifier: next,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {
|
||||
identifier: last,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
property: path.property,
|
||||
loc,
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
});
|
||||
last = next;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
export function writeScopeDependencies(
|
||||
terminal: ReactiveScopeTerminal | PrunedScopeTerminal,
|
||||
deps: Set<ReactiveScopeDependency>,
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const scopeDepBlock = fn.body.blocks.get(terminal.dependencies);
|
||||
|
||||
CompilerError.invariant(scopeDepBlock != null, {
|
||||
reason: 'Expected to find scope dependency block',
|
||||
loc: terminal.loc,
|
||||
});
|
||||
CompilerError.invariant(
|
||||
scopeDepBlock.instructions.length === 0 &&
|
||||
scopeDepBlock.terminal.kind === 'goto' &&
|
||||
scopeDepBlock.terminal.block === terminal.block,
|
||||
{
|
||||
reason: 'Expected scope.dependencies to be a goto block (invalid cfg)',
|
||||
loc: terminal.loc,
|
||||
},
|
||||
);
|
||||
const builder = new HIRBuilder(fn.env, {
|
||||
entryBlockKind: 'value',
|
||||
});
|
||||
|
||||
for (const dep of deps) {
|
||||
if (dep.path.every(path => !path.optional)) {
|
||||
const last = writeNonOptionalDependency(dep, fn.env, builder);
|
||||
terminal.scope.dependencies.push({
|
||||
kind: 'Identifier',
|
||||
identifier: last,
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Write all optional chaining deps
|
||||
for (const dep of deps) {
|
||||
if (!dep.path.every(path => !path.optional)) {
|
||||
const last = writeOptional(
|
||||
dep.path.length - 1,
|
||||
dep,
|
||||
builder,
|
||||
terminal,
|
||||
null,
|
||||
);
|
||||
terminal.scope.dependencies.push({
|
||||
kind: 'Identifier',
|
||||
identifier: last,
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder terminal for HIRBuilder, to be later replaced by a `goto` to an outer block
|
||||
const lastBlockId = builder.terminate(
|
||||
{
|
||||
kind: 'return',
|
||||
value: {
|
||||
kind: 'Identifier',
|
||||
identifier: makeTemporaryIdentifier(
|
||||
fn.env.nextIdentifierId,
|
||||
GeneratedSource,
|
||||
),
|
||||
effect: Effect.Freeze,
|
||||
loc: GeneratedSource,
|
||||
reactive: true,
|
||||
},
|
||||
loc: GeneratedSource,
|
||||
id: makeInstructionId(0),
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
const dependenciesHIR = builder.build();
|
||||
for (const [id, block] of dependenciesHIR.blocks) {
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
fn.body.blocks.delete(terminal.dependencies);
|
||||
|
||||
/**
|
||||
* Connect the newly constructed inner HIR to the outer HIR
|
||||
*/
|
||||
terminal.dependencies = dependenciesHIR.entry;
|
||||
// Rewire the replaceholder terminal to the correct goto
|
||||
fn.body.blocks.get(lastBlockId)!.terminal = scopeDepBlock.terminal;
|
||||
}
|
||||
|
||||
function writeOptional(
|
||||
idx: number,
|
||||
dep: ReactiveScopeDependency,
|
||||
builder: HIRBuilder,
|
||||
terminal: ReactiveScopeTerminal | PrunedScopeTerminal,
|
||||
parentAlternate: BlockId | null,
|
||||
): Identifier {
|
||||
const env = builder.environment;
|
||||
CompilerError.invariant(
|
||||
idx >= 0 && !dep.path.slice(0, idx + 1).every(path => !path.optional),
|
||||
{
|
||||
reason: '[WriteOptional] Expected optional path',
|
||||
description: `${idx} ${printDependency(dep)}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
const continuationBlock = builder.reserve(builder.currentBlockKind());
|
||||
const consequent = builder.reserve('value');
|
||||
|
||||
const returnPlace: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
let alternate;
|
||||
if (parentAlternate != null) {
|
||||
alternate = parentAlternate;
|
||||
} else {
|
||||
/**
|
||||
* Make outermost alternate block
|
||||
* $N = Primitive undefined
|
||||
* $M = StoreLocal $OptionalResult = $N
|
||||
* goto fallthrough
|
||||
*/
|
||||
alternate = builder.enter('value', () => {
|
||||
const temp = lowerValueToTemporary(builder, {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Const, place: {...returnPlace}},
|
||||
value: {...temp},
|
||||
type: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let testIdentifier: Identifier | null = null;
|
||||
const testBlock = builder.enter('value', () => {
|
||||
const firstOptional = dep.path.findIndex(path => path.optional);
|
||||
if (idx === firstOptional) {
|
||||
// Lower test block
|
||||
testIdentifier = writeNonOptionalDependency(
|
||||
{
|
||||
identifier: dep.identifier,
|
||||
reactive: dep.reactive,
|
||||
path: dep.path.slice(0, idx),
|
||||
},
|
||||
env,
|
||||
builder,
|
||||
);
|
||||
} else {
|
||||
testIdentifier = writeOptional(
|
||||
idx - 1,
|
||||
dep,
|
||||
builder,
|
||||
terminal,
|
||||
alternate,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'branch',
|
||||
test: {
|
||||
identifier: testIdentifier,
|
||||
effect: Effect.Freeze,
|
||||
kind: 'Identifier',
|
||||
loc: GeneratedSource,
|
||||
reactive: dep.reactive,
|
||||
},
|
||||
consequent: consequent.id,
|
||||
alternate,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
fallthrough: continuationBlock.id,
|
||||
};
|
||||
});
|
||||
|
||||
builder.enterReserved(consequent, () => {
|
||||
CompilerError.invariant(testIdentifier !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
const tmpConsequent = lowerValueToTemporary(builder, {
|
||||
kind: 'PropertyLoad',
|
||||
object: {
|
||||
identifier: testIdentifier,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
property: dep.path[idx].property,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Const, place: {...returnPlace}},
|
||||
value: {...tmpConsequent},
|
||||
type: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
});
|
||||
builder.terminateWithContinuation(
|
||||
{
|
||||
kind: 'optional',
|
||||
optional: dep.path[idx].optional,
|
||||
test: testBlock,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
continuationBlock,
|
||||
);
|
||||
|
||||
return returnPlace.identifier;
|
||||
}
|
||||
|
||||
export function readScopeDependencies(
|
||||
fn: HIRFunction,
|
||||
scope: ScopeId,
|
||||
): Set<ReactiveScopeDependency> {
|
||||
for (const [_, {terminal}] of fn.body.blocks) {
|
||||
if (terminal.kind !== 'scope' && terminal.kind !== 'pruned-scope') {
|
||||
continue;
|
||||
}
|
||||
if (terminal.scope.id !== scope) {
|
||||
continue;
|
||||
}
|
||||
const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
|
||||
const context: OptionalTraversalContext = {
|
||||
currFn: fn,
|
||||
blocks: fn.body.blocks,
|
||||
seenOptionals: new Set(),
|
||||
processedInstrsInOptional: new Set(),
|
||||
temporariesReadInOptional: temporaries,
|
||||
hoistableObjects: new Map(),
|
||||
};
|
||||
/**
|
||||
* Step 1: read all instructions between within scope dependencies block(s)
|
||||
*/
|
||||
let work = terminal.dependencies;
|
||||
while (true) {
|
||||
const block = fn.body.blocks.get(work)!;
|
||||
for (const {lvalue, value} of block.instructions) {
|
||||
if (value.kind === 'LoadLocal') {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
});
|
||||
} else if (value.kind === 'PropertyLoad') {
|
||||
const source = temporaries.get(value.object.identifier.id)!;
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: source.identifier,
|
||||
reactive: source.reactive,
|
||||
path: [...source.path, {property: value.property, optional: false}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (block.terminal.kind === 'optional') {
|
||||
traverseOptionalBlock(
|
||||
block as TBasicBlock<OptionalTerminal>,
|
||||
context,
|
||||
null,
|
||||
);
|
||||
work = block.terminal.fallthrough;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
block.terminal.kind === 'goto' &&
|
||||
block.terminal.block === terminal.block,
|
||||
{
|
||||
reason: 'unexpected terminal',
|
||||
description: `kind: ${block.terminal.kind}`,
|
||||
loc: block.terminal.loc,
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: look up scope dependencies from the temporaries sidemap
|
||||
*/
|
||||
const scopeOwnDependencies = new Set<ReactiveScopeDependency>();
|
||||
for (const dep of terminal.scope.dependencies) {
|
||||
const reactiveScopeDependency = temporaries.get(dep.identifier.id)!;
|
||||
CompilerError.invariant(reactiveScopeDependency != null, {
|
||||
reason: 'Expected dependency to be found',
|
||||
description: `${printPlace(dep)}`,
|
||||
loc: terminal.scope.loc,
|
||||
});
|
||||
scopeOwnDependencies.add(reactiveScopeDependency);
|
||||
}
|
||||
return scopeOwnDependencies;
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected scope to be found',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
|
||||
function assertNonNull<T>(value: T | null | undefined): T {
|
||||
if (value == null) {
|
||||
throw new Error('Expected nonnull value');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function readScopeDependenciesRHIRInstr(
|
||||
instr: ReactiveInstruction,
|
||||
sidemap: Map<IdentifierId, ReactiveScopeDependency>,
|
||||
): void {
|
||||
const value = reacScopeDependenciesRHIRValue(instr.value, sidemap);
|
||||
if (instr.lvalue != null) {
|
||||
sidemap.set(instr.lvalue.identifier.id, value);
|
||||
}
|
||||
}
|
||||
function reacScopeDependenciesRHIRValue(
|
||||
instr: ReactiveValue,
|
||||
sidemap: Map<IdentifierId, ReactiveScopeDependency>,
|
||||
): ReactiveScopeDependency {
|
||||
if (instr.kind === 'LoadLocal') {
|
||||
const base = sidemap.get(instr.place.identifier.id);
|
||||
if (base != null) {
|
||||
return base;
|
||||
} else {
|
||||
return {
|
||||
identifier: instr.place.identifier,
|
||||
reactive: instr.place.reactive,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
} else if (instr.kind === 'PropertyLoad') {
|
||||
const base = assertNonNull(sidemap.get(instr.object.identifier.id));
|
||||
return {
|
||||
identifier: base.identifier,
|
||||
reactive: base.reactive,
|
||||
path: [...base.path, {property: instr.property, optional: false}],
|
||||
};
|
||||
} else if (instr.kind === 'SequenceExpression') {
|
||||
for (const inner of instr.instructions) {
|
||||
readScopeDependenciesRHIRInstr(inner, sidemap);
|
||||
}
|
||||
return reacScopeDependenciesRHIRValue(instr.value, sidemap);
|
||||
} else if (instr.kind === 'OptionalExpression') {
|
||||
const value = reacScopeDependenciesRHIRValue(instr.value, sidemap);
|
||||
CompilerError.invariant(
|
||||
value.path.length > 0 && !value.path.at(-1)!.optional,
|
||||
{
|
||||
reason: 'Expected optional chain to be nonempty',
|
||||
loc: instr.loc,
|
||||
},
|
||||
);
|
||||
return {
|
||||
...value,
|
||||
path: [
|
||||
...value.path.slice(0, -1),
|
||||
{property: value.path.at(-1)!.property, optional: instr.optional},
|
||||
],
|
||||
};
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected value kind',
|
||||
description: instr.kind,
|
||||
loc: instr.loc,
|
||||
});
|
||||
}
|
||||
|
||||
export function readScopeDependenciesRHIR(
|
||||
scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock,
|
||||
): Map<Place, ReactiveScopeDependency> {
|
||||
const sidemap = new Map<IdentifierId, ReactiveScopeDependency>();
|
||||
for (const instr of scopeBlock.dependencyInstructions) {
|
||||
readScopeDependenciesRHIRInstr(instr.instruction, sidemap);
|
||||
}
|
||||
return new Map<Place, ReactiveScopeDependency>(
|
||||
scopeBlock.scope.dependencies.map(place => {
|
||||
return [place, assertNonNull(sidemap.get(place.identifier.id))];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run DCE to delete instructions which are not used by any scope dependencies
|
||||
*
|
||||
* Note: this only handles simple pruning i.e. deleted dependency entries,
|
||||
* not more complex rewrites such as pruned or edited entries.
|
||||
*/
|
||||
export function scopeDependenciesDCE(
|
||||
scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock,
|
||||
): void {
|
||||
const usage = new Map<IdentifierId, IdentifierId | null>();
|
||||
for (const {
|
||||
instruction: {lvalue, value},
|
||||
} of scopeBlock.dependencyInstructions) {
|
||||
if (lvalue == null) {
|
||||
continue;
|
||||
}
|
||||
switch (value.kind) {
|
||||
case 'LoadLocal': {
|
||||
usage.set(lvalue.identifier.id, null);
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad': {
|
||||
usage.set(lvalue.identifier.id, value.object.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'OptionalExpression': {
|
||||
usage.set(lvalue.identifier.id, null);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected value kind',
|
||||
description: value.kind,
|
||||
loc: value.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notUsed = new Set(usage.keys());
|
||||
const seen = new Set();
|
||||
for (const {identifier} of scopeBlock.scope.dependencies) {
|
||||
let curr: IdentifierId | undefined | null = identifier.id;
|
||||
while (curr != null) {
|
||||
CompilerError.invariant(!seen.has(curr), {
|
||||
reason: 'infinite loop',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
notUsed.delete(curr);
|
||||
seen.add(curr);
|
||||
curr = usage.get(curr);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Remove unused instructions in place
|
||||
*/
|
||||
let j = 0;
|
||||
for (let i = 0; i < scopeBlock.dependencyInstructions.length; i++) {
|
||||
const instr = scopeBlock.dependencyInstructions[i].instruction;
|
||||
if (instr.lvalue != null && !notUsed.has(instr.lvalue.identifier.id)) {
|
||||
scopeBlock.dependencyInstructions[j] =
|
||||
scopeBlock.dependencyInstructions[i];
|
||||
j++;
|
||||
}
|
||||
}
|
||||
scopeBlock.dependencyInstructions.length = j;
|
||||
}
|
||||
@@ -17,7 +17,6 @@ export {buildReactiveScopeTerminalsHIR} from './BuildReactiveScopeTerminalsHIR';
|
||||
export {computeDominatorTree, computePostDominatorTree} from './Dominator';
|
||||
export {
|
||||
Environment,
|
||||
parseConfigPragmaForTests,
|
||||
validateEnvironmentConfig,
|
||||
type EnvironmentConfig,
|
||||
type ExternalFunction,
|
||||
|
||||
@@ -860,13 +860,11 @@ export function mapTerminalSuccessors(
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
const dependencies = fn(terminal.dependencies);
|
||||
const block = fn(terminal.block);
|
||||
const fallthrough = fn(terminal.fallthrough);
|
||||
return {
|
||||
kind: terminal.kind,
|
||||
scope: terminal.scope,
|
||||
dependencies,
|
||||
block,
|
||||
fallthrough,
|
||||
id: makeInstructionId(0),
|
||||
@@ -1019,7 +1017,7 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
yield terminal.dependencies;
|
||||
yield terminal.block;
|
||||
break;
|
||||
}
|
||||
case 'unreachable':
|
||||
@@ -1070,13 +1068,6 @@ export function mapTerminalOperands(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
for (let i = 0; i < terminal.scope.dependencies.length; i++) {
|
||||
terminal.scope.dependencies[i] = fn(terminal.scope.dependencies[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'maybe-throw':
|
||||
case 'sequence':
|
||||
case 'label':
|
||||
@@ -1090,7 +1081,9 @@ export function mapTerminalOperands(
|
||||
case 'for-in':
|
||||
case 'goto':
|
||||
case 'unreachable':
|
||||
case 'unsupported': {
|
||||
case 'unsupported':
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
@@ -1134,13 +1127,6 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
for (const dep of terminal.scope.dependencies) {
|
||||
yield dep;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'maybe-throw':
|
||||
case 'sequence':
|
||||
case 'label':
|
||||
@@ -1154,7 +1140,9 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
|
||||
case 'for-in':
|
||||
case 'goto':
|
||||
case 'unreachable':
|
||||
case 'unsupported': {
|
||||
case 'unsupported':
|
||||
case 'scope':
|
||||
case 'pruned-scope': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 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 {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/**
|
||||
* If a function captures a mutable value but never gets called, we don't infer a
|
||||
* mutable range for that function. This means that we also don't alias the function
|
||||
* with its mutable captures.
|
||||
*
|
||||
* This case is tricky, because we don't generally know for sure what is a mutation
|
||||
* and what may just be a normal function call. For example:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo() {
|
||||
* const x = makeObject();
|
||||
* return () => {
|
||||
* return readObject(x); // could be a mutation!
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If we pessimistically assume that all such cases are mutations, we'd have to group
|
||||
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo(createEntryForKey) {
|
||||
* const cache = new WeakMap();
|
||||
* return (key) => {
|
||||
* let entry = cache.get(key);
|
||||
* if (entry == null) {
|
||||
* entry = createEntryForKey(key);
|
||||
* cache.set(key, entry); // known mutation!
|
||||
* }
|
||||
* return entry;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Then we have to ensure that the function and its mutable captures alias together and
|
||||
* end up in the same scope. However, aliasing together isn't enough if the function
|
||||
* and operands all have empty mutable ranges (end = start + 1).
|
||||
*
|
||||
* This pass finds function expressions and object methods that have an empty mutable range
|
||||
* and known-mutable operands which also don't have a mutable range, and ensures that the
|
||||
* function and those operands are aliased together *and* that their ranges are updated to
|
||||
* end after the function expression. This is sufficient to ensure that a reactive scope is
|
||||
* created for the alias set.
|
||||
*/
|
||||
export function inferAliasForUncalledFunctions(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
instrs: for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (
|
||||
value.kind !== 'ObjectMethod' &&
|
||||
value.kind !== 'FunctionExpression'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function is known to be mutated, we will have
|
||||
* already aliased any mutable operands with it
|
||||
*/
|
||||
const range = lvalue.identifier.mutableRange;
|
||||
if (range.end > range.start + 1) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function already has operands with an active mutable range,
|
||||
* then we don't need to do anything — the function will have already
|
||||
* been visited and included in some mutable alias set. This case can
|
||||
* also occur due to visiting the same function in an earlier iteration
|
||||
* of the outer fixpoint loop.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (isMutable(instr, operand)) {
|
||||
continue instrs;
|
||||
}
|
||||
}
|
||||
const operands: Set<Identifier> = new Set();
|
||||
for (const effect of value.loweredFunc.func.effects ?? []) {
|
||||
if (effect.kind !== 'ContextMutation') {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* We're looking for known-mutations only, so we look at the effects
|
||||
* rather than function context
|
||||
*/
|
||||
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
|
||||
for (const operand of effect.places) {
|
||||
/*
|
||||
* It's possible that function effect analysis thinks there was a context mutation,
|
||||
* but then InferReferenceEffects figures out some operands are globals and therefore
|
||||
* creates a non-mutable effect for those operands.
|
||||
* We should change InferReferenceEffects to swap the ContextMutation for a global
|
||||
* mutation in that case, but for now we just filter them out here
|
||||
*/
|
||||
if (
|
||||
isMutableEffect(operand.effect, operand.loc) &&
|
||||
!isRefOrRefLikeMutableType(operand.identifier.type)
|
||||
) {
|
||||
operands.add(operand.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operands.size !== 0) {
|
||||
operands.add(lvalue.identifier);
|
||||
aliases.union([...operands]);
|
||||
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
|
||||
for (const operand of operands) {
|
||||
operand.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
DependencyCollectionContext,
|
||||
handleInstruction,
|
||||
} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {readScopeDependencies} from '../HIR/ScopeDependencyUtils';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {empty} from '../Utils/Stack';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
@@ -98,7 +97,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
) {
|
||||
scopeInfos.set(
|
||||
block.terminal.scope.id,
|
||||
readScopeDependencies(fn, block.terminal.scope.id),
|
||||
block.terminal.scope.dependencies,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -486,7 +485,7 @@ function inferDependencies(
|
||||
start: fnInstr.id,
|
||||
end: makeInstructionId(fnInstr.id + 1),
|
||||
},
|
||||
dependencies: [],
|
||||
dependencies: new Set(),
|
||||
reassignments: new Set(),
|
||||
declarations: new Map(),
|
||||
earlyReturnValue: null,
|
||||
|
||||
@@ -176,9 +176,15 @@ export function inferMutableLifetimes(
|
||||
if (
|
||||
instr.value.kind === 'DeclareContext' ||
|
||||
(instr.value.kind === 'StoreContext' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign)
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign &&
|
||||
!contextVariableDeclarationInstructions.has(
|
||||
instr.value.lvalue.place.identifier,
|
||||
))
|
||||
) {
|
||||
// Save declarations of context variables
|
||||
/**
|
||||
* Save declarations of context variables if they hasn't already been
|
||||
* declared (due to hoisted declarations).
|
||||
*/
|
||||
contextVariableDeclarationInstructions.set(
|
||||
instr.value.lvalue.place.identifier,
|
||||
instr.id,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
||||
import {inferAliases} from './InferAlias';
|
||||
import {inferAliasForPhis} from './InferAliasForPhis';
|
||||
import {inferAliasForStores} from './InferAliasForStores';
|
||||
@@ -76,6 +77,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
inferAliasForUncalledFunctions(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
@@ -293,7 +292,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
let hasReactiveInput = false;
|
||||
/*
|
||||
* NOTE: we want to mark all operands as reactive or not, so we
|
||||
* avoid short-circuiting here
|
||||
* avoid short-circuting here
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const reactive = reactiveIdentifiers.isReactive(operand);
|
||||
@@ -376,41 +375,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
} while (reactiveIdentifiers.snapshot());
|
||||
|
||||
function propagateReactivityToInnerFunctions(
|
||||
fn: HIRFunction,
|
||||
isOutermost: boolean,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (!isOutermost) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
reactiveIdentifiers.isReactive(operand);
|
||||
}
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'ObjectMethod' ||
|
||||
instr.value.kind === 'FunctionExpression'
|
||||
) {
|
||||
propagateReactivityToInnerFunctions(
|
||||
instr.value.loweredFunc.func,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!isOutermost) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
reactiveIdentifiers.isReactive(operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate reactivity for inner functions, as we eventually hoist and dedupe
|
||||
* dependency instructions for scopes.
|
||||
*/
|
||||
propagateReactivityToInnerFunctions(fn, true);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -111,7 +111,10 @@ export default function inferReferenceEffects(
|
||||
* Initial state contains function params
|
||||
* TODO: include module declarations here as well
|
||||
*/
|
||||
const initialState = InferenceState.empty(fn.env);
|
||||
const initialState = InferenceState.empty(
|
||||
fn.env,
|
||||
options.isFunctionExpression,
|
||||
);
|
||||
const value: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
loc: fn.loc,
|
||||
@@ -255,6 +258,7 @@ type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
|
||||
// Maintains a mapping of top-level variables to the kind of value they hold
|
||||
class InferenceState {
|
||||
env: Environment;
|
||||
#isFunctionExpression: boolean;
|
||||
|
||||
// The kind of each value, based on its allocation site
|
||||
#values: Map<InstructionValue, AbstractValue>;
|
||||
@@ -267,16 +271,25 @@ class InferenceState {
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
isFunctionExpression: boolean,
|
||||
values: Map<InstructionValue, AbstractValue>,
|
||||
variables: Map<IdentifierId, Set<InstructionValue>>,
|
||||
) {
|
||||
this.env = env;
|
||||
this.#isFunctionExpression = isFunctionExpression;
|
||||
this.#values = values;
|
||||
this.#variables = variables;
|
||||
}
|
||||
|
||||
static empty(env: Environment): InferenceState {
|
||||
return new InferenceState(env, new Map(), new Map());
|
||||
static empty(
|
||||
env: Environment,
|
||||
isFunctionExpression: boolean,
|
||||
): InferenceState {
|
||||
return new InferenceState(env, isFunctionExpression, new Map(), new Map());
|
||||
}
|
||||
|
||||
get isFunctionExpression(): boolean {
|
||||
return this.#isFunctionExpression;
|
||||
}
|
||||
|
||||
// (Re)initializes a @param value with its default @param kind.
|
||||
@@ -394,9 +407,14 @@ class InferenceState {
|
||||
|
||||
freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
|
||||
for (const value of values) {
|
||||
if (value.kind === 'DeclareContext') {
|
||||
if (
|
||||
value.kind === 'DeclareContext' ||
|
||||
(value.kind === 'StoreContext' &&
|
||||
(value.lvalue.kind === InstructionKind.Let ||
|
||||
value.lvalue.kind === InstructionKind.Const))
|
||||
) {
|
||||
/**
|
||||
* Avoid freezing hoisted context declarations
|
||||
* Avoid freezing context variable declarations, hoisted or otherwise
|
||||
* function Component() {
|
||||
* const cb = useBar(() => foo(2)); // produces a hoisted context declaration
|
||||
* const foo = useFoo(); // reassigns to the context variable
|
||||
@@ -613,6 +631,7 @@ class InferenceState {
|
||||
} else {
|
||||
return new InferenceState(
|
||||
this.env,
|
||||
this.#isFunctionExpression,
|
||||
nextValues ?? new Map(this.#values),
|
||||
nextVariables ?? new Map(this.#variables),
|
||||
);
|
||||
@@ -627,6 +646,7 @@ class InferenceState {
|
||||
clone(): InferenceState {
|
||||
return new InferenceState(
|
||||
this.env,
|
||||
this.#isFunctionExpression,
|
||||
new Map(this.#values),
|
||||
new Map(this.#variables),
|
||||
);
|
||||
@@ -1591,6 +1611,14 @@ function inferBlock(
|
||||
);
|
||||
|
||||
const lvalue = instr.lvalue;
|
||||
if (instrValue.lvalue.kind !== InstructionKind.Reassign) {
|
||||
state.initialize(instrValue, {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
});
|
||||
state.define(instrValue.lvalue.place, instrValue);
|
||||
}
|
||||
state.alias(lvalue, instrValue.value);
|
||||
lvalue.effect = Effect.Store;
|
||||
continuation = {kind: 'funeffects'};
|
||||
@@ -1781,8 +1809,15 @@ function inferBlock(
|
||||
if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
|
||||
if (
|
||||
state.isDefined(operand) &&
|
||||
state.kind(operand).kind === ValueKind.Context
|
||||
((operand.identifier.type.kind === 'Function' &&
|
||||
state.isFunctionExpression) ||
|
||||
state.kind(operand).kind === ValueKind.Context)
|
||||
) {
|
||||
/**
|
||||
* Returned values should only be typed as 'frozen' if they are both (1)
|
||||
* local and (2) not a function expression which may capture and mutate
|
||||
* this function's outer context.
|
||||
*/
|
||||
effect = Effect.ConditionallyMutate;
|
||||
} else {
|
||||
effect = Effect.Freeze;
|
||||
|
||||
@@ -327,6 +327,23 @@ function evaluateInstruction(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case '-': {
|
||||
const operand = read(constants, value.value);
|
||||
if (
|
||||
operand !== null &&
|
||||
operand.kind === 'Primitive' &&
|
||||
typeof operand.value === 'number'
|
||||
) {
|
||||
const result: Primitive = {
|
||||
kind: 'Primitive',
|
||||
value: operand.value * -1,
|
||||
loc: value.loc,
|
||||
};
|
||||
instr.value = result;
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -492,6 +509,73 @@ function evaluateInstruction(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'TemplateLiteral': {
|
||||
if (value.subexprs.length === 0) {
|
||||
const result: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
value: value.quasis.map(q => q.cooked).join(''),
|
||||
loc: value.loc,
|
||||
};
|
||||
instr.value = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (value.subexprs.length !== value.quasis.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.quasis.some(q => q.cooked === undefined)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let quasiIndex = 0;
|
||||
let resultString = value.quasis[quasiIndex].cooked as string;
|
||||
++quasiIndex;
|
||||
|
||||
for (const subExpr of value.subexprs) {
|
||||
const subExprValue = read(constants, subExpr);
|
||||
if (!subExprValue || subExprValue.kind !== 'Primitive') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expressionValue = subExprValue.value;
|
||||
if (
|
||||
typeof expressionValue !== 'number' &&
|
||||
typeof expressionValue !== 'string' &&
|
||||
typeof expressionValue !== 'boolean' &&
|
||||
!(typeof expressionValue === 'object' && expressionValue === null)
|
||||
) {
|
||||
// value is not supported (function, object) or invalid (symbol), or something else
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffix = value.quasis[quasiIndex].cooked;
|
||||
++quasiIndex;
|
||||
|
||||
if (suffix === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Spec states that concat calls ToString(argument) internally on its parameters
|
||||
* -> we don't have to implement ToString(argument) ourselves and just use the engine implementation
|
||||
* Refs:
|
||||
* - https://tc39.es/ecma262/2024/#sec-tostring
|
||||
* - https://tc39.es/ecma262/2024/#sec-string.prototype.concat
|
||||
* - https://tc39.es/ecma262/2024/#sec-template-literals-runtime-semantics-evaluation
|
||||
*/
|
||||
resultString = resultString.concat(expressionValue as string, suffix);
|
||||
}
|
||||
|
||||
const result: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
value: resultString,
|
||||
loc: value.loc,
|
||||
};
|
||||
|
||||
instr.value = result;
|
||||
return result;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const placeValue = read(constants, value.place);
|
||||
if (placeValue !== null) {
|
||||
|
||||
@@ -387,10 +387,12 @@ export function inlineJsxTransform(
|
||||
|
||||
if (block.terminal.kind === 'scope') {
|
||||
const scope = block.terminal.scope;
|
||||
/**
|
||||
* Note that scope dependencies don't need to be renamed explicitly
|
||||
* as they will be visited when traversing scope terminal successors.
|
||||
*/
|
||||
for (const dep of scope.dependencies) {
|
||||
dep.identifier = handleIdentifier(
|
||||
dep.identifier,
|
||||
inlinedJsxDeclarations,
|
||||
);
|
||||
}
|
||||
|
||||
for (const [origId, decl] of [...scope.declarations]) {
|
||||
const newDecl = handleIdentifier(
|
||||
|
||||
@@ -17,16 +17,11 @@ import {
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {
|
||||
areEqualPaths,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
PrunedReactiveScopeBlock,
|
||||
ReactiveBreakTerminal,
|
||||
ReactiveContinueTerminal,
|
||||
ReactiveFunction,
|
||||
ReactiveInstructionStatement,
|
||||
ReactiveLogicalValue,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveSequenceValue,
|
||||
ReactiveTerminalStatement,
|
||||
ReactiveTerminalTargetKind,
|
||||
@@ -34,12 +29,7 @@ import {
|
||||
ReactiveValue,
|
||||
Terminal,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
readScopeDependencies,
|
||||
readScopeDependenciesRHIR,
|
||||
} from '../HIR/ScopeDependencyUtils';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {printDependency} from './PrintReactiveFunction';
|
||||
|
||||
/*
|
||||
* Converts from HIR (lower-level CFG) to ReactiveFunction, a tree representation
|
||||
@@ -48,7 +38,7 @@ import {printDependency} from './PrintReactiveFunction';
|
||||
* labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels.
|
||||
*/
|
||||
export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
|
||||
const cx = new Context(fn);
|
||||
const cx = new Context(fn.body);
|
||||
const driver = new Driver(cx);
|
||||
const body = driver.traverseBlock(cx.block(fn.body.entry));
|
||||
return {
|
||||
@@ -826,64 +816,17 @@ class Driver {
|
||||
} else {
|
||||
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
|
||||
}
|
||||
{
|
||||
const scheduleId = this.cx.schedule(terminal.block, 'if');
|
||||
scheduleIds.push(scheduleId);
|
||||
this.cx.scopeFallthroughs.add(terminal.block);
|
||||
}
|
||||
CompilerError.invariant(!this.cx.isScheduled(terminal.dependencies), {
|
||||
reason: `Unexpected 'scope' where the dependencies block is already scheduled`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
const dependencies: Array<ReactiveInstructionStatement> =
|
||||
this.traverseBlock(this.cx.ir.blocks.get(terminal.dependencies)!).map(
|
||||
dep => {
|
||||
CompilerError.invariant(dep.kind === 'instruction', {
|
||||
reason: '[BuildReactiveFunction] Expected reactive instruction',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return dep;
|
||||
},
|
||||
);
|
||||
|
||||
this.cx.unscheduleAll(scheduleIds);
|
||||
const scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock = {
|
||||
blockValue.push({
|
||||
kind: terminal.kind,
|
||||
dependencyInstructions: dependencies,
|
||||
instructions: block,
|
||||
scope: terminal.scope,
|
||||
};
|
||||
blockValue.push(scopeBlock);
|
||||
});
|
||||
if (fallthroughId !== null) {
|
||||
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity check: check that dependencies stay the same after converting
|
||||
* to ReactiveFunction.
|
||||
*/
|
||||
const hirDeps = readScopeDependencies(this.cx.fn, terminal.scope.id);
|
||||
const rhirDeps = [...readScopeDependenciesRHIR(scopeBlock)];
|
||||
CompilerError.invariant(hirDeps.size === rhirDeps.length, {
|
||||
reason: `[BuildReactiveFunction] Sanity check failed: mismatch in dependencies count`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
for (const hirDep of hirDeps) {
|
||||
CompilerError.invariant(
|
||||
rhirDeps.some(
|
||||
([, rhirDep]) =>
|
||||
hirDep.identifier === rhirDep.identifier &&
|
||||
areEqualPaths(hirDep.path, rhirDep.path),
|
||||
),
|
||||
{
|
||||
reason:
|
||||
'[BuildReactiveFunction] Sanity check failed: mismatching scope dependencies',
|
||||
description: `No match found for ${printDependency(hirDep)}. Candidates are ${rhirDeps.map(dep => printDependency(dep[1]))}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'unreachable': {
|
||||
@@ -1300,7 +1243,6 @@ class Driver {
|
||||
}
|
||||
|
||||
class Context {
|
||||
fn: HIRFunction;
|
||||
ir: HIR;
|
||||
#nextScheduleId: number = 0;
|
||||
|
||||
@@ -1331,9 +1273,8 @@ class Context {
|
||||
*/
|
||||
#controlFlowStack: Array<ControlFlowTarget> = [];
|
||||
|
||||
constructor(fn: HIRFunction) {
|
||||
this.fn = fn;
|
||||
this.ir = fn.body;
|
||||
constructor(ir: HIR) {
|
||||
this.ir = ir;
|
||||
}
|
||||
|
||||
block(id: BlockId): BasicBlock {
|
||||
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveInstructionStatement,
|
||||
ReactiveScope,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveScopeDeclaration,
|
||||
ReactiveScopeDependency,
|
||||
ReactiveTerminal,
|
||||
ReactiveValue,
|
||||
SourceLocation,
|
||||
@@ -54,7 +54,6 @@ import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
import {EMIT_FREEZE_GLOBAL_GATING, ReactFunctionType} from '../HIR/Environment';
|
||||
import {ProgramContext} from '../Entrypoint';
|
||||
import generate from '@babel/generator';
|
||||
|
||||
export const MEMO_CACHE_SENTINEL = 'react.memo_cache_sentinel';
|
||||
export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
|
||||
@@ -540,13 +539,7 @@ function codegenBlockNoReset(
|
||||
}
|
||||
case 'scope': {
|
||||
const temp = new Map(cx.temp);
|
||||
codegenReactiveScope(
|
||||
cx,
|
||||
statements,
|
||||
item.scope,
|
||||
item.instructions,
|
||||
item.dependencyInstructions,
|
||||
);
|
||||
codegenReactiveScope(cx, statements, item.scope, item.instructions);
|
||||
cx.temp = temp;
|
||||
break;
|
||||
}
|
||||
@@ -610,7 +603,6 @@ function codegenReactiveScope(
|
||||
statements: Array<t.Statement>,
|
||||
scope: ReactiveScope,
|
||||
block: ReactiveBlock,
|
||||
dependencyInstructions: Array<ReactiveInstructionStatement>,
|
||||
): void {
|
||||
const cacheStoreStatements: Array<t.Statement> = [];
|
||||
const cacheLoadStatements: Array<t.Statement> = [];
|
||||
@@ -623,39 +615,9 @@ function codegenReactiveScope(
|
||||
const changeExpressionComments: Array<string> = [];
|
||||
const outputComments: Array<string> = [];
|
||||
|
||||
/**
|
||||
* Step 1: Dependency instructions should codegen their expressions into
|
||||
* the `Context.Temporaries` map.
|
||||
*/
|
||||
for (const instr of dependencyInstructions) {
|
||||
/**
|
||||
* codegenInstructionNullable should never return a statement, as all
|
||||
* dependency instructions should be inlined into `Context.Temporaries`.
|
||||
*/
|
||||
const result = codegenInstructionNullable(cx, instr.instruction);
|
||||
CompilerError.invariant(result == null, {
|
||||
reason: 'Expected dependency instructions to be inlined',
|
||||
loc: instr.instruction.loc,
|
||||
});
|
||||
}
|
||||
const sortedDependencies: Array<[string, t.Expression]> = scope.dependencies
|
||||
.map<[string, t.Expression]>(dep => {
|
||||
const dependency = codegenPlace(cx, dep);
|
||||
CompilerError.invariant(dependency.type !== 'JSXText', {
|
||||
reason: 'Expected dependency to not be JSXText',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return [generate(dependency).code, dependency];
|
||||
})
|
||||
.sort(([aName], [bName]) => {
|
||||
if (aName < bName) return -1;
|
||||
else if (aName > bName) return 1;
|
||||
else return 0;
|
||||
});
|
||||
|
||||
for (const [code, dep] of sortedDependencies) {
|
||||
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
|
||||
const index = cx.nextCacheIndex;
|
||||
changeExpressionComments.push(code);
|
||||
changeExpressionComments.push(printDependencyComment(dep));
|
||||
const comparison = t.binaryExpression(
|
||||
'!==',
|
||||
t.memberExpression(
|
||||
@@ -663,7 +625,7 @@ function codegenReactiveScope(
|
||||
t.numericLiteral(index),
|
||||
true,
|
||||
),
|
||||
dep,
|
||||
codegenDependency(cx, dep),
|
||||
);
|
||||
|
||||
if (cx.env.config.enableChangeVariableCodegen) {
|
||||
@@ -690,7 +652,7 @@ function codegenReactiveScope(
|
||||
t.numericLiteral(index),
|
||||
true,
|
||||
),
|
||||
t.cloneNode(dep, true),
|
||||
codegenDependency(cx, dep),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1038,6 +1000,14 @@ function codegenTerminal(
|
||||
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
description: null,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default:
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
||||
@@ -1130,6 +1100,14 @@ function codegenTerminal(
|
||||
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
description: null,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default:
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
||||
@@ -1481,6 +1459,17 @@ function codegenForInit(
|
||||
}
|
||||
}
|
||||
|
||||
function printDependencyComment(dependency: ReactiveScopeDependency): string {
|
||||
const identifier = convertIdentifier(dependency.identifier);
|
||||
let name = identifier.name;
|
||||
if (dependency.path !== null) {
|
||||
for (const path of dependency.path) {
|
||||
name += `.${path.property}`;
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function printDelimitedCommentList(
|
||||
items: Array<string>,
|
||||
finalCompletion: string,
|
||||
@@ -1505,6 +1494,34 @@ function printDelimitedCommentList(
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
function codegenDependency(
|
||||
cx: Context,
|
||||
dependency: ReactiveScopeDependency,
|
||||
): t.Expression {
|
||||
let object: t.Expression = convertIdentifier(dependency.identifier);
|
||||
if (dependency.path.length !== 0) {
|
||||
const hasOptional = dependency.path.some(path => path.optional);
|
||||
for (const path of dependency.path) {
|
||||
const property =
|
||||
typeof path.property === 'string'
|
||||
? t.identifier(path.property)
|
||||
: t.numericLiteral(path.property);
|
||||
const isComputed = typeof path.property !== 'string';
|
||||
if (hasOptional) {
|
||||
object = t.optionalMemberExpression(
|
||||
object,
|
||||
property,
|
||||
isComputed,
|
||||
path.optional,
|
||||
);
|
||||
} else {
|
||||
object = t.memberExpression(object, property, isComputed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
function withLoc<T extends (...args: Array<any>) => t.Node>(
|
||||
fn: T,
|
||||
): (
|
||||
@@ -2310,9 +2327,12 @@ function codegenInstructionValue(
|
||||
* u0080 to u009F: C1 control codes
|
||||
* u00A0 to uFFFF: All non-basic Latin characters
|
||||
* https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
|
||||
*
|
||||
* u010000 to u10FFFF: Astral plane characters
|
||||
* https://mathiasbynens.be/notes/javascript-unicode
|
||||
*/
|
||||
const STRING_REQUIRES_EXPR_CONTAINER_PATTERN =
|
||||
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}]|"|\\/u;
|
||||
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}\u{010000}-\u{10FFFF}]|"|\\/u;
|
||||
function codegenJsxAttribute(
|
||||
cx: Context,
|
||||
attribute: JsxAttribute,
|
||||
@@ -2623,6 +2643,30 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
|
||||
return t.identifier(identifier.name.value);
|
||||
}
|
||||
|
||||
function compareScopeDependency(
|
||||
a: ReactiveScopeDependency,
|
||||
b: ReactiveScopeDependency,
|
||||
): number {
|
||||
CompilerError.invariant(
|
||||
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
|
||||
{
|
||||
reason: '[Codegen] Expected named identifier for dependency',
|
||||
loc: a.identifier.loc,
|
||||
},
|
||||
);
|
||||
const aName = [
|
||||
a.identifier.name.value,
|
||||
...a.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
|
||||
].join('.');
|
||||
const bName = [
|
||||
b.identifier.name.value,
|
||||
...b.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
|
||||
].join('.');
|
||||
if (aName < bName) return -1;
|
||||
else if (aName > bName) return 1;
|
||||
else return 0;
|
||||
}
|
||||
|
||||
function compareScopeDeclaration(
|
||||
a: ReactiveScopeDeclaration,
|
||||
b: ReactiveScopeDeclaration,
|
||||
|
||||
@@ -31,16 +31,14 @@ export function flattenReactiveLoopsHIR(fn: HIRFunction): void {
|
||||
}
|
||||
case 'scope': {
|
||||
if (activeLoops.length !== 0) {
|
||||
const newTerminal: PrunedScopeTerminal = {
|
||||
block.terminal = {
|
||||
kind: 'pruned-scope',
|
||||
block: terminal.block,
|
||||
dependencies: terminal.dependencies,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
scope: terminal.scope,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
} as PrunedScopeTerminal;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -83,50 +83,28 @@ export function flattenScopesWithHooksOrUseHIR(fn: HIRFunction): void {
|
||||
body.terminal.kind === 'goto' &&
|
||||
body.terminal.block === terminal.fallthrough
|
||||
) {
|
||||
/**
|
||||
* Note that scope blocks are unique in that they represent two nested
|
||||
* goto-labels. We entirely remove dependency blocks here to simplify
|
||||
* rewrite logic
|
||||
*/
|
||||
const dependencyBlock = fn.body.blocks.get(terminal.dependencies);
|
||||
CompilerError.invariant(
|
||||
dependencyBlock != null &&
|
||||
dependencyBlock.instructions.length === 0 &&
|
||||
dependencyBlock.terminal.kind === 'goto' &&
|
||||
dependencyBlock.terminal.block === terminal.block,
|
||||
{
|
||||
reason: `Expected scope dependency block to have no instructions and goto scope block`,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
);
|
||||
fn.body.blocks.delete(terminal.dependencies);
|
||||
const scopeBlock = fn.body.blocks.get(terminal.block)!;
|
||||
scopeBlock.preds = new Set([block.id]);
|
||||
/*
|
||||
* This was a scope just for a hook call, which doesn't need memoization.
|
||||
* flatten it away. We rely on the PrunedUnusedLabel step to do the actual
|
||||
* flattening
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block.terminal = {
|
||||
kind: 'label',
|
||||
block: terminal.block,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
} as LabelTerminal;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newTerminal: PrunedScopeTerminal = {
|
||||
block.terminal = {
|
||||
kind: 'pruned-scope',
|
||||
block: terminal.block,
|
||||
dependencies: terminal.dependencies,
|
||||
fallthrough: terminal.fallthrough,
|
||||
id: terminal.id,
|
||||
loc: terminal.loc,
|
||||
scope: terminal.scope,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
} as PrunedScopeTerminal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
scope = {
|
||||
id: fn.env.nextScopeId,
|
||||
range: identifier.mutableRange,
|
||||
dependencies: [],
|
||||
dependencies: new Set(),
|
||||
declarations: new Map(),
|
||||
reassignments: new Set(),
|
||||
earlyReturnValue: null,
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
BuiltInJsxId,
|
||||
BuiltInObjectId,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {eachInstructionLValue} from '../HIR/visitors';
|
||||
import {assertExhaustive, Iterable_some} from '../Utils/utils';
|
||||
import {printReactiveScopeSummary} from './PrintReactiveFunction';
|
||||
@@ -120,29 +119,21 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
|
||||
lastUsage: Map<DeclarationId, InstructionId>;
|
||||
cache: Map<ReactiveScopeBlock, ReactiveScopeDependencies> = new Map();
|
||||
|
||||
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
|
||||
super();
|
||||
this.lastUsage = lastUsage;
|
||||
}
|
||||
|
||||
dependency(scopeBlock: ReactiveScopeBlock): ReactiveScopeDependencies {
|
||||
let dependencies = this.cache.get(scopeBlock);
|
||||
if (dependencies == null) {
|
||||
dependencies = new Set(readScopeDependenciesRHIR(scopeBlock).values());
|
||||
this.cache.set(scopeBlock, dependencies);
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
override transformScope(
|
||||
scopeBlock: ReactiveScopeBlock,
|
||||
state: ReactiveScopeDependencies | null,
|
||||
): Transformed<ReactiveStatement> {
|
||||
const dependencies = this.dependency(scopeBlock);
|
||||
this.visitScope(scopeBlock, dependencies);
|
||||
if (state !== null && areEqualDependencies(state, dependencies)) {
|
||||
this.visitScope(scopeBlock, scopeBlock.scope.dependencies);
|
||||
if (
|
||||
state !== null &&
|
||||
areEqualDependencies(state, scopeBlock.scope.dependencies)
|
||||
) {
|
||||
return {kind: 'replace-many', value: scopeBlock.instructions};
|
||||
} else {
|
||||
return {kind: 'keep'};
|
||||
@@ -269,7 +260,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
case 'scope': {
|
||||
if (
|
||||
current !== null &&
|
||||
canMergeScopes(this, current.block, instr) &&
|
||||
canMergeScopes(current.block, instr) &&
|
||||
areLValuesLastUsedByScope(
|
||||
instr.scope,
|
||||
current.lvalues,
|
||||
@@ -433,7 +424,6 @@ function areLValuesLastUsedByScope(
|
||||
}
|
||||
|
||||
function canMergeScopes(
|
||||
transform: Transform,
|
||||
current: ReactiveScopeBlock,
|
||||
next: ReactiveScopeBlock,
|
||||
): boolean {
|
||||
@@ -445,10 +435,10 @@ function canMergeScopes(
|
||||
log(` cannot merge, has reassignments`);
|
||||
return false;
|
||||
}
|
||||
const currDeps = transform.dependency(current);
|
||||
const nextDeps = transform.dependency(next);
|
||||
// Merge scopes whose dependencies are identical
|
||||
if (areEqualDependencies(currDeps, nextDeps)) {
|
||||
if (
|
||||
areEqualDependencies(current.scope.dependencies, next.scope.dependencies)
|
||||
) {
|
||||
log(` canMergeScopes: dependencies are equal`);
|
||||
return true;
|
||||
}
|
||||
@@ -466,14 +456,13 @@ function canMergeScopes(
|
||||
new Set(
|
||||
[...current.scope.declarations.values()].map(declaration => ({
|
||||
identifier: declaration.identifier,
|
||||
reactive: false,
|
||||
path: [],
|
||||
})),
|
||||
),
|
||||
nextDeps,
|
||||
next.scope.dependencies,
|
||||
) ||
|
||||
(nextDeps.size !== 0 &&
|
||||
[...nextDeps].every(
|
||||
(next.scope.dependencies.size !== 0 &&
|
||||
[...next.scope.dependencies].every(
|
||||
dep =>
|
||||
isAlwaysInvalidatingType(dep.identifier.type) &&
|
||||
Iterable_some(
|
||||
@@ -548,7 +537,7 @@ function areEqualDependencies(
|
||||
* *never* change and it's also eligible for merging.
|
||||
*/
|
||||
function scopeIsEligibleForMerging(scopeBlock: ReactiveScopeBlock): boolean {
|
||||
if (scopeBlock.scope.dependencies.length === 0) {
|
||||
if (scopeBlock.scope.dependencies.size === 0) {
|
||||
/*
|
||||
* Regardless of the type of value produced, if the scope has no dependencies
|
||||
* then its value will never change.
|
||||
|
||||
@@ -64,7 +64,11 @@ export function printReactiveScopeSummary(scope: ReactiveScope): string {
|
||||
items.push('scope');
|
||||
items.push(`@${scope.id}`);
|
||||
items.push(`[${scope.range.start}:${scope.range.end}]`);
|
||||
items.push(`dependencies=[${scope.dependencies.map(printPlace).join(',')}]`);
|
||||
items.push(
|
||||
`dependencies=[${Array.from(scope.dependencies)
|
||||
.map(dep => printDependency(dep))
|
||||
.join(', ')}]`,
|
||||
);
|
||||
items.push(
|
||||
`declarations=[${Array.from(scope.declarations)
|
||||
.map(([, decl]) =>
|
||||
@@ -92,8 +96,6 @@ export function writeReactiveBlock(
|
||||
block: ReactiveScopeBlock,
|
||||
): void {
|
||||
writer.writeLine(`${printReactiveScopeSummary(block.scope)} {`);
|
||||
writeReactiveInstructions(writer, block.dependencyInstructions);
|
||||
writer.writeLine('} /* - end dependencies - */ {');
|
||||
writeReactiveInstructions(writer, block.instructions);
|
||||
writer.writeLine('}');
|
||||
}
|
||||
@@ -253,6 +255,12 @@ function writeReactiveValue(writer: Writer, value: ReactiveValue): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function printReactiveTerminal(terminal: ReactiveTerminal): string {
|
||||
const writer = new Writer();
|
||||
writeTerminal(writer, terminal);
|
||||
return writer.complete();
|
||||
}
|
||||
|
||||
function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
|
||||
switch (terminal.kind) {
|
||||
case 'break': {
|
||||
|
||||
@@ -26,13 +26,18 @@ import {
|
||||
} from '../HIR/HIR';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
|
||||
import {scopeDependenciesDCE} from '../HIR/ScopeDependencyUtils';
|
||||
|
||||
/**
|
||||
* Phase 2: Promote identifiers which are used in a place that requires a named variable.
|
||||
*/
|
||||
class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const {identifier} = dep;
|
||||
if (identifier.name == null) {
|
||||
promoteIdentifier(identifier, state);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* This is technically optional. We could prune ReactiveScopes
|
||||
* whose outputs are not used in another computation or return
|
||||
@@ -45,29 +50,7 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
promoteIdentifier(declaration.identifier, state);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Run DCE to remove dependency instructions that are no longer used due to
|
||||
* pruning passes.
|
||||
*/
|
||||
scopeDependenciesDCE(scopeBlock);
|
||||
/**
|
||||
* Traverse into scope dependency instructions to promote references to
|
||||
* unnamed outer identifiers.
|
||||
*/
|
||||
CompilerError.invariant(state.scopeDepContext == null, {
|
||||
reason: 'PromoteTemporaries: unexpected nested scopeDepContext',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
this.visitBlock(scopeBlock.dependencyInstructions, {
|
||||
...state,
|
||||
scopeDepContext: {
|
||||
declarations: new Set(),
|
||||
dependencies: new Set(
|
||||
[...scopeBlock.scope.dependencies].map(dep => dep.identifier),
|
||||
),
|
||||
},
|
||||
});
|
||||
this.visitBlock(scopeBlock.instructions, state);
|
||||
this.traverseScope(scopeBlock, state);
|
||||
}
|
||||
|
||||
override visitPrunedScope(
|
||||
@@ -92,33 +75,11 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
}
|
||||
}
|
||||
|
||||
override visitLValue(_id: InstructionId, lvalue: Place, state: State): void {
|
||||
// Track lvalues from within scope dependency blocks to avoid promoting them.
|
||||
state.scopeDepContext?.declarations.add(lvalue.identifier.declarationId);
|
||||
}
|
||||
|
||||
override visitValue(
|
||||
id: InstructionId,
|
||||
value: ReactiveValue,
|
||||
state: State,
|
||||
): void {
|
||||
if (
|
||||
state.scopeDepContext &&
|
||||
(value.kind === 'LoadLocal' || value.kind === 'LoadContext')
|
||||
) {
|
||||
/**
|
||||
* Scope dependency LoadLocal sources (defined by instructions external to
|
||||
* that scope) should be promoted, as scope boundaries represent
|
||||
* re-ordering barriers
|
||||
*/
|
||||
const identifier = value.place.identifier;
|
||||
if (
|
||||
!state.scopeDepContext.declarations.has(identifier.declarationId) &&
|
||||
identifier.name === null
|
||||
) {
|
||||
promoteIdentifier(identifier, state);
|
||||
}
|
||||
}
|
||||
this.traverseValue(id, value, state);
|
||||
if (value.kind === 'FunctionExpression' || value.kind === 'ObjectMethod') {
|
||||
this.visitHirFunction(value.loweredFunc.func, state);
|
||||
@@ -216,10 +177,6 @@ type State = {
|
||||
DeclarationId,
|
||||
{activeScopes: Array<ScopeId>; usedOutsideScope: boolean}
|
||||
>; // true if referenced within another scope, false if only accessed outside of scopes
|
||||
scopeDepContext: {
|
||||
declarations: Set<DeclarationId>;
|
||||
dependencies: ReadonlySet<Identifier>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -470,7 +427,6 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
|
||||
tags: new Set(),
|
||||
promoted: new Set(),
|
||||
pruned: new Map(),
|
||||
scopeDepContext: null,
|
||||
};
|
||||
visitReactiveFunction(fn, new CollectPromotableTemporaries(), state);
|
||||
for (const operand of fn.params) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
} from '../HIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
|
||||
/**
|
||||
* Some instructions will *always* produce a new value, and unless memoized will *always*
|
||||
@@ -88,8 +87,7 @@ class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitScope(scopeBlock, true);
|
||||
|
||||
const scopeDeps = readScopeDependenciesRHIR(scopeBlock);
|
||||
for (const [, dep] of scopeDeps) {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
if (this.unmemoizedValues.has(dep.identifier)) {
|
||||
/*
|
||||
* This scope depends on an always-invalidating value so the scope will always invalidate:
|
||||
@@ -109,7 +107,6 @@ class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
dependencyInstructions: scopeBlock.dependencyInstructions,
|
||||
scope: scopeBlock.scope,
|
||||
instructions: scopeBlock.instructions,
|
||||
},
|
||||
|
||||
@@ -7,12 +7,17 @@
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
DeclarationId,
|
||||
convertHoistedLValueKind,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
Place,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
} from '../HIR';
|
||||
import {empty, Stack} from '../Utils/Stack';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
@@ -22,138 +27,144 @@ import {
|
||||
/*
|
||||
* Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
|
||||
* original instruction kind.
|
||||
*
|
||||
* Also detects and bails out on context variables which are:
|
||||
* - function declarations, which are hoisted by JS engines to the nearest block scope
|
||||
* - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
|
||||
* - declared
|
||||
*
|
||||
* This is because React Compiler converts a `function foo()` function declaration to
|
||||
* 1. a `let foo;` declaration before reactive memo blocks
|
||||
* 2. a `foo = function foo() {}` assignment within the block
|
||||
*
|
||||
* This means references before the assignment are invalid (see fixture
|
||||
* error.todo-functiondecl-hoisting)
|
||||
*/
|
||||
export function pruneHoistedContexts(fn: ReactiveFunction): void {
|
||||
const hoistedIdentifiers: HoistedIdentifiers = new Map();
|
||||
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
|
||||
visitReactiveFunction(fn, new Visitor(), {
|
||||
activeScopes: empty(),
|
||||
uninitialized: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
const REWRITTEN_HOISTED_CONST: unique symbol = Symbol(
|
||||
'REWRITTEN_HOISTED_CONST',
|
||||
);
|
||||
const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET');
|
||||
type VisitorState = {
|
||||
activeScopes: Stack<Set<IdentifierId>>;
|
||||
uninitialized: Map<
|
||||
IdentifierId,
|
||||
| {
|
||||
kind: 'unknown-kind';
|
||||
}
|
||||
| {
|
||||
kind: 'func';
|
||||
definition: Place | null;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
type HoistedIdentifiers = Map<
|
||||
DeclarationId,
|
||||
| InstructionKind
|
||||
| typeof REWRITTEN_HOISTED_CONST
|
||||
| typeof REWRITTEN_HOISTED_LET
|
||||
>;
|
||||
|
||||
class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
|
||||
class Visitor extends ReactiveFunctionTransform<VisitorState> {
|
||||
override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {
|
||||
state.activeScopes = state.activeScopes.push(
|
||||
new Set(scope.scope.declarations.keys()),
|
||||
);
|
||||
/**
|
||||
* Add declared but not initialized / assigned variables. This may include
|
||||
* function declarations that escape the memo block.
|
||||
*/
|
||||
for (const decl of scope.scope.declarations.values()) {
|
||||
state.uninitialized.set(decl.identifier.id, {kind: 'unknown-kind'});
|
||||
}
|
||||
this.traverseScope(scope, state);
|
||||
state.activeScopes.pop();
|
||||
for (const decl of scope.scope.declarations.values()) {
|
||||
state.uninitialized.delete(decl.identifier.id);
|
||||
}
|
||||
}
|
||||
override visitPlace(
|
||||
_id: InstructionId,
|
||||
place: Place,
|
||||
state: VisitorState,
|
||||
): void {
|
||||
const maybeHoistedFn = state.uninitialized.get(place.identifier.id);
|
||||
if (
|
||||
maybeHoistedFn?.kind === 'func' &&
|
||||
maybeHoistedFn.definition !== place
|
||||
) {
|
||||
CompilerError.throwTodo({
|
||||
reason: '[PruneHoistedContexts] Rewrite hoisted function references',
|
||||
loc: place.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
override transformInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
state: HoistedIdentifiers,
|
||||
state: VisitorState,
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitInstruction(instruction, state);
|
||||
|
||||
/**
|
||||
* Remove hoisted declarations to preserve TDZ
|
||||
*/
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedConst'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Const,
|
||||
if (instruction.value.kind === 'DeclareContext') {
|
||||
const maybeNonHoisted = convertHoistedLValueKind(
|
||||
instruction.value.lvalue.kind,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedLet'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Let,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedFunction'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Function,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (instruction.value.kind === 'StoreContext') {
|
||||
const kind = state.get(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
);
|
||||
if (kind != null) {
|
||||
CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, {
|
||||
reason: 'Expected exactly one store to a hoisted const variable',
|
||||
loc: instruction.loc,
|
||||
});
|
||||
if (maybeNonHoisted != null) {
|
||||
if (
|
||||
kind === InstructionKind.Const ||
|
||||
kind === InstructionKind.Function
|
||||
maybeNonHoisted === InstructionKind.Function &&
|
||||
state.uninitialized.has(instruction.value.lvalue.place.identifier.id)
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
REWRITTEN_HOISTED_CONST,
|
||||
);
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'instruction',
|
||||
instruction: {
|
||||
...instruction,
|
||||
value: {
|
||||
...instruction.value,
|
||||
lvalue: {
|
||||
...instruction.value.lvalue,
|
||||
kind,
|
||||
},
|
||||
type: null,
|
||||
kind: 'StoreLocal',
|
||||
},
|
||||
},
|
||||
state.uninitialized.set(
|
||||
instruction.value.lvalue.place.identifier.id,
|
||||
{
|
||||
kind: 'func',
|
||||
definition: null,
|
||||
},
|
||||
};
|
||||
} else if (kind !== REWRITTEN_HOISTED_LET) {
|
||||
/**
|
||||
* Context variables declared with let may have reassignments. Only
|
||||
* insert a `DeclareContext` for the first encountered `StoreContext`
|
||||
* instruction.
|
||||
*/
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
REWRITTEN_HOISTED_LET,
|
||||
);
|
||||
return {
|
||||
kind: 'replace-many',
|
||||
value: [
|
||||
{
|
||||
kind: 'instruction',
|
||||
instruction: {
|
||||
id: instruction.id,
|
||||
lvalue: null,
|
||||
value: {
|
||||
kind: 'DeclareContext',
|
||||
lvalue: {
|
||||
kind: InstructionKind.Let,
|
||||
place: {...instruction.value.lvalue.place},
|
||||
},
|
||||
loc: instruction.value.loc,
|
||||
},
|
||||
loc: instruction.loc,
|
||||
},
|
||||
},
|
||||
{kind: 'instruction', instruction},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
}
|
||||
if (
|
||||
instruction.value.kind === 'StoreContext' &&
|
||||
instruction.value.lvalue.kind !== InstructionKind.Reassign
|
||||
) {
|
||||
/**
|
||||
* Rewrite StoreContexts let/const that will be pre-declared in
|
||||
* codegen to reassignments.
|
||||
*/
|
||||
const lvalueId = instruction.value.lvalue.place.identifier.id;
|
||||
const isDeclaredByScope = state.activeScopes.find(scope =>
|
||||
scope.has(lvalueId),
|
||||
);
|
||||
if (isDeclaredByScope) {
|
||||
if (
|
||||
instruction.value.lvalue.kind === InstructionKind.Let ||
|
||||
instruction.value.lvalue.kind === InstructionKind.Const
|
||||
) {
|
||||
instruction.value.lvalue.kind = InstructionKind.Reassign;
|
||||
} else if (instruction.value.lvalue.kind === InstructionKind.Function) {
|
||||
const maybeHoistedFn = state.uninitialized.get(lvalueId);
|
||||
if (maybeHoistedFn != null) {
|
||||
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
|
||||
reason: '[PruneHoistedContexts] Unexpected hoisted function',
|
||||
loc: instruction.loc,
|
||||
});
|
||||
maybeHoistedFn.definition = instruction.value.lvalue.place;
|
||||
/**
|
||||
* References to hoisted functions are now "safe" as variable assignments
|
||||
* have finished.
|
||||
*/
|
||||
state.uninitialized.delete(lvalueId);
|
||||
}
|
||||
} else {
|
||||
CompilerError.throwTodo({
|
||||
reason: '[PruneHoistedContexts] Unexpected kind',
|
||||
description: `(${instruction.value.lvalue.kind})`,
|
||||
loc: instruction.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.visitInstruction(instruction, state);
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
isUseRefType,
|
||||
isUseStateType,
|
||||
} from '../HIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {eachCallArgument, eachInstructionLValue} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
@@ -179,22 +178,14 @@ class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
|
||||
].map(id => this.map.get(id) ?? 'Unknown'),
|
||||
);
|
||||
super.visitScope(scope, state);
|
||||
// TODO
|
||||
const scopeDeps = readScopeDependenciesRHIR(scope);
|
||||
[...scopeDeps].forEach(([place, dep]) => {
|
||||
[...scope.scope.dependencies].forEach(ident => {
|
||||
let target: undefined | IdentifierId =
|
||||
this.aliases.find(dep.identifier.id) ?? dep.identifier.id;
|
||||
dep.path.forEach(token => {
|
||||
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') {
|
||||
const idx = scope.scope.dependencies.indexOf(place);
|
||||
CompilerError.invariant(idx !== -1, {
|
||||
reason: 'Expected dependency to be found',
|
||||
loc: place.loc,
|
||||
});
|
||||
|
||||
scope.scope.dependencies.splice(idx, 1);
|
||||
scope.scope.dependencies.delete(ident);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,19 +95,13 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
|
||||
state: ReactiveIdentifiers,
|
||||
): void {
|
||||
this.traverseScope(scopeBlock, state);
|
||||
let j = 0;
|
||||
for (let i = 0; i < scopeBlock.scope.dependencies.length; i++) {
|
||||
const isReactive = state.has(
|
||||
scopeBlock.scope.dependencies[i].identifier.id,
|
||||
);
|
||||
if (isReactive) {
|
||||
scopeBlock.scope.dependencies[j] = scopeBlock.scope.dependencies[i];
|
||||
j++;
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const isReactive = state.has(dep.identifier.id);
|
||||
if (!isReactive) {
|
||||
scopeBlock.scope.dependencies.delete(dep);
|
||||
}
|
||||
}
|
||||
scopeBlock.scope.dependencies.length = j;
|
||||
|
||||
if (scopeBlock.scope.dependencies.length !== 0) {
|
||||
if (scopeBlock.scope.dependencies.size !== 0) {
|
||||
/**
|
||||
* If any of a scope's dependencies are reactive, then all of its
|
||||
* outputs will re-evaluate whenever those dependencies change.
|
||||
|
||||
@@ -56,7 +56,6 @@ class Transform extends ReactiveFunctionTransform<State> {
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
scope: scopeBlock.scope,
|
||||
dependencyInstructions: scopeBlock.dependencyInstructions,
|
||||
instructions: scopeBlock.instructions,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
HIRFunction,
|
||||
InstructionId,
|
||||
@@ -186,20 +185,6 @@ export class ReactiveFunctionVisitor<TState = void> {
|
||||
this.traverseScope(scope, state);
|
||||
}
|
||||
traverseScope(scope: ReactiveScopeBlock, state: TState): void {
|
||||
this.visitBlock(scope.dependencyInstructions, state);
|
||||
const lastDependencyInstruction = scope.dependencyInstructions.at(-1);
|
||||
let lastInstructionId: InstructionId | null = null;
|
||||
if (lastDependencyInstruction !== undefined) {
|
||||
lastInstructionId = lastDependencyInstruction.instruction.id;
|
||||
}
|
||||
for (const dep of scope.scope.dependencies) {
|
||||
CompilerError.invariant(lastInstructionId !== null, {
|
||||
reason:
|
||||
'[ReactiveFunction] Expected at least one dependency instruction.',
|
||||
loc: scope.scope.loc,
|
||||
});
|
||||
this.visitPlace(lastInstructionId, dep, state);
|
||||
}
|
||||
this.visitBlock(scope.instructions, state);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 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 {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
defaultOptions,
|
||||
parsePluginOptions,
|
||||
PluginOptions,
|
||||
} from '../Entrypoint';
|
||||
import {EnvironmentConfig} from '..';
|
||||
import {
|
||||
EnvironmentConfigSchema,
|
||||
PartialEnvironmentConfig,
|
||||
} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from './Result';
|
||||
import {hasOwnProperty} from './utils';
|
||||
|
||||
function tryParseTestPragmaValue(val: string): Result<unknown, unknown> {
|
||||
try {
|
||||
let parsedVal: unknown;
|
||||
const stringMatch = /^"([^"]*)"$/.exec(val);
|
||||
if (stringMatch && stringMatch.length > 1) {
|
||||
parsedVal = stringMatch[1];
|
||||
} else {
|
||||
parsedVal = JSON.parse(val);
|
||||
}
|
||||
return Ok(parsedVal);
|
||||
} catch (e) {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
validateNoCapitalizedCalls: [],
|
||||
enableChangeDetectionForDebugging: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: '$structuralCheck',
|
||||
},
|
||||
enableEmitFreeze: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'makeReadOnly',
|
||||
},
|
||||
enableEmitInstrumentForget: {
|
||||
fn: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useRenderCounter',
|
||||
},
|
||||
gating: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'shouldInstrument',
|
||||
},
|
||||
globalGating: 'DEV',
|
||||
},
|
||||
enableEmitHookGuards: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: '$dispatcherGuard',
|
||||
},
|
||||
inlineJsxTransform: {
|
||||
elementSymbol: 'react.transitional.element',
|
||||
globalDevVar: 'DEV',
|
||||
},
|
||||
lowerContextAccess: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useContext_withSelector',
|
||||
},
|
||||
inferEffectDependencies: [
|
||||
{
|
||||
function: {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'shared-runtime',
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
},
|
||||
numRequiredArgs: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'useEffectWrapper',
|
||||
importSpecifierName: 'default',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
/**
|
||||
* For snap test fixtures and playground only.
|
||||
*/
|
||||
function parseConfigPragmaEnvironmentForTest(
|
||||
pragma: string,
|
||||
): EnvironmentConfig {
|
||||
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
|
||||
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
const keyVal = token.slice(1);
|
||||
const valIdx = keyVal.indexOf(':');
|
||||
const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
|
||||
const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
|
||||
const isSet = val === undefined || val === 'true';
|
||||
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSet && key in testComplexConfigDefaults) {
|
||||
maybeConfig[key] = testComplexConfigDefaults[key];
|
||||
} else if (isSet) {
|
||||
maybeConfig[key] = true;
|
||||
} else if (val === 'false') {
|
||||
maybeConfig[key] = false;
|
||||
} else if (val) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'customMacros' && typeof parsedVal === 'string') {
|
||||
const valSplit = parsedVal.split('.');
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
props.push({type: 'wildcard'});
|
||||
} else if (elt.length > 0) {
|
||||
props.push({type: 'name', name: elt});
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
continue;
|
||||
}
|
||||
maybeConfig[key] = parsedVal;
|
||||
}
|
||||
}
|
||||
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
|
||||
if (config.success) {
|
||||
/**
|
||||
* Unless explicitly enabled, do not insert HMR handling code
|
||||
* in test fixtures or playground to reduce visual noise.
|
||||
*/
|
||||
if (config.data.enableResetCacheOnSourceFileChanges == null) {
|
||||
config.data.enableResetCacheOnSourceFileChanges = false;
|
||||
}
|
||||
return config.data;
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Internal error, could not parse config from pragma string',
|
||||
description: `${fromZodError(config.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
|
||||
gating: {
|
||||
source: 'ReactForgetFeatureFlag',
|
||||
importSpecifierName: 'isForgetEnabled_Fixtures',
|
||||
},
|
||||
};
|
||||
export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
},
|
||||
): PluginOptions {
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment,
|
||||
};
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
const keyVal = token.slice(1);
|
||||
const idx = keyVal.indexOf(':');
|
||||
const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
|
||||
const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
|
||||
if (!hasOwnProperty(defaultOptions, key)) {
|
||||
continue;
|
||||
}
|
||||
const isSet = val === undefined || val === 'true';
|
||||
if (isSet && key in testComplexPluginOptionDefaults) {
|
||||
options[key] = testComplexPluginOptionDefaults[key];
|
||||
} else if (isSet) {
|
||||
options[key] = true;
|
||||
} else if (val === 'false') {
|
||||
options[key] = false;
|
||||
} else if (val != null) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'target' && parsedVal === 'donotuse_meta_internal') {
|
||||
options[key] = {
|
||||
kind: parsedVal,
|
||||
runtimeModule: 'react',
|
||||
};
|
||||
} else {
|
||||
options[key] = parsedVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
} from '../HIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {
|
||||
ReactiveFunctionVisitor,
|
||||
@@ -74,7 +73,7 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
|
||||
* memoized, allowing a transitive memoization check.
|
||||
*/
|
||||
let areDependenciesMemoized = true;
|
||||
for (const [, dep] of readScopeDependenciesRHIR(scopeBlock)) {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
if (isUnmemoized(dep.identifier, this.scopes)) {
|
||||
areDependenciesMemoized = false;
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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, Effect, ErrorSeverity} from '..';
|
||||
import {
|
||||
FunctionEffect,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates that functions with known mutations (ie due to types) cannot be passed
|
||||
* where a frozen value is expected. Example:
|
||||
*
|
||||
* ```
|
||||
* function Component() {
|
||||
* const cache = new Map();
|
||||
* const onClick = () => {
|
||||
* cache.set(...);
|
||||
* }
|
||||
* useHook(onClick); // ERROR: cannot pass a mutable value
|
||||
* return <Foo onClick={onClick} /> // ERROR: cannot pass a mutable value
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable
|
||||
* variables. But unlike other mutables values like an array, the receiver of the function has
|
||||
* no way to avoid mutation — for example, a function can receive an array and choose not to mutate
|
||||
* it, but there's no way to know that a function is mutable and avoid calling it.
|
||||
*
|
||||
* This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
|
||||
* that are passed where a frozen value is expected and rejects them.
|
||||
*/
|
||||
export function validateNoFreezingKnownMutableFunctions(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const contextMutationEffects: Map<
|
||||
IdentifierId,
|
||||
Extract<FunctionEffect, {kind: 'ContextMutation'}>
|
||||
> = new Map();
|
||||
|
||||
function visitOperand(operand: Place): void {
|
||||
if (operand.effect === Effect.Freeze) {
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
errors.push({
|
||||
reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
|
||||
description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
|
||||
loc: operand.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
errors.push({
|
||||
reason: `The function modifies a local variable here`,
|
||||
loc: effect.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'LoadLocal': {
|
||||
const effect = contextMutationEffects.get(value.place.identifier.id);
|
||||
if (effect != null) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, effect);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
const effect = contextMutationEffects.get(value.value.identifier.id);
|
||||
if (effect != null) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, effect);
|
||||
contextMutationEffects.set(
|
||||
value.lvalue.place.identifier.id,
|
||||
effect,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FunctionExpression': {
|
||||
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
|
||||
effect => {
|
||||
return (
|
||||
effect.kind === 'ContextMutation' &&
|
||||
(effect.effect === Effect.Store ||
|
||||
effect.effect === Effect.Mutate) &&
|
||||
Iterable_some(effect.places, place => {
|
||||
return (
|
||||
isMutableEffect(place.effect, place.loc) &&
|
||||
!isRefOrRefLikeMutableType(place.identifier.type)
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
if (knownMutation && knownMutation.kind === 'ContextMutation') {
|
||||
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
visitOperand(operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
visitOperand(operand);
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
|
||||
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization';
|
||||
import {
|
||||
@@ -142,14 +141,14 @@ function getCompareDependencyResultDescription(
|
||||
): string {
|
||||
switch (result) {
|
||||
case CompareDependencyResult.Ok:
|
||||
return 'dependencies equal';
|
||||
return 'Dependencies equal';
|
||||
case CompareDependencyResult.RootDifference:
|
||||
case CompareDependencyResult.PathDifference:
|
||||
return 'inferred different dependency than source';
|
||||
return 'Inferred different dependency than source';
|
||||
case CompareDependencyResult.RefAccessDifference:
|
||||
return 'differences in ref.current access';
|
||||
return 'Differences in ref.current access';
|
||||
case CompareDependencyResult.Subpath:
|
||||
return 'inferred less specific property than source';
|
||||
return 'Inferred less specific property than source';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,17 +279,20 @@ function validateInferredDep(
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected',
|
||||
description: DEBUG
|
||||
? `The inferred dependency was \`${prettyPrintScopeDependency(
|
||||
dep,
|
||||
)}\`, but the source dependencies were [${validDepsInMemoBlock
|
||||
.map(dep => printManualMemoDependency(dep, true))
|
||||
.join(', ')}]. Detail: ${
|
||||
errorDiagnostic
|
||||
? getCompareDependencyResultDescription(errorDiagnostic)
|
||||
: 'none'
|
||||
}`
|
||||
: null,
|
||||
description:
|
||||
DEBUG ||
|
||||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
|
||||
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
|
||||
? `The inferred dependency was \`${prettyPrintScopeDependency(
|
||||
dep,
|
||||
)}\`, but the source dependencies were [${validDepsInMemoBlock
|
||||
.map(dep => printManualMemoDependency(dep, true))
|
||||
.join(', ')}]. ${
|
||||
errorDiagnostic
|
||||
? getCompareDependencyResultDescription(errorDiagnostic)
|
||||
: 'Inferred dependency not present in source'
|
||||
}`
|
||||
: null,
|
||||
loc: memoLocation,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -407,8 +409,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
state.manualMemoState != null &&
|
||||
state.manualMemoState.depsFromSource != null
|
||||
) {
|
||||
const deps = readScopeDependenciesRHIR(scopeBlock).values();
|
||||
for (const dep of deps) {
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
validateInferredDep(
|
||||
dep,
|
||||
this.temporaries,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
const Test = () => <div />;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
@@ -15,7 +15,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
|
||||
const Test = () => {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
const Test = () => <div />;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture currently fails with
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: exception) bar is not a function
|
||||
*/
|
||||
function Foo({value}) {
|
||||
const result = bar();
|
||||
function bar() {
|
||||
return {value};
|
||||
}
|
||||
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{value: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Fixture currently fails with
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: exception) bar is not a function
|
||||
*/
|
||||
function Foo(t0) {
|
||||
const $ = _c(6);
|
||||
const { value } = t0;
|
||||
let bar;
|
||||
let result;
|
||||
if ($[0] !== value) {
|
||||
result = bar();
|
||||
bar = function bar() {
|
||||
return { value };
|
||||
};
|
||||
$[0] = value;
|
||||
$[1] = bar;
|
||||
$[2] = result;
|
||||
} else {
|
||||
bar = $[1];
|
||||
result = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== bar || $[4] !== result) {
|
||||
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
$[3] = bar;
|
||||
$[4] = result;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ value: 2 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -34,8 +34,7 @@ function bar(a, b) {
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [a, b];
|
||||
y = {};
|
||||
let t;
|
||||
t = {};
|
||||
let t = {};
|
||||
|
||||
y = x[0][1];
|
||||
t = x[1][0];
|
||||
|
||||
@@ -35,8 +35,7 @@ function bar(a, b) {
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [a, b];
|
||||
y = {};
|
||||
let t;
|
||||
t = {};
|
||||
let t = {};
|
||||
const f0 = function () {
|
||||
y = x[0][1];
|
||||
t = x[1][0];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
@@ -22,7 +22,7 @@ class Component {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
|
||||
@@ -33,8 +33,7 @@ function useTest() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
let w;
|
||||
w = {};
|
||||
let w = {};
|
||||
|
||||
const t1 = (w = 42);
|
||||
const t2 = w;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
@@ -24,7 +24,7 @@ function Foo(props) {
|
||||
|
||||
```javascript
|
||||
import { shouldInstrument, useRenderCounter } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @compilationMode(infer)
|
||||
// @flow @compilationMode:"infer"
|
||||
export default component Foo(bar: number) {
|
||||
return <Bar bar={bar} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @compilationMode(infer)
|
||||
// @flow @compilationMode:"infer"
|
||||
export default component Foo(bar: number) {
|
||||
return <Bar bar={bar} />;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
shouldInstrument as _shouldInstrument3,
|
||||
useRenderCounter,
|
||||
} from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
function foo() {
|
||||
try {
|
||||
identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
|
||||
} catch {}
|
||||
|
||||
return (
|
||||
<Stringify
|
||||
value={[
|
||||
`` === '',
|
||||
`\n` === '\n',
|
||||
`a\nb`,
|
||||
`\n`,
|
||||
`a${1}b`,
|
||||
` abc \u0041\n\u000a\ŧ`,
|
||||
`abc${1}def`,
|
||||
`abc${1}def${2}`,
|
||||
`abc${1}def${2}ghi`,
|
||||
`a${1 + 3}b${``}c${'d' + `e${2 + 4}f`}`,
|
||||
`1${2}${Math.sin(0)}`,
|
||||
`${NaN}`,
|
||||
`${Infinity}`,
|
||||
`${-Infinity}`,
|
||||
`${Number.MAX_SAFE_INTEGER}`,
|
||||
`${Number.MIN_SAFE_INTEGER}`,
|
||||
`${Number.MAX_VALUE}`,
|
||||
`${Number.MIN_VALUE}`,
|
||||
`${-0}`,
|
||||
`
|
||||
`,
|
||||
`${{}}`,
|
||||
`${[1, 2, 3]}`,
|
||||
`${true}`,
|
||||
`${false}`,
|
||||
`${null}`,
|
||||
`${undefined}`,
|
||||
`123456789${0}`,
|
||||
`${0}123456789`,
|
||||
`${0}123456789${0}`,
|
||||
`${0}1234${5}6789${0}`,
|
||||
`${0}1234${`${0}123456789${`${0}123456789${0}`}`}6789${0}`,
|
||||
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
|
||||
`${`${`${`${0}`}`}`}`,
|
||||
`${`${`${`${''}`}`}`}`,
|
||||
`${`${`${`${identity('')}`}`}`}`,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, identity } from "shared-runtime";
|
||||
|
||||
function foo() {
|
||||
const $ = _c(1);
|
||||
try {
|
||||
identity(`${Symbol("0")}`);
|
||||
} catch {}
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<Stringify
|
||||
value={[
|
||||
true,
|
||||
true,
|
||||
|
||||
"a\nb",
|
||||
"\n",
|
||||
"a1b",
|
||||
" abc A\n\n\u0167",
|
||||
"abc1def",
|
||||
"abc1def2",
|
||||
"abc1def2ghi",
|
||||
"a4bcde6f",
|
||||
`1${2}${Math.sin(0)}`,
|
||||
`${NaN}`,
|
||||
`${Infinity}`,
|
||||
`${-Infinity}`,
|
||||
`${Number.MAX_SAFE_INTEGER}`,
|
||||
`${Number.MIN_SAFE_INTEGER}`,
|
||||
`${Number.MAX_VALUE}`,
|
||||
`${Number.MIN_VALUE}`,
|
||||
"0",
|
||||
"\n ",
|
||||
|
||||
`${{}}`,
|
||||
`${[1, 2, 3]}`,
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
`${undefined}`,
|
||||
"1234567890",
|
||||
"0123456789",
|
||||
"01234567890",
|
||||
"01234567890",
|
||||
"0123401234567890123456789067890",
|
||||
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
|
||||
"0",
|
||||
"",
|
||||
`${`${`${`${identity("")}`}`}`}`,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"value":[true,true,"a\nb","\n","a1b"," abc A\n\nŧ","abc1def","abc1def2","abc1def2ghi","a4bcde6f","120","NaN","Infinity","-Infinity","9007199254740991","-9007199254740991","1.7976931348623157e+308","5e-324","0","\n ","[object Object]","1,2,3","true","false","null","undefined","1234567890","0123456789","01234567890","01234567890","0123401234567890123456789067890","012340123456789067890","0","",""]}</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
function foo() {
|
||||
try {
|
||||
identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
|
||||
} catch {}
|
||||
|
||||
return (
|
||||
<Stringify
|
||||
value={[
|
||||
`` === '',
|
||||
`\n` === '\n',
|
||||
`a\nb`,
|
||||
`\n`,
|
||||
`a${1}b`,
|
||||
` abc \u0041\n\u000a\ŧ`,
|
||||
`abc${1}def`,
|
||||
`abc${1}def${2}`,
|
||||
`abc${1}def${2}ghi`,
|
||||
`a${1 + 3}b${``}c${'d' + `e${2 + 4}f`}`,
|
||||
`1${2}${Math.sin(0)}`,
|
||||
`${NaN}`,
|
||||
`${Infinity}`,
|
||||
`${-Infinity}`,
|
||||
`${Number.MAX_SAFE_INTEGER}`,
|
||||
`${Number.MIN_SAFE_INTEGER}`,
|
||||
`${Number.MAX_VALUE}`,
|
||||
`${Number.MIN_VALUE}`,
|
||||
`${-0}`,
|
||||
`
|
||||
`,
|
||||
`${{}}`,
|
||||
`${[1, 2, 3]}`,
|
||||
`${true}`,
|
||||
`${false}`,
|
||||
`${null}`,
|
||||
`${undefined}`,
|
||||
`123456789${0}`,
|
||||
`${0}123456789`,
|
||||
`${0}123456789${0}`,
|
||||
`${0}1234${5}6789${0}`,
|
||||
`${0}1234${`${0}123456789${`${0}123456789${0}`}`}6789${0}`,
|
||||
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
|
||||
`${`${`${`${0}`}`}`}`,
|
||||
`${`${`${`${''}`}`}`}`,
|
||||
`${`${`${`${identity('')}`}`}`}`,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function foo() {
|
||||
const a = -1;
|
||||
return (
|
||||
<Stringify
|
||||
value={[
|
||||
2 * a,
|
||||
-0,
|
||||
0 === -0,
|
||||
-Infinity,
|
||||
-NaN,
|
||||
a * NaN,
|
||||
a * Infinity,
|
||||
a * -Infinity,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function foo() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<Stringify
|
||||
value={[
|
||||
-2,
|
||||
0,
|
||||
true,
|
||||
-Infinity,
|
||||
-NaN,
|
||||
|
||||
-1 * NaN,
|
||||
-1 * Infinity,
|
||||
-1 * -Infinity,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"value":[-2,0,true,null,null,null,null,null]}</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function foo() {
|
||||
const a = -1;
|
||||
return (
|
||||
<Stringify
|
||||
value={[
|
||||
2 * a,
|
||||
-0,
|
||||
0 === -0,
|
||||
-Infinity,
|
||||
-NaN,
|
||||
a * NaN,
|
||||
a * Infinity,
|
||||
a * -Infinity,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
@@ -58,7 +58,7 @@ function foo() {
|
||||
n0: true,
|
||||
n1: false,
|
||||
n2: false,
|
||||
n3: !-1,
|
||||
n3: false,
|
||||
s0: true,
|
||||
s1: false,
|
||||
s2: false,
|
||||
|
||||
@@ -30,8 +30,7 @@ function Component(props) {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
let x;
|
||||
x = null;
|
||||
let x = null;
|
||||
const callback = () => {
|
||||
console.log(x);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @eslintSuppressionRules(my-app/react-rule)
|
||||
// @eslintSuppressionRules:["my-app","react-rule"]
|
||||
|
||||
/* eslint-disable my-app/react-rule */
|
||||
function lowercasecomponent() {
|
||||
@@ -19,7 +19,7 @@ function lowercasecomponent() {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | // @eslintSuppressionRules(my-app/react-rule)
|
||||
1 | // @eslintSuppressionRules:["my-app","react-rule"]
|
||||
2 |
|
||||
> 3 | /* eslint-disable my-app/react-rule */
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable my-app/react-rule (3:3)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @eslintSuppressionRules(my-app/react-rule)
|
||||
// @eslintSuppressionRules:["my-app","react-rule"]
|
||||
|
||||
/* eslint-disable my-app/react-rule */
|
||||
function lowercasecomponent() {
|
||||
|
||||
@@ -41,7 +41,7 @@ function Component(props) {
|
||||
> 10 | return x;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 11 | }, [props?.items, props.cond]);
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11)
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
|
||||
12 | return (
|
||||
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
|
||||
14 | );
|
||||
|
||||
@@ -41,7 +41,7 @@ function Component(props) {
|
||||
> 10 | return x;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 11 | }, [props?.items, props.cond]);
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11)
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11)
|
||||
12 | return (
|
||||
13 | <ValidateMemoization inputs={[props?.items, props.cond]} output={data} />
|
||||
14 | );
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions
|
||||
|
||||
function useFoo() {
|
||||
const cache = new Map();
|
||||
useHook(() => {
|
||||
cache.set('key', 'value');
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
3 | function useFoo() {
|
||||
4 | const cache = new Map();
|
||||
> 5 | useHook(() => {
|
||||
| ^^^^^^^
|
||||
> 6 | cache.set('key', 'value');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | });
|
||||
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (6:6)
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// @validateNoFreezingKnownMutableFunctions
|
||||
|
||||
function useFoo() {
|
||||
const cache = new Map();
|
||||
useHook(() => {
|
||||
cache.set('key', 'value');
|
||||
});
|
||||
}
|
||||
@@ -29,7 +29,7 @@ function Component(props) {
|
||||
> 6 | // deps are optional
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | }, [props.items?.edges?.nodes]);
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:7)
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items.edges.nodes`, but the source dependencies were [props.items?.edges?.nodes]. Inferred different dependency than source (3:7)
|
||||
8 | return <Foo data={data} />;
|
||||
9 | }
|
||||
10 |
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions
|
||||
function Component() {
|
||||
const cache = new Map();
|
||||
const fn = () => {
|
||||
cache.set('key', 'value');
|
||||
};
|
||||
return <Foo fn={fn} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
5 | cache.set('key', 'value');
|
||||
6 | };
|
||||
> 7 | return <Foo fn={fn} />;
|
||||
| ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (5:5)
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// @validateNoFreezingKnownMutableFunctions
|
||||
function Component() {
|
||||
const cache = new Map();
|
||||
const fn = () => {
|
||||
cache.set('key', 'value');
|
||||
};
|
||||
return <Foo fn={fn} />;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
function Component({ref}) {
|
||||
const value = ref.current;
|
||||
return <div>{value}</div>;
|
||||
@@ -14,7 +14,7 @@ function Component({ref}) {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | // @validateRefAccessDuringRender @compilationMode(infer)
|
||||
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
2 | function Component({ref}) {
|
||||
> 3 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
function Component({ref}) {
|
||||
const value = ref.current;
|
||||
return <div>{value}</div>;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const value = props.ref.current;
|
||||
return <div>{value}</div>;
|
||||
@@ -14,7 +14,7 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | // @validateRefAccessDuringRender @compilationMode(infer)
|
||||
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
2 | function Component(props) {
|
||||
> 3 | const value = props.ref.current;
|
||||
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const value = props.ref.current;
|
||||
return <div>{value}</div>;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function useFoo() {
|
||||
useHook(); // for inference to kick in
|
||||
const cache = new Map();
|
||||
return () => {
|
||||
cache.set('key', 'value');
|
||||
};
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
5 | useHook(); // for inference to kick in
|
||||
6 | const cache = new Map();
|
||||
> 7 | return () => {
|
||||
| ^^^^^^^
|
||||
> 8 | cache.set('key', 'value');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | };
|
||||
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (8:8)
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// @validateNoFreezingKnownMutableFunctions
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function useFoo() {
|
||||
useHook(); // for inference to kick in
|
||||
const cache = new Map();
|
||||
return () => {
|
||||
cache.set('key', 'value');
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const ref = props.ref;
|
||||
ref.current = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const ref = props.ref;
|
||||
ref.current = true;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
> 12 | Ref.current?.click();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 13 | }, []);
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (11:13)
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `Ref.current`, but the source dependencies were []. Inferred dependency not present in source (11:13)
|
||||
14 |
|
||||
15 | return <button onClick={onClick} />;
|
||||
16 | }
|
||||
|
||||
@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
> 12 | notaref.current?.click();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 13 | }, []);
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (11:13)
|
||||
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `notaref.current`, but the source dependencies were []. Inferred dependency not present in source (11:13)
|
||||
14 |
|
||||
15 | return <button onClick={onClick} />;
|
||||
16 | }
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture currently fails with
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: exception) bar is not a function
|
||||
*/
|
||||
function Foo({value}) {
|
||||
const result = bar();
|
||||
function bar() {
|
||||
return {value};
|
||||
}
|
||||
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{value: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
10 | */
|
||||
11 | function Foo({value}) {
|
||||
> 12 | const result = bar();
|
||||
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (12:12)
|
||||
13 | function bar() {
|
||||
14 | return {value};
|
||||
15 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
function Component() {
|
||||
return <Foo />;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
function Component() {
|
||||
return <Foo />;
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
/**
|
||||
* Also see error.todo-functiondecl-hoisting.tsx which shows *invalid*
|
||||
* compilation cases.
|
||||
*
|
||||
* This bailout specifically is a false positive for since this function's only
|
||||
* reference-before-definition are within other functions which are not invoked.
|
||||
*/
|
||||
function Foo() {
|
||||
'use memo';
|
||||
|
||||
function foo() {
|
||||
return bar();
|
||||
}
|
||||
function bar() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
return <Stringify fn1={foo} fn2={bar} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
13 | return bar();
|
||||
14 | }
|
||||
> 15 | function bar() {
|
||||
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (15:15)
|
||||
16 | return 42;
|
||||
17 | }
|
||||
18 |
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user