5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {CompilerError, ErrorSeverity} from '../CompilerError';
8
+import {CompilerError} from '../CompilerError';
9
import {Environment} from '../HIR';
10
import {
11
AbstractValue,
26
Type,
27
ValueKind,
28
ValueReason,
29
- getHookKind,
29
isArrayType,
30
isMutableEffect,
31
isObjectType,
33
- isRefOrRefValue,
32
} from '../HIR/HIR';
33
import {FunctionSignature} from '../HIR/ObjectShape';
34
import {
46
eachTerminalSuccessor,
47
} from '../HIR/visitors';
48
import {assertExhaustive} from '../Utils/utils';
49
+import {
50
+ inferTerminalFunctionEffects,
51
+ inferInstructionFunctionEffects,
52
+ raiseFunctionEffectErrors,
53
+} from './InferFunctionEffects';
54
55
const UndefinedValue: InstructionValue = {
56
kind: 'Primitive',
231
232
statesByBlock.set(blockId, incomingState);
233
const state = incomingState.clone();
231
- inferBlock(fn.env, functionEffects, state, block);
234
+ inferBlock(fn.env, state, block, functionEffects);
235
236
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
237
queue(nextBlockId, state);
239
}
240
}
241
239
- if (!options.isFunctionExpression) {
240
- functionEffects.forEach(eff => {
241
- switch (eff.kind) {
242
- case 'ReactMutation':
243
- case 'GlobalMutation': {
244
- CompilerError.throw(eff.error);
245
- }
246
- case 'ContextMutation': {
247
- CompilerError.throw({
248
- severity: ErrorSeverity.Invariant,
249
- reason: `Unexpected ContextMutation in top-level function effects`,
250
- loc: eff.loc,
251
- });
252
- }
253
- default:
254
- assertExhaustive(
255
- eff,
256
- `Unexpected function effect kind \`${(eff as any).kind}\``,
257
- );
258
- }
259
- });
260
- } else {
242
+ if (options.isFunctionExpression) {
243
fn.effects = functionEffects;
244
+ } else {
245
+ raiseFunctionEffectErrors(functionEffects);
246
}
247
}
248
249
+type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
250
+
251
// Maintains a mapping of top-level variables to the kind of value they hold
252
class InferenceState {
253
#env: Environment;
254
269
- // The kind of reach value, based on its allocation site
255
+ // The kind of each value, based on its allocation site
256
#values: Map<InstructionValue, AbstractValue>;
257
/*
258
* The set of values pointed to by each identifier. This is a set
364
* value is already frozen or is immutable.
365
*/
366
referenceAndRecordEffects(
367
+ freezeActions: Array<FreezeAction>,
368
place: Place,
369
effectKind: Effect,
370
reason: ValueReason,
384
- functionEffects: Array<FunctionEffect>,
371
): void {
372
const values = this.#variables.get(place.identifier.id);
373
if (values === undefined) {
384
return;
385
}
386
401
- // Propagate effects of function expressions to the outer (ie current) effect context
402
- for (const value of values) {
403
- if (
404
- (value.kind === 'FunctionExpression' ||
405
- value.kind === 'ObjectMethod') &&
406
- value.loweredFunc.func.effects != null
407
- ) {
408
- for (const effect of value.loweredFunc.func.effects) {
409
- if (
410
- effect.kind === 'GlobalMutation' ||
411
- effect.kind === 'ReactMutation'
412
- ) {
413
- // Known effects are always propagated upwards
414
- functionEffects.push(effect);
415
- } else {
416
- /**
417
- * Contextual effects need to be replayed against the current inference
418
- * state, which may know more about the value to which the effect applied.
419
- * The main cases are:
420
- * 1. The mutated context value is _still_ a context value in the current scope,
421
- * so we have to continue propagating the original context mutation.
422
- * 2. The mutated context value is a mutable value in the current scope,
423
- * so the context mutation was fine and we can skip propagating the effect.
424
- * 3. The mutated context value is an immutable value in the current scope,
425
- * resulting in a non-ContextMutation FunctionEffect. We propagate that new,
426
- * more detailed effect to the current function context.
427
- */
428
- for (const place of effect.places) {
429
- if (this.isDefined(place)) {
430
- const replayedEffect = this.reference(
431
- {...place, loc: effect.loc},
432
- effect.effect,
433
- reason,
434
- );
435
- if (replayedEffect != null) {
436
- if (replayedEffect.kind === 'ContextMutation') {
437
- // Case 1, still a context value so propagate the original effect
438
- functionEffects.push(effect);
439
- } else {
440
- // Case 3, immutable value so propagate the more precise effect
441
- functionEffects.push(replayedEffect);
442
- }
443
- } // else case 2, local mutable value so this effect was fine
444
- }
445
- }
446
- }
447
- }
448
- }
449
- }
450
- const functionEffect = this.reference(place, effectKind, reason);
451
- if (functionEffect !== null) {
452
- functionEffects.push(functionEffect);
453
- }
387
+ const action = this.reference(place, effectKind, reason);
388
+ action && freezeActions.push(action);
389
}
390
391
freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
423
place: Place,
424
effectKind: Effect,
425
reason: ValueReason,
491
- ): FunctionEffect | null {
426
+ ): null | FreezeAction {
427
const values = this.#variables.get(place.identifier.id);
428
CompilerError.invariant(values !== undefined, {
429
reason: '[InferReferenceEffects] Expected value to be initialized',
433
});
434
let valueKind: AbstractValue | null = this.kind(place);
435
let effect: Effect | null = null;
501
- let functionEffect: FunctionEffect | null = null;
436
+ let freeze: null | FreezeAction = null;
437
switch (effectKind) {
438
case Effect.Freeze: {
439
if (
448
reason: reasonSet,
449
context: new Set(),
450
};
516
- this.freezeValues(values, reasonSet);
451
+ freeze = {values, reason: reasonSet};
452
} else {
453
effect = Effect.Read;
454
}
466
break;
467
}
468
case Effect.Mutate: {
534
- if (isRefOrRefValue(place.identifier)) {
535
- // no-op: refs are validate via ValidateNoRefAccessInRender
536
- } else if (valueKind.kind === ValueKind.Context) {
537
- functionEffect = {
538
- kind: 'ContextMutation',
539
- loc: place.loc,
540
- effect: effectKind,
541
- places:
542
- valueKind.context.size === 0
543
- ? new Set([place])
544
- : valueKind.context,
545
- };
546
- } else if (
547
- valueKind.kind !== ValueKind.Mutable &&
548
- // We ignore mutations of primitives since this is not a React-specific problem
549
- valueKind.kind !== ValueKind.Primitive
550
- ) {
551
- let reason = getWriteErrorReason(valueKind);
552
- functionEffect = {
553
- kind:
554
- valueKind.reason.size === 1 &&
555
- valueKind.reason.has(ValueReason.Global)
556
- ? 'GlobalMutation'
557
- : 'ReactMutation',
558
- error: {
559
- reason,
560
- description:
561
- place.identifier.name !== null &&
562
- place.identifier.name.kind === 'named'
563
- ? `Found mutation of \`${place.identifier.name.value}\``
564
- : null,
565
- loc: place.loc,
566
- suggestions: null,
567
- severity: ErrorSeverity.InvalidReact,
568
- },
569
- };
570
- }
469
effect = Effect.Mutate;
470
break;
471
}
472
case Effect.Store: {
575
- if (isRefOrRefValue(place.identifier)) {
576
- // no-op: refs are validate via ValidateNoRefAccessInRender
577
- } else if (valueKind.kind === ValueKind.Context) {
578
- functionEffect = {
579
- kind: 'ContextMutation',
580
- loc: place.loc,
581
- effect: effectKind,
582
- places:
583
- valueKind.context.size === 0
584
- ? new Set([place])
585
- : valueKind.context,
586
- };
587
- } else if (
588
- valueKind.kind !== ValueKind.Mutable &&
589
- // We ignore mutations of primitives since this is not a React-specific problem
590
- valueKind.kind !== ValueKind.Primitive
591
- ) {
592
- let reason = getWriteErrorReason(valueKind);
593
- functionEffect = {
594
- kind:
595
- valueKind.reason.size === 1 &&
596
- valueKind.reason.has(ValueReason.Global)
597
- ? 'GlobalMutation'
598
- : 'ReactMutation',
599
- error: {
600
- reason,
601
- description:
602
- place.identifier.name !== null &&
603
- place.identifier.name.kind === 'named'
604
- ? `Found mutation of \`${place.identifier.name.value}\``
605
- : null,
606
- loc: place.loc,
607
- suggestions: null,
608
- severity: ErrorSeverity.InvalidReact,
609
- },
610
- };
611
- }
612
-
473
/*
474
* TODO(gsn): This should be bailout once we add bailout infra.
475
*
521
suggestions: null,
522
});
523
place.effect = effect;
664
- return functionEffect;
524
+ return freeze;
525
}
526
527
/*
812
return {kind, reason, context};
813
}
814
815
+type Continuation =
816
+ | {
817
+ kind: 'initialize';
818
+ valueKind: AbstractValue;
819
+ effect: {kind: Effect; reason: ValueReason} | null;
820
+ lvalueEffect?: Effect;
821
+ }
822
+ | {kind: 'funeffects'};
823
+
824
/*
825
* Iterates over the given @param block, defining variables and
826
* recording references on the @param state according to JS semantics.
827
*/
828
function inferBlock(
829
env: Environment,
961
- functionEffects: Array<FunctionEffect>,
830
state: InferenceState,
831
block: BasicBlock,
832
+ functionEffects: Array<FunctionEffect>,
833
): void {
834
for (const phi of block.phis) {
835
state.inferPhi(phi);
837
838
for (const instr of block.instructions) {
839
const instrValue = instr.value;
971
- let effect: {kind: Effect; reason: ValueReason} | null = null;
972
- let lvalueEffect = Effect.ConditionallyMutate;
973
- let valueKind: AbstractValue;
840
+ const defaultLvalueEffect = Effect.ConditionallyMutate;
841
+ let continuation: Continuation;
842
+ const freezeActions: Array<FreezeAction> = [];
843
switch (instrValue.kind) {
844
case 'BinaryExpression': {
976
- valueKind = {
977
- kind: ValueKind.Primitive,
978
- reason: new Set([ValueReason.Other]),
979
- context: new Set(),
980
- };
981
- effect = {
982
- kind: Effect.Read,
983
- reason: ValueReason.Other,
845
+ continuation = {
846
+ kind: 'initialize',
847
+ valueKind: {
848
+ kind: ValueKind.Primitive,
849
+ reason: new Set([ValueReason.Other]),
850
+ context: new Set(),
851
+ },
852
+ effect: {
853
+ kind: Effect.Read,
854
+ reason: ValueReason.Other,
855
+ },
856
};
857
break;
858
}
859
case 'ArrayExpression': {
988
- valueKind = hasContextRefOperand(state, instrValue)
860
+ const valueKind: AbstractValue = hasContextRefOperand(state, instrValue)
861
? {
862
kind: ValueKind.Context,
863
reason: new Set([ValueReason.Other]),
868
reason: new Set([ValueReason.Other]),
869
context: new Set(),
870
};
999
- effect = {kind: Effect.Capture, reason: ValueReason.Other};
1000
- lvalueEffect = Effect.Store;
871
+ continuation = {
872
+ kind: 'initialize',
873
+ valueKind,
874
+ effect: {kind: Effect.Capture, reason: ValueReason.Other},
875
+ lvalueEffect: Effect.Store,
876
+ };
877
break;
878
}
879
case 'NewExpression': {
890
* Classes / functions created during render could technically capture and
891
* mutate their enclosing scope, which we currently do not detect.
892
*/
1017
- valueKind = {
893
+ const valueKind: AbstractValue = {
894
kind: ValueKind.Mutable,
895
reason: new Set([ValueReason.Other]),
896
context: new Set(),
897
};
898
state.referenceAndRecordEffects(
899
+ freezeActions,
900
instrValue.callee,
901
Effect.Read,
902
ValueReason.Other,
1026
- functionEffects,
903
);
904
905
for (const operand of eachCallArgument(instrValue.args)) {
906
state.referenceAndRecordEffects(
907
+ freezeActions,
908
operand,
909
Effect.ConditionallyMutate,
910
ValueReason.Other,
1034
- functionEffects,
911
);
912
}
913
914
state.initialize(instrValue, valueKind);
915
state.define(instr.lvalue, instrValue);
1040
- instr.lvalue.effect = lvalueEffect;
1041
- continue;
916
+ instr.lvalue.effect = Effect.ConditionallyMutate;
917
+ continuation = {kind: 'funeffects'};
918
+ break;
919
}
920
case 'ObjectExpression': {
1044
- valueKind = hasContextRefOperand(state, instrValue)
921
+ const valueKind: AbstractValue = hasContextRefOperand(state, instrValue)
922
? {
923
kind: ValueKind.Context,
924
reason: new Set([ValueReason.Other]),
936
if (property.key.kind === 'computed') {
937
// Object keys must be primitives, so we know they're frozen at this point
938
state.referenceAndRecordEffects(
939
+ freezeActions,
940
property.key.name,
941
Effect.Freeze,
942
ValueReason.Other,
1065
- functionEffects,
943
);
944
}
945
// Object construction captures but does not modify the key/property values
946
state.referenceAndRecordEffects(
947
+ freezeActions,
948
property.place,
949
Effect.Capture,
950
ValueReason.Other,
1073
- functionEffects,
951
);
952
break;
953
}
954
case 'Spread': {
955
// Object construction captures but does not modify the key/property values
956
state.referenceAndRecordEffects(
957
+ freezeActions,
958
property.place,
959
Effect.Capture,
960
ValueReason.Other,
1083
- functionEffects,
961
);
962
break;
963
}
973
state.initialize(instrValue, valueKind);
974
state.define(instr.lvalue, instrValue);
975
instr.lvalue.effect = Effect.Store;
1099
- continue;
976
+ continuation = {kind: 'funeffects'};
977
+ break;
978
}
979
case 'UnaryExpression': {
1102
- valueKind = {
1103
- kind: ValueKind.Primitive,
1104
- reason: new Set([ValueReason.Other]),
1105
- context: new Set(),
980
+ continuation = {
981
+ kind: 'initialize',
982
+ valueKind: {
983
+ kind: ValueKind.Primitive,
984
+ reason: new Set([ValueReason.Other]),
985
+ context: new Set(),
986
+ },
987
+ effect: {kind: Effect.Read, reason: ValueReason.Other},
988
};
1107
- effect = {kind: Effect.Read, reason: ValueReason.Other};
989
break;
990
}
991
case 'UnsupportedNode': {
992
// TODO: handle other statement kinds
1112
- valueKind = {
1113
- kind: ValueKind.Mutable,
1114
- reason: new Set([ValueReason.Other]),
1115
- context: new Set(),
993
+ continuation = {
994
+ kind: 'initialize',
995
+ valueKind: {
996
+ kind: ValueKind.Mutable,
997
+ reason: new Set([ValueReason.Other]),
998
+ context: new Set(),
999
+ },
1000
+ effect: null,
1001
};
1002
break;
1003
}
1004
case 'JsxExpression': {
1005
if (instrValue.tag.kind === 'Identifier') {
1006
state.referenceAndRecordEffects(
1007
+ freezeActions,
1008
instrValue.tag,
1009
Effect.Freeze,
1010
ValueReason.JsxCaptured,
1125
- functionEffects,
1011
);
1012
}
1013
if (instrValue.children !== null) {
1014
for (const child of instrValue.children) {
1015
state.referenceAndRecordEffects(
1016
+ freezeActions,
1017
child,
1018
Effect.Freeze,
1019
ValueReason.JsxCaptured,
1134
- functionEffects,
1020
);
1021
}
1022
}
1023
for (const attr of instrValue.props) {
1024
if (attr.kind === 'JsxSpreadAttribute') {
1025
state.referenceAndRecordEffects(
1026
+ freezeActions,
1027
attr.argument,
1028
Effect.Freeze,
1029
ValueReason.JsxCaptured,
1144
- functionEffects,
1030
);
1031
} else {
1147
- const propEffects: Array<FunctionEffect> = [];
1032
state.referenceAndRecordEffects(
1033
+ freezeActions,
1034
attr.place,
1035
Effect.Freeze,
1036
ValueReason.JsxCaptured,
1152
- propEffects,
1153
- );
1154
- functionEffects.push(
1155
- ...propEffects.filter(
1156
- effect => !isEffectSafeOutsideRender(effect),
1157
- ),
1037
);
1038
}
1039
}
1045
});
1046
state.define(instr.lvalue, instrValue);
1047
instr.lvalue.effect = Effect.ConditionallyMutate;
1169
- continue;
1048
+ continuation = {kind: 'funeffects'};
1049
+ break;
1050
}
1051
case 'JsxFragment': {
1172
- valueKind = {
1173
- kind: ValueKind.Frozen,
1174
- reason: new Set([ValueReason.Other]),
1175
- context: new Set(),
1176
- };
1177
- effect = {
1178
- kind: Effect.Freeze,
1179
- reason: ValueReason.Other,
1052
+ continuation = {
1053
+ kind: 'initialize',
1054
+ valueKind: {
1055
+ kind: ValueKind.Frozen,
1056
+ reason: new Set([ValueReason.Other]),
1057
+ context: new Set(),
1058
+ },
1059
+ effect: {
1060
+ kind: Effect.Freeze,
1061
+ reason: ValueReason.Other,
1062
+ },
1063
};
1064
break;
1065
}
1068
* template literal (with no tag function) always produces
1069
* an immutable string
1070
*/
1188
- valueKind = {
1189
- kind: ValueKind.Primitive,
1190
- reason: new Set([ValueReason.Other]),
1191
- context: new Set(),
1071
+ continuation = {
1072
+ kind: 'initialize',
1073
+ valueKind: {
1074
+ kind: ValueKind.Primitive,
1075
+ reason: new Set([ValueReason.Other]),
1076
+ context: new Set(),
1077
+ },
1078
+ effect: {kind: Effect.Read, reason: ValueReason.Other},
1079
};
1193
- effect = {kind: Effect.Read, reason: ValueReason.Other};
1080
break;
1081
}
1082
case 'RegExpLiteral': {
1083
// RegExp instances are mutable objects
1198
- valueKind = {
1199
- kind: ValueKind.Mutable,
1200
- reason: new Set([ValueReason.Other]),
1201
- context: new Set(),
1202
- };
1203
- effect = {
1204
- kind: Effect.ConditionallyMutate,
1205
- reason: ValueReason.Other,
1084
+ continuation = {
1085
+ kind: 'initialize',
1086
+ valueKind: {
1087
+ kind: ValueKind.Mutable,
1088
+ reason: new Set([ValueReason.Other]),
1089
+ context: new Set(),
1090
+ },
1091
+ effect: {
1092
+ kind: Effect.ConditionallyMutate,
1093
+ reason: ValueReason.Other,
1094
+ },
1095
};
1096
break;
1097
}
1098
case 'MetaProperty': {
1099
if (instrValue.meta !== 'import' || instrValue.property !== 'meta') {
1211
- continue;
1100
+ continuation = {kind: 'funeffects'};
1101
+ break;
1102
}
1213
-
1214
- valueKind = {
1215
- kind: ValueKind.Global,
1216
- reason: new Set([ValueReason.Global]),
1217
- context: new Set(),
1103
+ continuation = {
1104
+ kind: 'initialize',
1105
+ valueKind: {
1106
+ kind: ValueKind.Global,
1107
+ reason: new Set([ValueReason.Global]),
1108
+ context: new Set(),
1109
+ },
1110
+ effect: null,
1111
};
1112
break;
1113
}
1114
case 'LoadGlobal':
1222
- valueKind = {
1223
- kind: ValueKind.Global,
1224
- reason: new Set([ValueReason.Global]),
1225
- context: new Set(),
1115
+ continuation = {
1116
+ kind: 'initialize',
1117
+ valueKind: {
1118
+ kind: ValueKind.Global,
1119
+ reason: new Set([ValueReason.Global]),
1120
+ context: new Set(),
1121
+ },
1122
+ effect: null,
1123
};
1124
break;
1125
case 'Debugger':
1126
case 'JSXText':
1127
case 'Primitive': {
1231
- valueKind = {
1232
- kind: ValueKind.Primitive,
1233
- reason: new Set([ValueReason.Other]),
1234
- context: new Set(),
1128
+ continuation = {
1129
+ kind: 'initialize',
1130
+ valueKind: {
1131
+ kind: ValueKind.Primitive,
1132
+ reason: new Set([ValueReason.Other]),
1133
+ context: new Set(),
1134
+ },
1135
+ effect: null,
1136
};
1137
break;
1138
}
1142
const mutableOperands: Array<Place> = [];
1143
for (const operand of eachInstructionOperand(instr)) {
1144
state.referenceAndRecordEffects(
1145
+ freezeActions,
1146
operand,
1147
operand.effect === Effect.Unknown ? Effect.Read : operand.effect,
1148
ValueReason.Other,
1247
- [],
1149
);
1150
if (isMutableEffect(operand.effect, operand.loc)) {
1151
mutableOperands.push(operand);
1152
}
1153
hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
1253
-
1254
- /**
1255
- * If this function references other functions, propagate the referenced function's
1256
- * effects to this function.
1257
- *
1258
- * ```
1259
- * let f = () => global = true;
1260
- * let g = () => f();
1261
- * g();
1262
- * ```
1263
- *
1264
- * In this example, because `g` references `f`, we propagate the GlobalMutation from
1265
- * `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
1266
- * function effect context and report an error. But if instead we do:
1267
- *
1268
- * ```
1269
- * let f = () => global = true;
1270
- * let g = () => f();
1271
- * useEffect(() => g(), [g])
1272
- * ```
1273
- *
1274
- * Now `g`'s effects will be discarded since they're in a useEffect.
1275
- */
1276
- const values = state.values(operand);
1277
- for (const value of values) {
1278
- if (
1279
- (value.kind === 'ObjectMethod' ||
1280
- value.kind === 'FunctionExpression') &&
1281
- value.loweredFunc.func.effects !== null
1282
- ) {
1283
- instrValue.loweredFunc.func.effects ??= [];
1284
- instrValue.loweredFunc.func.effects.push(
1285
- ...value.loweredFunc.func.effects,
1286
- );
1287
- }
1288
- }
1154
}
1155
/*
1156
* If a closure did not capture any mutable values, then we can consider it to be
1163
});
1164
state.define(instr.lvalue, instrValue);
1165
instr.lvalue.effect = Effect.Store;
1301
- continue;
1166
+ continuation = {kind: 'funeffects'};
1167
+ break;
1168
}
1169
case 'TaggedTemplateExpression': {
1170
const operands = [...eachInstructionValueOperand(instrValue)];
1197
context: new Set(),
1198
};
1199
state.referenceAndRecordEffects(
1200
+ freezeActions,
1201
instrValue.tag,
1202
calleeEffect,
1203
ValueReason.Other,
1337
- functionEffects,
1204
);
1205
state.initialize(instrValue, returnValueKind);
1206
state.define(instr.lvalue, instrValue);
1207
instr.lvalue.effect = Effect.ConditionallyMutate;
1342
- continue;
1208
+ continuation = {kind: 'funeffects'};
1209
+ break;
1210
}
1211
case 'CallExpression': {
1212
const signature = getFunctionCallSignature(
1232
context: new Set(),
1233
};
1234
let hasCaptureArgument = false;
1368
- let isHook = getHookKind(env, instrValue.callee.identifier) != null;
1235
for (let i = 0; i < instrValue.args.length; i++) {
1370
- const argumentEffects: Array<FunctionEffect> = [];
1236
const arg = instrValue.args[i];
1237
const place = arg.kind === 'Identifier' ? arg : arg.place;
1238
if (effects !== null) {
1239
state.referenceAndRecordEffects(
1240
+ freezeActions,
1241
place,
1242
effects[i],
1243
ValueReason.Other,
1378
- argumentEffects,
1244
);
1245
} else {
1246
state.referenceAndRecordEffects(
1247
+ freezeActions,
1248
place,
1249
Effect.ConditionallyMutate,
1250
ValueReason.Other,
1385
- argumentEffects,
1251
);
1252
}
1388
- /*
1389
- * Join the effects of the argument with the effects of the enclosing function,
1390
- * unless the we're detecting a global mutation inside a useEffect hook
1391
- */
1392
- functionEffects.push(
1393
- ...argumentEffects.filter(
1394
- argEffect => !isHook || !isEffectSafeOutsideRender(argEffect),
1395
- ),
1396
- );
1253
hasCaptureArgument ||= place.effect === Effect.Capture;
1254
}
1255
if (signature !== null) {
1256
state.referenceAndRecordEffects(
1257
+ freezeActions,
1258
instrValue.callee,
1259
signature.calleeEffect,
1260
ValueReason.Other,
1404
- functionEffects,
1261
);
1262
} else {
1263
state.referenceAndRecordEffects(
1264
+ freezeActions,
1265
instrValue.callee,
1266
Effect.ConditionallyMutate,
1267
ValueReason.Other,
1411
- functionEffects,
1268
);
1269
}
1270
hasCaptureArgument ||= instrValue.callee.effect === Effect.Capture;
1274
instr.lvalue.effect = hasCaptureArgument
1275
? Effect.Store
1276
: Effect.ConditionallyMutate;
1421
- continue;
1277
+ continuation = {kind: 'funeffects'};
1278
+ break;
1279
}
1280
case 'MethodCall': {
1281
CompilerError.invariant(state.isDefined(instrValue.receiver), {
1286
suggestions: null,
1287
});
1288
state.referenceAndRecordEffects(
1289
+ freezeActions,
1290
instrValue.property,
1291
Effect.Read,
1292
ValueReason.Other,
1435
- functionEffects,
1293
);
1294
1295
const signature = getFunctionCallSignature(
1322
for (const arg of instrValue.args) {
1323
const place = arg.kind === 'Identifier' ? arg : arg.place;
1324
state.referenceAndRecordEffects(
1325
+ freezeActions,
1326
place,
1327
Effect.Read,
1328
ValueReason.Other,
1471
- functionEffects,
1329
);
1330
}
1331
state.referenceAndRecordEffects(
1332
+ freezeActions,
1333
instrValue.receiver,
1334
Effect.Capture,
1335
ValueReason.Other,
1478
- functionEffects,
1336
);
1337
state.initialize(instrValue, returnValueKind);
1338
state.define(instr.lvalue, instrValue);
1340
instrValue.receiver.effect === Effect.Capture
1341
? Effect.Store
1342
: Effect.ConditionallyMutate;
1486
- continue;
1343
+ continuation = {kind: 'funeffects'};
1344
+ break;
1345
}
1346
1347
const effects =
1348
signature !== null ? getFunctionEffects(instrValue, signature) : null;
1349
let hasCaptureArgument = false;
1492
- let isHook = getHookKind(env, instrValue.property.identifier) != null;
1350
for (let i = 0; i < instrValue.args.length; i++) {
1494
- const argumentEffects: Array<FunctionEffect> = [];
1351
const arg = instrValue.args[i];
1352
const place = arg.kind === 'Identifier' ? arg : arg.place;
1353
if (effects !== null) {
1356
* mutating effects
1357
*/
1358
state.referenceAndRecordEffects(
1359
+ freezeActions,
1360
place,
1361
effects[i],
1362
ValueReason.Other,
1506
- argumentEffects,
1363
);
1364
} else {
1365
state.referenceAndRecordEffects(
1366
+ freezeActions,
1367
place,
1368
Effect.ConditionallyMutate,
1369
ValueReason.Other,
1513
- argumentEffects,
1370
);
1371
}
1516
- /*
1517
- * Join the effects of the argument with the effects of the enclosing function,
1518
- * unless the we're detecting a global mutation inside a useEffect hook
1519
- */
1520
- functionEffects.push(
1521
- ...argumentEffects.filter(
1522
- argEffect => !isHook || !isEffectSafeOutsideRender(argEffect),
1523
- ),
1524
- );
1372
hasCaptureArgument ||= place.effect === Effect.Capture;
1373
}
1374
if (signature !== null) {
1375
state.referenceAndRecordEffects(
1376
+ freezeActions,
1377
instrValue.receiver,
1378
signature.calleeEffect,
1379
ValueReason.Other,
1532
- functionEffects,
1380
);
1381
} else {
1382
state.referenceAndRecordEffects(
1383
+ freezeActions,
1384
instrValue.receiver,
1385
Effect.ConditionallyMutate,
1386
ValueReason.Other,
1539
- functionEffects,
1387
);
1388
}
1389
hasCaptureArgument ||= instrValue.receiver.effect === Effect.Capture;
1393
instr.lvalue.effect = hasCaptureArgument
1394
? Effect.Store
1395
: Effect.ConditionallyMutate;
1549
- continue;
1396
+ continuation = {kind: 'funeffects'};
1397
+ break;
1398
}
1399
case 'PropertyStore': {
1400
const effect =
1402
? Effect.ConditionallyMutate
1403
: Effect.Capture;
1404
state.referenceAndRecordEffects(
1405
+ freezeActions,
1406
instrValue.value,
1407
effect,
1408
ValueReason.Other,
1560
- functionEffects,
1409
);
1410
state.referenceAndRecordEffects(
1411
+ freezeActions,
1412
instrValue.object,
1413
Effect.Store,
1414
ValueReason.Other,
1566
- functionEffects,
1415
);
1416
1417
const lvalue = instr.lvalue;
1418
state.alias(lvalue, instrValue.value);
1419
lvalue.effect = Effect.Store;
1572
- continue;
1420
+ continuation = {kind: 'funeffects'};
1421
+ break;
1422
}
1423
case 'PropertyDelete': {
1424
// `delete` returns a boolean (immutable) and modifies the object
1576
- valueKind = {
1577
- kind: ValueKind.Primitive,
1578
- reason: new Set([ValueReason.Other]),
1579
- context: new Set(),
1425
+ continuation = {
1426
+ kind: 'initialize',
1427
+ valueKind: {
1428
+ kind: ValueKind.Primitive,
1429
+ reason: new Set([ValueReason.Other]),
1430
+ context: new Set(),
1431
+ },
1432
+ effect: {kind: Effect.Mutate, reason: ValueReason.Other},
1433
};
1581
- effect = {kind: Effect.Mutate, reason: ValueReason.Other};
1434
break;
1435
}
1436
case 'PropertyLoad': {
1437
state.referenceAndRecordEffects(
1438
+ freezeActions,
1439
instrValue.object,
1440
Effect.Read,
1441
ValueReason.Other,
1589
- functionEffects,
1442
);
1443
const lvalue = instr.lvalue;
1444
lvalue.effect = Effect.ConditionallyMutate;
1445
state.initialize(instrValue, state.kind(instrValue.object));
1446
state.define(lvalue, instrValue);
1595
- continue;
1447
+ continuation = {kind: 'funeffects'};
1448
+ break;
1449
}
1450
case 'ComputedStore': {
1451
const effect =
1453
? Effect.ConditionallyMutate
1454
: Effect.Capture;
1455
state.referenceAndRecordEffects(
1456
+ freezeActions,
1457
instrValue.value,
1458
effect,
1459
ValueReason.Other,
1606
- functionEffects,
1460
);
1461
state.referenceAndRecordEffects(
1462
+ freezeActions,
1463
instrValue.property,
1464
Effect.Capture,
1465
ValueReason.Other,
1612
- functionEffects,
1466
);
1467
state.referenceAndRecordEffects(
1468
+ freezeActions,
1469
instrValue.object,
1470
Effect.Store,
1471
ValueReason.Other,
1618
- functionEffects,
1472
);
1473
1474
const lvalue = instr.lvalue;
1475
state.alias(lvalue, instrValue.value);
1476
lvalue.effect = Effect.Store;
1624
- continue;
1477
+ continuation = {kind: 'funeffects'};
1478
+ break;
1479
}
1480
case 'ComputedDelete': {
1481
state.referenceAndRecordEffects(
1482
+ freezeActions,
1483
instrValue.object,
1484
Effect.Mutate,
1485
ValueReason.Other,
1631
- functionEffects,
1486
);
1487
state.referenceAndRecordEffects(
1488
+ freezeActions,
1489
instrValue.property,
1490
Effect.Read,
1491
ValueReason.Other,
1637
- functionEffects,
1492
);
1493
state.initialize(instrValue, {
1494
kind: ValueKind.Primitive,
1497
});
1498
state.define(instr.lvalue, instrValue);
1499
instr.lvalue.effect = Effect.Mutate;
1646
- continue;
1500
+ continuation = {kind: 'funeffects'};
1501
+ break;
1502
}
1503
case 'ComputedLoad': {
1504
state.referenceAndRecordEffects(
1505
+ freezeActions,
1506
instrValue.object,
1507
Effect.Read,
1508
ValueReason.Other,
1653
- functionEffects,
1509
);
1510
state.referenceAndRecordEffects(
1511
+ freezeActions,
1512
instrValue.property,
1513
Effect.Read,
1514
ValueReason.Other,
1659
- functionEffects,
1515
);
1516
const lvalue = instr.lvalue;
1517
lvalue.effect = Effect.ConditionallyMutate;
1518
state.initialize(instrValue, state.kind(instrValue.object));
1519
state.define(lvalue, instrValue);
1665
- continue;
1520
+ continuation = {kind: 'funeffects'};
1521
+ break;
1522
}
1523
case 'Await': {
1524
state.initialize(instrValue, state.kind(instrValue.value));
1528
* will occur.
1529
*/
1530
state.referenceAndRecordEffects(
1531
+ freezeActions,
1532
instrValue.value,
1533
Effect.ConditionallyMutate,
1534
ValueReason.Other,
1678
- functionEffects,
1535
);
1536
const lvalue = instr.lvalue;
1537
lvalue.effect = Effect.ConditionallyMutate;
1538
state.alias(lvalue, instrValue.value);
1683
- continue;
1539
+ continuation = {kind: 'funeffects'};
1540
+ break;
1541
}
1542
case 'TypeCastExpression': {
1543
/*
1550
*/
1551
state.initialize(instrValue, state.kind(instrValue.value));
1552
state.referenceAndRecordEffects(
1553
+ freezeActions,
1554
instrValue.value,
1555
Effect.Read,
1556
ValueReason.Other,
1699
- functionEffects,
1557
);
1558
const lvalue = instr.lvalue;
1559
lvalue.effect = Effect.ConditionallyMutate;
1560
state.alias(lvalue, instrValue.value);
1704
- continue;
1561
+ continuation = {kind: 'funeffects'};
1562
+ break;
1563
}
1564
case 'StartMemoize':
1565
case 'FinishMemoize': {
1566
for (const val of eachInstructionValueOperand(instrValue)) {
1567
if (env.config.enablePreserveExistingMemoizationGuarantees) {
1568
state.referenceAndRecordEffects(
1569
+ freezeActions,
1570
val,
1571
Effect.Freeze,
1572
ValueReason.Other,
1714
- [],
1573
);
1574
} else {
1575
state.referenceAndRecordEffects(
1576
+ freezeActions,
1577
val,
1578
Effect.Read,
1579
ValueReason.Other,
1721
- [],
1580
);
1581
}
1582
}
1588
context: new Set(),
1589
});
1590
state.define(lvalue, instrValue);
1733
- continue;
1591
+ continuation = {kind: 'funeffects'};
1592
+ break;
1593
}
1594
case 'LoadLocal': {
1595
const lvalue = instr.lvalue;
1599
? Effect.ConditionallyMutate
1600
: Effect.Capture;
1601
state.referenceAndRecordEffects(
1602
+ freezeActions,
1603
instrValue.place,
1604
effect,
1605
ValueReason.Other,
1746
- [],
1606
);
1607
lvalue.effect = Effect.ConditionallyMutate;
1608
// direct aliasing: `a = b`;
1609
state.alias(lvalue, instrValue.place);
1751
- continue;
1610
+ continuation = {kind: 'funeffects'};
1611
+ break;
1612
}
1613
case 'LoadContext': {
1614
state.referenceAndRecordEffects(
1615
+ freezeActions,
1616
instrValue.place,
1617
Effect.Capture,
1618
ValueReason.Other,
1758
- functionEffects,
1619
);
1620
const lvalue = instr.lvalue;
1621
lvalue.effect = Effect.ConditionallyMutate;
1622
const valueKind = state.kind(instrValue.place);
1623
state.initialize(instrValue, valueKind);
1624
state.define(lvalue, instrValue);
1765
- continue;
1625
+ continuation = {kind: 'funeffects'};
1626
+ break;
1627
}
1628
case 'DeclareLocal': {
1629
const value = UndefinedValue;
1643
},
1644
);
1645
state.define(instrValue.lvalue.place, value);
1785
- continue;
1646
+ continuation = {kind: 'funeffects'};
1647
+ break;
1648
}
1649
case 'DeclareContext': {
1650
state.initialize(instrValue, {
1653
context: new Set(),
1654
});
1655
state.define(instrValue.lvalue.place, instrValue);
1794
- continue;
1656
+ continuation = {kind: 'funeffects'};
1657
+ break;
1658
}
1659
case 'PostfixUpdate':
1660
case 'PrefixUpdate': {
1664
? Effect.ConditionallyMutate
1665
: Effect.Capture;
1666
state.referenceAndRecordEffects(
1667
+ freezeActions,
1668
instrValue.value,
1669
effect,
1670
ValueReason.Other,
1807
- functionEffects,
1671
);
1672
1673
const lvalue = instr.lvalue;
1681
* replacing it
1682
*/
1683
instrValue.lvalue.effect = Effect.Store;
1821
- continue;
1684
+ continuation = {kind: 'funeffects'};
1685
+ break;
1686
}
1687
case 'StoreLocal': {
1688
const effect =
1691
? Effect.ConditionallyMutate
1692
: Effect.Capture;
1693
state.referenceAndRecordEffects(
1694
+ freezeActions,
1695
instrValue.value,
1696
effect,
1697
ValueReason.Other,
1833
- [],
1698
);
1699
1700
const lvalue = instr.lvalue;
1708
* replacing it
1709
*/
1710
instrValue.lvalue.place.effect = Effect.Store;
1847
- continue;
1711
+ continuation = {kind: 'funeffects'};
1712
+ break;
1713
}
1714
case 'StoreContext': {
1715
state.referenceAndRecordEffects(
1716
+ freezeActions,
1717
instrValue.value,
1718
Effect.ConditionallyMutate,
1719
ValueReason.Other,
1854
- functionEffects,
1720
);
1721
state.referenceAndRecordEffects(
1722
+ freezeActions,
1723
instrValue.lvalue.place,
1724
Effect.Mutate,
1725
ValueReason.Other,
1860
- functionEffects,
1726
);
1727
1728
const lvalue = instr.lvalue;
1729
state.alias(lvalue, instrValue.value);
1730
lvalue.effect = Effect.Store;
1866
- continue;
1731
+ continuation = {kind: 'funeffects'};
1732
+ break;
1733
}
1734
case 'StoreGlobal': {
1735
state.referenceAndRecordEffects(
1736
+ freezeActions,
1737
instrValue.value,
1738
Effect.Capture,
1739
ValueReason.Other,
1873
- functionEffects,
1740
);
1741
const lvalue = instr.lvalue;
1742
lvalue.effect = Effect.Store;
1877
-
1878
- functionEffects.push({
1879
- kind: 'GlobalMutation',
1880
- error: {
1881
- reason:
1882
- '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)',
1883
- loc: instr.loc,
1884
- suggestions: null,
1885
- severity: ErrorSeverity.InvalidReact,
1886
- },
1887
- });
1888
- continue;
1743
+ continuation = {kind: 'funeffects'};
1744
+ break;
1745
}
1746
case 'Destructure': {
1747
let effect: Effect = Effect.Capture;
1755
}
1756
}
1757
state.referenceAndRecordEffects(
1758
+ freezeActions,
1759
instrValue.value,
1760
effect,
1761
ValueReason.Other,
1905
- functionEffects,
1762
);
1763
1764
const lvalue = instr.lvalue;
1774
*/
1775
place.effect = Effect.Store;
1776
}
1921
- continue;
1777
+ continuation = {kind: 'funeffects'};
1778
+ break;
1779
}
1780
case 'GetIterator': {
1781
/**
1795
const kind = state.kind(instrValue.collection).kind;
1796
const isMutable =
1797
kind === ValueKind.Mutable || kind === ValueKind.Context;
1798
+ let effect;
1799
+ let valueKind: AbstractValue;
1800
if (!isMutable || isArrayType(instrValue.collection.identifier)) {
1801
// Case 1, assume iterator is a separate mutable object
1802
effect = {
1816
};
1817
valueKind = state.kind(instrValue.collection);
1818
}
1960
- lvalueEffect = Effect.Store;
1819
+ continuation = {
1820
+ kind: 'initialize',
1821
+ effect,
1822
+ valueKind,
1823
+ lvalueEffect: Effect.Store,
1824
+ };
1825
break;
1826
}
1827
case 'IteratorNext': {
1836
* ConditionallyMutate reflects this "mutate if mutable" semantic.
1837
*/
1838
state.referenceAndRecordEffects(
1839
+ freezeActions,
1840
instrValue.iterator,
1841
Effect.ConditionallyMutate,
1842
ValueReason.Other,
1978
- functionEffects,
1843
);
1844
/**
1845
* Regardless of the effect on the iterator, the *result* of advancing the iterator
1848
* ensure that the item is mutable or frozen if the collection is mutable/frozen.
1849
*/
1850
state.referenceAndRecordEffects(
1851
+ freezeActions,
1852
instrValue.collection,
1853
Effect.Capture,
1854
ValueReason.Other,
1990
- functionEffects,
1855
);
1856
state.initialize(instrValue, state.kind(instrValue.collection));
1857
state.define(instr.lvalue, instrValue);
1858
instr.lvalue.effect = Effect.Store;
1995
- continue;
1859
+ continuation = {kind: 'funeffects'};
1860
+ break;
1861
}
1862
case 'NextPropertyOf': {
1998
- effect = {kind: Effect.Read, reason: ValueReason.Other};
1999
- lvalueEffect = Effect.Store;
2000
- valueKind = {
2001
- kind: ValueKind.Primitive,
2002
- reason: new Set([ValueReason.Other]),
2003
- context: new Set(),
1863
+ continuation = {
1864
+ kind: 'initialize',
1865
+ effect: {kind: Effect.Read, reason: ValueReason.Other},
1866
+ lvalueEffect: Effect.Store,
1867
+ valueKind: {
1868
+ kind: ValueKind.Primitive,
1869
+ reason: new Set([ValueReason.Other]),
1870
+ context: new Set(),
1871
+ },
1872
};
1873
break;
1874
}
1877
}
1878
}
1879
2012
- for (const operand of eachInstructionOperand(instr)) {
2013
- CompilerError.invariant(effect != null, {
2014
- reason: `effectKind must be set for instruction value \`${instrValue.kind}\``,
2015
- description: null,
2016
- loc: instrValue.loc,
2017
- suggestions: null,
2018
- });
2019
- state.referenceAndRecordEffects(
2020
- operand,
2021
- effect.kind,
2022
- effect.reason,
2023
- functionEffects,
2024
- );
1880
+ if (continuation.kind === 'initialize') {
1881
+ for (const operand of eachInstructionOperand(instr)) {
1882
+ CompilerError.invariant(continuation.effect != null, {
1883
+ reason: `effectKind must be set for instruction value \`${instrValue.kind}\``,
1884
+ description: null,
1885
+ loc: instrValue.loc,
1886
+ suggestions: null,
1887
+ });
1888
+ state.referenceAndRecordEffects(
1889
+ freezeActions,
1890
+ operand,
1891
+ continuation.effect.kind,
1892
+ continuation.effect.reason,
1893
+ );
1894
+ }
1895
+
1896
+ state.initialize(instrValue, continuation.valueKind);
1897
+ state.define(instr.lvalue, instrValue);
1898
+ instr.lvalue.effect = continuation.lvalueEffect ?? defaultLvalueEffect;
1899
}
1900
2027
- state.initialize(instrValue, valueKind);
2028
- state.define(instr.lvalue, instrValue);
2029
- instr.lvalue.effect = lvalueEffect;
1901
+ functionEffects.push(...inferInstructionFunctionEffects(env, state, instr));
1902
+ freezeActions.forEach(({values, reason}) =>
1903
+ state.freezeValues(values, reason),
1904
+ );
1905
}
1906
1907
+ const terminalFreezeActions: Array<FreezeAction> = [];
1908
for (const operand of eachTerminalOperand(block.terminal)) {
1909
let effect;
1910
if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
1919
} else {
1920
effect = Effect.Read;
1921
}
2046
- const propEffects: Array<FunctionEffect> = [];
1922
state.referenceAndRecordEffects(
1923
+ terminalFreezeActions,
1924
operand,
1925
effect,
1926
ValueReason.Other,
2051
- propEffects,
2052
- );
2053
- functionEffects.push(
2054
- ...propEffects.filter(effect => !isEffectSafeOutsideRender(effect)),
1927
);
1928
}
1929
+ functionEffects.push(...inferTerminalFunctionEffects(state, block));
1930
+ terminalFreezeActions.forEach(({values, reason}) =>
1931
+ state.freezeValues(values, reason),
1932
+ );
1933
}
1934
1935
function hasContextRefOperand(
1965
* @param sig
1966
* @returns Inferred effects of function arguments, or null if inference fails.
1967
*/
2092
-function getFunctionEffects(
1968
+export function getFunctionEffects(
1969
fn: MethodCall | CallExpression,
1970
sig: FunctionSignature,
1971
): Array<Effect> | null {
2040
}
2041
return true;
2042
}
2167
-
2168
-function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
2169
- return effect.kind === 'GlobalMutation';
2170
-}
2171
-
2172
-function getWriteErrorReason(abstractValue: AbstractValue): string {
2173
- if (abstractValue.reason.has(ValueReason.Global)) {
2174
- return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
2175
- } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
2176
- return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
2177
- } else if (abstractValue.reason.has(ValueReason.Context)) {
2178
- return `Mutating a value returned from 'useContext()', which should not be mutated`;
2179
- } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
2180
- return 'Mutating a value returned from a function whose return value should not be mutated';
2181
- } else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
2182
- return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
2183
- } else if (abstractValue.reason.has(ValueReason.State)) {
2184
- return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
2185
- } else if (abstractValue.reason.has(ValueReason.ReducerState)) {
2186
- return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
2187
- } else {
2188
- return 'This mutates a variable that React considers immutable';
2189
- }
2190
-}