redscript-mc 1.2.24 → 1.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.github/workflows/publish-extension-on-ci.yml +1 -0
  2. package/dist/__tests__/cli.test.js +1 -1
  3. package/dist/__tests__/codegen.test.js +12 -6
  4. package/dist/__tests__/e2e.test.js +6 -6
  5. package/dist/__tests__/lowering.test.js +8 -8
  6. package/dist/__tests__/optimizer.test.js +31 -0
  7. package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
  8. package/dist/__tests__/stdlib-advanced.test.js +264 -0
  9. package/dist/__tests__/stdlib-math.test.d.ts +7 -0
  10. package/dist/__tests__/stdlib-math.test.js +352 -0
  11. package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
  12. package/dist/__tests__/stdlib-vec.test.js +264 -0
  13. package/dist/ast/types.d.ts +17 -1
  14. package/dist/codegen/mcfunction/index.js +159 -18
  15. package/dist/codegen/var-allocator.d.ts +17 -0
  16. package/dist/codegen/var-allocator.js +33 -3
  17. package/dist/compile.d.ts +14 -0
  18. package/dist/compile.js +62 -5
  19. package/dist/index.js +20 -1
  20. package/dist/ir/types.d.ts +4 -0
  21. package/dist/lexer/index.d.ts +1 -1
  22. package/dist/lexer/index.js +1 -0
  23. package/dist/lowering/index.d.ts +5 -0
  24. package/dist/lowering/index.js +83 -10
  25. package/dist/optimizer/dce.js +21 -5
  26. package/dist/optimizer/passes.js +18 -6
  27. package/dist/optimizer/structure.js +7 -0
  28. package/dist/parser/index.d.ts +5 -0
  29. package/dist/parser/index.js +43 -2
  30. package/dist/runtime/index.d.ts +6 -0
  31. package/dist/runtime/index.js +109 -9
  32. package/editors/vscode/package-lock.json +3 -3
  33. package/editors/vscode/package.json +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/cli.test.ts +1 -1
  36. package/src/__tests__/codegen.test.ts +12 -6
  37. package/src/__tests__/e2e.test.ts +6 -6
  38. package/src/__tests__/lowering.test.ts +8 -8
  39. package/src/__tests__/optimizer.test.ts +33 -0
  40. package/src/__tests__/stdlib-advanced.test.ts +259 -0
  41. package/src/__tests__/stdlib-math.test.ts +374 -0
  42. package/src/__tests__/stdlib-vec.test.ts +259 -0
  43. package/src/ast/types.ts +11 -1
  44. package/src/codegen/mcfunction/index.ts +148 -19
  45. package/src/codegen/var-allocator.ts +36 -3
  46. package/src/compile.ts +72 -5
  47. package/src/index.ts +21 -1
  48. package/src/ir/types.ts +2 -0
  49. package/src/lexer/index.ts +2 -1
  50. package/src/lowering/index.ts +96 -10
  51. package/src/optimizer/dce.ts +22 -5
  52. package/src/optimizer/passes.ts +18 -5
  53. package/src/optimizer/structure.ts +6 -1
  54. package/src/parser/index.ts +47 -2
  55. package/src/runtime/index.ts +108 -10
  56. package/src/stdlib/advanced.mcrs +249 -0
  57. package/src/stdlib/math.mcrs +259 -19
  58. package/src/stdlib/vec.mcrs +246 -0
@@ -16,7 +16,7 @@
16
16
  * parameters: "$p0", "$p1", ...
17
17
  */
18
18
 
