7
8
import { CompilerError } from "../CompilerError";
9
import {
10
+ BlockId,
11
+ GeneratedSource,
12
Identifier,
13
IdentifierId,
14
InstructionId,
137
type DeclMap = Map<IdentifierId, Decl>;
138
type Decl = {
139
id: InstructionId;
138
- scope: Stack<ReactiveScope>;
140
+ scope: Stack<ScopeTraversalState>;
141
};
142
143
+/**
144
+ * TraversalState and PoisonState is used to track the poisoned state of a scope.
145
+ *
146
+ * A scope is poisoned when either of these conditions hold:
147
+ * - one of its own nested blocks is a jump target (for break/continues)
148
+ * - it is a outermost scope and contains a throw / return
149
+ *
150
+ * When a scope is poisoned, all dependencies (from instructions and inner scopes)
151
+ * are added as conditionally accessed.
152
+ */
153
+type ScopeTraversalState = {
154
+ value: ReactiveScope;
155
+ ownBlocks: Stack<BlockId>;
156
+};
157
+
158
+class PoisonState {
159
+ poisonedBlocks: Set<BlockId> = new Set();
160
+ poisonedScopes: Set<ScopeId> = new Set();
161
+ isPoisoned: boolean = false;
162
+
163
+ constructor(
164
+ poisonedBlocks: Set<BlockId>,
165
+ poisonedScopes: Set<ScopeId>,
166
+ isPoisoned: boolean
167
+ ) {
168
+ this.poisonedBlocks = poisonedBlocks;
169
+ this.poisonedScopes = poisonedScopes;
170
+ this.isPoisoned = isPoisoned;
171
+ }
172
+
173
+ clone(): PoisonState {
174
+ return new PoisonState(
175
+ new Set(this.poisonedBlocks),
176
+ new Set(this.poisonedScopes),
177
+ this.isPoisoned
178
+ );
179
+ }
180
+
181
+ take(other: PoisonState): PoisonState {
182
+ const copy = new PoisonState(
183
+ this.poisonedBlocks,
184
+ this.poisonedScopes,
185
+ this.isPoisoned
186
+ );
187
+ this.poisonedBlocks = other.poisonedBlocks;
188
+ this.poisonedScopes = other.poisonedScopes;
189
+ this.isPoisoned = other.isPoisoned;
190
+ return copy;
191
+ }
192
+
193
+ merge(
194
+ others: Array<PoisonState>,
195
+ currentScope: ScopeTraversalState | null
196
+ ): void {
197
+ for (const other of others) {
198
+ for (const id of other.poisonedBlocks) {
199
+ this.poisonedBlocks.add(id);
200
+ }
201
+ for (const id of other.poisonedScopes) {
202
+ this.poisonedScopes.add(id);
203
+ }
204
+ }
205
+ this.#invalidate(currentScope);
206
+ }
207
+
208
+ #invalidate(currentScope: ScopeTraversalState | null): void {
209
+ if (currentScope != null) {
210
+ if (this.poisonedScopes.has(currentScope.value.id)) {
211
+ this.isPoisoned = true;
212
+ return;
213
+ } else if (
214
+ currentScope.ownBlocks.find((blockId) =>
215
+ this.poisonedBlocks.has(blockId)
216
+ )
217
+ ) {
218
+ this.isPoisoned = true;
219
+ return;
220
+ }
221
+ }
222
+ this.isPoisoned = false;
223
+ }
224
+
225
+ /**
226
+ * Mark a block or scope as poisoned and update the `isPoisoned` flag.
227
+ *
228
+ * @param targetBlock id of the block which ends non-linear control flow.
229
+ * For a break/continue instruction, this is the target block.
230
+ * Throw and return instructions have no target and will poison the earliest
231
+ * active scope
232
+ */
233
+ addPoisonTarget(
234
+ target: BlockId | null,
235
+ activeScopes: Stack<ScopeTraversalState>
236
+ ): void {
237
+ const currentScope = activeScopes.value;
238
+ if (target == null && currentScope != null) {
239
+ let cursor = activeScopes;
240
+ while (true) {
241
+ const next = cursor.pop();
242
+ if (next.value == null) {
243
+ const poisonedScope = cursor.value!.value.id;
244
+ this.poisonedScopes.add(poisonedScope);
245
+ if (poisonedScope === currentScope?.value.id) {
246
+ this.isPoisoned = true;
247
+ }
248
+ break;
249
+ } else {
250
+ cursor = next;
251
+ }
252
+ }
253
+ } else if (target != null) {
254
+ this.poisonedBlocks.add(target);
255
+ if (
256
+ !this.isPoisoned &&
257
+ currentScope?.ownBlocks.find((blockId) => blockId === target)
258
+ ) {
259
+ this.isPoisoned = true;
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Invoked during traversal when a poisoned scope becomes inactive
266
+ * @param id
267
+ * @param currentScope
268
+ */
269
+ removeMaybePoisonedScope(
270
+ id: ScopeId,
271
+ currentScope: ScopeTraversalState | null
272
+ ): void {
273
+ this.poisonedScopes.delete(id);
274
+ this.#invalidate(currentScope);
275
+ }
276
+
277
+ removeMaybePoisonedBlock(
278
+ id: BlockId,
279
+ currentScope: ScopeTraversalState | null
280
+ ): void {
281
+ this.poisonedBlocks.delete(id);
282
+ this.#invalidate(currentScope);
283
+ }
284
+}
285
+
286
class Context {
287
#temporariesUsedOutsideScope: Set<IdentifierId>;
288
#declarations: DeclMap = new Map();
308
*/
309
#depsInCurrentConditional: ReactiveScopeDependencyTree =
310
new ReactiveScopeDependencyTree();
166
- #scopes: Stack<ReactiveScope> = empty();
311
+ #scopes: Stack<ScopeTraversalState> = empty();
312
+ poisonState: PoisonState = new PoisonState(new Set(), new Set(), false);
313
314
constructor(temporariesUsedOutsideScope: Set<IdentifierId>) {
315
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
319
// Save context of previous scope
320
const prevInConditional = this.#inConditionalWithinScope;
321
const previousDependencies = this.#dependencies;
322
+ const prevDepsInConditional: ReactiveScopeDependencyTree | null = this
323
+ .isPoisoned
324
+ ? this.#depsInCurrentConditional
325
+ : null;
326
+ if (prevDepsInConditional != null) {
327
+ this.#depsInCurrentConditional = new ReactiveScopeDependencyTree();
328
+ }
329
330
/*
331
* Set context for new scope
336
const scopedDependencies = new ReactiveScopeDependencyTree();
337
this.#inConditionalWithinScope = false;
338
this.#dependencies = scopedDependencies;
186
- this.#scopes = this.#scopes.push(scope);
339
+ this.#scopes = this.#scopes.push({
340
+ value: scope,
341
+ ownBlocks: empty(),
342
+ });
343
+ this.poisonState.isPoisoned = false;
344
345
fn();
346
347
// Restore context of previous scope
348
this.#scopes = this.#scopes.pop();
349
+ this.poisonState.removeMaybePoisonedScope(scope.id, this.#scopes.value);
350
+
351
this.#dependencies = previousDependencies;
352
this.#inConditionalWithinScope = prevInConditional;
353
363
*/
364
this.#dependencies.addDepsFromInnerScope(
365
scopedDependencies,
207
- this.#inConditionalWithinScope,
366
+ this.#inConditionalWithinScope || this.isPoisoned,
367
this.#checkValidDependency.bind(this)
368
);
369
+
370
+ if (prevDepsInConditional != null) {
371
+ // Outer scope is poisoned
372
+ prevDepsInConditional.addDepsFromInnerScope(
373
+ this.#depsInCurrentConditional,
374
+ true,
375
+ this.#checkValidDependency.bind(this)
376
+ );
377
+ this.#depsInCurrentConditional = prevDepsInConditional;
378
+ }
379
+
380
return minInnerScopeDependencies;
381
}
382
538
const currentDeclaration =
539
this.#reassignments.get(identifier) ??
540
this.#declarations.get(identifier.id);
371
- const currentScope = this.#scopes !== null ? this.#scopes.value : null;
541
+ const currentScope = this.currentScope.value?.value;
542
return (
543
currentScope != null &&
544
currentDeclaration !== undefined &&
545
currentDeclaration.id < currentScope.range.start &&
546
(currentDeclaration.scope == null ||
377
- currentDeclaration.scope.value !== currentScope)
547
+ currentDeclaration.scope.value?.value !== currentScope)
548
);
549
}
550
552
if (this.#scopes === null) {
553
return false;
554
}
385
- return this.#scopes.contains(scope);
555
+ return this.#scopes.find((state) => state.value === scope);
556
}
557
388
- get currentScope(): Stack<ReactiveScope> {
558
+ get currentScope(): Stack<ScopeTraversalState> {
559
return this.#scopes;
560
}
561
562
+ get isPoisoned(): boolean {
563
+ return this.poisonState.isPoisoned;
564
+ }
565
+
566
visitOperand(place: Place): void {
567
const resolved = this.resolveTemporary(place);
568
/*
610
originalDeclaration.scope.value !== null
611
) {
612
originalDeclaration.scope.each((scope) => {
439
- if (!this.#isScopeActive(scope)) {
440
- scope.declarations.set(maybeDependency.identifier.id, {
613
+ if (!this.#isScopeActive(scope.value)) {
614
+ scope.value.declarations.set(maybeDependency.identifier.id, {
615
identifier: maybeDependency.identifier,
442
- scope: originalDeclaration.scope.value!, // checked above
616
+ scope: originalDeclaration.scope.value!.value,
617
});
618
}
619
});
620
}
621
622
if (this.#checkValidDependency(maybeDependency)) {
449
- this.#depsInCurrentConditional.add(maybeDependency, false);
623
+ const isPoisoned = this.isPoisoned;
624
+ this.#depsInCurrentConditional.add(maybeDependency, isPoisoned);
625
/*
626
* Add info about this dependency to the existing tree
627
* We do not try to join/reduce dependencies here due to missing info
628
*/
454
- this.#dependencies.add(maybeDependency, this.#inConditionalWithinScope);
629
+ this.#dependencies.add(
630
+ maybeDependency,
631
+ this.#inConditionalWithinScope || isPoisoned
632
+ );
633
}
634
}
635
638
* current one as a {@link ReactiveScope.reassignments}
639
*/
640
visitReassignment(place: Place): void {
641
+ const currentScope = this.currentScope.value?.value;
642
if (
464
- this.currentScope.value != null &&
465
- !Array.from(this.currentScope.value.reassignments).some(
643
+ currentScope != null &&
644
+ !Array.from(currentScope.reassignments).some(
645
(identifier) => identifier.id === place.identifier.id
646
) &&
647
this.#checkValidDependency({ identifier: place.identifier, path: [] })
648
) {
470
- this.currentScope.value.reassignments.add(place.identifier);
649
+ currentScope.reassignments.add(place.identifier);
650
+ }
651
+ }
652
+
653
+ pushLabeledBlock(id: BlockId): void {
654
+ const currentScope = this.#scopes.value;
655
+ if (currentScope != null) {
656
+ currentScope.ownBlocks = currentScope.ownBlocks.push(id);
657
+ }
658
+ }
659
+ popLabeledBlock(id: BlockId): void {
660
+ const currentScope = this.#scopes.value;
661
+ if (currentScope != null) {
662
+ const last = currentScope.ownBlocks.value;
663
+ currentScope.ownBlocks = currentScope.ownBlocks.pop();
664
+
665
+ CompilerError.invariant(last != null && last === id, {
666
+ reason: "[PropagateScopeDependencies] Misformed block stack",
667
+ loc: GeneratedSource,
668
+ });
669
}
670
+ this.poisonState.removeMaybePoisonedBlock(id, currentScope);
671
}
672
}
673
858
}
859
}
860
861
+ enterTerminal(stmt: ReactiveTerminalStatement, context: Context): void {
862
+ if (stmt.label != null) {
863
+ context.pushLabeledBlock(stmt.label.id);
864
+ }
865
+ const terminal = stmt.terminal;
866
+ switch (terminal.kind) {
867
+ case "continue":
868
+ case "break": {
869
+ context.poisonState.addPoisonTarget(
870
+ terminal.target,
871
+ context.currentScope
872
+ );
873
+ break;
874
+ }
875
+ case "throw":
876
+ case "return": {
877
+ context.poisonState.addPoisonTarget(null, context.currentScope);
878
+ break;
879
+ }
880
+ }
881
+ }
882
+ exitTerminal(stmt: ReactiveTerminalStatement, context: Context): void {
883
+ if (stmt.label != null) {
884
+ context.popLabeledBlock(stmt.label.id);
885
+ }
886
+ }
887
+
888
override visitTerminal(
889
stmt: ReactiveTerminalStatement,
890
context: Context
891
): void {
892
+ this.enterTerminal(stmt, context);
893
const terminal = stmt.terminal;
894
switch (terminal.kind) {
895
case "break":
946
case "if": {
947
context.visitOperand(terminal.test);
948
const { consequent, alternate } = terminal;
949
+ /*
950
+ * Consequent and alternate branches are mutually exclusive,
951
+ * so we save and restore the poison state here.
952
+ */
953
+ const prevPoisonState = context.poisonState.clone();
954
const depsInIf = context.enterConditional(() => {
955
this.visitBlock(consequent, context);
956
});
957
if (alternate !== null) {
958
+ const ifPoisonState = context.poisonState.take(prevPoisonState);
959
const depsInElse = context.enterConditional(() => {
960
this.visitBlock(alternate, context);
961
});
962
+ context.poisonState.merge(
963
+ [ifPoisonState],
964
+ context.currentScope.value
965
+ );
966
context.promoteDepsFromExhaustiveConditionals([depsInIf, depsInElse]);
967
}
968
break;
980
}
981
const depsInCases = [];
982
let foundDefault = false;
983
+ /**
984
+ * Switch branches are mutually exclusive
985
+ */
986
+ const prevPoisonState = context.poisonState.clone();
987
+ const mutExPoisonStates: Array<PoisonState> = [];
988
/*
989
* This can underestimate unconditional accesses due to the current
990
* CFG representation for fallthrough. This is safe. It only
997
foundDefault = true;
998
}
999
if (block !== undefined) {
1000
+ mutExPoisonStates.push(
1001
+ context.poisonState.take(prevPoisonState.clone())
1002
+ );
1003
depsInCases.push(
1004
context.enterConditional(() => {
1005
this.visitBlock(block, context);
1010
if (foundDefault) {
1011
context.promoteDepsFromExhaustiveConditionals(depsInCases);
1012
}
1013
+ context.poisonState.merge(
1014
+ mutExPoisonStates,
1015
+ context.currentScope.value
1016
+ );
1017
break;
1018
}
1019
case "label": {
1032
);
1033
}
1034
}
1035
+ this.exitTerminal(stmt, context);
1036
}
1037
}