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
|
@@ -23,6 +23,7 @@ describe('Parser', () => {
|
|
|
23
23
|
const program = parse('');
|
|
24
24
|
expect(program.namespace).toBe('test');
|
|
25
25
|
expect(program.declarations).toEqual([]);
|
|
26
|
+
expect(program.implBlocks).toEqual([]);
|
|
26
27
|
expect(program.enums).toEqual([]);
|
|
27
28
|
expect(program.consts).toEqual([]);
|
|
28
29
|
});
|
|
@@ -90,6 +91,12 @@ describe('Parser', () => {
|
|
|
90
91
|
{ name: 'on_death' },
|
|
91
92
|
]);
|
|
92
93
|
});
|
|
94
|
+
it('parses @on event decorators', () => {
|
|
95
|
+
const program = parse('@on(PlayerDeath)\nfn handle_death(player: Player) {}');
|
|
96
|
+
expect(program.declarations[0].decorators).toEqual([
|
|
97
|
+
{ name: 'on', args: { eventType: 'PlayerDeath' } },
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
93
100
|
});
|
|
94
101
|
describe('types', () => {
|
|
95
102
|
it('parses primitive types', () => {
|
|
@@ -134,6 +141,50 @@ describe('Parser', () => {
|
|
|
134
141
|
},
|
|
135
142
|
]);
|
|
136
143
|
});
|
|
144
|
+
it('parses impl blocks', () => {
|
|
145
|
+
const program = parse(`
|
|
146
|
+
struct Timer { duration: int }
|
|
147
|
+
|
|
148
|
+
impl Timer {
|
|
149
|
+
fn new(duration: int): Timer {
|
|
150
|
+
return { duration: duration };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn start(self) {}
|
|
154
|
+
}
|
|
155
|
+
`);
|
|
156
|
+
expect(program.implBlocks).toHaveLength(1);
|
|
157
|
+
expect(program.implBlocks[0].typeName).toBe('Timer');
|
|
158
|
+
expect(program.implBlocks[0].methods.map(method => method.name)).toEqual(['new', 'start']);
|
|
159
|
+
expect(program.implBlocks[0].methods[1].params[0]).toEqual({
|
|
160
|
+
name: 'self',
|
|
161
|
+
type: { kind: 'struct', name: 'Timer' },
|
|
162
|
+
default: undefined,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
it('parses impl blocks with static and instance methods', () => {
|
|
166
|
+
const program = parse(`
|
|
167
|
+
struct Point { x: int, y: int }
|
|
168
|
+
|
|
169
|
+
impl Point {
|
|
170
|
+
fn new(x: int, y: int) -> Point {
|
|
171
|
+
return { x: x, y: y };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn distance(self) -> int {
|
|
175
|
+
return self.x + self.y;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
`);
|
|
179
|
+
expect(program.implBlocks).toHaveLength(1);
|
|
180
|
+
expect(program.implBlocks[0].typeName).toBe('Point');
|
|
181
|
+
expect(program.implBlocks[0].methods[0].params.map(param => param.name)).toEqual(['x', 'y']);
|
|
182
|
+
expect(program.implBlocks[0].methods[1].params[0]).toEqual({
|
|
183
|
+
name: 'self',
|
|
184
|
+
type: { kind: 'struct', name: 'Point' },
|
|
185
|
+
default: undefined,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
137
188
|
});
|
|
138
189
|
describe('statements', () => {
|
|
139
190
|
it('parses let statement', () => {
|
|
@@ -173,6 +224,26 @@ describe('Parser', () => {
|
|
|
173
224
|
expect(stmt.kind).toBe('if');
|
|
174
225
|
expect(stmt.else_).toHaveLength(1);
|
|
175
226
|
});
|
|
227
|
+
it('parses entity is-checks in if conditions', () => {
|
|
228
|
+
const stmt = parseStmt('if (e is Player) { kill(@s); }');
|
|
229
|
+
expect(stmt.kind).toBe('if');
|
|
230
|
+
expect(stmt.cond).toEqual({
|
|
231
|
+
kind: 'is_check',
|
|
232
|
+
expr: { kind: 'ident', name: 'e' },
|
|
233
|
+
entityType: 'Player',
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
it('parses entity is-checks inside foreach bodies', () => {
|
|
237
|
+
const stmt = parseStmt('foreach (e in @e) { if (e is Zombie) { kill(e); } }');
|
|
238
|
+
expect(stmt.kind).toBe('foreach');
|
|
239
|
+
const innerIf = stmt.body[0];
|
|
240
|
+
expect(innerIf.kind).toBe('if');
|
|
241
|
+
expect(innerIf.cond).toEqual({
|
|
242
|
+
kind: 'is_check',
|
|
243
|
+
expr: { kind: 'ident', name: 'e' },
|
|
244
|
+
entityType: 'Zombie',
|
|
245
|
+
});
|
|
246
|
+
});
|
|
176
247
|
it('parses while statement', () => {
|
|
177
248
|
const stmt = parseStmt('while (i > 0) { i = i - 1; }');
|
|
178
249
|
expect(stmt.kind).toBe('while');
|
|
@@ -429,6 +500,15 @@ describe('Parser', () => {
|
|
|
429
500
|
});
|
|
430
501
|
});
|
|
431
502
|
});
|
|
503
|
+
it('parses static method calls', () => {
|
|
504
|
+
const expr = parseExpr('Timer::new(100)');
|
|
505
|
+
expect(expr).toEqual({
|
|
506
|
+
kind: 'static_call',
|
|
507
|
+
type: 'Timer',
|
|
508
|
+
method: 'new',
|
|
509
|
+
args: [{ kind: 'int_lit', value: 100 }],
|
|
510
|
+
});
|
|
511
|
+
});
|
|
432
512
|
describe('binary operators', () => {
|
|
433
513
|
it('parses arithmetic', () => {
|
|
434
514
|
const expr = parseExpr('1 + 2');
|
|
@@ -81,7 +81,7 @@ fn compute() {
|
|
|
81
81
|
`);
|
|
82
82
|
runtime.load();
|
|
83
83
|
runtime.execFunction('compute');
|
|
84
|
-
expect(runtime.getScore('math', 'result')).toBe(11);
|
|
84
|
+
expect(runtime.getScore('math', 'runtime.result')).toBe(11);
|
|
85
85
|
});
|
|
86
86
|
it('captures say, announce, actionbar, and title output in the chat log', () => {
|
|
87
87
|
const runtime = loadCompiledProgram(`
|
|
@@ -146,8 +146,8 @@ fn arrays() {
|
|
|
146
146
|
`);
|
|
147
147
|
runtime.load();
|
|
148
148
|
runtime.execFunction('arrays');
|
|
149
|
-
expect(runtime.getScore('arrays', 'len')).toBe(1);
|
|
150
|
-
expect(runtime.getScore('arrays', 'last')).toBe(9);
|
|
149
|
+
expect(runtime.getScore('arrays', 'runtime.len')).toBe(1);
|
|
150
|
+
expect(runtime.getScore('arrays', 'runtime.last')).toBe(9);
|
|
151
151
|
expect(runtime.getStorage('rs:heap.arr')).toEqual([4]);
|
|
152
152
|
});
|
|
153
153
|
it('tracks world state, weather, and time from compiled world commands', () => {
|
|
@@ -182,7 +182,7 @@ fn pulse() {
|
|
|
182
182
|
`);
|
|
183
183
|
runtime.load();
|
|
184
184
|
runtime.ticks(10);
|
|
185
|
-
expect(runtime.getScore('pulse', 'count')).toBe(2);
|
|
185
|
+
expect(runtime.getScore('pulse', 'runtime.count')).toBe(2);
|
|
186
186
|
});
|
|
187
187
|
it('executes only the matching match arm', () => {
|
|
188
188
|
const runtime = loadCompiledProgram(`
|
|
@@ -229,7 +229,7 @@ fn test() {
|
|
|
229
229
|
`);
|
|
230
230
|
runtime.load();
|
|
231
231
|
runtime.execFunction('test');
|
|
232
|
-
expect(runtime.getScore('lambda', 'direct')).toBe(10);
|
|
232
|
+
expect(runtime.getScore('lambda', 'runtime.direct')).toBe(10);
|
|
233
233
|
});
|
|
234
234
|
it('executes lambdas passed as callback arguments', () => {
|
|
235
235
|
const runtime = loadCompiledProgram(`
|
|
@@ -244,7 +244,7 @@ fn test() {
|
|
|
244
244
|
`);
|
|
245
245
|
runtime.load();
|
|
246
246
|
runtime.execFunction('test');
|
|
247
|
-
expect(runtime.getScore('lambda', 'callback')).toBe(15);
|
|
247
|
+
expect(runtime.getScore('lambda', 'runtime.callback')).toBe(15);
|
|
248
248
|
});
|
|
249
249
|
it('executes block-body lambdas', () => {
|
|
250
250
|
const runtime = loadCompiledProgram(`
|
|
@@ -259,7 +259,7 @@ fn test() {
|
|
|
259
259
|
`);
|
|
260
260
|
runtime.load();
|
|
261
261
|
runtime.execFunction('test');
|
|
262
|
-
expect(runtime.getScore('lambda', 'block')).toBe(11);
|
|
262
|
+
expect(runtime.getScore('lambda', 'runtime.block')).toBe(11);
|
|
263
263
|
});
|
|
264
264
|
it('executes immediately-invoked expression-body lambdas', () => {
|
|
265
265
|
const runtime = loadCompiledProgram(`
|
|
@@ -270,7 +270,7 @@ fn test() {
|
|
|
270
270
|
`);
|
|
271
271
|
runtime.load();
|
|
272
272
|
runtime.execFunction('test');
|
|
273
|
-
expect(runtime.getScore('lambda', 'iife')).toBe(10);
|
|
273
|
+
expect(runtime.getScore('lambda', 'runtime.iife')).toBe(10);
|
|
274
274
|
});
|
|
275
275
|
});
|
|
276
276
|
//# sourceMappingURL=runtime.test.js.map
|
|
@@ -193,6 +193,137 @@ fn test() {
|
|
|
193
193
|
`);
|
|
194
194
|
expect(errors).toHaveLength(0);
|
|
195
195
|
});
|
|
196
|
+
it('type checks timer builtins with void callbacks and interval IDs', () => {
|
|
197
|
+
const errors = typeCheck(`
|
|
198
|
+
fn test() {
|
|
199
|
+
setTimeout(100, () => {
|
|
200
|
+
say("later");
|
|
201
|
+
});
|
|
202
|
+
let intervalId: int = setInterval(20, () => {
|
|
203
|
+
say("tick");
|
|
204
|
+
});
|
|
205
|
+
clearInterval(intervalId);
|
|
206
|
+
}
|
|
207
|
+
`);
|
|
208
|
+
expect(errors).toHaveLength(0);
|
|
209
|
+
});
|
|
210
|
+
it('rejects timer callbacks with the wrong return type', () => {
|
|
211
|
+
const errors = typeCheck(`
|
|
212
|
+
fn test() {
|
|
213
|
+
setTimeout(100, () => 1);
|
|
214
|
+
}
|
|
215
|
+
`);
|
|
216
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
217
|
+
expect(errors[0].message).toContain('Return type mismatch: expected void, got int');
|
|
218
|
+
});
|
|
219
|
+
it('allows impl instance methods with inferred self type', () => {
|
|
220
|
+
const errors = typeCheck(`
|
|
221
|
+
struct Timer { duration: int }
|
|
222
|
+
|
|
223
|
+
impl Timer {
|
|
224
|
+
fn elapsed(self) -> int {
|
|
225
|
+
return self.duration;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn test() {
|
|
230
|
+
let timer: Timer = { duration: 10 };
|
|
231
|
+
let value: int = timer.elapsed();
|
|
232
|
+
}
|
|
233
|
+
`);
|
|
234
|
+
expect(errors).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
it('records then-branch entity narrowing for is-checks', () => {
|
|
237
|
+
const source = `
|
|
238
|
+
fn test() {
|
|
239
|
+
foreach (e in @e) {
|
|
240
|
+
if (e is Player) {
|
|
241
|
+
kill(e);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
const tokens = new lexer_1.Lexer(source).tokenize();
|
|
247
|
+
const ast = new parser_1.Parser(tokens).parse('test');
|
|
248
|
+
const checker = new typechecker_1.TypeChecker(source);
|
|
249
|
+
checker.check(ast);
|
|
250
|
+
const foreachStmt = ast.declarations[0].body[0];
|
|
251
|
+
const ifStmt = foreachStmt.body[0];
|
|
252
|
+
expect(checker.getThenBranchNarrowing(ifStmt.cond)).toEqual({
|
|
253
|
+
name: 'e',
|
|
254
|
+
type: { kind: 'entity', entityType: 'Player' },
|
|
255
|
+
mutable: false,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
it('allows static impl method calls', () => {
|
|
259
|
+
const errors = typeCheck(`
|
|
260
|
+
struct Timer { duration: int }
|
|
261
|
+
|
|
262
|
+
impl Timer {
|
|
263
|
+
fn new(duration: int) -> Timer {
|
|
264
|
+
return { duration: duration };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn test() {
|
|
269
|
+
let timer: Timer = Timer::new(10);
|
|
270
|
+
}
|
|
271
|
+
`);
|
|
272
|
+
expect(errors).toHaveLength(0);
|
|
273
|
+
});
|
|
274
|
+
it('rejects using is-checks on non-entity values', () => {
|
|
275
|
+
const errors = typeCheck(`
|
|
276
|
+
fn test() {
|
|
277
|
+
let x: int = 1;
|
|
278
|
+
if (x is Player) {
|
|
279
|
+
say("nope");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
`);
|
|
283
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
284
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression, got int");
|
|
285
|
+
});
|
|
286
|
+
it('rejects calling instance impl methods as static methods', () => {
|
|
287
|
+
const errors = typeCheck(`
|
|
288
|
+
struct Point { x: int, y: int }
|
|
289
|
+
|
|
290
|
+
impl Point {
|
|
291
|
+
fn distance(self) -> int {
|
|
292
|
+
return self.x + self.y;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn test() {
|
|
297
|
+
let total: int = Point::distance();
|
|
298
|
+
}
|
|
299
|
+
`);
|
|
300
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
301
|
+
expect(errors[0].message).toContain("Method 'Point::distance' is an instance method");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
describe('entity is-check narrowing', () => {
|
|
305
|
+
it('allows entity type checks on foreach bindings', () => {
|
|
306
|
+
const errors = typeCheck(`
|
|
307
|
+
fn test() {
|
|
308
|
+
foreach (e in @e) {
|
|
309
|
+
if (e is Player) {
|
|
310
|
+
kill(@s);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
`);
|
|
315
|
+
expect(errors).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
it('rejects is-checks on non-entity expressions', () => {
|
|
318
|
+
const errors = typeCheck(`
|
|
319
|
+
fn test() {
|
|
320
|
+
let x: int = 1;
|
|
321
|
+
if (x is Player) {}
|
|
322
|
+
}
|
|
323
|
+
`);
|
|
324
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
325
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression");
|
|
326
|
+
});
|
|
196
327
|
});
|
|
197
328
|
describe('return type checking', () => {
|
|
198
329
|
it('allows matching return type', () => {
|
|
@@ -360,5 +491,32 @@ fn broken() -> int {
|
|
|
360
491
|
expect(errors.length).toBeGreaterThanOrEqual(3);
|
|
361
492
|
});
|
|
362
493
|
});
|
|
494
|
+
describe('event handlers', () => {
|
|
495
|
+
it('accepts matching @on event signatures', () => {
|
|
496
|
+
const errors = typeCheck(`
|
|
497
|
+
@on(PlayerDeath)
|
|
498
|
+
fn handle_death(player: Player) {
|
|
499
|
+
tp(player, @p);
|
|
500
|
+
}
|
|
501
|
+
`);
|
|
502
|
+
expect(errors).toHaveLength(0);
|
|
503
|
+
});
|
|
504
|
+
it('rejects unknown event types', () => {
|
|
505
|
+
const errors = typeCheck(`
|
|
506
|
+
@on(NotARealEvent)
|
|
507
|
+
fn handle(player: Player) {}
|
|
508
|
+
`);
|
|
509
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
510
|
+
expect(errors[0].message).toContain("Unknown event type 'NotARealEvent'");
|
|
511
|
+
});
|
|
512
|
+
it('rejects mismatched event signatures', () => {
|
|
513
|
+
const errors = typeCheck(`
|
|
514
|
+
@on(BlockBreak)
|
|
515
|
+
fn handle_break(player: Player) {}
|
|
516
|
+
`);
|
|
517
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
518
|
+
expect(errors[0].message).toContain('must declare 2 parameter(s)');
|
|
519
|
+
});
|
|
520
|
+
});
|
|
363
521
|
});
|
|
364
522
|
//# sourceMappingURL=typechecker.test.js.map
|
package/dist/ast/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface Span {
|
|
|
12
12
|
endCol?: number;
|
|
13
13
|
}
|
|
14
14
|
export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double';
|
|
15
|
+
export type EntityTypeName = 'entity' | 'Player' | 'Mob' | 'HostileMob' | 'PassiveMob' | 'Zombie' | 'Skeleton' | 'Creeper' | 'Spider' | 'Enderman' | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager' | 'ArmorStand' | 'Item' | 'Arrow';
|
|
15
16
|
export type TypeNode = {
|
|
16
17
|
kind: 'named';
|
|
17
18
|
name: PrimitiveType;
|
|
@@ -28,6 +29,11 @@ export type TypeNode = {
|
|
|
28
29
|
kind: 'function_type';
|
|
29
30
|
params: TypeNode[];
|
|
30
31
|
return: TypeNode;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'entity';
|
|
34
|
+
entityType: EntityTypeName;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'selector';
|
|
31
37
|
};
|
|
32
38
|
export interface LambdaParam {
|
|
33
39
|
name: string;
|
|
@@ -143,6 +149,11 @@ export type Expr = {
|
|
|
143
149
|
left: Expr;
|
|
144
150
|
right: Expr;
|
|
145
151
|
span?: Span;
|
|
152
|
+
} | {
|
|
153
|
+
kind: 'is_check';
|
|
154
|
+
expr: Expr;
|
|
155
|
+
entityType: EntityTypeName;
|
|
156
|
+
span?: Span;
|
|
146
157
|
} | {
|
|
147
158
|
kind: 'unary';
|
|
148
159
|
op: '!' | '-';
|
|
@@ -311,9 +322,10 @@ export type Stmt = {
|
|
|
311
322
|
};
|
|
312
323
|
export type Block = Stmt[];
|
|
313
324
|
export interface Decorator {
|
|
314
|
-
name: 'tick' | 'load' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team';
|
|
325
|
+
name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team';
|
|
315
326
|
args?: {
|
|
316
327
|
rate?: number;
|
|
328
|
+
eventType?: string;
|
|
317
329
|
trigger?: string;
|
|
318
330
|
advancement?: string;
|
|
319
331
|
item?: string;
|
|
@@ -342,6 +354,12 @@ export interface StructDecl {
|
|
|
342
354
|
fields: StructField[];
|
|
343
355
|
span?: Span;
|
|
344
356
|
}
|
|
357
|
+
export interface ImplBlock {
|
|
358
|
+
kind: 'impl_block';
|
|
359
|
+
typeName: string;
|
|
360
|
+
methods: FnDecl[];
|
|
361
|
+
span?: Span;
|
|
362
|
+
}
|
|
345
363
|
export interface EnumVariant {
|
|
346
364
|
name: string;
|
|
347
365
|
value?: number;
|
|
@@ -370,6 +388,7 @@ export interface Program {
|
|
|
370
388
|
globals: GlobalDecl[];
|
|
371
389
|
declarations: FnDecl[];
|
|
372
390
|
structs: StructDecl[];
|
|
391
|
+
implBlocks: ImplBlock[];
|
|
373
392
|
enums: EnumDecl[];
|
|
374
393
|
consts: ConstDecl[];
|
|
375
394
|
}
|
|
@@ -21,6 +21,7 @@ exports.countMcfunctionCommands = countMcfunctionCommands;
|
|
|
21
21
|
exports.generateDatapackWithStats = generateDatapackWithStats;
|
|
22
22
|
exports.generateDatapack = generateDatapack;
|
|
23
23
|
const commands_1 = require("../../optimizer/commands");
|
|
24
|
+
const types_1 = require("../../events/types");
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Utilities
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
@@ -238,6 +239,8 @@ function generateDatapackWithStats(module, options = {}) {
|
|
|
238
239
|
// Collect all trigger handlers
|
|
239
240
|
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName);
|
|
240
241
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName));
|
|
242
|
+
const eventHandlers = module.functions.filter((fn) => !!fn.eventHandler && (0, types_1.isEventTypeName)(fn.eventHandler.eventType));
|
|
243
|
+
const eventTypes = new Set(eventHandlers.map(fn => fn.eventHandler.eventType));
|
|
241
244
|
// Collect all tick functions
|
|
242
245
|
const tickFunctionNames = [];
|
|
243
246
|
for (const fn of module.functions) {
|
|
@@ -265,6 +268,21 @@ function generateDatapackWithStats(module, options = {}) {
|
|
|
265
268
|
loadLines.push(`scoreboard objectives add ${triggerName} trigger`);
|
|
266
269
|
loadLines.push(`scoreboard players enable @a ${triggerName}`);
|
|
267
270
|
}
|
|
271
|
+
for (const eventType of eventTypes) {
|
|
272
|
+
const detection = types_1.EVENT_TYPES[eventType].detection;
|
|
273
|
+
if (eventType === 'PlayerDeath') {
|
|
274
|
+
loadLines.push('scoreboard objectives add rs.deaths deathCount');
|
|
275
|
+
}
|
|
276
|
+
else if (eventType === 'EntityKill') {
|
|
277
|
+
loadLines.push('scoreboard objectives add rs.kills totalKillCount');
|
|
278
|
+
}
|
|
279
|
+
else if (eventType === 'ItemUse') {
|
|
280
|
+
loadLines.push('# ItemUse detection requires a project-specific objective/tag setup');
|
|
281
|
+
}
|
|
282
|
+
else if (detection === 'tag' || detection === 'advancement') {
|
|
283
|
+
loadLines.push(`# ${eventType} detection expects tag ${types_1.EVENT_TYPES[eventType].tag} to be set externally`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
268
286
|
// Generate trigger dispatch functions
|
|
269
287
|
for (const triggerName of triggerNames) {
|
|
270
288
|
const handlers = triggerHandlers.filter(fn => fn.triggerName === triggerName);
|
|
@@ -339,8 +357,19 @@ function generateDatapackWithStats(module, options = {}) {
|
|
|
339
357
|
tickLines.push(`execute as @a[scores={${triggerName}=1..}] run function ${ns}:__trigger_${triggerName}_dispatch`);
|
|
340
358
|
}
|
|
341
359
|
}
|
|
360
|
+
if (eventHandlers.length > 0) {
|
|
361
|
+
tickLines.push('# Event checks');
|
|
362
|
+
for (const eventType of eventTypes) {
|
|
363
|
+
const tag = types_1.EVENT_TYPES[eventType].tag;
|
|
364
|
+
const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType);
|
|
365
|
+
for (const handler of handlers) {
|
|
366
|
+
tickLines.push(`execute as @a[tag=${tag}] run function ${ns}:${handler.name}`);
|
|
367
|
+
}
|
|
368
|
+
tickLines.push(`tag @a[tag=${tag}] remove ${tag}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
342
371
|
// Only generate __tick if there's something to run
|
|
343
|
-
if (tickFunctionNames.length > 0 || triggerNames.size > 0) {
|
|
372
|
+
if (tickFunctionNames.length > 0 || triggerNames.size > 0 || eventHandlers.length > 0) {
|
|
344
373
|
files.push({
|
|
345
374
|
path: `data/${ns}/function/__tick.mcfunction`,
|
|
346
375
|
content: tickLines.join('\n'),
|
|
@@ -10,6 +10,7 @@ const commands_1 = require("../../optimizer/commands");
|
|
|
10
10
|
const passes_1 = require("../../optimizer/passes");
|
|
11
11
|
const structure_1 = require("../../optimizer/structure");
|
|
12
12
|
const compile_1 = require("../../compile");
|
|
13
|
+
const types_1 = require("../../events/types");
|
|
13
14
|
const DATA_VERSION = 3953;
|
|
14
15
|
const MAX_WIDTH = 16;
|
|
15
16
|
const OBJ = 'rs';
|
|
@@ -62,6 +63,8 @@ function collectCommandEntriesFromModule(module) {
|
|
|
62
63
|
const entries = [];
|
|
63
64
|
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName);
|
|
64
65
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName));
|
|
66
|
+
const eventHandlers = module.functions.filter((fn) => !!fn.eventHandler && (0, types_1.isEventTypeName)(fn.eventHandler.eventType));
|
|
67
|
+
const eventTypes = new Set(eventHandlers.map(fn => fn.eventHandler.eventType));
|
|
65
68
|
const loadCommands = [
|
|
66
69
|
`scoreboard objectives add ${OBJ} dummy`,
|
|
67
70
|
...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
|
|
@@ -71,6 +74,14 @@ function collectCommandEntriesFromModule(module) {
|
|
|
71
74
|
]),
|
|
72
75
|
...Array.from(new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))).map(constSetup),
|
|
73
76
|
];
|
|
77
|
+
for (const eventType of eventTypes) {
|
|
78
|
+
if (eventType === 'PlayerDeath') {
|
|
79
|
+
loadCommands.push('scoreboard objectives add rs.deaths deathCount');
|
|
80
|
+
}
|
|
81
|
+
else if (eventType === 'EntityKill') {
|
|
82
|
+
loadCommands.push('scoreboard objectives add rs.kills totalKillCount');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
74
85
|
// Call @load functions from __load
|
|
75
86
|
for (const fn of module.functions) {
|
|
76
87
|
if (fn.isLoadInit) {
|
|
@@ -114,6 +125,20 @@ function collectCommandEntriesFromModule(module) {
|
|
|
114
125
|
});
|
|
115
126
|
}
|
|
116
127
|
}
|
|
128
|
+
if (eventHandlers.length > 0) {
|
|
129
|
+
for (const eventType of eventTypes) {
|
|
130
|
+
const tag = types_1.EVENT_TYPES[eventType].tag;
|
|
131
|
+
const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType);
|
|
132
|
+
for (const handler of handlers) {
|
|
133
|
+
tickCommands.push({
|
|
134
|
+
cmd: `execute as @a[tag=${tag}] run function ${module.namespace}:${handler.name}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
tickCommands.push({
|
|
138
|
+
cmd: `tag @a[tag=${tag}] remove ${tag}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
117
142
|
if (tickCommands.length > 0) {
|
|
118
143
|
sections.push({
|
|
119
144
|
name: '__tick',
|
package/dist/compile.d.ts
CHANGED
|
@@ -20,10 +20,20 @@ export interface CompileResult {
|
|
|
20
20
|
ir?: IRModule;
|
|
21
21
|
error?: DiagnosticError;
|
|
22
22
|
}
|
|
23
|
+
export interface SourceRange {
|
|
24
|
+
startLine: number;
|
|
25
|
+
endLine: number;
|
|
26
|
+
filePath: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PreprocessedSource {
|
|
29
|
+
source: string;
|
|
30
|
+
ranges: SourceRange[];
|
|
31
|
+
}
|
|
23
32
|
interface PreprocessOptions {
|
|
24
33
|
filePath?: string;
|
|
25
34
|
seen?: Set<string>;
|
|
26
35
|
}
|
|
36
|
+
export declare function preprocessSourceWithMetadata(source: string, options?: PreprocessOptions): PreprocessedSource;
|
|
27
37
|
export declare function preprocessSource(source: string, options?: PreprocessOptions): string;
|
|
28
38
|
export declare function compile(source: string, options?: CompileOptions): CompileResult;
|
|
29
39
|
export declare function formatCompileError(result: CompileResult): string;
|
package/dist/compile.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
};
|
|
39
39
|
})();
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.preprocessSourceWithMetadata = preprocessSourceWithMetadata;
|
|
41
42
|
exports.preprocessSource = preprocessSource;
|
|
42
43
|
exports.compile = compile;
|
|
43
44
|
exports.formatCompileError = formatCompileError;
|
|
@@ -50,7 +51,17 @@ const passes_1 = require("./optimizer/passes");
|
|
|
50
51
|
const mcfunction_1 = require("./codegen/mcfunction");
|
|
51
52
|
const diagnostics_1 = require("./diagnostics");
|
|
52
53
|
const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/;
|
|
53
|
-
function
|
|
54
|
+
function countLines(source) {
|
|
55
|
+
return source === '' ? 0 : source.split('\n').length;
|
|
56
|
+
}
|
|
57
|
+
function offsetRanges(ranges, lineOffset) {
|
|
58
|
+
return ranges.map(range => ({
|
|
59
|
+
startLine: range.startLine + lineOffset,
|
|
60
|
+
endLine: range.endLine + lineOffset,
|
|
61
|
+
filePath: range.filePath,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
function preprocessSourceWithMetadata(source, options = {}) {
|
|
54
65
|
const { filePath } = options;
|
|
55
66
|
const seen = options.seen ?? new Set();
|
|
56
67
|
if (filePath) {
|
|
@@ -78,7 +89,7 @@ function preprocessSource(source, options = {}) {
|
|
|
78
89
|
catch {
|
|
79
90
|
throw new diagnostics_1.DiagnosticError('ParseError', `Cannot import '${match[1]}'`, { file: filePath, line: i + 1, col: 1 }, lines);
|
|
80
91
|
}
|
|
81
|
-
imports.push(
|
|
92
|
+
imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }));
|
|
82
93
|
}
|
|
83
94
|
continue;
|
|
84
95
|
}
|
|
@@ -89,7 +100,26 @@ function preprocessSource(source, options = {}) {
|
|
|
89
100
|
parsingHeader = false;
|
|
90
101
|
bodyLines.push(line);
|
|
91
102
|
}
|
|
92
|
-
|
|
103
|
+
const body = bodyLines.join('\n');
|
|
104
|
+
const parts = [...imports.map(entry => entry.source), body].filter(Boolean);
|
|
105
|
+
const combined = parts.join('\n');
|
|
106
|
+
const ranges = [];
|
|
107
|
+
let lineOffset = 0;
|
|
108
|
+
for (const entry of imports) {
|
|
109
|
+
ranges.push(...offsetRanges(entry.ranges, lineOffset));
|
|
110
|
+
lineOffset += countLines(entry.source);
|
|
111
|
+
}
|
|
112
|
+
if (filePath && body) {
|
|
113
|
+
ranges.push({
|
|
114
|
+
startLine: lineOffset + 1,
|
|
115
|
+
endLine: lineOffset + countLines(body),
|
|
116
|
+
filePath: path.resolve(filePath),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return { source: combined, ranges };
|
|
120
|
+
}
|
|
121
|
+
function preprocessSource(source, options = {}) {
|
|
122
|
+
return preprocessSourceWithMetadata(source, options).source;
|
|
93
123
|
}
|
|
94
124
|
// ---------------------------------------------------------------------------
|
|
95
125
|
// Main Compile Function
|
|
@@ -98,14 +128,15 @@ function compile(source, options = {}) {
|
|
|
98
128
|
const { namespace = 'redscript', filePath, optimize: shouldOptimize = true } = options;
|
|
99
129
|
let sourceLines = source.split('\n');
|
|
100
130
|
try {
|
|
101
|
-
const
|
|
131
|
+
const preprocessed = preprocessSourceWithMetadata(source, { filePath });
|
|
132
|
+
const preprocessedSource = preprocessed.source;
|
|
102
133
|
sourceLines = preprocessedSource.split('\n');
|
|
103
134
|
// Lexing
|
|
104
135
|
const tokens = new lexer_1.Lexer(preprocessedSource, filePath).tokenize();
|
|
105
136
|
// Parsing
|
|
106
137
|
const ast = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
|
|
107
138
|
// Lowering
|
|
108
|
-
const ir = new lowering_1.Lowering(namespace).lower(ast);
|
|
139
|
+
const ir = new lowering_1.Lowering(namespace, preprocessed.ranges).lower(ast);
|
|
109
140
|
// Optimization
|
|
110
141
|
const optimized = shouldOptimize
|
|
111
142
|
? { ...ir, functions: ir.functions.map(fn => (0, passes_1.optimize)(fn)) }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TypeNode } from '../ast/types';
|
|
2
|
+
export declare const EVENT_TYPES: {
|
|
3
|
+
readonly PlayerDeath: {
|
|
4
|
+
readonly tag: "rs.just_died";
|
|
5
|
+
readonly params: readonly ["player: Player"];
|
|
6
|
+
readonly detection: "scoreboard";
|
|
7
|
+
};
|
|
8
|
+
readonly PlayerJoin: {
|
|
9
|
+
readonly tag: "rs.just_joined";
|
|
10
|
+
readonly params: readonly ["player: Player"];
|
|
11
|
+
readonly detection: "tag";
|
|
12
|
+
};
|
|
13
|
+
readonly BlockBreak: {
|
|
14
|
+
readonly tag: "rs.just_broke_block";
|
|
15
|
+
readonly params: readonly ["player: Player", "block: string"];
|
|
16
|
+
readonly detection: "advancement";
|
|
17
|
+
};
|
|
18
|
+
readonly EntityKill: {
|
|
19
|
+
readonly tag: "rs.just_killed";
|
|
20
|
+
readonly params: readonly ["player: Player"];
|
|
21
|
+
readonly detection: "scoreboard";
|
|
22
|
+
};
|
|
23
|
+
readonly ItemUse: {
|
|
24
|
+
readonly tag: "rs.just_used_item";
|
|
25
|
+
readonly params: readonly ["player: Player"];
|
|
26
|
+
readonly detection: "scoreboard";
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type EventTypeName = keyof typeof EVENT_TYPES;
|
|
30
|
+
export interface EventParamSpec {
|
|
31
|
+
name: string;
|
|
32
|
+
type: TypeNode;
|
|
33
|
+
}
|
|
34
|
+
export declare function isEventTypeName(value: string): value is EventTypeName;
|
|
35
|
+
export declare function getEventParamSpecs(eventType: EventTypeName): EventParamSpec[];
|