19
- import type { IRBlock, IRFunction, IRModule, Operand, Terminator } from '../../ir/types'
19
+ import type { IRBlock, IRFunction, IRInstr, 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
22
  import { VarAllocator } from '../var-allocator'
@@ -30,6 +30,7 @@ const OBJ = 'rs' // scoreboard objective name
30
30
  function operandToScore(op: Operand, alloc: VarAllocator): string {
31
31
  if (op.kind === 'var') return `${alloc.alloc(op.name)} ${OBJ}`
32
32
  if (op.kind === 'const') return `${alloc.constant(op.value)} ${OBJ}`
33
+ if (op.kind === 'param') return `${alloc.internal(`p${op.index}`)} ${OBJ}`
33
34
  throw new Error(`Cannot convert storage operand to score: ${op.path}`)
34
35
  }
35
36
 
@@ -74,6 +75,8 @@ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns:
74
75
  lines.push(`scoreboard players set ${dst} ${OBJ} ${src.value}`)
75
76
  } else if (src.kind === 'var') {
76
77
  lines.push(`scoreboard players operation ${dst} ${OBJ} = ${alloc.alloc(src.name)} ${OBJ}`)
78
+ } else if (src.kind === 'param') {
79
+ lines.push(`scoreboard players operation ${dst} ${OBJ} = ${alloc.internal(`p${src.index}`)} ${OBJ}`)
77
80
  } else {
78
81
  lines.push(`execute store result score ${dst} ${OBJ} run data get storage ${src.path}`)
79
82
  }
@@ -119,22 +122,41 @@ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns:
119
122
  }
120
123
 
121
124
  case 'call': {
122
- // Push args as param fake players
125
+ // Push args into the internal parameter slots ($p0, $p1, ...).
126
+ // We emit the copy commands directly (not via emitInstr/alloc.alloc) to
127
+ // ensure the destination resolves to alloc.internal('p{i}') rather than
128
+ // alloc.alloc('p{i}') which would create a *different* user-var slot.
123
129
  for (let i = 0; i < instr.args.length; i++) {
124
- const paramName = alloc.internal(`p${i}`)
125
- lines.push(...emitInstr({ op: 'assign', dst: paramName, src: instr.args[i] }, ns, alloc))
130
+ const paramSlot = alloc.internal(`p${i}`)
131
+ const arg = instr.args[i] as Operand
132
+ if (arg.kind === 'const') {
133
+ lines.push(`scoreboard players set ${paramSlot} ${OBJ} ${arg.value}`)
134
+ } else if (arg.kind === 'var') {
135
+ lines.push(`scoreboard players operation ${paramSlot} ${OBJ} = ${alloc.alloc(arg.name)} ${OBJ}`)
136
+ } else if (arg.kind === 'param') {
137
+ lines.push(`scoreboard players operation ${paramSlot} ${OBJ} = ${alloc.internal(`p${arg.index}`)} ${OBJ}`)
138
+ }
139
+ // storage args are rare for call sites; fall through to no-op
126
140
  }
127
141
  lines.push(`function ${ns}:${instr.fn}`)
128
142
  if (instr.dst) {
129
- const retName = alloc.internal('ret')
130
- lines.push(`scoreboard players operation ${alloc.alloc(instr.dst)} ${OBJ} = ${retName} ${OBJ}`)
143
+ const retSlot = alloc.internal('ret')
144
+ lines.push(`scoreboard players operation ${alloc.alloc(instr.dst)} ${OBJ} = ${retSlot} ${OBJ}`)
131
145
  }
132
146
  break
133
147
  }
134
148
 
135
- case 'raw':
136
- lines.push(instr.cmd as string)
149
+ case 'raw': {
150
+ // resolveRaw rewrites $var tokens that are registered in the allocator
151
+ // so that mangle=true mode produces correct mangled names instead of
152
+ // the raw IR names embedded by the lowering phase.
153
+ // \x01 is a sentinel for the MC macro line-start '$' (used by
154
+ // storage_get_int sub-functions). Replace it last, after resolveRaw,
155
+ // so '$execute' is never treated as a variable reference.
156
+ const rawResolved = alloc.resolveRaw(instr.cmd as string).replace(/^\x01/, '$')
157
+ lines.push(rawResolved)
137
158
  break
159
+ }
138
160
  }
139
161
 
140
162
  return lines
