@samitouri / QOS-React-2 / commits / afbaa8d3ca

Add a reason to ValueKind for better error messages (#2447)

Jan Kassens committed Dec 15, 2023 at 10:20 UTC afbaa8d3caf2bd2be72b5c5dbe2280808b4c4b20
13 files changed +376 -143
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+27
@@ -942,6 +942,33 @@ export type Identifier = {
942 type: Type;
943 };
944
945 +export type AbstractValue = {
946 + kind: ValueKind;
947 + reason: ReadonlySet<ValueReason>;
948 +};
949 +
950 +/**
951 + * The reason for the kind of a value.
952 + */
953 +export enum ValueReason {
954 + /**
955 + * Defined outside the React function.
956 + */
957 + Global = "global",
958 +
959 + /**
960 + * Used in a JSX expression.
961 + */
962 + JsxCaptured = "jsx-captured",
963 +
964 + /**
965 + * Return value of a function with known frozen return value, e.g. `useState`.
966 + */
967 + KnownReturnSignature = "known-return-signature",
968 +
969 + Other = "other",
970 +}
971 +
972 /*
973 * Distinguish between different kinds of values relevant to inference purposes:
974 * see the main docblock for the module for details.
compiler/packages/babel-plugin-react-forget/src/Inference/InferReferenceEffects.ts
+337 -127
@@ -8,6 +8,7 @@
8 import { CompilerError } from "../CompilerError";
9 import { Environment } from "../HIR";
10 import {
11 + AbstractValue,
12 BasicBlock,
13 BlockId,
14 CallExpression,
@@ -17,13 +18,14 @@ import {
18 IdentifierId,
19 InstructionKind,
20 InstructionValue,
20 - isMutableEffect,
21 - isObjectType,
21 MethodCall,
22 Phi,
23 Place,
24 Type,
25 ValueKind,
26 + ValueReason,
27 + isMutableEffect,
28 + isObjectType,
29 } from "../HIR/HIR";
30 import { FunctionSignature } from "../HIR/ObjectShape";
31 import {
@@ -103,7 +105,10 @@ export default function inferReferenceEffects(
105 loc: fn.loc,
106 value: undefined,
107 };
106 - initialState.initialize(value, ValueKind.Frozen);
108 + initialState.initialize(value, {
109 + kind: ValueKind.Frozen,
110 + reason: new Set([ValueReason.Other]),
111 + });
112
113 for (const ref of fn.context) {
114 // TODO(gsn): This is a hack.
@@ -112,13 +117,22 @@ export default function inferReferenceEffects(
117 properties: [],
118 loc: ref.loc,
119 };
115 - initialState.initialize(value, ValueKind.Context);
120 + initialState.initialize(value, {
121 + kind: ValueKind.Context,
122 + reason: new Set([ValueReason.Other]),
123 + });
124 initialState.define(ref, value);
125 }
126
119 - const paramKind = options.isFunctionExpression
120 - ? ValueKind.Mutable
121 - : ValueKind.Frozen;
127 + const paramKind: AbstractValue = options.isFunctionExpression
128 + ? {
129 + kind: ValueKind.Mutable,
130 + reason: new Set([ValueReason.Other]),
131 + }
132 + : {
133 + kind: ValueKind.Frozen,
134 + reason: new Set([ValueReason.Other]),
135 + };
136 for (const param of fn.params) {
137 let value: InstructionValue;
138 let place: Place;
@@ -194,7 +208,7 @@ class InferenceState {
208 #env: Environment;
209
210 // The kind of reach value, based on its allocation site
197 - #values: Map<InstructionValue, ValueKind>;
211 + #values: Map<InstructionValue, AbstractValue>;
212 /*
213 * The set of values pointed to by each identifier. This is a set
214 * to accomodate phi points (where a variable may have different
@@ -204,7 +218,7 @@ class InferenceState {
218
219 constructor(
220 env: Environment,
207 - values: Map<InstructionValue, ValueKind>,
221 + values: Map<InstructionValue, AbstractValue>,
222 variables: Map<IdentifierId, Set<InstructionValue>>
223 ) {
224 this.#env = env;
@@ -217,7 +231,7 @@ class InferenceState {
231 }
232
233 // (Re)initializes a @param value with its default @param kind.
220 - initialize(value: InstructionValue, kind: ValueKind): void {
234 + initialize(value: InstructionValue, kind: AbstractValue): void {
235 CompilerError.invariant(value.kind !== "LoadLocal", {
236 reason:
237 "Expected all top-level identifiers to be defined as variables, not values",
@@ -240,7 +254,7 @@ class InferenceState {
254 }
255
256 // Lookup the kind of the given @param value.
243 - kind(place: Place): ValueKind {
257 + kind(place: Place): AbstractValue {
258 const values = this.#variables.get(place.identifier.id);
259 CompilerError.invariant(values != null, {
260 reason: `[hoisting] Expected value kind to be initialized`,
@@ -248,10 +262,11 @@ class InferenceState {
262 loc: place.loc,
263 suggestions: null,
264 });
251 - let mergedKind: ValueKind | null = null;
265 + let mergedKind: AbstractValue | null = null;
266 for (const value of values) {
267 const kind = this.#values.get(value)!;
254 - mergedKind = mergedKind !== null ? mergeValues(mergedKind, kind) : kind;
268 + mergedKind =
269 + mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind;
270 }
271 CompilerError.invariant(mergedKind !== null, {
272 reason: `InferReferenceEffects::kind: Expected at least one value`,
@@ -303,7 +318,7 @@ class InferenceState {
318 * Similarly, a freeze reference is converted to readonly if the
319 * value is already frozen or is immutable.
320 */
306 - reference(place: Place, effectKind: Effect): void {
321 + reference(place: Place, effectKind: Effect, reason: ValueReason): void {
322 const values = this.#variables.get(place.identifier.id);
323 if (values === undefined) {
324 CompilerError.invariant(effectKind !== Effect.Store, {
@@ -318,24 +333,31 @@ class InferenceState {
333 : Effect.Read;
334 return;
335 }
321 - let valueKind: ValueKind | null = this.kind(place);
336 + let valueKind: AbstractValue | null = this.kind(place);
337 let effect: Effect | null = null;
338 switch (effectKind) {
339 case Effect.Freeze: {
340 if (
326 - valueKind === ValueKind.Mutable ||
327 - valueKind === ValueKind.Context ||
328 - valueKind === ValueKind.MaybeFrozen
341 + valueKind.kind === ValueKind.Mutable ||
342 + valueKind.kind === ValueKind.Context ||
343 + valueKind.kind === ValueKind.MaybeFrozen
344 ) {
345 + const reasonSet = new Set([reason]);
346 effect = Effect.Freeze;
331 - valueKind = ValueKind.Frozen;
347 + valueKind = {
348 + kind: ValueKind.Frozen,
349 + reason: reasonSet,
350 + };
351 values.forEach((value) => {
333 - this.#values.set(value, ValueKind.Frozen);
352 + this.#values.set(value, {
353 + kind: ValueKind.Frozen,
354 + reason: reasonSet,
355 + });
356
357 if (this.#env.config.enableTransitivelyFreezeFunctionExpressions) {
358 if (value.kind === "FunctionExpression") {
359 for (const operand of eachInstructionValueOperand(value)) {
338 - this.reference(operand, Effect.Freeze);
360 + this.reference(operand, Effect.Freeze, ValueReason.Other);
361 }
362 }
363 }
@@ -347,8 +369,8 @@ class InferenceState {
369 }
370 case Effect.ConditionallyMutate: {
371 if (
350 - valueKind === ValueKind.Mutable ||
351 - valueKind === ValueKind.Context
372 + valueKind.kind === ValueKind.Mutable ||
373 + valueKind.kind === ValueKind.Context
374 ) {
375 effect = Effect.ConditionallyMutate;
376 } else {
@@ -358,13 +380,14 @@ class InferenceState {
380 }
381 case Effect.Mutate: {
382 if (
361 - valueKind === ValueKind.Mutable ||
362 - valueKind === ValueKind.Context
383 + valueKind.kind === ValueKind.Mutable ||
384 + valueKind.kind === ValueKind.Context
385 ) {
386 effect = Effect.Mutate;
387 } else {
388 + let reason = getWriteErrorReason(valueKind);
389 CompilerError.throwInvalidReact({
367 - reason: `This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it`,
390 + reason,
391 description:
392 place.identifier.name !== null
393 ? `Found mutation of ${place.identifier.name}`
@@ -377,11 +400,13 @@ class InferenceState {
400 }
401 case Effect.Store: {
402 if (
380 - valueKind !== ValueKind.Mutable &&
381 - valueKind !== ValueKind.Context
403 + valueKind.kind !== ValueKind.Mutable &&
404 + valueKind.kind !== ValueKind.Context
405 ) {
406 + let reason = getWriteErrorReason(valueKind);
407 +
408 CompilerError.throwInvalidReact({
384 - reason: `This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it`,
409 + reason,
410 description:
411 place.identifier.name !== null
412 ? `Found mutation of ${place.identifier.name}`
@@ -395,7 +420,7 @@ class InferenceState {
420 * TODO(gsn): This should be bailout once we add bailout infra.
421 *
422 * invariant(
398 - * valueKind === ValueKind.Mutable,
423 + * valueKind.kind === ValueKindKind.Mutable,
424 * `expected valueKind to be 'Mutable' but found to be '${valueKind}'`
425 * );
426 */
@@ -404,9 +429,9 @@ class InferenceState {
429 }
430 case Effect.Capture: {
431 if (
407 - valueKind === ValueKind.Immutable ||
408 - valueKind === ValueKind.Frozen ||
409 - valueKind === ValueKind.MaybeFrozen
432 + valueKind.kind === ValueKind.Immutable ||
433 + valueKind.kind === ValueKind.Frozen ||
434 + valueKind.kind === ValueKind.MaybeFrozen
435 ) {
436 effect = Effect.Read;
437 } else {
@@ -456,13 +481,13 @@ class InferenceState {
481 * termination.
482 */
483 merge(other: InferenceState): InferenceState | null {
459 - let nextValues: Map<InstructionValue, ValueKind> | null = null;
484 + let nextValues: Map<InstructionValue, AbstractValue> | null = null;
485 let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
486
487 for (const [id, thisValue] of this.#values) {
488 const otherValue = other.#values.get(id);
489 if (otherValue !== undefined) {
465 - const mergedValue = mergeValues(thisValue, otherValue);
490 + const mergedValue = mergeAbstractValues(thisValue, otherValue);
491 if (mergedValue !== thisValue) {
492 nextValues = nextValues ?? new Map(this.#values);
493 nextValues.set(id, mergedValue);
@@ -658,6 +683,33 @@ function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
683 }
684 }
685
686 +/**
687 + * @returns `true` if `a` is a superset of `b`.
688 + */
689 +function isSuperset<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
690 + for (const v of b) {
691 + if (!a.has(v)) {
692 + return false;
693 + }
694 + }
695 + return true;
696 +}
697 +
698 +function mergeAbstractValues(
699 + a: AbstractValue,
700 + b: AbstractValue
701 +): AbstractValue {
702 + const kind = mergeValues(a.kind, b.kind);
703 + if (kind === a.kind && kind === b.kind && isSuperset(a.reason, b.reason)) {
704 + return a;
705 + }
706 + const reason = new Set(a.reason);
707 + for (const r of b.reason) {
708 + reason.add(r);
709 + }
710 + return { kind, reason };
711 +}
712 +
713 /*
714 * Iterates over the given @param block, defining variables and
715 * recording references on the @param state according to JS semantics.
@@ -673,47 +725,77 @@ function inferBlock(
725
726 for (const instr of block.instructions) {
727 const instrValue = instr.value;
676 - let effectKind: Effect | null = null;
728 + let effect: { kind: Effect; reason: ValueReason } | null = null;
729 let lvalueEffect = Effect.ConditionallyMutate;
678 - let valueKind: ValueKind;
730 + let valueKind: AbstractValue;
731 switch (instrValue.kind) {
732 case "BinaryExpression": {
681 - valueKind = ValueKind.Immutable;
682 - effectKind = Effect.Read;
733 + valueKind = {
734 + kind: ValueKind.Immutable,
735 + reason: new Set([ValueReason.Other]),
736 + };
737 + effect = {
738 + kind: Effect.Read,
739 + reason: ValueReason.Other,
740 + };
741 break;
742 }
743 case "ArrayExpression": {
744 valueKind = hasContextRefOperand(state, instrValue)
687 - ? ValueKind.Context
688 - : ValueKind.Mutable;
689 - effectKind = Effect.Capture;
745 + ? {
746 + kind: ValueKind.Context,
747 + reason: new Set([ValueReason.Other]),
748 + }
749 + : { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]) };
750 + effect = { kind: Effect.Capture, reason: ValueReason.Other };
751 lvalueEffect = Effect.Store;
752 break;
753 }
754 case "NewExpression": {
694 - valueKind = ValueKind.Mutable;
695 - effectKind = Effect.ConditionallyMutate;
755 + valueKind = {
756 + kind: ValueKind.Mutable,
757 + reason: new Set([ValueReason.Other]),
758 + };
759 + effect = {
760 + kind: Effect.ConditionallyMutate,
761 + reason: ValueReason.Other,
762 + };
763 break;
764 }
765 case "ObjectExpression": {
766 valueKind = hasContextRefOperand(state, instrValue)
700 - ? ValueKind.Context
701 - : ValueKind.Mutable;
767 + ? {
768 + kind: ValueKind.Context,
769 + reason: new Set([ValueReason.Other]),
770 + }
771 + : { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]) };
772
773 for (const property of instrValue.properties) {
774 switch (property.kind) {
775 case "ObjectProperty": {
776 if (property.key.kind === "computed") {
777 // Object keys must be primitives, so we know they're frozen at this point
708 - state.reference(property.key.name, Effect.Freeze);
778 + state.reference(
779 + property.key.name,
780 + Effect.Freeze,
781 + ValueReason.Other
782 + );
783 }
784 // Object construction captures but does not modify the key/property values
711 - state.reference(property.place, Effect.Capture);
785 + state.reference(
786 + property.place,
787 + Effect.Capture,
788 + ValueReason.Other
789 + );
790 break;
791 }
792 case "Spread": {
793 // Object construction captures but does not modify the key/property values
716 - state.reference(property.place, Effect.Capture);
794 + state.reference(
795 + property.place,
796 + Effect.Capture,
797 + ValueReason.Other
798 + );
799 break;
800 }
801 default: {
@@ -731,28 +813,49 @@ function inferBlock(
813 continue;
814 }
815 case "UnaryExpression": {
734 - valueKind = ValueKind.Immutable;
735 - effectKind = Effect.Read;
816 + valueKind = {
817 + kind: ValueKind.Immutable,
818 + reason: new Set([ValueReason.Other]),
819 + };
820 + effect = { kind: Effect.Read, reason: ValueReason.Other };
821 break;
822 }
823 case "UnsupportedNode": {
824 // TODO: handle other statement kinds
740 - valueKind = ValueKind.Mutable;
825 + valueKind = {
826 + kind: ValueKind.Mutable,
827 + reason: new Set([ValueReason.Other]),
828 + };
829 break;
830 }
831 case "JsxExpression": {
744 - valueKind = ValueKind.Frozen;
745 - effectKind = Effect.Freeze;
832 + valueKind = {
833 + kind: ValueKind.Frozen,
834 + reason: new Set([ValueReason.Other]),
835 + };
836 + effect = { kind: Effect.Freeze, reason: ValueReason.JsxCaptured };
837 break;
838 }
839 case "JsxFragment": {
749 - valueKind = ValueKind.Frozen;
750 - effectKind = Effect.Freeze;
840 + valueKind = {
841 + kind: ValueKind.Frozen,
842 + reason: new Set([ValueReason.Other]),
843 + };
844 + effect = {
845 + kind: Effect.Freeze,
846 + reason: ValueReason.Other,
847 + };
848 break;
849 }
850 case "TaggedTemplateExpression": {
754 - valueKind = ValueKind.Mutable;
755 - effectKind = Effect.ConditionallyMutate;
851 + valueKind = {
852 + kind: ValueKind.Mutable,
853 + reason: new Set([ValueReason.Other]),
854 + };
855 + effect = {
856 + kind: Effect.ConditionallyMutate,
857 + reason: ValueReason.Other,
858 + };
859 break;
860 }
861 case "TemplateLiteral": {
@@ -760,21 +863,38 @@ function inferBlock(
863 * template literal (with no tag function) always produces
864 * an immutable string
865 */
763 - valueKind = ValueKind.Immutable;
764 - effectKind = Effect.Read;
866 + valueKind = {
867 + kind: ValueKind.Immutable,
868 + reason: new Set([ValueReason.Other]),
869 + };
870 + effect = { kind: Effect.Read, reason: ValueReason.Other };
871 break;
872 }
873 case "RegExpLiteral": {
874 // RegExp instances are mutable objects
769 - valueKind = ValueKind.Mutable;
770 - effectKind = Effect.ConditionallyMutate;
875 + valueKind = {
876 + kind: ValueKind.Mutable,
877 + reason: new Set([ValueReason.Other]),
878 + };
879 + effect = {
880 + kind: Effect.ConditionallyMutate,
881 + reason: ValueReason.Other,
882 + };
883 break;
884 }
773 - case "Debugger":
885 case "LoadGlobal":
886 + valueKind = {
887 + kind: ValueKind.Immutable,
888 + reason: new Set([ValueReason.Global]),
889 + };
890 + break;
891 + case "Debugger":
892 case "JSXText":
893 case "Primitive": {
777 - valueKind = ValueKind.Immutable;
894 + valueKind = {
895 + kind: ValueKind.Immutable,
896 + reason: new Set([ValueReason.Other]),
897 + };
898 break;
899 }
900 case "ObjectMethod":
@@ -783,7 +903,8 @@ function inferBlock(
903 for (const operand of eachInstructionOperand(instr)) {
904 state.reference(
905 operand,
786 - operand.effect === Effect.Unknown ? Effect.Read : operand.effect
906 + operand.effect === Effect.Unknown ? Effect.Read : operand.effect,
907 + ValueReason.Other
908 );
909 hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
910 }
@@ -791,10 +912,10 @@ function inferBlock(
912 * If a closure did not capture any mutable values, then we can consider it to be
913 * frozen, which allows it to be independently memoized.
914 */
794 - state.initialize(
795 - instrValue,
796 - hasMutableOperand ? ValueKind.Mutable : ValueKind.Frozen
797 - );
915 + state.initialize(instrValue, {
916 + kind: hasMutableOperand ? ValueKind.Mutable : ValueKind.Frozen,
917 + reason: new Set([ValueReason.Other]),
918 + });
919 state.define(instr.lvalue, instrValue);
920 instr.lvalue.effect = Effect.Store;
921 continue;
@@ -807,23 +928,40 @@ function inferBlock(
928
929 const effects =
930 signature !== null ? getFunctionEffects(instrValue, signature) : null;
810 - const returnValueKind =
811 - signature !== null ? signature.returnValueKind : ValueKind.Mutable;
931 + const returnValueKind: AbstractValue =
932 + signature !== null
933 + ? {
934 + kind: signature.returnValueKind,
935 + reason: new Set([ValueReason.KnownReturnSignature]),
936 + }
937 + : { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]) };
938 let hasCaptureArgument = false;
939 for (let i = 0; i < instrValue.args.length; i++) {
940 const arg = instrValue.args[i];
941 const place = arg.kind === "Identifier" ? arg : arg.place;
942 if (effects !== null) {
817 - state.reference(place, effects[i]);
943 + state.reference(place, effects[i], ValueReason.Other);
944 } else {
819 - state.reference(place, Effect.ConditionallyMutate);
945 + state.reference(
946 + place,
947 + Effect.ConditionallyMutate,
948 + ValueReason.Other
949 + );
950 }
951 hasCaptureArgument ||= place.effect === Effect.Capture;
952 }
953 if (signature !== null) {
824 - state.reference(instrValue.callee, signature.calleeEffect);
954 + state.reference(
955 + instrValue.callee,
956 + signature.calleeEffect,
957 + ValueReason.Other
958 + );
959 } else {
826 - state.reference(instrValue.callee, Effect.ConditionallyMutate);
960 + state.reference(
961 + instrValue.callee,
962 + Effect.ConditionallyMutate,
963 + ValueReason.Other
964 + );
965 }
966 hasCaptureArgument ||= instrValue.callee.effect === Effect.Capture;
967
@@ -842,13 +980,21 @@ function inferBlock(
980 loc: instrValue.loc,
981 suggestions: null,
982 });
845 - state.reference(instrValue.property, Effect.Read);
983 + state.reference(instrValue.property, Effect.Read, ValueReason.Other);
984
985 const signature = getFunctionCallSignature(
986 env,
987 instrValue.property.identifier.type
988 );
989
990 + const returnValueKind: AbstractValue =
991 + signature !== null
992 + ? {
993 + kind: signature.returnValueKind,
994 + reason: new Set([ValueReason.Other]),
995 + }
996 + : { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]) };
997 +
998 if (
999 signature !== null &&
1000 signature.mutableOnlyIfOperandsAreMutable &&
@@ -860,10 +1006,14 @@ function inferBlock(
1006 */
1007 for (const arg of instrValue.args) {
1008 const place = arg.kind === "Identifier" ? arg : arg.place;
863 - state.reference(place, Effect.Read);
1009 + state.reference(place, Effect.Read, ValueReason.Other);
1010 }
865 - state.reference(instrValue.receiver, Effect.Capture);
866 - state.initialize(instrValue, signature.returnValueKind);
1011 + state.reference(
1012 + instrValue.receiver,
1013 + Effect.Capture,
1014 + ValueReason.Other
1015 + );
1016 + state.initialize(instrValue, returnValueKind);
1017 state.define(instr.lvalue, instrValue);
1018 instr.lvalue.effect =
1019 instrValue.receiver.effect === Effect.Capture
@@ -874,8 +1024,6 @@ function inferBlock(
1024
1025 const effects =
1026 signature !== null ? getFunctionEffects(instrValue, signature) : null;
877 - const returnValueKind =
878 - signature !== null ? signature.returnValueKind : ValueKind.Mutable;
1027 let hasCaptureArgument = false;
1028 for (let i = 0; i < instrValue.args.length; i++) {
1029 const arg = instrValue.args[i];
@@ -885,16 +1033,28 @@ function inferBlock(
1033 * If effects are inferred for an argument, we should fail invalid
1034 * mutating effects
1035 */
888 - state.reference(place, effects[i]);
1036 + state.reference(place, effects[i], ValueReason.Other);
1037 } else {
890 - state.reference(place, Effect.ConditionallyMutate);
1038 + state.reference(
1039 + place,
1040 + Effect.ConditionallyMutate,
1041 + ValueReason.Other
1042 + );
1043 }
1044 hasCaptureArgument ||= place.effect === Effect.Capture;
1045 }
1046 if (signature !== null) {
895 - state.reference(instrValue.receiver, signature.calleeEffect);
1047 + state.reference(
1048 + instrValue.receiver,
1049 + signature.calleeEffect,
1050 + ValueReason.Other
1051 + );
1052 } else {
897 - state.reference(instrValue.receiver, Effect.ConditionallyMutate);
1053 + state.reference(
1054 + instrValue.receiver,
1055 + Effect.ConditionallyMutate,
1056 + ValueReason.Other
1057 + );
1058 }
1059 hasCaptureArgument ||= instrValue.receiver.effect === Effect.Capture;
1060
@@ -907,11 +1067,11 @@ function inferBlock(
1067 }
1068 case "PropertyStore": {
1069 const effect =
910 - state.kind(instrValue.object) === ValueKind.Context
1070 + state.kind(instrValue.object).kind === ValueKind.Context
1071 ? Effect.ConditionallyMutate
1072 : Effect.Capture;
913 - state.reference(instrValue.value, effect);
914 - state.reference(instrValue.object, Effect.Store);
1073 + state.reference(instrValue.value, effect, ValueReason.Other);
1074 + state.reference(instrValue.object, Effect.Store, ValueReason.Other);
1075
1076 const lvalue = instr.lvalue;
1077 state.alias(lvalue, instrValue.value);
@@ -920,12 +1080,15 @@ function inferBlock(
1080 }
1081 case "PropertyDelete": {
1082 // `delete` returns a boolean (immutable) and modifies the object
923 - valueKind = ValueKind.Immutable;
924 - effectKind = Effect.Mutate;
1083 + valueKind = {
1084 + kind: ValueKind.Immutable,
1085 + reason: new Set([ValueReason.Other]),
1086 + };
1087 + effect = { kind: Effect.Mutate, reason: ValueReason.Other };
1088 break;
1089 }
1090 case "PropertyLoad": {
928 - state.reference(instrValue.object, Effect.Read);
1091 + state.reference(instrValue.object, Effect.Read, ValueReason.Other);
1092 const lvalue = instr.lvalue;
1093 lvalue.effect = Effect.ConditionallyMutate;
1094 state.initialize(instrValue, state.kind(instrValue.object));
@@ -934,12 +1097,12 @@ function inferBlock(
1097 }
1098 case "ComputedStore": {
1099 const effect =
937 - state.kind(instrValue.object) === ValueKind.Context
1100 + state.kind(instrValue.object).kind === ValueKind.Context
1101 ? Effect.ConditionallyMutate
1102 : Effect.Capture;
940 - state.reference(instrValue.value, effect);
941 - state.reference(instrValue.property, Effect.Capture);
942 - state.reference(instrValue.object, Effect.Store);
1103 + state.reference(instrValue.value, effect, ValueReason.Other);
1104 + state.reference(instrValue.property, Effect.Capture, ValueReason.Other);
1105 + state.reference(instrValue.object, Effect.Store, ValueReason.Other);
1106
1107 const lvalue = instr.lvalue;
1108 state.alias(lvalue, instrValue.value);
@@ -947,16 +1110,19 @@ function inferBlock(
1110 continue;
1111 }
1112 case "ComputedDelete": {
950 - state.reference(instrValue.object, Effect.Mutate);
951 - state.reference(instrValue.property, Effect.Read);
952 - state.initialize(instrValue, ValueKind.Immutable);
1113 + state.reference(instrValue.object, Effect.Mutate, ValueReason.Other);
1114 + state.reference(instrValue.property, Effect.Read, ValueReason.Other);
1115 + state.initialize(instrValue, {
1116 + kind: ValueKind.Immutable,
1117 + reason: new Set([ValueReason.Other]),
1118 + });
1119 state.define(instr.lvalue, instrValue);
1120 instr.lvalue.effect = Effect.Mutate;
1121 continue;
1122 }
1123 case "ComputedLoad": {
958 - state.reference(instrValue.object, Effect.Read);
959 - state.reference(instrValue.property, Effect.Read);
1124 + state.reference(instrValue.object, Effect.Read, ValueReason.Other);
1125 + state.reference(instrValue.property, Effect.Read, ValueReason.Other);
1126 const lvalue = instr.lvalue;
1127 lvalue.effect = Effect.ConditionallyMutate;
1128 state.initialize(instrValue, state.kind(instrValue.object));
@@ -970,7 +1136,11 @@ function inferBlock(
1136 * It also means that any side-effects which would occur as part of the promise evaluation
1137 * will occur.
1138 */
973 - state.reference(instrValue.value, Effect.ConditionallyMutate);
1139 + state.reference(
1140 + instrValue.value,
1141 + Effect.ConditionallyMutate,
1142 + ValueReason.Other
1143 + );
1144 const lvalue = instr.lvalue;
1145 lvalue.effect = Effect.ConditionallyMutate;
1146 state.alias(lvalue, instrValue.value);
@@ -986,7 +1156,7 @@ function inferBlock(
1156 * ```
1157 */
1158 state.initialize(instrValue, state.kind(instrValue.value));
989 - state.reference(instrValue.value, Effect.Read);
1159 + state.reference(instrValue.value, Effect.Read, ValueReason.Other);
1160 const lvalue = instr.lvalue;
1161 lvalue.effect = Effect.ConditionallyMutate;
1162 state.alias(lvalue, instrValue.value);
@@ -995,22 +1165,24 @@ function inferBlock(
1165 case "LoadLocal": {
1166 const lvalue = instr.lvalue;
1167 const effect =
998 - state.isDefined(lvalue) && state.kind(lvalue) === ValueKind.Context
1168 + state.isDefined(lvalue) &&
1169 + state.kind(lvalue).kind === ValueKind.Context
1170 ? Effect.ConditionallyMutate
1171 : Effect.Capture;
1001 - state.reference(instrValue.place, effect);
1172 + state.reference(instrValue.place, effect, ValueReason.Other);
1173 lvalue.effect = Effect.ConditionallyMutate;
1174 // direct aliasing: `a = b`;
1175 state.alias(lvalue, instrValue.place);
1176 continue;
1177 }
1178 case "LoadContext": {
1008 - state.reference(instrValue.place, Effect.Capture);
1179 + state.reference(instrValue.place, Effect.Capture, ValueReason.Other);
1180 const lvalue = instr.lvalue;
1181 lvalue.effect = Effect.ConditionallyMutate;
1182 const valueKind = state.kind(instrValue.place);
1183 CompilerError.invariant(
1013 - valueKind === ValueKind.Mutable || valueKind === ValueKind.Context,
1184 + valueKind.kind === ValueKind.Mutable ||
1185 + valueKind.kind === ValueKind.Context,
1186 {
1187 reason:
1188 "[InferReferenceEffects] Context variables are always mutable.",
@@ -1029,14 +1201,23 @@ function inferBlock(
1201 value,
1202 // Catch params may be aliased to mutable values
1203 instrValue.lvalue.kind === InstructionKind.Catch
1032 - ? ValueKind.Mutable
1033 - : ValueKind.Immutable
1204 + ? {
1205 + kind: ValueKind.Mutable,
1206 + reason: new Set([ValueReason.Other]),
1207 + }
1208 + : {
1209 + kind: ValueKind.Immutable,
1210 + reason: new Set([ValueReason.Other]),
1211 + }
1212 );
1213 state.define(instrValue.lvalue.place, value);
1214 continue;
1215 }
1216 case "DeclareContext": {
1039 - state.initialize(instrValue, ValueKind.Mutable);
1217 + state.initialize(instrValue, {
1218 + kind: ValueKind.Mutable,
1219 + reason: new Set([ValueReason.Other]),
1220 + });
1221 state.define(instrValue.lvalue.place, instrValue);
1222 continue;
1223 }
@@ -1044,10 +1225,10 @@ function inferBlock(
1225 case "PrefixUpdate": {
1226 const effect =
1227 state.isDefined(instrValue.lvalue) &&
1047 - state.kind(instrValue.lvalue) === ValueKind.Context
1228 + state.kind(instrValue.lvalue).kind === ValueKind.Context
1229 ? Effect.ConditionallyMutate
1230 : Effect.Capture;
1050 - state.reference(instrValue.value, effect);
1231 + state.reference(instrValue.value, effect, ValueReason.Other);
1232
1233 const lvalue = instr.lvalue;
1234 state.alias(lvalue, instrValue.value);
@@ -1065,10 +1246,10 @@ function inferBlock(
1246 case "StoreLocal": {
1247 const effect =
1248 state.isDefined(instrValue.lvalue.place) &&
1068 - state.kind(instrValue.lvalue.place) === ValueKind.Context
1249 + state.kind(instrValue.lvalue.place).kind === ValueKind.Context
1250 ? Effect.ConditionallyMutate
1251 : Effect.Capture;
1071 - state.reference(instrValue.value, effect);
1252 + state.reference(instrValue.value, effect, ValueReason.Other);
1253
1254 const lvalue = instr.lvalue;
1255 state.alias(lvalue, instrValue.value);
@@ -1084,8 +1265,16 @@ function inferBlock(
1265 continue;
1266 }
1267 case "StoreContext": {
1087 - state.reference(instrValue.value, Effect.ConditionallyMutate);
1088 - state.reference(instrValue.lvalue.place, Effect.Mutate);
1268 + state.reference(
1269 + instrValue.value,
1270 + Effect.ConditionallyMutate,
1271 + ValueReason.Other
1272 + );
1273 + state.reference(
1274 + instrValue.lvalue.place,
1275 + Effect.Mutate,
1276 + ValueReason.Other
1277 + );
1278
1279 const lvalue = instr.lvalue;
1280 state.alias(lvalue, instrValue.value);
@@ -1097,13 +1286,13 @@ function inferBlock(
1286 for (const place of eachPatternOperand(instrValue.lvalue.pattern)) {
1287 if (
1288 state.isDefined(place) &&
1100 - state.kind(place) === ValueKind.Context
1289 + state.kind(place).kind === ValueKind.Context
1290 ) {
1291 effect = Effect.ConditionallyMutate;
1292 break;
1293 }
1294 }
1106 - state.reference(instrValue.value, effect);
1295 + state.reference(instrValue.value, effect, ValueReason.Other);
1296
1297 const lvalue = instr.lvalue;
1298 state.alias(lvalue, instrValue.value);
@@ -1121,15 +1310,21 @@ function inferBlock(
1310 continue;
1311 }
1312 case "NextIterableOf": {
1124 - effectKind = Effect.Capture;
1313 + effect = { kind: Effect.Capture, reason: ValueReason.Other };
1314 lvalueEffect = Effect.Store;
1126 - valueKind = ValueKind.Mutable;
1315 + valueKind = {
1316 + kind: ValueKind.Mutable,
1317 + reason: new Set([ValueReason.Other]),
1318 + };
1319 break;
1320 }
1321 case "NextPropertyOf": {
1130 - effectKind = Effect.Read;
1322 + effect = { kind: Effect.Read, reason: ValueReason.Other };
1323 lvalueEffect = Effect.Store;
1132 - valueKind = ValueKind.Immutable;
1324 + valueKind = {
1325 + kind: ValueKind.Immutable,
1326 + reason: new Set([ValueReason.Other]),
1327 + };
1328 break;
1329 }
1330 default: {
@@ -1138,13 +1333,13 @@ function inferBlock(
1333 }
1334
1335 for (const operand of eachInstructionOperand(instr)) {
1141 - CompilerError.invariant(effectKind != null, {
1336 + CompilerError.invariant(effect != null, {
1337 reason: `effectKind must be set for instruction value \`${instrValue.kind}\``,
1338 description: null,
1339 loc: instrValue.loc,
1340 suggestions: null,
1341 });
1147 - state.reference(operand, effectKind);
1342 + state.reference(operand, effect.kind, effect.reason);
1343 }
1344
1345 state.initialize(instrValue, valueKind);
@@ -1157,7 +1352,7 @@ function inferBlock(
1352 if (block.terminal.kind === "return" || block.terminal.kind === "throw") {
1353 if (
1354 state.isDefined(operand) &&
1160 - state.kind(operand) === ValueKind.Context
1355 + state.kind(operand).kind === ValueKind.Context
1356 ) {
1357 effect = Effect.ConditionallyMutate;
1358 } else {
@@ -1166,7 +1361,7 @@ function inferBlock(
1361 } else {
1362 effect = Effect.Read;
1363 }
1169 - state.reference(operand, effect);
1364 + state.reference(operand, effect, ValueReason.Other);
1365 }
1366 }
1367
@@ -1175,7 +1370,10 @@ function hasContextRefOperand(
1370 instrValue: InstructionValue
1371 ): boolean {
1372 for (const place of eachInstructionValueOperand(instrValue)) {
1178 - if (state.isDefined(place) && state.kind(place) === ValueKind.Context) {
1373 + if (
1374 + state.isDefined(place) &&
1375 + state.kind(place).kind === ValueKind.Context
1376 + ) {
1377 return true;
1378 }
1379 }
@@ -1242,7 +1440,7 @@ function areArgumentsImmutableAndNonMutating(
1440 ): boolean {
1441 for (const arg of args) {
1442 const place = arg.kind === "Identifier" ? arg : arg.place;
1245 - const kind = state.kind(place);
1443 + const kind = state.kind(place).kind;
1444 switch (kind) {
1445 case ValueKind.Immutable:
1446 case ValueKind.Frozen: {
@@ -1274,3 +1472,15 @@ function areArgumentsImmutableAndNonMutating(
1472 }
1473 return true;
1474 }
1475 +
1476 +function getWriteErrorReason(abstractValue: AbstractValue): string {
1477 + if (abstractValue.reason.has(ValueReason.Global)) {
1478 + return "Writing to a variable defined outside a component or hook is not allowed. Consider using an effect.";
1479 + } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
1480 + return "Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX.";
1481 + } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
1482 + return "Mutating a value returned from a function that should not be mutated.";
1483 + } else {
1484 + return "This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it.";
1485 + }
1486 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/envConfig-test.ts
+2 -6
@@ -5,12 +5,8 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {
9 - CompilerError,
10 - Effect,
11 - ValueKind,
12 - validateEnvironmentConfig,
13 -} from "..";
8 +import { Effect, validateEnvironmentConfig } from "..";
9 +import { ValueKind } from "../HIR";
10
11 describe("parseConfigPragma()", () => {
12 it("passing null throws", () => {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md
+1 -1
@@ -15,7 +15,7 @@ function Component(props) {
15 ## Error
16
17 ```
18 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (4:4)
18 +[ReactForget] InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. (4:4)
19 ```
20
21
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md
+1 -1
@@ -16,7 +16,7 @@ function Component(props) {
16 ## Error
17
18 ```
19 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (5:5)
19 +[ReactForget] InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. (5:5)
20 ```
21
22
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md
+1 -1
@@ -16,7 +16,7 @@ function Component(props) {
16 ## Error
17
18 ```
19 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (5:5)
19 +[ReactForget] InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. (5:5)
20 ```
21
22
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md
+1 -1
@@ -16,7 +16,7 @@ function Component(props) {
16 ## Error
17
18 ```
19 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (5:5)
19 +[ReactForget] InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. (5:5)
20 ```
21
22
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-function-expression-mutates-immutable-value.expect.md
+1 -1
@@ -18,7 +18,7 @@ function Component(props) {
18 ## Error
19
20 ```
21 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (5:5)
21 +[ReactForget] InvalidReact: Mutating a value returned from a function that should not be mutated. (5:5)
22 ```
23
24
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-mutate-after-aliased-freeze.expect.md
+1 -1
@@ -25,7 +25,7 @@ function Component(props) {
25 ## Error
26
27 ```
28 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (13:13)
28 +[ReactForget] InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. (13:13)
29 ```
30
31
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-mutate-after-freeze.expect.md
+1 -1
@@ -19,7 +19,7 @@ function Component(props) {
19 ## Error
20
21 ```
22 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (7:7)
22 +[ReactForget] InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. (7:7)
23 ```
24
25
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-property-store-to-frozen-value.expect.md
+1 -1
@@ -16,7 +16,7 @@ function Component(props) {
16 ## Error
17
18 ```
19 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (5:5)
19 +[ReactForget] InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. (5:5)
20 ```
21
22
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.mutate-property-from-global.expect.md
+1 -1
@@ -15,7 +15,7 @@ function Foo() {
15 ## Error
16
17 ```
18 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (4:4)
18 +[ReactForget] InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect. (4:4)
19 ```
20
21
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.store-property-in-global.expect.md
+1 -1
@@ -15,7 +15,7 @@ function Foo() {
15 ## Error
16
17 ```
18 -[ReactForget] InvalidReact: This mutates a global or a variable after it was passed to React, which means that React cannot observe changes to it (4:4)
18 +[ReactForget] InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect. (4:4)
19 ```
20
21
\ No newline at end of file