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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
- package/CHANGELOG.md +112 -0
- package/CONTRIBUTING.md +140 -0
- package/README.md +28 -19
- package/README.zh.md +28 -19
- package/dist/__tests__/cli.test.js +148 -10
- package/dist/__tests__/codegen.test.js +26 -1
- package/dist/__tests__/diagnostics.test.js +5 -5
- package/dist/__tests__/e2e.test.js +336 -17
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +40 -0
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +200 -12
- package/dist/__tests__/mc-integration.test.js +370 -31
- package/dist/__tests__/mc-syntax.test.js +3 -3
- package/dist/__tests__/nbt.test.js +2 -2
- package/dist/__tests__/optimizer-advanced.test.js +5 -5
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +9 -9
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +40 -3
- package/dist/cli.js +25 -7
- package/dist/codegen/mcfunction/index.d.ts +1 -1
- package/dist/codegen/mcfunction/index.js +38 -3
- package/dist/codegen/structure/index.js +32 -1
- package/dist/compile.d.ts +10 -0
- package/dist/compile.js +36 -5
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +26 -0
- package/dist/index.js +3 -2
- package/dist/ir/builder.d.ts +2 -1
- package/dist/ir/types.d.ts +11 -2
- package/dist/ir/types.js +1 -1
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +34 -1
- package/dist/lowering/index.js +622 -23
- package/dist/mc-test/runner.d.ts +2 -2
- package/dist/mc-test/runner.js +3 -3
- package/dist/mc-test/setup.js +2 -2
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +153 -16
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/COMPILATION_STATS.md +24 -24
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/docs/IMPLEMENTATION_GUIDE.md +1 -1
- package/docs/STRUCTURE_TARGET.md +1 -1
- package/editors/vscode/.vscodeignore +1 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icons/mcrs.svg +7 -0
- package/editors/vscode/icons/redscript-icons.json +10 -0
- package/editors/vscode/out/extension.js +1295 -80
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +10 -3
- package/editors/vscode/src/hover.ts +55 -2
- package/editors/vscode/src/symbols.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +176 -10
- package/src/__tests__/codegen.test.ts +28 -1
- package/src/__tests__/diagnostics.test.ts +5 -5
- package/src/__tests__/e2e.test.ts +335 -17
- 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 +14 -2
- package/src/__tests__/lowering.test.ts +226 -12
- package/src/__tests__/mc-integration.test.ts +421 -31
- package/src/__tests__/mc-syntax.test.ts +3 -3
- package/src/__tests__/nbt.test.ts +2 -2
- package/src/__tests__/optimizer-advanced.test.ts +5 -5
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +9 -9
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +44 -3
- package/src/cli.ts +10 -10
- package/src/codegen/mcfunction/index.ts +40 -3
- package/src/codegen/structure/index.ts +35 -1
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/examples/capture_the_flag.mcrs +208 -0
- package/src/examples/{counter.rs → counter.mcrs} +1 -1
- package/src/examples/hunger_games.mcrs +301 -0
- package/src/examples/new_features_demo.mcrs +193 -0
- package/src/examples/parkour_race.mcrs +233 -0
- package/src/examples/rpg.mcrs +13 -0
- package/src/examples/{shop.rs → shop.mcrs} +1 -1
- package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
- package/src/examples/{turret.rs → turret.mcrs} +1 -1
- package/src/examples/zombie_survival.mcrs +314 -0
- package/src/index.ts +4 -3
- package/src/ir/builder.ts +3 -1
- package/src/ir/types.ts +12 -2
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +684 -24
- package/src/mc-test/runner.ts +3 -3
- package/src/mc-test/setup.ts +2 -2
- package/src/parser/index.ts +170 -19
- package/src/stdlib/README.md +178 -140
- package/src/stdlib/bossbar.mcrs +68 -0
- package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
- package/src/stdlib/effects.mcrs +64 -0
- package/src/stdlib/interactions.mcrs +195 -0
- package/src/stdlib/inventory.mcrs +38 -0
- package/src/stdlib/mobs.mcrs +99 -0
- package/src/stdlib/particles.mcrs +52 -0
- package/src/stdlib/sets.mcrs +20 -0
- package/src/stdlib/spawn.mcrs +41 -0
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/teams.mcrs +68 -0
- package/src/stdlib/timer.mcrs +72 -0
- package/src/stdlib/world.mcrs +92 -0
- package/src/typechecker/index.ts +404 -18
- package/src/examples/rpg.rs +0 -13
- package/src/stdlib/mobs.rs +0 -99
- package/src/stdlib/timer.rs +0 -51
- /package/src/examples/{arena.rs → arena.mcrs} +0 -0
- /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
- /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
- /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
- /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
- /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
- /package/src/stdlib/{math.rs → math.mcrs} +0 -0
- /package/src/stdlib/{player.rs → player.mcrs} +0 -0
- /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
- /package/src/templates/{combat.rs → combat.mcrs} +0 -0
- /package/src/templates/{economy.rs → economy.mcrs} +0 -0
- /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
- /package/src/templates/{quest.rs → quest.mcrs} +0 -0
- /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
package/src/lowering/index.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1431
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 = `$
|
|
2988
|
+
const name = `$_${LoweringBuilder.globalTempId++}`
|
|
2329
2989
|
this.locals.add(name)
|
|
2330
2990
|
return name
|
|
2331
2991
|
}
|