redscript-mc 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/cli.test.js +138 -0
  5. package/dist/__tests__/codegen.test.js +25 -0
  6. package/dist/__tests__/dce.test.d.ts +1 -0
  7. package/dist/__tests__/dce.test.js +137 -0
  8. package/dist/__tests__/e2e.test.js +190 -12
  9. package/dist/__tests__/lexer.test.js +31 -4
  10. package/dist/__tests__/lowering.test.js +172 -9
  11. package/dist/__tests__/mc-integration.test.js +145 -51
  12. package/dist/__tests__/mc-syntax.test.js +12 -0
  13. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  14. package/dist/__tests__/parser.test.js +90 -0
  15. package/dist/__tests__/runtime.test.js +21 -8
  16. package/dist/__tests__/typechecker.test.js +188 -0
  17. package/dist/ast/types.d.ts +42 -3
  18. package/dist/cli.js +15 -10
  19. package/dist/codegen/mcfunction/index.js +30 -1
  20. package/dist/codegen/structure/index.d.ts +4 -1
  21. package/dist/codegen/structure/index.js +29 -2
  22. package/dist/compile.d.ts +11 -0
  23. package/dist/compile.js +40 -6
  24. package/dist/events/types.d.ts +35 -0
  25. package/dist/events/types.js +59 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -3
  28. package/dist/ir/types.d.ts +4 -0
  29. package/dist/lexer/index.d.ts +2 -1
  30. package/dist/lexer/index.js +91 -1
  31. package/dist/lowering/index.d.ts +32 -1
  32. package/dist/lowering/index.js +476 -16
  33. package/dist/optimizer/dce.d.ts +23 -0
  34. package/dist/optimizer/dce.js +591 -0
  35. package/dist/parser/index.d.ts +4 -0
  36. package/dist/parser/index.js +160 -26
  37. package/dist/typechecker/index.d.ts +19 -0
  38. package/dist/typechecker/index.js +392 -17
  39. package/docs/ARCHITECTURE.zh.md +1088 -0
  40. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  41. package/editors/vscode/.vscodeignore +3 -0
  42. package/editors/vscode/CHANGELOG.md +9 -0
  43. package/editors/vscode/icon.png +0 -0
  44. package/editors/vscode/out/extension.js +1144 -72
  45. package/editors/vscode/package-lock.json +2 -2
  46. package/editors/vscode/package.json +1 -1
  47. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  48. package/examples/spiral.mcrs +79 -0
  49. package/logo.png +0 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/cli.test.ts +166 -0
  52. package/src/__tests__/codegen.test.ts +27 -0
  53. package/src/__tests__/dce.test.ts +129 -0
  54. package/src/__tests__/e2e.test.ts +201 -12
  55. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  56. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  57. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  58. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  59. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  60. package/src/__tests__/lexer.test.ts +35 -4
  61. package/src/__tests__/lowering.test.ts +187 -9
  62. package/src/__tests__/mc-integration.test.ts +166 -51
  63. package/src/__tests__/mc-syntax.test.ts +14 -0
  64. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  65. package/src/__tests__/parser.test.ts +102 -5
  66. package/src/__tests__/runtime.test.ts +24 -8
  67. package/src/__tests__/typechecker.test.ts +204 -0
  68. package/src/ast/types.ts +39 -2
  69. package/src/cli.ts +24 -10
  70. package/src/codegen/mcfunction/index.ts +31 -1
  71. package/src/codegen/structure/index.ts +40 -2
  72. package/src/compile.ts +59 -7
  73. package/src/events/types.ts +69 -0
  74. package/src/index.ts +9 -4
  75. package/src/ir/types.ts +4 -0
  76. package/src/lexer/index.ts +105 -2
  77. package/src/lowering/index.ts +566 -18
  78. package/src/optimizer/dce.ts +618 -0
  79. package/src/parser/index.ts +187 -29
  80. package/src/stdlib/README.md +34 -4
  81. package/src/stdlib/tags.mcrs +951 -0
  82. package/src/stdlib/timer.mcrs +54 -33
  83. package/src/typechecker/index.ts +469 -18
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "0.2.0",
3
+ "version": "1.0.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "0.2.0",
9
+ "version": "1.0.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../"
@@ -2,7 +2,7 @@
2
2
  "name": "redscript-vscode",
3
3
  "displayName": "RedScript for Minecraft",
4
4
  "description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
