redscript-mc 2.3.0 → 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 (45) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/src/__tests__/array-dynamic.test.d.ts +12 -0
  3. package/dist/src/__tests__/array-dynamic.test.js +131 -0
  4. package/dist/src/__tests__/array-write.test.d.ts +11 -0
  5. package/dist/src/__tests__/array-write.test.js +149 -0
  6. package/dist/src/ast/types.d.ts +7 -0
  7. package/dist/src/emit/modules.js +5 -0
  8. package/dist/src/hir/lower.js +29 -0
  9. package/dist/src/hir/monomorphize.js +2 -0
  10. package/dist/src/hir/types.d.ts +9 -2
  11. package/dist/src/lir/lower.js +131 -0
  12. package/dist/src/mir/lower.js +73 -3
  13. package/dist/src/mir/macro.js +5 -0
  14. package/dist/src/mir/types.d.ts +12 -0
  15. package/dist/src/mir/verify.js +7 -0
  16. package/dist/src/optimizer/copy_prop.js +5 -0
  17. package/dist/src/optimizer/coroutine.js +12 -0
  18. package/dist/src/optimizer/dce.js +9 -0
  19. package/dist/src/optimizer/unroll.js +3 -0
  20. package/dist/src/parser/index.js +5 -0
  21. package/dist/src/typechecker/index.js +5 -0
  22. package/editors/vscode/package-lock.json +3 -3
  23. package/editors/vscode/package.json +1 -1
  24. package/package.json +1 -1
  25. package/src/__tests__/array-dynamic.test.ts +147 -0
  26. package/src/__tests__/array-write.test.ts +169 -0
  27. package/src/ast/types.ts +1 -0
  28. package/src/emit/modules.ts +5 -0
  29. package/src/hir/lower.ts +30 -0
  30. package/src/hir/monomorphize.ts +2 -0
  31. package/src/hir/types.ts +3 -1
  32. package/src/lir/lower.ts +151 -0
  33. package/src/mir/lower.ts +75 -3
  34. package/src/mir/macro.ts +5 -0
  35. package/src/mir/types.ts +2 -0
  36. package/src/mir/verify.ts +7 -0
  37. package/src/optimizer/copy_prop.ts +5 -0
  38. package/src/optimizer/coroutine.ts +9 -0
  39. package/src/optimizer/dce.ts +6 -0
  40. package/src/optimizer/unroll.ts +3 -0
  41. package/src/parser/index.ts +9 -0
  42. package/src/stdlib/list.mcrs +43 -72
  43. package/src/stdlib/math.mcrs +137 -0
  44. package/src/stdlib/timer.mcrs +32 -0
  45. package/src/typechecker/index.ts +6 -0
