redscript-mc 1.2.20 → 1.2.24

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 (136) hide show
  1. package/.github/workflows/publish-extension-on-ci.yml +99 -0
  2. package/dist/__tests__/compile-all.test.js +5 -0
  3. package/dist/__tests__/entity-types.test.d.ts +1 -0
  4. package/dist/__tests__/entity-types.test.js +203 -0
  5. package/dist/__tests__/var-allocator.test.d.ts +1 -0
  6. package/dist/__tests__/var-allocator.test.js +69 -0
  7. package/dist/ast/types.d.ts +2 -1
  8. package/dist/cli.js +24 -7
  9. package/dist/codegen/mcfunction/index.d.ts +2 -0
  10. package/dist/codegen/mcfunction/index.js +47 -43
  11. package/dist/codegen/structure/index.d.ts +4 -1
  12. package/dist/codegen/structure/index.js +8 -12
  13. package/dist/codegen/var-allocator.d.ts +28 -0
  14. package/dist/codegen/var-allocator.js +74 -0
  15. package/dist/compile.d.ts +8 -0
  16. package/dist/compile.js +14 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +9 -7
  19. package/dist/lowering/index.d.ts +2 -0
  20. package/dist/lowering/index.js +62 -3
  21. package/dist/optimizer/dce.d.ts +2 -1
  22. package/dist/optimizer/dce.js +13 -2
  23. package/dist/parser/index.js +22 -1
  24. package/dist/typechecker/index.js +30 -0
  25. package/dist/types/entity-hierarchy.d.ts +29 -0
  26. package/dist/types/entity-hierarchy.js +107 -0
  27. package/editors/vscode/out/extension.js +29 -4
  28. package/editors/vscode/package-lock.json +6 -4
  29. package/editors/vscode/package.json +3 -3
  30. package/package.json +1 -1
  31. package/src/__tests__/compile-all.test.ts +6 -0
  32. package/src/__tests__/entity-types.test.ts +236 -0
  33. package/src/__tests__/var-allocator.test.ts +75 -0
  34. package/src/ast/types.ts +8 -4
  35. package/src/cli.ts +28 -8
  36. package/src/codegen/mcfunction/index.ts +55 -48
  37. package/src/codegen/structure/index.ts +9 -14
  38. package/src/codegen/var-allocator.ts +71 -0
  39. package/src/compile.ts +18 -0
  40. package/src/examples/capture_the_flag.mcrs +34 -34
  41. package/src/examples/hunger_games.mcrs +60 -60
  42. package/src/examples/new_features_demo.mcrs +32 -32
  43. package/src/examples/parkour_race.mcrs +58 -58
  44. package/src/examples/zombie_survival.mcrs +73 -73
  45. package/src/index.ts +11 -7
  46. package/src/lowering/index.ts +73 -8
  47. package/src/optimizer/dce.ts +18 -2
  48. package/src/parser/index.ts +20 -1
  49. package/src/typechecker/index.ts +30 -0
  50. package/src/types/entity-hierarchy.ts +120 -0
  51. package/dist/data/arena/function/__load.mcfunction +0 -6
  52. package/dist/data/arena/function/__tick.mcfunction +0 -2
  53. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +0 -3
  54. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +0 -1
  55. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +0 -3
  56. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +0 -7
  57. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +0 -1
  58. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +0 -4
  59. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +0 -6
  60. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +0 -1
  61. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +0 -4
  62. package/dist/data/arena/function/announce_leaders.mcfunction +0 -6
  63. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +0 -1
  64. package/dist/data/arena/function/arena_tick/then_0.mcfunction +0 -4
  65. package/dist/data/arena/function/arena_tick.mcfunction +0 -11
  66. package/dist/data/counter/function/__load.mcfunction +0 -5
  67. package/dist/data/counter/function/__tick.mcfunction +0 -2
  68. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +0 -1
  69. package/dist/data/counter/function/counter_tick/then_0.mcfunction +0 -3
  70. package/dist/data/counter/function/counter_tick.mcfunction +0 -11
  71. package/dist/data/minecraft/tags/function/load.json +0 -5
  72. package/dist/data/minecraft/tags/function/tick.json +0 -5
  73. package/dist/data/quiz/function/__load.mcfunction +0 -16
  74. package/dist/data/quiz/function/__tick.mcfunction +0 -6
  75. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +0 -4
  76. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +0 -4
  77. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +0 -4
  78. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +0 -4
  79. package/dist/data/quiz/function/answer_a.mcfunction +0 -4
  80. package/dist/data/quiz/function/answer_b.mcfunction +0 -4
  81. package/dist/data/quiz/function/answer_c.mcfunction +0 -4
  82. package/dist/data/quiz/function/ask_question/else_1.mcfunction +0 -5
  83. package/dist/data/quiz/function/ask_question/else_4.mcfunction +0 -5
  84. package/dist/data/quiz/function/ask_question/else_7.mcfunction +0 -4
  85. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +0 -1
  86. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +0 -2
  87. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +0 -2
  88. package/dist/data/quiz/function/ask_question/then_0.mcfunction +0 -4
  89. package/dist/data/quiz/function/ask_question/then_3.mcfunction +0 -4
  90. package/dist/data/quiz/function/ask_question/then_6.mcfunction +0 -4
  91. package/dist/data/quiz/function/ask_question.mcfunction +0 -7
  92. package/dist/data/quiz/function/finish_quiz.mcfunction +0 -6
  93. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +0 -5
  94. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +0 -3
  95. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +0 -3
  96. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +0 -3
  97. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +0 -5
  98. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +0 -2
  99. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +0 -2
  100. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +0 -2
  101. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +0 -8
  102. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +0 -2
  103. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +0 -2
  104. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +0 -5
  105. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +0 -5
  106. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +0 -6
  107. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +0 -6
  108. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +0 -5
  109. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +0 -6
  110. package/dist/data/quiz/function/handle_answer.mcfunction +0 -11
  111. package/dist/data/quiz/function/start_quiz.mcfunction +0 -5
  112. package/dist/data/shop/function/__load.mcfunction +0 -7
  113. package/dist/data/shop/function/__tick.mcfunction +0 -3
  114. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +0 -4
  115. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +0 -5
  116. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +0 -5
  117. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +0 -3
  118. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +0 -2
  119. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +0 -2
  120. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +0 -2
  121. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +0 -4
  122. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +0 -4
  123. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +0 -4
  124. package/dist/data/shop/function/complete_purchase.mcfunction +0 -7
  125. package/dist/data/shop/function/handle_shop_trigger.mcfunction +0 -3
  126. package/dist/data/turret/function/__load.mcfunction +0 -5
  127. package/dist/data/turret/function/__tick.mcfunction +0 -4
  128. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +0 -4
  129. package/dist/data/turret/function/deploy_turret.mcfunction +0 -8
  130. package/dist/data/turret/function/turret_tick/at_1.mcfunction +0 -2
  131. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +0 -2
  132. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +0 -2
  133. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +0 -3
  134. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +0 -1
  135. package/dist/data/turret/function/turret_tick.mcfunction +0 -5
  136. package/dist/pack.mcmeta +0 -6
