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.
- package/.github/workflows/publish-extension-on-ci.yml +1 -0
- package/dist/__tests__/cli.test.js +1 -1
- package/dist/__tests__/codegen.test.js +12 -6
- package/dist/__tests__/e2e.test.js +6 -6
- package/dist/__tests__/lowering.test.js +8 -8
- package/dist/__tests__/optimizer.test.js +31 -0
- package/dist/__tests__/stdlib-advanced.test.d.ts +4 -0
- package/dist/__tests__/stdlib-advanced.test.js +264 -0
- package/dist/__tests__/stdlib-math.test.d.ts +7 -0
- package/dist/__tests__/stdlib-math.test.js +352 -0
- package/dist/__tests__/stdlib-vec.test.d.ts +4 -0
- package/dist/__tests__/stdlib-vec.test.js +264 -0
- package/dist/ast/types.d.ts +17 -1
- package/dist/codegen/mcfunction/index.js +159 -18
- package/dist/codegen/var-allocator.d.ts +17 -0
- package/dist/codegen/var-allocator.js +33 -3
- package/dist/compile.d.ts +14 -0
- package/dist/compile.js +62 -5
- package/dist/index.js +20 -1
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +1 -0
- package/dist/lowering/index.d.ts +5 -0
- package/dist/lowering/index.js +83 -10
- package/dist/optimizer/dce.js +21 -5
- package/dist/optimizer/passes.js +18 -6
- package/dist/optimizer/structure.js +7 -0
- package/dist/parser/index.d.ts +5 -0
- package/dist/parser/index.js +43 -2
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +109 -9
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +1 -1
- package/src/__tests__/codegen.test.ts +12 -6
- package/src/__tests__/e2e.test.ts +6 -6
- package/src/__tests__/lowering.test.ts +8 -8
- package/src/__tests__/optimizer.test.ts +33 -0
- package/src/__tests__/stdlib-advanced.test.ts +259 -0
- package/src/__tests__/stdlib-math.test.ts +374 -0
- package/src/__tests__/stdlib-vec.test.ts +259 -0
- package/src/ast/types.ts +11 -1
- package/src/codegen/mcfunction/index.ts +148 -19
- package/src/codegen/var-allocator.ts +36 -3
- package/src/compile.ts +72 -5
- package/src/index.ts +21 -1
- package/src/ir/types.ts +2 -0
- package/src/lexer/index.ts +2 -1
- package/src/lowering/index.ts +96 -10
- package/src/optimizer/dce.ts +22 -5
- package/src/optimizer/passes.ts +18 -5
- package/src/optimizer/structure.ts +6 -1
- package/src/parser/index.ts +47 -2
- package/src/runtime/index.ts +108 -10
- package/src/stdlib/advanced.mcrs +249 -0
- package/src/stdlib/math.mcrs +259 -19
- 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
|
|
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
|
|
125
|
-
|
|
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
|
|
130
|
-
lines.push(`scoreboard players operation ${alloc.alloc(instr.dst)} ${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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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)
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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?: {
|
package/src/lexer/index.ts
CHANGED
|
@@ -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',
|