[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 general tool for compiler-users instead of just for compiler engineers.
- imports and other functions are preserved.
  We differentiate between imports and globals in many cases (e.g. `inferEffectDeps`), so it may be misleading to omit imports in printed output
- 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-16 12:06:48 -05:00
parent 031230d2e0
commit 46f738a2e8
17 changed files with 234 additions and 335 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,22 @@
* 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 * 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';
@@ -39,32 +38,18 @@ import {useStore, useStoreDispatch} from '../StoreContext';
import Input from './Input';
import {
CompilerOutput,
CompilerTransformOutput,
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
import {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 +62,45 @@ 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;
}> = [];
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();
},
});
} catch (e) {
console.error(e);
CompilerError.throwInvalidJS({
reason: String(e),
description: null,
loc: null,
suggestions: null,
});
environment: EnvironmentConfig,
logIR: (pipelineValue: CompilerPipelineValue) => void,
): CompilerTransformOutput {
const opts: PluginOptions = parsePluginOptions({
logger: {
debugLogIRs: logIR,
logEvent: () => {},
},
environment,
compilationMode: 'all',
panicThreshold: 'all_errors',
});
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');
}
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);
}
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 (findDirectiveDisablingMemoization(directives).length) {
return MemoizeDirectiveState.Disabled;
}
return MemoizeDirectiveState.Undefined;
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
@@ -216,37 +151,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();
@@ -264,71 +168,25 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
} else {
language = 'typescript';
}
let count = 0;
const withIdentifier = (id: t.Identifier | null): t.Identifier => {
if (id != null && id.name != null) {
return id;
} else {
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 +195,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 +204,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
case 'debug': {
upsert({
kind: 'debug',
fnName,
fnName: null,
name: result.name,
value: result.value,
});
@@ -357,8 +215,8 @@ 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 +243,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 +263,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,8 +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,
DocumentAddIcon,
@@ -21,17 +19,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;
@@ -41,8 +34,17 @@ export type PrintedCompilerPipelineValue =
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
| {kind: 'debug'; name: string; fnName: string | null; value: string};
export type CompilerTransformOutput = {
code: string;
sourceMaps: BabelFileResult['map'];
language: 'flow' | 'typescript';
};
export type CompilerOutput =
| {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
| {
kind: 'ok';
transformOutput: CompilerTransformOutput;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
}
| {
kind: 'err';
results: Map<string, Array<PrintedCompilerPipelineValue>>;
@@ -61,7 +63,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 +88,6 @@ async function tabify(
}
break;
}
case 'ast':
topLevelFnDecls.push(result.value);
break;
case 'debug': {
concattedResults.set(passName, result.value);
break;
@@ -114,13 +112,17 @@ 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 {transformOutput} = compilerOutput;
const sourceMapUrl = getSourceMapUrl(
transformOutput.code,
JSON.stringify(transformOutput.sourceMaps),
);
const code = await prettier.format(transformOutput.code, {
semi: true,
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
reorderedTabs.set(
'JS',
<TextTabContent
@@ -147,27 +149,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));
}
@@ -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 = {
@@ -112,7 +112,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
>,
@@ -122,7 +122,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,
@@ -134,12 +134,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;
}
@@ -147,17 +152,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): 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);
@@ -168,35 +178,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);
@@ -214,27 +224,27 @@ function* runWithEnvironment(
yield log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
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);
@@ -257,27 +267,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,
@@ -289,39 +299,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,
@@ -329,7 +339,7 @@ function* runWithEnvironment(
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
yield log({
log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
value: hir,
@@ -338,14 +348,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,
@@ -353,7 +363,7 @@ function* runWithEnvironment(
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
yield log({
log({
kind: 'hir',
name: 'PropagateScopeDependenciesHIR',
value: hir,
@@ -365,7 +375,7 @@ function* runWithEnvironment(
if (env.config.inlineJsxTransform) {
inlineJsxTransform(hir, env.config.inlineJsxTransform);
yield log({
log({
kind: 'hir',
name: 'inlineJsxTransform',
value: hir,
@@ -373,7 +383,7 @@ function* runWithEnvironment(
}
const reactiveFunction = buildReactiveFunction(hir);
yield log({
log({
kind: 'reactive',
name: 'BuildReactiveFunction',
value: reactiveFunction,
@@ -382,7 +392,7 @@ function* runWithEnvironment(
assertWellFormedBreakTargets(reactiveFunction);
pruneUnusedLabels(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneUnusedLabels',
value: reactiveFunction,
@@ -390,35 +400,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,
@@ -426,7 +436,7 @@ function* runWithEnvironment(
if (env.config.enableChangeDetectionForDebugging != null) {
pruneInitializationDependencies(reactiveFunction);
yield log({
log({
kind: 'reactive',
name: 'PruneInitializationDependencies',
value: reactiveFunction,
@@ -434,49 +444,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,
@@ -497,9 +507,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});
}
/**
@@ -525,7 +535,7 @@ export function compileFn(
filename: string | null,
code: string | null,
): CodegenFunction {
let generator = run(
return run(
func,
config,
fnType,
@@ -534,15 +544,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);
@@ -566,14 +570,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;
}
@@ -168,11 +168,19 @@ const EnvironmentConfigSchema = z.object({
customMacros: z.nullable(z.array(MacroSchema)).default(null),
/**
* Enable a check that resets the memoization cache when the source code of the file changes.
* 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.
* Enable a check that resets the memoization cache when the source code of
* the file changes. 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.
*
* When set to
* - true: code for HMR support is always generated, regardless of NODE_ENV
* or `globalThis.__DEV__`
* - false: code for HMR support is not generated
* - null: (default) code for HMR support is conditionally generated dependent
* on `NODE_ENV` and `globalThis.__DEV__` at the time of compilation.
*/
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
@@ -708,7 +716,10 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
continue;
}
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
if (
key !== 'enableResetCacheOnSourceFileChanges' &&
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
) {
// skip parsing non-boolean properties
continue;
}
@@ -718,9 +729,15 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
maybeConfig[key] = false;
}
}
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, {
@@ -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,