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