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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -2
  3. package/bin/optipack.js +12 -0
  4. package/example/manifest.json +27 -0
  5. package/example/scripts/drops.js +24 -0
  6. package/example/scripts/main.js +67 -0
  7. package/example/scripts/util.js +30 -0
  8. package/package.json +34 -6
  9. package/src/cli.js +180 -0
  10. package/src/config.js +141 -0
  11. package/src/diff.js +108 -0
  12. package/src/hotpath.js +158 -0
  13. package/src/lint.js +150 -0
  14. package/src/pack-walker.js +50 -0
  15. package/src/pass-utils.js +419 -0
  16. package/src/passes/b1-cache-getter-chains.js +406 -0
  17. package/src/passes/b2-closure-lift.js +316 -0
  18. package/src/passes/b3-string-build.js +167 -0
  19. package/src/passes/b4-dynprop-hoist.js +177 -0
  20. package/src/passes/b5-null-chain-flatten.js +137 -0
  21. package/src/passes/index.js +43 -0
  22. package/src/passes/r1-cross-tick-cache.js +192 -0
  23. package/src/passes/r2-json-fold.js +201 -0
  24. package/src/passes/r3-try-catch-strip.js +61 -0
  25. package/src/passes/r4-getter-cache-cross-barrier.js +29 -0
  26. package/src/passes/s1-const-fold.js +130 -0
  27. package/src/passes/s2-dead-code.js +183 -0
  28. package/src/passes/s3-bool-simplify.js +82 -0
  29. package/src/passes/s4-for-of-to-indexed.js +140 -0
  30. package/src/passes/s5-array-methods-to-loops.js +357 -0
  31. package/src/passes/s6-destructure-to-direct.js +186 -0
  32. package/src/passes/s7-hoist-invariant-members.js +327 -0
  33. package/src/pipeline.js +186 -0
  34. package/src/pragma.js +115 -0
  35. package/src/report.js +98 -0
  36. package/test/array-methods-to-loops/expected.js +58 -0
  37. package/test/array-methods-to-loops/expects.json +6 -0
  38. package/test/array-methods-to-loops/input.js +23 -0
  39. package/test/array-methods-to-loops/scenario.json +22 -0
  40. package/test/array-methods-to-loops-bail/expected.js +10 -0
  41. package/test/array-methods-to-loops-bail/input.js +24 -0
  42. package/test/bool-simplify/expected.js +14 -0
  43. package/test/bool-simplify/expects.json +11 -0
  44. package/test/bool-simplify/input.js +18 -0
  45. package/test/cache-getter-chains/expected.js +20 -0
  46. package/test/cache-getter-chains/expects.json +11 -0
  47. package/test/cache-getter-chains/input.js +20 -0
  48. package/test/cache-getter-chains/scenario.json +35 -0
  49. package/test/closure-lift/expected.js +58 -0
  50. package/test/closure-lift/expects.json +1 -0
  51. package/test/closure-lift/input.js +50 -0
  52. package/test/closure-lift/scenario.json +9 -0
  53. package/test/const-fold/expected.js +12 -0
  54. package/test/const-fold/expects.json +1 -0
  55. package/test/const-fold/input.js +12 -0
  56. package/test/const-fold-bail/expected.js +7 -0
  57. package/test/const-fold-bail/expects.json +3 -0
  58. package/test/const-fold-bail/input.js +7 -0
  59. package/test/cross-tick-cache/config.json +1 -0
  60. package/test/cross-tick-cache/expected.js +38 -0
  61. package/test/cross-tick-cache/expects.json +6 -0
  62. package/test/cross-tick-cache/input.js +13 -0
  63. package/test/cross-tick-cache/scenario.json +33 -0
  64. package/test/dead-code/expected.js +14 -0
  65. package/test/dead-code/expects.json +5 -0
  66. package/test/dead-code/input.js +23 -0
  67. package/test/destructure-to-direct/expected.js +37 -0
  68. package/test/destructure-to-direct/expects.json +11 -0
  69. package/test/destructure-to-direct/input.js +28 -0
  70. package/test/destructure-to-direct/scenario.json +22 -0
  71. package/test/destructure-to-direct-bail/expected.js +36 -0
  72. package/test/destructure-to-direct-bail/expects.json +11 -0
  73. package/test/destructure-to-direct-bail/input.js +25 -0
  74. package/test/differential.test.mjs +267 -0
  75. package/test/dynprop-hoist/expected.js +24 -0
  76. package/test/dynprop-hoist/expects.json +11 -0
  77. package/test/dynprop-hoist/input.js +25 -0
  78. package/test/dynprop-hoist/scenario.json +24 -0
  79. package/test/example-report-snapshot.json +107 -0
  80. package/test/example.test.mjs +115 -0
  81. package/test/for-of-to-indexed/expected.js +59 -0
  82. package/test/for-of-to-indexed/expects.json +11 -0
  83. package/test/for-of-to-indexed/input.js +43 -0
  84. package/test/for-of-to-indexed-bail/expected.js +25 -0
  85. package/test/for-of-to-indexed-bail/expects.json +11 -0
  86. package/test/for-of-to-indexed-bail/input.js +29 -0
  87. package/test/gen-expected.mjs +33 -0
  88. package/test/getter-cache-cross-barrier/config.json +1 -0
  89. package/test/getter-cache-cross-barrier/expected.js +10 -0
  90. package/test/getter-cache-cross-barrier/expects.json +6 -0
  91. package/test/getter-cache-cross-barrier/input.js +11 -0
  92. package/test/getter-cache-cross-barrier/scenario.json +28 -0
  93. package/test/harness/fixture.js +117 -0
  94. package/test/harness/setup.js +198 -0
  95. package/test/hoist-invariant-members/expected.js +37 -0
  96. package/test/hoist-invariant-members/expects.json +11 -0
  97. package/test/hoist-invariant-members/input.js +34 -0
  98. package/test/hoist-invariant-members/scenario.json +35 -0
  99. package/test/hoist-invariant-members-bail/expected.js +19 -0
  100. package/test/hoist-invariant-members-bail/expects.json +5 -0
  101. package/test/hoist-invariant-members-bail/input.js +22 -0
  102. package/test/infra.test.mjs +260 -0
  103. package/test/json-fold/config.json +1 -0
  104. package/test/json-fold/expected.js +14 -0
  105. package/test/json-fold/expects.json +12 -0
  106. package/test/json-fold/input.js +17 -0
  107. package/test/json-fold-bail/config.json +1 -0
  108. package/test/json-fold-bail/expected.js +10 -0
  109. package/test/json-fold-bail/expects.json +5 -0
  110. package/test/json-fold-bail/input.js +14 -0
  111. package/test/loader-hooks.mjs +38 -0
  112. package/test/mock/@minecraft/server.mjs +274 -0
  113. package/test/null-chain-flatten/expected.js +22 -0
  114. package/test/null-chain-flatten/expects.json +5 -0
  115. package/test/null-chain-flatten/input.js +22 -0
  116. package/test/null-chain-flatten/scenario.json +24 -0
  117. package/test/string-build/expected.js +29 -0
  118. package/test/string-build/expects.json +5 -0
  119. package/test/string-build/input.js +29 -0
  120. package/test/string-build/scenario.json +27 -0
  121. package/test/string-build-bail/expected.js +10 -0
  122. package/test/string-build-bail/input.js +21 -0
  123. package/test/transforms.test.mjs +43 -0
  124. package/test/try-catch-strip/config.json +1 -0
  125. package/test/try-catch-strip/expected.js +57 -0
  126. package/test/try-catch-strip/expects.json +1 -0
  127. package/test/try-catch-strip/input.js +34 -0
  128. package/test/try-catch-strip/scenario.json +28 -0
  129. package/test/update-example-snapshot.mjs +19 -0