@@ -159,15 +181,27 @@ function emitTerm(term: Terminator, ns: string, fnName: string, alloc: VarAlloca
159
181
  lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.else_}`)
160
182
  break
161
183
  case 'return': {
162
- const retName = alloc.internal('ret')
184
+ // Emit the copy to the shared return slot directly — do NOT go through
185
+ // emitInstr/alloc.alloc(retSlot) which would allocate a *user* var slot
186
+ // (different from the internal slot) and break mangle mode.
187
+ const retSlot = alloc.internal('ret')
163
188
  if (term.value) {
164
- lines.push(...emitInstr({ op: 'assign', dst: retName, src: term.value }, ns, alloc))
189
+ if (term.value.kind === 'const') {
190
+ lines.push(`scoreboard players set ${retSlot} ${OBJ} ${term.value.value}`)
191
+ } else if (term.value.kind === 'var') {
192
+ lines.push(`scoreboard players operation ${retSlot} ${OBJ} = ${alloc.alloc(term.value.name)} ${OBJ}`)
193
+ } else if (term.value.kind === 'param') {
194
+ lines.push(`scoreboard players operation ${retSlot} ${OBJ} = ${alloc.internal(`p${term.value.index}`)} ${OBJ}`)
195
+ }
165
196
  }
166
- // In MC 1.20+, use `return` command
197
+ // MC 1.20+: use `return` to propagate the value back to the caller's
198
+ // `execute store result … run function …` without an extra scoreboard read.
167
199
  if (term.value?.kind === 'const') {
168
200
  lines.push(`return ${term.value.value}`)
169
201
  } else if (term.value?.kind === 'var') {
170
202
  lines.push(`return run scoreboard players get ${alloc.alloc(term.value.name)} ${OBJ}`)
203
+ } else if (term.value?.kind === 'param') {
204
+ lines.push(`return run scoreboard players get ${alloc.internal(`p${term.value.index}`)} ${OBJ}`)
171
205
  }
172
206
  break
173
207
  }
@@ -266,6 +300,56 @@ export function countMcfunctionCommands(files: DatapackFile[]): number {
266
300
  }, 0)
267
301
  }
268
302
 
303
+ // ---------------------------------------------------------------------------
304
+ // Pre-allocation helpers for the two-pass mangle strategy
305
+ // ---------------------------------------------------------------------------
306
+
307
+ /** Register every variable referenced in an instruction with the allocator. */
308
+ function preAllocInstr(instr: IRInstr, alloc: VarAllocator): void {
309
+ switch (instr.op) {
310
+ case 'assign':
311
+ alloc.alloc(instr.dst)
312
+ if (instr.src.kind === 'var') alloc.alloc(instr.src.name)
313
+ break
314
+ case 'binop':
315
+ alloc.alloc(instr.dst)
316
+ if (instr.lhs.kind === 'var') alloc.alloc(instr.lhs.name)
317
+ if (instr.rhs.kind === 'var') alloc.alloc(instr.rhs.name)
318
+ break
319
+ case 'cmp':
320
+ alloc.alloc(instr.dst)
321
+ if (instr.lhs.kind === 'var') alloc.alloc(instr.lhs.name)
322
+ if (instr.rhs.kind === 'var') alloc.alloc(instr.rhs.name)
323
+ break
324
+ case 'call':
325
+ for (const arg of instr.args) {
326
+ if (arg.kind === 'var') alloc.alloc(arg.name)
327
+ }
328
+ if (instr.dst) alloc.alloc(instr.dst)
329
+ break
330
+ case 'raw':
331
+ // Scan for $varname tokens and pre-register each one
332
+ ;(instr.cmd as string).replace(/\$[A-Za-z_][A-Za-z0-9_]*/g, (tok) => {
333
+ alloc.alloc(tok)
334
+ return tok
335
+ })
336
+ break
337
+ }
338
+ }
339
+
340
+ /** Register every variable referenced in a terminator with the allocator. */
341
+ function preAllocTerm(term: Terminator, alloc: VarAllocator): void {
342
+ switch (term.op) {
343
+ case 'jump_if':
344
+ case 'jump_unless':
345
+ alloc.alloc(term.cond)
346
+ break
347
+ case 'return':
348
+ if (term.value?.kind === 'var') alloc.alloc(term.value.name)
349
+ break
350
+ }
351
+ }
352
+
269
353
  export function generateDatapackWithStats(
270
354
  module: IRModule,
271
355
  options: DatapackGenerationOptions = {},
@@ -359,6 +443,40 @@ export function generateDatapackWithStats(
359
443
  ))
360
444
  }
361
445
 
446
+ // ─────────────────────────────────────────────────────────────────────────
447
+ // Pre-allocation pass (mangle mode only)
448
+ //
449
+ // When mangle=true, the codegen assigns sequential names ($a, $b, …) the
450
+ // FIRST time alloc.alloc() is called for a given variable. Raw IR commands
451
+ // embed variable names (e.g. "$_0") as plain strings; resolveRaw() can only
452
+ // substitute them if the name was already registered in the allocator.
453
+ //
454
+ // Problem: a freshTemp ($\_0) used in a `raw` instruction and then in the
455
+ // immediately following `assign` gets registered by the `assign` AFTER the
456
+ // `raw` has already been emitted — so resolveRaw sees an unknown name and
457
+ // passes it through verbatim ($\_0), while the assign emits a different
458
+ // mangled slot ($e). The two slots never meet and the value is lost.
459
+ //
460
+ // Fix: walk every instruction (and terminator) of every function in order
461
+ // and call alloc.alloc() for each variable reference. This registers all
462
+ // names — with the same sequential order the main emit pass will encounter
463
+ // them — so that resolveRaw() can always find the correct mangled name.
464
+ // ─────────────────────────────────────────────────────────────────────────
465
+ if (mangle) {
466
+ for (const fn of module.functions) {
467
+ // Register internals used by the calling convention
468
+ for (let i = 0; i < fn.params.length; i++) alloc.internal(`p${i}`)
469
+ alloc.internal('ret')
470
+
471
+ for (const block of fn.blocks) {
472
+ for (const instr of block.instrs) {
473
+ preAllocInstr(instr as IRInstr, alloc)
474
+ }
475
+ preAllocTerm(block.term, alloc)
476
+ }
477
+ }
478
+ }
479
+
362
480
  // Generate each function
363
481
  for (const fn of module.functions) {
364
482
 
@@ -368,12 +486,9 @@ export function generateDatapackWithStats(
368
486
  const block = fn.blocks[i]
369
487
  const lines: string[] = [`# block: ${block.label}`]
