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