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.
Files changed (57) hide show
  1. package/dist/__tests__/cli.test.js +1 -1
  2. package/dist/__tests__/codegen.test.js +12 -6
  3. package/dist/__tests__/e2e.test.js +6 -6
  4. package/dist/__tests__/lowering.test.js +8 -8
  5. package/dist/__tests__/optimizer.test.js +31 -0
  6. package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
  7. package/dist/__tests__/stdlib-advanced.test.js +264 -0
  8. package/dist/__tests__/stdlib-math.test.d.ts +7 -0
  9. package/dist/__tests__/stdlib-math.test.js +352 -0
  10. package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
  11. package/dist/__tests__/stdlib-vec.test.js +264 -0
  12. package/dist/ast/types.d.ts +17 -1
  13. package/dist/codegen/mcfunction/index.js +154 -18
  14. package/dist/codegen/var-allocator.d.ts +17 -0
  15. package/dist/codegen/var-allocator.js +26 -0
  16. package/dist/compile.d.ts +14 -0
  17. package/dist/compile.js +62 -5
  18. package/dist/index.js +20 -1
  19. package/dist/ir/types.d.ts +4 -0
  20. package/dist/lexer/index.d.ts +1 -1
  21. package/dist/lexer/index.js +1 -0
  22. package/dist/lowering/index.d.ts +5 -0
  23. package/dist/lowering/index.js +83 -10
  24. package/dist/optimizer/dce.js +21 -5
  25. package/dist/optimizer/passes.js +18 -6
  26. package/dist/optimizer/structure.js +7 -0
  27. package/dist/parser/index.d.ts +5 -0
  28. package/dist/parser/index.js +43 -2
  29. package/dist/runtime/index.d.ts +6 -0
  30. package/dist/runtime/index.js +109 -9
  31. package/editors/vscode/package-lock.json +3 -3
  32. package/editors/vscode/package.json +1 -1
  33. package/package.json +1 -1
  34. package/src/__tests__/cli.test.ts +1 -1
  35. package/src/__tests__/codegen.test.ts +12 -6
  36. package/src/__tests__/e2e.test.ts +6 -6
  37. package/src/__tests__/lowering.test.ts +8 -8
  38. package/src/__tests__/optimizer.test.ts +33 -0
  39. package/src/__tests__/stdlib-advanced.test.ts +259 -0
  40. package/src/__tests__/stdlib-math.test.ts +374 -0
  41. package/src/__tests__/stdlib-vec.test.ts +259 -0
  42. package/src/ast/types.ts +11 -1
  43. package/src/codegen/mcfunction/index.ts +143 -19
  44. package/src/codegen/var-allocator.ts +29 -0
  45. package/src/compile.ts +72 -5
  46. package/src/index.ts +21 -1
  47. package/src/ir/types.ts +2 -0
  48. package/src/lexer/index.ts +2 -1
  49. package/src/lowering/index.ts +96 -10
  50. package/src/optimizer/dce.ts +22 -5
  51. package/src/optimizer/passes.ts +18 -5
  52. package/src/optimizer/structure.ts +6 -1
  53. package/src/parser/index.ts +47 -2
  54. package/src/runtime/index.ts +108 -10
  55. package/src/stdlib/advanced.mcrs +249 -0
  56. package/src/stdlib/math.mcrs +259 -19
  57. package/src/stdlib/vec.mcrs +246 -0
@@ -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, `$${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, `$${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 $p0, $p1, ... to named variables
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 = `$${paramName}`;
588
- this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` });
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(`$${param.name}`, { kind: 'const', value: 0 });
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 = `$${stmt.name}`;
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 = `$${stmt.varName}`;
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 = `$${stmt.binding}`;
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, `$execute store result score ${dst} rs run data get storage rs:heap ${arrayName}[$(${macroKey})]`);
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) {
@@ -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
- // All top-level functions are entry points (callable via /function)
119
- // Exception: functions starting with _ are considered private/internal
120
- if (!fn.name.startsWith('_')) {
121
- entries.add(fn.name);
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 (even if prefixed with _)
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 }))];
@@ -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
- copies.delete(instr.dst);
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
- copies.delete(instr.dst);
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
- copies.delete(instr.dst);
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}`,
@@ -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;
@@ -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
- return this.withLoc({ name, params, returnType, decorators, body }, fnToken);
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());
@@ -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;
@@ -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
- this.setScore(storeTarget.player, storeTarget.objective, value);
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
- const scoreParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/);
581
- if (scoreParts) {
582
- const [, player, obj, rangeStr, remaining] = scoreParts;
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
- this.setScore(storeTarget.player, storeTarget.objective, value);
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
- const fnName = cmd.slice(9).trim(); // remove 'function '
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(fnName, executor);
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 getMatch = cmd.match(/^data get storage (\S+) (\S+)$/);
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.16",
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.16",
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.25",
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.16",
5
+ "version": "1.0.28",
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": "1.2.25",
3
+ "version": "1.2.26",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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 $finished rs matches 1..')
176
+ expect(mainFn?.content).toContain('execute if score $main_finished rs matches 1..')
177
177
  })
178
178
 
179
179
  it('Timer.tick increments', () => {