diff --git a/compiler/forget/src/CompilerPipeline.ts b/compiler/forget/src/CompilerPipeline.ts index e89dbe5b6e..3ca516125b 100644 --- a/compiler/forget/src/CompilerPipeline.ts +++ b/compiler/forget/src/CompilerPipeline.ts @@ -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 { diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts index 4d7e602b72..5b74463780 100644 --- a/compiler/forget/src/HIR/BuildHIR.ts +++ b/compiler/forget/src/HIR/BuildHIR.ts @@ -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; @@ -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": { diff --git a/compiler/forget/src/HIR/Codegen.ts b/compiler/forget/src/HIR/Codegen.ts index d84100543f..ca393b4ea5 100644 --- a/compiler/forget/src/HIR/Codegen.ts +++ b/compiler/forget/src/HIR/Codegen.ts @@ -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; diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index 15441ed018..f48f6d1c1b 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -293,6 +293,11 @@ export type InstructionData = | { kind: "ArrayExpression"; elements: Array } | { kind: "JsxFragment"; children: Array } + // 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 diff --git a/compiler/forget/src/HIR/InferAlias.ts b/compiler/forget/src/HIR/InferAlias.ts index b42cb60789..163816b85a 100644 --- a/compiler/forget/src/HIR/InferAlias.ts +++ b/compiler/forget/src/HIR/InferAlias.ts @@ -35,6 +35,10 @@ function inferInstr(instr: Instruction, state: AliasAnalyser) { alias = instrValue; break; } + case "PropertyLoad": { + alias = instrValue.object; + break; + } default: return; } diff --git a/compiler/forget/src/HIR/InferMutableRanges.ts b/compiler/forget/src/HIR/InferMutableRanges.ts index 03e040e314..63a59bbd69 100644 --- a/compiler/forget/src/HIR/InferMutableRanges.ts +++ b/compiler/forget/src/HIR/InferMutableRanges.ts @@ -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); diff --git a/compiler/forget/src/HIR/InferReferenceEffects.ts b/compiler/forget/src/HIR/InferReferenceEffects.ts index d055726331..266eecb590 100644 --- a/compiler/forget/src/HIR/InferReferenceEffects.ts +++ b/compiler/forget/src/HIR/InferReferenceEffects.ts @@ -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; diff --git a/compiler/forget/src/HIR/PrintHIR.ts b/compiler/forget/src/HIR/PrintHIR.ts index b4d19d73e4..2a32cf77e6 100644 --- a/compiler/forget/src/HIR/PrintHIR.ts +++ b/compiler/forget/src/HIR/PrintHIR.ts @@ -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}`; diff --git a/compiler/forget/src/HIR/visitors.ts b/compiler/forget/src/HIR/visitors.ts index 6bd04c1e2c..6293b59f8c 100644 --- a/compiler/forget/src/HIR/visitors.ts +++ b/compiler/forget/src/HIR/visitors.ts @@ -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; diff --git a/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts b/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts index 60040610b1..7130f82a6d 100644 --- a/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts +++ b/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts @@ -164,6 +164,7 @@ function mayAllocate(value: InstructionValue): boolean { switch (value.kind) { case "BinaryExpression": case "Identifier": + case "PropertyLoad": case "JSXText": case "Primitive": { return false; diff --git a/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts index 9c36db0702..90eccb44d4 100644 --- a/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -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 = 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; +type DeclMap = Map; +type Decl = { kind: DeclKind; id: InstructionId }; type Scopes = Array; -function visit( - block: ReactiveBlock, - dependencies: Set, - declarations: DeclMap, - scopes: Scopes -): void { +class Context { + #declarations: DeclMap = new Map(); + #dependencies: Set = new Set(); + #properties: Map = new Map(); + #scopes: Scopes = []; + + enter(scope: ReactiveScope, fn: () => void): Set { + const previousDependencies = this.#dependencies; + const scopedDependencies = new Set(); + 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 = 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, - 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, - 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, - 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, - 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": diff --git a/compiler/forget/src/Utils/DisjointSet.ts b/compiler/forget/src/Utils/DisjointSet.ts index 717eb67f25..c6c71e86a7 100644 --- a/compiler/forget/src/Utils/DisjointSet.ts +++ b/compiler/forget/src/Utils/DisjointSet.ts @@ -61,16 +61,33 @@ export default class DisjointSet { 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; } /** diff --git a/compiler/forget/src/Utils/logger.ts b/compiler/forget/src/Utils/logger.ts index 61c79beb45..0ca70669f4 100644 --- a/compiler/forget/src/Utils/logger.ts +++ b/compiler/forget/src/Utils/logger.ts @@ -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(); diff --git a/compiler/forget/src/__tests__/fixtures/hir/_bug_invalid-scope.expect.md b/compiler/forget/src/__tests__/fixtures/hir/_bug_invalid-scope.expect.md new file mode 100644 index 0000000000..b8aa3506af --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/_bug_invalid-scope.expect.md @@ -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]; + } +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/_bug_invalid-scope.js b/compiler/forget/src/__tests__/fixtures/hir/_bug_invalid-scope.js new file mode 100644 index 0000000000..b40ff31a2f --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/_bug_invalid-scope.js @@ -0,0 +1,4 @@ +function g(a) { + a.b.c = a.b.c + 1; + a.b.c *= 2; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/assignment-variations-complex-lvalue.expect.md b/compiler/forget/src/__tests__/fixtures/hir/assignment-variations-complex-lvalue.expect.md new file mode 100644 index 0000000000..1c0a599870 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/assignment-variations-complex-lvalue.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/assignment-variations-complex-lvalue.js b/compiler/forget/src/__tests__/fixtures/hir/assignment-variations-complex-lvalue.js new file mode 100644 index 0000000000..44db6c38f5 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/assignment-variations-complex-lvalue.js @@ -0,0 +1,6 @@ +function g() { + const x = { y: { z: 1 } }; + x.y.z = x.y.z + 1; + x.y.z *= 2; + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/assignment-variations.expect.md b/compiler/forget/src/__tests__/fixtures/hir/assignment-variations.expect.md index 63851dcb60..39ace72bdf 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/assignment-variations.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/assignment-variations.expect.md @@ -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; -} - -``` \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/assignment-variations.js b/compiler/forget/src/__tests__/fixtures/hir/assignment-variations.js index a5385a78e1..2815565959 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/assignment-variations.js +++ b/compiler/forget/src/__tests__/fixtures/hir/assignment-variations.js @@ -4,8 +4,3 @@ function f() { x += 1; x >>>= 1; } - -function g(a) { - a.b.c = a.b.c + 1; - a.b.c *= 2; -} diff --git a/compiler/forget/src/__tests__/fixtures/hir/component.expect.md b/compiler/forget/src/__tests__/fixtures/hir/component.expect.md index f256b40321..4a0f4c75f6 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/component.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/component.expect.md @@ -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]; diff --git a/compiler/forget/src/__tests__/fixtures/hir/issue933-disjoint-set-infinite-loop.expect.md b/compiler/forget/src/__tests__/fixtures/hir/issue933-disjoint-set-infinite-loop.expect.md index 14d39faa8b..defe9e62a3 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/issue933-disjoint-set-infinite-loop.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/issue933-disjoint-set-infinite-loop.expect.md @@ -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; } ``` diff --git a/compiler/forget/src/__tests__/fixtures/hir/simple-scope.expect.md b/compiler/forget/src/__tests__/fixtures/hir/simple-scope.expect.md new file mode 100644 index 0000000000..f3c225ef69 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/simple-scope.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/simple-scope.js b/compiler/forget/src/__tests__/fixtures/hir/simple-scope.js new file mode 100644 index 0000000000..39357210b5 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/simple-scope.js @@ -0,0 +1,4 @@ +function foo(a) { + const x = [a.b]; + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/while-property.expect.md b/compiler/forget/src/__tests__/fixtures/hir/while-property.expect.md new file mode 100644 index 0000000000..41e5bb34da --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/while-property.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/while-property.js b/compiler/forget/src/__tests__/fixtures/hir/while-property.js new file mode 100644 index 0000000000..53feb4cb52 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/while-property.js @@ -0,0 +1,7 @@ +function foo(a, b) { + let x = 0; + while (a.b.c) { + x += b; + } + return x; +}