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.
- package/CHANGELOG.md +59 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +31 -4
- package/dist/__tests__/lowering.test.js +172 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +90 -0
- package/dist/__tests__/runtime.test.js +21 -8
- package/dist/__tests__/typechecker.test.js +188 -0
- package/dist/ast/types.d.ts +42 -3
- package/dist/cli.js +15 -10
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +29 -2
- package/dist/compile.d.ts +11 -0
- package/dist/compile.js +40 -6
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -3
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +91 -1
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +476 -16
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +591 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +160 -26
- package/dist/typechecker/index.d.ts +19 -0
- package/dist/typechecker/index.js +392 -17
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/out/extension.js +1144 -72
- 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 +79 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/dce.test.ts +129 -0
- package/src/__tests__/e2e.test.ts +201 -12
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +35 -4
- package/src/__tests__/lowering.test.ts +187 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +102 -5
- package/src/__tests__/runtime.test.ts +24 -8
- package/src/__tests__/typechecker.test.ts +204 -0
- package/src/ast/types.ts +39 -2
- package/src/cli.ts +24 -10
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +40 -2
- package/src/compile.ts +59 -7
- package/src/events/types.ts +69 -0
- package/src/index.ts +9 -4
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +105 -2
- package/src/lowering/index.ts +566 -18
- package/src/optimizer/dce.ts +618 -0
- package/src/parser/index.ts +187 -29
- package/src/stdlib/README.md +34 -4
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/timer.mcrs +54 -33
- package/src/typechecker/index.ts +469 -18
|
@@ -25,6 +25,7 @@ describe('Parser', () => {
|
|
|
25
25
|
const program = parse('')
|
|
26
26
|
expect(program.namespace).toBe('test')
|
|
27
27
|
expect(program.declarations).toEqual([])
|
|
28
|
+
expect(program.implBlocks).toEqual([])
|
|
28
29
|
expect(program.enums).toEqual([])
|
|
29
30
|
expect(program.consts).toEqual([])
|
|
30
31
|
})
|
|
@@ -102,6 +103,13 @@ describe('Parser', () => {
|
|
|
102
103
|
{ name: 'on_death' },
|
|
103
104
|
])
|
|
104
105
|
})
|
|
106
|
+
|
|
107
|
+
it('parses @on event decorators', () => {
|
|
108
|
+
const program = parse('@on(PlayerDeath)\nfn handle_death(player: Player) {}')
|
|
109
|
+
expect(program.declarations[0].decorators).toEqual([
|
|
110
|
+
{ name: 'on', args: { eventType: 'PlayerDeath' } },
|
|
111
|
+
])
|
|
112
|
+
})
|
|
105
113
|
})
|
|
106
114
|
|
|
107
115
|
describe('types', () => {
|
|
@@ -151,6 +159,52 @@ describe('Parser', () => {
|
|
|
151
159
|
},
|
|
152
160
|
])
|
|
153
161
|
})
|
|
162
|
+
|
|
163
|
+
it('parses impl blocks', () => {
|
|
164
|
+
const program = parse(`
|
|
165
|
+
struct Timer { duration: int }
|
|
166
|
+
|
|
167
|
+
impl Timer {
|
|
168
|
+
fn new(duration: int): Timer {
|
|
169
|
+
return { duration: duration };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn start(self) {}
|
|
173
|
+
}
|
|
174
|
+
`)
|
|
175
|
+
expect(program.implBlocks).toHaveLength(1)
|
|
176
|
+
expect(program.implBlocks[0].typeName).toBe('Timer')
|
|
177
|
+
expect(program.implBlocks[0].methods.map(method => method.name)).toEqual(['new', 'start'])
|
|
178
|
+
expect(program.implBlocks[0].methods[1].params[0]).toEqual({
|
|
179
|
+
name: 'self',
|
|
180
|
+
type: { kind: 'struct', name: 'Timer' },
|
|
181
|
+
default: undefined,
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('parses impl blocks with static and instance methods', () => {
|
|
186
|
+
const program = parse(`
|
|
187
|
+
struct Point { x: int, y: int }
|
|
188
|
+
|
|
189
|
+
impl Point {
|
|
190
|
+
fn new(x: int, y: int) -> Point {
|
|
191
|
+
return { x: x, y: y };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn distance(self) -> int {
|
|
195
|
+
return self.x + self.y;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
`)
|
|
199
|
+
expect(program.implBlocks).toHaveLength(1)
|
|
200
|
+
expect(program.implBlocks[0].typeName).toBe('Point')
|
|
201
|
+
expect(program.implBlocks[0].methods[0].params.map(param => param.name)).toEqual(['x', 'y'])
|
|
202
|
+
expect(program.implBlocks[0].methods[1].params[0]).toEqual({
|
|
203
|
+
name: 'self',
|
|
204
|
+
type: { kind: 'struct', name: 'Point' },
|
|
205
|
+
default: undefined,
|
|
206
|
+
})
|
|
207
|
+
})
|
|
154
208
|
})
|
|
155
209
|
|
|
156
210
|
describe('statements', () => {
|
|
@@ -197,6 +251,28 @@ describe('Parser', () => {
|
|
|
197
251
|
expect((stmt as any).else_).toHaveLength(1)
|
|
198
252
|
})
|
|
199
253
|
|
|
254
|
+
it('parses entity is-checks in if conditions', () => {
|
|
255
|
+
const stmt = parseStmt('if (e is Player) { kill(@s); }')
|
|
256
|
+
expect(stmt.kind).toBe('if')
|
|
257
|
+
expect((stmt as any).cond).toEqual({
|
|
258
|
+
kind: 'is_check',
|
|
259
|
+
expr: { kind: 'ident', name: 'e' },
|
|
260
|
+
entityType: 'Player',
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('parses entity is-checks inside foreach bodies', () => {
|
|
265
|
+
const stmt = parseStmt('foreach (e in @e) { if (e is Zombie) { kill(e); } }')
|
|
266
|
+
expect(stmt.kind).toBe('foreach')
|
|
267
|
+
const innerIf = (stmt as any).body[0]
|
|
268
|
+
expect(innerIf.kind).toBe('if')
|
|
269
|
+
expect(innerIf.cond).toEqual({
|
|
270
|
+
kind: 'is_check',
|
|
271
|
+
expr: { kind: 'ident', name: 'e' },
|
|
272
|
+
entityType: 'Zombie',
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
200
276
|
it('parses while statement', () => {
|
|
201
277
|
const stmt = parseStmt('while (i > 0) { i = i - 1; }')
|
|
202
278
|
expect(stmt.kind).toBe('while')
|
|
@@ -405,6 +481,17 @@ describe('Parser', () => {
|
|
|
405
481
|
})
|
|
406
482
|
})
|
|
407
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
|
+
|
|
408
495
|
it('parses boolean literals', () => {
|
|
409
496
|
expect(parseExpr('true')).toEqual({ kind: 'bool_lit', value: true })
|
|
410
497
|
expect(parseExpr('false')).toEqual({ kind: 'bool_lit', value: false })
|
|
@@ -459,11 +546,11 @@ describe('Parser', () => {
|
|
|
459
546
|
expect(expr).toEqual({ kind: 'ident', name: 'foo' })
|
|
460
547
|
})
|
|
461
548
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
549
|
+
it('parses function call', () => {
|
|
550
|
+
const expr = parseExpr('foo(1, 2)')
|
|
551
|
+
expect(expr).toEqual({
|
|
552
|
+
kind: 'call',
|
|
553
|
+
fn: 'foo',
|
|
467
554
|
args: [
|
|
468
555
|
{ kind: 'int_lit', value: 1 },
|
|
469
556
|
{ kind: 'int_lit', value: 2 },
|
|
@@ -486,6 +573,16 @@ describe('Parser', () => {
|
|
|
486
573
|
})
|
|
487
574
|
})
|
|
488
575
|
|
|
576
|
+
it('parses static method calls', () => {
|
|
577
|
+
const expr = parseExpr('Timer::new(100)')
|
|
578
|
+
expect(expr).toEqual({
|
|
579
|
+
kind: 'static_call',
|
|
580
|
+
type: 'Timer',
|
|
581
|
+
method: 'new',
|
|
582
|
+
args: [{ kind: 'int_lit', value: 100 }],
|
|
583
|
+
})
|
|
584
|
+
})
|
|
585
|
+
|
|
489
586
|
describe('binary operators', () => {
|
|
490
587
|
it('parses arithmetic', () => {
|
|
491
588
|
const expr = parseExpr('1 + 2')
|
|
@@ -57,7 +57,7 @@ fn compute() {
|
|
|
57
57
|
runtime.load()
|
|
58
58
|
runtime.execFunction('compute')
|
|
59
59
|
|
|
60
|
-
expect(runtime.getScore('math', 'result')).toBe(11)
|
|
60
|
+
expect(runtime.getScore('math', 'runtime.result')).toBe(11)
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
it('captures say, announce, actionbar, and title output in the chat log', () => {
|
|
@@ -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() {
|
|
@@ -135,8 +151,8 @@ fn arrays() {
|
|
|
135
151
|
runtime.load()
|
|
136
152
|
runtime.execFunction('arrays')
|
|
137
153
|
|
|
138
|
-
expect(runtime.getScore('arrays', 'len')).toBe(1)
|
|
139
|
-
expect(runtime.getScore('arrays', 'last')).toBe(9)
|
|
154
|
+
expect(runtime.getScore('arrays', 'runtime.len')).toBe(1)
|
|
155
|
+
expect(runtime.getScore('arrays', 'runtime.last')).toBe(9)
|
|
140
156
|
expect(runtime.getStorage('rs:heap.arr')).toEqual([4])
|
|
141
157
|
})
|
|
142
158
|
|
|
@@ -177,7 +193,7 @@ fn pulse() {
|
|
|
177
193
|
runtime.load()
|
|
178
194
|
runtime.ticks(10)
|
|
179
195
|
|
|
180
|
-
expect(runtime.getScore('pulse', 'count')).toBe(2)
|
|
196
|
+
expect(runtime.getScore('pulse', 'runtime.count')).toBe(2)
|
|
181
197
|
})
|
|
182
198
|
|
|
183
199
|
it('executes only the matching match arm', () => {
|
|
@@ -234,7 +250,7 @@ fn test() {
|
|
|
234
250
|
runtime.load()
|
|
235
251
|
runtime.execFunction('test')
|
|
236
252
|
|
|
237
|
-
expect(runtime.getScore('lambda', 'direct')).toBe(10)
|
|
253
|
+
expect(runtime.getScore('lambda', 'runtime.direct')).toBe(10)
|
|
238
254
|
})
|
|
239
255
|
|
|
240
256
|
it('executes lambdas passed as callback arguments', () => {
|
|
@@ -252,7 +268,7 @@ fn test() {
|
|
|
252
268
|
runtime.load()
|
|
253
269
|
runtime.execFunction('test')
|
|
254
270
|
|
|
255
|
-
expect(runtime.getScore('lambda', 'callback')).toBe(15)
|
|
271
|
+
expect(runtime.getScore('lambda', 'runtime.callback')).toBe(15)
|
|
256
272
|
})
|
|
257
273
|
|
|
258
274
|
it('executes block-body lambdas', () => {
|
|
@@ -270,7 +286,7 @@ fn test() {
|
|
|
270
286
|
runtime.load()
|
|
271
287
|
runtime.execFunction('test')
|
|
272
288
|
|
|
273
|
-
expect(runtime.getScore('lambda', 'block')).toBe(11)
|
|
289
|
+
expect(runtime.getScore('lambda', 'runtime.block')).toBe(11)
|
|
274
290
|
})
|
|
275
291
|
|
|
276
292
|
it('executes immediately-invoked expression-body lambdas', () => {
|
|
@@ -284,6 +300,6 @@ fn test() {
|
|
|
284
300
|
runtime.load()
|
|
285
301
|
runtime.execFunction('test')
|
|
286
302
|
|
|
287
|
-
expect(runtime.getScore('lambda', 'iife')).toBe(10)
|
|
303
|
+
expect(runtime.getScore('lambda', 'runtime.iife')).toBe(10)
|
|
288
304
|
})
|
|
289
305
|
})
|
|
@@ -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() {
|
|
@@ -211,6 +244,147 @@ fn test() {
|
|
|
211
244
|
`)
|
|
212
245
|
expect(errors).toHaveLength(0)
|
|
213
246
|
})
|
|
247
|
+
|
|
248
|
+
it('type checks timer builtins with void callbacks and interval IDs', () => {
|
|
249
|
+
const errors = typeCheck(`
|
|
250
|
+
fn test() {
|
|
251
|
+
setTimeout(100, () => {
|
|
252
|
+
say("later");
|
|
253
|
+
});
|
|
254
|
+
let intervalId: int = setInterval(20, () => {
|
|
255
|
+
say("tick");
|
|
256
|
+
});
|
|
257
|
+
clearInterval(intervalId);
|
|
258
|
+
}
|
|
259
|
+
`)
|
|
260
|
+
expect(errors).toHaveLength(0)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('rejects timer callbacks with the wrong return type', () => {
|
|
264
|
+
const errors = typeCheck(`
|
|
265
|
+
fn test() {
|
|
266
|
+
setTimeout(100, () => 1);
|
|
267
|
+
}
|
|
268
|
+
`)
|
|
269
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
270
|
+
expect(errors[0].message).toContain('Return type mismatch: expected void, got int')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('allows impl instance methods with inferred self type', () => {
|
|
274
|
+
const errors = typeCheck(`
|
|
275
|
+
struct Timer { duration: int }
|
|
276
|
+
|
|
277
|
+
impl Timer {
|
|
278
|
+
fn elapsed(self) -> int {
|
|
279
|
+
return self.duration;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fn test() {
|
|
284
|
+
let timer: Timer = { duration: 10 };
|
|
285
|
+
let value: int = timer.elapsed();
|
|
286
|
+
}
|
|
287
|
+
`)
|
|
288
|
+
expect(errors).toHaveLength(0)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('records then-branch entity narrowing for is-checks', () => {
|
|
292
|
+
const source = `
|
|
293
|
+
fn test() {
|
|
294
|
+
foreach (e in @e) {
|
|
295
|
+
if (e is Player) {
|
|
296
|
+
kill(e);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
`
|
|
301
|
+
const tokens = new Lexer(source).tokenize()
|
|
302
|
+
const ast = new Parser(tokens).parse('test')
|
|
303
|
+
const checker = new TypeChecker(source) as any
|
|
304
|
+
checker.check(ast)
|
|
305
|
+
|
|
306
|
+
const foreachStmt = ast.declarations[0].body[0] as any
|
|
307
|
+
const ifStmt = foreachStmt.body[0]
|
|
308
|
+
expect(checker.getThenBranchNarrowing(ifStmt.cond)).toEqual({
|
|
309
|
+
name: 'e',
|
|
310
|
+
type: { kind: 'entity', entityType: 'Player' },
|
|
311
|
+
mutable: false,
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('allows static impl method calls', () => {
|
|
316
|
+
const errors = typeCheck(`
|
|
317
|
+
struct Timer { duration: int }
|
|
318
|
+
|
|
319
|
+
impl Timer {
|
|
320
|
+
fn new(duration: int) -> Timer {
|
|
321
|
+
return { duration: duration };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
fn test() {
|
|
326
|
+
let timer: Timer = Timer::new(10);
|
|
327
|
+
}
|
|
328
|
+
`)
|
|
329
|
+
expect(errors).toHaveLength(0)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('rejects using is-checks on non-entity values', () => {
|
|
333
|
+
const errors = typeCheck(`
|
|
334
|
+
fn test() {
|
|
335
|
+
let x: int = 1;
|
|
336
|
+
if (x is Player) {
|
|
337
|
+
say("nope");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
`)
|
|
341
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
342
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression, got int")
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('rejects calling instance impl methods as static methods', () => {
|
|
346
|
+
const errors = typeCheck(`
|
|
347
|
+
struct Point { x: int, y: int }
|
|
348
|
+
|
|
349
|
+
impl Point {
|
|
350
|
+
fn distance(self) -> int {
|
|
351
|
+
return self.x + self.y;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fn test() {
|
|
356
|
+
let total: int = Point::distance();
|
|
357
|
+
}
|
|
358
|
+
`)
|
|
359
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
360
|
+
expect(errors[0].message).toContain("Method 'Point::distance' is an instance method")
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
describe('entity is-check narrowing', () => {
|
|
365
|
+
it('allows entity type checks on foreach bindings', () => {
|
|
366
|
+
const errors = typeCheck(`
|
|
367
|
+
fn test() {
|
|
368
|
+
foreach (e in @e) {
|
|
369
|
+
if (e is Player) {
|
|
370
|
+
kill(@s);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
`)
|
|
375
|
+
expect(errors).toHaveLength(0)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('rejects is-checks on non-entity expressions', () => {
|
|
379
|
+
const errors = typeCheck(`
|
|
380
|
+
fn test() {
|
|
381
|
+
let x: int = 1;
|
|
382
|
+
if (x is Player) {}
|
|
383
|
+
}
|
|
384
|
+
`)
|
|
385
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
386
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression")
|
|
387
|
+
})
|
|
214
388
|
})
|
|
215
389
|
|
|
216
390
|
describe('return type checking', () => {
|
|
@@ -392,4 +566,34 @@ fn broken() -> int {
|
|
|
392
566
|
expect(errors.length).toBeGreaterThanOrEqual(3)
|
|
393
567
|
})
|
|
394
568
|
})
|
|
569
|
+
|
|
570
|
+
describe('event handlers', () => {
|
|
571
|
+
it('accepts matching @on event signatures', () => {
|
|
572
|
+
const errors = typeCheck(`
|
|
573
|
+
@on(PlayerDeath)
|
|
574
|
+
fn handle_death(player: Player) {
|
|
575
|
+
tp(player, @p);
|
|
576
|
+
}
|
|
577
|
+
`)
|
|
578
|
+
expect(errors).toHaveLength(0)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('rejects unknown event types', () => {
|
|
582
|
+
const errors = typeCheck(`
|
|
583
|
+
@on(NotARealEvent)
|
|
584
|
+
fn handle(player: Player) {}
|
|
585
|
+
`)
|
|
586
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
587
|
+
expect(errors[0].message).toContain("Unknown event type 'NotARealEvent'")
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
it('rejects mismatched event signatures', () => {
|
|
591
|
+
const errors = typeCheck(`
|
|
592
|
+
@on(BlockBreak)
|
|
593
|
+
fn handle_break(player: Player) {}
|
|
594
|
+
`)
|
|
595
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
596
|
+
expect(errors[0].message).toContain('must declare 2 parameter(s)')
|
|
597
|
+
})
|
|
598
|
+
})
|
|
395
599
|
})
|
package/src/ast/types.ts
CHANGED
|
@@ -22,7 +22,19 @@ 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
|
+
|
|
27
|
+
// Entity type hierarchy
|
|
28
|
+
export type EntityTypeName =
|
|
29
|
+
| 'entity' // Base type
|
|
30
|
+
| 'Player' // @a, @p, @r
|
|
31
|
+
| 'Mob' // Base mob type
|
|
32
|
+
| 'HostileMob' // Hostile mobs
|
|
33
|
+
| 'PassiveMob' // Passive mobs
|
|
34
|
+
// Specific mob types (common ones)
|
|
35
|
+
| 'Zombie' | 'Skeleton' | 'Creeper' | 'Spider' | 'Enderman'
|
|
36
|
+
| 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager'
|
|
37
|
+
| 'ArmorStand' | 'Item' | 'Arrow'
|
|
26
38
|
|
|
27
39
|
export type TypeNode =
|
|
28
40
|
| { kind: 'named'; name: PrimitiveType }
|
|
@@ -30,6 +42,8 @@ export type TypeNode =
|
|
|
30
42
|
| { kind: 'struct'; name: string }
|
|
31
43
|
| { kind: 'enum'; name: string }
|
|
32
44
|
| { kind: 'function_type'; params: TypeNode[]; return: TypeNode }
|
|
45
|
+
| { kind: 'entity'; entityType: EntityTypeName } // Entity types
|
|
46
|
+
| { kind: 'selector' } // Selector type (multiple entities)
|
|
33
47
|
|
|
34
48
|
export interface LambdaParam {
|
|
35
49
|
name: string
|
|
@@ -43,6 +57,16 @@ export interface LambdaExpr {
|
|
|
43
57
|
body: Expr | Block
|
|
44
58
|
}
|
|
45
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
|
+
|
|
46
70
|
// ---------------------------------------------------------------------------
|
|
47
71
|
// Range Expression
|
|
48
72
|
// ---------------------------------------------------------------------------
|
|
@@ -115,15 +139,19 @@ export type Expr =
|
|
|
115
139
|
| { kind: 'short_lit'; value: number; span?: Span }
|
|
116
140
|
| { kind: 'long_lit'; value: number; span?: Span }
|
|
117
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)
|
|
118
144
|
| { kind: 'bool_lit'; value: boolean; span?: Span }
|
|
119
145
|
| { kind: 'str_lit'; value: string; span?: Span }
|
|
120
146
|
| { kind: 'mc_name'; value: string; span?: Span } // #health → "health" (MC identifier)
|
|
121
147
|
| { kind: 'str_interp'; parts: Array<string | Expr>; span?: Span }
|
|
148
|
+
| FStringExpr
|
|
122
149
|
| { kind: 'range_lit'; range: RangeExpr; span?: Span }
|
|
123
150
|
| (BlockPosExpr & { span?: Span })
|
|
124
151
|
| { kind: 'ident'; name: string; span?: Span }
|
|
125
152
|
| { kind: 'selector'; raw: string; isSingle: boolean; sel: EntitySelector; span?: Span }
|
|
126
153
|
| { kind: 'binary'; op: BinOp | CmpOp | '&&' | '||'; left: Expr; right: Expr; span?: Span }
|
|
154
|
+
| { kind: 'is_check'; expr: Expr; entityType: EntityTypeName; span?: Span }
|
|
127
155
|
| { kind: 'unary'; op: '!' | '-'; operand: Expr; span?: Span }
|
|
128
156
|
| { kind: 'assign'; target: string; op: AssignOp; value: Expr; span?: Span }
|
|
129
157
|
| { kind: 'call'; fn: string; args: Expr[]; span?: Span }
|
|
@@ -180,9 +208,10 @@ export type Block = Stmt[]
|
|
|
180
208
|
// ---------------------------------------------------------------------------
|
|
181
209
|
|
|
182
210
|
export interface Decorator {
|
|
183
|
-
name: 'tick' | 'load' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
|
|
211
|
+
name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
|
|
184
212
|
args?: {
|
|
185
213
|
rate?: number
|
|
214
|
+
eventType?: string
|
|
186
215
|
trigger?: string
|
|
187
216
|
advancement?: string
|
|
188
217
|
item?: string
|
|
@@ -224,6 +253,13 @@ export interface StructDecl {
|
|
|
224
253
|
span?: Span
|
|
225
254
|
}
|
|
226
255
|
|
|
256
|
+
export interface ImplBlock {
|
|
257
|
+
kind: 'impl_block'
|
|
258
|
+
typeName: string
|
|
259
|
+
methods: FnDecl[]
|
|
260
|
+
span?: Span
|
|
261
|
+
}
|
|
262
|
+
|
|
227
263
|
export interface EnumVariant {
|
|
228
264
|
name: string
|
|
229
265
|
value?: number
|
|
@@ -260,6 +296,7 @@ export interface Program {
|
|
|
260
296
|
globals: GlobalDecl[]
|
|
261
297
|
declarations: FnDecl[]
|
|
262
298
|
structs: StructDecl[]
|
|
299
|
+
implBlocks: ImplBlock[]
|
|
263
300
|
enums: EnumDecl[]
|
|
264
301
|
consts: ConstDecl[]
|
|
265
302
|
}
|
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
|
|