370
488
 
371
- // Param setup in entry block
372
- if (i === 0) {
373
- for (let j = 0; j < fn.params.length; j++) {
374
- lines.push(`scoreboard players operation ${alloc.alloc(fn.params[j])} ${OBJ} = ${alloc.internal(`p${j}`)} ${OBJ}`)
375
- }
376
- }
489
+ // Param setup is now handled by the lowering IR itself via { kind: 'param' }
490
+ // operands, so we no longer need a separate codegen param-copy loop here.
491
+ // (Removing it prevents the double-assignment that caused mangle-mode collisions.)
377
492
 
378
493
  for (const instr of block.instrs) {
379
494
  lines.push(...emitInstr(instr as any, ns, alloc))
@@ -384,15 +499,29 @@ export function generateDatapackWithStats(
384
499
  ? `data/${ns}/function/${fn.name}.mcfunction`
385
500
  : `data/${ns}/function/${fn.name}/${block.label}.mcfunction`
386
501
 
502
+ // Skip empty continuation blocks (only contain the block comment, no real commands)
503
+ // Entry block (i === 0) is always emitted so the function file exists
504
+ const hasRealContent = lines.some(l => !l.startsWith('#') && l.trim() !== '')
505
+ if (i !== 0 && !hasRealContent) continue
506
+
387
507
  files.push({ path: filePath, content: lines.join('\n') })
388
508
  }
389
509
  }
390
510
 
391
- // Call @load functions from __load
511
+ // Call @load functions and @requires-referenced load helpers from __load.
512
+ // We collect them in a set to deduplicate (multiple fns might @requires the same dep).
513
+ const loadCalls = new Set<string>()
392
514
  for (const fn of module.functions) {
393
515
  if (fn.isLoadInit) {
394
- loadLines.push(`function ${ns}:${fn.name}`)
516
+ loadCalls.add(fn.name)
395
517
  }
518
+ // @requires: if this fn is compiled in, its required load-helpers must also run
519
+ for (const dep of fn.requiredLoads ?? []) {
520
+ loadCalls.add(dep)
521
+ }
522
+ }
523
+ for (const name of loadCalls) {
524
+ loadLines.push(`function ${ns}:${name}`)
396
525
  }
