redscript-mc 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +58 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +10 -10
  9. package/dist/__tests__/codegen.test.js +1 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +146 -5
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lowering.test.js +36 -3
  15. package/dist/__tests__/mc-integration.test.js +255 -10
  16. package/dist/__tests__/mc-syntax.test.js +3 -3
  17. package/dist/__tests__/nbt.test.js +2 -2
  18. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  19. package/dist/__tests__/runtime.test.js +1 -1
  20. package/dist/ast/types.d.ts +21 -3
  21. package/dist/cli.js +25 -7
  22. package/dist/codegen/mcfunction/index.d.ts +1 -1
  23. package/dist/codegen/mcfunction/index.js +8 -2
  24. package/dist/codegen/structure/index.js +7 -1
  25. package/dist/formatter/index.d.ts +1 -0
  26. package/dist/formatter/index.js +26 -0
  27. package/dist/ir/builder.d.ts +2 -1
  28. package/dist/ir/types.d.ts +7 -2
  29. package/dist/ir/types.js +1 -1
  30. package/dist/lowering/index.d.ts +2 -0
  31. package/dist/lowering/index.js +183 -8
  32. package/dist/mc-test/runner.d.ts +2 -2
  33. package/dist/mc-test/runner.js +3 -3
  34. package/dist/mc-test/setup.js +2 -2
  35. package/dist/parser/index.d.ts +2 -0
  36. package/dist/parser/index.js +75 -7
  37. package/docs/COMPILATION_STATS.md +24 -24
  38. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  39. package/docs/STRUCTURE_TARGET.md +1 -1
  40. package/editors/vscode/.vscodeignore +1 -0
  41. package/editors/vscode/icons/mcrs.svg +7 -0
  42. package/editors/vscode/icons/redscript-icons.json +10 -0
  43. package/editors/vscode/out/extension.js +152 -9
  44. package/editors/vscode/package.json +10 -3
  45. package/editors/vscode/src/hover.ts +55 -2
  46. package/editors/vscode/src/symbols.ts +42 -0
  47. package/package.json +1 -1
  48. package/src/__tests__/cli.test.ts +10 -10
  49. package/src/__tests__/codegen.test.ts +1 -1
  50. package/src/__tests__/diagnostics.test.ts +5 -5
  51. package/src/__tests__/e2e.test.ts +134 -5
  52. package/src/__tests__/lowering.test.ts +48 -3
  53. package/src/__tests__/mc-integration.test.ts +285 -10
  54. package/src/__tests__/mc-syntax.test.ts +3 -3
  55. package/src/__tests__/nbt.test.ts +2 -2
  56. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  57. package/src/__tests__/runtime.test.ts +1 -1
  58. package/src/ast/types.ts +20 -3
  59. package/src/cli.ts +10 -10
  60. package/src/codegen/mcfunction/index.ts +9 -2
  61. package/src/codegen/structure/index.ts +8 -1
  62. package/src/examples/capture_the_flag.mcrs +208 -0
  63. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  64. package/src/examples/hunger_games.mcrs +301 -0
  65. package/src/examples/new_features_demo.mcrs +193 -0
  66. package/src/examples/parkour_race.mcrs +233 -0
  67. package/src/examples/rpg.mcrs +13 -0
  68. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  69. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  70. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  71. package/src/examples/zombie_survival.mcrs +314 -0
  72. package/src/ir/builder.ts +3 -1
  73. package/src/ir/types.ts +8 -2
  74. package/src/lowering/index.ts +156 -8
  75. package/src/mc-test/runner.ts +3 -3
  76. package/src/mc-test/setup.ts +2 -2
  77. package/src/parser/index.ts +81 -8
  78. package/src/stdlib/README.md +155 -147
  79. package/src/stdlib/bossbar.mcrs +68 -0
  80. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  81. package/src/stdlib/effects.mcrs +64 -0
  82. package/src/stdlib/interactions.mcrs +195 -0
  83. package/src/stdlib/inventory.mcrs +38 -0
  84. package/src/stdlib/mobs.mcrs +99 -0
  85. package/src/stdlib/particles.mcrs +52 -0
  86. package/src/stdlib/sets.mcrs +20 -0
  87. package/src/stdlib/spawn.mcrs +41 -0
  88. package/src/stdlib/teams.mcrs +68 -0
  89. package/src/stdlib/world.mcrs +92 -0
  90. package/src/examples/rpg.rs +0 -13
  91. package/src/stdlib/mobs.rs +0 -99
  92. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  93. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  94. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  95. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  96. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  97. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  98. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  99. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  100. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  101. /package/src/stdlib/{timer.rs → timer.mcrs} +0 -0
  102. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  103. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  104. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  105. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  106. /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