package/src/hir/lower.ts CHANGED
@@ -409,6 +409,36 @@ function lowerExpr(expr: Expr): HIRExpr {
409
409
  case 'index':
410
410
  return { kind: 'index', obj: lowerExpr(expr.obj), index: lowerExpr(expr.index), span: expr.span }
411
411
 
412
+ // --- Desugaring: compound index_assign → plain index_assign ---
413
+ case 'index_assign':
414
+ if (expr.op !== '=') {
415
+ const binOp = COMPOUND_TO_BINOP[expr.op]
416
+ const obj = lowerExpr(expr.obj)
417
+ const index = lowerExpr(expr.index)
418
+ return {
419
+ kind: 'index_assign',
420
+ obj,
421
+ index,
422
+ op: '=' as const,
423
+ value: {
424
+ kind: 'binary',
425
+ op: binOp as any,
426
+ left: { kind: 'index', obj, index },
427
+ right: lowerExpr(expr.value),
428
+ span: expr.span,
429
+ },
430
+ span: expr.span,
431
+ }
432
+ }
433
+ return {
434
+ kind: 'index_assign',
435
+ obj: lowerExpr(expr.obj),
436
+ index: lowerExpr(expr.index),
437
+ op: expr.op,
438
+ value: lowerExpr(expr.value),
439
+ span: expr.span,
440
+ }
441
+
412
442
  case 'call':
413
443
  return { kind: 'call', fn: expr.fn, args: expr.args.map(lowerExpr), typeArgs: expr.typeArgs, span: expr.span }
414
444
 
@@ -312,6 +312,8 @@ class Monomorphizer {
312
312
  return { ...expr, value: this.rewriteExpr(expr.value, ctx) }
313
313
  case 'member_assign':
314
314
  return { ...expr, obj: this.rewriteExpr(expr.obj, ctx), value: this.rewriteExpr(expr.value, ctx) }
315
+ case 'index_assign':
316
+ return { ...expr, obj: this.rewriteExpr(expr.obj, ctx), index: this.rewriteExpr(expr.index, ctx), value: this.rewriteExpr(expr.value, ctx) }
315
317
  case 'member':
316
318
  return { ...expr, obj: this.rewriteExpr(expr.obj, ctx) }
317
319
  case 'index':
package/src/hir/types.ts CHANGED
@@ -24,7 +24,7 @@ import type {
24
24
  EntityTypeName,
25
25
  LambdaParam,
26
26
  } from '../ast/types'
27
- import type { BinOp, CmpOp } from '../ast/types'
27
+ import type { BinOp, CmpOp, AssignOp } from '../ast/types'
28
28
 
29
29
  // Re-export types that HIR shares with AST unchanged
30
30
  export type {
@@ -40,6 +40,7 @@ export type {
40
40
  LambdaParam,
41
41
  BinOp,
42
42
  CmpOp,
43
+ AssignOp,
43
44
  }
44
45
 
45
46
  // ---------------------------------------------------------------------------
@@ -77,6 +78,7 @@ export type HIRExpr =
77
78
  // Assignment — only plain '=' (compound ops desugared)
78
79
  | { kind: 'assign'; target: string; value: HIRExpr; span?: Span }
79
80
  | { kind: 'member_assign'; obj: HIRExpr; field: string; value: HIRExpr; span?: Span }
81
+ | { kind: 'index_assign'; obj: HIRExpr; index: HIRExpr; op: AssignOp; value: HIRExpr; span?: Span }
80
82
  // Access
81
83
  | { kind: 'member'; obj: HIRExpr; field: string; span?: Span }
82
84
  | { kind: 'index'; obj: HIRExpr; index: HIRExpr; span?: Span }
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
@@ -1,45 +1,29 @@
1
- // list.mcrs — Dynamic NBT-backed integer list operations.
1
+ // list.mcrs — List and array utilities for RedScript datapacks.
2
2
  //
3
- // Lists are stored in NBT storage as int arrays.
4
- // The storage namespace is "rs:lists" and path is the variable name.
3
+ // NOTE: Array parameters cannot be passed to functions by reference in
4
+ // the current RedScript implementation. Functions that operate on arrays
5
+ // must be inlined by the user, OR use fixed-size static helpers below.
5
6
  //
6
- // IMPORTANT: These functions use raw() to emit NBT commands.
7
- // They operate on a NAMED list stored at a specific NBT path.
8
- // The caller must pass the list path as a namespace:path string.
7
+ // For dynamic array operations (sort, search, sum over variable-length arrays),
8
+ // write the loop directly in your code:
9
9
  //
10
- // Example usage:
11
- // // Create a list (declare as int[])
12
- // let mylist: int[] = [1, 2, 3];
10
+ // let nums: int[] = [30, 10, 20];
11
+ // // Manual bubble sort (2 elements):
12
+ // if (nums[0] > nums[1]) {
13
+ // let tmp: int = nums[0];
14
+ // nums[0] = nums[1];
15
+ // nums[1] = tmp;
16
+ // }
13
17
  //
14
- // // Use list_* functions with raw() for dynamic operations.
15
- // // Note: push/pop/len use raw() NBT commands directly.
16
- //
17
- // For fixed-size arrays, use int[] literals directly.
18
- // For dynamic lists, use the functions below.
19
- //
20
- // Storage convention: lists live at storage rs:lists <varname>
18
+ // The static helpers below work on up to 5 discrete values passed as arguments.
21
19
 
22
20
  module library;
23
21
 
24
- // list_len_of(ns, path): count elements in an NBT int list.
25
- // Returns the count stored in a scoreboard temp.
26
- // Use: raw("execute store result score $len __ns run data get storage ns:name path")
27
- // This is a convenience wrapper — in practice use raw() directly.
28
-
29
- // list_sum(a, b, c, d, e): sum of up to 5 int values (utility)
30
- fn list_sum5(a: int, b: int, c: int, d: int, e: int): int {
31
- return a + b + c + d + e;
32
- }
22
+ // ─── Static min/max/avg ──────────────────────────────────────────────────────
33
23
 
34
- fn list_sum4(a: int, b: int, c: int, d: int): int {
35
- return a + b + c + d;
36
- }
37
-
38
- fn list_sum3(a: int, b: int, c: int): int {
39
- return a + b + c;
40
- }
24
+ fn sort2_min(a: int, b: int): int { if (a <= b) { return a; } return b; }
25
+ fn sort2_max(a: int, b: int): int { if (a >= b) { return a; } return b; }
41
26
 
42
- // list_min3 / list_max3: min/max over 3 values (useful for array-derived work)
43
27
  fn list_min3(a: int, b: int, c: int): int {
44
28
  let m: int = a;
45
29
  if (b < m) { m = b; }
@@ -72,54 +56,41 @@ fn list_max5(a: int, b: int, c: int, d: int, e: int): int {
72
56
  return m;
73
57
  }
74
58
 
75
- // ─── Sorting (in-register, fixed size) ───────────────────────────────────────
59
+ fn list_sum5(a: int, b: int, c: int, d: int, e: int): int { return a + b + c + d + e; }
60
+ fn list_sum4(a: int, b: int, c: int, d: int): int { return a + b + c + d; }
61
+ fn list_sum3(a: int, b: int, c: int): int { return a + b + c; }
62
+ fn avg3(a: int, b: int, c: int): int { return (a + b + c) / 3; }
63
+ fn avg5(a: int, b: int, c: int, d: int, e: int): int { return (a + b + c + d + e) / 5; }
76
64
 
77
- // sort3_asc: sort 3 values ascending, returns packed as 3 separate ints via
78
- // side-effect scoreboard. Caller reads $sort3_a, $sort3_b, $sort3_c.
79
- // (This is a placeholder — proper NBT list sort requires runtime support.)
65
+ // ─── Static sort (sorting network) ───────────────────────────────────────────
80
66
 
81
- // swap helper: sort2 returns the smaller value
82
- fn sort2_min(a: int, b: int): int {
83
- if (a <= b) { return a; }
84
- return b;
85
- }
86
-
87
- fn sort2_max(a: int, b: int): int {
88
- if (a >= b) { return a; }
89
- return b;
90
- }
91
-
92
- // bubble_sort3: returns sorted value at position pos (0=min, 1=mid, 2=max)
67
+ // sort3(a, b, c, pos): return sorted value at position pos (0=min, 1=mid, 2=max)
93
68
  fn sort3(a: int, b: int, c: int, pos: int): int {
94
- // Sorting network for 3 elements
95
- let x: int = a;
96
- let y: int = b;
97
- let z: int = c;
98
- // Step 1: compare-swap (x, y)
99
- if (x > y) { let tmp: int = x; x = y; y = tmp; }
100
- // Step 2: compare-swap (y, z)
101
- if (y > z) { let tmp: int = y; y = z; z = tmp; }
102
- // Step 3: compare-swap (x, y)
103
- if (x > y) { let tmp: int = x; x = y; y = tmp; }
69
+ let x: int = a; let y: int = b; let z: int = c;
70
+ if (x > y) { let t: int = x; x = y; y = t; }
71
+ if (y > z) { let t: int = y; y = z; z = t; }
72
+ if (x > y) { let t: int = x; x = y; y = t; }
104
73
  if (pos == 0) { return x; }
105
74
  if (pos == 1) { return y; }
106
75
  return z;
107
76
  }
108
77
 
109
- // ─── Statistics ──────────────────────────────────────────────────────────────
110
-
111
- // avg3(a, b, c): integer average (truncated)
112
- fn avg3(a: int, b: int, c: int): int {
113
- return (a + b + c) / 3;
114
- }
78
+ // ─── Weighted random choice ───────────────────────────────────────────────────
115
79
 
116
- fn avg5(a: int, b: int, c: int, d: int, e: int): int {
117
- return (a + b + c + d + e) / 5;
80
+ fn weighted2(seed: int, w0: int, w1: int): int {
81
+ let total: int = w0 + w1;
82
+ let r: int = seed * 1664525 + 1013904223;
83
+ if (r < 0) { r = 0 - r; }
84
+ if (r % total < w0) { return 0; }
85
+ return 1;
118
86
  }
119
87
 
120
- // clamp_list3(a, b, c, lo, hi): clamp all 3 values
121
- fn clamp3(a: int, lo: int, hi: int): int {
122
- if (a < lo) { return lo; }
123
- if (a > hi) { return hi; }
124
- return a;
88
+ fn weighted3(seed: int, w0: int, w1: int, w2: int): int {
89
+ let total: int = w0 + w1 + w2;
90
+ let r: int = seed * 1664525 + 1013904223;
91
+ if (r < 0) { r = 0 - r; }
92
+ let v: int = r % total;
93
+ if (v < w0) { return 0; }
94
+ if (v < w0 + w1) { return 1; }
95
+ return 2;
125
96
  }