redscript-mc 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +31 -4
- package/dist/__tests__/lowering.test.js +172 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +90 -0
- package/dist/__tests__/runtime.test.js +21 -8
- package/dist/__tests__/typechecker.test.js +188 -0
- package/dist/ast/types.d.ts +42 -3
- package/dist/cli.js +15 -10
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +29 -2
- package/dist/compile.d.ts +11 -0
- package/dist/compile.js +40 -6
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -3
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +91 -1
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +476 -16
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +591 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +160 -26
- package/dist/typechecker/index.d.ts +19 -0
- package/dist/typechecker/index.js +392 -17
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/out/extension.js +1144 -72
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
- package/examples/spiral.mcrs +79 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/dce.test.ts +129 -0
- package/src/__tests__/e2e.test.ts +201 -12
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +35 -4
- package/src/__tests__/lowering.test.ts +187 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +102 -5
- package/src/__tests__/runtime.test.ts +24 -8
- package/src/__tests__/typechecker.test.ts +204 -0
- package/src/ast/types.ts +39 -2
- package/src/cli.ts +24 -10
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +40 -2
- package/src/compile.ts +59 -7
- package/src/events/types.ts +69 -0
- package/src/index.ts +9 -4
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +105 -2
- package/src/lowering/index.ts +566 -18
- package/src/optimizer/dce.ts +618 -0
- package/src/parser/index.ts +187 -29
- package/src/stdlib/README.md +34 -4
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/timer.mcrs +54 -33
- package/src/typechecker/index.ts +469 -18
|
@@ -110,6 +110,36 @@ fn test() {
|
|
|
110
110
|
`);
|
|
111
111
|
expect(errors).toHaveLength(0);
|
|
112
112
|
});
|
|
113
|
+
it('allows f-strings in runtime output builtins', () => {
|
|
114
|
+
const errors = typeCheck(`
|
|
115
|
+
fn test() {
|
|
116
|
+
let score: int = 5;
|
|
117
|
+
say(f"Score: {score}");
|
|
118
|
+
tellraw(@a, f"Score: {score}");
|
|
119
|
+
actionbar(@s, f"Score: {score}");
|
|
120
|
+
title(@s, f"Score: {score}");
|
|
121
|
+
}
|
|
122
|
+
`);
|
|
123
|
+
expect(errors).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
it('rejects f-strings outside runtime output builtins', () => {
|
|
126
|
+
const errors = typeCheck(`
|
|
127
|
+
fn test() {
|
|
128
|
+
let msg: string = f"Score";
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
132
|
+
expect(errors[0].message).toContain('expected string, got format_string');
|
|
133
|
+
});
|
|
134
|
+
it('rejects unsupported f-string placeholder types', () => {
|
|
135
|
+
const errors = typeCheck(`
|
|
136
|
+
fn test() {
|
|
137
|
+
say(f"Flag: {true}");
|
|
138
|
+
}
|
|
139
|
+
`);
|
|
140
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
141
|
+
expect(errors[0].message).toContain('f-string placeholder must be int or string');
|
|
142
|
+
});
|
|
113
143
|
it('detects too many arguments', () => {
|
|
114
144
|
const errors = typeCheck(`
|
|
115
145
|
fn greet() {
|
|
@@ -193,6 +223,137 @@ fn test() {
|
|
|
193
223
|
`);
|
|
194
224
|
expect(errors).toHaveLength(0);
|
|
195
225
|
});
|
|
226
|
+
it('type checks timer builtins with void callbacks and interval IDs', () => {
|
|
227
|
+
const errors = typeCheck(`
|
|
228
|
+
fn test() {
|
|
229
|
+
setTimeout(100, () => {
|
|
230
|
+
say("later");
|
|
231
|
+
});
|
|
232
|
+
let intervalId: int = setInterval(20, () => {
|
|
233
|
+
say("tick");
|
|
234
|
+
});
|
|
235
|
+
clearInterval(intervalId);
|
|
236
|
+
}
|
|
237
|
+
`);
|
|
238
|
+
expect(errors).toHaveLength(0);
|
|
239
|
+
});
|
|
240
|
+
it('rejects timer callbacks with the wrong return type', () => {
|
|
241
|
+
const errors = typeCheck(`
|
|
242
|
+
fn test() {
|
|
243
|
+
setTimeout(100, () => 1);
|
|
244
|
+
}
|
|
245
|
+
`);
|
|
246
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
247
|
+
expect(errors[0].message).toContain('Return type mismatch: expected void, got int');
|
|
248
|
+
});
|
|
249
|
+
it('allows impl instance methods with inferred self type', () => {
|
|
250
|
+
const errors = typeCheck(`
|
|
251
|
+
struct Timer { duration: int }
|
|
252
|
+
|
|
253
|
+
impl Timer {
|
|
254
|
+
fn elapsed(self) -> int {
|
|
255
|
+
return self.duration;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn test() {
|
|
260
|
+
let timer: Timer = { duration: 10 };
|
|
261
|
+
let value: int = timer.elapsed();
|
|
262
|
+
}
|
|
263
|
+
`);
|
|
264
|
+
expect(errors).toHaveLength(0);
|
|
265
|
+
});
|
|
266
|
+
it('records then-branch entity narrowing for is-checks', () => {
|
|
267
|
+
const source = `
|
|
268
|
+
fn test() {
|
|
269
|
+
foreach (e in @e) {
|
|
270
|
+
if (e is Player) {
|
|
271
|
+
kill(e);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
const tokens = new lexer_1.Lexer(source).tokenize();
|
|
277
|
+
const ast = new parser_1.Parser(tokens).parse('test');
|
|
278
|
+
const checker = new typechecker_1.TypeChecker(source);
|
|
279
|
+
checker.check(ast);
|
|
280
|
+
const foreachStmt = ast.declarations[0].body[0];
|
|
281
|
+
const ifStmt = foreachStmt.body[0];
|
|
282
|
+
expect(checker.getThenBranchNarrowing(ifStmt.cond)).toEqual({
|
|
283
|
+
name: 'e',
|
|
284
|
+
type: { kind: 'entity', entityType: 'Player' },
|
|
285
|
+
mutable: false,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
it('allows static impl method calls', () => {
|
|
289
|
+
const errors = typeCheck(`
|
|
290
|
+
struct Timer { duration: int }
|
|
291
|
+
|
|
292
|
+
impl Timer {
|
|
293
|
+
fn new(duration: int) -> Timer {
|
|
294
|
+
return { duration: duration };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn test() {
|
|
299
|
+
let timer: Timer = Timer::new(10);
|
|
300
|
+
}
|
|
301
|
+
`);
|
|
302
|
+
expect(errors).toHaveLength(0);
|
|
303
|
+
});
|
|
304
|
+
it('rejects using is-checks on non-entity values', () => {
|
|
305
|
+
const errors = typeCheck(`
|
|
306
|
+
fn test() {
|
|
307
|
+
let x: int = 1;
|
|
308
|
+
if (x is Player) {
|
|
309
|
+
say("nope");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
`);
|
|
313
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
314
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression, got int");
|
|
315
|
+
});
|
|
316
|
+
it('rejects calling instance impl methods as static methods', () => {
|
|
317
|
+
const errors = typeCheck(`
|
|
318
|
+
struct Point { x: int, y: int }
|
|
319
|
+
|
|
320
|
+
impl Point {
|
|
321
|
+
fn distance(self) -> int {
|
|
322
|
+
return self.x + self.y;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn test() {
|
|
327
|
+
let total: int = Point::distance();
|
|
328
|
+
}
|
|
329
|
+
`);
|
|
330
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
331
|
+
expect(errors[0].message).toContain("Method 'Point::distance' is an instance method");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
describe('entity is-check narrowing', () => {
|
|
335
|
+
it('allows entity type checks on foreach bindings', () => {
|
|
336
|
+
const errors = typeCheck(`
|
|
337
|
+
fn test() {
|
|
338
|
+
foreach (e in @e) {
|
|
339
|
+
if (e is Player) {
|
|
340
|
+
kill(@s);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
`);
|
|
345
|
+
expect(errors).toHaveLength(0);
|
|
346
|
+
});
|
|
347
|
+
it('rejects is-checks on non-entity expressions', () => {
|
|
348
|
+
const errors = typeCheck(`
|
|
349
|
+
fn test() {
|
|
350
|
+
let x: int = 1;
|
|
351
|
+
if (x is Player) {}
|
|
352
|
+
}
|
|
353
|
+
`);
|
|
354
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
355
|
+
expect(errors[0].message).toContain("'is' checks require an entity expression");
|
|
356
|
+
});
|
|
196
357
|
});
|
|
197
358
|
describe('return type checking', () => {
|
|
198
359
|
it('allows matching return type', () => {
|
|
@@ -360,5 +521,32 @@ fn broken() -> int {
|
|
|
360
521
|
expect(errors.length).toBeGreaterThanOrEqual(3);
|
|
361
522
|
});
|
|
362
523
|
});
|
|
524
|
+
describe('event handlers', () => {
|
|
525
|
+
it('accepts matching @on event signatures', () => {
|
|
526
|
+
const errors = typeCheck(`
|
|
527
|
+
@on(PlayerDeath)
|
|
528
|
+
fn handle_death(player: Player) {
|
|
529
|
+
tp(player, @p);
|
|
530
|
+
}
|
|
531
|
+
`);
|
|
532
|
+
expect(errors).toHaveLength(0);
|
|
533
|
+
});
|
|
534
|
+
it('rejects unknown event types', () => {
|
|
535
|
+
const errors = typeCheck(`
|
|
536
|
+
@on(NotARealEvent)
|
|
537
|
+
fn handle(player: Player) {}
|
|
538
|
+
`);
|
|
539
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
540
|
+
expect(errors[0].message).toContain("Unknown event type 'NotARealEvent'");
|
|
541
|
+
});
|
|
542
|
+
it('rejects mismatched event signatures', () => {
|
|
543
|
+
const errors = typeCheck(`
|
|
544
|
+
@on(BlockBreak)
|
|
545
|
+
fn handle_break(player: Player) {}
|
|
546
|
+
`);
|
|
547
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
548
|
+
expect(errors[0].message).toContain('must declare 2 parameter(s)');
|
|
549
|
+
});
|
|
550
|
+
});
|
|
363
551
|
});
|
|
364
552
|
//# sourceMappingURL=typechecker.test.js.map
|
package/dist/ast/types.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export interface Span {
|
|
|
11
11
|
endLine?: number;
|
|
12
12
|
endCol?: number;
|
|
13
13
|
}
|
|
14
|
-
export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double';
|
|
14
|
+
export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double' | 'format_string';
|
|
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;
|
|
@@ -39,6 +45,18 @@ export interface LambdaExpr {
|
|
|
39
45
|
returnType?: TypeNode;
|
|
40
46
|
body: Expr | Block;
|
|
41
47
|
}
|
|
48
|
+
export type FStringPart = {
|
|
49
|
+
kind: 'text';
|
|
50
|
+
value: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'expr';
|
|
53
|
+
expr: Expr;
|
|
54
|
+
};
|
|
55
|
+
export interface FStringExpr {
|
|
56
|
+
kind: 'f_string';
|
|
57
|
+
parts: FStringPart[];
|
|
58
|
+
span?: Span;
|
|
59
|
+
}
|
|
42
60
|
export interface RangeExpr {
|
|
43
61
|
min?: number;
|
|
44
62
|
max?: number;
|
|
@@ -105,6 +123,14 @@ export type Expr = {
|
|
|
105
123
|
kind: 'double_lit';
|
|
106
124
|
value: number;
|
|
107
125
|
span?: Span;
|
|
126
|
+
} | {
|
|
127
|
+
kind: 'rel_coord';
|
|
128
|
+
value: string;
|
|
129
|
+
span?: Span;
|
|
130
|
+
} | {
|
|
131
|
+
kind: 'local_coord';
|
|
132
|
+
value: string;
|
|
133
|
+
span?: Span;
|
|
108
134
|
} | {
|
|
109
135
|
kind: 'bool_lit';
|
|
110
136
|
value: boolean;
|
|
@@ -121,7 +147,7 @@ export type Expr = {
|
|
|
121
147
|
kind: 'str_interp';
|
|
122
148
|
parts: Array<string | Expr>;
|
|
123
149
|
span?: Span;
|
|
124
|
-
} | {
|
|
150
|
+
} | FStringExpr | {
|
|
125
151
|
kind: 'range_lit';
|
|
126
152
|
range: RangeExpr;
|
|
127
153
|
span?: Span;
|
|
@@ -143,6 +169,11 @@ export type Expr = {
|
|
|
143
169
|
left: Expr;
|
|
144
170
|
right: Expr;
|
|
145
171
|
span?: Span;
|
|
172
|
+
} | {
|
|
173
|
+
kind: 'is_check';
|
|
174
|
+
expr: Expr;
|
|
175
|
+
entityType: EntityTypeName;
|
|
176
|
+
span?: Span;
|
|
146
177
|
} | {
|
|
147
178
|
kind: 'unary';
|
|
148
179
|
op: '!' | '-';
|
|
@@ -311,9 +342,10 @@ export type Stmt = {
|
|
|
311
342
|
};
|
|
312
343
|
export type Block = Stmt[];
|
|
313
344
|
export interface Decorator {
|
|
314
|
-
name: 'tick' | 'load' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team';
|
|
345
|
+
name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team';
|
|
315
346
|
args?: {
|
|
316
347
|
rate?: number;
|
|
348
|
+
eventType?: string;
|
|
317
349
|
trigger?: string;
|
|
318
350
|
advancement?: string;
|
|
319
351
|
item?: string;
|
|
@@ -342,6 +374,12 @@ export interface StructDecl {
|
|
|
342
374
|
fields: StructField[];
|
|
343
375
|
span?: Span;
|
|
344
376
|
}
|
|
377
|
+
export interface ImplBlock {
|
|
378
|
+
kind: 'impl_block';
|
|
379
|
+
typeName: string;
|
|
380
|
+
methods: FnDecl[];
|
|
381
|
+
span?: Span;
|
|
382
|
+
}
|
|
345
383
|
export interface EnumVariant {
|
|
346
384
|
name: string;
|
|
347
385
|
value?: number;
|
|
@@ -370,6 +408,7 @@ export interface Program {
|
|
|
370
408
|
globals: GlobalDecl[];
|
|
371
409
|
declarations: FnDecl[];
|
|
372
410
|
structs: StructDecl[];
|
|
411
|
+
implBlocks: ImplBlock[];
|
|
373
412
|
enums: EnumDecl[];
|
|
374
413
|
consts: ConstDecl[];
|
|
375
414
|
}
|
package/dist/cli.js
CHANGED
|
@@ -57,7 +57,7 @@ function printUsage() {
|
|
|
57
57
|
RedScript Compiler
|
|
58
58
|
|
|
59
59
|
Usage:
|
|
60
|
-
redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
|
|
60
|
+
redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>] [--no-dce]
|
|
61
61
|
redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
|
|
62
62
|
redscript check <file>
|
|
63
63
|
redscript fmt <file.mcrs> [file2.mcrs ...]
|
|
@@ -77,6 +77,7 @@ Options:
|
|
|
77
77
|
--output-nbt <file> Output .nbt file path for structure target
|
|
78
78
|
--namespace <ns> Datapack namespace (default: derived from filename)
|
|
79
79
|
--target <target> Output target: datapack (default), cmdblock, or structure
|
|
80
|
+
--no-dce Disable AST dead code elimination
|
|
80
81
|
--stats Print optimizer statistics
|
|
81
82
|
--hot-reload <url> After each successful compile, POST to <url>/reload
|
|
82
83
|
(use with redscript-testharness; e.g. http://localhost:25561)
|
|
@@ -99,7 +100,7 @@ function printVersion() {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
function parseArgs(args) {
|
|
102
|
-
const result = {};
|
|
103
|
+
const result = { dce: true };
|
|
103
104
|
let i = 0;
|
|
104
105
|
while (i < args.length) {
|
|
105
106
|
const arg = args[i];
|
|
@@ -127,6 +128,10 @@ function parseArgs(args) {
|
|
|
127
128
|
result.stats = true;
|
|
128
129
|
i++;
|
|
129
130
|
}
|
|
131
|
+
else if (arg === '--no-dce') {
|
|
132
|
+
result.dce = false;
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
130
135
|
else if (arg === '--hot-reload') {
|
|
131
136
|
result.hotReload = args[++i];
|
|
132
137
|
i++;
|
|
@@ -174,7 +179,7 @@ function printOptimizationStats(stats) {
|
|
|
174
179
|
console.log(` constant folding: ${stats.constantFolds} constants folded`);
|
|
175
180
|
console.log(` Total mcfunction commands: ${stats.totalCommandsBefore} -> ${stats.totalCommandsAfter} (${formatReduction(stats.totalCommandsBefore, stats.totalCommandsAfter)} reduction)`);
|
|
176
181
|
}
|
|
177
|
-
function compileCommand(file, output, namespace, target = 'datapack', showStats = false) {
|
|
182
|
+
function compileCommand(file, output, namespace, target = 'datapack', showStats = false, dce = true) {
|
|
178
183
|
// Read source file
|
|
179
184
|
if (!fs.existsSync(file)) {
|
|
180
185
|
console.error(`Error: File not found: ${file}`);
|
|
@@ -183,7 +188,7 @@ function compileCommand(file, output, namespace, target = 'datapack', showStats
|
|
|
183
188
|
const source = fs.readFileSync(file, 'utf-8');
|
|
184
189
|
try {
|
|
185
190
|
if (target === 'cmdblock') {
|
|
186
|
-
const result = (0, index_1.compile)(source, { namespace, filePath: file });
|
|
191
|
+
const result = (0, index_1.compile)(source, { namespace, filePath: file, dce });
|
|
187
192
|
printWarnings(result.warnings);
|
|
188
193
|
// Generate command block JSON
|
|
189
194
|
const hasTick = result.files.some(f => f.path.includes('__tick.mcfunction'));
|
|
@@ -201,7 +206,7 @@ function compileCommand(file, output, namespace, target = 'datapack', showStats
|
|
|
201
206
|
}
|
|
202
207
|
}
|
|
203
208
|
else if (target === 'structure') {
|
|
204
|
-
const structure = (0, structure_1.compileToStructure)(source, namespace, file);
|
|
209
|
+
const structure = (0, structure_1.compileToStructure)(source, namespace, file, { dce });
|
|
205
210
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
206
211
|
fs.writeFileSync(output, structure.buffer);
|
|
207
212
|
console.log(`✓ Generated structure for ${file}`);
|
|
@@ -212,7 +217,7 @@ function compileCommand(file, output, namespace, target = 'datapack', showStats
|
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
else {
|
|
215
|
-
const result = (0, index_1.compile)(source, { namespace, filePath: file });
|
|
220
|
+
const result = (0, index_1.compile)(source, { namespace, filePath: file, dce });
|
|
216
221
|
printWarnings(result.warnings);
|
|
217
222
|
// Default: generate datapack
|
|
218
223
|
// Create output directory
|
|
@@ -266,7 +271,7 @@ async function hotReload(url) {
|
|
|
266
271
|
console.warn(`⚠ Hot reload failed (is the server running?): ${e.message}`);
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
|
-
function watchCommand(dir, output, namespace, hotReloadUrl) {
|
|
274
|
+
function watchCommand(dir, output, namespace, hotReloadUrl, dce = true) {
|
|
270
275
|
// Check if directory exists
|
|
271
276
|
if (!fs.existsSync(dir)) {
|
|
272
277
|
console.error(`Error: Directory not found: ${dir}`);
|
|
@@ -297,7 +302,7 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
|
|
|
297
302
|
try {
|
|
298
303
|
source = fs.readFileSync(file, 'utf-8');
|
|
299
304
|
const ns = namespace ?? deriveNamespace(file);
|
|
300
|
-
const result = (0, index_1.compile)(source, { namespace: ns, filePath: file });
|
|
305
|
+
const result = (0, index_1.compile)(source, { namespace: ns, filePath: file, dce });
|
|
301
306
|
printWarnings(result.warnings);
|
|
302
307
|
// Create output directory
|
|
303
308
|
fs.mkdirSync(output, { recursive: true });
|
|
@@ -374,7 +379,7 @@ async function main() {
|
|
|
374
379
|
const output = target === 'structure'
|
|
375
380
|
? (parsed.outputNbt ?? parsed.output ?? `./${namespace}.nbt`)
|
|
376
381
|
: (parsed.output ?? './dist');
|
|
377
|
-
compileCommand(parsed.file, output, namespace, target, parsed.stats);
|
|
382
|
+
compileCommand(parsed.file, output, namespace, target, parsed.stats, parsed.dce);
|
|
378
383
|
}
|
|
379
384
|
break;
|
|
380
385
|
case 'watch':
|
|
@@ -383,7 +388,7 @@ async function main() {
|
|
|
383
388
|
printUsage();
|
|
384
389
|
process.exit(1);
|
|
385
390
|
}
|
|
386
|
-
watchCommand(parsed.file, parsed.output ?? './dist', parsed.namespace, parsed.hotReload);
|
|
391
|
+
watchCommand(parsed.file, parsed.output ?? './dist', parsed.namespace, parsed.hotReload, parsed.dce);
|
|
387
392
|
break;
|
|
388
393
|
case 'check':
|
|
389
394
|
if (!parsed.file) {
|
|
@@ -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'),
|
|
@@ -14,5 +14,8 @@ export interface StructureCompileResult {
|
|
|
14
14
|
blocks: StructureBlockInfo[];
|
|
15
15
|
stats?: OptimizationStats;
|
|
16
16
|
}
|
|
17
|
+
export interface StructureCompileOptions {
|
|
18
|
+
dce?: boolean;
|
|
19
|
+
}
|
|
17
20
|
export declare function generateStructure(input: IRModule | DatapackFile[]): StructureCompileResult;
|
|
18
|
-
export declare function compileToStructure(source: string, namespace: string, filePath?: string): StructureCompileResult;
|
|
21
|
+
export declare function compileToStructure(source: string, namespace: string, filePath?: string, options?: StructureCompileOptions): StructureCompileResult;
|
|
@@ -9,7 +9,9 @@ const nbt_1 = require("../../nbt");
|
|
|
9
9
|
const commands_1 = require("../../optimizer/commands");
|
|
10
10
|
const passes_1 = require("../../optimizer/passes");
|
|
11
11
|
const structure_1 = require("../../optimizer/structure");
|
|
12
|
+
const dce_1 = require("../../optimizer/dce");
|
|
12
13
|
const compile_1 = require("../../compile");
|
|
14
|
+
const types_1 = require("../../events/types");
|
|
13
15
|
const DATA_VERSION = 3953;
|
|
14
16
|
const MAX_WIDTH = 16;
|
|
15
17
|
const OBJ = 'rs';
|
|
@@ -62,6 +64,8 @@ function collectCommandEntriesFromModule(module) {
|
|
|
62
64
|
const entries = [];
|
|
63
65
|
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName);
|
|
64
66
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName));
|
|
67
|
+
const eventHandlers = module.functions.filter((fn) => !!fn.eventHandler && (0, types_1.isEventTypeName)(fn.eventHandler.eventType));
|
|
68
|
+
const eventTypes = new Set(eventHandlers.map(fn => fn.eventHandler.eventType));
|
|
65
69
|
const loadCommands = [
|
|
66
70
|
`scoreboard objectives add ${OBJ} dummy`,
|
|
67
71
|
...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
|
|
@@ -71,6 +75,14 @@ function collectCommandEntriesFromModule(module) {
|
|
|
71
75
|
]),
|
|
72
76
|
...Array.from(new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))).map(constSetup),
|
|
73
77
|
];
|
|
78
|
+
for (const eventType of eventTypes) {
|
|
79
|
+
if (eventType === 'PlayerDeath') {
|
|
80
|
+
loadCommands.push('scoreboard objectives add rs.deaths deathCount');
|
|
81
|
+
}
|
|
82
|
+
else if (eventType === 'EntityKill') {
|
|
83
|
+
loadCommands.push('scoreboard objectives add rs.kills totalKillCount');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
74
86
|
// Call @load functions from __load
|
|
75
87
|
for (const fn of module.functions) {
|
|
76
88
|
if (fn.isLoadInit) {
|
|
@@ -114,6 +126,20 @@ function collectCommandEntriesFromModule(module) {
|
|
|
114
126
|
});
|
|
115
127
|
}
|
|
116
128
|
}
|
|
129
|
+
if (eventHandlers.length > 0) {
|
|
130
|
+
for (const eventType of eventTypes) {
|
|
131
|
+
const tag = types_1.EVENT_TYPES[eventType].tag;
|
|
132
|
+
const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType);
|
|
133
|
+
for (const handler of handlers) {
|
|
134
|
+
tickCommands.push({
|
|
135
|
+
cmd: `execute as @a[tag=${tag}] run function ${module.namespace}:${handler.name}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
tickCommands.push({
|
|
139
|
+
cmd: `tag @a[tag=${tag}] remove ${tag}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
117
143
|
if (tickCommands.length > 0) {
|
|
118
144
|
sections.push({
|
|
119
145
|
name: '__tick',
|
|
@@ -230,10 +256,11 @@ function generateStructure(input) {
|
|
|
230
256
|
})),
|
|
231
257
|
};
|
|
232
258
|
}
|
|
233
|
-
function compileToStructure(source, namespace, filePath) {
|
|
259
|
+
function compileToStructure(source, namespace, filePath, options = {}) {
|
|
234
260
|
const preprocessedSource = (0, compile_1.preprocessSource)(source, { filePath });
|
|
235
261
|
const tokens = new lexer_1.Lexer(preprocessedSource, filePath).tokenize();
|
|
236
|
-
const
|
|
262
|
+
const parsedAst = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
|
|
263
|
+
const ast = options.dce ?? true ? (0, dce_1.eliminateDeadCode)(parsedAst) : parsedAst;
|
|
237
264
|
const ir = new lowering_1.Lowering(namespace).lower(ast);
|
|
238
265
|
const stats = (0, commands_1.createEmptyOptimizationStats)();
|
|
239
266
|
const optimizedIRFunctions = ir.functions.map(fn => {
|
package/dist/compile.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface CompileOptions {
|
|
|
11
11
|
namespace?: string;
|
|
12
12
|
filePath?: string;
|
|
13
13
|
optimize?: boolean;
|
|
14
|
+
dce?: boolean;
|
|
14
15
|
}
|
|
15
16
|
export interface CompileResult {
|
|
16
17
|
success: boolean;
|
|
@@ -20,10 +21,20 @@ export interface CompileResult {
|
|
|
20
21
|
ir?: IRModule;
|
|
21
22
|
error?: DiagnosticError;
|
|
22
23
|
}
|
|
24
|
+
export interface SourceRange {
|
|
25
|
+
startLine: number;
|
|
26
|
+
endLine: number;
|
|
27
|
+
filePath: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PreprocessedSource {
|
|
30
|
+
source: string;
|
|
31
|
+
ranges: SourceRange[];
|
|
32
|
+
}
|
|
23
33
|
interface PreprocessOptions {
|
|
24
34
|
filePath?: string;
|
|
25
35
|
seen?: Set<string>;
|
|
26
36
|
}
|
|
37
|
+
export declare function preprocessSourceWithMetadata(source: string, options?: PreprocessOptions): PreprocessedSource;
|
|
27
38
|
export declare function preprocessSource(source: string, options?: PreprocessOptions): string;
|
|
28
39
|
export declare function compile(source: string, options?: CompileOptions): CompileResult;
|
|
29
40
|
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;
|
|
@@ -47,10 +48,21 @@ const lexer_1 = require("./lexer");
|
|
|
47
48
|
const parser_1 = require("./parser");
|
|
48
49
|
const lowering_1 = require("./lowering");
|
|
49
50
|
const passes_1 = require("./optimizer/passes");
|
|
51
|
+
const dce_1 = require("./optimizer/dce");
|
|
50
52
|
const mcfunction_1 = require("./codegen/mcfunction");
|
|
51
53
|
const diagnostics_1 = require("./diagnostics");
|
|
52
54
|
const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/;
|
|
53
|
-
function
|
|
55
|
+
function countLines(source) {
|
|
56
|
+
return source === '' ? 0 : source.split('\n').length;
|
|
57
|
+
}
|
|
58
|
+
function offsetRanges(ranges, lineOffset) {
|
|
59
|
+
return ranges.map(range => ({
|
|
60
|
+
startLine: range.startLine + lineOffset,
|
|
61
|
+
endLine: range.endLine + lineOffset,
|
|
62
|
+
filePath: range.filePath,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
function preprocessSourceWithMetadata(source, options = {}) {
|
|
54
66
|
const { filePath } = options;
|
|
55
67
|
const seen = options.seen ?? new Set();
|
|
56
68
|
if (filePath) {
|
|
@@ -78,7 +90,7 @@ function preprocessSource(source, options = {}) {
|
|
|
78
90
|
catch {
|
|
79
91
|
throw new diagnostics_1.DiagnosticError('ParseError', `Cannot import '${match[1]}'`, { file: filePath, line: i + 1, col: 1 }, lines);
|
|
80
92
|
}
|
|
81
|
-
imports.push(
|
|
93
|
+
imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }));
|
|
82
94
|
}
|
|
83
95
|
continue;
|
|
84
96
|
}
|
|
@@ -89,23 +101,45 @@ function preprocessSource(source, options = {}) {
|
|
|
89
101
|
parsingHeader = false;
|
|
90
102
|
bodyLines.push(line);
|
|
91
103
|
}
|
|
92
|
-
|
|
104
|
+
const body = bodyLines.join('\n');
|
|
105
|
+
const parts = [...imports.map(entry => entry.source), body].filter(Boolean);
|
|
106
|
+
const combined = parts.join('\n');
|
|
107
|
+
const ranges = [];
|
|
108
|
+
let lineOffset = 0;
|
|
109
|
+
for (const entry of imports) {
|
|
110
|
+
ranges.push(...offsetRanges(entry.ranges, lineOffset));
|
|
111
|
+
lineOffset += countLines(entry.source);
|
|
112
|
+
}
|
|
113
|
+
if (filePath && body) {
|
|
114
|
+
ranges.push({
|
|
115
|
+
startLine: lineOffset + 1,
|
|
116
|
+
endLine: lineOffset + countLines(body),
|
|
117
|
+
filePath: path.resolve(filePath),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return { source: combined, ranges };
|
|
121
|
+
}
|
|
122
|
+
function preprocessSource(source, options = {}) {
|
|
123
|
+
return preprocessSourceWithMetadata(source, options).source;
|
|
93
124
|
}
|
|
94
125
|
// ---------------------------------------------------------------------------
|
|
95
126
|
// Main Compile Function
|
|
96
127
|
// ---------------------------------------------------------------------------
|
|
97
128
|
function compile(source, options = {}) {
|
|
98
129
|
const { namespace = 'redscript', filePath, optimize: shouldOptimize = true } = options;
|
|
130
|
+
const shouldRunDce = options.dce ?? shouldOptimize;
|
|
99
131
|
let sourceLines = source.split('\n');
|
|
100
132
|
try {
|
|
101
|
-
const
|
|
133
|
+
const preprocessed = preprocessSourceWithMetadata(source, { filePath });
|
|
134
|
+
const preprocessedSource = preprocessed.source;
|
|
102
135
|
sourceLines = preprocessedSource.split('\n');
|
|
103
136
|
// Lexing
|
|
104
137
|
const tokens = new lexer_1.Lexer(preprocessedSource, filePath).tokenize();
|
|
105
138
|
// Parsing
|
|
106
|
-
const
|
|
139
|
+
const parsedAst = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
|
|
140
|
+
const ast = shouldRunDce ? (0, dce_1.eliminateDeadCode)(parsedAst) : parsedAst;
|
|
107
141
|
// Lowering
|
|
108
|
-
const ir = new lowering_1.Lowering(namespace).lower(ast);
|
|
142
|
+
const ir = new lowering_1.Lowering(namespace, preprocessed.ranges).lower(ast);
|
|
109
143
|
// Optimization
|
|
110
144
|
const optimized = shouldOptimize
|
|
111
145
|
? { ...ir, functions: ir.functions.map(fn => (0, passes_1.optimize)(fn)) }
|