redscript-mc 1.2.21 → 1.2.25

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 (126) hide show
  1. package/.github/workflows/publish-extension-on-ci.yml +100 -0
  2. package/dist/__tests__/entity-types.test.d.ts +1 -0
  3. package/dist/__tests__/entity-types.test.js +203 -0
  4. package/dist/__tests__/var-allocator.test.d.ts +1 -0
  5. package/dist/__tests__/var-allocator.test.js +69 -0
  6. package/dist/ast/types.d.ts +2 -1
  7. package/dist/cli.js +17 -6
  8. package/dist/codegen/mcfunction/index.d.ts +2 -0
  9. package/dist/codegen/mcfunction/index.js +52 -43
  10. package/dist/codegen/structure/index.d.ts +4 -1
  11. package/dist/codegen/structure/index.js +8 -12
  12. package/dist/codegen/var-allocator.d.ts +28 -0
  13. package/dist/codegen/var-allocator.js +78 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +8 -6
  16. package/dist/lowering/index.d.ts +2 -0
  17. package/dist/lowering/index.js +62 -3
  18. package/dist/parser/index.js +22 -1
  19. package/dist/typechecker/index.js +30 -0
  20. package/dist/types/entity-hierarchy.d.ts +29 -0
  21. package/dist/types/entity-hierarchy.js +107 -0
  22. package/editors/vscode/package-lock.json +6 -4
  23. package/editors/vscode/package.json +3 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/entity-types.test.ts +236 -0
  26. package/src/__tests__/var-allocator.test.ts +75 -0
  27. package/src/ast/types.ts +8 -4
  28. package/src/cli.ts +20 -6
  29. package/src/codegen/mcfunction/index.ts +60 -48
  30. package/src/codegen/structure/index.ts +9 -14
  31. package/src/codegen/var-allocator.ts +75 -0
  32. package/src/examples/capture_the_flag.mcrs +34 -34
  33. package/src/examples/hunger_games.mcrs +59 -59
  34. package/src/examples/new_features_demo.mcrs +32 -32
  35. package/src/examples/parkour_race.mcrs +58 -58
  36. package/src/index.ts +10 -6
  37. package/src/lowering/index.ts +73 -8
  38. package/src/parser/index.ts +20 -1
  39. package/src/typechecker/index.ts +30 -0
  40. package/src/types/entity-hierarchy.ts +120 -0
  41. package/dist/data/arena/function/__load.mcfunction +0 -6
  42. package/dist/data/arena/function/__tick.mcfunction +0 -2
  43. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +0 -3
  44. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +0 -1
  45. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +0 -3
  46. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +0 -7
  47. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +0 -1
  48. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +0 -4
  49. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +0 -6
  50. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +0 -1
  51. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +0 -4
  52. package/dist/data/arena/function/announce_leaders.mcfunction +0 -6
  53. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +0 -1
  54. package/dist/data/arena/function/arena_tick/then_0.mcfunction +0 -4
  55. package/dist/data/arena/function/arena_tick.mcfunction +0 -11
  56. package/dist/data/counter/function/__load.mcfunction +0 -5
  57. package/dist/data/counter/function/__tick.mcfunction +0 -2
  58. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +0 -1
  59. package/dist/data/counter/function/counter_tick/then_0.mcfunction +0 -3
  60. package/dist/data/counter/function/counter_tick.mcfunction +0 -11
  61. package/dist/data/minecraft/tags/function/load.json +0 -5
  62. package/dist/data/minecraft/tags/function/tick.json +0 -5
  63. package/dist/data/quiz/function/__load.mcfunction +0 -16
  64. package/dist/data/quiz/function/__tick.mcfunction +0 -6
  65. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +0 -4
  66. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +0 -4
  67. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +0 -4
  68. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +0 -4
  69. package/dist/data/quiz/function/answer_a.mcfunction +0 -4
  70. package/dist/data/quiz/function/answer_b.mcfunction +0 -4
  71. package/dist/data/quiz/function/answer_c.mcfunction +0 -4
  72. package/dist/data/quiz/function/ask_question/else_1.mcfunction +0 -5
  73. package/dist/data/quiz/function/ask_question/else_4.mcfunction +0 -5
  74. package/dist/data/quiz/function/ask_question/else_7.mcfunction +0 -4
  75. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +0 -1
  76. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +0 -2
  77. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +0 -2
  78. package/dist/data/quiz/function/ask_question/then_0.mcfunction +0 -4
  79. package/dist/data/quiz/function/ask_question/then_3.mcfunction +0 -4
  80. package/dist/data/quiz/function/ask_question/then_6.mcfunction +0 -4
  81. package/dist/data/quiz/function/ask_question.mcfunction +0 -7
  82. package/dist/data/quiz/function/finish_quiz.mcfunction +0 -6
  83. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +0 -5
  84. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +0 -3
  85. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +0 -3
  86. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +0 -3
  87. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +0 -5
  88. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +0 -2
  89. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +0 -2
  90. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +0 -2
  91. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +0 -8
  92. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +0 -2
  93. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +0 -2
  94. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +0 -5
  95. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +0 -5
  96. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +0 -6
  97. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +0 -6
  98. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +0 -5
  99. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +0 -6
  100. package/dist/data/quiz/function/handle_answer.mcfunction +0 -11
  101. package/dist/data/quiz/function/start_quiz.mcfunction +0 -5
  102. package/dist/data/shop/function/__load.mcfunction +0 -7
  103. package/dist/data/shop/function/__tick.mcfunction +0 -3
  104. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +0 -4
  105. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +0 -5
  106. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +0 -5
  107. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +0 -3
  108. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +0 -2
  109. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +0 -2
  110. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +0 -2
  111. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +0 -4
  112. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +0 -4
  113. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +0 -4
  114. package/dist/data/shop/function/complete_purchase.mcfunction +0 -7
  115. package/dist/data/shop/function/handle_shop_trigger.mcfunction +0 -3
  116. package/dist/data/turret/function/__load.mcfunction +0 -5
  117. package/dist/data/turret/function/__tick.mcfunction +0 -4
  118. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +0 -4
  119. package/dist/data/turret/function/deploy_turret.mcfunction +0 -8
  120. package/dist/data/turret/function/turret_tick/at_1.mcfunction +0 -2
  121. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +0 -2
  122. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +0 -2
  123. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +0 -3
  124. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +0 -1
  125. package/dist/data/turret/function/turret_tick.mcfunction +0 -5
  126. package/dist/pack.mcmeta +0 -6
