redscript-mc 1.2.13 → 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/ir/types.d.ts +2 -0
- package/dist/lowering/index.d.ts +23 -0
- package/dist/lowering/index.js +268 -6
- package/package.json +1 -1
- package/src/__tests__/fixtures/macro-test.mcrs +35 -0
- package/src/__tests__/macro.test.ts +343 -0
- package/src/ir/types.ts +3 -0
- package/src/lowering/index.ts +286 -6
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;
|
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
|
+
}
|