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
+ Effect,
11
+ ErrorSeverity,
12
+ SourceLocation,
13
+ ValueKind,
14
+} from '..';
15
+import {
16
+ BasicBlock,
17
+ BlockId,
18
+ DeclarationId,
19
+ Environment,
20
+ FunctionExpression,
21
+ HIRFunction,
22
+ Hole,
23
+ IdentifierId,
24
+ Instruction,
25
+ InstructionKind,
26
+ InstructionValue,
27
+ isArrayType,
28
+ isMapType,
29
+ isPrimitiveType,
30
+ isRefOrRefValue,
31
+ isSetType,
32
+ makeIdentifierId,
33
+ Phi,
34
+ Place,
35
+ SpreadPattern,
36
+ ValueReason,
37
+} from '../HIR';
38
+import {
39
+ eachInstructionValueLValue,
40
+ eachInstructionValueOperand,
41
+ eachTerminalSuccessor,
42
+} from '../HIR/visitors';
43
+import {Ok, Result} from '../Utils/Result';
44
+import {
45
+ getArgumentEffect,
46
+ getFunctionCallSignature,
47
+ isKnownMutableEffect,
48
+ mergeValueKinds,
49
+} from './InferReferenceEffects';
50
+import {
51
+ assertExhaustive,
52
+ getOrInsertWith,
53
+ Set_isSuperset,
54
+} from '../Utils/utils';
55
+import {
56
+ printAliasingEffect,
57
+ printAliasingSignature,
58
+ printIdentifier,
59
+ printInstruction,
60
+ printInstructionValue,
61
+ printPlace,
62
+ printSourceLocation,
63
+} from '../HIR/PrintHIR';
64
+import {FunctionSignature} from '../HIR/ObjectShape';
65
+import {getWriteErrorReason} from './InferFunctionEffects';
66
+import prettyFormat from 'pretty-format';
67
+import {createTemporaryPlace} from '../HIR/HIRBuilder';
68
+import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
69
+
70
+const DEBUG = false;
71
+
72
+/**
73
+ * Infers the mutation/aliasing effects for instructions and terminals and annotates
74
+ * them on the HIR, making the effects of builtin instructions/functions as well as
75
+ * user-defined functions explicit. These effects then form the basis for subsequent
76
+ * analysis to determine the mutable range of each value in the program — the set of
77
+ * instructions over which the value is created and mutated — as well as validation
78
+ * against invalid code.
79
+ *
80
+ * At a high level the approach is:
81
+ * - Determine a set of candidate effects based purely on the syntax of the instruction
82
+ * and the types involved. These candidate effects are cached the first time each
83
+ * instruction is visited. The idea is to reason about the semantics of the instruction
84
+ * or function in isolation, separately from how those effects may interact with later
85
+ * abstract interpretation.
86
+ * - Then we do abstract interpretation over the HIR, iterating until reaching a fixpoint.
87
+ * This phase tracks the abstract kind of each value (mutable, primitive, frozen, etc)
88
+ * and the set of values pointed to by each identifier. Each candidate effect is "applied"
89
+ * to the current abtract state, and effects may be dropped or rewritten accordingly.
90
+ * For example, a "MutateConditionally <x>" effect may be dropped if x is not a mutable
91
+ * value. A "Mutate <y>" effect may get converted into a "MutateFrozen <error>" effect
92
+ * if y is mutable, etc.
93
+ */
94
+export function inferMutationAliasingEffects(
95
+ fn: HIRFunction,
96
+ {isFunctionExpression}: {isFunctionExpression: boolean} = {
97
+ isFunctionExpression: false,
98
+ },
99
+): Result<void, CompilerError> {
100
+ const initialState = InferenceState.empty(fn.env, isFunctionExpression);
101
+
102
+ // Map of blocks to the last (merged) incoming state that was processed
103
+ const statesByBlock: Map<BlockId, InferenceState> = new Map();
104
+
105
+ for (const ref of fn.context) {
106
+ // TODO: using InstructionValue as a bit of a hack, but it's pragmatic
107
+ const value: InstructionValue = {
108
+ kind: 'ObjectExpression',
109
+ properties: [],
110
+ loc: ref.loc,
111
+ };
112
+ initialState.initialize(value, {
113
+ kind: ValueKind.Context,
114
+ reason: new Set([ValueReason.Other]),
115
+ });
116
+ initialState.define(ref, value);
117
+ }
118
+
119
+ const paramKind: AbstractValue = isFunctionExpression
120
+ ? {
121
+ kind: ValueKind.Mutable,
122
+ reason: new Set([ValueReason.Other]),
123
+ }
124
+ : {
125
+ kind: ValueKind.Frozen,
126
+ reason: new Set([ValueReason.ReactiveFunctionArgument]),
127
+ };
128
+
129
+ if (fn.fnType === 'Component') {
130
+ CompilerError.invariant(fn.params.length <= 2, {
131
+ reason:
132
+ 'Expected React component to have not more than two parameters: one for props and for ref',
133
+ description: null,
134
+ loc: fn.loc,
135
+ suggestions: null,
136
+ });
137
+ const [props, ref] = fn.params;
138
+ if (props != null) {
139
+ inferParam(props, initialState, paramKind);
140
+ }
141
+ if (ref != null) {
142
+ const place = ref.kind === 'Identifier' ? ref : ref.place;
143
+ const value: InstructionValue = {
144
+ kind: 'ObjectExpression',
145
+ properties: [],
146
+ loc: place.loc,
147
+ };
148
+ initialState.initialize(value, {
149
+ kind: ValueKind.Mutable,
150
+ reason: new Set([ValueReason.Other]),
151
+ });
152
+ initialState.define(place, value);
153
+ }
154
+ } else {
155
+ for (const param of fn.params) {
156
+ inferParam(param, initialState, paramKind);
157
+ }
158
+ }
159
+
160
+ /*
161
+ * Multiple predecessors may be visited prior to reaching a given successor,
162
+ * so track the list of incoming state for each successor block.
163
+ * These are merged when reaching that block again.
164
+ */
165
+ const queuedStates: Map<BlockId, InferenceState> = new Map();
166
+ function queue(blockId: BlockId, state: InferenceState): void {
167
+ let queuedState = queuedStates.get(blockId);
168
+ if (queuedState != null) {
169
+ // merge the queued states for this block
170
+ state = queuedState.merge(state) ?? queuedState;
171
+ queuedStates.set(blockId, state);
172
+ } else {
173
+ /*
174
+ * this is the first queued state for this block, see whether
175
+ * there are changed relative to the last time it was processed.
176
+ */
177
+ const prevState = statesByBlock.get(blockId);
178
+ const nextState = prevState != null ? prevState.merge(state) : state;
179
+ if (nextState != null) {
180
+ queuedStates.set(blockId, nextState);
181
+ }
182
+ }
183
+ }
184
+ queue(fn.body.entry, initialState);
185
+
186
+ const hoistedContextDeclarations = findHoistedContextDeclarations(fn);
187
+
188
+ const context = new Context(
189
+ isFunctionExpression,
190
+ fn,
191
+ hoistedContextDeclarations,
192
+ );
193
+
194
+ let count = 0;
195
+ while (queuedStates.size !== 0) {
196
+ count++;
197
+ if (count > 1000) {
198
+ console.log(
199
+ 'oops infinite loop',
200
+ fn.id,
201
+ typeof fn.loc !== 'symbol' ? fn.loc?.filename : null,
202
+ );
203
+ throw new Error('infinite loop');
204
+ }
205
+ for (const [blockId, block] of fn.body.blocks) {
206
+ const incomingState = queuedStates.get(blockId);
207
+ queuedStates.delete(blockId);
208
+ if (incomingState == null) {
209
+ continue;
210
+ }
211
+
212
+ statesByBlock.set(blockId, incomingState);
213
+ const state = incomingState.clone();
214
+ inferBlock(context, state, block);
215
+
216
+ for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
217
+ queue(nextBlockId, state);
218
+ }
219
+ }
220
+ }
221
+ return Ok(undefined);
222
+}
223
+
224
+function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
225
+ const hoisted = new Set<DeclarationId>();
226
+ for (const block of fn.body.blocks.values()) {
227
+ for (const instr of block.instructions) {
228
+ if (instr.value.kind === 'DeclareContext') {
229
+ const kind = instr.value.lvalue.kind;
230
+ if (
231
+ kind == InstructionKind.HoistedConst ||
232
+ kind == InstructionKind.HoistedFunction ||
233
+ kind == InstructionKind.HoistedLet
234
+ ) {
235
+ hoisted.add(instr.value.lvalue.place.identifier.declarationId);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return hoisted;
241
+}
242
+
243
+class Context {
244
+ internedEffects: Map<string, AliasingEffect> = new Map();
245
+ instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
246
+ effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
247
+ new Map();
248
+ catchHandlers: Map<BlockId, Place> = new Map();
249
+ isFuctionExpression: boolean;
250
+ fn: HIRFunction;
251
+ hoistedContextDeclarations: Set<DeclarationId>;
252
+
253
+ constructor(
254
+ isFunctionExpression: boolean,
255
+ fn: HIRFunction,
256
+ hoistedContextDeclarations: Set<DeclarationId>,
257
+ ) {
258
+ this.isFuctionExpression = isFunctionExpression;
259
+ this.fn = fn;
260
+ this.hoistedContextDeclarations = hoistedContextDeclarations;
261
+ }
262
+
263
+ internEffect(effect: AliasingEffect): AliasingEffect {
264
+ const hash = hashEffect(effect);
265
+ let interned = this.internedEffects.get(hash);
266
+ if (interned == null) {
267
+ this.internedEffects.set(hash, effect);
268
+ interned = effect;
269
+ }
270
+ return interned;
271
+ }
272
+}
273
+
274
+function inferParam(
275
+ param: Place | SpreadPattern,
276
+ initialState: InferenceState,
277
+ paramKind: AbstractValue,
278
+): void {
279
+ const place = param.kind === 'Identifier' ? param : param.place;
280
+ const value: InstructionValue = {
281
+ kind: 'Primitive',
282
+ loc: place.loc,
283
+ value: undefined,
284
+ };
285
+ initialState.initialize(value, paramKind);
286
+ initialState.define(place, value);
287
+}
288
+
289
+function inferBlock(
290
+ context: Context,
291
+ state: InferenceState,
292
+ block: BasicBlock,
293
+): void {
294
+ for (const phi of block.phis) {
295
+ state.inferPhi(phi);
296
+ }
297
+
298
+ for (const instr of block.instructions) {
299
+ let instructionSignature = context.instructionSignatureCache.get(instr);
300
+ if (instructionSignature == null) {
301
+ instructionSignature = computeSignatureForInstruction(
302
+ context,
303
+ state.env,
304
+ instr,
305
+ );
306
+ context.instructionSignatureCache.set(instr, instructionSignature);
307
+ }
308
+ const effects = applySignature(context, state, instructionSignature, instr);
309
+ instr.effects = effects;
310
+ }
311
+ const terminal = block.terminal;
312
+ if (terminal.kind === 'try' && terminal.handlerBinding != null) {
313
+ context.catchHandlers.set(terminal.handler, terminal.handlerBinding);
314
+ } else if (terminal.kind === 'maybe-throw') {
315
+ const handlerParam = context.catchHandlers.get(terminal.handler);
316
+ if (handlerParam != null) {
317
+ const effects: Array<AliasingEffect> = [];
318
+ for (const instr of block.instructions) {
319
+ if (
320
+ instr.value.kind === 'CallExpression' ||
321
+ instr.value.kind === 'MethodCall'
322
+ ) {
323
+ /**
324
+ * Many instructions can error, but only calls can throw their result as the error
325
+ * itself. For example, `c = a.b` can throw if `a` is nullish, but the thrown value
326
+ * is an error object synthesized by the JS runtime. Whereas `throwsInput(x)` can
327
+ * throw (effectively) the result of the call.
328
+ *
329
+ * TODO: call applyEffect() instead. This meant that the catch param wasn't inferred
330
+ * as a mutable value, though. See `try-catch-try-value-modified-in-catch-escaping.js`
331
+ * fixture as an example
332
+ */
333
+ state.appendAlias(handlerParam, instr.lvalue);
334
+ const kind = state.kind(instr.lvalue).kind;
335
+ if (kind === ValueKind.Mutable || kind == ValueKind.Context) {
336
+ effects.push({
337
+ kind: 'Alias',
338
+ from: instr.lvalue,
339
+ into: handlerParam,
340
+ });
341
+ }
342
+ }
343
+ }
344
+ terminal.effects = effects.length !== 0 ? effects : null;
345
+ }
346
+ } else if (terminal.kind === 'return') {
347
+ if (!context.isFuctionExpression) {
348
+ terminal.effects = [
349
+ {
350
+ kind: 'Freeze',
351
+ value: terminal.value,
352
+ reason: ValueReason.JsxCaptured,
353
+ },
354
+ ];
355
+ }
356
+ }
357
+}
358
+
359
+/**
360
+ * Applies the signature to the given state to determine the precise set of effects
361
+ * that will occur in practice. This takes into account the inferred state of each
362
+ * variable. For example, the signature may have a `ConditionallyMutate x` effect.
363
+ * Here, we check the abstract type of `x` and either record a `Mutate x` if x is mutable
364
+ * or no effect if x is a primitive, global, or frozen.
365
+ *
366
+ * This phase may also emit errors, for example MutateLocal on a frozen value is invalid.
367
+ */
368
+function applySignature(
369
+ context: Context,
370
+ state: InferenceState,
371
+ signature: InstructionSignature,
372
+ instruction: Instruction,
373
+): Array<AliasingEffect> | null {
374
+ const effects: Array<AliasingEffect> = [];
375
+ /**
376
+ * For function instructions, eagerly validate that they aren't mutating
377
+ * a known-frozen value.
378
+ *
379
+ * TODO: make sure we're also validating against global mutations somewhere, but
380
+ * account for this being allowed in effects/event handlers.
381
+ */
382
+ if (
383
+ instruction.value.kind === 'FunctionExpression' ||
384
+ instruction.value.kind === 'ObjectMethod'
385
+ ) {
386
+ const aliasingEffects =
387
+ instruction.value.loweredFunc.func.aliasingEffects ?? [];
388
+ const context = new Set(
389
+ instruction.value.loweredFunc.func.context.map(p => p.identifier.id),
390
+ );
391
+ for (const effect of aliasingEffects) {
392
+ if (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') {
393
+ if (!context.has(effect.value.identifier.id)) {
394
+ continue;
395
+ }
396
+ const value = state.kind(effect.value);
397
+ switch (value.kind) {
398
+ case ValueKind.Frozen: {
399
+ const reason = getWriteErrorReason({
400
+ kind: value.kind,
401
+ reason: value.reason,
402
+ context: new Set(),
403
+ });
404
+ effects.push({
405
+ kind: 'MutateFrozen',
406
+ place: effect.value,
407
+ error: {
408
+ severity: ErrorSeverity.InvalidReact,
409
+ reason,
410
+ description:
411
+ effect.value.identifier.name !== null &&
412
+ effect.value.identifier.name.kind === 'named'
413
+ ? `Found mutation of \`${effect.value.identifier.name.value}\``
414
+ : null,
415
+ loc: effect.value.loc,
416
+ suggestions: null,
417
+ },
418
+ });
419
+ }
420
+ }
421
+ }
422
+ }
423
+ }
424
+
425
+ /*
426
+ * Track which values we've already aliased once, so that we can switch to
427
+ * appendAlias() for subsequent aliases into the same value
428
+ */
429
+ const aliased = new Set<IdentifierId>();
430
+
431
+ if (DEBUG) {
432
+ console.log(printInstruction(instruction));
433
+ }
434
+
435
+ for (const effect of signature.effects) {
436
+ applyEffect(context, state, effect, aliased, effects);
437
+ }
438
+ if (DEBUG) {
439
+ console.log(
440
+ prettyFormat(state.debugAbstractValue(state.kind(instruction.lvalue))),
441
+ );
442
+ console.log(
443
+ effects.map(effect => ` ${printAliasingEffect(effect)}`).join('\n'),
444
+ );
445
+ }
446
+ if (
447
+ !(state.isDefined(instruction.lvalue) && state.kind(instruction.lvalue))
448
+ ) {
449
+ CompilerError.invariant(false, {
450
+ reason: `Expected instruction lvalue to be initialized`,
451
+ loc: instruction.loc,
452
+ });
453
+ }
454
+ return effects.length !== 0 ? effects : null;
455
+}
456
+
457
+function applyEffect(
458
+ context: Context,
459
+ state: InferenceState,
460
+ _effect: AliasingEffect,
461
+ aliased: Set<IdentifierId>,
462
+ effects: Array<AliasingEffect>,
463
+): void {
464
+ const effect = context.internEffect(_effect);
465
+ if (DEBUG) {
466
+ console.log(printAliasingEffect(effect));
467
+ }
468
+ switch (effect.kind) {
469
+ case 'Freeze': {
470
+ const didFreeze = state.freeze(effect.value, effect.reason);
471
+ if (didFreeze) {
472
+ effects.push(effect);
473
+ }
474
+ break;
475
+ }
476
+ case 'Create': {
477
+ let value = context.effectInstructionValueCache.get(effect);
478
+ if (value == null) {
479
+ value = {
480
+ kind: 'ObjectExpression',
481
+ properties: [],
482
+ loc: effect.into.loc,
483
+ };
484
+ context.effectInstructionValueCache.set(effect, value);
485
+ }
486
+ state.initialize(value, {
487
+ kind: effect.value,
488
+ reason: new Set([effect.reason]),
489
+ });
490
+ state.define(effect.into, value);
491
+ break;
492
+ }
493
+ case 'ImmutableCapture': {
494
+ const kind = state.kind(effect.from).kind;
495
+ switch (kind) {
496
+ case ValueKind.Global:
497
+ case ValueKind.Primitive: {
498
+ // no-op: we don't need to track data flow for copy types
499
+ break;
500
+ }
501
+ default: {
502
+ effects.push(effect);
503
+ }
504
+ }
505
+ break;
506
+ }
507
+ case 'CreateFrom': {
508
+ const fromValue = state.kind(effect.from);
509
+ let value = context.effectInstructionValueCache.get(effect);
510
+ if (value == null) {
511
+ value = {
512
+ kind: 'ObjectExpression',
513
+ properties: [],
514
+ loc: effect.into.loc,
515
+ };
516
+ context.effectInstructionValueCache.set(effect, value);
517
+ }
518
+ state.initialize(value, {
519
+ kind: fromValue.kind,
520
+ reason: new Set(fromValue.reason),
521
+ });
522
+ state.define(effect.into, value);
523
+ switch (fromValue.kind) {
524
+ case ValueKind.Primitive:
525
+ case ValueKind.Global: {
526
+ // no need to track this data flow
527
+ break;
528
+ }
529
+ case ValueKind.Frozen: {
530
+ effects.push({
531
+ kind: 'ImmutableCapture',
532
+ from: effect.from,
533
+ into: effect.into,
534
+ });
535
+ break;
536
+ }
537
+ default: {
538
+ effects.push({
539
+ // OK: recording information flow
540
+ kind: 'CreateFrom', // prev Alias
541
+ from: effect.from,
542
+ into: effect.into,
543
+ });
544
+ }
545
+ }
546
+ break;
547
+ }
548
+ case 'CreateFunction': {
549
+ effects.push(effect);
550
+ /**
551
+ * We consider the function mutable if it has any mutable context variables or
552
+ * any side-effects that need to be tracked if the function is called.
553
+ */
554
+ const hasCaptures = effect.captures.some(capture => {
555
+ switch (state.kind(capture).kind) {
556
+ case ValueKind.Context:
557
+ case ValueKind.Mutable: {
558
+ return true;
559
+ }
560
+ default: {
561
+ return false;
562
+ }
563
+ }
564
+ });
565
+ const hasTrackedSideEffects =
566
+ effect.function.loweredFunc.func.aliasingEffects?.some(
567
+ effect =>
568
+ // TODO; include "render" here?
569
+ effect.kind === 'MutateFrozen' ||
570
+ effect.kind === 'MutateGlobal' ||
571
+ effect.kind === 'Impure',
572
+ );
573
+ // For legacy compatibility
574
+ const capturesRef = effect.function.loweredFunc.func.context.some(
575
+ operand => isRefOrRefValue(operand.identifier),
576
+ );
577
+ const isMutable = hasCaptures || hasTrackedSideEffects || capturesRef;
578
+ for (const operand of effect.function.loweredFunc.func.context) {
579
+ if (operand.effect !== Effect.Capture) {
580
+ continue;
581
+ }
582
+ const kind = state.kind(operand).kind;
583
+ if (
584
+ kind === ValueKind.Primitive ||
585
+ kind == ValueKind.Frozen ||
586
+ kind == ValueKind.Global
587
+ ) {
588
+ operand.effect = Effect.Read;
589
+ }
590
+ }
591
+ state.initialize(effect.function, {
592
+ kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen,
593
+ reason: new Set([]),
594
+ });
595
+ state.define(effect.into, effect.function);
596
+ for (const capture of effect.captures) {
597
+ applyEffect(
598
+ context,
599
+ state,
600
+ {
601
+ kind: 'Capture',
602
+ from: capture,
603
+ into: effect.into,
604
+ },
605
+ aliased,
606
+ effects,
607
+ );
608
+ }
609
+ break;
610
+ }
611
+ case 'Alias':
612
+ case 'Capture': {
613
+ /*
614
+ * Capture describes potential information flow: storing a pointer to one value
615
+ * within another. If the destination is not mutable, or the source value has
616
+ * copy-on-write semantics, then we can prune the effect
617
+ */
618
+ const intoKind = state.kind(effect.into).kind;
619
+ let isMutableDesination: boolean;
620
+ switch (intoKind) {
621
+ case ValueKind.Context:
622
+ case ValueKind.Mutable:
623
+ case ValueKind.MaybeFrozen: {
624
+ isMutableDesination = true;
625
+ break;
626
+ }
627
+ default: {
628
+ isMutableDesination = false;
629
+ break;
630
+ }
631
+ }
632
+ const fromKind = state.kind(effect.from).kind;
633
+ let isMutableReferenceType: boolean;
634
+ switch (fromKind) {
635
+ case ValueKind.Global:
636
+ case ValueKind.Primitive: {
637
+ isMutableReferenceType = false;
638
+ break;
639
+ }
640
+ case ValueKind.Frozen: {
641
+ isMutableReferenceType = false;
642
+ effects.push({
643
+ kind: 'ImmutableCapture',
644
+ from: effect.from,
645
+ into: effect.into,
646
+ });
647
+ break;
648
+ }
649
+ default: {
650
+ isMutableReferenceType = true;
651
+ break;
652
+ }
653
+ }
654
+ if (isMutableDesination && isMutableReferenceType) {
655
+ effects.push(effect);
656
+ }
657
+ break;
658
+ }
659
+ case 'Assign': {
660
+ /*
661
+ * Alias represents potential pointer aliasing. If the type is a global,
662
+ * a primitive (copy-on-write semantics) then we can prune the effect
663
+ */
664
+ const fromValue = state.kind(effect.from);
665
+ const fromKind = fromValue.kind;
666
+ switch (fromKind) {
667
+ case ValueKind.Frozen: {
668
+ effects.push({
669
+ kind: 'ImmutableCapture',
670
+ from: effect.from,
671
+ into: effect.into,
672
+ });
673
+ let value = context.effectInstructionValueCache.get(effect);
674
+ if (value == null) {
675
+ value = {
676
+ kind: 'Primitive',
677
+ value: undefined,
678
+ loc: effect.from.loc,
679
+ };
680
+ context.effectInstructionValueCache.set(effect, value);
681
+ }
682
+ state.initialize(value, {
683
+ kind: fromKind,
684
+ reason: new Set(fromValue.reason),
685
+ });
686
+ state.define(effect.into, value);
687
+ break;
688
+ }
689
+ case ValueKind.Global:
690
+ case ValueKind.Primitive: {
691
+ let value = context.effectInstructionValueCache.get(effect);
692
+ if (value == null) {
693
+ value = {
694
+ kind: 'Primitive',
695
+ value: undefined,
696
+ loc: effect.from.loc,
697
+ };
698
+ context.effectInstructionValueCache.set(effect, value);
699
+ }
700
+ state.initialize(value, {
701
+ kind: fromKind,
702
+ reason: new Set(fromValue.reason),
703
+ });
704
+ state.define(effect.into, value);
705
+ break;
706
+ }
707
+ default: {
708
+ if (aliased.has(effect.into.identifier.id)) {
709
+ state.appendAlias(effect.into, effect.from);
710
+ } else {
711
+ aliased.add(effect.into.identifier.id);
712
+ state.alias(effect.into, effect.from);
713
+ }
714
+ effects.push(effect);
715
+ break;
716
+ }
717
+ }
718
+ break;
719
+ }
720
+ case 'Apply': {
721
+ const functionValues = state.values(effect.function);
722
+ if (
723
+ functionValues.length === 1 &&
724
+ functionValues[0].kind === 'FunctionExpression'
725
+ ) {
726
+ /*
727
+ * We're calling a locally declared function, we already know it's effects!
728
+ * We just have to substitute in the args for the params
729
+ */
730
+ const signature = buildSignatureFromFunctionExpression(
731
+ state.env,
732
+ functionValues[0],
733
+ );
734
+ if (DEBUG) {
735
+ console.log(
736
+ `constructed alias signature:\n${printAliasingSignature(signature)}`,
737
+ );
738
+ }
739
+ const signatureEffects = computeEffectsForSignature(
740
+ state.env,
741
+ signature,
742
+ effect.into,
743
+ effect.receiver,
744
+ effect.args,
745
+ functionValues[0].loweredFunc.func.context,
746
+ effect.loc,
747
+ );
748
+ if (signatureEffects != null) {
749
+ if (DEBUG) {
750
+ console.log('apply function expression effects');
751
+ }
752
+ applyEffect(
753
+ context,
754
+ state,
755
+ {kind: 'MutateTransitiveConditionally', value: effect.function},
756
+ aliased,
757
+ effects,
758
+ );
759
+ for (const signatureEffect of signatureEffects) {
760
+ applyEffect(context, state, signatureEffect, aliased, effects);
761
+ }
762
+ break;
763
+ }
764
+ }
765
+ const signatureEffects =
766
+ effect.signature?.aliasing != null
767
+ ? computeEffectsForSignature(
768
+ state.env,
769
+ effect.signature.aliasing,
770
+ effect.into,
771
+ effect.receiver,
772
+ effect.args,
773
+ [],
774
+ effect.loc,
775
+ )
776
+ : null;
777
+ if (signatureEffects != null) {
778
+ if (DEBUG) {
779
+ console.log('apply aliasing signature effects');
780
+ }
781
+ for (const signatureEffect of signatureEffects) {
782
+ applyEffect(context, state, signatureEffect, aliased, effects);
783
+ }
784
+ } else if (effect.signature != null) {
785
+ if (DEBUG) {
786
+ console.log('apply legacy signature effects');
787
+ }
788
+ const legacyEffects = computeEffectsForLegacySignature(
789
+ state,
790
+ effect.signature,
791
+ effect.into,
792
+ effect.receiver,
793
+ effect.args,
794
+ effect.loc,
795
+ );
796
+ for (const legacyEffect of legacyEffects) {
797
+ applyEffect(context, state, legacyEffect, aliased, effects);
798
+ }
799
+ } else {
800
+ if (DEBUG) {
801
+ console.log('default effects');
802
+ }
803
+ applyEffect(
804
+ context,
805
+ state,
806
+ {
807
+ kind: 'Create',
808
+ into: effect.into,
809
+ value: ValueKind.Mutable,
810
+ reason: ValueReason.Other,
811
+ },
812
+ aliased,
813
+ effects,
814
+ );
815
+ /*
816
+ * If no signature then by default:
817
+ * - All operands are conditionally mutated, except some instruction
818
+ * variants are assumed to not mutate the callee (such as `new`)
819
+ * - All operands are captured into (but not directly aliased as)
820
+ * every other argument.
821
+ */
822
+ for (const arg of [effect.receiver, effect.function, ...effect.args]) {
823
+ if (arg.kind === 'Hole') {
824
+ continue;
825
+ }
826
+ const operand = arg.kind === 'Identifier' ? arg : arg.place;
827
+ if (operand !== effect.function || effect.mutatesFunction) {
828
+ applyEffect(
829
+ context,
830
+ state,
831
+ {
832
+ kind: 'MutateTransitiveConditionally',
833
+ value: operand,
834
+ },
835
+ aliased,
836
+ effects,
837
+ );
838
+ }
839
+ const mutateIterator =
840
+ arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null;
841
+ if (mutateIterator) {
842
+ applyEffect(context, state, mutateIterator, aliased, effects);
843
+ }
844
+ applyEffect(
845
+ context,
846
+ state,
847
+ // OK: recording information flow
848
+ {kind: 'Alias', from: operand, into: effect.into},
849
+ aliased,
850
+ effects,
851
+ );
852
+ for (const otherArg of [
853
+ effect.receiver,
854
+ effect.function,
855
+ ...effect.args,
856
+ ]) {
857
+ if (otherArg.kind === 'Hole') {
858
+ continue;
859
+ }
860
+ const other =
861
+ otherArg.kind === 'Identifier' ? otherArg : otherArg.place;
862
+ if (other === arg) {
863
+ continue;
864
+ }
865
+ applyEffect(
866
+ context,
867
+ state,
868
+ {
869
+ /*
870
+ * OK: a function might store one operand into another,
871
+ * but it can't force one to alias another
872
+ */
873
+ kind: 'Capture',
874
+ from: operand,
875
+ into: other,
876
+ },
877
+ aliased,
878
+ effects,
879
+ );
880
+ }
881
+ }
882
+ }
883
+ break;
884
+ }
885
+ case 'Mutate':
886
+ case 'MutateConditionally':
887
+ case 'MutateTransitive':
888
+ case 'MutateTransitiveConditionally': {
889
+ const mutationKind = state.mutate(effect.kind, effect.value);
890
+ if (mutationKind === 'mutate') {
891
+ effects.push(effect);
892
+ } else if (mutationKind === 'mutate-ref') {
893
+ // no-op
894
+ } else if (
895
+ mutationKind !== 'none' &&
896
+ (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive')
897
+ ) {
898
+ const value = state.kind(effect.value);
899
+ if (DEBUG) {
900
+ console.log(`invalid mutation: ${printAliasingEffect(effect)}`);
901
+ console.log(prettyFormat(state.debugAbstractValue(value)));
902
+ }
903
+
904
+ const reason = getWriteErrorReason({
905
+ kind: value.kind,
906
+ reason: value.reason,
907
+ context: new Set(),
908
+ });
909
+ effects.push({
910
+ kind:
911
+ value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
912
+ place: effect.value,
913
+ error: {
914
+ severity: ErrorSeverity.InvalidReact,
915
+ reason,
916
+ description:
917
+ effect.value.identifier.name !== null &&
918
+ effect.value.identifier.name.kind === 'named'
919
+ ? `Found mutation of \`${effect.value.identifier.name.value}\``
920
+ : null,
921
+ loc: effect.value.loc,
922
+ suggestions: null,
923
+ },
924
+ });
925
+ }
926
+ break;
927
+ }
928
+ case 'Impure':
929
+ case 'Render':
930
+ case 'MutateFrozen':
931
+ case 'MutateGlobal': {
932
+ effects.push(effect);
933
+ break;
934
+ }
935
+ default: {
936
+ assertExhaustive(
937
+ effect,
938
+ `Unexpected effect kind '${(effect as any).kind as any}'`,
939
+ );
940
+ }
941
+ }
942
+}
943
+
944
+class InferenceState {
945
+ env: Environment;
946
+ #isFunctionExpression: boolean;
947
+
948
+ // The kind of each value, based on its allocation site
949
+ #values: Map<InstructionValue, AbstractValue>;
950
+ /*
951
+ * The set of values pointed to by each identifier. This is a set
952
+ * to accomodate phi points (where a variable may have different
953
+ * values from different control flow paths).
954
+ */
955
+ #variables: Map<IdentifierId, Set<InstructionValue>>;
956
+
957
+ constructor(
958
+ env: Environment,
959
+ isFunctionExpression: boolean,
960
+ values: Map<InstructionValue, AbstractValue>,
961
+ variables: Map<IdentifierId, Set<InstructionValue>>,
962
+ ) {
963
+ this.env = env;
964
+ this.#isFunctionExpression = isFunctionExpression;
965
+ this.#values = values;
966
+ this.#variables = variables;
967
+ }
968
+
969
+ static empty(
970
+ env: Environment,
971
+ isFunctionExpression: boolean,
972
+ ): InferenceState {
973
+ return new InferenceState(env, isFunctionExpression, new Map(), new Map());
974
+ }
975
+
976
+ get isFunctionExpression(): boolean {
977
+ return this.#isFunctionExpression;
978
+ }
979
+
980
+ // (Re)initializes a @param value with its default @param kind.
981
+ initialize(value: InstructionValue, kind: AbstractValue): void {
982
+ CompilerError.invariant(value.kind !== 'LoadLocal', {
983
+ reason:
984
+ '[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
985
+ description: null,
986
+ loc: value.loc,
987
+ suggestions: null,
988
+ });
989
+ this.#values.set(value, kind);
990
+ }
991
+
992
+ values(place: Place): Array<InstructionValue> {
993
+ const values = this.#variables.get(place.identifier.id);
994
+ CompilerError.invariant(values != null, {
995
+ reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
996
+ description: `${printPlace(place)}`,
997
+ loc: place.loc,
998
+ suggestions: null,
999
+ });
1000
+ return Array.from(values);
1001
+ }
1002
+
1003
+ // Lookup the kind of the given @param value.
1004
+ kind(place: Place): AbstractValue {
1005
+ const values = this.#variables.get(place.identifier.id);
1006
+ CompilerError.invariant(values != null, {
1007
+ reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
1008
+ description: `${printPlace(place)}`,
1009
+ loc: place.loc,
1010
+ suggestions: null,
1011
+ });
1012
+ let mergedKind: AbstractValue | null = null;
1013
+ for (const value of values) {
1014
+ const kind = this.#values.get(value)!;
1015
+ mergedKind =
1016
+ mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind;
1017
+ }
1018
+ CompilerError.invariant(mergedKind !== null, {
1019
+ reason: `[InferMutationAliasingEffects] Expected at least one value`,
1020
+ description: `No value found at \`${printPlace(place)}\``,
1021
+ loc: place.loc,
1022
+ suggestions: null,
1023
+ });
1024
+ return mergedKind;
1025
+ }
1026
+
1027
+ // Updates the value at @param place to point to the same value as @param value.
1028
+ alias(place: Place, value: Place): void {
1029
+ const values = this.#variables.get(value.identifier.id);
1030
+ CompilerError.invariant(values != null, {
1031
+ reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
1032
+ description: `${printIdentifier(value.identifier)}`,
1033
+ loc: value.loc,
1034
+ suggestions: null,
1035
+ });
1036
+ this.#variables.set(place.identifier.id, new Set(values));
1037
+ }
1038
+
1039
+ appendAlias(place: Place, value: Place): void {
1040
+ const values = this.#variables.get(value.identifier.id);
1041
+ CompilerError.invariant(values != null, {
1042
+ reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
1043
+ description: `${printIdentifier(value.identifier)}`,
1044
+ loc: value.loc,
1045
+ suggestions: null,
1046
+ });
1047
+ const prevValues = this.values(place);
1048
+ this.#variables.set(
1049
+ place.identifier.id,
1050
+ new Set([...prevValues, ...values]),
1051
+ );
1052
+ }
1053
+
1054
+ // Defines (initializing or updating) a variable with a specific kind of value.
1055
+ define(place: Place, value: InstructionValue): void {
1056
+ CompilerError.invariant(this.#values.has(value), {
1057
+ reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation(
1058
+ value.loc,
1059
+ )}'`,
1060
+ description: printInstructionValue(value),
1061
+ loc: value.loc,
1062
+ suggestions: null,
1063
+ });
1064
+ this.#variables.set(place.identifier.id, new Set([value]));
1065
+ }
1066
+
1067
+ isDefined(place: Place): boolean {
1068
+ return this.#variables.has(place.identifier.id);
1069
+ }
1070
+
1071
+ /**
1072
+ * Marks @param place as transitively frozen. Returns true if the value was not
1073
+ * already frozen, false if the value is already frozen (or already known immutable).
1074
+ */
1075
+ freeze(place: Place, reason: ValueReason): boolean {
1076
+ const value = this.kind(place);
1077
+ switch (value.kind) {
1078
+ case ValueKind.Context:
1079
+ case ValueKind.Mutable:
1080
+ case ValueKind.MaybeFrozen: {
1081
+ const values = this.values(place);
1082
+ for (const instrValue of values) {
1083
+ this.freezeValue(instrValue, reason);
1084
+ }
1085
+ return true;
1086
+ }
1087
+ case ValueKind.Frozen:
1088
+ case ValueKind.Global:
1089
+ case ValueKind.Primitive: {
1090
+ return false;
1091
+ }
1092
+ default: {
1093
+ assertExhaustive(
1094
+ value.kind,
1095
+ `Unexpected value kind '${(value as any).kind}'`,
1096
+ );
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ freezeValue(value: InstructionValue, reason: ValueReason): void {
1102
+ this.#values.set(value, {
1103
+ kind: ValueKind.Frozen,
1104
+ reason: new Set([reason]),
1105
+ });
1106
+ if (DEBUG) {
1107
+ console.log(`freeze value: ${printInstructionValue(value)} ${reason}`);
1108
+ }
1109
+ if (
1110
+ value.kind === 'FunctionExpression' &&
1111
+ (this.env.config.enablePreserveExistingMemoizationGuarantees ||
1112
+ this.env.config.enableTransitivelyFreezeFunctionExpressions)
1113
+ ) {
1114
+ for (const place of value.loweredFunc.func.context) {
1115
+ this.freeze(place, reason);
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ mutate(
1121
+ variant:
1122
+ | 'Mutate'
1123
+ | 'MutateConditionally'
1124
+ | 'MutateTransitive'
1125
+ | 'MutateTransitiveConditionally',
1126
+ place: Place,
1127
+ ): 'none' | 'mutate' | 'mutate-frozen' | 'mutate-global' | 'mutate-ref' {
1128
+ if (isRefOrRefValue(place.identifier)) {
1129
+ return 'mutate-ref';
1130
+ }
1131
+ const kind = this.kind(place).kind;
1132
+ switch (variant) {
1133
+ case 'MutateConditionally':
1134
+ case 'MutateTransitiveConditionally': {
1135
+ switch (kind) {
1136
+ case ValueKind.Mutable:
1137
+ case ValueKind.Context: {
1138
+ return 'mutate';
1139
+ }
1140
+ default: {
1141
+ return 'none';
1142
+ }
1143
+ }
1144
+ }
1145
+ case 'Mutate':
1146
+ case 'MutateTransitive': {
1147
+ switch (kind) {
1148
+ case ValueKind.Mutable:
1149
+ case ValueKind.Context: {
1150
+ return 'mutate';
1151
+ }
1152
+ case ValueKind.Primitive: {
1153
+ // technically an error, but it's not React specific
1154
+ return 'none';
1155
+ }
1156
+ case ValueKind.Frozen: {
1157
+ return 'mutate-frozen';
1158
+ }
1159
+ case ValueKind.Global: {
1160
+ return 'mutate-global';
1161
+ }
1162
+ case ValueKind.MaybeFrozen: {
1163
+ return 'none';
1164
+ }
1165
+ default: {
1166
+ assertExhaustive(kind, `Unexpected kind ${kind}`);
1167
+ }
1168
+ }
1169
+ }
1170
+ default: {
1171
+ assertExhaustive(variant, `Unexpected mutation variant ${variant}`);
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ /*
1177
+ * Combine the contents of @param this and @param other, returning a new
1178
+ * instance with the combined changes _if_ there are any changes, or
1179
+ * returning null if no changes would occur. Changes include:
1180
+ * - new entries in @param other that did not exist in @param this
1181
+ * - entries whose values differ in @param this and @param other,
1182
+ * and where joining the values produces a different value than
1183
+ * what was in @param this.
1184
+ *
1185
+ * Note that values are joined using a lattice operation to ensure
1186
+ * termination.
1187
+ */
1188
+ merge(other: InferenceState): InferenceState | null {
1189
+ let nextValues: Map<InstructionValue, AbstractValue> | null = null;
1190
+ let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
1191
+
1192
+ for (const [id, thisValue] of this.#values) {
1193
+ const otherValue = other.#values.get(id);
1194
+ if (otherValue !== undefined) {
1195
+ const mergedValue = mergeAbstractValues(thisValue, otherValue);
1196
+ if (mergedValue !== thisValue) {
1197
+ nextValues = nextValues ?? new Map(this.#values);
1198
+ nextValues.set(id, mergedValue);
1199
+ }
1200
+ }
1201
+ }
1202
+ for (const [id, otherValue] of other.#values) {
1203
+ if (this.#values.has(id)) {
1204
+ // merged above
1205
+ continue;
1206
+ }
1207
+ nextValues = nextValues ?? new Map(this.#values);
1208
+ nextValues.set(id, otherValue);
1209
+ }
1210
+
1211
+ for (const [id, thisValues] of this.#variables) {
1212
+ const otherValues = other.#variables.get(id);
1213
+ if (otherValues !== undefined) {
1214
+ let mergedValues: Set<InstructionValue> | null = null;
1215
+ for (const otherValue of otherValues) {
1216
+ if (!thisValues.has(otherValue)) {
1217
+ mergedValues = mergedValues ?? new Set(thisValues);
1218
+ mergedValues.add(otherValue);
1219
+ }
1220
+ }
1221
+ if (mergedValues !== null) {
1222
+ nextVariables = nextVariables ?? new Map(this.#variables);
1223
+ nextVariables.set(id, mergedValues);
1224
+ }
1225
+ }
1226
+ }
1227
+ for (const [id, otherValues] of other.#variables) {
1228
+ if (this.#variables.has(id)) {
1229
+ continue;
1230
+ }
1231
+ nextVariables = nextVariables ?? new Map(this.#variables);
1232
+ nextVariables.set(id, new Set(otherValues));
1233
+ }
1234
+
1235
+ if (nextVariables === null && nextValues === null) {
1236
+ return null;
1237
+ } else {
1238
+ return new InferenceState(
1239
+ this.env,
1240
+ this.#isFunctionExpression,
1241
+ nextValues ?? new Map(this.#values),
1242
+ nextVariables ?? new Map(this.#variables),
1243
+ );
1244
+ }
1245
+ }
1246
+
1247
+ /*
1248
+ * Returns a copy of this state.
1249
+ * TODO: consider using persistent data structures to make
1250
+ * clone cheaper.
1251
+ */
1252
+ clone(): InferenceState {
1253
+ return new InferenceState(
1254
+ this.env,
1255
+ this.#isFunctionExpression,
1256
+ new Map(this.#values),
1257
+ new Map(this.#variables),
1258
+ );
1259
+ }
1260
+
1261
+ /*
1262
+ * For debugging purposes, dumps the state to a plain
1263
+ * object so that it can printed as JSON.
1264
+ */
1265
+ debug(): any {
1266
+ const result: any = {values: {}, variables: {}};
1267
+ const objects: Map<InstructionValue, number> = new Map();
1268
+ function identify(value: InstructionValue): number {
1269
+ let id = objects.get(value);
1270
+ if (id == null) {
1271
+ id = objects.size;
1272
+ objects.set(value, id);
1273
+ }
1274
+ return id;
1275
+ }
1276
+ for (const [value, kind] of this.#values) {
1277
+ const id = identify(value);
1278
+ result.values[id] = {
1279
+ abstract: this.debugAbstractValue(kind),
1280
+ value: printInstructionValue(value),
1281
+ };
1282
+ }
1283
+ for (const [variable, values] of this.#variables) {
1284
+ result.variables[`$${variable}`] = [...values].map(identify);
1285
+ }
1286
+ return result;
1287
+ }
1288
+
1289
+ debugAbstractValue(value: AbstractValue): any {
1290
+ return {
1291
+ kind: value.kind,
1292
+ reason: [...value.reason],
1293
+ };
1294
+ }
1295
+
1296
+ inferPhi(phi: Phi): void {
1297
+ const values: Set<InstructionValue> = new Set();
1298
+ for (const [_, operand] of phi.operands) {
1299
+ const operandValues = this.#variables.get(operand.identifier.id);
1300
+ // This is a backedge that will be handled later by State.merge
1301
+ if (operandValues === undefined) continue;
1302
+ for (const v of operandValues) {
1303
+ values.add(v);
1304
+ }
1305
+ }
1306
+
1307
+ if (values.size > 0) {
1308
+ this.#variables.set(phi.place.identifier.id, values);
1309
+ }
1310
+ }
1311
+}
1312
+
1313
+/**
1314
+ * Returns a value that represents the combined states of the two input values.
1315
+ * If the two values are semantically equivalent, it returns the first argument.
1316
+ */
1317
+function mergeAbstractValues(
1318
+ a: AbstractValue,
1319
+ b: AbstractValue,
1320
+): AbstractValue {
1321
+ const kind = mergeValueKinds(a.kind, b.kind);
1322
+ if (
1323
+ kind === a.kind &&
1324
+ kind === b.kind &&
1325
+ Set_isSuperset(a.reason, b.reason)
1326
+ ) {
1327
+ return a;
1328
+ }
1329
+ const reason = new Set(a.reason);
1330
+ for (const r of b.reason) {
1331
+ reason.add(r);
1332
+ }
1333
+ return {kind, reason};
1334
+}
1335
+
1336
+type InstructionSignature = {
1337
+ effects: ReadonlyArray<AliasingEffect>;
1338
+};
1339
+
1340
+function conditionallyMutateIterator(place: Place): AliasingEffect | null {
1341
+ if (
1342
+ !(
1343
+ isArrayType(place.identifier) ||
1344
+ isSetType(place.identifier) ||
1345
+ isMapType(place.identifier)
1346
+ )
1347
+ ) {
1348
+ return {
1349
+ kind: 'MutateTransitiveConditionally',
1350
+ value: place,
1351
+ };
1352
+ }
1353
+ return null;
1354
+}
1355
+
1356
+/**
1357
+ * Computes an effect signature for the instruction _without_ looking at the inference state,
1358
+ * and only using the semantics of the instructions and the inferred types. The idea is to make
1359
+ * it easy to check that the semantics of each instruction are preserved by describing only the
1360
+ * effects and not making decisions based on the inference state.
1361
+ *
1362
+ * Then in applySignature(), above, we refine this signature based on the inference state.
1363
+ *
1364
+ * NOTE: this function is designed to be cached so it's only computed once upon first visiting
1365
+ * an instruction.
1366
+ */
1367
+function computeSignatureForInstruction(
1368
+ context: Context,
1369
+ env: Environment,
1370
+ instr: Instruction,
1371
+): InstructionSignature {
1372
+ const {lvalue, value} = instr;
1373
+ const effects: Array<AliasingEffect> = [];
1374
+ switch (value.kind) {
1375
+ case 'ArrayExpression': {
1376
+ effects.push({
1377
+ kind: 'Create',
1378
+ into: lvalue,
1379
+ value: ValueKind.Mutable,
1380
+ reason: ValueReason.Other,
1381
+ });
1382
+ // All elements are captured into part of the output value
1383
+ for (const element of value.elements) {
1384
+ if (element.kind === 'Identifier') {
1385
+ effects.push({
1386
+ kind: 'Capture',
1387
+ from: element,
1388
+ into: lvalue,
1389
+ });
1390
+ } else if (element.kind === 'Spread') {
1391
+ const mutateIterator = conditionallyMutateIterator(element.place);
1392
+ if (mutateIterator != null) {
1393
+ effects.push(mutateIterator);
1394
+ }
1395
+ effects.push({
1396
+ kind: 'Capture',
1397
+ from: element.place,
1398
+ into: lvalue,
1399
+ });
1400
+ } else {
1401
+ continue;
1402
+ }
1403
+ }
1404
+ break;
1405
+ }
1406
+ case 'ObjectExpression': {
1407
+ effects.push({
1408
+ kind: 'Create',
1409
+ into: lvalue,
1410
+ value: ValueKind.Mutable,
1411
+ reason: ValueReason.Other,
1412
+ });
1413
+ for (const property of value.properties) {
1414
+ if (property.kind === 'ObjectProperty') {
1415
+ effects.push({
1416
+ kind: 'Capture',
1417
+ from: property.place,
1418
+ into: lvalue,
1419
+ });
1420
+ } else {
1421
+ effects.push({
1422
+ kind: 'Capture',
1423
+ from: property.place,
1424
+ into: lvalue,
1425
+ });
1426
+ }
1427
+ }
1428
+ break;
1429
+ }
1430
+ case 'Await': {
1431
+ effects.push({
1432
+ kind: 'Create',
1433
+ into: lvalue,
1434
+ value: ValueKind.Mutable,
1435
+ reason: ValueReason.Other,
1436
+ });
1437
+ // Potentially mutates the receiver (awaiting it changes its state and can run side effects)
1438
+ effects.push({kind: 'MutateTransitiveConditionally', value: value.value});
1439
+ /**
1440
+ * Data from the promise may be returned into the result, but await does not directly return
1441
+ * the promise itself
1442
+ */
1443
+ effects.push({
1444
+ kind: 'Capture',
1445
+ from: value.value,
1446
+ into: lvalue,
1447
+ });
1448
+ break;
1449
+ }
1450
+ case 'NewExpression':
1451
+ case 'CallExpression':
1452
+ case 'MethodCall': {
1453
+ let callee;
1454
+ let receiver;
1455
+ let mutatesCallee;
1456
+ if (value.kind === 'NewExpression') {
1457
+ callee = value.callee;
1458
+ receiver = value.callee;
1459
+ mutatesCallee = false;
1460
+ } else if (value.kind === 'CallExpression') {
1461
+ callee = value.callee;
1462
+ receiver = value.callee;
1463
+ mutatesCallee = true;
1464
+ } else if (value.kind === 'MethodCall') {
1465
+ callee = value.property;
1466
+ receiver = value.receiver;
1467
+ mutatesCallee = false;
1468
+ } else {
1469
+ assertExhaustive(
1470
+ value,
1471
+ `Unexpected value kind '${(value as any).kind}'`,
1472
+ );
1473
+ }
1474
+ const signature = getFunctionCallSignature(env, callee.identifier.type);
1475
+ effects.push({
1476
+ kind: 'Apply',
1477
+ receiver,
1478
+ function: callee,
1479
+ mutatesFunction: mutatesCallee,
1480
+ args: value.args,
1481
+ into: lvalue,
1482
+ signature,
1483
+ loc: value.loc,
1484
+ });
1485
+ break;
1486
+ }
1487
+ case 'PropertyDelete':
1488
+ case 'ComputedDelete': {
1489
+ effects.push({
1490
+ kind: 'Create',
1491
+ into: lvalue,
1492
+ value: ValueKind.Primitive,
1493
+ reason: ValueReason.Other,
1494
+ });
1495
+ // Mutates the object by removing the property, no aliasing
1496
+ effects.push({kind: 'Mutate', value: value.object});
1497
+ break;
1498
+ }
1499
+ case 'PropertyLoad':
1500
+ case 'ComputedLoad': {
1501
+ if (isPrimitiveType(lvalue.identifier)) {
1502
+ effects.push({
1503
+ kind: 'Create',
1504
+ into: lvalue,
1505
+ value: ValueKind.Primitive,
1506
+ reason: ValueReason.Other,
1507
+ });
1508
+ } else {
1509
+ effects.push({
1510
+ kind: 'CreateFrom',
1511
+ from: value.object,
1512
+ into: lvalue,
1513
+ });
1514
+ }
1515
+ break;
1516
+ }
1517
+ case 'PropertyStore':
1518
+ case 'ComputedStore': {
1519
+ effects.push({kind: 'Mutate', value: value.object});
1520
+ effects.push({
1521
+ kind: 'Capture',
1522
+ from: value.value,
1523
+ into: value.object,
1524
+ });
1525
+ effects.push({
1526
+ kind: 'Create',
1527
+ into: lvalue,
1528
+ value: ValueKind.Primitive,
1529
+ reason: ValueReason.Other,
1530
+ });
1531
+ break;
1532
+ }
1533
+ case 'ObjectMethod':
1534
+ case 'FunctionExpression': {
1535
+ /**
1536
+ * We've already analyzed the function expression in AnalyzeFunctions. There, we assign
1537
+ * a Capture effect to any context variable that appears (locally) to be aliased and/or
1538
+ * mutated. The precise effects are annotated on the function expression's aliasingEffects
1539
+ * property, but we don't want to execute those effects yet. We can only use those when
1540
+ * we know exactly how the function is invoked — via an Apply effect from a custom signature.
1541
+ *
1542
+ * But in the general case, functions can be passed around and possibly called in ways where
1543
+ * we don't know how to interpret their precise effects. For example:
1544
+ *
1545
+ * ```
1546
+ * const a = {};
1547
+ *
1548
+ * // We don't want to consider a as mutating here, this just declares the function
1549
+ * const f = () => { maybeMutate(a) };
1550
+ *
1551
+ * // We don't want to consider a as mutating here either, it can't possibly call f yet
1552
+ * const x = [f];
1553
+ *
1554
+ * // Here we have to assume that f can be called (transitively), and have to consider a
1555
+ * // as mutating
1556
+ * callAllFunctionInArray(x);
1557
+ * ```
1558
+ *
1559
+ * So for any context variables that were inferred as captured or mutated, we record a
1560
+ * Capture effect. If the resulting function is transitively mutated, this will mean
1561
+ * that those operands are also considered mutated. If the function is never called,
1562
+ * they won't be!
1563
+ *
1564
+ * This relies on the rule that:
1565
+ * Capture a -> b and MutateTransitive(b) => Mutate(a)
1566
+ *
1567
+ * Substituting:
1568
+ * Capture contextvar -> function and MutateTransitive(function) => Mutate(contextvar)
1569
+ *
1570
+ * Note that if the type of the context variables are frozen, global, or primitive, the
1571
+ * Capture will either get pruned or downgraded to an ImmutableCapture.
1572
+ */
1573
+ effects.push({
1574
+ kind: 'CreateFunction',
1575
+ into: lvalue,
1576
+ function: value,
1577
+ captures: value.loweredFunc.func.context.filter(
1578
+ operand => operand.effect === Effect.Capture,
1579
+ ),
1580
+ });
1581
+ break;
1582
+ }
1583
+ case 'GetIterator': {
1584
+ effects.push({
1585
+ kind: 'Create',
1586
+ into: lvalue,
1587
+ value: ValueKind.Mutable,
1588
+ reason: ValueReason.Other,
1589
+ });
1590
+ if (
1591
+ isArrayType(value.collection.identifier) ||
1592
+ isMapType(value.collection.identifier) ||
1593
+ isSetType(value.collection.identifier)
1594
+ ) {
1595
+ /*
1596
+ * Builtin collections are known to return a fresh iterator on each call,
1597
+ * so the iterator does not alias the collection
1598
+ */
1599
+ effects.push({
1600
+ kind: 'Capture',
1601
+ from: value.collection,
1602
+ into: lvalue,
1603
+ });
1604
+ } else {
1605
+ /*
1606
+ * Otherwise, the object may return itself as the iterator, so we have to
1607
+ * assume that the result directly aliases the collection. Further, the
1608
+ * method to get the iterator could potentially mutate the collection
1609
+ */
1610
+ effects.push({kind: 'Alias', from: value.collection, into: lvalue});
1611
+ effects.push({
1612
+ kind: 'MutateTransitiveConditionally',
1613
+ value: value.collection,
1614
+ });
1615
+ }
1616
+ break;
1617
+ }
1618
+ case 'IteratorNext': {
1619
+ /*
1620
+ * Technically advancing an iterator will always mutate it (for any reasonable implementation)
1621
+ * But because we create an alias from the collection to the iterator if we don't know the type,
1622
+ * then it's possible the iterator is aliased to a frozen value and we wouldn't want to error.
1623
+ * so we mark this as conditional mutation to allow iterating frozen values.
1624
+ */
1625
+ effects.push({kind: 'MutateConditionally', value: value.iterator});
1626
+ // Extracts part of the original collection into the result
1627
+ effects.push({
1628
+ kind: 'CreateFrom',
1629
+ from: value.collection,
1630
+ into: lvalue,
1631
+ });
1632
+ break;
1633
+ }
1634
+ case 'NextPropertyOf': {
1635
+ effects.push({
1636
+ kind: 'Create',
1637
+ into: lvalue,
1638
+ value: ValueKind.Primitive,
1639
+ reason: ValueReason.Other,
1640
+ });
1641
+ break;
1642
+ }
1643
+ case 'JsxExpression':
1644
+ case 'JsxFragment': {
1645
+ effects.push({
1646
+ kind: 'Create',
1647
+ into: lvalue,
1648
+ value: ValueKind.Frozen,
1649
+ reason: ValueReason.JsxCaptured,
1650
+ });
1651
+ for (const operand of eachInstructionValueOperand(value)) {
1652
+ effects.push({
1653
+ kind: 'Freeze',
1654
+ value: operand,
1655
+ reason: ValueReason.JsxCaptured,
1656
+ });
1657
+ effects.push({
1658
+ kind: 'Capture',
1659
+ from: operand,
1660
+ into: lvalue,
1661
+ });
1662
+ }
1663
+ if (value.kind === 'JsxExpression') {
1664
+ if (value.tag.kind === 'Identifier') {
1665
+ // Tags are render function, by definition they're called during render
1666
+ effects.push({
1667
+ kind: 'Render',
1668
+ place: value.tag,
1669
+ });
1670
+ }
1671
+ if (value.children != null) {
1672
+ // Children are typically called during render, not used as an event/effect callback
1673
+ for (const child of value.children) {
1674
+ effects.push({
1675
+ kind: 'Render',
1676
+ place: child,
1677
+ });
1678
+ }
1679
+ }
1680
+ }
1681
+ break;
1682
+ }
1683
+ case 'DeclareLocal': {
1684
+ // TODO check this
1685
+ effects.push({
1686
+ kind: 'Create',
1687
+ into: value.lvalue.place,
1688
+ // TODO: what kind here???
1689
+ value: ValueKind.Primitive,
1690
+ reason: ValueReason.Other,
1691
+ });
1692
+ effects.push({
1693
+ kind: 'Create',
1694
+ into: lvalue,
1695
+ // TODO: what kind here???
1696
+ value: ValueKind.Primitive,
1697
+ reason: ValueReason.Other,
1698
+ });
1699
+ break;
1700
+ }
1701
+ case 'Destructure': {
1702
+ for (const patternLValue of eachInstructionValueLValue(value)) {
1703
+ if (isPrimitiveType(patternLValue.identifier)) {
1704
+ effects.push({
1705
+ kind: 'Create',
1706
+ into: patternLValue,
1707
+ value: ValueKind.Primitive,
1708
+ reason: ValueReason.Other,
1709
+ });
1710
+ } else {
1711
+ effects.push({
1712
+ kind: 'CreateFrom',
1713
+ from: value.value,
1714
+ into: patternLValue,
1715
+ });
1716
+ }
1717
+ }
1718
+ effects.push({kind: 'Assign', from: value.value, into: lvalue});
1719
+ break;
1720
+ }
1721
+ case 'LoadContext': {
1722
+ /*
1723
+ * Context variables are like mutable boxes. Loading from one
1724
+ * is equivalent to a PropertyLoad from the box, so we model it
1725
+ * with the same effect we use there (CreateFrom)
1726
+ */
1727
+ effects.push({kind: 'CreateFrom', from: value.place, into: lvalue});
1728
+ break;
1729
+ }
1730
+ case 'DeclareContext': {
1731
+ // Context variables are conceptually like mutable boxes
1732
+ const kind = value.lvalue.kind;
1733
+ if (
1734
+ !context.hoistedContextDeclarations.has(
1735
+ value.lvalue.place.identifier.declarationId,
1736
+ ) ||
1737
+ kind === InstructionKind.HoistedConst ||
1738
+ kind === InstructionKind.HoistedFunction ||
1739
+ kind === InstructionKind.HoistedLet
1740
+ ) {
1741
+ /**
1742
+ * If this context variable is not hoisted, or this is the declaration doing the hoisting,
1743
+ * then we create the box.
1744
+ */
1745
+ effects.push({
1746
+ kind: 'Create',
1747
+ into: value.lvalue.place,
1748
+ value: ValueKind.Mutable,
1749
+ reason: ValueReason.Other,
1750
+ });
1751
+ } else {
1752
+ /**
1753
+ * Otherwise this may be a "declare", but there was a previous DeclareContext that
1754
+ * hoisted this variable, and we're mutating it here.
1755
+ */
1756
+ effects.push({kind: 'Mutate', value: value.lvalue.place});
1757
+ }
1758
+ effects.push({
1759
+ kind: 'Create',
1760
+ into: lvalue,
1761
+ // The result can't be referenced so this value doesn't matter
1762
+ value: ValueKind.Primitive,
1763
+ reason: ValueReason.Other,
1764
+ });
1765
+ break;
1766
+ }
1767
+ case 'StoreContext': {
1768
+ /*
1769
+ * Context variables are like mutable boxes, so semantically
1770
+ * we're either creating (let/const) or mutating (reassign) a box,
1771
+ * and then capturing the value into it.
1772
+ */
1773
+ if (
1774
+ value.lvalue.kind === InstructionKind.Reassign ||
1775
+ context.hoistedContextDeclarations.has(
1776
+ value.lvalue.place.identifier.declarationId,
1777
+ )
1778
+ ) {
1779
+ effects.push({kind: 'Mutate', value: value.lvalue.place});
1780
+ } else {
1781
+ effects.push({
1782
+ kind: 'Create',
1783
+ into: value.lvalue.place,
1784
+ value: ValueKind.Mutable,
1785
+ reason: ValueReason.Other,
1786
+ });
1787
+ }
1788
+ effects.push({
1789
+ kind: 'Capture',
1790
+ from: value.value,
1791
+ into: value.lvalue.place,
1792
+ });
1793
+ effects.push({kind: 'Assign', from: value.value, into: lvalue});
1794
+ break;
1795
+ }
1796
+ case 'LoadLocal': {
1797
+ effects.push({kind: 'Assign', from: value.place, into: lvalue});
1798
+ break;
1799
+ }
1800
+ case 'StoreLocal': {
1801
+ effects.push({
1802
+ kind: 'Assign',
1803
+ from: value.value,
1804
+ into: value.lvalue.place,
1805
+ });
1806
+ effects.push({kind: 'Assign', from: value.value, into: lvalue});
1807
+ break;
1808
+ }
1809
+ case 'PostfixUpdate':
1810
+ case 'PrefixUpdate': {
1811
+ effects.push({
1812
+ kind: 'Create',
1813
+ into: lvalue,
1814
+ value: ValueKind.Primitive,
1815
+ reason: ValueReason.Other,
1816
+ });
1817
+ effects.push({
1818
+ kind: 'Create',
1819
+ into: value.lvalue,
1820
+ value: ValueKind.Primitive,
1821
+ reason: ValueReason.Other,
1822
+ });
1823
+ break;
1824
+ }
1825
+ case 'StoreGlobal': {
1826
+ effects.push({
1827
+ kind: 'MutateGlobal',
1828
+ place: value.value,
1829
+ error: {
1830
+ reason:
1831
+ '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)',
1832
+ loc: instr.loc,
1833
+ suggestions: null,
1834
+ severity: ErrorSeverity.InvalidReact,
1835
+ },
1836
+ });
1837
+ effects.push({kind: 'Assign', from: value.value, into: lvalue});
1838
+ break;
1839
+ }
1840
+ case 'TypeCastExpression': {
1841
+ effects.push({kind: 'Assign', from: value.value, into: lvalue});
1842
+ break;
1843
+ }
1844
+ case 'LoadGlobal': {
1845
+ effects.push({
1846
+ kind: 'Create',
1847
+ into: lvalue,
1848
+ value: ValueKind.Global,
1849
+ reason: ValueReason.Global,
1850
+ });
1851
+ break;
1852
+ }
1853
+ case 'StartMemoize':
1854
+ case 'FinishMemoize': {
1855
+ if (env.config.enablePreserveExistingMemoizationGuarantees) {
1856
+ for (const operand of eachInstructionValueOperand(value)) {
1857
+ effects.push({
1858
+ kind: 'Freeze',
1859
+ value: operand,
1860
+ reason: ValueReason.Other,
1861
+ });
1862
+ }
1863
+ }
1864
+ effects.push({
1865
+ kind: 'Create',
1866
+ into: lvalue,
1867
+ value: ValueKind.Primitive,
1868
+ reason: ValueReason.Other,
1869
+ });
1870
+ break;
1871
+ }
1872
+ case 'TaggedTemplateExpression':
1873
+ case 'BinaryExpression':
1874
+ case 'Debugger':
1875
+ case 'JSXText':
1876
+ case 'MetaProperty':
1877
+ case 'Primitive':
1878
+ case 'RegExpLiteral':
1879
+ case 'TemplateLiteral':
1880
+ case 'UnaryExpression':
1881
+ case 'UnsupportedNode': {
1882
+ effects.push({
1883
+ kind: 'Create',
1884
+ into: lvalue,
1885
+ value: ValueKind.Primitive,
1886
+ reason: ValueReason.Other,
1887
+ });
1888
+ break;
1889
+ }
1890
+ }
1891
+ return {
1892
+ effects,
1893
+ };
1894
+}
1895
+
1896
+/**
1897
+ * Creates a set of aliasing effects given a legacy FunctionSignature. This makes all of the
1898
+ * old implicit behaviors from the signatures and InferReferenceEffects explicit, see comments
1899
+ * in the body for details.
1900
+ *
1901
+ * The goal of this method is to make it easier to migrate incrementally to the new system,
1902
+ * so we don't have to immediately write new signatures for all the methods to get expected
1903
+ * compilation output.
1904
+ */
1905
+function computeEffectsForLegacySignature(
1906
+ state: InferenceState,
1907
+ signature: FunctionSignature,
1908
+ lvalue: Place,
1909
+ receiver: Place,
1910
+ args: Array<Place | SpreadPattern | Hole>,
1911
+ loc: SourceLocation,
1912
+): Array<AliasingEffect> {
1913
+ const returnValueReason = signature.returnValueReason ?? ValueReason.Other;
1914
+ const effects: Array<AliasingEffect> = [];
1915
+ effects.push({
1916
+ kind: 'Create',
1917
+ into: lvalue,
1918
+ value: signature.returnValueKind,
1919
+ reason: returnValueReason,
1920
+ });
1921
+ if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) {
1922
+ effects.push({
1923
+ kind: 'Impure',
1924
+ place: receiver,
1925
+ error: {
1926
+ reason:
1927
+ 'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
1928
+ description:
1929
+ signature.canonicalName != null
1930
+ ? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
1931
+ : null,
1932
+ severity: ErrorSeverity.InvalidReact,
1933
+ loc,
1934
+ suggestions: null,
1935
+ },
1936
+ });
1937
+ }
1938
+ const stores: Array<Place> = [];
1939
+ const captures: Array<Place> = [];
1940
+ function visit(place: Place, effect: Effect): void {
1941
+ switch (effect) {
1942
+ case Effect.Store: {
1943
+ effects.push({
1944
+ kind: 'Mutate',
1945
+ value: place,
1946
+ });
1947
+ stores.push(place);
1948
+ break;
1949
+ }
1950
+ case Effect.Capture: {
1951
+ captures.push(place);
1952
+ break;
1953
+ }
1954
+ case Effect.ConditionallyMutate: {
1955
+ effects.push({
1956
+ kind: 'MutateTransitiveConditionally',
1957
+ value: place,
1958
+ });
1959
+ break;
1960
+ }
1961
+ case Effect.ConditionallyMutateIterator: {
1962
+ if (
1963
+ isArrayType(place.identifier) ||
1964
+ isSetType(place.identifier) ||
1965
+ isMapType(place.identifier)
1966
+ ) {
1967
+ effects.push({
1968
+ kind: 'Capture',
1969
+ from: place,
1970
+ into: lvalue,
1971
+ });
1972
+ } else {
1973
+ effects.push({
1974
+ kind: 'Capture',
1975
+ from: place,
1976
+ into: lvalue,
1977
+ });
1978
+ captures.push(place);
1979
+ effects.push({
1980
+ kind: 'MutateTransitiveConditionally',
1981
+ value: place,
1982
+ });
1983
+ }
1984
+ break;
1985
+ }
1986
+ case Effect.Freeze: {
1987
+ effects.push({
1988
+ kind: 'Freeze',
1989
+ value: place,
1990
+ reason: returnValueReason,
1991
+ });
1992
+ break;
1993
+ }
1994
+ case Effect.Mutate: {
1995
+ effects.push({kind: 'MutateTransitive', value: place});
1996
+ break;
1997
+ }
1998
+ case Effect.Read: {
1999
+ effects.push({
2000
+ kind: 'ImmutableCapture',
2001
+ from: place,
2002
+ into: lvalue,
2003
+ });
2004
+ break;
2005
+ }
2006
+ }
2007
+ }
2008
+
2009
+ if (
2010
+ signature.mutableOnlyIfOperandsAreMutable &&
2011
+ areArgumentsImmutableAndNonMutating(state, args)
2012
+ ) {
2013
+ effects.push({
2014
+ kind: 'Alias',
2015
+ from: receiver,
2016
+ into: lvalue,
2017
+ });
2018
+ for (const arg of args) {
2019
+ if (arg.kind === 'Hole') {
2020
+ continue;
2021
+ }
2022
+ const place = arg.kind === 'Identifier' ? arg : arg.place;
2023
+ effects.push({
2024
+ kind: 'ImmutableCapture',
2025
+ from: place,
2026
+ into: lvalue,
2027
+ });
2028
+ }
2029
+ return effects;
2030
+ }
2031
+
2032
+ if (signature.calleeEffect !== Effect.Capture) {
2033
+ /*
2034
+ * InferReferenceEffects and FunctionSignature have an implicit assumption that the receiver
2035
+ * is captured into the return value. Consider for example the signature for Array.proto.pop:
2036
+ * the calleeEffect is Store, since it's a known mutation but non-transitive. But the return
2037
+ * of the pop() captures from the receiver! This isn't specified explicitly. So we add this
2038
+ * here, and rely on applySignature() to downgrade this to ImmutableCapture (or prune) if
2039
+ * the type doesn't actually need to be captured based on the input and return type.
2040
+ */
2041
+ effects.push({
2042
+ kind: 'Alias',
2043
+ from: receiver,
2044
+ into: lvalue,
2045
+ });
2046
+ }
2047
+ visit(receiver, signature.calleeEffect);
2048
+ for (let i = 0; i < args.length; i++) {
2049
+ const arg = args[i];
2050
+ if (arg.kind === 'Hole') {
2051
+ continue;
2052
+ }
2053
+ const place = arg.kind === 'Identifier' ? arg : arg.place;
2054
+ const signatureEffect =
2055
+ arg.kind === 'Identifier' && i < signature.positionalParams.length
2056
+ ? signature.positionalParams[i]!
2057
+ : (signature.restParam ?? Effect.ConditionallyMutate);
2058
+ const effect = getArgumentEffect(signatureEffect, arg);
2059
+
2060
+ visit(place, effect);
2061
+ }
2062
+ if (captures.length !== 0) {
2063
+ if (stores.length === 0) {
2064
+ // If no stores, then capture into the return value
2065
+ for (const capture of captures) {
2066
+ effects.push({kind: 'Alias', from: capture, into: lvalue});
2067
+ }
2068
+ } else {
2069
+ // Else capture into the stores
2070
+ for (const capture of captures) {
2071
+ for (const store of stores) {
2072
+ effects.push({kind: 'Capture', from: capture, into: store});
2073
+ }
2074
+ }
2075
+ }
2076
+ }
2077
+ return effects;
2078
+}
2079
+
2080
+/**
2081
+ * Returns true if all of the arguments are both non-mutable (immutable or frozen)
2082
+ * _and_ are not functions which might mutate their arguments. Note that function
2083
+ * expressions count as frozen so long as they do not mutate free variables: this
2084
+ * function checks that such functions also don't mutate their inputs.
2085
+ */
2086
+function areArgumentsImmutableAndNonMutating(
2087
+ state: InferenceState,
2088
+ args: Array<Place | SpreadPattern | Hole>,
2089
+): boolean {
2090
+ for (const arg of args) {
2091
+ if (arg.kind === 'Hole') {
2092
+ continue;
2093
+ }
2094
+ if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') {
2095
+ const fnShape = state.env.getFunctionSignature(arg.identifier.type);
2096
+ if (fnShape != null) {
2097
+ return (
2098
+ !fnShape.positionalParams.some(isKnownMutableEffect) &&
2099
+ (fnShape.restParam == null ||
2100
+ !isKnownMutableEffect(fnShape.restParam))
2101
+ );
2102
+ }
2103
+ }
2104
+ const place = arg.kind === 'Identifier' ? arg : arg.place;
2105
+
2106
+ const kind = state.kind(place).kind;
2107
+ switch (kind) {
2108
+ case ValueKind.Primitive:
2109
+ case ValueKind.Frozen: {
2110
+ /*
2111
+ * Only immutable values, or frozen lambdas are allowed.
2112
+ * A lambda may appear frozen even if it may mutate its inputs,
2113
+ * so we have a second check even for frozen value types
2114
+ */
2115
+ break;
2116
+ }
2117
+ default: {
2118
+ /**
2119
+ * Globals, module locals, and other locally defined functions may
2120
+ * mutate their arguments.
2121
+ */
2122
+ return false;
2123
+ }
2124
+ }
2125
+ const values = state.values(place);
2126
+ for (const value of values) {
2127
+ if (
2128
+ value.kind === 'FunctionExpression' &&
2129
+ value.loweredFunc.func.params.some(param => {
2130
+ const place = param.kind === 'Identifier' ? param : param.place;
2131
+ const range = place.identifier.mutableRange;
2132
+ return range.end > range.start + 1;
2133
+ })
2134
+ ) {
2135
+ // This is a function which may mutate its inputs
2136
+ return false;
2137
+ }
2138
+ }
2139
+ }
2140
+ return true;
2141
+}
2142
+
2143
+function computeEffectsForSignature(
2144
+ env: Environment,
2145
+ signature: AliasingSignature,
2146
+ lvalue: Place,
2147
+ receiver: Place,
2148
+ args: Array<Place | SpreadPattern | Hole>,
2149
+ // Used for signatures constructed dynamically which reference context variables
2150
+ context: Array<Place> = [],
2151
+ loc: SourceLocation,
2152
+): Array<AliasingEffect> | null {
2153
+ if (
2154
+ // Not enough args
2155
+ signature.params.length > args.length ||
2156
+ // Too many args and there is no rest param to hold them
2157
+ (args.length > signature.params.length && signature.rest == null)
2158
+ ) {
2159
+ if (DEBUG) {
2160
+ if (signature.params.length > args.length) {
2161
+ console.log(
2162
+ `not enough args: ${args.length} args for ${signature.params.length} params`,
2163
+ );
2164
+ } else {
2165
+ console.log(
2166
+ `too many args: ${args.length} args for ${signature.params.length} params, with no rest param`,
2167
+ );
2168
+ }
2169
+ }
2170
+ return null;
2171
+ }
2172
+ // Build substitutions
2173
+ const substitutions: Map<IdentifierId, Array<Place>> = new Map();
2174
+ substitutions.set(signature.receiver, [receiver]);
2175
+ substitutions.set(signature.returns, [lvalue]);
2176
+ const params = signature.params;
2177
+ for (let i = 0; i < args.length; i++) {
2178
+ const arg = args[i];
2179
+ if (arg.kind === 'Hole') {
2180
+ continue;
2181
+ } else if (params == null || i >= params.length || arg.kind === 'Spread') {
2182
+ if (signature.rest == null) {
2183
+ if (DEBUG) {
2184
+ console.log(`no rest value to hold param`);
2185
+ }
2186
+ return null;
2187
+ }
2188
+ const place = arg.kind === 'Identifier' ? arg : arg.place;
2189
+ getOrInsertWith(substitutions, signature.rest, () => []).push(place);
2190
+ } else {
2191
+ const param = params[i];
2192
+ substitutions.set(param, [arg]);
2193
+ }
2194
+ }
2195
+
2196
+ /*
2197
+ * Signatures constructed dynamically from function expressions will reference values
2198
+ * other than their receiver/args/etc. We populate the substitution table with these
2199
+ * values so that we can still exit for unpopulated substitutions
2200
+ */
2201
+ for (const operand of context) {
2202
+ substitutions.set(operand.identifier.id, [operand]);
2203
+ }
2204
+
2205
+ const effects: Array<AliasingEffect> = [];
2206
+ for (const signatureTemporary of signature.temporaries) {
2207
+ const temp = createTemporaryPlace(env, receiver.loc);
2208
+ substitutions.set(signatureTemporary.identifier.id, [temp]);
2209
+ }
2210
+
2211
+ // Apply substitutions
2212
+ for (const effect of signature.effects) {
2213
+ switch (effect.kind) {
2214
+ case 'Assign':
2215
+ case 'ImmutableCapture':
2216
+ case 'Alias':
2217
+ case 'CreateFrom':
2218
+ case 'Capture': {
2219
+ const from = substitutions.get(effect.from.identifier.id) ?? [];
2220
+ const to = substitutions.get(effect.into.identifier.id) ?? [];
2221
+ for (const fromId of from) {
2222
+ for (const toId of to) {
2223
+ effects.push({
2224
+ kind: effect.kind,
2225
+ from: fromId,
2226
+ into: toId,
2227
+ });
2228
+ }
2229
+ }
2230
+ break;
2231
+ }
2232
+ case 'Impure':
2233
+ case 'MutateFrozen':
2234
+ case 'MutateGlobal': {
2235
+ const values = substitutions.get(effect.place.identifier.id) ?? [];
2236
+ for (const value of values) {
2237
+ effects.push({kind: effect.kind, place: value, error: effect.error});
2238
+ }
2239
+ break;
2240
+ }
2241
+ case 'Render': {
2242
+ const values = substitutions.get(effect.place.identifier.id) ?? [];
2243
+ for (const value of values) {
2244
+ effects.push({kind: effect.kind, place: value});
2245
+ }
2246
+ break;
2247
+ }
2248
+ case 'Mutate':
2249
+ case 'MutateTransitive':
2250
+ case 'MutateTransitiveConditionally':
2251
+ case 'MutateConditionally': {
2252
+ const values = substitutions.get(effect.value.identifier.id) ?? [];
2253
+ for (const id of values) {
2254
+ effects.push({kind: effect.kind, value: id});
2255
+ }
2256
+ break;
2257
+ }
2258
+ case 'Freeze': {
2259
+ const values = substitutions.get(effect.value.identifier.id) ?? [];
2260
+ for (const value of values) {
2261
+ effects.push({kind: 'Freeze', value, reason: effect.reason});
2262
+ }
2263
+ break;
2264
+ }
2265
+ case 'Create': {
2266
+ const into = substitutions.get(effect.into.identifier.id) ?? [];
2267
+ for (const value of into) {
2268
+ effects.push({
2269
+ kind: 'Create',
2270
+ into: value,
2271
+ value: effect.value,
2272
+ reason: effect.reason,
2273
+ });
2274
+ }
2275
+ break;
2276
+ }
2277
+ case 'Apply': {
2278
+ const applyReceiver = substitutions.get(effect.receiver.identifier.id);
2279
+ if (applyReceiver == null || applyReceiver.length !== 1) {
2280
+ if (DEBUG) {
2281
+ console.log(`too many substitutions for receiver`);
2282
+ }
2283
+ return null;
2284
+ }
2285
+ const applyFunction = substitutions.get(effect.function.identifier.id);
2286
+ if (applyFunction == null || applyFunction.length !== 1) {
2287
+ if (DEBUG) {
2288
+ console.log(`too many substitutions for function`);
2289
+ }
2290
+ return null;
2291
+ }
2292
+ const applyInto = substitutions.get(effect.into.identifier.id);
2293
+ if (applyInto == null || applyInto.length !== 1) {
2294
+ if (DEBUG) {
2295
+ console.log(`too many substitutions for into`);
2296
+ }
2297
+ return null;
2298
+ }
2299
+ const applyArgs: Array<Place | SpreadPattern | Hole> = [];
2300
+ for (const arg of effect.args) {
2301
+ if (arg.kind === 'Hole') {
2302
+ applyArgs.push(arg);
2303
+ } else if (arg.kind === 'Identifier') {
2304
+ const applyArg = substitutions.get(arg.identifier.id);
2305
+ if (applyArg == null || applyArg.length !== 1) {
2306
+ if (DEBUG) {
2307
+ console.log(`too many substitutions for arg`);
2308
+ }
2309
+ return null;
2310
+ }
2311
+ applyArgs.push(applyArg[0]);
2312
+ } else {
2313
+ const applyArg = substitutions.get(arg.place.identifier.id);
2314
+ if (applyArg == null || applyArg.length !== 1) {
2315
+ if (DEBUG) {
2316
+ console.log(`too many substitutions for arg`);
2317
+ }
2318
+ return null;
2319
+ }
2320
+ applyArgs.push({kind: 'Spread', place: applyArg[0]});
2321
+ }
2322
+ }
2323
+ effects.push({
2324
+ kind: 'Apply',
2325
+ mutatesFunction: effect.mutatesFunction,
2326
+ receiver: applyReceiver[0],
2327
+ args: applyArgs,
2328
+ function: applyFunction[0],
2329
+ into: applyInto[0],
2330
+ signature: effect.signature,
2331
+ loc,
2332
+ });
2333
+ break;
2334
+ }
2335
+ case 'CreateFunction': {
2336
+ CompilerError.throwTodo({
2337
+ reason: `Support CreateFrom effects in signatures`,
2338
+ loc: receiver.loc,
2339
+ });
2340
+ }
2341
+ default: {
2342
+ assertExhaustive(
2343
+ effect,
2344
+ `Unexpected effect kind '${(effect as any).kind}'`,
2345
+ );
2346
+ }
2347
+ }
2348
+ }
2349
+ return effects;
2350
+}
2351
+
2352
+function buildSignatureFromFunctionExpression(
2353
+ env: Environment,
2354
+ fn: FunctionExpression,
2355
+): AliasingSignature {
2356
+ let rest: IdentifierId | null = null;
2357
+ const params: Array<IdentifierId> = [];
2358
+ for (const param of fn.loweredFunc.func.params) {
2359
+ if (param.kind === 'Identifier') {
2360
+ params.push(param.identifier.id);
2361
+ } else {
2362
+ rest = param.place.identifier.id;
2363
+ }
2364
+ }
2365
+ return {
2366
+ receiver: makeIdentifierId(0),
2367
+ params,
2368
+ rest: rest ?? createTemporaryPlace(env, fn.loc).identifier.id,
2369
+ returns: fn.loweredFunc.func.returns.identifier.id,
2370
+ effects: fn.loweredFunc.func.aliasingEffects ?? [],
2371
+ temporaries: [],
2372
+ };
2373
+}
2374
+
2375
+export type AbstractValue = {
2376
+ kind: ValueKind;
2377
+ reason: ReadonlySet<ValueReason>;
2378
+};