@@ -22,24 +22,18 @@ exports.generateDatapackWithStats = generateDatapackWithStats;
22
22
  exports.generateDatapack = generateDatapack;
23
23
  const commands_1 = require("../../optimizer/commands");
24
24
  const types_1 = require("../../events/types");
25
+ const var_allocator_1 = require("../var-allocator");
25
26
  // ---------------------------------------------------------------------------
26
27
  // Utilities
27
28
  // ---------------------------------------------------------------------------
28
29
  const OBJ = 'rs'; // scoreboard objective name
29
- function varRef(name) {
30
- // Ensure fake player prefix
31
- return name.startsWith('$') ? name : `$${name}`;
32
- }
33
- function operandToScore(op) {
30
+ function operandToScore(op, alloc) {
34
31
  if (op.kind === 'var')
35
- return `${varRef(op.name)} ${OBJ}`;
32
+ return `${alloc.alloc(op.name)} ${OBJ}`;
36
33
  if (op.kind === 'const')
37
- return `$const_${op.value} ${OBJ}`;
34
+ return `${alloc.constant(op.value)} ${OBJ}`;
38
35
  throw new Error(`Cannot convert storage operand to score: ${op.path}`);
39
36
  }
40
- function constSetup(value) {
41
- return `scoreboard players set $const_${value} ${OBJ} ${value}`;
42
- }
43
37
  // Collect all constants used in a function for pre-setup
44
38
  function collectConsts(fn) {
45
39
  const consts = new Set();
@@ -73,17 +67,17 @@ const BOP_OP = {
73
67
  // ---------------------------------------------------------------------------
74
68
  // Instruction codegen
75
69
  // ---------------------------------------------------------------------------
76
- function emitInstr(instr, ns) {
70
+ function emitInstr(instr, ns, alloc) {
77
71
  const lines = [];
78
72
  switch (instr.op) {
79
73
  case 'assign': {
80
- const dst = varRef(instr.dst);
74
+ const dst = alloc.alloc(instr.dst);
81
75
  const src = instr.src;
82
76
  if (src.kind === 'const') {
83
77
  lines.push(`scoreboard players set ${dst} ${OBJ} ${src.value}`);
84
78
  }
85
79
  else if (src.kind === 'var') {
86
- lines.push(`scoreboard players operation ${dst} ${OBJ} = ${varRef(src.name)} ${OBJ}`);
80
+ lines.push(`scoreboard players operation ${dst} ${OBJ} = ${alloc.alloc(src.name)} ${OBJ}`);
87
81
  }
88
82
  else {
89
83
  lines.push(`execute store result score ${dst} ${OBJ} run data get storage ${src.path}`);
@@ -91,18 +85,18 @@ function emitInstr(instr, ns) {
91
85
  break;
92
86
  }
93
87
  case 'binop': {
94
- const dst = varRef(instr.dst);
88
+ const dst = alloc.alloc(instr.dst);
95
89
  const bop = BOP_OP[instr.bop] ?? '+=';
96
90
  // Copy lhs → dst, then apply op with rhs
97
- lines.push(...emitInstr({ op: 'assign', dst: instr.dst, src: instr.lhs }, ns));
98
- lines.push(`scoreboard players operation ${dst} ${OBJ} ${bop} ${operandToScore(instr.rhs)}`);
91
+ lines.push(...emitInstr({ op: 'assign', dst: instr.dst, src: instr.lhs }, ns, alloc));
92
+ lines.push(`scoreboard players operation ${dst} ${OBJ} ${bop} ${operandToScore(instr.rhs, alloc)}`);
99
93
  break;
100
94
  }
101
95
  case 'cmp': {
102
96
  // MC doesn't have a direct compare-to-register; use execute store
103
- const dst = varRef(instr.dst);
104
- const lhsScore = operandToScore(instr.lhs);
105
- const rhsScore = operandToScore(instr.rhs);
97
+ const dst = alloc.alloc(instr.dst);
98
+ const lhsScore = operandToScore(instr.lhs, alloc);
99
+ const rhsScore = operandToScore(instr.rhs, alloc);
106
100
  lines.push(`scoreboard players set ${dst} ${OBJ} 0`);
107
101
  switch (instr.cop) {
108
102
  case '==':
@@ -127,13 +121,15 @@ function emitInstr(instr, ns) {
127
121
  break;
128
122
  }
129
123
  case 'call': {
130
- // Push args as fake players $p0, $p1, ...
124
+ // Push args as param fake players
131
125
  for (let i = 0; i < instr.args.length; i++) {
132
- lines.push(...emitInstr({ op: 'assign', dst: `$p${i}`, src: instr.args[i] }, ns));
126
+ const paramName = alloc.internal(`p${i}`);
127
+ lines.push(...emitInstr({ op: 'assign', dst: paramName, src: instr.args[i] }, ns, alloc));
133
128
  }
134
129
  lines.push(`function ${ns}:${instr.fn}`);
135
130
  if (instr.dst) {
136
- lines.push(`scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $ret ${OBJ}`);
131
+ const retName = alloc.internal('ret');
132
+ lines.push(`scoreboard players operation ${alloc.alloc(instr.dst)} ${OBJ} = ${retName} ${OBJ}`);
137
133
  }
138
134
  break;
139
135
  }
@@ -146,32 +142,34 @@ function emitInstr(instr, ns) {
146
142
  // ---------------------------------------------------------------------------
147
143
  // Terminator codegen
148
144
  // ---------------------------------------------------------------------------
149
- function emitTerm(term, ns, fnName) {
145
+ function emitTerm(term, ns, fnName, alloc) {
150
146
  const lines = [];
151
147
  switch (term.op) {
152
148
  case 'jump':
153
149
  lines.push(`function ${ns}:${fnName}/${term.target}`);
154
150
  break;
155
151
  case 'jump_if':
156
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.then}`);
157
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.else_}`);
152
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.then}`);
153
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.else_}`);
158
154
  break;
159
155
  case 'jump_unless':
160
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.then}`);
161
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.else_}`);
156
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.then}`);
157
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.else_}`);
162
158
  break;
163
- case 'return':
159
+ case 'return': {
160
+ const retName = alloc.internal('ret');
164
161
  if (term.value) {
165
- lines.push(...emitInstr({ op: 'assign', dst: '$ret', src: term.value }, ns));
162
+ lines.push(...emitInstr({ op: 'assign', dst: retName, src: term.value }, ns, alloc));
166
163
  }
167
164
  // In MC 1.20+, use `return` command
168
165
  if (term.value?.kind === 'const') {
169
166
  lines.push(`return ${term.value.value}`);
170
167
  }
171
168
  else if (term.value?.kind === 'var') {
172
- lines.push(`return run scoreboard players get ${varRef(term.value.name)} ${OBJ}`);
169
+ lines.push(`return run scoreboard players get ${alloc.alloc(term.value.name)} ${OBJ}`);
173
170
  }
174
171
  break;
172
+ }
175
173
  case 'tick_yield':
176
174
  lines.push(`schedule function ${ns}:${fnName}/${term.continuation} 1t replace`);
177
175
  break;
@@ -240,7 +238,8 @@ function countMcfunctionCommands(files) {
240
238
  }, 0);
241
239
  }
242
240
  function generateDatapackWithStats(module, options = {}) {
243
- const { optimizeCommands = true } = options;
241
+ const { optimizeCommands = true, mangle = false } = options;
242
+ const alloc = new var_allocator_1.VarAllocator(mangle);
244
243
  const files = [];
245
244
  const advancements = [];
246
245
  const ns = module.namespace;
@@ -269,7 +268,7 @@ function generateDatapackWithStats(module, options = {}) {
269
268
  `scoreboard objectives add ${OBJ} dummy`,
270
269
  ];
271
270
  for (const g of module.globals) {
272
- loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`);
271
+ loadLines.push(`scoreboard players set ${alloc.alloc(g.name)} ${OBJ} ${g.init}`);
273
272
  }
274
273
  // Add trigger objectives
275
274
  for (const triggerName of triggerNames) {
@@ -308,13 +307,17 @@ function generateDatapackWithStats(module, options = {}) {
308
307
  content: dispatchLines.join('\n'),
309
308
  });
310
309
  }
311
- // Generate each function (and collect constants for load)
310
+ // Collect all constants across all functions first (deduplicated)
311
+ const allConsts = new Set();
312
+ for (const fn of module.functions) {
313
+ for (const c of collectConsts(fn))
314
+ allConsts.add(c);
315
+ }
316
+ if (allConsts.size > 0) {
317
+ loadLines.push(...Array.from(allConsts).sort((a, b) => a - b).map(value => `scoreboard players set ${alloc.constant(value)} ${OBJ} ${value}`));
318
+ }
319
+ // Generate each function
312
320
  for (const fn of module.functions) {
313
- // Constant setup — place constants in __load.mcfunction
314
- const consts = collectConsts(fn);
315
- if (consts.size > 0) {
316
- loadLines.push(...Array.from(consts).map(constSetup));
317
- }
318
321
  // Entry block → <fn_name>.mcfunction
319
322
  // Continuation blocks → <fn_name>/<label>.mcfunction
320
323
  for (let i = 0; i < fn.blocks.length; i++) {
@@ -323,16 +326,21 @@ function generateDatapackWithStats(module, options = {}) {
323
326
  // Param setup in entry block
324
327
  if (i === 0) {
325
328
  for (let j = 0; j < fn.params.length; j++) {
326
- lines.push(`scoreboard players operation ${varRef(fn.params[j])} ${OBJ} = $p${j} ${OBJ}`);
329
+ lines.push(`scoreboard players operation ${alloc.alloc(fn.params[j])} ${OBJ} = ${alloc.internal(`p${j}`)} ${OBJ}`);
327
330
  }
328
331
  }
329
332
  for (const instr of block.instrs) {
330
- lines.push(...emitInstr(instr, ns));
333
+ lines.push(...emitInstr(instr, ns, alloc));
331
334
  }
332
- lines.push(...emitTerm(block.term, ns, fn.name));
335
+ lines.push(...emitTerm(block.term, ns, fn.name, alloc));
333
336
  const filePath = i === 0
334
337
  ? `data/${ns}/function/${fn.name}.mcfunction`
335
338
  : `data/${ns}/function/${fn.name}/${block.label}.mcfunction`;
339
+ // Skip empty continuation blocks (only contain the block comment, no real commands)
340
+ // Entry block (i === 0) is always emitted so the function file exists
341
+ const hasRealContent = lines.some(l => !l.startsWith('#') && l.trim() !== '');
342
+ if (i !== 0 && !hasRealContent)
343
+ continue;
336
344
  files.push({ path: filePath, content: lines.join('\n') });
337
345
  }
338
346
  }
@@ -442,12 +450,13 @@ function generateDatapackWithStats(module, options = {}) {
442
450
  });
443
451
  }
444
452
  const stats = (0, commands_1.createEmptyOptimizationStats)();
453
+ const sourceMap = mangle ? alloc.toSourceMap() : undefined;
445
454
  if (!optimizeCommands) {
446
- return { files, advancements, stats };
455
+ return { files, advancements, stats, sourceMap };
447
456
  }
448
457
  const optimized = applyFunctionOptimization(files);
449
458
  (0, commands_1.mergeOptimizationStats)(stats, optimized.stats);
450
- return { files: optimized.files, advancements, stats };
459
+ return { files: optimized.files, advancements, stats, sourceMap };
451
460
  }
452
461
  function generateDatapack(module) {
453
462
  const generated = generateDatapackWithStats(module);
@@ -16,6 +16,9 @@ export interface StructureCompileResult {
16
16
  }
17
17
  export interface StructureCompileOptions {
18
18
  dce?: boolean;
19
+ mangle?: boolean;
19
20
  }
20
- export declare function generateStructure(input: IRModule | DatapackFile[]): StructureCompileResult;
21
+ export declare function generateStructure(input: IRModule | DatapackFile[], options?: {
22
+ mangle?: boolean;
23
+ }): StructureCompileResult;
21
24
  export declare function compileToStructure(source: string, namespace: string, filePath?: string, options?: StructureCompileOptions): StructureCompileResult;
@@ -12,6 +12,7 @@ const structure_1 = require("../../optimizer/structure");
12
12
  const dce_1 = require("../../optimizer/dce");
13
13
  const compile_1 = require("../../compile");
14
14
  const types_1 = require("../../events/types");
15
+ const var_allocator_1 = require("../var-allocator");
15
16
  const DATA_VERSION = 3953;
16
17
  const MAX_WIDTH = 16;
17
18
  const OBJ = 'rs';
@@ -29,9 +30,6 @@ const palette = [
29
30
  function escapeJsonString(value) {
30
31
  return JSON.stringify(value).slice(1, -1);
31
32
  }
32
- function varRef(name) {
33
- return name.startsWith('$') ? name : `$${name}`;
34
- }
35
33
  function collectConsts(fn) {
36
34
  const consts = new Set();
37
35
  for (const block of fn.blocks) {
@@ -57,10 +55,8 @@ function collectConsts(fn) {
57
55
  }
58
56
  return consts;
59
57
  }
60
- function constSetup(value) {
61
- return `scoreboard players set $const_${value} ${OBJ} ${value}`;
62
- }
63
- function collectCommandEntriesFromModule(module) {
58
+ function collectCommandEntriesFromModule(module, mangle = false) {
59
+ const alloc = new var_allocator_1.VarAllocator(mangle);
64
60
  const entries = [];
65
61
  const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName);
66
62
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName));
@@ -68,12 +64,12 @@ function collectCommandEntriesFromModule(module) {
68
64
  const eventTypes = new Set(eventHandlers.map(fn => fn.eventHandler.eventType));
69
65
  const loadCommands = [
70
66
  `scoreboard objectives add ${OBJ} dummy`,
71
- ...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
67
+ ...module.globals.map(g => `scoreboard players set ${alloc.alloc(g.name)} ${OBJ} ${g.init}`),
72
68
  ...Array.from(triggerNames).flatMap(triggerName => [
73
69
  `scoreboard objectives add ${triggerName} trigger`,
74
70
  `scoreboard players enable @a ${triggerName}`,
75
71
  ]),
76
- ...Array.from(new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))).map(constSetup),
72
+ ...Array.from(new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))).map(value => `scoreboard players set ${alloc.constant(value)} ${OBJ} ${value}`),
77
73
  ];
78
74
  for (const eventType of eventTypes) {
79
75
  if (eventType === 'PlayerDeath') {
@@ -229,10 +225,10 @@ function createBlockTag(entry, index) {
229
225
  nbt: createBlockEntityTag(entry),
230
226
  });
231
227
  }
232
- function generateStructure(input) {
228
+ function generateStructure(input, options) {
233
229
  const entries = Array.isArray(input)
234
230
  ? collectCommandEntriesFromFiles(input)
235
- : collectCommandEntriesFromModule(input);
231
+ : collectCommandEntriesFromModule(input, options?.mangle);
236
232
  const blockTags = entries.map(createBlockTag);
237
233
  const sizeX = Math.max(1, Math.min(MAX_WIDTH, entries.length || 1));
238
234
  const sizeZ = Math.max(1, Math.min(MAX_WIDTH, Math.ceil(entries.length / MAX_WIDTH) || 1));
@@ -276,7 +272,7 @@ function compileToStructure(source, namespace, filePath, options = {}) {
276
272
  functions: structureOptimized.functions,
277
273
  };
278
274
  return {
279
- ...generateStructure(optimizedModule),
275
+ ...generateStructure(optimizedModule, { mangle: options.mangle }),
280
276
  stats,
281
277
  };
282
278
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * VarAllocator — assigns scoreboard fake-player names to variables.
3
+ *
4
+ * mangle=true: sequential short names ($a, $b, ..., $z, $aa, $ab, ...)
5
+ * mangle=false: legacy names ($<name> for vars, $const_<v> for consts, $p0/$ret for internals)
6
+ */
7
+ export declare class VarAllocator {
8
+ private readonly mangle;
9
+ private seq;
10
+ private readonly varCache;
11
+ private readonly constCache;
12
+ private readonly internalCache;
13
+ constructor(mangle?: boolean);
14
+ /** Allocate a name for a user variable. Strips leading '$' if present. */
15
+ alloc(originalName: string): string;
16
+ /** Allocate a name for a constant value (content-addressed). */
17
+ constant(value: number): string;
18
+ /** Allocate a name for a compiler internal (e.g. "ret", "p0"). */
19
+ internal(suffix: string): string;
20
+ /** Generate the next sequential name: a, b, ..., z, aa, ab, ..., az, ba, ... */
21
+ private nextSeqName;
22
+ /**
23
+ * Returns a sourcemap object mapping allocated name → original name.
24
+ * Useful for debugging: write to <output>.map.json alongside the datapack.
25
+ * Only meaningful when mangle=true.
26
+ */
27
+ toSourceMap(): Record<string, string>;
28
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * VarAllocator — assigns scoreboard fake-player names to variables.
4
+ *
5
+ * mangle=true: sequential short names ($a, $b, ..., $z, $aa, $ab, ...)
6
+ * mangle=false: legacy names ($<name> for vars, $const_<v> for consts, $p0/$ret for internals)
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.VarAllocator = void 0;
10
+ class VarAllocator {
11
+ constructor(mangle = true) {
12
+ this.seq = 0;
13
+ this.varCache = new Map();
14
+ this.constCache = new Map();
15
+ this.internalCache = new Map();
16
+ this.mangle = mangle;
17
+ }
18
+ /** Allocate a name for a user variable. Strips leading '$' if present. */
19
+ alloc(originalName) {
20
+ const clean = originalName.startsWith('$') ? originalName.slice(1) : originalName;
21
+ const cached = this.varCache.get(clean);
22
+ if (cached)
23
+ return cached;
24
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$${clean}`;
25
+ this.varCache.set(clean, name);
26
+ return name;
27
+ }
28
+ /** Allocate a name for a constant value (content-addressed). */
29
+ constant(value) {
30
+ const cached = this.constCache.get(value);
31
+ if (cached)
32
+ return cached;
33
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$const_${value}`;
34
+ this.constCache.set(value, name);
35
+ return name;
36
+ }
37
+ /** Allocate a name for a compiler internal (e.g. "ret", "p0"). */
38
+ internal(suffix) {
39
+ const cached = this.internalCache.get(suffix);
40
+ if (cached)
41
+ return cached;
42
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$${suffix}`;
43
+ this.internalCache.set(suffix, name);
44
+ return name;
45
+ }
46
+ /** Generate the next sequential name: a, b, ..., z, aa, ab, ..., az, ba, ... */
47
+ nextSeqName() {
48
+ const n = this.seq++;
49
+ let result = '';
50
+ let remaining = n;
51
+ do {
52
+ result = String.fromCharCode(97 + (remaining % 26)) + result;
53
+ remaining = Math.floor(remaining / 26) - 1;
54
+ } while (remaining >= 0);
55
+ return result;
56
+ }
57
+ /**
58
+ * Returns a sourcemap object mapping allocated name → original name.
59
+ * Useful for debugging: write to <output>.map.json alongside the datapack.
60
+ * Only meaningful when mangle=true.
61
+ */
62
+ toSourceMap() {
63
+ const map = {};
64
+ for (const [orig, alloc] of this.varCache) {
65
+ // Skip compiler-generated temporaries (start with _ followed by digits)
66
+ if (/^_\d+$/.test(orig))
67
+ continue;
68
+ map[alloc] = orig;
69
+ }
70
+ for (const [val, alloc] of this.constCache)
71
+ map[alloc] = `const:${val}`;
72
+ for (const [suf, alloc] of this.internalCache)
73
+ map[alloc] = `internal:${suf}`;
74
+ return map;
75
+ }
76
+ }
77
+ exports.VarAllocator = VarAllocator;
78
+ //# sourceMappingURL=var-allocator.js.map
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface CompileOptions {
16
16
  typeCheck?: boolean;
17
17
  filePath?: string;
18
18
  dce?: boolean;
19
+ mangle?: boolean;
19
20
  }
20
21
  export interface CompileResult {
21
22
  files: DatapackFile[];
@@ -25,6 +26,7 @@ export interface CompileResult {
25
26
  typeErrors?: DiagnosticError[];
26
27
  warnings?: Warning[];
27
28
  stats?: OptimizationStats;
29
+ sourceMap?: Record<string, string>;
28
30
  }
29
31
  /**
30
32
  * Compile RedScript source code to a Minecraft datapack.
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ function compile(source, options = {}) {
31
31
  const shouldOptimize = options.optimize ?? true;
32
32
  const shouldTypeCheck = options.typeCheck ?? true;
33
33
  const shouldRunDce = options.dce ?? shouldOptimize;
34
+ const mangle = options.mangle ?? false;
34
35
  const filePath = options.filePath;
35
36
  const preprocessed = (0, compile_1.preprocessSourceWithMetadata)(source, { filePath });
36
37
  const preprocessedSource = preprocessed.source;
@@ -50,7 +51,7 @@ function compile(source, options = {}) {
50
51
  const lowering = new lowering_1.Lowering(namespace, preprocessed.ranges);
51
52
  const ir = lowering.lower(ast);
52
53
  let optimizedIR = ir;
53
- let generated = (0, mcfunction_1.generateDatapackWithStats)(ir, { optimizeCommands: shouldOptimize });
54
+ let generated = (0, mcfunction_1.generateDatapackWithStats)(ir, { optimizeCommands: shouldOptimize, mangle });
54
55
  let optimizationStats;
55
56
  if (shouldOptimize) {
56
57
  const stats = (0, commands_1.createEmptyOptimizationStats)();
@@ -66,10 +67,10 @@ function compile(source, options = {}) {
66
67
  }
67
68
  const copyPropagatedIR = { ...ir, functions: copyPropagatedFunctions };
68
69
  optimizedIR = { ...ir, functions: deadCodeEliminatedFunctions };
69
- const baselineGenerated = (0, mcfunction_1.generateDatapackWithStats)(ir, { optimizeCommands: false });
70
- const beforeDceGenerated = (0, mcfunction_1.generateDatapackWithStats)(copyPropagatedIR, { optimizeCommands: false });
71
- const afterDceGenerated = (0, mcfunction_1.generateDatapackWithStats)(optimizedIR, { optimizeCommands: false });
72
- generated = (0, mcfunction_1.generateDatapackWithStats)(optimizedIR, { optimizeCommands: true });
70
+ const baselineGenerated = (0, mcfunction_1.generateDatapackWithStats)(ir, { optimizeCommands: false, mangle });
71
+ const beforeDceGenerated = (0, mcfunction_1.generateDatapackWithStats)(copyPropagatedIR, { optimizeCommands: false, mangle });
72
+ const afterDceGenerated = (0, mcfunction_1.generateDatapackWithStats)(optimizedIR, { optimizeCommands: false, mangle });
73
+ generated = (0, mcfunction_1.generateDatapackWithStats)(optimizedIR, { optimizeCommands: true, mangle });
73
74
  stats.deadCodeRemoved =
74
75
  (0, mcfunction_1.countMcfunctionCommands)(beforeDceGenerated.files) - (0, mcfunction_1.countMcfunctionCommands)(afterDceGenerated.files);
75
76
  stats.licmHoists = generated.stats.licmHoists;
@@ -85,7 +86,7 @@ function compile(source, options = {}) {
85
86
  }
86
87
  else {
87
88
  optimizedIR = ir;
88
- generated = (0, mcfunction_1.generateDatapackWithStats)(ir, { optimizeCommands: false });
89
+ generated = (0, mcfunction_1.generateDatapackWithStats)(ir, { optimizeCommands: false, mangle });
89
90
  }
90
91
  return {
91
92
  files: [...generated.files, ...generated.advancements],
@@ -95,6 +96,7 @@ function compile(source, options = {}) {
95
96
  typeErrors,
96
97
  warnings: [...dceResult.warnings, ...lowering.warnings],
97
98
  stats: optimizationStats,
99
+ sourceMap: generated.sourceMap,
98
100
  };
99
101
  }
100
102
  /**
@@ -29,6 +29,8 @@ export declare class Lowering {
29
29
  private timeoutCounter;
30
30
  private intervalCounter;
31
31
  readonly warnings: Warning[];
32
+ private entityContextStack;
33
+ private currentEntityContext;
32
34
  private builder;
33
35
  private varMap;
34
36
  private lambdaBindings;
@@ -44,6 +44,7 @@ const builder_1 = require("../ir/builder");
44
44
  const diagnostics_1 = require("../diagnostics");
45
45
  const path = __importStar(require("path"));
46
46
  const types_1 = require("../events/types");
47
+ const entity_hierarchy_1 = require("../types/entity-hierarchy");
47
48
  // ---------------------------------------------------------------------------
48
49
  // Macro-aware builtins (MC 1.20.2+)
49
50
  // These builtins generate commands where parameter variables cannot appear
@@ -140,11 +141,21 @@ const ENTITY_TO_MC_TYPE = {
140
141
  Creeper: 'minecraft:creeper',
141
142
  Spider: 'minecraft:spider',
142
143
  Enderman: 'minecraft:enderman',
144
+ Blaze: 'minecraft:blaze',
145
+ Witch: 'minecraft:witch',
146
+ Slime: 'minecraft:slime',
147
+ ZombieVillager: 'minecraft:zombie_villager',
148
+ Husk: 'minecraft:husk',
149
+ Drowned: 'minecraft:drowned',
150
+ Stray: 'minecraft:stray',
151
+ WitherSkeleton: 'minecraft:wither_skeleton',
152
+ CaveSpider: 'minecraft:cave_spider',
143
153
  Pig: 'minecraft:pig',
144
154
  Cow: 'minecraft:cow',
145
155
  Sheep: 'minecraft:sheep',
146
156
  Chicken: 'minecraft:chicken',
147
157
  Villager: 'minecraft:villager',
158
+ WanderingTrader: 'minecraft:wandering_trader',
148
159
  ArmorStand: 'minecraft:armor_stand',
149
160
  Item: 'minecraft:item',
150
161
  Arrow: 'minecraft:arrow',
@@ -185,6 +196,11 @@ function emitBlockPos(pos) {
185
196
  // Lowering Class
186
197
  // ---------------------------------------------------------------------------
187
198
  class Lowering {
199
+ currentEntityContext() {
200
+ return this.entityContextStack.length > 0
201
+ ? this.entityContextStack[this.entityContextStack.length - 1]
202
+ : 'Entity';
203
+ }
188
204
  constructor(namespace, sourceRanges = []) {
189
205
  this.functions = [];
190
206
  this.globals = [];
@@ -198,6 +214,8 @@ class Lowering {
198
214
  this.timeoutCounter = 0;
199
215
  this.intervalCounter = 0;
200
216
  this.warnings = [];
217
+ // Entity type context stack for W_IMPOSSIBLE_AS warnings
218
+ this.entityContextStack = [];
201
219
  this.varMap = new Map();
202
220
  this.lambdaBindings = new Map();
203
221
  this.intervalBindings = new Map();
@@ -938,11 +956,26 @@ class Lowering {
938
956
  throw new diagnostics_1.DiagnosticError('LoweringError', "'is' checks require an entity selector or entity binding", cond.span ?? stmt.span ?? { line: 0, col: 0 });
939
957
  }
940
958
  const mcType = ENTITY_TO_MC_TYPE[cond.entityType];
959
+ const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`;
941
960
  if (!mcType) {
942
- throw new diagnostics_1.DiagnosticError('LoweringError', `Cannot lower entity type check for '${cond.entityType}'`, cond.span ?? stmt.span ?? { line: 0, col: 0 });
961
+ // Abstract type check all concrete subtypes
962
+ const subtypes = (0, entity_hierarchy_1.getConcreteSubtypes)(cond.entityType);
963
+ if (subtypes.length === 0) {
964
+ throw new diagnostics_1.DiagnosticError('LoweringError', `Cannot lower entity type check for '${cond.entityType}'`, cond.span ?? stmt.span ?? { line: 0, col: 0 });
965
+ }
966
+ // Use a temp scoreboard variable to OR multiple type checks
967
+ this.builder.emitRaw(`scoreboard players set __is_result rs:temp 0`);
968
+ for (const subtype of subtypes) {
969
+ if (subtype.mcId) {
970
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, subtype.mcId)} run scoreboard players set __is_result rs:temp 1`);
971
+ }
972
+ }
973
+ this.builder.emitRaw(`execute if score __is_result rs:temp matches 1 run function ${this.namespace}:${thenFnName}`);
974
+ }
975
+ else {
976
+ // Concrete type — single check
977
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`);
943
978
  }
944
- const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`;
945
- this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`);
946
979
  const savedBuilder = this.builder;
947
980
  const savedVarMap = new Map(this.varMap);
948
981
  const savedBlockPosVars = new Map(this.blockPosVars);
@@ -1080,11 +1113,19 @@ class Lowering {
1080
1113
  this.blockPosVars = new Map(savedBlockPosVars);
1081
1114
  // In foreach body, the binding maps to @s
1082
1115
  this.varMap.set(stmt.binding, '@s');
1116
+ // Track entity context for type narrowing
1117
+ const selectorEntityType = (0, entity_hierarchy_1.getBaseSelectorType)(selector);
1118
+ if (selectorEntityType) {
1119
+ this.entityContextStack.push(selectorEntityType);
1120
+ }
1083
1121
  this.builder.startBlock('entry');
1084
1122
  this.lowerBlock(stmt.body);
1085
1123
  if (!this.builder.isBlockSealed()) {
1086
1124
  this.builder.emitReturn();
1087
1125
  }
1126
+ if (selectorEntityType) {
1127
+ this.entityContextStack.pop();
1128
+ }
1088
1129
  const subFn = this.builder.build(subFnName, [], false);
1089
1130
  this.functions.push(subFn);
1090
1131
  // Restore
@@ -1213,6 +1254,17 @@ class Lowering {
1213
1254
  lowerAsBlockStmt(stmt) {
1214
1255
  const selector = this.selectorToString(stmt.selector);
1215
1256
  const subFnName = `${this.currentFn}/as_${this.foreachCounter++}`;
1257
+ // Check for impossible type assertions (W_IMPOSSIBLE_AS)
1258
+ const innerType = (0, entity_hierarchy_1.getBaseSelectorType)(selector);
1259
+ const outerType = this.currentEntityContext();
1260
+ if (innerType && outerType !== 'Entity' && innerType !== 'Entity' && !(0, entity_hierarchy_1.areCompatibleTypes)(outerType, innerType)) {
1261
+ this.warnings.push({
1262
+ message: `Impossible type assertion: @s is ${outerType} but as-block targets ${innerType}`,
1263
+ code: 'W_IMPOSSIBLE_AS',
1264
+ line: stmt.span?.line,
1265
+ col: stmt.span?.col,
1266
+ });
1267
+ }
1216
1268
  this.builder.emitRaw(`execute as ${selector} run function ${this.namespace}:${subFnName}`);
1217
1269
  // Create sub-function
1218
1270
  const savedBuilder = this.builder;
@@ -1221,11 +1273,18 @@ class Lowering {
1221
1273
  this.builder = new LoweringBuilder();
1222
1274
  this.varMap = new Map(savedVarMap);
1223
1275
  this.blockPosVars = new Map(savedBlockPosVars);
1276
+ // Track entity context inside as-block
1277
+ if (innerType) {
1278
+ this.entityContextStack.push(innerType);
1279
+ }
1224
1280
  this.builder.startBlock('entry');
1225
1281
  this.lowerBlock(stmt.body);
1226
1282
  if (!this.builder.isBlockSealed()) {
1227
1283
  this.builder.emitReturn();
1228
1284
  }
1285
+ if (innerType) {
1286
+ this.entityContextStack.pop();
1287
+ }
1229
1288
  const subFn = this.builder.build(subFnName, [], false);
1230
1289
  this.functions.push(subFn);
1231
1290
  this.builder = savedBuilder;
@@ -32,11 +32,21 @@ const ENTITY_TYPE_NAMES = new Set([
32
32
  'Creeper',
33
33
  'Spider',
34
34
  'Enderman',
35
+ 'Blaze',
36
+ 'Witch',
37
+ 'Slime',
38
+ 'ZombieVillager',
39
+ 'Husk',
40
+ 'Drowned',
41
+ 'Stray',
42
+ 'WitherSkeleton',
43
+ 'CaveSpider',
35
44
  'Pig',
36
45
  'Cow',
37
46
  'Sheep',
38
47
  'Chicken',
39
48
  'Villager',
49
+ 'WanderingTrader',
40
50
  'ArmorStand',
41
51
  'Item',
42
52
  'Arrow',
@@ -384,7 +394,18 @@ class Parser {
384
394
  }
385
395
  else if (token.kind === 'ident') {
386
396
  this.advance();
387
- type = { kind: 'struct', name: token.value };
397
+ if (token.value === 'selector' && this.check('<')) {
398
+ this.advance(); // consume <
399
+ const entityType = this.expect('ident').value;
400
+ this.expect('>');
401
+ type = { kind: 'selector', entityType };
402
+ }
403
+ else if (token.value === 'selector') {
404
+ type = { kind: 'selector' };
405
+ }
406
+ else {
407
+ type = { kind: 'struct', name: token.value };
408
+ }
388
409
  }
389
410
  else {
390
411
  this.error(`Expected type, got '${token.kind}'`);