redscript-mc 1.2.12 → 1.2.14
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/dist/__tests__/macro.test.d.ts +8 -0
- package/dist/__tests__/macro.test.js +305 -0
- package/dist/codegen/mcfunction/index.js +9 -1
- package/dist/ir/types.d.ts +2 -0
- package/dist/lowering/index.d.ts +23 -0
- package/dist/lowering/index.js +268 -6
- package/dist/optimizer/commands.d.ts +1 -0
- package/dist/optimizer/commands.js +96 -1
- package/package.json +1 -1
- package/src/__tests__/fixtures/macro-test.mcrs +35 -0
- package/src/__tests__/macro.test.ts +343 -0
- package/src/codegen/mcfunction/index.ts +21 -12
- package/src/ir/types.ts +3 -0
- package/src/lowering/index.ts +286 -6
- package/src/optimizer/commands.ts +113 -1
package/dist/lowering/index.js
CHANGED
|
@@ -45,6 +45,16 @@ const diagnostics_1 = require("../diagnostics");
|
|
|
45
45
|
const path = __importStar(require("path"));
|
|
46
46
|
const types_1 = require("../events/types");
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
+
// Macro-aware builtins (MC 1.20.2+)
|
|
49
|
+
// These builtins generate commands where parameter variables cannot appear
|
|
50
|
+
// as literal values (coordinates, entity types, block types), so they
|
|
51
|
+
// require MC macro syntax when called with runtime variables.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
const MACRO_AWARE_BUILTINS = new Set([
|
|
54
|
+
'summon', 'particle', 'setblock', 'fill', 'clone',
|
|
55
|
+
'playsound', 'tp', 'tp_to', 'effect', 'effect_clear', 'give',
|
|
56
|
+
]);
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
48
58
|
// Builtin Functions
|
|
49
59
|
// ---------------------------------------------------------------------------
|
|
50
60
|
const BUILTINS = {
|
|
@@ -213,12 +223,194 @@ class Lowering {
|
|
|
213
223
|
this.worldObjCounter = 0;
|
|
214
224
|
// Loop context stack for break/continue
|
|
215
225
|
this.loopStack = [];
|
|
226
|
+
// MC 1.20.2+ macro function support
|
|
227
|
+
// Names of params in the current function being lowered
|
|
228
|
+
this.currentFnParamNames = new Set();
|
|
229
|
+
// Params in the current function that need macro treatment (used in literal positions)
|
|
230
|
+
this.currentFnMacroParams = new Set();
|
|
231
|
+
// Global registry: fnName → macroParamNames (populated by pre-scan + lowering)
|
|
232
|
+
this.macroFunctionInfo = new Map();
|
|
216
233
|
this.namespace = namespace;
|
|
217
234
|
this.sourceRanges = sourceRanges;
|
|
218
235
|
LoweringBuilder.resetTempCounter();
|
|
219
236
|
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// MC Macro pre-scan: identify which function params need macro treatment
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
preScanMacroFunctions(program) {
|
|
241
|
+
for (const fn of program.declarations) {
|
|
242
|
+
const paramNames = new Set(fn.params.map(p => p.name));
|
|
243
|
+
const macroParams = new Set();
|
|
244
|
+
this.preScanStmts(fn.body, paramNames, macroParams);
|
|
245
|
+
if (macroParams.size > 0) {
|
|
246
|
+
this.macroFunctionInfo.set(fn.name, [...macroParams]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
250
|
+
for (const method of implBlock.methods) {
|
|
251
|
+
const paramNames = new Set(method.params.map(p => p.name));
|
|
252
|
+
const macroParams = new Set();
|
|
253
|
+
this.preScanStmts(method.body, paramNames, macroParams);
|
|
254
|
+
if (macroParams.size > 0) {
|
|
255
|
+
this.macroFunctionInfo.set(`${implBlock.typeName}_${method.name}`, [...macroParams]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
preScanStmts(stmts, paramNames, macroParams) {
|
|
261
|
+
for (const stmt of stmts) {
|
|
262
|
+
this.preScanStmt(stmt, paramNames, macroParams);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
preScanStmt(stmt, paramNames, macroParams) {
|
|
266
|
+
switch (stmt.kind) {
|
|
267
|
+
case 'expr':
|
|
268
|
+
this.preScanExpr(stmt.expr, paramNames, macroParams);
|
|
269
|
+
break;
|
|
270
|
+
case 'let':
|
|
271
|
+
this.preScanExpr(stmt.init, paramNames, macroParams);
|
|
272
|
+
break;
|
|
273
|
+
case 'return':
|
|
274
|
+
if (stmt.value)
|
|
275
|
+
this.preScanExpr(stmt.value, paramNames, macroParams);
|
|
276
|
+
break;
|
|
277
|
+
case 'if':
|
|
278
|
+
this.preScanExpr(stmt.cond, paramNames, macroParams);
|
|
279
|
+
this.preScanStmts(stmt.then, paramNames, macroParams);
|
|
280
|
+
if (stmt.else_)
|
|
281
|
+
this.preScanStmts(stmt.else_, paramNames, macroParams);
|
|
282
|
+
break;
|
|
283
|
+
case 'while':
|
|
284
|
+
this.preScanExpr(stmt.cond, paramNames, macroParams);
|
|
285
|
+
this.preScanStmts(stmt.body, paramNames, macroParams);
|
|
286
|
+
break;
|
|
287
|
+
case 'for':
|
|
288
|
+
if (stmt.init)
|
|
289
|
+
this.preScanStmt(stmt.init, paramNames, macroParams);
|
|
290
|
+
this.preScanExpr(stmt.cond, paramNames, macroParams);
|
|
291
|
+
this.preScanStmts(stmt.body, paramNames, macroParams);
|
|
292
|
+
break;
|
|
293
|
+
case 'for_range':
|
|
294
|
+
this.preScanStmts(stmt.body, paramNames, macroParams);
|
|
295
|
+
break;
|
|
296
|
+
case 'foreach':
|
|
297
|
+
this.preScanStmts(stmt.body, paramNames, macroParams);
|
|
298
|
+
break;
|
|
299
|
+
case 'match':
|
|
300
|
+
this.preScanExpr(stmt.expr, paramNames, macroParams);
|
|
301
|
+
for (const arm of stmt.arms) {
|
|
302
|
+
this.preScanStmts(arm.body, paramNames, macroParams);
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
case 'as_block':
|
|
306
|
+
case 'at_block':
|
|
307
|
+
this.preScanStmts(stmt.body, paramNames, macroParams);
|
|
308
|
+
break;
|
|
309
|
+
case 'execute':
|
|
310
|
+
this.preScanStmts(stmt.body, paramNames, macroParams);
|
|
311
|
+
break;
|
|
312
|
+
// raw, break, continue have no nested exprs of interest
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
preScanExpr(expr, paramNames, macroParams) {
|
|
316
|
+
if (expr.kind === 'call' && MACRO_AWARE_BUILTINS.has(expr.fn)) {
|
|
317
|
+
// All ident args to macro-aware builtins that are params → macro params
|
|
318
|
+
for (const arg of expr.args) {
|
|
319
|
+
if (arg.kind === 'ident' && paramNames.has(arg.name)) {
|
|
320
|
+
macroParams.add(arg.name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Recurse into sub-expressions for other call types
|
|
326
|
+
if (expr.kind === 'call') {
|
|
327
|
+
for (const arg of expr.args)
|
|
328
|
+
this.preScanExpr(arg, paramNames, macroParams);
|
|
329
|
+
}
|
|
330
|
+
else if (expr.kind === 'binary') {
|
|
331
|
+
this.preScanExpr(expr.left, paramNames, macroParams);
|
|
332
|
+
this.preScanExpr(expr.right, paramNames, macroParams);
|
|
333
|
+
}
|
|
334
|
+
else if (expr.kind === 'unary') {
|
|
335
|
+
this.preScanExpr(expr.operand, paramNames, macroParams);
|
|
336
|
+
}
|
|
337
|
+
else if (expr.kind === 'assign') {
|
|
338
|
+
this.preScanExpr(expr.value, paramNames, macroParams);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Macro helpers
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
/**
|
|
345
|
+
* If `expr` is a function parameter that needs macro treatment (runtime value
|
|
346
|
+
* used in a literal position), returns the param name; otherwise null.
|
|
347
|
+
*/
|
|
348
|
+
tryGetMacroParam(expr) {
|
|
349
|
+
if (expr.kind !== 'ident')
|
|
350
|
+
return null;
|
|
351
|
+
if (!this.currentFnParamNames.has(expr.name))
|
|
352
|
+
return null;
|
|
353
|
+
if (this.constValues.has(expr.name))
|
|
354
|
+
return null;
|
|
355
|
+
if (this.stringValues.has(expr.name))
|
|
356
|
+
return null;
|
|
357
|
+
return expr.name;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Converts an expression to a string for use as a builtin arg.
|
|
361
|
+
* If the expression is a macro param, returns `$(name)` and sets macroParam.
|
|
362
|
+
*/
|
|
363
|
+
exprToBuiltinArg(expr) {
|
|
364
|
+
const macroParam = this.tryGetMacroParam(expr);
|
|
365
|
+
if (macroParam) {
|
|
366
|
+
return { str: `$(${macroParam})`, macroParam };
|
|
367
|
+
}
|
|
368
|
+
if (expr.kind === 'struct_lit' || expr.kind === 'array_lit') {
|
|
369
|
+
return { str: this.exprToSnbt(expr) };
|
|
370
|
+
}
|
|
371
|
+
return { str: this.exprToString(expr) };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Emits a call to a macro function, setting up both scoreboard params
|
|
375
|
+
* (for arithmetic use) and NBT macro args (for coordinate/literal use).
|
|
376
|
+
*/
|
|
377
|
+
emitMacroFunctionCall(fnName, args, macroParamNames, fnDecl) {
|
|
378
|
+
const params = fnDecl?.params ?? [];
|
|
379
|
+
const loweredArgs = args.map(arg => this.lowerExpr(arg));
|
|
380
|
+
// Set up regular scoreboard params (for arithmetic within the function)
|
|
381
|
+
for (let i = 0; i < loweredArgs.length; i++) {
|
|
382
|
+
const operand = loweredArgs[i];
|
|
383
|
+
if (operand.kind === 'const') {
|
|
384
|
+
this.builder.emitRaw(`scoreboard players set $p${i} rs ${operand.value}`);
|
|
385
|
+
}
|
|
386
|
+
else if (operand.kind === 'var') {
|
|
387
|
+
this.builder.emitRaw(`scoreboard players operation $p${i} rs = ${operand.name} rs`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Set up NBT storage for each macro param
|
|
391
|
+
for (const macroParam of macroParamNames) {
|
|
392
|
+
const paramIdx = params.findIndex(p => p.name === macroParam);
|
|
393
|
+
if (paramIdx < 0 || paramIdx >= loweredArgs.length)
|
|
394
|
+
continue;
|
|
395
|
+
const operand = loweredArgs[paramIdx];
|
|
396
|
+
if (operand.kind === 'const') {
|
|
397
|
+
this.builder.emitRaw(`data modify storage rs:macro_args ${macroParam} set value ${operand.value}`);
|
|
398
|
+
}
|
|
399
|
+
else if (operand.kind === 'var') {
|
|
400
|
+
this.builder.emitRaw(`execute store result storage rs:macro_args ${macroParam} int 1 run scoreboard players get ${operand.name} rs`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Call with macro storage
|
|
404
|
+
this.builder.emitRaw(`function ${this.namespace}:${fnName} with storage rs:macro_args`);
|
|
405
|
+
// Copy return value (callers may use it)
|
|
406
|
+
const dst = this.builder.freshTemp();
|
|
407
|
+
this.builder.emitRaw(`scoreboard players operation ${dst} rs = $ret rs`);
|
|
408
|
+
return { kind: 'var', name: dst };
|
|
409
|
+
}
|
|
220
410
|
lower(program) {
|
|
221
411
|
this.namespace = program.namespace;
|
|
412
|
+
// Pre-scan for macro functions before main lowering (so call sites can detect them)
|
|
413
|
+
this.preScanMacroFunctions(program);
|
|
222
414
|
// Load struct definitions
|
|
223
415
|
for (const struct of program.structs ?? []) {
|
|
224
416
|
const fields = new Map();
|
|
@@ -297,6 +489,9 @@ class Lowering {
|
|
|
297
489
|
this.blockPosVars = new Map();
|
|
298
490
|
this.stringValues = new Map();
|
|
299
491
|
this.builder = new LoweringBuilder();
|
|
492
|
+
// Initialize macro tracking for this function
|
|
493
|
+
this.currentFnParamNames = new Set(runtimeParams.map(p => p.name));
|
|
494
|
+
this.currentFnMacroParams = new Set();
|
|
300
495
|
// Map parameters
|
|
301
496
|
if (staticEventDec) {
|
|
302
497
|
for (let i = 0; i < fn.params.length; i++) {
|
|
@@ -401,6 +596,13 @@ class Lowering {
|
|
|
401
596
|
if (tickRate && tickRate > 1) {
|
|
402
597
|
this.wrapWithTickRate(irFn, tickRate);
|
|
403
598
|
}
|
|
599
|
+
// Set macro metadata if this function uses MC macro syntax
|
|
600
|
+
if (this.currentFnMacroParams.size > 0) {
|
|
601
|
+
irFn.isMacroFunction = true;
|
|
602
|
+
irFn.macroParamNames = [...this.currentFnMacroParams];
|
|
603
|
+
// Update registry (may refine the pre-scan result)
|
|
604
|
+
this.macroFunctionInfo.set(loweredName, irFn.macroParamNames);
|
|
605
|
+
}
|
|
404
606
|
this.functions.push(irFn);
|
|
405
607
|
}
|
|
406
608
|
getTickRate(decorators) {
|
|
@@ -1588,8 +1790,18 @@ class Lowering {
|
|
|
1588
1790
|
const targetFn = callbackBindings.size > 0 || stdlibCallSite
|
|
1589
1791
|
? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
|
|
1590
1792
|
: expr.fn;
|
|
1793
|
+
// Check if this is a call to a known macro function
|
|
1794
|
+
const macroParams = this.macroFunctionInfo.get(targetFn);
|
|
1795
|
+
if (macroParams && macroParams.length > 0) {
|
|
1796
|
+
return this.emitMacroFunctionCall(targetFn, runtimeArgs, macroParams, fnDecl);
|
|
1797
|
+
}
|
|
1591
1798
|
return this.emitDirectFunctionCall(targetFn, runtimeArgs);
|
|
1592
1799
|
}
|
|
1800
|
+
// Check for macro function (forward-declared or external)
|
|
1801
|
+
const macroParamsForUnknown = this.macroFunctionInfo.get(expr.fn);
|
|
1802
|
+
if (macroParamsForUnknown && macroParamsForUnknown.length > 0) {
|
|
1803
|
+
return this.emitMacroFunctionCall(expr.fn, fullArgs, macroParamsForUnknown, undefined);
|
|
1804
|
+
}
|
|
1593
1805
|
return this.emitDirectFunctionCall(expr.fn, fullArgs);
|
|
1594
1806
|
}
|
|
1595
1807
|
lowerStaticCallExpr(expr) {
|
|
@@ -1718,6 +1930,9 @@ class Lowering {
|
|
|
1718
1930
|
const savedBlockPosVars = new Map(this.blockPosVars);
|
|
1719
1931
|
const savedStringValues = new Map(this.stringValues);
|
|
1720
1932
|
const savedVarTypes = new Map(this.varTypes);
|
|
1933
|
+
// Macro tracking state
|
|
1934
|
+
const savedCurrentFnParamNames = new Set(this.currentFnParamNames);
|
|
1935
|
+
const savedCurrentFnMacroParams = new Set(this.currentFnMacroParams);
|
|
1721
1936
|
try {
|
|
1722
1937
|
return callback();
|
|
1723
1938
|
}
|
|
@@ -1734,6 +1949,8 @@ class Lowering {
|
|
|
1734
1949
|
this.blockPosVars = savedBlockPosVars;
|
|
1735
1950
|
this.stringValues = savedStringValues;
|
|
1736
1951
|
this.varTypes = savedVarTypes;
|
|
1952
|
+
this.currentFnParamNames = savedCurrentFnParamNames;
|
|
1953
|
+
this.currentFnMacroParams = savedCurrentFnMacroParams;
|
|
1737
1954
|
}
|
|
1738
1955
|
}
|
|
1739
1956
|
lowerBuiltinCall(name, args, callSpan) {
|
|
@@ -1967,16 +2184,31 @@ class Lowering {
|
|
|
1967
2184
|
code: 'W_DEPRECATED',
|
|
1968
2185
|
...(callSpan ? { line: callSpan.line, col: callSpan.col } : {}),
|
|
1969
2186
|
});
|
|
1970
|
-
const
|
|
1971
|
-
if (
|
|
1972
|
-
this.builder.emitRaw(
|
|
2187
|
+
const tpResult = this.lowerTpCommandMacroAware(args);
|
|
2188
|
+
if (tpResult) {
|
|
2189
|
+
this.builder.emitRaw(tpResult.cmd);
|
|
1973
2190
|
}
|
|
1974
2191
|
return { kind: 'const', value: 0 };
|
|
1975
2192
|
}
|
|
1976
2193
|
if (name === 'tp') {
|
|
1977
|
-
const
|
|
1978
|
-
if (
|
|
1979
|
-
this.builder.emitRaw(
|
|
2194
|
+
const tpResult = this.lowerTpCommandMacroAware(args);
|
|
2195
|
+
if (tpResult) {
|
|
2196
|
+
this.builder.emitRaw(tpResult.cmd);
|
|
2197
|
+
}
|
|
2198
|
+
return { kind: 'const', value: 0 };
|
|
2199
|
+
}
|
|
2200
|
+
// For macro-aware builtins, check if any arg is a param needing macro treatment
|
|
2201
|
+
if (MACRO_AWARE_BUILTINS.has(name)) {
|
|
2202
|
+
const argResults = args.map(arg => this.exprToBuiltinArg(arg));
|
|
2203
|
+
const hasMacroArg = argResults.some(r => r.macroParam !== undefined);
|
|
2204
|
+
if (hasMacroArg) {
|
|
2205
|
+
argResults.forEach(r => { if (r.macroParam)
|
|
2206
|
+
this.currentFnMacroParams.add(r.macroParam); });
|
|
2207
|
+
}
|
|
2208
|
+
const strArgs = argResults.map(r => r.str);
|
|
2209
|
+
const cmd = BUILTINS[name]?.(strArgs);
|
|
2210
|
+
if (cmd) {
|
|
2211
|
+
this.builder.emitRaw(hasMacroArg ? `$${cmd}` : cmd);
|
|
1980
2212
|
}
|
|
1981
2213
|
return { kind: 'const', value: 0 };
|
|
1982
2214
|
}
|
|
@@ -2488,6 +2720,36 @@ class Lowering {
|
|
|
2488
2720
|
}
|
|
2489
2721
|
return null;
|
|
2490
2722
|
}
|
|
2723
|
+
lowerTpCommandMacroAware(args) {
|
|
2724
|
+
const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null;
|
|
2725
|
+
const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null;
|
|
2726
|
+
// If blockpos args are used, no macro needed (coords are already resolved)
|
|
2727
|
+
if (args.length === 1 && pos0) {
|
|
2728
|
+
return { cmd: `tp ${emitBlockPos(pos0)}` };
|
|
2729
|
+
}
|
|
2730
|
+
if (args.length === 2 && pos1) {
|
|
2731
|
+
return { cmd: `tp ${this.exprToString(args[0])} ${emitBlockPos(pos1)}` };
|
|
2732
|
+
}
|
|
2733
|
+
// Check for macro args (int params used as coordinates)
|
|
2734
|
+
if (args.length >= 2) {
|
|
2735
|
+
const argResults = args.map(a => this.exprToBuiltinArg(a));
|
|
2736
|
+
const hasMacro = argResults.some(r => r.macroParam !== undefined);
|
|
2737
|
+
if (hasMacro) {
|
|
2738
|
+
argResults.forEach(r => { if (r.macroParam)
|
|
2739
|
+
this.currentFnMacroParams.add(r.macroParam); });
|
|
2740
|
+
const strs = argResults.map(r => r.str);
|
|
2741
|
+
if (args.length === 2) {
|
|
2742
|
+
return { cmd: `$tp ${strs[0]} ${strs[1]}` };
|
|
2743
|
+
}
|
|
2744
|
+
if (args.length === 4) {
|
|
2745
|
+
return { cmd: `$tp ${strs[0]} ${strs[1]} ${strs[2]} ${strs[3]}` };
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
// Fallback to non-macro
|
|
2750
|
+
const plain = this.lowerTpCommand(args);
|
|
2751
|
+
return plain ? { cmd: plain } : null;
|
|
2752
|
+
}
|
|
2491
2753
|
resolveBlockPosExpr(expr) {
|
|
2492
2754
|
if (expr.kind === 'blockpos') {
|
|
2493
2755
|
return expr;
|
|
@@ -23,6 +23,7 @@ function createEmptyOptimizationStats() {
|
|
|
23
23
|
setblockSavedCommands: 0,
|
|
24
24
|
deadCodeRemoved: 0,
|
|
25
25
|
constantFolds: 0,
|
|
26
|
+
inlinedTrivialFunctions: 0,
|
|
26
27
|
totalCommandsBefore: 0,
|
|
27
28
|
totalCommandsAfter: 0,
|
|
28
29
|
};
|
|
@@ -330,11 +331,105 @@ function batchSetblocks(functions) {
|
|
|
330
331
|
stats.totalCommandsAfter = optimized.reduce((sum, fn) => sum + fn.commands.length, 0);
|
|
331
332
|
return { functions: optimized, stats };
|
|
332
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Inline trivial functions:
|
|
336
|
+
* 1. Functions that only contain a single `function` call → inline the call
|
|
337
|
+
* 2. Empty functions (no commands) → remove and eliminate all calls to them
|
|
338
|
+
*/
|
|
339
|
+
function inlineTrivialFunctions(functions) {
|
|
340
|
+
const FUNCTION_CMD_RE = /^function ([^:]+):(.+)$/;
|
|
341
|
+
// Find trivial functions (only a single function call, no other commands)
|
|
342
|
+
const trivialMap = new Map(); // fn name -> target fn name
|
|
343
|
+
const emptyFunctions = new Set(); // functions with no commands
|
|
344
|
+
// System functions that should never be removed
|
|
345
|
+
const SYSTEM_FUNCTIONS = new Set(['__tick', '__load']);
|
|
346
|
+
for (const fn of functions) {
|
|
347
|
+
// Never remove system functions
|
|
348
|
+
if (SYSTEM_FUNCTIONS.has(fn.name) || fn.name.startsWith('__trigger_')) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const nonCommentCmds = fn.commands.filter(cmd => !cmd.cmd.startsWith('#'));
|
|
352
|
+
if (nonCommentCmds.length === 0 && fn.name.includes('/')) {
|
|
353
|
+
// Empty control-flow block (e.g., main/merge_5) - mark for removal
|
|
354
|
+
// Only remove if it's a sub-block (contains /), not a top-level function
|
|
355
|
+
emptyFunctions.add(fn.name);
|
|
356
|
+
}
|
|
357
|
+
else if (nonCommentCmds.length === 1 && fn.name.includes('/')) {
|
|
358
|
+
const match = nonCommentCmds[0].cmd.match(FUNCTION_CMD_RE);
|
|
359
|
+
if (match) {
|
|
360
|
+
// This function only calls another function
|
|
361
|
+
trivialMap.set(fn.name, match[2]);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Resolve chains: if A -> B -> C, then A -> C
|
|
366
|
+
// Also handle: A -> B where B is empty → A is effectively empty
|
|
367
|
+
let changed = true;
|
|
368
|
+
while (changed) {
|
|
369
|
+
changed = false;
|
|
370
|
+
for (const [from, to] of trivialMap) {
|
|
371
|
+
if (emptyFunctions.has(to)) {
|
|
372
|
+
// Target is empty, so this function is effectively empty too
|
|
373
|
+
trivialMap.delete(from);
|
|
374
|
+
emptyFunctions.add(from);
|
|
375
|
+
changed = true;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
const finalTarget = trivialMap.get(to);
|
|
379
|
+
if (finalTarget && finalTarget !== to) {
|
|
380
|
+
trivialMap.set(from, finalTarget);
|
|
381
|
+
changed = true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const totalRemoved = trivialMap.size + emptyFunctions.size;
|
|
387
|
+
if (totalRemoved === 0) {
|
|
388
|
+
return { functions, stats: {} };
|
|
389
|
+
}
|
|
390
|
+
// Set of all functions to remove
|
|
391
|
+
const removedNames = new Set([...trivialMap.keys(), ...emptyFunctions]);
|
|
392
|
+
// Rewrite all function calls to skip trivial wrappers or remove empty calls
|
|
393
|
+
const result = [];
|
|
394
|
+
for (const fn of functions) {
|
|
395
|
+
// Skip removed functions
|
|
396
|
+
if (removedNames.has(fn.name)) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// Rewrite function calls in this function
|
|
400
|
+
const rewrittenCmds = [];
|
|
401
|
+
for (const cmd of fn.commands) {
|
|
402
|
+
// Check if this is a call to an empty function
|
|
403
|
+
const emptyCallMatch = cmd.cmd.match(/^(?:execute .* run )?function ([^:]+):([^\s]+)$/);
|
|
404
|
+
if (emptyCallMatch) {
|
|
405
|
+
const targetFn = emptyCallMatch[2];
|
|
406
|
+
if (emptyFunctions.has(targetFn)) {
|
|
407
|
+
// Skip calls to empty functions entirely
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Rewrite calls to trivial wrapper functions
|
|
412
|
+
const rewritten = cmd.cmd.replace(/function ([^:]+):([^\s]+)/g, (match, ns, fnPath) => {
|
|
413
|
+
const target = trivialMap.get(fnPath);
|
|
414
|
+
return target ? `function ${ns}:${target}` : match;
|
|
415
|
+
});
|
|
416
|
+
rewrittenCmds.push({ ...cmd, cmd: rewritten });
|
|
417
|
+
}
|
|
418
|
+
result.push({ name: fn.name, commands: rewrittenCmds });
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
functions: result,
|
|
422
|
+
stats: { inlinedTrivialFunctions: totalRemoved }
|
|
423
|
+
};
|
|
424
|
+
}
|
|
333
425
|
function optimizeCommandFunctions(functions) {
|
|
334
426
|
const initial = cloneFunctions(functions);
|
|
335
427
|
const stats = createEmptyOptimizationStats();
|
|
336
428
|
stats.totalCommandsBefore = initial.reduce((sum, fn) => sum + fn.commands.length, 0);
|
|
337
|
-
|
|
429
|
+
// First pass: inline trivial functions
|
|
430
|
+
const inlined = inlineTrivialFunctions(initial);
|
|
431
|
+
mergeOptimizationStats(stats, inlined.stats);
|
|
432
|
+
const licm = applyLICM(inlined.functions);
|
|
338
433
|
mergeOptimizationStats(stats, licm.stats);
|
|
339
434
|
const cse = applyCSE(licm.functions);
|
|
340
435
|
mergeOptimizationStats(stats, cse.stats);
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// MC 1.20.2+ macro function test
|
|
2
|
+
// Tests that functions using parameters in coordinate/type positions
|
|
3
|
+
// generate macro syntax ($-prefixed commands with $(param) substitution)
|
|
4
|
+
//
|
|
5
|
+
// Compilation: redscript compile macro-test.mcrs -o output/ -n test
|
|
6
|
+
|
|
7
|
+
// Basic macro function: parameters used as summon coordinates
|
|
8
|
+
fn spawn_zombie(x: int, y: int, z: int) {
|
|
9
|
+
summon("minecraft:zombie", x, y, z);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Macro function: particle effect at coordinates
|
|
13
|
+
fn show_particle(x: int, y: int, z: int) {
|
|
14
|
+
particle("minecraft:flame", x, y, z);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Macro function: setblock at variable position
|
|
18
|
+
fn place_block(x: int, y: int, z: int) {
|
|
19
|
+
setblock(x, y, z, "minecraft:stone");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Non-macro function: all args are compile-time constants in the body
|
|
23
|
+
fn summon_fixed() {
|
|
24
|
+
summon("minecraft:zombie", 100, 64, 200);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Caller with constant args (calls macro function with inline NBT data)
|
|
28
|
+
fn call_with_constants() {
|
|
29
|
+
spawn_zombie(100, 64, 200);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Caller with variable args (calls macro function via 'with storage rs:macro_args')
|
|
33
|
+
fn call_with_vars(px: int, pz: int) {
|
|
34
|
+
spawn_zombie(px, 64, pz);
|
|
35
|
+
}
|