redscript-mc 1.2.0 → 1.2.2

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 (55) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/dce.test.d.ts +1 -0
  5. package/dist/__tests__/dce.test.js +137 -0
  6. package/dist/__tests__/lexer.test.js +19 -2
  7. package/dist/__tests__/lowering.test.js +8 -0
  8. package/dist/__tests__/mc-syntax.test.js +12 -0
  9. package/dist/__tests__/parser.test.js +10 -0
  10. package/dist/__tests__/runtime.test.js +13 -0
  11. package/dist/__tests__/typechecker.test.js +30 -0
  12. package/dist/ast/types.d.ts +22 -2
  13. package/dist/cli.js +15 -10
  14. package/dist/codegen/structure/index.d.ts +4 -1
  15. package/dist/codegen/structure/index.js +4 -2
  16. package/dist/compile.d.ts +1 -0
  17. package/dist/compile.js +4 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +4 -1
  20. package/dist/lexer/index.d.ts +2 -1
  21. package/dist/lexer/index.js +89 -1
  22. package/dist/lowering/index.js +37 -1
  23. package/dist/optimizer/dce.d.ts +23 -0
  24. package/dist/optimizer/dce.js +592 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +81 -16
  27. package/dist/typechecker/index.d.ts +2 -0
  28. package/dist/typechecker/index.js +49 -0
  29. package/docs/ARCHITECTURE.zh.md +1088 -0
  30. package/editors/vscode/.vscodeignore +3 -0
  31. package/editors/vscode/icon.png +0 -0
  32. package/editors/vscode/out/extension.js +834 -19
  33. package/editors/vscode/package-lock.json +2 -2
  34. package/editors/vscode/package.json +1 -1
  35. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  36. package/examples/spiral.mcrs +41 -0
  37. package/logo.png +0 -0
  38. package/package.json +1 -1
  39. package/src/__tests__/dce.test.ts +129 -0
  40. package/src/__tests__/lexer.test.ts +21 -2
  41. package/src/__tests__/lowering.test.ts +9 -0
  42. package/src/__tests__/mc-syntax.test.ts +14 -0
  43. package/src/__tests__/parser.test.ts +11 -0
  44. package/src/__tests__/runtime.test.ts +16 -0
  45. package/src/__tests__/typechecker.test.ts +33 -0
  46. package/src/ast/types.ts +14 -1
  47. package/src/cli.ts +24 -10
  48. package/src/codegen/structure/index.ts +13 -2
  49. package/src/compile.ts +5 -1
  50. package/src/index.ts +5 -1
  51. package/src/lexer/index.ts +102 -1
  52. package/src/lowering/index.ts +38 -2
  53. package/src/optimizer/dce.ts +619 -0
  54. package/src/parser/index.ts +97 -17
  55. package/src/typechecker/index.ts +65 -0
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "1.0.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": "1.0.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": "1.0.0",
5
+ "version": "1.0.4",
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,41 @@
1
+ // ===== Simple Particle Demo =====
2
+ // 展示: @tick, 状态管理, f-strings, 控制命令
3
+
4
+ // 状态
5
+ let counter: int = 0;
6
+ let running: bool = false;
7
+
8
+ // ===== 主循环 =====
9
+ @tick fn demo_tick() {
10
+ if (!running) { return; }
11
+
12
+ // 每 tick 增加计数器
13
+ counter = counter + 1;
14
+
15
+ // 在玩家位置生成粒子
16
+ particle("minecraft:end_rod", ~0, ~1, ~0, 0.5, 0.5, 0.5, 0.1, 5);
17
+
18
+ // 每 20 ticks (1秒) 报告一次
19
+ if (counter % 20 == 0) {
20
+ say(f"Running for {counter} ticks");
21
+ }
22
+ }
23
+
24
+ // ===== 控制命令 =====
25
+ // @keep 防止 DCE 删除
26
+ @keep fn start() {
27
+ running = true;
28
+ counter = 0;
29
+ say(f"Demo started!");
30
+ }
31
+
32
+ @keep fn stop() {
33
+ running = false;
34
+ say(f"Demo stopped at {counter} ticks.");
35
+ }
36
+
37
+ @keep fn reset() {
38
+ running = false;
39
+ counter = 0;
40
+ say(f"Demo reset.");
41
+ }
package/logo.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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
+ })
@@ -88,6 +88,14 @@ describe('Lexer', () => {
88
88
  ])