5
- "version": "0.8.2",
5
+ "version": "1.0.3",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
@@ -133,11 +133,11 @@
133
133
  "patterns": [
134
134
  {
135
135
  "name": "keyword.control.redscript",
136
- "match": "\\b(if|else|while|for|foreach|return|match|in|execute|as|at|unless|run)\\b"
136
+ "match": "\\b(if|else|while|for|foreach|return|match|in|execute|as|at|unless|run|is)\\b"
137
137
  },
138
138
  {
139
139
  "name": "keyword.declaration.redscript",
140
- "match": "\\b(fn|let|struct|enum|import|namespace|trigger)\\b"
140
+ "match": "\\b(fn|let|struct|enum|impl|import|namespace|trigger)\\b"
141
141
  },
142
142
  {
143
143
  "name": "keyword.declaration.const.redscript",
@@ -146,6 +146,10 @@
146
146
  {
147
147
  "name": "constant.language.boolean.redscript",
148
148
  "match": "\\b(true|false)\\b"
149
+ },
150
+ {
151
+ "name": "variable.language.self.redscript",
152
+ "match": "\\bself\\b"
149
153
  }
150
154
  ]
151
155
  },
@@ -0,0 +1,79 @@
1
+ // ===== Spiral Particle Tower =====
2
+ // 简单但视觉效果很酷的 demo
3
+ // 展示: @tick, impl blocks, 三角函数近似, 粒子效果
4
+
5
+ // 配置
6
+ const RADIUS: int = 5; // 螺旋半径
7
+ const HEIGHT: int = 20; // 塔高度
8
+ const SPEED: int = 3; // 旋转速度
9
+
10
+ // 状态
11
+ let angle: int = 0; // 当前角度 (0-360)
12
+ let height: int = 0; // 当前高度
13
+ let running: bool = false;
14
+
15
+ // ===== 简化的三角函数 (查表法) =====
16
+ // sin 和 cos 的近似值 * 100 (避免浮点)
17
+
18
+ fn sin100(deg: int): int {
19
+ let d: int = deg % 360;
20
+ if (d < 0) { d = d + 360; }
21
+
22
+ // 简化: 只处理 0, 90, 180, 270
23
+ if (d < 45) { return d * 2; } // ~0 到 ~90
24
+ if (d < 135) { return 100; } // ~90
25
+ if (d < 225) { return (180 - d) * 2; } // ~90 到 ~-90
26
+ if (d < 315) { return -100; } // ~-90
27
+ return (d - 360) * 2; // ~-90 到 ~0
28
+ }
29
+
30
+ fn cos100(deg: int): int {
31
+ return sin100(deg + 90);
32
+ }
33
+
34
+ // ===== 粒子生成 =====
35
+ fn spawn_particle(x: int, y: int, z: int) {
36
+ // x, y, z 是 *100 的值,需要转换
37
+ raw("particle end_rod ~${x/100} ~${y/100} ~${z/100} 0 0 0 0 1");
38
+ }
39
+
40
+ // ===== 主循环 =====
41
+ @tick fn spiral_tick() {
42
+ if (!running) { return; }
43
+
44
+ // 计算螺旋位置
45
+ let x: int = cos100(angle) * RADIUS; // x * 100
46
+ let z: int = sin100(angle) * RADIUS; // z * 100
47
+ let y: int = height * 100;
48
+
49
+ // 生成粒子
50
+ spawn_particle(x, y, z);
51
+
52
+ // 更新状态
53
+ angle = angle + SPEED;
54
+ if (angle >= 360) {
55
+ angle = 0;
56
+ height = height + 1;
57
+ if (height >= HEIGHT) {
58
+ height = 0;
59
+ }
60
+ }
61
+ }
62
+
63
+ // ===== 控制命令 =====
64
+ fn start() {
65
+ running = true;
66
+ say(f"Spiral started!");
67
+ }
68
+
69
+ fn stop() {
70
+ running = false;
71
+ say(f"Spiral stopped.");
72
+ }
73
+
74
+ fn reset() {
75
+ running = false;
76
+ angle = 0;
77
+ height = 0;
78
+ say(f"Spiral reset.");
79
+ }
package/logo.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -40,6 +40,172 @@ describe('CLI API', () => {
40
40
  expect(result.ir.functions.filter(fn => fn.name === 'from_a')).toHaveLength(1)
41
41
  expect(result.ir.functions.filter(fn => fn.name === 'from_b')).toHaveLength(1)
42
42
  })
