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
@@ -5,14 +5,92 @@
5
5
  * Collects errors but doesn't block compilation (warn mode).
6
6
  */
7
7
 
8
- import type { Program, FnDecl, Stmt, Expr, TypeNode, Block } from '../ast/types'
8
+ import type { Program, FnDecl, Stmt, Expr, TypeNode, Block, EntityTypeName, EntitySelector } from '../ast/types'
9
9
  import { DiagnosticError, DiagnosticCollector } from '../diagnostics'
10
+ import { getEventParamSpecs, isEventTypeName } from '../events/types'
10
11
 
11
12
  interface ScopeSymbol {
12
13
  type: TypeNode
13
14
  mutable: boolean
14
15
  }
15
16
 
17
+ interface BuiltinSignature {
18
+ params: TypeNode[]
19
+ return: TypeNode
20
+ }
21
+
22
+ // Entity type hierarchy for subtype checking
23
+ const ENTITY_HIERARCHY: Record<EntityTypeName, EntityTypeName | null> = {
24
+ 'entity': null,
25
+ 'Player': 'entity',
26
+ 'Mob': 'entity',
27
+ 'HostileMob': 'Mob',
28
+ 'PassiveMob': 'Mob',
29
+ 'Zombie': 'HostileMob',
30
+ 'Skeleton': 'HostileMob',
31
+ 'Creeper': 'HostileMob',
32
+ 'Spider': 'HostileMob',
33
+ 'Enderman': 'HostileMob',
34
+ 'Pig': 'PassiveMob',
35
+ 'Cow': 'PassiveMob',
36
+ 'Sheep': 'PassiveMob',
37
+ 'Chicken': 'PassiveMob',
38
+ 'Villager': 'PassiveMob',
39
+ 'ArmorStand': 'entity',
40
+ 'Item': 'entity',
41
+ 'Arrow': 'entity',
42
+ }
43
+
44
+ // Map Minecraft type names to entity types
45
+ const MC_TYPE_TO_ENTITY: Record<string, EntityTypeName> = {
46
+ 'zombie': 'Zombie',
47
+ 'minecraft:zombie': 'Zombie',
48
+ 'skeleton': 'Skeleton',
49
+ 'minecraft:skeleton': 'Skeleton',
50
+ 'creeper': 'Creeper',
51
+ 'minecraft:creeper': 'Creeper',
52
+ 'spider': 'Spider',
53
+ 'minecraft:spider': 'Spider',
54
+ 'enderman': 'Enderman',
55
+ 'minecraft:enderman': 'Enderman',
56
+ 'pig': 'Pig',
57
+ 'minecraft:pig': 'Pig',
58
+ 'cow': 'Cow',
59
+ 'minecraft:cow': 'Cow',
60
+ 'sheep': 'Sheep',
61
+ 'minecraft:sheep': 'Sheep',
62
+ 'chicken': 'Chicken',
63
+ 'minecraft:chicken': 'Chicken',
64
+ 'villager': 'Villager',
65
+ 'minecraft:villager': 'Villager',
66
+ 'armor_stand': 'ArmorStand',
67
+ 'minecraft:armor_stand': 'ArmorStand',
68
+ 'item': 'Item',
69
+ 'minecraft:item': 'Item',
70
+ 'arrow': 'Arrow',
71
+ 'minecraft:arrow': 'Arrow',
72
+ }
73
+
74
+ const VOID_TYPE: TypeNode = { kind: 'named', name: 'void' }
75
+ const INT_TYPE: TypeNode = { kind: 'named', name: 'int' }
76
+ const STRING_TYPE: TypeNode = { kind: 'named', name: 'string' }
77
+ const FORMAT_STRING_TYPE: TypeNode = { kind: 'named', name: 'format_string' }
78
+
79
+ const BUILTIN_SIGNATURES: Record<string, BuiltinSignature> = {
80
+ setTimeout: {
81
+ params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
82
+ return: VOID_TYPE,
83
+ },
84
+ setInterval: {
85
+ params: [INT_TYPE, { kind: 'function_type', params: [], return: VOID_TYPE }],
86
+ return: INT_TYPE,
87
+ },
88
+ clearInterval: {
89
+ params: [INT_TYPE],
90
+ return: VOID_TYPE,
91
+ },
92
+ }
93
+
16
94
  // ---------------------------------------------------------------------------
