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/src/lowering/index.ts
CHANGED
|
@@ -18,6 +18,18 @@ import type { GlobalVar } from '../ir/types'
|
|
|
18
18
|
import * as path from 'path'
|
|
19
19
|
import { EVENT_TYPES, getEventParamSpecs, isEventTypeName } from '../events/types'
|
|
20
20
|
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Macro-aware builtins (MC 1.20.2+)
|
|
23
|
+
// These builtins generate commands where parameter variables cannot appear
|
|
24
|
+
// as literal values (coordinates, entity types, block types), so they
|
|
25
|
+
// require MC macro syntax when called with runtime variables.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const MACRO_AWARE_BUILTINS = new Set([
|
|
29
|
+
'summon', 'particle', 'setblock', 'fill', 'clone',
|
|
30
|
+
'playsound', 'tp', 'tp_to', 'effect', 'effect_clear', 'give',
|
|
31
|
+
])
|
|
32
|
+
|
|
21
33
|
// ---------------------------------------------------------------------------
|
|
22
34
|
// Builtin Functions
|
|
23
35
|
// ---------------------------------------------------------------------------
|
|
@@ -229,15 +241,206 @@ export class Lowering {
|
|
|
229
241
|
// Loop context stack for break/continue
|
|
230
242
|
private loopStack: Array<{ breakLabel: string; continueLabel: string; stepFn?: () => void }> = []
|
|
231
243
|
|
|
244
|
+
// MC 1.20.2+ macro function support
|
|
245
|
+
// Names of params in the current function being lowered
|
|
246
|
+
private currentFnParamNames: Set<string> = new Set()
|
|
247
|
+
// Params in the current function that need macro treatment (used in literal positions)
|
|
248
|
+
private currentFnMacroParams: Set<string> = new Set()
|
|
249
|
+
// Global registry: fnName → macroParamNames (populated by pre-scan + lowering)
|
|
250
|
+
private macroFunctionInfo: Map<string, string[]> = new Map()
|
|
251
|
+
|
|
232
252
|
constructor(namespace: string, sourceRanges: SourceRange[] = []) {
|
|
233
253
|
this.namespace = namespace
|
|
234
254
|
this.sourceRanges = sourceRanges
|
|
235
255
|
LoweringBuilder.resetTempCounter()
|
|
236
256
|
}
|
|
237
257
|
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// MC Macro pre-scan: identify which function params need macro treatment
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
private preScanMacroFunctions(program: Program): void {
|
|
263
|
+
for (const fn of program.declarations) {
|
|
264
|
+
const paramNames = new Set(fn.params.map(p => p.name))
|
|
265
|
+
const macroParams = new Set<string>()
|
|
266
|
+
this.preScanStmts(fn.body, paramNames, macroParams)
|
|
267
|
+
if (macroParams.size > 0) {
|
|
268
|
+
this.macroFunctionInfo.set(fn.name, [...macroParams])
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
272
|
+
for (const method of implBlock.methods) {
|
|
273
|
+
const paramNames = new Set(method.params.map(p => p.name))
|
|
274
|
+
const macroParams = new Set<string>()
|
|
275
|
+
this.preScanStmts(method.body, paramNames, macroParams)
|
|
276
|
+
if (macroParams.size > 0) {
|
|
277
|
+
this.macroFunctionInfo.set(`${implBlock.typeName}_${method.name}`, [...macroParams])
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private preScanStmts(stmts: Block, paramNames: Set<string>, macroParams: Set<string>): void {
|
|
284
|
+
for (const stmt of stmts) {
|
|
285
|
+
this.preScanStmt(stmt, paramNames, macroParams)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private preScanStmt(stmt: Stmt, paramNames: Set<string>, macroParams: Set<string>): void {
|
|
290
|
+
switch (stmt.kind) {
|
|
291
|
+
case 'expr':
|
|
292
|
+
this.preScanExpr(stmt.expr, paramNames, macroParams)
|
|
293
|
+
break
|
|
294
|
+
case 'let':
|
|
295
|
+
this.preScanExpr(stmt.init, paramNames, macroParams)
|
|
296
|
+
break
|
|
297
|
+
case 'return':
|
|
298
|
+
if (stmt.value) this.preScanExpr(stmt.value, paramNames, macroParams)
|
|
299
|
+
break
|
|
300
|
+
case 'if':
|
|
301
|
+
this.preScanExpr(stmt.cond, paramNames, macroParams)
|
|
302
|
+
this.preScanStmts(stmt.then, paramNames, macroParams)
|
|
303
|
+
if (stmt.else_) this.preScanStmts(stmt.else_, paramNames, macroParams)
|
|
304
|
+
break
|
|
305
|
+
case 'while':
|
|
306
|
+
this.preScanExpr(stmt.cond, paramNames, macroParams)
|
|
307
|
+
this.preScanStmts(stmt.body, paramNames, macroParams)
|
|
308
|
+
break
|
|
309
|
+
case 'for':
|
|
310
|
+
if (stmt.init) this.preScanStmt(stmt.init, paramNames, macroParams)
|
|
311
|
+
this.preScanExpr(stmt.cond, paramNames, macroParams)
|
|
312
|
+
this.preScanStmts(stmt.body, paramNames, macroParams)
|
|
313
|
+
break
|
|
314
|
+
case 'for_range':
|
|
315
|
+
this.preScanStmts(stmt.body, paramNames, macroParams)
|
|
316
|
+
break
|
|
317
|
+
case 'foreach':
|
|
318
|
+
this.preScanStmts(stmt.body, paramNames, macroParams)
|
|
319
|
+
break
|
|
320
|
+
case 'match':
|
|
321
|
+
this.preScanExpr(stmt.expr, paramNames, macroParams)
|
|
322
|
+
for (const arm of stmt.arms) {
|
|
323
|
+
this.preScanStmts(arm.body, paramNames, macroParams)
|
|
324
|
+
}
|
|
325
|
+
break
|
|
326
|
+
case 'as_block':
|
|
327
|
+
case 'at_block':
|
|
328
|
+
this.preScanStmts(stmt.body, paramNames, macroParams)
|
|
329
|
+
break
|
|
330
|
+
case 'execute':
|
|
331
|
+
this.preScanStmts(stmt.body, paramNames, macroParams)
|
|
332
|
+
break
|
|
333
|
+
// raw, break, continue have no nested exprs of interest
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private preScanExpr(expr: Expr, paramNames: Set<string>, macroParams: Set<string>): void {
|
|
338
|
+
if (expr.kind === 'call' && MACRO_AWARE_BUILTINS.has(expr.fn)) {
|
|
339
|
+
// All ident args to macro-aware builtins that are params → macro params
|
|
340
|
+
for (const arg of expr.args) {
|
|
341
|
+
if (arg.kind === 'ident' && paramNames.has(arg.name)) {
|
|
342
|
+
macroParams.add(arg.name)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
// Recurse into sub-expressions for other call types
|
|
348
|
+
if (expr.kind === 'call') {
|
|
349
|
+
for (const arg of expr.args) this.preScanExpr(arg, paramNames, macroParams)
|
|
350
|
+
} else if (expr.kind === 'binary') {
|
|
351
|
+
this.preScanExpr(expr.left, paramNames, macroParams)
|
|
352
|
+
this.preScanExpr(expr.right, paramNames, macroParams)
|
|
353
|
+
} else if (expr.kind === 'unary') {
|
|
354
|
+
this.preScanExpr(expr.operand, paramNames, macroParams)
|
|
355
|
+
} else if (expr.kind === 'assign') {
|
|
356
|
+
this.preScanExpr(expr.value, paramNames, macroParams)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Macro helpers
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* If `expr` is a function parameter that needs macro treatment (runtime value
|
|
366
|
+
* used in a literal position), returns the param name; otherwise null.
|
|
367
|
+
*/
|
|
368
|
+
private tryGetMacroParam(expr: Expr): string | null {
|
|
369
|
+
if (expr.kind !== 'ident') return null
|
|
370
|
+
if (!this.currentFnParamNames.has(expr.name)) return null
|
|
371
|
+
if (this.constValues.has(expr.name)) return null
|
|
372
|
+
if (this.stringValues.has(expr.name)) return null
|
|
373
|
+
return expr.name
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Converts an expression to a string for use as a builtin arg.
|
|
378
|
+
* If the expression is a macro param, returns `$(name)` and sets macroParam.
|
|
379
|
+
*/
|
|
380
|
+
private exprToBuiltinArg(expr: Expr): { str: string; macroParam?: string } {
|
|
381
|
+
const macroParam = this.tryGetMacroParam(expr)
|
|
382
|
+
if (macroParam) {
|
|
383
|
+
return { str: `$(${macroParam})`, macroParam }
|
|
384
|
+
}
|
|
385
|
+
if (expr.kind === 'struct_lit' || expr.kind === 'array_lit') {
|
|
386
|
+
return { str: this.exprToSnbt(expr) }
|
|
387
|
+
}
|
|
388
|
+
return { str: this.exprToString(expr) }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Emits a call to a macro function, setting up both scoreboard params
|
|
393
|
+
* (for arithmetic use) and NBT macro args (for coordinate/literal use).
|
|
394
|
+
*/
|
|
395
|
+
private emitMacroFunctionCall(
|
|
396
|
+
fnName: string,
|
|
397
|
+
args: Expr[],
|
|
398
|
+
macroParamNames: string[],
|
|
399
|
+
fnDecl: FnDecl | undefined,
|
|
400
|
+
): Operand {
|
|
401
|
+
const params = fnDecl?.params ?? []
|
|
402
|
+
const loweredArgs: Operand[] = args.map(arg => this.lowerExpr(arg))
|
|
403
|
+
|
|
404
|
+
// Set up regular scoreboard params (for arithmetic within the function)
|
|
405
|
+
for (let i = 0; i < loweredArgs.length; i++) {
|
|
406
|
+
const operand = loweredArgs[i]
|
|
407
|
+
if (operand.kind === 'const') {
|
|
408
|
+
this.builder.emitRaw(`scoreboard players set $p${i} rs ${operand.value}`)
|
|
409
|
+
} else if (operand.kind === 'var') {
|
|
410
|
+
this.builder.emitRaw(`scoreboard players operation $p${i} rs = ${operand.name} rs`)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Set up NBT storage for each macro param
|
|
415
|
+
for (const macroParam of macroParamNames) {
|
|
416
|
+
const paramIdx = params.findIndex(p => p.name === macroParam)
|
|
417
|
+
if (paramIdx < 0 || paramIdx >= loweredArgs.length) continue
|
|
418
|
+
|
|
419
|
+
const operand = loweredArgs[paramIdx]
|
|
420
|
+
if (operand.kind === 'const') {
|
|
421
|
+
this.builder.emitRaw(`data modify storage rs:macro_args ${macroParam} set value ${operand.value}`)
|
|
422
|
+
} else if (operand.kind === 'var') {
|
|
423
|
+
this.builder.emitRaw(
|
|
424
|
+
`execute store result storage rs:macro_args ${macroParam} int 1 run scoreboard players get ${operand.name} rs`
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Call with macro storage
|
|
430
|
+
this.builder.emitRaw(`function ${this.namespace}:${fnName} with storage rs:macro_args`)
|
|
431
|
+
|
|
432
|
+
// Copy return value (callers may use it)
|
|
433
|
+
const dst = this.builder.freshTemp()
|
|
434
|
+
this.builder.emitRaw(`scoreboard players operation ${dst} rs = $ret rs`)
|
|
435
|
+
return { kind: 'var', name: dst }
|
|
436
|
+
}
|
|
437
|
+
|
|
238
438
|
lower(program: Program): IRModule {
|
|
239
439
|
this.namespace = program.namespace
|
|
240
440
|
|
|
441
|
+
// Pre-scan for macro functions before main lowering (so call sites can detect them)
|
|
442
|
+
this.preScanMacroFunctions(program)
|
|
443
|
+
|
|
241
444
|
// Load struct definitions
|
|
242
445
|
for (const struct of program.structs ?? []) {
|
|
243
446
|
const fields = new Map<string, TypeNode>()
|
|
@@ -335,6 +538,9 @@ export class Lowering {
|
|
|
335
538
|
this.blockPosVars = new Map()
|
|
336
539
|
this.stringValues = new Map()
|
|
337
540
|
this.builder = new LoweringBuilder()
|
|
541
|
+
// Initialize macro tracking for this function
|
|
542
|
+
this.currentFnParamNames = new Set(runtimeParams.map(p => p.name))
|
|
543
|
+
this.currentFnMacroParams = new Set()
|
|
338
544
|
|
|
339
545
|
// Map parameters
|
|
340
546
|
if (staticEventDec) {
|
|
@@ -458,6 +664,14 @@ export class Lowering {
|
|
|
458
664
|
this.wrapWithTickRate(irFn, tickRate)
|
|
459
665
|
}
|
|
460
666
|
|
|
667
|
+
// Set macro metadata if this function uses MC macro syntax
|
|
668
|
+
if (this.currentFnMacroParams.size > 0) {
|
|
669
|
+
irFn.isMacroFunction = true
|
|
670
|
+
irFn.macroParamNames = [...this.currentFnMacroParams]
|
|
671
|
+
// Update registry (may refine the pre-scan result)
|
|
672
|
+
this.macroFunctionInfo.set(loweredName, irFn.macroParamNames)
|
|
673
|
+
}
|
|
674
|
+
|
|
461
675
|
this.functions.push(irFn)
|
|
462
676
|
}
|
|
463
677
|
|
|
@@ -1847,9 +2061,22 @@ export class Lowering {
|
|
|
1847
2061
|
const targetFn = callbackBindings.size > 0 || stdlibCallSite
|
|
1848
2062
|
? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
|
|
1849
2063
|
: expr.fn
|
|
2064
|
+
|
|
2065
|
+
// Check if this is a call to a known macro function
|
|
2066
|
+
const macroParams = this.macroFunctionInfo.get(targetFn)
|
|
2067
|
+
if (macroParams && macroParams.length > 0) {
|
|
2068
|
+
return this.emitMacroFunctionCall(targetFn, runtimeArgs, macroParams, fnDecl)
|
|
2069
|
+
}
|
|
2070
|
+
|
|
1850
2071
|
return this.emitDirectFunctionCall(targetFn, runtimeArgs)
|
|
1851
2072
|
}
|
|
1852
2073
|
|
|
2074
|
+
// Check for macro function (forward-declared or external)
|
|
2075
|
+
const macroParamsForUnknown = this.macroFunctionInfo.get(expr.fn)
|
|
2076
|
+
if (macroParamsForUnknown && macroParamsForUnknown.length > 0) {
|
|
2077
|
+
return this.emitMacroFunctionCall(expr.fn, fullArgs, macroParamsForUnknown, undefined)
|
|
2078
|
+
}
|
|
2079
|
+
|
|
1853
2080
|
return this.emitDirectFunctionCall(expr.fn, fullArgs)
|
|
1854
2081
|
}
|
|
1855
2082
|
|
|
@@ -1998,6 +2225,9 @@ export class Lowering {
|
|
|
1998
2225
|
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
1999
2226
|
const savedStringValues = new Map(this.stringValues)
|
|
2000
2227
|
const savedVarTypes = new Map(this.varTypes)
|
|
2228
|
+
// Macro tracking state
|
|
2229
|
+
const savedCurrentFnParamNames = new Set(this.currentFnParamNames)
|
|
2230
|
+
const savedCurrentFnMacroParams = new Set(this.currentFnMacroParams)
|
|
2001
2231
|
|
|
2002
2232
|
try {
|
|
2003
2233
|
return callback()
|
|
@@ -2014,6 +2244,8 @@ export class Lowering {
|
|
|
2014
2244
|
this.blockPosVars = savedBlockPosVars
|
|
2015
2245
|
this.stringValues = savedStringValues
|
|
2016
2246
|
this.varTypes = savedVarTypes
|
|
2247
|
+
this.currentFnParamNames = savedCurrentFnParamNames
|
|
2248
|
+
this.currentFnMacroParams = savedCurrentFnMacroParams
|
|
2017
2249
|
}
|
|
2018
2250
|
}
|
|
2019
2251
|
|
|
@@ -2281,17 +2513,32 @@ export class Lowering {
|
|
|
2281
2513
|
code: 'W_DEPRECATED',
|
|
2282
2514
|
...(callSpan ? { line: callSpan.line, col: callSpan.col } : {}),
|
|
2283
2515
|
})
|
|
2284
|
-
const
|
|
2285
|
-
if (
|
|
2286
|
-
this.builder.emitRaw(
|
|
2516
|
+
const tpResult = this.lowerTpCommandMacroAware(args)
|
|
2517
|
+
if (tpResult) {
|
|
2518
|
+
this.builder.emitRaw(tpResult.cmd)
|
|
2287
2519
|
}
|
|
2288
2520
|
return { kind: 'const', value: 0 }
|
|
2289
2521
|
}
|
|
2290
2522
|
|
|
2291
2523
|
if (name === 'tp') {
|
|
2292
|
-
const
|
|
2293
|
-
if (
|
|
2294
|
-
this.builder.emitRaw(
|
|
2524
|
+
const tpResult = this.lowerTpCommandMacroAware(args)
|
|
2525
|
+
if (tpResult) {
|
|
2526
|
+
this.builder.emitRaw(tpResult.cmd)
|
|
2527
|
+
}
|
|
2528
|
+
return { kind: 'const', value: 0 }
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// For macro-aware builtins, check if any arg is a param needing macro treatment
|
|
2532
|
+
if (MACRO_AWARE_BUILTINS.has(name)) {
|
|
2533
|
+
const argResults = args.map(arg => this.exprToBuiltinArg(arg))
|
|
2534
|
+
const hasMacroArg = argResults.some(r => r.macroParam !== undefined)
|
|
2535
|
+
if (hasMacroArg) {
|
|
2536
|
+
argResults.forEach(r => { if (r.macroParam) this.currentFnMacroParams.add(r.macroParam) })
|
|
2537
|
+
}
|
|
2538
|
+
const strArgs = argResults.map(r => r.str)
|
|
2539
|
+
const cmd = BUILTINS[name]?.(strArgs)
|
|
2540
|
+
if (cmd) {
|
|
2541
|
+
this.builder.emitRaw(hasMacroArg ? `$${cmd}` : cmd)
|
|
2295
2542
|
}
|
|
2296
2543
|
return { kind: 'const', value: 0 }
|
|
2297
2544
|
}
|
|
@@ -2898,6 +3145,39 @@ export class Lowering {
|
|
|
2898
3145
|
return null
|
|
2899
3146
|
}
|
|
2900
3147
|
|
|
3148
|
+
private lowerTpCommandMacroAware(args: Expr[]): { cmd: string } | null {
|
|
3149
|
+
const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
|
|
3150
|
+
const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
|
|
3151
|
+
|
|
3152
|
+
// If blockpos args are used, no macro needed (coords are already resolved)
|
|
3153
|
+
if (args.length === 1 && pos0) {
|
|
3154
|
+
return { cmd: `tp ${emitBlockPos(pos0)}` }
|
|
3155
|
+
}
|
|
3156
|
+
if (args.length === 2 && pos1) {
|
|
3157
|
+
return { cmd: `tp ${this.exprToString(args[0])} ${emitBlockPos(pos1)}` }
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
// Check for macro args (int params used as coordinates)
|
|
3161
|
+
if (args.length >= 2) {
|
|
3162
|
+
const argResults = args.map(a => this.exprToBuiltinArg(a))
|
|
3163
|
+
const hasMacro = argResults.some(r => r.macroParam !== undefined)
|
|
3164
|
+
if (hasMacro) {
|
|
3165
|
+
argResults.forEach(r => { if (r.macroParam) this.currentFnMacroParams.add(r.macroParam) })
|
|
3166
|
+
const strs = argResults.map(r => r.str)
|
|
3167
|
+
if (args.length === 2) {
|
|
3168
|
+
return { cmd: `$tp ${strs[0]} ${strs[1]}` }
|
|
3169
|
+
}
|
|
3170
|
+
if (args.length === 4) {
|
|
3171
|
+
return { cmd: `$tp ${strs[0]} ${strs[1]} ${strs[2]} ${strs[3]}` }
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// Fallback to non-macro
|
|
3177
|
+
const plain = this.lowerTpCommand(args)
|
|
3178
|
+
return plain ? { cmd: plain } : null
|
|
3179
|
+
}
|
|
3180
|
+
|
|
2901
3181
|
private resolveBlockPosExpr(expr: Expr): BlockPosExpr | null {
|
|
2902
3182
|
if (expr.kind === 'blockpos') {
|
|
2903
3183
|
return expr
|