optipack 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +231 -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 -6
- package/src/cli.js +180 -0
- package/src/config.js +141 -0
- package/src/diff.js +108 -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/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/harness/setup.js +198 -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 +260 -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/loader-hooks.mjs +38 -0
- package/test/mock/@minecraft/server.mjs +274 -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/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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// S2: dead code shapes.
|
|
2
|
+
export function f(x) {
|
|
3
|
+
return x;
|
|
4
|
+
console.log('dead');
|
|
5
|
+
}
|
|
6
|
+
export function g() {
|
|
7
|
+
if (true) {
|
|
8
|
+
return 1;
|
|
9
|
+
} else {
|
|
10
|
+
return 2;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function w() {
|
|
14
|
+
while (false) {
|
|
15
|
+
sideEffect();
|
|
16
|
+
}
|
|
17
|
+
return 'after';
|
|
18
|
+
}
|
|
19
|
+
const unused = 1 + 2;
|
|
20
|
+
const kept = 3;
|
|
21
|
+
export function h() {
|
|
22
|
+
return kept;
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// S6: destructure-to-direct inside a hot handler.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
function onTick(cfg) {
|
|
4
|
+
world.sendMessage('' + cfg.x + cfg.y);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// > 3 total uses: bails, stays untouched.
|
|
8
|
+
function tooManyUses(cfg) {
|
|
9
|
+
const {
|
|
10
|
+
x,
|
|
11
|
+
y
|
|
12
|
+
} = cfg;
|
|
13
|
+
world.sendMessage('' + x);
|
|
14
|
+
world.sendMessage('' + y);
|
|
15
|
+
world.sendMessage('' + x);
|
|
16
|
+
world.sendMessage('' + y);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Default value hoisted to an if.
|
|
20
|
+
function withDefault(cfg) {
|
|
21
|
+
let z = cfg.z;
|
|
22
|
+
if (z === undefined) z = 42;
|
|
23
|
+
world.sendMessage('' + cfg.x + z);
|
|
24
|
+
}
|
|
25
|
+
system.runInterval(() => {
|
|
26
|
+
onTick({
|
|
27
|
+
x: 1,
|
|
28
|
+
y: 2
|
|
29
|
+
});
|
|
30
|
+
tooManyUses({
|
|
31
|
+
x: 1,
|
|
32
|
+
y: 2
|
|
33
|
+
});
|
|
34
|
+
withDefault({
|
|
35
|
+
x: 1
|
|
36
|
+
});
|
|
37
|
+
}, 20);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// S6: destructure-to-direct inside a hot handler.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
function onTick(cfg) {
|
|
5
|
+
const { x, y } = cfg;
|
|
6
|
+
world.sendMessage('' + x + y);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// > 3 total uses: bails, stays untouched.
|
|
10
|
+
function tooManyUses(cfg) {
|
|
11
|
+
const { x, y } = cfg;
|
|
12
|
+
world.sendMessage('' + x);
|
|
13
|
+
world.sendMessage('' + y);
|
|
14
|
+
world.sendMessage('' + x);
|
|
15
|
+
world.sendMessage('' + y);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Default value hoisted to an if.
|
|
19
|
+
function withDefault(cfg) {
|
|
20
|
+
const { x, z = 42 } = cfg;
|
|
21
|
+
world.sendMessage('' + x + z);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
system.runInterval(() => {
|
|
25
|
+
onTick({ x: 1, y: 2 });
|
|
26
|
+
tooManyUses({ x: 1, y: 2 });
|
|
27
|
+
withDefault({ x: 1 });
|
|
28
|
+
}, 20);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// S6 bails: nested patterns, rest, member-chain objects, loops.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
export function nested(cfg) {
|
|
4
|
+
const {
|
|
5
|
+
a: {
|
|
6
|
+
b
|
|
7
|
+
}
|
|
8
|
+
} = cfg;
|
|
9
|
+
return b;
|
|
10
|
+
}
|
|
11
|
+
export function withRest(cfg) {
|
|
12
|
+
const {
|
|
13
|
+
a,
|
|
14
|
+
...rest
|
|
15
|
+
} = cfg;
|
|
16
|
+
return a + rest.length;
|
|
17
|
+
}
|
|
18
|
+
export function memberObj(player) {
|
|
19
|
+
const {
|
|
20
|
+
x,
|
|
21
|
+
y
|
|
22
|
+
} = player.location;
|
|
23
|
+
return x + y;
|
|
24
|
+
}
|
|
25
|
+
export function loopHead(list) {
|
|
26
|
+
for (const {
|
|
27
|
+
id
|
|
28
|
+
} of list) {
|
|
29
|
+
world.sendMessage(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
system.runInterval(() => nested({
|
|
33
|
+
a: {
|
|
34
|
+
b: 1
|
|
35
|
+
}
|
|
36
|
+
}), 20);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// S6 bails: nested patterns, rest, member-chain objects, loops.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
export function nested(cfg) {
|
|
5
|
+
const { a: { b } } = cfg;
|
|
6
|
+
return b;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function withRest(cfg) {
|
|
10
|
+
const { a, ...rest } = cfg;
|
|
11
|
+
return a + rest.length;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function memberObj(player) {
|
|
15
|
+
const { x, y } = player.location;
|
|
16
|
+
return x + y;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loopHead(list) {
|
|
20
|
+
for (const { id } of list) {
|
|
21
|
+
world.sendMessage(id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
system.runInterval(() => nested({ a: { b: 1 } }), 20);
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bespoke differential scenarios.
|
|
3
|
+
*
|
|
4
|
+
* Every scenario loads the original and optimized entry with the mock
|
|
5
|
+
* @minecraft/server, runs identical steps (ticks / events), and requires the
|
|
6
|
+
* recorded EFFECTS to match exactly, in order. Reads return scripted sequences
|
|
7
|
+
* so data flows deterministically; eliminated reads are invisible by design.
|
|
8
|
+
*/
|
|
9
|
+
import test from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { differentialCheck, runVariant, pathToUrl, buildVariantPack } from './harness/setup.js';
|
|
12
|
+
|
|
13
|
+
const PLAYER_READS = {
|
|
14
|
+
'location:p1': [{ x: 1, y: 64, z: 2 }, { x: 2, y: 65, z: 3 }],
|
|
15
|
+
'location:p2': [{ x: 5, y: 70, z: 6 }, { x: 6, y: 71, z: 7 }],
|
|
16
|
+
'name:p1': ['Alice'],
|
|
17
|
+
'name:p2': ['Bob'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const TWO_PLAYERS = { players: [{ id: 'p1' }, { id: 'p2' }] };
|
|
21
|
+
|
|
22
|
+
test('differential: hot tick loop with heavy host reads (S4/S5/S7/B1/B3/B4)', async () => {
|
|
23
|
+
const main = [
|
|
24
|
+
"import { world, system } from '@minecraft/server';",
|
|
25
|
+
'',
|
|
26
|
+
'function onTick() {',
|
|
27
|
+
' const players = world.getAllPlayers();',
|
|
28
|
+
' let report = "";',
|
|
29
|
+
' for (let i = 0; i < players.length; i++) {',
|
|
30
|
+
' const p = players[i];',
|
|
31
|
+
' report += p.name + "@" + p.location.x + "," + p.location.x;',
|
|
32
|
+
' p.setDynamicProperty("optipack:x", p.location.x);',
|
|
33
|
+
' let baseline = 0;',
|
|
34
|
+
' for (let j = 0; j < 3; j++) {',
|
|
35
|
+
' baseline += p.location.y;',
|
|
36
|
+
' }',
|
|
37
|
+
' p.setDynamicProperty("optipack:baseline", baseline);',
|
|
38
|
+
' }',
|
|
39
|
+
' if (report.length > 0) {',
|
|
40
|
+
' world.sendMessage(report);',
|
|
41
|
+
' }',
|
|
42
|
+
' const zones = ["overworld", "nether", "the_end"].filter((z) => z.length > 4);',
|
|
43
|
+
' for (const z of zones) {',
|
|
44
|
+
' world.setDynamicProperty("optipack:zone:" + z, true);',
|
|
45
|
+
' }',
|
|
46
|
+
' const doubled = ["a", "bb"].map((v) => v + v);',
|
|
47
|
+
' world.setDynamicProperty("optipack:doubled", doubled.join(","));',
|
|
48
|
+
'}',
|
|
49
|
+
'',
|
|
50
|
+
'system.runInterval(onTick, 20);',
|
|
51
|
+
].join('\n');
|
|
52
|
+
|
|
53
|
+
const scenario = {
|
|
54
|
+
entry: 'scripts/main.js',
|
|
55
|
+
reads: PLAYER_READS,
|
|
56
|
+
state: TWO_PLAYERS,
|
|
57
|
+
steps: [{ op: 'tick', n: 40 }],
|
|
58
|
+
};
|
|
59
|
+
await differentialCheck({ 'scripts/main.js': main }, { level: 'balanced' }, scenario, 'hot-tick-loop');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('differential: event-driven worldLoad -> runInterval -> playerSpawn', async () => {
|
|
63
|
+
const main = [
|
|
64
|
+
"import { world, system } from '@minecraft/server';",
|
|
65
|
+
"import { makeGreeting } from './util.js';",
|
|
66
|
+
'',
|
|
67
|
+
'world.afterEvents.worldLoad.subscribe(() => {',
|
|
68
|
+
" world.sendMessage('loaded');",
|
|
69
|
+
' system.runInterval(onTick, 10);',
|
|
70
|
+
' world.afterEvents.playerSpawn.subscribe(onSpawn);',
|
|
71
|
+
'});',
|
|
72
|
+
'',
|
|
73
|
+
'function onTick() {',
|
|
74
|
+
' const players = world.getAllPlayers();',
|
|
75
|
+
' for (const p of players) {',
|
|
76
|
+
" p.setDynamicProperty('seen', true);",
|
|
77
|
+
' }',
|
|
78
|
+
'}',
|
|
79
|
+
'',
|
|
80
|
+
'function onSpawn(ev) {',
|
|
81
|
+
' const p = ev.player;',
|
|
82
|
+
' if (!p) return;',
|
|
83
|
+
' world.sendMessage(makeGreeting(p.name));',
|
|
84
|
+
" p.setDynamicProperty('spawned', 1);",
|
|
85
|
+
'}',
|
|
86
|
+
].join('\n');
|
|
87
|
+
|
|
88
|
+
const util = [
|
|
89
|
+
"const PREFIX = 'hello ';",
|
|
90
|
+
'export function makeGreeting(name) {',
|
|
91
|
+
' return PREFIX + name;',
|
|
92
|
+
'}',
|
|
93
|
+
].join('\n');
|
|
94
|
+
|
|
95
|
+
const scenario = {
|
|
96
|
+
entry: 'scripts/main.js',
|
|
97
|
+
reads: PLAYER_READS,
|
|
98
|
+
state: TWO_PLAYERS,
|
|
99
|
+
steps: [
|
|
100
|
+
{ op: 'fire', event: 'world.afterEvents.worldLoad', payload: {} },
|
|
101
|
+
{ op: 'tick', n: 25 },
|
|
102
|
+
{ op: 'fire', event: 'world.afterEvents.playerSpawn', payload: { player: { __entity: 'p1' } } },
|
|
103
|
+
{ op: 'tick', n: 5 },
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
await differentialCheck({ 'scripts/main.js': main, 'scripts/util.js': util }, { level: 'balanced' }, scenario, 'event-driven');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('differential: runJob generator with yield barriers', async () => {
|
|
110
|
+
const main = [
|
|
111
|
+
"import { world, system } from '@minecraft/server';",
|
|
112
|
+
'',
|
|
113
|
+
'function* chunkScan() {',
|
|
114
|
+
' for (let cx = 0; cx < 3; cx++) {',
|
|
115
|
+
" const before = world.getDynamicProperty('scan:' + cx);",
|
|
116
|
+
' yield;',
|
|
117
|
+
" world.setDynamicProperty('scan:' + cx, (before ?? 0) + 1);",
|
|
118
|
+
' }',
|
|
119
|
+
' yield;',
|
|
120
|
+
" world.sendMessage('scan done');",
|
|
121
|
+
'}',
|
|
122
|
+
'',
|
|
123
|
+
'world.afterEvents.worldLoad.subscribe(() => {',
|
|
124
|
+
' system.runJob(chunkScan());',
|
|
125
|
+
'});',
|
|
126
|
+
].join('\n');
|
|
127
|
+
|
|
128
|
+
const scenario = {
|
|
129
|
+
entry: 'scripts/main.js',
|
|
130
|
+
reads: { 'world.dp:scan:0': [0], 'world.dp:scan:1': [10], 'world.dp:scan:2': [20] },
|
|
131
|
+
state: {},
|
|
132
|
+
steps: [
|
|
133
|
+
{ op: 'fire', event: 'world.afterEvents.worldLoad', payload: {} },
|
|
134
|
+
{ op: 'tick', n: 10 },
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
await differentialCheck({ 'scripts/main.js': main }, { level: 'balanced' }, scenario, 'run-job');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('differential: risky cross-tick cache stays effects-equivalent', async () => {
|
|
141
|
+
const main = [
|
|
142
|
+
"import { world, system } from '@minecraft/server';",
|
|
143
|
+
'',
|
|
144
|
+
'function onTick(player) {',
|
|
145
|
+
' const health = player.getComponent("minecraft:health");',
|
|
146
|
+
' if (health) {',
|
|
147
|
+
' player.setDynamicProperty("hp", health.currentValue);',
|
|
148
|
+
' }',
|
|
149
|
+
' player.setDynamicProperty("id", player.id);',
|
|
150
|
+
'}',
|
|
151
|
+
'',
|
|
152
|
+
'system.runInterval(() => onTick(world.getAllPlayers()[0]), 1);',
|
|
153
|
+
].join('\n');
|
|
154
|
+
|
|
155
|
+
const scenario = {
|
|
156
|
+
entry: 'scripts/main.js',
|
|
157
|
+
reads: {
|
|
158
|
+
// Stable component value: R1's documented precondition (stale reads are
|
|
159
|
+
// only observable when the underlying component changes).
|
|
160
|
+
'component:p1:minecraft:health': [{ currentValue: 20 }],
|
|
161
|
+
'name:p1': ['Alice'],
|
|
162
|
+
},
|
|
163
|
+
state: { players: [{ id: 'p1' }] },
|
|
164
|
+
steps: [{ op: 'tick', n: 6 }],
|
|
165
|
+
};
|
|
166
|
+
await differentialCheck({ 'scripts/main.js': main }, { level: 'risky' }, scenario, 'cross-tick-cache');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('differential: string building over many ticks (B3 O(n^2) fix)', async () => {
|
|
170
|
+
const main = [
|
|
171
|
+
"import { world, system } from '@minecraft/server';",
|
|
172
|
+
'',
|
|
173
|
+
'function onTick() {',
|
|
174
|
+
' const players = world.getAllPlayers();',
|
|
175
|
+
' let scoreboard = "";',
|
|
176
|
+
' for (let i = 0; i < players.length; i++) {',
|
|
177
|
+
' scoreboard += players[i].name + ";";',
|
|
178
|
+
' }',
|
|
179
|
+
' world.setDynamicProperty("scoreboard", scoreboard);',
|
|
180
|
+
'}',
|
|
181
|
+
'',
|
|
182
|
+
'system.runInterval(onTick, 1);',
|
|
183
|
+
].join('\n');
|
|
184
|
+
|
|
185
|
+
const scenario = {
|
|
186
|
+
entry: 'scripts/main.js',
|
|
187
|
+
reads: PLAYER_READS,
|
|
188
|
+
state: TWO_PLAYERS,
|
|
189
|
+
steps: [{ op: 'tick', n: 12 }],
|
|
190
|
+
};
|
|
191
|
+
const { a, b } = await differentialCheck({ 'scripts/main.js': main }, { level: 'balanced' }, scenario, 'string-build');
|
|
192
|
+
// The optimized effect list must contain exactly the same number of
|
|
193
|
+
// setDynamicProperty effects with the same joined values.
|
|
194
|
+
assert.equal(a.effects.length, b.effects.length);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('differential: guards and optional chains (B5) across ticks', async () => {
|
|
198
|
+
const main = [
|
|
199
|
+
"import { world, system } from '@minecraft/server';",
|
|
200
|
+
'',
|
|
201
|
+
'function onTick(ev) {',
|
|
202
|
+
' const p = ev.player;',
|
|
203
|
+
' if (!p) return;',
|
|
204
|
+
" world.sendMessage('n=' + p?.name);",
|
|
205
|
+
" p.setDynamicProperty('x', p?.location.x);",
|
|
206
|
+
'}',
|
|
207
|
+
'',
|
|
208
|
+
'world.afterEvents.playerJoin.subscribe((ev) => onTick(ev));',
|
|
209
|
+
'system.runInterval(() => onTick({ player: null }), 20);',
|
|
210
|
+
].join('\n');
|
|
211
|
+
|
|
212
|
+
const scenario = {
|
|
213
|
+
entry: 'scripts/main.js',
|
|
214
|
+
reads: PLAYER_READS,
|
|
215
|
+
state: TWO_PLAYERS,
|
|
216
|
+
steps: [
|
|
217
|
+
{ op: 'fire', event: 'world.afterEvents.playerJoin', payload: { player: { __entity: 'p1' } } },
|
|
218
|
+
{ op: 'tick', n: 3 },
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
await differentialCheck({ 'scripts/main.js': main }, { level: 'balanced' }, scenario, 'null-guard');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('differential: destructure-to-direct with mutation via property access', async () => {
|
|
225
|
+
const main = [
|
|
226
|
+
"import { world, system } from '@minecraft/server';",
|
|
227
|
+
'',
|
|
228
|
+
'function onTick(cfg) {',
|
|
229
|
+
' const { label, weight } = cfg;',
|
|
230
|
+
" world.sendMessage(label + ':' + weight);",
|
|
231
|
+
'}',
|
|
232
|
+
'',
|
|
233
|
+
'system.runInterval(() => onTick({ label: "cfg", weight: 3 }), 20);',
|
|
234
|
+
].join('\n');
|
|
235
|
+
|
|
236
|
+
const scenario = {
|
|
237
|
+
entry: 'scripts/main.js',
|
|
238
|
+
reads: {},
|
|
239
|
+
state: TWO_PLAYERS,
|
|
240
|
+
steps: [{ op: 'tick', n: 5 }],
|
|
241
|
+
};
|
|
242
|
+
await differentialCheck({ 'scripts/main.js': main }, { level: 'balanced' }, scenario, 'destructure');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('differential: closure lift with registrations', async () => {
|
|
246
|
+
const main = [
|
|
247
|
+
"import { world, system } from '@minecraft/server';",
|
|
248
|
+
'',
|
|
249
|
+
"const GREETING = 'hi';",
|
|
250
|
+
'',
|
|
251
|
+
'world.afterEvents.worldLoad.subscribe(() => {',
|
|
252
|
+
" const announce = () => world.sendMessage(GREETING + ':lifted');",
|
|
253
|
+
' system.runInterval(announce, 30);',
|
|
254
|
+
'});',
|
|
255
|
+
].join('\n');
|
|
256
|
+
|
|
257
|
+
const scenario = {
|
|
258
|
+
entry: 'scripts/main.js',
|
|
259
|
+
reads: {},
|
|
260
|
+
state: {},
|
|
261
|
+
steps: [
|
|
262
|
+
{ op: 'fire', event: 'world.afterEvents.worldLoad', payload: {} },
|
|
263
|
+
{ op: 'tick', n: 65 },
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
await differentialCheck({ 'scripts/main.js': main }, { level: 'balanced' }, scenario, 'closure-lift');
|
|
267
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// B4: dynamic property reads hoisted out of hot loops.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
function onTick(player) {
|
|
4
|
+
let score = 0;
|
|
5
|
+
const _opDP = player.getDynamicProperty("optipack:active");
|
|
6
|
+
for (let i = 0; i < 4; i++) {
|
|
7
|
+
if (_opDP === true) {
|
|
8
|
+
score += 1;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
world.setDynamicProperty('score', score);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Loop contains another call: bails.
|
|
15
|
+
function withOtherCall(player) {
|
|
16
|
+
let score = 0;
|
|
17
|
+
for (let i = 0; i < 4; i++) {
|
|
18
|
+
score += player.getDynamicProperty('optipack:n');
|
|
19
|
+
world.sendMessage('x');
|
|
20
|
+
}
|
|
21
|
+
world.setDynamicProperty('score2', score);
|
|
22
|
+
}
|
|
23
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
24
|
+
system.runInterval(() => withOtherCall(world.getAllPlayers()[0]), 20);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// B4: dynamic property reads hoisted out of hot loops.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
function onTick(player) {
|
|
5
|
+
let score = 0;
|
|
6
|
+
for (let i = 0; i < 4; i++) {
|
|
7
|
+
if (player.getDynamicProperty('optipack:active') === true) {
|
|
8
|
+
score += 1;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
world.setDynamicProperty('score', score);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Loop contains another call: bails.
|
|
15
|
+
function withOtherCall(player) {
|
|
16
|
+
let score = 0;
|
|
17
|
+
for (let i = 0; i < 4; i++) {
|
|
18
|
+
score += player.getDynamicProperty('optipack:n');
|
|
19
|
+
world.sendMessage('x');
|
|
20
|
+
}
|
|
21
|
+
world.setDynamicProperty('score2', score);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
25
|
+
system.runInterval(() => withOtherCall(world.getAllPlayers()[0]), 20);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reads": {
|
|
3
|
+
"dp:p1:optipack:active": [
|
|
4
|
+
true
|
|
5
|
+
]
|
|
6
|
+
},
|
|
7
|
+
"entry": "scripts/input.js",
|
|
8
|
+
"steps": [
|
|
9
|
+
{
|
|
10
|
+
"n": 5,
|
|
11
|
+
"op": "tick"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"state": {
|
|
15
|
+
"players": [
|
|
16
|
+
{
|
|
17
|
+
"id": "p1"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "p2"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"level": "balanced",
|
|
3
|
+
"files": [
|
|
4
|
+
{
|
|
5
|
+
"path": "scripts/drops.js",
|
|
6
|
+
"hotFunctions": 0,
|
|
7
|
+
"transforms": {},
|
|
8
|
+
"bails": [],
|
|
9
|
+
"lint": [],
|
|
10
|
+
"residualAllocs": {
|
|
11
|
+
"closures": 0,
|
|
12
|
+
"arrays": 0,
|
|
13
|
+
"objects": 0
|
|
14
|
+
},
|
|
15
|
+
"bytes": {
|
|
16
|
+
"before": 710,
|
|
17
|
+
"after": 709
|
|
18
|
+
},
|
|
19
|
+
"warnings": 0
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "scripts/main.js",
|
|
23
|
+
"hotFunctions": 4,
|
|
24
|
+
"transforms": {
|
|
25
|
+
"for-of-to-indexed": 1,
|
|
26
|
+
"array-methods-to-loops": 1,
|
|
27
|
+
"hoist-invariant-members": 1,
|
|
28
|
+
"cache-getter-chains": 1,
|
|
29
|
+
"string-build": 1
|
|
30
|
+
},
|
|
31
|
+
"bails": [
|
|
32
|
+
{
|
|
33
|
+
"line": 18,
|
|
34
|
+
"transform": "S4",
|
|
35
|
+
"reason": "iterable not provably an array"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"line": 18,
|
|
39
|
+
"transform": "S6",
|
|
40
|
+
"reason": "declaration is a loop head"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"line": 48,
|
|
44
|
+
"transform": "S7",
|
|
45
|
+
"reason": "loop body contains calls (mutation barrier)"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"line": 20,
|
|
49
|
+
"transform": "B1",
|
|
50
|
+
"reason": "chain .location.y repeats but uses are split by call/assignment barriers"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"line": 48,
|
|
54
|
+
"transform": "B4",
|
|
55
|
+
"reason": "loop contains await/yield"
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"lint": [
|
|
59
|
+
{
|
|
60
|
+
"line": 16,
|
|
61
|
+
"rule": "per-tick-getAllPlayers",
|
|
62
|
+
"message": "getAllPlayers() runs unconditionally inside a per-tick callback — prefer event-driven updates or caching between ticks"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"line": 49,
|
|
66
|
+
"rule": "dynprop-in-loop",
|
|
67
|
+
"message": "getDynamicProperty inside a tick loop — reads cross the host boundary every iteration"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"residualAllocs": {
|
|
71
|
+
"closures": 0,
|
|
72
|
+
"arrays": 1,
|
|
73
|
+
"objects": 0
|
|
74
|
+
},
|
|
75
|
+
"bytes": {
|
|
76
|
+
"before": 2217,
|
|
77
|
+
"after": 2523
|
|
78
|
+
},
|
|
79
|
+
"warnings": 0
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"path": "scripts/util.js",
|
|
83
|
+
"hotFunctions": 0,
|
|
84
|
+
"transforms": {
|
|
85
|
+
"bool-simplify": 1
|
|
86
|
+
},
|
|
87
|
+
"bails": [],
|
|
88
|
+
"lint": [],
|
|
89
|
+
"residualAllocs": {
|
|
90
|
+
"closures": 0,
|
|
91
|
+
"arrays": 0,
|
|
92
|
+
"objects": 0
|
|
93
|
+
},
|
|
94
|
+
"bytes": {
|
|
95
|
+
"before": 820,
|
|
96
|
+
"after": 807
|
|
97
|
+
},
|
|
98
|
+
"warnings": 0
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"warnings": [],
|
|
102
|
+
"totals": {
|
|
103
|
+
"transforms": 6,
|
|
104
|
+
"bails": 5,
|
|
105
|
+
"warnings": 0
|
|
106
|
+
}
|
|
107
|
+
}
|