From ca9d49090a78e22393d0f7fb4dbbabfc99d07aed Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Fri, 3 Mar 2023 17:09:25 -0800 Subject: [PATCH] New destructuring representation modeled on StoreLocal Changes to explicitly model destructuring (array and object patterns), expanding support to include rest elements and preserving destructuring through the output. The new "Destructure" instruction is similar to "StoreLocal" but has a pattern instead of a place. For now each level of nested array/object patterns creates a separate destructure instruction, which ensures we have a temporary Place to talk about the intermediate array/object and its type/effects etc. Example: ``` // INPUT const [x, {y}, ...z] = a; // yay rest elements work now! // HIR [1] $2 = LoadLocal a$1 [2] $6 = Destructure Const [ x$3, $4, ... z$5 ] = $2 [3] $8 = Destructure Const { y: y$8 } = $4 // OUTPUT const [x, t0, ...z] = a; const {y} = t0; ``` Note that we can still collapse to a single destructure statement during codegen, independently of whether we have separate instructions internally. For now i'm going w the simple approach of emitting multiple statements in codegen (the code will very likely get further rewritten by downstream babel passes anyway). Also, I don't love the "if StoreLocal/Destructure else ..." pattern that the StoreLocal created and that this PR entrenches. As discussed w @gsathya offline, the long-term direction will be to add a separate visitor, roughly `eachLValue()` and `eachOperand()` so that we can treat all instructions the same. Existing Instruction.lvalue will go away and become a property of the other types of instructions. --- compiler/forget/src/HIR/BuildHIR.ts | 238 ++++++----- compiler/forget/src/HIR/HIR.ts | 34 ++ compiler/forget/src/HIR/PrintHIR.ts | 51 +++ compiler/forget/src/HIR/visitors.ts | 85 ++++ compiler/forget/src/Inference/InferAlias.ts | 5 +- .../src/Inference/InferAliasForStores.ts | 9 +- .../src/Inference/InferMutableLifetimes.ts | 8 +- .../src/Inference/InferReferenceEffects.ts | 25 +- .../src/Optimization/DeadCodeElimination.ts | 11 + .../ReactiveScopes/CodegenReactiveFunction.ts | 94 +++-- .../InferReactiveScopeVariables.ts | 18 +- .../PropagateScopeDependencies.ts | 16 +- compiler/forget/src/SSA/EnterSSA.ts | 6 + compiler/forget/src/SSA/LeaveSSA.ts | 105 +++-- .../fixtures/hir/bug_object-pattern.expect.md | 2 +- ...turing-function-member-expr-call.expect.md | 4 +- .../fixtures/hir/concise-arrow-expr.expect.md | 2 +- .../fixtures/hir/controlled-input.expect.md | 4 +- .../destructure-direct-reassignment.expect.md | 36 ++ .../hir/destructure-direct-reassignment.js | 6 + .../hir/destructuring-assignment.expect.md | 13 +- .../fixtures/hir/destructuring.expect.md | 45 ++- .../__tests__/fixtures/hir/destructuring.js | 8 +- .../hir/error.todo-kitchensink.expect.md | 369 ++++++++---------- .../fixtures/hir/error.todo-kitchensink.js | 7 +- ...rtent-mutability-readonly-lambda.expect.md | 2 +- .../hir/object-pattern-params.expect.md | 5 +- ...-via-destructuring-with-mutation.expect.md | 44 +++ ...enaming-via-destructuring-with-mutation.js | 11 + .../ssa-renaming-via-destructuring.expect.md | 48 +++ .../hir/ssa-renaming-via-destructuring.js | 10 + .../hir/use-callback-simple.expect.md | 4 +- 32 files changed, 925 insertions(+), 400 deletions(-) create mode 100644 compiler/forget/src/__tests__/fixtures/hir/destructure-direct-reassignment.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/hir/destructure-direct-reassignment.js create mode 100644 compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring-with-mutation.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring-with-mutation.js create mode 100644 compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring.js diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts index c5eea2ea2e..6d3ddb61c0 100644 --- a/compiler/forget/src/HIR/BuildHIR.ts +++ b/compiler/forget/src/HIR/BuildHIR.ts @@ -14,6 +14,7 @@ import { Err, Ok, Result } from "../Utils/Result"; import { assertExhaustive } from "../Utils/utils"; import { Environment, EnvironmentOptions } from "./Environment"; import { + ArrayPattern, BlockId, BranchTerminal, Case, @@ -27,6 +28,7 @@ import { InstructionValue, JsxAttribute, makeInstructionId, + ObjectPattern, Place, ReturnTerminal, SourceLocation, @@ -1967,6 +1969,11 @@ function lowerAssignment( return { kind: "LoadLocal", place, loc: temporary.loc }; } case "MemberExpression": { + // This can only occur because of a coding error, parsers enforce this condition + invariant( + kind === InstructionKind.Reassign, + "MemberExpression may only appear in an assignment expression" + ); const lvalue = lvaluePath as NodePath; const property = lvalue.get("property"); const object = lowerExpressionToTemporary(builder, lvalue.get("object")); @@ -2009,115 +2016,156 @@ function lowerAssignment( case "ArrayPattern": { const lvalue = lvaluePath as NodePath; const elements = lvalue.get("elements"); - let hasError = false; + const items: ArrayPattern["items"] = []; + const followups: Array<{ place: Place; path: NodePath }> = []; for (let i = 0; i < elements.length; i++) { const element = elements[i]; if (element.node == null) { continue; } - if (element.node.type === "RestElement") { - builder.errors.push({ - reason: `(BuildHIR::lowerAssignment) Handle ${element.type} in ArrayPattern`, - severity: ErrorSeverity.Todo, - nodePath: element, + if (element.isRestElement()) { + const argument = element.get("argument"); + if (!argument.isIdentifier()) { + builder.errors.push({ + reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ArrayPattern`, + severity: ErrorSeverity.Todo, + nodePath: element, + }); + continue; + } + const identifier = lowerIdentifier(builder, argument); + items.push({ + kind: "Spread", + place: identifier, }); - hasError = true; - continue; + } else if (element.isIdentifier()) { + const identifier = lowerIdentifier(builder, element); + items.push(identifier); + } else { + const temp = buildTemporaryPlace( + builder, + element.node.loc ?? GeneratedSource + ); + items.push({ ...temp }); + followups.push({ place: temp, path: element as NodePath }); // TODO remove type cast } - const property = buildTemporaryPlace( - builder, - element.node.loc ?? GeneratedSource - ); - builder.push({ - id: makeInstructionId(0), - lvalue: { ...property }, - value: { - kind: "Primitive", - value: i, - loc: element.node.loc ?? GeneratedSource, - }, - loc: element.node.loc ?? GeneratedSource, - }); - const propertyPlace = buildTemporaryPlace(builder, property.loc); - builder.push({ - id: makeInstructionId(0), - lvalue: { ...propertyPlace }, - value: { - kind: "ComputedLoad", - loc, - object: { ...value }, - property, - }, - loc, - }); - lowerAssignment( - builder, - loc, - kind, - element as NodePath, - propertyPlace - ); } - return hasError - ? { kind: "UnsupportedNode", node: lvalueNode, loc } - : { kind: "LoadLocal", place: value, loc: value.loc }; + const temp = buildTemporaryPlace(builder, loc); + builder.push({ + id: makeInstructionId(0), + lvalue: { ...temp }, + value: { + kind: "Destructure", + lvalue: { + kind, + pattern: { + kind: "ArrayPattern", + items, + }, + }, + value, + loc, + }, + loc, + }); + for (const { place, path } of followups) { + lowerAssignment(builder, path.node.loc ?? loc, kind, path, place); + } + return { kind: "LoadLocal", place: value, loc: value.loc }; } case "ObjectPattern": { const lvalue = lvaluePath as NodePath; - const properties = lvalue.get("properties"); - let hasError = false; - for (let i = 0; i < properties.length; i++) { - const property = properties[i]; - if (!property.isObjectProperty()) { - builder.errors.push({ - reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`, - severity: ErrorSeverity.Todo, - nodePath: property, + const propertiesPaths = lvalue.get("properties"); + const properties: ObjectPattern["properties"] = []; + const followups: Array<{ place: Place; path: NodePath }> = []; + for (let i = 0; i < propertiesPaths.length; i++) { + const property = propertiesPaths[i]; + if (property.isRestElement()) { + const argument = property.get("argument"); + if (!argument.isIdentifier()) { + builder.errors.push({ + reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ArrayPattern`, + severity: ErrorSeverity.Todo, + nodePath: argument, + }); + continue; + } + const identifier = lowerIdentifier(builder, argument); + properties.push({ + kind: "Spread", + place: identifier, }); - hasError = true; - continue; + } else { + // TODO: this should always be true given the if/else + if (!property.isObjectProperty()) { + builder.errors.push({ + reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`, + severity: ErrorSeverity.Todo, + nodePath: property, + }); + continue; + } + const key = property.get("key"); + if (!key.isIdentifier()) { + builder.errors.push({ + reason: `(BuildHIR::lowerAssignment) Handle ${key.type} keys in ObjectPattern`, + severity: ErrorSeverity.Todo, + nodePath: key, + }); + continue; + } + const element = property.get("value"); + if (!element.isLVal()) { + builder.errors.push({ + reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`, + severity: ErrorSeverity.InvalidInput, + nodePath: element, + }); + continue; + } + if (element.isIdentifier()) { + const identifier = lowerIdentifier(builder, element); + properties.push({ + kind: "ObjectProperty", + name: key.node.name, + place: identifier, + }); + } else { + const temp = buildTemporaryPlace( + builder, + element.node.loc ?? GeneratedSource + ); + properties.push({ + kind: "ObjectProperty", + name: key.node.name, + place: { ...temp }, + }); + followups.push({ place: temp, path: element as NodePath }); // TODO remove type cast + } } - const key = property.get("key"); - if (!key.isIdentifier()) { - builder.errors.push({ - reason: `(BuildHIR::lowerAssignment) Handle ${key.type} keys in ObjectPattern`, - severity: ErrorSeverity.Todo, - nodePath: key, - }); - hasError = true; - continue; - } - const element = property.get("value"); - if (!element.isLVal()) { - builder.errors.push({ - reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`, - severity: ErrorSeverity.InvalidInput, - nodePath: element, - }); - hasError = true; - continue; - } - const propertyPlace = buildTemporaryPlace( - builder, - property.node.loc ?? GeneratedSource - ); - builder.push({ - id: makeInstructionId(0), - lvalue: { ...propertyPlace }, - value: { - kind: "PropertyLoad", - loc, - object: { ...value }, - property: key.node.name, - optional: false, // Key of ObjectPattern (evaluation of LVal) cannot be optional. - }, - loc, - }); - lowerAssignment(builder, loc, kind, element, propertyPlace); } - return hasError - ? { kind: "UnsupportedNode", node: lvalueNode, loc } - : { kind: "LoadLocal", place: value, loc: value.loc }; + const temp = buildTemporaryPlace(builder, loc); + builder.push({ + id: makeInstructionId(0), + lvalue: { ...temp }, + value: { + kind: "Destructure", + lvalue: { + kind, + pattern: { + kind: "ObjectPattern", + properties, + }, + }, + value, + loc, + }, + loc, + }); + for (const { place, path } of followups) { + lowerAssignment(builder, path.node.loc ?? loc, kind, path, place); + } + return { kind: "LoadLocal", place: value, loc: value.loc }; } default: { builder.errors.push({ diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index f88b5c23f1..dad13a8129 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -375,6 +375,34 @@ export type LValue = { kind: InstructionKind; }; +export type LValuePattern = { + pattern: Pattern; + kind: InstructionKind; +}; + +export type Pattern = ArrayPattern | ObjectPattern; + +export type SpreadPattern = { + kind: "Spread"; + place: Place; +}; + +export type ArrayPattern = { + kind: "ArrayPattern"; + items: Array; +}; + +export type ObjectPattern = { + kind: "ObjectPattern"; + properties: Array; +}; + +export type ObjectProperty = { + kind: "ObjectProperty"; + name: string; // TODO: make a Place + place: Place; +}; + export enum InstructionKind { /** * const declaration @@ -424,6 +452,12 @@ export type InstructionValue = value: Place; loc: SourceLocation; } + | { + kind: "Destructure"; + lvalue: LValuePattern; + value: Place; + loc: SourceLocation; + } | { kind: "Primitive"; value: number | boolean | string | null | undefined; diff --git a/compiler/forget/src/HIR/PrintHIR.ts b/compiler/forget/src/HIR/PrintHIR.ts index b9890c67f8..e932fc1fd6 100644 --- a/compiler/forget/src/HIR/PrintHIR.ts +++ b/compiler/forget/src/HIR/PrintHIR.ts @@ -19,12 +19,14 @@ import { InstructionValue, LValue, MutableRange, + Pattern, Phi, Place, ReactiveInstruction, ReactiveScope, ReactiveValue, SourceLocation, + SpreadPattern, Terminal, Type, } from "./HIR"; @@ -321,6 +323,12 @@ export function printInstructionValue(instrValue: ReactiveValue): string { )} = ${printPlace(instrValue.value)}`; break; } + case "Destructure": { + value = `Destructure ${instrValue.lvalue.kind} ${printPattern( + instrValue.lvalue.pattern + )} = ${printPlace(instrValue.value)}`; + break; + } case "PropertyLoad": { value = `PropertyLoad ${printPlace(instrValue.object)}.${ instrValue.property @@ -458,6 +466,49 @@ export function printLValue(lval: LValue): string { } } +export function printPattern(pattern: Pattern | Place | SpreadPattern): string { + switch (pattern.kind) { + case "ArrayPattern": { + return ( + "[ " + pattern.items.map((item) => printPattern(item)).join(", ") + " ]" + ); + } + case "ObjectPattern": { + return ( + "{ " + + pattern.properties + .map((item) => { + switch (item.kind) { + case "ObjectProperty": { + return `${item.name}: ${printPattern(item.place)}`; + } + case "Spread": { + return printPattern(item); + } + default: { + assertExhaustive(item, "Unexpected object property kind"); + } + } + }) + .join(", ") + + " }" + ); + } + case "Spread": { + return `...${printPlace(pattern.place)}`; + } + case "Identifier": { + return printPlace(pattern); + } + default: { + assertExhaustive( + pattern, + `Unexpected pattern kind '${(pattern as any).kind}'` + ); + } + } +} + export function printPlace(place: Place): string { const items = [ place.effect, diff --git a/compiler/forget/src/HIR/visitors.ts b/compiler/forget/src/HIR/visitors.ts index 5c62a2f080..8aaeb8c996 100644 --- a/compiler/forget/src/HIR/visitors.ts +++ b/compiler/forget/src/HIR/visitors.ts @@ -13,6 +13,7 @@ import { Instruction, InstructionValue, makeInstructionId, + Pattern, Place, ReactiveScope, ScopeId, @@ -57,6 +58,11 @@ export function* eachInstructionValueOperand( yield instrValue.value; break; } + case "Destructure": { + yield* eachPatternOperand(instrValue.lvalue.pattern); + yield instrValue.value; + break; + } case "PropertyLoad": { yield instrValue.object; break; @@ -151,6 +157,49 @@ export function* eachInstructionValueOperand( } } +export function* eachPatternOperand(pattern: Pattern): Iterable { + switch (pattern.kind) { + case "ArrayPattern": { + for (const item of pattern.items) { + if (item.kind === "Identifier") { + yield item; + } else if (item.kind === "Spread") { + yield item.place; + } else { + assertExhaustive( + item, + `Unexpected item kind '${(item as any).kind}'` + ); + } + } + break; + } + case "ObjectPattern": { + for (const property of pattern.properties) { + if (property.kind === "ObjectProperty") { + if (property.place.kind === "Identifier") { + yield property.place; + } + } else if (property.kind === "Spread") { + yield property.place; + } else { + assertExhaustive( + property, + `Unexpected item kind '${(property as any).kind}'` + ); + } + } + break; + } + default: { + assertExhaustive( + pattern, + `Unexpected pattern kind '${(pattern as any).kind}'` + ); + } + } +} + export function mapInstructionOperands( instr: Instruction, fn: (place: Place) => Place @@ -191,6 +240,11 @@ export function mapInstructionOperands( instrValue.value = fn(instrValue.value); break; } + case "Destructure": { + mapPatternOperands(instrValue.lvalue.pattern, fn); + instrValue.value = fn(instrValue.value); + break; + } case "NewExpression": case "CallExpression": { instrValue.callee = fn(instrValue.callee); @@ -282,6 +336,37 @@ export function mapInstructionOperands( } } +export function mapPatternOperands( + pattern: Pattern, + fn: (place: Place) => Place +): void { + switch (pattern.kind) { + case "ArrayPattern": { + pattern.items = pattern.items.map((item) => { + if (item.kind === "Identifier") { + return fn(item); + } else { + item.place = fn(item.place); + return item; + } + }); + break; + } + case "ObjectPattern": { + for (const property of pattern.properties) { + property.place = fn(property.place); + } + break; + } + default: { + assertExhaustive( + pattern, + `Unexpected pattern kind '${(pattern as any).kind}'` + ); + } + } +} + /** * Maps a terminal node's block assignments using the provided function. */ diff --git a/compiler/forget/src/Inference/InferAlias.ts b/compiler/forget/src/Inference/InferAlias.ts index 9aacaeb94a..b2d127a7e3 100644 --- a/compiler/forget/src/Inference/InferAlias.ts +++ b/compiler/forget/src/Inference/InferAlias.ts @@ -32,7 +32,10 @@ function inferInstr(instr: Instruction, aliases: DisjointSet) { break; } case "StoreLocal": { - // aliases.union([instrValue.place.identifier, instrValue.value.identifier]); + alias = instrValue.value; + break; + } + case "Destructure": { alias = instrValue.value; break; } diff --git a/compiler/forget/src/Inference/InferAliasForStores.ts b/compiler/forget/src/Inference/InferAliasForStores.ts index 6aa9e44f99..cb59236c37 100644 --- a/compiler/forget/src/Inference/InferAliasForStores.ts +++ b/compiler/forget/src/Inference/InferAliasForStores.ts @@ -11,7 +11,10 @@ import { InstructionId, Place, } from "../HIR/HIR"; -import { eachInstructionValueOperand } from "../HIR/visitors"; +import { + eachInstructionValueOperand, + eachPatternOperand, +} from "../HIR/visitors"; import DisjointSet from "../Utils/DisjointSet"; export function inferAliasForStores( @@ -26,6 +29,10 @@ export function inferAliasForStores( } if (value.kind === "StoreLocal") { maybeAlias(aliases, value.lvalue.place, value.value, instr.id); + } else if (value.kind === "Destructure") { + for (const place of eachPatternOperand(value.lvalue.pattern)) { + maybeAlias(aliases, place, value.value, instr.id); + } } for (const operand of eachInstructionValueOperand(value)) { if ( diff --git a/compiler/forget/src/Inference/InferMutableLifetimes.ts b/compiler/forget/src/Inference/InferMutableLifetimes.ts index eb52f7d2b0..00e2d53e44 100644 --- a/compiler/forget/src/Inference/InferMutableLifetimes.ts +++ b/compiler/forget/src/Inference/InferMutableLifetimes.ts @@ -14,7 +14,7 @@ import { Place, } from "../HIR/HIR"; import { printInstruction, printPlace } from "../HIR/PrintHIR"; -import { eachInstructionOperand } from "../HIR/visitors"; +import { eachInstructionOperand, eachPatternOperand } from "../HIR/visitors"; import { assertExhaustive } from "../Utils/utils"; /** @@ -123,6 +123,12 @@ export function inferMutableLifetimes( instr.value.lvalue.place.identifier.mutableRange.start = instr.id; instr.value.lvalue.place.identifier.mutableRange.end = makeInstructionId(instr.id + 1); + } else if (instr.value.kind === "Destructure") { + inferPlace(instr.value.value, instr, inferMutableRangeForStores); + for (const place of eachPatternOperand(instr.value.lvalue.pattern)) { + place.identifier.mutableRange.start = instr.id; + place.identifier.mutableRange.end = makeInstructionId(instr.id + 1); + } } else { for (const input of eachInstructionOperand(instr)) { inferPlace(input, instr, inferMutableRangeForStores); diff --git a/compiler/forget/src/Inference/InferReferenceEffects.ts b/compiler/forget/src/Inference/InferReferenceEffects.ts index f8bc60fcec..2543532e27 100644 --- a/compiler/forget/src/Inference/InferReferenceEffects.ts +++ b/compiler/forget/src/Inference/InferReferenceEffects.ts @@ -28,6 +28,7 @@ import { import { eachInstructionOperand, eachInstructionValueOperand, + eachPatternOperand, eachTerminalOperand, eachTerminalSuccessor, } from "../HIR/visitors"; @@ -806,7 +807,29 @@ function inferBlock( state.alias(lvalue, instrValue.value); lvalue.effect = Effect.Store; state.alias(instrValue.lvalue.place, instrValue.value); - instrValue.lvalue.place.effect = Effect.Store; + state.reference(instrValue.lvalue.place, Effect.Store); + continue; + } + case "Destructure": { + let effect: Effect = Effect.Capture; + for (const place of eachPatternOperand(instrValue.lvalue.pattern)) { + if ( + state.isDefined(place) && + state.kind(place) === ValueKind.Context + ) { + effect = Effect.Mutate; + break; + } + } + state.reference(instrValue.value, effect); + + const lvalue = instr.lvalue; + state.alias(lvalue, instrValue.value); + lvalue.effect = Effect.Store; + for (const place of eachPatternOperand(instrValue.lvalue.pattern)) { + state.alias(place, instrValue.value); + state.reference(place, Effect.Store); + } continue; } default: { diff --git a/compiler/forget/src/Optimization/DeadCodeElimination.ts b/compiler/forget/src/Optimization/DeadCodeElimination.ts index 79210ba6b0..ff0d6c3514 100644 --- a/compiler/forget/src/Optimization/DeadCodeElimination.ts +++ b/compiler/forget/src/Optimization/DeadCodeElimination.ts @@ -8,6 +8,7 @@ import { BlockId, HIRFunction, Identifier, InstructionValue } from "../HIR"; import { eachInstructionValueOperand, + eachPatternOperand, eachTerminalOperand, } from "../HIR/visitors"; import { assertExhaustive, retainWhere } from "../Utils/utils"; @@ -85,6 +86,16 @@ function pruneableValue( // Stores are pruneable only if the identifier being stored to is never read later return !used.has(value.lvalue.place.identifier); } + case "Destructure": { + // Destructure is pruneable only if none of the identifiers are read from later + // TODO: as an optimization, prune unused properties where safe + for (const place of eachPatternOperand(value.lvalue.pattern)) { + if (used.has(place.identifier)) { + return false; + } + } + return true; + } case "CallExpression": case "ComputedCall": case "ComputedStore": diff --git a/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts index 150c56ff52..ad2b2752ab 100644 --- a/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -14,7 +14,7 @@ import { Identifier, IdentifierId, InstructionKind, - LValue, + Pattern, Place, ReactiveBlock, ReactiveFunction, @@ -24,7 +24,9 @@ import { ReactiveTerminal, ReactiveValue, SourceLocation, + SpreadPattern, } from "../HIR/HIR"; +import { eachPatternOperand } from "../HIR/visitors"; import { Err, Ok, Result } from "../Utils/Result"; import { assertExhaustive } from "../Utils/utils"; @@ -358,36 +360,39 @@ function codegenInstructionNullable( instr: ReactiveInstruction ): t.Statement | null { let statement; - if (instr.value.kind === "StoreLocal") { - const kind = cx.hasDeclared(instr.value.lvalue.place.identifier) - ? InstructionKind.Reassign - : instr.value.lvalue.kind; + if (instr.value.kind === "StoreLocal" || instr.value.kind === "Destructure") { + let kind: InstructionKind = instr.value.lvalue.kind; + let lvalue; + if (instr.value.kind === "StoreLocal") { + kind = cx.hasDeclared(instr.value.lvalue.place.identifier) + ? InstructionKind.Reassign + : kind; + lvalue = instr.value.lvalue.place; + } else { + lvalue = instr.value.lvalue.pattern; + for (const place of eachPatternOperand(lvalue)) { + if (cx.hasDeclared(place.identifier)) { + kind = InstructionKind.Reassign; + break; + } + } + } const value = codegenPlace(cx, instr.value.value); switch (kind) { case InstructionKind.Const: { return createVariableDeclaration(instr.loc, "const", [ - t.variableDeclarator( - convertIdentifier(instr.value.lvalue.place.identifier), - value - ), + t.variableDeclarator(codegenLValue(lvalue), value), ]); } case InstructionKind.Let: { return createVariableDeclaration(instr.loc, "let", [ - t.variableDeclarator( - convertIdentifier(instr.value.lvalue.place.identifier), - value - ), + t.variableDeclarator(codegenLValue(lvalue), value), ]); } case InstructionKind.Reassign: { return createExpressionStatement( instr.loc, - t.assignmentExpression( - "=", - convertIdentifier(instr.value.lvalue.place.identifier), - value - ) + t.assignmentExpression("=", codegenLValue(lvalue), value) ); } default: { @@ -717,12 +722,6 @@ function codegenInstructionValue( value = codegenPlace(cx, instrValue.place); break; } - case "StoreLocal": { - CompilerError.invariant( - `Unexpected StoreLocal in codegenInstructionValue`, - instrValue.loc - ); - } case "FunctionExpression": { value = instrValue.expr; break; @@ -814,6 +813,13 @@ function codegenInstructionValue( value = t.identifier(instrValue.name); break; } + case "Destructure": + case "StoreLocal": { + CompilerError.invariant( + `Unexpected StoreLocal in codegenInstructionValue`, + instrValue.loc + ); + } default: { assertExhaustive( instrValue, @@ -844,8 +850,44 @@ function codegenJsxElement( } } -function codegenLVal(lval: LValue): t.LVal { - return convertIdentifier(lval.place.identifier); +function codegenLValue( + pattern: Pattern | Place | SpreadPattern +): t.ArrayPattern | t.ObjectPattern | t.RestElement | t.Identifier { + switch (pattern.kind) { + case "ArrayPattern": { + return t.arrayPattern(pattern.items.map((item) => codegenLValue(item))); + } + case "ObjectPattern": { + return t.objectPattern( + pattern.properties.map((property) => { + if (property.kind === "ObjectProperty") { + const key = t.identifier(property.name); + const value = codegenLValue(property.place); + return t.objectProperty( + key, + value, + false, + value.type === "Identifier" && value.name === key.name + ); + } else { + return t.restElement(codegenLValue(property.place)); + } + }) + ); + } + case "Spread": { + return t.restElement(codegenLValue(pattern.place)); + } + case "Identifier": { + return convertIdentifier(pattern.identifier); + } + default: { + assertExhaustive( + pattern, + `Unexpected pattern kind '${(pattern as any).kind}'` + ); + } + } } function codegenValue( diff --git a/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts b/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts index 68bc5d93c0..daa48d5ee1 100644 --- a/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts +++ b/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts @@ -15,7 +15,7 @@ import { Place, ReactiveScope, } from "../HIR/HIR"; -import { eachInstructionOperand } from "../HIR/visitors"; +import { eachInstructionOperand, eachPatternOperand } from "../HIR/visitors"; import DisjointSet from "../Utils/DisjointSet"; import { assertExhaustive } from "../Utils/utils"; @@ -115,6 +115,21 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void { ) { operands.push(instr.value.value.identifier); } + } else if (instr.value.kind === "Destructure") { + for (const place of eachPatternOperand(instr.value.lvalue.pattern)) { + if ( + place.identifier.mutableRange.end > + place.identifier.mutableRange.start + 1 + ) { + operands.push(place.identifier); + } + } + if ( + isMutable(instr, instr.value.value) && + instr.value.value.identifier.mutableRange.start > 0 + ) { + operands.push(instr.value.value.identifier); + } } else { for (const operand of eachInstructionOperand(instr)) { if ( @@ -183,6 +198,7 @@ function isMutable({ id }: Instruction, place: Place): boolean { function mayAllocate(value: InstructionValue): boolean { switch (value.kind) { + case "Destructure": case "StoreLocal": case "LoadGlobal": case "TypeCastExpression": diff --git a/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts index 5505ce1487..fc00b44654 100644 --- a/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -20,7 +20,10 @@ import { ReactiveScopeDependency, ReactiveValue, } from "../HIR/HIR"; -import { eachInstructionValueOperand } from "../HIR/visitors"; +import { + eachInstructionValueOperand, + eachPatternOperand, +} from "../HIR/visitors"; import { assertExhaustive } from "../Utils/utils"; /** @@ -707,6 +710,17 @@ function visitInstructionValue( id, scope: context.currentScope, }); + } else if (value.kind === "Destructure") { + context.visitOperand(value.value); + for (const place of eachPatternOperand(value.lvalue.pattern)) { + if (value.lvalue.kind === InstructionKind.Reassign) { + context.visitReassignment(place); + } + context.declare(place.identifier, { + id, + scope: context.currentScope, + }); + } } else { visitReactiveValue(context, id, value); } diff --git a/compiler/forget/src/SSA/EnterSSA.ts b/compiler/forget/src/SSA/EnterSSA.ts index 8bd7b60667..f2c25eef87 100644 --- a/compiler/forget/src/SSA/EnterSSA.ts +++ b/compiler/forget/src/SSA/EnterSSA.ts @@ -16,6 +16,7 @@ import { printIdentifier } from "../HIR/PrintHIR"; import { eachTerminalSuccessor, mapInstructionOperands, + mapPatternOperands, mapTerminalOperands, } from "../HIR/visitors"; @@ -228,6 +229,11 @@ export default function enterSSA(func: HIRFunction): void { const newPlace = builder.definePlace(oldPlace); instr.value.lvalue.place = newPlace; + instr.value.value = builder.getPlace(instr.value.value); + } else if (instr.value.kind === "Destructure") { + mapPatternOperands(instr.value.lvalue.pattern, (place) => + builder.definePlace(place) + ); instr.value.value = builder.getPlace(instr.value.value); } else { mapInstructionOperands(instr, (place) => builder.getPlace(place)); diff --git a/compiler/forget/src/SSA/LeaveSSA.ts b/compiler/forget/src/SSA/LeaveSSA.ts index f6e1736d08..d75a8d6ac7 100644 --- a/compiler/forget/src/SSA/LeaveSSA.ts +++ b/compiler/forget/src/SSA/LeaveSSA.ts @@ -6,6 +6,7 @@ */ import invariant from "invariant"; +import { CompilerError } from "../CompilerError"; import { BasicBlock, Effect, @@ -16,12 +17,15 @@ import { InstructionId, InstructionKind, LValue, + LValuePattern, makeInstructionId, Phi, Place, } from "../HIR/HIR"; +import { printPlace } from "../HIR/PrintHIR"; import { eachInstructionValueOperand, + eachPatternOperand, eachTerminalOperand, } from "../HIR/visitors"; @@ -89,7 +93,10 @@ import { */ export function leaveSSA(fn: HIRFunction): void { // Maps identifier names to their original declaration. - const declarations: Map = new Map(); + const declarations: Map< + string, + { lvalue: LValue | LValuePattern; place: Place } + > = new Map(); // For non-memoizable phis, this maps original identifiers to the identifier they should be // *rewritten* to. The keys are the original identifiers, and the value will be _either_ the @@ -111,30 +118,74 @@ export function leaveSSA(fn: HIRFunction): void { // Iterate the instructions and perform any rewrites as well as promoting SSA variables to // `let` or `reassign` where possible. const { lvalue, value } = instr; - if ( - value.kind === "StoreLocal" && - value.lvalue.place.identifier.name != null - ) { - const originalLVal = declarations.get( - value.lvalue.place.identifier.name - ); - if (originalLVal === undefined) { - declarations.set(value.lvalue.place.identifier.name, value.lvalue); - value.lvalue.kind = InstructionKind.Const; - } else { - // This is an instance of the original id, so we need to promote the original declaration - // to a `let` and the current lval to a `reassign` - originalLVal.kind = InstructionKind.Let; + if (value.kind === "StoreLocal") { + if (value.lvalue.place.identifier.name != null) { + const originalLVal = declarations.get( + value.lvalue.place.identifier.name + ); + if (originalLVal === undefined) { + declarations.set(value.lvalue.place.identifier.name, { + lvalue: value.lvalue, + place: value.lvalue.place, + }); + value.lvalue.kind = InstructionKind.Const; + } else { + // This is an instance of the original id, so we need to promote the original declaration + // to a `let` and the current lval to a `reassign` + originalLVal.lvalue.kind = InstructionKind.Let; + } + } else if (rewrites.has(value.lvalue.place.identifier)) { + value.lvalue.kind = + rewrites.get(value.lvalue.place.identifier) === + value.lvalue.place.identifier + ? InstructionKind.Let + : InstructionKind.Reassign; } - } else if ( - value.kind === "StoreLocal" && - rewrites.has(value.lvalue.place.identifier) - ) { - value.lvalue.kind = - rewrites.get(value.lvalue.place.identifier) === - value.lvalue.place.identifier - ? InstructionKind.Let - : InstructionKind.Reassign; + } else if (value.kind === "Destructure") { + let kind: InstructionKind | null = null; + for (const place of eachPatternOperand(value.lvalue.pattern)) { + if (place.identifier.name == null) { + if (kind !== null && kind !== InstructionKind.Const) { + CompilerError.invariant( + `Expected consistent kind for destructuring, other places were '${kind}' but '${printPlace( + place + )}' is const`, + place.loc + ); + } + kind = InstructionKind.Const; + } else { + const originalLVal = declarations.get(place.identifier.name); + if (originalLVal === undefined) { + declarations.set(place.identifier.name, { + lvalue: value.lvalue, + place, + }); + if (kind !== null && kind !== InstructionKind.Const) { + CompilerError.invariant( + `Expected consistent kind for destructuring, other places were '${kind}' but '${printPlace( + place + )}' is const`, + place.loc + ); + } + kind = InstructionKind.Const; + } else { + if (kind !== null && kind !== InstructionKind.Reassign) { + CompilerError.invariant( + `Expected consistent kind for destructuring, other places were '${kind}' but '${printPlace( + place + )}' is reassigned`, + place.loc + ); + } + kind = InstructionKind.Reassign; + originalLVal.lvalue.kind = InstructionKind.Let; + } + } + } + invariant(kind !== null, "Expected at least one operand"); + value.lvalue.kind = kind; } rewritePlace(lvalue, rewrites, declarations); for (const operand of eachInstructionValueOperand(instr.value)) { @@ -304,7 +355,7 @@ export function leaveSSA(fn: HIRFunction): void { loc: GeneratedSource, }; block.instructions.push(instr); - declarations.set(phi.id.name, lvalue); + declarations.set(phi.id.name, { lvalue, place: lvalue.place }); phi.id.mutableRange.start = terminal.id; if (!isPhiMutatedAfterCreation) { phi.id.mutableRange.end = makeInstructionId(terminal.id + 1); @@ -343,7 +394,7 @@ export function leaveSSA(fn: HIRFunction): void { if (canonicalId.name !== null) { const declaration = declarations.get(canonicalId.name); if (declaration !== undefined) { - declaration.kind = InstructionKind.Let; + declaration.lvalue.kind = InstructionKind.Let; } } } @@ -370,7 +421,7 @@ export function leaveSSA(fn: HIRFunction): void { function rewritePlace( place: Place, rewrites: Map, - declarations: Map + declarations: Map ): void { const prevIdentifier = place.identifier; const nextIdentifier = rewrites.get(prevIdentifier); diff --git a/compiler/forget/src/__tests__/fixtures/hir/bug_object-pattern.expect.md b/compiler/forget/src/__tests__/fixtures/hir/bug_object-pattern.expect.md index 649298ccc0..31b19bf7d3 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/bug_object-pattern.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/bug_object-pattern.expect.md @@ -15,7 +15,7 @@ function component(t) { ```javascript function component(t) { const $ = React.unstable_useMemoCache(2); - const a = t.a; + const { a } = t; const c_0 = $[0] !== a; let t0; if (c_0) { diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-function-member-expr-call.expect.md b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-member-expr-call.expect.md index cd11bc3c6e..2dc57b8057 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/capturing-function-member-expr-call.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-member-expr-call.expect.md @@ -19,9 +19,9 @@ function component({ mutator }) { ## Code ```javascript -function component(t28) { +function component(t27) { const $ = React.unstable_useMemoCache(7); - const mutator = t28.mutator; + const { mutator } = t27; const c_0 = $[0] !== mutator; let t0; if (c_0) { diff --git a/compiler/forget/src/__tests__/fixtures/hir/concise-arrow-expr.expect.md b/compiler/forget/src/__tests__/fixtures/hir/concise-arrow-expr.expect.md index e43709ed22..517503efd5 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/concise-arrow-expr.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/concise-arrow-expr.expect.md @@ -15,7 +15,7 @@ function component() { ```javascript function component() { const $ = React.unstable_useMemoCache(4); - const setX = useState(0)[1]; + const [x, setX] = useState(0); const c_0 = $[0] !== setX; let t0; if (c_0) { diff --git a/compiler/forget/src/__tests__/fixtures/hir/controlled-input.expect.md b/compiler/forget/src/__tests__/fixtures/hir/controlled-input.expect.md index 1e18384a2c..417dcef789 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/controlled-input.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/controlled-input.expect.md @@ -15,9 +15,7 @@ function component() { ```javascript function component() { const $ = React.unstable_useMemoCache(5); - const t2 = useState(0); - const x = t2[0]; - const setX = t2[1]; + const [x, setX] = useState(0); const c_0 = $[0] !== setX; let t0; if (c_0) { diff --git a/compiler/forget/src/__tests__/fixtures/hir/destructure-direct-reassignment.expect.md b/compiler/forget/src/__tests__/fixtures/hir/destructure-direct-reassignment.expect.md new file mode 100644 index 0000000000..7dc266f451 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/destructure-direct-reassignment.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +function foo(props) { + let x, y; + ({ x, y } = { x: props.a, y: props.b }); + x = props.c; + return x + y; +} + +``` + +## Code + +```javascript +function foo(props) { + const $ = React.unstable_useMemoCache(3); + const c_0 = $[0] !== props.a; + const c_1 = $[1] !== props.b; + let t0; + if (c_0 || c_1) { + t0 = { x: props.a, y: props.b }; + $[0] = props.a; + $[1] = props.b; + $[2] = t0; + } else { + t0 = $[2]; + } + let { x, y } = t0; + x = props.c; + return x + y; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/destructure-direct-reassignment.js b/compiler/forget/src/__tests__/fixtures/hir/destructure-direct-reassignment.js new file mode 100644 index 0000000000..27fb91ec50 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/destructure-direct-reassignment.js @@ -0,0 +1,6 @@ +function foo(props) { + let x, y; + ({ x, y } = { x: props.a, y: props.b }); + x = props.c; + return x + y; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/destructuring-assignment.expect.md b/compiler/forget/src/__tests__/fixtures/hir/destructuring-assignment.expect.md index 556b6b1465..f6ceeaa99b 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/destructuring-assignment.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/destructuring-assignment.expect.md @@ -29,11 +29,16 @@ function foo(a, b, c) { function foo(a, b, c) { const $ = React.unstable_useMemoCache(5); - const d = a[0]; - const g = a[1][0].e.f; + const [d, t54] = a; - const n = b.l.m[0][0]; - const o = b.o; + const [t56] = t54; + const { e: t58 } = t56; + const { f: g } = t58; + + const { l: t63, o } = b; + const { m: t66 } = t63; + const [t68] = t66; + const [n] = t68; const c_0 = $[0] !== d; const c_1 = $[1] !== g; const c_2 = $[2] !== n; diff --git a/compiler/forget/src/__tests__/fixtures/hir/destructuring.expect.md b/compiler/forget/src/__tests__/fixtures/hir/destructuring.expect.md index 5548d4a434..f40bb9601b 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/destructuring.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/destructuring.expect.md @@ -8,16 +8,18 @@ function foo(a, b, c) { [ { e: { f }, + ...g }, ], + ...h ] = a; const { l: { - m: [[n]], + m: [[n], ...o], }, - o, + p, } = b; - return [d, f, n, o]; + return [d, f, g, h, n, o, p]; } ``` @@ -26,26 +28,37 @@ function foo(a, b, c) { ```javascript function foo(a, b, c) { - const $ = React.unstable_useMemoCache(5); - const d = a[0]; - const f = a[1][0].e.f; + const $ = React.unstable_useMemoCache(8); + const [d, t40, ...h] = a; - const n = b.l.m[0][0]; - const o = b.o; + const [t43] = t40; + const { e: t45, ...g } = t43; + const { f } = t45; + + const { l: t51, p } = b; + const { m: t54 } = t51; + const [t56, ...o] = t54; + const [n] = t56; const c_0 = $[0] !== d; const c_1 = $[1] !== f; - const c_2 = $[2] !== n; - const c_3 = $[3] !== o; + const c_2 = $[2] !== g; + const c_3 = $[3] !== h; + const c_4 = $[4] !== n; + const c_5 = $[5] !== o; + const c_6 = $[6] !== p; let t0; - if (c_0 || c_1 || c_2 || c_3) { - t0 = [d, f, n, o]; + if (c_0 || c_1 || c_2 || c_3 || c_4 || c_5 || c_6) { + t0 = [d, f, g, h, n, o, p]; $[0] = d; $[1] = f; - $[2] = n; - $[3] = o; - $[4] = t0; + $[2] = g; + $[3] = h; + $[4] = n; + $[5] = o; + $[6] = p; + $[7] = t0; } else { - t0 = $[4]; + t0 = $[7]; } return t0; } diff --git a/compiler/forget/src/__tests__/fixtures/hir/destructuring.js b/compiler/forget/src/__tests__/fixtures/hir/destructuring.js index d75719b816..1e226e3e72 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/destructuring.js +++ b/compiler/forget/src/__tests__/fixtures/hir/destructuring.js @@ -4,14 +4,16 @@ function foo(a, b, c) { [ { e: { f }, + ...g }, ], + ...h ] = a; const { l: { - m: [[n]], + m: [[n], ...o], }, - o, + p, } = b; - return [d, f, n, o]; + return [d, f, g, h, n, o, p]; } diff --git a/compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.expect.md b/compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.expect.md index c004bd6556..70dd995935 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.expect.md @@ -13,14 +13,11 @@ function foo([a, b], { c, d, e = "e" }, f = "f", ...args) { } } - const g = { ...a, b() {}, c: () => {} }; - const h = [...b]; + const g = { b() {}, c: () => {} }; new c(...args); c(...args); - const [y, ...yy] = useState(0); - const { z, aa = "aa", ...zz } = useCustom(); + const { z, aa = "aa" } = useCustom(); - ; ; ; ; @@ -126,293 +123,257 @@ let moduleLocal = false; 7 | constructor() { 8 | console.log(this.#secretSauce); -[ReactForget] TodoError: (BuildHIR::lowerExpression) Handle SpreadElement properties in ObjectExpression - 10 | } - 11 | -> 12 | const g = { ...a, b() {}, c: () => {} }; - | ^^^^ - 13 | const h = [...b]; - 14 | new c(...args); - 15 | c(...args); - [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle ObjectMethod properties in ObjectExpression 10 | } 11 | -> 12 | const g = { ...a, b() {}, c: () => {} }; - | ^^^^^^ - 13 | const h = [...b]; - 14 | new c(...args); - 15 | c(...args); - -[ReactForget] TodoError: (BuildHIR::lowerExpression) Handle SpreadElement elements in ArrayExpression - 11 | - 12 | const g = { ...a, b() {}, c: () => {} }; -> 13 | const h = [...b]; - | ^^^^ - 14 | new c(...args); - 15 | c(...args); - 16 | const [y, ...yy] = useState(0); +> 12 | const g = { b() {}, c: () => {} }; + | ^^^^^^ + 13 | new c(...args); + 14 | c(...args); + 15 | const { z, aa = "aa" } = useCustom(); [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle SpreadElement arguments in NewExpression - 12 | const g = { ...a, b() {}, c: () => {} }; - 13 | const h = [...b]; -> 14 | new c(...args); + 11 | + 12 | const g = { b() {}, c: () => {} }; +> 13 | new c(...args); | ^^^^^^^ - 15 | c(...args); - 16 | const [y, ...yy] = useState(0); - 17 | const { z, aa = "aa", ...zz } = useCustom(); + 14 | c(...args); + 15 | const { z, aa = "aa" } = useCustom(); + 16 | [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle SpreadElement arguments in CallExpression - 13 | const h = [...b]; - 14 | new c(...args); -> 15 | c(...args); + 12 | const g = { b() {}, c: () => {} }; + 13 | new c(...args); +> 14 | c(...args); | ^^^^^^^ - 16 | const [y, ...yy] = useState(0); - 17 | const { z, aa = "aa", ...zz } = useCustom(); - 18 | - -[ReactForget] TodoError: (BuildHIR::lowerAssignment) Handle RestElement in ArrayPattern - 14 | new c(...args); - 15 | c(...args); -> 16 | const [y, ...yy] = useState(0); - | ^^^^^ - 17 | const { z, aa = "aa", ...zz } = useCustom(); - 18 | - 19 | ; + 15 | const { z, aa = "aa" } = useCustom(); + 16 | + 17 | ; [ReactForget] TodoError: (BuildHIR::lowerAssignment) Handle AssignmentPattern assignments - 15 | c(...args); - 16 | const [y, ...yy] = useState(0); -> 17 | const { z, aa = "aa", ...zz } = useCustom(); + 13 | new c(...args); + 14 | c(...args); +> 15 | const { z, aa = "aa" } = useCustom(); | ^^^^^^^^^ - 18 | - 19 | ; - 20 | ; - -[ReactForget] TodoError: (BuildHIR::lowerAssignment) Handle RestElement properties in ObjectPattern - 15 | c(...args); - 16 | const [y, ...yy] = useState(0); -> 17 | const { z, aa = "aa", ...zz } = useCustom(); - | ^^^^^ - 18 | - 19 | ; - 20 | ; + 16 | + 17 | ; + 18 | ; [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle JSXNamespacedName attribute names in JSXElement - 18 | - 19 | ; -> 20 | ; + 15 | const { z, aa = "aa" } = useCustom(); + 16 | +> 17 | ; | ^^^^^^^^^^ - 21 | ; - 22 | ; - 23 | ; + 18 | ; + 19 | ; + 20 | ; [ReactForget] TodoError: (BuildHIR::lowerJsxElement) Handle JSXEmptyExpression expressions - 20 | ; - 21 | ; -> 22 | ; + 17 | ; + 18 | ; +> 19 | ; | ^^^^^^^^^^^^ - 23 | ; - 24 | - 25 | const j = function bar([quz, qux], ...args) {}; + 20 | ; + 21 | + 22 | const j = function bar([quz, qux], ...args) {}; [ReactForget] TodoError: (BuildHIR::lowerJsxElementName) Handle JSXMemberExpression tags - 21 | ; - 22 | ; -> 23 | ; + 18 | ; + 19 | ; +> 20 | ; | ^^^^^^^^^^^^^^^^^^^ - 24 | - 25 | const j = function bar([quz, qux], ...args) {}; - 26 | + 21 | + 22 | const j = function bar([quz, qux], ...args) {}; + 23 | [ReactForget] TodoError: (BuildHIR::lower) Handle ArrayPattern params - 23 | ; - 24 | -> 25 | const j = function bar([quz, qux], ...args) {}; + 20 | ; + 21 | +> 22 | const j = function bar([quz, qux], ...args) {}; | ^^^^^^^^^^ - 26 | - 27 | for (; i < 3; i += 1) { - 28 | x.push(i); + 23 | + 24 | for (; i < 3; i += 1) { + 25 | x.push(i); [ReactForget] TodoError: (BuildHIR::lower) Handle RestElement params - 23 | ; - 24 | -> 25 | const j = function bar([quz, qux], ...args) {}; + 20 | ; + 21 | +> 22 | const j = function bar([quz, qux], ...args) {}; | ^^^^^^^ - 26 | - 27 | for (; i < 3; i += 1) { - 28 | x.push(i); + 23 | + 24 | for (; i < 3; i += 1) { + 25 | x.push(i); [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement - 25 | const j = function bar([quz, qux], ...args) {}; - 26 | -> 27 | for (; i < 3; i += 1) { + 22 | const j = function bar([quz, qux], ...args) {}; + 23 | +> 24 | for (; i < 3; i += 1) { | ^ - 28 | x.push(i); - 29 | } - 30 | for (; i < 3; ) { + 25 | x.push(i); + 26 | } + 27 | for (; i < 3; ) { [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement - 28 | x.push(i); - 29 | } -> 30 | for (; i < 3; ) { + 25 | x.push(i); + 26 | } +> 27 | for (; i < 3; ) { | ^ - 31 | break; - 32 | } - 33 | for (;;) { + 28 | break; + 29 | } + 30 | for (;;) { [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle empty update in ForStatement - 28 | x.push(i); - 29 | } -> 30 | for (; i < 3; ) { + 25 | x.push(i); + 26 | } +> 27 | for (; i < 3; ) { | ^ - 31 | break; - 32 | } - 33 | for (;;) { + 28 | break; + 29 | } + 30 | for (;;) { [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement + 28 | break; + 29 | } +> 30 | for (;;) { + | ^ 31 | break; 32 | } -> 33 | for (;;) { - | ^ - 34 | break; - 35 | } - 36 | + 33 | [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle empty update in ForStatement + 28 | break; + 29 | } +> 30 | for (;;) { + | ^ 31 | break; 32 | } -> 33 | for (;;) { - | ^ - 34 | break; - 35 | } - 36 | + 33 | [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle empty test in ForStatement + 28 | break; + 29 | } +> 30 | for (;;) { + | ^ 31 | break; 32 | } -> 33 | for (;;) { - | ^ - 34 | break; - 35 | } - 36 | + 33 | [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle tagged template with interpolations - 35 | } - 36 | -> 37 | graphql` + 32 | } + 33 | +> 34 | graphql` | ^ - 38 | ${g} - 39 | `; - 40 | + 35 | ${g} + 36 | `; + 37 | [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value - 39 | `; - 40 | -> 41 | graphql`\\t\n`; + 36 | `; + 37 | +> 38 | graphql`\\t\n`; | ^^^^^^^^^^^^^^ - 42 | - 43 | for (const c of [1, 2]) { - 44 | } + 39 | + 40 | for (const c of [1, 2]) { + 41 | } [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle ForOfStatement statements - 41 | graphql`\\t\n`; - 42 | -> 43 | for (const c of [1, 2]) { + 38 | graphql`\\t\n`; + 39 | +> 40 | for (const c of [1, 2]) { | ^ - 44 | } - 45 | - 46 | for (let x in { a: 1 }) { + 41 | } + 42 | + 43 | for (let x in { a: 1 }) { [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle ForInStatement statements + 41 | } + 42 | +> 43 | for (let x in { a: 1 }) { + | ^ 44 | } 45 | -> 46 | for (let x in { a: 1 }) { - | ^ - 47 | } - 48 | - 49 | let updateIdentifier = 0; + 46 | let updateIdentifier = 0; [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle prefix UpdateExpression - 48 | - 49 | let updateIdentifier = 0; -> 50 | --updateIdentifier; + 45 | + 46 | let updateIdentifier = 0; +> 47 | --updateIdentifier; | ^^^^^^^^^^^^^^^^^^ - 51 | ++updateIdentifier; - 52 | updateIdentifier.y++; - 53 | updateIdentifier.y--; + 48 | ++updateIdentifier; + 49 | updateIdentifier.y++; + 50 | updateIdentifier.y--; [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle prefix UpdateExpression - 49 | let updateIdentifier = 0; - 50 | --updateIdentifier; -> 51 | ++updateIdentifier; + 46 | let updateIdentifier = 0; + 47 | --updateIdentifier; +> 48 | ++updateIdentifier; | ^^^^^^^^^^^^^^^^^^ - 52 | updateIdentifier.y++; - 53 | updateIdentifier.y--; - 54 | + 49 | updateIdentifier.y++; + 50 | updateIdentifier.y--; + 51 | [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle UpdateExpression with MemberExpression argument - 50 | --updateIdentifier; - 51 | ++updateIdentifier; -> 52 | updateIdentifier.y++; + 47 | --updateIdentifier; + 48 | ++updateIdentifier; +> 49 | updateIdentifier.y++; | ^^^^^^^^^^^^^^^^^^^^ - 53 | updateIdentifier.y--; - 54 | - 55 | switch (i) { + 50 | updateIdentifier.y--; + 51 | + 52 | switch (i) { [ReactForget] TodoError: (BuildHIR::lowerExpression) Handle UpdateExpression with MemberExpression argument - 51 | ++updateIdentifier; - 52 | updateIdentifier.y++; -> 53 | updateIdentifier.y--; + 48 | ++updateIdentifier; + 49 | updateIdentifier.y++; +> 50 | updateIdentifier.y--; | ^^^^^^^^^^^^^^^^^^^^ - 54 | - 55 | switch (i) { - 56 | case 1 + 1: { + 51 | + 52 | switch (i) { + 53 | case 1 + 1: { [ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported - 58 | case foo(): { - 59 | } -> 60 | case x.y: { + 55 | case foo(): { + 56 | } +> 57 | case x.y: { | ^^^ - 61 | } - 62 | default: { - 63 | } + 58 | } + 59 | default: { + 60 | } [ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported - 56 | case 1 + 1: { - 57 | } -> 58 | case foo(): { + 53 | case 1 + 1: { + 54 | } +> 55 | case foo(): { | ^^^^^ - 59 | } - 60 | case x.y: { - 61 | } + 56 | } + 57 | case x.y: { + 58 | } [ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported - 54 | - 55 | switch (i) { -> 56 | case 1 + 1: { + 51 | + 52 | switch (i) { +> 53 | case 1 + 1: { | ^^^^^ - 57 | } - 58 | case foo(): { - 59 | } + 54 | } + 55 | case foo(): { + 56 | } [ReactForget] InvalidInputError: (BuildHIR::lowerAssignment) Assigning to an identifier defined outside the function scope is not supported. - 65 | - 66 | // Cannot assign to globals -> 67 | someUnknownGlobal = true; + 62 | + 63 | // Cannot assign to globals +> 64 | someUnknownGlobal = true; | ^^^^^^^^^^^^^^^^^ - 68 | moduleLocal = true; - 69 | } - 70 | + 65 | moduleLocal = true; + 66 | } + 67 | [ReactForget] InvalidInputError: (BuildHIR::lowerAssignment) Assigning to an identifier defined outside the function scope is not supported. - 66 | // Cannot assign to globals - 67 | someUnknownGlobal = true; -> 68 | moduleLocal = true; + 63 | // Cannot assign to globals + 64 | someUnknownGlobal = true; +> 65 | moduleLocal = true; | ^^^^^^^^^^^ - 69 | } - 70 | - 71 | let moduleLocal = false; + 66 | } + 67 | + 68 | let moduleLocal = false; ``` \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.js b/compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.js index 3c94e92198..5f79d9c548 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.js +++ b/compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.js @@ -9,14 +9,11 @@ function foo([a, b], { c, d, e = "e" }, f = "f", ...args) { } } - const g = { ...a, b() {}, c: () => {} }; - const h = [...b]; + const g = { b() {}, c: () => {} }; new c(...args); c(...args); - const [y, ...yy] = useState(0); - const { z, aa = "aa", ...zz } = useCustom(); + const { z, aa = "aa" } = useCustom(); - ; ; ; ; diff --git a/compiler/forget/src/__tests__/fixtures/hir/inadvertent-mutability-readonly-lambda.expect.md b/compiler/forget/src/__tests__/fixtures/hir/inadvertent-mutability-readonly-lambda.expect.md index b84ed09282..b4de9d23f9 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/inadvertent-mutability-readonly-lambda.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/inadvertent-mutability-readonly-lambda.expect.md @@ -22,7 +22,7 @@ function Component(props) { ```javascript function Component(props) { - const setValue = useState(null)[1]; + const [value, setValue] = useState(null); const onChange = (e) => setValue((value) => value + e.target.value); diff --git a/compiler/forget/src/__tests__/fixtures/hir/object-pattern-params.expect.md b/compiler/forget/src/__tests__/fixtures/hir/object-pattern-params.expect.md index 09d63d71ff..8ad14aa253 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/object-pattern-params.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/object-pattern-params.expect.md @@ -13,10 +13,9 @@ function component({ a, b }) { ## Code ```javascript -function component(t19) { +function component(t16) { const $ = React.unstable_useMemoCache(7); - const a = t19.a; - const b = t19.b; + const { a, b } = t16; const c_0 = $[0] !== a; let t0; if (c_0) { diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring-with-mutation.expect.md b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring-with-mutation.expect.md new file mode 100644 index 0000000000..741838448e --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring-with-mutation.expect.md @@ -0,0 +1,44 @@ + +## Input + +```javascript +function foo(props) { + let { x } = { x: [] }; + x.push(props.bar); + if (props.cond) { + ({ x } = { x: {} }); + ({ x } = { x: [] }); + x.push(props.foo); + } + mut(x); + return x; +} + +``` + +## Code + +```javascript +function foo(props) { + const $ = React.unstable_useMemoCache(2); + const c_0 = $[0] !== props; + let x; + if (c_0) { + ({ x } = { x: [] }); + x.push(props.bar); + if (props.cond) { + ({ x } = { x: [] }); + x.push(props.foo); + } + + mut(x); + $[0] = props; + $[1] = x; + } else { + x = $[1]; + } + return x; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring-with-mutation.js b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring-with-mutation.js new file mode 100644 index 0000000000..bb6e80749f --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring-with-mutation.js @@ -0,0 +1,11 @@ +function foo(props) { + let { x } = { x: [] }; + x.push(props.bar); + if (props.cond) { + ({ x } = { x: {} }); + ({ x } = { x: [] }); + x.push(props.foo); + } + mut(x); + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring.expect.md b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring.expect.md new file mode 100644 index 0000000000..23c6be2ab4 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring.expect.md @@ -0,0 +1,48 @@ + +## Input + +```javascript +function foo(props) { + let { x } = { x: [] }; + x.push(props.bar); + if (props.cond) { + ({ x } = { x: {} }); + ({ x } = { x: [] }); + x.push(props.foo); + } + return x; +} + +``` + +## Code + +```javascript +function foo(props) { + const $ = React.unstable_useMemoCache(4); + const c_0 = $[0] !== props.bar; + let x; + if (c_0) { + ({ x } = { x: [] }); + x.push(props.bar); + $[0] = props.bar; + $[1] = x; + } else { + x = $[1]; + } + if (props.cond) { + const c_2 = $[2] !== props.foo; + if (c_2) { + ({ x } = { x: [] }); + x.push(props.foo); + $[2] = props.foo; + $[3] = x; + } else { + x = $[3]; + } + } + return x; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring.js b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring.js new file mode 100644 index 0000000000..05fe3bbdf9 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-renaming-via-destructuring.js @@ -0,0 +1,10 @@ +function foo(props) { + let { x } = { x: [] }; + x.push(props.bar); + if (props.cond) { + ({ x } = { x: {} }); + ({ x } = { x: [] }); + x.push(props.foo); + } + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/use-callback-simple.expect.md b/compiler/forget/src/__tests__/fixtures/hir/use-callback-simple.expect.md index da5aab321d..27e04507db 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/use-callback-simple.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/use-callback-simple.expect.md @@ -16,9 +16,7 @@ function component() { ```javascript function component() { const $ = React.unstable_useMemoCache(5); - const t2 = useState(0); - const count = t2[0]; - const setCount = t2[1]; + const [count, setCount] = useState(0); const c_0 = $[0] !== setCount; const c_1 = $[1] !== count; let t0;