redscript-mc 1.2.25 → 1.2.26
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/dist/__tests__/cli.test.js +1 -1
- package/dist/__tests__/codegen.test.js +12 -6
- package/dist/__tests__/e2e.test.js +6 -6
- package/dist/__tests__/lowering.test.js +8 -8
- package/dist/__tests__/optimizer.test.js +31 -0
- package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
- package/dist/__tests__/stdlib-advanced.test.js +264 -0
- package/dist/__tests__/stdlib-math.test.d.ts +7 -0
- package/dist/__tests__/stdlib-math.test.js +352 -0
- package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
- package/dist/__tests__/stdlib-vec.test.js +264 -0
- package/dist/ast/types.d.ts +17 -1
- package/dist/codegen/mcfunction/index.js +154 -18
- package/dist/codegen/var-allocator.d.ts +17 -0
- package/dist/codegen/var-allocator.js +26 -0
- package/dist/compile.d.ts +14 -0
- package/dist/compile.js +62 -5
- package/dist/index.js +20 -1
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +1 -0
- package/dist/lowering/index.d.ts +5 -0
- package/dist/lowering/index.js +83 -10
- package/dist/optimizer/dce.js +21 -5
- package/dist/optimizer/passes.js +18 -6
- package/dist/optimizer/structure.js +7 -0
- package/dist/parser/index.d.ts +5 -0
- package/dist/parser/index.js +43 -2
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +109 -9
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +1 -1
- package/src/__tests__/codegen.test.ts +12 -6
- package/src/__tests__/e2e.test.ts +6 -6
- package/src/__tests__/lowering.test.ts +8 -8
- package/src/__tests__/optimizer.test.ts +33 -0
- package/src/__tests__/stdlib-advanced.test.ts +259 -0
- package/src/__tests__/stdlib-math.test.ts +374 -0
- package/src/__tests__/stdlib-vec.test.ts +259 -0
- package/src/ast/types.ts +11 -1
- package/src/codegen/mcfunction/index.ts +143 -19
- package/src/codegen/var-allocator.ts +29 -0
- package/src/compile.ts +72 -5
- package/src/index.ts +21 -1
- package/src/ir/types.ts +2 -0
- package/src/lexer/index.ts +2 -1
- package/src/lowering/index.ts +96 -10
- package/src/optimizer/dce.ts +22 -5
- package/src/optimizer/passes.ts +18 -5
- package/src/optimizer/structure.ts +6 -1
- package/src/parser/index.ts +47 -2
- package/src/runtime/index.ts +108 -10
- package/src/stdlib/advanced.mcrs +249 -0
- package/src/stdlib/math.mcrs +259 -19
- package/src/stdlib/vec.mcrs +246 -0
package/dist/lowering/index.js
CHANGED
|
@@ -128,6 +128,8 @@ const BUILTINS = {
|
|
|
128
128
|
setTimeout: () => null, // Special handling
|
|
129
129
|
setInterval: () => null, // Special handling
|
|
130
130
|
clearInterval: () => null, // Special handling
|
|
131
|
+
storage_get_int: () => null, // Special handling (dynamic NBT array read via macro)
|
|
132
|
+
storage_set_array: () => null, // Special handling (write literal NBT array to storage)
|
|
131
133
|
};
|
|
132
134
|
function getSpan(node) {
|
|
133
135
|
return node?.span;
|
|
@@ -196,6 +198,13 @@ function emitBlockPos(pos) {
|
|
|
196
198
|
// Lowering Class
|
|
197
199
|
// ---------------------------------------------------------------------------
|
|
198
200
|
class Lowering {
|
|
201
|
+
/** Unique IR variable name for a local variable, scoped to the current function.
|
|
202
|
+
* Prevents cross-function scoreboard slot collisions: $fn_x ≠ $gn_x.
|
|
203
|
+
* Only applies to user-defined locals/params; internal slots ($p0, $ret) are
|
|
204
|
+
* intentionally global (calling convention). */
|
|
205
|
+
fnVar(name) {
|
|
206
|
+
return `$${this.currentFn}_${name}`;
|
|
207
|
+
}
|
|
199
208
|
currentEntityContext() {
|
|
200
209
|
return this.entityContextStack.length > 0
|
|
201
210
|
? this.entityContextStack[this.entityContextStack.length - 1]
|
|
@@ -564,13 +573,13 @@ class Lowering {
|
|
|
564
573
|
this.stringValues.set(param.name, '');
|
|
565
574
|
continue;
|
|
566
575
|
}
|
|
567
|
-
this.varMap.set(param.name,
|
|
576
|
+
this.varMap.set(param.name, this.fnVar(param.name));
|
|
568
577
|
}
|
|
569
578
|
}
|
|
570
579
|
else {
|
|
571
580
|
for (const param of runtimeParams) {
|
|
572
581
|
const paramName = param.name;
|
|
573
|
-
this.varMap.set(paramName,
|
|
582
|
+
this.varMap.set(paramName, this.fnVar(paramName));
|
|
574
583
|
this.varTypes.set(paramName, this.normalizeType(param.type));
|
|
575
584
|
}
|
|
576
585
|
}
|
|
@@ -581,18 +590,22 @@ class Lowering {
|
|
|
581
590
|
}
|
|
582
591
|
// Start entry block
|
|
583
592
|
this.builder.startBlock('entry');
|
|
584
|
-
// Copy params from
|
|
593
|
+
// Copy params from the parameter-passing slots to named local variables.
|
|
594
|
+
// Use { kind: 'param', index: i } so the codegen resolves to
|
|
595
|
+
// alloc.internal('p{i}') consistently in both mangle and no-mangle modes,
|
|
596
|
+
// avoiding the slot-collision between the internal register and a user variable
|
|
597
|
+
// named 'p0'/'p1' that occurred with { kind: 'var', name: '$p0' }.
|
|
585
598
|
for (let i = 0; i < runtimeParams.length; i++) {
|
|
586
599
|
const paramName = runtimeParams[i].name;
|
|
587
|
-
const varName =
|
|
588
|
-
this.builder.emitAssign(varName, { kind: '
|
|
600
|
+
const varName = this.fnVar(paramName);
|
|
601
|
+
this.builder.emitAssign(varName, { kind: 'param', index: i });
|
|
589
602
|
}
|
|
590
603
|
if (staticEventDec) {
|
|
591
604
|
for (let i = 0; i < fn.params.length; i++) {
|
|
592
605
|
const param = fn.params[i];
|
|
593
606
|
const expected = eventParamSpecs[i];
|
|
594
607
|
if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
|
|
595
|
-
this.builder.emitAssign(
|
|
608
|
+
this.builder.emitAssign(this.fnVar(param.name), { kind: 'const', value: 0 });
|
|
596
609
|
}
|
|
597
610
|
}
|
|
598
611
|
}
|
|
@@ -649,6 +662,22 @@ class Lowering {
|
|
|
649
662
|
if (fn.decorators.some(d => d.name === 'load')) {
|
|
650
663
|
irFn.isLoadInit = true;
|
|
651
664
|
}
|
|
665
|
+
// @requires("dep_fn") — when this function is compiled in, dep_fn is also
|
|
666
|
+
// called from __load. The dep_fn itself does NOT need @load; it can be a
|
|
667
|
+
// private (_) function that only runs at load time when this fn is used.
|
|
668
|
+
const requiredLoads = [];
|
|
669
|
+
for (const d of fn.decorators) {
|
|
670
|
+
if (d.name === 'require_on_load') {
|
|
671
|
+
for (const arg of d.rawArgs ?? []) {
|
|
672
|
+
if (arg.kind === 'string') {
|
|
673
|
+
requiredLoads.push(arg.value);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (requiredLoads.length > 0) {
|
|
679
|
+
irFn.requiredLoads = requiredLoads;
|
|
680
|
+
}
|
|
652
681
|
// Handle tick rate counter if needed
|
|
653
682
|
if (tickRate && tickRate > 1) {
|
|
654
683
|
this.wrapWithTickRate(irFn, tickRate);
|
|
@@ -773,7 +802,7 @@ class Lowering {
|
|
|
773
802
|
if (this.currentContext.binding === stmt.name) {
|
|
774
803
|
throw new diagnostics_1.DiagnosticError('LoweringError', `Cannot redeclare foreach binding '${stmt.name}'`, stmt.span ?? { line: 0, col: 0 });
|
|
775
804
|
}
|
|
776
|
-
const varName =
|
|
805
|
+
const varName = this.fnVar(stmt.name);
|
|
777
806
|
this.varMap.set(stmt.name, varName);
|
|
778
807
|
// Track variable type
|
|
779
808
|
const declaredType = stmt.type ? this.normalizeType(stmt.type) : this.inferExprType(stmt.init);
|
|
@@ -1049,7 +1078,7 @@ class Lowering {
|
|
|
1049
1078
|
this.builder.startBlock(exitLabel);
|
|
1050
1079
|
}
|
|
1051
1080
|
lowerForRangeStmt(stmt) {
|
|
1052
|
-
const loopVar =
|
|
1081
|
+
const loopVar = this.fnVar(stmt.varName);
|
|
1053
1082
|
const subFnName = `${this.currentFn}/__for_${this.foreachCounter++}`;
|
|
1054
1083
|
// Initialize loop variable
|
|
1055
1084
|
this.varMap.set(stmt.varName, loopVar);
|
|
@@ -1208,7 +1237,7 @@ class Lowering {
|
|
|
1208
1237
|
return;
|
|
1209
1238
|
}
|
|
1210
1239
|
const arrayType = this.inferExprType(stmt.iterable);
|
|
1211
|
-
const bindingVar =
|
|
1240
|
+
const bindingVar = this.fnVar(stmt.binding);
|
|
1212
1241
|
const indexVar = this.builder.freshTemp();
|
|
1213
1242
|
const lengthVar = this.builder.freshTemp();
|
|
1214
1243
|
const condVar = this.builder.freshTemp();
|
|
@@ -2216,6 +2245,50 @@ class Lowering {
|
|
|
2216
2245
|
this.builder.emitRaw(`execute store result score ${dst} rs run data get ${targetType} ${target} ${path} ${scale}`);
|
|
2217
2246
|
return { kind: 'var', name: dst };
|
|
2218
2247
|
}
|
|
2248
|
+
// storage_get_int(storage_ns, array_key, index) -> int
|
|
2249
|
+
// Reads one element from an NBT int-array stored in data storage.
|
|
2250
|
+
// storage_ns : e.g. "math:tables"
|
|
2251
|
+
// array_key : e.g. "sin"
|
|
2252
|
+
// index : integer index (const or runtime)
|
|
2253
|
+
//
|
|
2254
|
+
// Const index: execute store result score $dst rs run data get storage math:tables sin[N] 1
|
|
2255
|
+
// Runtime index: macro sub-function via rs:heap, mirrors readArrayElement.
|
|
2256
|
+
if (name === 'storage_get_int') {
|
|
2257
|
+
const storageNs = this.exprToString(args[0]); // "math:tables"
|
|
2258
|
+
const arrayKey = this.exprToString(args[1]); // "sin"
|
|
2259
|
+
const indexOperand = this.lowerExpr(args[2]);
|
|
2260
|
+
const dst = this.builder.freshTemp();
|
|
2261
|
+
if (indexOperand.kind === 'const') {
|
|
2262
|
+
this.builder.emitRaw(`execute store result score ${dst} rs run data get storage ${storageNs} ${arrayKey}[${indexOperand.value}] 1`);
|
|
2263
|
+
}
|
|
2264
|
+
else {
|
|
2265
|
+
// Runtime index: store the index into rs:heap under a unique key,
|
|
2266
|
+
// then call a macro sub-function that uses $(key) to index the array.
|
|
2267
|
+
const macroKey = `__sgi_${this.foreachCounter++}`;
|
|
2268
|
+
const subFnName = `${this.currentFn}/__sgi_${this.foreachCounter++}`;
|
|
2269
|
+
const indexVar = indexOperand.kind === 'var'
|
|
2270
|
+
? indexOperand.name
|
|
2271
|
+
: this.operandToVar(indexOperand);
|
|
2272
|
+
this.builder.emitRaw(`execute store result storage rs:heap ${macroKey} int 1 run scoreboard players get ${indexVar} rs`);
|
|
2273
|
+
this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`);
|
|
2274
|
+
// Prefix \x01 is a sentinel for the MC macro '$' line-start marker.
|
|
2275
|
+
// We avoid using literal '$execute' here so the pre-alloc pass
|
|
2276
|
+
// doesn't mistakenly register 'execute' as a scoreboard variable.
|
|
2277
|
+
// Codegen replaces \x01 → '$' when emitting the mc function file.
|
|
2278
|
+
this.emitRawSubFunction(subFnName, `\x01execute store result score ${dst} rs run data get storage ${storageNs} ${arrayKey}[$(${macroKey})] 1`);
|
|
2279
|
+
}
|
|
2280
|
+
return { kind: 'var', name: dst };
|
|
2281
|
+
}
|
|
2282
|
+
// storage_set_array(storage_ns, array_key, nbt_array_literal)
|
|
2283
|
+
// Writes a literal NBT int array to data storage (used in @load for tables).
|
|
2284
|
+
// storage_set_array("math:tables", "sin", "[0, 17, 35, ...]")
|
|
2285
|
+
if (name === 'storage_set_array') {
|
|
2286
|
+
const storageNs = this.exprToString(args[0]);
|
|
2287
|
+
const arrayKey = this.exprToString(args[1]);
|
|
2288
|
+
const nbtLiteral = this.exprToString(args[2]);
|
|
2289
|
+
this.builder.emitRaw(`data modify storage ${storageNs} ${arrayKey} set value ${nbtLiteral}`);
|
|
2290
|
+
return { kind: 'const', value: 0 };
|
|
2291
|
+
}
|
|
2219
2292
|
// data_merge(target, nbt) — merge NBT into entity/block/storage
|
|
2220
2293
|
// data_merge(@s, { Invisible: 1b, Silent: 1b })
|
|
2221
2294
|
if (name === 'data_merge') {
|
|
@@ -3006,7 +3079,7 @@ class Lowering {
|
|
|
3006
3079
|
const indexVar = index.kind === 'var' ? index.name : this.operandToVar(index);
|
|
3007
3080
|
this.builder.emitRaw(`execute store result storage rs:heap ${macroKey} int 1 run scoreboard players get ${indexVar} rs`);
|
|
3008
3081
|
this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`);
|
|
3009
|
-
this.emitRawSubFunction(subFnName,
|
|
3082
|
+
this.emitRawSubFunction(subFnName, `\x01execute store result score ${dst} rs run data get storage rs:heap ${arrayName}[$(${macroKey})]`);
|
|
3010
3083
|
return { kind: 'var', name: dst };
|
|
3011
3084
|
}
|
|
3012
3085
|
emitRawSubFunction(name, ...commands) {
|
package/dist/optimizer/dce.js
CHANGED
|
@@ -115,12 +115,17 @@ class DeadCodeEliminator {
|
|
|
115
115
|
findEntryPoints(program) {
|
|
116
116
|
const entries = new Set();
|
|
117
117
|
for (const fn of program.declarations) {
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
// Library functions (from `module library;` or `librarySources`) are
|
|
119
|
+
// NOT MC entry points — they're only kept if reachable from user code.
|
|
120
|
+
// Exception: decorators like @tick / @load / @on / @keep always force inclusion.
|
|
121
|
+
if (!fn.isLibraryFn) {
|
|
122
|
+
// All top-level non-library functions are entry points (callable via /function)
|
|
123
|
+
// Exception: functions starting with _ are considered private/internal
|
|
124
|
+
if (!fn.name.startsWith('_')) {
|
|
125
|
+
entries.add(fn.name);
|
|
126
|
+
}
|
|
122
127
|
}
|
|
123
|
-
// Decorated functions are always entry points
|
|
128
|
+
// Decorated functions are always entry points regardless of library mode or _ prefix
|
|
124
129
|
if (fn.decorators.some(decorator => [
|
|
125
130
|
'tick',
|
|
126
131
|
'load',
|
|
@@ -148,6 +153,17 @@ class DeadCodeEliminator {
|
|
|
148
153
|
}
|
|
149
154
|
this.reachableFunctions.add(fnName);
|
|
150
155
|
this.collectFunctionRefs(fn);
|
|
156
|
+
// @requires("dep") — when fn is reachable, its required dependencies are
|
|
157
|
+
// also pulled into the reachable set so they survive DCE.
|
|
158
|
+
for (const decorator of fn.decorators) {
|
|
159
|
+
if (decorator.name === 'require_on_load') {
|
|
160
|
+
for (const arg of decorator.rawArgs ?? []) {
|
|
161
|
+
if (arg.kind === 'string') {
|
|
162
|
+
this.markReachable(arg.value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
151
167
|
}
|
|
152
168
|
collectFunctionRefs(fn) {
|
|
153
169
|
const scope = [fn.params.map(param => ({ id: `param:${fn.name}:${param.name}`, name: param.name }))];
|
package/dist/optimizer/passes.js
CHANGED
|
@@ -93,32 +93,44 @@ function copyPropagation(fn) {
|
|
|
93
93
|
return op;
|
|
94
94
|
return copies.get(op.name) ?? op;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Invalidate all copies that became stale because `written` was modified.
|
|
98
|
+
* When $y is overwritten, any mapping copies[$tmp] = $y is now stale:
|
|
99
|
+
* reading $tmp would return the OLD $y value via the copy, but $y now holds
|
|
100
|
+
* a different value. Remove both the direct entry (copies[$y]) and every
|
|
101
|
+
* reverse entry that points at $y.
|
|
102
|
+
*/
|
|
103
|
+
function invalidate(written) {
|
|
104
|
+
copies.delete(written);
|
|
105
|
+
for (const [k, v] of copies) {
|
|
106
|
+
if (v.kind === 'var' && v.name === written)
|
|
107
|
+
copies.delete(k);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
96
110
|
const newInstrs = [];
|
|
97
111
|
for (const instr of block.instrs) {
|
|
98
112
|
switch (instr.op) {
|
|
99
113
|
case 'assign': {
|
|
100
114
|
const src = resolve(instr.src);
|
|
115
|
+
invalidate(instr.dst);
|
|
101
116
|
// Only propagate scalars (var or const), not storage
|
|
102
117
|
if (src.kind === 'var' || src.kind === 'const') {
|
|
103
118
|
copies.set(instr.dst, src);
|
|
104
119
|
}
|
|
105
|
-
else {
|
|
106
|
-
copies.delete(instr.dst);
|
|
107
|
-
}
|
|
108
120
|
newInstrs.push({ ...instr, src });
|
|
109
121
|
break;
|
|
110
122
|
}
|
|
111
123
|
case 'binop':
|
|
112
|
-
|
|
124
|
+
invalidate(instr.dst);
|
|
113
125
|
newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) });
|
|
114
126
|
break;
|
|
115
127
|
case 'cmp':
|
|
116
|
-
|
|
128
|
+
invalidate(instr.dst);
|
|
117
129
|
newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) });
|
|
118
130
|
break;
|
|
119
131
|
case 'call':
|
|
120
132
|
if (instr.dst)
|
|
121
|
-
|
|
133
|
+
invalidate(instr.dst);
|
|
122
134
|
newInstrs.push({ ...instr, args: instr.args.map(resolve) });
|
|
123
135
|
break;
|
|
124
136
|
default:
|
|
@@ -21,6 +21,8 @@ function operandToScore(op) {
|
|
|
21
21
|
return `${varRef(op.name)} ${OBJ}`;
|
|
22
22
|
if (op.kind === 'const')
|
|
23
23
|
return `$const_${op.value} ${OBJ}`;
|
|
24
|
+
if (op.kind === 'param')
|
|
25
|
+
return `$p${op.index} ${OBJ}`;
|
|
24
26
|
throw new Error(`Cannot convert storage operand to score: ${op.path}`);
|
|
25
27
|
}
|
|
26
28
|
function emitInstr(instr, namespace) {
|
|
@@ -35,6 +37,11 @@ function emitInstr(instr, namespace) {
|
|
|
35
37
|
cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = ${varRef(instr.src.name)} ${OBJ}`,
|
|
36
38
|
});
|
|
37
39
|
}
|
|
40
|
+
else if (instr.src.kind === 'param') {
|
|
41
|
+
commands.push({
|
|
42
|
+
cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $p${instr.src.index} ${OBJ}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
38
45
|
else {
|
|
39
46
|
commands.push({
|
|
40
47
|
cmd: `execute store result score ${varRef(instr.dst)} ${OBJ} run data get storage ${instr.src.path}`,
|
package/dist/parser/index.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ export declare class Parser {
|
|
|
11
11
|
private pos;
|
|
12
12
|
private sourceLines;
|
|
13
13
|
private filePath?;
|
|
14
|
+
/** Set to true once `module library;` is seen — all subsequent fn declarations
|
|
15
|
+
* will be marked isLibraryFn=true. When library sources are parsed via the
|
|
16
|
+
* `librarySources` compile option, each source is parsed by its own fresh
|
|
17
|
+
* Parser instance, so this flag never bleeds into user code. */
|
|
18
|
+
private inLibraryMode;
|
|
14
19
|
constructor(tokens: Token[], source?: string, filePath?: string);
|
|
15
20
|
private peek;
|
|
16
21
|
private advance;
|
package/dist/parser/index.js
CHANGED
|
@@ -64,6 +64,11 @@ function computeIsSingle(raw) {
|
|
|
64
64
|
class Parser {
|
|
65
65
|
constructor(tokens, source, filePath) {
|
|
66
66
|
this.pos = 0;
|
|
67
|
+
/** Set to true once `module library;` is seen — all subsequent fn declarations
|
|
68
|
+
* will be marked isLibraryFn=true. When library sources are parsed via the
|
|
69
|
+
* `librarySources` compile option, each source is parsed by its own fresh
|
|
70
|
+
* Parser instance, so this flag never bleeds into user code. */
|
|
71
|
+
this.inLibraryMode = false;
|
|
67
72
|
this.tokens = tokens;
|
|
68
73
|
this.sourceLines = source?.split('\n') ?? [];
|
|
69
74
|
this.filePath = filePath;
|
|
@@ -135,6 +140,7 @@ class Parser {
|
|
|
135
140
|
const implBlocks = [];
|
|
136
141
|
const enums = [];
|
|
137
142
|
const consts = [];
|
|
143
|
+
let isLibrary = false;
|
|
138
144
|
// Check for namespace declaration
|
|
139
145
|
if (this.check('namespace')) {
|
|
140
146
|
this.advance();
|
|
@@ -142,6 +148,19 @@ class Parser {
|
|
|
142
148
|
namespace = name.value;
|
|
143
149
|
this.expect(';');
|
|
144
150
|
}
|
|
151
|
+
// Check for module declaration: `module library;`
|
|
152
|
+
// Library-mode: all functions parsed from this point are marked isLibraryFn=true.
|
|
153
|
+
// When using the `librarySources` compile option, each library source is parsed
|
|
154
|
+
// by its own fresh Parser — so this flag never bleeds into user code.
|
|
155
|
+
if (this.check('module')) {
|
|
156
|
+
this.advance();
|
|
157
|
+
const modKind = this.expect('ident');
|
|
158
|
+
if (modKind.value === 'library') {
|
|
159
|
+
isLibrary = true;
|
|
160
|
+
this.inLibraryMode = true;
|
|
161
|
+
}
|
|
162
|
+
this.expect(';');
|
|
163
|
+
}
|
|
145
164
|
// Parse struct and function declarations
|
|
146
165
|
while (!this.check('eof')) {
|
|
147
166
|
if (this.check('let')) {
|
|
@@ -168,7 +187,7 @@ class Parser {
|
|
|
168
187
|
declarations.push(this.parseFnDecl());
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
|
-
return { namespace, globals, declarations, structs, implBlocks, enums, consts };
|
|
190
|
+
return { namespace, globals, declarations, structs, implBlocks, enums, consts, isLibrary };
|
|
172
191
|
}
|
|
173
192
|
// -------------------------------------------------------------------------
|
|
174
193
|
// Struct Declaration
|
|
@@ -267,7 +286,8 @@ class Parser {
|
|
|
267
286
|
returnType = this.parseType();
|
|
268
287
|
}
|
|
269
288
|
const body = this.parseBlock();
|
|
270
|
-
|
|
289
|
+
const fn = this.withLoc({ name, params, returnType, decorators, body, isLibraryFn: this.inLibraryMode || undefined }, fnToken);
|
|
290
|
+
return fn;
|
|
271
291
|
}
|
|
272
292
|
/** Parse a `declare fn name(params): returnType;` stub — no body, just discard. */
|
|
273
293
|
parseDeclareStub() {
|
|
@@ -336,6 +356,27 @@ class Parser {
|
|
|
336
356
|
return { name, args };
|
|
337
357
|
}
|
|
338
358
|
}
|
|
359
|
+
// @require_on_load(fn_name) — when this fn is used, fn_name is called from __load.
|
|
360
|
+
// Accepts bare identifiers (with optional leading _) or quoted strings.
|
|
361
|
+
if (name === 'require_on_load') {
|
|
362
|
+
const rawArgs = [];
|
|
363
|
+
for (const part of argsStr.split(',')) {
|
|
364
|
+
const trimmed = part.trim();
|
|
365
|
+
// Bare identifier: @require_on_load(_math_init)
|
|
366
|
+
const identMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
367
|
+
if (identMatch) {
|
|
368
|
+
rawArgs.push({ kind: 'string', value: identMatch[1] });
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
// Quoted string fallback: @require_on_load("_math_init")
|
|
372
|
+
const strMatch = trimmed.match(/^"([^"]*)"$/);
|
|
373
|
+
if (strMatch) {
|
|
374
|
+
rawArgs.push({ kind: 'string', value: strMatch[1] });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return { name, rawArgs };
|
|
379
|
+
}
|
|
339
380
|
// Handle key=value format (e.g., rate=20)
|
|
340
381
|
for (const part of argsStr.split(',')) {
|
|
341
382
|
const [key, val] = part.split('=').map(s => s.trim());
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export declare class MCRuntime {
|
|
|
43
43
|
private entityIdCounter;
|
|
44
44
|
private returnValue;
|
|
45
45
|
private shouldReturn;
|
|
46
|
+
private currentMacroContext;
|
|
46
47
|
constructor(namespace: string);
|
|
47
48
|
loadDatapack(dir: string): void;
|
|
48
49
|
loadFunction(name: string, lines: string[]): void;
|
|
@@ -56,8 +57,13 @@ export declare class MCRuntime {
|
|
|
56
57
|
private execExecute;
|
|
57
58
|
private parseNextSelector;
|
|
58
59
|
private execFunctionCmd;
|
|
60
|
+
/** Expand MC macro placeholders: $(key) → value from currentMacroContext */
|
|
61
|
+
private expandMacro;
|
|
59
62
|
private execData;
|
|
60
63
|
private parseDataValue;
|
|
64
|
+
/** Return the whole storage compound at storagePath as a flat key→value map.
|
|
65
|
+
* Used by 'function ... with storage' to provide macro context. */
|
|
66
|
+
private getStorageCompound;
|
|
61
67
|
private getStorageField;
|
|
62
68
|
private setStorageField;
|
|
63
69
|
private removeStorageField;
|
package/dist/runtime/index.js
CHANGED
|
@@ -235,6 +235,8 @@ class MCRuntime {
|
|
|
235
235
|
this.entityIdCounter = 0;
|
|
236
236
|
// Flag to stop function execution (for return)
|
|
237
237
|
this.shouldReturn = false;
|
|
238
|
+
// Current MC macro context: key → value (set by 'function ... with storage')
|
|
239
|
+
this.currentMacroContext = null;
|
|
238
240
|
this.namespace = namespace;
|
|
239
241
|
// Initialize default objective
|
|
240
242
|
this.scoreboard.set('rs', new Map());
|
|
@@ -321,6 +323,12 @@ class MCRuntime {
|
|
|
321
323
|
cmd = cmd.trim();
|
|
322
324
|
if (!cmd || cmd.startsWith('#'))
|
|
323
325
|
return true;
|
|
326
|
+
// MC macro command: line starts with '$'.
|
|
327
|
+
// Expand $(key) placeholders from currentMacroContext, then execute.
|
|
328
|
+
if (cmd.startsWith('$')) {
|
|
329
|
+
const expanded = this.expandMacro(cmd.slice(1));
|
|
330
|
+
return this.execCommand(expanded, executor);
|
|
331
|
+
}
|
|
324
332
|
// Parse command
|
|
325
333
|
if (cmd.startsWith('scoreboard ')) {
|
|
326
334
|
return this.execScoreboard(cmd);
|
|
@@ -502,7 +510,12 @@ class MCRuntime {
|
|
|
502
510
|
const value = storeTarget.type === 'result'
|
|
503
511
|
? (this.returnValue ?? (result ? 1 : 0))
|
|
504
512
|
: (result ? 1 : 0);
|
|
505
|
-
|
|
513
|
+
if ('storagePath' in storeTarget) {
|
|
514
|
+
this.setStorageField(storeTarget.storagePath, storeTarget.field, value);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
this.setScore(storeTarget.player, storeTarget.objective, value);
|
|
518
|
+
}
|
|
506
519
|
}
|
|
507
520
|
return result;
|
|
508
521
|
}
|
|
@@ -577,15 +590,44 @@ class MCRuntime {
|
|
|
577
590
|
// Handle 'unless score ...'
|
|
578
591
|
if (rest.startsWith('unless score ')) {
|
|
579
592
|
rest = rest.slice(13);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
593
|
+
// unless score <player> <obj> matches <range>
|
|
594
|
+
const matchesParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/);
|
|
595
|
+
if (matchesParts) {
|
|
596
|
+
const [, player, obj, rangeStr, remaining] = matchesParts;
|
|
583
597
|
const range = parseRange(rangeStr);
|
|
584
598
|
const score = this.getScore(player, obj);
|
|
585
599
|
condition = condition && !matchesRange(score, range);
|
|
586
600
|
rest = remaining.trim();
|
|
587
601
|
continue;
|
|
588
602
|
}
|
|
603
|
+
// unless score <p1> <o1> <op> <p2> <o2>
|
|
604
|
+
const compareMatch = rest.match(/^(\S+)\s+(\S+)\s+([<>=]+)\s+(\S+)\s+(\S+)(.*)$/);
|
|
605
|
+
if (compareMatch) {
|
|
606
|
+
const [, p1, o1, op, p2, o2, remaining] = compareMatch;
|
|
607
|
+
const v1 = this.getScore(p1, o1);
|
|
608
|
+
const v2 = this.getScore(p2, o2);
|
|
609
|
+
let matches = false;
|
|
610
|
+
switch (op) {
|
|
611
|
+
case '=':
|
|
612
|
+
matches = v1 === v2;
|
|
613
|
+
break;
|
|
614
|
+
case '<':
|
|
615
|
+
matches = v1 < v2;
|
|
616
|
+
break;
|
|
617
|
+
case '<=':
|
|
618
|
+
matches = v1 <= v2;
|
|
619
|
+
break;
|
|
620
|
+
case '>':
|
|
621
|
+
matches = v1 > v2;
|
|
622
|
+
break;
|
|
623
|
+
case '>=':
|
|
624
|
+
matches = v1 >= v2;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
condition = condition && !matches; // unless = negate
|
|
628
|
+
rest = remaining.trim();
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
589
631
|
}
|
|
590
632
|
// Handle 'if entity <selector>'
|
|
591
633
|
if (rest.startsWith('if entity ')) {
|
|
@@ -605,6 +647,18 @@ class MCRuntime {
|
|
|
605
647
|
condition = condition && entities.length === 0;
|
|
606
648
|
continue;
|
|
607
649
|
}
|
|
650
|
+
// Handle 'store result storage <ns:path> <field> <type> <scale>'
|
|
651
|
+
if (rest.startsWith('store result storage ')) {
|
|
652
|
+
rest = rest.slice(21);
|
|
653
|
+
// format: <ns:path> <field> <type> <scale> <run-cmd>
|
|
654
|
+
const storageParts = rest.match(/^(\S+)\s+(\S+)\s+(\S+)\s+([\d.]+)\s+(.*)$/);
|
|
655
|
+
if (storageParts) {
|
|
656
|
+
const [, storagePath, field, , , remaining] = storageParts;
|
|
657
|
+
storeTarget = { storagePath, field, type: 'result' };
|
|
658
|
+
rest = remaining.trim();
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
608
662
|
// Handle 'store result score <player> <obj>'
|
|
609
663
|
if (rest.startsWith('store result score ')) {
|
|
610
664
|
rest = rest.slice(19);
|
|
@@ -637,7 +691,12 @@ class MCRuntime {
|
|
|
637
691
|
const value = storeTarget.type === 'result'
|
|
638
692
|
? (this.returnValue ?? (condition ? 1 : 0))
|
|
639
693
|
: (condition ? 1 : 0);
|
|
640
|
-
|
|
694
|
+
if ('storagePath' in storeTarget) {
|
|
695
|
+
this.setStorageField(storeTarget.storagePath, storeTarget.field, value);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
this.setScore(storeTarget.player, storeTarget.objective, value);
|
|
699
|
+
}
|
|
641
700
|
}
|
|
642
701
|
return condition;
|
|
643
702
|
}
|
|
@@ -659,12 +718,35 @@ class MCRuntime {
|
|
|
659
718
|
// Function Command
|
|
660
719
|
// -------------------------------------------------------------------------
|
|
661
720
|
execFunctionCmd(cmd, executor) {
|
|
662
|
-
|
|
721
|
+
let fnRef = cmd.slice(9).trim(); // remove 'function '
|
|
722
|
+
// Handle 'function ns:name with storage ns:path' — MC macro calling convention.
|
|
723
|
+
// The called function may have $( ) placeholders that need to be expanded
|
|
724
|
+
// using the provided storage compound. We execute the function after
|
|
725
|
+
// expanding its macro context.
|
|
726
|
+
const withStorageMatch = fnRef.match(/^(\S+)\s+with\s+storage\s+(\S+)$/);
|
|
727
|
+
if (withStorageMatch) {
|
|
728
|
+
const [, actualFnName, storagePath] = withStorageMatch;
|
|
729
|
+
const macroContext = this.getStorageCompound(storagePath) ?? {};
|
|
730
|
+
const outerShouldReturn = this.shouldReturn;
|
|
731
|
+
const outerMacroCtx = this.currentMacroContext;
|
|
732
|
+
this.currentMacroContext = macroContext;
|
|
733
|
+
this.execFunction(actualFnName, executor);
|
|
734
|
+
this.currentMacroContext = outerMacroCtx;
|
|
735
|
+
this.shouldReturn = outerShouldReturn;
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
663
738
|
const outerShouldReturn = this.shouldReturn;
|
|
664
|
-
this.execFunction(
|
|
739
|
+
this.execFunction(fnRef, executor);
|
|
665
740
|
this.shouldReturn = outerShouldReturn;
|
|
666
741
|
return true;
|
|
667
742
|
}
|
|
743
|
+
/** Expand MC macro placeholders: $(key) → value from currentMacroContext */
|
|
744
|
+
expandMacro(cmd) {
|
|
745
|
+
return cmd.replace(/\$\(([^)]+)\)/g, (_, key) => {
|
|
746
|
+
const val = this.currentMacroContext?.[key];
|
|
747
|
+
return val !== undefined ? String(val) : `$(${key})`;
|
|
748
|
+
});
|
|
749
|
+
}
|
|
668
750
|
// -------------------------------------------------------------------------
|
|
669
751
|
// Data Commands
|
|
670
752
|
// -------------------------------------------------------------------------
|
|
@@ -689,8 +771,18 @@ class MCRuntime {
|
|
|
689
771
|
}
|
|
690
772
|
return true;
|
|
691
773
|
}
|
|
692
|
-
// data get storage <ns:path> <field>
|
|
693
|
-
const
|
|
774
|
+
// data get storage <ns:path> <field>[<index>] [scale] (array element access)
|
|
775
|
+
const getArrMatch = cmd.match(/^data get storage (\S+) (\S+)\[(\d+)\](?:\s+[\d.]+)?$/);
|
|
776
|
+
if (getArrMatch) {
|
|
777
|
+
const [, storagePath, field, indexStr] = getArrMatch;
|
|
778
|
+
const arr = this.getStorageField(storagePath, field);
|
|
779
|
+
const idx = parseInt(indexStr, 10);
|
|
780
|
+
const value = Array.isArray(arr) ? arr[idx] : undefined;
|
|
781
|
+
this.returnValue = typeof value === 'number' ? value : 0;
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
// data get storage <ns:path> <field> [scale]
|
|
785
|
+
const getMatch = cmd.match(/^data get storage (\S+) (\S+)(?:\s+[\d.]+)?$/);
|
|
694
786
|
if (getMatch) {
|
|
695
787
|
const [, storagePath, field] = getMatch;
|
|
696
788
|
const value = this.getStorageField(storagePath, field);
|
|
@@ -736,6 +828,14 @@ class MCRuntime {
|
|
|
736
828
|
return str;
|
|
737
829
|
}
|
|
738
830
|
}
|
|
831
|
+
/** Return the whole storage compound at storagePath as a flat key→value map.
|
|
832
|
+
* Used by 'function ... with storage' to provide macro context. */
|
|
833
|
+
getStorageCompound(storagePath) {
|
|
834
|
+
const data = this.storage.get(storagePath);
|
|
835
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
836
|
+
return null;
|
|
837
|
+
return data;
|
|
838
|
+
}
|
|
739
839
|
getStorageField(storagePath, field) {
|
|
740
840
|
const data = this.storage.get(storagePath) ?? {};
|
|
741
841
|
const segments = this.parseStoragePath(field);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "redscript-vscode",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.28",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"redscript": "file:../../"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"../..": {
|
|
25
25
|
"name": "redscript-mc",
|
|
26
|
-
"version": "1.2.
|
|
26
|
+
"version": "1.2.26",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"bin": {
|
|
29
29
|
"redscript": "dist/cli.js",
|
|
@@ -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.0.
|
|
5
|
+
"version": "1.0.28",
|
|
6
6
|
"publisher": "bkmashiro",
|
|
7
7
|
"icon": "icon.png",
|
|
8
8
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -173,7 +173,7 @@ describe('CLI API', () => {
|
|
|
173
173
|
const mainFn = result.files.find(file => file.path.endsWith('/main.mcfunction'))
|
|
174
174
|
expect(doneFn?.content).toContain('scoreboard players get timer_ticks rs')
|
|
175
175
|
expect(doneFn?.content).toContain('return run scoreboard players get')
|
|
176
|
-
expect(mainFn?.content).toContain('execute if score $
|
|
176
|
+
expect(mainFn?.content).toContain('execute if score $main_finished rs matches 1..')
|
|
177
177
|
})
|
|
178
178
|
|
|
179
179
|
it('Timer.tick increments', () => {
|