Lower all operands to temporaries

This PR changes BuildHIR to lower all operands to temporaries. Example: 

```javascript 

// Input 

a + b; 

// Previous Lowering 

Const t0 = BinaryOperation Place(a) "+" Place(b) 

// New Lowering 

Const t0 = Place(a); 

Const t1 = Place(b); 

BinaryOperation Place(t0) "+" Place(t1) 

``` 

This is necessary to ensure we're always referring to the correct version of a 
variable, even in the case of reassignment mid-expression. For example, we 
previously evaluated `let x=1; x + (x = 2) + x` incorrectly to 6 because we 
lowered the `x = 2` prior to the binary operators. We now lowers each instance 
of x to a temporary, ensuring they refer to the correct SSA version of the 
variable, and produce the correct result (5). 

Note that with this change, the _only_ place a variable can appear as an 
operator is when the InstructionValue is a raw identifier. This was already the 
case for globals (as of the LoadGlobal instruction). All other instruction value 
variants will only ever receive temporaries as arguments. 

This necessitated a few changes to our inference: 

* The logic to extend the range of phi operands (if the phi is mutated) was 
previously in LeaveSSA, but that was actually too late. The introduction of 
lowering to temporaries help discover failing cases, which I fixed earlier in 
the stack by moving the logic to extend the range of phi operands into the 
InferMutableRanges fixpoint loop. 

* PropagateScopeDependencies now has to track variable reassignments in addition 
to tracking property accesses 

* AnalyzeFunctions now has to track variable reassignments in addition to 
tracking property accesses 

* InferReactiveIdentifiers now needs a fixpoint iteration, because identifiers 
don't directly appear together in the same instruction anymore (such that we can 
directly propagate the reactivity between them). Instead, we'll first see that 
the temporaries are reactive, and have to propagate that back to the identifiers 
the temporaries were loaded from. 

Overall while this does introduce a bit more complexity, it also makes the 
compiler more robust. As with the phi example illustrates, there are legitimate 
inputs that can create similar indirections to that introduced by lowering 
identifiers to temporaries. 

