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 @@
|
|
|
1
|
+
{"level": "risky"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// R2 bail: JSON.parse result is mutated and passed around.
|
|
2
|
+
import { world } from '@minecraft/server';
|
|
3
|
+
const CFG = JSON.parse('{"n":1}');
|
|
4
|
+
export function passAround() {
|
|
5
|
+
return handle(CFG);
|
|
6
|
+
}
|
|
7
|
+
function handle(c) {
|
|
8
|
+
return c.n;
|
|
9
|
+
}
|
|
10
|
+
world.sendMessage(passAround());
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// R2 bail: JSON.parse result is mutated and passed around.
|
|
2
|
+
import { world } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
const CFG = JSON.parse('{"n":1}');
|
|
5
|
+
|
|
6
|
+
export function passAround() {
|
|
7
|
+
return handle(CFG);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function handle(c) {
|
|
11
|
+
return c.n;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
world.sendMessage(passAround());
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node module loader hooks for differential testing.
|
|
3
|
+
*
|
|
4
|
+
* - `@minecraft/server` resolves to the mock module (single instance; the
|
|
5
|
+
* recorder is passed via globalThis.__opkRecorder).
|
|
6
|
+
* - Pack files are loaded with a `?opkv=<variant>` query so the original and
|
|
7
|
+
* optimized variants get separate ESM cache entries; the query is stripped
|
|
8
|
+
* when reading from disk and propagated to relative imports.
|
|
9
|
+
*/
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const MOCK_URL = pathToFileURL(
|
|
14
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), 'mock', '@minecraft', 'server.mjs')
|
|
15
|
+
).href;
|
|
16
|
+
|
|
17
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
18
|
+
if (specifier === '@minecraft/server') {
|
|
19
|
+
return { url: MOCK_URL, shortCircuit: true };
|
|
20
|
+
}
|
|
21
|
+
const res = await nextResolve(specifier, context);
|
|
22
|
+
const parentURL = context.parentURL || '';
|
|
23
|
+
if (parentURL.includes('opkv=') && res.url.startsWith('file:') && !res.url.includes('opkv=')) {
|
|
24
|
+
const q = new URL(parentURL).search;
|
|
25
|
+
return { ...res, url: res.url + q, shortCircuit: true };
|
|
26
|
+
}
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function load(url, context, nextLoad) {
|
|
31
|
+
const u = new URL(url);
|
|
32
|
+
if (u.protocol === 'file:' && u.search) {
|
|
33
|
+
const clean = pathToFileURL(fileURLToPath(u)).href;
|
|
34
|
+
const r = await nextLoad(clean, context);
|
|
35
|
+
return { format: r.format || 'module', source: r.source, shortCircuit: true };
|
|
36
|
+
}
|
|
37
|
+
return nextLoad(url, context);
|
|
38
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock @minecraft/server for differential testing.
|
|
3
|
+
*
|
|
4
|
+
* Effect-recorder contract: only MUTATING calls and registrations are recorded
|
|
5
|
+
* as effects. Pure reads return scripted sequences and are intentionally not
|
|
6
|
+
* recorded — transforms may legitimately eliminate reads, that is the point.
|
|
7
|
+
*
|
|
8
|
+
* The active recorder is read from globalThis.__opkRecorder at call time, so a
|
|
9
|
+
* single mock module instance serves both the original and the optimized
|
|
10
|
+
* variant of a script.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const rec = () => globalThis.__opkRecorder;
|
|
14
|
+
|
|
15
|
+
function scriptedRead(key, fallback) {
|
|
16
|
+
const r = rec();
|
|
17
|
+
if (!r) return fallback;
|
|
18
|
+
return r.scriptedRead(key, fallback);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function effect(call, args) {
|
|
22
|
+
const r = rec();
|
|
23
|
+
if (r) r.effect(call, args === undefined ? null : args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function serialize(v) {
|
|
27
|
+
if (v === null) return null;
|
|
28
|
+
const t = typeof v;
|
|
29
|
+
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'undefined') return v;
|
|
30
|
+
if (v instanceof Entity) return { __entity: v.id };
|
|
31
|
+
if (v instanceof Dimension) return { __dimension: v.id };
|
|
32
|
+
if (Array.isArray(v)) return v.map(serialize);
|
|
33
|
+
if (t === 'object') {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const k of Object.keys(v)) out[k] = serialize(v[k]);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
return String(v);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class Dimension {
|
|
42
|
+
constructor(id) {
|
|
43
|
+
this.id = id;
|
|
44
|
+
}
|
|
45
|
+
getEntities(options) {
|
|
46
|
+
effect('dimension.getEntities', { dimension: this.id, options: serialize(options) });
|
|
47
|
+
return scriptedRead(`entities:${this.id}`, []);
|
|
48
|
+
}
|
|
49
|
+
getPlayers() {
|
|
50
|
+
effect('dimension.getPlayers', { dimension: this.id });
|
|
51
|
+
return scriptedRead(`players:${this.id}`, []);
|
|
52
|
+
}
|
|
53
|
+
spawnEntity(type, location) {
|
|
54
|
+
effect('dimension.spawnEntity', { dimension: this.id, type, location: serialize(location) });
|
|
55
|
+
return scriptedRead(`spawn:${this.id}:${type}`, null);
|
|
56
|
+
}
|
|
57
|
+
spawnParticle(effectName, location, molangVariables) {
|
|
58
|
+
effect('dimension.spawnParticle', { dimension: this.id, effectName, location: serialize(location), molang: serialize(molangVariables) });
|
|
59
|
+
}
|
|
60
|
+
runCommand(commandString) {
|
|
61
|
+
effect('dimension.runCommand', { dimension: this.id, command: commandString });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class Entity {
|
|
66
|
+
constructor(id, typeId, dimension) {
|
|
67
|
+
this._id = id;
|
|
68
|
+
this.typeId = typeId;
|
|
69
|
+
this._dimension = dimension;
|
|
70
|
+
}
|
|
71
|
+
get id() {
|
|
72
|
+
return this._id;
|
|
73
|
+
}
|
|
74
|
+
get name() {
|
|
75
|
+
return scriptedRead(`name:${this._id}`, 'player' + this._id);
|
|
76
|
+
}
|
|
77
|
+
get location() {
|
|
78
|
+
return scriptedRead(`location:${this._id}`, { x: 0, y: 0, z: 0 });
|
|
79
|
+
}
|
|
80
|
+
get dimension() {
|
|
81
|
+
return this._dimension;
|
|
82
|
+
}
|
|
83
|
+
getComponent(componentId) {
|
|
84
|
+
return scriptedRead(`component:${this._id}:${componentId}`, undefined);
|
|
85
|
+
}
|
|
86
|
+
getComponents() {
|
|
87
|
+
effect('entity.getComponents', { entity: this._id });
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
getDynamicProperty(identifier) {
|
|
91
|
+
return scriptedRead(`dp:${this._id}:${identifier}`, undefined);
|
|
92
|
+
}
|
|
93
|
+
setDynamicProperty(identifier, value) {
|
|
94
|
+
effect('entity.setDynamicProperty', { entity: this._id, key: identifier, value: serialize(value) });
|
|
95
|
+
}
|
|
96
|
+
sendMessage(message) {
|
|
97
|
+
effect('entity.sendMessage', { entity: this._id, message: serialize(message) });
|
|
98
|
+
}
|
|
99
|
+
runCommand(commandString) {
|
|
100
|
+
effect('entity.runCommand', { entity: this._id, command: commandString });
|
|
101
|
+
}
|
|
102
|
+
teleport(location, teleportOptions) {
|
|
103
|
+
effect('entity.teleport', { entity: this._id, location: serialize(location), options: serialize(teleportOptions) });
|
|
104
|
+
}
|
|
105
|
+
applyDamage(amount, options) {
|
|
106
|
+
effect('entity.applyDamage', { entity: this._id, amount, options: serialize(options) });
|
|
107
|
+
return scriptedRead(`damage:${this._id}`, true);
|
|
108
|
+
}
|
|
109
|
+
kill() {
|
|
110
|
+
effect('entity.kill', { entity: this._id });
|
|
111
|
+
}
|
|
112
|
+
addTag(tag) {
|
|
113
|
+
effect('entity.addTag', { entity: this._id, tag });
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
removeTag(tag) {
|
|
117
|
+
effect('entity.removeTag', { entity: this._id, tag });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
hasTag(tag) {
|
|
121
|
+
return scriptedRead(`tag:${this._id}:${tag}`, false);
|
|
122
|
+
}
|
|
123
|
+
playAnimation(animationName, options) {
|
|
124
|
+
effect('entity.playAnimation', { entity: this._id, animationName, options: serialize(options) });
|
|
125
|
+
}
|
|
126
|
+
triggerEvent(eventName) {
|
|
127
|
+
effect('entity.triggerEvent', { entity: this._id, eventName });
|
|
128
|
+
}
|
|
129
|
+
getRotation() {
|
|
130
|
+
return scriptedRead(`rotation:${this._id}`, { x: 0, y: 0 });
|
|
131
|
+
}
|
|
132
|
+
getVelocity() {
|
|
133
|
+
return scriptedRead(`velocity:${this._id}`, { x: 0, y: 0, z: 0 });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function makeEvent(fullName) {
|
|
138
|
+
return {
|
|
139
|
+
subscribe(callback, options) {
|
|
140
|
+
effect('subscribe', { event: fullName, options: serialize(options) });
|
|
141
|
+
const r = rec();
|
|
142
|
+
if (r) r.addHandler(fullName, callback);
|
|
143
|
+
},
|
|
144
|
+
unsubscribe(callback) {
|
|
145
|
+
effect('unsubscribe', { event: fullName });
|
|
146
|
+
const r = rec();
|
|
147
|
+
if (r) r.removeHandler(fullName, callback);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const worldAfterEvents = {
|
|
153
|
+
worldLoad: makeEvent('world.afterEvents.worldLoad'),
|
|
154
|
+
worldInitialize: makeEvent('world.afterEvents.worldInitialize'),
|
|
155
|
+
playerSpawn: makeEvent('world.afterEvents.playerSpawn'),
|
|
156
|
+
playerJoin: makeEvent('world.afterEvents.playerJoin'),
|
|
157
|
+
playerLeave: makeEvent('world.afterEvents.playerLeave'),
|
|
158
|
+
entityDie: makeEvent('world.afterEvents.entityDie'),
|
|
159
|
+
entityHurt: makeEvent('world.afterEvents.entityHurt'),
|
|
160
|
+
itemUse: makeEvent('world.afterEvents.itemUse'),
|
|
161
|
+
chatSend: makeEvent('world.afterEvents.chatSend'),
|
|
162
|
+
};
|
|
163
|
+
const worldBeforeEvents = {
|
|
164
|
+
worldLoad: makeEvent('world.beforeEvents.worldLoad'),
|
|
165
|
+
playerInteractWithBlock: makeEvent('world.beforeEvents.playerInteractWithBlock'),
|
|
166
|
+
playerInteractWithEntity: makeEvent('world.beforeEvents.playerInteractWithEntity'),
|
|
167
|
+
itemUse: makeEvent('world.beforeEvents.itemUse'),
|
|
168
|
+
chatSend: makeEvent('world.beforeEvents.chatSend'),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function currentPlayers() {
|
|
172
|
+
const r = rec();
|
|
173
|
+
if (!r) return [];
|
|
174
|
+
ensurePlayers(r);
|
|
175
|
+
return r._players;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ensurePlayers(r) {
|
|
179
|
+
if (r._players) return r._players;
|
|
180
|
+
const specs = (r.state && r.state.players) || [];
|
|
181
|
+
r._players = specs.map((s) => new Entity(s.id, s.typeId || 'minecraft:player', new Dimension(s.dimension || 'minecraft:overworld')));
|
|
182
|
+
return r._players;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The differential driver hydrates event payloads via this hook.
|
|
186
|
+
if (typeof globalThis !== 'undefined') {
|
|
187
|
+
globalThis.__opkEnsurePlayers = ensurePlayers;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const world = {
|
|
191
|
+
get afterEvents() {
|
|
192
|
+
return worldAfterEvents;
|
|
193
|
+
},
|
|
194
|
+
get beforeEvents() {
|
|
195
|
+
return worldBeforeEvents;
|
|
196
|
+
},
|
|
197
|
+
getAllPlayers() {
|
|
198
|
+
effect('world.getAllPlayers', null);
|
|
199
|
+
return currentPlayers();
|
|
200
|
+
},
|
|
201
|
+
getPlayers() {
|
|
202
|
+
effect('world.getPlayers', null);
|
|
203
|
+
return currentPlayers();
|
|
204
|
+
},
|
|
205
|
+
getDynamicProperty(identifier) {
|
|
206
|
+
return scriptedRead(`world.dp:${identifier}`, undefined);
|
|
207
|
+
},
|
|
208
|
+
setDynamicProperty(identifier, value) {
|
|
209
|
+
effect('world.setDynamicProperty', { key: identifier, value: serialize(value) });
|
|
210
|
+
},
|
|
211
|
+
sendMessage(message) {
|
|
212
|
+
effect('world.sendMessage', { message: serialize(message) });
|
|
213
|
+
},
|
|
214
|
+
getEntity(identifier) {
|
|
215
|
+
effect('world.getEntity', { id: identifier });
|
|
216
|
+
return scriptedRead(`world.entity:${identifier}`, undefined);
|
|
217
|
+
},
|
|
218
|
+
getDimension(dimensionId) {
|
|
219
|
+
effect('world.getDimension', { id: dimensionId });
|
|
220
|
+
return new Dimension(dimensionId);
|
|
221
|
+
},
|
|
222
|
+
playSound(soundId, options) {
|
|
223
|
+
effect('world.playSound', { soundId, options: serialize(options) });
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const systemBeforeEvents = {
|
|
228
|
+
tick: makeEvent('system.beforeEvents.tick'),
|
|
229
|
+
watchdogTerminate: makeEvent('system.beforeEvents.watchdogTerminate'),
|
|
230
|
+
};
|
|
231
|
+
const systemAfterEvents = {
|
|
232
|
+
tick: makeEvent('system.afterEvents.tick'),
|
|
233
|
+
watchdogTerminate: makeEvent('system.afterEvents.watchdogTerminate'),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const system = {
|
|
237
|
+
get beforeEvents() {
|
|
238
|
+
return systemBeforeEvents;
|
|
239
|
+
},
|
|
240
|
+
get afterEvents() {
|
|
241
|
+
return systemAfterEvents;
|
|
242
|
+
},
|
|
243
|
+
runInterval(callback, tickInterval) {
|
|
244
|
+
const interval = tickInterval === undefined ? 1 : tickInterval;
|
|
245
|
+
effect('system.runInterval', { interval });
|
|
246
|
+
const r = rec();
|
|
247
|
+
return r ? r.addInterval(callback, interval) : -1;
|
|
248
|
+
},
|
|
249
|
+
runTimeout(callback, timeoutTicks) {
|
|
250
|
+
const delay = timeoutTicks === undefined ? 1 : timeoutTicks;
|
|
251
|
+
effect('system.runTimeout', { delay });
|
|
252
|
+
const r = rec();
|
|
253
|
+
return r ? r.addTimeout(callback, delay) : -1;
|
|
254
|
+
},
|
|
255
|
+
run(callback) {
|
|
256
|
+
effect('system.run', null);
|
|
257
|
+
const r = rec();
|
|
258
|
+
return r ? r.addTimeout(callback, 1) : -1;
|
|
259
|
+
},
|
|
260
|
+
runJob(generator) {
|
|
261
|
+
effect('system.runJob', null);
|
|
262
|
+
const r = rec();
|
|
263
|
+
if (r) r.addJob(generator);
|
|
264
|
+
return { cancel() { effect('system.clearRun', { id: 'job' }); } };
|
|
265
|
+
},
|
|
266
|
+
clearRun(runId) {
|
|
267
|
+
effect('system.clearRun', { id: serialize(runId) });
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export { Entity, Dimension };
|
|
272
|
+
export const TicksPerSecond = 20;
|
|
273
|
+
export const GameMode = { survival: 'survival', creative: 'creative', adventure: 'adventure' };
|
|
274
|
+
export default { world, system, Entity, Dimension, TicksPerSecond, GameMode };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// B5: optional chains flattened after proven guards.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
function onTick(player) {
|
|
4
|
+
if (!player) return;
|
|
5
|
+
world.sendMessage('n=' + player.name);
|
|
6
|
+
const loc = player?.location;
|
|
7
|
+
world.sendMessage('y=' + (loc === undefined ? -1 : loc.y));
|
|
8
|
+
|
|
9
|
+
// Barrier (call) ends the guard window: `player?.name` below stays.
|
|
10
|
+
world.sendMessage('mid');
|
|
11
|
+
world.sendMessage('after=' + player?.name);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Guard on an undefined-strictness check.
|
|
15
|
+
function withEqNull(ev) {
|
|
16
|
+
if (ev.player == null) return;
|
|
17
|
+
world.sendMessage('eq=' + ev.player?.name);
|
|
18
|
+
}
|
|
19
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
20
|
+
system.runInterval(() => withEqNull({
|
|
21
|
+
player: null
|
|
22
|
+
}), 20);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// B5: optional chains flattened after proven guards.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
function onTick(player) {
|
|
5
|
+
if (!player) return;
|
|
6
|
+
world.sendMessage('n=' + player?.name);
|
|
7
|
+
const loc = player?.location;
|
|
8
|
+
world.sendMessage('y=' + (loc === undefined ? -1 : loc.y));
|
|
9
|
+
|
|
10
|
+
// Barrier (call) ends the guard window: `player?.name` below stays.
|
|
11
|
+
world.sendMessage('mid');
|
|
12
|
+
world.sendMessage('after=' + player?.name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Guard on an undefined-strictness check.
|
|
16
|
+
function withEqNull(ev) {
|
|
17
|
+
if (ev.player == null) return;
|
|
18
|
+
world.sendMessage('eq=' + ev.player?.name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
22
|
+
system.runInterval(() => withEqNull({ player: null }), 20);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// B3: string accumulation in a hot loop -> parts + join.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
function onTick(players) {
|
|
4
|
+
let summary = '';
|
|
5
|
+
const _opParts = [];
|
|
6
|
+
for (let i = 0; i < players.length; i++) {
|
|
7
|
+
_opParts.push("" + (players[i].name + ';'));
|
|
8
|
+
}
|
|
9
|
+
summary = _opParts.join("");
|
|
10
|
+
world.sendMessage(summary);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Written outside a loop: bails.
|
|
14
|
+
export function notALoop(players) {
|
|
15
|
+
let s = '';
|
|
16
|
+
s += 'x';
|
|
17
|
+
world.sendMessage(s);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Read inside the loop: bails.
|
|
21
|
+
export function readInside(players) {
|
|
22
|
+
let s = '';
|
|
23
|
+
for (let i = 0; i < players.length; i++) {
|
|
24
|
+
s += players[i].name;
|
|
25
|
+
world.sendMessage(s);
|
|
26
|
+
}
|
|
27
|
+
world.sendMessage(s);
|
|
28
|
+
}
|
|
29
|
+
system.runInterval(() => onTick(world.getAllPlayers()), 20);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// B3: string accumulation in a hot loop -> parts + join.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
function onTick(players) {
|
|
5
|
+
let summary = '';
|
|
6
|
+
for (let i = 0; i < players.length; i++) {
|
|
7
|
+
summary += players[i].name + ';';
|
|
8
|
+
}
|
|
9
|
+
world.sendMessage(summary);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Written outside a loop: bails.
|
|
13
|
+
export function notALoop(players) {
|
|
14
|
+
let s = '';
|
|
15
|
+
s += 'x';
|
|
16
|
+
world.sendMessage(s);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Read inside the loop: bails.
|
|
20
|
+
export function readInside(players) {
|
|
21
|
+
let s = '';
|
|
22
|
+
for (let i = 0; i < players.length; i++) {
|
|
23
|
+
s += players[i].name;
|
|
24
|
+
world.sendMessage(s);
|
|
25
|
+
}
|
|
26
|
+
world.sendMessage(s);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
system.runInterval(() => onTick(world.getAllPlayers()), 20);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reads": {
|
|
3
|
+
"name:p1": [
|
|
4
|
+
"Alice"
|
|
5
|
+
],
|
|
6
|
+
"name:p2": [
|
|
7
|
+
"Bob"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
"entry": "scripts/input.js",
|
|
11
|
+
"steps": [
|
|
12
|
+
{
|
|
13
|
+
"n": 5,
|
|
14
|
+
"op": "tick"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"state": {
|
|
18
|
+
"players": [
|
|
19
|
+
{
|
|
20
|
+
"id": "p1"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "p2"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// B3 bails: prepend pattern and accumulator read inside the loop.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
function prepend(players) {
|
|
4
|
+
let s = '';
|
|
5
|
+
for (let i = 0; i < players.length; i++) {
|
|
6
|
+
s = players[i].name + s;
|
|
7
|
+
}
|
|
8
|
+
world.sendMessage(s);
|
|
9
|
+
}
|
|
10
|
+
system.runInterval(() => prepend(world.getAllPlayers()), 20);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// B3 bails: prepend pattern and accumulator read inside the loop.
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
|
|
4
|
+
function prepend(players) {
|
|
5
|
+
let s = '';
|
|
6
|
+
for (let i = 0; i < players.length; i++) {
|
|
7
|
+
s = players[i].name + s;
|
|
8
|
+
}
|
|
9
|
+
world.sendMessage(s);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readInside(players) {
|
|
13
|
+
let s = '';
|
|
14
|
+
for (let i = 0; i < players.length; i++) {
|
|
15
|
+
s += players[i].name;
|
|
16
|
+
world.sendMessage(s.length);
|
|
17
|
+
}
|
|
18
|
+
world.sendMessage(s);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
system.runInterval(() => prepend(world.getAllPlayers()), 20);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-transform unit tests: every test/<transformId>/ fixture runs through the
|
|
3
|
+
* full pipeline, compares exact output against expected.js (or expects the
|
|
4
|
+
* input unchanged), asserts report expectations, and — when scenario.json is
|
|
5
|
+
* present — checks effects-equivalence between the original and optimized
|
|
6
|
+
* code against the mock @minecraft/server.
|
|
7
|
+
*/
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
const { runFixtureDir, fixtureDirs } = await import('./harness/fixture.js');
|
|
17
|
+
const { differentialCheck } = await import('./harness/setup.js');
|
|
18
|
+
|
|
19
|
+
const dirs = fixtureDirs(here);
|
|
20
|
+
|
|
21
|
+
for (const dir of dirs) {
|
|
22
|
+
const name = path.basename(dir);
|
|
23
|
+
test(`fixture: ${name}`, async () => {
|
|
24
|
+
const result = await runFixtureDir(dir);
|
|
25
|
+
assert.ok(result.output.length > 0);
|
|
26
|
+
// Round-trip parse is enforced inside the pipeline; double-check here.
|
|
27
|
+
const { parseSource } = await import('../src/pipeline.js');
|
|
28
|
+
parseSource(name, result.output);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(path.join(dir, 'scenario.json'))) {
|
|
32
|
+
test(`differential: ${name}`, async () => {
|
|
33
|
+
const scenario = JSON.parse(fs.readFileSync(path.join(dir, 'scenario.json'), 'utf8'));
|
|
34
|
+
const configRaw = fs.existsSync(path.join(dir, 'config.json'))
|
|
35
|
+
? JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf8').replace(/^\uFEFF/, ''))
|
|
36
|
+
: { level: 'balanced' };
|
|
37
|
+
const files = { 'scripts/input.js': fs.readFileSync(path.join(dir, 'input.js'), 'utf8') };
|
|
38
|
+
const { a, b } = await differentialCheck(files, configRaw, scenario, name);
|
|
39
|
+
assert.ok(Array.isArray(a.effects));
|
|
40
|
+
assert.ok(Array.isArray(b.effects));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"level": "risky"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// R3: try/catch stripping in hot functions (risky).
|
|
2
|
+
import { world, system } from '@minecraft/server';
|
|
3
|
+
const __opR1Cache = new WeakMap();
|
|
4
|
+
function __opR1Component(obj, key) {
|
|
5
|
+
let m = __opR1Cache.get(obj);
|
|
6
|
+
if (m === undefined) {
|
|
7
|
+
m = new Map();
|
|
8
|
+
__opR1Cache.set(obj, m);
|
|
9
|
+
}
|
|
10
|
+
let v = m.get(key);
|
|
11
|
+
if (v === undefined) {
|
|
12
|
+
v = obj.getComponent(key);
|
|
13
|
+
m.set(key, v);
|
|
14
|
+
}
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
function __opR1Field(obj, key) {
|
|
18
|
+
let m = __opR1Cache.get(obj);
|
|
19
|
+
if (m === undefined) {
|
|
20
|
+
m = new Map();
|
|
21
|
+
__opR1Cache.set(obj, m);
|
|
22
|
+
}
|
|
23
|
+
let v = m.get(key);
|
|
24
|
+
if (v === undefined) {
|
|
25
|
+
v = obj[key];
|
|
26
|
+
m.set(key, v);
|
|
27
|
+
}
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
function onTick(player) {
|
|
31
|
+
let x = 0;
|
|
32
|
+
x = player.location.y;
|
|
33
|
+
world.setDynamicProperty('x', x);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Catch with a body: bails.
|
|
37
|
+
function withCatchBody(player) {
|
|
38
|
+
let x = 0;
|
|
39
|
+
try {
|
|
40
|
+
x = player.location.y;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
world.sendMessage('err');
|
|
43
|
+
}
|
|
44
|
+
world.setDynamicProperty('x2', x);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Try body with a call: bails.
|
|
48
|
+
function withCall(player) {
|
|
49
|
+
let x = 0;
|
|
50
|
+
try {
|
|
51
|
+
x = __opR1Component(player, "minecraft:health").currentValue;
|
|
52
|
+
} catch {}
|
|
53
|
+
world.setDynamicProperty('x3', x);
|
|
54
|
+
}
|
|
55
|
+
system.runInterval(() => onTick(world.getAllPlayers()[0]), 20);
|
|
56
|
+
system.runInterval(() => withCatchBody(world.getAllPlayers()[0]), 20);
|
|
57
|
+
system.runInterval(() => withCall(world.getAllPlayers()[0]), 20);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"transforms": {"try-catch-strip": 1}, "warnings": 2, "bails": [{"transform": "R3", "reasonContains": "catch body"}]}
|