@@ -26,7 +26,7 @@ fn turret_tick() {
26
26
  const result = (0, index_1.compile)(source, { namespace: 'test' });
27
27
  const parent = getFileContent(result.files, 'data/test/function/turret_tick.mcfunction');
28
28
  const loopBody = getFileContent(result.files, 'data/test/function/turret_tick/foreach_0.mcfunction');
29
- const hoistedRead = 'execute store result score $t0 rs run scoreboard players get config turret_range';
29
+ const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config turret_range';
30
30
  const executeCall = 'execute as @e[tag=turret] run function test:turret_tick/foreach_0';
31
31
  expect(parent).toContain(hoistedRead);
32
32
  expect(parent.indexOf(hoistedRead)).toBeLessThan(parent.indexOf(executeCall));
@@ -48,7 +48,7 @@ fn read_twice() {
48
48
  const fn = getFileContent(result.files, 'data/test/function/read_twice.mcfunction');
49
49
  const readMatches = fn.match(/scoreboard players get @s coins/g) ?? [];
50
50
  expect(readMatches).toHaveLength(1);
51
- expect(fn).toContain('scoreboard players operation $t1 rs = $t0 rs');
51
+ expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs');
52
52
  });
53
53
  test('reuses duplicate arithmetic sequences', () => {
54
54
  const source = `
@@ -65,7 +65,7 @@ fn math() {
65
65
  const fn = getFileContent(result.files, 'data/test/function/math.mcfunction');
66
66
  const addMatches = fn.match(/\+= \$const_2 rs/g) ?? [];
67
67
  expect(addMatches).toHaveLength(1);
68
- expect(fn).toContain('scoreboard players operation $t1 rs = $t0 rs');
68
+ expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs');
69
69
  });
70
70
  });
71
71
  describe('setblock batching', () => {
@@ -58,7 +58,7 @@ function loadExample(name) {
58
58
  }
59
59
  describe('MCRuntime behavioral integration', () => {
60
60
  it('runs the counter example and increments the scoreboard across ticks', () => {
61
- const runtime = loadCompiledProgram(loadExample('counter.rs'));
61
+ const runtime = loadCompiledProgram(loadExample('counter.mcrs'));
62
62
  runtime.load();
63
63
  runtime.ticks(5);
64
64
  expect(runtime.getScore('counter', 'ticks')).toBe(5);
@@ -54,6 +54,11 @@ export interface SelectorFilter {
54
54
  sort?: 'nearest' | 'furthest' | 'random' | 'arbitrary';
55
55
  nbt?: string;
56
56
  gamemode?: string;
57
+ x?: RangeExpr;
58
+ y?: RangeExpr;
59
+ z?: RangeExpr;
60
+ x_rotation?: RangeExpr;
61
+ y_rotation?: RangeExpr;
57
62
  }
58
63
  export interface EntitySelector {
59
64
  kind: SelectorKind;
@@ -213,10 +218,14 @@ export type ExecuteSubcommand = {
213
218
  selector: EntitySelector;
214
219
  } | {
215
220
  kind: 'if_entity';
216
- selector: EntitySelector;
221
+ selector?: EntitySelector;
222
+ varName?: string;
223
+ filters?: SelectorFilter;
217
224
  } | {
218
225
  kind: 'unless_entity';
219
- selector: EntitySelector;
226
+ selector?: EntitySelector;
227
+ varName?: string;
228
+ filters?: SelectorFilter;
220
229
  } | {
221
230
  kind: 'in';
222
231
  dimension: string;
@@ -302,7 +311,7 @@ export type Stmt = {
302
311
  };
303
312
  export type Block = Stmt[];
304
313
  export interface Decorator {
305
- name: 'tick' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team';
314
+ name: 'tick' | 'load' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team';
306
315
  args?: {
307
316
  rate?: number;
308
317
  trigger?: string;
@@ -348,8 +357,17 @@ export interface ConstDecl {
348
357
  value: LiteralExpr;
349
358
  span?: Span;
350
359
  }
360
+ export interface GlobalDecl {
361
+ kind: 'global';
362
+ name: string;
363
+ type: TypeNode;
364
+ init: Expr;
365
+ mutable: boolean;
366
+ span?: Span;
367
+ }
351
368
  export interface Program {
352
369
  namespace: string;
370
+ globals: GlobalDecl[];
353
371
  declarations: FnDecl[];
354
372
  structs: StructDecl[];
355
373
  enums: EnumDecl[];
package/dist/cli.js CHANGED
@@ -60,13 +60,15 @@ Usage:
60
60
  redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
61
61
  redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
62
62
  redscript check <file>
63
+ redscript fmt <file.mcrs> [file2.mcrs ...]
63
64
  redscript repl
64
65
  redscript version
65
66
 
66
67
  Commands:
67
68
  compile Compile a RedScript file to a Minecraft datapack
68
- watch Watch a directory for .rs file changes, recompile, and hot reload
69
+ watch Watch a directory for .mcrs file changes, recompile, and hot reload
69
70
  check Check a RedScript file for errors without generating output
71
+ fmt Auto-format RedScript source files
70
72
  repl Start an interactive RedScript REPL
71
73
  version Print the RedScript version
72
74
 
@@ -275,18 +277,18 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
275
277
  console.error(`Error: ${dir} is not a directory`);
276
278
  process.exit(1);
277
279
  }
278
- console.log(`👁 Watching ${dir} for .rs file changes...`);
280
+ console.log(`👁 Watching ${dir} for .mcrs file changes...`);
279
281
  console.log(` Output: ${output}`);
280
282
  if (hotReloadUrl)
281
283
  console.log(` Hot reload: ${hotReloadUrl}`);
282
284
  console.log(` Press Ctrl+C to stop\n`);
283
285
  // Debounce timer
284
286
  let debounceTimer = null;
285
- // Compile all .rs files in directory
287
+ // Compile all .mcrs files in directory
286
288
  async function compileAll() {
287
289
  const files = findRsFiles(dir);
288
290
  if (files.length === 0) {
289
- console.log(`⚠ No .rs files found in ${dir}`);
291
+ console.log(`⚠ No .mcrs files found in ${dir}`);
290
292
  return;
291
293
  }
292
294
  let hasErrors = false;
@@ -321,7 +323,7 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
321
323
  console.log('');
322
324
  }
323
325
  }
324
- // Find all .rs files recursively
326
+ // Find all .mcrs files recursively
325
327
  function findRsFiles(directory) {
326
328
  const results = [];
327
329
  const entries = fs.readdirSync(directory, { withFileTypes: true });
@@ -330,7 +332,7 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
330
332
  if (entry.isDirectory()) {
331
333
  results.push(...findRsFiles(fullPath));
332
334
  }
333
- else if (entry.isFile() && entry.name.endsWith('.rs')) {
335
+ else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
334
336
  results.push(fullPath);
335
337
  }
336
338
  }
@@ -340,7 +342,7 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
340
342
  void compileAll();
341
343
  // Watch for changes
342
344
  fs.watch(dir, { recursive: true }, (eventType, filename) => {
343
- if (filename && filename.endsWith('.rs')) {
345
+ if (filename && filename.endsWith('.mcrs')) {
344
346
  // Debounce rapid changes
345
347
  if (debounceTimer) {
346
348
  clearTimeout(debounceTimer);
@@ -391,6 +393,22 @@ async function main() {
391
393
  }
392
394
  checkCommand(parsed.file);
393
395
  break;
396
+ case 'fmt':
397
+ case 'format': {
398
+ const files = args.filter(a => a.endsWith('.mcrs'));
399
+ if (files.length === 0) {
400
+ console.error('Usage: redscript fmt <file.mcrs> [file2.mcrs ...]');
401
+ process.exit(1);
402
+ }
403
+ const { format } = require('./formatter');
404
+ for (const file of files) {
405
+ const content = fs.readFileSync(file, 'utf8');
406
+ const formatted = format(content);
407
+ fs.writeFileSync(file, formatted);
408
+ console.log(`Formatted: ${file}`);
409
+ }
410
+ break;
411
+ }
394
412
  case 'repl':
395
413
  await (0, repl_1.startRepl)(parsed.namespace ?? 'repl');
396
414
  break;
@@ -11,7 +11,7 @@
11
11
  * Variable mapping:
12
12
  * scoreboard objective: "rs"
13
13
  * fake player: "$<varname>"
14
- * temporaries: "$t0", "$t1", ...
14
+ * temporaries: "$_0", "$_1", ...
15
15
  * return value: "$ret"
16
16
  * parameters: "$p0", "$p1", ...
17
17
  */
@@ -12,7 +12,7 @@
12
12
  * Variable mapping:
13
13
  * scoreboard objective: "rs"
14
14
  * fake player: "$<varname>"
15
- * temporaries: "$t0", "$t1", ...
15
+ * temporaries: "$_0", "$_1", ...
16
16
  * return value: "$ret"
17
17
  * parameters: "$p0", "$p1", ...
18
18
  */
@@ -258,7 +258,7 @@ function generateDatapackWithStats(module, options = {}) {
258
258
  `scoreboard objectives add ${OBJ} dummy`,
259
259
  ];
260
260
  for (const g of module.globals) {
261
- loadLines.push(`scoreboard players set ${varRef(g)} ${OBJ} 0`);
261
+ loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`);
262
262
  }
