1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {
9
+ CompilerError,
10
+ CompilerErrorDetail,
11
+ ErrorSeverity,
12
+} from "../CompilerError";
13
+import {
14
+ Effect,
15
+ FunctionExpression,
16
+ HIRFunction,
17
+ IdentifierId,
18
+ isMutableEffect,
19
+ isRefValueType,
20
+ isUseRefType,
21
+} from "./HIR";
22
+import { eachInstructionValueOperand } from "./visitors";
23
+
24
+/**
25
+ * Various APIs in React take ownership of the values passed to them, such that it is invalid
26
+ * to subsequently modify those values. Examples include:
27
+ * - Passing a value as a prop to JSX. Subsequently mutating this value will result in undefined
28
+ * behavior, since the mutation may or may not be observed depending on when the child re-renders.
29
+ * In addition, the value may be used as an input to memoization in children, and mutation could
30
+ * invalidate that memoization.
31
+ * - Passing a value to `useState()`, for the same reason.
32
+ * - Passing a value to a hook, for the same reason.
33
+ *
34
+ * Most "normal" data types (objects, arrays, etc) can be "frozen" when passed to a React API simply
35
+ * by not calling any mutating methods on them. However, mutable lambdas are an exception: if a lambda
36
+ * has side-effects, there is no way to do something to the lambda that would allow calling it without
37
+ * triggering those side effects. The only thing a developer could do is not call the lambda, but developers
38
+ * also have no way of knowing that they can't call the lambda.
39
+ *
40
+ * From a type system perspective, the above APIs that "take ownership" of their values really accept
41
+ * *already frozen* values as input. Thus it is invalid to pass a value that cannot be frozen to these APIs,
42
+ * and it is therefore invalid to pass a mutable lambda.
43
+ *
44
+ * This pass validates the above rule. Note that this validation can by bypassed by storing a mutable lambda
45
+ * inside some value (eg as an array element or object property). In these cases we trust that the developer
46
+ * is not breaking the rules. The goal of this validation is to find cases that are provably wrong and help
47
+ * the developer fix the mistake earlier.
48
+ */
49
+export function validateFrozenLambdas(fn: HIRFunction): void {
50
+ const lambdas = new Map<IdentifierId, FunctionExpression>();
51
+ const temporaries = new Map<IdentifierId, IdentifierId>();
52
+
53
+ const errors = new CompilerError();
54
+ for (const [, block] of fn.body.blocks) {
55
+ for (const instr of block.instructions) {
56
+ if (instr.value.kind === "FunctionExpression") {
57
+ lambdas.set(instr.lvalue.identifier.id, instr.value);
58
+ } else if (instr.value.kind === "LoadLocal") {
59
+ const resolvedId =
60
+ temporaries.get(instr.value.place.identifier.id) ??
61
+ instr.value.place.identifier.id;
62
+ temporaries.set(instr.lvalue.identifier.id, resolvedId);
63
+ } else if (instr.value.kind === "StoreLocal") {
64
+ const resolvedId =
65
+ temporaries.get(instr.value.value.identifier.id) ??
66
+ instr.value.value.identifier.id;
67
+ temporaries.set(instr.value.lvalue.place.identifier.id, resolvedId);
68
+ } else {
69
+ for (const operand of eachInstructionValueOperand(instr.value)) {
70
+ if (operand.effect === Effect.Freeze) {
71
+ const operandId =
72
+ temporaries.get(operand.identifier.id) ?? operand.identifier.id;
73
+ const lambda = lambdas.get(operandId);
74
+ if (
75
+ lambda !== undefined &&
76
+ lambda.dependencies.some(
77
+ (place) =>
78
+ isMutableEffect(place.effect, place.loc) &&
79
+ !isRefValueType(place.identifier) &&
80
+ !isUseRefType(place.identifier)
81
+ )
82
+ ) {
83
+ errors.pushErrorDetail(
84
+ new CompilerErrorDetail({
85
+ codeframe: null,
86
+ description: null,
87
+ loc: typeof operand.loc !== "symbol" ? operand.loc : null,
88
+ reason:
89
+ "Cannot use a mutable function where an immutable value is expected",
90
+ severity: ErrorSeverity.InvalidInput,
91
+ })
92
+ );
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ if (errors.hasErrors()) {
100
+ throw errors;
101
+ }
102
+}