@samitouri / QOS-React-2 / commits / 70a7d5f41e

Codegen from ReactiveFunction (no memoization yet)

Currently codegen operates from HIR using a tree visitor, but for scope construction we're converting the HIR (CFG) into a ReactiveFunction (AST-like). Our original idea for codegen was that we would convert the ReactiveFunction back to HIR, and then codegen from there. However, the ReactiveFunction is already in tree form...which makes it very straightforward to generate code from. So this PR implements codegen from ReactiveFunction. The output is _identical_ thanks in large part to reusing as much logic from Codegen.ts as possible. The next PR will add memoization logic.

Joe Savona committed Dec 14, 2022 at 14:54 UTC 70a7d5f41e10ca04bfa800ddba2cc649f3248b78
4 files changed +315 -68
compiler/forget/src/HIR/Codegen.ts
+60 -52
@@ -44,13 +44,13 @@ function withLoc<TNode extends t.Node, T extends (...args: any[]) => TNode>(
44 };
45 }
46
47 -const createBinaryExpression = withLoc(t.binaryExpression);
48 -const createCallExpression = withLoc(t.callExpression);
49 -const createExpressionStatement = withLoc(t.expressionStatement);
50 -const createFunctionDeclaration = withLoc(t.functionDeclaration);
51 -const createLabelledStatement = withLoc(t.labeledStatement);
52 -const createVariableDeclaration = withLoc(t.variableDeclaration);
53 -const createWhileStatement = withLoc(t.whileStatement);
47 +export const createBinaryExpression = withLoc(t.binaryExpression);
48 +export const createCallExpression = withLoc(t.callExpression);
49 +export const createExpressionStatement = withLoc(t.expressionStatement);
50 +export const createFunctionDeclaration = withLoc(t.functionDeclaration);
51 +export const createLabelledStatement = withLoc(t.labeledStatement);
52 +export const createVariableDeclaration = withLoc(t.variableDeclaration);
53 +export const createWhileStatement = withLoc(t.whileStatement);
54
55 /**
56 * Converts HIR into Babel nodes, which can then be printed into source text.
@@ -87,7 +87,7 @@ export default function codegen(fn: HIRFunction): t.Function {
87 );
88 }
89
90 -type Temporaries = Map<IdentifierId, t.Expression>;
90 +export type Temporaries = Map<IdentifierId, t.Expression>;
91
92 class CodegenVisitor
93 implements
@@ -189,45 +189,7 @@ class CodegenVisitor
189 return codegenInstructionValue(this.temp, value);
190 }
191 visitInstruction(instr: Instruction, value: t.Expression): t.Statement {
192 - if (t.isStatement(value)) {
193 - return value;
194 - }
195 - if (instr.lvalue === null) {
196 - return t.expressionStatement(value);
197 - }
198 - if (
199 - instr.lvalue.place.memberPath === null &&
200 - instr.lvalue.place.identifier.name === null
201 - ) {
202 - // temporary
203 - this.temp.set(instr.lvalue.place.identifier.id, value);
204 - return t.emptyStatement();
205 - } else {
206 - switch (instr.lvalue.kind) {
207 - case InstructionKind.Const: {
208 - return createVariableDeclaration(instr.loc, "const", [
209 - t.variableDeclarator(codegenLVal(instr.lvalue), value),
210 - ]);
211 - }
212 - case InstructionKind.Let: {
213 - return createVariableDeclaration(instr.loc, "let", [
214 - t.variableDeclarator(codegenLVal(instr.lvalue), value),
215 - ]);
216 - }
217 - case InstructionKind.Reassign: {
218 - return createExpressionStatement(
219 - instr.loc,
220 - t.assignmentExpression("=", codegenLVal(instr.lvalue), value)
221 - );
222 - }
223 - default: {
224 - assertExhaustive(
225 - instr.lvalue.kind,
226 - `Unexpected instruction kind '${instr.lvalue.kind}'`
227 - );
228 - }
229 - }
230 - }
192 + return codegenInstruction(this.temp, instr, value);
193 }
194 visitTerminalId(id: InstructionId): void {}
195 visitImplicitTerminal(): t.Statement | null {
@@ -325,11 +287,57 @@ class CodegenVisitor
287 }
288 }
289
328 -function codegenLabel(id: BlockId): string {
290 +export function codegenLabel(id: BlockId): string {
291 return `bb${id}`;
292 }
293
332 -function codegenInstructionValue(
294 +export function codegenInstruction(
295 + temp: Temporaries,
296 + instr: Instruction,
297 + value: t.Expression
298 +): t.Statement {
299 + if (t.isStatement(value)) {
300 + return value;
301 + }
302 + if (instr.lvalue === null) {
303 + return t.expressionStatement(value);
304 + }
305 + if (
306 + instr.lvalue.place.memberPath === null &&
307 + instr.lvalue.place.identifier.name === null
308 + ) {
309 + // temporary
310 + temp.set(instr.lvalue.place.identifier.id, value);
311 + return t.emptyStatement();
312 + } else {
313 + switch (instr.lvalue.kind) {
314 + case InstructionKind.Const: {
315 + return createVariableDeclaration(instr.loc, "const", [
316 + t.variableDeclarator(codegenLVal(instr.lvalue), value),
317 + ]);
318 + }
319 + case InstructionKind.Let: {
320 + return createVariableDeclaration(instr.loc, "let", [
321 + t.variableDeclarator(codegenLVal(instr.lvalue), value),
322 + ]);
323 + }
324 + case InstructionKind.Reassign: {
325 + return createExpressionStatement(
326 + instr.loc,
327 + t.assignmentExpression("=", codegenLVal(instr.lvalue), value)
328 + );
329 + }
330 + default: {
331 + assertExhaustive(
332 + instr.lvalue.kind,
333 + `Unexpected instruction kind '${instr.lvalue.kind}'`
334 + );
335 + }
336 + }
337 + }
338 +}
339 +
340 +export function codegenInstructionValue(
341 temp: Temporaries,
342 instrValue: InstructionValue
343 ): t.Expression {
@@ -484,7 +492,7 @@ function codegenJsxElement(
492 }
493 }
494
487 -function codegenLVal(lval: LValue): t.LVal {
495 +export function codegenLVal(lval: LValue): t.LVal {
496 const expr = convertIdentifier(lval.place.identifier);
497 const memberPath = lval.place.memberPath;
498 return memberPath == null
@@ -515,7 +523,7 @@ function codegenValue(
523 }
524 }
525
518 -function codegenPlace(temp: Temporaries, place: Place): t.Expression {
526 +export function codegenPlace(temp: Temporaries, place: Place): t.Expression {
527 todoInvariant(place.kind === "Identifier", "support scope values");
528 if (place.memberPath === null) {
529 let tmp = temp.get(place.identifier.id);
@@ -532,7 +540,7 @@ function codegenPlace(temp: Temporaries, place: Place): t.Expression {
540 }
541 }
542
535 -function convertIdentifier(identifier: Identifier): t.Identifier {
543 +export function convertIdentifier(identifier: Identifier): t.Identifier {
544 if (identifier.name !== null) {
545 return t.identifier(`${identifier.name}$${identifier.id}`);
546 }
compiler/forget/src/HIR/CodegenReactiveFunction.ts new
+237
@@ -0,0 +1,237 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import * as t from "@babel/types";
9 +import invariant from "invariant";
10 +import { assertExhaustive } from "../Common/utils";
11 +import {
12 + codegenInstruction,
13 + codegenInstructionValue,
14 + codegenLabel,
15 + codegenPlace,
16 + convertIdentifier,
17 + createFunctionDeclaration,
18 + Temporaries,
19 +} from "./Codegen";
20 +import {
21 + Instruction,
22 + ReactiveBasicBlock,
23 + ReactiveFunction,
24 + ReactiveScope,
25 + ReactiveTerminal,
26 + ReactiveValueBlock,
27 +} from "./HIR";
28 +import { todoInvariant } from "./todo";
29 +
30 +export function codegenReactiveFunction(fn: ReactiveFunction): t.Function {
31 + const cx = new Context();
32 + const params = fn.params.map((param) => convertIdentifier(param.identifier));
33 + const body = codegenBlock(cx, fn.body);
34 + const statements = body.body;
35 + if (statements.length !== 0) {
36 + const last = statements[statements.length - 1];
37 + if (last.type === "ReturnStatement" && last.argument == null) {
38 + statements.pop();
39 + }
40 + }
41 + return createFunctionDeclaration(
42 + fn.loc,
43 + fn.id !== null ? convertIdentifier(fn.id) : null,
44 + params,
45 + body,
46 + fn.generator,
47 + fn.async
48 + );
49 +}
50 +
51 +class Context {
52 + temp: Temporaries = new Map();
53 +}
54 +
55 +function codegenBlock(
56 + cx: Context,
57 + block: ReactiveBasicBlock
58 +): t.BlockStatement {
59 + const statements: Array<t.Statement> = [];
60 + for (const item of block) {
61 + switch (item.kind) {
62 + case "instruction": {
63 + const statement = codegenInstructionNullable(
64 + cx.temp,
65 + item.instruction,
66 + codegenInstructionValue(cx.temp, item.instruction.value)
67 + );
68 + if (statement !== null) {
69 + statements.push(statement);
70 + }
71 + break;
72 + }
73 + case "block": {
74 + codegenReactiveScope(cx, statements, item.scope, item.instructions);
75 + break;
76 + }
77 + case "terminal": {
78 + const statement = codegenTerminal(cx, item.terminal);
79 + if (item.label !== null) {
80 + statements.push(
81 + t.labeledStatement(
82 + t.identifier(codegenLabel(item.label)),
83 + statement
84 + )
85 + );
86 + } else {
87 + statements.push(statement);
88 + }
89 + break;
90 + }
91 + default: {
92 + assertExhaustive(item, `Unexpected item kind '${(item as any).kind}'`);
93 + }
94 + }
95 + }
96 + return t.blockStatement(statements);
97 +}
98 +
99 +function codegenReactiveScope(
100 + cx: Context,
101 + statements: Array<t.Statement>,
102 + scope: ReactiveScope,
103 + block: ReactiveBasicBlock
104 +): void {
105 + // TODO @josephsavona: Emit memoized blocks!
106 + const body = codegenBlock(cx, block).body;
107 + statements.push(...body);
108 +}
109 +
110 +function codegenTerminal(cx: Context, terminal: ReactiveTerminal): t.Statement {
111 + switch (terminal.kind) {
112 + case "break": {
113 + return t.breakStatement(
114 + terminal.label !== null
115 + ? t.identifier(codegenLabel(terminal.label))
116 + : null
117 + );
118 + }
119 + case "continue": {
120 + return t.continueStatement(
121 + terminal.label !== null
122 + ? t.identifier(codegenLabel(terminal.label))
123 + : null
124 + );
125 + }
126 + case "for": {
127 + return t.forStatement(
128 + codegenForInit(cx, terminal.init),
129 + codegenValueBlock(cx, terminal.test),
130 + codegenValueBlock(cx, terminal.update),
131 + codegenBlock(cx, terminal.loop)
132 + );
133 + }
134 + case "if": {
135 + return t.ifStatement(
136 + codegenPlace(cx.temp, terminal.test),
137 + codegenBlock(cx, terminal.consequent),
138 + terminal.alternate !== null
139 + ? codegenBlock(cx, terminal.alternate)
140 + : null
141 + );
142 + }
143 + case "return": {
144 + return t.returnStatement(
145 + terminal.value !== null ? codegenPlace(cx.temp, terminal.value) : null
146 + );
147 + }
148 + case "switch": {
149 + return t.switchStatement(
150 + codegenPlace(cx.temp, terminal.test),
151 + terminal.cases.map((case_) => {
152 + const test =
153 + case_.test !== null ? codegenPlace(cx.temp, case_.test) : null;
154 + const block = codegenBlock(cx, case_.block!);
155 + return t.switchCase(test, [block]);
156 + })
157 + );
158 + }
159 + case "throw": {
160 + return t.throwStatement(codegenPlace(cx.temp, terminal.value));
161 + }
162 + case "while": {
163 + const test = codegenValueBlock(cx, terminal.test);
164 + return t.whileStatement(test, codegenBlock(cx, terminal.loop));
165 + }
166 + default: {
167 + assertExhaustive(
168 + terminal,
169 + `Unexpected terminal kind '${(terminal as any).kind}'`
170 + );
171 + }
172 + }
173 +}
174 +
175 +export function codegenInstructionNullable(
176 + temp: Temporaries,
177 + instr: Instruction,
178 + value: t.Expression
179 +): t.Statement | null {
180 + const statement = codegenInstruction(temp, instr, value);
181 + if (statement.type === "EmptyStatement") {
182 + return null;
183 + }
184 + return statement;
185 +}
186 +
187 +function codegenForInit(
188 + cx: Context,
189 + init: ReactiveValueBlock
190 +): t.Expression | t.VariableDeclaration | null {
191 + const body = codegenBlock(cx, init.instructions).body;
192 + if (init.value !== null) {
193 + invariant(
194 + body.length === 0,
195 + "Expected for init block to produce only temporaries"
196 + );
197 + return codegenInstructionValue(cx.temp, init.value);
198 + } else {
199 + invariant(
200 + body.length === 1,
201 + "Expected for init to have a variable declaration"
202 + );
203 + const declaration = body[0]!;
204 + invariant(
205 + declaration.type === "VariableDeclaration",
206 + "Expected a variable declaration"
207 + );
208 + return declaration;
209 + }
210 +}
211 +
212 +function codegenValueBlock(
213 + cx: Context,
214 + block: ReactiveValueBlock
215 +): t.Expression {
216 + const body = codegenBlock(cx, block.instructions).body;
217 + const expressions = body.map((stmt) => {
218 + if (stmt.type === "ExpressionStatement") {
219 + return stmt.expression;
220 + } else {
221 + todoInvariant(false, `Handle conversion of ${stmt.type} to expression`);
222 + }
223 + });
224 + if (block.value !== null) {
225 + const value = codegenInstructionValue(cx.temp, block.value);
226 + expressions.push(value);
227 + }
228 + invariant(
229 + expressions.length !== 0,
230 + "Expected a value block to produce one or more expressions"
231 + );
232 + if (expressions.length === 1) {
233 + return expressions[0];
234 + } else {
235 + return t.sequenceExpression(expressions);
236 + }
237 +}
compiler/forget/src/HIR/Pipeline.ts
+15 -4
@@ -12,13 +12,17 @@ import enterSSA from "../HIR/EnterSSA";
12 import { Environment } from "../HIR/HIRBuilder";
13 import inferReferenceEffects from "../HIR/InferReferenceEffects";
14 import { leaveSSA } from "../HIR/LeaveSSA";
15 -import codegen from "./Codegen";
15 +import { buildReactiveFunction } from "./BuildReactiveFunction";
16 +import { codegenReactiveFunction } from "./CodegenReactiveFunction";
17 +import { flattenReactiveLoops } from "./FlattenReactiveLoops";
18 import { HIRFunction } from "./HIR";
19 import { inferMutableRanges } from "./InferMutableRanges";
20 import { inferReactiveScopes } from "./InferReactiveScopes";
21 import { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
22 import { inferTypes } from "./InferTypes";
23 import { logHIRFunction } from "./logger";
24 +import { printReactiveFunction } from "./PrintReactiveFunction";
25 +import { propagateScopeDependencies } from "./PropagateScopeDependencies";
26
27 export type CompilerFlags = {
28 eliminateRedundantPhi: boolean;
@@ -35,6 +39,7 @@ export type CompilerFlags = {
39 export type CompilerResult = {
40 ir: HIRFunction;
41 ast: t.Function | null;
42 + scopes: string | null;
43 };
44
45 export default function (
@@ -83,11 +88,17 @@ export default function (
88 }
89
90 if (flags.codegen) {
91 + const reactiveFunction = buildReactiveFunction(ir);
92 + flattenReactiveLoops(reactiveFunction);
93 + propagateScopeDependencies(reactiveFunction);
94 + const scopes = printReactiveFunction(reactiveFunction);
95 + const ast = codegenReactiveFunction(reactiveFunction);
96 return {
87 - ast: codegen(ir),
88 - ir: ir,
97 + ast,
98 + ir,
99 + scopes,
100 };
101 }
102
92 - return { ast: null, ir: ir };
103 + return { ast: null, scopes: null, ir: ir };
104 }
compiler/forget/src/__tests__/hir-test.ts
+3 -12
@@ -14,13 +14,9 @@ import { wasmFolder } from "@hpcc-js/wasm";
14 import invariant from "invariant";
15 import path from "path";
16 import prettier from "prettier";
17 -import { buildReactiveFunction } from "../HIR/BuildReactiveFunction";
18 -import { flattenReactiveLoops } from "../HIR/FlattenReactiveLoops";
17 import { toggleLogging } from "../HIR/logger";
18 import run from "../HIR/Pipeline";
19 import { printFunction } from "../HIR/PrintHIR";
22 -import { printReactiveFunction } from "../HIR/PrintReactiveFunction";
23 -import { propagateScopeDependencies } from "../HIR/PropagateScopeDependencies";
20 import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
21
22 function wrapWithTripleBackticks(s: string, ext?: string) {
@@ -137,7 +133,7 @@ function transform(text: string, file: string): Array<TestOutput> {
133 traverse(ast, {
134 FunctionDeclaration: {
135 enter(nodePath) {
140 - const { ir, ast } = run(nodePath, {
136 + const { ir, scopes, ast } = run(nodePath, {
137 eliminateRedundantPhi: true,
138 inferReferenceEffects: true,
139 inferTypes: true,
@@ -149,14 +145,9 @@ function transform(text: string, file: string): Array<TestOutput> {
145 codegen: true,
146 });
147
152 - const reactiveFunction = buildReactiveFunction(ir);
153 - flattenReactiveLoops(reactiveFunction);
154 - propagateScopeDependencies(reactiveFunction);
155 - const scopes = printReactiveFunction(reactiveFunction);
156 -
148 const textHIR = printFunction(ir);
158 -
159 - invariant(ast !== null, "ast is null when codegen option is enabled");
149 + invariant(ast, "Expected an ast");
150 + invariant(scopes, "Expected printed scope data");
151 const text = prettier.format(generate(ast).code.replace("\n\n", "\n"), {
152 semi: true,
153 parser: "babel-ts",