263
263
  // Add trigger objectives
264
264
  for (const triggerName of triggerNames) {
@@ -310,6 +310,12 @@ function generateDatapackWithStats(module, options = {}) {
310
310
  files.push({ path: filePath, content: lines.join('\n') });
311
311
  }
312
312
  }
313
+ // Call @load functions from __load
314
+ for (const fn of module.functions) {
315
+ if (fn.isLoadInit) {
316
+ loadLines.push(`function ${ns}:${fn.name}`);
317
+ }
318
+ }
313
319
  // Write __load.mcfunction
314
320
  files.push({
315
321
  path: `data/${ns}/function/__load.mcfunction`,
@@ -64,13 +64,19 @@ function collectCommandEntriesFromModule(module) {
64
64
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName));
65
65
  const loadCommands = [
66
66
  `scoreboard objectives add ${OBJ} dummy`,
67
- ...module.globals.map(globalName => `scoreboard players set ${varRef(globalName)} ${OBJ} 0`),
67
+ ...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
68
68
  ...Array.from(triggerNames).flatMap(triggerName => [
69
69
  `scoreboard objectives add ${triggerName} trigger`,
70
70
  `scoreboard players enable @a ${triggerName}`,
71
71
  ]),
72
72
  ...Array.from(new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))).map(constSetup),
