redscript-mc 1.2.0 → 1.2.2

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 (55) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/dce.test.d.ts +1 -0
  5. package/dist/__tests__/dce.test.js +137 -0
  6. package/dist/__tests__/lexer.test.js +19 -2
  7. package/dist/__tests__/lowering.test.js +8 -0
  8. package/dist/__tests__/mc-syntax.test.js +12 -0
  9. package/dist/__tests__/parser.test.js +10 -0
  10. package/dist/__tests__/runtime.test.js +13 -0
  11. package/dist/__tests__/typechecker.test.js +30 -0
  12. package/dist/ast/types.d.ts +22 -2
  13. package/dist/cli.js +15 -10
  14. package/dist/codegen/structure/index.d.ts +4 -1
  15. package/dist/codegen/structure/index.js +4 -2
  16. package/dist/compile.d.ts +1 -0
  17. package/dist/compile.js +4 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +4 -1
  20. package/dist/lexer/index.d.ts +2 -1
  21. package/dist/lexer/index.js +89 -1
  22. package/dist/lowering/index.js +37 -1
  23. package/dist/optimizer/dce.d.ts +23 -0
  24. package/dist/optimizer/dce.js +592 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +81 -16
  27. package/dist/typechecker/index.d.ts +2 -0
  28. package/dist/typechecker/index.js +49 -0
  29. package/docs/ARCHITECTURE.zh.md +1088 -0
  30. package/editors/vscode/.vscodeignore +3 -0
  31. package/editors/vscode/icon.png +0 -0
  32. package/editors/vscode/out/extension.js +834 -19
  33. package/editors/vscode/package-lock.json +2 -2
  34. package/editors/vscode/package.json +1 -1
  35. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  36. package/examples/spiral.mcrs +41 -0
  37. package/logo.png +0 -0
  38. package/package.json +1 -1
  39. package/src/__tests__/dce.test.ts +129 -0
  40. package/src/__tests__/lexer.test.ts +21 -2
  41. package/src/__tests__/lowering.test.ts +9 -0
  42. package/src/__tests__/mc-syntax.test.ts +14 -0
  43. package/src/__tests__/parser.test.ts +11 -0
  44. package/src/__tests__/runtime.test.ts +16 -0
  45. package/src/__tests__/typechecker.test.ts +33 -0
  46. package/src/ast/types.ts +14 -1
  47. package/src/cli.ts +24 -10
  48. package/src/codegen/structure/index.ts +13 -2
  49. package/src/compile.ts +5 -1
  50. package/src/index.ts +5 -1
  51. package/src/lexer/index.ts +102 -1
  52. package/src/lowering/index.ts +38 -2
  53. package/src/optimizer/dce.ts +619 -0
  54. package/src/parser/index.ts +97 -17
  55. package/src/typechecker/index.ts +65 -0
