Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2145ea7b59 | ||
|
|
eaf6adb127 | ||
|
|
6ffcac8558 | ||
|
|
724b324b96 | ||
|
|
45a6532a08 | ||
|
|
8dba9311e5 | ||
|
|
2d98b45d92 | ||
|
|
2ba7b07ce1 | ||
|
|
a96a0f3903 | ||
|
|
02a8811864 | ||
|
|
379a083b9a | ||
|
|
534bed5fa7 | ||
|
|
db06f6b751 | ||
|
|
9433fe357a | ||
|
|
0032b2a3ee | ||
|
|
14c50e344c | ||
|
|
f1222f7652 | ||
|
|
9baecbf02b | ||
|
|
0422a00e3e | ||
|
|
47fd2f5e14 | ||
|
|
1dc3bdead1 | ||
|
|
de06211dbe | ||
|
|
ac7820a99e | ||
|
|
3c67bbe5f9 | ||
|
|
2c9a42dfd7 | ||
|
|
f1e70b5e0a | ||
|
|
d587434c35 | ||
|
|
ca292f7a57 | ||
|
|
62a634b972 | ||
|
|
53d07944df | ||
|
|
34ce3acafd | ||
|
|
6445b3154e | ||
|
|
ab5238d5a4 | ||
|
|
7a934a16b8 | ||
|
|
59ef3c4baf | ||
|
|
72965f3615 | ||
|
|
594fb5e9ab | ||
|
|
98286cf8e3 | ||
|
|
cf6e502ed2 | ||
|
|
3958d5d84b | ||
|
|
738aebdbac | ||
|
|
4c9c109cea | ||
|
|
552a5dadcf | ||
|
|
f468d37739 | ||
|
|
c403a7c548 | ||
|
|
fa212fc2b1 | ||
|
|
b080063331 | ||
|
|
66f09bd054 | ||
|
|
0825d019be | ||
|
|
c97ec75324 | ||
|
|
99fd4f2ac1 | ||
|
|
7deda941f7 | ||
|
|
d3b26b2953 | ||
|
|
b211d7023c | ||
|
|
ba4bdb2ab5 | ||
|
|
be11cb5c4b | ||
|
|
557745eb0b | ||
|
|
d3f800d47a | ||
|
|
8e3db095aa | ||
|
|
041754697c | ||
|
|
30fca45c1c | ||
|
|
c499adf8c8 | ||
|
|
1d163962b2 | ||
|
|
ddf8bc3fba | ||
|
|
0860b9cc1f | ||
|
|
538ac7ae4b | ||
|
|
52612a7cbd | ||
|
|
bdb4a96f62 | ||
|
|
c260b38d0a | ||
|
|
5bbf9be246 | ||
|
|
8de7aed892 | ||
|
|
98773466ce | ||
|
|
9784cb379e | ||
|
|
dcf2a6f665 | ||
|
|
36c63d4f9c | ||
|
|
88b40f6e41 | ||
|
|
04a7a61918 |
@@ -58,11 +58,15 @@ export type CompilerDiagnosticDetail =
|
||||
/**
|
||||
* A/the source of the error
|
||||
*/
|
||||
{
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string;
|
||||
};
|
||||
| {
|
||||
kind: 'error';
|
||||
loc: SourceLocation | null;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
kind: 'hint';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export enum CompilerSuggestionOperation {
|
||||
InsertBefore,
|
||||
@@ -134,7 +138,12 @@ export class CompilerDiagnostic {
|
||||
}
|
||||
|
||||
primaryLocation(): SourceLocation | null {
|
||||
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
|
||||
const firstErrorDetail = this.options.details.filter(
|
||||
d => d.kind === 'error',
|
||||
)[0];
|
||||
return firstErrorDetail != null && firstErrorDetail.kind === 'error'
|
||||
? firstErrorDetail.loc
|
||||
: null;
|
||||
}
|
||||
|
||||
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
|
||||
@@ -167,9 +176,14 @@ export class CompilerDiagnostic {
|
||||
buffer.push(codeFrame);
|
||||
break;
|
||||
}
|
||||
case 'hint': {
|
||||
buffer.push('\n\n');
|
||||
buffer.push(detail.message);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
detail.kind,
|
||||
detail,
|
||||
`Unexpected detail kind ${(detail as any).kind}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,9 +33,7 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
|
||||
import {
|
||||
analyseFunctions,
|
||||
dropManualMemoization,
|
||||
inferMutableRanges,
|
||||
inferReactivePlaces,
|
||||
inferReferenceEffects,
|
||||
inlineImmediatelyInvokedFunctionExpressions,
|
||||
inferEffectDependencies,
|
||||
} from '../Inference';
|
||||
@@ -92,7 +90,6 @@ import {
|
||||
} from '../Validation';
|
||||
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
|
||||
import {outlineFunctions} from '../Optimization/OutlineFunctions';
|
||||
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
|
||||
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
|
||||
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
|
||||
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
|
||||
@@ -101,11 +98,11 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
|
||||
import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -229,26 +226,12 @@ function runWithEnvironment(
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
}
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
@@ -263,20 +246,15 @@ function runWithEnvironment(
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingRangeErrors.isErr()) {
|
||||
throw mutabilityAliasingRangeErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
@@ -292,6 +270,10 @@ function runWithEnvironment(
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir));
|
||||
}
|
||||
@@ -304,12 +286,7 @@ function runWithEnvironment(
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.validateNoFreezingKnownMutableFunctions ||
|
||||
env.config.enableNewMutationAliasingModel
|
||||
) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
@@ -322,13 +299,6 @@ function runWithEnvironment(
|
||||
value: hir,
|
||||
});
|
||||
|
||||
propagatePhiTypes(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagatePhiTypes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.validateStaticComponents) {
|
||||
env.logErrors(validateStaticComponents(hir));
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
/**
|
||||
* TypeScript definitions for Flow type JSON representations
|
||||
* Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml
|
||||
*/
|
||||
|
||||
// Base type for all Flow types with a kind field
|
||||
export interface BaseFlowType {
|
||||
kind: string;
|
||||
}
|
||||
|
||||
// Type for representing polarity
|
||||
export type Polarity = 'positive' | 'negative' | 'neutral';
|
||||
|
||||
// Type for representing a name that might be null
|
||||
export type OptionalName = string | null;
|
||||
|
||||
// Open type
|
||||
export interface OpenType extends BaseFlowType {
|
||||
kind: 'Open';
|
||||
}
|
||||
|
||||
// Def type
|
||||
export interface DefType extends BaseFlowType {
|
||||
kind: 'Def';
|
||||
def: DefT;
|
||||
}
|
||||
|
||||
// Eval type
|
||||
export interface EvalType extends BaseFlowType {
|
||||
kind: 'Eval';
|
||||
type: FlowType;
|
||||
destructor: Destructor;
|
||||
}
|
||||
|
||||
// Generic type
|
||||
export interface GenericType extends BaseFlowType {
|
||||
kind: 'Generic';
|
||||
name: string;
|
||||
bound: FlowType;
|
||||
no_infer: boolean;
|
||||
}
|
||||
|
||||
// ThisInstance type
|
||||
export interface ThisInstanceType extends BaseFlowType {
|
||||
kind: 'ThisInstance';
|
||||
instance: InstanceT;
|
||||
is_this: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ThisTypeApp type
|
||||
export interface ThisTypeAppType extends BaseFlowType {
|
||||
kind: 'ThisTypeApp';
|
||||
t1: FlowType;
|
||||
t2: FlowType;
|
||||
t_list?: Array<FlowType>;
|
||||
}
|
||||
|
||||
// TypeApp type
|
||||
export interface TypeAppType extends BaseFlowType {
|
||||
kind: 'TypeApp';
|
||||
type: FlowType;
|
||||
targs: Array<FlowType>;
|
||||
from_value: boolean;
|
||||
use_desc: boolean;
|
||||
}
|
||||
|
||||
// FunProto type
|
||||
export interface FunProtoType extends BaseFlowType {
|
||||
kind: 'FunProto';
|
||||
}
|
||||
|
||||
// ObjProto type
|
||||
export interface ObjProtoType extends BaseFlowType {
|
||||
kind: 'ObjProto';
|
||||
}
|
||||
|
||||
// NullProto type
|
||||
export interface NullProtoType extends BaseFlowType {
|
||||
kind: 'NullProto';
|
||||
}
|
||||
|
||||
// FunProtoBind type
|
||||
export interface FunProtoBindType extends BaseFlowType {
|
||||
kind: 'FunProtoBind';
|
||||
}
|
||||
|
||||
// Intersection type
|
||||
export interface IntersectionType extends BaseFlowType {
|
||||
kind: 'Intersection';
|
||||
members: Array<FlowType>;
|
||||
}
|
||||
|
||||
// Union type
|
||||
export interface UnionType extends BaseFlowType {
|
||||
kind: 'Union';
|
||||
members: Array<FlowType>;
|
||||
}
|
||||
|
||||
// Maybe type
|
||||
export interface MaybeType extends BaseFlowType {
|
||||
kind: 'Maybe';
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
// Optional type
|
||||
export interface OptionalType extends BaseFlowType {
|
||||
kind: 'Optional';
|
||||
type: FlowType;
|
||||
use_desc: boolean;
|
||||
}
|
||||
|
||||
// Keys type
|
||||
export interface KeysType extends BaseFlowType {
|
||||
kind: 'Keys';
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
// Annot type
|
||||
export interface AnnotType extends BaseFlowType {
|
||||
kind: 'Annot';
|
||||
type: FlowType;
|
||||
use_desc: boolean;
|
||||
}
|
||||
|
||||
// Opaque type
|
||||
export interface OpaqueType extends BaseFlowType {
|
||||
kind: 'Opaque';
|
||||
opaquetype: {
|
||||
opaque_id: string;
|
||||
underlying_t: FlowType | null;
|
||||
super_t: FlowType | null;
|
||||
opaque_type_args: Array<{
|
||||
name: string;
|
||||
type: FlowType;
|
||||
polarity: Polarity;
|
||||
}>;
|
||||
opaque_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Namespace type
|
||||
export interface NamespaceType extends BaseFlowType {
|
||||
kind: 'Namespace';
|
||||
namespace_symbol: {
|
||||
symbol: string;
|
||||
};
|
||||
values_type: FlowType;
|
||||
types_tmap: PropertyMap;
|
||||
}
|
||||
|
||||
// Any type
|
||||
export interface AnyType extends BaseFlowType {
|
||||
kind: 'Any';
|
||||
}
|
||||
|
||||
// StrUtil type
|
||||
export interface StrUtilType extends BaseFlowType {
|
||||
kind: 'StrUtil';
|
||||
op: 'StrPrefix' | 'StrSuffix';
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
remainder?: FlowType;
|
||||
}
|
||||
|
||||
// TypeParam definition
|
||||
export interface TypeParam {
|
||||
name: string;
|
||||
bound: FlowType;
|
||||
polarity: Polarity;
|
||||
default: FlowType | null;
|
||||
}
|
||||
|
||||
// EnumInfo types
|
||||
export type EnumInfo = ConcreteEnum | AbstractEnum;
|
||||
|
||||
export interface ConcreteEnum {
|
||||
kind: 'ConcreteEnum';
|
||||
enum_name: string;
|
||||
enum_id: string;
|
||||
members: Array<string>;
|
||||
representation_t: FlowType;
|
||||
has_unknown_members: boolean;
|
||||
}
|
||||
|
||||
export interface AbstractEnum {
|
||||
kind: 'AbstractEnum';
|
||||
representation_t: FlowType;
|
||||
}
|
||||
|
||||
// CanonicalRendersForm types
|
||||
export type CanonicalRendersForm =
|
||||
| InstrinsicRenders
|
||||
| NominalRenders
|
||||
| StructuralRenders
|
||||
| DefaultRenders;
|
||||
|
||||
export interface InstrinsicRenders {
|
||||
kind: 'InstrinsicRenders';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface NominalRenders {
|
||||
kind: 'NominalRenders';
|
||||
renders_id: string;
|
||||
renders_name: string;
|
||||
renders_super: FlowType;
|
||||
}
|
||||
|
||||
export interface StructuralRenders {
|
||||
kind: 'StructuralRenders';
|
||||
renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar';
|
||||
renders_structural_type: FlowType;
|
||||
}
|
||||
|
||||
export interface DefaultRenders {
|
||||
kind: 'DefaultRenders';
|
||||
}
|
||||
|
||||
// InstanceT definition
|
||||
export interface InstanceT {
|
||||
inst: InstType;
|
||||
static: FlowType;
|
||||
super: FlowType;
|
||||
implements: Array<FlowType>;
|
||||
}
|
||||
|
||||
// InstType definition
|
||||
export interface InstType {
|
||||
class_name: string | null;
|
||||
class_id: string;
|
||||
type_args: Array<{
|
||||
name: string;
|
||||
type: FlowType;
|
||||
polarity: Polarity;
|
||||
}>;
|
||||
own_props: PropertyMap;
|
||||
proto_props: PropertyMap;
|
||||
call_t: null | {
|
||||
id: number;
|
||||
call: FlowType;
|
||||
};
|
||||
}
|
||||
|
||||
// DefT types
|
||||
export type DefT =
|
||||
| NumGeneralType
|
||||
| StrGeneralType
|
||||
| BoolGeneralType
|
||||
| BigIntGeneralType
|
||||
| EmptyType
|
||||
| MixedType
|
||||
| NullType
|
||||
| VoidType
|
||||
| SymbolType
|
||||
| FunType
|
||||
| ObjType
|
||||
| ArrType
|
||||
| ClassType
|
||||
| InstanceType
|
||||
| SingletonStrType
|
||||
| NumericStrKeyType
|
||||
| SingletonNumType
|
||||
| SingletonBoolType
|
||||
| SingletonBigIntType
|
||||
| TypeType
|
||||
| PolyType
|
||||
| ReactAbstractComponentType
|
||||
| RendersType
|
||||
| EnumValueType
|
||||
| EnumObjectType;
|
||||
|
||||
export interface NumGeneralType extends BaseFlowType {
|
||||
kind: 'NumGeneral';
|
||||
}
|
||||
|
||||
export interface StrGeneralType extends BaseFlowType {
|
||||
kind: 'StrGeneral';
|
||||
}
|
||||
|
||||
export interface BoolGeneralType extends BaseFlowType {
|
||||
kind: 'BoolGeneral';
|
||||
}
|
||||
|
||||
export interface BigIntGeneralType extends BaseFlowType {
|
||||
kind: 'BigIntGeneral';
|
||||
}
|
||||
|
||||
export interface EmptyType extends BaseFlowType {
|
||||
kind: 'Empty';
|
||||
}
|
||||
|
||||
export interface MixedType extends BaseFlowType {
|
||||
kind: 'Mixed';
|
||||
}
|
||||
|
||||
export interface NullType extends BaseFlowType {
|
||||
kind: 'Null';
|
||||
}
|
||||
|
||||
export interface VoidType extends BaseFlowType {
|
||||
kind: 'Void';
|
||||
}
|
||||
|
||||
export interface SymbolType extends BaseFlowType {
|
||||
kind: 'Symbol';
|
||||
}
|
||||
|
||||
export interface FunType extends BaseFlowType {
|
||||
kind: 'Fun';
|
||||
static: FlowType;
|
||||
funtype: FunTypeObj;
|
||||
}
|
||||
|
||||
export interface ObjType extends BaseFlowType {
|
||||
kind: 'Obj';
|
||||
objtype: ObjTypeObj;
|
||||
}
|
||||
|
||||
export interface ArrType extends BaseFlowType {
|
||||
kind: 'Arr';
|
||||
arrtype: ArrTypeObj;
|
||||
}
|
||||
|
||||
export interface ClassType extends BaseFlowType {
|
||||
kind: 'Class';
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
export interface InstanceType extends BaseFlowType {
|
||||
kind: 'Instance';
|
||||
instance: InstanceT;
|
||||
}
|
||||
|
||||
export interface SingletonStrType extends BaseFlowType {
|
||||
kind: 'SingletonStr';
|
||||
from_annot: boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface NumericStrKeyType extends BaseFlowType {
|
||||
kind: 'NumericStrKey';
|
||||
number: string;
|
||||
string: string;
|
||||
}
|
||||
|
||||
export interface SingletonNumType extends BaseFlowType {
|
||||
kind: 'SingletonNum';
|
||||
from_annot: boolean;
|
||||
number: string;
|
||||
string: string;
|
||||
}
|
||||
|
||||
export interface SingletonBoolType extends BaseFlowType {
|
||||
kind: 'SingletonBool';
|
||||
from_annot: boolean;
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
export interface SingletonBigIntType extends BaseFlowType {
|
||||
kind: 'SingletonBigInt';
|
||||
from_annot: boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface TypeType extends BaseFlowType {
|
||||
kind: 'Type';
|
||||
type_kind: TypeTKind;
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
export type TypeTKind =
|
||||
| 'TypeAliasKind'
|
||||
| 'TypeParamKind'
|
||||
| 'OpaqueKind'
|
||||
| 'ImportTypeofKind'
|
||||
| 'ImportClassKind'
|
||||
| 'ImportEnumKind'
|
||||
| 'InstanceKind'
|
||||
| 'RenderTypeKind';
|
||||
|
||||
export interface PolyType extends BaseFlowType {
|
||||
kind: 'Poly';
|
||||
tparams: Array<TypeParam>;
|
||||
t_out: FlowType;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ReactAbstractComponentType extends BaseFlowType {
|
||||
kind: 'ReactAbstractComponent';
|
||||
config: FlowType;
|
||||
renders: FlowType;
|
||||
instance: ComponentInstance;
|
||||
component_kind: ComponentKind;
|
||||
}
|
||||
|
||||
export type ComponentInstance =
|
||||
| {kind: 'RefSetterProp'; type: FlowType}
|
||||
| {kind: 'Omitted'};
|
||||
|
||||
export type ComponentKind =
|
||||
| {kind: 'Structural'}
|
||||
| {kind: 'Nominal'; id: string; name: string; types: Array<FlowType> | null};
|
||||
|
||||
export interface RendersType extends BaseFlowType {
|
||||
kind: 'Renders';
|
||||
form: CanonicalRendersForm;
|
||||
}
|
||||
|
||||
export interface EnumValueType extends BaseFlowType {
|
||||
kind: 'EnumValue';
|
||||
enum_info: EnumInfo;
|
||||
}
|
||||
|
||||
export interface EnumObjectType extends BaseFlowType {
|
||||
kind: 'EnumObject';
|
||||
enum_value_t: FlowType;
|
||||
enum_info: EnumInfo;
|
||||
}
|
||||
|
||||
// ObjKind types
|
||||
export type ObjKind =
|
||||
| {kind: 'Exact'}
|
||||
| {kind: 'Inexact'}
|
||||
| {kind: 'Indexed'; dicttype: DictType};
|
||||
|
||||
// DictType definition
|
||||
export interface DictType {
|
||||
dict_name: string | null;
|
||||
key: FlowType;
|
||||
value: FlowType;
|
||||
dict_polarity: Polarity;
|
||||
}
|
||||
|
||||
// ArrType types
|
||||
export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT;
|
||||
|
||||
export interface ArrayAT {
|
||||
kind: 'ArrayAT';
|
||||
elem_t: FlowType;
|
||||
}
|
||||
|
||||
export interface TupleAT {
|
||||
kind: 'TupleAT';
|
||||
elem_t: FlowType;
|
||||
elements: Array<TupleElement>;
|
||||
min_arity: number;
|
||||
max_arity: number;
|
||||
inexact: boolean;
|
||||
}
|
||||
|
||||
export interface ROArrayAT {
|
||||
kind: 'ROArrayAT';
|
||||
elem_t: FlowType;
|
||||
}
|
||||
|
||||
// TupleElement definition
|
||||
export interface TupleElement {
|
||||
name: string | null;
|
||||
t: FlowType;
|
||||
polarity: Polarity;
|
||||
optional: boolean;
|
||||
}
|
||||
|
||||
// Flags definition
|
||||
export interface Flags {
|
||||
obj_kind: ObjKind;
|
||||
}
|
||||
|
||||
// Property types
|
||||
export type Property =
|
||||
| FieldProperty
|
||||
| GetProperty
|
||||
| SetProperty
|
||||
| GetSetProperty
|
||||
| MethodProperty;
|
||||
|
||||
export interface FieldProperty {
|
||||
kind: 'Field';
|
||||
type: FlowType;
|
||||
polarity: Polarity;
|
||||
}
|
||||
|
||||
export interface GetProperty {
|
||||
kind: 'Get';
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
export interface SetProperty {
|
||||
kind: 'Set';
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
export interface GetSetProperty {
|
||||
kind: 'GetSet';
|
||||
get_type: FlowType;
|
||||
set_type: FlowType;
|
||||
}
|
||||
|
||||
export interface MethodProperty {
|
||||
kind: 'Method';
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
// PropertyMap definition
|
||||
export interface PropertyMap {
|
||||
[key: string]: Property; // For other properties in the map
|
||||
}
|
||||
|
||||
// ObjType definition
|
||||
export interface ObjTypeObj {
|
||||
flags: Flags;
|
||||
props: PropertyMap;
|
||||
proto_t: FlowType;
|
||||
call_t: number | null;
|
||||
}
|
||||
|
||||
// FunType definition
|
||||
export interface FunTypeObj {
|
||||
this_t: {
|
||||
type: FlowType;
|
||||
status: ThisStatus;
|
||||
};
|
||||
params: Array<{
|
||||
name: string | null;
|
||||
type: FlowType;
|
||||
}>;
|
||||
rest_param: null | {
|
||||
name: string | null;
|
||||
type: FlowType;
|
||||
};
|
||||
return_t: FlowType;
|
||||
type_guard: null | {
|
||||
inferred: boolean;
|
||||
param_name: string;
|
||||
type_guard: FlowType;
|
||||
one_sided: boolean;
|
||||
};
|
||||
effect: Effect;
|
||||
}
|
||||
|
||||
// ThisStatus types
|
||||
export type ThisStatus =
|
||||
| {kind: 'This_Method'; unbound: boolean}
|
||||
| {kind: 'This_Function'};
|
||||
|
||||
// Effect types
|
||||
export type Effect =
|
||||
| {kind: 'HookDecl'; id: string}
|
||||
| {kind: 'HookAnnot'}
|
||||
| {kind: 'ArbitraryEffect'}
|
||||
| {kind: 'AnyEffect'};
|
||||
|
||||
// Destructor types
|
||||
export type Destructor =
|
||||
| NonMaybeTypeDestructor
|
||||
| PropertyTypeDestructor
|
||||
| ElementTypeDestructor
|
||||
| OptionalIndexedAccessNonMaybeTypeDestructor
|
||||
| OptionalIndexedAccessResultTypeDestructor
|
||||
| ExactTypeDestructor
|
||||
| ReadOnlyTypeDestructor
|
||||
| PartialTypeDestructor
|
||||
| RequiredTypeDestructor
|
||||
| SpreadTypeDestructor
|
||||
| SpreadTupleTypeDestructor
|
||||
| RestTypeDestructor
|
||||
| ValuesTypeDestructor
|
||||
| ConditionalTypeDestructor
|
||||
| TypeMapDestructor
|
||||
| ReactElementPropsTypeDestructor
|
||||
| ReactElementConfigTypeDestructor
|
||||
| ReactCheckComponentConfigDestructor
|
||||
| ReactDRODestructor
|
||||
| MakeHooklikeDestructor
|
||||
| MappedTypeDestructor
|
||||
| EnumTypeDestructor;
|
||||
|
||||
export interface NonMaybeTypeDestructor {
|
||||
kind: 'NonMaybeType';
|
||||
}
|
||||
|
||||
export interface PropertyTypeDestructor {
|
||||
kind: 'PropertyType';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ElementTypeDestructor {
|
||||
kind: 'ElementType';
|
||||
index_type: FlowType;
|
||||
}
|
||||
|
||||
export interface OptionalIndexedAccessNonMaybeTypeDestructor {
|
||||
kind: 'OptionalIndexedAccessNonMaybeType';
|
||||
index: OptionalIndexedAccessIndex;
|
||||
}
|
||||
|
||||
export type OptionalIndexedAccessIndex =
|
||||
| {kind: 'StrLitIndex'; name: string}
|
||||
| {kind: 'TypeIndex'; type: FlowType};
|
||||
|
||||
export interface OptionalIndexedAccessResultTypeDestructor {
|
||||
kind: 'OptionalIndexedAccessResultType';
|
||||
}
|
||||
|
||||
export interface ExactTypeDestructor {
|
||||
kind: 'ExactType';
|
||||
}
|
||||
|
||||
export interface ReadOnlyTypeDestructor {
|
||||
kind: 'ReadOnlyType';
|
||||
}
|
||||
|
||||
export interface PartialTypeDestructor {
|
||||
kind: 'PartialType';
|
||||
}
|
||||
|
||||
export interface RequiredTypeDestructor {
|
||||
kind: 'RequiredType';
|
||||
}
|
||||
|
||||
export interface SpreadTypeDestructor {
|
||||
kind: 'SpreadType';
|
||||
target: SpreadTarget;
|
||||
operands: Array<SpreadOperand>;
|
||||
operand_slice: Slice | null;
|
||||
}
|
||||
|
||||
export type SpreadTarget =
|
||||
| {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'}
|
||||
| {kind: 'Annot'; make_exact: boolean};
|
||||
|
||||
export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice;
|
||||
|
||||
export interface Slice {
|
||||
kind: 'Slice';
|
||||
prop_map: PropertyMap;
|
||||
generics: Array<string>;
|
||||
dict: DictType | null;
|
||||
reachable_targs: Array<{
|
||||
type: FlowType;
|
||||
polarity: Polarity;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SpreadTupleTypeDestructor {
|
||||
kind: 'SpreadTupleType';
|
||||
inexact: boolean;
|
||||
resolved_rev: string;
|
||||
unresolved: string;
|
||||
}
|
||||
|
||||
export interface RestTypeDestructor {
|
||||
kind: 'RestType';
|
||||
merge_mode: RestMergeMode;
|
||||
type: FlowType;
|
||||
}
|
||||
|
||||
export type RestMergeMode =
|
||||
| {kind: 'SpreadReversal'}
|
||||
| {kind: 'ReactConfigMerge'; polarity: Polarity}
|
||||
| {kind: 'Omit'};
|
||||
|
||||
export interface ValuesTypeDestructor {
|
||||
kind: 'ValuesType';
|
||||
}
|
||||
|
||||
export interface ConditionalTypeDestructor {
|
||||
kind: 'ConditionalType';
|
||||
distributive_tparam_name: string | null;
|
||||
infer_tparams: string;
|
||||
extends_t: FlowType;
|
||||
true_t: FlowType;
|
||||
false_t: FlowType;
|
||||
}
|
||||
|
||||
export interface TypeMapDestructor {
|
||||
kind: 'ObjectKeyMirror';
|
||||
}
|
||||
|
||||
export interface ReactElementPropsTypeDestructor {
|
||||
kind: 'ReactElementPropsType';
|
||||
}
|
||||
|
||||
export interface ReactElementConfigTypeDestructor {
|
||||
kind: 'ReactElementConfigType';
|
||||
}
|
||||
|
||||
export interface ReactCheckComponentConfigDestructor {
|
||||
kind: 'ReactCheckComponentConfig';
|
||||
props: {
|
||||
[key: string]: Property;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReactDRODestructor {
|
||||
kind: 'ReactDRO';
|
||||
dro_type:
|
||||
| 'HookReturn'
|
||||
| 'HookArg'
|
||||
| 'Props'
|
||||
| 'ImmutableAnnot'
|
||||
| 'DebugAnnot';
|
||||
}
|
||||
|
||||
export interface MakeHooklikeDestructor {
|
||||
kind: 'MakeHooklike';
|
||||
}
|
||||
|
||||
export interface MappedTypeDestructor {
|
||||
kind: 'MappedType';
|
||||
homomorphic: Homomorphic;
|
||||
distributive_tparam_name: string | null;
|
||||
property_type: FlowType;
|
||||
mapped_type_flags: {
|
||||
variance: Polarity;
|
||||
optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality';
|
||||
};
|
||||
}
|
||||
|
||||
export type Homomorphic =
|
||||
| {kind: 'Homomorphic'}
|
||||
| {kind: 'Unspecialized'}
|
||||
| {kind: 'SemiHomomorphic'; type: FlowType};
|
||||
|
||||
export interface EnumTypeDestructor {
|
||||
kind: 'EnumType';
|
||||
}
|
||||
|
||||
// Union of all possible Flow types
|
||||
export type FlowType =
|
||||
| OpenType
|
||||
| DefType
|
||||
| EvalType
|
||||
| GenericType
|
||||
| ThisInstanceType
|
||||
| ThisTypeAppType
|
||||
| TypeAppType
|
||||
| FunProtoType
|
||||
| ObjProtoType
|
||||
| NullProtoType
|
||||
| FunProtoBindType
|
||||
| IntersectionType
|
||||
| UnionType
|
||||
| MaybeType
|
||||
| OptionalType
|
||||
| KeysType
|
||||
| AnnotType
|
||||
| OpaqueType
|
||||
| NamespaceType
|
||||
| AnyType
|
||||
| StrUtilType;
|
||||
@@ -0,0 +1,131 @@
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
ConcreteType,
|
||||
printConcrete,
|
||||
printType,
|
||||
StructuralValue,
|
||||
Type,
|
||||
VariableId,
|
||||
} from './Types';
|
||||
|
||||
export function unsupportedLanguageFeature(
|
||||
desc: string,
|
||||
loc: SourceLocation,
|
||||
): never {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Typedchecker does not currently support language feature: ${desc}`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
export type UnificationError =
|
||||
| {
|
||||
kind: 'TypeUnification';
|
||||
left: ConcreteType<Type>;
|
||||
right: ConcreteType<Type>;
|
||||
}
|
||||
| {
|
||||
kind: 'StructuralUnification';
|
||||
left: StructuralValue;
|
||||
right: ConcreteType<Type>;
|
||||
};
|
||||
|
||||
function printUnificationError(err: UnificationError): string {
|
||||
if (err.kind === 'TypeUnification') {
|
||||
return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`;
|
||||
} else {
|
||||
return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function raiseUnificationErrors(
|
||||
errs: null | Array<UnificationError>,
|
||||
loc: SourceLocation,
|
||||
): void {
|
||||
if (errs != null) {
|
||||
if (errs.length === 0) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Should not have array of zero errors',
|
||||
loc,
|
||||
});
|
||||
} else if (errs.length === 1) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Unable to unify types because ${printUnificationError(errs[0])}`,
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
const messages = errs
|
||||
.map(err => `\t* ${printUnificationError(err)}`)
|
||||
.join('\n');
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Unable to unify types because:\n${messages}`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unresolvableTypeVariable(
|
||||
id: VariableId,
|
||||
loc: SourceLocation,
|
||||
): never {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Unable to resolve free variable ${id} to a concrete type`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
|
||||
if (explicit) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Undefined is not a valid operand of \`+\``,
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Value may be undefined, which is not a valid operand of \`+\``,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function unsupportedTypeAnnotation(
|
||||
desc: string,
|
||||
loc: SourceLocation,
|
||||
): never {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Typedchecker does not currently support type annotation: ${desc}`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
export function checkTypeArgumentArity(
|
||||
desc: string,
|
||||
expected: number,
|
||||
actual: number,
|
||||
loc: SourceLocation,
|
||||
): void {
|
||||
if (expected !== actual) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function notAFunction(desc: string, loc: SourceLocation): void {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Cannot call ${desc} because it is not a function`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
export function notAPolymorphicFunction(
|
||||
desc: string,
|
||||
loc: SourceLocation,
|
||||
): void {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
import {GeneratedSource} from '../HIR';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {unsupportedLanguageFeature} from './TypeErrors';
|
||||
import {
|
||||
ConcreteType,
|
||||
ResolvedType,
|
||||
TypeParameter,
|
||||
TypeParameterId,
|
||||
DEBUG,
|
||||
printConcrete,
|
||||
printType,
|
||||
} from './Types';
|
||||
|
||||
export function substitute(
|
||||
type: ConcreteType<ResolvedType>,
|
||||
typeParameters: Array<TypeParameter<ResolvedType>>,
|
||||
typeArguments: Array<ResolvedType>,
|
||||
): ResolvedType {
|
||||
const substMap = new Map<TypeParameterId, ResolvedType>();
|
||||
for (let i = 0; i < typeParameters.length; i++) {
|
||||
// TODO: Length checks to make sure type params match up with args
|
||||
const typeParameter = typeParameters[i];
|
||||
const typeArgument = typeArguments[i];
|
||||
substMap.set(typeParameter.id, typeArgument);
|
||||
}
|
||||
const substitutionFunction = (t: ResolvedType): ResolvedType => {
|
||||
// TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types
|
||||
if (t.type.kind === 'Generic' && substMap.has(t.type.id)) {
|
||||
const substitutedType = substMap.get(t.type.id)!;
|
||||
return substitutedType;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'Concrete',
|
||||
type: mapType(substitutionFunction, t.type),
|
||||
platform: t.platform,
|
||||
};
|
||||
};
|
||||
|
||||
const substituted = mapType(substitutionFunction, type);
|
||||
|
||||
if (DEBUG) {
|
||||
let substs = '';
|
||||
for (let i = 0; i < typeParameters.length; i++) {
|
||||
const typeParameter = typeParameters[i];
|
||||
const typeArgument = typeArguments[i];
|
||||
substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`;
|
||||
}
|
||||
console.log(
|
||||
`${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'};
|
||||
}
|
||||
|
||||
export function mapType<T, U>(
|
||||
f: (t: T) => U,
|
||||
type: ConcreteType<T>,
|
||||
): ConcreteType<U> {
|
||||
switch (type.kind) {
|
||||
case 'Mixed':
|
||||
case 'Number':
|
||||
case 'String':
|
||||
case 'Boolean':
|
||||
case 'Void':
|
||||
return type;
|
||||
|
||||
case 'Nullable':
|
||||
return {
|
||||
kind: 'Nullable',
|
||||
type: f(type.type),
|
||||
};
|
||||
|
||||
case 'Array':
|
||||
return {
|
||||
kind: 'Array',
|
||||
element: f(type.element),
|
||||
};
|
||||
|
||||
case 'Set':
|
||||
return {
|
||||
kind: 'Set',
|
||||
element: f(type.element),
|
||||
};
|
||||
|
||||
case 'Map':
|
||||
return {
|
||||
kind: 'Map',
|
||||
key: f(type.key),
|
||||
value: f(type.value),
|
||||
};
|
||||
|
||||
case 'Function':
|
||||
return {
|
||||
kind: 'Function',
|
||||
typeParameters:
|
||||
type.typeParameters?.map(param => ({
|
||||
id: param.id,
|
||||
name: param.name,
|
||||
bound: f(param.bound),
|
||||
})) ?? null,
|
||||
params: type.params.map(f),
|
||||
returnType: f(type.returnType),
|
||||
};
|
||||
|
||||
case 'Component': {
|
||||
return {
|
||||
kind: 'Component',
|
||||
children: type.children != null ? f(type.children) : null,
|
||||
props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])),
|
||||
};
|
||||
}
|
||||
|
||||
case 'Generic':
|
||||
return {
|
||||
kind: 'Generic',
|
||||
id: type.id,
|
||||
bound: f(type.bound),
|
||||
};
|
||||
|
||||
case 'Object':
|
||||
return type;
|
||||
|
||||
case 'Tuple':
|
||||
return {
|
||||
kind: 'Tuple',
|
||||
id: type.id,
|
||||
members: type.members.map(f),
|
||||
};
|
||||
|
||||
case 'Structural':
|
||||
return type;
|
||||
|
||||
case 'Enum':
|
||||
case 'Union':
|
||||
case 'Instance':
|
||||
unsupportedLanguageFeature(type.kind, GeneratedSource);
|
||||
|
||||
default:
|
||||
assertExhaustive(type, 'Unknown type kind');
|
||||
}
|
||||
}
|
||||
|
||||
export function diff<R, T>(
|
||||
a: ConcreteType<T>,
|
||||
b: ConcreteType<T>,
|
||||
onChild: (a: T, b: T) => R,
|
||||
onChildMismatch: (child: R, cur: R) => R,
|
||||
onMismatch: (a: ConcreteType<T>, b: ConcreteType<T>, cur: R) => R,
|
||||
init: R,
|
||||
): R {
|
||||
let errors = init;
|
||||
|
||||
// Check if kinds match
|
||||
if (a.kind !== b.kind) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Based on kind, check other properties
|
||||
switch (a.kind) {
|
||||
case 'Mixed':
|
||||
case 'Number':
|
||||
case 'String':
|
||||
case 'Boolean':
|
||||
case 'Void':
|
||||
// Simple types, no further checks needed
|
||||
break;
|
||||
|
||||
case 'Nullable':
|
||||
// Check the nested type
|
||||
errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors);
|
||||
break;
|
||||
|
||||
case 'Array':
|
||||
case 'Set':
|
||||
// Check the element type
|
||||
errors = onChildMismatch(
|
||||
onChild(a.element, (b as typeof a).element),
|
||||
errors,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'Map':
|
||||
// Check both key and value types
|
||||
errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors);
|
||||
errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors);
|
||||
break;
|
||||
|
||||
case 'Function': {
|
||||
const bFunc = b as typeof a;
|
||||
|
||||
// Check type parameters
|
||||
if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
|
||||
if (a.typeParameters != null && bFunc.typeParameters != null) {
|
||||
if (a.typeParameters.length !== bFunc.typeParameters.length) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
|
||||
// Type parameters are just numbers, so we can compare them directly
|
||||
for (let i = 0; i < a.typeParameters.length; i++) {
|
||||
if (a.typeParameters[i] !== bFunc.typeParameters[i]) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check parameters
|
||||
if (a.params.length !== bFunc.params.length) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.params.length; i++) {
|
||||
errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors);
|
||||
}
|
||||
|
||||
// Check return type
|
||||
errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Component': {
|
||||
const bComp = b as typeof a;
|
||||
|
||||
// Check children
|
||||
if (a.children !== bComp.children) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
|
||||
// Check props
|
||||
if (a.props.size !== bComp.props.size) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
|
||||
for (const [k, v] of a.props) {
|
||||
const bProp = bComp.props.get(k);
|
||||
if (bProp == null) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
} else {
|
||||
errors = onChildMismatch(onChild(v, bProp), errors);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Generic': {
|
||||
// Check that the type parameter IDs match
|
||||
if (a.id !== (b as typeof a).id) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Structural': {
|
||||
const bStruct = b as typeof a;
|
||||
|
||||
// Check that the structural IDs match
|
||||
if (a.id !== bStruct.id) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Object': {
|
||||
const bNom = b as typeof a;
|
||||
|
||||
// Check that the nominal IDs match
|
||||
if (a.id !== bNom.id) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Tuple': {
|
||||
const bTuple = b as typeof a;
|
||||
|
||||
// Check that the tuple IDs match
|
||||
if (a.id !== bTuple.id) {
|
||||
errors = onMismatch(a, b, errors);
|
||||
}
|
||||
for (let i = 0; i < a.members.length; i++) {
|
||||
errors = onChildMismatch(
|
||||
onChild(a.members[i], bTuple.members[i]),
|
||||
errors,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Enum':
|
||||
case 'Instance':
|
||||
case 'Union': {
|
||||
unsupportedLanguageFeature(a.kind, GeneratedSource);
|
||||
}
|
||||
|
||||
default:
|
||||
assertExhaustive(a, 'Unknown type kind');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function filterOptional(t: ResolvedType): ResolvedType {
|
||||
if (t.kind === 'Concrete' && t.type.kind === 'Nullable') {
|
||||
return t.type.type;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
1000
compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts
Normal file
1000
compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,7 @@ import {
|
||||
} from './ObjectShape';
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
import {FlowTypeEnv} from '../Flood/Types';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
elementSymbol: z.union([
|
||||
@@ -244,9 +245,10 @@ export const EnvironmentConfigSchema = z.object({
|
||||
enableUseTypeAnnotations: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enable a new model for mutability and aliasing inference
|
||||
* Allows specifying a function that can populate HIR with type information from
|
||||
* Flow
|
||||
*/
|
||||
enableNewMutationAliasingModel: z.boolean().default(true),
|
||||
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
@@ -323,6 +325,12 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoSetStateInEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that effects are not used to calculate derived data which could instead be computed
|
||||
* during render.
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
* instead.
|
||||
@@ -691,6 +699,8 @@ export class Environment {
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
parentFunction: NodePath<t.Function>;
|
||||
|
||||
#flowTypeEnvironment: FlowTypeEnv | null;
|
||||
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
fnType: ReactFunctionType,
|
||||
@@ -759,6 +769,26 @@ export class Environment {
|
||||
this.parentFunction = parentFunction;
|
||||
this.#contextIdentifiers = contextIdentifiers;
|
||||
this.#hoistedIdentifiers = new Set();
|
||||
|
||||
if (config.flowTypeProvider != null) {
|
||||
this.#flowTypeEnvironment = new FlowTypeEnv();
|
||||
CompilerError.invariant(code != null, {
|
||||
reason:
|
||||
'Expected Environment to be initialized with source code when a Flow type provider is specified',
|
||||
loc: null,
|
||||
});
|
||||
this.#flowTypeEnvironment.init(this, code);
|
||||
} else {
|
||||
this.#flowTypeEnvironment = null;
|
||||
}
|
||||
}
|
||||
|
||||
get typeContext(): FlowTypeEnv {
|
||||
CompilerError.invariant(this.#flowTypeEnvironment != null, {
|
||||
reason: 'Flow type environment not initialized',
|
||||
loc: null,
|
||||
});
|
||||
return this.#flowTypeEnvironment;
|
||||
}
|
||||
|
||||
get isInferredMemoEnabled(): boolean {
|
||||
|
||||
@@ -114,6 +114,99 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'entries',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@object'],
|
||||
rest: null,
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
value: ValueKind.Mutable,
|
||||
},
|
||||
// Object values are captured into the return
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@object',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
'keys',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@object'],
|
||||
rest: null,
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
value: ValueKind.Mutable,
|
||||
},
|
||||
// Only keys are captured, and keys are immutable
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: '@object',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
'values',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@object'],
|
||||
rest: null,
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
value: ValueKind.Mutable,
|
||||
},
|
||||
// Object values are captured into the return
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@object',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
|
||||
import {BindingKind} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {isReservedWord} from '../Utils/Keyword';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -281,30 +282,13 @@ export type HIRFunction = {
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returns: Place;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
body: HIR;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
directives: Array<string>;
|
||||
aliasingEffects?: Array<AliasingEffect> | null;
|
||||
aliasingEffects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type FunctionEffect =
|
||||
| {
|
||||
kind: 'GlobalMutation';
|
||||
error: CompilerErrorDetailOptions;
|
||||
}
|
||||
| {
|
||||
kind: 'ReactMutation';
|
||||
error: CompilerErrorDetailOptions;
|
||||
}
|
||||
| {
|
||||
kind: 'ContextMutation';
|
||||
places: ReadonlySet<Place>;
|
||||
effect: Effect;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
/*
|
||||
* Each reactive scope may have its own control-flow, so the instructions form
|
||||
* a control-flow graph. The graph comprises a set of basic blocks which reference
|
||||
@@ -1320,12 +1304,21 @@ export function forkTemporaryIdentifier(
|
||||
* original source code.
|
||||
*/
|
||||
export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
CompilerError.invariant(t.isValidIdentifier(name), {
|
||||
reason: `Expected a valid identifier name`,
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
});
|
||||
if (isReservedWord(name)) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
});
|
||||
} else {
|
||||
CompilerError.invariant(t.isValidIdentifier(name), {
|
||||
reason: `Expected a valid identifier name`,
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
|
||||
@@ -142,6 +142,7 @@ function parseAliasingSignatureConfig(
|
||||
const effects = typeConfig.effects.map(
|
||||
(effect: AliasingEffectConfig): AliasingEffect => {
|
||||
switch (effect.kind) {
|
||||
case 'ImmutableCapture':
|
||||
case 'CreateFrom':
|
||||
case 'Capture':
|
||||
case 'Alias':
|
||||
|
||||
@@ -554,23 +554,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
const context = instrValue.loweredFunc.func.context
|
||||
.map(dep => printPlace(dep))
|
||||
.join(',');
|
||||
const effects =
|
||||
instrValue.loweredFunc.func.effects
|
||||
?.map(effect => {
|
||||
if (effect.kind === 'ContextMutation') {
|
||||
return `ContextMutation places=[${[...effect.places]
|
||||
.map(place => printPlace(place))
|
||||
.join(', ')}] effect=${effect.effect}`;
|
||||
} else {
|
||||
return `GlobalMutation`;
|
||||
}
|
||||
})
|
||||
.join(', ') ?? '';
|
||||
const aliasingEffects =
|
||||
instrValue.loweredFunc.func.aliasingEffects
|
||||
?.map(printAliasingEffect)
|
||||
?.join(', ') ?? '';
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
|
||||
@@ -111,6 +111,19 @@ export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type ImmutableCaptureEffectConfig = {
|
||||
kind: 'ImmutableCapture';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const ImmutableCaptureEffectSchema: z.ZodType<ImmutableCaptureEffectConfig> =
|
||||
z.object({
|
||||
kind: z.literal('ImmutableCapture'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CaptureEffectConfig = {
|
||||
kind: 'Capture';
|
||||
from: string;
|
||||
@@ -187,6 +200,7 @@ export type AliasingEffectConfig =
|
||||
| AssignEffectConfig
|
||||
| AliasEffectConfig
|
||||
| CaptureEffectConfig
|
||||
| ImmutableCaptureEffectConfig
|
||||
| ImpureEffectConfig
|
||||
| MutateEffectConfig
|
||||
| MutateTransitiveConditionallyConfig
|
||||
@@ -199,6 +213,7 @@ export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
|
||||
AssignEffectSchema,
|
||||
AliasEffectSchema,
|
||||
CaptureEffectSchema,
|
||||
ImmutableCaptureEffectSchema,
|
||||
ImpureEffectSchema,
|
||||
MutateEffectSchema,
|
||||
MutateTransitiveConditionallySchema,
|
||||
|
||||
@@ -50,7 +50,7 @@ export type AliasingEffect =
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
|
||||
*/
|
||||
| {kind: 'Mutate'; value: Place}
|
||||
| {kind: 'Mutate'; value: Place; reason?: MutationReason | null}
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
|
||||
* This should be rare.
|
||||
@@ -174,6 +174,8 @@ export type AliasingEffect =
|
||||
place: Place;
|
||||
};
|
||||
|
||||
export type MutationReason = {kind: 'AssignCurrentProperty'};
|
||||
|
||||
export function hashEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Apply': {
|
||||
|
||||
@@ -6,20 +6,10 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
@@ -30,12 +20,7 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
if (!func.env.config.enableNewMutationAliasingModel) {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
} else {
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
}
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
@@ -140,58 +125,3 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
value: fn,
|
||||
});
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
analyseFunctions(func);
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
deadCodeElimination(func);
|
||||
inferMutableRanges(func);
|
||||
rewriteInstructionKindsBasedOnReassignment(func);
|
||||
inferReactiveScopeVariables(func);
|
||||
func.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: func,
|
||||
});
|
||||
}
|
||||
|
||||
function infer(loweredFunc: LoweredFunction): void {
|
||||
for (const operand of loweredFunc.func.context) {
|
||||
const identifier = operand.identifier;
|
||||
CompilerError.invariant(operand.effect === Effect.Unknown, {
|
||||
reason:
|
||||
'[AnalyseFunctions] Expected Function context effects to not have been set',
|
||||
loc: operand.loc,
|
||||
});
|
||||
if (isRefOrRefValue(identifier)) {
|
||||
/*
|
||||
* TODO: this is a hack to ensure we treat functions which reference refs
|
||||
* as having a capture and therefore being considered mutable. this ensures
|
||||
* the function gets a mutable range which accounts for anywhere that it
|
||||
* could be called, and allows us to help ensure it isn't called during
|
||||
* render
|
||||
*/
|
||||
operand.effect = Effect.Capture;
|
||||
} else if (isMutatedOrReassigned(identifier)) {
|
||||
/**
|
||||
* Reflects direct reassignments, PropertyStores, and ConditionallyMutate
|
||||
* (directly or through maybe-aliases)
|
||||
*/
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMutatedOrReassigned(id: Identifier): boolean {
|
||||
/*
|
||||
* This check checks for mutation and reassingnment, so the usual check for
|
||||
* mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
|
||||
* enough.
|
||||
*
|
||||
* We need to track re-assignments in context refs as we need to reflect the
|
||||
* re-assignment back to the captured refs.
|
||||
*/
|
||||
return id.mutableRange.end > id.mutableRange.start;
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
Instruction,
|
||||
isPrimitiveType,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export type AliasSet = Set<Identifier>;
|
||||
|
||||
export function inferAliases(func: HIRFunction): DisjointSet<Identifier> {
|
||||
const aliases = new DisjointSet<Identifier>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
inferInstr(instr, aliases);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function inferInstr(
|
||||
instr: Instruction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const {lvalue, value: instrValue} = instr;
|
||||
let alias: Place | null = null;
|
||||
switch (instrValue.kind) {
|
||||
case 'LoadLocal':
|
||||
case 'LoadContext': {
|
||||
if (isPrimitiveType(instrValue.place.identifier)) {
|
||||
return;
|
||||
}
|
||||
alias = instrValue.place;
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal':
|
||||
case 'StoreContext': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad': {
|
||||
alias = instrValue.object;
|
||||
break;
|
||||
}
|
||||
case 'TypeCastExpression': {
|
||||
alias = instrValue.value;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
aliases.union([lvalue.identifier, alias.identifier]);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForPhis(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (isPhiMutatedAfterCreation) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
aliases.union([phi.place.identifier, operand.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferAliasForStores(
|
||||
func: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
const isStore =
|
||||
lvalue.effect === Effect.Store ||
|
||||
/*
|
||||
* Some typed functions annotate callees or arguments
|
||||
* as Effect.Store.
|
||||
*/
|
||||
![...eachInstructionValueOperand(value)].every(
|
||||
operand => operand.effect !== Effect.Store,
|
||||
);
|
||||
|
||||
if (!isStore) {
|
||||
continue;
|
||||
}
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
maybeAlias(aliases, lvalue, operand, instr.id);
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (
|
||||
operand.effect === Effect.Capture ||
|
||||
operand.effect === Effect.Store
|
||||
) {
|
||||
maybeAlias(aliases, lvalue, operand, instr.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeAlias(
|
||||
aliases: DisjointSet<Identifier>,
|
||||
lvalue: Place,
|
||||
rvalue: Place,
|
||||
id: InstructionId,
|
||||
): void {
|
||||
if (
|
||||
lvalue.identifier.mutableRange.end > id + 1 ||
|
||||
rvalue.identifier.mutableRange.end > id
|
||||
) {
|
||||
aliases.union([lvalue.identifier, rvalue.identifier]);
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetailOptions,
|
||||
ErrorSeverity,
|
||||
ValueKind,
|
||||
} from '..';
|
||||
import {
|
||||
AbstractValue,
|
||||
BasicBlock,
|
||||
Effect,
|
||||
Environment,
|
||||
FunctionEffect,
|
||||
Instruction,
|
||||
InstructionValue,
|
||||
Place,
|
||||
ValueReason,
|
||||
getHookKind,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
interface State {
|
||||
kind(place: Place): AbstractValue;
|
||||
values(place: Place): Array<InstructionValue>;
|
||||
isDefined(place: Place): boolean;
|
||||
}
|
||||
|
||||
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
|
||||
const value = state.kind(place);
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Expected operand to have a kind',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
switch (place.effect) {
|
||||
case Effect.Store:
|
||||
case Effect.Mutate: {
|
||||
if (isRefOrRefValue(place.identifier)) {
|
||||
break;
|
||||
} else if (value.kind === ValueKind.Context) {
|
||||
CompilerError.invariant(value.context.size > 0, {
|
||||
reason:
|
||||
"[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
|
||||
loc: place.loc,
|
||||
});
|
||||
return {
|
||||
kind: 'ContextMutation',
|
||||
loc: place.loc,
|
||||
effect: place.effect,
|
||||
places: value.context,
|
||||
};
|
||||
} else if (
|
||||
value.kind !== ValueKind.Mutable &&
|
||||
// We ignore mutations of primitives since this is not a React-specific problem
|
||||
value.kind !== ValueKind.Primitive
|
||||
) {
|
||||
let reason = getWriteErrorReason(value);
|
||||
return {
|
||||
kind:
|
||||
value.reason.size === 1 && value.reason.has(ValueReason.Global)
|
||||
? 'GlobalMutation'
|
||||
: 'ReactMutation',
|
||||
error: {
|
||||
reason,
|
||||
description:
|
||||
place.identifier.name !== null &&
|
||||
place.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${place.identifier.name.value}\``
|
||||
: null,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inheritFunctionEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects = inferFunctionInstrEffects(state, place);
|
||||
|
||||
return effects
|
||||
.flatMap(effect => {
|
||||
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
|
||||
return [effect];
|
||||
} else {
|
||||
const effects: Array<FunctionEffect | null> = [];
|
||||
CompilerError.invariant(effect.kind === 'ContextMutation', {
|
||||
reason: 'Expected ContextMutation',
|
||||
loc: null,
|
||||
});
|
||||
/**
|
||||
* Contextual effects need to be replayed against the current inference
|
||||
* state, which may know more about the value to which the effect applied.
|
||||
* The main cases are:
|
||||
* 1. The mutated context value is _still_ a context value in the current scope,
|
||||
* so we have to continue propagating the original context mutation.
|
||||
* 2. The mutated context value is a mutable value in the current scope,
|
||||
* so the context mutation was fine and we can skip propagating the effect.
|
||||
* 3. The mutated context value is an immutable value in the current scope,
|
||||
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
|
||||
* more detailed effect to the current function context.
|
||||
*/
|
||||
for (const place of effect.places) {
|
||||
if (state.isDefined(place)) {
|
||||
const replayedEffect = inferOperandEffect(state, {
|
||||
...place,
|
||||
loc: effect.loc,
|
||||
effect: effect.effect,
|
||||
});
|
||||
if (replayedEffect != null) {
|
||||
if (replayedEffect.kind === 'ContextMutation') {
|
||||
// Case 1, still a context value so propagate the original effect
|
||||
effects.push(effect);
|
||||
} else {
|
||||
// Case 3, immutable value so propagate the more precise effect
|
||||
effects.push(replayedEffect);
|
||||
}
|
||||
} // else case 2, local mutable value so this effect was fine
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
})
|
||||
.filter((effect): effect is FunctionEffect => effect != null);
|
||||
}
|
||||
|
||||
function inferFunctionInstrEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects: Array<FunctionEffect> = [];
|
||||
const instrs = state.values(place);
|
||||
CompilerError.invariant(instrs != null, {
|
||||
reason: 'Expected operand to have instructions',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
for (const instr of instrs) {
|
||||
if (
|
||||
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
|
||||
instr.loweredFunc.func.effects != null
|
||||
) {
|
||||
effects.push(...instr.loweredFunc.func.effects);
|
||||
}
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
function operandEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
filterRenderSafe: boolean,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
const effect = inferOperandEffect(state, place);
|
||||
effect && functionEffects.push(effect);
|
||||
functionEffects.push(...inheritFunctionEffects(state, place));
|
||||
if (filterRenderSafe) {
|
||||
return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
|
||||
} else {
|
||||
return functionEffects;
|
||||
}
|
||||
}
|
||||
|
||||
export function inferInstructionFunctionEffects(
|
||||
env: Environment,
|
||||
state: State,
|
||||
instr: Instruction,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression': {
|
||||
if (instr.value.tag.kind === 'Identifier') {
|
||||
functionEffects.push(...operandEffects(state, instr.value.tag, false));
|
||||
}
|
||||
instr.value.children?.forEach(child =>
|
||||
functionEffects.push(...operandEffects(state, child, false)),
|
||||
);
|
||||
for (const attr of instr.value.props) {
|
||||
if (attr.kind === 'JsxSpreadAttribute') {
|
||||
functionEffects.push(...operandEffects(state, attr.argument, false));
|
||||
} else {
|
||||
functionEffects.push(...operandEffects(state, attr.place, true));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
/**
|
||||
* If this function references other functions, propagate the referenced function's
|
||||
* effects to this function.
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* g();
|
||||
* ```
|
||||
*
|
||||
* In this example, because `g` references `f`, we propagate the GlobalMutation from
|
||||
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
|
||||
* function effect context and report an error. But if instead we do:
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* useEffect(() => g(), [g])
|
||||
* ```
|
||||
*
|
||||
* Now `g`'s effects will be discarded since they're in a useEffect.
|
||||
*/
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
instr.value.loweredFunc.func.effects ??= [];
|
||||
instr.value.loweredFunc.func.effects.push(
|
||||
...inferFunctionInstrEffects(state, operand),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall':
|
||||
case 'CallExpression': {
|
||||
let callee;
|
||||
if (instr.value.kind === 'MethodCall') {
|
||||
callee = instr.value.property;
|
||||
functionEffects.push(
|
||||
...operandEffects(state, instr.value.receiver, false),
|
||||
);
|
||||
} else {
|
||||
callee = instr.value.callee;
|
||||
}
|
||||
functionEffects.push(...operandEffects(state, callee, false));
|
||||
let isHook = getHookKind(env, callee.identifier) != null;
|
||||
for (const arg of instr.value.args) {
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
/*
|
||||
* Join the effects of the argument with the effects of the enclosing function,
|
||||
* unless the we're detecting a global mutation inside a useEffect hook
|
||||
*/
|
||||
functionEffects.push(...operandEffects(state, place, isHook));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize':
|
||||
case 'LoadLocal':
|
||||
case 'StoreLocal': {
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
functionEffects.push({
|
||||
kind: 'GlobalMutation',
|
||||
error: {
|
||||
reason:
|
||||
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
functionEffects.push(...operandEffects(state, operand, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function inferTerminalFunctionEffects(
|
||||
state: State,
|
||||
block: BasicBlock,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
functionEffects.push(...operandEffects(state, operand, true));
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function transformFunctionEffectErrors(
|
||||
functionEffects: Array<FunctionEffect>,
|
||||
): Array<CompilerErrorDetailOptions> {
|
||||
return functionEffects.map(eff => {
|
||||
switch (eff.kind) {
|
||||
case 'ReactMutation':
|
||||
case 'GlobalMutation': {
|
||||
return eff.error;
|
||||
}
|
||||
case 'ContextMutation': {
|
||||
return {
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected ContextMutation in top-level function effects`,
|
||||
loc: eff.loc,
|
||||
};
|
||||
}
|
||||
default:
|
||||
assertExhaustive(
|
||||
eff,
|
||||
`Unexpected function effect kind \`${(eff as any).kind}\``,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
return effect.kind === 'GlobalMutation';
|
||||
}
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isRefOrRefValue,
|
||||
isSetType,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/*
|
||||
* For each usage of a value in the given function, determines if the usage
|
||||
* may be succeeded by a mutable usage of that same value and if so updates
|
||||
* the usage to be mutable.
|
||||
*
|
||||
* Stated differently, this inference ensures that inferred capabilities of
|
||||
* each reference are as follows:
|
||||
* - freeze: the value is frozen at this point
|
||||
* - readonly: the value is not modified at this point *or any subsequent
|
||||
* point*
|
||||
* - mutable: the value is modified at this point *or some subsequent point*.
|
||||
*
|
||||
* Note that this refines the capabilities inferered by InferReferenceCapability,
|
||||
* which looks at individual references and not the lifetime of a value's mutability.
|
||||
*
|
||||
* == Algorithm
|
||||
*
|
||||
* TODO:
|
||||
* 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
|
||||
* which only tracks aliasing of top-level variables (`y = x`), this analysis needs
|
||||
* to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
|
||||
* all possible locations which may have aliased a value. The concrete result is
|
||||
* a mapping of each Place to the set of possibly-mutable values it may alias.
|
||||
*
|
||||
* ```
|
||||
* const x = []; // {x: v0; v0: mutable []}
|
||||
* const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
|
||||
* y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
|
||||
* mutate(y); // can infer that y mutates v0 and v1
|
||||
* ```
|
||||
*
|
||||
* DONE:
|
||||
* 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
|
||||
* the CFG and track which values are mutated in a successor.
|
||||
*
|
||||
* ```
|
||||
* mutate(y); // mutable y => v0, v1 mutated
|
||||
* read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
|
||||
* ...
|
||||
* ```
|
||||
*/
|
||||
|
||||
function infer(place: Place, instrId: InstructionId): void {
|
||||
if (!isRefOrRefValue(place.identifier)) {
|
||||
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function inferPlace(
|
||||
place: Place,
|
||||
instrId: InstructionId,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
switch (place.effect) {
|
||||
case Effect.Unknown: {
|
||||
throw new Error(`Found an unknown place ${printPlace(place)}}!`);
|
||||
}
|
||||
case Effect.Capture:
|
||||
case Effect.Read:
|
||||
case Effect.Freeze:
|
||||
return;
|
||||
case Effect.Store:
|
||||
if (inferMutableRangeForStores) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
case Effect.ConditionallyMutateIterator: {
|
||||
const identifier = place.identifier;
|
||||
if (
|
||||
!isArrayType(identifier) &&
|
||||
!isSetType(identifier) &&
|
||||
!isMapType(identifier)
|
||||
) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.Mutate: {
|
||||
infer(place, instrId);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
|
||||
}
|
||||
}
|
||||
|
||||
export function inferMutableLifetimes(
|
||||
func: HIRFunction,
|
||||
inferMutableRangeForStores: boolean,
|
||||
): void {
|
||||
/*
|
||||
* Context variables only appear to mutate where they are assigned, but we need
|
||||
* to force their range to start at their declaration. Track the declaring instruction
|
||||
* id so that the ranges can be extended if/when they are reassigned
|
||||
*/
|
||||
const contextVariableDeclarationInstructions = new Map<
|
||||
Identifier,
|
||||
InstructionId
|
||||
>();
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
if (
|
||||
inferMutableRangeForStores &&
|
||||
isPhiMutatedAfterCreation &&
|
||||
phi.place.identifier.mutableRange.start === 0
|
||||
) {
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (phi.place.identifier.mutableRange.start === 0) {
|
||||
phi.place.identifier.mutableRange.start =
|
||||
operand.identifier.mutableRange.start;
|
||||
} else {
|
||||
phi.place.identifier.mutableRange.start = makeInstructionId(
|
||||
Math.min(
|
||||
phi.place.identifier.mutableRange.start,
|
||||
operand.identifier.mutableRange.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
const lvalueId = operand.identifier;
|
||||
|
||||
/*
|
||||
* lvalue start being mutable when they're initially assigned a
|
||||
* value.
|
||||
*/
|
||||
lvalueId.mutableRange.start = instr.id;
|
||||
|
||||
/*
|
||||
* Let's be optimistic and assume this lvalue is not mutable by
|
||||
* default.
|
||||
*/
|
||||
lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
inferPlace(operand, instr.id, inferMutableRangeForStores);
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'DeclareContext' ||
|
||||
(instr.value.kind === 'StoreContext' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign &&
|
||||
!contextVariableDeclarationInstructions.has(
|
||||
instr.value.lvalue.place.identifier,
|
||||
))
|
||||
) {
|
||||
/**
|
||||
* 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,
|
||||
);
|
||||
} else if (instr.value.kind === 'StoreContext') {
|
||||
/*
|
||||
* Else this is a reassignment, extend the range from the declaration (if present).
|
||||
* Note that declarations may not be present for context variables that are reassigned
|
||||
* within a function expression before (or without) a read of the same variable
|
||||
*/
|
||||
const declaration = contextVariableDeclarationInstructions.get(
|
||||
instr.value.lvalue.place.identifier,
|
||||
);
|
||||
if (
|
||||
declaration != null &&
|
||||
!isRefOrRefValue(instr.value.lvalue.place.identifier)
|
||||
) {
|
||||
const range = instr.value.lvalue.place.identifier.mutableRange;
|
||||
if (range.start === 0) {
|
||||
range.start = declaration;
|
||||
} else {
|
||||
range.start = makeInstructionId(Math.min(range.start, declaration));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
||||
import {inferAliases} from './InferAlias';
|
||||
import {inferAliasForPhis} from './InferAliasForPhis';
|
||||
import {inferAliasForStores} from './InferAliasForStores';
|
||||
import {inferMutableLifetimes} from './InferMutableLifetimes';
|
||||
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
|
||||
import {inferTryCatchAliases} from './InferTryCatchAliases';
|
||||
|
||||
export function inferMutableRanges(ir: HIRFunction): void {
|
||||
// Infer mutable ranges for non fields
|
||||
inferMutableLifetimes(ir, false);
|
||||
|
||||
// Calculate aliases
|
||||
const aliases = inferAliases(ir);
|
||||
/*
|
||||
* Calculate aliases for try/catch, where any value created
|
||||
* in the try block could be aliased to the catch param
|
||||
*/
|
||||
inferTryCatchAliases(ir, aliases);
|
||||
|
||||
/*
|
||||
* Eagerly canonicalize so that if nothing changes we can bail out
|
||||
* after a single iteration
|
||||
*/
|
||||
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
|
||||
while (true) {
|
||||
// Infer mutable ranges for aliases that are not fields
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
|
||||
// Update aliasing information of fields
|
||||
inferAliasForStores(ir, aliases);
|
||||
|
||||
// Update aliasing information of phis
|
||||
inferAliasForPhis(ir, aliases);
|
||||
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
|
||||
// Re-infer mutable ranges for all values
|
||||
inferMutableLifetimes(ir, true);
|
||||
|
||||
/**
|
||||
* The second inferMutableLifetimes() call updates mutable ranges
|
||||
* of values to account for Store effects. Now we need to update
|
||||
* all aliases of such values to extend their ranges as well. Note
|
||||
* that the store only mutates the the directly aliased value and
|
||||
* not any of its inner captured references. For example:
|
||||
*
|
||||
* ```
|
||||
* let y;
|
||||
* if (cond) {
|
||||
* y = [];
|
||||
* } else {
|
||||
* y = [{}];
|
||||
* }
|
||||
* y.push(z);
|
||||
* ```
|
||||
*
|
||||
* The Store effect from the `y.push` modifies the values that `y`
|
||||
* directly aliases - the two arrays from the if/else branches -
|
||||
* but does not modify values that `y` "contains" such as the
|
||||
* object literal or `z`.
|
||||
*/
|
||||
prevAliases = aliases.canonicalize();
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
inferAliasForUncalledFunctions(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
}
|
||||
|
||||
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const [key, value] of a) {
|
||||
if (!b.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (b.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferMutableRangesForAlias(
|
||||
_fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const aliasSets = aliases.buildSets();
|
||||
for (const aliasSet of aliasSets) {
|
||||
/*
|
||||
* Update mutableRange.end only if the identifiers have actually been
|
||||
* mutated.
|
||||
*/
|
||||
const mutatingIdentifiers = [...aliasSet].filter(
|
||||
id =>
|
||||
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
|
||||
);
|
||||
|
||||
if (mutatingIdentifiers.length > 0) {
|
||||
// Find final instruction which mutates this alias set.
|
||||
let lastMutatingInstructionId = 0;
|
||||
for (const id of mutatingIdentifiers) {
|
||||
if (id.mutableRange.end > lastMutatingInstructionId) {
|
||||
lastMutatingInstructionId = id.mutableRange.end;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Update mutableRange.end for all aliases in this set ending before the
|
||||
* last mutation.
|
||||
*/
|
||||
for (const alias of aliasSet) {
|
||||
if (
|
||||
alias.mutableRange.end < lastMutatingInstructionId &&
|
||||
!isRefOrRefValue(alias)
|
||||
) {
|
||||
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
DeclarationId,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
Phi,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
Type,
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {
|
||||
@@ -43,12 +45,6 @@ import {
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
getArgumentEffect,
|
||||
getFunctionCallSignature,
|
||||
isKnownMutableEffect,
|
||||
mergeValueKinds,
|
||||
} from './InferReferenceEffects';
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertDefault,
|
||||
@@ -65,10 +61,14 @@ import {
|
||||
printSourceLocation,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {getWriteErrorReason} from './InferFunctionEffects';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
|
||||
import {
|
||||
AliasingEffect,
|
||||
AliasingSignature,
|
||||
hashEffect,
|
||||
MutationReason,
|
||||
} from './AliasingEffects';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -445,25 +445,34 @@ function applySignature(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
effects.push({
|
||||
kind: 'MutateFrozen',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
error: diagnostic,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1059,13 +1068,30 @@ function applyEffect(
|
||||
const reason = getWriteErrorReason({
|
||||
kind: value.kind,
|
||||
reason: value.reason,
|
||||
context: new Set(),
|
||||
});
|
||||
const variable =
|
||||
effect.value.identifier.name !== null &&
|
||||
effect.value.identifier.name.kind === 'named'
|
||||
? `\`${effect.value.identifier.name.value}\``
|
||||
: 'value';
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
});
|
||||
if (
|
||||
effect.kind === 'Mutate' &&
|
||||
effect.reason?.kind === 'AssignCurrentProperty'
|
||||
) {
|
||||
diagnostic.withDetail({
|
||||
kind: 'hint',
|
||||
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
|
||||
});
|
||||
}
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
@@ -1075,15 +1101,7 @@ function applyEffect(
|
||||
? 'MutateFrozen'
|
||||
: 'MutateGlobal',
|
||||
place: effect.value,
|
||||
error: CompilerDiagnostic.create({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
category: 'This value cannot be modified',
|
||||
description: `${reason}.`,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.value.loc,
|
||||
message: `${variable} cannot be modified`,
|
||||
}),
|
||||
error: diagnostic,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
@@ -1680,7 +1698,15 @@ function computeSignatureForInstruction(
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'ComputedStore': {
|
||||
effects.push({kind: 'Mutate', value: value.object});
|
||||
const mutationReason: MutationReason | null =
|
||||
value.kind === 'PropertyStore' && value.property === 'current'
|
||||
? {kind: 'AssignCurrentProperty'}
|
||||
: null;
|
||||
effects.push({
|
||||
kind: 'Mutate',
|
||||
value: value.object,
|
||||
reason: mutationReason,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'Capture',
|
||||
from: value.value,
|
||||
@@ -2534,3 +2560,196 @@ export type AbstractValue = {
|
||||
kind: ValueKind;
|
||||
reason: ReadonlySet<ValueReason>;
|
||||
};
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Modifying a value returned from 'useContext()' is not allowed.`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Modifying a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This modifies a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
|
||||
function getArgumentEffect(
|
||||
signatureEffect: Effect | null,
|
||||
arg: Place | SpreadPattern,
|
||||
): Effect {
|
||||
if (signatureEffect != null) {
|
||||
if (arg.kind === 'Identifier') {
|
||||
return signatureEffect;
|
||||
} else if (
|
||||
signatureEffect === Effect.Mutate ||
|
||||
signatureEffect === Effect.ConditionallyMutate
|
||||
) {
|
||||
return signatureEffect;
|
||||
} else {
|
||||
// see call-spread-argument-mutable-iterator test fixture
|
||||
if (signatureEffect === Effect.Freeze) {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support spread syntax for hook arguments',
|
||||
loc: arg.place.loc,
|
||||
});
|
||||
}
|
||||
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
|
||||
return Effect.ConditionallyMutateIterator;
|
||||
}
|
||||
} else {
|
||||
return Effect.ConditionallyMutate;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFunctionCallSignature(
|
||||
env: Environment,
|
||||
type: Type,
|
||||
): FunctionSignature | null {
|
||||
if (type.kind !== 'Function') {
|
||||
return null;
|
||||
}
|
||||
return env.getFunctionSignature(type);
|
||||
}
|
||||
|
||||
export function isKnownMutableEffect(effect: Effect): boolean {
|
||||
switch (effect) {
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
case Effect.Read:
|
||||
case Effect.Capture:
|
||||
case Effect.Freeze: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins two values using the following rules:
|
||||
* == Effect Transitions ==
|
||||
*
|
||||
* Freezing an immutable value has not effect:
|
||||
* ┌───────────────┐
|
||||
* │ │
|
||||
* ▼ │ Freeze
|
||||
* ┌──────────────────────────┐ │
|
||||
* │ Immutable │──┘
|
||||
* └──────────────────────────┘
|
||||
*
|
||||
* Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
|
||||
* value has no effect:
|
||||
* ┌───────────────┐
|
||||
* ┌─────────────────────────┐ Freeze │ │
|
||||
* │ MaybeFrozen │────┐ ▼ │ Freeze
|
||||
* └─────────────────────────┘ │ ┌──────────────────────────┐ │
|
||||
* ├────▶│ Frozen │──┘
|
||||
* │ └──────────────────────────┘
|
||||
* ┌─────────────────────────┐ │
|
||||
* │ Mutable │────┘
|
||||
* └─────────────────────────┘
|
||||
*
|
||||
* == Join Lattice ==
|
||||
* - immutable | mutable => mutable
|
||||
* The justification is that immutable and mutable values are different types,
|
||||
* and functions can introspect them to tell the difference (if the argument
|
||||
* is null return early, else if its an object mutate it).
|
||||
* - frozen | mutable => maybe-frozen
|
||||
* Frozen values are indistinguishable from mutable values at runtime, so callers
|
||||
* cannot dynamically avoid mutation of "frozen" values. If a value could be
|
||||
* frozen we have to distinguish it from a mutable value. But it also isn't known
|
||||
* frozen yet, so we distinguish as maybe-frozen.
|
||||
* - immutable | frozen => frozen
|
||||
* This is subtle and falls out of the above rules. If a value could be any of
|
||||
* immutable, mutable, or frozen, then at runtime it could either be a primitive
|
||||
* or a reference type, and callers can't distinguish frozen or not for reference
|
||||
* types. To ensure that any sequence of joins btw those three states yields the
|
||||
* correct maybe-frozen, these two have to produce a frozen value.
|
||||
* - <any> | maybe-frozen => maybe-frozen
|
||||
* - immutable | context => context
|
||||
* - mutable | context => context
|
||||
* - frozen | context => maybe-frozen
|
||||
*
|
||||
* ┌──────────────────────────┐
|
||||
* │ Immutable │───┐
|
||||
* └──────────────────────────┘ │
|
||||
* │ ┌─────────────────────────┐
|
||||
* ├───▶│ Frozen │──┐
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘ │
|
||||
* │ Frozen │───┤ │ ┌─────────────────────────┐
|
||||
* └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
|
||||
* │ ┌─────────────────────────┐ │ └─────────────────────────┘
|
||||
* ├───▶│ MaybeFrozen │──┘
|
||||
* ┌──────────────────────────┐ │ └─────────────────────────┘
|
||||
* │ Mutable │───┘
|
||||
* └──────────────────────────┘
|
||||
*/
|
||||
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
if (a === b) {
|
||||
return a;
|
||||
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
|
||||
return ValueKind.MaybeFrozen;
|
||||
// after this a and b differ and neither are MaybeFrozen
|
||||
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | mutable
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
// context | mutable
|
||||
return ValueKind.Context;
|
||||
} else {
|
||||
// mutable | immutable
|
||||
return ValueKind.Mutable;
|
||||
}
|
||||
} else if (a === ValueKind.Context || b === ValueKind.Context) {
|
||||
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
// frozen | context
|
||||
return ValueKind.MaybeFrozen;
|
||||
} else {
|
||||
// context | immutable
|
||||
return ValueKind.Context;
|
||||
}
|
||||
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
|
||||
return ValueKind.Frozen;
|
||||
} else if (a === ValueKind.Global || b === ValueKind.Global) {
|
||||
return ValueKind.Global;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
a === ValueKind.Primitive && b == ValueKind.Primitive,
|
||||
{
|
||||
reason: `Unexpected value kind in mergeValues()`,
|
||||
description: `Found kinds ${a} and ${b}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
return ValueKind.Primitive;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {BlockId, HIRFunction, Identifier} from '../HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/*
|
||||
* Any values created within a try/catch block could be aliased to the try handler.
|
||||
* Our lowering ensures that every instruction within a try block will be lowered into a
|
||||
* basic block ending in a maybe-throw terminal that points to its catch block, so we can
|
||||
* iterate such blocks and alias their instruction lvalues to the handler's param (if present).
|
||||
*/
|
||||
export function inferTryCatchAliases(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
const handlerParams: Map<BlockId, Identifier> = new Map();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
if (
|
||||
block.terminal.kind === 'try' &&
|
||||
block.terminal.handlerBinding !== null
|
||||
) {
|
||||
handlerParams.set(
|
||||
block.terminal.handler,
|
||||
block.terminal.handlerBinding.identifier,
|
||||
);
|
||||
} else if (block.terminal.kind === 'maybe-throw') {
|
||||
const handlerParam = handlerParams.get(block.terminal.handler);
|
||||
if (handlerParam === undefined) {
|
||||
/*
|
||||
* There's no catch clause param, nothing to alias to so
|
||||
* skip this block
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Otherwise alias all values created in this block to the
|
||||
* catch clause param
|
||||
*/
|
||||
for (const instr of block.instructions) {
|
||||
aliases.union([handlerParam, instr.lvalue.identifier]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
export {default as analyseFunctions} from './AnalyseFunctions';
|
||||
export {dropManualMemoization} from './DropManualMemoization';
|
||||
export {inferMutableRanges} from './InferMutableRanges';
|
||||
export {inferReactivePlaces} from './InferReactivePlaces';
|
||||
export {default as inferReferenceEffects} from './InferReferenceEffects';
|
||||
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
|
||||
export {inferEffectDependencies} from './InferEffectDependencies';
|
||||
|
||||
@@ -255,7 +255,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
@@ -263,6 +262,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
|
||||
reversePostorderBlocks(fn.body);
|
||||
|
||||
@@ -370,7 +370,6 @@ function emitOutlinedFn(
|
||||
returnTypeAnnotation: null,
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
@@ -378,6 +377,7 @@ function emitOutlinedFn(
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
aliasingEffects: [],
|
||||
};
|
||||
return fn;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
|
||||
lastUsage: Map<DeclarationId, InstructionId>;
|
||||
temporaries: Map<DeclarationId, DeclarationId> = new Map();
|
||||
|
||||
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
|
||||
super();
|
||||
@@ -215,6 +216,12 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
current.lvalues.add(
|
||||
instr.instruction.lvalue.identifier.declarationId,
|
||||
);
|
||||
if (instr.instruction.value.kind === 'LoadLocal') {
|
||||
this.temporaries.set(
|
||||
instr.instruction.lvalue.identifier.declarationId,
|
||||
instr.instruction.value.place.identifier.declarationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -236,6 +243,13 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
)) {
|
||||
current.lvalues.add(lvalue.identifier.declarationId);
|
||||
}
|
||||
this.temporaries.set(
|
||||
instr.instruction.value.lvalue.place.identifier
|
||||
.declarationId,
|
||||
this.temporaries.get(
|
||||
instr.instruction.value.value.identifier.declarationId,
|
||||
) ?? instr.instruction.value.value.identifier.declarationId,
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
`Reset scope @${current.block.scope.id} from StoreLocal in [${instr.instruction.id}]`,
|
||||
@@ -260,7 +274,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
case 'scope': {
|
||||
if (
|
||||
current !== null &&
|
||||
canMergeScopes(current.block, instr) &&
|
||||
canMergeScopes(current.block, instr, this.temporaries) &&
|
||||
areLValuesLastUsedByScope(
|
||||
instr.scope,
|
||||
current.lvalues,
|
||||
@@ -426,6 +440,7 @@ function areLValuesLastUsedByScope(
|
||||
function canMergeScopes(
|
||||
current: ReactiveScopeBlock,
|
||||
next: ReactiveScopeBlock,
|
||||
temporaries: Map<DeclarationId, DeclarationId>,
|
||||
): boolean {
|
||||
// Don't merge scopes with reassignments
|
||||
if (
|
||||
@@ -465,11 +480,14 @@ function canMergeScopes(
|
||||
(next.scope.dependencies.size !== 0 &&
|
||||
[...next.scope.dependencies].every(
|
||||
dep =>
|
||||
dep.path.length === 0 &&
|
||||
isAlwaysInvalidatingType(dep.identifier.type) &&
|
||||
Iterable_some(
|
||||
current.scope.declarations.values(),
|
||||
decl =>
|
||||
decl.identifier.declarationId === dep.identifier.declarationId,
|
||||
decl.identifier.declarationId === dep.identifier.declarationId ||
|
||||
decl.identifier.declarationId ===
|
||||
temporaries.get(dep.identifier.declarationId),
|
||||
),
|
||||
))
|
||||
) {
|
||||
@@ -477,12 +495,16 @@ function canMergeScopes(
|
||||
return true;
|
||||
}
|
||||
log(` cannot merge scopes:`);
|
||||
log(` ${printReactiveScopeSummary(current.scope)}`);
|
||||
log(` ${printReactiveScopeSummary(next.scope)}`);
|
||||
log(
|
||||
` ${printReactiveScopeSummary(current.scope)} ${[...current.scope.declarations.values()].map(decl => decl.identifier.declarationId)}`,
|
||||
);
|
||||
log(
|
||||
` ${printReactiveScopeSummary(next.scope)} ${[...next.scope.dependencies].map(dep => `${dep.identifier.declarationId} ${temporaries.get(dep.identifier.declarationId) ?? dep.identifier.declarationId}`)}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
function isAlwaysInvalidatingType(type: Type): boolean {
|
||||
export function isAlwaysInvalidatingType(type: Type): boolean {
|
||||
switch (type.kind) {
|
||||
case 'Object': {
|
||||
switch (type.shapeId) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
getHookKind,
|
||||
isMutableEffect,
|
||||
} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
|
||||
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
|
||||
import {
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
visitReactiveFunction,
|
||||
} from './visitors';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/*
|
||||
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR';
|
||||
|
||||
/**
|
||||
* Temporary workaround for InferTypes not propagating the types of phis.
|
||||
* Previously, LeaveSSA would replace all the identifiers for each phi (operands and
|
||||
* the phi itself) with a single "canonical" identifier, generally chosen as the first
|
||||
* operand to flow into the phi. In case of a phi whose operand was a phi, this could
|
||||
* sometimes be an operand from the earlier phi.
|
||||
*
|
||||
* As a result, even though InferTypes did not propagate types for phis, LeaveSSA
|
||||
* could end up replacing the phi Identifier with another identifer from an operand,
|
||||
* which _did_ have a type inferred.
|
||||
*
|
||||
* This didn't affect the initial construction of mutable ranges because InferMutableRanges
|
||||
* runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
|
||||
* notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
|
||||
* will always invalidate with its input.
|
||||
*
|
||||
* The long-term correct approach is to update InferTypes to infer the types of phis,
|
||||
* but this is complicated because InferMutableRanges inadvertently depends on phis
|
||||
* never having a known type, such that a Store effect cannot occur on a phi value.
|
||||
* Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
|
||||
* to handle this case.
|
||||
*
|
||||
* As a temporary workaround, this pass propagates the type of phis and can be called
|
||||
* safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
|
||||
* type if all operands have the same type, it's its more correct.
|
||||
*/
|
||||
export function propagatePhiTypes(fn: HIRFunction): void {
|
||||
/**
|
||||
* We track which SSA ids have had their types propagated to handle nested ternaries,
|
||||
* see the StoreLocal handling below
|
||||
*/
|
||||
const propagated = new Set<IdentifierId>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
/*
|
||||
* We replicate the previous LeaveSSA behavior and only propagate types for
|
||||
* unnamed variables. LeaveSSA would have chosen one of the operands as the
|
||||
* canonical id and taken its type as the type of all identifiers. We're
|
||||
* more conservative and only propagate if the types are the same and the
|
||||
* phi didn't have a type inferred.
|
||||
*
|
||||
* Note that this can change output slightly in cases such as
|
||||
* `cond ? <div /> : null`.
|
||||
*
|
||||
* Previously the first operand's type (BuiltInJsx) would have been propagated,
|
||||
* and this expression may have been merged with subsequent reactive scopes
|
||||
* since it appears (based on that type) to always invalidate.
|
||||
*
|
||||
* But the correct type is `BuiltInJsx | null`, which we can't express and
|
||||
* so leave as a generic `Type`, which does not always invalidate and therefore
|
||||
* does not merge with subsequent scopes.
|
||||
*
|
||||
* We also don't propagate scopes for named variables, to preserve compatibility
|
||||
* with previous LeaveSSA behavior.
|
||||
*/
|
||||
if (
|
||||
phi.place.identifier.type.kind !== 'Type' ||
|
||||
phi.place.identifier.name !== null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let type: Type | null = null;
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (type === null) {
|
||||
type = operand.identifier.type;
|
||||
} else if (!typeEquals(type, operand.identifier.type)) {
|
||||
type = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (type !== null) {
|
||||
phi.place.identifier.type = type;
|
||||
propagated.add(phi.place.identifier.id);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'StoreLocal': {
|
||||
/**
|
||||
* Nested ternaries can lower to a form with an intermediate StoreLocal where
|
||||
* the value.lvalue is the temporary of the outer ternary, and the value.value
|
||||
* is the result of the inner ternary.
|
||||
*
|
||||
* This is a common pattern in practice and easy enough to support. Again, the
|
||||
* long-term approach is to update InferTypes and InferMutableRanges.
|
||||
*/
|
||||
const lvalue = value.lvalue.place;
|
||||
if (
|
||||
propagated.has(value.value.identifier.id) &&
|
||||
lvalue.identifier.type.kind === 'Type' &&
|
||||
lvalue.identifier.name === null
|
||||
) {
|
||||
lvalue.identifier.type = value.value.identifier.type;
|
||||
propagated.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,10 @@ export default class DisjointSet<T> {
|
||||
return root;
|
||||
}
|
||||
|
||||
has(item: T): boolean {
|
||||
return this.#entries.has(item);
|
||||
}
|
||||
|
||||
/*
|
||||
* Forces the set into canonical form, ie with all items pointing directly to
|
||||
* their root, and returns a Map representing the mapping of items to their roots.
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
|
||||
*/
|
||||
|
||||
/**
|
||||
* Note: `await` and `yield` are contextually allowed as identifiers.
|
||||
* await: reserved inside async functions and modules
|
||||
* yield: reserved inside generator functions
|
||||
*
|
||||
* Note: `async` is not reserved.
|
||||
*/
|
||||
const RESERVED_WORDS = new Set([
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'debugger',
|
||||
'default',
|
||||
'delete',
|
||||
'do',
|
||||
'else',
|
||||
'enum',
|
||||
'export',
|
||||
'extends',
|
||||
'false',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'if',
|
||||
'import',
|
||||
'in',
|
||||
'instanceof',
|
||||
'new',
|
||||
'null',
|
||||
'return',
|
||||
'super',
|
||||
'switch',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typeof',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Reserved when a module has a 'use strict' directive.
|
||||
*/
|
||||
const STRICT_MODE_RESERVED_WORDS = new Set([
|
||||
'let',
|
||||
'static',
|
||||
'implements',
|
||||
'interface',
|
||||
'package',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
]);
|
||||
/**
|
||||
* The names arguments and eval are not keywords, but they are subject to some restrictions in
|
||||
* strict mode code.
|
||||
*/
|
||||
const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
|
||||
|
||||
/**
|
||||
* Conservative check for whether an identifer name is reserved or not. We assume that code is
|
||||
* written with strict mode.
|
||||
*/
|
||||
export function isReservedWord(identifierName: string): boolean {
|
||||
return (
|
||||
RESERVED_WORDS.has(identifierName) ||
|
||||
STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
|
||||
STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
|
||||
);
|
||||
}
|
||||
@@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never {
|
||||
// Modifies @param array in place, retaining only the items where the predicate returns true.
|
||||
export function retainWhere<T>(
|
||||
array: Array<T>,
|
||||
predicate: (item: T) => boolean,
|
||||
predicate: (item: T, index: number) => boolean,
|
||||
): void {
|
||||
let writeIndex = 0;
|
||||
for (let readIndex = 0; readIndex < array.length; readIndex++) {
|
||||
const item = array[readIndex];
|
||||
if (predicate(item) === true) {
|
||||
if (predicate(item, readIndex) === true) {
|
||||
array[writeIndex++] = item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/**
|
||||
* Validates that local variables cannot be reassigned after render.
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 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, ErrorSeverity, SourceLocation} from '..';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
candidateDependencies.set(lvalue.identifier.id, value);
|
||||
} else if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
const deps = candidateDependencies.get(value.args[1].identifier.id);
|
||||
if (
|
||||
effectFunction != null &&
|
||||
deps != null &&
|
||||
deps.elements.length !== 0 &&
|
||||
deps.elements.every(element => element.kind === 'Identifier')
|
||||
) {
|
||||
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
|
||||
CompilerError.invariant(dep.kind === 'Identifier', {
|
||||
reason: `Dependency is checked as a place above`,
|
||||
loc: value.loc,
|
||||
});
|
||||
return locals.get(dep.identifier.id) ?? dep.identifier.id;
|
||||
});
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
) {
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
case 'LoadGlobal': {
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const deps = values.get(instr.value.place.identifier.id);
|
||||
if (deps != null) {
|
||||
values.set(instr.lvalue.identifier.id, deps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad':
|
||||
case 'BinaryExpression':
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,8 @@
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {
|
||||
FunctionEffect,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
@@ -18,8 +16,8 @@ import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates that functions with known mutations (ie due to types) cannot be passed
|
||||
@@ -50,14 +48,14 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
const errors = new CompilerError();
|
||||
const contextMutationEffects: Map<
|
||||
IdentifierId,
|
||||
Extract<FunctionEffect, {kind: 'ContextMutation'}>
|
||||
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
|
||||
> = new Map();
|
||||
|
||||
function visitOperand(operand: Place): void {
|
||||
if (operand.effect === Effect.Freeze) {
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
const place = [...effect.places][0];
|
||||
const place = effect.value;
|
||||
const variable =
|
||||
place != null &&
|
||||
place.identifier.name != null &&
|
||||
@@ -77,7 +75,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
})
|
||||
.withDetail({
|
||||
kind: 'error',
|
||||
loc: effect.loc,
|
||||
loc: effect.value.loc,
|
||||
message: `This modifies ${variable}`,
|
||||
}),
|
||||
);
|
||||
@@ -108,27 +106,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
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);
|
||||
} else if (
|
||||
fn.env.config.enableNewMutationAliasingModel &&
|
||||
value.loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
if (value.loweredFunc.func.aliasingEffects != null) {
|
||||
const context = new Set(
|
||||
value.loweredFunc.func.context.map(p => p.identifier.id),
|
||||
);
|
||||
@@ -149,12 +127,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
context.has(effect.value.identifier.id) &&
|
||||
!isRefOrRefLikeMutableType(effect.value.identifier.type)
|
||||
) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, {
|
||||
kind: 'ContextMutation',
|
||||
effect: Effect.Mutate,
|
||||
loc: effect.value.loc,
|
||||
places: new Set([effect.value]),
|
||||
});
|
||||
contextMutationEffects.set(lvalue.identifier.id, effect);
|
||||
break effects;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
// spreading a function is weird, but it doesn't call the function so this is allowed
|
||||
return <div {...foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const foo = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div {...foo} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _temp() {
|
||||
someGlobal = true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -3,5 +3,6 @@ function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
// spreading a function is weird, but it doesn't call the function so this is allowed
|
||||
return <div {...foo} />;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function Component(props) {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(7);
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] !== props.x) {
|
||||
t0 = foo(props.x);
|
||||
@@ -31,26 +31,19 @@ function Component(props) {
|
||||
const x = t0;
|
||||
let t1;
|
||||
if ($[2] !== props || $[3] !== x) {
|
||||
t1 = function () {
|
||||
const fn = function () {
|
||||
const arr = [...bar(props)];
|
||||
return arr.at(x);
|
||||
};
|
||||
|
||||
t1 = fn();
|
||||
$[2] = props;
|
||||
$[3] = x;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const fn = t1;
|
||||
let t2;
|
||||
if ($[5] !== fn) {
|
||||
t2 = fn();
|
||||
$[5] = fn;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
const fnResult = t2;
|
||||
const fnResult = t1;
|
||||
return fnResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,34 +23,18 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.a) {
|
||||
t0 = { a: props.a };
|
||||
const item = { a: props.a };
|
||||
const items = [item];
|
||||
t0 = items.map(_temp);
|
||||
$[0] = props.a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const item = t0;
|
||||
let t1;
|
||||
if ($[2] !== item) {
|
||||
t1 = [item];
|
||||
$[2] = item;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const items = t1;
|
||||
let t2;
|
||||
if ($[4] !== items) {
|
||||
t2 = items.map(_temp);
|
||||
$[4] = items;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const mapped = t2;
|
||||
const mapped = t0;
|
||||
return mapped;
|
||||
}
|
||||
function _temp(item_0) {
|
||||
|
||||
@@ -21,26 +21,18 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
const f = _temp;
|
||||
let t0;
|
||||
if ($[0] !== props.items) {
|
||||
t0 = [...props.items].map(f);
|
||||
const x = [...props.items].map(f);
|
||||
t0 = [x, f];
|
||||
$[0] = props.items;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const x = t0;
|
||||
let t1;
|
||||
if ($[2] !== x) {
|
||||
t1 = [x, f];
|
||||
$[2] = x;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
function _temp(item) {
|
||||
return item;
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
|
||||
* aliases from `LoadLocal`, `PropertyLoad`, etc
|
||||
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
|
||||
* - We avoid assigning mutable ranges to loads after y's mutable range, as
|
||||
* these are working with an immutable value. As a result, `LoadLocal y` and
|
||||
* `PropertyLoad y` do not get mutable ranges
|
||||
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
|
||||
* as according to the 'co-mutation' of different values
|
||||
* - Here, we infer that
|
||||
* - `arrayPush(y, x)` might alias `x` and `y` to each other
|
||||
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
|
||||
* - This pass correctly extends the mutable range of `y`
|
||||
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
|
||||
* PropertyLoads still don't have a mutable range
|
||||
*
|
||||
* Note that the this bug is an edge case. Compiler output is only invalid for:
|
||||
* - function expressions with
|
||||
* `enableTransitivelyFreezeFunctionExpressions:false`
|
||||
* - functions that throw and get retried without clearing the memocache
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(5);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [];
|
||||
const y = { value: a };
|
||||
|
||||
arrayPush(x, y);
|
||||
const y_alias = y;
|
||||
let t2;
|
||||
if ($[3] !== y_alias.value) {
|
||||
t2 = () => y_alias.value;
|
||||
$[3] = y_alias.value;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const cb = t2;
|
||||
setPropertyByKey(x[0], "value", b);
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2, b: 10 }],
|
||||
sequentialRenders: [
|
||||
{ a: 2, b: 10 },
|
||||
{ a: 2, b: 11 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
|
||||
* aliases from `LoadLocal`, `PropertyLoad`, etc
|
||||
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
|
||||
* - We avoid assigning mutable ranges to loads after y's mutable range, as
|
||||
* these are working with an immutable value. As a result, `LoadLocal y` and
|
||||
* `PropertyLoad y` do not get mutable ranges
|
||||
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
|
||||
* as according to the 'co-mutation' of different values
|
||||
* - Here, we infer that
|
||||
* - `arrayPush(y, x)` might alias `x` and `y` to each other
|
||||
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
|
||||
* - This pass correctly extends the mutable range of `y`
|
||||
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
|
||||
* PropertyLoads still don't have a mutable range
|
||||
*
|
||||
* Note that the this bug is an edge case. Compiler output is only invalid for:
|
||||
* - function expressions with
|
||||
* `enableTransitivelyFreezeFunctionExpressions:false`
|
||||
* - functions that throw and get retried without clearing the memocache
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
function useFoo({a, b}: {a: number, b: number}) {
|
||||
const x = [];
|
||||
const y = {value: a};
|
||||
|
||||
arrayPush(x, y); // x and y co-mutate
|
||||
const y_alias = y;
|
||||
const cb = () => y_alias.value;
|
||||
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2, b: 10}],
|
||||
sequentialRenders: [
|
||||
{a: 2, b: 10},
|
||||
{a: 2, b: 11},
|
||||
],
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Variation of bug in `bug-aliased-capture-aliased-mutate`
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
|
||||
function useFoo({a}: {a: number, b: number}) {
|
||||
const arr = [];
|
||||
const obj = {value: a};
|
||||
|
||||
setPropertyByKey(obj, 'arr', arr);
|
||||
const obj_alias = obj;
|
||||
const cb = () => obj_alias.arr.length;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2}],
|
||||
sequentialRenders: [{a: 2}, {a: 3}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { setPropertyByKey, Stringify } from "shared-runtime";
|
||||
|
||||
function useFoo(t0) {
|
||||
const $ = _c(4);
|
||||
const { a } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
const arr = [];
|
||||
const obj = { value: a };
|
||||
|
||||
setPropertyByKey(obj, "arr", arr);
|
||||
const obj_alias = obj;
|
||||
let t2;
|
||||
if ($[2] !== obj_alias.arr.length) {
|
||||
t2 = () => obj_alias.arr.length;
|
||||
$[2] = obj_alias.arr.length;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const cb = t2;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
|
||||
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{ a: 2 }],
|
||||
sequentialRenders: [{ a: 2 }, { a: 3 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Variation of bug in `bug-aliased-capture-aliased-mutate`
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: ok)
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
|
||||
*/
|
||||
|
||||
function useFoo({a}: {a: number, b: number}) {
|
||||
const arr = [];
|
||||
const obj = {value: a};
|
||||
|
||||
setPropertyByKey(obj, 'arr', arr);
|
||||
const obj_alias = obj;
|
||||
const cb = () => obj_alias.arr.length;
|
||||
for (let i = 0; i < a; i++) {
|
||||
arr.push(i);
|
||||
}
|
||||
return <Stringify cb={cb} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [{a: 2}],
|
||||
sequentialRenders: [{a: 2}, {a: 3}],
|
||||
};
|
||||
@@ -85,19 +85,11 @@ import { makeArray, mutate } from "shared-runtime";
|
||||
* used when we analyze CallExpressions.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(3);
|
||||
const { foo, bar } = t0;
|
||||
let t1;
|
||||
if ($[0] !== foo) {
|
||||
t1 = { foo };
|
||||
$[0] = foo;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const x = t1;
|
||||
let y;
|
||||
if ($[2] !== bar || $[3] !== x) {
|
||||
if ($[0] !== bar || $[1] !== foo) {
|
||||
const x = { foo };
|
||||
y = { bar };
|
||||
const f0 = function () {
|
||||
const a = makeArray(y);
|
||||
@@ -108,11 +100,11 @@ function Component(t0) {
|
||||
|
||||
f0();
|
||||
mutate(y.x);
|
||||
$[2] = bar;
|
||||
$[3] = x;
|
||||
$[4] = y;
|
||||
$[0] = bar;
|
||||
$[1] = foo;
|
||||
$[2] = y;
|
||||
} else {
|
||||
y = $[4];
|
||||
y = $[2];
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
|
||||
const boxedInner = [obj?.inner];
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error('invariant broken');
|
||||
}
|
||||
return <Stringify obj={obj} inner={boxedInner} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{arg: 0}],
|
||||
sequentialRenders: [{arg: 0}, {arg: 1}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const $ = _c(4);
|
||||
const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [obj?.inner];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const boxedInner = t0;
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error("invariant broken");
|
||||
}
|
||||
let t1;
|
||||
if ($[1] !== boxedInner || $[2] !== obj) {
|
||||
t1 = <Stringify obj={obj} inner={boxedInner} />;
|
||||
$[1] = boxedInner;
|
||||
$[2] = obj;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ arg: 0 }],
|
||||
sequentialRenders: [{ arg: 0 }, { arg: 1 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture showing an edge case for ReactiveScope variable propagation.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* Forget:
|
||||
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
|
||||
* [[ (exception in render) Error: invariant broken ]]
|
||||
*
|
||||
*/
|
||||
function Component() {
|
||||
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
|
||||
const boxedInner = [obj?.inner];
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
if (boxedInner[0] !== obj?.inner) {
|
||||
throw new Error('invariant broken');
|
||||
}
|
||||
return <Stringify obj={obj} inner={boxedInner} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{arg: 0}],
|
||||
sequentialRenders: [{arg: 0}, {arg: 1}],
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = {
|
||||
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
|
||||
[tmp]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { identity, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const $ = _c(8);
|
||||
let key;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
key = {};
|
||||
t0 = (mutate(key), key);
|
||||
$[0] = key;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
key = $[0];
|
||||
t0 = $[1];
|
||||
}
|
||||
const tmp = t0;
|
||||
let t1;
|
||||
if ($[2] !== props.value) {
|
||||
t1 = identity([props.value]);
|
||||
$[2] = props.value;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== t1) {
|
||||
t2 = { [tmp]: t1 };
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const context = t2;
|
||||
|
||||
mutate(key);
|
||||
let t3;
|
||||
if ($[6] !== context) {
|
||||
t3 = [context, key];
|
||||
$[6] = context;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
sequentialRenders: [{ value: 42 }, { value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
|
||||
* with the mutation hoisted to a named variable instead of being directly
|
||||
* inlined into the Object key.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* Forget:
|
||||
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
|
||||
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
|
||||
*/
|
||||
function Component(props) {
|
||||
const key = {};
|
||||
const tmp = (mutate(key), key);
|
||||
const context = {
|
||||
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
|
||||
[tmp]: identity([props.value]),
|
||||
};
|
||||
mutate(key);
|
||||
return [context, key];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
sequentialRenders: [{value: 42}, {value: 42}],
|
||||
};
|
||||
@@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function component(a) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== a) {
|
||||
t0 = { a };
|
||||
const z = { a };
|
||||
t0 = () => {
|
||||
console.log(z);
|
||||
};
|
||||
$[0] = a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const z = t0;
|
||||
let t1;
|
||||
if ($[2] !== z) {
|
||||
t1 = () => {
|
||||
console.log(z);
|
||||
};
|
||||
$[2] = z;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const x = t1;
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function component(a) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== a) {
|
||||
t0 = { a };
|
||||
const z = { a };
|
||||
t0 = function () {
|
||||
console.log(z);
|
||||
};
|
||||
$[0] = a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const z = t0;
|
||||
let t1;
|
||||
if ($[2] !== z) {
|
||||
t1 = function () {
|
||||
console.log(z);
|
||||
};
|
||||
$[2] = z;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const x = t1;
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,35 +22,19 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const $ = _c(2);
|
||||
const { a } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a) {
|
||||
t1 = { a };
|
||||
const z = { a };
|
||||
const p = () => <Stringify>{z}</Stringify>;
|
||||
t1 = p();
|
||||
$[0] = a;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const z = t1;
|
||||
let t2;
|
||||
if ($[2] !== z) {
|
||||
t2 = () => <Stringify>{z}</Stringify>;
|
||||
$[2] = z;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const p = t2;
|
||||
let t3;
|
||||
if ($[4] !== p) {
|
||||
t3 = p();
|
||||
$[4] = p;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -25,27 +25,19 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function component(a) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== a) {
|
||||
t0 = { a };
|
||||
const z = { a };
|
||||
t0 = function () {
|
||||
console.log(z);
|
||||
};
|
||||
$[0] = a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const z = t0;
|
||||
let t1;
|
||||
if ($[2] !== z) {
|
||||
t1 = function () {
|
||||
console.log(z);
|
||||
};
|
||||
$[2] = z;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const x = t1;
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,29 +25,21 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function component(a) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== a) {
|
||||
t0 = { a };
|
||||
const z = { a };
|
||||
t0 = function () {
|
||||
(function () {
|
||||
console.log(z);
|
||||
})();
|
||||
};
|
||||
$[0] = a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const z = t0;
|
||||
let t1;
|
||||
if ($[2] !== z) {
|
||||
t1 = function () {
|
||||
(function () {
|
||||
console.log(z);
|
||||
})();
|
||||
};
|
||||
$[2] = z;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const x = t1;
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useRef} from 'react';
|
||||
|
||||
function useThing(fn) {
|
||||
const fnRef = useRef(fn);
|
||||
const ref = useRef(null);
|
||||
|
||||
if (ref.current === null) {
|
||||
ref.current = function (this: unknown, ...args) {
|
||||
return fnRef.current.call(this, ...args);
|
||||
};
|
||||
}
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Expected a non-reserved identifier name
|
||||
|
||||
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import {useRef} from 'react';
|
||||
|
||||
function useThing(fn) {
|
||||
const fnRef = useRef(fn);
|
||||
const ref = useRef(null);
|
||||
|
||||
if (ref.current === null) {
|
||||
ref.current = function (this: unknown, ...args) {
|
||||
return fnRef.current.call(this, ...args);
|
||||
};
|
||||
}
|
||||
return ref.current;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
};
|
||||
return <div {...foo} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
|
||||
error.assign-global-in-jsx-spread-attribute.ts:4:4
|
||||
2 | function Component() {
|
||||
3 | const foo = () => {
|
||||
> 4 | someGlobal = true;
|
||||
| ^^^^^^^^^^ Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)
|
||||
5 | };
|
||||
6 | return <div {...foo} />;
|
||||
7 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useCallback, useRef} from 'react';
|
||||
|
||||
export default function useThunkDispatch(state, dispatch, extraArg) {
|
||||
const stateRef = useRef(state);
|
||||
stateRef.current = state;
|
||||
|
||||
return useCallback(
|
||||
function thunk(action) {
|
||||
if (typeof action === 'function') {
|
||||
return action(thunk, () => stateRef.current, extraArg);
|
||||
} else {
|
||||
dispatch(action);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[dispatch, extraArg]
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
|
||||
|
||||
<unknown> thunk$14.
|
||||
|
||||
error.bug-infer-mutation-aliasing-effects.ts:10:22
|
||||
8 | function thunk(action) {
|
||||
9 | if (typeof action === 'function') {
|
||||
> 10 | return action(thunk, () => stateRef.current, extraArg);
|
||||
| ^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized
|
||||
11 | } else {
|
||||
12 | dispatch(action);
|
||||
13 | return undefined;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import {useCallback, useRef} from 'react';
|
||||
|
||||
export default function useThunkDispatch(state, dispatch, extraArg) {
|
||||
const stateRef = useRef(state);
|
||||
stateRef.current = state;
|
||||
|
||||
return useCallback(
|
||||
function thunk(action) {
|
||||
if (typeof action === 'function') {
|
||||
return action(thunk, () => stateRef.current, extraArg);
|
||||
} else {
|
||||
dispatch(action);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[dispatch, extraArg]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
const YearsAndMonthsSince = () => {
|
||||
const diff = foo();
|
||||
const months = Math.floor(diff.bar());
|
||||
return <>{months}</>;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
|
||||
|
||||
error.bug-invariant-codegen-methodcall.ts:3:17
|
||||
1 | const YearsAndMonthsSince = () => {
|
||||
2 | const diff = foo();
|
||||
> 3 | const months = Math.floor(diff.bar());
|
||||
| ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
|
||||
4 | return <>{months}</>;
|
||||
5 | };
|
||||
6 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const YearsAndMonthsSince = () => {
|
||||
const diff = foo();
|
||||
const months = Math.floor(diff.bar());
|
||||
return <>{months}</>;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useEffect} from 'react';
|
||||
|
||||
export function Foo() {
|
||||
useEffect(() => {
|
||||
try {
|
||||
// do something
|
||||
} catch ({status}) {
|
||||
// do something
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: (BuildHIR::lowerAssignment) Could not find binding for declaration.
|
||||
|
||||
error.bug-invariant-couldnt-find-binding-for-decl.ts:7:14
|
||||
5 | try {
|
||||
6 | // do something
|
||||
> 7 | } catch ({status}) {
|
||||
| ^^^^^^ (BuildHIR::lowerAssignment) Could not find binding for declaration.
|
||||
8 | // do something
|
||||
9 | }
|
||||
10 | }, []);
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import {useEffect} from 'react';
|
||||
|
||||
export function Foo() {
|
||||
useEffect(() => {
|
||||
try {
|
||||
// do something
|
||||
} catch ({status}) {
|
||||
// do something
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
|
||||
export default function useFoo(text) {
|
||||
return useMemo(() => {
|
||||
try {
|
||||
let formattedText = '';
|
||||
try {
|
||||
formattedText = format(text);
|
||||
} catch {
|
||||
console.log('error');
|
||||
}
|
||||
return formattedText || '';
|
||||
} catch (e) {}
|
||||
}, [text]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: Expected a break target
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import {useMemo} from 'react';
|
||||
|
||||
export default function useFoo(text) {
|
||||
return useMemo(() => {
|
||||
try {
|
||||
let formattedText = '';
|
||||
try {
|
||||
formattedText = format(text);
|
||||
} catch {
|
||||
console.log('error');
|
||||
}
|
||||
return formattedText || '';
|
||||
} catch (e) {}
|
||||
}, [text]);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useMemo} from 'react';
|
||||
import {useFoo, formatB, Baz} from './lib';
|
||||
|
||||
export const Example = ({data}) => {
|
||||
let a;
|
||||
let b;
|
||||
|
||||
if (data) {
|
||||
({a, b} = data);
|
||||
}
|
||||
|
||||
const foo = useFoo(a);
|
||||
const bar = useMemo(() => formatB(b), [b]);
|
||||
|
||||
return <Baz foo={foo} bar={bar} />;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: Expected consistent kind for destructuring
|
||||
|
||||
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
|
||||
|
||||
error.bug-invariant-expected-consistent-destructuring.ts:9:9
|
||||
7 |
|
||||
8 | if (data) {
|
||||
> 9 | ({a, b} = data);
|
||||
| ^ Expected consistent kind for destructuring
|
||||
10 | }
|
||||
11 |
|
||||
12 | const foo = useFoo(a);
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {useMemo} from 'react';
|
||||
import {useFoo, formatB, Baz} from './lib';
|
||||
|
||||
export const Example = ({data}) => {
|
||||
let a;
|
||||
let b;
|
||||
|
||||
if (data) {
|
||||
({a, b} = data);
|
||||
}
|
||||
|
||||
const foo = useFoo(a);
|
||||
const bar = useMemo(() => formatB(b), [b]);
|
||||
|
||||
return <Baz foo={foo} bar={bar} />;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useState} from 'react';
|
||||
import {bar} from './bar';
|
||||
|
||||
export const useFoot = () => {
|
||||
const [, setState] = useState(null);
|
||||
try {
|
||||
const {data} = bar();
|
||||
setState({
|
||||
data,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
setState(_prevState => ({
|
||||
loading: false,
|
||||
error: err,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: Expected all references to a variable to be consistently local or context references
|
||||
|
||||
Identifier <unknown> err$7 is referenced as a context variable, but was previously referenced as a [object Object] variable.
|
||||
|
||||
error.bug-invariant-local-or-context-references.ts:15:13
|
||||
13 | setState(_prevState => ({
|
||||
14 | loading: false,
|
||||
> 15 | error: err,
|
||||
| ^^^ Expected all references to a variable to be consistently local or context references
|
||||
16 | }));
|
||||
17 | }
|
||||
18 | };
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import {useState} from 'react';
|
||||
import {bar} from './bar';
|
||||
|
||||
export const useFoot = () => {
|
||||
const [, setState] = useState(null);
|
||||
try {
|
||||
const {data} = bar();
|
||||
setState({
|
||||
data,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
setState(_prevState => ({
|
||||
loading: false,
|
||||
error: err,
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
const Foo = ({json}) => {
|
||||
try {
|
||||
const foo = JSON.parse(json)?.foo;
|
||||
return <span>{foo}</span>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: Unexpected terminal in optional
|
||||
|
||||
error.bug-invariant-unexpected-terminal-in-optional.ts:3:16
|
||||
1 | const Foo = ({json}) => {
|
||||
2 | try {
|
||||
> 3 | const foo = JSON.parse(json)?.foo;
|
||||
| ^^^^ Unexpected terminal in optional
|
||||
4 | return <span>{foo}</span>;
|
||||
5 | } catch {
|
||||
6 | return null;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const Foo = ({json}) => {
|
||||
try {
|
||||
const foo = JSON.parse(json)?.foo;
|
||||
return <span>{foo}</span>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import Bar from './Bar';
|
||||
|
||||
export function Foo() {
|
||||
return (
|
||||
<Bar
|
||||
renderer={(...props) => {
|
||||
return <span {...props}>{displayValue}</span>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass
|
||||
|
||||
identifier 15 is unnamed.
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import Bar from './Bar';
|
||||
|
||||
export function Foo() {
|
||||
return (
|
||||
<Bar
|
||||
renderer={(...props) => {
|
||||
return <span {...props}>{displayValue}</span>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot modify local variables after render completes
|
||||
|
||||
This argument is a function which may reassign or mutate a local variable after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
|
||||
|
||||
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:20:12
|
||||
18 | );
|
||||
19 | const ref = useRef(null);
|
||||
> 20 | useEffect(() => {
|
||||
| ^^^^^^^
|
||||
> 21 | if (ref.current === null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 22 | update();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | }, [update]);
|
||||
| ^^^^ This function may (indirectly) reassign or modify a local variable after render
|
||||
25 |
|
||||
26 | return 'ok';
|
||||
27 | }
|
||||
|
||||
error.bug-old-inference-false-positive-ref-validation-in-use-effect.ts:14:6
|
||||
12 | ...partialParams,
|
||||
13 | };
|
||||
> 14 | nextParams.param = 'value';
|
||||
| ^^^^^^^^^^ This modifies a local variable
|
||||
15 | console.log(nextParams);
|
||||
16 | },
|
||||
17 | [params]
|
||||
```
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
|
||||
component Foo() {
|
||||
const foo = useFoo();
|
||||
foo.current = true;
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed.
|
||||
|
||||
3 | component Foo() {
|
||||
4 | const foo = useFoo();
|
||||
> 5 | foo.current = true;
|
||||
| ^^^ value cannot be modified
|
||||
6 | return <div />;
|
||||
7 | }
|
||||
8 |
|
||||
|
||||
Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// @flow
|
||||
|
||||
component Foo() {
|
||||
const foo = useFoo();
|
||||
foo.current = true;
|
||||
return <div />;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
function BadExample() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(capitalize(firstName + ' ' + lastName));
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
function BadExample() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const [lastName, setLastName] = useState('Swift');
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(capitalize(firstName + ' ' + lastName));
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Foo() {
|
||||
const x = () => {
|
||||
window.href = 'foo';
|
||||
};
|
||||
const y = {x};
|
||||
return <Bar y={y} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
|
||||
|
||||
error.object-capture-global-mutation.ts:4:4
|
||||
2 | function Foo() {
|
||||
3 | const x = () => {
|
||||
> 4 | window.href = 'foo';
|
||||
| ^^^^^^ Modifying a variable defined outside a component or hook is not allowed. Consider using an effect
|
||||
5 | };
|
||||
6 | const y = {x};
|
||||
7 | return <Bar y={y} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const object = {object: props.object};
|
||||
const entries = useMemo(() => Object.entries(object), [object]);
|
||||
entries.map(([, value]) => {
|
||||
value.updated = true;
|
||||
});
|
||||
return <Stringify entries={entries} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{object: {key: makeObject_Primitives()}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Memoization: Compilation skipped because existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
|
||||
|
||||
error.validate-object-entries-mutation.ts:6:57
|
||||
4 | function Component(props) {
|
||||
5 | const object = {object: props.object};
|
||||
> 6 | const entries = useMemo(() => Object.entries(object), [object]);
|
||||
| ^^^^^^ This dependency may be modified later
|
||||
7 | entries.map(([, value]) => {
|
||||
8 | value.updated = true;
|
||||
9 | });
|
||||
|
||||
Memoization: Compilation skipped because existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
|
||||
|
||||
error.validate-object-entries-mutation.ts:6:18
|
||||
4 | function Component(props) {
|
||||
5 | const object = {object: props.object};
|
||||
> 6 | const entries = useMemo(() => Object.entries(object), [object]);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization
|
||||
7 | entries.map(([, value]) => {
|
||||
8 | value.updated = true;
|
||||
9 | });
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const object = {object: props.object};
|
||||
const entries = useMemo(() => Object.entries(object), [object]);
|
||||
entries.map(([, value]) => {
|
||||
value.updated = true;
|
||||
});
|
||||
return <Stringify entries={entries} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{object: {key: makeObject_Primitives()}}],
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const object = {object: props.object};
|
||||
const values = useMemo(() => Object.values(object), [object]);
|
||||
values.map(value => {
|
||||
value.updated = true;
|
||||
});
|
||||
return <Stringify values={values} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{object: {key: makeObject_Primitives()}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Memoization: Compilation skipped because existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
|
||||
|
||||
error.validate-object-values-mutation.ts:6:55
|
||||
4 | function Component(props) {
|
||||
5 | const object = {object: props.object};
|
||||
> 6 | const values = useMemo(() => Object.values(object), [object]);
|
||||
| ^^^^^^ This dependency may be modified later
|
||||
7 | values.map(value => {
|
||||
8 | value.updated = true;
|
||||
9 | });
|
||||
|
||||
Memoization: Compilation skipped because existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
|
||||
|
||||
error.validate-object-values-mutation.ts:6:17
|
||||
4 | function Component(props) {
|
||||
5 | const object = {object: props.object};
|
||||
> 6 | const values = useMemo(() => Object.values(object), [object]);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization
|
||||
7 | values.map(value => {
|
||||
8 | value.updated = true;
|
||||
9 | });
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const object = {object: props.object};
|
||||
const values = useMemo(() => Object.values(object), [object]);
|
||||
values.map(value => {
|
||||
value.updated = true;
|
||||
});
|
||||
return <Stringify values={values} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{object: {key: makeObject_Primitives()}}],
|
||||
};
|
||||
@@ -40,36 +40,29 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(7);
|
||||
const $ = _c(5);
|
||||
let t0;
|
||||
if ($[0] !== props.a) {
|
||||
t0 = [props.a];
|
||||
const a = [props.a];
|
||||
|
||||
t0 = [a];
|
||||
$[0] = props.a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const a = t0;
|
||||
let t1;
|
||||
if ($[2] !== a) {
|
||||
t1 = [a];
|
||||
$[2] = a;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const b = t1;
|
||||
const b = t0;
|
||||
let c;
|
||||
if ($[4] !== b || $[5] !== props.b) {
|
||||
if ($[2] !== b || $[3] !== props.b) {
|
||||
c = [];
|
||||
const d = {};
|
||||
d.b = b;
|
||||
c.push(props.b);
|
||||
$[4] = b;
|
||||
$[5] = props.b;
|
||||
$[6] = c;
|
||||
$[2] = b;
|
||||
$[3] = props.b;
|
||||
$[4] = c;
|
||||
} else {
|
||||
c = $[6];
|
||||
c = $[4];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.name) {
|
||||
t0 = fbt._(
|
||||
const element = fbt._(
|
||||
"Hello {a really long description that got split into multiple lines}",
|
||||
[
|
||||
fbt._param(
|
||||
@@ -46,21 +46,14 @@ function Component(props) {
|
||||
],
|
||||
{ hk: "1euPUp" },
|
||||
);
|
||||
|
||||
t0 = element.toString();
|
||||
$[0] = props.name;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const element = t0;
|
||||
let t1;
|
||||
if ($[2] !== element) {
|
||||
t1 = element.toString();
|
||||
$[2] = element;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -27,27 +27,28 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.name) {
|
||||
t0 = fbt._('Hello {"user" name}', [fbt._param('"user" name', props.name)], {
|
||||
hk: "S0vMe",
|
||||
});
|
||||
const element = fbt._(
|
||||
'Hello {"user" name}',
|
||||
[
|
||||
fbt._param(
|
||||
'"user" name',
|
||||
|
||||
props.name,
|
||||
),
|
||||
],
|
||||
{ hk: "S0vMe" },
|
||||
);
|
||||
|
||||
t0 = element.toString();
|
||||
$[0] = props.name;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const element = t0;
|
||||
let t1;
|
||||
if ($[2] !== element) {
|
||||
t1 = element.toString();
|
||||
$[2] = element;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -27,29 +27,28 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.name) {
|
||||
t0 = fbt._(
|
||||
const element = fbt._(
|
||||
"Hello {user name ☺}",
|
||||
[fbt._param("user name \u263A", props.name)],
|
||||
[
|
||||
fbt._param(
|
||||
"user name \u263A",
|
||||
|
||||
props.name,
|
||||
),
|
||||
],
|
||||
{ hk: "1En1lp" },
|
||||
);
|
||||
|
||||
t0 = element.toString();
|
||||
$[0] = props.name;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const element = t0;
|
||||
let t1;
|
||||
if ($[2] !== element) {
|
||||
t1 = element.toString();
|
||||
$[2] = element;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -27,27 +27,28 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import fbt from "fbt";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.name) {
|
||||
t0 = fbt._("Hello {user name}", [fbt._param("user name", props.name)], {
|
||||
hk: "2zEDKF",
|
||||
});
|
||||
const element = fbt._(
|
||||
"Hello {user name}",
|
||||
[
|
||||
fbt._param(
|
||||
"user name",
|
||||
|
||||
props.name,
|
||||
),
|
||||
],
|
||||
{ hk: "2zEDKF" },
|
||||
);
|
||||
|
||||
t0 = element.toString();
|
||||
$[0] = props.name;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const element = t0;
|
||||
let t1;
|
||||
if ($[2] !== element) {
|
||||
t1 = element.toString();
|
||||
$[2] = element;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -22,28 +22,21 @@ function Component(props) {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
const id = useSelectedEntitytId();
|
||||
let t0;
|
||||
if ($[0] !== id) {
|
||||
t0 = () => {
|
||||
const onLoad = () => {
|
||||
log(id);
|
||||
};
|
||||
|
||||
t0 = <Foo onLoad={onLoad} />;
|
||||
$[0] = id;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const onLoad = t0;
|
||||
let t1;
|
||||
if ($[2] !== onLoad) {
|
||||
t1 = <Foo onLoad={onLoad} />;
|
||||
$[2] = onLoad;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -21,27 +21,20 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = function () {
|
||||
const f = function () {
|
||||
return <div>{props.name}</div>;
|
||||
};
|
||||
|
||||
t0 = f.call();
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const f = t0;
|
||||
let t1;
|
||||
if ($[2] !== f) {
|
||||
t1 = f.call();
|
||||
$[2] = f;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -55,33 +55,26 @@ import { Stringify } from "shared-runtime";
|
||||
* (kind: exception) Cannot read properties of null (reading 'prop')
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const $ = _c(3);
|
||||
const { obj, isObjNull } = t0;
|
||||
let t1;
|
||||
if ($[0] !== isObjNull || $[1] !== obj) {
|
||||
t1 = () => {
|
||||
const callback = () => {
|
||||
if (!isObjNull) {
|
||||
return obj.prop;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
t1 = <Stringify shouldInvokeFns={true} callback={callback} />;
|
||||
$[0] = isObjNull;
|
||||
$[1] = obj;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const callback = t1;
|
||||
let t2;
|
||||
if ($[3] !== callback) {
|
||||
t2 = <Stringify shouldInvokeFns={true} callback={callback} />;
|
||||
$[3] = callback;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -27,7 +27,7 @@ function useFoo() {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
function useFoo() {
|
||||
const $ = _c(9);
|
||||
const $ = _c(7);
|
||||
const onClick = (response) => {
|
||||
setState(DISABLED_FORM);
|
||||
};
|
||||
@@ -48,31 +48,24 @@ function useFoo() {
|
||||
const handleLogout = t1;
|
||||
let t2;
|
||||
if ($[2] !== handleLogout) {
|
||||
t2 = () => <ColumnItem onPress={() => handleLogout()} />;
|
||||
const getComponent = () => <ColumnItem onPress={() => handleLogout()} />;
|
||||
|
||||
t2 = getComponent();
|
||||
$[2] = handleLogout;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const getComponent = t2;
|
||||
let t3;
|
||||
if ($[4] !== getComponent) {
|
||||
t3 = getComponent();
|
||||
$[4] = getComponent;
|
||||
$[5] = t3;
|
||||
if ($[4] !== onClick || $[5] !== t2) {
|
||||
t3 = [t2, onClick];
|
||||
$[4] = onClick;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== onClick || $[7] !== t3) {
|
||||
t4 = [t3, onClick];
|
||||
$[6] = onClick;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -42,74 +42,58 @@ import { c as _c } from "react/compiler-runtime"; /**
|
||||
* conservative and assume that all named lambdas are conditionally called.
|
||||
*/
|
||||
function useFoo(t0) {
|
||||
const $ = _c(17);
|
||||
const $ = _c(13);
|
||||
const { arr1, arr2 } = t0;
|
||||
let t1;
|
||||
if ($[0] !== arr1[0]) {
|
||||
t1 = () => arr1[0].value;
|
||||
const getVal1 = () => arr1[0].value;
|
||||
t1 = (e) => getVal1() + e.value;
|
||||
$[0] = arr1[0];
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const getVal1 = t1;
|
||||
const cb1 = t1;
|
||||
let t2;
|
||||
if ($[2] !== getVal1) {
|
||||
t2 = (e) => getVal1() + e.value;
|
||||
$[2] = getVal1;
|
||||
$[3] = t2;
|
||||
if ($[2] !== arr1 || $[3] !== cb1) {
|
||||
t2 = arr1.map(cb1);
|
||||
$[2] = arr1;
|
||||
$[3] = cb1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
t2 = $[4];
|
||||
}
|
||||
const cb1 = t2;
|
||||
const x = t2;
|
||||
let t3;
|
||||
if ($[4] !== arr1 || $[5] !== cb1) {
|
||||
t3 = arr1.map(cb1);
|
||||
$[4] = arr1;
|
||||
$[5] = cb1;
|
||||
if ($[5] !== arr2) {
|
||||
const getVal2 = () => arr2[0].value;
|
||||
t3 = (e_0) => getVal2() + e_0.value;
|
||||
$[5] = arr2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const x = t3;
|
||||
const cb2 = t3;
|
||||
let t4;
|
||||
if ($[7] !== arr2) {
|
||||
t4 = () => arr2[0].value;
|
||||
$[7] = arr2;
|
||||
$[8] = t4;
|
||||
if ($[7] !== arr1 || $[8] !== cb2) {
|
||||
t4 = arr1.map(cb2);
|
||||
$[7] = arr1;
|
||||
$[8] = cb2;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
t4 = $[9];
|
||||
}
|
||||
const getVal2 = t4;
|
||||
const y = t4;
|
||||
let t5;
|
||||
if ($[9] !== getVal2) {
|
||||
t5 = (e_0) => getVal2() + e_0.value;
|
||||
$[9] = getVal2;
|
||||
$[10] = t5;
|
||||
if ($[10] !== x || $[11] !== y) {
|
||||
t5 = [x, y];
|
||||
$[10] = x;
|
||||
$[11] = y;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
t5 = $[12];
|
||||
}
|
||||
const cb2 = t5;
|
||||
let t6;
|
||||
if ($[11] !== arr1 || $[12] !== cb2) {
|
||||
t6 = arr1.map(cb2);
|
||||
$[11] = arr1;
|
||||
$[12] = cb2;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
const y = t6;
|
||||
let t7;
|
||||
if ($[14] !== x || $[15] !== y) {
|
||||
t7 = [x, y];
|
||||
$[14] = x;
|
||||
$[15] = y;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
return t7;
|
||||
return t5;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -42,7 +42,7 @@ import { useRef } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(9);
|
||||
const $ = _c(7);
|
||||
const { a, b } = t0;
|
||||
let t1;
|
||||
if ($[0] !== a.value) {
|
||||
@@ -70,29 +70,22 @@ function Component(t0) {
|
||||
const hasLogged = useRef(false);
|
||||
let t3;
|
||||
if ($[4] !== logA || $[5] !== logB) {
|
||||
t3 = () => {
|
||||
const log = () => {
|
||||
if (!hasLogged.current) {
|
||||
logA();
|
||||
logB();
|
||||
hasLogged.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
t3 = <Stringify log={log} shouldInvokeFns={true} />;
|
||||
$[4] = logA;
|
||||
$[5] = logB;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const log = t3;
|
||||
let t4;
|
||||
if ($[7] !== log) {
|
||||
t4 = <Stringify log={log} shouldInvokeFns={true} />;
|
||||
$[7] = log;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -24,28 +24,20 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import * as SharedRuntime from "shared-runtime";
|
||||
import { invoke } from "shared-runtime";
|
||||
function useComponentFactory(t0) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
const { name } = t0;
|
||||
let t1;
|
||||
if ($[0] !== name) {
|
||||
t1 = () => (
|
||||
const cb = () => (
|
||||
<SharedRuntime.Stringify>hello world {name}</SharedRuntime.Stringify>
|
||||
);
|
||||
t1 = invoke(cb);
|
||||
$[0] = name;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const cb = t1;
|
||||
let t2;
|
||||
if ($[2] !== cb) {
|
||||
t2 = invoke(cb);
|
||||
$[2] = cb;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -25,34 +25,18 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.a) {
|
||||
t0 = { a: props.a };
|
||||
const item = { a: props.a };
|
||||
const items = [item];
|
||||
t0 = items.map(_temp);
|
||||
$[0] = props.a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const item = t0;
|
||||
let t1;
|
||||
if ($[2] !== item) {
|
||||
t1 = [item];
|
||||
$[2] = item;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const items = t1;
|
||||
let t2;
|
||||
if ($[4] !== items) {
|
||||
t2 = items.map(_temp);
|
||||
$[4] = items;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const mapped = t2;
|
||||
const mapped = t0;
|
||||
return mapped;
|
||||
}
|
||||
function _temp(item_0) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user