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.
- package/CHANGELOG.md +11 -0
- package/dist/src/__tests__/array-dynamic.test.d.ts +12 -0
- package/dist/src/__tests__/array-dynamic.test.js +131 -0
- package/dist/src/__tests__/array-write.test.d.ts +11 -0
- package/dist/src/__tests__/array-write.test.js +149 -0
- package/dist/src/ast/types.d.ts +7 -0
- package/dist/src/emit/modules.js +5 -0
- package/dist/src/hir/lower.js +29 -0
- package/dist/src/hir/monomorphize.js +2 -0
- package/dist/src/hir/types.d.ts +9 -2
- package/dist/src/lir/lower.js +131 -0
- package/dist/src/mir/lower.js +73 -3
- package/dist/src/mir/macro.js +5 -0
- package/dist/src/mir/types.d.ts +12 -0
- package/dist/src/mir/verify.js +7 -0
- package/dist/src/optimizer/copy_prop.js +5 -0
- package/dist/src/optimizer/coroutine.js +12 -0
- package/dist/src/optimizer/dce.js +9 -0
- package/dist/src/optimizer/unroll.js +3 -0
- package/dist/src/parser/index.js +5 -0
- package/dist/src/typechecker/index.js +5 -0
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/array-dynamic.test.ts +147 -0
- package/src/__tests__/array-write.test.ts +169 -0
- package/src/ast/types.ts +1 -0
- package/src/emit/modules.ts +5 -0
- package/src/hir/lower.ts +30 -0
- package/src/hir/monomorphize.ts +2 -0
- package/src/hir/types.ts +3 -1
- package/src/lir/lower.ts +151 -0
- package/src/mir/lower.ts +75 -3
- package/src/mir/macro.ts +5 -0
- package/src/mir/types.ts +2 -0
- package/src/mir/verify.ts +7 -0
- package/src/optimizer/copy_prop.ts +5 -0
- package/src/optimizer/coroutine.ts +9 -0
- package/src/optimizer/dce.ts +6 -0
- package/src/optimizer/unroll.ts +3 -0
- package/src/parser/index.ts +9 -0
- package/src/stdlib/list.mcrs +43 -72
- package/src/stdlib/math.mcrs +137 -0
- package/src/stdlib/timer.mcrs +32 -0
- package/src/typechecker/index.ts +6 -0
package/dist/src/mir/lower.js
CHANGED
|
@@ -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
|
|
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
|
|
935
|
+
if (arrInfo) {
|
|
936
936
|
const t = ctx.freshTemp();
|
|
937
|
-
|
|
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];
|
package/dist/src/mir/macro.js
CHANGED
|
@@ -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;
|
package/dist/src/mir/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/mir/verify.js
CHANGED
|
@@ -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':
|
package/dist/src/parser/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
5
|
+
"version": "1.2.34",
|
|
6
6
|
"publisher": "bkmashiro",
|
|
7
7
|
"icon": "icon.png",
|
|
8
8
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -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 }
|
package/src/emit/modules.ts
CHANGED
|
@@ -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
|