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
@@ -929,12 +929,20 @@ function lowerExpr(expr, ctx, scope) {
929
929
  return { kind: 'temp', name: t };
930
930
  }
931
931
  case 'index': {
932
- // Check if obj is a tracked array variable with a constant index
932
+ // Check if obj is a tracked array variable
933
933
  if (expr.obj.kind === 'ident') {
934
934
  const arrInfo = ctx.arrayVars.get(expr.obj.name);
935
- if (arrInfo && expr.index.kind === 'int_lit') {
935
+ if (arrInfo) {
936
936
  const t = ctx.freshTemp();
937
- ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, scale: 1 });
937
+ if (expr.index.kind === 'int_lit') {
938
+ // Constant index: direct NBT read
939
+ ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, scale: 1 });
940
+ }
941
+ else {
942
+ // Dynamic index: emit nbt_read_dynamic
943
+ const idxOp = lowerExpr(expr.index, ctx, scope);
944
+ ctx.emit({ kind: 'nbt_read_dynamic', dst: t, ns: arrInfo.ns, pathPrefix: arrInfo.pathPrefix, indexSrc: idxOp });
945
+ }
938
946
  return { kind: 'temp', name: t };
939
947
  }
940
948
  }
@@ -944,6 +952,25 @@ function lowerExpr(expr, ctx, scope) {
944
952
  ctx.emit({ kind: 'copy', dst: t, src: obj });
945
953
  return { kind: 'temp', name: t };
946
954
  }
955
+ case 'index_assign': {
956
+ const valOp = lowerExpr(expr.value, ctx, scope);
957
+ if (expr.obj.kind === 'ident') {
958
+ const arrInfo = ctx.arrayVars.get(expr.obj.name);
959
+ if (arrInfo) {
960
+ if (expr.index.kind === 'int_lit') {
961
+ // constant index → direct nbt_write
962
+ ctx.emit({ kind: 'nbt_write', ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, type: 'int', scale: 1, src: valOp });
963
+ }
964
+ else {
965
+ // dynamic index → nbt_write_dynamic
966
+ const idxOp = lowerExpr(expr.index, ctx, scope);
967
+ ctx.emit({ kind: 'nbt_write_dynamic', ns: arrInfo.ns, pathPrefix: arrInfo.pathPrefix, indexSrc: idxOp, valueSrc: valOp });
968
+ }
969
+ return valOp;
970
+ }
971
+ }
972
+ return valOp;
973
+ }
947
974
  case 'call': {
948
975
  // Handle scoreboard_get / score — read from vanilla MC scoreboard
949
976
  if (expr.fn === 'scoreboard_get' || expr.fn === 'score') {
@@ -963,6 +990,49 @@ function lowerExpr(expr, ctx, scope) {
963
990
  ctx.emit({ kind: 'const', dst: t, value: 0 });
964
991
  return { kind: 'temp', name: t };
965
992
  }
993
+ // Handle list_push(arr_name, val) — append an int to an NBT int array
994
+ // list_push("rs:lists", "mylist", val) or simpler: uses the array's storage path
995
+ if (expr.fn === 'list_push') {
996
+ // list_push(array_var, value)
997
+ // 1. Append a placeholder 0
998
+ // 2. Overwrite [-1] with the actual value
999
+ if (expr.args[0].kind === 'ident') {
1000
+ const arrInfo = ctx.arrayVars.get(expr.args[0].name);
1001
+ if (arrInfo) {
1002
+ const valOp = lowerExpr(expr.args[1], ctx, scope);
1003
+ // Step 1: append placeholder
1004
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${arrInfo.ns} ${arrInfo.pathPrefix} append value 0`, args: [] });
1005
+ // Step 2: overwrite last element with actual value
1006
+ ctx.emit({ kind: 'nbt_write', ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[-1]`, type: 'int', scale: 1, src: valOp });
1007
+ const t = ctx.freshTemp();
1008
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1009
+ return { kind: 'temp', name: t };
1010
+ }
1011
+ }
1012
+ }
1013
+ // Handle list_pop(arr_var) — remove last element from NBT int array
1014
+ if (expr.fn === 'list_pop') {
1015
+ if (expr.args[0].kind === 'ident') {
1016
+ const arrInfo = ctx.arrayVars.get(expr.args[0].name);
1017
+ if (arrInfo) {
1018
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:data remove storage ${arrInfo.ns} ${arrInfo.pathPrefix}[-1]`, args: [] });
1019
+ const t = ctx.freshTemp();
1020
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1021
+ return { kind: 'temp', name: t };
1022
+ }
1023
+ }
1024
+ }
1025
+ // Handle list_len(arr_var) — get length of NBT int array
1026
+ if (expr.fn === 'list_len') {
1027
+ if (expr.args[0].kind === 'ident') {
1028
+ const arrInfo = ctx.arrayVars.get(expr.args[0].name);
1029
+ if (arrInfo) {
1030
+ const t = ctx.freshTemp();
1031
+ ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}`, scale: 1 });
1032
+ return { kind: 'temp', name: t };
1033
+ }
1034
+ }
1035
+ }
966
1036
  // Handle setTimeout/setInterval: lift lambda arg to a named helper function