@@ -0,0 +1,14 @@
1
+ // S2: dead code shapes.
2
+ export function f(x) {
3
+ return x;
4
+ }
5
+ export function g() {
6
+ return 1;
7
+ }
8
+ export function w() {
9
+ return 'after';
10
+ }
11
+ const kept = 3;
12
+ export function h() {
13
+ return kept;
14
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "transforms": {
3
+ "dead-code": 4
4
+ }
5
+ }
@@ -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,11 @@
1
+ {
2
+ "transforms": {
3
+ "destructure-to-direct": 2
4
+ },
5
+ "bails": [
6
+ {
7
+ "transform": "S6",
8
+ "reasonContains": "total uses"
9
+ }
10
+ ]
11
+ }
@@ -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,22 @@
1
+ {
2
+ "reads": {
3
+
4
+ },
5
+ "entry": "scripts/input.js",
6
+ "steps": [
7
+ {
8
+ "n": 5,
9
+ "op": "tick"
10
+ }
11
+ ],
12
+ "state": {
13
+ "players": [
14
+ {
15
+ "id": "p1"
16
+ },
17
+ {
18
+ "id": "p2"
19
+ }
20
+ ]
21
+ }
22
+ }
@@ -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,11 @@
1
+ {
2
+ "transforms": {
3
+ "destructure-to-direct": 0
4
+ },
5
+ "bails": [
6
+ {
7
+ "transform": "S6",
8
+ "reasonContains": "nested pattern"
9
+ }
10
+ ]
11
+ }
@@ -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,11 @@
1
+ {
2
+ "transforms": {
3
+ "dynprop-hoist": 1
4
+ },
5
+ "bails": [
6
+ {
7
+ "transform": "B4",
8
+ "reasonContains": "calls other than"
9
+ }
10
+ ]
11
+ }
@@ -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
+ }