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.
Files changed (83) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/cli.test.js +138 -0
  5. package/dist/__tests__/codegen.test.js +25 -0
  6. package/dist/__tests__/dce.test.d.ts +1 -0
  7. package/dist/__tests__/dce.test.js +137 -0
  8. package/dist/__tests__/e2e.test.js +190 -12
  9. package/dist/__tests__/lexer.test.js +31 -4
  10. package/dist/__tests__/lowering.test.js +172 -9
  11. package/dist/__tests__/mc-integration.test.js +145 -51
  12. package/dist/__tests__/mc-syntax.test.js +12 -0
  13. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  14. package/dist/__tests__/parser.test.js +90 -0
  15. package/dist/__tests__/runtime.test.js +21 -8
  16. package/dist/__tests__/typechecker.test.js +188 -0
  17. package/dist/ast/types.d.ts +42 -3
  18. package/dist/cli.js +15 -10
  19. package/dist/codegen/mcfunction/index.js +30 -1
  20. package/dist/codegen/structure/index.d.ts +4 -1
  21. package/dist/codegen/structure/index.js +29 -2
  22. package/dist/compile.d.ts +11 -0
  23. package/dist/compile.js +40 -6
  24. package/dist/events/types.d.ts +35 -0
  25. package/dist/events/types.js +59 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -3
  28. package/dist/ir/types.d.ts +4 -0
  29. package/dist/lexer/index.d.ts +2 -1
  30. package/dist/lexer/index.js +91 -1
  31. package/dist/lowering/index.d.ts +32 -1
  32. package/dist/lowering/index.js +476 -16
  33. package/dist/optimizer/dce.d.ts +23 -0
  34. package/dist/optimizer/dce.js +591 -0
  35. package/dist/parser/index.d.ts +4 -0
  36. package/dist/parser/index.js +160 -26
  37. package/dist/typechecker/index.d.ts +19 -0
  38. package/dist/typechecker/index.js +392 -17
  39. package/docs/ARCHITECTURE.zh.md +1088 -0
  40. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  41. package/editors/vscode/.vscodeignore +3 -0
  42. package/editors/vscode/CHANGELOG.md +9 -0
  43. package/editors/vscode/icon.png +0 -0
  44. package/editors/vscode/out/extension.js +1144 -72
  45. package/editors/vscode/package-lock.json +2 -2
  46. package/editors/vscode/package.json +1 -1
  47. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  48. package/examples/spiral.mcrs +79 -0
  49. package/logo.png +0 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/cli.test.ts +166 -0
  52. package/src/__tests__/codegen.test.ts +27 -0
  53. package/src/__tests__/dce.test.ts +129 -0
  54. package/src/__tests__/e2e.test.ts +201 -12
  55. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  56. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  57. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  58. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  59. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  60. package/src/__tests__/lexer.test.ts +35 -4
  61. package/src/__tests__/lowering.test.ts +187 -9
  62. package/src/__tests__/mc-integration.test.ts +166 -51
  63. package/src/__tests__/mc-syntax.test.ts +14 -0
  64. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  65. package/src/__tests__/parser.test.ts +102 -5
  66. package/src/__tests__/runtime.test.ts +24 -8
  67. package/src/__tests__/typechecker.test.ts +204 -0
  68. package/src/ast/types.ts +39 -2
  69. package/src/cli.ts +24 -10
  70. package/src/codegen/mcfunction/index.ts +31 -1
  71. package/src/codegen/structure/index.ts +40 -2
  72. package/src/compile.ts +59 -7
  73. package/src/events/types.ts +69 -0
  74. package/src/index.ts +9 -4
  75. package/src/ir/types.ts +4 -0
  76. package/src/lexer/index.ts +105 -2
  77. package/src/lowering/index.ts +566 -18
  78. package/src/optimizer/dce.ts +618 -0
  79. package/src/parser/index.ts +187 -29
  80. package/src/stdlib/README.md +34 -4
  81. package/src/stdlib/tags.mcrs +951 -0
  82. package/src/stdlib/timer.mcrs +54 -33
  83. 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
@@ -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 ast = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
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 preprocessSource(source, options = {}) {
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(preprocessSource(importedSource, { filePath: importPath, seen }));
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
- return [...imports, bodyLines.join('\n')].filter(Boolean).join('\n');
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 preprocessedSource = preprocessSource(source, { filePath });
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 ast = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
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)) }