@samitouri / QOS-React-2 / commits / b3086817d7

[tests][hir-typer] Add hir-tests fixture

--- Currently, we run type inference passes early in the pipeline and do not check inference output in any tests, test fixtures, or verifier passes. In fact, the only ways to view inferred types are (1) locally add a test fixture with`@only` and inspect console logs or (2) scroll to the relevant section on a playground example. However, inferred types and effects significantly affect the output of later passes (Alias / MutableRange analysis, InferReactiveIdentifiers, etc), and we have already found some bugs due to incorrect inference (e.g. #1274). This PR add the `typer-tests` fixture with the following goals 1. Record relevant current compiler type + effect inference output. 2. Have relatively stable output (with respect to changes in HIR and PrintHIR). - we try to achieve this by annotating the source code.

Mofei Zhang committed Mar 13, 2023 at 14:52 UTC b3086817d747dec142a2c04911bab48f6dc5d487
7 files changed +228 -23
compiler/forget/src/__tests__/compiler-test.ts
+1 -23
@@ -28,32 +28,10 @@ wasmFolder(
28 path.join(__dirname, "..", "..", "node_modules", "@hpcc-js", "wasm", "dist")
29 );
30
31 -const Pragma_RE = /\/\/\s*@enable\((\w+)\)$/gm;
32 -const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
33 -
31 describe("React Forget (HIR version)", () => {
32 generateTestsFromFixtures(
33 path.join(__dirname, "fixtures", "compiler"),
34 (input, file, options) => {
38 - const matches = input.matchAll(Pragma_RE);
39 -
40 - for (const match of matches) {
41 - const [, key, value] = match;
42 - switch (key) {
43 - case "Pass":
44 - // do something with value;
45 - break;
46 - default:
47 - throw new Error(`unknown pragma: ${key}`);
48 - }
49 - }
50 -
51 - let useFlow: boolean = false;
52 - for (const flowPragma of FlowPragmas) {
53 - useFlow ||= !!input.match(flowPragma);
54 - }
55 -
56 - let language: "flow" | "typescript" = useFlow ? "flow" : "typescript";
35 let items: Array<TestOutput> = [];
36 let error: Error | null = null;
37 if (options.debug) {
@@ -61,7 +39,7 @@ describe("React Forget (HIR version)", () => {
39 }
40 try {
41 items.push({
64 - js: runReactForgetBabelPlugin(input, file, language, {
42 + js: runReactForgetBabelPlugin(input, file, options.language, {
43 enableOnlyOnUseForgetDirective:
44 options.enableOnlyOnUseForgetDirective,
45 environment: {
compiler/forget/src/__tests__/fixtures/hir/simple-array.expect.md new
+31
@@ -0,0 +1,31 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const x = props.a + 3;
7 + const y = foo(x);
8 + return [x, y];
9 +}
10 +
11 +```
12 +
13 +## HIR
14 +
15 +```javascript
16 +bb0 (block):
17 + [1] mutate $17 = LoadLocal read props$16
18 + [2] mutate $18:TPrimitive = PropertyLoad read $17.a
19 + [3] mutate $19:TPrimitive = 3
20 + [4] mutate $20:TPrimitive = Binary read $18:TPrimitive + read $19:TPrimitive
21 + [5] store $22:TPrimitive = StoreLocal Const mutate x$21:TPrimitive = read $20:TPrimitive
22 + [6] mutate $23:TFunction = Global foo
23 + [7] mutate $24:TPrimitive = LoadLocal read x$21:TPrimitive
24 + [8] mutate $25 = Call read $23:TFunction(read $24:TPrimitive)
25 + [9] store $27 = StoreLocal Const mutate y$26 = capture $25
26 + [10] mutate $28:TPrimitive = LoadLocal read x$21:TPrimitive
27 + [11] mutate $29 = LoadLocal capture y$26
28 + [12] store $30 = Array [read $28:TPrimitive, capture $29]
29 + [13] Return freeze $30
30 +```
31 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/simple-array.js new
+5
@@ -0,0 +1,5 @@
1 +function Component(props) {
2 + const x = props.a + 3;
3 + const y = foo(x);
4 + return [x, y];
5 +}
compiler/forget/src/__tests__/fixtures/hir/simple-object.expect.md new
+31
@@ -0,0 +1,31 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const x = props.a + 3;
7 + const y = foo(x);
8 + return { x, y };
9 +}
10 +
11 +```
12 +
13 +## HIR
14 +
15 +```javascript
16 +bb0 (block):
17 + [1] mutate $17 = LoadLocal read props$16
18 + [2] mutate $18:TPrimitive = PropertyLoad read $17.a
19 + [3] mutate $19:TPrimitive = 3
20 + [4] mutate $20:TPrimitive = Binary read $18:TPrimitive + read $19:TPrimitive
21 + [5] store $22:TPrimitive = StoreLocal Const mutate x$21:TPrimitive = read $20:TPrimitive
22 + [6] mutate $23:TFunction = Global foo
23 + [7] mutate $24:TPrimitive = LoadLocal read x$21:TPrimitive
24 + [8] mutate $25 = Call read $23:TFunction(read $24:TPrimitive)
25 + [9] store $27 = StoreLocal Const mutate y$26 = capture $25
26 + [10] mutate $28:TPrimitive = LoadLocal read x$21:TPrimitive
27 + [11] mutate $29 = LoadLocal capture y$26
28 + [12] store $30:TObject = Object { x: read $28:TPrimitive, y: capture $29 }
29 + [13] Return freeze $30:TObject
30 +```
31 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/simple-object.js new
+5
@@ -0,0 +1,5 @@
1 +function Component(props) {
2 + const x = props.a + 3;
3 + const y = foo(x);
4 + return { x, y };
5 +}
compiler/forget/src/__tests__/hir-test.ts new
+144
@@ -0,0 +1,144 @@
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 +"use strict";
9 +
10 +import * as t from "@babel/types";
11 +import { parse } from "@babel/parser";
12 +import traverse, { NodePath } from "@babel/traverse";
13 +import path from "path";
14 +
15 +import * as CompilerPipeline from "../CompilerPipeline";
16 +import { Effect, ValueKind } from "../HIR";
17 +import { printFunction } from "../HIR/PrintHIR";
18 +import { EnvironmentOptions } from "../HIR/Environment";
19 +import { Result, Ok, Err } from "../Utils/Result";
20 +import { toggleLogging } from "../Utils/logger";
21 +import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
22 +import invariant from "invariant";
23 +
24 +// TODO: make pipeline names an enum
25 +// Currently, this is the last pass that operates on hir
26 +const LAST_HIR_PASS = "LeaveSSA";
27 +
28 +describe("React Forget (HIR version)", () => {
29 + generateTestsFromFixtures(
30 + path.join(__dirname, "fixtures", "hir"),
31 + (input, file, options) => {
32 + if (options.debug) {
33 + toggleLogging(options.debug);
34 + }
35 +
36 + const compileResult = compile(input, options.language, {
37 + customHooks: new Map([
38 + [
39 + "useFreeze",
40 + {
41 + name: "useFreeze",
42 + kind: "Custom",
43 + valueKind: ValueKind.Frozen,
44 + effectKind: Effect.Freeze,
45 + },
46 + ],
47 + ]),
48 + });
49 +
50 + if (compileResult.isErr()) {
51 + const error = compileResult.unwrapErr();
52 + error.message = `Expected fixture '${file}' to succeed but it failed with error:\n\n${error.message}`;
53 + throw error;
54 + }
55 + const transformedSources = compileResult.unwrap().map(({ hir }) => {
56 + return `
57 +## HIR
58 +
59 +${wrapWithTripleBackticks(hir, "javascript")}
60 +
61 + `.trim();
62 + });
63 + if (transformedSources === null || transformedSources.length === 0) {
64 + throw new Error(`Expected at least one output for file '${file}'.`);
65 + }
66 + return `
67 +## Input
68 +
69 +${wrapWithTripleBackticks(input, "javascript")}
70 +
71 +${transformedSources.join("\n")}
72 + `;
73 + }
74 + );
75 +});
76 +
77 +type CompileResult = {
78 + hir: string;
79 +};
80 +
81 +function compile(
82 + source: string,
83 + language: "flow" | "typescript",
84 + compilerEnv: Partial<EnvironmentOptions>
85 +): Result<Array<CompileResult>, Error> {
86 + const transformedFns = new Array<CompileResult>();
87 + const babelAsts = parseFunctions(source, language);
88 + if (babelAsts.isErr()) {
89 + return babelAsts;
90 + }
91 +
92 + try {
93 + for (const ast of babelAsts.unwrap()) {
94 + let hirString: string | null = null;
95 + for (const result of CompilerPipeline.run(ast, compilerEnv)) {
96 + switch (result.kind) {
97 + case "hir": {
98 + if (result.name === LAST_HIR_PASS) {
99 + hirString = printFunction(result.value);
100 + break;
101 + }
102 + }
103 + }
104 + }
105 + invariant(
106 + hirString !== null,
107 + `Expected to find pass with name ${LAST_HIR_PASS}`
108 + );
109 + transformedFns.push({ hir: hirString });
110 + }
111 + return Ok(transformedFns);
112 + } catch (e) {
113 + return Err(e);
114 + }
115 +}
116 +
117 +function parseFunctions(
118 + source: string,
119 + language: "flow" | "typescript"
120 +): Result<Array<NodePath<t.FunctionDeclaration>>, Error> {
121 + const items: Array<NodePath<t.FunctionDeclaration>> = [];
122 + try {
123 + const ast = parse(source, {
124 + plugins: [language, "jsx"],
125 + sourceType: "module",
126 + });
127 + traverse(ast, {
128 + // Only collect top-level functions
129 + FunctionDeclaration(nodePath) {
130 + items.push(nodePath);
131 + nodePath.skip();
132 + },
133 + });
134 + } catch (e) {
135 + return Err(e);
136 + }
137 + return Ok(items);
138 +}
139 +
140 +function wrapWithTripleBackticks(s: string, ext?: string) {
141 + return `\`\`\`${ext ?? ""}
142 +${s}
143 +\`\`\``;
144 +}
compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts
+11
@@ -49,6 +49,7 @@ export default function generateTestsFromFixtures(
49 debug: boolean;
50 enableOnlyOnUseForgetDirective: boolean;
51 gatingModule: string | null;
52 + language: "flow" | "typescript";
53 }
54 ) => string
55 ) {
@@ -111,6 +112,7 @@ export default function generateTestsFromFixtures(
112 debug,
113 enableOnlyOnUseForgetDirective,
114 gatingModule,
115 + language: parseLanguage(input),
116 });
117 } else {
118 receivedOutput = "<<input deleted>>";
@@ -146,6 +148,15 @@ export default function generateTestsFromFixtures(
148 });
149 }
150
151 +const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
152 +function parseLanguage(source: string): "flow" | "typescript" {
153 + let useFlow: boolean = false;
154 + for (const flowPragma of FlowPragmas) {
155 + useFlow ||= !!source.match(flowPragma);
156 + }
157 + return useFlow ? "flow" : "typescript";
158 +}
159 +
160 function determineSnapshotMode() {
161 // Determine which snapshot mode we're in: ignoring snapshots,
162 // updating new files only, or updating all files