redscript-mc 2.2.1 → 2.4.0

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +18 -2
  3. package/dist/src/__tests__/array-dynamic.test.d.ts +12 -0
  4. package/dist/src/__tests__/array-dynamic.test.js +131 -0
  5. package/dist/src/__tests__/array-write.test.d.ts +11 -0
  6. package/dist/src/__tests__/array-write.test.js +149 -0
  7. package/dist/src/__tests__/tuner/engine.test.d.ts +4 -0
  8. package/dist/src/__tests__/tuner/engine.test.js +232 -0
  9. package/dist/src/ast/types.d.ts +7 -0
  10. package/dist/src/emit/modules.js +5 -0
  11. package/dist/src/hir/lower.js +29 -0
  12. package/dist/src/hir/monomorphize.js +2 -0
  13. package/dist/src/hir/types.d.ts +9 -2
  14. package/dist/src/lir/lower.js +131 -0
  15. package/dist/src/mir/lower.js +73 -3
  16. package/dist/src/mir/macro.js +5 -0
  17. package/dist/src/mir/types.d.ts +12 -0
  18. package/dist/src/mir/verify.js +7 -0
  19. package/dist/src/optimizer/copy_prop.js +5 -0
  20. package/dist/src/optimizer/coroutine.js +12 -0
  21. package/dist/src/optimizer/dce.js +9 -0
  22. package/dist/src/optimizer/unroll.js +3 -0
  23. package/dist/src/parser/index.js +5 -0
  24. package/dist/src/tuner/adapters/ln-polynomial.d.ts +23 -0
  25. package/dist/src/tuner/adapters/ln-polynomial.js +142 -0
  26. package/dist/src/tuner/adapters/sqrt-newton.d.ts +28 -0
  27. package/dist/src/tuner/adapters/sqrt-newton.js +125 -0
  28. package/dist/src/tuner/cli.d.ts +5 -0
  29. package/dist/src/tuner/cli.js +168 -0
  30. package/dist/src/tuner/engine.d.ts +17 -0
  31. package/dist/src/tuner/engine.js +215 -0
  32. package/dist/src/tuner/metrics.d.ts +15 -0
  33. package/dist/src/tuner/metrics.js +51 -0
  34. package/dist/src/tuner/simulator.d.ts +35 -0
  35. package/dist/src/tuner/simulator.js +78 -0
  36. package/dist/src/tuner/types.d.ts +32 -0
  37. package/dist/src/tuner/types.js +6 -0
  38. package/dist/src/typechecker/index.js +5 -0
  39. package/docs/STDLIB_ROADMAP.md +142 -0
  40. package/editors/vscode/package-lock.json +3 -3
  41. package/editors/vscode/package.json +1 -1
  42. package/package.json +1 -1
  43. package/src/__tests__/array-dynamic.test.ts +147 -0
  44. package/src/__tests__/array-write.test.ts +169 -0
  45. package/src/__tests__/tuner/engine.test.ts +260 -0
  46. package/src/ast/types.ts +1 -0
  47. package/src/emit/modules.ts +5 -0
  48. package/src/hir/lower.ts +30 -0
  49. package/src/hir/monomorphize.ts +2 -0
  50. package/src/hir/types.ts +3 -1
  51. package/src/lir/lower.ts +151 -0
  52. package/src/mir/lower.ts +75 -3
  53. package/src/mir/macro.ts +5 -0
  54. package/src/mir/types.ts +2 -0
  55. package/src/mir/verify.ts +7 -0
  56. package/src/optimizer/copy_prop.ts +5 -0
  57. package/src/optimizer/coroutine.ts +9 -0
  58. package/src/optimizer/dce.ts +6 -0
  59. package/src/optimizer/unroll.ts +3 -0
  60. package/src/parser/index.ts +9 -0
  61. package/src/stdlib/bigint.mcrs +155 -192
  62. package/src/stdlib/bits.mcrs +158 -0
  63. package/src/stdlib/color.mcrs +160 -0
  64. package/src/stdlib/geometry.mcrs +124 -0
  65. package/src/stdlib/list.mcrs +96 -0
  66. package/src/stdlib/math.mcrs +227 -0
  67. package/src/stdlib/math_hp.mcrs +65 -0
  68. package/src/stdlib/random.mcrs +67 -0
  69. package/src/stdlib/signal.mcrs +112 -0
  70. package/src/stdlib/timer.mcrs +32 -0
  71. package/src/stdlib/vec.mcrs +27 -0
  72. package/src/tuner/adapters/ln-polynomial.ts +147 -0
  73. package/src/tuner/adapters/sqrt-newton.ts +135 -0
  74. package/src/tuner/cli.ts +158 -0
  75. package/src/tuner/engine.ts +272 -0
  76. package/src/tuner/metrics.ts +66 -0
  77. package/src/tuner/simulator.ts +69 -0
  78. package/src/tuner/types.ts +44 -0
  79. package/src/typechecker/index.ts +6 -0
  80. package/docs/ARCHITECTURE.zh.md +0 -1088
  81. package/docs/COMPILATION_STATS.md +0 -142
  82. package/docs/IMPLEMENTATION_GUIDE.md +0 -512