@@ -0,0 +1,619 @@
1
+ import type { Block, Expr, FnDecl, Program, Stmt } from '../ast/types'
2
+
3
+ interface ScopeEntry {
4
+ id: string
5
+ name: string
6
+ }
7
+
8
+ function copySpan<T extends object>(target: T, source: object): T {
9
+ const descriptor = Object.getOwnPropertyDescriptor(source, 'span')
10
+ if (descriptor) {
11
+ Object.defineProperty(target, 'span', descriptor)
12
+ }
13
+ return target
14
+ }
15
+
16
+ function isConstantBoolean(expr: Expr): boolean | null {
17
+ if (expr.kind === 'bool_lit') {
18
+ return expr.value
19
+ }
20
+ return null
21
+ }
22
+
23
+ function isPureExpr(expr: Expr): boolean {
24
+ switch (expr.kind) {
25
+ case 'int_lit':
26
+ case 'float_lit':
27
+ case 'byte_lit':
28
+ case 'short_lit':
29
+ case 'long_lit':
30
+ case 'double_lit':
31
+ case 'rel_coord':
32
+ case 'local_coord':
33
+ case 'bool_lit':
34
+ case 'str_lit':
35
+ case 'mc_name':
36
+ case 'range_lit':
37
+ case 'selector':
38
+ case 'ident':
39
+ case 'blockpos':
40
+ return true
41
+ case 'str_interp':
42
+ return expr.parts.every(part => typeof part === 'string' || isPureExpr(part))
43
+ case 'f_string':
44
+ return expr.parts.every(part => part.kind === 'text' || isPureExpr(part.expr))
45
+ case 'binary':
46
+ return isPureExpr(expr.left) && isPureExpr(expr.right)
47
+ case 'is_check':
48
+ return isPureExpr(expr.expr)
49
+ case 'unary':
50
+ return isPureExpr(expr.operand)
51
+ case 'member':
52
+ return isPureExpr(expr.obj)
53
+ case 'index':
54
+ return isPureExpr(expr.obj) && isPureExpr(expr.index)
55
+ case 'array_lit':
56
+ return expr.elements.every(isPureExpr)
57
+ case 'struct_lit':
58
+ return expr.fields.every(field => isPureExpr(field.value))
59
+ case 'lambda':
60
+ return true
61
+ case 'assign':
62
+ case 'member_assign':
63
+ case 'call':
64
+ case 'invoke':
65
+ case 'static_call':
66
+ return false
67
+ }
68
+ }
69
+
70
+ export class DeadCodeEliminator {
71
+ private readonly functionMap = new Map<string, FnDecl>()
72
+ private readonly reachableFunctions = new Set<string>()
73
+ private readonly usedConstants = new Set<string>()
74
+ private readonly localReads = new Set<string>()
75
+ private readonly localDeclIds = new WeakMap<Stmt, string>()
76
+ private localIdCounter = 0
77
+
78
+ eliminate(program: Program): Program {
79
+ this.functionMap.clear()
80
+ this.reachableFunctions.clear()
81
+ this.usedConstants.clear()
82
+ this.localReads.clear()
83
+ this.localIdCounter = 0
84
+
85
+ for (const fn of program.declarations) {
86
+ this.functionMap.set(fn.name, fn)
87
+ }
88
+
89
+ const entryPoints = this.findEntryPoints(program)
90
+ if (entryPoints.length === 0) {
91
+ for (const fn of program.declarations) {
92
+ this.markReachable(fn.name)
93
+ }
94
+ } else {
95
+ for (const fnName of entryPoints) {
96
+ this.markReachable(fnName)
97
+ }
98
+ }
99
+
100
+ for (const global of program.globals) {
101
+ this.collectExprRefs(global.init, [])
102
+ }
103
+
104
+ for (const implBlock of program.implBlocks) {
105
+ for (const method of implBlock.methods) {
106
+ this.collectFunctionRefs(method)
107
+ }
108
+ }
109
+
110
+ return {
111
+ ...program,
112
+ declarations: program.declarations
113
+ .filter(fn => this.reachableFunctions.has(fn.name))
114
+ .map(fn => this.transformFunction(fn)),
115
+ consts: program.consts.filter(constDecl => this.usedConstants.has(constDecl.name)),
116
+ implBlocks: program.implBlocks.map(implBlock => ({
117
+ ...implBlock,
118
+ methods: implBlock.methods.map(method => this.transformFunction(method)),
119
+ })),
120
+ }
121
+ }
122
+
123
+ private findEntryPoints(program: Program): string[] {
124
+ const entries = new Set<string>()
125
+
126
+ for (const fn of program.declarations) {
127
+ if (fn.name === 'main') {
128
+ entries.add(fn.name)
129
+ }
130
+
131
+ if (fn.decorators.some(decorator => [
132
+ 'tick',
133
+ 'load',
134
+ 'on',
135
+ 'on_trigger',
136
+ 'on_advancement',
137
+ 'on_craft',
138
+ 'on_death',
139
+ 'on_login',
140
+ 'on_join_team',
141
+ 'keep', // Prevent DCE from removing this function
142
+ ].includes(decorator.name))) {
143
+ entries.add(fn.name)
144
+ }
145
+ }
146
+
147
+ return [...entries]
148
+ }
149
+
150
+ private markReachable(fnName: string): void {
151
+ if (this.reachableFunctions.has(fnName)) {
152
+ return
153
+ }
154
+
155
+ const fn = this.functionMap.get(fnName)
156
+ if (!fn) {
157
+ return
158
+ }
159
+
160
+ this.reachableFunctions.add(fnName)
161
+ this.collectFunctionRefs(fn)
162
+ }
163
+
164
+ private collectFunctionRefs(fn: FnDecl): void {
165
+ const scope: ScopeEntry[][] = [fn.params.map(param => ({ id: `param:${fn.name}:${param.name}`, name: param.name }))]
166
+ for (const param of fn.params) {
167
+ if (param.default) {
168
+ this.collectExprRefs(param.default, scope)
169
+ }
170
+ }
171
+ this.collectStmtRefs(fn.body, scope)
172
+ }
173
+
174
+ private collectStmtRefs(block: Block, scope: ScopeEntry[][]): void {
175
+ scope.push([])
176
+
177
+ for (const stmt of block) {
178
+ this.collectStmtRef(stmt, scope)
179
+ }
180
+
181
+ scope.pop()
182
+ }
183
+
184
+ private collectStmtRef(stmt: Stmt, scope: ScopeEntry[][]): void {
185
+ switch (stmt.kind) {
186
+ case 'let': {
187
+ this.collectExprRefs(stmt.init, scope)
188
+ const id = `local:${stmt.name}:${this.localIdCounter++}:${(stmt.span?.line ?? 0)}:${(stmt.span?.col ?? 0)}`
189
+ this.localDeclIds.set(stmt, id)
190
+ scope[scope.length - 1].push({ id, name: stmt.name })
191
+ break
192
+ }
193
+ case 'expr':
194
+ this.collectExprRefs(stmt.expr, scope)
195
+ break
196
+ case 'return':
197
+ if (stmt.value) {
198
+ this.collectExprRefs(stmt.value, scope)
199
+ }
200
+ break
201
+ case 'if': {
202
+ this.collectExprRefs(stmt.cond, scope)
203
+ const constant = isConstantBoolean(stmt.cond)
204
+ if (constant === true) {
205
+ this.collectStmtRefs(stmt.then, scope)
206
+ } else if (constant === false) {
207
+ if (stmt.else_) {
208
+ this.collectStmtRefs(stmt.else_, scope)
209
+ }
210
+ } else {
211
+ this.collectStmtRefs(stmt.then, scope)
212
+ if (stmt.else_) {
213
+ this.collectStmtRefs(stmt.else_, scope)
214
+ }
215
+ }
216
+ break
217
+ }
218
+ case 'while':
219
+ this.collectExprRefs(stmt.cond, scope)
220
+ this.collectStmtRefs(stmt.body, scope)
221
+ break
222
+ case 'for':
223
+ scope.push([])
224
+ if (stmt.init) {
225
+ this.collectStmtRef(stmt.init, scope)
226
+ }
227
+ this.collectExprRefs(stmt.cond, scope)
228
+ this.collectExprRefs(stmt.step, scope)
229
+ this.collectStmtRefs(stmt.body, scope)
230
+ scope.pop()
231
+ break
232
+ case 'foreach':
233
+ this.collectExprRefs(stmt.iterable, scope)
234
+ scope.push([{ id: `foreach:${stmt.binding}:${stmt.span?.line ?? 0}:${stmt.span?.col ?? 0}`, name: stmt.binding }])
235
+ this.collectStmtRefs(stmt.body, scope)
236
+ scope.pop()
237
+ break
238
+ case 'for_range':
239
+ this.collectExprRefs(stmt.start, scope)
240
+ this.collectExprRefs(stmt.end, scope)
241
+ scope.push([{ id: `range:${stmt.varName}:${stmt.span?.line ?? 0}:${stmt.span?.col ?? 0}`, name: stmt.varName }])
242
+ this.collectStmtRefs(stmt.body, scope)
243
+ scope.pop()
244
+ break
245
+ case 'match':
246
+ this.collectExprRefs(stmt.expr, scope)
247
+ for (const arm of stmt.arms) {
248
+ if (arm.pattern) {
249
+ this.collectExprRefs(arm.pattern, scope)
250
+ }
251
+ this.collectStmtRefs(arm.body, scope)
252
+ }
253
+ break
254
+ case 'as_block':
255
+ case 'at_block':
256
+ case 'as_at':
257
+ case 'execute':
258
+ this.collectNestedStmtRefs(stmt, scope)
259
+ break
260
+ case 'raw':
261
+ break
262
+ }
263
+ }
264
+
265
+ private collectNestedStmtRefs(
266
+ stmt: Extract<Stmt, { kind: 'as_block' | 'at_block' | 'as_at' | 'execute' }>,
267
+ scope: ScopeEntry[][]
268
+ ): void {
269
+ if (stmt.kind === 'execute') {
270
+ for (const sub of stmt.subcommands) {
271
+ if ('varName' in sub && sub.varName) {
272
+ const resolved = this.resolveLocal(sub.varName, scope)
273
+ if (resolved) {
274
+ this.localReads.add(resolved.id)
275
+ }
276
+ }
277
+ }
278
+ }
279
+ this.collectStmtRefs(stmt.body, scope)
280
+ }
281
+
282
+ private collectExprRefs(expr: Expr, scope: ScopeEntry[][]): void {
283
+ switch (expr.kind) {
284
+ case 'ident': {
285
+ const resolved = this.resolveLocal(expr.name, scope)
286
+ if (resolved) {
287
+ this.localReads.add(resolved.id)
288
+ } else {
289
+ this.usedConstants.add(expr.name)
290
+ }
291
+ break
292
+ }
293
+ case 'call':
294
+ {
295
+ const resolved = this.resolveLocal(expr.fn, scope)
296
+ if (resolved) {
297
+ this.localReads.add(resolved.id)
298
+ } else if (this.functionMap.has(expr.fn)) {
299
+ this.markReachable(expr.fn)
300
+ }
301
+ }
302
+ for (const arg of expr.args) {
303
+ this.collectExprRefs(arg, scope)
304
+ }
305
+ break
306
+ case 'static_call':
307
+ for (const arg of expr.args) {
308
+ this.collectExprRefs(arg, scope)
309
+ }
310
+ break
311
+ case 'invoke':
312
+ this.collectExprRefs(expr.callee, scope)
313
+ for (const arg of expr.args) {
314
+ this.collectExprRefs(arg, scope)
315
+ }
316
+ break
317
+ case 'member':
318
+ this.collectExprRefs(expr.obj, scope)
319
+ break
320
+ case 'member_assign':
321
+ this.collectExprRefs(expr.obj, scope)
322
+ this.collectExprRefs(expr.value, scope)
323
+ break
324
+ case 'index':
325
+ this.collectExprRefs(expr.obj, scope)
326
+ this.collectExprRefs(expr.index, scope)
327
+ break
328
+ case 'array_lit':
329
+ expr.elements.forEach(element => this.collectExprRefs(element, scope))
330
+ break
331
+ case 'struct_lit':
332
+ expr.fields.forEach(field => this.collectExprRefs(field.value, scope))
333
+ break
334
+ case 'binary':
335
+ this.collectExprRefs(expr.left, scope)
336
+ this.collectExprRefs(expr.right, scope)
337
+ break
338
+ case 'is_check':
339
+ this.collectExprRefs(expr.expr, scope)
340
+ break
341
+ case 'unary':
342
+ this.collectExprRefs(expr.operand, scope)
343
+ break
344
+ case 'assign': {
345
+ this.collectExprRefs(expr.value, scope)
346
+ break
347
+ }
348
+ case 'str_interp':
349
+ expr.parts.forEach(part => {
350
+ if (typeof part !== 'string') {
351
+ this.collectExprRefs(part, scope)
352
+ }
353
+ })
354
+ break
355
+ case 'f_string':
356
+ expr.parts.forEach(part => {
357
+ if (part.kind === 'expr') {
358
+ this.collectExprRefs(part.expr, scope)
359
+ }
360
+ })
361
+ break
362
+ case 'lambda': {
363
+ const lambdaScope: ScopeEntry[][] = [
364
+ ...scope.map(entries => [...entries]),
365
+ expr.params.map(param => ({ id: `lambda:${param.name}:${expr.span?.line ?? 0}:${expr.span?.col ?? 0}`, name: param.name })),
366
+ ]
367
+ if (Array.isArray(expr.body)) {
368
+ this.collectStmtRefs(expr.body, lambdaScope)
369
+ } else {
370
+ this.collectExprRefs(expr.body, lambdaScope)
371
+ }
372
+ break
373
+ }
374
+ case 'blockpos':
375
+ case 'bool_lit':
376
+ case 'byte_lit':
377
+ case 'double_lit':
378
+ case 'float_lit':
379
+ case 'int_lit':
380
+ case 'long_lit':
381
+ case 'mc_name':
382
+ case 'range_lit':
383
+ case 'selector':
384
+ case 'short_lit':
385
+ case 'str_lit':
386
+ break
387
+ }
388
+ }
389
+
390
+ private resolveLocal(name: string, scope: ScopeEntry[][]): ScopeEntry | null {
391
+ for (let i = scope.length - 1; i >= 0; i--) {
392
+ for (let j = scope[i].length - 1; j >= 0; j--) {
393
+ if (scope[i][j].name === name) {
394
+ return scope[i][j]
395
+ }
396
+ }
397
+ }
398
+ return null
399
+ }
400
+
401
+ private transformFunction(fn: FnDecl): FnDecl {
402
+ const scope: ScopeEntry[][] = [fn.params.map(param => ({ id: `param:${fn.name}:${param.name}`, name: param.name }))]
403
+ const body = this.transformBlock(fn.body, scope)
404
+ return body === fn.body ? fn : copySpan({ ...fn, body }, fn)
405
+ }
406
+
407
+ private transformBlock(block: Block, scope: ScopeEntry[][]): Block {
408
+ scope.push([])
409
+ const transformed: Stmt[] = []
410
+
411
+ for (const stmt of block) {
412
+ const next = this.transformStmt(stmt, scope)
413
+ transformed.push(...next)
414
+ }
415
+
416
+ scope.pop()
417
+ return transformed
418
+ }
419
+
420
+ private transformStmt(stmt: Stmt, scope: ScopeEntry[][]): Stmt[] {
421
+ switch (stmt.kind) {
422
+ case 'let': {
423
+ const init = this.transformExpr(stmt.init, scope)
424
+ const id = this.localDeclIds.get(stmt) ?? `local:${stmt.name}:${stmt.span?.line ?? 0}:${stmt.span?.col ?? 0}`
425
+ scope[scope.length - 1].push({ id, name: stmt.name })
426
+ if (this.localReads.has(id)) {
427
+ if (init === stmt.init) {
428
+ return [stmt]
429
+ }
430
+ return [copySpan({ ...stmt, init }, stmt)]
431
+ }
432
+ if (isPureExpr(init)) {
433
+ return []
434
+ }
435
+ return [copySpan({ kind: 'expr', expr: init }, stmt)]
436
+ }
437
+ case 'expr': {
438
+ const expr = this.transformExpr(stmt.expr, scope)
439
+ if (expr.kind === 'assign') {
440
+ const resolved = this.resolveLocal(expr.target, scope)
441
+ if (resolved && !this.localReads.has(resolved.id)) {
442
+ if (isPureExpr(expr.value)) {
443
+ return []
444
+ }
445
+ return [copySpan({ kind: 'expr', expr: expr.value }, stmt)]
446
+ }
447
+ }
448
+ if (expr === stmt.expr) {
449
+ return [stmt]
450
+ }
451
+ return [copySpan({ ...stmt, expr }, stmt)]
452
+ }
453
+ case 'return': {
454
+ if (!stmt.value) {
455
+ return [stmt]
456
+ }
457
+ const value = this.transformExpr(stmt.value, scope)
458
+ if (value === stmt.value) {
459
+ return [stmt]
460
+ }
461
+ return [copySpan({ ...stmt, value }, stmt)]
462
+ }
463
+ case 'if': {
464
+ const cond = this.transformExpr(stmt.cond, scope)
465
+ const constant = isConstantBoolean(cond)
466
+ if (constant === true) {
467
+ return this.transformBlock(stmt.then, scope)
468
+ }
469
+ if (constant === false) {
470
+ return stmt.else_ ? this.transformBlock(stmt.else_, scope) : []
471
+ }
472
+ const thenBlock = this.transformBlock(stmt.then, scope)
473
+ const elseBlock = stmt.else_ ? this.transformBlock(stmt.else_, scope) : undefined
474
+ if (cond === stmt.cond && thenBlock === stmt.then && elseBlock === stmt.else_) {
475
+ return [stmt]
476
+ }
477
+ return [copySpan({ ...stmt, cond, then: thenBlock, else_: elseBlock }, stmt)]
478
+ }
479
+ case 'while': {
480
+ const cond = this.transformExpr(stmt.cond, scope)
481
+ if (isConstantBoolean(cond) === false) {
482
+ return []
483
+ }
484
+ const body = this.transformBlock(stmt.body, scope)
485
+ return [copySpan({ ...stmt, cond, body }, stmt)]
486
+ }
487
+ case 'for': {
488
+ const forScope: ScopeEntry[][] = [...scope, []]
489
+ const init = stmt.init ? this.transformStmt(stmt.init, forScope)[0] : undefined
490
+ const cond = this.transformExpr(stmt.cond, forScope)
491
+ if (isConstantBoolean(cond) === false) {
492
+ return init ? [init] : []
493
+ }
494
+ const step = this.transformExpr(stmt.step, forScope)
495
+ const body = this.transformBlock(stmt.body, forScope)
496
+ return [copySpan({ ...stmt, init, cond, step, body }, stmt)]
497
+ }
498
+ case 'foreach': {
499
+ const iterable = this.transformExpr(stmt.iterable, scope)
500
+ const foreachScope: ScopeEntry[][] = [...scope, [{ id: `foreach:${stmt.binding}:${stmt.span?.line ?? 0}:${stmt.span?.col ?? 0}`, name: stmt.binding }]]
501
+ const body = this.transformBlock(stmt.body, foreachScope)
502
+ return [copySpan({ ...stmt, iterable, body }, stmt)]
503
+ }
504
+ case 'for_range': {
505
+ const start = this.transformExpr(stmt.start, scope)
506
+ const end = this.transformExpr(stmt.end, scope)
507
+ const rangeScope: ScopeEntry[][] = [...scope, [{ id: `range:${stmt.varName}:${stmt.span?.line ?? 0}:${stmt.span?.col ?? 0}`, name: stmt.varName }]]
508
+ const body = this.transformBlock(stmt.body, rangeScope)
509
+ return [copySpan({ ...stmt, start, end, body }, stmt)]
510
+ }
511
+ case 'match': {
512
+ const expr = this.transformExpr(stmt.expr, scope)
513
+ const arms = stmt.arms.map(arm => ({
514
+ pattern: arm.pattern ? this.transformExpr(arm.pattern, scope) : null,
515
+ body: this.transformBlock(arm.body, scope),
516
+ }))
517
+ return [copySpan({ ...stmt, expr, arms }, stmt)]
518
+ }
519
+ case 'as_block':
520
+ return [copySpan({ ...stmt, body: this.transformBlock(stmt.body, scope) }, stmt)]
521
+ case 'at_block':
522
+ return [copySpan({ ...stmt, body: this.transformBlock(stmt.body, scope) }, stmt)]
523
+ case 'as_at':
524
+ return [copySpan({ ...stmt, body: this.transformBlock(stmt.body, scope) }, stmt)]
525
+ case 'execute':
526
+ return [copySpan({ ...stmt, body: this.transformBlock(stmt.body, scope) }, stmt)]
527
+ case 'raw':
528
+ return [stmt]
529
+ }
530
+ }
531
+
532
+ private transformExpr(expr: Expr, scope: ScopeEntry[][]): Expr {
533
+ switch (expr.kind) {
534
+ case 'call':
535
+ return copySpan({ ...expr, args: expr.args.map(arg => this.transformExpr(arg, scope)) }, expr)
536
+ case 'static_call':
537
+ return copySpan({ ...expr, args: expr.args.map(arg => this.transformExpr(arg, scope)) }, expr)
538
+ case 'invoke':
539
+ return copySpan({
540
+ ...expr,
541
+ callee: this.transformExpr(expr.callee, scope),
542
+ args: expr.args.map(arg => this.transformExpr(arg, scope)),
543
+ }, expr)
544
+ case 'binary':
545
+ return copySpan({
546
+ ...expr,
547
+ left: this.transformExpr(expr.left, scope),
548
+ right: this.transformExpr(expr.right, scope),
549
+ }, expr)
550
+ case 'is_check':
551
+ return copySpan({ ...expr, expr: this.transformExpr(expr.expr, scope) }, expr)
552
+ case 'unary':
553
+ return copySpan({ ...expr, operand: this.transformExpr(expr.operand, scope) }, expr)
554
+ case 'assign':
555
+ return copySpan({ ...expr, value: this.transformExpr(expr.value, scope) }, expr)
556
+ case 'member':
557
+ return copySpan({ ...expr, obj: this.transformExpr(expr.obj, scope) }, expr)
558
+ case 'member_assign':
559
+ return copySpan({
560
+ ...expr,
561
+ obj: this.transformExpr(expr.obj, scope),
562
+ value: this.transformExpr(expr.value, scope),
563
+ }, expr)
564
+ case 'index':
565
+ return copySpan({
566
+ ...expr,
567
+ obj: this.transformExpr(expr.obj, scope),
568
+ index: this.transformExpr(expr.index, scope),
569
+ }, expr)
570
+ case 'array_lit':
571
+ return copySpan({ ...expr, elements: expr.elements.map(element => this.transformExpr(element, scope)) }, expr)
572
+ case 'struct_lit':
573
+ return copySpan({
574
+ ...expr,
575
+ fields: expr.fields.map(field => ({ ...field, value: this.transformExpr(field.value, scope) })),
576
+ }, expr)
577
+ case 'str_interp':
578
+ return copySpan({
579
+ ...expr,
580
+ parts: expr.parts.map(part => typeof part === 'string' ? part : this.transformExpr(part, scope)),
581
+ }, expr)
582
+ case 'f_string':
583
+ return copySpan({
584
+ ...expr,
585
+ parts: expr.parts.map(part => part.kind === 'text' ? part : { kind: 'expr', expr: this.transformExpr(part.expr, scope) }),
586
+ }, expr)
587
+ case 'lambda': {
588
+ const lambdaScope: ScopeEntry[][] = [
589
+ ...scope.map(entries => [...entries]),
590
+ expr.params.map(param => ({ id: `lambda:${param.name}:${expr.span?.line ?? 0}:${expr.span?.col ?? 0}`, name: param.name })),
591
+ ]
592
+ const body = Array.isArray(expr.body)
593
+ ? this.transformBlock(expr.body, lambdaScope)
594
+ : this.transformExpr(expr.body, lambdaScope)
595
+ return copySpan({ ...expr, body }, expr)
596
+ }
597
+ case 'blockpos':
598
+ case 'bool_lit':
599
+ case 'byte_lit':
600
+ case 'double_lit':
601
+ case 'float_lit':
602
+ case 'ident':
603
+ case 'int_lit':
604
+ case 'long_lit':
605
+ case 'mc_name':
606
+ case 'range_lit':
607
+ case 'rel_coord':
608
+ case 'local_coord':
609
+ case 'selector':
610
+ case 'short_lit':
611
+ case 'str_lit':
612
+ return expr
613
+ }
614
+ }
615
+ }
616
+
617
+ export function eliminateDeadCode(program: Program): Program {
618
+ return new DeadCodeEliminator().eliminate(program)
619
+ }