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.
- package/CHANGELOG.md +5 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/lexer.test.js +19 -2
- package/dist/__tests__/lowering.test.js +8 -0
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/parser.test.js +10 -0
- package/dist/__tests__/runtime.test.js +13 -0
- package/dist/__tests__/typechecker.test.js +30 -0
- package/dist/ast/types.d.ts +22 -2
- package/dist/cli.js +15 -10
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +4 -2
- package/dist/compile.d.ts +1 -0
- package/dist/compile.js +4 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +89 -1
- package/dist/lowering/index.js +37 -1
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +592 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +81 -16
- package/dist/typechecker/index.d.ts +2 -0
- package/dist/typechecker/index.js +49 -0
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/out/extension.js +834 -19
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
- package/examples/spiral.mcrs +41 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/dce.test.ts +129 -0
- package/src/__tests__/lexer.test.ts +21 -2
- package/src/__tests__/lowering.test.ts +9 -0
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/parser.test.ts +11 -0
- package/src/__tests__/runtime.test.ts +16 -0
- package/src/__tests__/typechecker.test.ts +33 -0
- package/src/ast/types.ts +14 -1
- package/src/cli.ts +24 -10
- package/src/codegen/structure/index.ts +13 -2
- package/src/compile.ts +5 -1
- package/src/index.ts +5 -1
- package/src/lexer/index.ts +102 -1
- package/src/lowering/index.ts +38 -2
- package/src/optimizer/dce.ts +619 -0
- package/src/parser/index.ts +97 -17
- package/src/typechecker/index.ts +65 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
|
-
"version": "1.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.
|
|
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.
|
|
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
|
@@ -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(['+', '-', '*', '/', '%', '
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|