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.
@@ -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 tpCommand = this.lowerTpCommand(args)
2285
- if (tpCommand) {
2286
- this.builder.emitRaw(tpCommand)
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 tpCommand = this.lowerTpCommand(args)
2293
- if (tpCommand) {
2294
- this.builder.emitRaw(tpCommand)
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