redscript-mc 1.1.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/CHANGELOG.md +54 -0
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +164 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +8 -8
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +20 -1
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.js +25 -0
- 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/index.js +3 -2
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +439 -15
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +79 -10
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/CHANGELOG.md +9 -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/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -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 +14 -2
- package/src/__tests__/lowering.test.ts +178 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +8 -8
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +25 -1
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +27 -0
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/index.ts +4 -3
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +528 -16
- package/src/parser/index.ts +90 -12
- 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 +404 -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')
|
|
@@ -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')
|
|
@@ -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
|
|
@@ -124,6 +138,7 @@ export type Expr =
|
|
|
124
138
|
| { kind: 'ident'; name: string; span?: Span }
|
|
125
139
|
| { kind: 'selector'; raw: string; isSingle: boolean; sel: EntitySelector; span?: Span }
|
|
126
140
|
| { kind: 'binary'; op: BinOp | CmpOp | '&&' | '||'; left: Expr; right: Expr; span?: Span }
|
|
141
|
+
| { kind: 'is_check'; expr: Expr; entityType: EntityTypeName; span?: Span }
|
|
127
142
|
| { kind: 'unary'; op: '!' | '-'; operand: Expr; span?: Span }
|
|
128
143
|
| { kind: 'assign'; target: string; op: AssignOp; value: Expr; span?: Span }
|
|
129
144
|
| { kind: 'call'; fn: string; args: Expr[]; span?: Span }
|
|
@@ -180,9 +195,10 @@ export type Block = Stmt[]
|
|
|
180
195
|
// ---------------------------------------------------------------------------
|
|
181
196
|
|
|
182
197
|
export interface Decorator {
|
|
183
|
-
name: 'tick' | 'load' | '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'
|
|
184
199
|
args?: {
|
|
185
200
|
rate?: number
|
|
201
|
+
eventType?: string
|
|
186
202
|
trigger?: string
|
|
187
203
|
advancement?: string
|
|
188
204
|
item?: string
|
|
@@ -224,6 +240,13 @@ export interface StructDecl {
|
|
|
224
240
|
span?: Span
|
|
225
241
|
}
|
|
226
242
|
|
|
243
|
+
export interface ImplBlock {
|
|
244
|
+
kind: 'impl_block'
|
|
245
|
+
typeName: string
|
|
246
|
+
methods: FnDecl[]
|
|
247
|
+
span?: Span
|
|
248
|
+
}
|
|
249
|
+
|
|
227
250
|
export interface EnumVariant {
|
|
228
251
|
name: string
|
|
229
252
|
value?: number
|
|
@@ -260,6 +283,7 @@ export interface Program {
|
|
|
260
283
|
globals: GlobalDecl[]
|
|
261
284
|
declarations: FnDecl[]
|
|
262
285
|
structs: StructDecl[]
|
|
286
|
+
implBlocks: ImplBlock[]
|
|
263
287
|
enums: EnumDecl[]
|
|
264
288
|
consts: ConstDecl[]
|
|
265
289
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
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[] = []
|
|
@@ -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)
|
|
@@ -391,8 +409,20 @@ export function generateDatapackWithStats(
|
|
|
391
409
|
}
|
|
392
410
|
}
|
|
393
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
|
+
|
|
394
424
|
// Only generate __tick if there's something to run
|
|
395
|
-
if (tickFunctionNames.length > 0 || triggerNames.size > 0) {
|
|
425
|
+
if (tickFunctionNames.length > 0 || triggerNames.size > 0 || eventHandlers.length > 0) {
|
|
396
426
|
files.push({
|
|
397
427
|
path: `data/${ns}/function/__tick.mcfunction`,
|
|
398
428
|
content: tickLines.join('\n'),
|
|
@@ -8,6 +8,7 @@ import { optimizeForStructure, optimizeForStructureWithStats } from '../../optim
|
|
|
8
8
|
import { preprocessSource } from '../../compile'
|
|
9
9
|
import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
|
|
10
10
|
import type { DatapackFile } from '../mcfunction'
|
|
11
|
+
import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
|
|
11
12
|
|
|
12
13
|
const DATA_VERSION = 3953
|
|
13
14
|
const MAX_WIDTH = 16
|
|
@@ -87,6 +88,10 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
|
87
88
|
const entries: CommandEntry[] = []
|
|
88
89
|
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
|
|
89
90
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
|
|
91
|
+
const eventHandlers = module.functions.filter((fn): fn is IRFunction & { eventHandler: { eventType: EventTypeName; tag: string } } =>
|
|
92
|
+
!!fn.eventHandler && isEventTypeName(fn.eventHandler.eventType)
|
|
93
|
+
)
|
|
94
|
+
const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
|
|
90
95
|
const loadCommands = [
|
|
91
96
|
`scoreboard objectives add ${OBJ} dummy`,
|
|
92
97
|
...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
|
|
@@ -99,6 +104,14 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
|
99
104
|
).map(constSetup),
|
|
100
105
|
]
|
|
101
106
|
|
|
107
|
+
for (const eventType of eventTypes) {
|
|
108
|
+
if (eventType === 'PlayerDeath') {
|
|
109
|
+
loadCommands.push('scoreboard objectives add rs.deaths deathCount')
|
|
110
|
+
} else if (eventType === 'EntityKill') {
|
|
111
|
+
loadCommands.push('scoreboard objectives add rs.kills totalKillCount')
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
// Call @load functions from __load
|
|
103
116
|
for (const fn of module.functions) {
|
|
104
117
|
if (fn.isLoadInit) {
|
|
@@ -146,6 +159,20 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
|
146
159
|
})
|
|
147
160
|
}
|
|
148
161
|
}
|
|
162
|
+
if (eventHandlers.length > 0) {
|
|
163
|
+
for (const eventType of eventTypes) {
|
|
164
|
+
const tag = EVENT_TYPES[eventType].tag
|
|
165
|
+
const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType)
|
|
166
|
+
for (const handler of handlers) {
|
|
167
|
+
tickCommands.push({
|
|
168
|
+
cmd: `execute as @a[tag=${tag}] run function ${module.namespace}:${handler.name}`,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
tickCommands.push({
|
|
172
|
+
cmd: `tag @a[tag=${tag}] remove ${tag}`,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
149
176
|
if (tickCommands.length > 0) {
|
|
150
177
|
sections.push({
|
|
151
178
|
name: '__tick',
|
package/src/compile.ts
CHANGED
|
@@ -39,6 +39,17 @@ export interface CompileResult {
|
|
|
39
39
|
error?: DiagnosticError
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface SourceRange {
|
|
43
|
+
startLine: number
|
|
44
|
+
endLine: number
|
|
45
|
+
filePath: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PreprocessedSource {
|
|
49
|
+
source: string
|
|
50
|
+
ranges: SourceRange[]
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
|
|
43
54
|
|
|
44
55
|
interface PreprocessOptions {
|
|
@@ -46,7 +57,19 @@ interface PreprocessOptions {
|
|
|
46
57
|
seen?: Set<string>
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
function countLines(source: string): number {
|
|
61
|
+
return source === '' ? 0 : source.split('\n').length
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function offsetRanges(ranges: SourceRange[], lineOffset: number): SourceRange[] {
|
|
65
|
+
return ranges.map(range => ({
|
|
66
|
+
startLine: range.startLine + lineOffset,
|
|
67
|
+
endLine: range.endLine + lineOffset,
|
|
68
|
+
filePath: range.filePath,
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function preprocessSourceWithMetadata(source: string, options: PreprocessOptions = {}): PreprocessedSource {
|
|
50
73
|
const { filePath } = options
|
|
51
74
|
const seen = options.seen ?? new Set<string>()
|
|
52
75
|
|
|
@@ -55,7 +78,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
|
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
const lines = source.split('\n')
|
|
58
|
-
const imports:
|
|
81
|
+
const imports: PreprocessedSource[] = []
|
|
59
82
|
const bodyLines: string[] = []
|
|
60
83
|
let parsingHeader = true
|
|
61
84
|
|
|
@@ -90,7 +113,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
|
|
|
90
113
|
)
|
|
91
114
|
}
|
|
92
115
|
|
|
93
|
-
imports.push(
|
|
116
|
+
imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }))
|
|
94
117
|
}
|
|
95
118
|
continue
|
|
96
119
|
}
|
|
@@ -104,7 +127,31 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
|
|
|
104
127
|
bodyLines.push(line)
|
|
105
128
|
}
|
|
106
129
|
|
|
107
|
-
|
|
130
|
+
const body = bodyLines.join('\n')
|
|
131
|
+
const parts = [...imports.map(entry => entry.source), body].filter(Boolean)
|
|
132
|
+
const combined = parts.join('\n')
|
|
133
|
+
|
|
134
|
+
const ranges: SourceRange[] = []
|
|
135
|
+
let lineOffset = 0
|
|
136
|
+
|
|
137
|
+
for (const entry of imports) {
|
|
138
|
+
ranges.push(...offsetRanges(entry.ranges, lineOffset))
|
|
139
|
+
lineOffset += countLines(entry.source)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (filePath && body) {
|
|
143
|
+
ranges.push({
|
|
144
|
+
startLine: lineOffset + 1,
|
|
145
|
+
endLine: lineOffset + countLines(body),
|
|
146
|
+
filePath: path.resolve(filePath),
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { source: combined, ranges }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
|
|
154
|
+
return preprocessSourceWithMetadata(source, options).source
|
|
108
155
|
}
|
|
109
156
|
|
|
110
157
|
// ---------------------------------------------------------------------------
|
|
@@ -116,7 +163,8 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
|
|
|
116
163
|
let sourceLines = source.split('\n')
|
|
117
164
|
|
|
118
165
|
try {
|
|
119
|
-
const
|
|
166
|
+
const preprocessed = preprocessSourceWithMetadata(source, { filePath })
|
|
167
|
+
const preprocessedSource = preprocessed.source
|
|
120
168
|
sourceLines = preprocessedSource.split('\n')
|
|
121
169
|
|
|
122
170
|
// Lexing
|
|
@@ -126,7 +174,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
|
|
|
126
174
|
const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
|
|
127
175
|
|
|
128
176
|
// Lowering
|
|
129
|
-
const ir = new Lowering(namespace).lower(ast)
|
|
177
|
+
const ir = new Lowering(namespace, preprocessed.ranges).lower(ast)
|
|
130
178
|
|
|
131
179
|
// Optimization
|
|
132
180
|
const optimized: IRModule = shouldOptimize
|