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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to RedScript will be documented in this file.
4
4
 
5
+ ## [2.4.0] - 2026-03-17
6
+
7
+ ### Added
8
+ - Dynamic array index read: `arr[i]` where `i` is a variable (MC Function Macro, MC 1.20.2+)
9
+ - Dynamic array index write: `arr[i] = val`, `arr[i] += val` compound assignment
10
+ - `list_push(arr, val)` / `list_pop(arr)` / `list_len(arr)` builtins for NBT array manipulation
11
+
12
+ ### Known Limitations
13
+ - Array parameters in function calls do not pass the array by reference yet; use `while` loops with dynamic index directly in the calling scope
14
+ - `for` loops with dynamic array access may incorrectly inline when loop bounds are constants; use `while` loops instead
15
+
5
16
  ## [2.3.0] - 2026-03-17
6
17
 
7
18
  ### Added
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Tests for dynamic array index read: arr[i] where i is a variable.
3
+ *
4
+ * Covers:
5
+ * - MIR: nbt_read_dynamic instruction is emitted instead of falling back to
6
+ * copy(obj) (which returned the array length, not the value)
7
+ * - LIR/Emit: generates a macro helper function and calls it with
8
+ * `function ns:__dyn_idx_... with storage rs:macro_args`
9
+ * - The generated .mcfunction contains 'with storage' (function macro call)
10
+ * - The helper function contains the $return macro line
11
+ */
12
+ export {};
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for dynamic array index read: arr[i] where i is a variable.
4
+ *
5
+ * Covers:
6
+ * - MIR: nbt_read_dynamic instruction is emitted instead of falling back to
7
+ * copy(obj) (which returned the array length, not the value)
8
+ * - LIR/Emit: generates a macro helper function and calls it with
9
+ * `function ns:__dyn_idx_... with storage rs:macro_args`
10
+ * - The generated .mcfunction contains 'with storage' (function macro call)
11
+ * - The helper function contains the $return macro line
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const compile_1 = require("../emit/compile");
15
+ // Helper: find file in compiled output by path substring
16
+ function getFile(files, pathSubstr) {
17
+ const f = files.find(f => f.path.includes(pathSubstr));
18
+ return f?.content;
19
+ }
20
+ // Helper: get the content of the function file for `fnName` in namespace
21
+ function getFunctionBody(files, fnName, ns = 'test') {
22
+ const content = getFile(files, `${fnName}.mcfunction`);
23
+ if (!content) {
24
+ // list files for debug
25
+ const paths = files.map(f => f.path).join('\n');
26
+ throw new Error(`Function '${fnName}' not found in output. Files:\n${paths}`);
27
+ }
28
+ return content;
29
+ }
30
+ describe('Dynamic array index read: arr[i]', () => {
31
+ const src = `
32
+ fn test() {
33
+ let nums: int[] = [10, 20, 30, 40, 50];
34
+ let i: int = 2;
35
+ i = i + 1;
36
+ let v: int = nums[i];
37
+ scoreboard_set("#out", "test", v);
38
+ }
39
+ `;
40
+ let files;
41
+ beforeAll(() => {
42
+ const result = (0, compile_1.compile)(src, { namespace: 'test' });
43
+ files = result.files;
44
+ });
45
+ test('compiles without error', () => {
46
+ // beforeAll would have thrown if compilation failed
47
+ expect(files.length).toBeGreaterThan(0);
48
+ });
49
+ test('test function contains "with storage" (macro call)', () => {
50
+ const body = getFunctionBody(files, 'test');
51
+ expect(body).toContain('with storage');
52
+ });
53
+ test('test function does NOT contain fallback "scoreboard players set #out test 5"', () => {
54
+ // Old fallback would copy the array length (5 elements) as the result
55
+ const body = getFunctionBody(files, 'test');
56
+ expect(body).not.toContain('scoreboard players set #out test 5');
57
+ });
58
+ test('a macro helper function is generated for the array', () => {
59
+ // Should have a function file matching __dyn_idx_
60
+ const helperFile = files.find(f => f.path.includes('__dyn_idx_'));
61
+ expect(helperFile).toBeDefined();
62
+ });
63
+ test('macro helper function contains $return run data get', () => {
64
+ const helperFile = files.find(f => f.path.includes('__dyn_idx_'));
65
+ expect(helperFile).toBeDefined();
66
+ expect(helperFile.content).toContain('$return run data get');
67
+ expect(helperFile.content).toContain('$(arr_idx)');
68
+ });
69
+ test('macro helper function references the correct array path (nums)', () => {
70
+ const helperFile = files.find(f => f.path.includes('__dyn_idx_'));
71
+ expect(helperFile).toBeDefined();
72
+ expect(helperFile.content).toContain('nums[$(arr_idx)]');
73
+ });
74
+ test('test function stores index to rs:macro_args', () => {
75
+ const body = getFunctionBody(files, 'test');
76
+ // Should store the index value into rs:macro_args arr_idx
77
+ expect(body).toContain('rs:macro_args');
78
+ });
79
+ });
80
+ describe('Dynamic array index: constant index still uses direct nbt_read', () => {
81
+ const src = `
82
+ fn test_const() {
83
+ let nums: int[] = [10, 20, 30];
84
+ let v: int = nums[1];
85
+ scoreboard_set("#out", "test", v);
86
+ }
87
+ `;
88
+ let files;
89
+ beforeAll(() => {
90
+ const result = (0, compile_1.compile)(src, { namespace: 'test' });
91
+ files = result.files;
92
+ });
93
+ test('constant index does NOT generate macro call (uses direct data get)', () => {
94
+ const body = getFunctionBody(files, 'test_const');
95
+ // Direct nbt_read emits store_nbt_to_score → execute store result score ... run data get ...
96
+ // without 'with storage'
97
+ expect(body).not.toContain('with storage');
98
+ expect(body).toContain('data get storage');
99
+ expect(body).toContain('nums[1]');
100
+ });
101
+ });
102
+ describe('Dynamic array index: multiple arrays, separate helpers', () => {
103
+ const src = `
104
+ fn test_multi() {
105
+ let a: int[] = [1, 2, 3];
106
+ let b: int[] = [10, 20, 30];
107
+ let i: int = 1;
108
+ i = i + 0;
109
+ let va: int = a[i];
110
+ let vb: int = b[i];
111
+ scoreboard_set("#va", "test", va);
112
+ scoreboard_set("#vb", "test", vb);
113
+ }
114
+ `;
115
+ let files;
116
+ beforeAll(() => {
117
+ const result = (0, compile_1.compile)(src, { namespace: 'test' });
118
+ files = result.files;
119
+ });
120
+ test('two separate macro helpers are generated for arrays a and b', () => {
121
+ const helperFiles = files.filter(f => f.path.includes('__dyn_idx_'));
122
+ expect(helperFiles.length).toBe(2);
123
+ });
124
+ test('each helper references its respective array path', () => {
125
+ const helperFiles = files.filter(f => f.path.includes('__dyn_idx_'));
126
+ const contents = helperFiles.map(f => f.content).join('\n');
127
+ expect(contents).toContain('a[$(arr_idx)]');
128
+ expect(contents).toContain('b[$(arr_idx)]');
129
+ });
130
+ });
131
+ //# sourceMappingURL=array-dynamic.test.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for array index write: arr[i] = val (constant and dynamic index).
3
+ *
4
+ * Covers:
5
+ * - Parser: arr[i] = val parses as index_assign (no "Expected ';'" error)
6
+ * - MIR: constant index → nbt_write, dynamic index → nbt_write_dynamic
7
+ * - LIR/Emit: constant index uses store_score_to_nbt to path[N]
8
+ * dynamic index generates a macro helper function for write
9
+ * - Compound assignments: arr[i] += 5 desugars to read + write
10
+ */
11
+ export {};
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for array index write: arr[i] = val (constant and dynamic index).
4
+ *
5
+ * Covers:
6
+ * - Parser: arr[i] = val parses as index_assign (no "Expected ';'" error)
7
+ * - MIR: constant index → nbt_write, dynamic index → nbt_write_dynamic
8
+ * - LIR/Emit: constant index uses store_score_to_nbt to path[N]
9
+ * dynamic index generates a macro helper function for write
10
+ * - Compound assignments: arr[i] += 5 desugars to read + write
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const compile_1 = require("../emit/compile");
14
+ // Helper: find file in compiled output by path substring
15
+ function getFile(files, pathSubstr) {
16
+ const f = files.find(f => f.path.includes(pathSubstr));
17
+ return f?.content;
18
+ }
19
+ // Helper: get the content of the function file for `fnName` in namespace
20
+ function getFunctionBody(files, fnName, ns = 'test') {
21
+ const content = getFile(files, `${fnName}.mcfunction`);
22
+ if (!content) {
23
+ const paths = files.map(f => f.path).join('\n');
24
+ throw new Error(`Function '${fnName}' not found in output. Files:\n${paths}`);
25
+ }
26
+ return content;
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Constant index write: arr[1] = 99
30
+ // ---------------------------------------------------------------------------
31
+ describe('Constant index write: arr[1] = 99', () => {
32
+ const src = `
33
+ fn test() {
34
+ let nums: int[] = [10, 20, 30];
35
+ nums[1] = 99;
36
+ scoreboard_set("#out", "test", nums[1]);
37
+ }
38
+ `;
39
+ let files;
40
+ beforeAll(() => {
41
+ const result = (0, compile_1.compile)(src, { namespace: 'test' });
42
+ files = result.files;
43
+ });
44
+ test('compiles without error', () => {
45
+ expect(files.length).toBeGreaterThan(0);
46
+ });
47
+ test('test function contains nbt store to array path [1] (constant write)', () => {
48
+ const body = getFunctionBody(files, 'test');
49
+ // Should write to path like "nums[1]" via execute store result storage
50
+ expect(body).toMatch(/nums\[1\]/);
51
+ });
52
+ test('test function reads back from nums[1] after writing', () => {
53
+ const body = getFunctionBody(files, 'test');
54
+ // Should also read nums[1] for scoreboard_set
55
+ expect(body).toMatch(/nums\[1\]/);
56
+ });
57
+ });
58
+ // ---------------------------------------------------------------------------
59
+ // Dynamic index write: arr[i] = 99
60
+ // ---------------------------------------------------------------------------
61
+ describe('Dynamic index write: arr[i] = 99', () => {
62
+ const src = `
63
+ fn test() {
64
+ let nums: int[] = [10, 20, 30];
65
+ let i: int = 1;
66
+ nums[i] = 99;
67
+ scoreboard_set("#out", "test", nums[i]);
68
+ }
69
+ `;
70
+ let files;
71
+ beforeAll(() => {
72
+ const result = (0, compile_1.compile)(src, { namespace: 'test' });
73
+ files = result.files;
74
+ });
75
+ test('compiles without error', () => {
76
+ expect(files.length).toBeGreaterThan(0);
77
+ });
78
+ test('test function contains "with storage" (macro call for write)', () => {
79
+ const body = getFunctionBody(files, 'test');
80
+ expect(body).toContain('with storage');
81
+ });
82
+ test('a __dyn_wrt_ helper function is generated', () => {
83
+ const helperFile = files.find(f => f.path.includes('__dyn_wrt_'));
84
+ expect(helperFile).toBeDefined();
85
+ });
86
+ test('the write helper contains a macro line with arr_idx and arr_val', () => {
87
+ const helperFile = files.find(f => f.path.includes('__dyn_wrt_'));
88
+ expect(helperFile).toBeDefined();
89
+ expect(helperFile.content).toContain('$(arr_idx)');
90
+ expect(helperFile.content).toContain('$(arr_val)');
91
+ });
92
+ test('the write helper uses data modify set value', () => {
93
+ const helperFile = files.find(f => f.path.includes('__dyn_wrt_'));
94
+ expect(helperFile.content).toContain('data modify storage');
95
+ expect(helperFile.content).toContain('set value');
96
+ });
97
+ });
98
+ // ---------------------------------------------------------------------------
99
+ // Compound assignment: arr[i] += 5
100
+ // ---------------------------------------------------------------------------
101
+ describe('Compound index assignment: arr[i] += 5', () => {
102
+ const src = `
103
+ fn test() {
104
+ let nums: int[] = [10, 20, 30];
105
+ let i: int = 0;
106
+ nums[i] += 5;
107
+ scoreboard_set("#out", "test", nums[i]);
108
+ }
109
+ `;
110
+ let files;
111
+ beforeAll(() => {
112
+ const result = (0, compile_1.compile)(src, { namespace: 'test' });
113
+ files = result.files;
114
+ });
115
+ test('compiles without error', () => {
116
+ expect(files.length).toBeGreaterThan(0);
117
+ });
118
+ test('compound assignment generates both read and write macro calls', () => {
119
+ const body = getFunctionBody(files, 'test');
120
+ // Should call with storage at least twice (read for += and write + scoreboard_set read)
121
+ const matches = (body.match(/with storage/g) || []).length;
122
+ expect(matches).toBeGreaterThanOrEqual(1);
123
+ });
124
+ });
125
+ // ---------------------------------------------------------------------------
126
+ // Constant compound assignment: arr[0] += 5
127
+ // ---------------------------------------------------------------------------
128
+ describe('Constant compound index assignment: arr[0] += 5', () => {
129
+ const src = `
130
+ fn test() {
131
+ let nums: int[] = [10, 20, 30];
132
+ nums[0] += 5;
133
+ scoreboard_set("#out", "test", nums[0]);
134
+ }
135
+ `;
136
+ let files;
137
+ beforeAll(() => {
138
+ const result = (0, compile_1.compile)(src, { namespace: 'test' });
139
+ files = result.files;
140
+ });
141
+ test('compiles without error', () => {
142
+ expect(files.length).toBeGreaterThan(0);
143
+ });
144
+ test('test function contains array path [0] for read and write', () => {
145
+ const body = getFunctionBody(files, 'test');
146
+ expect(body).toMatch(/nums\[0\]/);
147
+ });
148
+ });
149
+ //# sourceMappingURL=array-write.test.js.map
@@ -228,6 +228,13 @@ export type Expr = {
228
228
  obj: Expr;
229
229
  index: Expr;
230
230
  span?: Span;
231
+ } | {
232
+ kind: 'index_assign';
233
+ obj: Expr;
234
+ index: Expr;
235
+ op: AssignOp;
236
+ value: Expr;
237
+ span?: Span;
231
238
  } | {
232
239
  kind: 'array_lit';
233
240
  elements: Expr[];
@@ -465,6 +465,11 @@ function rewriteExpr(expr, symbolMap) {
465
465
  rewriteExpr(expr.obj, symbolMap);
466
466
  rewriteExpr(expr.index, symbolMap);
467
467
  break;
468
+ case 'index_assign':
469
+ rewriteExpr(expr.obj, symbolMap);
470
+ rewriteExpr(expr.index, symbolMap);
471
+ rewriteExpr(expr.value, symbolMap);
472
+ break;
468
473
  case 'array_lit':
469
474
  for (const el of expr.elements)
470
475
  rewriteExpr(el, symbolMap);
@@ -360,6 +360,35 @@ function lowerExpr(expr) {
360
360
  return { kind: 'member', obj: lowerExpr(expr.obj), field: expr.field, span: expr.span };
361
361
  case 'index':
362
362
  return { kind: 'index', obj: lowerExpr(expr.obj), index: lowerExpr(expr.index), span: expr.span };
363
+ // --- Desugaring: compound index_assign → plain index_assign ---
364
+ case 'index_assign':
365
+ if (expr.op !== '=') {
366
+ const binOp = COMPOUND_TO_BINOP[expr.op];
367
+ const obj = lowerExpr(expr.obj);
368
+ const index = lowerExpr(expr.index);
369
+ return {
370
+ kind: 'index_assign',
371
+ obj,
372
+ index,
373
+ op: '=',
374
+ value: {
375
+ kind: 'binary',
376
+ op: binOp,
377
+ left: { kind: 'index', obj, index },
378
+ right: lowerExpr(expr.value),
379
+ span: expr.span,
380
+ },
381
+ span: expr.span,
382
+ };
383
+ }
384
+ return {
385
+ kind: 'index_assign',
386
+ obj: lowerExpr(expr.obj),
387
+ index: lowerExpr(expr.index),
388
+ op: expr.op,
389
+ value: lowerExpr(expr.value),
390
+ span: expr.span,
391
+ };
363
392
  case 'call':
364
393
  return { kind: 'call', fn: expr.fn, args: expr.args.map(lowerExpr), typeArgs: expr.typeArgs, span: expr.span };
365
394
  case 'invoke':
@@ -278,6 +278,8 @@ class Monomorphizer {
278
278
  return { ...expr, value: this.rewriteExpr(expr.value, ctx) };
279
279
  case 'member_assign':
280
280
  return { ...expr, obj: this.rewriteExpr(expr.obj, ctx), value: this.rewriteExpr(expr.value, ctx) };
281
+ case 'index_assign':
282
+ return { ...expr, obj: this.rewriteExpr(expr.obj, ctx), index: this.rewriteExpr(expr.index, ctx), value: this.rewriteExpr(expr.value, ctx) };
281
283
  case 'member':
282
284
  return { ...expr, obj: this.rewriteExpr(expr.obj, ctx) };
283
285
  case 'index':
@@ -12,8 +12,8 @@
12
12
  * All types and names are preserved from the AST.
13
13
  */
14
14
  import type { Span, TypeNode, EntitySelector, CoordComponent, Decorator, RangeExpr, FStringPart, SelectorFilter, EntityTypeName, LambdaParam } from '../ast/types';
15
- import type { BinOp, CmpOp } from '../ast/types';
16
- export type { Span, TypeNode, EntitySelector, CoordComponent, Decorator, RangeExpr, FStringPart, SelectorFilter, EntityTypeName, LambdaParam, BinOp, CmpOp, };
15
+ import type { BinOp, CmpOp, AssignOp } from '../ast/types';
16
+ export type { Span, TypeNode, EntitySelector, CoordComponent, Decorator, RangeExpr, FStringPart, SelectorFilter, EntityTypeName, LambdaParam, BinOp, CmpOp, AssignOp, };
17
17
  export type HIRExpr = {
18
18
  kind: 'int_lit';
19
19
  value: number;
@@ -124,6 +124,13 @@ export type HIRExpr = {
124
124
  field: string;
125
125
  value: HIRExpr;
126
126
  span?: Span;
127
+ } | {
128
+ kind: 'index_assign';
129
+ obj: HIRExpr;
130
+ index: HIRExpr;
131
+ op: AssignOp;
132
+ value: HIRExpr;
133
+ span?: Span;
127
134
  } | {
128
135
  kind: 'member';
129
136
  obj: HIRExpr;
@@ -41,6 +41,10 @@ class LoweringContext {
41
41
  this.currentMIRFn = null;
42
42
  /** Block map for quick lookup */
43
43
  this.blockMap = new Map();
44
+ /** Track generated dynamic array macro helper functions to avoid duplicates: key → fn name */
45
+ this.dynIdxHelpers = new Map();
46
+ /** Track generated dynamic array write helper functions: key → fn name */
47
+ this.dynWrtHelpers = new Map();
44
48
  this.namespace = namespace;
45
49
  this.objective = objective;
46
50
  }
@@ -62,6 +66,68 @@ class LoweringContext {
62
66
  addFunction(fn) {
63
67
  this.functions.push(fn);
64
68
  }
69
+ /**
70
+ * Get or create a macro helper function for dynamic array index reads.
71
+ * The helper function is: $return run data get storage <ns> <pathPrefix>[$(arr_idx)] 1
72
+ * Returns the qualified MC function name (namespace:fnName).
73
+ */
74
+ getDynIdxHelper(ns, pathPrefix) {
75
+ const key = `${ns}\0${pathPrefix}`;
76
+ const existing = this.dynIdxHelpers.get(key);
77
+ if (existing)
78
+ return existing;
79
+ // Generate deterministic name from ns and pathPrefix
80
+ const sanitize = (s) => s.replace(/[^a-z0-9_]/gi, '_').toLowerCase();
81
+ // Extract just the storage name part from ns (e.g. "myns:arrays" → "myns_arrays")
82
+ const nsStr = sanitize(ns);
83
+ const prefixStr = sanitize(pathPrefix);
84
+ const helperName = `__dyn_idx_${nsStr}_${prefixStr}`;
85
+ // The helper is placed in the current namespace
86
+ const qualifiedName = `${this.namespace}:${helperName}`;
87
+ // Generate the macro function content:
88
+ // $return run data get storage <ns> <pathPrefix>[$(arr_idx)] 1
89
+ const macroLine = {
90
+ kind: 'macro_line',
91
+ template: `return run data get storage ${ns} ${pathPrefix}[$(arr_idx)] 1`,
92
+ };
93
+ this.addFunction({
94
+ name: helperName,
95
+ instructions: [macroLine],
96
+ isMacro: true,
97
+ macroParams: ['arr_idx'],
98
+ });
99
+ this.dynIdxHelpers.set(key, qualifiedName);
100
+ return qualifiedName;
101
+ }
102
+ /**
103
+ * Get or create a macro helper function for dynamic array index writes.
104
+ * The helper function: $data modify storage <ns> <pathPrefix>[$(arr_idx)] set value $(arr_val)
105
+ * Returns the qualified MC function name.
106
+ */
107
+ getDynWrtHelper(ns, pathPrefix) {
108
+ const key = `${ns}\0${pathPrefix}`;
109
+ const existing = this.dynWrtHelpers.get(key);
110
+ if (existing)
111
+ return existing;
112
+ const sanitize = (s) => s.replace(/[^a-z0-9_]/gi, '_').toLowerCase();
113
+ const nsStr = sanitize(ns);
114
+ const prefixStr = sanitize(pathPrefix);
115
+ const helperName = `__dyn_wrt_${nsStr}_${prefixStr}`;
116
+ const qualifiedName = `${this.namespace}:${helperName}`;
117
+ // Macro line: $data modify storage <ns> <pathPrefix>[$(arr_idx)] set value $(arr_val)
118
+ const macroLine = {
119
+ kind: 'macro_line',
120
+ template: `data modify storage ${ns} ${pathPrefix}[$(arr_idx)] set value $(arr_val)`,
121
+ };
122
+ this.addFunction({
123
+ name: helperName,
124
+ instructions: [macroLine],
125
+ isMacro: true,
126
+ macroParams: ['arr_idx', 'arr_val'],
127
+ });
128
+ this.dynWrtHelpers.set(key, qualifiedName);
129
+ return qualifiedName;
130
+ }
65
131
  /** Attach sourceLoc to newly added instructions (from the given start index onward) */
66
132
  tagSourceLoc(instrs, fromIndex, sourceLoc) {
67
133
  if (!sourceLoc)
@@ -263,6 +329,41 @@ function lowerInstrInner(instr, fn, ctx, instrs) {
263
329
  });
264
330
  break;
265
331
  }
332
+ case 'nbt_read_dynamic': {
333
+ // Strategy:
334
+ // 1. Store the index value into rs:macro_args __arr_idx (int)
335
+ // 2. Call the per-array macro helper function with 'with storage rs:macro_args'
336
+ // 3. Result comes back via $ret scoreboard slot (the macro uses $return)
337
+ const dst = ctx.slot(instr.dst);
338
+ const idxSlot = operandToSlot(instr.indexSrc, ctx, instrs);
339
+ // Step 1: store index score → rs:macro_args arr_idx (int, scale 1)
340
+ instrs.push({
341
+ kind: 'store_score_to_nbt',
342
+ ns: 'rs:macro_args',
343
+ path: 'arr_idx',
344
+ type: 'int',
345
+ scale: 1,
346
+ src: idxSlot,
347
+ });
348
+ // Step 2: get or create the macro helper function, then call it
349
+ const helperFn = ctx.getDynIdxHelper(instr.ns, instr.pathPrefix);
350
+ instrs.push({ kind: 'call_macro', fn: helperFn, storage: 'rs:macro_args' });
351
+ // Step 3: the macro uses $return which sets the MC return value.
352
+ // We need to capture that. In MC, $return run ... returns the result
353
+ // to the caller via the execute store mechanism.
354
+ // Use store_cmd_to_score to capture the return value of the macro call.
355
+ // Actually, the call_macro instruction above already ran the function.
356
+ // The $return run data get ... sets the scoreboard return value for the
357
+ // *calling* function context. We need to use execute store result score.
358
+ // Rewrite: use raw command instead:
359
+ instrs.pop(); // remove the call_macro we just added
360
+ instrs.push({
361
+ kind: 'store_cmd_to_score',
362
+ dst,
363
+ cmd: { kind: 'call_macro', fn: helperFn, storage: 'rs:macro_args' },
364
+ });
365
+ break;
366
+ }
266
367
  case 'nbt_write': {
267
368
  const srcSlot = operandToSlot(instr.src, ctx, instrs);
268
369
  instrs.push({
@@ -275,6 +376,36 @@ function lowerInstrInner(instr, fn, ctx, instrs) {
275
376
  });
276
377
  break;
277
378
  }
379
+ case 'nbt_write_dynamic': {
380
+ // Strategy:
381
+ // 1. Store index score → rs:macro_args arr_idx (int)
382
+ // 2. Store value score → rs:macro_args arr_val (int)
383
+ // 3. Call macro helper: $data modify storage <ns> <pathPrefix>[$(arr_idx)] set value $(arr_val)
384
+ const idxSlot = operandToSlot(instr.indexSrc, ctx, instrs);
385
+ const valSlot = operandToSlot(instr.valueSrc, ctx, instrs);
386
+ // Store index
387
+ instrs.push({
388
+ kind: 'store_score_to_nbt',
389
+ ns: 'rs:macro_args',
390
+ path: 'arr_idx',
391
+ type: 'int',
392
+ scale: 1,
393
+ src: idxSlot,
394
+ });
395
+ // Store value
396
+ instrs.push({
397
+ kind: 'store_score_to_nbt',
398
+ ns: 'rs:macro_args',
399
+ path: 'arr_val',
400
+ type: 'int',
401
+ scale: 1,
402
+ src: valSlot,
403
+ });
404
+ // Call macro helper function
405
+ const helperFn = ctx.getDynWrtHelper(instr.ns, instr.pathPrefix);
406
+ instrs.push({ kind: 'call_macro', fn: helperFn, storage: 'rs:macro_args' });
407
+ break;
408
+ }
278
409
  case 'score_read': {
279
410
  // execute store result score $dst __obj run scoreboard players get <player> <obj>
280
411
  const dst = ctx.slot(instr.dst);