redscript-mc 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +112 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +148 -10
  9. package/dist/__tests__/codegen.test.js +26 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +336 -17
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lexer.test.js +12 -2
  15. package/dist/__tests__/lowering.test.js +200 -12
  16. package/dist/__tests__/mc-integration.test.js +370 -31
  17. package/dist/__tests__/mc-syntax.test.js +3 -3
  18. package/dist/__tests__/nbt.test.js +2 -2
  19. package/dist/__tests__/optimizer-advanced.test.js +5 -5
  20. package/dist/__tests__/parser.test.js +80 -0
  21. package/dist/__tests__/runtime.test.js +9 -9
  22. package/dist/__tests__/typechecker.test.js +158 -0
  23. package/dist/ast/types.d.ts +40 -3
  24. package/dist/cli.js +25 -7
  25. package/dist/codegen/mcfunction/index.d.ts +1 -1
  26. package/dist/codegen/mcfunction/index.js +38 -3
  27. package/dist/codegen/structure/index.js +32 -1
  28. package/dist/compile.d.ts +10 -0
  29. package/dist/compile.js +36 -5
  30. package/dist/events/types.d.ts +35 -0
  31. package/dist/events/types.js +59 -0
  32. package/dist/formatter/index.d.ts +1 -0
  33. package/dist/formatter/index.js +26 -0
  34. package/dist/index.js +3 -2
  35. package/dist/ir/builder.d.ts +2 -1
  36. package/dist/ir/types.d.ts +11 -2
  37. package/dist/ir/types.js +1 -1
  38. package/dist/lexer/index.d.ts +1 -1
  39. package/dist/lexer/index.js +2 -0
  40. package/dist/lowering/index.d.ts +34 -1
  41. package/dist/lowering/index.js +622 -23
  42. package/dist/mc-test/runner.d.ts +2 -2
  43. package/dist/mc-test/runner.js +3 -3
  44. package/dist/mc-test/setup.js +2 -2
  45. package/dist/parser/index.d.ts +4 -0
  46. package/dist/parser/index.js +153 -16
  47. package/dist/typechecker/index.d.ts +17 -0
  48. package/dist/typechecker/index.js +343 -17
  49. package/docs/COMPILATION_STATS.md +24 -24
  50. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  51. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  52. package/docs/STRUCTURE_TARGET.md +1 -1
  53. package/editors/vscode/.vscodeignore +1 -0
  54. package/editors/vscode/CHANGELOG.md +9 -0
  55. package/editors/vscode/icons/mcrs.svg +7 -0
  56. package/editors/vscode/icons/redscript-icons.json +10 -0
  57. package/editors/vscode/out/extension.js +1295 -80
  58. package/editors/vscode/package-lock.json +2 -2
  59. package/editors/vscode/package.json +10 -3
  60. package/editors/vscode/src/hover.ts +55 -2
  61. package/editors/vscode/src/symbols.ts +42 -0
  62. package/package.json +1 -1
  63. package/src/__tests__/cli.test.ts +176 -10
  64. package/src/__tests__/codegen.test.ts +28 -1
  65. package/src/__tests__/diagnostics.test.ts +5 -5
  66. package/src/__tests__/e2e.test.ts +335 -17
  67. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  68. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  69. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  70. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  71. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  72. package/src/__tests__/lexer.test.ts +14 -2
  73. package/src/__tests__/lowering.test.ts +226 -12
  74. package/src/__tests__/mc-integration.test.ts +421 -31
  75. package/src/__tests__/mc-syntax.test.ts +3 -3
  76. package/src/__tests__/nbt.test.ts +2 -2
  77. package/src/__tests__/optimizer-advanced.test.ts +5 -5
  78. package/src/__tests__/parser.test.ts +91 -5
  79. package/src/__tests__/runtime.test.ts +9 -9
  80. package/src/__tests__/typechecker.test.ts +171 -0
  81. package/src/ast/types.ts +44 -3
  82. package/src/cli.ts +10 -10
  83. package/src/codegen/mcfunction/index.ts +40 -3
  84. package/src/codegen/structure/index.ts +35 -1
  85. package/src/compile.ts +54 -6
  86. package/src/events/types.ts +69 -0
  87. package/src/examples/capture_the_flag.mcrs +208 -0
  88. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  89. package/src/examples/hunger_games.mcrs +301 -0
  90. package/src/examples/new_features_demo.mcrs +193 -0
  91. package/src/examples/parkour_race.mcrs +233 -0
  92. package/src/examples/rpg.mcrs +13 -0
  93. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  94. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  95. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  96. package/src/examples/zombie_survival.mcrs +314 -0
  97. package/src/index.ts +4 -3
  98. package/src/ir/builder.ts +3 -1
  99. package/src/ir/types.ts +12 -2
  100. package/src/lexer/index.ts +3 -1
  101. package/src/lowering/index.ts +684 -24
  102. package/src/mc-test/runner.ts +3 -3
  103. package/src/mc-test/setup.ts +2 -2
  104. package/src/parser/index.ts +170 -19
  105. package/src/stdlib/README.md +178 -140
  106. package/src/stdlib/bossbar.mcrs +68 -0
  107. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  108. package/src/stdlib/effects.mcrs +64 -0
  109. package/src/stdlib/interactions.mcrs +195 -0
  110. package/src/stdlib/inventory.mcrs +38 -0
  111. package/src/stdlib/mobs.mcrs +99 -0
  112. package/src/stdlib/particles.mcrs +52 -0
  113. package/src/stdlib/sets.mcrs +20 -0
  114. package/src/stdlib/spawn.mcrs +41 -0
  115. package/src/stdlib/tags.mcrs +951 -0
  116. package/src/stdlib/teams.mcrs +68 -0
  117. package/src/stdlib/timer.mcrs +72 -0
  118. package/src/stdlib/world.mcrs +92 -0
  119. package/src/typechecker/index.ts +404 -18
  120. package/src/examples/rpg.rs +0 -13
  121. package/src/stdlib/mobs.rs +0 -99
  122. package/src/stdlib/timer.rs +0 -51
  123. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  124. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  125. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  126. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  127. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  128. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  129. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  130. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  131. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  132. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  133. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  134. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  135. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  136. /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
@@ -9,10 +9,14 @@ import type { IRBuilder } from '../ir/builder'
9
9
  import { buildModule } from '../ir/builder'
10
10
  import type { IRFunction, IRModule, Operand, BinOp, CmpOp } from '../ir/types'
11
11
  import { DiagnosticError } from '../diagnostics'
12
+ import type { SourceRange } from '../compile'
12
13
  import type {
13
- Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, Program, RangeExpr, Span, Stmt,
14
- StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent
14
+ Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, Program, RangeExpr, Span, Stmt,
15
+ StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent, EntityTypeName
15
16
  } from '../ast/types'