17
95
  // Type Checker
18
96
  // ---------------------------------------------------------------------------
@@ -20,12 +98,25 @@ interface ScopeSymbol {
20
98
  export class TypeChecker {
21
99
  private collector: DiagnosticCollector
22
100
  private functions: Map<string, FnDecl> = new Map()
101
+ private implMethods: Map<string, Map<string, FnDecl>> = new Map()
23
102
  private structs: Map<string, Map<string, TypeNode>> = new Map()
24
103
  private enums: Map<string, Map<string, number>> = new Map()
25
104
  private consts: Map<string, TypeNode> = new Map()
26
105
  private currentFn: FnDecl | null = null
27
106
  private currentReturnType: TypeNode | null = null
28
107
  private scope: Map<string, ScopeSymbol> = new Map()
108
+ // Stack for tracking @s type in different contexts
109
+ private selfTypeStack: EntityTypeName[] = ['entity']
110
+
111
+ private readonly richTextBuiltins = new Map<string, { messageIndex: number }>([
112
+ ['say', { messageIndex: 0 }],
113
+ ['announce', { messageIndex: 0 }],
114
+ ['tell', { messageIndex: 1 }],
115
+ ['tellraw', { messageIndex: 1 }],
116
+ ['title', { messageIndex: 1 }],
117
+ ['actionbar', { messageIndex: 1 }],
118
+ ['subtitle', { messageIndex: 1 }],
119
+ ])
29
120
 
30
121
  constructor(source?: string, filePath?: string) {
31
122
  this.collector = new DiagnosticCollector(source, filePath)
@@ -53,6 +144,28 @@ export class TypeChecker {
53
144
  this.functions.set(fn.name, fn)
54
145
  }
55
146
 
147
+ for (const implBlock of program.implBlocks ?? []) {
148
+ let methods = this.implMethods.get(implBlock.typeName)
149
+ if (!methods) {
150
+ methods = new Map()
151
+ this.implMethods.set(implBlock.typeName, methods)
152
+ }
153
+
154
+ for (const method of implBlock.methods) {
155
+ const selfIndex = method.params.findIndex(param => param.name === 'self')
156
+ if (selfIndex > 0) {
157
+ this.report(`Method '${method.name}' must declare 'self' as the first parameter`, method.params[selfIndex])
158
+ }
159
+ if (selfIndex === 0) {
160
+ const selfType = this.normalizeType(method.params[0].type)
161
+ if (selfType.kind !== 'struct' || selfType.name !== implBlock.typeName) {
162
+ this.report(`Method '${method.name}' has invalid 'self' type`, method.params[0])
163
+ }
164
+ }
165
+ methods.set(method.name, method)
166
+ }
167
+ }
168
+
56
169
  for (const struct of program.structs ?? []) {
57
170
  const fields = new Map<string, TypeNode>()
58
171
  for (const field of struct.fields) {
@@ -86,6 +199,12 @@ export class TypeChecker {
86
199
  this.checkFunction(fn)
87
200
  }
88
201
 
202
+ for (const implBlock of program.implBlocks ?? []) {
203
+ for (const method of implBlock.methods) {
204
+ this.checkFunction(method)
205
+ }
206
+ }
207
+
89
208
  return this.collector.getErrors()
90
209
  }
91
210
 
@@ -95,6 +214,8 @@ export class TypeChecker {
95
214
  this.scope = new Map()
96
215
  let seenDefault = false
97
216
 
217
+ this.checkFunctionDecorators(fn)
218
+
98
219
  for (const [name, type] of this.consts.entries()) {
99
220
  this.scope.set(name, { type, mutable: false })
100
221
  }
@@ -125,6 +246,49 @@ export class TypeChecker {
125
246
  this.currentReturnType = null
126
247
  }
127
248
 
249
+ private checkFunctionDecorators(fn: FnDecl): void {
250
+ const eventDecorators = fn.decorators.filter(decorator => decorator.name === 'on')
251
+ if (eventDecorators.length === 0) {
252
+ return
253
+ }
254
+
255
+ if (eventDecorators.length > 1) {
256
+ this.report(`Function '${fn.name}' cannot have multiple @on decorators`, fn)
257
+ return
258
+ }
259
+
260
+ const eventType = eventDecorators[0].args?.eventType
261
+ if (!eventType) {
262
+ this.report(`Function '${fn.name}' is missing an event type in @on(...)`, fn)
263
+ return
264
+ }
265
+
266
+ if (!isEventTypeName(eventType)) {
267
+ this.report(`Unknown event type '${eventType}'`, fn)
268
+ return
269
+ }
270
+
271
+ const expectedParams = getEventParamSpecs(eventType)
272
+ if (fn.params.length !== expectedParams.length) {
273
+ this.report(
274
+ `Event handler '${fn.name}' for ${eventType} must declare ${expectedParams.length} parameter(s), got ${fn.params.length}`,
275
+ fn
276
+ )
277
+ return
278
+ }
279
+
280
+ for (let i = 0; i < expectedParams.length; i++) {
281
+ const actual = this.normalizeType(fn.params[i].type)
282
+ const expected = this.normalizeType(expectedParams[i].type)
283
+ if (!this.typesMatch(expected, actual)) {
284
+ this.report(
285
+ `Event handler '${fn.name}' parameter ${i + 1} must be ${this.typeToString(expected)}, got ${this.typeToString(actual)}`,
286
+ fn.params[i]
287
+ )
288
+ }
289
+ }
290
+ }
291
+
128
292
  private checkBlock(stmts: Block): void {
129
293
  for (const stmt of stmts) {
130
294
  this.checkStmt(stmt)
@@ -141,8 +305,7 @@ export class TypeChecker {
141
305
  break
142
306
  case 'if':
143
307
  this.checkExpr(stmt.cond)
144
- this.checkBlock(stmt.then)
145
- if (stmt.else_) this.checkBlock(stmt.else_)
308
+ this.checkIfBranches(stmt)
146
309
  break
147
310
  case 'while':
148
311
  this.checkExpr(stmt.cond)
@@ -157,7 +320,16 @@ export class TypeChecker {
157
320
  case 'foreach':
158
321
  this.checkExpr(stmt.iterable)
159
322
  if (stmt.iterable.kind === 'selector') {
160
- this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true }) // Entity marker
323
+ // Infer entity type from selector (access .sel for the EntitySelector)
324
+ const entityType = this.inferEntityTypeFromSelector(stmt.iterable.sel)
325
+ this.scope.set(stmt.binding, {
326
+ type: { kind: 'entity', entityType },
327
+ mutable: false // Entity bindings are not reassignable
328
+ })
329
+ // Push self type context for @s inside the loop
330
+ this.pushSelfType(entityType)
331
+ this.checkBlock(stmt.body)
332
+ this.popSelfType()
161
333
  } else {
162
334
  const iterableType = this.inferType(stmt.iterable)
163
335
  if (iterableType.kind === 'array') {
@@ -165,8 +337,8 @@ export class TypeChecker {
165
337
  } else {
166
338
  this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true })
167
339
  }
340
+ this.checkBlock(stmt.body)
168
341
  }
169
- this.checkBlock(stmt.body)
170
342
  break
171
343
  case 'match':
172
344
  this.checkExpr(stmt.expr)
@@ -180,15 +352,41 @@ export class TypeChecker {
180
352
  this.checkBlock(arm.body)
181
353
  }
182
354
  break
183
- case 'as_block':
355
+ case 'as_block': {
356
+ // as block changes @s to the selector's entity type
357
+ const entityType = this.inferEntityTypeFromSelector(stmt.selector)
358
+ this.pushSelfType(entityType)
359
+ this.checkBlock(stmt.body)
360
+ this.popSelfType()
361
+ break
362
+ }
184
363
  case 'at_block':
364
+ // at block doesn't change @s type, only position
185
365
  this.checkBlock(stmt.body)
186
366
  break
187
- case 'as_at':
367
+ case 'as_at': {
368
+ // as @x at @y - @s becomes the as selector's type
369
+ const entityType = this.inferEntityTypeFromSelector(stmt.as_sel)
370
+ this.pushSelfType(entityType)
188
371
  this.checkBlock(stmt.body)
372
+ this.popSelfType()
189
373
  break
374
+ }
190
375
  case 'execute':
376
+ // execute with subcommands - check for 'as' subcommands
377
+ for (const sub of stmt.subcommands) {
378
+ if (sub.kind === 'as' && sub.selector) {
379
+ const entityType = this.inferEntityTypeFromSelector(sub.selector)
380
+ this.pushSelfType(entityType)
381
+ }
382
+ }
191
383
  this.checkBlock(stmt.body)
384
+ // Pop for each 'as' subcommand
385
+ for (const sub of stmt.subcommands) {
386
+ if (sub.kind === 'as') {
387
+ this.popSelfType()
388
+ }
389
+ }
192
390
  break
193
391
  case 'expr':
194
392
  this.checkExpr(stmt.expr)
@@ -265,12 +463,24 @@ export class TypeChecker {
265
463
  case 'member':
266
464
  this.checkMemberExpr(expr)
267
465
  break
466
+ case 'static_call':
467
+ this.checkStaticCallExpr(expr)
468
+ break
268
469
 
269
470
  case 'binary':
270
471
  this.checkExpr(expr.left)
271
472
  this.checkExpr(expr.right)
272
473
  break
273
474
 
475
+ case 'is_check': {
476
+ this.checkExpr(expr.expr)
477
+ const checkedType = this.inferType(expr.expr)
478
+ if (checkedType.kind !== 'entity') {
479
+ this.report(`'is' checks require an entity expression, got ${this.typeToString(checkedType)}`, expr.expr)
480
+ }
481
+ break
482
+ }
483
+
274
484
  case 'unary':
275
485
  this.checkExpr(expr.operand)
276
486
  break
@@ -312,6 +522,24 @@ export class TypeChecker {
312
522
  }
313
523
  break
314
524
 
525
+ case 'f_string':
526
+ for (const part of expr.parts) {
527
+ if (part.kind !== 'expr') {
528
+ continue
529
+ }
530
+ this.checkExpr(part.expr)
531
+ const partType = this.inferType(part.expr)
532
+ if (
533
+ !(partType.kind === 'named' && (partType.name === 'int' || partType.name === 'string' || partType.name === 'format_string'))
534
+ ) {
535
+ this.report(
536
+ `f-string placeholder must be int or string, got ${this.typeToString(partType)}`,
537
+ part.expr
538
+ )
539
+ }
540
+ }
541
+ break
542
+
315
543
  case 'array_lit':
316
544
  for (const elem of expr.elements) {
317
545
  this.checkExpr(elem)
@@ -325,12 +553,6 @@ export class TypeChecker {
325
553
  case 'blockpos':
326
554
  break
327
555
 
328
- case 'static_call':
329
- for (const arg of expr.args) {
330
- this.checkExpr(arg)
331
- }
332
- break
333
-
334
556
  // Literals don't need checking
335
557
  case 'int_lit':
336
558
  case 'float_lit':
@@ -352,6 +574,18 @@ export class TypeChecker {
352
574
  this.checkTpCall(expr)
353
575
  }
354
576
 
577
+ const richTextBuiltin = this.richTextBuiltins.get(expr.fn)
578
+ if (richTextBuiltin) {
579
+ this.checkRichTextBuiltinCall(expr, richTextBuiltin.messageIndex)
580
+ return
581
+ }
582
+
583
+ const builtin = BUILTIN_SIGNATURES[expr.fn]
584
+ if (builtin) {
585
+ this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr)
586
+ return
587
+ }
588
+
355
589
  // Check if function exists and arg count matches
356
590
  const fn = this.functions.get(expr.fn)
357
591
  if (fn) {
@@ -387,12 +621,45 @@ export class TypeChecker {
387
621
  return
388
622
  }
389
623
 
624
+ const implMethod = this.resolveInstanceMethod(expr)
625
+ if (implMethod) {
626
+ this.checkFunctionCallArgs(
627
+ expr.args,
628
+ implMethod.params.map(param => this.normalizeType(param.type)),
629
+ implMethod.name,
630
+ expr
631
+ )
632
+ return
633
+ }
634
+
390
635
  for (const arg of expr.args) {
391
636
  this.checkExpr(arg)
392
637
  }
393
638
  // Built-in functions are not checked for arg count
394
639
  }
395
640
 
641
+ private checkRichTextBuiltinCall(expr: Extract<Expr, { kind: 'call' }>, messageIndex: number): void {
642
+ for (let i = 0; i < expr.args.length; i++) {
643
+ this.checkExpr(expr.args[i], i === messageIndex ? undefined : STRING_TYPE)
644
+ }
645
+
646
+ const message = expr.args[messageIndex]
647
+ if (!message) {
648
+ return
649
+ }
650
+
651
+ const messageType = this.inferType(message)
652
+ if (
653
+ messageType.kind !== 'named' ||
654
+ (messageType.name !== 'string' && messageType.name !== 'format_string')
655
+ ) {
656
+ this.report(
657
+ `Argument ${messageIndex + 1} of '${expr.fn}' expects string or format_string, got ${this.typeToString(messageType)}`,
658
+ message
659
+ )
660
+ }
661
+ }
662
+
396
663
  private checkInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): void {
397
664
  this.checkExpr(expr.callee)
398
665
  const calleeType = this.inferType(expr.callee)
@@ -497,6 +764,29 @@ export class TypeChecker {
497
764
  }
498
765
  }
499
766
 
767
+ private checkStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): void {
768
+ const method = this.implMethods.get(expr.type)?.get(expr.method)
769
+ if (!method) {
770
+ this.report(`Type '${expr.type}' has no static method '${expr.method}'`, expr)
771
+ for (const arg of expr.args) {
772
+ this.checkExpr(arg)
773
+ }
774
+ return
775
+ }
776
+
777
+ if (method.params[0]?.name === 'self') {
778
+ this.report(`Method '${expr.type}::${expr.method}' is an instance method`, expr)
779
+ return
780
+ }
781
+
782
+ this.checkFunctionCallArgs(
783
+ expr.args,
784
+ method.params.map(param => this.normalizeType(param.type)),
785
+ `${expr.type}::${expr.method}`,
786
+ expr
787
+ )
788
+ }
789
+
500
790
  private checkLambdaExpr(expr: Extract<Expr, { kind: 'lambda' }>, expectedType?: TypeNode): void {
501
791
  const normalizedExpected = expectedType ? this.normalizeType(expectedType) : undefined
502
792
  const expectedFnType = normalizedExpected?.kind === 'function_type' ? normalizedExpected : undefined
@@ -544,6 +834,42 @@ export class TypeChecker {
544
834
  this.currentReturnType = outerReturnType
545
835
  }
546
836
 
837
+ private checkIfBranches(stmt: Extract<Stmt, { kind: 'if' }>): void {
838
+ const narrowed = this.getThenBranchNarrowing(stmt.cond)
839
+
840
+ if (narrowed) {
841
+ const thenScope = new Map(this.scope)
842
+ thenScope.set(narrowed.name, { type: narrowed.type, mutable: narrowed.mutable })
843
+ const outerScope = this.scope
844
+ this.scope = thenScope
845
+ this.checkBlock(stmt.then)
846
+ this.scope = outerScope
847
+ } else {
848
+ this.checkBlock(stmt.then)
849
+ }
850
+
851
+ if (stmt.else_) {
852
+ this.checkBlock(stmt.else_)
853
+ }
854
+ }
855
+
856
+ private getThenBranchNarrowing(cond: Expr): { name: string; type: Extract<TypeNode, { kind: 'entity' }>; mutable: boolean } | null {
857
+ if (cond.kind !== 'is_check' || cond.expr.kind !== 'ident') {
858
+ return null
859
+ }
860
+
861
+ const symbol = this.scope.get(cond.expr.name)
862
+ if (!symbol || symbol.type.kind !== 'entity') {
863
+ return null
864
+ }
865
+
866
+ return {
867
+ name: cond.expr.name,
868
+ type: { kind: 'entity', entityType: cond.entityType },
869
+ mutable: symbol.mutable,
870
+ }
871
+ }
872
+
547
873
  private inferType(expr: Expr, expectedType?: TypeNode): TypeNode {
548
874
  switch (expr.kind) {
549
875
  case 'int_lit':
@@ -570,13 +896,24 @@ export class TypeChecker {
570
896
  }
571
897
  }
572
898
  return { kind: 'named', name: 'string' }
899
+ case 'f_string':
900
+ for (const part of expr.parts) {
901
+ if (part.kind === 'expr') {
902
+ this.checkExpr(part.expr)
903
+ }
904
+ }
905
+ return FORMAT_STRING_TYPE
573
906
  case 'blockpos':
574
907
  return { kind: 'named', name: 'BlockPos' }
575
908
  case 'ident':
576
909
  return this.scope.get(expr.name)?.type ?? { kind: 'named', name: 'void' }
577
910
  case 'call': {
911
+ const builtin = BUILTIN_SIGNATURES[expr.fn]
912
+ if (builtin) {
913
+ return builtin.return
914
+ }
578
915
  if (expr.fn === '__array_push') {
579
- return { kind: 'named', name: 'void' }
916
+ return VOID_TYPE
580
917
  }
581
918
  if (expr.fn === '__array_pop') {
582
919
  const target = expr.args[0]
@@ -584,20 +921,28 @@ export class TypeChecker {
584
921
  const targetType = this.scope.get(target.name)?.type
585
922
  if (targetType?.kind === 'array') return targetType.elem
586
923
  }
587
- return { kind: 'named', name: 'int' }
924
+ return INT_TYPE
588
925
  }
589
926
  if (expr.fn === 'bossbar_get_value') {
590
- return { kind: 'named', name: 'int' }
927
+ return INT_TYPE
591
928
  }
592
929
  if (expr.fn === 'random_sequence') {
593
- return { kind: 'named', name: 'void' }
930
+ return VOID_TYPE
594
931
  }
595
932
  const varType = this.scope.get(expr.fn)?.type
596
933
  if (varType?.kind === 'function_type') {
597
934
  return varType.return
598
935
  }
936
+ const implMethod = this.resolveInstanceMethod(expr)
937
+ if (implMethod) {
938
+ return this.normalizeType(implMethod.returnType)
939
+ }
599
940
  const fn = this.functions.get(expr.fn)
600
- return fn?.returnType ?? { kind: 'named', name: 'int' }
941
+ return fn?.returnType ?? INT_TYPE
942
+ }
943
+ case 'static_call': {
944
+ const method = this.implMethods.get(expr.type)?.get(expr.method)
945
+ return method ? this.normalizeType(method.returnType) : { kind: 'named', name: 'void' }
601
946
  }
602
947
  case 'invoke': {
603
948
  const calleeType = this.inferType(expr.callee)
@@ -627,6 +972,8 @@ export class TypeChecker {
627
972
  return { kind: 'named', name: 'bool' }
628
973
  }
629
974
  return this.inferType(expr.left)
975
+ case 'is_check':
976
+ return { kind: 'named', name: 'bool' }
630
977
  case 'unary':
631
978
  if (expr.op === '!') return { kind: 'named', name: 'bool' }
632
979
  return this.inferType(expr.operand)
@@ -635,6 +982,14 @@ export class TypeChecker {
635
982
  return { kind: 'array', elem: this.inferType(expr.elements[0]) }
636
983
  }
637
984
  return { kind: 'array', elem: { kind: 'named', name: 'int' } }
985
+ case 'struct_lit':
986
+ if (expectedType) {
987
+ const normalized = this.normalizeType(expectedType)
988
+ if (normalized.kind === 'struct') {
989
+ return normalized
990
+ }
991
+ }
992
+ return { kind: 'named', name: 'void' }
638
993
  case 'lambda':
639
994
  return this.inferLambdaType(
640
995
  expr,
@@ -673,6 +1028,80 @@ export class TypeChecker {
673
1028
  return { kind: 'function_type', params, return: returnType }
674
1029
  }
675
1030
 
1031
+ // ---------------------------------------------------------------------------
1032
+ // Entity Type Helpers
1033
+ // ---------------------------------------------------------------------------
1034
+
1035
+ /** Infer entity type from a selector */
1036
+ private inferEntityTypeFromSelector(selector: EntitySelector): EntityTypeName {
1037
+ // @a, @p, @r always return Player
1038
+ if (selector.kind === '@a' || selector.kind === '@p' || selector.kind === '@r') {
1039
+ return 'Player'
1040
+ }
1041
+
1042
+ // @e or @s with type= filter
1043
+ if (selector.filters?.type) {
1044
+ const mcType = selector.filters.type.toLowerCase()
1045
+ return MC_TYPE_TO_ENTITY[mcType] ?? 'entity'
1046
+ }
1047
+
1048
+ // @s uses current context
1049
+ if (selector.kind === '@s') {
1050
+ return this.selfTypeStack[this.selfTypeStack.length - 1]
1051
+ }
1052
+
1053
+ // Default to entity
1054
+ return 'entity'
1055
+ }
1056
+
1057
+ private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): FnDecl | null {
1058
+ const receiver = expr.args[0]
1059
+ if (!receiver) {
1060
+ return null
1061
+ }
1062
+
1063
+ const receiverType = this.inferType(receiver)
1064
+ if (receiverType.kind !== 'struct') {
1065
+ return null
1066
+ }
1067
+
1068
+ const method = this.implMethods.get(receiverType.name)?.get(expr.fn)
1069
+ if (!method || method.params[0]?.name !== 'self') {
1070
+ return null
1071
+ }
1072
+
1073
+ return method
1074
+ }
1075
+
1076
+ /** Check if childType is a subtype of parentType */
1077
+ private isEntitySubtype(childType: EntityTypeName, parentType: EntityTypeName): boolean {
1078
+ if (childType === parentType) return true
1079
+
1080
+ let current: EntityTypeName | null = childType
1081
+ while (current !== null) {
1082
+ if (current === parentType) return true
1083
+ current = ENTITY_HIERARCHY[current]
1084
+ }
1085
+ return false
1086
+ }
1087
+
1088
+ /** Push a new self type context */
1089
+ private pushSelfType(entityType: EntityTypeName): void {
1090
+ this.selfTypeStack.push(entityType)
1091
+ }
1092
+
1093
+ /** Pop self type context */
1094
+ private popSelfType(): void {
1095
+ if (this.selfTypeStack.length > 1) {
1096
+ this.selfTypeStack.pop()
1097
+ }
1098
+ }
1099
+
1100
+ /** Get current @s type */
1101
+ private getCurrentSelfType(): EntityTypeName {
1102
+ return this.selfTypeStack[this.selfTypeStack.length - 1]
1103
+ }
1104
+
676
1105
  private typesMatch(expected: TypeNode, actual: TypeNode): boolean {
677
1106
  if (expected.kind !== actual.kind) return false
678
1107
 
@@ -700,6 +1129,16 @@ export class TypeChecker {
700
1129
  this.typesMatch(expected.return, actual.return)
701
1130
  }
702
1131
 
1132
+ // Entity type matching with subtype support
1133
+ if (expected.kind === 'entity' && actual.kind === 'entity') {
1134
+ return this.isEntitySubtype(actual.entityType, expected.entityType)
1135
+ }
1136
+
1137
+ // Selector matches any entity type
1138
+ if (expected.kind === 'selector' && actual.kind === 'entity') {
1139
+ return true
1140
+ }
1141
+
703
1142
  return false
704
1143
  }
705
1144
 
@@ -715,6 +1154,12 @@ export class TypeChecker {
715
1154
  return type.name
716
1155
  case 'function_type':
717
1156
  return `(${type.params.map(param => this.typeToString(param)).join(', ')}) -> ${this.typeToString(type.return)}`
1157
+ case 'entity':
1158
+ return type.entityType
1159
+ case 'selector':
1160
+ return 'selector'
1161
+ default:
1162
+ return 'unknown'
718
1163
  }
719
1164
  }
720
1165
 
@@ -732,6 +1177,12 @@ export class TypeChecker {
732
1177
  if ((type.kind === 'struct' || type.kind === 'enum') && this.enums.has(type.name)) {
733
1178
  return { kind: 'enum', name: type.name }
734
1179
  }
1180
+ if (type.kind === 'struct' && type.name in ENTITY_HIERARCHY) {
1181
+ return { kind: 'entity', entityType: type.name as EntityTypeName }
1182
+ }
1183
+ if (type.kind === 'named' && type.name in ENTITY_HIERARCHY) {
1184
+ return { kind: 'entity', entityType: type.name as EntityTypeName }
1185
+ }
735
1186
  return type
736
1187
  }
737
1188
  }