Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e64b889dd3 | ||
|
|
fbe7bc21b9 | ||
|
|
9518f11856 | ||
|
|
557a64795c | ||
|
|
8a8df5dbdd | ||
|
|
946da518eb | ||
|
|
a437c99ff7 | ||
|
|
4206fe4982 | ||
|
|
4a702865dd | ||
|
|
0ff1d13b80 | ||
|
|
53c9f81049 | ||
|
|
e5a8de81e5 | ||
|
|
7a2c7045ae | ||
|
|
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 | ||
|
|
88b9767404 | ||
|
|
0038c501a3 |
10
.github/workflows/runtime_commit_artifacts.yml
vendored
10
.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
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -274,6 +275,10 @@ function runWithEnvironment(
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoFreezingKnownMutableFunctions) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
|
||||
@@ -3609,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;
|
||||
|
||||
@@ -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,
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -746,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 {
|
||||
@@ -880,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;
|
||||
@@ -1692,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';
|
||||
}
|
||||
|
||||
@@ -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'}],
|
||||
[
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
FunctionExpression,
|
||||
ObjectMethod,
|
||||
PropertyLiteral,
|
||||
convertHoistedLValueKind,
|
||||
} from './HIR';
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
@@ -246,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;
|
||||
}
|
||||
@@ -471,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 {
|
||||
@@ -672,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)) {
|
||||
@@ -698,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);
|
||||
|
||||
@@ -17,7 +17,6 @@ export {buildReactiveScopeTerminalsHIR} from './BuildReactiveScopeTerminalsHIR';
|
||||
export {computeDominatorTree, computePostDominatorTree} from './Dominator';
|
||||
export {
|
||||
Environment,
|
||||
parseConfigPragmaForTests,
|
||||
validateEnvironmentConfig,
|
||||
type EnvironmentConfig,
|
||||
type ExternalFunction,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1000,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`,
|
||||
@@ -1092,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`,
|
||||
@@ -2311,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,
|
||||
|
||||
@@ -255,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': {
|
||||
|
||||
@@ -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'};
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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,
|
||||
PanicThresholdOptions,
|
||||
parsePluginOptions,
|
||||
PluginOptions,
|
||||
} from '../Entrypoint';
|
||||
import {EnvironmentConfig} from '..';
|
||||
import {
|
||||
EnvironmentConfigSchema,
|
||||
PartialEnvironmentConfig,
|
||||
} from '../HIR/Environment';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -141,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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,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,
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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');
|
||||
};
|
||||
}
|
||||
@@ -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 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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: [],
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import fbt from 'fbt';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<Stringify>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
---
|
||||
t3
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import fbt from 'fbt';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
|
||||
* Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
|
||||
* /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
|
||||
* ---
|
||||
* t3
|
||||
* ---
|
||||
*/
|
||||
function Component({firstname, lastname}) {
|
||||
'use memo';
|
||||
return (
|
||||
<Stringify>
|
||||
{fbt(
|
||||
[
|
||||
'Name: ',
|
||||
fbt.param('firstname', <Stringify key={0} name={firstname} />),
|
||||
', ',
|
||||
fbt.param(
|
||||
'lastname',
|
||||
<Stringify key={0} name={lastname}>
|
||||
{fbt('(inner fbt)', 'Inner fbt value')}
|
||||
</Stringify>
|
||||
),
|
||||
],
|
||||
'Name'
|
||||
)}
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstname: 'first', lastname: 'last'}],
|
||||
sequentialRenders: [{firstname: 'first', lastname: 'last'}],
|
||||
};
|
||||
@@ -2,13 +2,22 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const data = useData();
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
items.push(
|
||||
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
|
||||
);
|
||||
}
|
||||
return <>{items}</>;
|
||||
}
|
||||
@@ -17,10 +26,6 @@ const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
@@ -32,41 +37,47 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const data = useData();
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] !== data) {
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = new Map([
|
||||
[0, "value0"],
|
||||
[1, "value1"],
|
||||
]);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const data = useIdentity(t0);
|
||||
let t1;
|
||||
if ($[1] !== data) {
|
||||
const items = [];
|
||||
for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i)}
|
||||
shouldInvokeFns={true}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
t0 = <>{items}</>;
|
||||
$[0] = data;
|
||||
$[1] = t0;
|
||||
t1 = <>{items}</>;
|
||||
$[1] = data;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
return t0;
|
||||
return t1;
|
||||
}
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = new Map();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
@@ -75,4 +86,4 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div></div><div></div><div></div><div></div>
|
||||
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value0"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div>
|
||||
@@ -1,10 +1,19 @@
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const data = useData();
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(<div key={i} onClick={() => data.set(i)} />);
|
||||
items.push(
|
||||
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
|
||||
);
|
||||
}
|
||||
return <>{items}</>;
|
||||
}
|
||||
@@ -13,10 +22,6 @@ const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
function useData() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {CONST_TRUE, useIdentity} from 'shared-runtime';
|
||||
|
||||
const hidden = CONST_TRUE;
|
||||
function useFoo() {
|
||||
const makeCb = useIdentity(() => {
|
||||
const logIntervalId = () => {
|
||||
log(intervalId);
|
||||
};
|
||||
|
||||
let intervalId;
|
||||
if (!hidden) {
|
||||
intervalId = 2;
|
||||
}
|
||||
return () => {
|
||||
logIntervalId();
|
||||
};
|
||||
});
|
||||
|
||||
return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { CONST_TRUE, useIdentity } from "shared-runtime";
|
||||
|
||||
const hidden = CONST_TRUE;
|
||||
function useFoo() {
|
||||
const $ = _c(4);
|
||||
const makeCb = useIdentity(_temp);
|
||||
let t0;
|
||||
if ($[0] !== makeCb) {
|
||||
t0 = makeCb();
|
||||
$[0] = makeCb;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== t0) {
|
||||
t1 = <Stringify fn={t0} shouldInvokeFns={true} />;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp() {
|
||||
const logIntervalId = () => {
|
||||
log(intervalId);
|
||||
};
|
||||
let intervalId;
|
||||
if (!hidden) {
|
||||
intervalId = 2;
|
||||
}
|
||||
return () => {
|
||||
logIntervalId();
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Stringify is not defined
|
||||
@@ -0,0 +1,25 @@
|
||||
import {CONST_TRUE, useIdentity} from 'shared-runtime';
|
||||
|
||||
const hidden = CONST_TRUE;
|
||||
function useFoo() {
|
||||
const makeCb = useIdentity(() => {
|
||||
const logIntervalId = () => {
|
||||
log(intervalId);
|
||||
};
|
||||
|
||||
let intervalId;
|
||||
if (!hidden) {
|
||||
intervalId = 2;
|
||||
}
|
||||
return () => {
|
||||
logIntervalId();
|
||||
};
|
||||
});
|
||||
|
||||
return <Stringify fn={makeCb()} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -30,8 +30,7 @@ function Foo() {
|
||||
getX = () => x;
|
||||
console.log(getX());
|
||||
|
||||
let x;
|
||||
x = 4;
|
||||
let x = 4;
|
||||
x = x + 5;
|
||||
$[0] = getX;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {CONST_NUMBER1, Stringify} from 'shared-runtime';
|
||||
|
||||
function useHook({cond}) {
|
||||
'use memo';
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
if (cond) {
|
||||
x = CONST_NUMBER1;
|
||||
}
|
||||
return <Stringify getX={getX} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: () => {},
|
||||
params: [{cond: true}],
|
||||
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { CONST_NUMBER1, Stringify } from "shared-runtime";
|
||||
|
||||
function useHook(t0) {
|
||||
"use memo";
|
||||
const $ = _c(2);
|
||||
const { cond } = t0;
|
||||
let t1;
|
||||
if ($[0] !== cond) {
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
if (cond) {
|
||||
x = CONST_NUMBER1;
|
||||
}
|
||||
|
||||
t1 = <Stringify getX={getX} shouldInvokeFns={true} />;
|
||||
$[0] = cond;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: () => {},
|
||||
params: [{ cond: true }],
|
||||
sequentialRenders: [{ cond: true }, { cond: true }, { cond: false }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import {CONST_NUMBER1, Stringify} from 'shared-runtime';
|
||||
|
||||
function useHook({cond}) {
|
||||
'use memo';
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
if (cond) {
|
||||
x = CONST_NUMBER1;
|
||||
}
|
||||
return <Stringify getX={getX} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: () => {},
|
||||
params: [{cond: true}],
|
||||
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
|
||||
};
|
||||
@@ -36,8 +36,7 @@ function hoisting(cond) {
|
||||
items.push(bar());
|
||||
};
|
||||
|
||||
let bar;
|
||||
bar = _temp;
|
||||
let bar = _temp;
|
||||
foo();
|
||||
}
|
||||
$[0] = cond;
|
||||
|
||||
@@ -41,11 +41,9 @@ function hoisting() {
|
||||
return result;
|
||||
};
|
||||
|
||||
let foo;
|
||||
foo = () => bar + baz;
|
||||
let foo = () => bar + baz;
|
||||
|
||||
let bar;
|
||||
bar = 3;
|
||||
let bar = 3;
|
||||
const baz = 2;
|
||||
t0 = qux();
|
||||
$[0] = t0;
|
||||
|
||||
@@ -37,8 +37,7 @@ function useHook(t0) {
|
||||
if ($[0] !== cond) {
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
x = CONST_NUMBER0;
|
||||
let x = CONST_NUMBER0;
|
||||
if (cond) {
|
||||
x = x + CONST_NUMBER1;
|
||||
x;
|
||||
|
||||
@@ -38,8 +38,7 @@ function useHook(t0) {
|
||||
if ($[0] !== cond) {
|
||||
const getX = () => x;
|
||||
|
||||
let x;
|
||||
x = CONST_NUMBER0;
|
||||
let x = CONST_NUMBER0;
|
||||
if (cond) {
|
||||
x = x + CONST_NUMBER1;
|
||||
x;
|
||||
|
||||
@@ -29,10 +29,8 @@ function hoisting() {
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
foo = () => bar + baz;
|
||||
|
||||
let bar;
|
||||
bar = 3;
|
||||
let baz;
|
||||
baz = 2;
|
||||
let bar = 3;
|
||||
let baz = 2;
|
||||
$[0] = foo;
|
||||
} else {
|
||||
foo = $[0];
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
i = i + 1;
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop2}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 'prop1', prop2: 'prop2'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'changed', prop2: 'prop2'},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
"use memo";
|
||||
const $ = _c(12);
|
||||
const { prop1, prop2 } = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = new Map([
|
||||
[0, "value0"],
|
||||
[1, "value1"],
|
||||
]);
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const data = useIdentity(t1);
|
||||
let t2;
|
||||
if ($[1] !== data || $[2] !== prop1 || $[3] !== prop2) {
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>,
|
||||
);
|
||||
i = i + 1;
|
||||
|
||||
const t3 = i;
|
||||
let t4;
|
||||
if ($[5] !== data || $[6] !== i || $[7] !== prop2) {
|
||||
t4 = () => data.get(i) + prop2;
|
||||
$[5] = data;
|
||||
$[6] = i;
|
||||
$[7] = prop2;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
let t5;
|
||||
if ($[9] !== t3 || $[10] !== t4) {
|
||||
t5 = <Stringify key={t3} onClick={t4} shouldInvokeFns={true} />;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
items.push(t5);
|
||||
t2 = <>{items}</>;
|
||||
$[1] = data;
|
||||
$[2] = prop1;
|
||||
$[3] = prop2;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prop1: "prop1", prop2: "prop2" }],
|
||||
sequentialRenders: [
|
||||
{ prop1: "prop1", prop2: "prop2" },
|
||||
{ prop1: "prop1", prop2: "prop2" },
|
||||
{ prop1: "changed", prop2: "prop2" },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
<div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
<div>{"onClick":{"kind":"Function","result":"value1changed"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
i = i + 1;
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop2}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 'prop1', prop2: 'prop2'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'changed', prop2: 'prop2'},
|
||||
],
|
||||
};
|
||||
@@ -11,6 +11,7 @@ function Component() {
|
||||
<Text value={'Lauren'} />
|
||||
<Text value={'சத்யா'} />
|
||||
<Text value={'Sathya'} />
|
||||
<Text value={'welcome 👋'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,6 +43,7 @@ function Component() {
|
||||
<Text value="Lauren" />
|
||||
<Text value={"\u0B9A\u0BA4\u0BCD\u0BAF\u0BBE"} />
|
||||
<Text value="Sathya" />
|
||||
<Text value={"welcome \uD83D\uDC4B"} />
|
||||
</div>
|
||||
);
|
||||
$[0] = t0;
|
||||
@@ -74,4 +76,4 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><span>
|
||||
</span><span>A E</span><span>나은</span><span>Lauren</span><span>சத்யா</span><span>Sathya</span></div>
|
||||
</span><span>A E</span><span>나은</span><span>Lauren</span><span>சத்யா</span><span>Sathya</span><span>welcome 👋</span></div>
|
||||
@@ -7,6 +7,7 @@ function Component() {
|
||||
<Text value={'Lauren'} />
|
||||
<Text value={'சத்யா'} />
|
||||
<Text value={'Sathya'} />
|
||||
<Text value={'welcome 👋'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ function Component() {
|
||||
}
|
||||
const x = t0;
|
||||
|
||||
let x_0;
|
||||
x_0 = 56;
|
||||
let x_0 = 56;
|
||||
const fn = function () {
|
||||
x_0 = 42;
|
||||
};
|
||||
|
||||
@@ -33,8 +33,7 @@ function component(a) {
|
||||
m(x);
|
||||
};
|
||||
|
||||
let x;
|
||||
x = { a };
|
||||
let x = { a };
|
||||
m(x);
|
||||
$[0] = a;
|
||||
$[1] = y;
|
||||
|
||||
@@ -41,7 +41,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
> 10 | }
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
> 11 | }, [propA, propB.x.y]);
|
||||
| ^^^^ 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 (5: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 `propB`, but the source dependencies were [propA, propB.x.y]. Inferred less specific property than source (5:11)
|
||||
12 | }
|
||||
13 |
|
||||
14 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -48,9 +48,9 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
> 13 | }
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 14 | }, [propA.a, propB.x.y]);
|
||||
| ^^^^ 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 (6:14)
|
||||
| ^^^^ 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 `propA`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
|
||||
|
||||
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 (6:14)
|
||||
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 `propB`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
|
||||
15 | }
|
||||
16 |
|
||||
17 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -24,7 +24,7 @@ function useHook(maybeRef) {
|
||||
> 6 | return [maybeRef.current];
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | }, [maybeRef]);
|
||||
| ^^^^ 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 (5: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 `maybeRef.current`, but the source dependencies were [maybeRef]. Differences in ref.current access (5:7)
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ function useHook(maybeRef, shouldRead) {
|
||||
> 6 | return () => [maybeRef.current];
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | }, [shouldRead, maybeRef]);
|
||||
| ^^^^ 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 (5: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 `maybeRef.current`, but the source dependencies were [shouldRead, maybeRef]. Differences in ref.current access (5:7)
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
@@ -39,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
> 12 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 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 (9: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`, but the source dependencies were []. Inferred dependency not present in source (9:13)
|
||||
14 | }
|
||||
15 |
|
||||
16 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -22,7 +22,7 @@ function useHook(x) {
|
||||
7 | const aliasedProp = x.y.z;
|
||||
8 |
|
||||
> 9 | return useCallback(() => [aliasedX, x.y.z], [x, aliasedProp]);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ 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 (9:9)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ 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 `aliasedX`, but the source dependencies were [x, aliasedProp]. Inferred different dependency than source (9:9)
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
> 9 | };
|
||||
| ^^^^^^^^^^^^
|
||||
> 10 | }, [propA, propB.x.y]);
|
||||
| ^^^^ 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 (5:10)
|
||||
| ^^^^ 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 `propB?.x.y`, but the source dependencies were [propA, propB.x.y]. Inferred different dependency than source (5:10)
|
||||
11 | }
|
||||
12 |
|
||||
13 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -43,7 +43,7 @@ function Component({propA, propB}) {
|
||||
> 13 | }
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 14 | }, [propA?.a, propB.x.y]);
|
||||
| ^^^^ 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 (6:14)
|
||||
| ^^^^ 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 `propB`, but the source dependencies were [propA?.a, propB.x.y]. Inferred less specific property than source (6:14)
|
||||
15 | }
|
||||
16 |
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ function Component({propA}) {
|
||||
> 6 | return propA.x();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | }, [propA.x]);
|
||||
| ^^^^ 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 (5: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 `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:7)
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ function useHook(x) {
|
||||
7 | const aliasedProp = x.y.z;
|
||||
8 |
|
||||
> 9 | return useMemo(() => [x, x.y.z], [aliasedX, aliasedProp]);
|
||||
| ^^^^^^^^^^^^^^^^ 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 (9:9)
|
||||
| ^^^^^^^^^^^^^^^^ 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 `x`, but the source dependencies were [aliasedX, aliasedProp]. Inferred different dependency than source (9:9)
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
@@ -43,7 +43,7 @@ function Component({propA, propB}) {
|
||||
> 13 | }
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 14 | }, [propA?.a, propB.x.y]);
|
||||
| ^^^^ 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 (6:14)
|
||||
| ^^^^ 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 `propB`, but the source dependencies were [propA?.a, propB.x.y]. Inferred less specific property than source (6:14)
|
||||
15 | }
|
||||
16 |
|
||||
```
|
||||
|
||||
@@ -43,9 +43,9 @@ function Component({propA, propB}) {
|
||||
> 13 | }
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 14 | }, [propA.a, propB.x.y]);
|
||||
| ^^^^ 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 (6:14)
|
||||
| ^^^^ 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 `propA`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
|
||||
|
||||
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 (6:14)
|
||||
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 `propB`, but the source dependencies were [propA.a, propB.x.y]. Inferred less specific property than source (6:14)
|
||||
15 | }
|
||||
16 |
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ function Component({propA}) {
|
||||
> 8 | };
|
||||
| ^^^^^^^^^^^^
|
||||
> 9 | }, [propA.x]);
|
||||
| ^^^^ 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 (5:9)
|
||||
| ^^^^ 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 `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:9)
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ function Component({propA}) {
|
||||
> 6 | return propA.x();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | }, [propA.x]);
|
||||
| ^^^^ 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 (5: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 `propA`, but the source dependencies were [propA.x]. Inferred less specific property than source (5:7)
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ function useFoo(input1) {
|
||||
> 17 | return [y];
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 18 | }, [(mutate(x), y)]);
|
||||
| ^^^^ 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 (16:18)
|
||||
| ^^^^ 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 `input1`, but the source dependencies were [y]. Inferred different dependency than source (16:18)
|
||||
19 |
|
||||
20 | return [x, memoized];
|
||||
21 | }
|
||||
|
||||
@@ -65,8 +65,7 @@ function useBar(t0, cond) {
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let x;
|
||||
x = useIdentity(t1);
|
||||
let x = useIdentity(t1);
|
||||
if (cond) {
|
||||
x = b;
|
||||
}
|
||||
|
||||
@@ -47,8 +47,7 @@ function Foo(t0) {
|
||||
if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
|
||||
const x = [arr1];
|
||||
|
||||
let y;
|
||||
y = [];
|
||||
let y = [];
|
||||
|
||||
getVal1 = _temp;
|
||||
|
||||
|
||||
@@ -47,8 +47,7 @@ function Foo(t0) {
|
||||
if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
|
||||
const x = [arr1];
|
||||
|
||||
let y;
|
||||
y = [];
|
||||
let y = [];
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
|
||||
@@ -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,30 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @panicThreshold(none)
|
||||
'use no memo';
|
||||
|
||||
function Foo() {
|
||||
return <button onClick={() => alert('hello!')}>Click me!</button>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @panicThreshold(none)
|
||||
"use no memo";
|
||||
|
||||
function Foo() {
|
||||
return <button onClick={() => alert("hello!")}>Click me!</button>;
|
||||
}
|
||||
function _temp() {
|
||||
return alert("hello!");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,6 @@
|
||||
// @panicThreshold(none)
|
||||
'use no memo';
|
||||
|
||||
function Foo() {
|
||||
return <button onClick={() => alert('hello!')}>Click me!</button>;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useState, useEffect} from 'react';
|
||||
import {invoke, Stringify} from 'shared-runtime';
|
||||
|
||||
function Content() {
|
||||
const [announcement, setAnnouncement] = useState('');
|
||||
const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]);
|
||||
|
||||
// This was originally passed down as an onClick, but React Compiler's test
|
||||
// evaluator doesn't yet support events outside of React
|
||||
useEffect(() => {
|
||||
if (users.length === 2) {
|
||||
let removedUserName = '';
|
||||
setUsers(prevUsers => {
|
||||
const newUsers = [...prevUsers];
|
||||
removedUserName = newUsers.at(-1).name;
|
||||
newUsers.pop();
|
||||
return newUsers;
|
||||
});
|
||||
|
||||
setAnnouncement(`Removed user (${removedUserName})`);
|
||||
}
|
||||
}, [users]);
|
||||
|
||||
return <Stringify users={users} announcement={announcement} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Content,
|
||||
params: [{}],
|
||||
sequentialRenders: [{}, {}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useState, useEffect } from "react";
|
||||
import { invoke, Stringify } from "shared-runtime";
|
||||
|
||||
function Content() {
|
||||
const $ = _c(8);
|
||||
const [announcement, setAnnouncement] = useState("");
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [{ name: "John Doe" }, { name: "Jane Doe" }];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const [users, setUsers] = useState(t0);
|
||||
let t1;
|
||||
if ($[1] !== users.length) {
|
||||
t1 = () => {
|
||||
if (users.length === 2) {
|
||||
let removedUserName = "";
|
||||
setUsers((prevUsers) => {
|
||||
const newUsers = [...prevUsers];
|
||||
removedUserName = newUsers.at(-1).name;
|
||||
newUsers.pop();
|
||||
return newUsers;
|
||||
});
|
||||
|
||||
setAnnouncement(`Removed user (${removedUserName})`);
|
||||
}
|
||||
};
|
||||
$[1] = users.length;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== users) {
|
||||
t2 = [users];
|
||||
$[3] = users;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== announcement || $[6] !== users) {
|
||||
t3 = <Stringify users={users} announcement={announcement} />;
|
||||
$[5] = announcement;
|
||||
$[6] = users;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Content,
|
||||
params: [{}],
|
||||
sequentialRenders: [{}, {}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}</div>
|
||||
<div>{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
import {invoke, Stringify} from 'shared-runtime';
|
||||
|
||||
function Content() {
|
||||
const [announcement, setAnnouncement] = useState('');
|
||||
const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]);
|
||||
|
||||
// This was originally passed down as an onClick, but React Compiler's test
|
||||
// evaluator doesn't yet support events outside of React
|
||||
useEffect(() => {
|
||||
if (users.length === 2) {
|
||||
let removedUserName = '';
|
||||
setUsers(prevUsers => {
|
||||
const newUsers = [...prevUsers];
|
||||
removedUserName = newUsers.at(-1).name;
|
||||
newUsers.pop();
|
||||
return newUsers;
|
||||
});
|
||||
|
||||
setAnnouncement(`Removed user (${removedUserName})`);
|
||||
}
|
||||
}, [users]);
|
||||
|
||||
return <Stringify users={users} announcement={announcement} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Content,
|
||||
params: [{}],
|
||||
sequentialRenders: [{}, {}],
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function useHook(nodeID, condition) {
|
||||
const graph = useContext(GraphContext);
|
||||
const node = nodeID != null ? graph[nodeID] : null;
|
||||
|
||||
for (const key of Object.keys(node?.fields ?? {})) {
|
||||
if (condition) {
|
||||
return new Class(node.fields?.[field]);
|
||||
}
|
||||
}
|
||||
return new Class();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function useHook(nodeID, condition) {
|
||||
const $ = _c(7);
|
||||
const graph = useContext(GraphContext);
|
||||
const node = nodeID != null ? graph[nodeID] : null;
|
||||
let t0;
|
||||
if ($[0] !== node?.fields) {
|
||||
t0 = Object.keys(node?.fields ?? {});
|
||||
$[0] = node?.fields;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== condition || $[3] !== node || $[4] !== t0) {
|
||||
t1 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: for (const key of t0) {
|
||||
if (condition) {
|
||||
t1 = new Class(node.fields?.[field]);
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
$[2] = condition;
|
||||
$[3] = node;
|
||||
$[4] = t0;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t1;
|
||||
}
|
||||
let t2;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = new Class();
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
function useHook(nodeID, condition) {
|
||||
const graph = useContext(GraphContext);
|
||||
const node = nodeID != null ? graph[nodeID] : null;
|
||||
|
||||
for (const key of Object.keys(node?.fields ?? {})) {
|
||||
if (condition) {
|
||||
return new Class(node.fields?.[field]);
|
||||
}
|
||||
}
|
||||
return new Class();
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Example showing that returned inner function expressions should not be
|
||||
* typed with `freeze` effects.
|
||||
*/
|
||||
function Foo({a, b}) {
|
||||
'use memo';
|
||||
const obj = {};
|
||||
const updaterFactory = () => {
|
||||
/**
|
||||
* This returned function expression *is* a local value. But it might (1)
|
||||
* capture and mutate its context environment and (2) be called during
|
||||
* render.
|
||||
* Typing it with `freeze` effects would be incorrect as it would mean
|
||||
* inferring that calls to updaterFactory()() do not mutate its captured
|
||||
* context.
|
||||
*/
|
||||
return newValue => {
|
||||
obj.value = newValue;
|
||||
obj.a = a;
|
||||
};
|
||||
};
|
||||
|
||||
const updater = updaterFactory();
|
||||
updater(b);
|
||||
return <Stringify cb={obj} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{a: 1, b: 2}],
|
||||
sequentialRenders: [
|
||||
{a: 1, b: 2},
|
||||
{a: 1, b: 3},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Example showing that returned inner function expressions should not be
|
||||
* typed with `freeze` effects.
|
||||
*/
|
||||
function Foo(t0) {
|
||||
"use memo";
|
||||
const $ = _c(3);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const obj = {};
|
||||
const updaterFactory = () => (newValue) => {
|
||||
obj.value = newValue;
|
||||
obj.a = a;
|
||||
};
|
||||
|
||||
const updater = updaterFactory();
|
||||
updater(b);
|
||||
t1 = <Stringify cb={obj} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ a: 1, b: 2 }],
|
||||
sequentialRenders: [
|
||||
{ a: 1, b: 2 },
|
||||
{ a: 1, b: 3 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"cb":{"value":2,"a":1},"shouldInvokeFns":true}</div>
|
||||
<div>{"cb":{"value":3,"a":1},"shouldInvokeFns":true}</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Example showing that returned inner function expressions should not be
|
||||
* typed with `freeze` effects.
|
||||
*/
|
||||
function Foo({a, b}) {
|
||||
'use memo';
|
||||
const obj = {};
|
||||
const updaterFactory = () => {
|
||||
/**
|
||||
* This returned function expression *is* a local value. But it might (1)
|
||||
* capture and mutate its context environment and (2) be called during
|
||||
* render.
|
||||
* Typing it with `freeze` effects would be incorrect as it would mean
|
||||
* inferring that calls to updaterFactory()() do not mutate its captured
|
||||
* context.
|
||||
*/
|
||||
return newValue => {
|
||||
obj.value = newValue;
|
||||
obj.a = a;
|
||||
};
|
||||
};
|
||||
|
||||
const updater = updaterFactory();
|
||||
updater(b);
|
||||
return <Stringify cb={obj} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{a: 1, b: 2}],
|
||||
sequentialRenders: [
|
||||
{a: 1, b: 2},
|
||||
{a: 1, b: 3},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {makeArray, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Example showing that returned inner function expressions should not be
|
||||
* typed with `freeze` effects.
|
||||
* Also see repro-returned-inner-fn-mutates-context
|
||||
*/
|
||||
function Foo({b}) {
|
||||
'use memo';
|
||||
|
||||
const fnFactory = () => {
|
||||
/**
|
||||
* This returned function expression *is* a local value. But it might (1)
|
||||
* capture and mutate its context environment and (2) be called during
|
||||
* render.
|
||||
* Typing it with `freeze` effects would be incorrect as it would mean
|
||||
* inferring that calls to updaterFactory()() do not mutate its captured
|
||||
* context.
|
||||
*/
|
||||
return () => {
|
||||
myVar = () => console.log('a');
|
||||
};
|
||||
};
|
||||
let myVar = () => console.log('b');
|
||||
useIdentity();
|
||||
|
||||
const fn = fnFactory();
|
||||
const arr = makeArray(b);
|
||||
fn(arr);
|
||||
return <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{b: 1}],
|
||||
sequentialRenders: [{b: 1}, {b: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { makeArray, Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Example showing that returned inner function expressions should not be
|
||||
* typed with `freeze` effects.
|
||||
* Also see repro-returned-inner-fn-mutates-context
|
||||
*/
|
||||
function Foo(t0) {
|
||||
"use memo";
|
||||
const $ = _c(3);
|
||||
const { b } = t0;
|
||||
|
||||
const fnFactory = () => () => {
|
||||
myVar = _temp;
|
||||
};
|
||||
|
||||
let myVar = _temp2;
|
||||
useIdentity();
|
||||
|
||||
const fn = fnFactory();
|
||||
const arr = makeArray(b);
|
||||
fn(arr);
|
||||
let t1;
|
||||
if ($[0] !== arr || $[1] !== myVar) {
|
||||
t1 = <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
|
||||
$[0] = arr;
|
||||
$[1] = myVar;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp2() {
|
||||
return console.log("b");
|
||||
}
|
||||
function _temp() {
|
||||
return console.log("a");
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ b: 1 }],
|
||||
sequentialRenders: [{ b: 1 }, { b: 2 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"cb":{"kind":"Function"},"value":[1],"shouldInvokeFns":true}</div>
|
||||
<div>{"cb":{"kind":"Function"},"value":[2],"shouldInvokeFns":true}</div>
|
||||
logs: ['a','a']
|
||||
@@ -0,0 +1,37 @@
|
||||
import {makeArray, Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Example showing that returned inner function expressions should not be
|
||||
* typed with `freeze` effects.
|
||||
* Also see repro-returned-inner-fn-mutates-context
|
||||
*/
|
||||
function Foo({b}) {
|
||||
'use memo';
|
||||
|
||||
const fnFactory = () => {
|
||||
/**
|
||||
* This returned function expression *is* a local value. But it might (1)
|
||||
* capture and mutate its context environment and (2) be called during
|
||||
* render.
|
||||
* Typing it with `freeze` effects would be incorrect as it would mean
|
||||
* inferring that calls to updaterFactory()() do not mutate its captured
|
||||
* context.
|
||||
*/
|
||||
return () => {
|
||||
myVar = () => console.log('a');
|
||||
};
|
||||
};
|
||||
let myVar = () => console.log('b');
|
||||
useIdentity();
|
||||
|
||||
const fn = fnFactory();
|
||||
const arr = makeArray(b);
|
||||
fn(arr);
|
||||
return <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{b: 1}],
|
||||
sequentialRenders: [{b: 1}, {b: 2}],
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
/**
|
||||
* This hook returns a function that when called with an input object,
|
||||
* will return the result of mapping that input with the supplied map
|
||||
* function. Results are cached, so if the same input is passed again,
|
||||
* the same output object will be returned.
|
||||
*
|
||||
* Note that this technically violates the rules of React and is unsafe:
|
||||
* hooks must return immutable objects and be pure, and a function which
|
||||
* captures and mutates a value when called is inherently not pure.
|
||||
*
|
||||
* However, in this case it is technically safe _if_ the mapping function
|
||||
* is pure *and* the resulting objects are never modified. This is because
|
||||
* the function only caches: the result of `returnedFunction(someInput)`
|
||||
* strictly depends on `returnedFunction` and `someInput`, and cannot
|
||||
* otherwise change over time.
|
||||
*/
|
||||
hook useMemoMap<TInput: interface {}, TOutput>(
|
||||
map: TInput => TOutput
|
||||
): TInput => TOutput {
|
||||
return useMemo(() => {
|
||||
// The original issue is that `cache` was not memoized together with the returned
|
||||
// function. This was because neither appears to ever be mutated — the function
|
||||
// is known to mutate `cache` but the function isn't called.
|
||||
//
|
||||
// The fix is to detect cases like this — functions that are mutable but not called -
|
||||
// and ensure that their mutable captures are aliased together into the same scope.
|
||||
const cache = new WeakMap<TInput, TOutput>();
|
||||
return input => {
|
||||
let output = cache.get(input);
|
||||
if (output == null) {
|
||||
output = map(input);
|
||||
cache.set(input, output);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
}, [map]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
|
||||
function useMemoMap(map) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== map) {
|
||||
const cache = new WeakMap();
|
||||
t1 = (input) => {
|
||||
let output = cache.get(input);
|
||||
if (output == null) {
|
||||
output = map(input);
|
||||
cache.set(input, output);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
$[0] = map;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
t0 = t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,38 @@
|
||||
// @flow
|
||||
/**
|
||||
* This hook returns a function that when called with an input object,
|
||||
* will return the result of mapping that input with the supplied map
|
||||
* function. Results are cached, so if the same input is passed again,
|
||||
* the same output object will be returned.
|
||||
*
|
||||
* Note that this technically violates the rules of React and is unsafe:
|
||||
* hooks must return immutable objects and be pure, and a function which
|
||||
* captures and mutates a value when called is inherently not pure.
|
||||
*
|
||||
* However, in this case it is technically safe _if_ the mapping function
|
||||
* is pure *and* the resulting objects are never modified. This is because
|
||||
* the function only caches: the result of `returnedFunction(someInput)`
|
||||
* strictly depends on `returnedFunction` and `someInput`, and cannot
|
||||
* otherwise change over time.
|
||||
*/
|
||||
hook useMemoMap<TInput: interface {}, TOutput>(
|
||||
map: TInput => TOutput
|
||||
): TInput => TOutput {
|
||||
return useMemo(() => {
|
||||
// The original issue is that `cache` was not memoized together with the returned
|
||||
// function. This was because neither appears to ever be mutated — the function
|
||||
// is known to mutate `cache` but the function isn't called.
|
||||
//
|
||||
// The fix is to detect cases like this — functions that are mutable but not called -
|
||||
// and ensure that their mutable captures are aliased together into the same scope.
|
||||
const cache = new WeakMap<TInput, TOutput>();
|
||||
return input => {
|
||||
let output = cache.get(input);
|
||||
if (output == null) {
|
||||
output = map(input);
|
||||
cache.set(input, output);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
}, [map]);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function useHook(nodeID, condition) {
|
||||
const graph = useContext(GraphContext);
|
||||
const node = nodeID != null ? graph[nodeID] : null;
|
||||
|
||||
// (2) Instead we can create a scope around the loop since the loop produces an escaping value
|
||||
let value;
|
||||
for (const key of Object.keys(node?.fields ?? {})) {
|
||||
if (condition) {
|
||||
// (1) We currently create a scope just for this instruction, then later prune the scope because
|
||||
// it's inside a loop
|
||||
value = new Class(node.fields?.[field]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function useHook(nodeID, condition) {
|
||||
const $ = _c(6);
|
||||
const graph = useContext(GraphContext);
|
||||
const node = nodeID != null ? graph[nodeID] : null;
|
||||
|
||||
let value;
|
||||
let t0;
|
||||
if ($[0] !== node?.fields) {
|
||||
t0 = Object.keys(node?.fields ?? {});
|
||||
$[0] = node?.fields;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
if ($[2] !== condition || $[3] !== node || $[4] !== t0) {
|
||||
for (const key of t0) {
|
||||
if (condition) {
|
||||
value = new Class(node.fields?.[field]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$[2] = condition;
|
||||
$[3] = node;
|
||||
$[4] = t0;
|
||||
$[5] = value;
|
||||
} else {
|
||||
value = $[5];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,16 @@
|
||||
function useHook(nodeID, condition) {
|
||||
const graph = useContext(GraphContext);
|
||||
const node = nodeID != null ? graph[nodeID] : null;
|
||||
|
||||
// (2) Instead we can create a scope around the loop since the loop produces an escaping value
|
||||
let value;
|
||||
for (const key of Object.keys(node?.fields ?? {})) {
|
||||
if (condition) {
|
||||
// (1) We currently create a scope just for this instruction, then later prune the scope because
|
||||
// it's inside a loop
|
||||
value = new Class(node.fields?.[field]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
/**
|
||||
* Example of a function expression whose return value shouldn't have
|
||||
* a "freeze" effect on all operands.
|
||||
*
|
||||
* This is because the function expression is passed to `useEffect` and
|
||||
* thus is not a render function. `cleanedUp` is also created within
|
||||
* the effect and is not a render variable.
|
||||
*/
|
||||
function Component({prop}) {
|
||||
const [cleanupCount, setCleanupCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let cleanedUp = false;
|
||||
setTimeout(() => {
|
||||
if (!cleanedUp) {
|
||||
cleanedUp = true;
|
||||
setCleanupCount(c => c + 1);
|
||||
}
|
||||
}, 0);
|
||||
// This return value should not have freeze effects
|
||||
// on its operands
|
||||
return () => {
|
||||
if (!cleanedUp) {
|
||||
cleanedUp = true;
|
||||
setCleanupCount(c => c + 1);
|
||||
}
|
||||
};
|
||||
}, [prop]);
|
||||
return <div>{cleanupCount}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop: 5}],
|
||||
sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Example of a function expression whose return value shouldn't have
|
||||
* a "freeze" effect on all operands.
|
||||
*
|
||||
* This is because the function expression is passed to `useEffect` and
|
||||
* thus is not a render function. `cleanedUp` is also created within
|
||||
* the effect and is not a render variable.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { prop } = t0;
|
||||
const [cleanupCount, setCleanupCount] = useState(0);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
let cleanedUp = false;
|
||||
setTimeout(() => {
|
||||
if (!cleanedUp) {
|
||||
cleanedUp = true;
|
||||
setCleanupCount(_temp);
|
||||
}
|
||||
}, 0);
|
||||
return () => {
|
||||
if (!cleanedUp) {
|
||||
cleanedUp = true;
|
||||
setCleanupCount(_temp2);
|
||||
}
|
||||
};
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== prop) {
|
||||
t2 = [prop];
|
||||
$[1] = prop;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== cleanupCount) {
|
||||
t3 = <div>{cleanupCount}</div>;
|
||||
$[3] = cleanupCount;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp2(c_0) {
|
||||
return c_0 + 1;
|
||||
}
|
||||
function _temp(c) {
|
||||
return c + 1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prop: 5 }],
|
||||
sequentialRenders: [{ prop: 5 }, { prop: 5 }, { prop: 6 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>0</div>
|
||||
<div>0</div>
|
||||
<div>1</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
/**
|
||||
* Example of a function expression whose return value shouldn't have
|
||||
* a "freeze" effect on all operands.
|
||||
*
|
||||
* This is because the function expression is passed to `useEffect` and
|
||||
* thus is not a render function. `cleanedUp` is also created within
|
||||
* the effect and is not a render variable.
|
||||
*/
|
||||
function Component({prop}) {
|
||||
const [cleanupCount, setCleanupCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let cleanedUp = false;
|
||||
setTimeout(() => {
|
||||
if (!cleanedUp) {
|
||||
cleanedUp = true;
|
||||
setCleanupCount(c => c + 1);
|
||||
}
|
||||
}, 0);
|
||||
// This return value should not have freeze effects
|
||||
// on its operands
|
||||
return () => {
|
||||
if (!cleanedUp) {
|
||||
cleanedUp = true;
|
||||
setCleanupCount(c => c + 1);
|
||||
}
|
||||
};
|
||||
}, [prop]);
|
||||
return <div>{cleanupCount}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop: 5}],
|
||||
sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}],
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user