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