Skip to content

Commit 3242a61

Browse files
crisbetothePunderWoman
authored andcommitted
fix(compiler): variable counter visiting some expressions twice
The `countVariables` phase visits all ops in the list and all nested expressions in order to count how many variables are used. Currently it does so by going through `unit.ops()` and then calling `visitExpressionsInOp` on each op. This leads to expressions in ops that have nested ops (e.g. `ListenerOp`) to be visited twice, because `unit.ops()` descends into child ops and then `visitExpressionsInOp` does the same. It hasn't been a problem so far, because the only expressions that can have vars in host bindings are pure functions and they aren't generated for listeners, but it will become a problem for arrow functions since they can be used in listeners. These changes resolve the issue by iterating over the `unit.create` and `unit.update` instead.
1 parent 17733b7 commit 3242a61

File tree

1 file changed

+54
-39
lines changed

1 file changed

+54
-39
lines changed

packages/compiler/src/template/pipeline/src/phases/var_counting.ts

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import * as ir from '../../ir';
10+
import * as o from '../../../../output/output_ast';
1011
import {CompilationJob, ComponentCompilationJob} from '../compilation';
1112

1213
/**
@@ -28,50 +29,64 @@ export function countVariables(job: CompilationJob): void {
2829
// Count variables on expressions inside ops. We do this later because some of these expressions
2930
// might be conditional (e.g. `pipeBinding` inside of a ternary), and we don't want to interfere
3031
// with indices for top-level binding slots (e.g. `property`).
31-
for (const op of unit.ops()) {
32-
ir.visitExpressionsInOp(op, (expr) => {
33-
if (!ir.isIrExpression(expr)) {
34-
return;
35-
}
32+
const firstPassCountExpressionVars = (expr: o.Expression) => {
33+
if (!ir.isIrExpression(expr)) {
34+
return;
35+
}
3636

37-
// TemplateDefinitionBuilder assigns variable offsets for everything but pure functions
38-
// first, and then assigns offsets to pure functions lazily. We emulate that behavior by
39-
// assigning offsets in two passes instead of one, only in compatibility mode.
40-
if (
41-
job.compatibility === ir.CompatibilityMode.TemplateDefinitionBuilder &&
42-
expr instanceof ir.PureFunctionExpr
43-
) {
44-
return;
45-
}
37+
// TemplateDefinitionBuilder assigns variable offsets for everything but pure functions
38+
// first, and then assigns offsets to pure functions lazily. We emulate that behavior by
39+
// assigning offsets in two passes instead of one, only in compatibility mode.
40+
if (
41+
job.compatibility === ir.CompatibilityMode.TemplateDefinitionBuilder &&
42+
expr instanceof ir.PureFunctionExpr
43+
) {
44+
return;
45+
}
4646

47-
// Some expressions require knowledge of the number of variable slots consumed.
48-
if (ir.hasUsesVarOffsetTrait(expr)) {
49-
expr.varOffset = varCount;
50-
}
47+
// Some expressions require knowledge of the number of variable slots consumed.
48+
if (ir.hasUsesVarOffsetTrait(expr)) {
49+
expr.varOffset = varCount;
50+
}
5151

52-
if (ir.hasConsumesVarsTrait(expr)) {
53-
varCount += varsUsedByIrExpression(expr);
54-
}
55-
});
56-
}
52+
if (ir.hasConsumesVarsTrait(expr)) {
53+
varCount += varsUsedByIrExpression(expr);
54+
}
55+
};
5756

5857
// Compatibility mode pass for pure function offsets (as explained above).
58+
const secondPassCountExpressionVars = (expr: o.Expression) => {
59+
if (!ir.isIrExpression(expr) || !(expr instanceof ir.PureFunctionExpr)) {
60+
return;
61+
}
62+
63+
// Some expressions require knowledge of the number of variable slots consumed.
64+
if (ir.hasUsesVarOffsetTrait(expr)) {
65+
expr.varOffset = varCount;
66+
}
67+
68+
if (ir.hasConsumesVarsTrait(expr)) {
69+
varCount += varsUsedByIrExpression(expr);
70+
}
71+
};
72+
73+
// Note: we iterate over `create` and `update` separately, instead of going using `unit.ops()`,
74+
// because `unit.ops()` will visit nested ops too (e.g. the `ListenerOp.handlerOps`). We
75+
// don't want that, because the `visitExpressionsInOp` call below will visit the same nested
76+
// ops again, leading to vars in some expressions to be counted twice.
77+
for (const createOp of unit.create) {
78+
ir.visitExpressionsInOp(createOp, firstPassCountExpressionVars);
79+
}
80+
for (const updateOp of unit.update) {
81+
ir.visitExpressionsInOp(updateOp, firstPassCountExpressionVars);
82+
}
83+
5984
if (job.compatibility === ir.CompatibilityMode.TemplateDefinitionBuilder) {
60-
for (const op of unit.ops()) {
61-
ir.visitExpressionsInOp(op, (expr) => {
62-
if (!ir.isIrExpression(expr) || !(expr instanceof ir.PureFunctionExpr)) {
63-
return;
64-
}
65-
66-
// Some expressions require knowledge of the number of variable slots consumed.
67-
if (ir.hasUsesVarOffsetTrait(expr)) {
68-
expr.varOffset = varCount;
69-
}
70-
71-
if (ir.hasConsumesVarsTrait(expr)) {
72-
varCount += varsUsedByIrExpression(expr);
73-
}
74-
});
85+
for (const createOp of unit.create) {
86+
ir.visitExpressionsInOp(createOp, secondPassCountExpressionVars);
87+
}
88+
for (const updateOp of unit.update) {
89+
ir.visitExpressionsInOp(updateOp, secondPassCountExpressionVars);
7590
}
7691
}
7792

@@ -164,7 +179,7 @@ function varsUsedByOp(op: (ir.CreateOp | ir.UpdateOp) & ir.ConsumesVarsTrait): n
164179
}
165180
}
166181

167-
export function varsUsedByIrExpression(expr: ir.Expression & ir.ConsumesVarsTrait): number {
182+
function varsUsedByIrExpression(expr: ir.Expression & ir.ConsumesVarsTrait): number {
168183
switch (expr.kind) {
169184
case ir.ExpressionKind.PureFunctionExpr:
170185
return 1 + expr.args.length;

0 commit comments

Comments
 (0)