397
526
 
398
527
  // Write __load.mcfunction
@@ -35,6 +35,35 @@ export class VarAllocator {
35
35
  return name
36
36
  }
37
37
 
38
+ /**
39
+ * Look up the allocated name for a raw scoreboard fake-player name such as
40
+ * "$_2", "$x", "$p0", or "$ret". Returns the mangled name when mangle=true,
41
+ * or the original name when mangle=false or the name is not yet known.
42
+ *
43
+ * Unlike alloc/internal/constant this does NOT create a new slot — it only
44
+ * resolves names that were already registered. Used by the codegen to
45
+ * rewrite variable references inside `raw` IR instructions.
46
+ */
47
+ resolve(rawName: string): string {
48
+ const clean = rawName.startsWith('$') ? rawName.slice(1) : rawName
49
+ // Check every cache in priority order: vars, internals, consts
50
+ return (
51
+ this.varCache.get(clean) ??
52
+ this.internalCache.get(clean) ??
53
+ rawName // not registered → return as-is (literal fake player, not a var)
54
+ )
55
+ }
56
+
57
+ /**
58
+ * Rewrite all $varname tokens in a raw mcfunction command string so that
59
+ * IR variable names are replaced by their allocated (possibly mangled) names.
60
+ * Tokens that are not registered in the allocator are left untouched (they
61
+ * are literal scoreboard fake-player names like "out" or "#rs").
62
+ */
63
+ resolveRaw(cmd: string): string {
64
+ return cmd.replace(/\$[A-Za-z_][A-Za-z0-9_]*/g, (tok) => this.resolve(tok))
65
+ }
66
+
38
67
  /** Allocate a name for a compiler internal (e.g. "ret", "p0"). */