Note that there’s a theme to the changes here: several analysis passes need to 
map an operand back to its identifier value. Ideally our HIR structure would 
directly support looking up the value for a temporary. For example, if operands 
were references to eg the index of the instruction that produced them. Because 
we don’t have such a representation yet (it would fall out naturally if we were 
writing in Rust), we have to do some bookkeeping. The key takeaway here is that 
this bookkeeping is incidental complexity given our current representation, not 
fundamental complexity of the algorithm.
This commit is contained in:
Joe Savona
2023-02-22 15:53:23 -08:00
parent 256071460d
commit 1dfaf8a94b
38 changed files with 330 additions and 245 deletions
+75 -49
View File
@@ -127,7 +127,7 @@ export function lower(
const terminal: ReturnTerminal = {
kind: "return",
loc: GeneratedSource,
value: lowerExpressionToPlace(builder, body),
value: lowerExpressionToTemporary(builder, body),
id: makeInstructionId(0),
};
builder.terminateWithContinuation(terminal, fallthrough);
@@ -169,7 +169,7 @@ function lowerStatement(
switch (stmtNode.type) {
case "ThrowStatement": {
const stmt = stmtPath as NodePath<t.ThrowStatement>;
const value = lowerExpressionToPlace(builder, stmt.get("argument"));
const value = lowerExpressionToTemporary(builder, stmt.get("argument"));
const terminal: ThrowTerminal = {
kind: "throw",
value,
@@ -183,7 +183,10 @@ function lowerStatement(
const argument = stmt.get("argument");
const value =
argument.node != null
? lowerExpressionToPlace(builder, argument as NodePath<t.Expression>)
? lowerExpressionToTemporary(
builder,
argument as NodePath<t.Expression>
)
: null;
const terminal: ReturnTerminal = {
kind: "return",
@@ -225,7 +228,7 @@ function lowerStatement(
// If there is no else clause, use the continuation directly
alternateBlock = continuationBlock.id;
}
const test = lowerExpressionToPlace(builder, stmt.get("test"));
const test = lowerExpressionToTemporary(builder, stmt.get("test"));
const terminal: IfTerminal = {
kind: "if",
test,
@@ -353,7 +356,7 @@ function lowerStatement(
builder.terminateWithContinuation(
{
kind: "branch",
test: lowerExpressionToPlace(
test: lowerExpressionToTemporary(
builder,
test as NodePath<t.Expression>
),
@@ -409,7 +412,7 @@ function lowerStatement(
* The conditional block is empty and exists solely as conditional for
* (re)entering or exiting the loop
*/
const test = lowerExpressionToPlace(builder, stmt.get("test"));
const test = lowerExpressionToTemporary(builder, stmt.get("test"));
const terminal: BranchTerminal = {
kind: "branch",
test,
@@ -525,7 +528,7 @@ function lowerStatement(
});
}
}
test = lowerExpressionToPlace(
test = lowerExpressionToTemporary(
builder,
testExpr as NodePath<t.Expression>
);
@@ -549,7 +552,10 @@ function lowerStatement(
cases.push({ test: null, block: continuationBlock.id });
}
const test = lowerExpressionToPlace(builder, stmt.get("discriminant"));
const test = lowerExpressionToTemporary(
builder,
stmt.get("discriminant")
);
builder.terminateWithContinuation(
{
kind: "switch",
@@ -772,7 +778,7 @@ function lowerExpression(
hasError = true;
continue;
}
const value = lowerExpressionToPlace(builder, valuePath);
const value = lowerExpressionToTemporary(builder, valuePath);
properties.set(key.name, value);
}
return hasError
@@ -798,7 +804,7 @@ function lowerExpression(
continue;
}
elements.push(
lowerExpressionToPlace(builder, element as NodePath<t.Expression>)
lowerExpressionToTemporary(builder, element as NodePath<t.Expression>)
);
}
return hasError
@@ -820,7 +826,7 @@ function lowerExpression(
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
const callee = lowerExpressionToPlace(builder, calleePath);
const callee = lowerExpressionToTemporary(builder, calleePath);
let args: Place[] = [];
let hasError = false;
for (const argPath of expr.get("arguments")) {
@@ -833,7 +839,7 @@ function lowerExpression(
hasError = true;
continue;
}
args.push(lowerExpressionToPlace(builder, argPath));
args.push(lowerExpressionToTemporary(builder, argPath));
}
return hasError
@@ -873,7 +879,7 @@ function lowerExpression(
hasError = true;
continue;
}
args.push(lowerExpressionToPlace(builder, argPath));
args.push(lowerExpressionToTemporary(builder, argPath));
}
if (typeof property === "string") {
return {
@@ -893,7 +899,7 @@ function lowerExpression(
};
}
} else {
const callee = lowerExpressionToPlace(builder, calleePath);
const callee = lowerExpressionToTemporary(builder, calleePath);
let args: Place[] = [];
for (const argPath of expr.get("arguments")) {
if (!argPath.isExpression()) {
@@ -905,7 +911,7 @@ function lowerExpression(
hasError = true;
continue;
}
args.push(lowerExpressionToPlace(builder, argPath));
args.push(lowerExpressionToTemporary(builder, argPath));
}
return hasError
? { kind: "UnsupportedNode", node: exprNode, loc: exprLoc }
@@ -928,8 +934,8 @@ function lowerExpression(
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
const left = lowerExpressionToPlace(builder, leftPath);
const right = lowerExpressionToPlace(builder, expr.get("right"));
const left = lowerExpressionToTemporary(builder, leftPath);
const right = lowerExpressionToTemporary(builder, expr.get("right"));
const operator = expr.node.operator;
return {
kind: "BinaryExpression",
@@ -945,7 +951,7 @@ function lowerExpression(
let last: Place | null = null;
for (const item of expr.get("expressions")) {
last = lowerExpressionToPlace(builder, item);
last = lowerExpressionToTemporary(builder, item);
}
if (last === null) {
builder.errors.push({
@@ -971,7 +977,7 @@ function lowerExpression(
builder.push({
id: makeInstructionId(0),
lvalue: { kind: InstructionKind.Reassign, place: { ...place } },
value: lowerExpressionToPlace(builder, expr.get("consequent")),
value: lowerExpressionToTemporary(builder, expr.get("consequent")),
loc: exprLoc,
});
return {
@@ -986,7 +992,7 @@ function lowerExpression(
builder.push({
id: makeInstructionId(0),
lvalue: { kind: InstructionKind.Reassign, place: { ...place } },
value: lowerExpressionToPlace(builder, expr.get("alternate")),
value: lowerExpressionToTemporary(builder, expr.get("alternate")),
loc: exprLoc,
});
return {
@@ -1007,7 +1013,7 @@ function lowerExpression(
},
testBlock
);
const testPlace = lowerExpressionToPlace(builder, expr.get("test"));
const testPlace = lowerExpressionToTemporary(builder, expr.get("test"));
builder.terminateWithContinuation(
{
kind: "branch",
@@ -1048,7 +1054,7 @@ function lowerExpression(
builder.push({
id: makeInstructionId(0),
lvalue: { kind: InstructionKind.Reassign, place: { ...place } },
value: lowerExpressionToPlace(builder, expr.get("right")),
value: lowerExpressionToTemporary(builder, expr.get("right")),
loc: exprLoc,
});
return {
@@ -1072,7 +1078,7 @@ function lowerExpression(
builder.push({
id: makeInstructionId(0),
lvalue: { kind: InstructionKind.Reassign, place: { ...leftPlace } },
value: lowerExpressionToPlace(builder, expr.get("left")),
value: lowerExpressionToTemporary(builder, expr.get("left")),
loc: exprLoc,
});
builder.terminateWithContinuation(
@@ -1093,7 +1099,7 @@ function lowerExpression(
if (builder.currentBlockKind() === "value") {
// try lowering the RHS in case it also contains errors
lowerExpressionToPlace(builder, expr.get("right"));
lowerExpressionToTemporary(builder, expr.get("right"));
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle AssignmentExpression within a LogicalExpression or ConditionalExpression`,
severity: ErrorSeverity.Todo,
@@ -1109,7 +1115,8 @@ function lowerExpression(
left.node.loc ?? GeneratedSource,
InstructionKind.Reassign,
left,
lowerExpression(builder, expr.get("right"))
// NOTE: it's okay not to lower to a temporary here because this is the entire RHS value, not a single operand
lowerExpressionToPlace(builder, expr.get("right"))
);
}
@@ -1130,7 +1137,7 @@ function lowerExpression(
const binaryOperator = operators[operator];
if (binaryOperator == null) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${operator} operaators in AssignmentExpression`,
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
severity: ErrorSeverity.Todo,
nodePath: expr.get("operator"),
});
@@ -1141,8 +1148,8 @@ function lowerExpression(
switch (leftNode.type) {
case "Identifier": {
const leftExpr = left as NodePath<t.Identifier>;
const place = lowerExpressionToPlace(builder, leftExpr);
const right = lowerExpressionToPlace(builder, expr.get("right"));
const place = lowerIdentifier(builder, leftExpr);
const right = lowerExpressionToTemporary(builder, expr.get("right"));
builder.push({
id: makeInstructionId(0),
lvalue: { place: { ...place }, kind: InstructionKind.Reassign },
@@ -1191,7 +1198,7 @@ function lowerExpression(
kind: "BinaryExpression",
operator: binaryOperator,
left: { ...previousValuePlace },
right: lowerExpressionToPlace(builder, expr.get("right")),
right: lowerExpressionToTemporary(builder, expr.get("right")),
loc: leftExpr.node.loc ?? GeneratedSource,
},
loc: leftExpr.node.loc ?? GeneratedSource,
@@ -1252,7 +1259,7 @@ function lowerExpression(
let hasError = false;
for (const attribute of opening.get("attributes")) {
if (attribute.isJSXSpreadAttribute()) {
const argument = lowerExpressionToPlace(
const argument = lowerExpressionToTemporary(
builder,
attribute.get("argument")
);
@@ -1281,7 +1288,7 @@ function lowerExpression(
const valueExpr = attribute.get("value");
let value;
if (valueExpr.isJSXElement() || valueExpr.isStringLiteral()) {
value = lowerExpressionToPlace(builder, valueExpr);
value = lowerExpressionToTemporary(builder, valueExpr);
} else {
if (!valueExpr.isJSXExpressionContainer()) {
builder.errors.push({
@@ -1302,7 +1309,7 @@ function lowerExpression(
hasError = true;
continue;
}
value = lowerExpressionToPlace(builder, expression);
value = lowerExpressionToTemporary(builder, expression);
}
const prop: string = name.node.name;
props.push({ kind: "JsxAttribute", name: prop, place: value });
@@ -1402,7 +1409,7 @@ function lowerExpression(
return {
kind: "TaggedTemplateExpression",
tag: lowerExpressionToPlace(builder, expr.get("tag")),
tag: lowerExpressionToTemporary(builder, expr.get("tag")),
value,
loc: exprLoc,
};
@@ -1431,7 +1438,7 @@ function lowerExpression(
}
const subexprPlaces = subexprs.map((e) =>
lowerExpressionToPlace(builder, e as NodePath<t.Expression>)
lowerExpressionToTemporary(builder, e as NodePath<t.Expression>)
);
return {
@@ -1446,7 +1453,7 @@ function lowerExpression(
return {
kind: "UnaryExpression",
operator: expr.node.operator,
value: lowerExpressionToPlace(builder, expr.get("argument")),
value: lowerExpressionToTemporary(builder, expr.get("argument")),
loc: exprLoc,
};
}
@@ -1454,7 +1461,7 @@ function lowerExpression(
let expr = exprPath as NodePath<t.TypeCastExpression>;
return {
kind: "TypeCastExpression",
value: lowerExpressionToPlace(builder, expr.get("expression")),
value: lowerExpressionToTemporary(builder, expr.get("expression")),
type: expr.get("typeAnnotation").node,
loc: exprLoc,
};
@@ -1493,7 +1500,7 @@ function lowerExpression(
loc: expr.node.loc ?? GeneratedSource,
});
const identifier = argument as NodePath<t.Identifier>;
const place = lowerExpressionToPlace(builder, identifier);
const place = lowerIdentifier(builder, identifier);
builder.push({
id: makeInstructionId(0),
lvalue: { place: { ...place }, kind: InstructionKind.Reassign },
@@ -1525,7 +1532,7 @@ function lowerMemberExpression(
): { object: Place; property: Place | string; value: InstructionValue } {
const exprNode = expr.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
const object = lowerExpressionToPlace(builder, expr.get("object"));
const object = lowerExpressionToTemporary(builder, expr.get("object"));
const property = expr.get("property");
if (!expr.node.computed) {
if (!property.isIdentifier()) {
@@ -1565,7 +1572,7 @@ function lowerMemberExpression(
},
};
}
const propertyPlace = lowerExpressionToPlace(builder, property);
const propertyPlace = lowerExpressionToTemporary(builder, property);
const value: InstructionValue = {
kind: "ComputedLoad",
object: { ...object },
@@ -1635,7 +1642,7 @@ function lowerJsxElement(
const exprNode = exprPath.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
if (exprPath.isJSXElement() || exprPath.isJSXFragment()) {
return lowerExpressionToPlace(builder, exprPath);
return lowerExpressionToTemporary(builder, exprPath);
} else if (exprPath.isJSXExpressionContainer()) {
const expression = exprPath.get("expression");
if (!expression.isExpression()) {
@@ -1657,7 +1664,7 @@ function lowerJsxElement(
});
return { ...place };
}
return lowerExpressionToPlace(builder, expression);
return lowerExpressionToTemporary(builder, expression);
} else if (exprPath.isJSXText()) {
const place: Place = buildTemporaryPlace(builder, exprLoc);
builder.push({
@@ -1694,19 +1701,38 @@ function lowerJsxElement(
}
}
function lowerExpressionToPlace(
function lowerExpressionToTemporary(
builder: HIRBuilder,
exprPath: NodePath<t.Expression>
): Place {
const instr = lowerExpression(builder, exprPath);
if (instr.kind === "Identifier") {
return instr;
const value = lowerExpression(builder, exprPath);
if (value.kind === "Identifier" && value.identifier.name === null) {
return value;
}
const exprLoc = exprPath.node.loc ?? GeneratedSource;
const place: Place = buildTemporaryPlace(builder, exprLoc);
builder.push({
id: makeInstructionId(0),
value: instr,
value: value,
loc: exprLoc,
lvalue: { place: { ...place }, kind: InstructionKind.Const },
});
return place;
}
function lowerExpressionToPlace(
builder: HIRBuilder,
exprPath: NodePath<t.Expression>
): Place {
const value = lowerExpression(builder, exprPath);
if (value.kind === "Identifier") {
return value;
}
const exprLoc = exprPath.node.loc ?? GeneratedSource;
const place: Place = buildTemporaryPlace(builder, exprLoc);
builder.push({
id: makeInstructionId(0),
value: value,
loc: exprLoc,
lvalue: { place: { ...place }, kind: InstructionKind.Const },
});
@@ -1837,7 +1863,7 @@ function lowerAssignment(
case "MemberExpression": {
const lvalue = lvaluePath as NodePath<t.MemberExpression>;
const property = lvalue.get("property");
const object = lowerExpressionToPlace(builder, lvalue.get("object"));
const object = lowerExpressionToTemporary(builder, lvalue.get("object"));
let valuePlace: Place;
if (value.kind === "Identifier") {
valuePlace = value;
@@ -1876,7 +1902,7 @@ function lowerAssignment(
});
return { kind: "UnsupportedNode", node: lvalueNode, loc };
}
const propertyPlace = lowerExpressionToPlace(builder, property);
const propertyPlace = lowerExpressionToTemporary(builder, property);
return {
kind: "ComputedStore",
object,
@@ -2056,7 +2082,7 @@ function gatherCapturedDeps(
path.skip();
capturedIds.add(binding.identifier);
capturedRefs.add(lowerExpressionToPlace(builder, path));
capturedRefs.add(lowerExpressionToTemporary(builder, path));
},
});
@@ -1,11 +1,12 @@
import invariant from "invariant";
import {
HIRFunction,
Effect,
FunctionExpression,
HIRFunction,
Identifier,
mergeConsecutiveBlocks,
Place,
Effect,
ReactiveScopeDependency,
} from "../HIR";
import { constantPropagation } from "../Optimization";
import { eliminateRedundantPhi, enterSSA } from "../SSA";
@@ -14,48 +15,58 @@ import { logHIRFunction } from "../Utils/logger";
import { inferMutableRanges } from "./InferMutableRanges";
import inferReferenceEffects from "./InferReferenceEffects";
type Dependency = {
place: Place;
path: Array<string> | null;
};
class State {
properties: Map<Identifier, ReactiveScopeDependency> = new Map();
function declareProperty(
properties: Map<Identifier, Dependency>,
lvalue: Place,
object: Place,
property: string
): void {
const objectDependency = properties.get(object.identifier);
let nextDependency: Dependency;
if (objectDependency === undefined) {
nextDependency = { place: object, path: [property] };
} else {
nextDependency = {
place: objectDependency.place,
path: [...(objectDependency.path ?? []), property],
declareProperty(lvalue: Place, object: Place, property: string): void {
const objectDependency = this.properties.get(object.identifier);
let nextDependency: ReactiveScopeDependency;
if (objectDependency === undefined) {
nextDependency = { place: object, path: [property] };
} else {
nextDependency = {
place: objectDependency.place,
path: [...(objectDependency.path ?? []), property],
};
}
this.properties.set(lvalue.identifier, nextDependency);
}
declareTemporary(lvalue: Place, value: Place): void {
const resolved: ReactiveScopeDependency = this.properties.get(
value.identifier
) ?? {
place: value,
path: null,
};
this.properties.set(lvalue.identifier, resolved);
}
properties.set(lvalue.identifier, nextDependency);
}
export default function analyseFunctions(func: HIRFunction) {
const properties: Map<Identifier, Dependency> = new Map();
const state = new State();
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case "FunctionExpression": {
lower(instr.value.loweredFunc);
infer(instr.value, properties, func.context);
infer(instr.value, state, func.context);
break;
}
case "PropertyLoad": {
declareProperty(
properties,
state.declareProperty(
instr.lvalue.place,
instr.value.object,
instr.value.property
);
break;
}
case "Identifier": {
if (instr.lvalue.place.identifier.name === null) {
state.declareTemporary(instr.lvalue.place, instr.value);
}
break;
}
}
}
@@ -74,11 +85,7 @@ function lower(func: HIRFunction) {
logHIRFunction("AnalyseFunction (inner)", func);
}
function infer(
value: FunctionExpression,
properties: Map<Identifier, Dependency>,
context: Place[]
) {
function infer(value: FunctionExpression, state: State, context: Place[]) {
const mutations = new Set(
value.loweredFunc.context
.filter((dep) => isMutated(dep.identifier))
@@ -89,8 +96,8 @@ function infer(
for (const dep of value.dependencies) {
let name: string | null = null;
if (properties.has(dep.identifier)) {
const receiver = properties.get(dep.identifier)!;
if (state.properties.has(dep.identifier)) {
const receiver = state.properties.get(dep.identifier)!;
name = receiver.place.identifier.name;
} else {
name = dep.identifier.name;
@@ -11,7 +11,6 @@ import {
InstructionId,
Place,
} from "../HIR/HIR";
import { printInstructionValue } from "../HIR/PrintHIR";
import { eachInstructionValueOperand } from "../HIR/visitors";
import DisjointSet from "../Utils/DisjointSet";
@@ -25,30 +24,12 @@ export function inferAliasForStores(
if (lvalue.place.effect !== Effect.Store) {
continue;
}
switch (value.kind) {
case "ArrayExpression":
case "ObjectExpression":
case "ComputedStore":
case "PropertyStore":
case "FunctionExpression": {
for (const operand of eachInstructionValueOperand(value)) {
if (
operand.effect === Effect.Capture ||
operand.effect === Effect.Store
) {
maybeAlias(aliases, lvalue.place, operand, instr.id);
}
}
break;
}
default: {
// Effect.Capture & Effect.Store are only used for aliasing
// instructions.
throw new Error(
`Unexpected capture/store instruction: ${printInstructionValue(
value
)}`
);
for (const operand of eachInstructionValueOperand(value)) {
if (
operand.effect === Effect.Capture ||
operand.effect === Effect.Store
) {
maybeAlias(aliases, lvalue.place, operand, instr.id);
}
}
}
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { HIRFunction } from "../HIR/HIR";
import { HIRFunction, Identifier } from "../HIR/HIR";
import { inferAliases } from "./InferAlias";
import { inferAliasForStores } from "./InferAliasForStores";
import { inferMutableLifetimes } from "./InferMutableLifetimes";
@@ -17,18 +17,22 @@ export function inferMutableRanges(ir: HIRFunction) {
// Calculate aliases
const aliases = inferAliases(ir);
let size = aliases.size;
// Eagerly canonicalize so that if nothing changes we can bail out
// after a single iteration
aliases.canonicalize();
do {
size = aliases.size;
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
while (true) {
// Infer mutable ranges for aliases that are not fields
inferMutableRangesForAlias(ir, aliases);
// Update aliasing information of fields
inferAliasForStores(ir, aliases);
} while (aliases.size > size || !aliases.canonicalize());
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
prevAliases = nextAliases;
}
// Re-infer mutable ranges for all values
inferMutableLifetimes(ir, true);
@@ -36,3 +40,18 @@ export function inferMutableRanges(ir: HIRFunction) {
// Re-infer mutable ranges for aliases
inferMutableRangesForAlias(ir, aliases);
}
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
if (a.size !== b.size) {
return false;
}
for (const [key, value] of a) {
if (!b.has(key)) {
return false;
}
if (b.get(key) !== value) {
return false;
}
}
return true;
}
@@ -5,19 +5,6 @@ export function inferMutableRangesForAlias(
fn: HIRFunction,
aliases: DisjointSet<Identifier>
) {
for (const [_, block] of fn.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.id.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (isPhiMutatedAfterCreation) {
for (const [, operand] of phi.operands) {
aliases.union([phi.id, operand]);
}
}
}
}
const aliasSets = aliases.buildSets();
for (const aliasSet of aliasSets) {
// Update mutableRange.end only if the identifiers have actually been
@@ -44,4 +31,17 @@ export function inferMutableRangesForAlias(
}
}
}
for (const [_, block] of fn.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.id.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (isPhiMutatedAfterCreation) {
for (const [, operand] of phi.operands) {
aliases.union([phi.id, operand]);
}
}
}
}
}
@@ -28,18 +28,20 @@ class Visitor extends ReactiveFunctionVisitor<IdentifierReactivity> {
) {
this.traverseInstruction(instr, reactivityMap);
const lval = instr.lvalue;
if (lval == null || reactivityMap.get(lval.place.identifier.id) === true) {
if (lval == null) {
return;
}
const { value } = instr;
let hasReactiveInput = false;
for (const operand of eachReactiveValueOperand(value)) {
// We currently treat free variables (from module or global scope) as
// non-reactive. We may later want type information about specific
// free variables, or a toggle `treatFreeVarsAsReactive`.
if (reactivityMap.get(operand.identifier.id)) {
hasReactiveInput = true;
break;
let hasReactiveInput = reactivityMap.get(lval.place.identifier.id) === true;
if (!hasReactiveInput && value.kind !== "LoadGlobal") {
for (const operand of eachReactiveValueOperand(value)) {
// We currently treat free variables (from module or global scope) as
// non-reactive. We may later want type information about specific
// free variables, or a toggle `treatFreeVarsAsReactive`.
if (reactivityMap.get(operand.identifier.id)) {
hasReactiveInput = true;
break;
}
}
}
if (
@@ -136,7 +138,11 @@ export function inferReactiveIdentifiers(
for (const param of fn.params) {
reactivityMap.set(param.identifier.id, true);
}
visitReactiveFunction(fn, visitor, reactivityMap);
let size: number;
do {
size = reactivityMap.size;
visitReactiveFunction(fn, visitor, reactivityMap);
} while (reactivityMap.size > size);
const result = new Set<IdentifierId>();
reactivityMap.forEach((isReactive, id) => {
@@ -71,6 +71,7 @@ class Context {
// This helps with.. temporaries that are created only for property loads
// but can be generalized to all non-allocating temporaries
#properties: Map<Identifier, ReactiveScopeDependency> = new Map();
#temporaries: Map<Identifier, Place> = new Map();
#scopes: Scopes = [];
enter(scope: ReactiveScope, fn: () => void): Set<ReactiveScopeDependency> {
@@ -97,11 +98,16 @@ class Context {
this.#reassignments.set(identifier, decl);
}
declareTemporary(lvalue: Place, value: Place): void {
this.#temporaries.set(lvalue.identifier, value);
}
declareProperty(lvalue: Place, object: Place, property: string): void {
const objectDependency = this.#properties.get(object.identifier);
const resolvedObject = this.#temporaries.get(object.identifier) ?? object;
const objectDependency = this.#properties.get(resolvedObject.identifier);
let nextDependency: ReactiveScopeDependency;
if (objectDependency === undefined) {
nextDependency = { place: object, path: [property] };
nextDependency = { place: resolvedObject, path: [property] };
} else {
nextDependency = {
place: objectDependency.place,
@@ -120,14 +126,16 @@ class Context {
}
visitOperand(place: Place): void {
this.visitDependency({ place, path: null });
const resolved = this.#temporaries.get(place.identifier) ?? place;
this.visitDependency({ place: resolved, path: null });
}
visitProperty(object: Place, property: string): void {
const objectDependency = this.#properties.get(object.identifier);
const resolvedObject = this.#temporaries.get(object.identifier) ?? object;
const objectDependency = this.#properties.get(resolvedObject.identifier);
let nextDependency: ReactiveScopeDependency;
if (objectDependency === undefined) {
nextDependency = { place: object, path: [property] };
nextDependency = { place: resolvedObject, path: [property] };
} else {
nextDependency = {
place: objectDependency.place,
@@ -362,7 +370,16 @@ function visitInstructionValue(
value: ReactiveValue,
lvalue: LValue | null
): void {
if (value.kind === "PropertyLoad") {
if (value.kind === "Identifier" && lvalue !== null) {
if (
value.identifier.name !== null &&
lvalue.place.identifier.name === null
) {
context.declareTemporary(lvalue.place, value);
} else {
context.visitOperand(value);
}
} else if (value.kind === "PropertyLoad") {
if (lvalue !== null) {
context.declareProperty(lvalue.place, value.object, value.property);
} else {
+6 -9
View File
@@ -75,19 +75,16 @@ export default class DisjointSet<T> {
/**
* Forces the set into canonical form, ie with all items pointing directly to
* their root. Returns true if the set was already in canonical form, false
* otherwise.
* their root, and returns a Map representing the mapping of items to their roots.
*/
canonicalize(): boolean {
let isCanonical = true;
canonicalize(): Map<T, T> {
const entries = new Map<T, T>();
for (const item of this.#entries.keys()) {
const parent = this.#entries.get(item)!;
const root = this.find(item);
if (parent !== root) {
isCanonical = false;
}
const root = this.find(item)!;
entries.set(item, root);
}
return isCanonical;
return entries;
}
/**
@@ -28,12 +28,12 @@ function component(a) {
} else {
x = $[1];
}
const y = undefined;
(function () {
y = x;
})();
mutate(y);
return y;
mutate(undefined);
return undefined;
}
```
@@ -1,6 +0,0 @@
function f() {
let x = 1;
// BUG: `x` has different values within this expression. Currently, the
// assignment is evaluated too early.
return x + (x = 2) + x;
}
@@ -21,10 +21,11 @@ function Component(props) {
let x;
if (c_0) {
x = [props.x];
const index = 0;
x[index] = x[index] * 2;
const t0 = "0";
x[t0] = x[t0] + 3;
const t0 = 0;
x[t0] = x[t0] * 2;
const t1 = "0";
x[t1] = x[t1] + 3;
$[0] = props.x;
$[1] = x;
} else {
@@ -24,12 +24,14 @@ function useBar(props) {
let z = undefined;
if (props.a) {
if (props.b) {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
z = baz();
$[0] = z;
t0 = baz();
$[0] = t0;
} else {
z = $[0];
t0 = $[0];
}
z = t0;
}
}
return z;
@@ -4,7 +4,7 @@
```javascript
function component(a) {
let x = { a };
let y;
let y = {};
(function () {
y["x"] = x;
})();
@@ -23,16 +23,16 @@ function component(a) {
let y;
if (c_0) {
const x = { a: a };
y = undefined;
y = {};
(function () {
y["x"] = x;
})();
mutate(y);
$[0] = a;
$[1] = y;
} else {
y = $[1];
}
mutate(y);
return y;
}
@@ -1,6 +1,6 @@
function component(a) {
let x = { a };
let y;
let y = {};
(function () {
y["x"] = x;
})();
@@ -4,7 +4,7 @@
```javascript
function component(a) {
let x = { a };
let y;
let y = {};
(function () {
y.x = x;
})();
@@ -23,16 +23,16 @@ function component(a) {
let y;
if (c_0) {
const x = { a: a };
y = undefined;
y = {};
(function () {
y.x = x;
})();
mutate(y);
$[0] = a;
$[1] = y;
} else {
y = $[1];
}
mutate(y);
return y;
}
@@ -1,6 +1,6 @@
function component(a) {
let x = { a };
let y;
let y = {};
(function () {
y.x = x;
})();
@@ -19,9 +19,9 @@ function component({ mutator }) {
## Code
```javascript
function component(t15) {
function component(t23) {
const $ = React.unstable_useMemoCache(7);
const t0 = t15;
const t0 = t23;
const mutator = t0.mutator;
const c_0 = $[0] !== mutator;
let poke;
@@ -30,16 +30,17 @@ function component(a) {
z = $[1];
}
const c_2 = $[2] !== z;
let x;
let t0;
if (c_2) {
x = function () {
t0 = function () {
z;
};
$[2] = z;
$[3] = x;
$[3] = t0;
} else {
x = $[3];
t0 = $[3];
}
const x = t0;
return x;
}
@@ -28,9 +28,7 @@ function foo() {
```javascript
function foo() {
console.log("foo");
const j = -6;
return j;
return -6;
}
```
@@ -0,0 +1,21 @@
## Input
```javascript
function f(y) {
let x = y;
return x + (x = 2) + x;
}
```
## Code
```javascript
function f(y) {
const x = y;
return x + 2 + 2;
}
```
@@ -0,0 +1,4 @@
function f(y) {
let x = y;
return x + (x = 2) + x;
}
@@ -4,8 +4,6 @@
```javascript
function f() {
let x = 1;
// BUG: `x` has different values within this expression. Currently, the
// assignment is evaluated too early.
return x + (x = 2) + x;
}
@@ -15,7 +13,7 @@ function f() {
```javascript
function f() {
return 6;
return 5;
}
```
@@ -0,0 +1,4 @@
function f() {
let x = 1;
return x + (x = 2) + x;
}
@@ -33,12 +33,14 @@ function Component(props) {
if (cond) {
a = x;
} else {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
a = [];
$[0] = a;
t0 = [];
$[0] = t0;
} else {
a = $[0];
t0 = $[0];
}
a = t0;
}
useFreeze(a);
@@ -23,22 +23,26 @@ function foo(a, b, c, d) {
let x = undefined;
if (someVal) {
const c_0 = $[0] !== b;
let t0;
if (c_0) {
x = { b: b };
t0 = { b: b };
$[0] = b;
$[1] = x;
$[1] = t0;
} else {
x = $[1];
t0 = $[1];
}
x = t0;
} else {
const c_2 = $[2] !== c;
let t1;
if (c_2) {
x = { c: c };
t1 = { c: c };
$[2] = c;
$[3] = x;
$[3] = t1;
} else {
x = $[3];
t1 = $[3];
}
x = t1;
}
return x;
}
@@ -13,10 +13,10 @@ function component({ a, b }) {
## Code
```javascript
function component(t8) {
function component(t12) {
const $ = React.unstable_useMemoCache(7);
const a = t8.a;
const b = t8.b;
const a = t12.a;
const b = t12.b;
const c_0 = $[0] !== a;
let y;
if (c_0) {
@@ -36,27 +36,29 @@ function Component(props) {
}
const y = x;
if (props.p1) {
let t0;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
x = [];
$[2] = x;
t0 = [];
$[2] = t0;
} else {
x = $[2];
t0 = $[2];
}
x = t0;
}
y.push(props.p2);
const c_3 = $[3] !== x;
const c_4 = $[4] !== y;
let t0;
let t1;
if (c_3 || c_4) {
t0 = <Component x={x} y={y}></Component>;
t1 = <Component x={x} y={y}></Component>;
$[3] = x;
$[4] = y;
$[5] = t0;
$[5] = t1;
} else {
t0 = $[5];
t1 = $[5];
}
return t0;
return t1;
}
```
@@ -30,12 +30,14 @@ function Component(props) {
x = [];
x.push(props.p0);
y = x;
let t0;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
x = [];
$[4] = x;
t0 = [];
$[4] = t0;
} else {
x = $[4];
t0 = $[4];
}
x = t0;
y.push(props.p1);
$[0] = props.p0;
@@ -48,16 +50,16 @@ function Component(props) {
}
const c_5 = $[5] !== x;
const c_6 = $[6] !== y;
let t0;
let t1;
if (c_5 || c_6) {
t0 = <Component x={x} y={y}></Component>;
t1 = <Component x={x} y={y}></Component>;
$[5] = x;
$[6] = y;
$[7] = t0;
$[7] = t1;
} else {
t0 = $[7];
t1 = $[7];
}
return t0;
return t1;
}
```
@@ -18,18 +18,25 @@ function foo() {}
```javascript
function sequence(props) {
const $ = React.unstable_useMemoCache(1);
const $ = React.unstable_useMemoCache(2);
Math.max(1, 2);
let x;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = foo();
t0 = foo();
$[0] = t0;
} else {
t0 = $[0];
}
let x;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
x = t0;
while ((foo(), true)) {
foo();
x = 2;
}
$[0] = x;
$[1] = x;
} else {
x = $[0];
x = $[1];
}
return x;
}
@@ -16,11 +16,9 @@ function Component(props) {
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(1);
const a = 1;
const b = 2;
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = [a, b];
x = [1, 2];
$[0] = x;
} else {
x = $[0];
@@ -22,13 +22,12 @@ function foo(a, b, c) {
```javascript
// @xonly
function foo(a, b, c) {
const x = 0;
while (a) {
while (b) {
while (c) {}
}
}
return x;
return 0;
}
```
@@ -16,11 +16,9 @@ function Component(props) {
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(1);
const a = 1;
const b = 2;
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = { a: a, b: b };
x = { a: 1, b: 2 };
$[0] = x;
} else {
x = $[0];
@@ -25,8 +25,7 @@ function log() {}
function Foo(cond) {
let str = "";
if (cond) {
const str_0 = "other test";
log(str_0);
log("other test");
} else {
str = "fallthrough test";
}
@@ -28,8 +28,7 @@ function foo() {
```javascript
function foo() {
const x = 1;
bb1: switch (x) {
bb1: switch (1) {
case 1: {
break bb1;
}
@@ -17,9 +17,8 @@ function foo() {
```javascript
function foo() {
const x = 1;
while (true) {}
return x;
return 1;
}
```
@@ -46,12 +46,14 @@ function Component(props) {
}
case true: {
x.push(props.p2);
let t0;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
y = [];
$[4] = y;
t0 = [];
$[4] = t0;
} else {
y = $[4];
t0 = $[4];
}
y = t0;
break bb1;
}
default: {
@@ -81,16 +83,16 @@ function Component(props) {
y.push(props.p4);
const c_7 = $[7] !== y;
const c_8 = $[8] !== child;
let t0;
let t1;
if (c_7 || c_8) {
t0 = <Component data={y}>{child}</Component>;
t1 = <Component data={y}>{child}</Component>;
$[7] = y;
$[8] = child;
$[9] = t0;
$[9] = t1;
} else {
t0 = $[9];
t1 = $[9];
}
return t0;
return t1;
}
```
@@ -15,8 +15,7 @@ function component() {
```javascript
function component() {
const y = 2;
return y;
return 2;
}
```
@@ -17,8 +17,7 @@ function foo(a) {
```javascript
function foo(a) {
const x = 1;
return a + x;
return a + 1;
}
```