43
+
44
+ it('uses rs-prefixed scoreboard objectives for imported stdlib files', () => {
45
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-'))
46
+ const stdlibDir = path.join(tempDir, 'src', 'stdlib')
47
+ const stdlibPath = path.join(stdlibDir, 'timer.mcrs')
48
+ const mainPath = path.join(tempDir, 'main.mcrs')
49
+
50
+ fs.mkdirSync(stdlibDir, { recursive: true })
51
+ fs.writeFileSync(stdlibPath, 'fn tick_timer() { scoreboard_set("#rs", "timer_ticks", 1); }\n')
52
+ fs.writeFileSync(mainPath, 'import "./src/stdlib/timer.mcrs"\n\nfn main() { tick_timer(); }\n')
53
+
54
+ const source = fs.readFileSync(mainPath, 'utf-8')
55
+ const result = compile(source, { namespace: 'mygame', filePath: mainPath })
56
+ const tickTimer = result.files.find(file => file.path.endsWith('/tick_timer.mcfunction'))
57
+
58
+ expect(tickTimer?.content).toContain('scoreboard players set #rs rs.timer_ticks 1')
59
+ })
60
+
61
+ it('adds a call-site hash for stdlib internal scoreboard objectives', () => {
62
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-hash-'))
63
+ const stdlibDir = path.join(tempDir, 'src', 'stdlib')
64
+ const stdlibPath = path.join(stdlibDir, 'timer.mcrs')
65
+ const mainPath = path.join(tempDir, 'main.mcrs')
66
+
67
+ fs.mkdirSync(stdlibDir, { recursive: true })
68
+ fs.writeFileSync(stdlibPath, [
69
+ 'fn timer_start(name: string, duration: int) {',
70
+ ' scoreboard_set("timer_ticks", #rs, duration);',
71
+ ' scoreboard_set("timer_active", #rs, 1);',
72
+ '}',
73
+ '',
74
+ ].join('\n'))
75
+ fs.writeFileSync(mainPath, [
76
+ 'import "./src/stdlib/timer.mcrs"',
77
+ '',
78
+ 'fn main() {',
79
+ ' timer_start("x", 100);',
80
+ ' timer_start("x", 100);',
81
+ '}',
82
+ '',
83
+ ].join('\n'))
84
+
85
+ const source = fs.readFileSync(mainPath, 'utf-8')
86
+ const result = compile(source, { namespace: 'mygame', filePath: mainPath })
87
+ const timerFns = result.files.filter(file => /timer_start__callsite_[0-9a-f]{4}\.mcfunction$/.test(file.path))
88
+
89
+ expect(timerFns).toHaveLength(2)
90
+
91
+ const objectives = timerFns
92
+ .flatMap(file => [...file.content.matchAll(/rs\._timer_([0-9a-f]{4})/g)].map(match => match[0]))
93
+
94
+ expect(new Set(objectives).size).toBe(2)
95
+ })
96
+
97
+ it('Timer::new creates timer', () => {
98
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-new-'))
99
+ const mainPath = path.join(tempDir, 'main.mcrs')
100
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
101
+
102
+ fs.writeFileSync(mainPath, [
103
+ `import "${timerPath}"`,
104
+ '',
105
+ 'fn main() {',
106
+ ' let timer: Timer = Timer::new(20);',
107
+ '}',
108
+ '',
109
+ ].join('\n'))
110
+
111
+ const source = fs.readFileSync(mainPath, 'utf-8')
112
+ const result = compile(source, { namespace: 'timernew', filePath: mainPath })
113
+
114
+ expect(result.typeErrors).toEqual([])
115
+ const newFn = result.files.find(file => file.path.endsWith('/Timer_new.mcfunction'))
116
+ expect(newFn?.content).toContain('scoreboard players set timer_ticks rs 0')
117
+ expect(newFn?.content).toContain('scoreboard players set timer_active rs 0')
118
+ })
119
+
120
+ it('Timer.start/pause/reset', () => {
121
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-state-'))
122
+ const mainPath = path.join(tempDir, 'main.mcrs')
123
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
124
+
125
+ fs.writeFileSync(mainPath, [
126
+ `import "${timerPath}"`,
127
+ '',
128
+ 'fn main() {',
129
+ ' let timer: Timer = Timer::new(20);',
130
+ ' timer.start();',
131
+ ' timer.pause();',
132
+ ' timer.reset();',
133
+ '}',
134
+ '',
135
+ ].join('\n'))
136
+
137
+ const source = fs.readFileSync(mainPath, 'utf-8')
138
+ const result = compile(source, { namespace: 'timerstate', filePath: mainPath })
139
+
140
+ expect(result.typeErrors).toEqual([])
141
+ const startFn = result.files.find(file => file.path.endsWith('/Timer_start.mcfunction'))
142
+ const pauseFn = result.files.find(file => file.path.endsWith('/Timer_pause.mcfunction'))
143
+ const resetFn = result.files.find(file => file.path.endsWith('/Timer_reset.mcfunction'))
144
+
145
+ expect(startFn?.content).toContain('scoreboard players set timer_active rs 1')
146
+ expect(pauseFn?.content).toContain('scoreboard players set timer_active rs 0')
147
+ expect(resetFn?.content).toContain('scoreboard players set timer_ticks rs 0')
148
+ })
149
+
150
+ it('Timer.done returns bool', () => {
151
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-done-'))
152
+ const mainPath = path.join(tempDir, 'main.mcrs')
153
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
154
+
155
+ fs.writeFileSync(mainPath, [
156
+ `import "${timerPath}"`,
157
+ '',
158
+ 'fn main() {',
159
+ ' let timer: Timer = Timer::new(20);',
160
+ ' let finished: bool = timer.done();',
161
+ ' if (finished) {',
162
+ ' say("done");',
163
+ ' }',
164
+ '}',
165
+ '',
166
+ ].join('\n'))
167
+
168
+ const source = fs.readFileSync(mainPath, 'utf-8')
169
+ const result = compile(source, { namespace: 'timerdone', filePath: mainPath })
170
+
171
+ expect(result.typeErrors).toEqual([])
172
+ const doneFn = result.files.find(file => file.path.endsWith('/Timer_done.mcfunction'))
173
+ const mainFn = result.files.find(file => file.path.endsWith('/main.mcfunction'))
174
+ expect(doneFn?.content).toContain('scoreboard players get timer_ticks rs')
175
+ expect(doneFn?.content).toContain('return run scoreboard players get')
176
+ expect(mainFn?.content).toContain('execute if score $finished rs matches 1..')
177
+ })
178
+
179
+ it('Timer.tick increments', () => {
180
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-tick-'))
181
+ const mainPath = path.join(tempDir, 'main.mcrs')
182
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
183
+
184
+ fs.writeFileSync(mainPath, [
185
+ `import "${timerPath}"`,
186
+ '',
187
+ 'fn main() {',
188
+ ' let timer: Timer = Timer::new(20);',
189
+ ' timer.start();',
190
+ ' timer.tick();',
191
+ '}',
192
+ '',
193
+ ].join('\n'))
194
+
195
+ const source = fs.readFileSync(mainPath, 'utf-8')
196
+ const result = compile(source, { namespace: 'timertick', filePath: mainPath })
197
+
198
+ expect(result.typeErrors).toEqual([])
199
+ const tickOutput = result.files
200
+ .filter(file => file.path.includes('/Timer_tick'))
201
+ .map(file => file.content)
202
+ .join('\n')
203
+
204
+ expect(tickOutput).toContain('scoreboard players get timer_active rs')
205
+ expect(tickOutput).toContain('scoreboard players get timer_ticks rs')
206
+ expect(tickOutput).toContain(' += $const_1 rs')
207
+ expect(tickOutput).toContain('execute store result score timer_ticks rs run scoreboard players get $_')
208
+ })
43
209
  })