39
68
  internal(suffix: string): string {
40
69
  const cached = this.internalCache.get(suffix)
@@ -63,9 +92,13 @@ export class VarAllocator {
63
92
  */
64
93
  toSourceMap(): Record<string, string> {
65
94
  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}`
95
+ for (const [orig, alloc] of this.varCache) {
96
+ // Skip compiler-generated temporaries (start with _ followed by digits)
97
+ if (/^_\d+$/.test(orig)) continue
98
+ map[alloc] = orig
99
+ }
100
+ for (const [val, alloc] of this.constCache) map[alloc] = `const:${val}`
101
+ for (const [suf, alloc] of this.internalCache) map[alloc] = `internal:${suf}`
69
102
  return map
70
103
  }
71
104
  }
package/src/compile.ts CHANGED
@@ -26,6 +26,13 @@ export interface CompileOptions {
26
26
  filePath?: string
27
27
  optimize?: boolean
28
28
  dce?: boolean
29
+ mangle?: boolean
30
+ /** Additional source files that should be treated as *library* code.
31
+ * Functions in these files are DCE-eligible: they are only compiled into
32
+ * the datapack when actually called from user code. Each string is parsed
33
+ * independently (as if it had `module library;` at the top), so library
34
+ * mode never bleeds into the main `source`. */
35
+ librarySources?: string[]
29
36
  }
30
37
 
31
38
  // ---------------------------------------------------------------------------
@@ -50,6 +57,10 @@ export interface SourceRange {
50
57
  export interface PreprocessedSource {
51
58
  source: string
52
59
  ranges: SourceRange[]
60
+ /** Imported files that declared `module library;` — parsed separately
61
+ * in library mode so their functions are DCE-eligible. Never concatenated
62
+ * into `source`. */
63
+ libraryImports?: Array<{ source: string; filePath: string }>
53
64
  }
54
65
 
55
66
  /**
@@ -72,6 +83,17 @@ export function resolveSourceLine(
72
83
 
73
84
  const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
74
85
 
86
+ /** Returns true if the source file declares `module library;` at its top
87
+ * (before any non-comment/non-blank lines). */
88
+ function isLibrarySource(source: string): boolean {
89
+ for (const line of source.split('\n')) {
90
+ const trimmed = line.trim()
91
+ if (!trimmed || trimmed.startsWith('//')) continue
92
+ return /^module\s+library\s*;/.test(trimmed)
93
+ }
94
+ return false
95
+ }
96
+
75
97
  interface PreprocessOptions {
76
98
  filePath?: string
77
99
  seen?: Set<string>
@@ -99,6 +121,8 @@ export function preprocessSourceWithMetadata(source: string, options: Preprocess
99
121
 
100
122
  const lines = source.split('\n')
101
123
  const imports: PreprocessedSource[] = []
124
+ /** Library imports: `module library;` files routed here instead of concatenated. */
125
+ const libraryImports: Array<{ source: string; filePath: string }> = []
102
126
  const bodyLines: string[] = []
103
127
  let parsingHeader = true
104
128
 
@@ -133,7 +157,16 @@ export function preprocessSourceWithMetadata(source: string, options: Preprocess
133
157
  )
134
158
  }
135
159
 
136
- imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }))
160
+ if (isLibrarySource(importedSource)) {
161
+ // Library file: parse separately so its functions are DCE-eligible.
162
+ // Also collect any transitive library imports inside it.
163
+ const nested = preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen })
164
+ libraryImports.push({ source: importedSource, filePath: importPath })
165
+ // Propagate transitive library imports (e.g. math.mcrs imports vec.mcrs)
166
+ if (nested.libraryImports) libraryImports.push(...nested.libraryImports)
167
+ } else {
168
+ imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }))
169
+ }
137
170
  }
138
171
  continue
139
172
  }
@@ -167,7 +200,11 @@ export function preprocessSourceWithMetadata(source: string, options: Preprocess
167
200
  })
168
201
  }
169
202
 
170
- return { source: combined, ranges }
203
+ return {
204
+ source: combined,
205
+ ranges,
206
+ libraryImports: libraryImports.length > 0 ? libraryImports : undefined,
207
+ }
171
208
  }
172
209
 
173
210
  export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
@@ -191,8 +228,37 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
191
228
  // Lexing
192
229
  const tokens = new Lexer(preprocessedSource, filePath).tokenize()
193
230
 
194
- // Parsing
231
+ // Parsing — user source
195
232
  const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
233
+
234
+ // Collect all library sources: explicit `librarySources` option +
235
+ // auto-detected imports (files with `module library;` pulled out by the
236
+ // preprocessor rather than concatenated).
237
+ const allLibrarySources: Array<{ src: string; fp?: string }> = []
238
+ for (const libSrc of options.librarySources ?? []) {
239
+ allLibrarySources.push({ src: libSrc })
240
+ }
241
+ for (const li of preprocessed.libraryImports ?? []) {
242
+ allLibrarySources.push({ src: li.source, fp: li.filePath })
243
+ }
244
+
245
+ // Parse library sources independently (fresh Parser per source) so that
246
+ // `inLibraryMode` never bleeds into user code. All resulting functions get
247
+ // isLibraryFn=true (either via `module library;` in the source, or forced below).
248
+ for (const { src, fp } of allLibrarySources) {
249
+ const libPreprocessed = preprocessSourceWithMetadata(src, fp ? { filePath: fp } : {})
250
+ const libTokens = new Lexer(libPreprocessed.source, fp).tokenize()
251
+ const libAst = new Parser(libTokens, libPreprocessed.source, fp).parse(namespace)
252
+ // Force all functions to library mode (even if source lacks `module library;`)
253
+ for (const fn of libAst.declarations) fn.isLibraryFn = true
254
+ // Merge into main AST
255
+ parsedAst.declarations.push(...libAst.declarations)
256
+ parsedAst.structs.push(...libAst.structs)
257
+ parsedAst.implBlocks.push(...libAst.implBlocks)
258
+ parsedAst.enums.push(...libAst.enums)
259
+ parsedAst.consts.push(...libAst.consts)
260
+ parsedAst.globals.push(...libAst.globals)
261
+ }
196
262
  const dceResult = shouldRunDce ? eliminateDeadCode(parsedAst) : { program: parsedAst, warnings: [] }
197
263
  const ast = dceResult.program
198
264
 
@@ -204,8 +270,9 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
204
270
  ? { ...ir, functions: ir.functions.map(fn => optimize(fn)) }
205
271
  : ir
206
272
 
207
- // Code generation
208
- const generated = generateDatapackWithStats(optimized)
273
+ // Code generation — mangle=true by default to prevent cross-function
274
+ // scoreboard variable collisions in the global MC scoreboard namespace.
275
+ const generated = generateDatapackWithStats(optimized, { mangle: options.mangle ?? true })
209
276
 
210
277
  return {
211
278
  success: true,
package/src/index.ts CHANGED
@@ -69,8 +69,28 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
69
69
  // Lexing
70
70
  const tokens = new Lexer(preprocessedSource, filePath).tokenize()
71
71
 
72
- // Parsing
72
+ // Parsing — user source
73
73
  const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
74
+
75
+ // Library imports: files that declared `module library;` are parsed independently
76
+ // (fresh Parser per file) so their functions are DCE-eligible but never bleed into user code.
77
+ const allLibrarySources: Array<{ src: string; fp?: string }> = []
78
+ for (const li of preprocessed.libraryImports ?? []) {
79
+ allLibrarySources.push({ src: li.source, fp: li.filePath })
80
+ }
81
+ for (const { src, fp } of allLibrarySources) {
82
+ const libPreprocessed = preprocessSourceWithMetadata(src, fp ? { filePath: fp } : {})
83
+ const libTokens = new Lexer(libPreprocessed.source, fp).tokenize()
84
+ const libAst = new Parser(libTokens, libPreprocessed.source, fp).parse(namespace)
85
+ for (const fn of libAst.declarations) fn.isLibraryFn = true
86
+ parsedAst.declarations.push(...libAst.declarations)
87
+ parsedAst.structs.push(...libAst.structs)
88
+ parsedAst.implBlocks.push(...libAst.implBlocks)
89
+ parsedAst.enums.push(...libAst.enums)
90
+ parsedAst.consts.push(...libAst.consts)
91
+ parsedAst.globals.push(...libAst.globals)
92
+ }
93
+
74
94
  const dceResult = shouldRunDce ? eliminateDeadCode(parsedAst, preprocessed.ranges) : { program: parsedAst, warnings: [] }
75
95
  const ast = dceResult.program
76
96
 
package/src/ir/types.ts CHANGED
@@ -19,6 +19,7 @@ export type Operand =
19
19
  | { kind: 'var'; name: string } // scoreboard fake player
20
20
  | { kind: 'const'; value: number } // integer literal
21
21
  | { kind: 'storage'; path: string } // NBT storage path (e.g. "redscript:heap data.x")
22
+ | { kind: 'param'; index: number } // function parameter slot (alloc.internal('p{i}')), avoids mangle collision
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Binary operators (all map to `scoreboard players operation`)
@@ -101,6 +102,7 @@ export interface IRFunction {
101
102
  commands?: IRCommand[] // structure target command stream
102
103
  isTickLoop?: boolean // true → Repeat command block (runs every tick)
103
104
  isLoadInit?: boolean // true → called from __load.mcfunction
105
+ requiredLoads?: string[] // @requires("fn") — these fns are also called from __load when this fn is compiled in
104
106
  isTriggerHandler?: boolean // true → handles a trigger event
105
107
  triggerName?: string // the trigger objective name
106
108
  eventTrigger?: {
@@ -15,7 +15,7 @@ import { DiagnosticError } from '../diagnostics'
15
15
  export type TokenKind =
16
16
  // Keywords
17
17
  | 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match'
18
- | 'return' | 'break' | 'continue' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace'
18
+ | 'return' | 'break' | 'continue' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace' | 'module'
19
19
  | 'execute' | 'run' | 'unless' | 'declare'
20
20
  // Types
21
21
  | 'int' | 'bool' | 'float' | 'string' | 'void'
@@ -86,6 +86,7 @@ const KEYWORDS: Record<string, TokenKind> = {
86
86
  enum: 'enum',
87
87
  trigger: 'trigger',
88
88
  namespace: 'namespace',
89
+ module: 'module',
89
90
  execute: 'execute',
90
91
  run: 'run',
91
92
  unless: 'unless',