89
89
  })
90
90
 
91
+ it('tokenizes f-strings as a dedicated token', () => {
92
+ const tokens = tokenize('f"Hello {name}!"')
93
+ expect(tokens.map(t => [t.kind, t.value])).toEqual([
94
+ ['f_string', 'Hello {name}!'],
95
+ ['eof', ''],
96
+ ])
97
+ })
98
+
91
99
  it('tokenizes byte literals (b suffix)', () => {
92
100
  const tokens = tokenize('20b 0B 127b')
93
101
  expect(tokens.map(t => [t.kind, t.value])).toEqual([
@@ -170,8 +178,19 @@ describe('Lexer', () => {
170
178
 
171
179
  describe('operators', () => {
172
180
  it('tokenizes arithmetic operators', () => {
173
- const tokens = tokenize('+ - * / % ~ ^')
174
- expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', '~', '^', 'eof'])
181
+ const tokens = tokenize('+ - * / %')
182
+ expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', 'eof'])
183
+ })
184
+
185
+ it('tokenizes relative and local coordinates', () => {
186
+ const tokens = tokenize('~ ~5 ~-3 ^ ^10 ^-2')
187
+ expect(kinds(tokens)).toEqual(['rel_coord', 'rel_coord', 'rel_coord', 'local_coord', 'local_coord', 'local_coord', 'eof'])
188
+ expect(tokens[0].value).toBe('~')
189
+ expect(tokens[1].value).toBe('~5')
190
+ expect(tokens[2].value).toBe('~-3')
191
+ expect(tokens[3].value).toBe('^')
192
+ expect(tokens[4].value).toBe('^10')
193
+ expect(tokens[5].value).toBe('^-2')
175
194
  })
176
195
 
177
196
  it('tokenizes comparison operators', () => {
@@ -582,6 +582,15 @@ fn choose(dir: Direction) {
582
582
  expect(rawCmds).toContain('tellraw @a ["",{"text":"You have "},{"score":{"name":"$score","objective":"rs"}},{"text":" points"}]')
583
583
  })
584
584
 
585
+ it('lowers f-string output builtins to tellraw/title JSON components', () => {
586
+ const ir = compile('fn test() { let score: int = 7; say(f"Score: {score}"); tellraw(@a, f"Score: {score}"); actionbar(@s, f"Score: {score}"); title(@s, f"Score: {score}"); }')
587
+ const fn = getFunction(ir, 'test')!
588
+ const rawCmds = getRawCommands(fn)
589
+ expect(rawCmds).toContain('tellraw @a ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
590
+ expect(rawCmds).toContain('title @s actionbar ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
591
+ expect(rawCmds).toContain('title @s title ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]')
592
+ })
593
+
585
594
  it('lowers summon()', () => {
586
595
  const ir = compile('fn test() { summon("zombie"); }')
587
596
  const fn = getFunction(ir, 'test')!
@@ -62,6 +62,20 @@ fn chat() {
62
62
  expect(errors).toHaveLength(0)
63
63
  })
64
64
 
65
+ test('f-strings generate valid tellraw/title commands', () => {
66
+ const errors = validateSource(validator, `
67
+ fn chat() {
68
+ let score: int = 7;
69
+ say(f"You have {score} points");
70
+ tellraw(@a, f"Score: {score}");
71
+ actionbar(@s, f"Score: {score}");
72
+ title(@s, f"Score: {score}");
73
+ }
74
+ `, 'f-string')
75
+
76
+ expect(errors).toHaveLength(0)
77
+ })
78
+
65
79
  test('array operations generate valid data commands', () => {
66
80
  const errors = validateSource(validator, `
67
81
  fn arrays() {
@@ -481,6 +481,17 @@ impl Point {
481
481
  })
482
482
  })
483
483
 
484
+ it('parses f-string literal', () => {
485
+ const expr = parseExpr('f"Score: {x}"')
486
+ expect(expr).toEqual({
487
+ kind: 'f_string',
488
+ parts: [
489
+ { kind: 'text', value: 'Score: ' },
490
+ { kind: 'expr', expr: { kind: 'ident', name: 'x' } },
491
+ ],
492
+ })
493
+ })
494
+
484
495
  it('parses boolean literals', () => {
485
496
  expect(parseExpr('true')).toEqual({ kind: 'bool_lit', value: true })
486
497
  expect(parseExpr('false')).toEqual({ kind: 'bool_lit', value: false })
@@ -97,6 +97,22 @@ fn chat() {
97
97
  ])
98
98
  })
99
99
 
100
+ it('renders f-strings through tellraw score components', () => {
101
+ const runtime = loadCompiledProgram(`
102
+ fn chat() {
103
+ let score: int = 7;
104
+ say(f"You have {score} points");
105
+ }
106
+ `)
107
+
108
+ runtime.load()
109
+ runtime.execFunction('chat')
110
+
111
+ expect(runtime.getChatLog()).toEqual([
112
+ 'You have 7 points',
113
+ ])
114
+ })
115
+
100
116
  it('kills only entities matched by a foreach selector', () => {
101
117
  const runtime = loadCompiledProgram(`
102
118
  fn purge_zombies() {
@@ -120,6 +120,39 @@ fn test() {
120
120
  expect(errors).toHaveLength(0)
121
121
  })
122
122
 
123
+ it('allows f-strings in runtime output builtins', () => {
124
+ const errors = typeCheck(`
125
+ fn test() {
126
+ let score: int = 5;
127
+ say(f"Score: {score}");
128
+ tellraw(@a, f"Score: {score}");
129
+ actionbar(@s, f"Score: {score}");
130
+ title(@s, f"Score: {score}");
131
+ }
132
+ `)
133
+ expect(errors).toHaveLength(0)
134
+ })
135
+
136
+ it('rejects f-strings outside runtime output builtins', () => {
137
+ const errors = typeCheck(`
138
+ fn test() {
139
+ let msg: string = f"Score";
140
+ }
141
+ `)
142
+ expect(errors.length).toBeGreaterThan(0)
143
+ expect(errors[0].message).toContain('expected string, got format_string')
144
+ })
145
+
146
+ it('rejects unsupported f-string placeholder types', () => {
147
+ const errors = typeCheck(`
148
+ fn test() {
149
+ say(f"Flag: {true}");
150
+ }
151
+ `)
152
+ expect(errors.length).toBeGreaterThan(0)
153
+ expect(errors[0].message).toContain('f-string placeholder must be int or string')
154
+ })
155
+
123
156
  it('detects too many arguments', () => {
124
157
  const errors = typeCheck(`
125
158
  fn greet() {
package/src/ast/types.ts CHANGED
@@ -22,7 +22,7 @@ export interface Span {
22
22
  // Type Nodes
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
- export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double'
25
+ export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double' | 'format_string'
26
26
 
27
27
  // Entity type hierarchy
28
28
  export type EntityTypeName =
@@ -57,6 +57,16 @@ export interface LambdaExpr {
57
57
  body: Expr | Block
58
58
  }
59
59
 
60
+ export type FStringPart =
61
+ | { kind: 'text'; value: string }
62
+ | { kind: 'expr'; expr: Expr }
63
+
64
+ export interface FStringExpr {
65
+ kind: 'f_string'
66
+ parts: FStringPart[]
67
+ span?: Span
68
+ }
69
+
60
70
  // ---------------------------------------------------------------------------
61
71
  // Range Expression
62
72
  // ---------------------------------------------------------------------------
@@ -129,10 +139,13 @@ export type Expr =
129
139
  | { kind: 'short_lit'; value: number; span?: Span }
130
140
  | { kind: 'long_lit'; value: number; span?: Span }
131
141
  | { kind: 'double_lit'; value: number; span?: Span }
142
+ | { kind: 'rel_coord'; value: string; span?: Span } // ~ ~5 ~-3 (relative coordinate)
143
+ | { kind: 'local_coord'; value: string; span?: Span } // ^ ^5 ^-3 (local/facing coordinate)
132
144
  | { kind: 'bool_lit'; value: boolean; span?: Span }
133
145
  | { kind: 'str_lit'; value: string; span?: Span }
134
146
  | { kind: 'mc_name'; value: string; span?: Span } // #health → "health" (MC identifier)
135
147
  | { kind: 'str_interp'; parts: Array<string | Expr>; span?: Span }
148
+ | FStringExpr
136
149
  | { kind: 'range_lit'; range: RangeExpr; span?: Span }
137
150
  | (BlockPosExpr & { span?: Span })
138
151
  | { kind: 'ident'; name: string; span?: Span }
package/src/cli.ts CHANGED
@@ -26,7 +26,7 @@ function printUsage(): void {
26
26
  RedScript Compiler
27
27
 
28
28
  Usage:
29
- redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
29
+ redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>] [--no-dce]
30
30
  redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
31
31
  redscript check <file>
32
32
  redscript fmt <file.mcrs> [file2.mcrs ...]
@@ -46,6 +46,7 @@ Options:
46
46
  --output-nbt <file> Output .nbt file path for structure target
47
47
  --namespace <ns> Datapack namespace (default: derived from filename)
48
48
  --target <target> Output target: datapack (default), cmdblock, or structure
49
+ --no-dce Disable AST dead code elimination
49
50
  --stats Print optimizer statistics
50
51
  --hot-reload <url> After each successful compile, POST to <url>/reload
51
52
  (use with redscript-testharness; e.g. http://localhost:25561)
@@ -78,8 +79,9 @@ function parseArgs(args: string[]): {
78
79
  stats?: boolean
79
80
  help?: boolean
80
81
  hotReload?: string
82
+ dce?: boolean
81
83
  } {
82
- const result: ReturnType<typeof parseArgs> = {}
84
+ const result: ReturnType<typeof parseArgs> = { dce: true }
83
85
  let i = 0
84
86
 
85
87
  while (i < args.length) {
@@ -103,6 +105,9 @@ function parseArgs(args: string[]): {
103
105
  } else if (arg === '--stats') {
104
106
  result.stats = true
105
107
  i++
108
+ } else if (arg === '--no-dce') {
109
+ result.dce = false
110
+ i++
106
111
  } else if (arg === '--hot-reload') {
107
112
  result.hotReload = args[++i]
108
113
  i++
@@ -153,7 +158,14 @@ function printOptimizationStats(stats: OptimizationStats | undefined): void {
153
158
  console.log(` Total mcfunction commands: ${stats.totalCommandsBefore} -> ${stats.totalCommandsAfter} (${formatReduction(stats.totalCommandsBefore, stats.totalCommandsAfter)} reduction)`)
154
159
  }
155
160
 
156
- function compileCommand(file: string, output: string, namespace: string, target: string = 'datapack', showStats = false): void {
161
+ function compileCommand(
162
+ file: string,
163
+ output: string,
164
+ namespace: string,
165
+ target: string = 'datapack',
166
+ showStats = false,
167
+ dce = true
168
+ ): void {
157
169
  // Read source file
158
170
  if (!fs.existsSync(file)) {
159
171
  console.error(`Error: File not found: ${file}`)
@@ -164,7 +176,7 @@ function compileCommand(file: string, output: string, namespace: string, target:
164
176
 
165
177
  try {
166
178
  if (target === 'cmdblock') {
167
- const result = compile(source, { namespace, filePath: file })
179
+ const result = compile(source, { namespace, filePath: file, dce })
168
180
  printWarnings(result.warnings)
169
181
 
170
182
  // Generate command block JSON
@@ -184,7 +196,7 @@ function compileCommand(file: string, output: string, namespace: string, target:
184
196
  printOptimizationStats(result.stats)
185
197
  }
186
198
  } else if (target === 'structure') {
187
- const structure = compileToStructure(source, namespace, file)
199
+ const structure = compileToStructure(source, namespace, file, { dce })
188
200
  fs.mkdirSync(path.dirname(output), { recursive: true })
189
201
  fs.writeFileSync(output, structure.buffer)
190
202
 
@@ -195,7 +207,7 @@ function compileCommand(file: string, output: string, namespace: string, target:
195
207
  printOptimizationStats(structure.stats)
196
208
  }
197
209
  } else {
198
- const result = compile(source, { namespace, filePath: file })
210
+ const result = compile(source, { namespace, filePath: file, dce })
199
211
  printWarnings(result.warnings)
200
212
 
201
213
  // Default: generate datapack
@@ -255,7 +267,7 @@ async function hotReload(url: string): Promise<void> {
255
267
  }
256
268
  }
257
269
 
258
- function watchCommand(dir: string, output: string, namespace?: string, hotReloadUrl?: string): void {
270
+ function watchCommand(dir: string, output: string, namespace?: string, hotReloadUrl?: string, dce = true): void {
259
271
  // Check if directory exists
260
272
  if (!fs.existsSync(dir)) {
261
273
  console.error(`Error: Directory not found: ${dir}`)
@@ -290,7 +302,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
290
302
  try {
291
303
  source = fs.readFileSync(file, 'utf-8')
292
304
  const ns = namespace ?? deriveNamespace(file)
293
- const result = compile(source, { namespace: ns, filePath: file })
305
+ const result = compile(source, { namespace: ns, filePath: file, dce })
294
306
  printWarnings(result.warnings)
295
307
 
296
308
  // Create output directory
@@ -382,7 +394,8 @@ async function main(): Promise<void> {
382
394
  output,
383
395
  namespace,
384
396
  target,
385
- parsed.stats
397
+ parsed.stats,
398
+ parsed.dce
386
399
  )
387
400
  }
388
401
  break
@@ -397,7 +410,8 @@ async function main(): Promise<void> {
397
410
  parsed.file,
398
411
  parsed.output ?? './dist',
399
412
  parsed.namespace,
400
- parsed.hotReload
413
+ parsed.hotReload,
414
+ parsed.dce
401
415
  )
402
416
  break
403
417
 
@@ -5,6 +5,7 @@ import { nbt, TagType, writeNbt, type CompoundTag, type NbtTag } from '../../nbt
5
5
  import { createEmptyOptimizationStats, mergeOptimizationStats, type OptimizationStats } from '../../optimizer/commands'
6
6
  import { optimizeWithStats } from '../../optimizer/passes'
7
7
  import { optimizeForStructure, optimizeForStructureWithStats } from '../../optimizer/structure'
8
+ import { eliminateDeadCode } from '../../optimizer/dce'
8
9
  import { preprocessSource } from '../../compile'
9
10
  import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
10
11
  import type { DatapackFile } from '../mcfunction'
@@ -51,6 +52,10 @@ export interface StructureCompileResult {
51
52
  stats?: OptimizationStats
52
53
  }
53
54
 
55
+ export interface StructureCompileOptions {
56
+ dce?: boolean
57
+ }
58
+
54
59
  function escapeJsonString(value: string): string {
55
60
  return JSON.stringify(value).slice(1, -1)
56
61
  }
@@ -315,10 +320,16 @@ export function generateStructure(input: IRModule | DatapackFile[]): StructureCo
315
320
  }
316
321
  }
317
322
 
318
- export function compileToStructure(source: string, namespace: string, filePath?: string): StructureCompileResult {
323
+ export function compileToStructure(
324
+ source: string,
325
+ namespace: string,
326
+ filePath?: string,
327
+ options: StructureCompileOptions = {}
328
+ ): StructureCompileResult {
319
329
  const preprocessedSource = preprocessSource(source, { filePath })
320
330
  const tokens = new Lexer(preprocessedSource, filePath).tokenize()
321
- const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
331
+ const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
332
+ const ast = options.dce ?? true ? eliminateDeadCode(parsedAst) : parsedAst
322
333
  const ir = new Lowering(namespace).lower(ast)
323
334
  const stats = createEmptyOptimizationStats()
324
335
  const optimizedIRFunctions = ir.functions.map(fn => {
package/src/compile.ts CHANGED
@@ -11,6 +11,7 @@ import { Lexer } from './lexer'
11
11
  import { Parser } from './parser'
12
12
  import { Lowering } from './lowering'
13
13
  import { optimize } from './optimizer/passes'
14
+ import { eliminateDeadCode } from './optimizer/dce'
14
15
  import { generateDatapackWithStats, DatapackFile } from './codegen/mcfunction'
15
16
  import { DiagnosticError, formatError, parseErrorMessage } from './diagnostics'
16
17
  import type { IRModule } from './ir/types'
@@ -24,6 +25,7 @@ export interface CompileOptions {
24
25
  namespace?: string
25
26
  filePath?: string
26
27
  optimize?: boolean
28
+ dce?: boolean
27
29
  }
28
30
 
29
31
  // ---------------------------------------------------------------------------
@@ -160,6 +162,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
160
162
 
161
163
  export function compile(source: string, options: CompileOptions = {}): CompileResult {
162
164
  const { namespace = 'redscript', filePath, optimize: shouldOptimize = true } = options
165
+ const shouldRunDce = options.dce ?? shouldOptimize
163
166
  let sourceLines = source.split('\n')
164
167
 
165
168
  try {
@@ -171,7 +174,8 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
171
174
  const tokens = new Lexer(preprocessedSource, filePath).tokenize()
172
175
 
173
176
  // Parsing
174
- const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
177
+ const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
178
+ const ast = shouldRunDce ? eliminateDeadCode(parsedAst) : parsedAst
175
179
 
176
180
  // Lowering
177
181
  const ir = new Lowering(namespace, preprocessed.ranges).lower(ast)
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  copyPropagation,
15
15
  deadCodeEliminationWithStats,
16
16
  } from './optimizer/passes'
17
+ import { eliminateDeadCode } from './optimizer/dce'
17
18
  import {
18
19
  countMcfunctionCommands,
19
20
  generateDatapackWithStats,
@@ -30,6 +31,7 @@ export interface CompileOptions {
30
31
  optimize?: boolean
31
32
  typeCheck?: boolean
32
33
  filePath?: string
34
+ dce?: boolean
33
35
  }
34
36
 
35
37
  export interface CompileResult {
@@ -53,6 +55,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
53
55
  const namespace = options.namespace ?? 'redscript'
54
56
  const shouldOptimize = options.optimize ?? true
55
57
  const shouldTypeCheck = options.typeCheck ?? true
58
+ const shouldRunDce = options.dce ?? shouldOptimize
56
59
  const filePath = options.filePath
57
60
  const preprocessed = preprocessSourceWithMetadata(source, { filePath })
58
61
  const preprocessedSource = preprocessed.source
@@ -61,7 +64,8 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
61
64
  const tokens = new Lexer(preprocessedSource, filePath).tokenize()
62
65
 
63
66
  // Parsing
64
- const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
67
+ const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
68
+ const ast = shouldRunDce ? eliminateDeadCode(parsedAst) : parsedAst
65
69
 
66
70
  // Type checking (warn mode - collect errors but don't block)
67
71
  let typeErrors: DiagnosticError[] | undefined