[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,40 +39,76 @@ type RunnerOptions = {
debug: boolean; debug: boolean;
}; };
const opts: RunnerOptions = yargs async function runTestCommand(opts: RunnerOptions): Promise<void> {
.boolean('sync') await main(opts);
.describe( }
'sync',
'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.', 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')
.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('sync', false) .command(
.boolean('worker-threads') 'minimize',
.describe( 'Minimize a test case to reproduce a compiler error',
'worker-threads', yargs => {
'Run compiler in worker threads (instead of subprocesses). Defaults to true.', 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);
},
) )
.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)
.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"