optipack 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BENCHMARKING.md +127 -0
- package/LICENSE +21 -0
- package/README.md +136 -2
- package/bin/optipack.js +12 -0
- package/example/manifest.json +27 -0
- package/example/scripts/drops.js +24 -0
- package/example/scripts/main.js +67 -0
- package/example/scripts/util.js +30 -0
- package/package.json +34 -4
- package/src/bench.js +231 -0
- package/src/cli.js +245 -0
- package/src/config.js +141 -0
- package/src/diff.js +108 -0
- package/src/harness/driver.js +216 -0
- package/src/harness/loader-hooks.mjs +63 -0
- package/src/harness/mock/@minecraft/common.mjs +17 -0
- package/src/harness/mock/@minecraft/debug-utilities.mjs +10 -0
- package/src/harness/mock/@minecraft/diagnostics.mjs +14 -0
- package/src/harness/mock/@minecraft/server-admin.mjs +14 -0
- package/src/harness/mock/@minecraft/server-editor.mjs +39 -0
- package/src/harness/mock/@minecraft/server-extras.mjs +538 -0
- package/src/harness/mock/@minecraft/server-gametest.mjs +22 -0
- package/src/harness/mock/@minecraft/server-graphics.mjs +21 -0
- package/src/harness/mock/@minecraft/server-net.mjs +15 -0
- package/src/harness/mock/@minecraft/server-ui.mjs +125 -0
- package/src/harness/mock/@minecraft/server.mjs +394 -0
- package/src/harness/mock/permissive.mjs +71 -0
- package/src/hotpath.js +158 -0
- package/src/lint.js +150 -0
- package/src/pack-walker.js +50 -0
- package/src/pass-utils.js +419 -0
- package/src/passes/b1-cache-getter-chains.js +406 -0
- package/src/passes/b2-closure-lift.js +316 -0
- package/src/passes/b3-string-build.js +167 -0
- package/src/passes/b4-dynprop-hoist.js +177 -0
- package/src/passes/b5-null-chain-flatten.js +137 -0
- package/src/passes/index.js +43 -0
- package/src/passes/r1-cross-tick-cache.js +192 -0
- package/src/passes/r2-json-fold.js +201 -0
- package/src/passes/r3-try-catch-strip.js +61 -0
- package/src/passes/r4-getter-cache-cross-barrier.js +29 -0
- package/src/passes/s1-const-fold.js +130 -0
- package/src/passes/s2-dead-code.js +183 -0
- package/src/passes/s3-bool-simplify.js +82 -0
- package/src/passes/s4-for-of-to-indexed.js +140 -0
- package/src/passes/s5-array-methods-to-loops.js +357 -0
- package/src/passes/s6-destructure-to-direct.js +186 -0
- package/src/passes/s7-hoist-invariant-members.js +327 -0
- package/src/pipeline.js +186 -0
- package/src/pragma.js +115 -0
- package/src/report.js +98 -0
- package/test/array-methods-to-loops/expected.js +58 -0
- package/test/array-methods-to-loops/expects.json +6 -0
- package/test/array-methods-to-loops/input.js +23 -0
- package/test/array-methods-to-loops/scenario.json +22 -0
- package/test/array-methods-to-loops-bail/expected.js +10 -0
- package/test/array-methods-to-loops-bail/input.js +24 -0
- package/test/bool-simplify/expected.js +14 -0
- package/test/bool-simplify/expects.json +11 -0
- package/test/bool-simplify/input.js +18 -0
- package/test/cache-getter-chains/expected.js +20 -0
- package/test/cache-getter-chains/expects.json +11 -0
- package/test/cache-getter-chains/input.js +20 -0
- package/test/cache-getter-chains/scenario.json +35 -0
- package/test/closure-lift/expected.js +58 -0
- package/test/closure-lift/expects.json +1 -0
- package/test/closure-lift/input.js +50 -0
- package/test/closure-lift/scenario.json +9 -0
- package/test/const-fold/expected.js +12 -0
- package/test/const-fold/expects.json +1 -0
- package/test/const-fold/input.js +12 -0
- package/test/const-fold-bail/expected.js +7 -0
- package/test/const-fold-bail/expects.json +3 -0
- package/test/const-fold-bail/input.js +7 -0
- package/test/cross-tick-cache/config.json +1 -0
- package/test/cross-tick-cache/expected.js +38 -0
- package/test/cross-tick-cache/expects.json +6 -0
- package/test/cross-tick-cache/input.js +13 -0
- package/test/cross-tick-cache/scenario.json +33 -0
- package/test/dead-code/expected.js +14 -0
- package/test/dead-code/expects.json +5 -0
- package/test/dead-code/input.js +23 -0
- package/test/destructure-to-direct/expected.js +37 -0
- package/test/destructure-to-direct/expects.json +11 -0
- package/test/destructure-to-direct/input.js +28 -0
- package/test/destructure-to-direct/scenario.json +22 -0
- package/test/destructure-to-direct-bail/expected.js +36 -0
- package/test/destructure-to-direct-bail/expects.json +11 -0
- package/test/destructure-to-direct-bail/input.js +25 -0
- package/test/differential.test.mjs +267 -0
- package/test/dynprop-hoist/expected.js +24 -0
- package/test/dynprop-hoist/expects.json +11 -0
- package/test/dynprop-hoist/input.js +25 -0
- package/test/dynprop-hoist/scenario.json +24 -0
- package/test/example-report-snapshot.json +107 -0
- package/test/example.test.mjs +115 -0
- package/test/fetch-corpus.mjs +92 -0
- package/test/for-of-to-indexed/expected.js +59 -0
- package/test/for-of-to-indexed/expects.json +11 -0
- package/test/for-of-to-indexed/input.js +43 -0
- package/test/for-of-to-indexed-bail/expected.js +25 -0
- package/test/for-of-to-indexed-bail/expects.json +11 -0
- package/test/for-of-to-indexed-bail/input.js +29 -0
- package/test/gen-expected.mjs +33 -0
- package/test/getter-cache-cross-barrier/config.json +1 -0
- package/test/getter-cache-cross-barrier/expected.js +10 -0
- package/test/getter-cache-cross-barrier/expects.json +6 -0
- package/test/getter-cache-cross-barrier/input.js +11 -0
- package/test/getter-cache-cross-barrier/scenario.json +28 -0
- package/test/harness/fixture.js +117 -0
- package/test/hoist-invariant-members/expected.js +37 -0
- package/test/hoist-invariant-members/expects.json +11 -0
- package/test/hoist-invariant-members/input.js +34 -0
- package/test/hoist-invariant-members/scenario.json +35 -0
- package/test/hoist-invariant-members-bail/expected.js +19 -0
- package/test/hoist-invariant-members-bail/expects.json +5 -0
- package/test/hoist-invariant-members-bail/input.js +22 -0
- package/test/infra.test.mjs +270 -0
- package/test/json-fold/config.json +1 -0
- package/test/json-fold/expected.js +14 -0
- package/test/json-fold/expects.json +12 -0
- package/test/json-fold/input.js +17 -0
- package/test/json-fold-bail/config.json +1 -0
- package/test/json-fold-bail/expected.js +10 -0
- package/test/json-fold-bail/expects.json +5 -0
- package/test/json-fold-bail/input.js +14 -0
- package/test/null-chain-flatten/expected.js +22 -0
- package/test/null-chain-flatten/expects.json +5 -0
- package/test/null-chain-flatten/input.js +22 -0
- package/test/null-chain-flatten/scenario.json +24 -0
- package/test/server-ui-forms/expected.js +15 -0
- package/test/server-ui-forms/expects.json +1 -0
- package/test/server-ui-forms/input.js +25 -0
- package/test/server-ui-forms/scenario.json +1 -0
- package/test/string-build/expected.js +29 -0
- package/test/string-build/expects.json +5 -0
- package/test/string-build/input.js +29 -0
- package/test/string-build/scenario.json +27 -0
- package/test/string-build-bail/expected.js +10 -0
- package/test/string-build-bail/input.js +21 -0
- package/test/transforms.test.mjs +43 -0
- package/test/try-catch-strip/config.json +1 -0
- package/test/try-catch-strip/expected.js +57 -0
- package/test/try-catch-strip/expects.json +1 -0
- package/test/try-catch-strip/input.js +34 -0
- package/test/try-catch-strip/scenario.json +28 -0
- package/test/update-example-snapshot.mjs +19 -0
- package/test/update-mock-stubs.mjs +91 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example pack end-to-end: run the CLI over example/, verify parse validity,
|
|
3
|
+
* manifest fidelity, effects-equivalence across the whole script graph, report
|
|
4
|
+
* snapshot, and determinism.
|
|
5
|
+
*/
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const root = path.join(here, '..');
|
|
16
|
+
const CLI = path.join(root, 'bin', 'optipack.js');
|
|
17
|
+
const EXAMPLE = path.join(root, 'example');
|
|
18
|
+
|
|
19
|
+
function mkOut(prefix) {
|
|
20
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('example: CLI produces a valid optimized pack + report', async () => {
|
|
24
|
+
const out = mkOut('optipack-example-');
|
|
25
|
+
const res = spawnSync(process.execPath, [CLI, EXAMPLE, out], { encoding: 'utf8' });
|
|
26
|
+
assert.equal(res.status, 0, res.stderr);
|
|
27
|
+
|
|
28
|
+
// Manifest and non-scripts copied verbatim.
|
|
29
|
+
assert.equal(
|
|
30
|
+
fs.readFileSync(path.join(out, 'manifest.json'), 'utf8'),
|
|
31
|
+
fs.readFileSync(path.join(EXAMPLE, 'manifest.json'), 'utf8')
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Report exists with the expected schema shape.
|
|
35
|
+
const report = JSON.parse(fs.readFileSync(path.join(out, 'optipack-report.json'), 'utf8'));
|
|
36
|
+
assert.equal(report.level, 'balanced');
|
|
37
|
+
assert.ok(Array.isArray(report.files) && report.files.length === 3);
|
|
38
|
+
assert.ok(report.files.every((f) => f.path.startsWith('scripts/')));
|
|
39
|
+
assert.ok(report.totals.transforms > 0, 'transforms applied');
|
|
40
|
+
for (const f of report.files) {
|
|
41
|
+
for (const key of ['path', 'hotFunctions', 'transforms', 'bails', 'lint', 'residualAllocs', 'bytes']) {
|
|
42
|
+
assert.ok(key in f, `report file has ${key}`);
|
|
43
|
+
}
|
|
44
|
+
assert.ok(f.bytes.after > 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Every output script re-parses as ESM.
|
|
48
|
+
const { parseSource } = await import('../src/pipeline.js');
|
|
49
|
+
for (const f of ['scripts/main.js', 'scripts/util.js', 'scripts/drops.js']) {
|
|
50
|
+
const src = fs.readFileSync(path.join(out, f), 'utf8');
|
|
51
|
+
assert.doesNotThrow(() => parseSource(f, src), `${f} round-trips`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('example: effects-equivalence across the whole script graph', async () => {
|
|
56
|
+
const out = mkOut('optipack-example-');
|
|
57
|
+
const res = spawnSync(process.execPath, [CLI, EXAMPLE, out], { encoding: 'utf8' });
|
|
58
|
+
assert.equal(res.status, 0, res.stderr);
|
|
59
|
+
|
|
60
|
+
const { runVariant, assertSameEffects, pathToUrl } = await import('../src/harness/driver.js');
|
|
61
|
+
const scenario = {
|
|
62
|
+
reads: {
|
|
63
|
+
'location:p1': [{ x: 10, y: 64, z: 20 }, { x: 12, y: 65, z: 22 }],
|
|
64
|
+
'name:p1': ['Alex'],
|
|
65
|
+
'dp:p1:optipack:lastDist': [undefined],
|
|
66
|
+
'dp:p1:optipack:baseline': [undefined],
|
|
67
|
+
'dp:p1:optipack:drops': [undefined],
|
|
68
|
+
'world.dp:scan:0': [0],
|
|
69
|
+
'world.dp:scan:1': [0],
|
|
70
|
+
'world.dp:scan:2': [0],
|
|
71
|
+
'world.dp:scan:3': [0],
|
|
72
|
+
},
|
|
73
|
+
state: { players: [{ id: 'p1' }] },
|
|
74
|
+
steps: [
|
|
75
|
+
{ op: 'fire', event: 'world.afterEvents.worldLoad', payload: {} },
|
|
76
|
+
{ op: 'tick', n: 60 },
|
|
77
|
+
{ op: 'fire', event: 'world.afterEvents.playerSpawn', payload: { player: { __entity: 'p1' } } },
|
|
78
|
+
{ op: 'tick', n: 40 },
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const a = await runVariant(pathToUrl(path.join(EXAMPLE, 'scripts', 'main.js')), 'orig', scenario);
|
|
83
|
+
const b = await runVariant(pathToUrl(path.join(out, 'scripts', 'main.js')), 'opt', scenario);
|
|
84
|
+
assertSameEffects(a, b, 'example pack');
|
|
85
|
+
assert.ok(a.effects.length > 10, 'scenario exercised real effects');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('example: report snapshot is stable', () => {
|
|
89
|
+
const out = mkOut('optipack-example-');
|
|
90
|
+
const res = spawnSync(process.execPath, [CLI, EXAMPLE, out, '--report', path.join(out, 'r.json')], { encoding: 'utf8' });
|
|
91
|
+
assert.equal(res.status, 0, res.stderr);
|
|
92
|
+
const actual = fs.readFileSync(path.join(out, 'r.json'), 'utf8');
|
|
93
|
+
const snapshotPath = path.join(here, 'example-report-snapshot.json');
|
|
94
|
+
if (process.env.UPDATE_SNAPSHOT) {
|
|
95
|
+
fs.writeFileSync(snapshotPath, actual);
|
|
96
|
+
}
|
|
97
|
+
assert.ok(fs.existsSync(snapshotPath), 'snapshot exists (run npm run update-snapshot)');
|
|
98
|
+
const expected = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'));
|
|
99
|
+
const got = JSON.parse(actual);
|
|
100
|
+
assert.deepEqual(got, expected, 'report matches snapshot');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('example: deterministic output — two runs are byte-identical', () => {
|
|
104
|
+
const out1 = mkOut('optipack-det1-');
|
|
105
|
+
const out2 = mkOut('optipack-det2-');
|
|
106
|
+
spawnSync(process.execPath, [CLI, EXAMPLE, out1], { encoding: 'utf8' });
|
|
107
|
+
spawnSync(process.execPath, [CLI, EXAMPLE, out2], { encoding: 'utf8' });
|
|
108
|
+
for (const f of ['scripts/main.js', 'scripts/util.js', 'scripts/drops.js', 'optipack-report.json']) {
|
|
109
|
+
assert.equal(
|
|
110
|
+
fs.readFileSync(path.join(out1, f), 'utf8'),
|
|
111
|
+
fs.readFileSync(path.join(out2, f), 'utf8'),
|
|
112
|
+
`${f} identical across runs`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch the real-world benchmark corpus.
|
|
3
|
+
*
|
|
4
|
+
* Downloads MIT-licensed community behavior-pack scripts from JaylyDev/ScriptAPI
|
|
5
|
+
* (used with attribution; see the header comment of each fetched file for its
|
|
6
|
+
* original author), assembles a runnable pack under test/corpus/jayly-pack/,
|
|
7
|
+
* and records the source commit in SOURCES.json.
|
|
8
|
+
*
|
|
9
|
+
* The corpus is gitignored — run `npm run fetch-corpus` before benching it.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const corpusDir = path.join(here, 'corpus');
|
|
19
|
+
const repo = 'JaylyDev/ScriptAPI';
|
|
20
|
+
|
|
21
|
+
// Self-registering modules (subscribe or runInterval at import time) that the
|
|
22
|
+
// corpus pack's entry imports. Others ship in scripts/ for relative imports.
|
|
23
|
+
const ENTRY_IMPORTS = [
|
|
24
|
+
'./anti-nuker/index.js',
|
|
25
|
+
'./anti-32k/index.js',
|
|
26
|
+
'./anti-32k-event/index.js',
|
|
27
|
+
'./world-border/index.js',
|
|
28
|
+
'./health-display/index.js',
|
|
29
|
+
'./player-death-event/index.js',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const manifest = {
|
|
33
|
+
format_version: 2,
|
|
34
|
+
header: {
|
|
35
|
+
name: 'optipack bench corpus (JaylyDev/ScriptAPI)',
|
|
36
|
+
description: 'Real-world community scripts assembled for optipack A/B benchmarking. Scripts are MIT-licensed by their original authors.',
|
|
37
|
+
uuid: '3f7c2d91-8a44-4e0b-9c55-1b2c3d4e5f60',
|
|
38
|
+
version: [1, 0, 0],
|
|
39
|
+
min_engine_version: [1, 21, 0],
|
|
40
|
+
},
|
|
41
|
+
modules: [
|
|
42
|
+
{ type: 'data', uuid: '4a8d3e02-9b55-4f1c-8d66-2c3d4e5f6a70', version: [1, 0, 0] },
|
|
43
|
+
{ type: 'script', language: 'javascript', uuid: '5b9e4f13-ac66-402d-9e77-3d4e5f6a7b80', entry: 'scripts/main.js', version: [1, 0, 0] },
|
|
44
|
+
],
|
|
45
|
+
dependencies: [{ module_name: '@minecraft/server', version: '2.0.0' }],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// 1. Download + extract.
|
|
49
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-corpus-'));
|
|
50
|
+
const tgz = path.join(tmp, 'repo.tar.gz');
|
|
51
|
+
console.log(`fetching ${repo} tarball...`);
|
|
52
|
+
execSync(`curl -sL "https://github.com/${repo}/archive/refs/heads/main.tar.gz" -o "${tgz}"`, { stdio: 'inherit' });
|
|
53
|
+
fs.mkdirSync(path.join(tmp, 'x'), { recursive: true });
|
|
54
|
+
execSync(`tar -xzf "${tgz}" -C "${path.join(tmp, 'x')}"`);
|
|
55
|
+
const extracted = fs.readdirSync(path.join(tmp, 'x'))[0];
|
|
56
|
+
const commit = execSync(`git ls-remote https://github.com/${repo}.git HEAD`, { encoding: 'utf8' }).trim().split(/\s+/)[0];
|
|
57
|
+
|
|
58
|
+
// 2. Assemble the corpus pack.
|
|
59
|
+
const packDir = path.join(corpusDir, 'jayly-pack');
|
|
60
|
+
fs.rmSync(corpusDir, { recursive: true, force: true });
|
|
61
|
+
fs.mkdirSync(path.join(packDir, 'scripts'), { recursive: true });
|
|
62
|
+
fs.cpSync(path.join(tmp, 'x', extracted, 'scripts'), path.join(packDir, 'scripts'), { recursive: true });
|
|
63
|
+
fs.writeFileSync(path.join(packDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
64
|
+
|
|
65
|
+
const imports = ENTRY_IMPORTS.map((rel) => `import '${rel}';`).join('\n');
|
|
66
|
+
const entry = [
|
|
67
|
+
'// optipack bench corpus entry — imports real-world community scripts',
|
|
68
|
+
'// (JaylyDev/ScriptAPI, MIT). Each module self-registers its handlers.',
|
|
69
|
+
imports,
|
|
70
|
+
'',
|
|
71
|
+
].join('\n');
|
|
72
|
+
fs.writeFileSync(path.join(packDir, 'scripts', 'main.js'), entry);
|
|
73
|
+
|
|
74
|
+
// 3. Record provenance.
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(corpusDir, 'SOURCES.json'),
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
sources: [{ repo, commit, license: 'MIT', fetched: new Date().toISOString() }],
|
|
79
|
+
note: 'Scripts belong to their original authors (see file headers). Fetched for optipack benchmarking only.',
|
|
80
|
+
}, null, 2) + '\n'
|
|
81
|
+
);
|
|
82
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
83
|
+
|
|
84
|
+
const scriptCount = (function walk(dir) {
|
|
85
|
+
return fs.readdirSync(dir, { withFileTypes: true }).reduce((n, e) => {
|
|
86
|
+
const p = path.join(dir, e.name);
|
|
87
|
+
return n + (e.isDirectory() ? walk(p) : e.name.endsWith('.js') ? 1 : 0);
|
|
88
|
+
}, 0);
|
|
89
|
+
})(path.join(packDir, 'scripts'));
|
|
90
|
+
|
|
91
|
+
console.log(`corpus ready: ${packDir} (${scriptCount} script files, ${repo}@${commit.slice(0, 9)})`);
|
|
92
|
+
console.log('next: node bin/optipack.js bench test/corpus/jayly-pack --scenario test/corpus/jayly-scenario.json');
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// S4: for-of conversion over provably-array iterables.
|
|
2
|
+
const names = ['a', 'b', 'c'];
|
|
3
|
+
export function sumLengths() {
|
|
4
|
+
let n = 0;
|
|
5
|
+
for (let _opI = 0; _opI < names.length; _opI++) {
|
|
6
|
+
const v = names[_opI];
|
|
7
|
+
{
|
|
8
|
+
n += v.length;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return n;
|
|
12
|
+
}
|
|
13
|
+
export function letters() {
|
|
14
|
+
let s = 0;
|
|
15
|
+
{
|
|
16
|
+
const _opArr = 'hello'.split('');
|
|
17
|
+
for (let _opI2 = 0; _opI2 < _opArr.length; _opI2++) {
|
|
18
|
+
const ch = _opArr[_opI2];
|
|
19
|
+
{
|
|
20
|
+
if (ch === 'l') s += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return s;
|
|
25
|
+
}
|
|
26
|
+
export function mapped() {
|
|
27
|
+
const vals = ['a', 'bb'].map(v => v + '!');
|
|
28
|
+
let out = 0;
|
|
29
|
+
for (let _opI3 = 0; _opI3 < vals.length; _opI3++) {
|
|
30
|
+
const v = vals[_opI3];
|
|
31
|
+
{
|
|
32
|
+
out += v.length;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export function withDestructure(pairs) {
|
|
38
|
+
const entries = Object.entries({
|
|
39
|
+
a: 1,
|
|
40
|
+
b: 2
|
|
41
|
+
});
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (let _opI4 = 0; _opI4 < entries.length; _opI4++) {
|
|
44
|
+
const _opEl = entries[_opI4];
|
|
45
|
+
const k = _opEl[0];
|
|
46
|
+
const v = _opEl[1];
|
|
47
|
+
{
|
|
48
|
+
total += v;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return total;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Host returns are never converted (bail).
|
|
55
|
+
export function hostIter(players) {
|
|
56
|
+
for (const p of players) {
|
|
57
|
+
p.x = 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// S4: for-of conversion over provably-array iterables.
|
|
2
|
+
const names = ['a', 'b', 'c'];
|
|
3
|
+
|
|
4
|
+
export function sumLengths() {
|
|
5
|
+
let n = 0;
|
|
6
|
+
for (const v of names) {
|
|
7
|
+
n += v.length;
|
|
8
|
+
}
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function letters() {
|
|
13
|
+
let s = 0;
|
|
14
|
+
for (const ch of 'hello'.split('')) {
|
|
15
|
+
if (ch === 'l') s += 1;
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mapped() {
|
|
21
|
+
const vals = ['a', 'bb'].map((v) => v + '!');
|
|
22
|
+
let out = 0;
|
|
23
|
+
for (const v of vals) {
|
|
24
|
+
out += v.length;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function withDestructure(pairs) {
|
|
30
|
+
const entries = Object.entries({ a: 1, b: 2 });
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (const [k, v] of entries) {
|
|
33
|
+
total += v;
|
|
34
|
+
}
|
|
35
|
+
return total;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Host returns are never converted (bail).
|
|
39
|
+
export function hostIter(players) {
|
|
40
|
+
for (const p of players) {
|
|
41
|
+
p.x = 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// S4 bails: Map/Set/strings/host returns are never converted.
|
|
2
|
+
const m = new Map();
|
|
3
|
+
export function mapIter(m2) {
|
|
4
|
+
for (const [k, v] of m2) {
|
|
5
|
+
m.set(k, v);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function stringIter(s) {
|
|
9
|
+
let n = 0;
|
|
10
|
+
for (const ch of s) {
|
|
11
|
+
n += 1;
|
|
12
|
+
}
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
export function hostReturns(players) {
|
|
16
|
+
for (const p of worldPlayers()) {
|
|
17
|
+
p.ping = 1;
|
|
18
|
+
}
|
|
19
|
+
for (const p of players) {
|
|
20
|
+
p.pong = 2;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function worldPlayers() {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// S4 bails: Map/Set/strings/host returns are never converted.
|
|
2
|
+
const m = new Map();
|
|
3
|
+
|
|
4
|
+
export function mapIter(m2) {
|
|
5
|
+
for (const [k, v] of m2) {
|
|
6
|
+
m.set(k, v);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function stringIter(s) {
|
|
11
|
+
let n = 0;
|
|
12
|
+
for (const ch of s) {
|
|
13
|
+
n += 1;
|
|
14
|
+
}
|
|
15
|
+
return n;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hostReturns(players) {
|
|
19
|
+
for (const p of worldPlayers()) {
|
|
20
|
+
p.ping = 1;
|
|
21
|
+
}
|
|
22
|
+
for (const p of players) {
|
|
23
|
+
p.pong = 2;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function worldPlayers() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// One-off helper: generate expected.js for every fixture that lacks one.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const root = here; // fixtures live directly under test/
|
|
8
|
+
|
|
9
|
+
const { normalizeConfig } = await import('../src/config.js');
|
|
10
|
+
const { transformSource } = await import('../src/pipeline.js');
|
|
11
|
+
|
|
12
|
+
const dirs = fs.readdirSync(root, { withFileTypes: true })
|
|
13
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(root, e.name, 'input.js')))
|
|
14
|
+
.map((e) => e.name)
|
|
15
|
+
.sort();
|
|
16
|
+
|
|
17
|
+
const only = process.argv[2] || null;
|
|
18
|
+
for (const dir of dirs) {
|
|
19
|
+
if (only && dir !== only) continue;
|
|
20
|
+
const fdir = path.join(root, dir);
|
|
21
|
+
if (fs.existsSync(path.join(fdir, 'expected.js'))) continue;
|
|
22
|
+
const input = fs.readFileSync(path.join(fdir, 'input.js'), 'utf8');
|
|
23
|
+
const configPath = path.join(fdir, 'config.json');
|
|
24
|
+
const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : { level: 'balanced' };
|
|
25
|
+
const cfg = normalizeConfig(raw);
|
|
26
|
+
try {
|
|
27
|
+
const { code, fileReport } = transformSource('input.js', input, cfg);
|
|
28
|
+
fs.writeFileSync(path.join(fdir, 'expected.js'), code);
|
|
29
|
+
console.log(`${dir}: generated (${Object.keys(fileReport.transforms).join(', ') || 'no transforms'})`);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error(`${dir}: FAILED: ${e.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"level": "risky"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// R4: getter caching across call barriers (risky).
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
function onTick(player) {
|
|
4
|
+
const _opC = player.location.x;
|
|
5
|
+
const before = _opC;
|
|
6
|
+
world.sendMessage('moving'); // call barrier — B1 would bail, R4 crosses it
|
|
7
|
+
const after = _opC;
|
|
8
|
+
world.sendMessage('moved=' + (after - before));
|
|
9
|
+
}
|
|
10
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// R4: getter caching across call barriers (risky).
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
function onTick(player) {
|
|
5
|
+
const before = player.location.x;
|
|
6
|
+
world.sendMessage('moving'); // call barrier — B1 would bail, R4 crosses it
|
|
7
|
+
const after = player.location.x;
|
|
8
|
+
world.sendMessage('moved=' + (after - before));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reads": {
|
|
3
|
+
"location:p1": [
|
|
4
|
+
{
|
|
5
|
+
"y": 64,
|
|
6
|
+
"z": 2,
|
|
7
|
+
"x": 1
|
|
8
|
+
}
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"entry": "scripts/input.js",
|
|
12
|
+
"steps": [
|
|
13
|
+
{
|
|
14
|
+
"n": 5,
|
|
15
|
+
"op": "tick"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"state": {
|
|
19
|
+
"players": [
|
|
20
|
+
{
|
|
21
|
+
"id": "p1"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "p2"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture runner for per-transform unit tests.
|
|
3
|
+
*
|
|
4
|
+
* A fixture directory contains:
|
|
5
|
+
* input.js — source fed through the full pipeline
|
|
6
|
+
* expected.js — (optional) exact expected output; when absent the input
|
|
7
|
+
* must come out unchanged (bail fixtures)
|
|
8
|
+
* config.json — (optional) { level, transforms, hotPath, lint } user config
|
|
9
|
+
* expects.json — (optional) assertions on the report:
|
|
10
|
+
* { transforms: {id: count}, bails: [{transform, reasonContains}],
|
|
11
|
+
* lint: [{rule}], warnings: n }
|
|
12
|
+
* scenario.json— (optional) differential scenario { entry, reads, state, steps }
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
|
|
18
|
+
export async function runFixtureDir(dir) {
|
|
19
|
+
const inputPath = path.join(dir, 'input.js');
|
|
20
|
+
const input = fs.readFileSync(inputPath, 'utf8');
|
|
21
|
+
const expectedPath = path.join(dir, 'expected.js');
|
|
22
|
+
const hasExpected = fs.existsSync(expectedPath);
|
|
23
|
+
const expected = hasExpected ? fs.readFileSync(expectedPath, 'utf8') : null;
|
|
24
|
+
|
|
25
|
+
const configPath = path.join(dir, 'config.json');
|
|
26
|
+
const config = fs.existsSync(configPath)
|
|
27
|
+
? JSON.parse(fs.readFileSync(configPath, 'utf8').replace(/^\uFEFF/, ''))
|
|
28
|
+
: { level: 'balanced' };
|
|
29
|
+
|
|
30
|
+
const expectsPath = path.join(dir, 'expects.json');
|
|
31
|
+
const expects = fs.existsSync(expectsPath) ? JSON.parse(fs.readFileSync(expectsPath, 'utf8')) : null;
|
|
32
|
+
|
|
33
|
+
const scenarioPath = path.join(dir, 'scenario.json');
|
|
34
|
+
const scenario = fs.existsSync(scenarioPath) ? JSON.parse(fs.readFileSync(scenarioPath, 'utf8')) : null;
|
|
35
|
+
|
|
36
|
+
const { normalizeConfig } = await import('../../src/config.js');
|
|
37
|
+
const { runPack } = await import('../../src/pipeline.js');
|
|
38
|
+
|
|
39
|
+
const inputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-fx-in-'));
|
|
40
|
+
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'optipack-fx-out-'));
|
|
41
|
+
fs.mkdirSync(path.join(inputDir, 'scripts'), { recursive: true });
|
|
42
|
+
fs.writeFileSync(path.join(inputDir, 'scripts', 'input.js'), input);
|
|
43
|
+
const cfg = normalizeConfig(config);
|
|
44
|
+
const report = runPack(inputDir, outputDir, cfg);
|
|
45
|
+
const output = fs.readFileSync(path.join(outputDir, 'scripts', 'input.js'), 'utf8');
|
|
46
|
+
|
|
47
|
+
const result = { input, output, expected, report, expects, scenario, outputDir, config: cfg };
|
|
48
|
+
|
|
49
|
+
// Exact output comparison.
|
|
50
|
+
const want = hasExpected ? expected : input;
|
|
51
|
+
const got = output.replace(/\r\n/g, '\n');
|
|
52
|
+
const wantNorm = want.replace(/\r\n/g, '\n');
|
|
53
|
+
if (got !== wantNorm) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`fixture output mismatch for ${path.basename(dir)}:\n` +
|
|
56
|
+
`--- expected ---\n${wantNorm}\n--- actual ---\n${got}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Report expectations.
|
|
61
|
+
if (expects) {
|
|
62
|
+
const file = report.files[0];
|
|
63
|
+
if (expects.transforms) {
|
|
64
|
+
for (const [id, count] of Object.entries(expects.transforms)) {
|
|
65
|
+
const gotCount = (file.transforms[id] || 0);
|
|
66
|
+
if (gotCount !== count) {
|
|
67
|
+
throw new Error(`fixture ${path.basename(dir)}: expected ${count} of ${id}, got ${gotCount}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (expects.noTransforms) {
|
|
72
|
+
const total = Object.values(file.transforms).reduce((a, b) => a + b, 0);
|
|
73
|
+
if (total !== 0) {
|
|
74
|
+
throw new Error(`fixture ${path.basename(dir)}: expected no transforms, got ${JSON.stringify(file.transforms)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (expects.bails) {
|
|
78
|
+
for (const wantBail of expects.bails) {
|
|
79
|
+
const found = file.bails.find((b) =>
|
|
80
|
+
b.transform === wantBail.transform &&
|
|
81
|
+
(wantBail.reasonContains === undefined || b.reason.includes(wantBail.reasonContains)) &&
|
|
82
|
+
(wantBail.line === undefined || b.line === wantBail.line));
|
|
83
|
+
if (!found) {
|
|
84
|
+
throw new Error(`fixture ${path.basename(dir)}: expected bail ${JSON.stringify(wantBail)}, have ${JSON.stringify(file.bails)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (expects.lint) {
|
|
89
|
+
for (const wantLint of expects.lint) {
|
|
90
|
+
const found = file.lint.find((l) => l.rule === wantLint.rule &&
|
|
91
|
+
(wantLint.line === undefined || l.line === wantLint.line));
|
|
92
|
+
if (!found) {
|
|
93
|
+
throw new Error(`fixture ${path.basename(dir)}: expected lint ${JSON.stringify(wantLint)}, have ${JSON.stringify(file.lint)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (expects.warnings !== undefined) {
|
|
98
|
+
if (report.warnings.length !== expects.warnings) {
|
|
99
|
+
throw new Error(`fixture ${path.basename(dir)}: expected ${expects.warnings} warnings, got ${report.warnings.length}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (expects.hotFunctions !== undefined) {
|
|
103
|
+
if (file.hotFunctions !== expects.hotFunctions) {
|
|
104
|
+
throw new Error(`fixture ${path.basename(dir)}: expected ${expects.hotFunctions} hot functions, got ${file.hotFunctions}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function fixtureDirs(baseDir) {
|
|
113
|
+
return fs.readdirSync(baseDir, { withFileTypes: true })
|
|
114
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(baseDir, e.name, 'input.js')))
|
|
115
|
+
.map((e) => path.join(baseDir, e.name))
|
|
116
|
+
.sort();
|
|
117
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// S7: loop-invariant member hoisting inside hot functions.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
// Before-loop hoist: player.location read once instead of 10 times.
|
|
5
|
+
function onTick(player) {
|
|
6
|
+
let total = 0;
|
|
7
|
+
const _opH = player.location.y;
|
|
8
|
+
for (let i = 0; i < 10; i++) {
|
|
9
|
+
total += _opH;
|
|
10
|
+
}
|
|
11
|
+
world.setDynamicProperty('total', total);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Two chains, each used twice per iteration: both hoisted.
|
|
15
|
+
function onTick2(chest) {
|
|
16
|
+
let dx = 0;
|
|
17
|
+
const _opC = chest.location;
|
|
18
|
+
const _opH2 = _opC.x;
|
|
19
|
+
const _opH3 = _opC.y;
|
|
20
|
+
for (let i = 0; i < 5; i++) {
|
|
21
|
+
dx += _opH2 + _opH3;
|
|
22
|
+
}
|
|
23
|
+
world.setDynamicProperty('dx', dx);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Loop body with a call: barrier, bails.
|
|
27
|
+
function onTick3(player) {
|
|
28
|
+
let total = 0;
|
|
29
|
+
for (let i = 0; i < 4; i++) {
|
|
30
|
+
total += player.location.y;
|
|
31
|
+
world.sendMessage('tick');
|
|
32
|
+
}
|
|
33
|
+
world.setDynamicProperty('total3', total);
|
|
34
|
+
}
|
|
35
|
+
system.runInterval(onTick, 20);
|
|
36
|
+
system.runInterval(onTick2, 20);
|
|
37
|
+
system.runInterval(onTick3, 20);
|