@@ -0,0 +1,75 @@
1
+ import { VarAllocator } from '../codegen/var-allocator'
2
+
3
+ describe('VarAllocator', () => {
4
+ describe('mangle mode (default)', () => {
5
+ it('generates sequential names: a, b, ..., z, aa, ab', () => {
6
+ const alloc = new VarAllocator(true)
7
+ const names: string[] = []
8
+ for (let i = 0; i < 28; i++) {
9
+ names.push(alloc.alloc(`var${i}`))
10
+ }
11
+ expect(names[0]).toBe('$a')
12
+ expect(names[1]).toBe('$b')
13
+ expect(names[25]).toBe('$z')
14
+ expect(names[26]).toBe('$aa')
15
+ expect(names[27]).toBe('$ab')
16
+ })
17
+
18
+ it('caches: same name returns same result', () => {
19
+ const alloc = new VarAllocator(true)
20
+ const first = alloc.alloc('x')
21
+ const second = alloc.alloc('x')
22
+ expect(first).toBe(second)
23
+ })
24
+
25
+ it('constant() is content-addressed: same value returns same result', () => {
26
+ const alloc = new VarAllocator(true)
27
+ const first = alloc.constant(42)
28
+ const second = alloc.constant(42)
29
+ expect(first).toBe(second)
30
+ })
31
+
32
+ it('different variables get different names', () => {
33
+ const alloc = new VarAllocator(true)
34
+ const a = alloc.alloc('foo')
35
+ const b = alloc.alloc('bar')
36
+ expect(a).not.toBe(b)
37
+ })
38
+
39
+ it('alloc, constant, and internal share the same sequential pool', () => {
40
+ const alloc = new VarAllocator(true)
41
+ const v = alloc.alloc('x') // $a
42
+ const c = alloc.constant(1) // $b
43
+ const i = alloc.internal('ret') // $c
44
+ expect(v).toBe('$a')
45
+ expect(c).toBe('$b')
46
+ expect(i).toBe('$c')
47
+ })
48
+
49
+ it('strips $ prefix from variable names', () => {
50
+ const alloc = new VarAllocator(true)
51
+ const a = alloc.alloc('$foo')
52
+ const b = alloc.alloc('foo')
53
+ expect(a).toBe(b) // same underlying name
54
+ })
55
+ })
56
+
57
+ describe('no-mangle mode', () => {
58
+ it('uses $<name> for user vars', () => {
59
+ const alloc = new VarAllocator(false)
60
+ expect(alloc.alloc('counter')).toBe('$counter')
61
+ })
62
+
63
+ it('uses $const_<value> for constants', () => {
64
+ const alloc = new VarAllocator(false)
65
+ expect(alloc.constant(10)).toBe('$const_10')
66
+ expect(alloc.constant(-3)).toBe('$const_-3')
67
+ })
68
+
69
+ it('uses $<suffix> for internals', () => {
70
+ const alloc = new VarAllocator(false)
71
+ expect(alloc.internal('ret')).toBe('$ret')
72
+ expect(alloc.internal('p0')).toBe('$p0')
73
+ })
74
+ })
75
+ })
package/src/ast/types.ts CHANGED
@@ -25,15 +25,19 @@ export interface Span {
25
25
  export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double' | 'format_string'
26
26
 
27
27
  // Entity type hierarchy
28
- export type EntityTypeName =
28
+ export type EntityTypeName =
29
29
  | 'entity' // Base type
30
30
  | 'Player' // @a, @p, @r
31
31
  | 'Mob' // Base mob type
32
32
  | 'HostileMob' // Hostile mobs
33
33
  | 'PassiveMob' // Passive mobs
34
- // Specific mob types (common ones)
34
+ // Hostile mob types
35
35
  | 'Zombie' | 'Skeleton' | 'Creeper' | 'Spider' | 'Enderman'
36
- | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager'
36
+ | 'Blaze' | 'Witch' | 'Slime' | 'ZombieVillager' | 'Husk'
37
+ | 'Drowned' | 'Stray' | 'WitherSkeleton' | 'CaveSpider'
38
+ // Passive mob types
39
+ | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager' | 'WanderingTrader'
40
+ // Non-mob entities
37
41
  | 'ArmorStand' | 'Item' | 'Arrow'
38
42
 
39
43
  export type TypeNode =
@@ -43,7 +47,7 @@ export type TypeNode =
43
47
  | { kind: 'enum'; name: string }
44
48
  | { kind: 'function_type'; params: TypeNode[]; return: TypeNode }
45
49
  | { kind: 'entity'; entityType: EntityTypeName } // Entity types
46
- | { kind: 'selector' } // Selector type (multiple entities)
50
+ | { kind: 'selector'; entityType?: string } // Selector type, optionally parameterized: selector<Player>
47
51
 
48
52
  export interface LambdaParam {
49
53
  name: string
package/src/cli.ts CHANGED
@@ -53,6 +53,7 @@ Options:
53
53
  --namespace <ns> Datapack namespace (default: derived from filename)
54
54
  --target <target> Output target: datapack (default), cmdblock, or structure
55
55
  --no-dce Disable AST dead code elimination
56
+ --no-mangle Disable variable name mangling (use readable names)
56
57
  --stats Print optimizer statistics
57
58
  --hot-reload <url> After each successful compile, POST to <url>/reload
58
59
  (use with redscript-testharness; e.g. http://localhost:25561)
@@ -166,8 +167,9 @@ function parseArgs(args: string[]): {
166
167
  help?: boolean
167
168
  hotReload?: string
168
169
  dce?: boolean
170
+ mangle?: boolean
169
171
  } {
170
- const result: ReturnType<typeof parseArgs> = { dce: true }
172
+ const result: ReturnType<typeof parseArgs> = { dce: true, mangle: true }
171
173
  let i = 0
172
174
 
173
175
  while (i < args.length) {
@@ -194,6 +196,9 @@ function parseArgs(args: string[]): {
194
196
  } else if (arg === '--no-dce') {
195
197
  result.dce = false
196
198
  i++
199
+ } else if (arg === '--no-mangle') {
200
+ result.mangle = false
201
+ i++
197
202
  } else if (arg === '--hot-reload') {
198
203
  result.hotReload = args[++i]
199
204
  i++
@@ -217,13 +222,19 @@ function deriveNamespace(filePath: string): string {
217
222
  return basename.toLowerCase().replace(/[^a-z0-9]/g, '_')
218
223
  }
219
224
 
220
- function printWarnings(warnings: Array<{ code: string; message: string }> | undefined): void {
225
+ function printWarnings(warnings: Array<{ code: string; message: string; line?: number; col?: number; filePath?: string }> | undefined): void {
221
226
  if (!warnings || warnings.length === 0) {
222
227
  return
223
228
  }
224
229
 
225
230
  for (const warning of warnings) {
226
- console.error(`Warning [${warning.code}]: ${warning.message}`)
231
+ const loc = warning.filePath
232
+ ? `${warning.filePath}:${warning.line ?? '?'}`
233
+ : warning.line != null
234
+ ? `line ${warning.line}`
235
+ : null
236
+ const locStr = loc ? ` (${loc})` : ''
237
+ console.error(`Warning [${warning.code}]: ${warning.message}${locStr}`)
227
238
  }
228
239
  }
229
240
 
@@ -250,7 +261,8 @@ function compileCommand(
250
261
  namespace: string,
251
262
  target: string = 'datapack',
252
263
  showStats = false,
253
- dce = true
264
+ dce = true,
265
+ mangle = true
254
266
  ): void {
255
267
  // Read source file
256
268
  if (!fs.existsSync(file)) {
@@ -262,7 +274,7 @@ function compileCommand(
262
274
 
263
275
  try {
264
276
  if (target === 'cmdblock') {
265
- const result = compile(source, { namespace, filePath: file, dce })
277
+ const result = compile(source, { namespace, filePath: file, dce, mangle })
266
278
  printWarnings(result.warnings)
267
279
 
268
280
  // Generate command block JSON
@@ -282,7 +294,7 @@ function compileCommand(
282
294
  printOptimizationStats(result.stats)
283
295
  }
284
296
  } else if (target === 'structure') {
285
- const structure = compileToStructure(source, namespace, file, { dce })
297
+ const structure = compileToStructure(source, namespace, file, { dce, mangle })
286
298
  fs.mkdirSync(path.dirname(output), { recursive: true })
287
299
  fs.writeFileSync(output, structure.buffer)
288
300
 
@@ -293,7 +305,7 @@ function compileCommand(
293
305
  printOptimizationStats(structure.stats)
294
306
  }
295
307
  } else {
296
- const result = compile(source, { namespace, filePath: file, dce })
308
+ const result = compile(source, { namespace, filePath: file, dce, mangle })
297
309
  printWarnings(result.warnings)
298
310
 
299
311
  // Default: generate datapack
@@ -308,6 +320,13 @@ function compileCommand(
308
320
  fs.writeFileSync(filePath, dataFile.content)
309
321
  }
310
322
 
323
+ // Write sourcemap alongside datapack when mangle mode is active
324
+ if (mangle && result.sourceMap && Object.keys(result.sourceMap).length > 0) {
325
+ const mapPath = path.join(output, `${namespace}.map.json`)
326
+ fs.writeFileSync(mapPath, JSON.stringify(result.sourceMap, null, 2))
327
+ console.log(` Sourcemap: ${mapPath}`)
328
+ }
329
+
311
330
  console.log(`✓ Compiled ${file} to ${output}/`)
312
331
  console.log(` Namespace: ${namespace}`)
313
332
  console.log(` Functions: ${result.ir.functions.length}`)
@@ -488,7 +507,8 @@ async function main(): Promise<void> {
488
507
  namespace,
489
508
  target,
490
509
  parsed.stats,
491
- parsed.dce
510
+ parsed.dce,
511
+ parsed.mangle
492
512
  )
493
513
  }
494
514
  break
@@ -19,6 +19,7 @@
19
19
  import type { IRBlock, IRFunction, IRModule, Operand, Terminator } from '../../ir/types'
20
20
  import { optimizeCommandFunctions, type OptimizationStats, createEmptyOptimizationStats, mergeOptimizationStats } from '../../optimizer/commands'
21
21
  import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
22
+ import { VarAllocator } from '../var-allocator'
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Utilities
@@ -26,21 +27,12 @@ import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/t
26
27
 
27
28
  const OBJ = 'rs' // scoreboard objective name
28
29
 
29
- function varRef(name: string): string {
30
- // Ensure fake player prefix
31
- return name.startsWith('$') ? name : `$${name}`
32
- }
33
-
34
- function operandToScore(op: Operand): string {
35
- if (op.kind === 'var') return `${varRef(op.name)} ${OBJ}`
36
- if (op.kind === 'const') return `$const_${op.value} ${OBJ}`
30
+ function operandToScore(op: Operand, alloc: VarAllocator): string {
31
+ if (op.kind === 'var') return `${alloc.alloc(op.name)} ${OBJ}`
32
+ if (op.kind === 'const') return `${alloc.constant(op.value)} ${OBJ}`
37
33
  throw new Error(`Cannot convert storage operand to score: ${op.path}`)
38
34
  }
39
35
 
40
- function constSetup(value: number): string {
41
- return `scoreboard players set $const_${value} ${OBJ} ${value}`
42
- }
43
-
44
36
  // Collect all constants used in a function for pre-setup
45
37
  function collectConsts(fn: IRFunction): Set<number> {
46
38
  const consts = new Set<number>()
@@ -71,17 +63,17 @@ const BOP_OP: Record<string, string> = {
71
63
  // Instruction codegen
72
64
  // ---------------------------------------------------------------------------
73
65
 
74
- function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns: string): string[] {
66
+ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns: string, alloc: VarAllocator): string[] {
75
67
  const lines: string[] = []
76
68
 
77
69
  switch (instr.op) {
78
70
  case 'assign': {
79
- const dst = varRef(instr.dst)
71
+ const dst = alloc.alloc(instr.dst)
80
72
  const src = instr.src as Operand
81
73
  if (src.kind === 'const') {
82
74
  lines.push(`scoreboard players set ${dst} ${OBJ} ${src.value}`)
83
75
  } else if (src.kind === 'var') {
84
- lines.push(`scoreboard players operation ${dst} ${OBJ} = ${varRef(src.name)} ${OBJ}`)
76
+ lines.push(`scoreboard players operation ${dst} ${OBJ} = ${alloc.alloc(src.name)} ${OBJ}`)
85
77
  } else {
86
78
  lines.push(`execute store result score ${dst} ${OBJ} run data get storage ${src.path}`)
87
79
  }
@@ -89,19 +81,19 @@ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns:
89
81
  }
90
82
 
91
83
  case 'binop': {
92
- const dst = varRef(instr.dst)
84
+ const dst = alloc.alloc(instr.dst)
93
85
  const bop = BOP_OP[instr.bop as string] ?? '+='
94
86
  // Copy lhs → dst, then apply op with rhs
95
- lines.push(...emitInstr({ op: 'assign', dst: instr.dst, src: instr.lhs }, ns))
96
- lines.push(`scoreboard players operation ${dst} ${OBJ} ${bop} ${operandToScore(instr.rhs)}`)
87
+ lines.push(...emitInstr({ op: 'assign', dst: instr.dst, src: instr.lhs }, ns, alloc))
88
+ lines.push(`scoreboard players operation ${dst} ${OBJ} ${bop} ${operandToScore(instr.rhs, alloc)}`)
97
89
  break
98
90
  }
99
91
 
100
92
  case 'cmp': {
101
93
  // MC doesn't have a direct compare-to-register; use execute store
102
- const dst = varRef(instr.dst)
103
- const lhsScore = operandToScore(instr.lhs)
104
- const rhsScore = operandToScore(instr.rhs)
94
+ const dst = alloc.alloc(instr.dst)
95
+ const lhsScore = operandToScore(instr.lhs, alloc)
96
+ const rhsScore = operandToScore(instr.rhs, alloc)
105
97
  lines.push(`scoreboard players set ${dst} ${OBJ} 0`)
106
98
  switch (instr.cop) {
107
99
  case '==':
@@ -127,13 +119,15 @@ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns:
127
119
  }
128
120
 
129
121
  case 'call': {
130
- // Push args as fake players $p0, $p1, ...
122
+ // Push args as param fake players
131
123
  for (let i = 0; i < instr.args.length; i++) {
132
- lines.push(...emitInstr({ op: 'assign', dst: `$p${i}`, src: instr.args[i] }, ns))
124
+ const paramName = alloc.internal(`p${i}`)
125
+ lines.push(...emitInstr({ op: 'assign', dst: paramName, src: instr.args[i] }, ns, alloc))
133
126
  }
134
127
  lines.push(`function ${ns}:${instr.fn}`)
135
128
  if (instr.dst) {
136
- lines.push(`scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $ret ${OBJ}`)
129
+ const retName = alloc.internal('ret')
130
+ lines.push(`scoreboard players operation ${alloc.alloc(instr.dst)} ${OBJ} = ${retName} ${OBJ}`)
137
131
  }
138
132
  break
139
133
  }
@@ -150,31 +144,33 @@ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns:
150
144
  // Terminator codegen
151
145
  // ---------------------------------------------------------------------------
152
146
 
153
- function emitTerm(term: Terminator, ns: string, fnName: string): string[] {
147
+ function emitTerm(term: Terminator, ns: string, fnName: string, alloc: VarAllocator): string[] {
154
148
  const lines: string[] = []
155
149
  switch (term.op) {
156
150
  case 'jump':
157
151
  lines.push(`function ${ns}:${fnName}/${term.target}`)
158
152
  break
159
153
  case 'jump_if':
160
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.then}`)
161
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.else_}`)
154
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.then}`)
155
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.else_}`)
162
156
  break
163
157
  case 'jump_unless':
164
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.then}`)
165
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.else_}`)
158
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.then}`)
159
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.else_}`)
166
160
  break
167
- case 'return':
161
+ case 'return': {
162
+ const retName = alloc.internal('ret')
168
163
  if (term.value) {
169
- lines.push(...emitInstr({ op: 'assign', dst: '$ret', src: term.value }, ns))
164
+ lines.push(...emitInstr({ op: 'assign', dst: retName, src: term.value }, ns, alloc))
170
165
  }
171
166
  // In MC 1.20+, use `return` command
172
167
  if (term.value?.kind === 'const') {
173
168
  lines.push(`return ${term.value.value}`)
174
169
  } else if (term.value?.kind === 'var') {
175
- lines.push(`return run scoreboard players get ${varRef(term.value.name)} ${OBJ}`)
170
+ lines.push(`return run scoreboard players get ${alloc.alloc(term.value.name)} ${OBJ}`)
176
171
  }
177
172
  break
173
+ }
178
174
  case 'tick_yield':
179
175
  lines.push(`schedule function ${ns}:${fnName}/${term.continuation} 1t replace`)
180
176
  break
@@ -220,7 +216,7 @@ function applyFunctionOptimization(
220
216
 
221
217
  // Filter out files for functions that were removed (inlined trivial functions)
222
218
  const optimizedNames = new Set(optimized.functions.map(fn => fn.name))
223
-
219
+
224
220
  return {
225
221
  files: files
226
222
  .filter(file => {
@@ -248,10 +244,12 @@ export interface DatapackGenerationResult {
248
244
  files: DatapackFile[]
249
245
  advancements: DatapackFile[]
250
246
  stats: OptimizationStats
247
+ sourceMap?: Record<string, string>
251
248
  }
252
249
 
253
250
  export interface DatapackGenerationOptions {
254
251
  optimizeCommands?: boolean
252
+ mangle?: boolean
255
253
  }
256
254
 
257
255
  export function countMcfunctionCommands(files: DatapackFile[]): number {
@@ -272,7 +270,8 @@ export function generateDatapackWithStats(
272
270
  module: IRModule,
273
271
  options: DatapackGenerationOptions = {},
274
272
  ): DatapackGenerationResult {
275
- const { optimizeCommands = true } = options
273
+ const { optimizeCommands = true, mangle = false } = options
274
+ const alloc = new VarAllocator(mangle)
276
275
  const files: DatapackFile[] = []
277
276
  const advancements: DatapackFile[] = []
278
277
  const ns = module.namespace
@@ -307,7 +306,7 @@ export function generateDatapackWithStats(
307
306
  `scoreboard objectives add ${OBJ} dummy`,
308
307
  ]
309
308
  for (const g of module.globals) {
310
- loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`)
309
+ loadLines.push(`scoreboard players set ${alloc.alloc(g.name)} ${OBJ} ${g.init}`)
311
310
  }
312
311
 
313
312
  // Add trigger objectives
@@ -349,13 +348,19 @@ export function generateDatapackWithStats(
349
348
  })
350
349
  }
351
350
 
352
- // Generate each function (and collect constants for load)
351
+ // Collect all constants across all functions first (deduplicated)
352
+ const allConsts = new Set<number>()
353
+ for (const fn of module.functions) {
354
+ for (const c of collectConsts(fn)) allConsts.add(c)
355
+ }
356
+ if (allConsts.size > 0) {
357
+ loadLines.push(...Array.from(allConsts).sort((a, b) => a - b).map(
358
+ value => `scoreboard players set ${alloc.constant(value)} ${OBJ} ${value}`
359
+ ))
360
+ }
361
+
362
+ // Generate each function
353
363
  for (const fn of module.functions) {
354
- // Constant setup — place constants in __load.mcfunction
355
- const consts = collectConsts(fn)
356
- if (consts.size > 0) {
357
- loadLines.push(...Array.from(consts).map(constSetup))
358
- }
359
364
 
360
365
  // Entry block → <fn_name>.mcfunction
361
366
  // Continuation blocks → <fn_name>/<label>.mcfunction
@@ -366,14 +371,14 @@ export function generateDatapackWithStats(
366
371
  // Param setup in entry block
367
372
  if (i === 0) {
368
373
  for (let j = 0; j < fn.params.length; j++) {
369
- lines.push(`scoreboard players operation ${varRef(fn.params[j])} ${OBJ} = $p${j} ${OBJ}`)
374
+ lines.push(`scoreboard players operation ${alloc.alloc(fn.params[j])} ${OBJ} = ${alloc.internal(`p${j}`)} ${OBJ}`)
370
375
  }
371
376
  }
372
377
 
373
378
  for (const instr of block.instrs) {
374
- lines.push(...emitInstr(instr as any, ns))
379
+ lines.push(...emitInstr(instr as any, ns, alloc))
375
380
  }
376
- lines.push(...emitTerm(block.term, ns, fn.name))
381
+ lines.push(...emitTerm(block.term, ns, fn.name, alloc))
377
382
 
378
383
  const filePath = i === 0
379
384
  ? `data/${ns}/function/${fn.name}.mcfunction`
@@ -404,12 +409,12 @@ export function generateDatapackWithStats(
404
409
 
405
410
  // __tick.mcfunction — calls all @tick functions + trigger check
406
411
  const tickLines = ['# RedScript tick dispatcher']
407
-
412
+
408
413
  // Call all @tick functions
409
414
  for (const fnName of tickFunctionNames) {
410
415
  tickLines.push(`function ${ns}:${fnName}`)
411
416
  }
412
-
417
+
413
418
  // Call trigger check if there are triggers
414
419
  if (triggerNames.size > 0) {
415
420
  tickLines.push(`# Trigger checks`)
@@ -502,13 +507,15 @@ export function generateDatapackWithStats(
502
507
  }
503
508
 
504
509
  const stats = createEmptyOptimizationStats()
510
+ const sourceMap = mangle ? alloc.toSourceMap() : undefined
511
+
505
512
  if (!optimizeCommands) {
506
- return { files, advancements, stats }
513
+ return { files, advancements, stats, sourceMap }
507
514
  }
508
515
 
509
516
  const optimized = applyFunctionOptimization(files)
510
517
  mergeOptimizationStats(stats, optimized.stats)
511
- return { files: optimized.files, advancements, stats }
518
+ return { files: optimized.files, advancements, stats, sourceMap }
512
519
  }
513
520
 
514
521
  export function generateDatapack(module: IRModule): DatapackFile[] {
@@ -10,6 +10,7 @@ import { preprocessSource } from '../../compile'
10
10
  import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
11
11
  import type { DatapackFile } from '../mcfunction'
12
12
  import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
13
+ import { VarAllocator } from '../var-allocator'
13
14
 
14
15
  const DATA_VERSION = 3953
15
16
  const MAX_WIDTH = 16
@@ -54,16 +55,13 @@ export interface StructureCompileResult {
54
55
 
55
56
  export interface StructureCompileOptions {
56
57
  dce?: boolean
58
+ mangle?: boolean
57
59
  }
58
60
 
59
61
  function escapeJsonString(value: string): string {
60
62
  return JSON.stringify(value).slice(1, -1)
61
63
  }
62
64
 
63
- function varRef(name: string): string {
64
- return name.startsWith('$') ? name : `$${name}`
65
- }
66
-
67
65
  function collectConsts(fn: IRFunction): Set<number> {
68
66
  const consts = new Set<number>()
69
67
  for (const block of fn.blocks) {
@@ -85,11 +83,8 @@ function collectConsts(fn: IRFunction): Set<number> {
85
83
  return consts
86
84
  }
87
85
 
88
- function constSetup(value: number): string {
89
- return `scoreboard players set $const_${value} ${OBJ} ${value}`
90
- }
91
-
92
- function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
86
+ function collectCommandEntriesFromModule(module: IRModule, mangle = false): CommandEntry[] {
87
+ const alloc = new VarAllocator(mangle)
93
88
  const entries: CommandEntry[] = []
94
89
  const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
95
90
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
@@ -99,14 +94,14 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
99
94
  const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
100
95
  const loadCommands = [
101
96
  `scoreboard objectives add ${OBJ} dummy`,
102
- ...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
97
+ ...module.globals.map(g => `scoreboard players set ${alloc.alloc(g.name)} ${OBJ} ${g.init}`),
103
98
  ...Array.from(triggerNames).flatMap(triggerName => [
104
99
  `scoreboard objectives add ${triggerName} trigger`,
105
100
  `scoreboard players enable @a ${triggerName}`,
106
101
  ]),
107
102
  ...Array.from(
108
103
  new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))
109
- ).map(constSetup),
104
+ ).map(value => `scoreboard players set ${alloc.constant(value)} ${OBJ} ${value}`),
110
105
  ]
111
106
 
112
107
  for (const eventType of eventTypes) {
@@ -289,10 +284,10 @@ function createBlockTag(entry: CommandEntry, index: number): CompoundTag {
289
284
  })
290
285
  }
291
286
 
292
- export function generateStructure(input: IRModule | DatapackFile[]): StructureCompileResult {
287
+ export function generateStructure(input: IRModule | DatapackFile[], options?: { mangle?: boolean }): StructureCompileResult {
293
288
  const entries = Array.isArray(input)
294
289
  ? collectCommandEntriesFromFiles(input)
295
- : collectCommandEntriesFromModule(input)
290
+ : collectCommandEntriesFromModule(input, options?.mangle)
296
291
 
297
292
  const blockTags = entries.map(createBlockTag)
298
293
  const sizeX = Math.max(1, Math.min(MAX_WIDTH, entries.length || 1))
@@ -345,7 +340,7 @@ export function compileToStructure(
345
340
  functions: structureOptimized.functions,
346
341
  }
347
342
  return {
348
- ...generateStructure(optimizedModule),
343
+ ...generateStructure(optimizedModule, { mangle: options.mangle }),
349
344
  stats,
350
345
  }
351
346
  }
@@ -0,0 +1,71 @@
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
+
8
+ export class VarAllocator {
9
+ private readonly mangle: boolean
10
+ private seq = 0
11
+ private readonly varCache = new Map<string, string>()
12
+ private readonly constCache = new Map<number, string>()
13
+ private readonly internalCache = new Map<string, string>()
14
+
15
+ constructor(mangle = true) {
16
+ this.mangle = mangle
17
+ }
18
+
19
+ /** Allocate a name for a user variable. Strips leading '$' if present. */
20
+ alloc(originalName: string): string {
21
+ const clean = originalName.startsWith('$') ? originalName.slice(1) : originalName
22
+ const cached = this.varCache.get(clean)
23
+ if (cached) return cached
24
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$${clean}`
25
+ this.varCache.set(clean, name)
26
+ return name
27
+ }
28
+
29
+ /** Allocate a name for a constant value (content-addressed). */
30
+ constant(value: number): string {
31
+ const cached = this.constCache.get(value)
32
+ if (cached) return cached
33
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$const_${value}`
34
+ this.constCache.set(value, name)
35
+ return name
36
+ }
37
+
38
+ /** Allocate a name for a compiler internal (e.g. "ret", "p0"). */
39
+ internal(suffix: string): string {
40
+ const cached = this.internalCache.get(suffix)
41
+ if (cached) return cached
42
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$${suffix}`
43
+ this.internalCache.set(suffix, name)
44
+ return name
45
+ }
46
+
47
+ /** Generate the next sequential name: a, b, ..., z, aa, ab, ..., az, ba, ... */
48
+ private nextSeqName(): string {
49
+ const n = this.seq++
50
+ let result = ''
51
+ let remaining = n
52
+ do {
53
+ result = String.fromCharCode(97 + (remaining % 26)) + result
54
+ remaining = Math.floor(remaining / 26) - 1
55
+ } while (remaining >= 0)
56
+ return result
57
+ }
58
+
59
+ /**
60
+ * Returns a sourcemap object mapping allocated name → original name.
61
+ * Useful for debugging: write to <output>.map.json alongside the datapack.
62
+ * Only meaningful when mangle=true.
63
+ */
64
+ toSourceMap(): Record<string, string> {
65
+ const map: Record<string, string> = {}
66
+ for (const [orig, alloc] of this.varCache) map[alloc] = orig
67
+ for (const [val, alloc] of this.constCache) map[alloc] = `const(${val})`
68
+ for (const [suf, alloc] of this.internalCache) map[alloc] = `__${suf}`
69
+ return map
70
+ }
71
+ }
package/src/compile.ts CHANGED
@@ -52,6 +52,24 @@ export interface PreprocessedSource {
52
52
  ranges: SourceRange[]
53
53
  }
54
54
 
55
+ /**
56
+ * Resolve a combined-source line number back to the original file and line.
57
+ * Returns { filePath, line } if a mapping is found, otherwise returns the input unchanged.
58
+ */
59
+ export function resolveSourceLine(
60
+ combinedLine: number,
61
+ ranges: SourceRange[],
62
+ fallbackFile?: string
63
+ ): { filePath?: string; line: number } {
64
+ for (const range of ranges) {
65
+ if (combinedLine >= range.startLine && combinedLine <= range.endLine) {
66
+ const localLine = combinedLine - range.startLine + 1
67
+ return { filePath: range.filePath, line: localLine }
68
+ }
69
+ }
70
+ return { filePath: fallbackFile, line: combinedLine }
71
+ }
72
+
55
73
  const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
56
74
 
57
75
  interface PreprocessOptions {