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
@@ -25,6 +25,7 @@ describe('Parser', () => {
25
25
  const program = parse('')
26
26
  expect(program.namespace).toBe('test')
27
27
  expect(program.declarations).toEqual([])
28
+ expect(program.implBlocks).toEqual([])
28
29
  expect(program.enums).toEqual([])
29
30
  expect(program.consts).toEqual([])
30
31
  })
@@ -102,6 +103,13 @@ describe('Parser', () => {
102
103
  { name: 'on_death' },
103
104
  ])
104
105
  })
106
+
107
+ it('parses @on event decorators', () => {
108
+ const program = parse('@on(PlayerDeath)\nfn handle_death(player: Player) {}')
109
+ expect(program.declarations[0].decorators).toEqual([
110
+ { name: 'on', args: { eventType: 'PlayerDeath' } },
111
+ ])
112
+ })
105
113
  })
106
114
 
107
115
  describe('types', () => {
@@ -151,6 +159,52 @@ describe('Parser', () => {
151
159
  },
152
160
  ])
153
161
  })
162
+
163
+ it('parses impl blocks', () => {
164
+ const program = parse(`
165
+ struct Timer { duration: int }
166
+
167
+ impl Timer {
168
+ fn new(duration: int): Timer {
169
+ return { duration: duration };
170
+ }
171
+
172
+ fn start(self) {}
173
+ }
174
+ `)
175
+ expect(program.implBlocks).toHaveLength(1)
176
+ expect(program.implBlocks[0].typeName).toBe('Timer')
177
+ expect(program.implBlocks[0].methods.map(method => method.name)).toEqual(['new', 'start'])
178
+ expect(program.implBlocks[0].methods[1].params[0]).toEqual({
179
+ name: 'self',
180
+ type: { kind: 'struct', name: 'Timer' },
181
+ default: undefined,
182
+ })
183
+ })
184
+
185
+ it('parses impl blocks with static and instance methods', () => {
186
+ const program = parse(`
187
+ struct Point { x: int, y: int }
188
+
189
+ impl Point {
190
+ fn new(x: int, y: int) -> Point {
191
+ return { x: x, y: y };
192
+ }
193
+
194
+ fn distance(self) -> int {
195
+ return self.x + self.y;
196
+ }
197
+ }
198
+ `)
199
+ expect(program.implBlocks).toHaveLength(1)
200
+ expect(program.implBlocks[0].typeName).toBe('Point')
201
+ expect(program.implBlocks[0].methods[0].params.map(param => param.name)).toEqual(['x', 'y'])
202
+ expect(program.implBlocks[0].methods[1].params[0]).toEqual({
203
+ name: 'self',
204
+ type: { kind: 'struct', name: 'Point' },
205
+ default: undefined,
206
+ })
207
+ })
154
208
  })
155
209
 
