redscript-mc 2.1.0 → 2.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 +11 -0
- package/README.md +86 -21
- package/README.zh.md +61 -61
- package/dist/src/__tests__/e2e/basic.test.js +25 -0
- package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
- package/dist/src/__tests__/lsp.test.js +76 -0
- package/dist/src/__tests__/mc-integration.test.js +25 -13
- package/dist/src/__tests__/mc-syntax.test.js +1 -6
- package/dist/src/__tests__/schedule.test.js +105 -0
- package/dist/src/__tests__/stdlib-include.test.d.ts +1 -0
- package/dist/src/__tests__/stdlib-include.test.js +86 -0
- package/dist/src/__tests__/typechecker.test.js +63 -0
- package/dist/src/cli.js +10 -3
- package/dist/src/compile.d.ts +1 -0
- package/dist/src/compile.js +33 -10
- package/dist/src/emit/compile.d.ts +2 -0
- package/dist/src/emit/compile.js +3 -2
- package/dist/src/emit/index.js +3 -1
- package/dist/src/lir/lower.js +26 -0
- package/dist/src/lsp/server.js +51 -0
- package/dist/src/mir/lower.js +341 -12
- package/dist/src/mir/types.d.ts +10 -0
- package/dist/src/optimizer/copy_prop.js +4 -0
- package/dist/src/optimizer/coroutine.d.ts +2 -0
- package/dist/src/optimizer/coroutine.js +33 -1
- package/dist/src/optimizer/dce.js +7 -1
- package/dist/src/optimizer/lir/const_imm.js +1 -1
- package/dist/src/optimizer/lir/dead_slot.js +1 -1
- package/dist/src/typechecker/index.d.ts +2 -0
- package/dist/src/typechecker/index.js +29 -0
- package/docs/ROADMAP.md +35 -0
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +34 -0
- package/examples/coroutine-demo.mcrs +51 -0
- package/examples/enum-demo.mcrs +95 -0
- package/examples/scheduler-demo.mcrs +59 -0
- package/jest.config.js +19 -0
- package/package.json +1 -1
- package/src/__tests__/e2e/basic.test.ts +27 -0
- package/src/__tests__/e2e/coroutine.test.ts +23 -0
- package/src/__tests__/fixtures/array-test.mcrs +21 -22
- package/src/__tests__/fixtures/counter.mcrs +17 -0
- package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
- package/src/__tests__/lsp.test.ts +89 -0
- package/src/__tests__/mc-integration.test.ts +25 -13
- package/src/__tests__/mc-syntax.test.ts +1 -7
- package/src/__tests__/schedule.test.ts +112 -0
- package/src/__tests__/stdlib-include.test.ts +61 -0
- package/src/__tests__/typechecker.test.ts +68 -0
- package/src/cli.ts +9 -1
- package/src/compile.ts +44 -15
- package/src/emit/compile.ts +5 -2
- package/src/emit/index.ts +3 -1
- package/src/lir/lower.ts +27 -0
- package/src/lsp/server.ts +55 -0
- package/src/mir/lower.ts +355 -9
- package/src/mir/types.ts +4 -0
- package/src/optimizer/copy_prop.ts +4 -0
- package/src/optimizer/coroutine.ts +37 -1
- package/src/optimizer/dce.ts +6 -1
- package/src/optimizer/lir/const_imm.ts +1 -1
- package/src/optimizer/lir/dead_slot.ts +1 -1
- package/src/stdlib/timer.mcrs +10 -5
- package/src/typechecker/index.ts +39 -0
- package/examples/spiral.mcrs +0 -43
- package/src/examples/arena.mcrs +0 -44
- package/src/examples/counter.mcrs +0 -12
- package/src/examples/new_features_demo.mcrs +0 -193
- package/src/examples/rpg.mcrs +0 -13
- package/src/examples/stdlib_demo.mcrs +0 -181
package/src/lsp/server.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { Parser } from '../parser'
|
|
|
34
34
|
import { TypeChecker } from '../typechecker'
|
|
35
35
|
import { DiagnosticError } from '../diagnostics'
|
|
36
36
|
import type { Program, FnDecl, Span, TypeNode } from '../ast/types'
|
|
37
|
+
import { BUILTIN_METADATA } from '../builtins/metadata'
|
|
37
38
|
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// Connection and document manager
|
|
@@ -123,6 +124,25 @@ function toDiagnostic(err: DiagnosticError): Diagnostic {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Decorator hover docs
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
const DECORATOR_DOCS: Record<string, string> = {
|
|
132
|
+
tick: 'Runs every MC game tick (~20 Hz). No arguments.',
|
|
133
|
+
load: 'Runs on `/reload`. Use for initialization logic.',
|
|
134
|
+
coroutine: 'Splits loops into tick-spread continuations. Arg: `batch=N` (steps per tick, default 1).',
|
|
135
|
+
schedule: 'Schedules the function to run after N ticks. Arg: `ticks=N`.',
|
|
136
|
+
on_trigger: 'Runs when a trigger scoreboard objective is set by a player. Arg: trigger name.',
|
|
137
|
+
keep: 'Prevents the compiler from dead-code-eliminating this function.',
|
|
138
|
+
on: 'Generic event handler decorator.',
|
|
139
|
+
on_advancement: 'Runs when a player earns an advancement. Arg: advancement id.',
|
|
140
|
+
on_craft: 'Runs when a player crafts an item. Arg: item id.',
|
|
141
|
+
on_death: 'Runs when a player dies.',
|
|
142
|
+
on_join_team: 'Runs when a player joins a team. Arg: team name.',
|
|
143
|
+
on_login: 'Runs when a player logs in.',
|
|
144
|
+
}
|
|
145
|
+
|
|
126
146
|
// ---------------------------------------------------------------------------
|
|
127
147
|
// Hover helpers
|
|
128
148
|
// ---------------------------------------------------------------------------
|
|
@@ -290,9 +310,44 @@ connection.onHover((params: TextDocumentPositionParams): Hover | null => {
|
|
|
290
310
|
const program = cached?.program ?? null
|
|
291
311
|
if (!program) return null
|
|
292
312
|
|
|
313
|
+
// Check if cursor is on a decorator (@tick, @load, etc.)
|
|
314
|
+
const lines = source.split('\n')
|
|
315
|
+
const lineText = lines[params.position.line] ?? ''
|
|
316
|
+
const decoratorMatch = lineText.match(/@([a-zA-Z_][a-zA-Z0-9_]*)/)
|
|
317
|
+
if (decoratorMatch) {
|
|
318
|
+
const ch = params.position.character
|
|
319
|
+
const atIdx = lineText.indexOf('@')
|
|
320
|
+
const decoratorEnd = atIdx + 1 + decoratorMatch[1].length
|
|
321
|
+
if (ch >= atIdx && ch <= decoratorEnd) {
|
|
322
|
+
const decoratorName = decoratorMatch[1]
|
|
323
|
+
const decoratorDoc = DECORATOR_DOCS[decoratorName]
|
|
324
|
+
if (decoratorDoc) {
|
|
325
|
+
const content: MarkupContent = {
|
|
326
|
+
kind: MarkupKind.Markdown,
|
|
327
|
+
value: `**@${decoratorName}** — ${decoratorDoc}`,
|
|
328
|
+
}
|
|
329
|
+
return { contents: content }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
293
334
|
const word = wordAt(source, params.position)
|
|
294
335
|
if (!word) return null
|
|
295
336
|
|
|
337
|
+
// Check builtins
|
|
338
|
+
const builtin = BUILTIN_METADATA[word]
|
|
339
|
+
if (builtin) {
|
|
340
|
+
const paramStr = builtin.params
|
|
341
|
+
.map(p => `${p.name}: ${p.type}${p.required ? '' : '?'}`)
|
|
342
|
+
.join(', ')
|
|
343
|
+
const sig = `fn ${builtin.name}(${paramStr}): ${builtin.returns}`
|
|
344
|
+
const content: MarkupContent = {
|
|
345
|
+
kind: MarkupKind.Markdown,
|
|
346
|
+
value: `\`\`\`redscript\n${sig}\n\`\`\`\n${builtin.doc}`,
|
|
347
|
+
}
|
|
348
|
+
return { contents: content }
|
|
349
|
+
}
|
|
350
|
+
|
|
296
351
|
// Check if it's a known function
|
|
297
352
|
const fn = findFunction(program, word)
|
|
298
353
|
if (fn) {
|
package/src/mir/lower.ts
CHANGED
|
@@ -61,16 +61,18 @@ export function lowerToMIR(hir: HIRModule, sourceFile?: string): MIRModule {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
const timerCounter = { count: 0, timerId: 0 }
|
|
65
|
+
|
|
64
66
|
const allFunctions: MIRFunction[] = []
|
|
65
67
|
for (const f of hir.functions) {
|
|
66
|
-
const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile)
|
|
68
|
+
const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter)
|
|
67
69
|
allFunctions.push(fn, ...helpers)
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
// Lower impl block methods
|
|
71
73
|
for (const ib of hir.implBlocks) {
|
|
72
74
|
for (const m of ib.methods) {
|
|
73
|
-
const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile)
|
|
75
|
+
const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter)
|
|
74
76
|
allFunctions.push(fn, ...helpers)
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -105,6 +107,8 @@ class FnContext {
|
|
|
105
107
|
readonly structVars = new Map<string, { typeName: string; fields: Map<string, Temp> }>()
|
|
106
108
|
/** Tuple variable tracking: varName → array of element temps (index = slot) */
|
|
107
109
|
readonly tupleVars = new Map<string, Temp[]>()
|
|
110
|
+
/** Array variable tracking: varName → { ns, pathPrefix } for NBT-backed int[] */
|
|
111
|
+
readonly arrayVars = new Map<string, { ns: string; pathPrefix: string }>()
|
|
108
112
|
/** Macro function info for all functions in the module */
|
|
109
113
|
readonly macroInfo: Map<string, MacroFunctionInfo>
|
|
110
114
|
/** Function parameter info for call_macro generation */
|
|
@@ -117,6 +121,10 @@ class FnContext {
|
|
|
117
121
|
currentSourceLoc: SourceLoc | undefined = undefined
|
|
118
122
|
/** Source file path for the module being compiled */
|
|
119
123
|
sourceFile: string | undefined = undefined
|
|
124
|
+
/** Shared counter for setTimeout/setInterval callback naming and Timer static IDs (module-wide) */
|
|
125
|
+
readonly timerCounter: { count: number; timerId: number }
|
|
126
|
+
/** Tracks temps whose values are known compile-time constants (for Timer static ID propagation) */
|
|
127
|
+
readonly constTemps = new Map<Temp, number>()
|
|
120
128
|
|
|
121
129
|
constructor(
|
|
122
130
|
namespace: string,
|
|
@@ -126,6 +134,7 @@ class FnContext {
|
|
|
126
134
|
macroInfo: Map<string, MacroFunctionInfo> = new Map(),
|
|
127
135
|
fnParamInfo: Map<string, HIRParam[]> = new Map(),
|
|
128
136
|
enumDefs: Map<string, Map<string, number>> = new Map(),
|
|
137
|
+
timerCounter: { count: number; timerId: number } = { count: 0, timerId: 0 },
|
|
129
138
|
) {
|
|
130
139
|
this.namespace = namespace
|
|
131
140
|
this.fnName = fnName
|
|
@@ -135,6 +144,7 @@ class FnContext {
|
|
|
135
144
|
this.fnParamInfo = fnParamInfo
|
|
136
145
|
this.currentMacroParams = macroInfo.get(fnName)?.macroParams ?? new Set()
|
|
137
146
|
this.enumDefs = enumDefs
|
|
147
|
+
this.timerCounter = timerCounter
|
|
138
148
|
const entry = this.makeBlock('entry')
|
|
139
149
|
this.currentBlock = entry
|
|
140
150
|
}
|
|
@@ -214,8 +224,9 @@ function lowerFunction(
|
|
|
214
224
|
fnParamInfo: Map<string, HIRParam[]> = new Map(),
|
|
215
225
|
enumDefs: Map<string, Map<string, number>> = new Map(),
|
|
216
226
|
sourceFile?: string,
|
|
227
|
+
timerCounter: { count: number; timerId: number } = { count: 0, timerId: 0 },
|
|
217
228
|
): { fn: MIRFunction; helpers: MIRFunction[] } {
|
|
218
|
-
const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs)
|
|
229
|
+
const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter)
|
|
219
230
|
ctx.sourceFile = sourceFile
|
|
220
231
|
const fnMacroInfo = macroInfo.get(fn.name)
|
|
221
232
|
|
|
@@ -267,9 +278,10 @@ function lowerImplMethod(
|
|
|
267
278
|
fnParamInfo: Map<string, HIRParam[]> = new Map(),
|
|
268
279
|
enumDefs: Map<string, Map<string, number>> = new Map(),
|
|
269
280
|
sourceFile?: string,
|
|
281
|
+
timerCounter: { count: number; timerId: number } = { count: 0, timerId: 0 },
|
|
270
282
|
): { fn: MIRFunction; helpers: MIRFunction[] } {
|
|
271
283
|
const fnName = `${typeName}::${method.name}`
|
|
272
|
-
const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs)
|
|
284
|
+
const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter)
|
|
273
285
|
ctx.sourceFile = sourceFile
|
|
274
286
|
const fields = structDefs.get(typeName) ?? []
|
|
275
287
|
const hasSelf = method.params.length > 0 && method.params[0].name === 'self'
|
|
@@ -444,6 +456,12 @@ function lowerStmt(
|
|
|
444
456
|
const t = ctx.freshTemp()
|
|
445
457
|
ctx.emit({ kind: 'copy', dst: t, src: { kind: 'temp', name: `__rf_${fieldName}` } })
|
|
446
458
|
fieldTemps.set(fieldName, t)
|
|
459
|
+
// Propagate compile-time constants from return slots (e.g. Timer._id from Timer::new)
|
|
460
|
+
const rfSlot = `__rf_${fieldName}`
|
|
461
|
+
const constVal = ctx.constTemps.get(rfSlot)
|
|
462
|
+
if (constVal !== undefined) {
|
|
463
|
+
ctx.constTemps.set(t, constVal)
|
|
464
|
+
}
|
|
447
465
|
}
|
|
448
466
|
ctx.structVars.set(stmt.name, { typeName: stmt.type.name, fields: fieldTemps })
|
|
449
467
|
} else {
|
|
@@ -452,6 +470,33 @@ function lowerStmt(
|
|
|
452
470
|
ctx.emit({ kind: 'copy', dst: t, src: valOp })
|
|
453
471
|
scope.set(stmt.name, t)
|
|
454
472
|
}
|
|
473
|
+
} else if (stmt.init.kind === 'array_lit') {
|
|
474
|
+
// Array literal: write to NBT storage, track the var for index access
|
|
475
|
+
const ns = `${ctx.getNamespace()}:arrays`
|
|
476
|
+
const pathPrefix = stmt.name
|
|
477
|
+
ctx.arrayVars.set(stmt.name, { ns, pathPrefix })
|
|
478
|
+
const elems = stmt.init.elements
|
|
479
|
+
// Check if all elements are pure integer literals (no side-effects)
|
|
480
|
+
const allConst = elems.every(e => e.kind === 'int_lit')
|
|
481
|
+
if (allConst) {
|
|
482
|
+
// Emit a single raw 'data modify ... set value [...]' to initialize the whole list
|
|
483
|
+
const vals = elems.map(e => (e as { kind: 'int_lit'; value: number }).value).join(', ')
|
|
484
|
+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${vals}]`, args: [] })
|
|
485
|
+
} else {
|
|
486
|
+
// Initialize with zeros, then overwrite dynamic elements
|
|
487
|
+
const zeros = elems.map(() => '0').join(', ')
|
|
488
|
+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${zeros}]`, args: [] })
|
|
489
|
+
for (let i = 0; i < elems.length; i++) {
|
|
490
|
+
const elemOp = lowerExpr(elems[i], ctx, scope)
|
|
491
|
+
if (elemOp.kind !== 'const' || (elems[i].kind !== 'int_lit')) {
|
|
492
|
+
ctx.emit({ kind: 'nbt_write', ns, path: `${pathPrefix}[${i}]`, type: 'int', scale: 1, src: elemOp })
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Store array length as a temp in scope (for .len access)
|
|
497
|
+
const lenTemp = ctx.freshTemp()
|
|
498
|
+
ctx.emit({ kind: 'const', dst: lenTemp, value: elems.length })
|
|
499
|
+
scope.set(stmt.name, lenTemp)
|
|
455
500
|
} else {
|
|
456
501
|
const valOp = lowerExpr(stmt.init, ctx, scope)
|
|
457
502
|
const t = ctx.freshTemp()
|
|
@@ -730,6 +775,40 @@ function lowerStmt(
|
|
|
730
775
|
if (isPlaceholderTerm(ctx.current().term)) {
|
|
731
776
|
ctx.terminate({ kind: 'jump', target: mergeBlock.id })
|
|
732
777
|
}
|
|
778
|
+
} else if (arm.pattern.kind === 'range_lit') {
|
|
779
|
+
// Range pattern: e.g. 0..59 => emit ge/le comparisons
|
|
780
|
+
const range = arm.pattern.range
|
|
781
|
+
const armBody = ctx.newBlock('match_arm')
|
|
782
|
+
const nextArm = ctx.newBlock('match_next')
|
|
783
|
+
|
|
784
|
+
// Chain checks: if min defined, check matchVal >= min; if max defined, check matchVal <= max
|
|
785
|
+
// Each failed check jumps to nextArm
|
|
786
|
+
const checks: Array<{ op: 'ge' | 'le'; bound: number }> = []
|
|
787
|
+
if (range.min !== undefined) checks.push({ op: 'ge', bound: range.min })
|
|
788
|
+
if (range.max !== undefined) checks.push({ op: 'le', bound: range.max })
|
|
789
|
+
|
|
790
|
+
if (checks.length === 0) {
|
|
791
|
+
// Open range — always matches
|
|
792
|
+
ctx.terminate({ kind: 'jump', target: armBody.id })
|
|
793
|
+
} else {
|
|
794
|
+
// Emit checks sequentially; each check passes → continue to next or armBody
|
|
795
|
+
for (let ci = 0; ci < checks.length; ci++) {
|
|
796
|
+
const { op, bound } = checks[ci]
|
|
797
|
+
const cmpTemp = ctx.freshTemp()
|
|
798
|
+
ctx.emit({ kind: 'cmp', dst: cmpTemp, op, a: matchVal, b: { kind: 'const', value: bound } })
|
|
799
|
+
const passBlock = ci === checks.length - 1 ? armBody : ctx.newBlock('match_range_check')
|
|
800
|
+
ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: cmpTemp }, then: passBlock.id, else: nextArm.id })
|
|
801
|
+
if (ci < checks.length - 1) ctx.switchTo(passBlock)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
ctx.switchTo(armBody)
|
|
806
|
+
lowerBlock(arm.body, ctx, new Map(scope))
|
|
807
|
+
if (isPlaceholderTerm(ctx.current().term)) {
|
|
808
|
+
ctx.terminate({ kind: 'jump', target: mergeBlock.id })
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
ctx.switchTo(nextArm)
|
|
733
812
|
} else {
|
|
734
813
|
const patOp = lowerExpr(arm.pattern, ctx, scope)
|
|
735
814
|
const cmpTemp = ctx.freshTemp()
|
|
@@ -996,14 +1075,116 @@ function lowerExpr(
|
|
|
996
1075
|
}
|
|
997
1076
|
|
|
998
1077
|
case 'index': {
|
|
1078
|
+
// Check if obj is a tracked array variable with a constant index
|
|
1079
|
+
if (expr.obj.kind === 'ident') {
|
|
1080
|
+
const arrInfo = ctx.arrayVars.get(expr.obj.name)
|
|
1081
|
+
if (arrInfo && expr.index.kind === 'int_lit') {
|
|
1082
|
+
const t = ctx.freshTemp()
|
|
1083
|
+
ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, scale: 1 })
|
|
1084
|
+
return { kind: 'temp', name: t }
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
999
1087
|
const obj = lowerExpr(expr.obj, ctx, scope)
|
|
1000
|
-
|
|
1088
|
+
lowerExpr(expr.index, ctx, scope)
|
|
1001
1089
|
const t = ctx.freshTemp()
|
|
1002
1090
|
ctx.emit({ kind: 'copy', dst: t, src: obj })
|
|
1003
1091
|
return { kind: 'temp', name: t }
|
|
1004
1092
|
}
|
|
1005
1093
|
|
|
1006
1094
|
case 'call': {
|
|
1095
|
+
// Handle scoreboard_get / score — read from vanilla MC scoreboard
|
|
1096
|
+
if (expr.fn === 'scoreboard_get' || expr.fn === 'score') {
|
|
1097
|
+
const player = hirExprToStringLiteral(expr.args[0])
|
|
1098
|
+
const obj = hirExprToStringLiteral(expr.args[1])
|
|
1099
|
+
const t = ctx.freshTemp()
|
|
1100
|
+
ctx.emit({ kind: 'score_read', dst: t, player, obj })
|
|
1101
|
+
return { kind: 'temp', name: t }
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Handle scoreboard_set — write to vanilla MC scoreboard
|
|
1105
|
+
if (expr.fn === 'scoreboard_set') {
|
|
1106
|
+
const player = hirExprToStringLiteral(expr.args[0])
|
|
1107
|
+
const obj = hirExprToStringLiteral(expr.args[1])
|
|
1108
|
+
const src = lowerExpr(expr.args[2], ctx, scope)
|
|
1109
|
+
ctx.emit({ kind: 'score_write', player, obj, src })
|
|
1110
|
+
const t = ctx.freshTemp()
|
|
1111
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1112
|
+
return { kind: 'temp', name: t }
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Handle setTimeout/setInterval: lift lambda arg to a named helper function
|
|
1116
|
+
if ((expr.fn === 'setTimeout' || expr.fn === 'setInterval') && expr.args.length === 2) {
|
|
1117
|
+
const ticksArg = expr.args[0]
|
|
1118
|
+
const callbackArg = expr.args[1]
|
|
1119
|
+
const ns = ctx.getNamespace()
|
|
1120
|
+
const id = ctx.timerCounter.count++
|
|
1121
|
+
const callbackName = `__timeout_callback_${id}`
|
|
1122
|
+
|
|
1123
|
+
// Extract ticks value for the schedule command
|
|
1124
|
+
let ticksLiteral: number | null = null
|
|
1125
|
+
if (ticksArg.kind === 'int_lit') {
|
|
1126
|
+
ticksLiteral = ticksArg.value
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Build the callback MIRFunction from the lambda body
|
|
1130
|
+
if (callbackArg.kind === 'lambda') {
|
|
1131
|
+
const cbCtx = new FnContext(
|
|
1132
|
+
ns,
|
|
1133
|
+
callbackName,
|
|
1134
|
+
ctx.structDefs,
|
|
1135
|
+
ctx.implMethods,
|
|
1136
|
+
ctx.macroInfo,
|
|
1137
|
+
ctx.fnParamInfo,
|
|
1138
|
+
ctx.enumDefs,
|
|
1139
|
+
ctx.timerCounter,
|
|
1140
|
+
)
|
|
1141
|
+
cbCtx.sourceFile = ctx.sourceFile
|
|
1142
|
+
|
|
1143
|
+
const cbBody = Array.isArray(callbackArg.body) ? callbackArg.body : [{ kind: 'expr' as const, expr: callbackArg.body }]
|
|
1144
|
+
|
|
1145
|
+
// For setInterval: reschedule at end of body
|
|
1146
|
+
const bodyStmts: typeof cbBody = [...cbBody]
|
|
1147
|
+
if (expr.fn === 'setInterval' && ticksLiteral !== null) {
|
|
1148
|
+
// Append: raw `schedule function ns:callbackName ticksT`
|
|
1149
|
+
bodyStmts.push({
|
|
1150
|
+
kind: 'raw' as const,
|
|
1151
|
+
cmd: `schedule function ${ns}:${callbackName} ${ticksLiteral}t`,
|
|
1152
|
+
} as any)
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
lowerBlock(bodyStmts, cbCtx, new Map())
|
|
1156
|
+
const cbCur = cbCtx.current()
|
|
1157
|
+
if (isPlaceholderTerm(cbCur.term)) {
|
|
1158
|
+
cbCtx.terminate({ kind: 'return', value: null })
|
|
1159
|
+
}
|
|
1160
|
+
const cbReachable = computeReachable(cbCtx.blocks, 'entry')
|
|
1161
|
+
const cbLiveBlocks = cbCtx.blocks.filter(b => cbReachable.has(b.id))
|
|
1162
|
+
computePreds(cbLiveBlocks)
|
|
1163
|
+
const cbFn: MIRFunction = {
|
|
1164
|
+
name: callbackName,
|
|
1165
|
+
params: [],
|
|
1166
|
+
blocks: cbLiveBlocks,
|
|
1167
|
+
entry: 'entry',
|
|
1168
|
+
isMacro: false,
|
|
1169
|
+
}
|
|
1170
|
+
ctx.helperFunctions.push(cbFn, ...cbCtx.helperFunctions)
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Emit: schedule function ns:callbackName ticksT
|
|
1174
|
+
if (ticksLiteral !== null) {
|
|
1175
|
+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:schedule function ${ns}:${callbackName} ${ticksLiteral}t`, args: [] })
|
|
1176
|
+
} else {
|
|
1177
|
+
// Dynamic ticks: lower ticks operand and emit a raw schedule (best-effort)
|
|
1178
|
+
const ticksOp = lowerExpr(ticksArg, ctx, scope)
|
|
1179
|
+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:schedule function ${ns}:${callbackName} 1t`, args: [ticksOp] })
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// setTimeout returns void (0), setInterval returns an int ID (0 for now)
|
|
1183
|
+
const t = ctx.freshTemp()
|
|
1184
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1185
|
+
return { kind: 'temp', name: t }
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1007
1188
|
// Handle builtin calls → raw MC commands
|
|
1008
1189
|
if (BUILTIN_SET.has(expr.fn)) {
|
|
1009
1190
|
const cmd = formatBuiltinCall(expr.fn, expr.args, ctx.currentMacroParams)
|
|
@@ -1017,6 +1198,14 @@ function lowerExpr(
|
|
|
1017
1198
|
if (expr.args.length > 0 && expr.args[0].kind === 'ident') {
|
|
1018
1199
|
const sv = ctx.structVars.get(expr.args[0].name)
|
|
1019
1200
|
if (sv) {
|
|
1201
|
+
// Intercept Timer method calls when _id is a known compile-time constant
|
|
1202
|
+
if (sv.typeName === 'Timer') {
|
|
1203
|
+
const idTemp = sv.fields.get('_id')
|
|
1204
|
+
const timerId = idTemp !== undefined ? ctx.constTemps.get(idTemp) : undefined
|
|
1205
|
+
if (timerId !== undefined) {
|
|
1206
|
+
return lowerTimerMethod(expr.fn, timerId, sv, ctx, scope, expr.args.slice(1))
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1020
1209
|
const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.fn)
|
|
1021
1210
|
if (methodInfo?.hasSelf) {
|
|
1022
1211
|
// Build args: self fields first, then remaining explicit args
|
|
@@ -1069,6 +1258,14 @@ function lowerExpr(
|
|
|
1069
1258
|
if (expr.callee.kind === 'member' && expr.callee.obj.kind === 'ident') {
|
|
1070
1259
|
const sv = ctx.structVars.get(expr.callee.obj.name)
|
|
1071
1260
|
if (sv) {
|
|
1261
|
+
// Intercept Timer method calls when _id is a known compile-time constant
|
|
1262
|
+
if (sv.typeName === 'Timer') {
|
|
1263
|
+
const idTemp = sv.fields.get('_id')
|
|
1264
|
+
const timerId = idTemp !== undefined ? ctx.constTemps.get(idTemp) : undefined
|
|
1265
|
+
if (timerId !== undefined) {
|
|
1266
|
+
return lowerTimerMethod(expr.callee.field, timerId, sv, ctx, scope, expr.args)
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1072
1269
|
const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.callee.field)
|
|
1073
1270
|
if (methodInfo?.hasSelf) {
|
|
1074
1271
|
// Build args: self fields first, then explicit args
|
|
@@ -1094,6 +1291,24 @@ function lowerExpr(
|
|
|
1094
1291
|
}
|
|
1095
1292
|
|
|
1096
1293
|
case 'static_call': {
|
|
1294
|
+
// Intercept Timer::new() to statically allocate a unique ID
|
|
1295
|
+
if (expr.type === 'Timer' && expr.method === 'new' && expr.args.length === 1) {
|
|
1296
|
+
const id = ctx.timerCounter.timerId++
|
|
1297
|
+
const ns = ctx.getNamespace()
|
|
1298
|
+
const playerName = `__timer_${id}`
|
|
1299
|
+
// Emit scoreboard initialization: ticks=0, active=0
|
|
1300
|
+
ctx.emit({ kind: 'score_write', player: `${playerName}_ticks`, obj: ns, src: { kind: 'const', value: 0 } })
|
|
1301
|
+
ctx.emit({ kind: 'score_write', player: `${playerName}_active`, obj: ns, src: { kind: 'const', value: 0 } })
|
|
1302
|
+
// Lower the duration argument
|
|
1303
|
+
const durationOp = lowerExpr(expr.args[0], ctx, scope)
|
|
1304
|
+
// Return fields via __rf_ slots (Timer has fields: _id, _duration)
|
|
1305
|
+
ctx.emit({ kind: 'const', dst: '__rf__id', value: id })
|
|
1306
|
+
ctx.constTemps.set('__rf__id', id)
|
|
1307
|
+
ctx.emit({ kind: 'copy', dst: '__rf__duration', src: durationOp })
|
|
1308
|
+
const t = ctx.freshTemp()
|
|
1309
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1310
|
+
return { kind: 'temp', name: t }
|
|
1311
|
+
}
|
|
1097
1312
|
const args = expr.args.map(a => lowerExpr(a, ctx, scope))
|
|
1098
1313
|
const t = ctx.freshTemp()
|
|
1099
1314
|
ctx.emit({ kind: 'call', dst: t, fn: `${expr.type}::${expr.method}`, args })
|
|
@@ -1198,6 +1413,98 @@ function lowerShortCircuitOr(
|
|
|
1198
1413
|
return { kind: 'temp', name: result }
|
|
1199
1414
|
}
|
|
1200
1415
|
|
|
1416
|
+
// ---------------------------------------------------------------------------
|
|
1417
|
+
// Timer method inlining
|
|
1418
|
+
// ---------------------------------------------------------------------------
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Inline a Timer instance method call using the statically-assigned timer ID.
|
|
1422
|
+
* Emits scoreboard operations directly, bypassing the Timer::* function calls.
|
|
1423
|
+
*/
|
|
1424
|
+
function lowerTimerMethod(
|
|
1425
|
+
method: string,
|
|
1426
|
+
timerId: number,
|
|
1427
|
+
sv: { typeName: string; fields: Map<string, Temp> },
|
|
1428
|
+
ctx: FnContext,
|
|
1429
|
+
scope: Map<string, Temp>,
|
|
1430
|
+
extraArgs: HIRExpr[],
|
|
1431
|
+
): Operand {
|
|
1432
|
+
const ns = ctx.getNamespace()
|
|
1433
|
+
const player = `__timer_${timerId}`
|
|
1434
|
+
const t = ctx.freshTemp()
|
|
1435
|
+
|
|
1436
|
+
if (method === 'start') {
|
|
1437
|
+
ctx.emit({ kind: 'score_write', player: `${player}_active`, obj: ns, src: { kind: 'const', value: 1 } })
|
|
1438
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1439
|
+
} else if (method === 'pause') {
|
|
1440
|
+
ctx.emit({ kind: 'score_write', player: `${player}_active`, obj: ns, src: { kind: 'const', value: 0 } })
|
|
1441
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1442
|
+
} else if (method === 'reset') {
|
|
1443
|
+
ctx.emit({ kind: 'score_write', player: `${player}_ticks`, obj: ns, src: { kind: 'const', value: 0 } })
|
|
1444
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1445
|
+
} else if (method === 'tick') {
|
|
1446
|
+
const durationTemp = sv.fields.get('_duration')
|
|
1447
|
+
const activeTemp = ctx.freshTemp()
|
|
1448
|
+
const ticksTemp = ctx.freshTemp()
|
|
1449
|
+
ctx.emit({ kind: 'score_read', dst: activeTemp, player: `${player}_active`, obj: ns })
|
|
1450
|
+
ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns })
|
|
1451
|
+
const innerThen = ctx.newBlock('timer_tick_inner')
|
|
1452
|
+
const innerMerge = ctx.newBlock('timer_tick_after_lt')
|
|
1453
|
+
const outerMerge = ctx.newBlock('timer_tick_done')
|
|
1454
|
+
const activeCheck = ctx.freshTemp()
|
|
1455
|
+
ctx.emit({ kind: 'cmp', op: 'eq', dst: activeCheck, a: { kind: 'temp', name: activeTemp }, b: { kind: 'const', value: 1 } })
|
|
1456
|
+
ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: activeCheck }, then: innerThen.id, else: outerMerge.id })
|
|
1457
|
+
ctx.switchTo(innerThen)
|
|
1458
|
+
const lessCheck = ctx.freshTemp()
|
|
1459
|
+
if (durationTemp) {
|
|
1460
|
+
ctx.emit({ kind: 'cmp', op: 'lt', dst: lessCheck, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'temp', name: durationTemp } })
|
|
1461
|
+
} else {
|
|
1462
|
+
ctx.emit({ kind: 'const', dst: lessCheck, value: 0 })
|
|
1463
|
+
}
|
|
1464
|
+
const doIncBlock = ctx.newBlock('timer_tick_inc')
|
|
1465
|
+
ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: lessCheck }, then: doIncBlock.id, else: innerMerge.id })
|
|
1466
|
+
ctx.switchTo(doIncBlock)
|
|
1467
|
+
const newTicks = ctx.freshTemp()
|
|
1468
|
+
ctx.emit({ kind: 'add', dst: newTicks, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'const', value: 1 } })
|
|
1469
|
+
ctx.emit({ kind: 'score_write', player: `${player}_ticks`, obj: ns, src: { kind: 'temp', name: newTicks } })
|
|
1470
|
+
ctx.terminate({ kind: 'jump', target: innerMerge.id })
|
|
1471
|
+
ctx.switchTo(innerMerge)
|
|
1472
|
+
ctx.terminate({ kind: 'jump', target: outerMerge.id })
|
|
1473
|
+
ctx.switchTo(outerMerge)
|
|
1474
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1475
|
+
} else if (method === 'done') {
|
|
1476
|
+
const durationTemp = sv.fields.get('_duration')
|
|
1477
|
+
const ticksTemp = ctx.freshTemp()
|
|
1478
|
+
ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns })
|
|
1479
|
+
if (durationTemp) {
|
|
1480
|
+
ctx.emit({ kind: 'cmp', op: 'ge', dst: t, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'temp', name: durationTemp } })
|
|
1481
|
+
} else {
|
|
1482
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1483
|
+
}
|
|
1484
|
+
} else if (method === 'elapsed') {
|
|
1485
|
+
ctx.emit({ kind: 'score_read', dst: t, player: `${player}_ticks`, obj: ns })
|
|
1486
|
+
} else if (method === 'remaining') {
|
|
1487
|
+
const durationTemp = sv.fields.get('_duration')
|
|
1488
|
+
const ticksTemp = ctx.freshTemp()
|
|
1489
|
+
ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns })
|
|
1490
|
+
if (durationTemp) {
|
|
1491
|
+
ctx.emit({ kind: 'sub', dst: t, a: { kind: 'temp', name: durationTemp }, b: { kind: 'temp', name: ticksTemp } })
|
|
1492
|
+
} else {
|
|
1493
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 })
|
|
1494
|
+
}
|
|
1495
|
+
} else {
|
|
1496
|
+
// Unknown Timer method — emit regular call
|
|
1497
|
+
const fields = ['_id', '_duration']
|
|
1498
|
+
const selfArgs: Operand[] = fields.map(f => {
|
|
1499
|
+
const temp = sv.fields.get(f)
|
|
1500
|
+
return temp ? { kind: 'temp' as const, name: temp } : { kind: 'const' as const, value: 0 }
|
|
1501
|
+
})
|
|
1502
|
+
const explicitArgs = extraArgs.map(a => lowerExpr(a, ctx, scope))
|
|
1503
|
+
ctx.emit({ kind: 'call', dst: t, fn: `Timer::${method}`, args: [...selfArgs, ...explicitArgs] })
|
|
1504
|
+
}
|
|
1505
|
+
return { kind: 'temp', name: t }
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1201
1508
|
// ---------------------------------------------------------------------------
|
|
1202
1509
|
// Execute subcommand lowering
|
|
1203
1510
|
// ---------------------------------------------------------------------------
|
|
@@ -1310,13 +1617,32 @@ function formatBuiltinCall(
|
|
|
1310
1617
|
break
|
|
1311
1618
|
}
|
|
1312
1619
|
case 'setblock': {
|
|
1313
|
-
|
|
1314
|
-
|
|
1620
|
+
// args: blockpos, block — expand blockpos to x y z
|
|
1621
|
+
const [posOrX, blockOrY] = args
|
|
1622
|
+
if (posOrX?.kind === 'blockpos') {
|
|
1623
|
+
const px = coordStr(posOrX.x)
|
|
1624
|
+
const py = coordStr(posOrX.y)
|
|
1625
|
+
const pz = coordStr(posOrX.z)
|
|
1626
|
+
const blk = exprToCommandArg(blockOrY, macroParams).str
|
|
1627
|
+
cmd = `setblock ${px} ${py} ${pz} ${blk}`
|
|
1628
|
+
} else {
|
|
1629
|
+
const [x, y, z, block] = strs
|
|
1630
|
+
cmd = `setblock ${x} ${y} ${z} ${block}`
|
|
1631
|
+
}
|
|
1315
1632
|
break
|
|
1316
1633
|
}
|
|
1317
1634
|
case 'fill': {
|
|
1318
|
-
|
|
1319
|
-
|
|
1635
|
+
// args: blockpos1, blockpos2, block — expand both blockpos
|
|
1636
|
+
const [p1, p2, blkArg] = args
|
|
1637
|
+
if (p1?.kind === 'blockpos' && p2?.kind === 'blockpos') {
|
|
1638
|
+
const x1 = coordStr(p1.x); const y1 = coordStr(p1.y); const z1 = coordStr(p1.z)
|
|
1639
|
+
const x2 = coordStr(p2.x); const y2 = coordStr(p2.y); const z2 = coordStr(p2.z)
|
|
1640
|
+
const blk = exprToCommandArg(blkArg, macroParams).str
|
|
1641
|
+
cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${blk}`
|
|
1642
|
+
} else {
|
|
1643
|
+
const [x1, y1, z1, x2, y2, z2, block] = strs
|
|
1644
|
+
cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`
|
|
1645
|
+
}
|
|
1320
1646
|
break
|
|
1321
1647
|
}
|
|
1322
1648
|
case 'say': cmd = `say ${strs[0] ?? ''}`; break
|
|
@@ -1355,6 +1681,15 @@ function formatBuiltinCall(
|
|
|
1355
1681
|
}
|
|
1356
1682
|
|
|
1357
1683
|
/** Convert an HIR expression to its MC command string representation */
|
|
1684
|
+
/** Convert a CoordComponent to a MC coordinate string */
|
|
1685
|
+
function coordStr(c: import('../ast/types').CoordComponent): string {
|
|
1686
|
+
switch (c.kind) {
|
|
1687
|
+
case 'absolute': return String(c.value)
|
|
1688
|
+
case 'relative': return c.offset === 0 ? '~' : `~${c.offset}`
|
|
1689
|
+
case 'local': return c.offset === 0 ? '^' : `^${c.offset}`
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1358
1693
|
function exprToCommandArg(
|
|
1359
1694
|
expr: HIRExpr,
|
|
1360
1695
|
macroParams: Set<string>,
|
|
@@ -1401,3 +1736,14 @@ function exprToCommandArg(
|
|
|
1401
1736
|
return { str: '~', isMacro: false }
|
|
1402
1737
|
}
|
|
1403
1738
|
}
|
|
1739
|
+
|
|
1740
|
+
/** Extract a string literal from a HIR expression for use in MC commands */
|
|
1741
|
+
function hirExprToStringLiteral(expr: HIRExpr): string {
|
|
1742
|
+
switch (expr.kind) {
|
|
1743
|
+
case 'str_lit': return expr.value
|
|
1744
|
+
case 'mc_name': return expr.value
|
|
1745
|
+
case 'selector': return expr.raw
|
|
1746
|
+
case 'int_lit': return String(expr.value)
|
|
1747
|
+
default: return ''
|
|
1748
|
+
}
|
|
1749
|
+
}
|
package/src/mir/types.ts
CHANGED
|
@@ -80,6 +80,10 @@ export type MIRInstr = MIRInstrBase & (
|
|
|
80
80
|
| { kind: 'nbt_read'; dst: Temp; ns: string; path: string; scale: number }
|
|
81
81
|
| { kind: 'nbt_write'; ns: string; path: string; type: NBTType; scale: number; src: Operand }
|
|
82
82
|
|
|
83
|
+
// ── Vanilla scoreboard interop ────────────────────────────────────────────
|
|
84
|
+
| { kind: 'score_read'; dst: Temp; player: string; obj: string }
|
|
85
|
+
| { kind: 'score_write'; player: string; obj: string; src: Operand }
|
|
86
|
+
|
|
83
87
|
// ── Function calls ────────────────────────────────────────────────────────
|
|
84
88
|
| { kind: 'call'; dst: Temp | null; fn: string; args: Operand[] }
|
|
85
89
|
| { kind: 'call_macro'; dst: Temp | null; fn: string; args: { name: string; value: Operand; type: NBTType; scale: number }[] }
|
|
@@ -85,6 +85,8 @@ function rewriteUses(instr: MIRInstr, copies: Map<Temp, Operand>): MIRInstr {
|
|
|
85
85
|
return { ...instr, cond: resolve(instr.cond, copies) }
|
|
86
86
|
case 'return':
|
|
87
87
|
return { ...instr, value: instr.value ? resolve(instr.value, copies) : null }
|
|
88
|
+
case 'score_write':
|
|
89
|
+
return { ...instr, src: resolve(instr.src, copies) }
|
|
88
90
|
default:
|
|
89
91
|
return instr
|
|
90
92
|
}
|
|
@@ -100,6 +102,8 @@ function getDst(instr: MIRInstr): Temp | null {
|
|
|
100
102
|
return instr.dst
|
|
101
103
|
case 'call': case 'call_macro':
|
|
102
104
|
return instr.dst
|
|
105
|
+
case 'score_read':
|
|
106
|
+
return instr.dst
|
|
103
107
|
default:
|
|
104
108
|
return null
|
|
105
109
|
}
|