44
210
 
45
211
  describe('compile()', () => {
@@ -125,4 +125,31 @@ describe('generateDatapack', () => {
125
125
  expect(json.criteria.trigger.trigger).toBe('minecraft:story/mine_diamond')
126
126
  expect(json.rewards.function).toBe('mypack:on_mine_diamond')
127
127
  })
128
+
129
+ it('generates static event dispatcher in __tick', () => {
130
+ const mod: IRModule = {
131
+ namespace: 'mypack',
132
+ globals: [],
133
+ functions: [{
134
+ name: 'handle_death',
135
+ params: [],
136
+ locals: [],
137
+ blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
138
+ eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
139
+ }, {
140
+ name: 'handle_death_2',
141
+ params: [],
142
+ locals: [],
143
+ blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
144
+ eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
145
+ }],
146
+ }
147
+
148
+ const files = generateDatapack(mod)
149
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'))
150
+ expect(tickFn).toBeDefined()
151
+ expect(tickFn!.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death')
152
+ expect(tickFn!.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death_2')
153
+ expect(tickFn!.content).toContain('tag @a[tag=rs.just_died] remove rs.just_died')
154
+ })
128
155
  })
@@ -0,0 +1,129 @@
1
+ import * as fs from 'fs'
2
+ import * as os from 'os'
3
+ import * as path from 'path'
4
+ import { execFileSync } from 'child_process'
5
+
6
+ import { compile } from '../index'
7
+
8
+ function getFileContent(files: ReturnType<typeof compile>['files'], suffix: string): string {
9
+ const file = files.find(candidate => candidate.path.endsWith(suffix))
10
+ if (!file) {
11
+ throw new Error(`Missing file: ${suffix}`)
12
+ }
13
+ return file.content
14
+ }
15
+
16
+ describe('AST dead code elimination', () => {
17
+ it('removes unused functions reachable from entry points', () => {
18
+ const source = `
19
+ fn unused() { say("never called"); }
20
+ fn used() { say("called"); }
21
+ @tick fn main() { used(); }
22
+ `
23
+
24
+ const result = compile(source, { namespace: 'test' })
25
+
26
+ expect(result.ast.declarations.map(fn => fn.name)).toEqual(['used', 'main'])
27
+ expect(result.ir.functions.some(fn => fn.name === 'unused')).toBe(false)
28
+ })
29
+
30
+ it('removes unused local variables from the AST body', () => {
31
+ const source = `
32
+ fn helper() {
33
+ let unused: int = 10;
34
+ let used: int = 20;
35
+ say_int(used);
36
+ }
37
+ @tick fn main() { helper(); }
38
+ `
39
+
40
+ const result = compile(source, { namespace: 'test' })
41
+ const helper = result.ast.declarations.find(fn => fn.name === 'helper')
42
+
43
+ expect(helper?.body.filter(stmt => stmt.kind === 'let')).toHaveLength(1)
44
+ expect(helper?.body.some(stmt => stmt.kind === 'let' && stmt.name === 'unused')).toBe(false)
45
+ })
46
+
47
+ it('removes unused constants', () => {
48
+ const source = `
49
+ const UNUSED: int = 10;
50
+ const USED: int = 20;
51
+
52
+ @tick fn main() {
53
+ say_int(USED);
54
+ }
55
+ `
56
+
57
+ const result = compile(source, { namespace: 'test' })
58
+
59
+ expect(result.ast.consts.map(constDecl => constDecl.name)).toEqual(['USED'])
60
+ })
61
+
62
+ it('eliminates dead branches with constant conditions', () => {
63
+ const source = `
64
+ @tick fn main() {
65
+ if (false) {
66
+ say("dead code");
67
+ } else {
68
+ say("live code");
69
+ }
70
+ }
71
+ `
72
+
73
+ const result = compile(source, { namespace: 'test' })
74
+ const output = getFileContent(result.files, 'data/test/function/main.mcfunction')
75
+
76
+ expect(output).not.toContain('dead code')
77
+ expect(output).toContain('live code')
78
+ })
79
+
80
+ it('keeps decorated entry points', () => {
81
+ const source = `
82
+ @tick fn ticker() { }
83
+ @load fn loader() { }
84
+ @on(PlayerDeath) fn handler(player: Player) { say("event"); }
85
+ `
86
+
87
+ const result = compile(source, { namespace: 'test' })
88
+ const names = result.ast.declarations.map(fn => fn.name)
89
+
90
+ expect(names).toContain('ticker')
91
+ expect(names).toContain('loader')
92
+ expect(names).toContain('handler')
93
+ })
94
+
95
+ it('can disable AST DCE through the compile API', () => {
96
+ const source = `
97
+ fn unused() { say("never called"); }
98
+ @tick fn main() { say("live"); }
99
+ `
100
+
101
+ const result = compile(source, { namespace: 'test', dce: false })
102
+
103
+ expect(result.ast.declarations.map(fn => fn.name)).toEqual(['unused', 'main'])
104
+ expect(result.ir.functions.some(fn => fn.name === 'unused')).toBe(true)
105
+ })
106
+ })
107
+
108
+ describe('CLI --no-dce', () => {
109
+ it('preserves unused functions when requested', () => {
110
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-dce-cli-'))
111
+ const inputPath = path.join(tempDir, 'main.mcrs')
112
+ const outputDir = path.join(tempDir, 'out')
113
+
114
+ fs.writeFileSync(inputPath, [
115
+ 'fn unused() { say("keep me"); }',
116
+ '@tick fn main() { say("live"); }',
117
+ '',
118
+ ].join('\n'))
119
+
120
+ execFileSync(
121
+ process.execPath,
122
+ ['-r', 'ts-node/register', 'src/cli.ts', 'compile', inputPath, '-o', outputDir, '--namespace', 'test', '--no-dce'],
123
+ { cwd: path.resolve(process.cwd()) }
124
+ )
125
+
126
+ const unusedPath = path.join(outputDir, 'data', 'test', 'function', 'unused.mcfunction')
127
+ expect(fs.existsSync(unusedPath)).toBe(true)
128
+ })
129
+ })