package/src/lir/lower.ts CHANGED
@@ -50,6 +50,10 @@ class LoweringContext {
50
50
  private currentMIRFn: MIRFunction | null = null
51
51
  /** Block map for quick lookup */
52
52
  private blockMap = new Map<BlockId, MIRBlock>()
53
+ /** Track generated dynamic array macro helper functions to avoid duplicates: key → fn name */
54
+ private dynIdxHelpers = new Map<string, string>()
55
+ /** Track generated dynamic array write helper functions: key → fn name */
56
+ private dynWrtHelpers = new Map<string, string>()
53
57
 
54
58
  constructor(namespace: string, objective: string) {
55
59
  this.namespace = namespace
@@ -77,6 +81,78 @@ class LoweringContext {
77
81
  this.functions.push(fn)
78
82
  }
79
83
 
84
+ /**
85
+ * Get or create a macro helper function for dynamic array index reads.
86
+ * The helper function is: $return run data get storage <ns> <pathPrefix>[$(arr_idx)] 1
87
+ * Returns the qualified MC function name (namespace:fnName).
88
+ */
89
+ getDynIdxHelper(ns: string, pathPrefix: string): string {
90
+ const key = `${ns}\0${pathPrefix}`
91
+ const existing = this.dynIdxHelpers.get(key)
92
+ if (existing) return existing
93
+
94
+ // Generate deterministic name from ns and pathPrefix
95
+ const sanitize = (s: string) => s.replace(/[^a-z0-9_]/gi, '_').toLowerCase()
96
+ // Extract just the storage name part from ns (e.g. "myns:arrays" → "myns_arrays")
97
+ const nsStr = sanitize(ns)
98
+ const prefixStr = sanitize(pathPrefix)
99
+ const helperName = `__dyn_idx_${nsStr}_${prefixStr}`
100
+
101
+ // The helper is placed in the current namespace
102
+ const qualifiedName = `${this.namespace}:${helperName}`
103
+
104
+ // Generate the macro function content:
105
+ // $return run data get storage <ns> <pathPrefix>[$(arr_idx)] 1
106
+ const macroLine: LIRInstr = {
107
+ kind: 'macro_line',
108
+ template: `return run data get storage ${ns} ${pathPrefix}[$(arr_idx)] 1`,
109
+ }
110
+
111
+ this.addFunction({
112
+ name: helperName,
113
+ instructions: [macroLine],
114
+ isMacro: true,
115
+ macroParams: ['arr_idx'],
116
+ })
117
+
118
+ this.dynIdxHelpers.set(key, qualifiedName)
119
+ return qualifiedName
120
+ }
121
+
122
+ /**
123
+ * Get or create a macro helper function for dynamic array index writes.
124
+ * The helper function: $data modify storage <ns> <pathPrefix>[$(arr_idx)] set value $(arr_val)
125
+ * Returns the qualified MC function name.
126
+ */
127
+ getDynWrtHelper(ns: string, pathPrefix: string): string {
128
+ const key = `${ns}\0${pathPrefix}`
129
+ const existing = this.dynWrtHelpers.get(key)
130
+ if (existing) return existing
131
+
132
+ const sanitize = (s: string) => s.replace(/[^a-z0-9_]/gi, '_').toLowerCase()
133
+ const nsStr = sanitize(ns)
134
+ const prefixStr = sanitize(pathPrefix)
135
+ const helperName = `__dyn_wrt_${nsStr}_${prefixStr}`
136
+
137
+ const qualifiedName = `${this.namespace}:${helperName}`
138
+
139
+ // Macro line: $data modify storage <ns> <pathPrefix>[$(arr_idx)] set value $(arr_val)
140
+ const macroLine: LIRInstr = {
141
+ kind: 'macro_line',
142
+ template: `data modify storage ${ns} ${pathPrefix}[$(arr_idx)] set value $(arr_val)`,
143
+ }
144
+
145
+ this.addFunction({
146
+ name: helperName,
147
+ instructions: [macroLine],
148
+ isMacro: true,
149
+ macroParams: ['arr_idx', 'arr_val'],
150
+ })
151
+
152
+ this.dynWrtHelpers.set(key, qualifiedName)
153
+ return qualifiedName
154
+ }
155
+
80
156
  /** Attach sourceLoc to newly added instructions (from the given start index onward) */
81
157
  tagSourceLoc(instrs: LIRInstr[], fromIndex: number, sourceLoc: SourceLoc | undefined): void {
82
158
  if (!sourceLoc) return
@@ -321,6 +397,46 @@ function lowerInstrInner(
321
397
  break
322
398
  }
323
399
 
400
+ case 'nbt_read_dynamic': {
401
+ // Strategy:
402
+ // 1. Store the index value into rs:macro_args __arr_idx (int)
403
+ // 2. Call the per-array macro helper function with 'with storage rs:macro_args'
404
+ // 3. Result comes back via $ret scoreboard slot (the macro uses $return)
405
+
406
+ const dst = ctx.slot(instr.dst)
407
+ const idxSlot = operandToSlot(instr.indexSrc, ctx, instrs)
408
+
409
+ // Step 1: store index score → rs:macro_args arr_idx (int, scale 1)
410
+ instrs.push({
411
+ kind: 'store_score_to_nbt',
412
+ ns: 'rs:macro_args',
413
+ path: 'arr_idx',
414
+ type: 'int',
415
+ scale: 1,
416
+ src: idxSlot,
417
+ })
418
+
419
+ // Step 2: get or create the macro helper function, then call it
420
+ const helperFn = ctx.getDynIdxHelper(instr.ns, instr.pathPrefix)
421
+ instrs.push({ kind: 'call_macro', fn: helperFn, storage: 'rs:macro_args' })
422
+
423
+ // Step 3: the macro uses $return which sets the MC return value.
424
+ // We need to capture that. In MC, $return run ... returns the result
425
+ // to the caller via the execute store mechanism.
426
+ // Use store_cmd_to_score to capture the return value of the macro call.
427
+ // Actually, the call_macro instruction above already ran the function.
428
+ // The $return run data get ... sets the scoreboard return value for the
429
+ // *calling* function context. We need to use execute store result score.
430
+ // Rewrite: use raw command instead:
431
+ instrs.pop() // remove the call_macro we just added
432
+ instrs.push({
433
+ kind: 'store_cmd_to_score',
434
+ dst,
435
+ cmd: { kind: 'call_macro', fn: helperFn, storage: 'rs:macro_args' },
436
+ })
437
+ break
438
+ }
439
+
324
440
  case 'nbt_write': {
325
441
  const srcSlot = operandToSlot(instr.src, ctx, instrs)
326
442
  instrs.push({
@@ -334,6 +450,41 @@ function lowerInstrInner(
334
450
  break
335
451
  }
336
452
 
453
+ case 'nbt_write_dynamic': {
454
+ // Strategy:
455
+ // 1. Store index score → rs:macro_args arr_idx (int)
456
+ // 2. Store value score → rs:macro_args arr_val (int)
457
+ // 3. Call macro helper: $data modify storage <ns> <pathPrefix>[$(arr_idx)] set value $(arr_val)
458
+
459
+ const idxSlot = operandToSlot(instr.indexSrc, ctx, instrs)
460
+ const valSlot = operandToSlot(instr.valueSrc, ctx, instrs)
461
+
462
+ // Store index
463
+ instrs.push({
464
+ kind: 'store_score_to_nbt',
465
+ ns: 'rs:macro_args',
466
+ path: 'arr_idx',
467
+ type: 'int',
468
+ scale: 1,
469
+ src: idxSlot,
470
+ })
471
+
472
+ // Store value
473
+ instrs.push({
474
+ kind: 'store_score_to_nbt',
475
+ ns: 'rs:macro_args',
476
+ path: 'arr_val',
477
+ type: 'int',
478
+ scale: 1,
479
+ src: valSlot,
480
+ })
481
+
482
+ // Call macro helper function
483
+ const helperFn = ctx.getDynWrtHelper(instr.ns, instr.pathPrefix)
484
+ instrs.push({ kind: 'call_macro', fn: helperFn, storage: 'rs:macro_args' })
485
+ break
486
+ }
487
+
337
488
  case 'score_read': {
338
489
  // execute store result score $dst __obj run scoreboard players get <player> <obj>
339
490
  const dst = ctx.slot(instr.dst)
package/src/mir/lower.ts CHANGED
@@ -1075,12 +1075,19 @@ function lowerExpr(
1075
1075
  }
1076
1076
 
1077
1077
  case 'index': {
1078
- // Check if obj is a tracked array variable with a constant index
1078
+ // Check if obj is a tracked array variable
1079
1079
  if (expr.obj.kind === 'ident') {
1080
1080
  const arrInfo = ctx.arrayVars.get(expr.obj.name)
1081
- if (arrInfo && expr.index.kind === 'int_lit') {
1081
+ if (arrInfo) {
1082
1082
  const t = ctx.freshTemp()
1083
- ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, scale: 1 })
1083
+ if (expr.index.kind === 'int_lit') {
1084
+ // Constant index: direct NBT read
1085
+ ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, scale: 1 })
1086
+ } else {
1087
+ // Dynamic index: emit nbt_read_dynamic
1088
+ const idxOp = lowerExpr(expr.index, ctx, scope)
1089
+ ctx.emit({ kind: 'nbt_read_dynamic', dst: t, ns: arrInfo.ns, pathPrefix: arrInfo.pathPrefix, indexSrc: idxOp })
1090
+ }
1084
1091
  return { kind: 'temp', name: t }
1085
1092
  }
1086
1093
  }
@@ -1091,6 +1098,25 @@ function lowerExpr(
1091
1098
  return { kind: 'temp', name: t }
1092
1099
  }
1093
1100
 
1101
+ case 'index_assign': {
1102
+ const valOp = lowerExpr(expr.value, ctx, scope)
1103
+ if (expr.obj.kind === 'ident') {
1104
+ const arrInfo = ctx.arrayVars.get(expr.obj.name)
1105
+ if (arrInfo) {
1106
+ if (expr.index.kind === 'int_lit') {
1107
+ // constant index → direct nbt_write
1108
+ ctx.emit({ kind: 'nbt_write', ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, type: 'int', scale: 1, src: valOp })
1109
+ } else {
1110
+ // dynamic index → nbt_write_dynamic
1111
+ const idxOp = lowerExpr(expr.index, ctx, scope)
1112
+ ctx.emit({ kind: 'nbt_write_dynamic', ns: arrInfo.ns, pathPrefix: arrInfo.pathPrefix, indexSrc: idxOp, valueSrc: valOp })
1113
+ }
1114
+ return valOp
1115
+ }
1116
+ }
1117
+ return valOp
1118
+ }
1119
+
1094
1120
  case 'call': {
1095
1121
  // Handle scoreboard_get / score — read from vanilla MC scoreboard
1096
1122
  if (expr.fn === 'scoreboard_get' || expr.fn === 'score') {
@@ -1112,6 +1138,52 @@ function lowerExpr(
1112
1138
  return { kind: 'temp', name: t }
1113
1139
  }
1114
1140
 
1141
+ // Handle list_push(arr_name, val) — append an int to an NBT int array
1142
+ // list_push("rs:lists", "mylist", val) or simpler: uses the array's storage path
1143
+ if (expr.fn === 'list_push') {
1144
+ // list_push(array_var, value)
1145
+ // 1. Append a placeholder 0
1146
+ // 2. Overwrite [-1] with the actual value
1147
+ if (expr.args[0].kind === 'ident') {
1148
+ const arrInfo = ctx.arrayVars.get((expr.args[0] as { kind: 'ident'; name: string }).name)
1149
+ if (arrInfo) {
1150
+ const valOp = lowerExpr(expr.args[1], ctx, scope)
1151
+ // Step 1: append placeholder
1152
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${arrInfo.ns} ${arrInfo.pathPrefix} append value 0`, args: [] })
1153
+ // Step 2: overwrite last element with actual value
1154
+ ctx.emit({ kind: 'nbt_write', ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[-1]`, type: 'int', scale: 1, src: valOp })
1155
+ const t = ctx.freshTemp()
1156
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1157
+ return { kind: 'temp', name: t }
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ // Handle list_pop(arr_var) — remove last element from NBT int array
1163
+ if (expr.fn === 'list_pop') {
1164
+ if (expr.args[0].kind === 'ident') {
1165
+ const arrInfo = ctx.arrayVars.get((expr.args[0] as { kind: 'ident'; name: string }).name)
1166
+ if (arrInfo) {
1167
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:data remove storage ${arrInfo.ns} ${arrInfo.pathPrefix}[-1]`, args: [] })
1168
+ const t = ctx.freshTemp()
1169
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1170
+ return { kind: 'temp', name: t }
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ // Handle list_len(arr_var) — get length of NBT int array
1176
+ if (expr.fn === 'list_len') {
1177
+ if (expr.args[0].kind === 'ident') {
1178
+ const arrInfo = ctx.arrayVars.get((expr.args[0] as { kind: 'ident'; name: string }).name)
1179
+ if (arrInfo) {
1180
+ const t = ctx.freshTemp()
1181
+ ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}`, scale: 1 })
1182
+ return { kind: 'temp', name: t }
1183
+ }
1184
+ }
1185
+ }
1186
+
1115
1187
  // Handle setTimeout/setInterval: lift lambda arg to a named helper function
1116
1188
  if ((expr.fn === 'setTimeout' || expr.fn === 'setInterval') && expr.args.length === 2) {
1117
1189
  const ticksArg = expr.args[0]
package/src/mir/macro.ts CHANGED
@@ -140,6 +140,11 @@ function scanExpr(expr: HIRExpr, paramNames: Set<string>, macroParams: Set<strin
140
140
  scanExpr(expr.obj, paramNames, macroParams)
141
141
  scanExpr(expr.value, paramNames, macroParams)
142
142
  break
143
+ case 'index_assign':
144
+ scanExpr(expr.obj, paramNames, macroParams)
145
+ scanExpr(expr.index, paramNames, macroParams)
146
+ scanExpr(expr.value, paramNames, macroParams)
147
+ break
143
148
  case 'member':
144
149
  scanExpr(expr.obj, paramNames, macroParams)
145
150
  break
package/src/mir/types.ts CHANGED
@@ -78,7 +78,9 @@ export type MIRInstr = MIRInstrBase & (
78
78
 
79
79
  // ── NBT storage ──────────────────────────────────────────────────────────
80
80
  | { kind: 'nbt_read'; dst: Temp; ns: string; path: string; scale: number }
81
+ | { kind: 'nbt_read_dynamic'; dst: Temp; ns: string; pathPrefix: string; indexSrc: Operand }
81
82
  | { kind: 'nbt_write'; ns: string; path: string; type: NBTType; scale: number; src: Operand }
83
+ | { kind: 'nbt_write_dynamic'; ns: string; pathPrefix: string; indexSrc: Operand; valueSrc: Operand }
82
84
 
83
85
  // ── Vanilla scoreboard interop ────────────────────────────────────────────
84
86
  | { kind: 'score_read'; dst: Temp; player: string; obj: string }
package/src/mir/verify.ts CHANGED
@@ -164,6 +164,7 @@ function getDst(instr: MIRInstr): Temp | null {
164
164
  case 'cmp':
165
165
  case 'and': case 'or': case 'not':
166
166
  case 'nbt_read':
167
+ case 'nbt_read_dynamic':
167
168
  return instr.dst
168
169
  case 'call':
169
170
  case 'call_macro':
@@ -194,9 +195,15 @@ function getUsedTemps(instr: MIRInstr): Temp[] {
194
195
  break
195
196
  case 'nbt_read':
196
197
  break
198
+ case 'nbt_read_dynamic':
199
+ temps.push(...getOperandTemps(instr.indexSrc))
200
+ break
197
201
  case 'nbt_write':
198
202
  temps.push(...getOperandTemps(instr.src))
199
203
  break
204
+ case 'nbt_write_dynamic':
205
+ temps.push(...getOperandTemps(instr.indexSrc), ...getOperandTemps(instr.valueSrc))
206
+ break
200
207
  case 'call':
201
208
  for (const arg of instr.args) temps.push(...getOperandTemps(arg))
202
209
  break
@@ -77,6 +77,10 @@ function rewriteUses(instr: MIRInstr, copies: Map<Temp, Operand>): MIRInstr {
77
77
  return { ...instr, a: resolve(instr.a, copies), b: resolve(instr.b, copies) }
78
78
  case 'nbt_write':
79
79
  return { ...instr, src: resolve(instr.src, copies) }
80
+ case 'nbt_write_dynamic':
81
+ return { ...instr, indexSrc: resolve(instr.indexSrc, copies), valueSrc: resolve(instr.valueSrc, copies) }
82
+ case 'nbt_read_dynamic':
83
+ return { ...instr, indexSrc: resolve(instr.indexSrc, copies) }
80
84
  case 'call':
81
85
  return { ...instr, args: instr.args.map(a => resolve(a, copies)) }
82
86
  case 'call_macro':
@@ -99,6 +103,7 @@ function getDst(instr: MIRInstr): Temp | null {
99
103
  case 'neg': case 'cmp':
100
104
  case 'and': case 'or': case 'not':
101
105
  case 'nbt_read':
106
+ case 'nbt_read_dynamic':
102
107
  return instr.dst
103
108
  case 'call': case 'call_macro':
104
109
  return instr.dst
@@ -946,8 +946,12 @@ function rewriteInstr(instr: MIRInstr, promoted: Map<Temp, Temp>): MIRInstr {
946
946
  return { ...instr, dst: rTemp(instr.dst), src: rOp(instr.src) }
947
947
  case 'nbt_read':
948
948
  return { ...instr, dst: rTemp(instr.dst) }
949
+ case 'nbt_read_dynamic':
950
+ return { ...instr, dst: rTemp(instr.dst), indexSrc: rOp(instr.indexSrc) }
949
951
  case 'nbt_write':
950
952
  return { ...instr, src: rOp(instr.src) }
953
+ case 'nbt_write_dynamic':
954
+ return { ...instr, indexSrc: rOp(instr.indexSrc), valueSrc: rOp(instr.valueSrc) }
951
955
  case 'call':
952
956
  return { ...instr, dst: instr.dst ? rTemp(instr.dst) : null, args: instr.args.map(rOp) }
953
957
  case 'call_macro':
@@ -999,6 +1003,7 @@ function getDst(instr: MIRInstr): Temp | null {
999
1003
  case 'neg': case 'cmp':
1000
1004
  case 'and': case 'or': case 'not':
1001
1005
  case 'nbt_read':
1006
+ case 'nbt_read_dynamic':
1002
1007
  return instr.dst
1003
1008
  case 'call': case 'call_macro':
1004
1009
  return instr.dst
@@ -1019,6 +1024,10 @@ function getUsedTemps(instr: MIRInstr): Temp[] {
1019
1024
  addOp(instr.a); addOp(instr.b); break
1020
1025
  case 'nbt_write':
1021
1026
  addOp(instr.src); break
1027
+ case 'nbt_write_dynamic':
1028
+ addOp(instr.indexSrc); addOp(instr.valueSrc); break
1029
+ case 'nbt_read_dynamic':
1030
+ addOp(instr.indexSrc); break
1022
1031
  case 'call':
1023
1032
  instr.args.forEach(addOp); break
1024
1033
  case 'call_macro':
@@ -79,6 +79,7 @@ function recomputePreds(blocks: MIRBlock[]): MIRBlock[] {
79
79
  function hasSideEffects(instr: MIRInstr): boolean {
80
80
  if (instr.kind === 'call' || instr.kind === 'call_macro' ||
81
81
  instr.kind === 'call_context' || instr.kind === 'nbt_write' ||
82
+ instr.kind === 'nbt_write_dynamic' ||
82
83
  instr.kind === 'score_write') return true
83
84
  // Return field temps (__rf_) write to global return slots — not dead even if unused locally
84
85
  // Option slot temps (__opt_) write observable scoreboard state — preserve even if var unused
@@ -102,6 +103,7 @@ function getDst(instr: MIRInstr): Temp | null {
102
103
  case 'neg': case 'cmp':
103
104
  case 'and': case 'or': case 'not':
104
105
  case 'nbt_read':
106
+ case 'nbt_read_dynamic':
105
107
  return instr.dst
106
108
  case 'call': case 'call_macro':
107
109
  return instr.dst
@@ -124,6 +126,10 @@ function getUsedTemps(instr: MIRInstr): Temp[] {
124
126
  addOp(instr.a); addOp(instr.b); break
125
127
  case 'nbt_write':
126
128
  addOp(instr.src); break
129
+ case 'nbt_write_dynamic':
130
+ addOp(instr.indexSrc); addOp(instr.valueSrc); break
131
+ case 'nbt_read_dynamic':
132
+ addOp(instr.indexSrc); break
127
133
  case 'call':
128
134
  instr.args.forEach(addOp); break
129
135
  case 'call_macro':
@@ -334,6 +334,8 @@ function substituteInstr(instr: MIRInstr, sub: Map<Temp, Operand>): MIRInstr {
334
334
  return { ...instr, a: substituteOp(instr.a, sub), b: substituteOp(instr.b, sub) }
335
335
  case 'nbt_write':
336
336
  return { ...instr, src: substituteOp(instr.src, sub) }
337
+ case 'nbt_write_dynamic':
338
+ return { ...instr, indexSrc: substituteOp(instr.indexSrc, sub), valueSrc: substituteOp(instr.valueSrc, sub) }
337
339
  case 'call':
338
340
  return { ...instr, args: instr.args.map(a => substituteOp(a, sub)) }
339
341
  case 'call_macro':
@@ -357,6 +359,7 @@ function getInstrDst(instr: MIRInstr): Temp | null {
357
359
  case 'add': case 'sub': case 'mul': case 'div': case 'mod':
358
360
  case 'neg': case 'cmp': case 'and': case 'or': case 'not':
359
361
  case 'nbt_read':
362
+ case 'nbt_read_dynamic':
360
363
  return instr.dst
361
364
  case 'call': case 'call_macro':
362
365
  return instr.dst
@@ -1162,6 +1162,15 @@ export class Parser {
1162
1162
  this.getLocToken(left) ?? token
1163
1163
  )
1164
1164
  }
1165
+
1166
+ // Index assignment: arr[0] = val, arr[i] = val
1167
+ if (left.kind === 'index') {
1168
+ const value = this.parseAssignment()
1169
+ return this.withLoc(
1170
+ { kind: 'index_assign', obj: left.obj, index: left.index, op, value },
1171
+ this.getLocToken(left) ?? token
1172
+ )
1173
+ }
1165
1174
  }
1166
1175
 
1167
1176
  return left