redscript-mc 1.2.25 → 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/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 +154 -18
- package/dist/codegen/var-allocator.d.ts +17 -0
- package/dist/codegen/var-allocator.js +26 -0
- 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 +143 -19
- package/src/codegen/var-allocator.ts +29 -0
- 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))
|
|
@@ -393,11 +508,20 @@ export function generateDatapackWithStats(
|
|
|
393
508
|
}
|
|
394
509
|
}
|
|
395
510
|
|
|
396
|
-
// 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>()
|
|
397
514
|
for (const fn of module.functions) {
|
|
398
515
|
if (fn.isLoadInit) {
|
|
399
|
-
|
|
516
|
+
loadCalls.add(fn.name)
|
|
400
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}`)
|
|
401
525
|
}
|
|
402
526
|
|
|
403
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)
|
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',
|
package/src/lowering/index.ts
CHANGED
|
@@ -106,6 +106,8 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
|
|
|
106
106
|
setTimeout: () => null, // Special handling
|
|
107
107
|
setInterval: () => null, // Special handling
|
|
108
108
|
clearInterval: () => null, // Special handling
|
|
109
|
+
storage_get_int: () => null, // Special handling (dynamic NBT array read via macro)
|
|
110
|
+
storage_set_array: () => null, // Special handling (write literal NBT array to storage)
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
export interface Warning {
|
|
@@ -215,6 +217,14 @@ export class Lowering {
|
|
|
215
217
|
private implMethods: Map<string, Map<string, { fn: FnDecl; loweredName: string }>> = new Map()
|
|
216
218
|
private specializedFunctions: Map<string, string> = new Map()
|
|
217
219
|
private currentFn: string = ''
|
|
220
|
+
|
|
221
|
+
/** Unique IR variable name for a local variable, scoped to the current function.
|
|
222
|
+
* Prevents cross-function scoreboard slot collisions: $fn_x ≠ $gn_x.
|
|
223
|
+
* Only applies to user-defined locals/params; internal slots ($p0, $ret) are
|
|
224
|
+
* intentionally global (calling convention). */
|
|
225
|
+
private fnVar(name: string): string {
|
|
226
|
+
return `$${this.currentFn}_${name}`
|
|
227
|
+
}
|
|
218
228
|
private currentStdlibCallSite?: StdlibCallSiteContext
|
|
219
229
|
private foreachCounter: number = 0
|
|
220
230
|
private lambdaCounter: number = 0
|
|
@@ -617,12 +627,12 @@ export class Lowering {
|
|
|
617
627
|
continue
|
|
618
628
|
}
|
|
619
629
|
|
|
620
|
-
this.varMap.set(param.name,
|
|
630
|
+
this.varMap.set(param.name, this.fnVar(param.name))
|
|
621
631
|
}
|
|
622
632
|
} else {
|
|
623
633
|
for (const param of runtimeParams) {
|
|
624
634
|
const paramName = param.name
|
|
625
|
-
this.varMap.set(paramName,
|
|
635
|
+
this.varMap.set(paramName, this.fnVar(paramName))
|
|
626
636
|
this.varTypes.set(paramName, this.normalizeType(param.type))
|
|
627
637
|
}
|
|
628
638
|
}
|
|
@@ -635,11 +645,15 @@ export class Lowering {
|
|
|
635
645
|
// Start entry block
|
|
636
646
|
this.builder.startBlock('entry')
|
|
637
647
|
|
|
638
|
-
// Copy params from
|
|
648
|
+
// Copy params from the parameter-passing slots to named local variables.
|
|
649
|
+
// Use { kind: 'param', index: i } so the codegen resolves to
|
|
650
|
+
// alloc.internal('p{i}') consistently in both mangle and no-mangle modes,
|
|
651
|
+
// avoiding the slot-collision between the internal register and a user variable
|
|
652
|
+
// named 'p0'/'p1' that occurred with { kind: 'var', name: '$p0' }.
|
|
639
653
|
for (let i = 0; i < runtimeParams.length; i++) {
|
|
640
654
|
const paramName = runtimeParams[i].name
|
|
641
|
-
const varName =
|
|
642
|
-
this.builder.emitAssign(varName, { kind: '
|
|
655
|
+
const varName = this.fnVar(paramName)
|
|
656
|
+
this.builder.emitAssign(varName, { kind: 'param', index: i })
|
|
643
657
|
}
|
|
644
658
|
|
|
645
659
|
if (staticEventDec) {
|
|
@@ -647,7 +661,7 @@ export class Lowering {
|
|
|
647
661
|
const param = fn.params[i]
|
|
648
662
|
const expected = eventParamSpecs[i]
|
|
649
663
|
if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
|
|
650
|
-
this.builder.emitAssign(
|
|
664
|
+
this.builder.emitAssign(this.fnVar(param.name), { kind: 'const', value: 0 })
|
|
651
665
|
}
|
|
652
666
|
}
|
|
653
667
|
}
|
|
@@ -716,6 +730,23 @@ export class Lowering {
|
|
|
716
730
|
irFn.isLoadInit = true
|
|
717
731
|
}
|
|
718
732
|
|
|
733
|
+
// @requires("dep_fn") — when this function is compiled in, dep_fn is also
|
|
734
|
+
// called from __load. The dep_fn itself does NOT need @load; it can be a
|
|
735
|
+
// private (_) function that only runs at load time when this fn is used.
|
|
736
|
+
const requiredLoads: string[] = []
|
|
737
|
+
for (const d of fn.decorators) {
|
|
738
|
+
if (d.name === 'require_on_load') {
|
|
739
|
+
for (const arg of d.rawArgs ?? []) {
|
|
740
|
+
if (arg.kind === 'string') {
|
|
741
|
+
requiredLoads.push(arg.value)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (requiredLoads.length > 0) {
|
|
747
|
+
irFn.requiredLoads = requiredLoads
|
|
748
|
+
}
|
|
749
|
+
|
|
719
750
|
// Handle tick rate counter if needed
|
|
720
751
|
if (tickRate && tickRate > 1) {
|
|
721
752
|
this.wrapWithTickRate(irFn, tickRate)
|
|
@@ -860,7 +891,7 @@ export class Lowering {
|
|
|
860
891
|
)
|
|
861
892
|
}
|
|
862
893
|
|
|
863
|
-
const varName =
|
|
894
|
+
const varName = this.fnVar(stmt.name)
|
|
864
895
|
this.varMap.set(stmt.name, varName)
|
|
865
896
|
|
|
866
897
|
// Track variable type
|
|
@@ -1196,7 +1227,7 @@ export class Lowering {
|
|
|
1196
1227
|
}
|
|
1197
1228
|
|
|
1198
1229
|
private lowerForRangeStmt(stmt: Extract<Stmt, { kind: 'for_range' }>): void {
|
|
1199
|
-
const loopVar =
|
|
1230
|
+
const loopVar = this.fnVar(stmt.varName)
|
|
1200
1231
|
const subFnName = `${this.currentFn}/__for_${this.foreachCounter++}`
|
|
1201
1232
|
|
|
1202
1233
|
// Initialize loop variable
|
|
@@ -1385,7 +1416,7 @@ export class Lowering {
|
|
|
1385
1416
|
}
|
|
1386
1417
|
|
|
1387
1418
|
const arrayType = this.inferExprType(stmt.iterable)
|
|
1388
|
-
const bindingVar =
|
|
1419
|
+
const bindingVar = this.fnVar(stmt.binding)
|
|
1389
1420
|
const indexVar = this.builder.freshTemp()
|
|
1390
1421
|
const lengthVar = this.builder.freshTemp()
|
|
1391
1422
|
const condVar = this.builder.freshTemp()
|
|
@@ -2543,6 +2574,61 @@ export class Lowering {
|
|
|
2543
2574
|
return { kind: 'var', name: dst }
|
|
2544
2575
|
}
|
|
2545
2576
|
|
|
2577
|
+
// storage_get_int(storage_ns, array_key, index) -> int
|
|
2578
|
+
// Reads one element from an NBT int-array stored in data storage.
|
|
2579
|
+
// storage_ns : e.g. "math:tables"
|
|
2580
|
+
// array_key : e.g. "sin"
|
|
2581
|
+
// index : integer index (const or runtime)
|
|
2582
|
+
//
|
|
2583
|
+
// Const index: execute store result score $dst rs run data get storage math:tables sin[N] 1
|
|
2584
|
+
// Runtime index: macro sub-function via rs:heap, mirrors readArrayElement.
|
|
2585
|
+
if (name === 'storage_get_int') {
|
|
2586
|
+
const storageNs = this.exprToString(args[0]) // "math:tables"
|
|
2587
|
+
const arrayKey = this.exprToString(args[1]) // "sin"
|
|
2588
|
+
const indexOperand = this.lowerExpr(args[2])
|
|
2589
|
+
const dst = this.builder.freshTemp()
|
|
2590
|
+
|
|
2591
|
+
if (indexOperand.kind === 'const') {
|
|
2592
|
+
this.builder.emitRaw(
|
|
2593
|
+
`execute store result score ${dst} rs run data get storage ${storageNs} ${arrayKey}[${indexOperand.value}] 1`
|
|
2594
|
+
)
|
|
2595
|
+
} else {
|
|
2596
|
+
// Runtime index: store the index into rs:heap under a unique key,
|
|
2597
|
+
// then call a macro sub-function that uses $(key) to index the array.
|
|
2598
|
+
const macroKey = `__sgi_${this.foreachCounter++}`
|
|
2599
|
+
const subFnName = `${this.currentFn}/__sgi_${this.foreachCounter++}`
|
|
2600
|
+
const indexVar = indexOperand.kind === 'var'
|
|
2601
|
+
? indexOperand.name
|
|
2602
|
+
: this.operandToVar(indexOperand)
|
|
2603
|
+
this.builder.emitRaw(
|
|
2604
|
+
`execute store result storage rs:heap ${macroKey} int 1 run scoreboard players get ${indexVar} rs`
|
|
2605
|
+
)
|
|
2606
|
+
this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`)
|
|
2607
|
+
// Prefix \x01 is a sentinel for the MC macro '$' line-start marker.
|
|
2608
|
+
// We avoid using literal '$execute' here so the pre-alloc pass
|
|
2609
|
+
// doesn't mistakenly register 'execute' as a scoreboard variable.
|
|
2610
|
+
// Codegen replaces \x01 → '$' when emitting the mc function file.
|
|
2611
|
+
this.emitRawSubFunction(
|
|
2612
|
+
subFnName,
|
|
2613
|
+
`\x01execute store result score ${dst} rs run data get storage ${storageNs} ${arrayKey}[$(${macroKey})] 1`
|
|
2614
|
+
)
|
|
2615
|
+
}
|
|
2616
|
+
return { kind: 'var', name: dst }
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// storage_set_array(storage_ns, array_key, nbt_array_literal)
|
|
2620
|
+
// Writes a literal NBT int array to data storage (used in @load for tables).
|
|
2621
|
+
// storage_set_array("math:tables", "sin", "[0, 17, 35, ...]")
|
|
2622
|
+
if (name === 'storage_set_array') {
|
|
2623
|
+
const storageNs = this.exprToString(args[0])
|
|
2624
|
+
const arrayKey = this.exprToString(args[1])
|
|
2625
|
+
const nbtLiteral = this.exprToString(args[2])
|
|
2626
|
+
this.builder.emitRaw(
|
|
2627
|
+
`data modify storage ${storageNs} ${arrayKey} set value ${nbtLiteral}`
|
|
2628
|
+
)
|
|
2629
|
+
return { kind: 'const', value: 0 }
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2546
2632
|
// data_merge(target, nbt) — merge NBT into entity/block/storage
|
|
2547
2633
|
// data_merge(@s, { Invisible: 1b, Silent: 1b })
|
|
2548
2634
|
if (name === 'data_merge') {
|
|
@@ -3447,7 +3533,7 @@ export class Lowering {
|
|
|
3447
3533
|
this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`)
|
|
3448
3534
|
this.emitRawSubFunction(
|
|
3449
3535
|
subFnName,
|
|
3450
|
-
|
|
3536
|
+
`\x01execute store result score ${dst} rs run data get storage rs:heap ${arrayName}[$(${macroKey})]`
|
|
3451
3537
|
)
|
|
3452
3538
|
return { kind: 'var', name: dst }
|
|
3453
3539
|
}
|