diff --git a/compiler/forget/src/__tests__/compiler-test.ts b/compiler/forget/src/__tests__/compiler-test.ts index 4771b2875f..6a3b6d4eb4 100644 --- a/compiler/forget/src/__tests__/compiler-test.ts +++ b/compiler/forget/src/__tests__/compiler-test.ts @@ -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 = []; 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: { diff --git a/compiler/forget/src/__tests__/fixtures/hir/simple-array.expect.md b/compiler/forget/src/__tests__/fixtures/hir/simple-array.expect.md new file mode 100644 index 0000000000..cfcdf7c01b --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/simple-array.expect.md @@ -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 +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/simple-array.js b/compiler/forget/src/__tests__/fixtures/hir/simple-array.js new file mode 100644 index 0000000000..f053ef85fa --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/simple-array.js @@ -0,0 +1,5 @@ +function Component(props) { + const x = props.a + 3; + const y = foo(x); + return [x, y]; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/simple-object.expect.md b/compiler/forget/src/__tests__/fixtures/hir/simple-object.expect.md new file mode 100644 index 0000000000..a8bf278561 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/simple-object.expect.md @@ -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 +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/simple-object.js b/compiler/forget/src/__tests__/fixtures/hir/simple-object.js new file mode 100644 index 0000000000..ef55633090 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/simple-object.js @@ -0,0 +1,5 @@ +function Component(props) { + const x = props.a + 3; + const y = foo(x); + return { x, y }; +} diff --git a/compiler/forget/src/__tests__/hir-test.ts b/compiler/forget/src/__tests__/hir-test.ts new file mode 100644 index 0000000000..ef5fc5457f --- /dev/null +++ b/compiler/forget/src/__tests__/hir-test.ts @@ -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 +): Result, Error> { + const transformedFns = new Array(); + 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>, Error> { + const items: Array> = []; + 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} +\`\`\``; +} diff --git a/compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts b/compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts index 5440301508..05fe17a45e 100644 --- a/compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts +++ b/compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts @@ -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 = "<>"; @@ -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