From d4a325df4dbc595cceede81555dd177fc791e8e1 Mon Sep 17 00:00:00 2001
From: Joseph Savona <6425824+josephsavona@users.noreply.github.com>
Date: Mon, 2 Feb 2026 19:03:47 -0800
Subject: [PATCH] [compiler] Add snap subcommand to minimize a test input
(#35663)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Snap now supports subcommands 'test' (default) and 'minimize`. The
minimize subcommand attempts to minimize a single failing input fixture
by incrementally simplifying the ast so long as the same error occurs. I
spot-checked it and it seemed to work pretty well. This is intended for
use in a new subagent designed for investigating bugs — fixture
simplification is an important part of the process and we can automate
this rather than light tokens on fire.
Example Input:
```js
function Component(props) {
const x = [];
let result;
for (let i = 0; i < 10; i++) {
if (cond) {
try {
result = {key: bar([props.cond && props.foo])};
} catch (e) {
console.log(e);
}
}
}
x.push(result);
return ;
}
```
Command output:
```
$ yarn snap minimize --path .../input.js
Minimizing: .../input.js
Minimizing................
--- Minimized Code ---
function Component(props) {
try {
props && props;
} catch (e) {}
}
Reduced from 16 lines to 5 lines
```
This demonstrates things like:
* Removing one statement at at time
* Replacing if/else with the test, consequent, or alternate. Similar for
other control-flow statements including try/catch
* Removing individual array/object expression properties
* Replacing single-value array/object with the value
* Replacing control-flow expression (logical, consequent) w the test or
left/right values
* Removing call arguments
* Replacing calls with a single argument with the argument
* Replacing calls with multiple arguments with an array of the arguments
* Replacing optional member/call with non-optional versions
* Replacing member expression with the object. If computed, also try
replacing w the key
* And a bunch more strategies, see the code
---
compiler/packages/snap/package.json | 2 +
compiler/packages/snap/src/minimize.ts | 2070 ++++++++++++++++++++++++
compiler/packages/snap/src/runner.ts | 111 +-
compiler/yarn.lock | 41 +-
4 files changed, 2189 insertions(+), 35 deletions(-)
create mode 100644 compiler/packages/snap/src/minimize.ts
diff --git a/compiler/packages/snap/package.json b/compiler/packages/snap/package.json
index 085422ab8303..92a170b365d5 100644
--- a/compiler/packages/snap/package.json
+++ b/compiler/packages/snap/package.json
@@ -22,6 +22,7 @@
},
"dependencies": {
"@babel/code-frame": "^7.22.5",
+ "@babel/generator": "^7.19.1",
"@babel/plugin-syntax-jsx": "^7.18.6",
"@babel/preset-flow": "^7.7.4",
"@babel/preset-typescript": "^7.26.0",
@@ -58,6 +59,7 @@
"resolutions": {
"./**/@babel/parser": "7.7.4",
"./**/@babel/types": "7.7.4",
+ "@babel/generator": "7.2.0",
"@babel/preset-flow": "7.22.5"
}
}
diff --git a/compiler/packages/snap/src/minimize.ts b/compiler/packages/snap/src/minimize.ts
new file mode 100644
index 000000000000..30848241f7e0
--- /dev/null
+++ b/compiler/packages/snap/src/minimize.ts
@@ -0,0 +1,2070 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import type {PluginObj} from '@babel/core';
+import {transformFromAstSync} from '@babel/core';
+import generate from '@babel/generator';
+import traverse from '@babel/traverse';
+import * as t from '@babel/types';
+import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils';
+import fs from 'fs';
+import path from 'path';
+import {parseInput, parseLanguage, parseSourceType} from './compiler.js';
+import {PARSE_CONFIG_PRAGMA_IMPORT, PROJECT_SRC} from './constants.js';
+
+type MinimizeOptions = {
+ path: string;
+};
+
+type CompileSuccess = {kind: 'success'};
+type CompileParseError = {kind: 'parse_error'; message: string};
+type CompileErrors = {
+ kind: 'errors';
+ errors: Array<{category: string; reason: string}>;
+};
+type CompileResult = CompileSuccess | CompileParseError | CompileErrors;
+
+/**
+ * Compile code and extract error information
+ */
+function compileAndGetError(
+ code: string,
+ filename: string,
+ language: 'flow' | 'typescript',
+ sourceType: 'module' | 'script',
+ plugin: PluginObj,
+ parseConfigPragmaFn: typeof ParseConfigPragma,
+): CompileResult {
+ let ast: t.File;
+ try {
+ ast = parseInput(code, filename, language, sourceType);
+ } catch (e: unknown) {
+ return {kind: 'parse_error', message: (e as Error).message};
+ }
+
+ const firstLine = code.substring(0, code.indexOf('\n'));
+ const config = parseConfigPragmaFn(firstLine, {compilationMode: 'all'});
+ const options = {
+ ...config,
+ environment: {
+ ...config.environment,
+ },
+ logger: {
+ logEvent: () => {},
+ debugLogIRs: () => {},
+ },
+ enableReanimatedCheck: false,
+ };
+
+ try {
+ transformFromAstSync(ast, code, {
+ filename: '/' + filename,
+ highlightCode: false,
+ retainLines: true,
+ compact: true,
+ plugins: [[plugin, options]],
+ sourceType: 'module',
+ ast: false,
+ cloneInputAst: true,
+ configFile: false,
+ babelrc: false,
+ });
+ return {kind: 'success'};
+ } catch (e: unknown) {
+ const error = e as Error & {
+ details?: Array<{category: string; reason: string}>;
+ };
+ // Check if this is a CompilerError with details
+ if (error.details && error.details.length > 0) {
+ return {
+ kind: 'errors',
+ errors: error.details.map(detail => ({
+ category: detail.category,
+ reason: detail.reason,
+ })),
+ };
+ }
+ // Fallback for other errors - use error name/message
+ return {
+ kind: 'errors',
+ errors: [
+ {
+ category: error.name ?? 'Error',
+ reason: error.message,
+ },
+ ],
+ };
+ }
+}
+
+/**
+ * Check if two compile errors match
+ */
+function errorsMatch(a: CompileErrors, b: CompileResult): boolean {
+ if (b.kind !== 'errors') {
+ return false;
+ }
+ if (a.errors.length !== b.errors.length) {
+ return false;
+ }
+ for (let i = 0; i < a.errors.length; i++) {
+ if (
+ a.errors[i].category !== b.errors[i].category ||
+ a.errors[i].reason !== b.errors[i].reason
+ ) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Convert AST to code string
+ */
+function astToCode(ast: t.File): string {
+ return generate(ast).code;
+}
+
+/**
+ * Clone an AST node deeply
+ */
+function cloneAst(ast: t.File): t.File {
+ return t.cloneNode(ast, true);
+}
+
+/**
+ * Generator that yields ASTs with statements removed one at a time
+ */
+function* removeStatements(ast: t.File): Generator {
+ // Collect all statement locations: which container (by index) and which statement index
+ const statementLocations: Array<{containerIndex: number; stmtIndex: number}> =
+ [];
+ let containerIndex = 0;
+
+ t.traverseFast(ast, node => {
+ if (t.isBlockStatement(node) || t.isProgram(node)) {
+ const body = node.body as t.Statement[];
+ // Iterate in reverse order so removing later statements first
+ for (let i = body.length - 1; i >= 0; i--) {
+ statementLocations.push({containerIndex, stmtIndex: i});
+ }
+ containerIndex++;
+ }
+ });
+
+ for (const {
+ containerIndex: targetContainerIdx,
+ stmtIndex,
+ } of statementLocations) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ t.traverseFast(cloned, node => {
+ if (modified) return;
+ if (t.isBlockStatement(node) || t.isProgram(node)) {
+ if (idx === targetContainerIdx) {
+ const body = node.body as t.Statement[];
+ if (stmtIndex < body.length) {
+ body.splice(stmtIndex, 1);
+ modified = true;
+ }
+ }
+ idx++;
+ }
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that yields ASTs with call arguments removed one at a time
+ */
+function* removeCallArguments(ast: t.File): Generator {
+ // Collect all call expressions with their argument counts
+ const callSites: Array<{callIndex: number; argCount: number}> = [];
+ let callIndex = 0;
+ t.traverseFast(ast, node => {
+ if (t.isCallExpression(node) && node.arguments.length > 0) {
+ callSites.push({callIndex, argCount: node.arguments.length});
+ callIndex++;
+ }
+ });
+
+ // For each call site, try removing each argument one at a time (from end to start)
+ for (const {callIndex: targetCallIdx, argCount} of callSites) {
+ for (let argIdx = argCount - 1; argIdx >= 0; argIdx--) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ t.traverseFast(cloned, node => {
+ if (modified) return;
+ if (t.isCallExpression(node) && node.arguments.length > 0) {
+ if (idx === targetCallIdx && argIdx < node.arguments.length) {
+ node.arguments.splice(argIdx, 1);
+ modified = true;
+ }
+ idx++;
+ }
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that simplifies call expressions by replacing them with their arguments.
+ * For single argument: foo(x) -> x
+ * For multiple arguments: foo(x, y) -> [x, y]
+ */
+function* simplifyCallExpressions(ast: t.File): Generator {
+ // Count call expressions with arguments
+ let callCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isCallExpression(node) && node.arguments.length > 0) {
+ callCount++;
+ }
+ });
+
+ // For each call, try replacing with arguments
+ for (let targetIdx = 0; targetIdx < callCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ CallExpression(path) {
+ if (modified) return;
+ if (path.node.arguments.length > 0 && idx === targetIdx) {
+ const args = path.node.arguments;
+ // Filter to only Expression arguments (not SpreadElement)
+ const exprArgs = args.filter((arg): arg is t.Expression =>
+ t.isExpression(arg),
+ );
+ if (exprArgs.length === 0) {
+ idx++;
+ return;
+ }
+ if (exprArgs.length === 1) {
+ // Single argument: replace call with the argument
+ path.replaceWith(exprArgs[0]);
+ } else {
+ // Multiple arguments: replace call with array of arguments
+ path.replaceWith(t.arrayExpression(exprArgs));
+ }
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Also try replacing with each individual argument for multi-arg calls
+ for (let targetIdx = 0; targetIdx < callCount; targetIdx++) {
+ // First, find the arg count for this call
+ let argCount = 0;
+ let currentIdx = 0;
+ t.traverseFast(ast, node => {
+ if (t.isCallExpression(node) && node.arguments.length > 0) {
+ if (currentIdx === targetIdx) {
+ argCount = node.arguments.length;
+ }
+ currentIdx++;
+ }
+ });
+
+ // Try replacing with each argument individually
+ for (let argIdx = 0; argIdx < argCount; argIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ CallExpression(path) {
+ if (modified) return;
+ if (path.node.arguments.length > 0 && idx === targetIdx) {
+ const arg = path.node.arguments[argIdx];
+ if (t.isExpression(arg)) {
+ path.replaceWith(arg);
+ modified = true;
+ }
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that simplifies conditional expressions (a ? b : c) -> a, b, or c
+ */
+function* simplifyConditionals(ast: t.File): Generator {
+ // Count conditionals
+ let condCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isConditionalExpression(node)) {
+ condCount++;
+ }
+ });
+
+ // Try replacing with test condition
+ for (let targetIdx = 0; targetIdx < condCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let modified = false;
+ let idx = 0;
+
+ traverse(cloned, {
+ ConditionalExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.test);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with consequent
+ for (let targetIdx = 0; targetIdx < condCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let modified = false;
+ let idx = 0;
+
+ traverse(cloned, {
+ ConditionalExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.consequent);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Also try replacing with alternate
+ for (let targetIdx = 0; targetIdx < condCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let modified = false;
+ let idx = 0;
+
+ traverse(cloned, {
+ ConditionalExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.alternate);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies logical expressions (a && b) -> a or b
+ */
+function* simplifyLogicalExpressions(ast: t.File): Generator {
+ // Count logical expressions
+ let logicalCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isLogicalExpression(node)) {
+ logicalCount++;
+ }
+ });
+
+ // Try replacing with left side
+ for (let targetIdx = 0; targetIdx < logicalCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ LogicalExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.left);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with right side
+ for (let targetIdx = 0; targetIdx < logicalCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ LogicalExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.right);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies optional chains (a?.b) -> a.b
+ */
+function* simplifyOptionalChains(ast: t.File): Generator {
+ // Count optional expressions
+ let optionalCount = 0;
+ t.traverseFast(ast, node => {
+ if (
+ t.isOptionalMemberExpression(node) ||
+ t.isOptionalCallExpression(node)
+ ) {
+ optionalCount++;
+ }
+ });
+
+ for (let targetIdx = 0; targetIdx < optionalCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ OptionalMemberExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ const {object, property, computed} = path.node;
+ path.replaceWith(t.memberExpression(object, property, computed));
+ modified = true;
+ }
+ idx++;
+ },
+ OptionalCallExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ const {callee, arguments: args} = path.node;
+ if (t.isExpression(callee)) {
+ path.replaceWith(t.callExpression(callee, args));
+ modified = true;
+ }
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies await expressions: await expr -> expr
+ */
+function* simplifyAwaitExpressions(ast: t.File): Generator {
+ // Count await expressions
+ let awaitCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isAwaitExpression(node)) {
+ awaitCount++;
+ }
+ });
+
+ for (let targetIdx = 0; targetIdx < awaitCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ AwaitExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.argument);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies if statements:
+ * - Replace with test expression (as expression statement)
+ * - Replace with consequent block
+ * - Replace with alternate block (if present)
+ */
+function* simplifyIfStatements(ast: t.File): Generator {
+ // Count if statements
+ let ifCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isIfStatement(node)) {
+ ifCount++;
+ }
+ });
+
+ // Try replacing with test expression
+ for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ IfStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(t.expressionStatement(path.node.test));
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with consequent
+ for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ IfStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.consequent);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with alternate (if present)
+ for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ IfStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx && path.node.alternate) {
+ path.replaceWith(path.node.alternate);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies switch statements:
+ * - Replace with discriminant expression
+ * - Replace with each case's consequent statements
+ */
+function* simplifySwitchStatements(ast: t.File): Generator {
+ // Count switch statements
+ let switchCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isSwitchStatement(node)) {
+ switchCount++;
+ }
+ });
+
+ // Try replacing with discriminant
+ for (let targetIdx = 0; targetIdx < switchCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ SwitchStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(t.expressionStatement(path.node.discriminant));
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // For each switch, try replacing with each case's body
+ for (let targetIdx = 0; targetIdx < switchCount; targetIdx++) {
+ // Find case count for this switch
+ let caseCount = 0;
+ let currentIdx = 0;
+ t.traverseFast(ast, node => {
+ if (t.isSwitchStatement(node)) {
+ if (currentIdx === targetIdx) {
+ caseCount = node.cases.length;
+ }
+ currentIdx++;
+ }
+ });
+
+ for (let caseIdx = 0; caseIdx < caseCount; caseIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ SwitchStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ const switchCase = path.node.cases[caseIdx];
+ if (switchCase && switchCase.consequent.length > 0) {
+ path.replaceWithMultiple(switchCase.consequent);
+ modified = true;
+ }
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that simplifies while statements:
+ * - Replace with test expression
+ * - Replace with body
+ */
+function* simplifyWhileStatements(ast: t.File): Generator {
+ // Count while statements
+ let whileCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isWhileStatement(node)) {
+ whileCount++;
+ }
+ });
+
+ // Try replacing with test
+ for (let targetIdx = 0; targetIdx < whileCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ WhileStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(t.expressionStatement(path.node.test));
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with body
+ for (let targetIdx = 0; targetIdx < whileCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ WhileStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.body);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies do-while statements:
+ * - Replace with test expression
+ * - Replace with body
+ */
+function* simplifyDoWhileStatements(ast: t.File): Generator {
+ // Count do-while statements
+ let doWhileCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isDoWhileStatement(node)) {
+ doWhileCount++;
+ }
+ });
+
+ // Try replacing with test
+ for (let targetIdx = 0; targetIdx < doWhileCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ DoWhileStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(t.expressionStatement(path.node.test));
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with body
+ for (let targetIdx = 0; targetIdx < doWhileCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ DoWhileStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.body);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies for statements:
+ * - Replace with init (if expression)
+ * - Replace with test expression
+ * - Replace with update expression
+ * - Replace with body
+ */
+function* simplifyForStatements(ast: t.File): Generator {
+ // Count for statements
+ let forCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isForStatement(node)) {
+ forCount++;
+ }
+ });
+
+ // Try replacing with init (if it's an expression)
+ for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx && path.node.init) {
+ if (t.isExpression(path.node.init)) {
+ path.replaceWith(t.expressionStatement(path.node.init));
+ } else {
+ // It's a VariableDeclaration
+ path.replaceWith(path.node.init);
+ }
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with test
+ for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx && path.node.test) {
+ path.replaceWith(t.expressionStatement(path.node.test));
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with update
+ for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx && path.node.update) {
+ path.replaceWith(t.expressionStatement(path.node.update));
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with body
+ for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.body);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies for-in statements:
+ * - Replace with left (variable declaration or expression)
+ * - Replace with right expression
+ * - Replace with body
+ */
+function* simplifyForInStatements(ast: t.File): Generator {
+ // Count for-in statements
+ let forInCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isForInStatement(node)) {
+ forInCount++;
+ }
+ });
+
+ // Try replacing with left
+ for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForInStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ const left = path.node.left;
+ if (t.isExpression(left)) {
+ path.replaceWith(t.expressionStatement(left));
+ } else {
+ path.replaceWith(left);
+ }
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with right
+ for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForInStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(t.expressionStatement(path.node.right));
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with body
+ for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForInStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.body);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies for-of statements:
+ * - Replace with left (variable declaration or expression)
+ * - Replace with right expression
+ * - Replace with body
+ */
+function* simplifyForOfStatements(ast: t.File): Generator {
+ // Count for-of statements
+ let forOfCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isForOfStatement(node)) {
+ forOfCount++;
+ }
+ });
+
+ // Try replacing with left
+ for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForOfStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ const left = path.node.left;
+ if (t.isExpression(left)) {
+ path.replaceWith(t.expressionStatement(left));
+ } else {
+ path.replaceWith(left);
+ }
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with right
+ for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForOfStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(t.expressionStatement(path.node.right));
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with body
+ for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ForOfStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.body);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies variable declarations by removing init expressions.
+ * let x = expr; -> let x;
+ * var x = expr; -> var x;
+ * Note: const without init is invalid, so we skip const declarations.
+ */
+function* simplifyVariableDeclarations(ast: t.File): Generator {
+ // Collect all variable declarators with init expressions (excluding const)
+ const declaratorSites: Array<{declIndex: number}> = [];
+ let declIndex = 0;
+ t.traverseFast(ast, node => {
+ if (t.isVariableDeclaration(node) && node.kind !== 'const') {
+ for (const declarator of node.declarations) {
+ if (declarator.init) {
+ declaratorSites.push({declIndex});
+ declIndex++;
+ }
+ }
+ }
+ });
+
+ // Try removing init from each declarator
+ for (const {declIndex: targetDeclIdx} of declaratorSites) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ t.traverseFast(cloned, node => {
+ if (modified) return;
+ if (t.isVariableDeclaration(node) && node.kind !== 'const') {
+ for (const declarator of node.declarations) {
+ if (declarator.init) {
+ if (idx === targetDeclIdx) {
+ declarator.init = null;
+ modified = true;
+ return;
+ }
+ idx++;
+ }
+ }
+ }
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies try/catch/finally statements:
+ * - Replace with try block contents
+ * - Replace with catch block contents (if present)
+ * - Replace with finally block contents (if present)
+ */
+function* simplifyTryStatements(ast: t.File): Generator {
+ // Count try statements
+ let tryCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isTryStatement(node)) {
+ tryCount++;
+ }
+ });
+
+ // Try replacing with try block contents
+ for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ TryStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.block);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with catch block contents (if present)
+ for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ TryStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx && path.node.handler) {
+ path.replaceWith(path.node.handler.body);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with finally block contents (if present)
+ for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ TryStatement(path) {
+ if (modified) return;
+ if (idx === targetIdx && path.node.finalizer) {
+ path.replaceWith(path.node.finalizer);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies single-statement block statements:
+ * { statement } -> statement
+ */
+function* simplifySingleStatementBlocks(ast: t.File): Generator {
+ // Count block statements with exactly one statement
+ let blockCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isBlockStatement(node) && node.body.length === 1) {
+ blockCount++;
+ }
+ });
+
+ for (let targetIdx = 0; targetIdx < blockCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ BlockStatement(path) {
+ if (modified) return;
+ if (path.node.body.length === 1 && idx === targetIdx) {
+ // Don't unwrap blocks that require BlockStatement syntax
+ if (
+ t.isFunction(path.parent) ||
+ t.isCatchClause(path.parent) ||
+ t.isClassMethod(path.parent) ||
+ t.isObjectMethod(path.parent) ||
+ t.isTryStatement(path.parent)
+ ) {
+ idx++;
+ return;
+ }
+ path.replaceWith(path.node.body[0]);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that removes array elements one at a time
+ */
+function* removeArrayElements(ast: t.File): Generator {
+ // Collect all array expressions with their element counts
+ const arraySites: Array<{arrayIndex: number; elementCount: number}> = [];
+ let arrayIndex = 0;
+ t.traverseFast(ast, node => {
+ if (t.isArrayExpression(node) && node.elements.length > 0) {
+ arraySites.push({arrayIndex, elementCount: node.elements.length});
+ arrayIndex++;
+ }
+ });
+
+ // For each array, try removing each element one at a time (from end to start)
+ for (const {arrayIndex: targetArrayIdx, elementCount} of arraySites) {
+ for (let elemIdx = elementCount - 1; elemIdx >= 0; elemIdx--) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ t.traverseFast(cloned, node => {
+ if (modified) return;
+ if (t.isArrayExpression(node) && node.elements.length > 0) {
+ if (idx === targetArrayIdx && elemIdx < node.elements.length) {
+ node.elements.splice(elemIdx, 1);
+ modified = true;
+ }
+ idx++;
+ }
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that removes JSX element attributes (props) one at a time
+ */
+function* removeJSXAttributes(ast: t.File): Generator {
+ // Collect all JSX elements with their attribute counts
+ const jsxSites: Array<{jsxIndex: number; attrCount: number}> = [];
+ let jsxIndex = 0;
+ t.traverseFast(ast, node => {
+ if (t.isJSXOpeningElement(node) && node.attributes.length > 0) {
+ jsxSites.push({jsxIndex, attrCount: node.attributes.length});
+ jsxIndex++;
+ }
+ });
+
+ // For each JSX element, try removing each attribute one at a time (from end to start)
+ for (const {jsxIndex: targetJsxIdx, attrCount} of jsxSites) {
+ for (let attrIdx = attrCount - 1; attrIdx >= 0; attrIdx--) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ t.traverseFast(cloned, node => {
+ if (modified) return;
+ if (t.isJSXOpeningElement(node) && node.attributes.length > 0) {
+ if (idx === targetJsxIdx && attrIdx < node.attributes.length) {
+ node.attributes.splice(attrIdx, 1);
+ modified = true;
+ }
+ idx++;
+ }
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that removes JSX element children one at a time
+ */
+function* removeJSXChildren(ast: t.File): Generator {
+ // Collect all JSX elements with children
+ const jsxSites: Array<{jsxIndex: number; childCount: number}> = [];
+ let jsxIndex = 0;
+ t.traverseFast(ast, node => {
+ if (t.isJSXElement(node) && node.children.length > 0) {
+ jsxSites.push({jsxIndex, childCount: node.children.length});
+ jsxIndex++;
+ }
+ });
+
+ // For each JSX element, try removing each child one at a time (from end to start)
+ for (const {jsxIndex: targetJsxIdx, childCount} of jsxSites) {
+ for (let childIdx = childCount - 1; childIdx >= 0; childIdx--) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ t.traverseFast(cloned, node => {
+ if (modified) return;
+ if (t.isJSXElement(node) && node.children.length > 0) {
+ if (idx === targetJsxIdx && childIdx < node.children.length) {
+ node.children.splice(childIdx, 1);
+ modified = true;
+ }
+ idx++;
+ }
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that removes JSX fragment children one at a time
+ */
+function* removeJSXFragmentChildren(ast: t.File): Generator {
+ // Collect all JSX fragments with children
+ const fragmentSites: Array<{fragIndex: number; childCount: number}> = [];
+ let fragIndex = 0;
+ t.traverseFast(ast, node => {
+ if (t.isJSXFragment(node) && node.children.length > 0) {
+ fragmentSites.push({fragIndex, childCount: node.children.length});
+ fragIndex++;
+ }
+ });
+
+ // For each fragment, try removing each child one at a time (from end to start)
+ for (const {fragIndex: targetFragIdx, childCount} of fragmentSites) {
+ for (let childIdx = childCount - 1; childIdx >= 0; childIdx--) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ t.traverseFast(cloned, node => {
+ if (modified) return;
+ if (t.isJSXFragment(node) && node.children.length > 0) {
+ if (idx === targetFragIdx && childIdx < node.children.length) {
+ node.children.splice(childIdx, 1);
+ modified = true;
+ }
+ idx++;
+ }
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that replaces single-element arrays with the element itself
+ */
+function* simplifySingleElementArrays(ast: t.File): Generator {
+ // Count single-element arrays
+ let arrayCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isArrayExpression(node) && node.elements.length === 1) {
+ arrayCount++;
+ }
+ });
+
+ for (let targetIdx = 0; targetIdx < arrayCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ArrayExpression(path) {
+ if (modified) return;
+ if (path.node.elements.length === 1 && idx === targetIdx) {
+ const elem = path.node.elements[0];
+ if (t.isExpression(elem)) {
+ path.replaceWith(elem);
+ modified = true;
+ }
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that replaces single-property objects with the property value.
+ * For regular properties: {key: value} -> value
+ * For computed properties: {[key]: value} -> key (also try value)
+ */
+function* simplifySinglePropertyObjects(ast: t.File): Generator {
+ // Count single-property objects
+ let objectCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isObjectExpression(node) && node.properties.length === 1) {
+ objectCount++;
+ }
+ });
+
+ // Try replacing with value
+ for (let targetIdx = 0; targetIdx < objectCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ObjectExpression(path) {
+ if (modified) return;
+ if (path.node.properties.length === 1 && idx === targetIdx) {
+ const prop = path.node.properties[0];
+ if (t.isObjectProperty(prop) && t.isExpression(prop.value)) {
+ path.replaceWith(prop.value);
+ modified = true;
+ }
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // For computed properties, also try replacing with key
+ for (let targetIdx = 0; targetIdx < objectCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ ObjectExpression(path) {
+ if (modified) return;
+ if (path.node.properties.length === 1 && idx === targetIdx) {
+ const prop = path.node.properties[0];
+ if (
+ t.isObjectProperty(prop) &&
+ prop.computed &&
+ t.isExpression(prop.key)
+ ) {
+ path.replaceWith(prop.key);
+ modified = true;
+ }
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that removes object properties one at a time
+ */
+function* removeObjectProperties(ast: t.File): Generator {
+ // Collect all object expressions with their property counts
+ const objectSites: Array<{objectIndex: number; propCount: number}> = [];
+ let objectIndex = 0;
+ t.traverseFast(ast, node => {
+ if (t.isObjectExpression(node) && node.properties.length > 0) {
+ objectSites.push({objectIndex, propCount: node.properties.length});
+ objectIndex++;
+ }
+ });
+
+ // For each object, try removing each property one at a time (from end to start)
+ for (const {objectIndex: targetObjIdx, propCount} of objectSites) {
+ for (let propIdx = propCount - 1; propIdx >= 0; propIdx--) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ t.traverseFast(cloned, node => {
+ if (modified) return;
+ if (t.isObjectExpression(node) && node.properties.length > 0) {
+ if (idx === targetObjIdx && propIdx < node.properties.length) {
+ node.properties.splice(propIdx, 1);
+ modified = true;
+ }
+ idx++;
+ }
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that simplifies assignment expressions (a = b) -> a or b
+ */
+function* simplifyAssignmentExpressions(ast: t.File): Generator {
+ // Count assignment expressions
+ let assignmentCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isAssignmentExpression(node)) {
+ assignmentCount++;
+ }
+ });
+
+ // Try replacing with left side (assignment target)
+ for (let targetIdx = 0; targetIdx < assignmentCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ AssignmentExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ const left = path.node.left;
+ if (t.isExpression(left)) {
+ path.replaceWith(left);
+ modified = true;
+ }
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with right side (assignment value)
+ for (let targetIdx = 0; targetIdx < assignmentCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ AssignmentExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.right);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies binary expressions (a + b) -> a or b
+ */
+function* simplifyBinaryExpressions(ast: t.File): Generator {
+ // Count binary expressions
+ let binaryCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isBinaryExpression(node)) {
+ binaryCount++;
+ }
+ });
+
+ // Try replacing with left side
+ for (let targetIdx = 0; targetIdx < binaryCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ BinaryExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.left);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // Try replacing with right side
+ for (let targetIdx = 0; targetIdx < binaryCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ BinaryExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.right);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Generator that simplifies member expressions (obj.value) -> obj
+ * For computed expressions: obj[key] -> obj or key
+ */
+function* simplifyMemberExpressions(ast: t.File): Generator {
+ // Count member expressions
+ let memberCount = 0;
+ t.traverseFast(ast, node => {
+ if (t.isMemberExpression(node)) {
+ memberCount++;
+ }
+ });
+
+ // Try replacing with object
+ for (let targetIdx = 0; targetIdx < memberCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ MemberExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx) {
+ path.replaceWith(path.node.object);
+ modified = true;
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+
+ // For computed expressions, also try replacing with key
+ for (let targetIdx = 0; targetIdx < memberCount; targetIdx++) {
+ const cloned = cloneAst(ast);
+ let idx = 0;
+ let modified = false;
+
+ traverse(cloned, {
+ MemberExpression(path) {
+ if (modified) return;
+ if (idx === targetIdx && path.node.computed) {
+ const property = path.node.property;
+ if (t.isExpression(property)) {
+ path.replaceWith(property);
+ modified = true;
+ }
+ }
+ idx++;
+ },
+ });
+
+ if (modified) {
+ yield cloned;
+ }
+ }
+}
+
+/**
+ * Helper to collect all unique identifier names in the AST
+ */
+function collectUniqueIdentifierNames(ast: t.File): Set {
+ const names = new Set();
+ t.traverseFast(ast, node => {
+ if (t.isIdentifier(node)) {
+ names.add(node.name);
+ }
+ });
+ return names;
+}
+
+/**
+ * Helper to rename all occurrences of an identifier throughout the AST
+ */
+function renameAllIdentifiers(
+ ast: t.File,
+ oldName: string,
+ newName: string,
+): boolean {
+ let modified = false;
+ t.traverseFast(ast, node => {
+ if (t.isIdentifier(node) && node.name === oldName) {
+ node.name = newName;
+ modified = true;
+ }
+ });
+ return modified;
+}
+
+/**
+ * Generator that simplifies identifiers by removing "on" prefix.
+ * onClick -> Click
+ */
+function* simplifyIdentifiersRemoveOnPrefix(ast: t.File): Generator {
+ const names = collectUniqueIdentifierNames(ast);
+
+ for (const name of names) {
+ // Check if name starts with "on" followed by uppercase letter
+ if (
+ name.length > 2 &&
+ name.startsWith('on') &&
+ name[2] === name[2].toUpperCase()
+ ) {
+ const newName = name.slice(2);
+ // Skip if the new name would conflict with an existing identifier
+ if (names.has(newName)) {
+ continue;
+ }
+ const cloned = cloneAst(ast);
+ if (renameAllIdentifiers(cloned, name, newName)) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that simplifies identifiers by removing "Ref" suffix.
+ * inputRef -> input
+ */
+function* simplifyIdentifiersRemoveRefSuffix(ast: t.File): Generator {
+ const names = collectUniqueIdentifierNames(ast);
+
+ for (const name of names) {
+ // Check if name ends with "Ref" and has more characters before it
+ if (name.length > 3 && name.endsWith('Ref')) {
+ const newName = name.slice(0, -3);
+ // Skip if the new name would conflict with an existing identifier
+ if (names.has(newName)) {
+ continue;
+ }
+ // Skip if new name would be empty or just whitespace
+ if (newName.length === 0) {
+ continue;
+ }
+ const cloned = cloneAst(ast);
+ if (renameAllIdentifiers(cloned, name, newName)) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * Generator that rewrites "ref" identifier to "ref_" to avoid conflicts.
+ */
+function* simplifyIdentifiersRenameRef(ast: t.File): Generator {
+ const names = collectUniqueIdentifierNames(ast);
+
+ if (names.has('ref')) {
+ // Only rename if ref_ doesn't already exist
+ if (!names.has('ref_')) {
+ const cloned = cloneAst(ast);
+ if (renameAllIdentifiers(cloned, 'ref', 'ref_')) {
+ yield cloned;
+ }
+ }
+ }
+}
+
+/**
+ * All simplification strategies in order of priority (coarse to fine)
+ */
+const simplificationStrategies = [
+ {name: 'removeStatements', generator: removeStatements},
+ {name: 'removeCallArguments', generator: removeCallArguments},
+ {name: 'removeArrayElements', generator: removeArrayElements},
+ {name: 'removeObjectProperties', generator: removeObjectProperties},
+ {name: 'removeJSXAttributes', generator: removeJSXAttributes},
+ {name: 'removeJSXChildren', generator: removeJSXChildren},
+ {name: 'removeJSXFragmentChildren', generator: removeJSXFragmentChildren},
+ {name: 'simplifyCallExpressions', generator: simplifyCallExpressions},
+ {name: 'simplifyConditionals', generator: simplifyConditionals},
+ {name: 'simplifyLogicalExpressions', generator: simplifyLogicalExpressions},
+ {name: 'simplifyBinaryExpressions', generator: simplifyBinaryExpressions},
+ {
+ name: 'simplifyAssignmentExpressions',
+ generator: simplifyAssignmentExpressions,
+ },
+ {name: 'simplifySingleElementArrays', generator: simplifySingleElementArrays},
+ {
+ name: 'simplifySinglePropertyObjects',
+ generator: simplifySinglePropertyObjects,
+ },
+ {name: 'simplifyMemberExpressions', generator: simplifyMemberExpressions},
+ {name: 'simplifyOptionalChains', generator: simplifyOptionalChains},
+ {name: 'simplifyAwaitExpressions', generator: simplifyAwaitExpressions},
+ {name: 'simplifyIfStatements', generator: simplifyIfStatements},
+ {name: 'simplifySwitchStatements', generator: simplifySwitchStatements},
+ {name: 'simplifyWhileStatements', generator: simplifyWhileStatements},
+ {name: 'simplifyDoWhileStatements', generator: simplifyDoWhileStatements},
+ {name: 'simplifyForStatements', generator: simplifyForStatements},
+ {name: 'simplifyForInStatements', generator: simplifyForInStatements},
+ {name: 'simplifyForOfStatements', generator: simplifyForOfStatements},
+ {
+ name: 'simplifyVariableDeclarations',
+ generator: simplifyVariableDeclarations,
+ },
+ {name: 'simplifyTryStatements', generator: simplifyTryStatements},
+ {
+ name: 'simplifySingleStatementBlocks',
+ generator: simplifySingleStatementBlocks,
+ },
+ {
+ name: 'simplifyIdentifiersRemoveOnPrefix',
+ generator: simplifyIdentifiersRemoveOnPrefix,
+ },
+ {
+ name: 'simplifyIdentifiersRemoveRefSuffix',
+ generator: simplifyIdentifiersRemoveRefSuffix,
+ },
+ {
+ name: 'simplifyIdentifiersRenameRef',
+ generator: simplifyIdentifiersRenameRef,
+ },
+];
+
+type MinimizeResult =
+ | {kind: 'success'}
+ | {kind: 'minimal'}
+ | {kind: 'minimized'; source: string};
+
+/**
+ * Core minimization loop that attempts to reduce the input source code
+ * while preserving the compiler error.
+ */
+export function minimize(
+ input: string,
+ filename: string,
+ language: 'flow' | 'typescript',
+ sourceType: 'module' | 'script',
+): MinimizeResult {
+ // Load the compiler plugin
+ const importedCompilerPlugin = require(PROJECT_SRC) as Record<
+ string,
+ unknown
+ >;
+ const BabelPluginReactCompiler = importedCompilerPlugin[
+ 'default'
+ ] as PluginObj;
+ const parseConfigPragmaForTests = importedCompilerPlugin[
+ PARSE_CONFIG_PRAGMA_IMPORT
+ ] as typeof ParseConfigPragma;
+
+ // Get the initial error
+ const initialResult = compileAndGetError(
+ input,
+ filename,
+ language,
+ sourceType,
+ BabelPluginReactCompiler,
+ parseConfigPragmaForTests,
+ );
+
+ if (initialResult.kind === 'success') {
+ return {kind: 'success'};
+ }
+
+ if (initialResult.kind === 'parse_error') {
+ return {kind: 'success'};
+ }
+
+ const targetError = initialResult;
+
+ // Parse the initial AST
+ let currentAst = parseInput(input, filename, language, sourceType);
+ let currentCode = input;
+ let changed = true;
+ let iterations = 0;
+ const maxIterations = 1000; // Safety limit
+
+ process.stdout.write('\nMinimizing');
+
+ while (changed && iterations < maxIterations) {
+ changed = false;
+ iterations++;
+
+ // Try each simplification strategy
+ for (const strategy of simplificationStrategies) {
+ const generator = strategy.generator(currentAst);
+
+ for (const candidateAst of generator) {
+ let candidateCode: string;
+ try {
+ candidateCode = astToCode(candidateAst);
+ } catch {
+ // If code generation fails, skip this candidate
+ continue;
+ }
+
+ const result = compileAndGetError(
+ candidateCode,
+ filename,
+ language,
+ sourceType,
+ BabelPluginReactCompiler,
+ parseConfigPragmaForTests,
+ );
+
+ if (errorsMatch(targetError, result)) {
+ // This simplification preserves the error, keep it
+ currentAst = candidateAst;
+ currentCode = candidateCode;
+ changed = true;
+ process.stdout.write('.');
+ break; // Restart from the beginning with the new AST
+ }
+ }
+
+ if (changed) {
+ break; // Restart the outer loop
+ }
+ }
+ }
+
+ console.log('\n');
+
+ // Check if any minimization was achieved
+ if (currentCode === input) {
+ return {kind: 'minimal'};
+ }
+
+ return {kind: 'minimized', source: currentCode};
+}
+
+/**
+ * Main minimize function that reads the input file, runs minimization,
+ * and reports results.
+ */
+export async function runMinimize(options: MinimizeOptions): Promise {
+ // Resolve the input path
+ const inputPath = path.isAbsolute(options.path)
+ ? options.path
+ : path.resolve(process.cwd(), options.path);
+
+ // Check if file exists
+ if (!fs.existsSync(inputPath)) {
+ console.error(`Error: File not found: ${inputPath}`);
+ process.exit(1);
+ }
+
+ // Read the input file
+ const input = fs.readFileSync(inputPath, 'utf-8');
+ const filename = path.basename(inputPath);
+ const firstLine = input.substring(0, input.indexOf('\n'));
+ const language = parseLanguage(firstLine);
+ const sourceType = parseSourceType(firstLine);
+
+ console.log(`Minimizing: ${inputPath}`);
+
+ const originalLines = input.split('\n').length;
+
+ // Run the minimization
+ const result = minimize(input, filename, language, sourceType);
+
+ if (result.kind === 'success') {
+ console.log('Could not minimize: the input compiles successfully.');
+ process.exit(0);
+ }
+
+ if (result.kind === 'minimal') {
+ console.log(
+ 'Could not minimize: the input fails but is already minimal and cannot be reduced further.',
+ );
+ process.exit(0);
+ }
+
+ // Output the minimized code
+ console.log('--- Minimized Code ---');
+ console.log(result.source);
+
+ const minimizedLines = result.source.split('\n').length;
+ console.log(
+ `\nReduced from ${originalLines} lines to ${minimizedLines} lines`,
+ );
+}
diff --git a/compiler/packages/snap/src/runner.ts b/compiler/packages/snap/src/runner.ts
index 21320048eb21..f127bd5a35e5 100644
--- a/compiler/packages/snap/src/runner.ts
+++ b/compiler/packages/snap/src/runner.ts
@@ -23,6 +23,7 @@ import {
} from './runner-watch';
import * as runnerWorker from './runner-worker';
import {execSync} from 'child_process';
+import {runMinimize} from './minimize';
const WORKER_PATH = require.resolve('./runner-worker.js');
const NUM_WORKERS = cpus().length - 1;
@@ -38,40 +39,76 @@ type RunnerOptions = {
debug: boolean;
};
-const opts: RunnerOptions = yargs
- .boolean('sync')
- .describe(
- 'sync',
- 'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.',
- )
- .default('sync', false)
- .boolean('worker-threads')
- .describe(
- 'worker-threads',
- 'Run compiler in worker threads (instead of subprocesses). Defaults to true.',
+async function runTestCommand(opts: RunnerOptions): Promise {
+ await main(opts);
+}
+
+async function runMinimizeCommand(path: string): Promise {
+ await runMinimize({path});
+}
+
+yargs(hideBin(process.argv))
+ .command(
+ ['test', '$0'],
+ 'Run compiler tests',
+ yargs => {
+ return yargs
+ .boolean('sync')
+ .describe(
+ 'sync',
+ 'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.',
+ )
+ .default('sync', false)
+ .boolean('worker-threads')
+ .describe(
+ 'worker-threads',
+ 'Run compiler in worker threads (instead of subprocesses). Defaults to true.',
+ )
+ .default('worker-threads', true)
+ .boolean('watch')
+ .describe(
+ 'watch',
+ 'Run compiler in watch mode, re-running after changes',
+ )
+ .alias('w', 'watch')
+ .default('watch', false)
+ .boolean('update')
+ .alias('u', 'update')
+ .describe('update', 'Update fixtures')
+ .default('update', false)
+ .string('pattern')
+ .alias('p', 'pattern')
+ .describe(
+ 'pattern',
+ 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
+ )
+ .boolean('debug')
+ .alias('d', 'debug')
+ .describe('debug', 'Enable debug logging to print HIR for each pass')
+ .default('debug', false);
+ },
+ async argv => {
+ await runTestCommand(argv as RunnerOptions);
+ },
)
- .default('worker-threads', true)
- .boolean('watch')
- .describe('watch', 'Run compiler in watch mode, re-running after changes')
- .alias('w', 'watch')
- .default('watch', false)
- .boolean('update')
- .alias('u', 'update')
- .describe('update', 'Update fixtures')
- .default('update', false)
- .string('pattern')
- .alias('p', 'pattern')
- .describe(
- 'pattern',
- 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
+ .command(
+ 'minimize',
+ 'Minimize a test case to reproduce a compiler error',
+ yargs => {
+ return yargs
+ .string('path')
+ .alias('p', 'path')
+ .describe('path', 'Path to the file to minimize')
+ .demandOption('path');
+ },
+ async argv => {
+ await runMinimizeCommand(argv.path as string);
+ },
)
- .boolean('debug')
- .alias('d', 'debug')
- .describe('debug', 'Enable debug logging to print HIR for each pass')
- .default('debug', false)
.help('help')
.strict()
- .parseSync(hideBin(process.argv)) as RunnerOptions;
+ .demandCommand()
+ .parse();
/**
* Do a test run and return the test results
@@ -82,6 +119,7 @@ async function runFixtures(
compilerVersion: number,
debug: boolean,
requireSingleFixture: boolean,
+ sync: boolean,
): Promise {
// We could in theory be fancy about tracking the contents of the fixtures
// directory via our file subscription, but it's simpler to just re-read
@@ -91,7 +129,7 @@ async function runFixtures(
const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);
let entries: Array<[string, TestResult]>;
- if (!opts.sync) {
+ if (!sync) {
// Note: promise.all to ensure parallelism when enabled
const work: Array> = [];
for (const [fixtureName, fixture] of fixtures) {
@@ -123,6 +161,7 @@ async function runFixtures(
async function onChange(
worker: Worker & typeof runnerWorker,
state: RunnerState,
+ sync: boolean,
) {
const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
if (isCompilerBuildValid) {
@@ -140,6 +179,7 @@ async function onChange(
compilerVersion,
debug,
true, // requireSingleFixture in watch mode
+ sync,
);
const end = performance.now();
@@ -192,7 +232,11 @@ export async function main(opts: RunnerOptions): Promise {
const shouldWatch = opts.watch;
if (shouldWatch) {
- makeWatchRunner(state => onChange(worker, state), opts.debug, opts.pattern);
+ makeWatchRunner(
+ state => onChange(worker, state, opts.sync),
+ opts.debug,
+ opts.pattern,
+ );
if (opts.pattern) {
/**
* Warm up wormers when in watch mode. Loading the Forget babel plugin
@@ -251,6 +295,7 @@ export async function main(opts: RunnerOptions): Promise {
0,
opts.debug,
false, // no requireSingleFixture in non-watch mode
+ opts.sync,
);
if (opts.update) {
update(results);
@@ -269,5 +314,3 @@ export async function main(opts: RunnerOptions): Promise {
);
}
}
-
-main(opts).catch(error => console.error(error));
diff --git a/compiler/yarn.lock b/compiler/yarn.lock
index 764200c2ce9d..f480ff498cdf 100644
--- a/compiler/yarn.lock
+++ b/compiler/yarn.lock
@@ -268,6 +268,17 @@
source-map "^0.5.0"
trim-right "^1.0.1"
+"@babel/generator@^7.19.1":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1"
+ integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==
+ dependencies:
+ "@babel/parser" "^7.28.6"
+ "@babel/types" "^7.28.6"
+ "@jridgewell/gen-mapping" "^0.3.12"
+ "@jridgewell/trace-mapping" "^0.3.28"
+ jsesc "^3.0.2"
+
"@babel/generator@^7.2.0", "@babel/generator@^7.26.0", "@babel/generator@^7.26.10", "@babel/generator@^7.26.3", "@babel/generator@^7.27.0", "@babel/generator@^7.7.2", "@babel/generator@^7.7.4":
version "7.27.0"
resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz"
@@ -662,6 +673,13 @@
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz"
integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==
+"@babel/parser@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd"
+ integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==
+ dependencies:
+ "@babel/types" "^7.28.6"
+
"@babel/parser@^7.7.4":
version "7.21.4"
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz"
@@ -1592,7 +1610,7 @@
debug "^4.3.1"
globals "^11.1.0"
-"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
+"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.28.6", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
@@ -2732,6 +2750,14 @@
"@types/yargs" "^17.0.8"
chalk "^4.0.0"
+"@jridgewell/gen-mapping@^0.3.12":
+ version "0.3.13"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
+ integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
"@jridgewell/gen-mapping@^0.3.2":
version "0.3.8"
resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz"
@@ -2765,6 +2791,11 @@
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+"@jridgewell/sourcemap-codec@^1.5.0":
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
+ integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
+
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz"
@@ -2789,6 +2820,14 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
+"@jridgewell/trace-mapping@^0.3.28":
+ version "0.3.31"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
+ integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
"@modelcontextprotocol/sdk@^1.9.0":
version "1.9.0"
resolved "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz"