redscript-mc 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +31 -4
- package/dist/__tests__/lowering.test.js +172 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +90 -0
- package/dist/__tests__/runtime.test.js +21 -8
- package/dist/__tests__/typechecker.test.js +188 -0
- package/dist/ast/types.d.ts +42 -3
- package/dist/cli.js +15 -10
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +29 -2
- package/dist/compile.d.ts +11 -0
- package/dist/compile.js +40 -6
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -3
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +91 -1
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +476 -16
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +591 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +160 -26
- package/dist/typechecker/index.d.ts +19 -0
- package/dist/typechecker/index.js +392 -17
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/out/extension.js +1144 -72
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
- package/examples/spiral.mcrs +79 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/dce.test.ts +129 -0
- package/src/__tests__/e2e.test.ts +201 -12
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +35 -4
- package/src/__tests__/lowering.test.ts +187 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +102 -5
- package/src/__tests__/runtime.test.ts +24 -8
- package/src/__tests__/typechecker.test.ts +204 -0
- package/src/ast/types.ts +39 -2
- package/src/cli.ts +24 -10
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +40 -2
- package/src/compile.ts +59 -7
- package/src/events/types.ts +69 -0
- package/src/index.ts +9 -4
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +105 -2
- package/src/lowering/index.ts +566 -18
- package/src/optimizer/dce.ts +618 -0
- package/src/parser/index.ts +187 -29
- package/src/stdlib/README.md +34 -4
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/timer.mcrs +54 -33
- package/src/typechecker/index.ts +469 -18
package/src/lowering/index.ts
CHANGED
|
@@ -9,11 +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
14
|
Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, Program, RangeExpr, Span, Stmt,
|
|
14
|
-
StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent
|
|
15
|
+
StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent, EntityTypeName
|
|
15
16
|
} from '../ast/types'
|
|
16
17
|
import type { GlobalVar } from '../ir/types'
|
|
18
|
+
import * as path from 'path'
|
|
19
|
+
import { EVENT_TYPES, getEventParamSpecs, isEventTypeName } from '../events/types'
|
|
17
20
|
|
|
18
21
|
// ---------------------------------------------------------------------------
|
|
19
22
|
// Builtin Functions
|
|
@@ -22,6 +25,7 @@ import type { GlobalVar } from '../ir/types'
|
|
|
22
25
|
const BUILTINS: Record<string, (args: string[]) => string | null> = {
|
|
23
26
|
say: ([msg]) => `say ${msg}`,
|
|
24
27
|
tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
|
|
28
|
+
tellraw: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
|
|
25
29
|
title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
|
|
26
30
|
actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
|
|
27
31
|
subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
|
|
@@ -88,6 +92,9 @@ const BUILTINS: Record<string, (args: string[]) => string | null> = {
|
|
|
88
92
|
set_contains: () => null, // Special handling (returns 1/0)
|
|
89
93
|
set_remove: () => null, // Special handling
|
|
90
94
|
set_clear: () => null, // Special handling
|
|
95
|
+
setTimeout: () => null, // Special handling
|
|
96
|
+
setInterval: () => null, // Special handling
|
|
97
|
+
clearInterval: () => null, // Special handling
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
export interface Warning {
|
|
@@ -97,6 +104,12 @@ export interface Warning {
|
|
|
97
104
|
col?: number
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
interface StdlibCallSiteContext {
|
|
108
|
+
filePath?: string
|
|
109
|
+
line: number
|
|
110
|
+
col: number
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
function getSpan(node: unknown): Span | undefined {
|
|
101
114
|
return (node as { span?: Span } | undefined)?.span
|
|
102
115
|
}
|
|
@@ -104,6 +117,23 @@ function getSpan(node: unknown): Span | undefined {
|
|
|
104
117
|
const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/
|
|
105
118
|
const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/
|
|
106
119
|
|
|
120
|
+
const ENTITY_TO_MC_TYPE: Partial<Record<EntityTypeName, string>> = {
|
|
121
|
+
Player: 'player',
|
|
122
|
+
Zombie: 'zombie',
|
|
123
|
+
Skeleton: 'skeleton',
|
|
124
|
+
Creeper: 'creeper',
|
|
125
|
+
Spider: 'spider',
|
|
126
|
+
Enderman: 'enderman',
|
|
127
|
+
Pig: 'pig',
|
|
128
|
+
Cow: 'cow',
|
|
129
|
+
Sheep: 'sheep',
|
|
130
|
+
Chicken: 'chicken',
|
|
131
|
+
Villager: 'villager',
|
|
132
|
+
ArmorStand: 'armor_stand',
|
|
133
|
+
Item: 'item',
|
|
134
|
+
Arrow: 'arrow',
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
function normalizeSelector(selector: string, warnings: Warning[]): string {
|
|
108
138
|
return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
|
|
109
139
|
const trimmed = entityType.trim()
|
|
@@ -156,20 +186,27 @@ function emitBlockPos(pos: BlockPosExpr): string {
|
|
|
156
186
|
|
|
157
187
|
export class Lowering {
|
|
158
188
|
private namespace: string
|
|
189
|
+
private readonly sourceRanges: SourceRange[]
|
|
159
190
|
private functions: IRFunction[] = []
|
|
160
191
|
private globals: GlobalVar[] = []
|
|
161
192
|
private globalNames: Map<string, { mutable: boolean }> = new Map()
|
|
162
193
|
private fnDecls: Map<string, FnDecl> = new Map()
|
|
194
|
+
private implMethods: Map<string, Map<string, { fn: FnDecl; loweredName: string }>> = new Map()
|
|
163
195
|
private specializedFunctions: Map<string, string> = new Map()
|
|
164
196
|
private currentFn: string = ''
|
|
197
|
+
private currentStdlibCallSite?: StdlibCallSiteContext
|
|
165
198
|
private foreachCounter: number = 0
|
|
166
199
|
private lambdaCounter: number = 0
|
|
200
|
+
private timeoutCounter: number = 0
|
|
201
|
+
private intervalCounter: number = 0
|
|
167
202
|
readonly warnings: Warning[] = []
|
|
168
203
|
|
|
169
204
|
// Builder state for current function
|
|
170
205
|
private builder!: LoweringBuilder
|
|
171
206
|
private varMap: Map<string, string> = new Map()
|
|
172
207
|
private lambdaBindings: Map<string, string> = new Map()
|
|
208
|
+
private intervalBindings: Map<string, string> = new Map()
|
|
209
|
+
private intervalFunctions: Map<number, string> = new Map()
|
|
173
210
|
private currentCallbackBindings: Map<string, string> = new Map()
|
|
174
211
|
private currentContext: { binding?: string } = {}
|
|
175
212
|
private blockPosVars: Map<string, BlockPosExpr> = new Map()
|
|
@@ -187,8 +224,9 @@ export class Lowering {
|
|
|
187
224
|
// World object counter for unique tags
|
|
188
225
|
private worldObjCounter: number = 0
|
|
189
226
|
|
|
190
|
-
constructor(namespace: string) {
|
|
227
|
+
constructor(namespace: string, sourceRanges: SourceRange[] = []) {
|
|
191
228
|
this.namespace = namespace
|
|
229
|
+
this.sourceRanges = sourceRanges
|
|
192
230
|
LoweringBuilder.resetTempCounter()
|
|
193
231
|
}
|
|
194
232
|
|
|
@@ -230,10 +268,31 @@ export class Lowering {
|
|
|
230
268
|
this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
|
|
231
269
|
}
|
|
232
270
|
|
|
271
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
272
|
+
let methods = this.implMethods.get(implBlock.typeName)
|
|
273
|
+
if (!methods) {
|
|
274
|
+
methods = new Map()
|
|
275
|
+
this.implMethods.set(implBlock.typeName, methods)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const method of implBlock.methods) {
|
|
279
|
+
const loweredName = `${implBlock.typeName}_${method.name}`
|
|
280
|
+
methods.set(method.name, { fn: method, loweredName })
|
|
281
|
+
this.fnDecls.set(loweredName, method)
|
|
282
|
+
this.functionDefaults.set(loweredName, method.params.map(param => param.default))
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
233
286
|
for (const fn of program.declarations) {
|
|
234
287
|
this.lowerFn(fn)
|
|
235
288
|
}
|
|
236
289
|
|
|
290
|
+
for (const implBlock of program.implBlocks ?? []) {
|
|
291
|
+
for (const method of implBlock.methods) {
|
|
292
|
+
this.lowerFn(method, { name: `${implBlock.typeName}_${method.name}` })
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
237
296
|
return buildModule(this.namespace, this.functions, this.globals)
|
|
238
297
|
}
|
|
239
298
|
|
|
@@ -246,16 +305,25 @@ export class Lowering {
|
|
|
246
305
|
options: {
|
|
247
306
|
name?: string
|
|
248
307
|
callbackBindings?: Map<string, string>
|
|
308
|
+
stdlibCallSite?: StdlibCallSiteContext
|
|
249
309
|
} = {}
|
|
250
310
|
): void {
|
|
251
311
|
const loweredName = options.name ?? fn.name
|
|
252
312
|
const callbackBindings = options.callbackBindings ?? new Map<string, string>()
|
|
253
|
-
const
|
|
313
|
+
const stdlibCallSite = options.stdlibCallSite
|
|
314
|
+
const staticEventDec = fn.decorators.find(d => d.name === 'on')
|
|
315
|
+
const eventType = staticEventDec?.args?.eventType
|
|
316
|
+
const eventParamSpecs = eventType && isEventTypeName(eventType) ? getEventParamSpecs(eventType) : []
|
|
317
|
+
const runtimeParams = staticEventDec
|
|
318
|
+
? []
|
|
319
|
+
: fn.params.filter(param => !callbackBindings.has(param.name))
|
|
254
320
|
|
|
255
321
|
this.currentFn = loweredName
|
|
322
|
+
this.currentStdlibCallSite = stdlibCallSite
|
|
256
323
|
this.foreachCounter = 0
|
|
257
324
|
this.varMap = new Map()
|
|
258
325
|
this.lambdaBindings = new Map()
|
|
326
|
+
this.intervalBindings = new Map()
|
|
259
327
|
this.currentCallbackBindings = new Map(callbackBindings)
|
|
260
328
|
this.currentContext = {}
|
|
261
329
|
this.blockPosVars = new Map()
|
|
@@ -263,10 +331,31 @@ export class Lowering {
|
|
|
263
331
|
this.builder = new LoweringBuilder()
|
|
264
332
|
|
|
265
333
|
// Map parameters
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
334
|
+
if (staticEventDec) {
|
|
335
|
+
for (let i = 0; i < fn.params.length; i++) {
|
|
336
|
+
const param = fn.params[i]
|
|
337
|
+
const expected = eventParamSpecs[i]
|
|
338
|
+
const normalizedType = this.normalizeType(param.type)
|
|
339
|
+
this.varTypes.set(param.name, normalizedType)
|
|
340
|
+
|
|
341
|
+
if (expected?.type.kind === 'entity') {
|
|
342
|
+
this.varMap.set(param.name, '@s')
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (expected?.type.kind === 'named' && expected.type.name === 'string') {
|
|
347
|
+
this.stringValues.set(param.name, '')
|
|
348
|
+
continue
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.varMap.set(param.name, `$${param.name}`)
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
for (const param of runtimeParams) {
|
|
355
|
+
const paramName = param.name
|
|
356
|
+
this.varMap.set(paramName, `$${paramName}`)
|
|
357
|
+
this.varTypes.set(paramName, this.normalizeType(param.type))
|
|
358
|
+
}
|
|
270
359
|
}
|
|
271
360
|
for (const param of fn.params) {
|
|
272
361
|
if (callbackBindings.has(param.name)) {
|
|
@@ -284,6 +373,16 @@ export class Lowering {
|
|
|
284
373
|
this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` })
|
|
285
374
|
}
|
|
286
375
|
|
|
376
|
+
if (staticEventDec) {
|
|
377
|
+
for (let i = 0; i < fn.params.length; i++) {
|
|
378
|
+
const param = fn.params[i]
|
|
379
|
+
const expected = eventParamSpecs[i]
|
|
380
|
+
if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
|
|
381
|
+
this.builder.emitAssign(`$${param.name}`, { kind: 'const', value: 0 })
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
287
386
|
// Lower body
|
|
288
387
|
this.lowerBlock(fn.body)
|
|
289
388
|
|
|
@@ -336,6 +435,13 @@ export class Lowering {
|
|
|
336
435
|
}
|
|
337
436
|
}
|
|
338
437
|
|
|
438
|
+
if (eventType && isEventTypeName(eventType)) {
|
|
439
|
+
irFn.eventHandler = {
|
|
440
|
+
eventType,
|
|
441
|
+
tag: EVENT_TYPES[eventType].tag,
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
339
445
|
// Check for @load decorator
|
|
340
446
|
if (fn.decorators.some(d => d.name === 'load')) {
|
|
341
447
|
irFn.isLoadInit = true
|
|
@@ -489,6 +595,16 @@ export class Lowering {
|
|
|
489
595
|
return
|
|
490
596
|
}
|
|
491
597
|
|
|
598
|
+
if (stmt.init.kind === 'call' && stmt.init.fn === 'setInterval') {
|
|
599
|
+
const value = this.lowerExpr(stmt.init)
|
|
600
|
+
const intervalFn = this.intervalFunctions.get(value.kind === 'const' ? value.value : NaN)
|
|
601
|
+
if (intervalFn) {
|
|
602
|
+
this.intervalBindings.set(stmt.name, intervalFn)
|
|
603
|
+
}
|
|
604
|
+
this.builder.emitAssign(varName, value)
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
|
|
492
608
|
// Handle struct literal initialization
|
|
493
609
|
if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
|
|
494
610
|
const structName = stmt.type.name.toLowerCase()
|
|
@@ -567,6 +683,11 @@ export class Lowering {
|
|
|
567
683
|
}
|
|
568
684
|
|
|
569
685
|
private lowerIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
|
|
686
|
+
if (stmt.cond.kind === 'is_check') {
|
|
687
|
+
this.lowerIsCheckIfStmt(stmt)
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
|
|
570
691
|
const condVar = this.lowerExpr(stmt.cond)
|
|
571
692
|
const condName = this.operandToVar(condVar)
|
|
572
693
|
|
|
@@ -596,6 +717,66 @@ export class Lowering {
|
|
|
596
717
|
this.builder.startBlock(mergeLabel)
|
|
597
718
|
}
|
|
598
719
|
|
|
720
|
+
private lowerIsCheckIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
|
|
721
|
+
const cond = stmt.cond
|
|
722
|
+
if (cond.kind !== 'is_check') {
|
|
723
|
+
throw new DiagnosticError(
|
|
724
|
+
'LoweringError',
|
|
725
|
+
"Internal error: expected 'is' check condition",
|
|
726
|
+
stmt.span ?? { line: 0, col: 0 }
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (stmt.else_) {
|
|
731
|
+
throw new DiagnosticError(
|
|
732
|
+
'LoweringError',
|
|
733
|
+
"'is' checks with else branches are not yet supported",
|
|
734
|
+
cond.span ?? stmt.span ?? { line: 0, col: 0 }
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const selector = this.exprToEntitySelector(cond.expr)
|
|
739
|
+
if (!selector) {
|
|
740
|
+
throw new DiagnosticError(
|
|
741
|
+
'LoweringError',
|
|
742
|
+
"'is' checks require an entity selector or entity binding",
|
|
743
|
+
cond.span ?? stmt.span ?? { line: 0, col: 0 }
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const mcType = ENTITY_TO_MC_TYPE[cond.entityType]
|
|
748
|
+
if (!mcType) {
|
|
749
|
+
throw new DiagnosticError(
|
|
750
|
+
'LoweringError',
|
|
751
|
+
`Cannot lower entity type check for '${cond.entityType}'`,
|
|
752
|
+
cond.span ?? stmt.span ?? { line: 0, col: 0 }
|
|
753
|
+
)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`
|
|
757
|
+
this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`)
|
|
758
|
+
|
|
759
|
+
const savedBuilder = this.builder
|
|
760
|
+
const savedVarMap = new Map(this.varMap)
|
|
761
|
+
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
762
|
+
|
|
763
|
+
this.builder = new LoweringBuilder()
|
|
764
|
+
this.varMap = new Map(savedVarMap)
|
|
765
|
+
this.blockPosVars = new Map(savedBlockPosVars)
|
|
766
|
+
|
|
767
|
+
this.builder.startBlock('entry')
|
|
768
|
+
this.lowerBlock(stmt.then)
|
|
769
|
+
if (!this.builder.isBlockSealed()) {
|
|
770
|
+
this.builder.emitReturn()
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
this.functions.push(this.builder.build(thenFnName, [], false))
|
|
774
|
+
|
|
775
|
+
this.builder = savedBuilder
|
|
776
|
+
this.varMap = savedVarMap
|
|
777
|
+
this.blockPosVars = savedBlockPosVars
|
|
778
|
+
}
|
|
779
|
+
|
|
599
780
|
private lowerWhileStmt(stmt: Extract<Stmt, { kind: 'while' }>): void {
|
|
600
781
|
const checkLabel = this.builder.freshLabel('loop_check')
|
|
601
782
|
const bodyLabel = this.builder.freshLabel('loop_body')
|
|
@@ -1054,6 +1235,7 @@ export class Lowering {
|
|
|
1054
1235
|
return { kind: 'const', value: 0 } // Handled inline in exprToString
|
|
1055
1236
|
|
|
1056
1237
|
case 'str_interp':
|
|
1238
|
+
case 'f_string':
|
|
1057
1239
|
// Interpolated strings are handled inline in message builtins.
|
|
1058
1240
|
return { kind: 'const', value: 0 }
|
|
1059
1241
|
|
|
@@ -1098,6 +1280,13 @@ export class Lowering {
|
|
|
1098
1280
|
case 'binary':
|
|
1099
1281
|
return this.lowerBinaryExpr(expr)
|
|
1100
1282
|
|
|
1283
|
+
case 'is_check':
|
|
1284
|
+
throw new DiagnosticError(
|
|
1285
|
+
'LoweringError',
|
|
1286
|
+
"'is' checks are only supported as if conditions",
|
|
1287
|
+
expr.span ?? { line: 0, col: 0 }
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1101
1290
|
case 'unary':
|
|
1102
1291
|
return this.lowerUnaryExpr(expr)
|
|
1103
1292
|
|
|
@@ -1107,6 +1296,9 @@ export class Lowering {
|
|
|
1107
1296
|
case 'call':
|
|
1108
1297
|
return this.lowerCallExpr(expr)
|
|
1109
1298
|
|
|
1299
|
+
case 'static_call':
|
|
1300
|
+
return this.lowerStaticCallExpr(expr)
|
|
1301
|
+
|
|
1110
1302
|
case 'invoke':
|
|
1111
1303
|
return this.lowerInvokeExpr(expr)
|
|
1112
1304
|
|
|
@@ -1454,6 +1646,11 @@ export class Lowering {
|
|
|
1454
1646
|
return this.emitDirectFunctionCall(callbackTarget, expr.args)
|
|
1455
1647
|
}
|
|
1456
1648
|
|
|
1649
|
+
const implMethod = this.resolveInstanceMethod(expr)
|
|
1650
|
+
if (implMethod) {
|
|
1651
|
+
return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args)
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1457
1654
|
// Regular function call
|
|
1458
1655
|
const fnDecl = this.fnDecls.get(expr.fn)
|
|
1459
1656
|
const defaultArgs = this.functionDefaults.get(expr.fn) ?? []
|
|
@@ -1483,8 +1680,9 @@ export class Lowering {
|
|
|
1483
1680
|
runtimeArgs.push(fullArgs[i])
|
|
1484
1681
|
}
|
|
1485
1682
|
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1683
|
+
const stdlibCallSite = this.getStdlibCallSiteContext(fnDecl, getSpan(expr))
|
|
1684
|
+
const targetFn = callbackBindings.size > 0 || stdlibCallSite
|
|
1685
|
+
? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
|
|
1488
1686
|
: expr.fn
|
|
1489
1687
|
return this.emitDirectFunctionCall(targetFn, runtimeArgs)
|
|
1490
1688
|
}
|
|
@@ -1492,6 +1690,12 @@ export class Lowering {
|
|
|
1492
1690
|
return this.emitDirectFunctionCall(expr.fn, fullArgs)
|
|
1493
1691
|
}
|
|
1494
1692
|
|
|
1693
|
+
private lowerStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): Operand {
|
|
1694
|
+
const method = this.implMethods.get(expr.type)?.get(expr.method)
|
|
1695
|
+
const targetFn = method?.loweredName ?? `${expr.type}_${expr.method}`
|
|
1696
|
+
return this.emitMethodCall(targetFn, method?.fn, expr.args)
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1495
1699
|
private lowerInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): Operand {
|
|
1496
1700
|
if (expr.callee.kind === 'lambda') {
|
|
1497
1701
|
if (!Array.isArray(expr.callee.body)) {
|
|
@@ -1543,6 +1747,19 @@ export class Lowering {
|
|
|
1543
1747
|
return { kind: 'var', name: dst }
|
|
1544
1748
|
}
|
|
1545
1749
|
|
|
1750
|
+
private emitMethodCall(fn: string, fnDecl: FnDecl | undefined, args: Expr[]): Operand {
|
|
1751
|
+
const defaultArgs = this.functionDefaults.get(fn) ?? fnDecl?.params.map(param => param.default) ?? []
|
|
1752
|
+
const fullArgs = [...args]
|
|
1753
|
+
for (let i = fullArgs.length; i < defaultArgs.length; i++) {
|
|
1754
|
+
const defaultExpr = defaultArgs[i]
|
|
1755
|
+
if (!defaultExpr) {
|
|
1756
|
+
break
|
|
1757
|
+
}
|
|
1758
|
+
fullArgs.push(defaultExpr)
|
|
1759
|
+
}
|
|
1760
|
+
return this.emitDirectFunctionCall(fn, fullArgs)
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1546
1763
|
private resolveFunctionRefExpr(expr: Expr): string | null {
|
|
1547
1764
|
if (expr.kind === 'lambda') {
|
|
1548
1765
|
return this.lowerLambdaExpr(expr)
|
|
@@ -1558,9 +1775,21 @@ export class Lowering {
|
|
|
1558
1775
|
}
|
|
1559
1776
|
|
|
1560
1777
|
private ensureSpecializedFunction(fn: FnDecl, callbackBindings: Map<string, string>): string {
|
|
1778
|
+
return this.ensureSpecializedFunctionWithContext(fn, callbackBindings)
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
private ensureSpecializedFunctionWithContext(
|
|
1782
|
+
fn: FnDecl,
|
|
1783
|
+
callbackBindings: Map<string, string>,
|
|
1784
|
+
stdlibCallSite?: StdlibCallSiteContext
|
|
1785
|
+
): string {
|
|
1561
1786
|
const parts = [...callbackBindings.entries()]
|
|
1562
1787
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
1563
1788
|
.map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`)
|
|
1789
|
+
const callSiteHash = stdlibCallSite ? this.shortHash(this.serializeCallSite(stdlibCallSite)) : null
|
|
1790
|
+
if (callSiteHash) {
|
|
1791
|
+
parts.push(`callsite_${callSiteHash}`)
|
|
1792
|
+
}
|
|
1564
1793
|
const key = `${fn.name}::${parts.join('::')}`
|
|
1565
1794
|
const cached = this.specializedFunctions.get(key)
|
|
1566
1795
|
if (cached) {
|
|
@@ -1570,7 +1799,7 @@ export class Lowering {
|
|
|
1570
1799
|
const specializedName = `${fn.name}__${parts.join('__')}`
|
|
1571
1800
|
this.specializedFunctions.set(key, specializedName)
|
|
1572
1801
|
this.withSavedFunctionState(() => {
|
|
1573
|
-
this.lowerFn(fn, { name: specializedName, callbackBindings })
|
|
1802
|
+
this.lowerFn(fn, { name: specializedName, callbackBindings, stdlibCallSite })
|
|
1574
1803
|
})
|
|
1575
1804
|
return specializedName
|
|
1576
1805
|
}
|
|
@@ -1595,10 +1824,12 @@ export class Lowering {
|
|
|
1595
1824
|
|
|
1596
1825
|
private withSavedFunctionState<T>(callback: () => T): T {
|
|
1597
1826
|
const savedCurrentFn = this.currentFn
|
|
1827
|
+
const savedStdlibCallSite = this.currentStdlibCallSite
|
|
1598
1828
|
const savedForeachCounter = this.foreachCounter
|
|
1599
1829
|
const savedBuilder = this.builder
|
|
1600
1830
|
const savedVarMap = new Map(this.varMap)
|
|
1601
1831
|
const savedLambdaBindings = new Map(this.lambdaBindings)
|
|
1832
|
+
const savedIntervalBindings = new Map(this.intervalBindings)
|
|
1602
1833
|
const savedCallbackBindings = new Map(this.currentCallbackBindings)
|
|
1603
1834
|
const savedContext = this.currentContext
|
|
1604
1835
|
const savedBlockPosVars = new Map(this.blockPosVars)
|
|
@@ -1609,10 +1840,12 @@ export class Lowering {
|
|
|
1609
1840
|
return callback()
|
|
1610
1841
|
} finally {
|
|
1611
1842
|
this.currentFn = savedCurrentFn
|
|
1843
|
+
this.currentStdlibCallSite = savedStdlibCallSite
|
|
1612
1844
|
this.foreachCounter = savedForeachCounter
|
|
1613
1845
|
this.builder = savedBuilder
|
|
1614
1846
|
this.varMap = savedVarMap
|
|
1615
1847
|
this.lambdaBindings = savedLambdaBindings
|
|
1848
|
+
this.intervalBindings = savedIntervalBindings
|
|
1616
1849
|
this.currentCallbackBindings = savedCallbackBindings
|
|
1617
1850
|
this.currentContext = savedContext
|
|
1618
1851
|
this.blockPosVars = savedBlockPosVars
|
|
@@ -1628,6 +1861,18 @@ export class Lowering {
|
|
|
1628
1861
|
return { kind: 'const', value: 0 }
|
|
1629
1862
|
}
|
|
1630
1863
|
|
|
1864
|
+
if (name === 'setTimeout') {
|
|
1865
|
+
return this.lowerSetTimeout(args)
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (name === 'setInterval') {
|
|
1869
|
+
return this.lowerSetInterval(args)
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
if (name === 'clearInterval') {
|
|
1873
|
+
return this.lowerClearInterval(args, callSpan)
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1631
1876
|
// Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
|
|
1632
1877
|
if (name === 'random') {
|
|
1633
1878
|
const dst = this.builder.freshTemp()
|
|
@@ -1658,7 +1903,7 @@ export class Lowering {
|
|
|
1658
1903
|
if (name === 'scoreboard_get' || name === 'score') {
|
|
1659
1904
|
const dst = this.builder.freshTemp()
|
|
1660
1905
|
const player = this.exprToTargetString(args[0])
|
|
1661
|
-
const objective = this.
|
|
1906
|
+
const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
|
|
1662
1907
|
this.builder.emitRaw(`execute store result score ${dst} rs run scoreboard players get ${player} ${objective}`)
|
|
1663
1908
|
return { kind: 'var', name: dst }
|
|
1664
1909
|
}
|
|
@@ -1666,7 +1911,7 @@ export class Lowering {
|
|
|
1666
1911
|
// Special case: scoreboard_set — write to vanilla MC scoreboard
|
|
1667
1912
|
if (name === 'scoreboard_set') {
|
|
1668
1913
|
const player = this.exprToTargetString(args[0])
|
|
1669
|
-
const objective = this.
|
|
1914
|
+
const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
|
|
1670
1915
|
const value = this.lowerExpr(args[2])
|
|
1671
1916
|
if (value.kind === 'const') {
|
|
1672
1917
|
this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`)
|
|
@@ -1680,7 +1925,7 @@ export class Lowering {
|
|
|
1680
1925
|
|
|
1681
1926
|
if (name === 'scoreboard_display') {
|
|
1682
1927
|
const slot = this.exprToString(args[0])
|
|
1683
|
-
const objective = this.
|
|
1928
|
+
const objective = this.resolveScoreboardObjective(undefined, args[1], callSpan)
|
|
1684
1929
|
this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`)
|
|
1685
1930
|
return { kind: 'const', value: 0 }
|
|
1686
1931
|
}
|
|
@@ -1692,7 +1937,7 @@ export class Lowering {
|
|
|
1692
1937
|
}
|
|
1693
1938
|
|
|
1694
1939
|
if (name === 'scoreboard_add_objective') {
|
|
1695
|
-
const objective = this.
|
|
1940
|
+
const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
|
|
1696
1941
|
const criteria = this.exprToString(args[1])
|
|
1697
1942
|
const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : ''
|
|
1698
1943
|
this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`)
|
|
@@ -1700,7 +1945,7 @@ export class Lowering {
|
|
|
1700
1945
|
}
|
|
1701
1946
|
|
|
1702
1947
|
if (name === 'scoreboard_remove_objective') {
|
|
1703
|
-
const objective = this.
|
|
1948
|
+
const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
|
|
1704
1949
|
this.builder.emitRaw(`scoreboard objectives remove ${objective}`)
|
|
1705
1950
|
return { kind: 'const', value: 0 }
|
|
1706
1951
|
}
|
|
@@ -1902,6 +2147,119 @@ export class Lowering {
|
|
|
1902
2147
|
return { kind: 'const', value: 0 }
|
|
1903
2148
|
}
|
|
1904
2149
|
|
|
2150
|
+
private lowerSetTimeout(args: Expr[]): Operand {
|
|
2151
|
+
const delay = this.exprToLiteral(args[0])
|
|
2152
|
+
const callback = args[1]
|
|
2153
|
+
if (!callback || callback.kind !== 'lambda') {
|
|
2154
|
+
throw new DiagnosticError(
|
|
2155
|
+
'LoweringError',
|
|
2156
|
+
'setTimeout requires a lambda callback',
|
|
2157
|
+
getSpan(callback) ?? { line: 1, col: 1 }
|
|
2158
|
+
)
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
const fnName = `__timeout_${this.timeoutCounter++}`
|
|
2162
|
+
this.lowerNamedLambdaFunction(fnName, callback)
|
|
2163
|
+
this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
|
|
2164
|
+
return { kind: 'const', value: 0 }
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
private lowerSetInterval(args: Expr[]): Operand {
|
|
2168
|
+
const delay = this.exprToLiteral(args[0])
|
|
2169
|
+
const callback = args[1]
|
|
2170
|
+
if (!callback || callback.kind !== 'lambda') {
|
|
2171
|
+
throw new DiagnosticError(
|
|
2172
|
+
'LoweringError',
|
|
2173
|
+
'setInterval requires a lambda callback',
|
|
2174
|
+
getSpan(callback) ?? { line: 1, col: 1 }
|
|
2175
|
+
)
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
const id = this.intervalCounter++
|
|
2179
|
+
const bodyName = `__interval_body_${id}`
|
|
2180
|
+
const fnName = `__interval_${id}`
|
|
2181
|
+
|
|
2182
|
+
this.lowerNamedLambdaFunction(bodyName, callback)
|
|
2183
|
+
this.lowerIntervalWrapperFunction(fnName, bodyName, delay)
|
|
2184
|
+
this.intervalFunctions.set(id, fnName)
|
|
2185
|
+
this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
|
|
2186
|
+
|
|
2187
|
+
return { kind: 'const', value: id }
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
private lowerClearInterval(args: Expr[], callSpan?: Span): Operand {
|
|
2191
|
+
const fnName = this.resolveIntervalFunctionName(args[0])
|
|
2192
|
+
if (!fnName) {
|
|
2193
|
+
throw new DiagnosticError(
|
|
2194
|
+
'LoweringError',
|
|
2195
|
+
'clearInterval requires an interval ID returned from setInterval',
|
|
2196
|
+
callSpan ?? getSpan(args[0]) ?? { line: 1, col: 1 }
|
|
2197
|
+
)
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
this.builder.emitRaw(`schedule clear ${this.namespace}:${fnName}`)
|
|
2201
|
+
return { kind: 'const', value: 0 }
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
private lowerNamedLambdaFunction(name: string, expr: Extract<Expr, { kind: 'lambda' }>): void {
|
|
2205
|
+
const lambdaFn: FnDecl = {
|
|
2206
|
+
name,
|
|
2207
|
+
params: expr.params.map(param => ({
|
|
2208
|
+
name: param.name,
|
|
2209
|
+
type: param.type ?? { kind: 'named', name: 'int' },
|
|
2210
|
+
})),
|
|
2211
|
+
returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
|
|
2212
|
+
decorators: [],
|
|
2213
|
+
body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
this.withSavedFunctionState(() => {
|
|
2217
|
+
this.lowerFn(lambdaFn)
|
|
2218
|
+
})
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
private lowerIntervalWrapperFunction(name: string, bodyName: string, delay: string): void {
|
|
2222
|
+
const intervalFn: FnDecl = {
|
|
2223
|
+
name,
|
|
2224
|
+
params: [],
|
|
2225
|
+
returnType: { kind: 'named', name: 'void' },
|
|
2226
|
+
decorators: [],
|
|
2227
|
+
body: [
|
|
2228
|
+
{ kind: 'raw', cmd: `function ${this.namespace}:${bodyName}` },
|
|
2229
|
+
{ kind: 'raw', cmd: `schedule function ${this.namespace}:${name} ${delay}t` },
|
|
2230
|
+
],
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
this.withSavedFunctionState(() => {
|
|
2234
|
+
this.lowerFn(intervalFn)
|
|
2235
|
+
})
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
private resolveIntervalFunctionName(expr: Expr | undefined): string | null {
|
|
2239
|
+
if (!expr) {
|
|
2240
|
+
return null
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
if (expr.kind === 'ident') {
|
|
2244
|
+
const boundInterval = this.intervalBindings.get(expr.name)
|
|
2245
|
+
if (boundInterval) {
|
|
2246
|
+
return boundInterval
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
const constValue = this.constValues.get(expr.name)
|
|
2250
|
+
if (constValue?.kind === 'int_lit') {
|
|
2251
|
+
return this.intervalFunctions.get(constValue.value) ?? null
|
|
2252
|
+
}
|
|
2253
|
+
return null
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
if (expr.kind === 'int_lit') {
|
|
2257
|
+
return this.intervalFunctions.get(expr.value) ?? null
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
return null
|
|
2261
|
+
}
|
|
2262
|
+
|
|
1905
2263
|
private lowerRichTextBuiltin(name: string, args: Expr[]): string | null {
|
|
1906
2264
|
const messageArgIndex = this.getRichTextArgIndex(name)
|
|
1907
2265
|
if (messageArgIndex === null) {
|
|
@@ -1909,7 +2267,7 @@ export class Lowering {
|
|
|
1909
2267
|
}
|
|
1910
2268
|
|
|
1911
2269
|
const messageExpr = args[messageArgIndex]
|
|
1912
|
-
if (!messageExpr || messageExpr.kind !== 'str_interp') {
|
|
2270
|
+
if (!messageExpr || (messageExpr.kind !== 'str_interp' && messageExpr.kind !== 'f_string')) {
|
|
1913
2271
|
return null
|
|
1914
2272
|
}
|
|
1915
2273
|
|
|
@@ -1920,6 +2278,7 @@ export class Lowering {
|
|
|
1920
2278
|
case 'announce':
|
|
1921
2279
|
return `tellraw @a ${json}`
|
|
1922
2280
|
case 'tell':
|
|
2281
|
+
case 'tellraw':
|
|
1923
2282
|
return `tellraw ${this.exprToString(args[0])} ${json}`
|
|
1924
2283
|
case 'title':
|
|
1925
2284
|
return `title ${this.exprToString(args[0])} title ${json}`
|
|
@@ -1938,6 +2297,7 @@ export class Lowering {
|
|
|
1938
2297
|
case 'announce':
|
|
1939
2298
|
return 0
|
|
1940
2299
|
case 'tell':
|
|
2300
|
+
case 'tellraw':
|
|
1941
2301
|
case 'title':
|
|
1942
2302
|
case 'actionbar':
|
|
1943
2303
|
case 'subtitle':
|
|
@@ -1947,9 +2307,22 @@ export class Lowering {
|
|
|
1947
2307
|
}
|
|
1948
2308
|
}
|
|
1949
2309
|
|
|
1950
|
-
private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' }>): string {
|
|
2310
|
+
private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' | 'f_string' }>): string {
|
|
1951
2311
|
const components: Array<string | Record<string, unknown>> = ['']
|
|
1952
2312
|
|
|
2313
|
+
if (expr.kind === 'f_string') {
|
|
2314
|
+
for (const part of expr.parts) {
|
|
2315
|
+
if (part.kind === 'text') {
|
|
2316
|
+
if (part.value.length > 0) {
|
|
2317
|
+
components.push({ text: part.value })
|
|
2318
|
+
}
|
|
2319
|
+
continue
|
|
2320
|
+
}
|
|
2321
|
+
this.appendRichTextExpr(components, part.expr)
|
|
2322
|
+
}
|
|
2323
|
+
return JSON.stringify(components)
|
|
2324
|
+
}
|
|
2325
|
+
|
|
1953
2326
|
for (const part of expr.parts) {
|
|
1954
2327
|
if (typeof part === 'string') {
|
|
1955
2328
|
if (part.length > 0) {
|
|
@@ -1998,6 +2371,19 @@ export class Lowering {
|
|
|
1998
2371
|
return
|
|
1999
2372
|
}
|
|
2000
2373
|
|
|
2374
|
+
if (expr.kind === 'f_string') {
|
|
2375
|
+
for (const part of expr.parts) {
|
|
2376
|
+
if (part.kind === 'text') {
|
|
2377
|
+
if (part.value.length > 0) {
|
|
2378
|
+
components.push({ text: part.value })
|
|
2379
|
+
}
|
|
2380
|
+
} else {
|
|
2381
|
+
this.appendRichTextExpr(components, part.expr)
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
return
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2001
2387
|
if (expr.kind === 'bool_lit') {
|
|
2002
2388
|
components.push({ text: expr.value ? 'true' : 'false' })
|
|
2003
2389
|
return
|
|
@@ -2036,6 +2422,10 @@ export class Lowering {
|
|
|
2036
2422
|
return `${expr.value}L`
|
|
2037
2423
|
case 'double_lit':
|
|
2038
2424
|
return `${expr.value}d`
|
|
2425
|
+
case 'rel_coord':
|
|
2426
|
+
return expr.value // ~ or ~5 or ~-3 - output as-is for MC commands
|
|
2427
|
+
case 'local_coord':
|
|
2428
|
+
return expr.value // ^ or ^5 or ^-3 - output as-is for MC commands
|
|
2039
2429
|
case 'bool_lit':
|
|
2040
2430
|
return expr.value ? '1' : '0'
|
|
2041
2431
|
case 'str_lit':
|
|
@@ -2043,6 +2433,7 @@ export class Lowering {
|
|
|
2043
2433
|
case 'mc_name':
|
|
2044
2434
|
return expr.value // #health → "health" (no quotes, used as bare MC name)
|
|
2045
2435
|
case 'str_interp':
|
|
2436
|
+
case 'f_string':
|
|
2046
2437
|
return this.buildRichTextJson(expr)
|
|
2047
2438
|
case 'blockpos':
|
|
2048
2439
|
return emitBlockPos(expr)
|
|
@@ -2078,6 +2469,32 @@ export class Lowering {
|
|
|
2078
2469
|
}
|
|
2079
2470
|
}
|
|
2080
2471
|
|
|
2472
|
+
private exprToEntitySelector(expr: Expr): string | null {
|
|
2473
|
+
if (expr.kind === 'selector') {
|
|
2474
|
+
return this.selectorToString(expr.sel)
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
if (expr.kind === 'ident') {
|
|
2478
|
+
const constValue = this.constValues.get(expr.name)
|
|
2479
|
+
if (constValue) {
|
|
2480
|
+
return this.exprToEntitySelector(constValue)
|
|
2481
|
+
}
|
|
2482
|
+
const mapped = this.varMap.get(expr.name)
|
|
2483
|
+
if (mapped?.startsWith('@')) {
|
|
2484
|
+
return mapped
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
return null
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
private appendTypeFilter(selector: string, mcType: string): string {
|
|
2492
|
+
if (selector.endsWith(']')) {
|
|
2493
|
+
return `${selector.slice(0, -1)},type=${mcType}]`
|
|
2494
|
+
}
|
|
2495
|
+
return `${selector}[type=${mcType}]`
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2081
2498
|
private exprToSnbt(expr: Expr): string {
|
|
2082
2499
|
switch (expr.kind) {
|
|
2083
2500
|
case 'struct_lit': {
|
|
@@ -2152,6 +2569,113 @@ export class Lowering {
|
|
|
2152
2569
|
return option === 'displayName' || option === 'prefix' || option === 'suffix'
|
|
2153
2570
|
}
|
|
2154
2571
|
|
|
2572
|
+
private exprToScoreboardObjective(expr: Expr, span?: Span): string {
|
|
2573
|
+
if (expr.kind === 'mc_name') {
|
|
2574
|
+
return expr.value
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
const objective = this.exprToString(expr)
|
|
2578
|
+
if (objective.startsWith('#') || objective.includes('.')) {
|
|
2579
|
+
return objective.startsWith('#') ? objective.slice(1) : objective
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
return `${this.getObjectiveNamespace(span)}.${objective}`
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
private resolveScoreboardObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string {
|
|
2586
|
+
const stdlibInternalObjective = this.tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span)
|
|
2587
|
+
if (stdlibInternalObjective) {
|
|
2588
|
+
return stdlibInternalObjective
|
|
2589
|
+
}
|
|
2590
|
+
return this.exprToScoreboardObjective(objectiveExpr, span)
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
private getObjectiveNamespace(span?: Span): string {
|
|
2594
|
+
const filePath = this.filePathForSpan(span)
|
|
2595
|
+
if (!filePath) {
|
|
2596
|
+
return this.namespace
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
return this.isStdlibFile(filePath) ? 'rs' : this.namespace
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
private tryGetStdlibInternalObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string | null {
|
|
2603
|
+
if (!span || !this.currentStdlibCallSite || objectiveExpr.kind !== 'mc_name' || objectiveExpr.value !== 'rs') {
|
|
2604
|
+
return null
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
const filePath = this.filePathForSpan(span)
|
|
2608
|
+
if (!filePath || !this.isStdlibFile(filePath)) {
|
|
2609
|
+
return null
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
const resourceBase = this.getStdlibInternalResourceBase(playerExpr)
|
|
2613
|
+
if (!resourceBase) {
|
|
2614
|
+
return null
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
const hash = this.shortHash(this.serializeCallSite(this.currentStdlibCallSite))
|
|
2618
|
+
return `rs._${resourceBase}_${hash}`
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
private getStdlibInternalResourceBase(playerExpr: Expr | undefined): string | null {
|
|
2622
|
+
if (!playerExpr || playerExpr.kind !== 'str_lit') {
|
|
2623
|
+
return null
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
const match = playerExpr.value.match(/^([a-z0-9]+)_/)
|
|
2627
|
+
return match?.[1] ?? null
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
private getStdlibCallSiteContext(fn: FnDecl, exprSpan?: Span): StdlibCallSiteContext | undefined {
|
|
2631
|
+
const fnFilePath = this.filePathForSpan(getSpan(fn))
|
|
2632
|
+
if (!fnFilePath || !this.isStdlibFile(fnFilePath)) {
|
|
2633
|
+
return undefined
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
if (this.currentStdlibCallSite) {
|
|
2637
|
+
return this.currentStdlibCallSite
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
if (!exprSpan) {
|
|
2641
|
+
return undefined
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
return {
|
|
2645
|
+
filePath: this.filePathForSpan(exprSpan),
|
|
2646
|
+
line: exprSpan.line,
|
|
2647
|
+
col: exprSpan.col,
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
private serializeCallSite(callSite: StdlibCallSiteContext): string {
|
|
2652
|
+
return `${callSite.filePath ?? '<memory>'}:${callSite.line}:${callSite.col}`
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
private shortHash(input: string): string {
|
|
2656
|
+
let hash = 2166136261
|
|
2657
|
+
for (let i = 0; i < input.length; i++) {
|
|
2658
|
+
hash ^= input.charCodeAt(i)
|
|
2659
|
+
hash = Math.imul(hash, 16777619)
|
|
2660
|
+
}
|
|
2661
|
+
return (hash >>> 0).toString(16).padStart(8, '0').slice(0, 4)
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
private isStdlibFile(filePath: string): boolean {
|
|
2665
|
+
const normalized = path.normalize(filePath)
|
|
2666
|
+
const stdlibSegment = `${path.sep}src${path.sep}stdlib${path.sep}`
|
|
2667
|
+
return normalized.includes(stdlibSegment)
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
private filePathForSpan(span?: Span): string | undefined {
|
|
2671
|
+
if (!span) {
|
|
2672
|
+
return undefined
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
const line = span.line
|
|
2676
|
+
return this.sourceRanges.find(range => line >= range.startLine && line <= range.endLine)?.filePath
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2155
2679
|
private lowerCoordinateBuiltin(name: string, args: Expr[]): string | null {
|
|
2156
2680
|
const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
|
|
2157
2681
|
const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
|
|
@@ -2243,6 +2767,7 @@ export class Lowering {
|
|
|
2243
2767
|
if (expr.kind === 'float_lit') return { kind: 'named', name: 'float' }
|
|
2244
2768
|
if (expr.kind === 'bool_lit') return { kind: 'named', name: 'bool' }
|
|
2245
2769
|
if (expr.kind === 'str_lit' || expr.kind === 'str_interp') return { kind: 'named', name: 'string' }
|
|
2770
|
+
if (expr.kind === 'f_string') return { kind: 'named', name: 'format_string' }
|
|
2246
2771
|
if (expr.kind === 'blockpos') return { kind: 'named', name: 'BlockPos' }
|
|
2247
2772
|
if (expr.kind === 'ident') {
|
|
2248
2773
|
const constValue = this.constValues.get(expr.name)
|
|
@@ -2268,7 +2793,11 @@ export class Lowering {
|
|
|
2268
2793
|
}
|
|
2269
2794
|
}
|
|
2270
2795
|
if (expr.kind === 'call') {
|
|
2271
|
-
|
|
2796
|
+
const resolved = this.resolveFunctionRefByName(expr.fn) ?? this.resolveInstanceMethod(expr)?.loweredName ?? expr.fn
|
|
2797
|
+
return this.fnDecls.get(resolved)?.returnType
|
|
2798
|
+
}
|
|
2799
|
+
if (expr.kind === 'static_call') {
|
|
2800
|
+
return this.implMethods.get(expr.type)?.get(expr.method)?.fn.returnType
|
|
2272
2801
|
}
|
|
2273
2802
|
if (expr.kind === 'invoke') {
|
|
2274
2803
|
const calleeType = this.inferExprType(expr.callee)
|
|
@@ -2297,6 +2826,25 @@ export class Lowering {
|
|
|
2297
2826
|
return undefined
|
|
2298
2827
|
}
|
|
2299
2828
|
|
|
2829
|
+
private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): { fn: FnDecl; loweredName: string } | null {
|
|
2830
|
+
const receiver = expr.args[0]
|
|
2831
|
+
if (!receiver) {
|
|
2832
|
+
return null
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
const receiverType = this.inferExprType(receiver)
|
|
2836
|
+
if (receiverType?.kind !== 'struct') {
|
|
2837
|
+
return null
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
const method = this.implMethods.get(receiverType.name)?.get(expr.fn)
|
|
2841
|
+
if (!method || method.fn.params[0]?.name !== 'self') {
|
|
2842
|
+
return null
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
return method
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2300
2848
|
private normalizeType(type: TypeNode): TypeNode {
|
|
2301
2849
|
if (type.kind === 'array') {
|
|
2302
2850
|
return { kind: 'array', elem: this.normalizeType(type.elem) }
|