967
1037
  if ((expr.fn === 'setTimeout' || expr.fn === 'setInterval') && expr.args.length === 2) {
968
1038
  const ticksArg = expr.args[0];
@@ -139,6 +139,11 @@ function scanExpr(expr, paramNames, macroParams) {
139
139
  scanExpr(expr.obj, paramNames, macroParams);
140
140
  scanExpr(expr.value, paramNames, macroParams);
141
141
  break;
142
+ case 'index_assign':
143
+ scanExpr(expr.obj, paramNames, macroParams);
144
+ scanExpr(expr.index, paramNames, macroParams);
145
+ scanExpr(expr.value, paramNames, macroParams);
146
+ break;
142
147
  case 'member':
143
148
  scanExpr(expr.obj, paramNames, macroParams);
144
149
  break;
@@ -130,6 +130,12 @@ export type MIRInstr = MIRInstrBase & ({
130
130
  ns: string;
131
131
  path: string;
132
132
  scale: number;
133
+ } | {
134
+ kind: 'nbt_read_dynamic';
135
+ dst: Temp;
136
+ ns: string;
137
+ pathPrefix: string;
138
+ indexSrc: Operand;
133
139
  } | {
134
140
  kind: 'nbt_write';
135
141
  ns: string;
@@ -137,6 +143,12 @@ export type MIRInstr = MIRInstrBase & ({
137
143
  type: NBTType;
138
144
  scale: number;
139
145
  src: Operand;
146
+ } | {
147
+ kind: 'nbt_write_dynamic';
148
+ ns: string;
149
+ pathPrefix: string;
150
+ indexSrc: Operand;
151
+ valueSrc: Operand;
140
152
  } | {
141
153
  kind: 'score_read';
142
154
  dst: Temp;
@@ -155,6 +155,7 @@ function getDst(instr) {
155
155
  case 'or':
156
156
  case 'not':
157
157
  case 'nbt_read':
158
+ case 'nbt_read_dynamic':
158
159
  return instr.dst;
159
160
  case 'call':
160
161
  case 'call_macro':
@@ -188,9 +189,15 @@ function getUsedTemps(instr) {
188
189
  break;
189
190
  case 'nbt_read':
190
191
  break;
192
+ case 'nbt_read_dynamic':
193
+ temps.push(...getOperandTemps(instr.indexSrc));
194
+ break;
191
195
  case 'nbt_write':
192
196
  temps.push(...getOperandTemps(instr.src));
193
197
  break;
198
+ case 'nbt_write_dynamic':
199
+ temps.push(...getOperandTemps(instr.indexSrc), ...getOperandTemps(instr.valueSrc));
200
+ break;
194
201
  case 'call':
195
202
  for (const arg of instr.args)
196
203
  temps.push(...getOperandTemps(arg));
@@ -75,6 +75,10 @@ function rewriteUses(instr, copies) {
75
75
  return { ...instr, a: resolve(instr.a, copies), b: resolve(instr.b, copies) };
76
76
  case 'nbt_write':
77
77
  return { ...instr, src: resolve(instr.src, copies) };
78
+ case 'nbt_write_dynamic':
79
+ return { ...instr, indexSrc: resolve(instr.indexSrc, copies), valueSrc: resolve(instr.valueSrc, copies) };
80
+ case 'nbt_read_dynamic':
81
+ return { ...instr, indexSrc: resolve(instr.indexSrc, copies) };
78
82
  case 'call':
79
83
  return { ...instr, args: instr.args.map(a => resolve(a, copies)) };
80
84
  case 'call_macro':
@@ -104,6 +108,7 @@ function getDst(instr) {
104
108
  case 'or':
105
109
  case 'not':
106
110
  case 'nbt_read':
111
+ case 'nbt_read_dynamic':
107
112
  return instr.dst;
108
113
  case 'call':
109
114
  case 'call_macro':
@@ -713,8 +713,12 @@ function rewriteInstr(instr, promoted) {
713
713
  return { ...instr, dst: rTemp(instr.dst), src: rOp(instr.src) };
714
714
  case 'nbt_read':
715
715
  return { ...instr, dst: rTemp(instr.dst) };
716
+ case 'nbt_read_dynamic':
717
+ return { ...instr, dst: rTemp(instr.dst), indexSrc: rOp(instr.indexSrc) };
716
718
  case 'nbt_write':
717
719
  return { ...instr, src: rOp(instr.src) };
720
+ case 'nbt_write_dynamic':
721
+ return { ...instr, indexSrc: rOp(instr.indexSrc), valueSrc: rOp(instr.valueSrc) };
718
722
  case 'call':
719
723
  return { ...instr, dst: instr.dst ? rTemp(instr.dst) : null, args: instr.args.map(rOp) };
720
724
  case 'call_macro':
@@ -770,6 +774,7 @@ function getDst(instr) {
770
774
  case 'or':
771
775
  case 'not':
772
776
  case 'nbt_read':
777
+ case 'nbt_read_dynamic':
773
778
  return instr.dst;
774
779
  case 'call':
775
780
  case 'call_macro':
@@ -802,6 +807,13 @@ function getUsedTemps(instr) {
802
807
  case 'nbt_write':
803
808
  addOp(instr.src);
804
809
  break;
810
+ case 'nbt_write_dynamic':
811
+ addOp(instr.indexSrc);
812
+ addOp(instr.valueSrc);
813
+ break;
814
+ case 'nbt_read_dynamic':
815
+ addOp(instr.indexSrc);
816
+ break;
805
817
  case 'call':
806
818
  instr.args.forEach(addOp);
807
819
  break;
@@ -75,6 +75,7 @@ function recomputePreds(blocks) {
75
75
  function hasSideEffects(instr) {
76
76
  if (instr.kind === 'call' || instr.kind === 'call_macro' ||
77
77
  instr.kind === 'call_context' || instr.kind === 'nbt_write' ||
78
+ instr.kind === 'nbt_write_dynamic' ||
78
79
  instr.kind === 'score_write')
79
80
  return true;
80
81
  // Return field temps (__rf_) write to global return slots — not dead even if unused locally
@@ -106,6 +107,7 @@ function getDst(instr) {
106
107
  case 'or':
107
108
  case 'not':
108
109
  case 'nbt_read':
110
+ case 'nbt_read_dynamic':
109
111
  return instr.dst;
110
112
  case 'call':
111
113
  case 'call_macro':
@@ -140,6 +142,13 @@ function getUsedTemps(instr) {
140
142
  case 'nbt_write':
141
143
  addOp(instr.src);
142
144
  break;
145
+ case 'nbt_write_dynamic':
146
+ addOp(instr.indexSrc);
147
+ addOp(instr.valueSrc);
148
+ break;
149
+ case 'nbt_read_dynamic':
150
+ addOp(instr.indexSrc);
151
+ break;
143
152
  case 'call':
144
153
  instr.args.forEach(addOp);
145
154
  break;
@@ -287,6 +287,8 @@ function substituteInstr(instr, sub) {
287
287
  return { ...instr, a: substituteOp(instr.a, sub), b: substituteOp(instr.b, sub) };
288
288
  case 'nbt_write':
289
289
  return { ...instr, src: substituteOp(instr.src, sub) };
290
+ case 'nbt_write_dynamic':
291
+ return { ...instr, indexSrc: substituteOp(instr.indexSrc, sub), valueSrc: substituteOp(instr.valueSrc, sub) };
290
292
  case 'call':
291
293
  return { ...instr, args: instr.args.map(a => substituteOp(a, sub)) };
292
294
  case 'call_macro':
@@ -317,6 +319,7 @@ function getInstrDst(instr) {
317
319
  case 'or':
318
320
  case 'not':
319
321
  case 'nbt_read':
322
+ case 'nbt_read_dynamic':
320
323
  return instr.dst;
321
324
  case 'call':
322
325
  case 'call_macro':
@@ -1048,6 +1048,11 @@ class Parser {
1048
1048
  const value = this.parseAssignment();
1049
1049
  return this.withLoc({ kind: 'member_assign', obj: left.obj, field: left.field, op, value }, this.getLocToken(left) ?? token);
1050
1050
  }
1051
+ // Index assignment: arr[0] = val, arr[i] = val
1052
+ if (left.kind === 'index') {
1053
+ const value = this.parseAssignment();
1054
+ return this.withLoc({ kind: 'index_assign', obj: left.obj, index: left.index, op, value }, this.getLocToken(left) ?? token);
1055
+ }
1051
1056
  }
1052
1057
  return left;
1053
1058
  }
@@ -535,6 +535,11 @@ class TypeChecker {
535
535
  this.checkExpr(expr.obj);
536
536
  this.checkExpr(expr.value);
537
537
  break;
538
+ case 'index_assign':
539
+ this.checkExpr(expr.obj);
540
+ this.checkExpr(expr.index);
541
+ this.checkExpr(expr.value);
542
+ break;
538
543
  case 'index':
539
544
  this.checkExpr(expr.obj);
540
545
  this.checkExpr(expr.index);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "1.2.26",
3
+ "version": "1.2.34",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "1.2.26",
9
+ "version": "1.2.34",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "../..": {
26
26
  "name": "redscript-mc",
27
- "version": "2.3.0",
27
+ "version": "2.4.0",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "vscode-languageserver": "^9.0.1",
@@ -2,7 +2,7 @@
2
2
  "name": "redscript-vscode",
3
3
  "displayName": "RedScript for Minecraft",
4
4
  "description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
5
- "version": "1.2.26",
5
+ "version": "1.2.34",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/src/index.js",
6
6
  "bin": {
@@ -0,0 +1,147 @@
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
+
13
+ import { compile } from '../emit/compile'
14
+
15
+ // Helper: find file in compiled output by path substring
16
+ function getFile(files: { path: string; content: string }[], pathSubstr: string): string | undefined {
17
+ const f = files.find(f => f.path.includes(pathSubstr))
18
+ return f?.content
19
+ }
20
+
21
+ // Helper: get the content of the function file for `fnName` in namespace
22
+ function getFunctionBody(files: { path: string; content: string }[], fnName: string, ns = 'test'): string {
23
+ const content = getFile(files, `${fnName}.mcfunction`)
24
+ if (!content) {
25
+ // list files for debug
26
+ const paths = files.map(f => f.path).join('\n')
27
+ throw new Error(`Function '${fnName}' not found in output. Files:\n${paths}`)
28
+ }
29
+ return content
30
+ }
31
+
32
+ describe('Dynamic array index read: arr[i]', () => {
33
+ const src = `
34
+ fn test() {
35
+ let nums: int[] = [10, 20, 30, 40, 50];
36
+ let i: int = 2;
37
+ i = i + 1;
38
+ let v: int = nums[i];
39
+ scoreboard_set("#out", "test", v);
40
+ }
41
+ `
42
+
43
+ let files: { path: string; content: string }[]
44
+ beforeAll(() => {
45
+ const result = compile(src, { namespace: 'test' })
46
+ files = result.files
47
+ })
48
+
49
+ test('compiles without error', () => {
50
+ // beforeAll would have thrown if compilation failed
51
+ expect(files.length).toBeGreaterThan(0)
52
+ })
53
+
54
+ test('test function contains "with storage" (macro call)', () => {
55
+ const body = getFunctionBody(files, 'test')
56
+ expect(body).toContain('with storage')
57
+ })
58
+
59
+ test('test function does NOT contain fallback "scoreboard players set #out test 5"', () => {
60
+ // Old fallback would copy the array length (5 elements) as the result
61
+ const body = getFunctionBody(files, 'test')
62
+ expect(body).not.toContain('scoreboard players set #out test 5')
63
+ })
64
+
65
+ test('a macro helper function is generated for the array', () => {
66
+ // Should have a function file matching __dyn_idx_
67
+ const helperFile = files.find(f => f.path.includes('__dyn_idx_'))
68
+ expect(helperFile).toBeDefined()
69
+ })
70
+
71
+ test('macro helper function contains $return run data get', () => {
72
+ const helperFile = files.find(f => f.path.includes('__dyn_idx_'))
73
+ expect(helperFile).toBeDefined()
74
+ expect(helperFile!.content).toContain('$return run data get')
75
+ expect(helperFile!.content).toContain('$(arr_idx)')
76
+ })
77
+
78
+ test('macro helper function references the correct array path (nums)', () => {
79
+ const helperFile = files.find(f => f.path.includes('__dyn_idx_'))
80
+ expect(helperFile).toBeDefined()
81
+ expect(helperFile!.content).toContain('nums[$(arr_idx)]')
82
+ })
83
+
84
+ test('test function stores index to rs:macro_args', () => {
85
+ const body = getFunctionBody(files, 'test')
86
+ // Should store the index value into rs:macro_args arr_idx
87
+ expect(body).toContain('rs:macro_args')
88
+ })
89
+ })
90
+
91
+ describe('Dynamic array index: constant index still uses direct nbt_read', () => {
92
+ const src = `
93
+ fn test_const() {
94
+ let nums: int[] = [10, 20, 30];
95
+ let v: int = nums[1];
96
+ scoreboard_set("#out", "test", v);
97
+ }
98
+ `
99
+
100
+ let files: { path: string; content: string }[]
101
+ beforeAll(() => {
102
+ const result = compile(src, { namespace: 'test' })
103
+ files = result.files
104
+ })
105
+
106
+ test('constant index does NOT generate macro call (uses direct data get)', () => {
107
+ const body = getFunctionBody(files, 'test_const')
108
+ // Direct nbt_read emits store_nbt_to_score → execute store result score ... run data get ...
109
+ // without 'with storage'
110
+ expect(body).not.toContain('with storage')
111
+ expect(body).toContain('data get storage')
112
+ expect(body).toContain('nums[1]')
113
+ })
114
+ })
115
+
116
+ describe('Dynamic array index: multiple arrays, separate helpers', () => {
117
+ const src = `
118
+ fn test_multi() {
119
+ let a: int[] = [1, 2, 3];
120
+ let b: int[] = [10, 20, 30];
121
+ let i: int = 1;
122
+ i = i + 0;
123
+ let va: int = a[i];
124
+ let vb: int = b[i];
125
+ scoreboard_set("#va", "test", va);
126
+ scoreboard_set("#vb", "test", vb);
127
+ }
128
+ `
129
+
130
+ let files: { path: string; content: string }[]
131
+ beforeAll(() => {
132
+ const result = compile(src, { namespace: 'test' })
133
+ files = result.files
134
+ })
135
+
136
+ test('two separate macro helpers are generated for arrays a and b', () => {
137
+ const helperFiles = files.filter(f => f.path.includes('__dyn_idx_'))
138
+ expect(helperFiles.length).toBe(2)
139
+ })
140
+
141
+ test('each helper references its respective array path', () => {
142
+ const helperFiles = files.filter(f => f.path.includes('__dyn_idx_'))
143
+ const contents = helperFiles.map(f => f.content).join('\n')
144
+ expect(contents).toContain('a[$(arr_idx)]')
145
+ expect(contents).toContain('b[$(arr_idx)]')
146
+ })
147
+ })
@@ -0,0 +1,169 @@
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
+
12
+ import { compile } from '../emit/compile'
13
+
14
+ // Helper: find file in compiled output by path substring
15
+ function getFile(files: { path: string; content: string }[], pathSubstr: string): string | undefined {
16
+ const f = files.find(f => f.path.includes(pathSubstr))
17
+ return f?.content
18
+ }
19
+
20
+ // Helper: get the content of the function file for `fnName` in namespace
21
+ function getFunctionBody(files: { path: string; content: string }[], fnName: string, ns = 'test'): string {
22
+ const content = getFile(files, `${fnName}.mcfunction`)
23
+ if (!content) {
24
+ const paths = files.map(f => f.path).join('\n')
25
+ throw new Error(`Function '${fnName}' not found in output. Files:\n${paths}`)
26
+ }
27
+ return content
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Constant index write: arr[1] = 99
32
+ // ---------------------------------------------------------------------------
33
+ describe('Constant index write: arr[1] = 99', () => {
34
+ const src = `
35
+ fn test() {
36
+ let nums: int[] = [10, 20, 30];
37
+ nums[1] = 99;
38
+ scoreboard_set("#out", "test", nums[1]);
39
+ }
40
+ `
41
+
42
+ let files: { path: string; content: string }[]
43
+ beforeAll(() => {
44
+ const result = compile(src, { namespace: 'test' })
45
+ files = result.files
46
+ })
47
+
48
+ test('compiles without error', () => {
49
+ expect(files.length).toBeGreaterThan(0)
50
+ })
51
+
52
+ test('test function contains nbt store to array path [1] (constant write)', () => {
53
+ const body = getFunctionBody(files, 'test')
54
+ // Should write to path like "nums[1]" via execute store result storage
55
+ expect(body).toMatch(/nums\[1\]/)
56
+ })
57
+
58
+ test('test function reads back from nums[1] after writing', () => {
59
+ const body = getFunctionBody(files, 'test')
60
+ // Should also read nums[1] for scoreboard_set
61
+ expect(body).toMatch(/nums\[1\]/)
62
+ })
63
+ })
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Dynamic index write: arr[i] = 99
67
+ // ---------------------------------------------------------------------------
68
+ describe('Dynamic index write: arr[i] = 99', () => {
69
+ const src = `
70
+ fn test() {
71
+ let nums: int[] = [10, 20, 30];
72
+ let i: int = 1;
73
+ nums[i] = 99;
74
+ scoreboard_set("#out", "test", nums[i]);
75
+ }
76
+ `
77
+
78
+ let files: { path: string; content: string }[]
79
+ beforeAll(() => {
80
+ const result = compile(src, { namespace: 'test' })
81
+ files = result.files
82
+ })
83
+
84
+ test('compiles without error', () => {
85
+ expect(files.length).toBeGreaterThan(0)
86
+ })
87
+
88
+ test('test function contains "with storage" (macro call for write)', () => {
89
+ const body = getFunctionBody(files, 'test')
90
+ expect(body).toContain('with storage')
91
+ })
92
+
93
+ test('a __dyn_wrt_ helper function is generated', () => {
94
+ const helperFile = files.find(f => f.path.includes('__dyn_wrt_'))
95
+ expect(helperFile).toBeDefined()
96
+ })
97
+
98
+ test('the write helper contains a macro line with arr_idx and arr_val', () => {
99
+ const helperFile = files.find(f => f.path.includes('__dyn_wrt_'))
100
+ expect(helperFile).toBeDefined()
101
+ expect(helperFile!.content).toContain('$(arr_idx)')
102
+ expect(helperFile!.content).toContain('$(arr_val)')
103
+ })
104
+
105
+ test('the write helper uses data modify set value', () => {
106
+ const helperFile = files.find(f => f.path.includes('__dyn_wrt_'))
107
+ expect(helperFile!.content).toContain('data modify storage')
108
+ expect(helperFile!.content).toContain('set value')
109
+ })
110
+ })
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Compound assignment: arr[i] += 5
114
+ // ---------------------------------------------------------------------------
115
+ describe('Compound index assignment: arr[i] += 5', () => {
116
+ const src = `
117
+ fn test() {
118
+ let nums: int[] = [10, 20, 30];
119
+ let i: int = 0;
120
+ nums[i] += 5;
121
+ scoreboard_set("#out", "test", nums[i]);
122
+ }
123
+ `
124
+
125
+ let files: { path: string; content: string }[]
126
+ beforeAll(() => {
127
+ const result = compile(src, { namespace: 'test' })
128
+ files = result.files
129
+ })
130
+
131
+ test('compiles without error', () => {
132
+ expect(files.length).toBeGreaterThan(0)
133
+ })
134
+
135
+ test('compound assignment generates both read and write macro calls', () => {
136
+ const body = getFunctionBody(files, 'test')
137
+ // Should call with storage at least twice (read for += and write + scoreboard_set read)
138
+ const matches = (body.match(/with storage/g) || []).length
139
+ expect(matches).toBeGreaterThanOrEqual(1)
140
+ })
141
+ })
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Constant compound assignment: arr[0] += 5
145
+ // ---------------------------------------------------------------------------
146
+ describe('Constant compound index assignment: arr[0] += 5', () => {
147
+ const src = `
148
+ fn test() {
149
+ let nums: int[] = [10, 20, 30];
150
+ nums[0] += 5;
151
+ scoreboard_set("#out", "test", nums[0]);
152
+ }
153
+ `
154
+
155
+ let files: { path: string; content: string }[]
156
+ beforeAll(() => {
157
+ const result = compile(src, { namespace: 'test' })
158
+ files = result.files
159
+ })
160
+
161
+ test('compiles without error', () => {
162
+ expect(files.length).toBeGreaterThan(0)
163
+ })
164
+
165
+ test('test function contains array path [0] for read and write', () => {
166
+ const body = getFunctionBody(files, 'test')
167
+ expect(body).toMatch(/nums\[0\]/)
168
+ })
169
+ })
package/src/ast/types.ts CHANGED
@@ -171,6 +171,7 @@ export type Expr =
171
171
  | { kind: 'struct_lit'; fields: { name: string; value: Expr }[]; span?: Span }
172
172
  | { kind: 'member_assign'; obj: Expr; field: string; op: AssignOp; value: Expr; span?: Span }
173
173
  | { kind: 'index'; obj: Expr; index: Expr; span?: Span }
174
+ | { kind: 'index_assign'; obj: Expr; index: Expr; op: AssignOp; value: Expr; span?: Span }
174
175
  | { kind: 'array_lit'; elements: Expr[]; span?: Span }
175
176
  | { kind: 'static_call'; type: string; method: string; args: Expr[]; span?: Span }
176
177
  | { kind: 'path_expr'; enumName: string; variant: string; span?: Span }
@@ -560,6 +560,11 @@ function rewriteExpr(expr: Expr, symbolMap: Map<string, string>): void {
560
560
  rewriteExpr(expr.obj, symbolMap)
561
561
  rewriteExpr(expr.index, symbolMap)
562
562
  break
563
+ case 'index_assign':
564
+ rewriteExpr(expr.obj, symbolMap)
565
+ rewriteExpr(expr.index, symbolMap)
566
+ rewriteExpr(expr.value, symbolMap)
567
+ break
563
568
  case 'array_lit':
564
569
  for (const el of expr.elements) rewriteExpr(el, symbolMap)
565
570
  break