mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
RHS member expression converts to PropertyLoad
This is an incremental step to removing `Place.memberPath`. This PR changes how we handle MemberExpressions in rvalue position, converting to a new `PropertyLoad` InstructionValue variant. Example: ``` let x = a.b; x.y = b.c; => Const tmp1 = PropertyLoad a, 'b'; Const x = Place tmp1; Const tmp2 = PropertyLoad b, 'c'; Reassign x.y = tmp2 ``` That we already recently made a chance to ensure that _if_ the lvalue is a member expression, that we convert the RHS to a Place. So although `x.y = b.c` could technically be lowered to a single instruction (with the`b.c` as a PropertyLoad), we force this to a temporary to ensure that we can independently memoize the RHS value. The net result is that the following combinations are possible: * `x = y`, lvalue identifier, rvalue identifier * `x.y = y` lvalue member path, rvalue identifier * `x = y.z` lvalue identifier, rvalue property load As noted above, `x.y = a.b` no longer occurs (and there's an invariant for this in one of the passes). A follow-up PR will add a PropertyStore instruction so that we can remove member paths in lvalue position too.
This commit is contained in:
@@ -27,7 +27,7 @@ import {
|
||||
renameVariables,
|
||||
} from "./ReactiveScopes";
|
||||
import { eliminateRedundantPhi, enterSSA, leaveSSA } from "./SSA";
|
||||
import { logHIRFunction } from "./Utils/logger";
|
||||
import { logHIRFunction, logReactiveFunction } from "./Utils/logger";
|
||||
|
||||
export type CompilerResult = {
|
||||
ast: t.Function;
|
||||
@@ -68,11 +68,23 @@ export default function (
|
||||
logHIRFunction("inferReactiveScopes", ir);
|
||||
|
||||
const reactiveFunction = buildReactiveFunction(ir);
|
||||
logReactiveFunction("buildReactiveFunction", reactiveFunction);
|
||||
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
logReactiveFunction("pruneUnusedLabels", reactiveFunction);
|
||||
|
||||
flattenReactiveLoops(reactiveFunction);
|
||||
logReactiveFunction("flattenReactiveLoops", reactiveFunction);
|
||||
|
||||
propagateScopeDependencies(reactiveFunction);
|
||||
logReactiveFunction("propagateScopeDependencies", reactiveFunction);
|
||||
|
||||
pruneUnusedScopes(reactiveFunction);
|
||||
logReactiveFunction("pruneUnusedScopes", reactiveFunction);
|
||||
|
||||
renameVariables(reactiveFunction);
|
||||
logReactiveFunction("renameVariables", reactiveFunction);
|
||||
|
||||
const ast = codegenReactiveFunction(reactiveFunction);
|
||||
|
||||
return {
|
||||
|
||||
@@ -981,11 +981,17 @@ function lowerExpression(
|
||||
`Unhandled assignment operator '${operator}'`
|
||||
);
|
||||
|
||||
const left = lowerLVal(builder, expr.get("left"));
|
||||
const lvalue = lowerLVal(builder, expr.get("left"));
|
||||
const leftPath = expr.get("left");
|
||||
invariant(
|
||||
leftPath.isIdentifier() || leftPath.isMemberExpression(),
|
||||
"Expected assignment expression lvalue to be an identifier or member expression"
|
||||
);
|
||||
const left = lowerExpressionToPlace(builder, leftPath);
|
||||
const right = lowerExpressionToPlace(builder, expr.get("right"));
|
||||
builder.push({
|
||||
id: makeInstructionId(0),
|
||||
lvalue: { place: left, kind: InstructionKind.Reassign },
|
||||
lvalue: { place: lvalue, kind: InstructionKind.Reassign },
|
||||
value: {
|
||||
kind: "BinaryExpression",
|
||||
operator: binaryOperator,
|
||||
@@ -995,7 +1001,7 @@ function lowerExpression(
|
||||
},
|
||||
loc: exprLoc,
|
||||
});
|
||||
return left;
|
||||
return lvalue;
|
||||
}
|
||||
case "MemberExpression": {
|
||||
const expr = exprPath as NodePath<t.MemberExpression>;
|
||||
@@ -1006,13 +1012,25 @@ function lowerExpression(
|
||||
property.isIdentifier(),
|
||||
"Handle non-identifier properties"
|
||||
);
|
||||
const place: Place = {
|
||||
kind: "Identifier",
|
||||
identifier: object.identifier,
|
||||
memberPath: [...(object.memberPath ?? []), property.node.name],
|
||||
effect: Effect.Unknown,
|
||||
const value: InstructionValue = {
|
||||
kind: "PropertyLoad",
|
||||
object,
|
||||
property: property.node.name,
|
||||
loc: exprLoc,
|
||||
};
|
||||
const place: Place = {
|
||||
kind: "Identifier",
|
||||
identifier: builder.makeTemporary(),
|
||||
memberPath: null,
|
||||
effect: Effect.Read,
|
||||
loc: exprLoc,
|
||||
};
|
||||
builder.push({
|
||||
id: makeInstructionId(0),
|
||||
lvalue: { place: { ...place }, kind: InstructionKind.Const },
|
||||
value,
|
||||
loc: exprLoc,
|
||||
});
|
||||
return place;
|
||||
}
|
||||
case "JSXElement": {
|
||||
|
||||
@@ -458,6 +458,13 @@ export function codegenInstructionValue(
|
||||
value = node;
|
||||
break;
|
||||
}
|
||||
case "PropertyLoad": {
|
||||
value = t.memberExpression(
|
||||
codegenPlace(temp, instrValue.object),
|
||||
t.identifier(instrValue.property)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "Identifier": {
|
||||
value = codegenPlace(temp, instrValue);
|
||||
break;
|
||||
|
||||
@@ -293,6 +293,11 @@ export type InstructionData =
|
||||
| { kind: "ArrayExpression"; elements: Array<Place> }
|
||||
| { kind: "JsxFragment"; children: Array<Place> }
|
||||
|
||||
// store `object.property = value`
|
||||
// | { kind: "PropertyStore"; object: Place; property: string; value: Place }
|
||||
// load `object.property`
|
||||
| { kind: "PropertyLoad"; object: Place; property: string }
|
||||
|
||||
/**
|
||||
* Catch-all for statements such as type imports, nested class declarations, etc
|
||||
* which are not directly represented, but included for completeness and to allow
|
||||
|
||||
@@ -35,6 +35,10 @@ function inferInstr(instr: Instruction, state: AliasAnalyser) {
|
||||
alias = instrValue;
|
||||
break;
|
||||
}
|
||||
case "PropertyLoad": {
|
||||
alias = instrValue.object;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { HIRFunction } from "./HIR";
|
||||
import { inferAliases } from "./InferAlias";
|
||||
import { inferAliasForStores } from "./InferAliasForStores";
|
||||
@@ -11,6 +18,9 @@ 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;
|
||||
// Infer mutable ranges for aliases that are not fields
|
||||
@@ -18,7 +28,7 @@ export function inferMutableRanges(ir: HIRFunction) {
|
||||
|
||||
// Update aliasing information of fields
|
||||
inferAliasForStores(ir, aliases);
|
||||
} while (aliases.size > size);
|
||||
} while (aliases.size > size || !aliases.canonicalize());
|
||||
|
||||
// Re-infer mutable ranges for all values
|
||||
inferMutableLifetimes(ir, true);
|
||||
|
||||
@@ -229,6 +229,10 @@ class Environment {
|
||||
this.#variables.set(place.identifier.id, new Set([value]));
|
||||
}
|
||||
|
||||
isDefined(place: Place): boolean {
|
||||
return this.#variables.has(place.identifier.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that a given Place was accessed with the given kind and:
|
||||
* - Updates the effect of @param place based on the kind of value
|
||||
@@ -571,7 +575,35 @@ function inferBlock(env: Environment, block: BasicBlock) {
|
||||
valueKind = ValueKind.Immutable;
|
||||
break;
|
||||
}
|
||||
case "PropertyLoad": {
|
||||
if (!env.isDefined(instrValue.object)) {
|
||||
// TODO @josephsavona: improve handling of globals
|
||||
const value: InstructionValue = {
|
||||
kind: "Primitive",
|
||||
loc: instrValue.loc,
|
||||
value: undefined,
|
||||
};
|
||||
env.initialize(value, ValueKind.Frozen);
|
||||
env.define(instrValue.object, value);
|
||||
}
|
||||
|
||||
env.reference(instrValue.object, Effect.Read);
|
||||
const lvalue = instr.lvalue;
|
||||
if (lvalue !== null) {
|
||||
invariant(
|
||||
lvalue.place.memberPath === null,
|
||||
"PropertyLoad must always be saved to a temporary"
|
||||
);
|
||||
env.initialize(instrValue, env.kind(instrValue.object));
|
||||
env.define(lvalue.place, instrValue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case "Identifier": {
|
||||
invariant(
|
||||
instrValue.memberPath === null,
|
||||
"Expected RHS memberPath to be lowered to PropertyLoad"
|
||||
);
|
||||
env.reference(instrValue, Effect.Read);
|
||||
const lvalue = instr.lvalue;
|
||||
if (lvalue !== null) {
|
||||
@@ -582,14 +614,8 @@ function inferBlock(env: Environment, block: BasicBlock) {
|
||||
) {
|
||||
// direct aliasing: `a = b`;
|
||||
env.alias(lvalue.place, instrValue);
|
||||
} else if (lvalue.place.memberPath === null) {
|
||||
// redefine lvalue: `a = b.c.d`
|
||||
env.initialize(instrValue, env.kind(instrValue));
|
||||
env.define(lvalue.place, instrValue);
|
||||
} else {
|
||||
// no-op: `a.b.c = d`
|
||||
// or
|
||||
// no-op: `a.b.c = d.e.f`
|
||||
const effect = isObjectType(lvalue.place.identifier)
|
||||
? Effect.Store
|
||||
: Effect.Mutate;
|
||||
|
||||
@@ -265,6 +265,12 @@ export function printInstructionValue(instrValue: InstructionValue): string {
|
||||
value = printPlace(instrValue);
|
||||
break;
|
||||
}
|
||||
case "PropertyLoad": {
|
||||
value = `PropertyLoad ${printPlace(instrValue.object)}.${
|
||||
instrValue.property
|
||||
}`;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
instrValue,
|
||||
@@ -291,7 +297,6 @@ function printMutableRange(identifier: Identifier): string {
|
||||
|
||||
export function printLValue(lval: LValue): string {
|
||||
let place = printPlace(lval.place);
|
||||
place += printMutableRange(lval.place.identifier);
|
||||
switch (lval.kind) {
|
||||
case InstructionKind.Let: {
|
||||
return `Let ${place}`;
|
||||
|
||||
@@ -41,6 +41,10 @@ export function* eachInstructionValueOperand(
|
||||
yield instrValue;
|
||||
break;
|
||||
}
|
||||
case "PropertyLoad": {
|
||||
yield instrValue.object;
|
||||
break;
|
||||
}
|
||||
case "UnaryExpression": {
|
||||
yield instrValue.value;
|
||||
break;
|
||||
@@ -92,6 +96,10 @@ export function mapInstructionOperands(
|
||||
instrValue.right = fn(instrValue.right);
|
||||
break;
|
||||
}
|
||||
case "PropertyLoad": {
|
||||
instrValue.object = fn(instrValue.object);
|
||||
break;
|
||||
}
|
||||
case "Identifier": {
|
||||
instr.value = fn(instrValue);
|
||||
break;
|
||||
|
||||
@@ -164,6 +164,7 @@ function mayAllocate(value: InstructionValue): boolean {
|
||||
switch (value.kind) {
|
||||
case "BinaryExpression":
|
||||
case "Identifier":
|
||||
case "PropertyLoad":
|
||||
case "JSXText":
|
||||
case "Primitive": {
|
||||
return false;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
LValue,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
ReactiveBlock,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
ReactiveValueBlock,
|
||||
} from "../HIR/HIR";
|
||||
import { eachInstructionValueOperand } from "../HIR/visitors";
|
||||
import { invariant } from "../Utils/CompilerError";
|
||||
import { assertExhaustive } from "../Utils/utils";
|
||||
|
||||
/**
|
||||
@@ -28,18 +30,17 @@ import { assertExhaustive } from "../Utils/utils";
|
||||
* their direct dependencies and those of their child scopes.
|
||||
*/
|
||||
export function propagateScopeDependencies(fn: ReactiveFunction): void {
|
||||
const dependencies: Set<Place> = new Set();
|
||||
const declarations: DeclMap = new Map();
|
||||
const context = new Context();
|
||||
if (fn.id !== null) {
|
||||
declarations.set(fn.id, { kind: DeclKind.Const, id: makeInstructionId(0) });
|
||||
context.declare(fn.id, { kind: DeclKind.Const, id: makeInstructionId(0) });
|
||||
}
|
||||
for (const param of fn.params) {
|
||||
declarations.set(param.identifier, {
|
||||
context.declare(param.identifier, {
|
||||
kind: DeclKind.Dynamic,
|
||||
id: makeInstructionId(0),
|
||||
});
|
||||
}
|
||||
visit(fn.body, dependencies, declarations, []);
|
||||
visit(context, fn.body);
|
||||
}
|
||||
|
||||
enum DeclKind {
|
||||
@@ -47,40 +48,151 @@ enum DeclKind {
|
||||
Dynamic = "Dynamic",
|
||||
}
|
||||
|
||||
type DeclMap = Map<Identifier, { kind: DeclKind; id: InstructionId }>;
|
||||
type DeclMap = Map<Identifier, Decl>;
|
||||
type Decl = { kind: DeclKind; id: InstructionId };
|
||||
|
||||
type Scopes = Array<ReactiveScope>;
|
||||
|
||||
function visit(
|
||||
block: ReactiveBlock,
|
||||
dependencies: Set<Place>,
|
||||
declarations: DeclMap,
|
||||
scopes: Scopes
|
||||
): void {
|
||||
class Context {
|
||||
#declarations: DeclMap = new Map();
|
||||
#dependencies: Set<Place> = new Set();
|
||||
#properties: Map<Identifier, Place> = new Map();
|
||||
#scopes: Scopes = [];
|
||||
|
||||
enter(scope: ReactiveScope, fn: () => void): Set<Place> {
|
||||
const previousDependencies = this.#dependencies;
|
||||
const scopedDependencies = new Set<Place>();
|
||||
this.#dependencies = scopedDependencies;
|
||||
this.#scopes.push(scope);
|
||||
fn();
|
||||
this.#scopes.pop();
|
||||
this.#dependencies = previousDependencies;
|
||||
return scopedDependencies;
|
||||
}
|
||||
|
||||
declare(identifier: Identifier, decl: Decl): void {
|
||||
this.#declarations.set(identifier, decl);
|
||||
}
|
||||
|
||||
declareProperty(lvalue: Place, object: Place, property: string): void {
|
||||
invariant(
|
||||
lvalue.memberPath === null,
|
||||
"Expected property loads to be stored to a temporary (no member path)"
|
||||
);
|
||||
invariant(
|
||||
object.memberPath === null,
|
||||
"Expected operands to have null memberPath"
|
||||
);
|
||||
const objectPlace = this.#properties.get(object.identifier);
|
||||
let place: Place;
|
||||
if (objectPlace === undefined) {
|
||||
place = { ...object, memberPath: [property] };
|
||||
} else {
|
||||
place = {
|
||||
...objectPlace,
|
||||
memberPath: [...(objectPlace.memberPath ?? []), property],
|
||||
};
|
||||
}
|
||||
this.#properties.set(lvalue.identifier, place);
|
||||
}
|
||||
|
||||
#isScopeActive(scope: ReactiveScope): boolean {
|
||||
return this.#scopes.indexOf(scope) !== -1;
|
||||
}
|
||||
|
||||
get #currentScope(): ReactiveScope {
|
||||
return this.#scopes.at(-1)!;
|
||||
}
|
||||
|
||||
visitOperand(operand: Place): void {
|
||||
let maybeDependency: Place;
|
||||
if (operand.memberPath !== null) {
|
||||
// Operands may have memberPaths when propagating depenencies of an inner scope upward
|
||||
// In this case we use the dependency as-is
|
||||
maybeDependency = operand;
|
||||
} else {
|
||||
// Otherwise if this operand is a temporary created for a property load, resolve it to
|
||||
// the expanded Place. Fall back to using the operand as-is.
|
||||
maybeDependency = this.#properties.get(operand.identifier) ?? operand;
|
||||
}
|
||||
|
||||
const decl = this.#declarations.get(maybeDependency.identifier);
|
||||
|
||||
// Any value used after its defining scope has concluded must be added as an
|
||||
// output of its defining scope. Regardless of whether its a const or not,
|
||||
// some later code needs access to the value.
|
||||
if (decl !== undefined) {
|
||||
const operandScope = maybeDependency.identifier.scope;
|
||||
if (operandScope !== null && !this.#isScopeActive(operandScope)) {
|
||||
operandScope.outputs.add(maybeDependency.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// If this operand is used in a scope, has a dynamic value, and was defined
|
||||
// before this scope, then its a dependency of the scope.
|
||||
const currentScope = this.#currentScope;
|
||||
if (
|
||||
decl !== undefined &&
|
||||
decl.kind !== DeclKind.Const &&
|
||||
currentScope !== undefined &&
|
||||
decl.id < currentScope.range.start
|
||||
) {
|
||||
// Check if there is an existing dependency that describes this operand
|
||||
for (const dep of this.#dependencies) {
|
||||
// not the same identifier
|
||||
if (dep.identifier !== maybeDependency.identifier) {
|
||||
continue;
|
||||
}
|
||||
const depPath = dep.memberPath;
|
||||
// existing dep covers all paths
|
||||
if (depPath === null) {
|
||||
return;
|
||||
}
|
||||
const operandPath = maybeDependency.memberPath;
|
||||
// existing dep is for a path, this operand covers all paths so swap them
|
||||
if (operandPath === null) {
|
||||
this.#dependencies.delete(dep);
|
||||
this.#dependencies.add(maybeDependency);
|
||||
return;
|
||||
}
|
||||
// both the operand and dep have paths, determine if the existing path
|
||||
// is a subset of the new path
|
||||
let commonPathIndex = 0;
|
||||
while (
|
||||
commonPathIndex < operandPath.length &&
|
||||
commonPathIndex < depPath.length &&
|
||||
operandPath[commonPathIndex] === depPath[commonPathIndex]
|
||||
) {
|
||||
commonPathIndex++;
|
||||
}
|
||||
if (commonPathIndex === depPath.length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.#dependencies.add(maybeDependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function visit(context: Context, block: ReactiveBlock): void {
|
||||
for (const item of block) {
|
||||
switch (item.kind) {
|
||||
case "scope": {
|
||||
const scopeDependencies: Set<Place> = new Set();
|
||||
// TODO: it would be sufficient to use a single mapping of declarations
|
||||
const scopeDeclarations: DeclMap = new Map(declarations);
|
||||
scopes.push(item.scope);
|
||||
visit(item.instructions, scopeDependencies, scopeDeclarations, scopes);
|
||||
scopes.pop();
|
||||
const scopeDependencies = context.enter(item.scope, () => {
|
||||
visit(context, item.instructions);
|
||||
});
|
||||
item.scope.dependencies = scopeDependencies;
|
||||
for (const dep of scopeDependencies) {
|
||||
// propagate dependencies upward using the same rules as
|
||||
// normal dependency collection. child scopes may have dependencies
|
||||
// on values created within the outer scope, which necessarily cannot
|
||||
// be dependencies of the outer scope
|
||||
visitOperand(dep, dependencies, declarations, scopes);
|
||||
}
|
||||
for (const [ident, kind] of scopeDeclarations) {
|
||||
declarations.set(ident, kind);
|
||||
context.visitOperand(dep);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "instruction": {
|
||||
visitInstruction(item.instruction, dependencies, declarations, scopes);
|
||||
visitInstruction(context, item.instruction);
|
||||
break;
|
||||
}
|
||||
case "terminal": {
|
||||
@@ -92,44 +204,39 @@ function visit(
|
||||
}
|
||||
case "return": {
|
||||
if (terminal.value !== null) {
|
||||
visitOperand(terminal.value, dependencies, declarations, scopes);
|
||||
context.visitOperand(terminal.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "throw": {
|
||||
visitOperand(terminal.value, dependencies, declarations, scopes);
|
||||
context.visitOperand(terminal.value);
|
||||
break;
|
||||
}
|
||||
case "for": {
|
||||
visitValueBlock(terminal.init, dependencies, declarations, scopes);
|
||||
visitValueBlock(terminal.test, dependencies, declarations, scopes);
|
||||
visitValueBlock(
|
||||
terminal.update,
|
||||
dependencies,
|
||||
declarations,
|
||||
scopes
|
||||
);
|
||||
visit(terminal.loop, dependencies, declarations, scopes);
|
||||
visitValueBlock(context, terminal.init);
|
||||
visitValueBlock(context, terminal.test);
|
||||
visitValueBlock(context, terminal.update);
|
||||
visit(context, terminal.loop);
|
||||
break;
|
||||
}
|
||||
case "while": {
|
||||
visitValueBlock(terminal.test, dependencies, declarations, scopes);
|
||||
visit(terminal.loop, dependencies, declarations, scopes);
|
||||
visitValueBlock(context, terminal.test);
|
||||
visit(context, terminal.loop);
|
||||
break;
|
||||
}
|
||||
case "if": {
|
||||
visitOperand(terminal.test, dependencies, declarations, scopes);
|
||||
visit(terminal.consequent, dependencies, declarations, scopes);
|
||||
context.visitOperand(terminal.test);
|
||||
visit(context, terminal.consequent);
|
||||
if (terminal.alternate !== null) {
|
||||
visit(terminal.alternate, dependencies, declarations, scopes);
|
||||
visit(context, terminal.alternate);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "switch": {
|
||||
visitOperand(terminal.test, dependencies, declarations, scopes);
|
||||
context.visitOperand(terminal.test);
|
||||
for (const case_ of terminal.cases) {
|
||||
if (case_.block !== undefined) {
|
||||
visit(case_.block, dependencies, declarations, scopes);
|
||||
visit(context, case_.block);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -150,95 +257,21 @@ function visit(
|
||||
}
|
||||
}
|
||||
|
||||
function visitValueBlock(
|
||||
block: ReactiveValueBlock,
|
||||
dependencies: Set<Place>,
|
||||
declarations: DeclMap,
|
||||
scopes: Scopes
|
||||
): void {
|
||||
function visitValueBlock(context: Context, block: ReactiveValueBlock): void {
|
||||
for (const initItem of block.instructions) {
|
||||
if (initItem.kind === "instruction") {
|
||||
visitInstruction(
|
||||
initItem.instruction,
|
||||
dependencies,
|
||||
declarations,
|
||||
scopes
|
||||
);
|
||||
visitInstruction(context, initItem.instruction);
|
||||
}
|
||||
}
|
||||
if (block.value !== null) {
|
||||
visitInstructionValue(block.value, dependencies, declarations, scopes);
|
||||
}
|
||||
}
|
||||
|
||||
function visitOperand(
|
||||
maybeDependency: Place,
|
||||
dependencies: Set<Place>,
|
||||
declarations: DeclMap,
|
||||
scopes: Scopes
|
||||
): void {
|
||||
const decl = declarations.get(maybeDependency.identifier);
|
||||
|
||||
// Any value used after its defining scope has concluded must be added as an
|
||||
// output of its defining scope. Regardless of whether its a const or not,
|
||||
// some later code needs access to the value.
|
||||
if (decl !== undefined) {
|
||||
const operandScope = maybeDependency.identifier.scope;
|
||||
if (operandScope !== null && scopes.indexOf(operandScope) === -1) {
|
||||
operandScope.outputs.add(maybeDependency.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// If this operand is used in a scope, has a dynamic value, and was defined
|
||||
// before this scope, then its a dependency of the scope.
|
||||
const currentScope = scopes.at(-1);
|
||||
if (
|
||||
decl !== undefined &&
|
||||
decl.kind !== DeclKind.Const &&
|
||||
currentScope !== undefined &&
|
||||
decl.id < currentScope.range.start
|
||||
) {
|
||||
// Check if there is an existing dependency that describes this operand
|
||||
for (const dep of dependencies) {
|
||||
// not the same identifier
|
||||
if (dep.identifier !== maybeDependency.identifier) {
|
||||
continue;
|
||||
}
|
||||
const depPath = dep.memberPath;
|
||||
// existing dep covers all paths
|
||||
if (depPath === null) {
|
||||
return;
|
||||
}
|
||||
const operandPath = maybeDependency.memberPath;
|
||||
// existing dep is for a path, this operand covers all paths so swap them
|
||||
if (operandPath === null) {
|
||||
dependencies.delete(dep);
|
||||
dependencies.add(maybeDependency);
|
||||
return;
|
||||
}
|
||||
// both the operand and dep have paths, determine if the existing path
|
||||
// is a subset of the new path
|
||||
let commonPathIndex = 0;
|
||||
while (
|
||||
commonPathIndex < operandPath.length &&
|
||||
commonPathIndex < depPath.length &&
|
||||
operandPath[commonPathIndex] === depPath[commonPathIndex]
|
||||
) {
|
||||
commonPathIndex++;
|
||||
}
|
||||
if (commonPathIndex === depPath.length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
dependencies.add(maybeDependency);
|
||||
visitInstructionValue(context, block.value, null);
|
||||
}
|
||||
}
|
||||
|
||||
function visitInstructionValue(
|
||||
context: Context,
|
||||
value: InstructionValue,
|
||||
dependencies: Set<Place>,
|
||||
declarations: DeclMap,
|
||||
scopes: Scopes
|
||||
lvalue: LValue | null
|
||||
): void {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
// check for method invocation, we want to depend on the callee, not the method
|
||||
@@ -251,21 +284,18 @@ function visitInstructionValue(
|
||||
...operand,
|
||||
memberPath: operand.memberPath.slice(0, -1),
|
||||
};
|
||||
visitOperand(callee, dependencies, declarations, scopes);
|
||||
context.visitOperand(callee);
|
||||
} else if (value.kind === "PropertyLoad" && lvalue !== null) {
|
||||
context.declareProperty(lvalue.place, value.object, value.property);
|
||||
} else {
|
||||
visitOperand(operand, dependencies, declarations, scopes);
|
||||
context.visitOperand(operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function visitInstruction(
|
||||
instr: Instruction,
|
||||
dependencies: Set<Place>,
|
||||
declarations: DeclMap,
|
||||
scopes: Scopes
|
||||
): void {
|
||||
visitInstructionValue(instr.value, dependencies, declarations, scopes);
|
||||
function visitInstruction(context: Context, instr: Instruction): void {
|
||||
const { lvalue } = instr;
|
||||
visitInstructionValue(context, instr.value, lvalue);
|
||||
if (
|
||||
lvalue !== null &&
|
||||
lvalue.kind !== InstructionKind.Reassign &&
|
||||
@@ -275,7 +305,10 @@ function visitInstruction(
|
||||
// TODO: only assign Const if the value is never reassigned
|
||||
const kind =
|
||||
range.end === range.start + 1 ? valueKind(instr.value) : DeclKind.Dynamic;
|
||||
declarations.set(lvalue.place.identifier, { kind, id: instr.id });
|
||||
context.declare(lvalue.place.identifier, {
|
||||
kind,
|
||||
id: lvalue.place.identifier.mutableRange.start,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +319,7 @@ function valueKind(value: InstructionValue): DeclKind {
|
||||
case "Primitive": {
|
||||
return DeclKind.Const;
|
||||
}
|
||||
case "PropertyLoad":
|
||||
case "Identifier":
|
||||
case "ArrayExpression":
|
||||
case "CallExpression":
|
||||
|
||||
@@ -61,16 +61,33 @@ export default class DisjointSet<T> {
|
||||
if (!this.#entries.has(item)) {
|
||||
return null;
|
||||
}
|
||||
let current = item;
|
||||
let parent = this.#entries.get(current)!;
|
||||
while (current !== parent) {
|
||||
current = parent;
|
||||
parent = this.#entries.get(current)!;
|
||||
const parent = this.#entries.get(item)!;
|
||||
if (parent === item) {
|
||||
// this is the root element
|
||||
return item;
|
||||
}
|
||||
if (item !== current) {
|
||||
this.#entries.set(item, current);
|
||||
// Recurse to find the root (caching all elements along the path to the root)
|
||||
const root = this.find(parent)!;
|
||||
// Cache the element itself
|
||||
this.#entries.set(item, root);
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
canonicalize(): boolean {
|
||||
let isCanonical = true;
|
||||
for (const item of this.#entries.keys()) {
|
||||
const parent = this.#entries.get(item)!;
|
||||
const root = this.find(item);
|
||||
if (parent !== root) {
|
||||
isCanonical = false;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
return isCanonical;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { HIR, HIRFunction } from "../HIR/HIR";
|
||||
import { HIR, HIRFunction, ReactiveFunction } from "../HIR/HIR";
|
||||
import printHIR, { printFunction } from "../HIR/PrintHIR";
|
||||
import { printReactiveFunction } from "../ReactiveScopes";
|
||||
|
||||
let ENABLED: boolean = false;
|
||||
|
||||
@@ -22,6 +23,10 @@ export function logHIRFunction(step: string, fn: HIRFunction): void {
|
||||
log(() => `${step}:\n${printFunction(fn)}`);
|
||||
}
|
||||
|
||||
export function logReactiveFunction(step: string, fn: ReactiveFunction): void {
|
||||
log(() => `${step}:\n${printReactiveFunction(fn)}`);
|
||||
}
|
||||
|
||||
export function log(fn: () => string) {
|
||||
if (ENABLED) {
|
||||
const message = fn();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function g(a) {
|
||||
a.b.c = a.b.c + 1;
|
||||
a.b.c *= 2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function g(a) {
|
||||
const $ = React.useMemoCache();
|
||||
let a;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
a.c.b = a.b.c + 1;
|
||||
a.c.b = a.b.c * 2;
|
||||
$[0] = a;
|
||||
} else {
|
||||
a = $[0];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
function g(a) {
|
||||
a.b.c = a.b.c + 1;
|
||||
a.b.c *= 2;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function g() {
|
||||
const x = { y: { z: 1 } };
|
||||
x.y.z = x.y.z + 1;
|
||||
x.y.z *= 2;
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function g() {
|
||||
const $ = React.useMemoCache();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {
|
||||
z: 1,
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
|
||||
const c_1 = $[1] !== t0;
|
||||
let x;
|
||||
|
||||
if (c_1) {
|
||||
x = {
|
||||
y: t0,
|
||||
};
|
||||
x.z.y = x.y.z + 1;
|
||||
x.z.y = x.y.z * 2;
|
||||
$[1] = t0;
|
||||
$[2] = x;
|
||||
} else {
|
||||
x = $[2];
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
function g() {
|
||||
const x = { y: { z: 1 } };
|
||||
x.y.z = x.y.z + 1;
|
||||
x.y.z *= 2;
|
||||
return x;
|
||||
}
|
||||
@@ -9,11 +9,6 @@ function f() {
|
||||
x >>>= 1;
|
||||
}
|
||||
|
||||
function g(a) {
|
||||
a.b.c = a.b.c + 1;
|
||||
a.b.c *= 2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
@@ -27,13 +22,4 @@ function f() {
|
||||
}
|
||||
|
||||
```
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function g(a) {
|
||||
a.c.b = a.b.c + 1;
|
||||
a.c.b = a.b.c * 2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -4,8 +4,3 @@ function f() {
|
||||
x += 1;
|
||||
x >>>= 1;
|
||||
}
|
||||
|
||||
function g(a) {
|
||||
a.b.c = a.b.c + 1;
|
||||
a.b.c *= 2;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ function Component(props) {
|
||||
const maxItems = props.maxItems;
|
||||
const c_0 = $[0] !== maxItems;
|
||||
const c_1 = $[1] !== items.length;
|
||||
const c_2 = $[2] !== items;
|
||||
const c_2 = $[2] !== items.at;
|
||||
let renderedItems;
|
||||
if (c_0 || c_1 || c_2) {
|
||||
renderedItems = [];
|
||||
@@ -77,7 +77,7 @@ function Component(props) {
|
||||
|
||||
$[0] = maxItems;
|
||||
$[1] = items.length;
|
||||
$[2] = items;
|
||||
$[2] = items.at;
|
||||
$[3] = renderedItems;
|
||||
} else {
|
||||
renderedItems = $[3];
|
||||
|
||||
+9
-37
@@ -13,51 +13,23 @@ function MyApp(props) {
|
||||
|
||||
```
|
||||
|
||||
## HIR
|
||||
|
||||
```
|
||||
bb0:
|
||||
[1] Const mutate y$7_@0[1:5] = Call mutate makeObj$2:TFunction()
|
||||
[2] Const mutate tmp$8_@0[1:5] = read y$7_@0.a
|
||||
[3] Const mutate tmp2$9_@0[1:5] = read tmp$8_@0.b
|
||||
[4] Call mutate y$7_@0.push(mutate tmp2$9_@0)
|
||||
[5] Return freeze y$7_@0
|
||||
```
|
||||
|
||||
## Reactive Scopes
|
||||
|
||||
```
|
||||
function MyApp(
|
||||
props,
|
||||
) {
|
||||
scope @0 [1:5] deps=[] out=[y$7_@0] {
|
||||
[1] Const mutate y$7_@0[1:5] = Call mutate makeObj$2:TFunction()
|
||||
[2] Const mutate tmp$8_@0[1:5] = read y$7_@0.a
|
||||
[3] Const mutate tmp2$9_@0[1:5] = read tmp$8_@0.b
|
||||
[4] Call mutate y$7_@0.push(mutate tmp2$9_@0)
|
||||
}
|
||||
return freeze y$7_@0
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function MyApp$0(props$6) {
|
||||
function MyApp(props) {
|
||||
const $ = React.useMemoCache();
|
||||
let y$7;
|
||||
let y;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
y$7 = makeObj$2();
|
||||
const tmp$8 = y$7.a;
|
||||
const tmp2$9 = tmp$8.b;
|
||||
y$7.push(tmp2$9);
|
||||
$[0] = y$7;
|
||||
y = makeObj();
|
||||
const tmp = y.a;
|
||||
const tmp2 = tmp.b;
|
||||
y.push(tmp2);
|
||||
$[0] = y;
|
||||
} else {
|
||||
y$7 = $[0];
|
||||
y = $[0];
|
||||
}
|
||||
|
||||
return y$7;
|
||||
return y;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function foo(a) {
|
||||
const x = [a.b];
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function foo(a) {
|
||||
const $ = React.useMemoCache();
|
||||
const c_0 = $[0] !== a.b;
|
||||
let x;
|
||||
if (c_0) {
|
||||
x = [a.b];
|
||||
$[0] = a.b;
|
||||
$[1] = x;
|
||||
} else {
|
||||
x = $[1];
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
function foo(a) {
|
||||
const x = [a.b];
|
||||
return x;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function foo(a, b) {
|
||||
let x = 0;
|
||||
while (a.b.c) {
|
||||
x += b;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
function foo(a, b) {
|
||||
const $ = React.useMemoCache();
|
||||
const c_0 = $[0] !== a.b.c;
|
||||
const c_1 = $[1] !== b;
|
||||
let x;
|
||||
if (c_0 || c_1) {
|
||||
x = 0;
|
||||
|
||||
while (a.b.c) {
|
||||
x = x + b;
|
||||
}
|
||||
|
||||
$[0] = a.b.c;
|
||||
$[1] = b;
|
||||
$[2] = x;
|
||||
} else {
|
||||
x = $[2];
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
function foo(a, b) {
|
||||
let x = 0;
|
||||
while (a.b.c) {
|
||||
x += b;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
Reference in New Issue
Block a user