73
73
  ];
74
+ // Call @load functions from __load
75
+ for (const fn of module.functions) {
76
+ if (fn.isLoadInit) {
77
+ loadCommands.push(`function ${module.namespace}:${fn.name}`);
78
+ }
79
+ }
74
80
  const sections = [];
75
81
  if (loadCommands.length > 0) {
76
82
  sections.push({
@@ -0,0 +1 @@
1
+ export declare function format(source: string): string;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.format = format;
4
+ function format(source) {
5
+ const lines = source.split("\n");
6
+ let indent = 0;
7
+ const result = [];
8
+ for (let line of lines) {
9
+ line = line.trim();
10
+ if (!line) {
11
+ result.push("");
12
+ continue;
13
+ }
14
+ // Decrease indent before }
15
+ if (line.startsWith("}"))
16
+ indent = Math.max(0, indent - 1);
17
+ // Add indentation
18
+ result.push(" ".repeat(indent) + line);
19
+ // Increase indent after {
20
+ if (line.endsWith("{"))
21
+ indent++;
22
+ }
23
+ // Ensure single newline at end
24
+ return result.join("\n").trimEnd() + "\n";
25
+ }
26
+ //# sourceMappingURL=index.js.map
@@ -29,4 +29,5 @@ export declare class IRBuilder {
29
29
  emitTickYield(continuation: string): void;
30
30
  build(name: string, params: string[], isTickLoop?: boolean): IRFunction;
31
31
  }
32
- export declare function buildModule(namespace: string, fns: IRFunction[], globals?: string[]): IRModule;
32
+ import type { GlobalVar } from './types';
33
+ export declare function buildModule(namespace: string, fns: IRFunction[], globals?: GlobalVar[]): IRModule;
@@ -8,7 +8,7 @@
8
8
  * - Integer vars → scoreboard fake player ($name on objective "rs_vars")
9
9
  * - Complex data → NBT storage (redscript:stack / redscript:heap)
10
10
  * - Return value → fake player $ret
11
- * - Temporaries → $t0, $t1, ...
11
+ * - Temporaries → $_0, $_1, ...
12
12
  */
13
13
  export type Operand = {
14
14
  kind: 'var';
@@ -103,6 +103,7 @@ export interface IRFunction {
103
103
  blocks: IRBlock[];
104
104
  commands?: IRCommand[];
105
105
  isTickLoop?: boolean;
106
+ isLoadInit?: boolean;
106
107
  isTriggerHandler?: boolean;
107
108
  triggerName?: string;
108
109
  eventTrigger?: {
@@ -110,8 +111,12 @@ export interface IRFunction {
110
111
  value?: string;
111
112
  };
112
113
  }
114
+ export interface GlobalVar {
115
+ name: string;
116
+ init: number;
117
+ }
113
118
  export interface IRModule {
114
119
  namespace: string;
115
120
  functions: IRFunction[];
116
- globals: string[];
121
+ globals: GlobalVar[];
117
122
  }
package/dist/ir/types.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * - Integer vars → scoreboard fake player ($name on objective "rs_vars")
10
10
  * - Complex data → NBT storage (redscript:stack / redscript:heap)
11
11
  * - Return value → fake player $ret
12
- * - Temporaries → $t0, $t1, ...
12
+ * - Temporaries → $_0, $_1, ...
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  //# sourceMappingURL=types.js.map
@@ -16,6 +16,7 @@ export declare class Lowering {
16
16
  private namespace;
17
17
  private functions;
18
18
  private globals;
19
+ private globalNames;
19
20
  private fnDecls;
20
21
  private specializedFunctions;
21
22
  private currentFn;
@@ -80,6 +81,7 @@ export declare class Lowering {
80
81
  private buildRichTextJson;
81
82
  private appendRichTextExpr;
82
83
  private exprToString;
84
+ private exprToSnbt;
83
85
  private exprToTargetString;
84
86
  private exprToLiteral;
85
87
  private exprToQuotedString;
@@ -20,9 +20,10 @@ const BUILTINS = {
20
20
  subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
21
21
  title_times: ([sel, fadeIn, stay, fadeOut]) => `title ${sel} times ${fadeIn} ${stay} ${fadeOut}`,
22
22
  announce: ([msg]) => `tellraw @a {"text":"${msg}"}`,
23
- give: ([sel, item, count]) => `give ${sel} ${item} ${count ?? '1'}`,
23
+ give: ([sel, item, count, nbt]) => nbt ? `give ${sel} ${item}${nbt} ${count ?? '1'}` : `give ${sel} ${item} ${count ?? '1'}`,
24
24
  kill: ([sel]) => `kill ${sel ?? '@s'}`,
25
25
  effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
26
+ effect_clear: ([sel, eff]) => eff ? `effect clear ${sel} ${eff}` : `effect clear ${sel}`,
26
27
  summon: ([type, x, y, z, nbt]) => {
27
28
  const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ');
28
29
  return nbt ? `summon ${type} ${pos} ${nbt}` : `summon ${type} ${pos}`;
@@ -73,6 +74,12 @@ const BUILTINS = {
73
74
  team_leave: () => null, // Special handling
74
75
  team_option: () => null, // Special handling
75
76
  data_get: () => null, // Special handling (returns value from NBT)
77
+ data_merge: () => null, // Special handling (merge NBT)
78
+ set_new: () => null, // Special handling (returns set ID)
79
+ set_add: () => null, // Special handling
80
+ set_contains: () => null, // Special handling (returns 1/0)
81
+ set_remove: () => null, // Special handling
82
+ set_clear: () => null, // Special handling
76
83
  };
77
84
  function getSpan(node) {
78
85
  return node?.span;
@@ -118,6 +125,7 @@ class Lowering {
118
125
  constructor(namespace) {
119
126
  this.functions = [];
120
127
  this.globals = [];
128
+ this.globalNames = new Map();
121
129
  this.fnDecls = new Map();
122
130
  this.specializedFunctions = new Map();
123
131
  this.currentFn = '';
@@ -142,6 +150,7 @@ class Lowering {
142
150
  // World object counter for unique tags
143
151
  this.worldObjCounter = 0;
144
152
  this.namespace = namespace;
153
+ LoweringBuilder.resetTempCounter();
145
154
  }
146
155
  lower(program) {
147
156
  this.namespace = program.namespace;
@@ -164,6 +173,13 @@ class Lowering {
164
173
  this.constValues.set(constDecl.name, constDecl.value);
165
174
  this.varTypes.set(constDecl.name, this.normalizeType(constDecl.type));
166
175
  }
176
+ // Process global variable declarations (top-level let)
177
+ for (const g of program.globals ?? []) {
178
+ this.globalNames.set(g.name, { mutable: g.mutable });
179
+ this.varTypes.set(g.name, this.normalizeType(g.type));
180
+ const initValue = g.init.kind === 'int_lit' ? g.init.value : 0;
181
+ this.globals.push({ name: `$${g.name}`, init: initValue });
182
+ }
167
183
  for (const fn of program.declarations) {
168
184
  this.fnDecls.set(fn.name, fn);
169
185
  this.functionDefaults.set(fn.name, fn.params.map(param => param.default));
@@ -251,6 +267,10 @@ class Lowering {
251
267
  break;
252
268
  }
253
269
  }
270
+ // Check for @load decorator
271
+ if (fn.decorators.some(d => d.name === 'load')) {
272
+ irFn.isLoadInit = true;
273
+ }
254
274
  // Handle tick rate counter if needed
255
275
  if (tickRate && tickRate > 1) {
256
276
  this.wrapWithTickRate(irFn, tickRate);
@@ -264,7 +284,7 @@ class Lowering {
264
284
  wrapWithTickRate(fn, rate) {
265
285
  // Add tick counter logic to entry block
266
286
  const counterVar = `$__tick_${fn.name}`;
267
- this.globals.push(counterVar);
287
+ this.globals.push({ name: counterVar, init: 0 });
268
288
  // Prepend counter logic to entry block
269
289
  const entry = fn.blocks[0];
270
290
  const originalInstrs = [...entry.instrs];
@@ -357,6 +377,10 @@ class Lowering {
357
377
  }
358
378
  }
359
379
  lowerLetStmt(stmt) {
380
+ // Check for duplicate declaration of foreach binding
381
+ if (this.currentContext.binding === stmt.name) {
382
+ throw new diagnostics_1.DiagnosticError('LoweringError', `Cannot redeclare foreach binding '${stmt.name}'`, stmt.span ?? { line: 0, col: 0 });
383
+ }
360
384
  const varName = `$${stmt.name}`;
361
385
  this.varMap.set(stmt.name, varName);
362
386
  // Track variable type
@@ -406,6 +430,13 @@ class Lowering {
406
430
  }
407
431
  return;
408
432
  }
433
+ // Handle set_new returning a set ID string
434
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'set_new') {
435
+ const setId = `__set_${this.foreachCounter++}`;
436
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`);
437
+ this.stringValues.set(stmt.name, setId);
438
+ return;
439
+ }
409
440
  // Handle spawn_object returning entity handle
410
441
  if (stmt.init.kind === 'call' && stmt.init.fn === 'spawn_object') {
411
442
  const value = this.lowerExpr(stmt.init);
@@ -760,10 +791,24 @@ class Lowering {
760
791
  parts.push(`at ${this.selectorToString(sub.selector)}`);
761
792
  break;
762
793
  case 'if_entity':
763
- parts.push(`if entity ${this.selectorToString(sub.selector)}`);
794
+ if (sub.selector) {
795
+ parts.push(`if entity ${this.selectorToString(sub.selector)}`);
796
+ }
797
+ else if (sub.varName) {
798
+ // Variable with filters - substitute with @s and apply filters
799
+ const sel = { kind: '@s', filters: sub.filters };
800
+ parts.push(`if entity ${this.selectorToString(sel)}`);
801
+ }
764
802
  break;
765
803
  case 'unless_entity':
766
- parts.push(`unless entity ${this.selectorToString(sub.selector)}`);
804
+ if (sub.selector) {
805
+ parts.push(`unless entity ${this.selectorToString(sub.selector)}`);
806
+ }
807
+ else if (sub.varName) {
808
+ // Variable with filters - substitute with @s and apply filters
809
+ const sel = { kind: '@s', filters: sub.filters };
810
+ parts.push(`unless entity ${this.selectorToString(sel)}`);
811
+ }
767
812
  break;
768
813
  case 'in':
769
814
  parts.push(`in ${sub.dimension}`);
@@ -1051,6 +1096,14 @@ class Lowering {
1051
1096
  return { kind: 'var', name: dst };
1052
1097
  }
1053
1098
  lowerAssignExpr(expr) {
1099
+ // Check for const reassignment (both compile-time consts and immutable globals)
1100
+ if (this.constValues.has(expr.target)) {
1101
+ throw new diagnostics_1.DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 });
1102
+ }
1103
+ const globalInfo = this.globalNames.get(expr.target);
1104
+ if (globalInfo && !globalInfo.mutable) {
1105
+ throw new diagnostics_1.DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 });
1106
+ }
1054
1107
  const blockPosValue = this.resolveBlockPosExpr(expr.value);
1055
1108
  if (blockPosValue) {
1056
1109
  this.blockPosVars.set(expr.target, blockPosValue);
@@ -1473,6 +1526,62 @@ class Lowering {
1473
1526
  this.builder.emitRaw(`execute store result score ${dst} rs run data get ${targetType} ${target} ${path} ${scale}`);
1474
1527
  return { kind: 'var', name: dst };
1475
1528
  }
1529
+ // data_merge(target, nbt) — merge NBT into entity/block/storage
1530
+ // data_merge(@s, { Invisible: 1b, Silent: 1b })
1531
+ if (name === 'data_merge') {
1532
+ const target = args[0];
1533
+ const nbt = args[1];
1534
+ const nbtStr = this.exprToSnbt ? this.exprToSnbt(nbt) : this.exprToString(nbt);
1535
+ // Check if target is a selector (entity) or string (block/storage)
1536
+ if (target.kind === 'selector') {
1537
+ const sel = this.exprToTargetString(target);
1538
+ this.builder.emitRaw(`data merge entity ${sel} ${nbtStr}`);
1539
+ }
1540
+ else {
1541
+ // Assume block position or storage
1542
+ const targetStr = this.exprToString(target);
1543
+ // If it looks like coordinates, use block; otherwise storage
1544
+ if (targetStr.match(/^~|^\d|^\^/)) {
1545
+ this.builder.emitRaw(`data merge block ${targetStr} ${nbtStr}`);
1546
+ }
1547
+ else {
1548
+ this.builder.emitRaw(`data merge storage ${targetStr} ${nbtStr}`);
1549
+ }
1550
+ }
1551
+ return { kind: 'const', value: 0 };
1552
+ }
1553
+ // Set data structure operations — unique collections via NBT storage
1554
+ // set_new is primarily handled in lowerLetStmt for proper string tracking.
1555
+ // This fallback handles standalone set_new() calls without assignment.
1556
+ if (name === 'set_new') {
1557
+ const setId = `__set_${this.foreachCounter++}`;
1558
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`);
1559
+ return { kind: 'const', value: 0 };
1560
+ }
1561
+ if (name === 'set_add') {
1562
+ const setId = this.exprToString(args[0]);
1563
+ const value = this.exprToString(args[1]);
1564
+ this.builder.emitRaw(`execute unless data storage rs:sets ${setId}[{value:${value}}] run data modify storage rs:sets ${setId} append value {value:${value}}`);
1565
+ return { kind: 'const', value: 0 };
1566
+ }
1567
+ if (name === 'set_contains') {
1568
+ const dst = this.builder.freshTemp();
1569
+ const setId = this.exprToString(args[0]);
1570
+ const value = this.exprToString(args[1]);
1571
+ this.builder.emitRaw(`execute store result score ${dst} rs if data storage rs:sets ${setId}[{value:${value}}]`);
1572
+ return { kind: 'var', name: dst };
1573
+ }
1574
+ if (name === 'set_remove') {
1575
+ const setId = this.exprToString(args[0]);
1576
+ const value = this.exprToString(args[1]);
1577
+ this.builder.emitRaw(`data remove storage rs:sets ${setId}[{value:${value}}]`);
1578
+ return { kind: 'const', value: 0 };
1579
+ }
1580
+ if (name === 'set_clear') {
1581
+ const setId = this.exprToString(args[0]);
1582
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`);
1583
+ return { kind: 'const', value: 0 };
1584
+ }
1476
1585
  const coordCommand = this.lowerCoordinateBuiltin(name, args);
1477
1586
  if (coordCommand) {
1478
1587
  this.builder.emitRaw(coordCommand);
@@ -1497,8 +1606,10 @@ class Lowering {
1497
1606
  }
1498
1607
  return { kind: 'const', value: 0 };
1499
1608
  }
1500
- // Convert args to strings for builtin
1501
- const strArgs = args.map(arg => this.exprToString(arg));
1609
+ // Convert args to strings for builtin (use SNBT for struct/array literals)
1610
+ const strArgs = args.map(arg => arg.kind === 'struct_lit' || arg.kind === 'array_lit'
1611
+ ? this.exprToSnbt(arg)
1612
+ : this.exprToString(arg));
1502
1613
  const cmd = BUILTINS[name](strArgs);
1503
1614
  if (cmd) {
1504
1615
  this.builder.emitRaw(cmd);
@@ -1647,12 +1758,53 @@ class Lowering {
1647
1758
  }
1648
1759
  case 'selector':
1649
1760
  return this.selectorToString(expr.sel);
1761
+ case 'unary':
1762
+ // Handle unary minus on literals directly
1763
+ if (expr.op === '-' && expr.operand.kind === 'int_lit') {
1764
+ return (-expr.operand.value).toString();
1765
+ }
1766
+ if (expr.op === '-' && expr.operand.kind === 'float_lit') {
1767
+ return Math.trunc(-expr.operand.value).toString();
1768
+ }
1769
+ // Fall through to default for complex cases
1770
+ const unaryOp = this.lowerExpr(expr);
1771
+ return this.operandToVar(unaryOp);
1650
1772
  default:
1651
1773
  // Complex expression - lower and return var name
1652
1774
  const op = this.lowerExpr(expr);
1653
1775
  return this.operandToVar(op);
1654
1776
  }
1655
1777
  }
1778
+ exprToSnbt(expr) {
1779
+ switch (expr.kind) {
1780
+ case 'struct_lit': {
1781
+ const entries = expr.fields.map(f => `${f.name}:${this.exprToSnbt(f.value)}`);
1782
+ return `{${entries.join(',')}}`;
1783
+ }
1784
+ case 'array_lit': {
1785
+ const items = expr.elements.map(e => this.exprToSnbt(e));
1786
+ return `[${items.join(',')}]`;
1787
+ }
1788
+ case 'str_lit':
1789
+ return `"${expr.value}"`;
1790
+ case 'int_lit':
1791
+ return String(expr.value);
1792
+ case 'float_lit':
1793
+ return String(expr.value);
1794
+ case 'byte_lit':
1795
+ return `${expr.value}b`;
1796
+ case 'short_lit':
1797
+ return `${expr.value}s`;
1798
+ case 'long_lit':
1799
+ return `${expr.value}L`;
1800
+ case 'double_lit':
1801
+ return `${expr.value}d`;
1802
+ case 'bool_lit':
1803
+ return expr.value ? '1b' : '0b';
1804
+ default:
1805
+ return this.exprToString(expr);
1806
+ }
1807
+ }
1656
1808
  exprToTargetString(expr) {
1657
1809
  if (expr.kind === 'selector') {
1658
1810
  return this.selectorToString(expr.sel);
@@ -1712,6 +1864,13 @@ class Lowering {
1712
1864
  }
1713
1865
  return null;
1714
1866
  }
1867
+ if (name === 'summon') {
1868
+ if (args.length >= 2 && pos1) {
1869
+ const nbt = args[2] ? ` ${this.exprToString(args[2])}` : '';
1870
+ return `summon ${this.exprToString(args[0])} ${emitBlockPos(pos1)}${nbt}`;
1871
+ }
1872
+ return null;
1873
+ }
1715
1874
  return null;
1716
1875
  }
1717
1876
  lowerTpCommand(args) {
@@ -1941,6 +2100,18 @@ class Lowering {
1941
2100
  parts.push(`nbt=${filters.nbt}`);
1942
2101
  if (filters.gamemode)
1943
2102
  parts.push(`gamemode=${filters.gamemode}`);
2103
+ // Position filters
2104
+ if (filters.x)
2105
+ parts.push(`x=${this.rangeToString(filters.x)}`);
2106
+ if (filters.y)
2107
+ parts.push(`y=${this.rangeToString(filters.y)}`);
2108
+ if (filters.z)
2109
+ parts.push(`z=${this.rangeToString(filters.z)}`);
2110
+ // Rotation filters
2111
+ if (filters.x_rotation)
2112
+ parts.push(`x_rotation=${this.rangeToString(filters.x_rotation)}`);
2113
+ if (filters.y_rotation)
2114
+ parts.push(`y_rotation=${this.rangeToString(filters.y_rotation)}`);
1944
2115
  return this.finalizeSelector(parts.length ? `${kind}[${parts.join(',')}]` : kind);
1945
2116
  }
1946
2117
  finalizeSelector(selector) {
@@ -1965,14 +2136,17 @@ exports.Lowering = Lowering;
1965
2136
  // ---------------------------------------------------------------------------
1966
2137
  class LoweringBuilder {
1967
2138
  constructor() {
1968
- this.tempCount = 0;
1969
2139
  this.labelCount = 0;
1970
2140
  this.blocks = [];
1971
2141
  this.currentBlock = null;
1972
2142
  this.locals = new Set();
1973
2143
  }
2144
+ /** Reset the global temp counter (call between compilations). */
2145
+ static resetTempCounter() {
2146
+ LoweringBuilder.globalTempId = 0;
2147
+ }
1974
2148
  freshTemp() {
1975
- const name = `$t${this.tempCount++}`;
2149
+ const name = `$_${LoweringBuilder.globalTempId++}`;
1976
2150
  this.locals.add(name);
1977
2151
  return name;
1978
2152
  }
@@ -2038,4 +2212,5 @@ class LoweringBuilder {
2038
2212
  };
2039
2213
  }
2040
2214
  }
2215
+ LoweringBuilder.globalTempId = 0;
2041
2216
  //# sourceMappingURL=index.js.map