[compiler] Add snap subcommand to minimize a test input (#35663)

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 <Stringify x={x} />;
}
```

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
This commit is contained in:
Joseph Savona
2026-02-02 19:03:47 -08:00
committed by GitHub
parent 7b023d7073
commit d4a325df4d
4 changed files with 2189 additions and 35 deletions

View File

@@ -22,6 +22,7 @@
}, },
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.22.5", "@babel/code-frame": "^7.22.5",
"@babel/generator": "^7.19.1",
"@babel/plugin-syntax-jsx": "^7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6",
"@babel/preset-flow": "^7.7.4", "@babel/preset-flow": "^7.7.4",
"@babel/preset-typescript": "^7.26.0", "@babel/preset-typescript": "^7.26.0",
@@ -58,6 +59,7 @@
"resolutions": { "resolutions": {
"./**/@babel/parser": "7.7.4", "./**/@babel/parser": "7.7.4",
"./**/@babel/types": "7.7.4", "./**/@babel/types": "7.7.4",
"@babel/generator": "7.2.0",
"@babel/preset-flow": "7.22.5" "@babel/preset-flow": "7.22.5"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ import {
} from './runner-watch'; } from './runner-watch';
import * as runnerWorker from './runner-worker'; import * as runnerWorker from './runner-worker';
import {execSync} from 'child_process'; import {execSync} from 'child_process';
import {runMinimize} from './minimize';
const WORKER_PATH = require.resolve('./runner-worker.js'); const WORKER_PATH = require.resolve('./runner-worker.js');
const NUM_WORKERS = cpus().length - 1; const NUM_WORKERS = cpus().length - 1;
@@ -38,7 +39,20 @@ type RunnerOptions = {
debug: boolean; debug: boolean;
}; };
const opts: RunnerOptions = yargs async function runTestCommand(opts: RunnerOptions): Promise<void> {
await main(opts);
}
async function runMinimizeCommand(path: string): Promise<void> {
await runMinimize({path});
}
yargs(hideBin(process.argv))
.command(
['test', '$0'],
'Run compiler tests',
yargs => {
return yargs
.boolean('sync') .boolean('sync')
.describe( .describe(
'sync', 'sync',
@@ -52,7 +66,10 @@ const opts: RunnerOptions = yargs
) )
.default('worker-threads', true) .default('worker-threads', true)
.boolean('watch') .boolean('watch')
.describe('watch', 'Run compiler in watch mode, re-running after changes') .describe(
'watch',
'Run compiler in watch mode, re-running after changes',
)
.alias('w', 'watch') .alias('w', 'watch')
.default('watch', false) .default('watch', false)
.boolean('update') .boolean('update')
@@ -68,10 +85,30 @@ const opts: RunnerOptions = yargs
.boolean('debug') .boolean('debug')
.alias('d', 'debug') .alias('d', 'debug')
.describe('debug', 'Enable debug logging to print HIR for each pass') .describe('debug', 'Enable debug logging to print HIR for each pass')
.default('debug', false) .default('debug', false);
},
async argv => {
await runTestCommand(argv as RunnerOptions);
},
)
.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);
},
)
.help('help') .help('help')
.strict() .strict()
.parseSync(hideBin(process.argv)) as RunnerOptions; .demandCommand()
.parse();
/** /**
* Do a test run and return the test results * Do a test run and return the test results
@@ -82,6 +119,7 @@ async function runFixtures(
compilerVersion: number, compilerVersion: number,
debug: boolean, debug: boolean,
requireSingleFixture: boolean, requireSingleFixture: boolean,
sync: boolean,
): Promise<TestResults> { ): Promise<TestResults> {
// We could in theory be fancy about tracking the contents of the fixtures // 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 // 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); const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);
let entries: Array<[string, TestResult]>; let entries: Array<[string, TestResult]>;
if (!opts.sync) { if (!sync) {
// Note: promise.all to ensure parallelism when enabled // Note: promise.all to ensure parallelism when enabled
const work: Array<Promise<[string, TestResult]>> = []; const work: Array<Promise<[string, TestResult]>> = [];
for (const [fixtureName, fixture] of fixtures) { for (const [fixtureName, fixture] of fixtures) {
@@ -123,6 +161,7 @@ async function runFixtures(
async function onChange( async function onChange(
worker: Worker & typeof runnerWorker, worker: Worker & typeof runnerWorker,
state: RunnerState, state: RunnerState,
sync: boolean,
) { ) {
const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state; const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
if (isCompilerBuildValid) { if (isCompilerBuildValid) {
@@ -140,6 +179,7 @@ async function onChange(
compilerVersion, compilerVersion,
debug, debug,
true, // requireSingleFixture in watch mode true, // requireSingleFixture in watch mode
sync,
); );
const end = performance.now(); const end = performance.now();
@@ -192,7 +232,11 @@ export async function main(opts: RunnerOptions): Promise<void> {
const shouldWatch = opts.watch; const shouldWatch = opts.watch;
if (shouldWatch) { 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) { if (opts.pattern) {
/** /**
* Warm up wormers when in watch mode. Loading the Forget babel plugin * Warm up wormers when in watch mode. Loading the Forget babel plugin
@@ -251,6 +295,7 @@ export async function main(opts: RunnerOptions): Promise<void> {
0, 0,
opts.debug, opts.debug,
false, // no requireSingleFixture in non-watch mode false, // no requireSingleFixture in non-watch mode
opts.sync,
); );
if (opts.update) { if (opts.update) {
update(results); update(results);
@@ -269,5 +314,3 @@ export async function main(opts: RunnerOptions): Promise<void> {
); );
} }
} }
main(opts).catch(error => console.error(error));

View File

@@ -268,6 +268,17 @@
source-map "^0.5.0" source-map "^0.5.0"
trim-right "^1.0.1" 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": "@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" version "7.27.0"
resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz" 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" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz"
integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== 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": "@babel/parser@^7.7.4":
version "7.21.4" version "7.21.4"
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz"
@@ -1592,7 +1610,7 @@
debug "^4.3.1" debug "^4.3.1"
globals "^11.1.0" 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" version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
@@ -2732,6 +2750,14 @@
"@types/yargs" "^17.0.8" "@types/yargs" "^17.0.8"
chalk "^4.0.0" 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": "@jridgewell/gen-mapping@^0.3.2":
version "0.3.8" version "0.3.8"
resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz" 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" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== 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": "@jridgewell/trace-mapping@0.3.9":
version "0.3.9" version "0.3.9"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" 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/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14" "@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": "@modelcontextprotocol/sdk@^1.9.0":
version "1.9.0" version "1.9.0"
resolved "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz" resolved "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz"