redscript-mc 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
- package/CHANGELOG.md +112 -0
- package/CONTRIBUTING.md +140 -0
- package/README.md +28 -19
- package/README.zh.md +28 -19
- package/dist/__tests__/cli.test.js +148 -10
- package/dist/__tests__/codegen.test.js +26 -1
- package/dist/__tests__/diagnostics.test.js +5 -5
- package/dist/__tests__/e2e.test.js +336 -17
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +40 -0
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +200 -12
- package/dist/__tests__/mc-integration.test.js +370 -31
- package/dist/__tests__/mc-syntax.test.js +3 -3
- package/dist/__tests__/nbt.test.js +2 -2
- package/dist/__tests__/optimizer-advanced.test.js +5 -5
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +9 -9
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +40 -3
- package/dist/cli.js +25 -7
- package/dist/codegen/mcfunction/index.d.ts +1 -1
- package/dist/codegen/mcfunction/index.js +38 -3
- package/dist/codegen/structure/index.js +32 -1
- package/dist/compile.d.ts +10 -0
- package/dist/compile.js +36 -5
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +26 -0
- package/dist/index.js +3 -2
- package/dist/ir/builder.d.ts +2 -1
- package/dist/ir/types.d.ts +11 -2
- package/dist/ir/types.js +1 -1
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +34 -1
- package/dist/lowering/index.js +622 -23
- package/dist/mc-test/runner.d.ts +2 -2
- package/dist/mc-test/runner.js +3 -3
- package/dist/mc-test/setup.js +2 -2
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +153 -16
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/COMPILATION_STATS.md +24 -24
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/docs/IMPLEMENTATION_GUIDE.md +1 -1
- package/docs/STRUCTURE_TARGET.md +1 -1
- package/editors/vscode/.vscodeignore +1 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icons/mcrs.svg +7 -0
- package/editors/vscode/icons/redscript-icons.json +10 -0
- package/editors/vscode/out/extension.js +1295 -80
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +10 -3
- package/editors/vscode/src/hover.ts +55 -2
- package/editors/vscode/src/symbols.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +176 -10
- package/src/__tests__/codegen.test.ts +28 -1
- package/src/__tests__/diagnostics.test.ts +5 -5
- package/src/__tests__/e2e.test.ts +335 -17
- 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 +14 -2
- package/src/__tests__/lowering.test.ts +226 -12
- package/src/__tests__/mc-integration.test.ts +421 -31
- package/src/__tests__/mc-syntax.test.ts +3 -3
- package/src/__tests__/nbt.test.ts +2 -2
- package/src/__tests__/optimizer-advanced.test.ts +5 -5
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +9 -9
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +44 -3
- package/src/cli.ts +10 -10
- package/src/codegen/mcfunction/index.ts +40 -3
- package/src/codegen/structure/index.ts +35 -1
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/examples/capture_the_flag.mcrs +208 -0
- package/src/examples/{counter.rs → counter.mcrs} +1 -1
- package/src/examples/hunger_games.mcrs +301 -0
- package/src/examples/new_features_demo.mcrs +193 -0
- package/src/examples/parkour_race.mcrs +233 -0
- package/src/examples/rpg.mcrs +13 -0
- package/src/examples/{shop.rs → shop.mcrs} +1 -1
- package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
- package/src/examples/{turret.rs → turret.mcrs} +1 -1
- package/src/examples/zombie_survival.mcrs +314 -0
- package/src/index.ts +4 -3
- package/src/ir/builder.ts +3 -1
- package/src/ir/types.ts +12 -2
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +684 -24
- package/src/mc-test/runner.ts +3 -3
- package/src/mc-test/setup.ts +2 -2
- package/src/parser/index.ts +170 -19
- package/src/stdlib/README.md +178 -140
- package/src/stdlib/bossbar.mcrs +68 -0
- package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
- package/src/stdlib/effects.mcrs +64 -0
- package/src/stdlib/interactions.mcrs +195 -0
- package/src/stdlib/inventory.mcrs +38 -0
- package/src/stdlib/mobs.mcrs +99 -0
- package/src/stdlib/particles.mcrs +52 -0
- package/src/stdlib/sets.mcrs +20 -0
- package/src/stdlib/spawn.mcrs +41 -0
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/teams.mcrs +68 -0
- package/src/stdlib/timer.mcrs +72 -0
- package/src/stdlib/world.mcrs +92 -0
- package/src/typechecker/index.ts +404 -18
- package/src/examples/rpg.rs +0 -13
- package/src/stdlib/mobs.rs +0 -99
- package/src/stdlib/timer.rs +0 -51
- /package/src/examples/{arena.rs → arena.mcrs} +0 -0
- /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
- /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
- /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
- /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
- /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
- /package/src/stdlib/{math.rs → math.mcrs} +0 -0
- /package/src/stdlib/{player.rs → player.mcrs} +0 -0
- /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
- /package/src/templates/{combat.rs → combat.mcrs} +0 -0
- /package/src/templates/{economy.rs → economy.mcrs} +0 -0
- /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
- /package/src/templates/{quest.rs → quest.mcrs} +0 -0
- /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
|
@@ -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')
|
|
@@ -459,11 +535,11 @@ describe('Parser', () => {
|
|
|
459
535
|
expect(expr).toEqual({ kind: 'ident', name: 'foo' })
|
|
460
536
|
})
|
|
461
537
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
538
|
+
it('parses function call', () => {
|
|
539
|
+
const expr = parseExpr('foo(1, 2)')
|
|
540
|
+
expect(expr).toEqual({
|
|
541
|
+
kind: 'call',
|
|
542
|
+
fn: 'foo',
|
|
467
543
|
args: [
|
|
468
544
|
{ kind: 'int_lit', value: 1 },
|
|
469
545
|
{ kind: 'int_lit', value: 2 },
|
|
@@ -486,6 +562,16 @@ describe('Parser', () => {
|
|
|
486
562
|
})
|
|
487
563
|
})
|
|
488
564
|
|
|
565
|
+
it('parses static method calls', () => {
|
|
566
|
+
const expr = parseExpr('Timer::new(100)')
|
|
567
|
+
expect(expr).toEqual({
|
|
568
|
+
kind: 'static_call',
|
|
569
|
+
type: 'Timer',
|
|
570
|
+
method: 'new',
|
|
571
|
+
args: [{ kind: 'int_lit', value: 100 }],
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
|
|
489
575
|
describe('binary operators', () => {
|
|
490
576
|
it('parses arithmetic', () => {
|
|
491
577
|
const expr = parseExpr('1 + 2')
|
|
@@ -29,7 +29,7 @@ function loadExample(name: string): string {
|
|
|
29
29
|
|
|
30
30
|
describe('MCRuntime behavioral integration', () => {
|
|
31
31
|
it('runs the counter example and increments the scoreboard across ticks', () => {
|
|
32
|
-
const runtime = loadCompiledProgram(loadExample('counter.
|
|
32
|
+
const runtime = loadCompiledProgram(loadExample('counter.mcrs'))
|
|
33
33
|
|
|
34
34
|
runtime.load()
|
|
35
35
|
runtime.ticks(5)
|
|
@@ -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', () => {
|
|
@@ -135,8 +135,8 @@ fn arrays() {
|
|
|
135
135
|
runtime.load()
|
|
136
136
|
runtime.execFunction('arrays')
|
|
137
137
|
|
|
138
|
-
expect(runtime.getScore('arrays', 'len')).toBe(1)
|
|
139
|
-
expect(runtime.getScore('arrays', 'last')).toBe(9)
|
|
138
|
+
expect(runtime.getScore('arrays', 'runtime.len')).toBe(1)
|
|
139
|
+
expect(runtime.getScore('arrays', 'runtime.last')).toBe(9)
|
|
140
140
|
expect(runtime.getStorage('rs:heap.arr')).toEqual([4])
|
|
141
141
|
})
|
|
142
142
|
|
|
@@ -177,7 +177,7 @@ fn pulse() {
|
|
|
177
177
|
runtime.load()
|
|
178
178
|
runtime.ticks(10)
|
|
179
179
|
|
|
180
|
-
expect(runtime.getScore('pulse', 'count')).toBe(2)
|
|
180
|
+
expect(runtime.getScore('pulse', 'runtime.count')).toBe(2)
|
|
181
181
|
})
|
|
182
182
|
|
|
183
183
|
it('executes only the matching match arm', () => {
|
|
@@ -234,7 +234,7 @@ fn test() {
|
|
|
234
234
|
runtime.load()
|
|
235
235
|
runtime.execFunction('test')
|
|
236
236
|
|
|
237
|
-
expect(runtime.getScore('lambda', 'direct')).toBe(10)
|
|
237
|
+
expect(runtime.getScore('lambda', 'runtime.direct')).toBe(10)
|
|
238
238
|
})
|
|
239
239
|
|
|
240
240
|
it('executes lambdas passed as callback arguments', () => {
|
|
@@ -252,7 +252,7 @@ fn test() {
|
|
|
252
252
|
runtime.load()
|
|
253
253
|
runtime.execFunction('test')
|
|
254
254
|
|
|
255
|
-
expect(runtime.getScore('lambda', 'callback')).toBe(15)
|
|
255
|
+
expect(runtime.getScore('lambda', 'runtime.callback')).toBe(15)
|
|
256
256
|
})
|
|
257
257
|
|
|
258
258
|
it('executes block-body lambdas', () => {
|
|
@@ -270,7 +270,7 @@ fn test() {
|
|
|
270
270
|
runtime.load()
|
|
271
271
|
runtime.execFunction('test')
|
|
272
272
|
|
|
273
|
-
expect(runtime.getScore('lambda', 'block')).toBe(11)
|
|
273
|
+
expect(runtime.getScore('lambda', 'runtime.block')).toBe(11)
|
|
274
274
|
})
|
|
275
275
|
|
|
276
276
|
it('executes immediately-invoked expression-body lambdas', () => {
|
|
@@ -284,6 +284,6 @@ fn test() {
|
|
|
284
284
|
runtime.load()
|
|
285
285
|
runtime.execFunction('test')
|
|
286
286
|
|
|
287
|
-
expect(runtime.getScore('lambda', 'iife')).toBe(10)
|
|
287
|
+
expect(runtime.getScore('lambda', 'runtime.iife')).toBe(10)
|
|
288
288
|
})
|
|
289
289
|
})
|
|
@@ -211,6 +211,147 @@ fn test() {
|
|
|
211
211
|
`)
|
|
212
212
|
expect(errors).toHaveLength(0)
|
|
213
213
|
})
|
|
214
|
+
|
|
215
|
+
it('type checks timer builtins with void callbacks and interval IDs', () => {
|
|
216
|
+
const errors = typeCheck(`
|
|
217
|
+
fn test() {
|
|
218
|
+
setTimeout(100, () => {
|
|
219
|
+
say("later");
|
|
220
|
+
});
|
|
221
|
+
let intervalId: int = setInterval(20, () => {
|
|
222
|
+
say("tick");
|
|
223
|
+
});
|
|
224
|
+
clearInterval(intervalId);
|
|
225
|
+
}
|
|
226
|
+
`)
|
|
227
|
+
expect(errors).toHaveLength(0)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('rejects timer callbacks with the wrong return type', () => {
|
|
231
|
+
const errors = typeCheck(`
|
|
232
|
+
fn test() {
|
|
233
|
+
setTimeout(100, () => 1);
|
|
234
|
+
}
|
|
235
|
+
`)
|
|
236
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
237
|
+
expect(errors[0].message).toContain('Return type mismatch: expected void, got int')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('allows impl instance methods with inferred self type', () => {
|
|
241
|
+
const errors = typeCheck(`
|
|
242
|
+
struct Timer { duration: int }
|
|
243
|
+
|
|
244
|
+
impl Timer {
|
|
245
|
+
fn elapsed(self) -> int {
|
|
246
|
+
return self.duration;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn test() {
|
|
251
|
+
let timer: Timer = { duration: 10 };
|
|
252
|
+
let value: int = timer.elapsed();
|
|
253
|
+
}
|
|
254
|
+
`)
|
|
255
|
+
expect(errors).toHaveLength(0)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('records then-branch entity narrowing for is-checks', () => {
|
|
259
|
+
const source = `
|
|
260
|
+
fn test() {
|
|
261
|
+
foreach (e in @e) {
|
|
262
|
+
if (e is Player) {
|
|
263
|
+
kill(e);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
`
|
|
268
|
+
const tokens = new Lexer(source).tokenize()
|
|
269
|
+
const ast = new Parser(tokens).parse('test')
|
|
270
|
+
const checker = new TypeChecker(source) as any
|
|
271
|
+
checker.check(ast)
|
|
272
|
+
|
|
273
|
+
const foreachStmt = ast.declarations[0].body[0] as any
|
|
274
|
+
const ifStmt = foreachStmt.body[0]
|
|
275
|
+
expect(checker.getThenBranchNarrowing(ifStmt.cond)).toEqual({
|
|
276
|
+
name: 'e',
|
|
277
|
+
type: { kind: 'entity', entityType: 'Player' },
|
|
278
|
+
mutable: false,
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('allows static impl method calls', () => {
|
|
283
|
+
const errors = typeCheck(`
|
|
284
|
+
struct Timer { duration: int }
|
|
285
|
+
|
|
286
|
+
impl Timer {
|
|
287
|
+
fn new(duration: int) -> Timer {
|
|
288
|
+
return { duration: duration };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn test() {
|
|
293
|
+
let timer: Timer = Timer::new(10);
|
|
294
|
+
}
|
|
295
|
+
`)
|
|
296
|
+
expect(errors).toHaveLength(0)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('rejects using is-checks on non-entity values', () => {
|
|
300
|
+
const errors = typeCheck(`
|
|
301
|
+
fn test() {
|
|
302
|
+
let x: int = 1;
|
|
303
|
+
if (x is Player) {
|
|
304
|
+
say("nope");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
`)
|
|
308
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
309
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression, got int")
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('rejects calling instance impl methods as static methods', () => {
|
|
313
|
+
const errors = typeCheck(`
|
|
314
|
+
struct Point { x: int, y: int }
|
|
315
|
+
|
|
316
|
+
impl Point {
|
|
317
|
+
fn distance(self) -> int {
|
|
318
|
+
return self.x + self.y;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fn test() {
|
|
323
|
+
let total: int = Point::distance();
|
|
324
|
+
}
|
|
325
|
+
`)
|
|
326
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
327
|
+
expect(errors[0].message).toContain("Method 'Point::distance' is an instance method")
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe('entity is-check narrowing', () => {
|
|
332
|
+
it('allows entity type checks on foreach bindings', () => {
|
|
333
|
+
const errors = typeCheck(`
|
|
334
|
+
fn test() {
|
|
335
|
+
foreach (e in @e) {
|
|
336
|
+
if (e is Player) {
|
|
337
|
+
kill(@s);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
`)
|
|
342
|
+
expect(errors).toHaveLength(0)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('rejects is-checks on non-entity expressions', () => {
|
|
346
|
+
const errors = typeCheck(`
|
|
347
|
+
fn test() {
|
|
348
|
+
let x: int = 1;
|
|
349
|
+
if (x is Player) {}
|
|
350
|
+
}
|
|
351
|
+
`)
|
|
352
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
353
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression")
|
|
354
|
+
})
|
|
214
355
|
})
|
|
215
356
|
|
|
216
357
|
describe('return type checking', () => {
|
|
@@ -392,4 +533,34 @@ fn broken() -> int {
|
|
|
392
533
|
expect(errors.length).toBeGreaterThanOrEqual(3)
|
|
393
534
|
})
|
|
394
535
|
})
|
|
536
|
+
|
|
537
|
+
describe('event handlers', () => {
|
|
538
|
+
it('accepts matching @on event signatures', () => {
|
|
539
|
+
const errors = typeCheck(`
|
|
540
|
+
@on(PlayerDeath)
|
|
541
|
+
fn handle_death(player: Player) {
|
|
542
|
+
tp(player, @p);
|
|
543
|
+
}
|
|
544
|
+
`)
|
|
545
|
+
expect(errors).toHaveLength(0)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('rejects unknown event types', () => {
|
|
549
|
+
const errors = typeCheck(`
|
|
550
|
+
@on(NotARealEvent)
|
|
551
|
+
fn handle(player: Player) {}
|
|
552
|
+
`)
|
|
553
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
554
|
+
expect(errors[0].message).toContain("Unknown event type 'NotARealEvent'")
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
it('rejects mismatched event signatures', () => {
|
|
558
|
+
const errors = typeCheck(`
|
|
559
|
+
@on(BlockBreak)
|
|
560
|
+
fn handle_break(player: Player) {}
|
|
561
|
+
`)
|
|
562
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
563
|
+
expect(errors[0].message).toContain('must declare 2 parameter(s)')
|
|
564
|
+
})
|
|
565
|
+
})
|
|
395
566
|
})
|
package/src/ast/types.ts
CHANGED
|
@@ -24,12 +24,26 @@ export interface Span {
|
|
|
24
24
|
|
|
25
25
|
export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double'
|
|
26
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'
|
|
38
|
+
|
|
27
39
|
export type TypeNode =
|
|
28
40
|
| { kind: 'named'; name: PrimitiveType }
|
|
29
41
|
| { kind: 'array'; elem: 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
|
|
@@ -68,6 +82,13 @@ export interface SelectorFilter {
|
|
|
68
82
|
sort?: 'nearest' | 'furthest' | 'random' | 'arbitrary'
|
|
69
83
|
nbt?: string
|
|
70
84
|
gamemode?: string
|
|
85
|
+
// Position filters
|
|
86
|
+
x?: RangeExpr
|
|
87
|
+
y?: RangeExpr
|
|
88
|
+
z?: RangeExpr
|
|
89
|
+
// Rotation filters
|
|
90
|
+
x_rotation?: RangeExpr
|
|
91
|
+
y_rotation?: RangeExpr
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
export interface EntitySelector {
|
|
@@ -117,6 +138,7 @@ export type Expr =
|
|
|
117
138
|
| { kind: 'ident'; name: string; span?: Span }
|
|
118
139
|
| { kind: 'selector'; raw: string; isSingle: boolean; sel: EntitySelector; span?: Span }
|
|
119
140
|
| { kind: 'binary'; op: BinOp | CmpOp | '&&' | '||'; left: Expr; right: Expr; span?: Span }
|
|
141
|
+
| { kind: 'is_check'; expr: Expr; entityType: EntityTypeName; span?: Span }
|
|
120
142
|
| { kind: 'unary'; op: '!' | '-'; operand: Expr; span?: Span }
|
|
121
143
|
| { kind: 'assign'; target: string; op: AssignOp; value: Expr; span?: Span }
|
|
122
144
|
| { kind: 'call'; fn: string; args: Expr[]; span?: Span }
|
|
@@ -146,8 +168,8 @@ export type LiteralExpr =
|
|
|
146
168
|
export type ExecuteSubcommand =
|
|
147
169
|
| { kind: 'as'; selector: EntitySelector }
|
|
148
170
|
| { kind: 'at'; selector: EntitySelector }
|
|
149
|
-
| { kind: 'if_entity'; selector
|
|
150
|
-
| { kind: 'unless_entity'; selector
|
|
171
|
+
| { kind: 'if_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
|
|
172
|
+
| { kind: 'unless_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
|
|
151
173
|
| { kind: 'in'; dimension: string }
|
|
152
174
|
|
|
153
175
|
export type Stmt =
|
|
@@ -173,9 +195,10 @@ export type Block = Stmt[]
|
|
|
173
195
|
// ---------------------------------------------------------------------------
|
|
174
196
|
|
|
175
197
|
export interface Decorator {
|
|
176
|
-
name: 'tick' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
|
|
198
|
+
name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
|
|
177
199
|
args?: {
|
|
178
200
|
rate?: number
|
|
201
|
+
eventType?: string
|
|
179
202
|
trigger?: string
|
|
180
203
|
advancement?: string
|
|
181
204
|
item?: string
|
|
@@ -217,6 +240,13 @@ export interface StructDecl {
|
|
|
217
240
|
span?: Span
|
|
218
241
|
}
|
|
219
242
|
|
|
243
|
+
export interface ImplBlock {
|
|
244
|
+
kind: 'impl_block'
|
|
245
|
+
typeName: string
|
|
246
|
+
methods: FnDecl[]
|
|
247
|
+
span?: Span
|
|
248
|
+
}
|
|
249
|
+
|
|
220
250
|
export interface EnumVariant {
|
|
221
251
|
name: string
|
|
222
252
|
value?: number
|
|
@@ -235,14 +265,25 @@ export interface ConstDecl {
|
|
|
235
265
|
span?: Span
|
|
236
266
|
}
|
|
237
267
|
|
|
268
|
+
export interface GlobalDecl {
|
|
269
|
+
kind: 'global'
|
|
270
|
+
name: string
|
|
271
|
+
type: TypeNode
|
|
272
|
+
init: Expr
|
|
273
|
+
mutable: boolean // let = true, const = false
|
|
274
|
+
span?: Span
|
|
275
|
+
}
|
|
276
|
+
|
|
238
277
|
// ---------------------------------------------------------------------------
|
|
239
278
|
// Program (Top-Level)
|
|
240
279
|
// ---------------------------------------------------------------------------
|
|
241
280
|
|
|
242
281
|
export interface Program {
|
|
243
282
|
namespace: string // Inferred from filename or `namespace mypack;`
|
|
283
|
+
globals: GlobalDecl[]
|
|
244
284
|
declarations: FnDecl[]
|
|
245
285
|
structs: StructDecl[]
|
|
286
|
+
implBlocks: ImplBlock[]
|
|
246
287
|
enums: EnumDecl[]
|
|
247
288
|
consts: ConstDecl[]
|
|
248
289
|
}
|
package/src/cli.ts
CHANGED
|
@@ -29,13 +29,13 @@ Usage:
|
|
|
29
29
|
redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
|
|
30
30
|
redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
|
|
31
31
|
redscript check <file>
|
|
32
|
-
redscript fmt <file.
|
|
32
|
+
redscript fmt <file.mcrs> [file2.mcrs ...]
|
|
33
33
|
redscript repl
|
|
34
34
|
redscript version
|
|
35
35
|
|
|
36
36
|
Commands:
|
|
37
37
|
compile Compile a RedScript file to a Minecraft datapack
|
|
38
|
-
watch Watch a directory for .
|
|
38
|
+
watch Watch a directory for .mcrs file changes, recompile, and hot reload
|
|
39
39
|
check Check a RedScript file for errors without generating output
|
|
40
40
|
fmt Auto-format RedScript source files
|
|
41
41
|
repl Start an interactive RedScript REPL
|
|
@@ -268,7 +268,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
268
268
|
process.exit(1)
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
console.log(`👁 Watching ${dir} for .
|
|
271
|
+
console.log(`👁 Watching ${dir} for .mcrs file changes...`)
|
|
272
272
|
console.log(` Output: ${output}`)
|
|
273
273
|
if (hotReloadUrl) console.log(` Hot reload: ${hotReloadUrl}`)
|
|
274
274
|
console.log(` Press Ctrl+C to stop\n`)
|
|
@@ -276,11 +276,11 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
276
276
|
// Debounce timer
|
|
277
277
|
let debounceTimer: NodeJS.Timeout | null = null
|
|
278
278
|
|
|
279
|
-
// Compile all .
|
|
279
|
+
// Compile all .mcrs files in directory
|
|
280
280
|
async function compileAll(): Promise<void> {
|
|
281
281
|
const files = findRsFiles(dir)
|
|
282
282
|
if (files.length === 0) {
|
|
283
|
-
console.log(`⚠ No .
|
|
283
|
+
console.log(`⚠ No .mcrs files found in ${dir}`)
|
|
284
284
|
return
|
|
285
285
|
}
|
|
286
286
|
|
|
@@ -319,7 +319,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
// Find all .
|
|
322
|
+
// Find all .mcrs files recursively
|
|
323
323
|
function findRsFiles(directory: string): string[] {
|
|
324
324
|
const results: string[] = []
|
|
325
325
|
const entries = fs.readdirSync(directory, { withFileTypes: true })
|
|
@@ -328,7 +328,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
328
328
|
const fullPath = path.join(directory, entry.name)
|
|
329
329
|
if (entry.isDirectory()) {
|
|
330
330
|
results.push(...findRsFiles(fullPath))
|
|
331
|
-
} else if (entry.isFile() && entry.name.endsWith('.
|
|
331
|
+
} else if (entry.isFile() && entry.name.endsWith('.mcrs')) {
|
|
332
332
|
results.push(fullPath)
|
|
333
333
|
}
|
|
334
334
|
}
|
|
@@ -341,7 +341,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
|
|
|
341
341
|
|
|
342
342
|
// Watch for changes
|
|
343
343
|
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
344
|
-
if (filename && filename.endsWith('.
|
|
344
|
+
if (filename && filename.endsWith('.mcrs')) {
|
|
345
345
|
// Debounce rapid changes
|
|
346
346
|
if (debounceTimer) {
|
|
347
347
|
clearTimeout(debounceTimer)
|
|
@@ -412,9 +412,9 @@ async function main(): Promise<void> {
|
|
|
412
412
|
|
|
413
413
|
case 'fmt':
|
|
414
414
|
case 'format': {
|
|
415
|
-
const files = args.filter(a => a.endsWith('.
|
|
415
|
+
const files = args.filter(a => a.endsWith('.mcrs'))
|
|
416
416
|
if (files.length === 0) {
|
|
417
|
-
console.error('Usage: redscript fmt <file.
|
|
417
|
+
console.error('Usage: redscript fmt <file.mcrs> [file2.mcrs ...]')
|
|
418
418
|
process.exit(1)
|
|
419
419
|
}
|
|
420
420
|
const { format } = require('./formatter')
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
* Variable mapping:
|
|
12
12
|
* scoreboard objective: "rs"
|
|
13
13
|
* fake player: "$<varname>"
|
|
14
|
-
* temporaries: "$
|
|
14
|
+
* temporaries: "$_0", "$_1", ...
|
|
15
15
|
* return value: "$ret"
|
|
16
16
|
* parameters: "$p0", "$p1", ...
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { IRBlock, IRFunction, IRModule, Operand, Terminator } from '../../ir/types'
|
|
20
20
|
import { optimizeCommandFunctions, type OptimizationStats, createEmptyOptimizationStats, mergeOptimizationStats } from '../../optimizer/commands'
|
|
21
|
+
import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Utilities
|
|
@@ -270,6 +271,10 @@ export function generateDatapackWithStats(
|
|
|
270
271
|
// Collect all trigger handlers
|
|
271
272
|
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
|
|
272
273
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
|
|
274
|
+
const eventHandlers = module.functions.filter((fn): fn is IRFunction & { eventHandler: { eventType: EventTypeName; tag: string } } =>
|
|
275
|
+
!!fn.eventHandler && isEventTypeName(fn.eventHandler.eventType)
|
|
276
|
+
)
|
|
277
|
+
const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
|
|
273
278
|
|
|
274
279
|
// Collect all tick functions
|
|
275
280
|
const tickFunctionNames: string[] = []
|
|
@@ -293,7 +298,7 @@ export function generateDatapackWithStats(
|
|
|
293
298
|
`scoreboard objectives add ${OBJ} dummy`,
|
|
294
299
|
]
|
|
295
300
|
for (const g of module.globals) {
|
|
296
|
-
loadLines.push(`scoreboard players set ${varRef(g)} ${OBJ}
|
|
301
|
+
loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`)
|
|
297
302
|
}
|
|
298
303
|
|
|
299
304
|
// Add trigger objectives
|
|
@@ -302,6 +307,19 @@ export function generateDatapackWithStats(
|
|
|
302
307
|
loadLines.push(`scoreboard players enable @a ${triggerName}`)
|
|
303
308
|
}
|
|
304
309
|
|
|
310
|
+
for (const eventType of eventTypes) {
|
|
311
|
+
const detection = EVENT_TYPES[eventType].detection
|
|
312
|
+
if (eventType === 'PlayerDeath') {
|
|
313
|
+
loadLines.push('scoreboard objectives add rs.deaths deathCount')
|
|
314
|
+
} else if (eventType === 'EntityKill') {
|
|
315
|
+
loadLines.push('scoreboard objectives add rs.kills totalKillCount')
|
|
316
|
+
} else if (eventType === 'ItemUse') {
|
|
317
|
+
loadLines.push('# ItemUse detection requires a project-specific objective/tag setup')
|
|
318
|
+
} else if (detection === 'tag' || detection === 'advancement') {
|
|
319
|
+
loadLines.push(`# ${eventType} detection expects tag ${EVENT_TYPES[eventType].tag} to be set externally`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
305
323
|
// Generate trigger dispatch functions
|
|
306
324
|
for (const triggerName of triggerNames) {
|
|
307
325
|
const handlers = triggerHandlers.filter(fn => fn.triggerName === triggerName)
|
|
@@ -356,6 +374,13 @@ export function generateDatapackWithStats(
|
|
|
356
374
|
}
|
|
357
375
|
}
|
|
358
376
|
|
|
377
|
+
// Call @load functions from __load
|
|
378
|
+
for (const fn of module.functions) {
|
|
379
|
+
if (fn.isLoadInit) {
|
|
380
|
+
loadLines.push(`function ${ns}:${fn.name}`)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
359
384
|
// Write __load.mcfunction
|
|
360
385
|
files.push({
|
|
361
386
|
path: `data/${ns}/function/__load.mcfunction`,
|
|
@@ -384,8 +409,20 @@ export function generateDatapackWithStats(
|
|
|
384
409
|
}
|
|
385
410
|
}
|
|
386
411
|
|
|
412
|
+
if (eventHandlers.length > 0) {
|
|
413
|
+
tickLines.push('# Event checks')
|
|
414
|
+
for (const eventType of eventTypes) {
|
|
415
|
+
const tag = EVENT_TYPES[eventType].tag
|
|
416
|
+
const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType)
|
|
417
|
+
for (const handler of handlers) {
|
|
418
|
+
tickLines.push(`execute as @a[tag=${tag}] run function ${ns}:${handler.name}`)
|
|
419
|
+
}
|
|
420
|
+
tickLines.push(`tag @a[tag=${tag}] remove ${tag}`)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
387
424
|
// Only generate __tick if there's something to run
|
|
388
|
-
if (tickFunctionNames.length > 0 || triggerNames.size > 0) {
|
|
425
|
+
if (tickFunctionNames.length > 0 || triggerNames.size > 0 || eventHandlers.length > 0) {
|
|
389
426
|
files.push({
|
|
390
427
|
path: `data/${ns}/function/__tick.mcfunction`,
|
|
391
428
|
content: tickLines.join('\n'),
|