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.
@@ -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 tpCommand = this.lowerTpCommand(args)
2285
- if (tpCommand) {
2286
- this.builder.emitRaw(tpCommand)
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 tpCommand = this.lowerTpCommand(args)
2293
- if (tpCommand) {
2294
- this.builder.emitRaw(tpCommand)
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
- // Convert args to strings for builtin (use SNBT for struct/array literals)
2300
- const strArgs = args.map(arg =>
2301
- arg.kind === 'struct_lit' || arg.kind === 'array_lit'
2302
- ? this.exprToSnbt(arg)
2303
- : this.exprToString(arg)
2304
- )
2305
- const cmd = BUILTINS[name](strArgs)
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