[compiler][be] Playground now compiles entire program

Compiler playground now runs the entire program through `babel-plugin-react-compiler` instead of a custom pipeline, which previously duplicated function inference logic from `Program.ts`. In addition, the playground output reflects the tranformed file (instead of a "virtual file" of manually concatenated functions).

This helps with the following:
- Reduce potential discrepencies between playground and babel plugin behavior. See attached fixture output for an example where we previously diverged.
- Let playground users see compiler-inserted imports (e.g. `_c` or `useFire`)

This also helps us repurpose playground into a more generic tool for compiler-users instead of just compiler engineers.
- imports and other functions are preserved.
  This is important as we now differentiate between imports and globals in many cases,
- playground now shows other program-changing behavior like position of outlined functions and hoisted declarations
- emitted compiled functions do not need synthetic names
This commit is contained in:
Mofei Zhang
2024-12-13 19:36:04 -05:00
parent 152080276c
commit ad7b4b04de
21 changed files with 248 additions and 324 deletions
@@ -1,4 +1,5 @@
function TestComponent(t0) {
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;
@@ -1,4 +1,5 @@
function MyApp() {
import { c as _c } from "react/compiler-runtime";
export default function MyApp() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
@@ -1,4 +1,6 @@
function TestComponent(t0) {
"use memo";
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;
@@ -1,3 +1,4 @@
function TestComponent({ x }) {
"use no memo";
export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
@@ -0,0 +1,5 @@
"use no memo";
function TestComponent({ x }) {
"use memo";
return <Button>{x}</Button>;
}
@@ -1,3 +1,4 @@
import { c as _c } from "react/compiler-runtime";
function TestComponent(t0) {
"use memo";
const $ = _c(2);
@@ -12,7 +13,7 @@ function TestComponent(t0) {
}
return t1;
}
function anonymous_1(t0) {
const TestComponent2 = (t0) => {
"use memo";
const $ = _c(2);
const { x } = t0;
@@ -25,4 +26,4 @@ function anonymous_1(t0) {
t1 = $[1];
}
return t1;
}
};
@@ -1,8 +1,8 @@
function anonymous_1() {
const TestComponent = function () {
"use no memo";
return <Button>{x}</Button>;
}
function anonymous_3({ x }) {
};
const TestComponent2 = ({ x }) => {
"use no memo";
return <Button>{x}</Button>;
}
};
@@ -55,7 +55,7 @@ const TestComponent2 = ({ x }) => {
};`,
},
{
name: 'function-scope-beats-module-scope',
name: 'todo-function-scope-does-not-beat-module-scope',
input: `
'use no memo';
function TestComponent({ x }) {
@@ -5,23 +5,23 @@
* LICENSE file in the root directory of this source tree.
*/
import {parse as babelParse} from '@babel/parser';
import {parse as babelParse, ParseResult} from '@babel/parser';
import transformStripFlowTypes from "@babel/plugin-transform-flow-strip-types";
import * as HermesParser from 'hermes-parser';
import traverse, {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
Effect,
ErrorSeverity,
parseConfigPragmaForTests,
ValueKind,
runPlayground,
type Hook,
findDirectiveDisablingMemoization,
findDirectiveEnablingMemoization,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
} from 'babel-plugin-react-compiler/src';
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
import {type EnvironmentConfig} from 'babel-plugin-react-compiler/src/HIR/Environment';
import clsx from 'clsx';
import invariant from 'invariant';
import {useSnackbar} from 'notistack';
@@ -44,27 +44,9 @@ import {
} from './Output';
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
import { BabelFileResult, transformFromAstSync } from '@babel/core';
type FunctionLike =
| NodePath<t.FunctionDeclaration>
| NodePath<t.ArrowFunctionExpression>
| NodePath<t.FunctionExpression>;
enum MemoizeDirectiveState {
Enabled = 'Enabled',
Disabled = 'Disabled',
Undefined = 'Undefined',
}
const MEMOIZE_ENABLED_OR_UNDEFINED_STATES = new Set([
MemoizeDirectiveState.Enabled,
MemoizeDirectiveState.Undefined,
]);
const MEMOIZE_ENABLED_OR_DISABLED_STATES = new Set([
MemoizeDirectiveState.Enabled,
MemoizeDirectiveState.Disabled,
]);
function parseInput(input: string, language: 'flow' | 'typescript'): any {
function parseInput(input: string, language: 'flow' | 'typescript'): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
@@ -77,95 +59,70 @@ function parseInput(input: string, language: 'flow' | 'typescript'): any {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
});
}) as ParseResult<t.File>;
}
}
function parseFunctions(
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
): Array<{
compilationEnabled: boolean;
fn: FunctionLike;
}> {
const items: Array<{
compilationEnabled: boolean;
fn: FunctionLike;
}> = [];
environment: EnvironmentConfig,
logIR: (pipelineValue: CompilerPipelineValue) => void,
): { ast: t.File, code: string, sourceMaps: BabelFileResult['map']} {
const opts: PluginOptions = parsePluginOptions({
logger: {
debugLogIRs: logIR,
logEvent: () => {}
},
environment,
compilationMode: 'all'
});
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [
[BabelPluginReactCompiler, opts],
],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error("Expected successful compilation");
}
try {
const ast = parseInput(source, language);
traverse(ast, {
FunctionDeclaration(nodePath) {
items.push({
compilationEnabled: shouldCompile(nodePath),
fn: nodePath,
});
nodePath.skip();
},
ArrowFunctionExpression(nodePath) {
items.push({
compilationEnabled: shouldCompile(nodePath),
fn: nodePath,
});
nodePath.skip();
},
FunctionExpression(nodePath) {
items.push({
compilationEnabled: shouldCompile(nodePath),
fn: nodePath,
});
nodePath.skip();
},
});
babelParse(result.code, {
plugins: ['jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
} catch (e) {
console.error(e);
CompilerError.throwInvalidJS({
reason: String(e),
description: null,
loc: null,
suggestions: null,
});
}
return items;
}
function shouldCompile(fn: FunctionLike): boolean {
const {body} = fn.node;
if (t.isBlockStatement(body)) {
const selfCheck = checkExplicitMemoizeDirectives(body.directives);
if (selfCheck === MemoizeDirectiveState.Enabled) return true;
if (selfCheck === MemoizeDirectiveState.Disabled) return false;
const parentWithDirective = fn.findParent(parentPath => {
if (parentPath.isBlockStatement() || parentPath.isProgram()) {
const directiveCheck = checkExplicitMemoizeDirectives(
parentPath.node.directives,
);
return MEMOIZE_ENABLED_OR_DISABLED_STATES.has(directiveCheck);
console.warn("Retrying after parse error (likely due to flow annotations): ", e)
result = transformFromAstSync(ast, source,
{
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [transformStripFlowTypes],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
}
return false;
});
if (!parentWithDirective) return true;
const parentDirectiveCheck = checkExplicitMemoizeDirectives(
(parentWithDirective.node as t.Program | t.BlockStatement).directives,
);
return MEMOIZE_ENABLED_OR_UNDEFINED_STATES.has(parentDirectiveCheck);
}
return false;
}
function checkExplicitMemoizeDirectives(
directives: Array<t.Directive>,
): MemoizeDirectiveState {
if (findDirectiveEnablingMemoization(directives).length) {
return MemoizeDirectiveState.Enabled;
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error("Expected successful compilation");
}
if (findDirectiveDisablingMemoization(directives).length) {
return MemoizeDirectiveState.Disabled;
}
return MemoizeDirectiveState.Undefined;
return {
ast: result.ast,
code: result.code,
sourceMaps: result.map
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
@@ -216,37 +173,6 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
],
];
function isHookName(s: string): boolean {
return /^use[A-Z0-9]/.test(s);
}
function getReactFunctionType(id: t.Identifier | null): ReactFunctionType {
if (id != null) {
if (isHookName(id.name)) {
return 'Hook';
}
const isPascalCaseNameSpace = /^[A-Z].*/;
if (isPascalCaseNameSpace.test(id.name)) {
return 'Component';
}
}
return 'Other';
}
function getFunctionIdentifier(
fn:
| NodePath<t.FunctionDeclaration>
| NodePath<t.ArrowFunctionExpression>
| NodePath<t.FunctionExpression>,
): t.Identifier | null {
if (fn.isArrowFunctionExpression()) {
return null;
}
const id = fn.get('id');
return Array.isArray(id) === false && id.isIdentifier() ? id.node : null;
}
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
@@ -272,63 +198,21 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
return t.identifier(`anonymous_${count++}`);
}
};
let transformOutput;
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const config = parseConfigPragmaForTests(pragma);
const parsedFunctions = parseFunctions(source, language);
for (const func of parsedFunctions) {
const id = withIdentifier(getFunctionIdentifier(func.fn));
const fnName = id.name;
if (!func.compilationEnabled) {
upsert({
kind: 'ast',
fnName,
name: 'CodeGen',
value: {
type: 'FunctionDeclaration',
id:
func.fn.isArrowFunctionExpression() ||
func.fn.isFunctionExpression()
? withIdentifier(null)
: func.fn.node.id,
async: func.fn.node.async,
generator: !!func.fn.node.generator,
body: func.fn.node.body as t.BlockStatement,
params: func.fn.node.params,
},
});
continue;
}
for (const result of runPlayground(
func.fn,
{
...config,
customHooks: new Map([...COMMON_HOOKS]),
},
getReactFunctionType(id),
)) {
transformOutput = invokeCompiler(source, language, {...config, customHooks: new Map([...COMMON_HOOKS])}, (result) => {
switch (result.kind) {
case 'ast': {
upsert({
kind: 'ast',
fnName,
name: result.name,
value: {
type: 'FunctionDeclaration',
id: withIdentifier(result.value.id),
async: result.value.async,
generator: result.value.generator,
body: result.value.body,
params: result.value.params,
},
});
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName,
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
@@ -337,7 +221,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
case 'reactive': {
upsert({
kind: 'reactive',
fnName,
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
@@ -346,7 +230,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
case 'debug': {
upsert({
kind: 'debug',
fnName,
fnName: null,
name: result.name,
value: result.value,
});
@@ -357,8 +241,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
throw new Error(`Unhandled result ${result}`);
}
}
}
}
});
} catch (err) {
/**
* error might be an invariant violation or other runtime error
@@ -385,7 +268,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
}
return [{kind: 'ok', results}, language];
return [{kind: 'ok', results, transformOutput}, language];
}
export default function Editor(): JSX.Element {
@@ -405,7 +288,7 @@ export default function Editor(): JSX.Element {
} catch (e) {
invariant(e instanceof Error, 'Only Error may be caught.');
enqueueSnackbar(e.message, {
variant: 'message',
variant: 'warning',
...createMessage(
'Bad URL - fell back to the default Playground.',
MessageLevel.Info,
@@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
import generate from '@babel/generator';
import * as t from '@babel/types';
import {
CodeIcon,
@@ -21,17 +20,12 @@ import {memo, ReactNode, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import { BabelFileResult } from '@babel/core';
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export type PrintedCompilerPipelineValue =
| {
kind: 'ast';
name: string;
fnName: string | null;
value: t.FunctionDeclaration;
}
| {
kind: 'hir';
name: string;
@@ -42,7 +36,11 @@ export type PrintedCompilerPipelineValue =
| {kind: 'debug'; name: string; fnName: string | null; value: string};
export type CompilerOutput =
| {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
| {kind: 'ok'; transformOutput: {
ast: t.File
code: string
sourceMaps: BabelFileResult['map']
}, results: Map<string, Array<PrintedCompilerPipelineValue>>}
| {
kind: 'err';
results: Map<string, Array<PrintedCompilerPipelineValue>>;
@@ -61,7 +59,6 @@ async function tabify(
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
const concattedResults = new Map<string, string>();
let topLevelFnDecls: Array<t.FunctionDeclaration> = [];
// Concat all top level function declaration results into a single tab for each pass
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
@@ -87,9 +84,6 @@ async function tabify(
}
break;
}
case 'ast':
topLevelFnDecls.push(result.value);
break;
case 'debug': {
concattedResults.set(passName, result.value);
break;
@@ -114,13 +108,16 @@ async function tabify(
lastPassOutput = text;
}
// Ensure that JS and the JS source map come first
if (topLevelFnDecls.length > 0) {
/**
* Make a synthetic Program so we can have a single AST with all the top level
* FunctionDeclarations
*/
const ast = t.program(topLevelFnDecls);
const {code, sourceMapUrl} = await codegen(ast, source);
if (compilerOutput.kind === 'ok') {
const sourceMapUrl = getSourceMapUrl(
compilerOutput.transformOutput.code,
JSON.stringify(compilerOutput.transformOutput.sourceMaps),
);
const code = await prettier.format(compilerOutput.transformOutput.code, {
semi: true,
parser: 'babel',
plugins: [parserBabel, prettierPluginEstree],
});
reorderedTabs.set(
'JS',
<TextTabContent
@@ -147,27 +144,6 @@ async function tabify(
return reorderedTabs;
}
async function codegen(
ast: t.Program,
source: string,
): Promise<{code: any; sourceMapUrl: string | null}> {
const generated = generate(
ast,
{sourceMaps: true, sourceFileName: 'input.js'},
source,
);
const sourceMapUrl = getSourceMapUrl(
generated.code,
JSON.stringify(generated.map),
);
const codegenOutput = await prettier.format(generated.code, {
semi: true,
parser: 'babel',
plugins: [parserBabel, prettierPluginEstree],
});
return {code: codegenOutput, sourceMapUrl};
}
function utf16ToUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}
+1
View File
@@ -42,6 +42,7 @@
"react-dom": "19.0.0-rc-77b637d6-20241016"
},
"devDependencies": {
"@babel/plugin-transform-flow-strip-types": "^7.25.9",
"@types/node": "18.11.9",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
+20
View File
@@ -128,6 +128,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878"
integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==
"@babel/helper-plugin-utils@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46"
integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==
"@babel/helper-replace-supers@^7.25.0":
version "7.25.0"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz#ff44deac1c9f619523fe2ca1fd650773792000a9"
@@ -193,6 +198,13 @@
dependencies:
"@babel/types" "^7.25.6"
"@babel/plugin-syntax-flow@^7.25.9":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz#96507595c21b45fccfc2bc758d5c45452e6164fa"
integrity sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==
dependencies:
"@babel/helper-plugin-utils" "^7.25.9"
"@babel/plugin-syntax-jsx@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d"
@@ -214,6 +226,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.24.8"
"@babel/plugin-transform-flow-strip-types@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.9.tgz#85879b42a8f5948fd6317069978e98f23ef8aec1"
integrity sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==
dependencies:
"@babel/helper-plugin-utils" "^7.25.9"
"@babel/plugin-syntax-flow" "^7.25.9"
"@babel/plugin-transform-modules-commonjs@^7.18.9", "@babel/plugin-transform-modules-commonjs@^7.24.7":
version "7.24.8"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c"
@@ -39,7 +39,10 @@ export default function BabelPluginReactCompiler(
) {
opts = injectReanimatedFlag(opts);
}
if (isDev) {
if (
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
isDev
) {
opts = {
...opts,
environment: {
@@ -15,6 +15,7 @@ import {
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error';
import {CompilerPipelineValue} from './Pipeline';
const PanicThresholdOptionsSchema = z.enum([
/*
@@ -209,6 +210,7 @@ export type LoggerEvent =
export type Logger = {
logEvent: (filename: string | null, event: LoggerEvent) => void;
debugLogIRs?: (value: CompilerPipelineValue) => void;
};
export const defaultOptions: PluginOptions = {
@@ -111,7 +111,7 @@ export type CompilerPipelineValue =
| {kind: 'reactive'; name: string; value: ReactiveFunction}
| {kind: 'debug'; name: string; value: string};
export function* run(
function run(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
@@ -121,7 +121,7 @@ export function* run(
logger: Logger | null,
filename: string | null,
code: string | null,
): Generator<CompilerPipelineValue, CodegenFunction> {
): CodegenFunction {
const contextIdentifiers = findContextIdentifiers(func);
const env = new Environment(
func.scope,
@@ -133,12 +133,17 @@ export function* run(
code,
useMemoCacheIdentifier,
);
yield log({
env.logger?.debugLogIRs?.({
kind: 'debug',
name: 'EnvironmentConfig',
value: prettyFormat(env.config),
});
const ast = yield* runWithEnvironment(func, env);
printLog({
kind: 'debug',
name: 'EnvironmentConfig',
value: prettyFormat(env.config),
});
const ast = runWithEnvironment(func, env);
return ast;
}
@@ -146,17 +151,22 @@ export function* run(
* Note: this is split from run() to make `config` out of scope, so that all
* access to feature flags has to be through the Environment for consistency.
*/
function* runWithEnvironment(
function runWithEnvironment(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
env: Environment,
): Generator<CompilerPipelineValue, CodegenFunction> {
): CodegenFunction {
const log = (value: CompilerPipelineValue) => {
printLog(value);
env.logger?.debugLogIRs?.(value);
return value;
};
const hir = lower(func, env).unwrap();
yield log({kind: 'hir', name: 'HIR', value: hir});
log({kind: 'hir', name: 'HIR', value: hir});
pruneMaybeThrows(hir);
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
validateContextVariableLValues(hir);
validateUseMemo(hir);
@@ -167,35 +177,35 @@ function* runWithEnvironment(
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir);
yield log({kind: 'hir', name: 'DropManualMemoization', value: hir});
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
inlineImmediatelyInvokedFunctionExpressions(hir);
yield log({
log({
kind: 'hir',
name: 'InlineImmediatelyInvokedFunctionExpressions',
value: hir,
});
mergeConsecutiveBlocks(hir);
yield log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
assertConsistentIdentifiers(hir);
assertTerminalSuccessorsExist(hir);
enterSSA(hir);
yield log({kind: 'hir', name: 'SSA', value: hir});
log({kind: 'hir', name: 'SSA', value: hir});
eliminateRedundantPhi(hir);
yield log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
assertConsistentIdentifiers(hir);
constantPropagation(hir);
yield log({kind: 'hir', name: 'ConstantPropagation', value: hir});
log({kind: 'hir', name: 'ConstantPropagation', value: hir});
inferTypes(hir);
yield log({kind: 'hir', name: 'InferTypes', value: hir});
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
@@ -210,27 +220,27 @@ function* runWithEnvironment(
}
analyseFunctions(hir);
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferReferenceEffects(hir);
yield log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
yield log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
if (env.config.enableInstructionReordering) {
instructionReordering(hir);
yield log({kind: 'hir', name: 'InstructionReordering', value: hir});
log({kind: 'hir', name: 'InstructionReordering', value: hir});
}
pruneMaybeThrows(hir);
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
inferMutableRanges(hir);
yield log({kind: 'hir', name: 'InferMutableRanges', value: hir});
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
@@ -253,27 +263,27 @@ function* runWithEnvironment(
}
inferReactivePlaces(hir);
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
rewriteInstructionKindsBasedOnReassignment(hir);
yield log({
log({
kind: 'hir',
name: 'RewriteInstructionKindsBasedOnReassignment',
value: hir,
});
propagatePhiTypes(hir);
yield log({
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
inferReactiveScopeVariables(hir);
yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
yield log({
log({
kind: 'hir',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
value: hir,
@@ -285,39 +295,39 @@ function* runWithEnvironment(
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
}
alignMethodCallScopes(hir);
yield log({
log({
kind: 'hir',
name: 'AlignMethodCallScopes',
value: hir,
});
alignObjectMethodScopes(hir);
yield log({
log({
kind: 'hir',
name: 'AlignObjectMethodScopes',
value: hir,
});
pruneUnusedLabelsHIR(hir);
yield log({
log({
kind: 'hir',
name: 'PruneUnusedLabelsHIR',
value: hir,
});
alignReactiveScopesToBlockScopesHIR(hir);
yield log({
log({
kind: 'hir',
name: 'AlignReactiveScopesToBlockScopesHIR',
value: hir,
});
mergeOverlappingReactiveScopesHIR(hir);
yield log({
log({
kind: 'hir',
name: 'MergeOverlappingReactiveScopesHIR',
value: hir,
@@ -325,7 +335,7 @@ function* runWithEnvironment(
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
yield log({
log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
value: hir,
@@ -334,14 +344,14 @@ function* runWithEnvironment(
assertValidBlockNesting(hir);
flattenReactiveLoopsHIR(hir);
yield log({
log({
kind: 'hir',
name: 'FlattenReactiveLoopsHIR',
value: hir,
});
flattenScopesWithHooksOrUseHIR(hir);
yield log({
log({
kind: 'hir',
name: 'FlattenScopesWithHooksOrUseHIR',
value: hir,
@@ -349,7 +359,7 @@ function* runWithEnvironment(
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
yield log({
log({
kind: 'hir',
name: 'PropagateScopeDependenciesHIR',
value: hir,
@@ -361,7 +371,7 @@ function* runWithEnvironment(
if (env.config.inlineJsxTransform) {
inlineJsxTransform(hir, env.config.inlineJsxTransform);
yield log({
log({
kind: 'hir',
name: 'inlineJsxTransform',
value: hir,
@@ -369,7 +379,7 @@ function* runWithEnvironment(
}
const reactiveFunction = buildReactiveFunction(hir);
yield log({
log({
kind: 'reactive',
name: 'BuildReactiveFunction',
value: reactiveFunction,
@@ -378,7 +388,7 @@ function* runWithEnvironment(
assertWellFormedBreakTargets(reactiveFunction);
pruneUnusedLabels(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneUnusedLabels',
value: reactiveFunction,
@@ -386,35 +396,35 @@ function* runWithEnvironment(
assertScopeInstructionsWithinScopes(reactiveFunction);
pruneNonEscapingScopes(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneNonEscapingScopes',
value: reactiveFunction,
});
pruneNonReactiveDependencies(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneNonReactiveDependencies',
value: reactiveFunction,
});
pruneUnusedScopes(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneUnusedScopes',
value: reactiveFunction,
});
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'MergeReactiveScopesThatInvalidateTogether',
value: reactiveFunction,
});
pruneAlwaysInvalidatingScopes(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneAlwaysInvalidatingScopes',
value: reactiveFunction,
@@ -422,7 +432,7 @@ function* runWithEnvironment(
if (env.config.enableChangeDetectionForDebugging != null) {
pruneInitializationDependencies(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneInitializationDependencies',
value: reactiveFunction,
@@ -430,49 +440,49 @@ function* runWithEnvironment(
}
propagateEarlyReturns(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PropagateEarlyReturns',
value: reactiveFunction,
});
pruneUnusedLValues(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneUnusedLValues',
value: reactiveFunction,
});
promoteUsedTemporaries(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PromoteUsedTemporaries',
value: reactiveFunction,
});
extractScopeDeclarationsFromDestructuring(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'ExtractScopeDeclarationsFromDestructuring',
value: reactiveFunction,
});
stabilizeBlockIds(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'StabilizeBlockIds',
value: reactiveFunction,
});
const uniqueIdentifiers = renameVariables(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'RenameVariables',
value: reactiveFunction,
});
pruneHoistedContexts(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneHoistedContexts',
value: reactiveFunction,
@@ -493,9 +503,9 @@ function* runWithEnvironment(
uniqueIdentifiers,
fbtOperands,
}).unwrap();
yield log({kind: 'ast', name: 'Codegen', value: ast});
log({kind: 'ast', name: 'Codegen', value: ast});
for (const outlined of ast.outlined) {
yield log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
/**
@@ -521,7 +531,7 @@ export function compileFn(
filename: string | null,
code: string | null,
): CodegenFunction {
let generator = run(
return run(
func,
config,
fnType,
@@ -530,15 +540,9 @@ export function compileFn(
filename,
code,
);
while (true) {
const next = generator.next();
if (next.done) {
return next.value;
}
}
}
export function log(value: CompilerPipelineValue): CompilerPipelineValue {
function printLog(value: CompilerPipelineValue): CompilerPipelineValue {
switch (value.kind) {
case 'ast': {
logCodegenFunction(value.name, value.value);
@@ -562,14 +566,3 @@ export function log(value: CompilerPipelineValue): CompilerPipelineValue {
}
return value;
}
export function* runPlayground(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
): Generator<CompilerPipelineValue, CodegenFunction> {
const ast = yield* run(func, config, fnType, '_c', null, null, null);
return ast;
}
@@ -172,7 +172,7 @@ const EnvironmentConfigSchema = z.object({
* This is intended to support hot module reloading (HMR), where the same runtime component
* instance will be reused across different versions of the component source.
*/
enableResetCacheOnSourceFileChanges: z.boolean().default(false),
enableResetCacheOnSourceFileChanges: z.nullable(z.boolean()).default(null),
/**
* Enable using information from existing useMemo/useCallback to understand when a value is done
@@ -721,6 +721,13 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
if (config.success) {
/**
* Unless explicitly enabled, do not insert HMR handling code
* in test fixtures or playground to reduce visual noise.
*/
if (config.data.enableResetCacheOnSourceFileChanges == null) {
config.data.enableResetCacheOnSourceFileChanges = false;
}
return config.data;
}
CompilerError.invariant(false, {
@@ -17,10 +17,6 @@ let ENABLED: boolean = false;
let lastLogged: string;
export function toggleLogging(enabled: boolean): void {
ENABLED = enabled;
}
export function logDebug(step: string, value: string): void {
if (ENABLED) {
process.stdout.write(`${chalk.green(step)}:\n${value}\n\n`);
@@ -0,0 +1,29 @@
## Input
```javascript
// @compilationMode(all)
'use no memo';
function TestComponent({x}) {
'use memo';
return <Button>{x}</Button>;
}
```
## Code
```javascript
// @compilationMode(all)
"use no memo";
function TestComponent({ x }) {
"use memo";
return <Button>{x}</Button>;
}
```
### Eval output
(kind: exception) Fixture not implemented
@@ -0,0 +1,7 @@
// @compilationMode(all)
'use no memo';
function TestComponent({x}) {
'use memo';
return <Button>{x}</Button>;
}
@@ -17,8 +17,6 @@ export {
compileFn as compile,
compileProgram,
parsePluginOptions,
run,
runPlayground,
OPT_OUT_DIRECTIVES,
OPT_IN_DIRECTIVES,
findDirectiveEnablingMemoization,
+2 -4
View File
@@ -64,14 +64,12 @@ async function compile(
const {Effect: EffectEnum, ValueKind: ValueKindEnum} = require(
COMPILER_INDEX_PATH,
);
const {toggleLogging} = require(LOGGER_PATH);
// const {toggleLogging} = require(LOGGER_PATH);
const {parseConfigPragmaForTests} = require(PARSE_CONFIG_PRAGMA_PATH) as {
parseConfigPragmaForTests: typeof ParseConfigPragma;
};
// only try logging if we filtered out all but one fixture,
// since console log order is non-deterministic
toggleLogging(shouldLog);
// TODO configure debugIR logger
const result = await transformFixtureInput(
input,
fixturePath,