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:
Joe Savona
2022-12-22 14:00:48 -08:00
parent 8246956331
commit 1fd4bad525
25 changed files with 488 additions and 214 deletions
+13 -1
View File
@@ -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 {
+26 -8
View File
@@ -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": {
+7
View File
@@ -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;
+5
View File
@@ -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
+4
View File
@@ -35,6 +35,10 @@ function inferInstr(instr: Instruction, state: AliasAnalyser) {
alias = instrValue;
break;
}
case "PropertyLoad": {
alias = instrValue.object;
break;
}
default:
return;
}
+11 -1
View File
@@ -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;
+6 -1
View File
@@ -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}`;
+8
View File
@@ -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":
+25 -8
View File
@@ -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;
}
/**
+6 -1
View File
@@ -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;
}
@@ -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];
@@ -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;
}