156
210
  describe('statements', () => {
@@ -197,6 +251,28 @@ describe('Parser', () => {
197
251
  expect((stmt as any).else_).toHaveLength(1)
198
252
  })
199
253
 
254
+ it('parses entity is-checks in if conditions', () => {
255
+ const stmt = parseStmt('if (e is Player) { kill(@s); }')
256
+ expect(stmt.kind).toBe('if')
257
+ expect((stmt as any).cond).toEqual({
258
+ kind: 'is_check',
259
+ expr: { kind: 'ident', name: 'e' },
260
+ entityType: 'Player',
261
+ })
262
+ })
263
+
264
+ it('parses entity is-checks inside foreach bodies', () => {
265
+ const stmt = parseStmt('foreach (e in @e) { if (e is Zombie) { kill(e); } }')
266
+ expect(stmt.kind).toBe('foreach')
267
+ const innerIf = (stmt as any).body[0]
268
+ expect(innerIf.kind).toBe('if')
269
+ expect(innerIf.cond).toEqual({
270
+ kind: 'is_check',
271
+ expr: { kind: 'ident', name: 'e' },
272
+ entityType: 'Zombie',
273
+ })
274
+ })
275
+
200
276
  it('parses while statement', () => {
201
277
  const stmt = parseStmt('while (i > 0) { i = i - 1; }')
202
278
  expect(stmt.kind).toBe('while')
@@ -405,6 +481,17 @@ describe('Parser', () => {
405
481
  })
406
482
  })
407
483
 
484
+ it('parses f-string literal', () => {
485
+ const expr = parseExpr('f"Score: {x}"')
486
+ expect(expr).toEqual({
487
+ kind: 'f_string',
488
+ parts: [
489
+ { kind: 'text', value: 'Score: ' },
490
+ { kind: 'expr', expr: { kind: 'ident', name: 'x' } },
491
+ ],
492
+ })
493
+ })
494
+
408
495
  it('parses boolean literals', () => {
409
496
  expect(parseExpr('true')).toEqual({ kind: 'bool_lit', value: true })
410
497
  expect(parseExpr('false')).toEqual({ kind: 'bool_lit', value: false })
@@ -459,11 +546,11 @@ describe('Parser', () => {
459
546
  expect(expr).toEqual({ kind: 'ident', name: 'foo' })
460
547
  })
461
548
 
462
- it('parses function call', () => {
463
- const expr = parseExpr('foo(1, 2)')
464
- expect(expr).toEqual({
465
- kind: 'call',
466
- fn: 'foo',
549
+ it('parses function call', () => {
550
+ const expr = parseExpr('foo(1, 2)')
551
+ expect(expr).toEqual({
552
+ kind: 'call',
553
+ fn: 'foo',
467
554
  args: [
468
555
  { kind: 'int_lit', value: 1 },
469
556
  { kind: 'int_lit', value: 2 },
@@ -486,6 +573,16 @@ describe('Parser', () => {
486
573
  })
487
574
  })
488
575
 
576
+ it('parses static method calls', () => {
577
+ const expr = parseExpr('Timer::new(100)')
578
+ expect(expr).toEqual({
579
+ kind: 'static_call',
580
+ type: 'Timer',
581
+ method: 'new',
582
+ args: [{ kind: 'int_lit', value: 100 }],
583
+ })
584
+ })
585
+
489
586
  describe('binary operators', () => {
490
587
  it('parses arithmetic', () => {
491
588
  const expr = parseExpr('1 + 2')
@@ -57,7 +57,7 @@ fn compute() {
57
57
  runtime.load()
58
58
  runtime.execFunction('compute')
59
59
 
60
- expect(runtime.getScore('math', 'result')).toBe(11)
60
+ expect(runtime.getScore('math', 'runtime.result')).toBe(11)
61
61
  })
62
62
 
63
63
  it('captures say, announce, actionbar, and title output in the chat log', () => {
@@ -97,6 +97,22 @@ fn chat() {
97
97
  ])
98
98
  })
99
99
 
100
+ it('renders f-strings through tellraw score components', () => {
101
+ const runtime = loadCompiledProgram(`
102
+ fn chat() {
103
+ let score: int = 7;
104
+ say(f"You have {score} points");
105
+ }
106
+ `)
107
+
108
+ runtime.load()
109
+ runtime.execFunction('chat')
110
+
111
+ expect(runtime.getChatLog()).toEqual([
112
+ 'You have 7 points',
113
+ ])
114
+ })
115
+
100
116
  it('kills only entities matched by a foreach selector', () => {
101
117
  const runtime = loadCompiledProgram(`
102
118
  fn purge_zombies() {
@@ -135,8 +151,8 @@ fn arrays() {
135
151
  runtime.load()
136
152
  runtime.execFunction('arrays')
137
153
 
138
- expect(runtime.getScore('arrays', 'len')).toBe(1)
139
- expect(runtime.getScore('arrays', 'last')).toBe(9)
154
+ expect(runtime.getScore('arrays', 'runtime.len')).toBe(1)
155
+ expect(runtime.getScore('arrays', 'runtime.last')).toBe(9)
140
156
  expect(runtime.getStorage('rs:heap.arr')).toEqual([4])
141
157
  })
142
158
 
@@ -177,7 +193,7 @@ fn pulse() {
177
193
  runtime.load()
178
194
  runtime.ticks(10)
179
195
 
180
- expect(runtime.getScore('pulse', 'count')).toBe(2)
196
+ expect(runtime.getScore('pulse', 'runtime.count')).toBe(2)
181
197
  })
182
198
 
183
199
  it('executes only the matching match arm', () => {
@@ -234,7 +250,7 @@ fn test() {
234
250
  runtime.load()
235
251
  runtime.execFunction('test')
236
252
 
237
- expect(runtime.getScore('lambda', 'direct')).toBe(10)
253
+ expect(runtime.getScore('lambda', 'runtime.direct')).toBe(10)
238
254
  })
239
255
 
240
256
  it('executes lambdas passed as callback arguments', () => {
@@ -252,7 +268,7 @@ fn test() {
252
268
  runtime.load()
253
269
  runtime.execFunction('test')
254
270
 
255
- expect(runtime.getScore('lambda', 'callback')).toBe(15)
271
+ expect(runtime.getScore('lambda', 'runtime.callback')).toBe(15)
256
272
  })
257
273
 
258
274
  it('executes block-body lambdas', () => {
@@ -270,7 +286,7 @@ fn test() {
270
286
  runtime.load()
271
287
  runtime.execFunction('test')
272
288
 
273
- expect(runtime.getScore('lambda', 'block')).toBe(11)
289
+ expect(runtime.getScore('lambda', 'runtime.block')).toBe(11)
274
290
  })
275
291
 
276
292
  it('executes immediately-invoked expression-body lambdas', () => {
@@ -284,6 +300,6 @@ fn test() {
284
300
  runtime.load()
285
301
  runtime.execFunction('test')
286
302
 
287
- expect(runtime.getScore('lambda', 'iife')).toBe(10)
303
+ expect(runtime.getScore('lambda', 'runtime.iife')).toBe(10)
288
304
  })
289
305
  })
@@ -120,6 +120,39 @@ fn test() {
120
120
  expect(errors).toHaveLength(0)
121
121
  })
122
122
 
123
+ it('allows f-strings in runtime output builtins', () => {
124
+ const errors = typeCheck(`
125
+ fn test() {
126
+ let score: int = 5;
127
+ say(f"Score: {score}");
128
+ tellraw(@a, f"Score: {score}");
129
+ actionbar(@s, f"Score: {score}");
130
+ title(@s, f"Score: {score}");
131
+ }
132
+ `)
133
+ expect(errors).toHaveLength(0)
134
+ })
135
+
136
+ it('rejects f-strings outside runtime output builtins', () => {
137
+ const errors = typeCheck(`
138
+ fn test() {
139
+ let msg: string = f"Score";
140
+ }
141
+ `)
142
+ expect(errors.length).toBeGreaterThan(0)
143
+ expect(errors[0].message).toContain('expected string, got format_string')
144
+ })
145
+
146
+ it('rejects unsupported f-string placeholder types', () => {
147
+ const errors = typeCheck(`
148
+ fn test() {
149
+ say(f"Flag: {true}");
150
+ }
151
+ `)
152
+ expect(errors.length).toBeGreaterThan(0)
153
+ expect(errors[0].message).toContain('f-string placeholder must be int or string')
154
+ })
155
+
123
156
  it('detects too many arguments', () => {
124
157
  const errors = typeCheck(`
125
158
  fn greet() {
@@ -211,6 +244,147 @@ fn test() {
211
244
  `)
212
245
  expect(errors).toHaveLength(0)
213
246
  })
247
+
248
+ it('type checks timer builtins with void callbacks and interval IDs', () => {
249
+ const errors = typeCheck(`
250
+ fn test() {
251
+ setTimeout(100, () => {
252
+ say("later");
253
+ });
254
+ let intervalId: int = setInterval(20, () => {
255
+ say("tick");
256
+ });
257
+ clearInterval(intervalId);
258
+ }
259
+ `)
260
+ expect(errors).toHaveLength(0)
261
+ })
262
+
263
+ it('rejects timer callbacks with the wrong return type', () => {
264
+ const errors = typeCheck(`
265
+ fn test() {
266
+ setTimeout(100, () => 1);
267
+ }
268
+ `)
269
+ expect(errors.length).toBeGreaterThan(0)
270
+ expect(errors[0].message).toContain('Return type mismatch: expected void, got int')
271
+ })
272
+
273
+ it('allows impl instance methods with inferred self type', () => {
274
+ const errors = typeCheck(`
275
+ struct Timer { duration: int }
276
+
277
+ impl Timer {
278
+ fn elapsed(self) -> int {
279
+ return self.duration;
280
+ }
281
+ }
282
+
283
+ fn test() {
284
+ let timer: Timer = { duration: 10 };
285
+ let value: int = timer.elapsed();
286
+ }
287
+ `)
288
+ expect(errors).toHaveLength(0)
289
+ })
290
+
291
+ it('records then-branch entity narrowing for is-checks', () => {
292
+ const source = `
293
+ fn test() {
294
+ foreach (e in @e) {
295
+ if (e is Player) {
296
+ kill(e);
297
+ }
298
+ }
299
+ }
300
+ `
301
+ const tokens = new Lexer(source).tokenize()
302
+ const ast = new Parser(tokens).parse('test')
303
+ const checker = new TypeChecker(source) as any
304
+ checker.check(ast)
305
+
306
+ const foreachStmt = ast.declarations[0].body[0] as any
307
+ const ifStmt = foreachStmt.body[0]
308
+ expect(checker.getThenBranchNarrowing(ifStmt.cond)).toEqual({
309
+ name: 'e',
310
+ type: { kind: 'entity', entityType: 'Player' },
311
+ mutable: false,
312
+ })
313
+ })
314
+
315
+ it('allows static impl method calls', () => {
316
+ const errors = typeCheck(`
317
+ struct Timer { duration: int }
318
+
319
+ impl Timer {
320
+ fn new(duration: int) -> Timer {
321
+ return { duration: duration };
322
+ }
323
+ }
324
+
325
+ fn test() {
326
+ let timer: Timer = Timer::new(10);
327
+ }
328
+ `)
329
+ expect(errors).toHaveLength(0)
330
+ })
331
+
332
+ it('rejects using is-checks on non-entity values', () => {
333
+ const errors = typeCheck(`
334
+ fn test() {
335
+ let x: int = 1;
336
+ if (x is Player) {
337
+ say("nope");
338
+ }
339
+ }
340
+ `)
341
+ expect(errors.length).toBeGreaterThan(0)
342
+ expect(errors[0].message).toContain("'is' checks require an entity expression, got int")
343
+ })
344
+
345
+ it('rejects calling instance impl methods as static methods', () => {
346
+ const errors = typeCheck(`
347
+ struct Point { x: int, y: int }
348
+
349
+ impl Point {
350
+ fn distance(self) -> int {
351
+ return self.x + self.y;
352
+ }
353
+ }
354
+
355
+ fn test() {
356
+ let total: int = Point::distance();
357
+ }
358
+ `)
359
+ expect(errors.length).toBeGreaterThan(0)
360
+ expect(errors[0].message).toContain("Method 'Point::distance' is an instance method")
361
+ })
362
+ })
363
+
364
+ describe('entity is-check narrowing', () => {
365
+ it('allows entity type checks on foreach bindings', () => {
366
+ const errors = typeCheck(`
367
+ fn test() {
368
+ foreach (e in @e) {
369
+ if (e is Player) {
370
+ kill(@s);
371
+ }
372
+ }
373
+ }
374
+ `)
375
+ expect(errors).toHaveLength(0)
376
+ })
377
+
378
+ it('rejects is-checks on non-entity expressions', () => {
379
+ const errors = typeCheck(`
380
+ fn test() {
381
+ let x: int = 1;
382
+ if (x is Player) {}
383
+ }
384
+ `)
385
+ expect(errors.length).toBeGreaterThan(0)
386
+ expect(errors[0].message).toContain("'is' checks require an entity expression")
387
+ })
214
388
  })
215
389
 
216
390
  describe('return type checking', () => {
@@ -392,4 +566,34 @@ fn broken() -> int {
392
566
  expect(errors.length).toBeGreaterThanOrEqual(3)
393
567
  })
394
568
  })
569
+
570
+ describe('event handlers', () => {
571
+ it('accepts matching @on event signatures', () => {
572
+ const errors = typeCheck(`
573
+ @on(PlayerDeath)
574
+ fn handle_death(player: Player) {
575
+ tp(player, @p);
576
+ }
577
+ `)
578
+ expect(errors).toHaveLength(0)
579
+ })
580
+
581
+ it('rejects unknown event types', () => {
582
+ const errors = typeCheck(`
583
+ @on(NotARealEvent)
584
+ fn handle(player: Player) {}
585
+ `)
586
+ expect(errors.length).toBeGreaterThan(0)
587
+ expect(errors[0].message).toContain("Unknown event type 'NotARealEvent'")
588
+ })
589
+
590
+ it('rejects mismatched event signatures', () => {
591
+ const errors = typeCheck(`
592
+ @on(BlockBreak)
593
+ fn handle_break(player: Player) {}
594
+ `)
595
+ expect(errors.length).toBeGreaterThan(0)
596
+ expect(errors[0].message).toContain('must declare 2 parameter(s)')
597
+ })
598
+ })
395
599
  })
package/src/ast/types.ts CHANGED
@@ -22,7 +22,19 @@ export interface Span {
22
22
  // Type Nodes
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
- export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double'
25
+ export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double' | 'format_string'
26
+
27
+ // Entity type hierarchy
28
+ export type EntityTypeName =
29
+ | 'entity' // Base type
30
+ | 'Player' // @a, @p, @r
31
+ | 'Mob' // Base mob type
32
+ | 'HostileMob' // Hostile mobs
33
+ | 'PassiveMob' // Passive mobs
34
+ // Specific mob types (common ones)
35
+ | 'Zombie' | 'Skeleton' | 'Creeper' | 'Spider' | 'Enderman'
36
+ | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager'
37
+ | 'ArmorStand' | 'Item' | 'Arrow'
26
38
 
27
39
  export type TypeNode =
28
40
  | { kind: 'named'; name: PrimitiveType }
@@ -30,6 +42,8 @@ export type TypeNode =
30
42
  | { kind: 'struct'; name: string }
31
43
  | { kind: 'enum'; name: string }
32
44
  | { kind: 'function_type'; params: TypeNode[]; return: TypeNode }
45
+ | { kind: 'entity'; entityType: EntityTypeName } // Entity types
46
+ | { kind: 'selector' } // Selector type (multiple entities)
33
47
 
34
48
  export interface LambdaParam {
35
49
  name: string
@@ -43,6 +57,16 @@ export interface LambdaExpr {
43
57
  body: Expr | Block
44
58
  }
45
59
 
60
+ export type FStringPart =
61
+ | { kind: 'text'; value: string }
62
+ | { kind: 'expr'; expr: Expr }
63
+
64
+ export interface FStringExpr {
65
+ kind: 'f_string'
66
+ parts: FStringPart[]
67
+ span?: Span
68
+ }
69
+
46
70
  // ---------------------------------------------------------------------------
47
71
  // Range Expression
48
72
  // ---------------------------------------------------------------------------
@@ -115,15 +139,19 @@ export type Expr =
115
139
  | { kind: 'short_lit'; value: number; span?: Span }
116
140
  | { kind: 'long_lit'; value: number; span?: Span }
117
141
  | { kind: 'double_lit'; value: number; span?: Span }
142
+ | { kind: 'rel_coord'; value: string; span?: Span } // ~ ~5 ~-3 (relative coordinate)
143
+ | { kind: 'local_coord'; value: string; span?: Span } // ^ ^5 ^-3 (local/facing coordinate)
118
144
  | { kind: 'bool_lit'; value: boolean; span?: Span }
119
145
  | { kind: 'str_lit'; value: string; span?: Span }
120
146
  | { kind: 'mc_name'; value: string; span?: Span } // #health → "health" (MC identifier)
121
147
  | { kind: 'str_interp'; parts: Array<string | Expr>; span?: Span }
148
+ | FStringExpr
122
149
  | { kind: 'range_lit'; range: RangeExpr; span?: Span }
123
150
  | (BlockPosExpr & { span?: Span })
124
151
  | { kind: 'ident'; name: string; span?: Span }
125
152
  | { kind: 'selector'; raw: string; isSingle: boolean; sel: EntitySelector; span?: Span }
126
153
  | { kind: 'binary'; op: BinOp | CmpOp | '&&' | '||'; left: Expr; right: Expr; span?: Span }
154
+ | { kind: 'is_check'; expr: Expr; entityType: EntityTypeName; span?: Span }
127
155
  | { kind: 'unary'; op: '!' | '-'; operand: Expr; span?: Span }
128
156
  | { kind: 'assign'; target: string; op: AssignOp; value: Expr; span?: Span }
129
157
  | { kind: 'call'; fn: string; args: Expr[]; span?: Span }
@@ -180,9 +208,10 @@ export type Block = Stmt[]
180
208
  // ---------------------------------------------------------------------------
181
209
 
182
210
  export interface Decorator {
183
- name: 'tick' | 'load' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
211
+ name: 'tick' | 'load' | 'on' | 'on_trigger' | 'on_advancement' | 'on_craft' | 'on_death' | 'on_login' | 'on_join_team'
184
212
  args?: {
185
213
  rate?: number
214
+ eventType?: string
186
215
  trigger?: string
187
216
  advancement?: string
188
217
  item?: string
@@ -224,6 +253,13 @@ export interface StructDecl {
224
253
  span?: Span
225
254
  }
226
255
 
256
+ export interface ImplBlock {
257
+ kind: 'impl_block'
258
+ typeName: string
259
+ methods: FnDecl[]
260
+ span?: Span
261
+ }
262
+
227
263
  export interface EnumVariant {
228
264
  name: string
229
265
  value?: number
@@ -260,6 +296,7 @@ export interface Program {
260
296
  globals: GlobalDecl[]
261
297
  declarations: FnDecl[]
262
298
  structs: StructDecl[]
299
+ implBlocks: ImplBlock[]
263
300
  enums: EnumDecl[]
264
301
  consts: ConstDecl[]
265
302
  }
package/src/cli.ts CHANGED
@@ -26,7 +26,7 @@ function printUsage(): void {
26
26
  RedScript Compiler
27
27
 
28
28
  Usage:
29
- redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
29
+ redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>] [--no-dce]
30
30
  redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
31
31
  redscript check <file>
32
32
  redscript fmt <file.mcrs> [file2.mcrs ...]
@@ -46,6 +46,7 @@ Options:
46
46
  --output-nbt <file> Output .nbt file path for structure target
47
47
  --namespace <ns> Datapack namespace (default: derived from filename)
48
48
  --target <target> Output target: datapack (default), cmdblock, or structure
49
+ --no-dce Disable AST dead code elimination
49
50
  --stats Print optimizer statistics
50
51
  --hot-reload <url> After each successful compile, POST to <url>/reload
51
52
  (use with redscript-testharness; e.g. http://localhost:25561)
@@ -78,8 +79,9 @@ function parseArgs(args: string[]): {
78
79
  stats?: boolean
79
80
  help?: boolean
80
81
  hotReload?: string
82
+ dce?: boolean
81
83
  } {
82
- const result: ReturnType<typeof parseArgs> = {}
84
+ const result: ReturnType<typeof parseArgs> = { dce: true }
83
85
  let i = 0
84
86
 
85
87
  while (i < args.length) {
@@ -103,6 +105,9 @@ function parseArgs(args: string[]): {
103
105
  } else if (arg === '--stats') {
104
106
  result.stats = true
105
107
  i++
108
+ } else if (arg === '--no-dce') {
109
+ result.dce = false
110
+ i++
106
111
  } else if (arg === '--hot-reload') {
107
112
  result.hotReload = args[++i]
108
113
  i++
@@ -153,7 +158,14 @@ function printOptimizationStats(stats: OptimizationStats | undefined): void {
153
158
  console.log(` Total mcfunction commands: ${stats.totalCommandsBefore} -> ${stats.totalCommandsAfter} (${formatReduction(stats.totalCommandsBefore, stats.totalCommandsAfter)} reduction)`)
154
159
  }
155
160
 
156
- function compileCommand(file: string, output: string, namespace: string, target: string = 'datapack', showStats = false): void {
161
+ function compileCommand(
162
+ file: string,
163
+ output: string,
164
+ namespace: string,
165
+ target: string = 'datapack',
166
+ showStats = false,
167
+ dce = true
168
+ ): void {
157
169
  // Read source file
158
170
  if (!fs.existsSync(file)) {
159
171
  console.error(`Error: File not found: ${file}`)
@@ -164,7 +176,7 @@ function compileCommand(file: string, output: string, namespace: string, target:
164
176
 
165
177
  try {
166
178
  if (target === 'cmdblock') {
167
- const result = compile(source, { namespace, filePath: file })
179
+ const result = compile(source, { namespace, filePath: file, dce })
168
180
  printWarnings(result.warnings)
169
181
 
170
182
  // Generate command block JSON
@@ -184,7 +196,7 @@ function compileCommand(file: string, output: string, namespace: string, target:
184
196
  printOptimizationStats(result.stats)
185
197
  }
186
198
  } else if (target === 'structure') {
187
- const structure = compileToStructure(source, namespace, file)
199
+ const structure = compileToStructure(source, namespace, file, { dce })
188
200
  fs.mkdirSync(path.dirname(output), { recursive: true })
189
201
  fs.writeFileSync(output, structure.buffer)
190
202
 
@@ -195,7 +207,7 @@ function compileCommand(file: string, output: string, namespace: string, target:
195
207
  printOptimizationStats(structure.stats)
196
208
  }
197
209
  } else {
198
- const result = compile(source, { namespace, filePath: file })
210
+ const result = compile(source, { namespace, filePath: file, dce })
199
211
  printWarnings(result.warnings)
200
212
 
201
213
  // Default: generate datapack
@@ -255,7 +267,7 @@ async function hotReload(url: string): Promise<void> {
255
267
  }
256
268
  }
257
269
 
258
- function watchCommand(dir: string, output: string, namespace?: string, hotReloadUrl?: string): void {
270
+ function watchCommand(dir: string, output: string, namespace?: string, hotReloadUrl?: string, dce = true): void {
259
271
  // Check if directory exists
260
272
  if (!fs.existsSync(dir)) {
261
273
  console.error(`Error: Directory not found: ${dir}`)
@@ -290,7 +302,7 @@ function watchCommand(dir: string, output: string, namespace?: string, hotReload
290
302
  try {
291
303
  source = fs.readFileSync(file, 'utf-8')
292
304
  const ns = namespace ?? deriveNamespace(file)
293
- const result = compile(source, { namespace: ns, filePath: file })
305
+ const result = compile(source, { namespace: ns, filePath: file, dce })
294
306
  printWarnings(result.warnings)
295
307
 
296
308
  // Create output directory
@@ -382,7 +394,8 @@ async function main(): Promise<void> {
382
394
  output,
383
395
  namespace,
384
396
  target,
385
- parsed.stats
397
+ parsed.stats,
398
+ parsed.dce
386
399
  )
387
400
  }
388
401
  break
@@ -397,7 +410,8 @@ async function main(): Promise<void> {
397
410
  parsed.file,
398
411
  parsed.output ?? './dist',
399
412
  parsed.namespace,
400
- parsed.hotReload
413
+ parsed.hotReload,
414
+ parsed.dce
401
415
  )
402
416
  break
403
417