[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.
This commit is contained in:
Mofei Zhang
2023-03-13 14:52:15 -04:00
parent 03f3e541ce
commit b3086817d7
7 changed files with 228 additions and 23 deletions
+1 -23
View File
@@ -28,32 +28,10 @@ wasmFolder(
path.join(__dirname, "..", "..", "node_modules", "@hpcc-js", "wasm", "dist")
);
const Pragma_RE = /\/\/\s*@enable\((\w+)\)$/gm;
const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
describe("React Forget (HIR version)", () => {
generateTestsFromFixtures(
path.join(__dirname, "fixtures", "compiler"),
(input, file, options) => {
const matches = input.matchAll(Pragma_RE);
for (const match of matches) {
const [, key, value] = match;
switch (key) {
case "Pass":
// do something with value;
break;
default:
throw new Error(`unknown pragma: ${key}`);
}
}
let useFlow: boolean = false;
for (const flowPragma of FlowPragmas) {
useFlow ||= !!input.match(flowPragma);
}
let language: "flow" | "typescript" = useFlow ? "flow" : "typescript";
let items: Array<TestOutput> = [];
let error: Error | null = null;
if (options.debug) {
@@ -61,7 +39,7 @@ describe("React Forget (HIR version)", () => {
}
try {
items.push({
js: runReactForgetBabelPlugin(input, file, language, {
js: runReactForgetBabelPlugin(input, file, options.language, {
enableOnlyOnUseForgetDirective:
options.enableOnlyOnUseForgetDirective,
environment: {
@@ -0,0 +1,31 @@
## Input
```javascript
function Component(props) {
const x = props.a + 3;
const y = foo(x);
return [x, y];
}
```
## HIR
```javascript
bb0 (block):
[1] mutate $17 = LoadLocal read props$16
[2] mutate $18:TPrimitive = PropertyLoad read $17.a
[3] mutate $19:TPrimitive = 3
[4] mutate $20:TPrimitive = Binary read $18:TPrimitive + read $19:TPrimitive
[5] store $22:TPrimitive = StoreLocal Const mutate x$21:TPrimitive = read $20:TPrimitive
[6] mutate $23:TFunction = Global foo
[7] mutate $24:TPrimitive = LoadLocal read x$21:TPrimitive
[8] mutate $25 = Call read $23:TFunction(read $24:TPrimitive)
[9] store $27 = StoreLocal Const mutate y$26 = capture $25
[10] mutate $28:TPrimitive = LoadLocal read x$21:TPrimitive
[11] mutate $29 = LoadLocal capture y$26
[12] store $30 = Array [read $28:TPrimitive, capture $29]
[13] Return freeze $30
```
@@ -0,0 +1,5 @@
function Component(props) {
const x = props.a + 3;
const y = foo(x);
return [x, y];
}
@@ -0,0 +1,31 @@
## Input
```javascript
function Component(props) {
const x = props.a + 3;
const y = foo(x);
return { x, y };
}
```
## HIR
```javascript
bb0 (block):
[1] mutate $17 = LoadLocal read props$16
[2] mutate $18:TPrimitive = PropertyLoad read $17.a
[3] mutate $19:TPrimitive = 3
[4] mutate $20:TPrimitive = Binary read $18:TPrimitive + read $19:TPrimitive
[5] store $22:TPrimitive = StoreLocal Const mutate x$21:TPrimitive = read $20:TPrimitive
[6] mutate $23:TFunction = Global foo
[7] mutate $24:TPrimitive = LoadLocal read x$21:TPrimitive
[8] mutate $25 = Call read $23:TFunction(read $24:TPrimitive)
[9] store $27 = StoreLocal Const mutate y$26 = capture $25
[10] mutate $28:TPrimitive = LoadLocal read x$21:TPrimitive
[11] mutate $29 = LoadLocal capture y$26
[12] store $30:TObject = Object { x: read $28:TPrimitive, y: capture $29 }
[13] Return freeze $30:TObject
```
@@ -0,0 +1,5 @@
function Component(props) {
const x = props.a + 3;
const y = foo(x);
return { x, y };
}
+144
View File
@@ -0,0 +1,144 @@
/**
* 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.
*/
"use strict";
import * as t from "@babel/types";
import { parse } from "@babel/parser";
import traverse, { NodePath } from "@babel/traverse";
import path from "path";
import * as CompilerPipeline from "../CompilerPipeline";
import { Effect, ValueKind } from "../HIR";
import { printFunction } from "../HIR/PrintHIR";
import { EnvironmentOptions } from "../HIR/Environment";
import { Result, Ok, Err } from "../Utils/Result";
import { toggleLogging } from "../Utils/logger";
import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
import invariant from "invariant";
// TODO: make pipeline names an enum
// Currently, this is the last pass that operates on hir
const LAST_HIR_PASS = "LeaveSSA";
describe("React Forget (HIR version)", () => {
generateTestsFromFixtures(
path.join(__dirname, "fixtures", "hir"),
(input, file, options) => {
if (options.debug) {
toggleLogging(options.debug);
}
const compileResult = compile(input, options.language, {
customHooks: new Map([
[
"useFreeze",
{
name: "useFreeze",
kind: "Custom",
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
},
],
]),
});
if (compileResult.isErr()) {
const error = compileResult.unwrapErr();
error.message = `Expected fixture '${file}' to succeed but it failed with error:\n\n${error.message}`;
throw error;
}
const transformedSources = compileResult.unwrap().map(({ hir }) => {
return `
## HIR
${wrapWithTripleBackticks(hir, "javascript")}
`.trim();
});
if (transformedSources === null || transformedSources.length === 0) {
throw new Error(`Expected at least one output for file '${file}'.`);
}
return `
## Input
${wrapWithTripleBackticks(input, "javascript")}
${transformedSources.join("\n")}
`;
}
);
});
type CompileResult = {
hir: string;
};
function compile(
source: string,
language: "flow" | "typescript",
compilerEnv: Partial<EnvironmentOptions>
): Result<Array<CompileResult>, Error> {
const transformedFns = new Array<CompileResult>();
const babelAsts = parseFunctions(source, language);
if (babelAsts.isErr()) {
return babelAsts;
}
try {
for (const ast of babelAsts.unwrap()) {
let hirString: string | null = null;
for (const result of CompilerPipeline.run(ast, compilerEnv)) {
switch (result.kind) {
case "hir": {
if (result.name === LAST_HIR_PASS) {
hirString = printFunction(result.value);
break;
}
}
}
}
invariant(
hirString !== null,
`Expected to find pass with name ${LAST_HIR_PASS}`
);
transformedFns.push({ hir: hirString });
}
return Ok(transformedFns);
} catch (e) {
return Err(e);
}
}
function parseFunctions(
source: string,
language: "flow" | "typescript"
): Result<Array<NodePath<t.FunctionDeclaration>>, Error> {
const items: Array<NodePath<t.FunctionDeclaration>> = [];
try {
const ast = parse(source, {
plugins: [language, "jsx"],
sourceType: "module",
});
traverse(ast, {
// Only collect top-level functions
FunctionDeclaration(nodePath) {
items.push(nodePath);
nodePath.skip();
},
});
} catch (e) {
return Err(e);
}
return Ok(items);
}
function wrapWithTripleBackticks(s: string, ext?: string) {
return `\`\`\`${ext ?? ""}
${s}
\`\`\``;
}
@@ -49,6 +49,7 @@ export default function generateTestsFromFixtures(
debug: boolean;
enableOnlyOnUseForgetDirective: boolean;
gatingModule: string | null;
language: "flow" | "typescript";
}
) => string
) {
@@ -111,6 +112,7 @@ export default function generateTestsFromFixtures(
debug,
enableOnlyOnUseForgetDirective,
gatingModule,
language: parseLanguage(input),
});
} else {
receivedOutput = "<<input deleted>>";
@@ -146,6 +148,15 @@ export default function generateTestsFromFixtures(
});
}
const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
function parseLanguage(source: string): "flow" | "typescript" {
let useFlow: boolean = false;
for (const flowPragma of FlowPragmas) {
useFlow ||= !!source.match(flowPragma);
}
return useFlow ? "flow" : "typescript";
}
function determineSnapshotMode() {
// Determine which snapshot mode we're in: ignoring snapshots,
// updating new files only, or updating all files