17
+ import type { GlobalVar } from '../ir/types'
18
+ import * as path from 'path'
19
+ import { EVENT_TYPES, getEventParamSpecs, isEventTypeName } from '../events/types'
16
20
 
17
21
  // ---------------------------------------------------------------------------
18
22
  // Builtin Functions
@@ -28,7 +32,8 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
28
32
  announce: ([msg]) => `tellraw @a {"text":"${msg}"}`,
29
33
  give: ([sel, item, count, nbt]) => nbt ? `give ${sel} ${item}${nbt} ${count ?? '1'}` : `give ${sel} ${item} ${count ?? '1'}`,
30
34
  kill: ([sel]) => `kill ${sel ?? '@s'}`,
31
- effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
35
+ effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
36
+ effect_clear: ([sel, eff]) => eff ? `effect clear ${sel} ${eff}` : `effect clear ${sel}`,
32
37
  summon: ([type, x, y, z, nbt]) => {
33
38
  const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
34
39
  return nbt ? `summon ${type} ${pos} ${nbt}` : `summon ${type} ${pos}`
@@ -80,11 +85,15 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
80
85
  team_leave: () => null, // Special handling
81
86
  team_option: () => null, // Special handling
82
87
  data_get: () => null, // Special handling (returns value from NBT)
88
+ data_merge: () => null, // Special handling (merge NBT)
83
89
  set_new: () => null, // Special handling (returns set ID)
84
90
  set_add: () => null, // Special handling
85
91
  set_contains: () => null, // Special handling (returns 1/0)
86
92
  set_remove: () => null, // Special handling
87
93
  set_clear: () => null, // Special handling
94
+ setTimeout: () => null, // Special handling
95
+ setInterval: () => null, // Special handling
96
+ clearInterval: () => null, // Special handling
88
97
  }
89
98
 
90
99
  export interface Warning {
@@ -94,6 +103,12 @@ export interface Warning {
94
103
  col?: number
95
104
  }
96
105
 
106
+ interface StdlibCallSiteContext {
107
+ filePath?: string
108
+ line: number
109
+ col: number
110
+ }
111
+
97
112
  function getSpan(node: unknown): Span | undefined {
98
113
  return (node as { span?: Span } | undefined)?.span
99
114
  }
@@ -101,6 +116,23 @@ function getSpan(node: unknown): Span | undefined {
101
116
  const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/
102
117
  const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/
103
118
 
119
+ const ENTITY_TO_MC_TYPE: Partial<Record<EntityTypeName, string>> = {
120
+ Player: 'player',
121
+ Zombie: 'zombie',
122
+ Skeleton: 'skeleton',
123
+ Creeper: 'creeper',
124
+ Spider: 'spider',
125
+ Enderman: 'enderman',
126
+ Pig: 'pig',
127
+ Cow: 'cow',
128
+ Sheep: 'sheep',
129
+ Chicken: 'chicken',
130
+ Villager: 'villager',
131
+ ArmorStand: 'armor_stand',
132
+ Item: 'item',
133
+ Arrow: 'arrow',
134
+ }
135
+
104
136
  function normalizeSelector(selector: string, warnings: Warning[]): string {
105
137
  return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
106
138
  const trimmed = entityType.trim()
@@ -153,19 +185,27 @@ function emitBlockPos(pos: BlockPosExpr): string {
153
185
 
154
186
  export class Lowering {
155
187
  private namespace: string
188
+ private readonly sourceRanges: SourceRange[]
156
189
  private functions: IRFunction[] = []
157
- private globals: string[] = []
190
+ private globals: GlobalVar[] = []
191
+ private globalNames: Map<string, { mutable: boolean }> = new Map()
158
192
  private fnDecls: Map<string, FnDecl> = new Map()
193
+ private implMethods: Map<string, Map<string, { fn: FnDecl; loweredName: string }>> = new Map()
159
194
  private specializedFunctions: Map<string, string> = new Map()
160
195
  private currentFn: string = ''
196
+ private currentStdlibCallSite?: StdlibCallSiteContext
161
197
  private foreachCounter: number = 0
162
198
  private lambdaCounter: number = 0
199
+ private timeoutCounter: number = 0
200
+ private intervalCounter: number = 0
163
201
  readonly warnings: Warning[] = []
164
202
 
165
203
  // Builder state for current function
166
204
  private builder!: LoweringBuilder
167
205
  private varMap: Map<string, string> = new Map()
168
206
  private lambdaBindings: Map<string, string> = new Map()
207
+ private intervalBindings: Map<string, string> = new Map()
208
+ private intervalFunctions: Map<number, string> = new Map()
169
209
  private currentCallbackBindings: Map<string, string> = new Map()
170
210
  private currentContext: { binding?: string } = {}
171
211
  private blockPosVars: Map<string, BlockPosExpr> = new Map()
@@ -183,8 +223,10 @@ export class Lowering {
183
223
  // World object counter for unique tags
184
224
  private worldObjCounter: number = 0
185
225
 
186
- constructor(namespace: string) {
226
+ constructor(namespace: string, sourceRanges: SourceRange[] = []) {
187
227
  this.namespace = namespace
228
+ this.sourceRanges = sourceRanges
229
+ LoweringBuilder.resetTempCounter()
188
230
  }
189
231
 
190
232
  lower(program: Program): IRModule {
@@ -212,15 +254,44 @@ export class Lowering {
212
254
  this.varTypes.set(constDecl.name, this.normalizeType(constDecl.type))
213
255
  }
214
256
 
257
+ // Process global variable declarations (top-level let)
258
+ for (const g of program.globals ?? []) {
259
+ this.globalNames.set(g.name, { mutable: g.mutable })
260
+ this.varTypes.set(g.name, this.normalizeType(g.type))
261
+ const initValue = g.init.kind === 'int_lit' ? g.init.value : 0
262
+ this.globals.push({ name: `$${g.name}`, init: initValue })
263
+ }
264
+
215
265
  for (const fn of program.declarations) {
216
266
  this.fnDecls.set(fn.name, fn)
217
267
  this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
218
268
  }
219
269
 
270
+ for (const implBlock of program.implBlocks ?? []) {
271
+ let methods = this.implMethods.get(implBlock.typeName)
272
+ if (!methods) {
273
+ methods = new Map()
274
+ this.implMethods.set(implBlock.typeName, methods)
275
+ }
276
+
277
+ for (const method of implBlock.methods) {
278
+ const loweredName = `${implBlock.typeName}_${method.name}`
279
+ methods.set(method.name, { fn: method, loweredName })
280
+ this.fnDecls.set(loweredName, method)
281
+ this.functionDefaults.set(loweredName, method.params.map(param => param.default))
282
+ }
283
+ }
284
+
220
285
  for (const fn of program.declarations) {
221
286
  this.lowerFn(fn)
222
287
  }
223
288
 
289
+ for (const implBlock of program.implBlocks ?? []) {
290
+ for (const method of implBlock.methods) {
291
+ this.lowerFn(method, { name: `${implBlock.typeName}_${method.name}` })
292
+ }
293
+ }
294
+
224
295
  return buildModule(this.namespace, this.functions, this.globals)
225
296
  }
226
297
 
@@ -233,16 +304,25 @@ export class Lowering {
233
304
  options: {
234
305
  name?: string
235
306
  callbackBindings?: Map<string, string>
307
+ stdlibCallSite?: StdlibCallSiteContext
236
308
  } = {}
237
309
  ): void {
238
310
  const loweredName = options.name ?? fn.name
239
311
  const callbackBindings = options.callbackBindings ?? new Map<string, string>()
240
- const runtimeParams = fn.params.filter(param => !callbackBindings.has(param.name))
312
+ const stdlibCallSite = options.stdlibCallSite
313
+ const staticEventDec = fn.decorators.find(d => d.name === 'on')
314
+ const eventType = staticEventDec?.args?.eventType
315
+ const eventParamSpecs = eventType && isEventTypeName(eventType) ? getEventParamSpecs(eventType) : []
316
+ const runtimeParams = staticEventDec
317
+ ? []
318
+ : fn.params.filter(param => !callbackBindings.has(param.name))
241
319
 
242
320
  this.currentFn = loweredName
321
+ this.currentStdlibCallSite = stdlibCallSite
243
322
  this.foreachCounter = 0
244
323
  this.varMap = new Map()
245
324
  this.lambdaBindings = new Map()
325
+ this.intervalBindings = new Map()
246
326
  this.currentCallbackBindings = new Map(callbackBindings)
247
327
  this.currentContext = {}
248
328
  this.blockPosVars = new Map()
@@ -250,10 +330,31 @@ export class Lowering {
250
330
  this.builder = new LoweringBuilder()
251
331
 
252
332
  // Map parameters
253
- for (const param of runtimeParams) {
254
- const paramName = param.name
255
- this.varMap.set(paramName, `$${paramName}`)
256
- this.varTypes.set(paramName, this.normalizeType(param.type))
333
+ if (staticEventDec) {
334
+ for (let i = 0; i < fn.params.length; i++) {
335
+ const param = fn.params[i]
336
+ const expected = eventParamSpecs[i]
337
+ const normalizedType = this.normalizeType(param.type)
338
+ this.varTypes.set(param.name, normalizedType)
339
+
340
+ if (expected?.type.kind === 'entity') {
341
+ this.varMap.set(param.name, '@s')
342
+ continue
343
+ }
344
+
345
+ if (expected?.type.kind === 'named' && expected.type.name === 'string') {
346
+ this.stringValues.set(param.name, '')
347
+ continue
348
+ }
349
+
350
+ this.varMap.set(param.name, `$${param.name}`)
351
+ }
352
+ } else {
353
+ for (const param of runtimeParams) {
354
+ const paramName = param.name
355
+ this.varMap.set(paramName, `$${paramName}`)
356
+ this.varTypes.set(paramName, this.normalizeType(param.type))
357
+ }
257
358
  }
258
359
  for (const param of fn.params) {
259
360
  if (callbackBindings.has(param.name)) {
@@ -271,6 +372,16 @@ export class Lowering {
271
372
  this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` })
272
373
  }
273
374
 
375
+ if (staticEventDec) {
376
+ for (let i = 0; i < fn.params.length; i++) {
377
+ const param = fn.params[i]
378
+ const expected = eventParamSpecs[i]
379
+ if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
380
+ this.builder.emitAssign(`$${param.name}`, { kind: 'const', value: 0 })
381
+ }
382
+ }
383
+ }
384
+
274
385
  // Lower body
275
386
  this.lowerBlock(fn.body)
276
387
 
@@ -323,6 +434,18 @@ export class Lowering {
323
434
  }
324
435
  }
325
436
 
437
+ if (eventType && isEventTypeName(eventType)) {
438
+ irFn.eventHandler = {
439
+ eventType,
440
+ tag: EVENT_TYPES[eventType].tag,
441
+ }
442
+ }
443
+
444
+ // Check for @load decorator
445
+ if (fn.decorators.some(d => d.name === 'load')) {
446
+ irFn.isLoadInit = true
447
+ }
448
+
326
449
  // Handle tick rate counter if needed
327
450
  if (tickRate && tickRate > 1) {
328
451
  this.wrapWithTickRate(irFn, tickRate)
@@ -339,7 +462,7 @@ export class Lowering {
339
462
  private wrapWithTickRate(fn: IRFunction, rate: number): void {
340
463
  // Add tick counter logic to entry block
341
464
  const counterVar = `$__tick_${fn.name}`
342
- this.globals.push(counterVar)
465
+ this.globals.push({ name: counterVar, init: 0 })
343
466
 
344
467
  // Prepend counter logic to entry block
345
468
  const entry = fn.blocks[0]
@@ -443,6 +566,15 @@ export class Lowering {
443
566
  }
444
567
 
445
568
  private lowerLetStmt(stmt: Extract<Stmt, { kind: 'let' }>): void {
569
+ // Check for duplicate declaration of foreach binding
570
+ if (this.currentContext.binding === stmt.name) {
571
+ throw new DiagnosticError(
572
+ 'LoweringError',
573
+ `Cannot redeclare foreach binding '${stmt.name}'`,
574
+ stmt.span ?? { line: 0, col: 0 }
575
+ )
576
+ }
577
+
446
578
  const varName = `$${stmt.name}`
447
579
  this.varMap.set(stmt.name, varName)
448
580
 
@@ -462,6 +594,16 @@ export class Lowering {
462
594
  return
463
595
  }
464
596
 
597
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'setInterval') {
598
+ const value = this.lowerExpr(stmt.init)
599
+ const intervalFn = this.intervalFunctions.get(value.kind === 'const' ? value.value : NaN)
600
+ if (intervalFn) {
601
+ this.intervalBindings.set(stmt.name, intervalFn)
602
+ }
603
+ this.builder.emitAssign(varName, value)
604
+ return
605
+ }
606
+
465
607
  // Handle struct literal initialization
466
608
  if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
467
609
  const structName = stmt.type.name.toLowerCase()
@@ -495,6 +637,14 @@ export class Lowering {
495
637
  return
496
638
  }
497
639
 
640
+ // Handle set_new returning a set ID string
641
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'set_new') {
642
+ const setId = `__set_${this.foreachCounter++}`
643
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
644
+ this.stringValues.set(stmt.name, setId)
645
+ return
646
+ }
647
+
498
648
  // Handle spawn_object returning entity handle
499
649
  if (stmt.init.kind === 'call' && stmt.init.fn === 'spawn_object') {
500
650
  const value = this.lowerExpr(stmt.init)
@@ -532,6 +682,11 @@ export class Lowering {
532
682
  }
533
683
 
534
684
  private lowerIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
685
+ if (stmt.cond.kind === 'is_check') {
686
+ this.lowerIsCheckIfStmt(stmt)
687
+ return
688
+ }
689
+
535
690
  const condVar = this.lowerExpr(stmt.cond)
536
691
  const condName = this.operandToVar(condVar)
537
692
 
@@ -561,6 +716,66 @@ export class Lowering {
561
716
  this.builder.startBlock(mergeLabel)
562
717
  }
563
718
 
719
+ private lowerIsCheckIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
720
+ const cond = stmt.cond
721
+ if (cond.kind !== 'is_check') {
722
+ throw new DiagnosticError(
723
+ 'LoweringError',
724
+ "Internal error: expected 'is' check condition",
725
+ stmt.span ?? { line: 0, col: 0 }
726
+ )
727
+ }
728
+
729
+ if (stmt.else_) {
730
+ throw new DiagnosticError(
731
+ 'LoweringError',
732
+ "'is' checks with else branches are not yet supported",
733
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
734
+ )
735
+ }
736
+
737
+ const selector = this.exprToEntitySelector(cond.expr)
738
+ if (!selector) {
739
+ throw new DiagnosticError(
740
+ 'LoweringError',
741
+ "'is' checks require an entity selector or entity binding",
742
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
743
+ )
744
+ }
745
+
746
+ const mcType = ENTITY_TO_MC_TYPE[cond.entityType]
747
+ if (!mcType) {
748
+ throw new DiagnosticError(
749
+ 'LoweringError',
750
+ `Cannot lower entity type check for '${cond.entityType}'`,
751
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
752
+ )
753
+ }
754
+
755
+ const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`
756
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`)
757
+
758
+ const savedBuilder = this.builder
759
+ const savedVarMap = new Map(this.varMap)
760
+ const savedBlockPosVars = new Map(this.blockPosVars)
761
+
762
+ this.builder = new LoweringBuilder()
763
+ this.varMap = new Map(savedVarMap)
764
+ this.blockPosVars = new Map(savedBlockPosVars)
765
+
766
+ this.builder.startBlock('entry')
767
+ this.lowerBlock(stmt.then)
768
+ if (!this.builder.isBlockSealed()) {
769
+ this.builder.emitReturn()
770
+ }
771
+
772
+ this.functions.push(this.builder.build(thenFnName, [], false))
773
+
774
+ this.builder = savedBuilder
775
+ this.varMap = savedVarMap
776
+ this.blockPosVars = savedBlockPosVars
777
+ }
778
+
564
779
  private lowerWhileStmt(stmt: Extract<Stmt, { kind: 'while' }>): void {
565
780
  const checkLabel = this.builder.freshLabel('loop_check')
566
781
  const bodyLabel = this.builder.freshLabel('loop_body')
@@ -933,10 +1148,22 @@ export class Lowering {
933
1148
  parts.push(`at ${this.selectorToString(sub.selector)}`)
934
1149
  break
935
1150
  case 'if_entity':
936
- parts.push(`if entity ${this.selectorToString(sub.selector)}`)
1151
+ if (sub.selector) {
1152
+ parts.push(`if entity ${this.selectorToString(sub.selector)}`)
1153
+ } else if (sub.varName) {
1154
+ // Variable with filters - substitute with @s and apply filters
1155
+ const sel: EntitySelector = { kind: '@s', filters: sub.filters }
1156
+ parts.push(`if entity ${this.selectorToString(sel)}`)
1157
+ }
937
1158
  break
938
1159
  case 'unless_entity':
939
- parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
1160
+ if (sub.selector) {
1161
+ parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
1162
+ } else if (sub.varName) {
1163
+ // Variable with filters - substitute with @s and apply filters
1164
+ const sel: EntitySelector = { kind: '@s', filters: sub.filters }
1165
+ parts.push(`unless entity ${this.selectorToString(sel)}`)
1166
+ }
940
1167
  break
941
1168
  case 'in':
942
1169
  parts.push(`in ${sub.dimension}`)
@@ -1051,6 +1278,13 @@ export class Lowering {
1051
1278
  case 'binary':
1052
1279
  return this.lowerBinaryExpr(expr)
1053
1280
 
1281
+ case 'is_check':
1282
+ throw new DiagnosticError(
1283
+ 'LoweringError',
1284
+ "'is' checks are only supported as if conditions",
1285
+ expr.span ?? { line: 0, col: 0 }
1286
+ )
1287
+
1054
1288
  case 'unary':
1055
1289
  return this.lowerUnaryExpr(expr)
1056
1290
 
@@ -1060,6 +1294,9 @@ export class Lowering {
1060
1294
  case 'call':
1061
1295
  return this.lowerCallExpr(expr)
1062
1296
 
1297
+ case 'static_call':
1298
+ return this.lowerStaticCallExpr(expr)
1299
+
1063
1300
  case 'invoke':
1064
1301
  return this.lowerInvokeExpr(expr)
1065
1302
 
@@ -1269,6 +1506,15 @@ export class Lowering {
1269
1506
  }
1270
1507
 
1271
1508
  private lowerAssignExpr(expr: Extract<Expr, { kind: 'assign' }>): Operand {
1509
+ // Check for const reassignment (both compile-time consts and immutable globals)
1510
+ if (this.constValues.has(expr.target)) {
1511
+ throw new DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 })
1512
+ }
1513
+ const globalInfo = this.globalNames.get(expr.target)
1514
+ if (globalInfo && !globalInfo.mutable) {
1515
+ throw new DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 })
1516
+ }
1517
+
1272
1518
  const blockPosValue = this.resolveBlockPosExpr(expr.value)
1273
1519
  if (blockPosValue) {
1274
1520
  this.blockPosVars.set(expr.target, blockPosValue)
@@ -1398,6 +1644,11 @@ export class Lowering {
1398
1644
  return this.emitDirectFunctionCall(callbackTarget, expr.args)
1399
1645
  }
1400
1646
 
1647
+ const implMethod = this.resolveInstanceMethod(expr)
1648
+ if (implMethod) {
1649
+ return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args)
1650
+ }
1651
+
1401
1652
  // Regular function call
1402
1653
  const fnDecl = this.fnDecls.get(expr.fn)
1403
1654
  const defaultArgs = this.functionDefaults.get(expr.fn) ?? []
@@ -1427,8 +1678,9 @@ export class Lowering {
1427
1678
  runtimeArgs.push(fullArgs[i])
1428
1679
  }
1429
1680
 
1430
- const targetFn = callbackBindings.size > 0
1431
- ? this.ensureSpecializedFunction(fnDecl, callbackBindings)
1681
+ const stdlibCallSite = this.getStdlibCallSiteContext(fnDecl, getSpan(expr))
1682
+ const targetFn = callbackBindings.size > 0 || stdlibCallSite
1683
+ ? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
1432
1684
  : expr.fn
1433
1685
  return this.emitDirectFunctionCall(targetFn, runtimeArgs)
1434
1686
  }
@@ -1436,6 +1688,12 @@ export class Lowering {
1436
1688
  return this.emitDirectFunctionCall(expr.fn, fullArgs)
1437
1689
  }
1438
1690
 
1691
+ private lowerStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): Operand {
1692
+ const method = this.implMethods.get(expr.type)?.get(expr.method)
1693
+ const targetFn = method?.loweredName ?? `${expr.type}_${expr.method}`
1694
+ return this.emitMethodCall(targetFn, method?.fn, expr.args)
1695
+ }
1696
+
1439
1697
  private lowerInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): Operand {
1440
1698
  if (expr.callee.kind === 'lambda') {
1441
1699
  if (!Array.isArray(expr.callee.body)) {
@@ -1487,6 +1745,19 @@ export class Lowering {
1487
1745
  return { kind: 'var', name: dst }
1488
1746
  }
1489
1747
 
1748
+ private emitMethodCall(fn: string, fnDecl: FnDecl | undefined, args: Expr[]): Operand {
1749
+ const defaultArgs = this.functionDefaults.get(fn) ?? fnDecl?.params.map(param => param.default) ?? []
1750
+ const fullArgs = [...args]
1751
+ for (let i = fullArgs.length; i < defaultArgs.length; i++) {
1752
+ const defaultExpr = defaultArgs[i]
1753
+ if (!defaultExpr) {
1754
+ break
1755
+ }
1756
+ fullArgs.push(defaultExpr)
1757
+ }
1758
+ return this.emitDirectFunctionCall(fn, fullArgs)
1759
+ }
1760
+
1490
1761
  private resolveFunctionRefExpr(expr: Expr): string | null {
1491
1762
  if (expr.kind === 'lambda') {
1492
1763
  return this.lowerLambdaExpr(expr)
@@ -1502,9 +1773,21 @@ export class Lowering {
1502
1773
  }
1503
1774
 
1504
1775
  private ensureSpecializedFunction(fn: FnDecl, callbackBindings: Map<string, string>): string {
1776
+ return this.ensureSpecializedFunctionWithContext(fn, callbackBindings)
1777
+ }
1778
+
1779
+ private ensureSpecializedFunctionWithContext(
1780
+ fn: FnDecl,
1781
+ callbackBindings: Map<string, string>,
1782
+ stdlibCallSite?: StdlibCallSiteContext
1783
+ ): string {
1505
1784
  const parts = [...callbackBindings.entries()]
1506
1785
  .sort(([left], [right]) => left.localeCompare(right))
1507
1786
  .map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`)
1787
+ const callSiteHash = stdlibCallSite ? this.shortHash(this.serializeCallSite(stdlibCallSite)) : null
1788
+ if (callSiteHash) {
1789
+ parts.push(`callsite_${callSiteHash}`)
1790
+ }
1508
1791
  const key = `${fn.name}::${parts.join('::')}`
1509
1792
  const cached = this.specializedFunctions.get(key)
1510
1793
  if (cached) {
@@ -1514,7 +1797,7 @@ export class Lowering {
1514
1797
  const specializedName = `${fn.name}__${parts.join('__')}`
1515
1798
  this.specializedFunctions.set(key, specializedName)
1516
1799
  this.withSavedFunctionState(() => {
1517
- this.lowerFn(fn, { name: specializedName, callbackBindings })
1800
+ this.lowerFn(fn, { name: specializedName, callbackBindings, stdlibCallSite })
1518
1801
  })
1519
1802
  return specializedName
1520
1803
  }
@@ -1539,10 +1822,12 @@ export class Lowering {
1539
1822
 
1540
1823
  private withSavedFunctionState<T>(callback: () => T): T {
1541
1824
  const savedCurrentFn = this.currentFn
1825
+ const savedStdlibCallSite = this.currentStdlibCallSite
1542
1826
  const savedForeachCounter = this.foreachCounter
1543
1827
  const savedBuilder = this.builder
1544
1828
  const savedVarMap = new Map(this.varMap)
1545
1829
  const savedLambdaBindings = new Map(this.lambdaBindings)
1830
+ const savedIntervalBindings = new Map(this.intervalBindings)
1546
1831
  const savedCallbackBindings = new Map(this.currentCallbackBindings)
1547
1832
  const savedContext = this.currentContext
1548
1833
  const savedBlockPosVars = new Map(this.blockPosVars)
@@ -1553,10 +1838,12 @@ export class Lowering {
1553
1838
  return callback()
1554
1839
  } finally {
1555
1840
  this.currentFn = savedCurrentFn
1841
+ this.currentStdlibCallSite = savedStdlibCallSite
1556
1842
  this.foreachCounter = savedForeachCounter
1557
1843
  this.builder = savedBuilder
1558
1844
  this.varMap = savedVarMap
1559
1845
  this.lambdaBindings = savedLambdaBindings
1846
+ this.intervalBindings = savedIntervalBindings
1560
1847
  this.currentCallbackBindings = savedCallbackBindings
1561
1848
  this.currentContext = savedContext
1562
1849
  this.blockPosVars = savedBlockPosVars
@@ -1572,6 +1859,18 @@ export class Lowering {
1572
1859
  return { kind: 'const', value: 0 }
1573
1860
  }
1574
1861
 
1862
+ if (name === 'setTimeout') {
1863
+ return this.lowerSetTimeout(args)
1864
+ }
1865
+
1866
+ if (name === 'setInterval') {
1867
+ return this.lowerSetInterval(args)
1868
+ }
1869
+
1870
+ if (name === 'clearInterval') {
1871
+ return this.lowerClearInterval(args, callSpan)
1872
+ }
1873
+
1575
1874
  // Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
1576
1875
  if (name === 'random') {
1577
1876
  const dst = this.builder.freshTemp()
@@ -1602,7 +1901,7 @@ export class Lowering {
1602
1901
  if (name === 'scoreboard_get' || name === 'score') {
1603
1902
  const dst = this.builder.freshTemp()
1604
1903
  const player = this.exprToTargetString(args[0])
1605
- const objective = this.exprToString(args[1])
1904
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
1606
1905
  this.builder.emitRaw(`execute store result score ${dst} rs run scoreboard players get ${player} ${objective}`)
1607
1906
  return { kind: 'var', name: dst }
1608
1907
  }
@@ -1610,7 +1909,7 @@ export class Lowering {
1610
1909
  // Special case: scoreboard_set — write to vanilla MC scoreboard
1611
1910
  if (name === 'scoreboard_set') {
1612
1911
  const player = this.exprToTargetString(args[0])
1613
- const objective = this.exprToString(args[1])
1912
+ const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
1614
1913
  const value = this.lowerExpr(args[2])
1615
1914
  if (value.kind === 'const') {
1616
1915
  this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`)
@@ -1624,7 +1923,7 @@ export class Lowering {
1624
1923
 
1625
1924
  if (name === 'scoreboard_display') {
1626
1925
  const slot = this.exprToString(args[0])
1627
- const objective = this.exprToString(args[1])
1926
+ const objective = this.resolveScoreboardObjective(undefined, args[1], callSpan)
1628
1927
  this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`)
1629
1928
  return { kind: 'const', value: 0 }
1630
1929
  }
@@ -1636,7 +1935,7 @@ export class Lowering {
1636
1935
  }
1637
1936
 
1638
1937
  if (name === 'scoreboard_add_objective') {
1639
- const objective = this.exprToString(args[0])
1938
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
1640
1939
  const criteria = this.exprToString(args[1])
1641
1940
  const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : ''
1642
1941
  this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`)
@@ -1644,7 +1943,7 @@ export class Lowering {
1644
1943
  }
1645
1944
 
1646
1945
  if (name === 'scoreboard_remove_objective') {
1647
- const objective = this.exprToString(args[0])
1946
+ const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
1648
1947
  this.builder.emitRaw(`scoreboard objectives remove ${objective}`)
1649
1948
  return { kind: 'const', value: 0 }
1650
1949
  }
@@ -1744,6 +2043,67 @@ export class Lowering {
1744
2043
  return { kind: 'var', name: dst }
1745
2044
  }
1746
2045
 
2046
+ // data_merge(target, nbt) — merge NBT into entity/block/storage
2047
+ // data_merge(@s, { Invisible: 1b, Silent: 1b })
2048
+ if (name === 'data_merge') {
2049
+ const target = args[0]
2050
+ const nbt = args[1]
2051
+ const nbtStr = this.exprToSnbt ? this.exprToSnbt(nbt) : this.exprToString(nbt)
2052
+
2053
+ // Check if target is a selector (entity) or string (block/storage)
2054
+ if (target.kind === 'selector') {
2055
+ const sel = this.exprToTargetString(target)
2056
+ this.builder.emitRaw(`data merge entity ${sel} ${nbtStr}`)
2057
+ } else {
2058
+ // Assume block position or storage
2059
+ const targetStr = this.exprToString(target)
2060
+ // If it looks like coordinates, use block; otherwise storage
2061
+ if (targetStr.match(/^~|^\d|^\^/)) {
2062
+ this.builder.emitRaw(`data merge block ${targetStr} ${nbtStr}`)
2063
+ } else {
2064
+ this.builder.emitRaw(`data merge storage ${targetStr} ${nbtStr}`)
2065
+ }
2066
+ }
2067
+ return { kind: 'const', value: 0 }
2068
+ }
2069
+
2070
+ // Set data structure operations — unique collections via NBT storage
2071
+ // set_new is primarily handled in lowerLetStmt for proper string tracking.
2072
+ // This fallback handles standalone set_new() calls without assignment.
2073
+ if (name === 'set_new') {
2074
+ const setId = `__set_${this.foreachCounter++}`
2075
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
2076
+ return { kind: 'const', value: 0 }
2077
+ }
2078
+
2079
+ if (name === 'set_add') {
2080
+ const setId = this.exprToString(args[0])
2081
+ const value = this.exprToString(args[1])
2082
+ this.builder.emitRaw(`execute unless data storage rs:sets ${setId}[{value:${value}}] run data modify storage rs:sets ${setId} append value {value:${value}}`)
2083
+ return { kind: 'const', value: 0 }
2084
+ }
2085
+
2086
+ if (name === 'set_contains') {
2087
+ const dst = this.builder.freshTemp()
2088
+ const setId = this.exprToString(args[0])
2089
+ const value = this.exprToString(args[1])
2090
+ this.builder.emitRaw(`execute store result score ${dst} rs if data storage rs:sets ${setId}[{value:${value}}]`)
2091
+ return { kind: 'var', name: dst }
2092
+ }
2093
+
2094
+ if (name === 'set_remove') {
2095
+ const setId = this.exprToString(args[0])
2096
+ const value = this.exprToString(args[1])
2097
+ this.builder.emitRaw(`data remove storage rs:sets ${setId}[{value:${value}}]`)
2098
+ return { kind: 'const', value: 0 }
2099
+ }
2100
+
2101
+ if (name === 'set_clear') {
2102
+ const setId = this.exprToString(args[0])
2103
+ this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
2104
+ return { kind: 'const', value: 0 }
2105
+ }
2106
+
1747
2107
  const coordCommand = this.lowerCoordinateBuiltin(name, args)
1748
2108
  if (coordCommand) {
1749
2109
  this.builder.emitRaw(coordCommand)
@@ -1785,6 +2145,119 @@ export class Lowering {
1785
2145
  return { kind: 'const', value: 0 }
1786
2146
  }
1787
2147
 
2148
+ private lowerSetTimeout(args: Expr[]): Operand {
2149
+ const delay = this.exprToLiteral(args[0])
2150
+ const callback = args[1]
2151
+ if (!callback || callback.kind !== 'lambda') {
2152
+ throw new DiagnosticError(
2153
+ 'LoweringError',
2154
+ 'setTimeout requires a lambda callback',
2155
+ getSpan(callback) ?? { line: 1, col: 1 }
2156
+ )
2157
+ }
2158
+
2159
+ const fnName = `__timeout_${this.timeoutCounter++}`
2160
+ this.lowerNamedLambdaFunction(fnName, callback)
2161
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
2162
+ return { kind: 'const', value: 0 }
2163
+ }
2164
+
2165
+ private lowerSetInterval(args: Expr[]): Operand {
2166
+ const delay = this.exprToLiteral(args[0])
2167
+ const callback = args[1]
2168
+ if (!callback || callback.kind !== 'lambda') {
2169
+ throw new DiagnosticError(
2170
+ 'LoweringError',
2171
+ 'setInterval requires a lambda callback',
2172
+ getSpan(callback) ?? { line: 1, col: 1 }
2173
+ )
2174
+ }
2175
+
2176
+ const id = this.intervalCounter++
2177
+ const bodyName = `__interval_body_${id}`
2178
+ const fnName = `__interval_${id}`
2179
+
2180
+ this.lowerNamedLambdaFunction(bodyName, callback)
2181
+ this.lowerIntervalWrapperFunction(fnName, bodyName, delay)
2182
+ this.intervalFunctions.set(id, fnName)
2183
+ this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
2184
+
2185
+ return { kind: 'const', value: id }
2186
+ }
2187
+
2188
+ private lowerClearInterval(args: Expr[], callSpan?: Span): Operand {
2189
+ const fnName = this.resolveIntervalFunctionName(args[0])
2190
+ if (!fnName) {
2191
+ throw new DiagnosticError(
2192
+ 'LoweringError',
2193
+ 'clearInterval requires an interval ID returned from setInterval',
2194
+ callSpan ?? getSpan(args[0]) ?? { line: 1, col: 1 }
2195
+ )
2196
+ }
2197
+
2198
+ this.builder.emitRaw(`schedule clear ${this.namespace}:${fnName}`)
2199
+ return { kind: 'const', value: 0 }
2200
+ }
2201
+
2202
+ private lowerNamedLambdaFunction(name: string, expr: Extract<Expr, { kind: 'lambda' }>): void {
2203
+ const lambdaFn: FnDecl = {
2204
+ name,
2205
+ params: expr.params.map(param => ({
2206
+ name: param.name,
2207
+ type: param.type ?? { kind: 'named', name: 'int' },
2208
+ })),
2209
+ returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
2210
+ decorators: [],
2211
+ body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
2212
+ }
2213
+
2214
+ this.withSavedFunctionState(() => {
2215
+ this.lowerFn(lambdaFn)
2216
+ })
2217
+ }
2218
+
2219
+ private lowerIntervalWrapperFunction(name: string, bodyName: string, delay: string): void {
2220
+ const intervalFn: FnDecl = {
2221
+ name,
2222
+ params: [],
2223
+ returnType: { kind: 'named', name: 'void' },
2224
+ decorators: [],
2225
+ body: [
2226
+ { kind: 'raw', cmd: `function ${this.namespace}:${bodyName}` },
2227
+ { kind: 'raw', cmd: `schedule function ${this.namespace}:${name} ${delay}t` },
2228
+ ],
2229
+ }
2230
+
2231
+ this.withSavedFunctionState(() => {
2232
+ this.lowerFn(intervalFn)
2233
+ })
2234
+ }
2235
+
2236
+ private resolveIntervalFunctionName(expr: Expr | undefined): string | null {
2237
+ if (!expr) {
2238
+ return null
2239
+ }
2240
+
2241
+ if (expr.kind === 'ident') {
2242
+ const boundInterval = this.intervalBindings.get(expr.name)
2243
+ if (boundInterval) {
2244
+ return boundInterval
2245
+ }
2246
+
2247
+ const constValue = this.constValues.get(expr.name)
2248
+ if (constValue?.kind === 'int_lit') {
2249
+ return this.intervalFunctions.get(constValue.value) ?? null
2250
+ }
2251
+ return null
2252
+ }
2253
+
2254
+ if (expr.kind === 'int_lit') {
2255
+ return this.intervalFunctions.get(expr.value) ?? null
2256
+ }
2257
+
2258
+ return null
2259
+ }
2260
+
1788
2261
  private lowerRichTextBuiltin(name: string, args: Expr[]): string | null {
1789
2262
  const messageArgIndex = this.getRichTextArgIndex(name)
1790
2263
  if (messageArgIndex === null) {
@@ -1943,6 +2416,17 @@ export class Lowering {
1943
2416
  }
1944
2417
  case 'selector':
1945
2418
  return this.selectorToString(expr.sel)
2419
+ case 'unary':
2420
+ // Handle unary minus on literals directly
2421
+ if (expr.op === '-' && expr.operand.kind === 'int_lit') {
2422
+ return (-expr.operand.value).toString()
2423
+ }
2424
+ if (expr.op === '-' && expr.operand.kind === 'float_lit') {
2425
+ return Math.trunc(-expr.operand.value).toString()
2426
+ }
2427
+ // Fall through to default for complex cases
2428
+ const unaryOp = this.lowerExpr(expr)
2429
+ return this.operandToVar(unaryOp)
1946
2430
  default:
1947
2431
  // Complex expression - lower and return var name
1948
2432
  const op = this.lowerExpr(expr)
@@ -1950,6 +2434,32 @@ export class Lowering {
1950
2434
  }
1951
2435
  }
1952
2436
 
2437
+ private exprToEntitySelector(expr: Expr): string | null {
2438
+ if (expr.kind === 'selector') {
2439
+ return this.selectorToString(expr.sel)
2440
+ }
2441
+
2442
+ if (expr.kind === 'ident') {
2443
+ const constValue = this.constValues.get(expr.name)
2444
+ if (constValue) {
2445
+ return this.exprToEntitySelector(constValue)
2446
+ }
2447
+ const mapped = this.varMap.get(expr.name)
2448
+ if (mapped?.startsWith('@')) {
2449
+ return mapped
2450
+ }
2451
+ }
2452
+
2453
+ return null
2454
+ }
2455
+
2456
+ private appendTypeFilter(selector: string, mcType: string): string {
2457
+ if (selector.endsWith(']')) {
2458
+ return `${selector.slice(0, -1)},type=${mcType}]`
2459
+ }
2460
+ return `${selector}[type=${mcType}]`
2461
+ }
2462
+
1953
2463
  private exprToSnbt(expr: Expr): string {
1954
2464
  switch (expr.kind) {
1955
2465
  case 'struct_lit': {
@@ -2024,6 +2534,113 @@ export class Lowering {
2024
2534
  return option === 'displayName' || option === 'prefix' || option === 'suffix'
2025
2535
  }
2026
2536
 
2537
+ private exprToScoreboardObjective(expr: Expr, span?: Span): string {
2538
+ if (expr.kind === 'mc_name') {
2539
+ return expr.value
2540
+ }
2541
+
2542
+ const objective = this.exprToString(expr)
2543
+ if (objective.startsWith('#') || objective.includes('.')) {
2544
+ return objective.startsWith('#') ? objective.slice(1) : objective
2545
+ }
2546
+
2547
+ return `${this.getObjectiveNamespace(span)}.${objective}`
2548
+ }
2549
+
2550
+ private resolveScoreboardObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string {
2551
+ const stdlibInternalObjective = this.tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span)
2552
+ if (stdlibInternalObjective) {
2553
+ return stdlibInternalObjective
2554
+ }
2555
+ return this.exprToScoreboardObjective(objectiveExpr, span)
2556
+ }
2557
+
2558
+ private getObjectiveNamespace(span?: Span): string {
2559
+ const filePath = this.filePathForSpan(span)
2560
+ if (!filePath) {
2561
+ return this.namespace
2562
+ }
2563
+
2564
+ return this.isStdlibFile(filePath) ? 'rs' : this.namespace
2565
+ }
2566
+
2567
+ private tryGetStdlibInternalObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string | null {
2568
+ if (!span || !this.currentStdlibCallSite || objectiveExpr.kind !== 'mc_name' || objectiveExpr.value !== 'rs') {
2569
+ return null
2570
+ }
2571
+
2572
+ const filePath = this.filePathForSpan(span)
2573
+ if (!filePath || !this.isStdlibFile(filePath)) {
2574
+ return null
2575
+ }
2576
+
2577
+ const resourceBase = this.getStdlibInternalResourceBase(playerExpr)
2578
+ if (!resourceBase) {
2579
+ return null
2580
+ }
2581
+
2582
+ const hash = this.shortHash(this.serializeCallSite(this.currentStdlibCallSite))
2583
+ return `rs._${resourceBase}_${hash}`
2584
+ }
2585
+
2586
+ private getStdlibInternalResourceBase(playerExpr: Expr | undefined): string | null {
2587
+ if (!playerExpr || playerExpr.kind !== 'str_lit') {
2588
+ return null
2589
+ }
2590
+
2591
+ const match = playerExpr.value.match(/^([a-z0-9]+)_/)
2592
+ return match?.[1] ?? null
2593
+ }
2594
+
2595
+ private getStdlibCallSiteContext(fn: FnDecl, exprSpan?: Span): StdlibCallSiteContext | undefined {
2596
+ const fnFilePath = this.filePathForSpan(getSpan(fn))
2597
+ if (!fnFilePath || !this.isStdlibFile(fnFilePath)) {
2598
+ return undefined
2599
+ }
2600
+
2601
+ if (this.currentStdlibCallSite) {
2602
+ return this.currentStdlibCallSite
2603
+ }
2604
+
2605
+ if (!exprSpan) {
2606
+ return undefined
2607
+ }
2608
+
2609
+ return {
2610
+ filePath: this.filePathForSpan(exprSpan),
2611
+ line: exprSpan.line,
2612
+ col: exprSpan.col,
2613
+ }
2614
+ }
2615
+
2616
+ private serializeCallSite(callSite: StdlibCallSiteContext): string {
2617
+ return `${callSite.filePath ?? '<memory>'}:${callSite.line}:${callSite.col}`
2618
+ }
2619
+
2620
+ private shortHash(input: string): string {
2621
+ let hash = 2166136261
2622
+ for (let i = 0; i < input.length; i++) {
2623
+ hash ^= input.charCodeAt(i)
2624
+ hash = Math.imul(hash, 16777619)
2625
+ }
2626
+ return (hash >>> 0).toString(16).padStart(8, '0').slice(0, 4)
2627
+ }
2628
+
2629
+ private isStdlibFile(filePath: string): boolean {
2630
+ const normalized = path.normalize(filePath)
2631
+ const stdlibSegment = `${path.sep}src${path.sep}stdlib${path.sep}`
2632
+ return normalized.includes(stdlibSegment)
2633
+ }
2634
+
2635
+ private filePathForSpan(span?: Span): string | undefined {
2636
+ if (!span) {
2637
+ return undefined
2638
+ }
2639
+
2640
+ const line = span.line
2641
+ return this.sourceRanges.find(range => line >= range.startLine && line <= range.endLine)?.filePath
2642
+ }
2643
+
2027
2644
  private lowerCoordinateBuiltin(name: string, args: Expr[]): string | null {
2028
2645
  const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
2029
2646
  const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
@@ -2050,6 +2667,14 @@ export class Lowering {
2050
2667
  return null
2051
2668
  }
2052
2669
 
2670
+ if (name === 'summon') {
2671
+ if (args.length >= 2 && pos1) {
2672
+ const nbt = args[2] ? ` ${this.exprToString(args[2])}` : ''
2673
+ return `summon ${this.exprToString(args[0])} ${emitBlockPos(pos1)}${nbt}`
2674
+ }
2675
+ return null
2676
+ }
2677
+
2053
2678
  return null
2054
2679
  }
2055
2680
 
@@ -2132,7 +2757,11 @@ export class Lowering {
2132
2757
  }
2133
2758
  }
2134
2759
  if (expr.kind === 'call') {
2135
- return this.fnDecls.get(this.resolveFunctionRefByName(expr.fn) ?? expr.fn)?.returnType
2760
+ const resolved = this.resolveFunctionRefByName(expr.fn) ?? this.resolveInstanceMethod(expr)?.loweredName ?? expr.fn
2761
+ return this.fnDecls.get(resolved)?.returnType
2762
+ }
2763
+ if (expr.kind === 'static_call') {
2764
+ return this.implMethods.get(expr.type)?.get(expr.method)?.fn.returnType
2136
2765
  }
2137
2766
  if (expr.kind === 'invoke') {
2138
2767
  const calleeType = this.inferExprType(expr.callee)
@@ -2161,6 +2790,25 @@ export class Lowering {
2161
2790
  return undefined
2162
2791
  }
2163
2792
 
2793
+ private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): { fn: FnDecl; loweredName: string } | null {
2794
+ const receiver = expr.args[0]
2795
+ if (!receiver) {
2796
+ return null
2797
+ }
2798
+
2799
+ const receiverType = this.inferExprType(receiver)
2800
+ if (receiverType?.kind !== 'struct') {
2801
+ return null
2802
+ }
2803
+
2804
+ const method = this.implMethods.get(receiverType.name)?.get(expr.fn)
2805
+ if (!method || method.fn.params[0]?.name !== 'self') {
2806
+ return null
2807
+ }
2808
+
2809
+ return method
2810
+ }
2811
+
2164
2812
  private normalizeType(type: TypeNode): TypeNode {
2165
2813
  if (type.kind === 'array') {
2166
2814
  return { kind: 'array', elem: this.normalizeType(type.elem) }
@@ -2294,6 +2942,13 @@ export class Lowering {
2294
2942
  }
2295
2943
  if (filters.nbt) parts.push(`nbt=${filters.nbt}`)
2296
2944
  if (filters.gamemode) parts.push(`gamemode=${filters.gamemode}`)
2945
+ // Position filters
2946
+ if (filters.x) parts.push(`x=${this.rangeToString(filters.x)}`)
2947
+ if (filters.y) parts.push(`y=${this.rangeToString(filters.y)}`)
2948
+ if (filters.z) parts.push(`z=${this.rangeToString(filters.z)}`)
2949
+ // Rotation filters
2950
+ if (filters.x_rotation) parts.push(`x_rotation=${this.rangeToString(filters.x_rotation)}`)
2951
+ if (filters.y_rotation) parts.push(`y_rotation=${this.rangeToString(filters.y_rotation)}`)
2297
2952
 
2298
2953
  return this.finalizeSelector(parts.length ? `${kind}[${parts.join(',')}]` : kind)
2299
2954
  }
@@ -2318,14 +2973,19 @@ export class Lowering {
2318
2973
  // ---------------------------------------------------------------------------
2319
2974
 
2320
2975
  class LoweringBuilder {
2321
- private tempCount = 0
2976
+ private static globalTempId = 0
2322
2977
  private labelCount = 0
2323
2978
  private blocks: any[] = []
2324
2979
  private currentBlock: any = null
2325
2980
  private locals = new Set<string>()
2326
2981
 
2982
+ /** Reset the global temp counter (call between compilations). */
2983
+ static resetTempCounter(): void {
2984
+ LoweringBuilder.globalTempId = 0
2985
+ }
2986
+
2327
2987
  freshTemp(): string {
2328
- const name = `$t${this.tempCount++}`
2988
+ const name = `$_${LoweringBuilder.globalTempId++}`
2329
2989
  this.locals.add(name)
2330
2990
  return name
2331
2991
  }