redscript-mc 2.1.1 → 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 +50 -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__/mc-integration.test.js +25 -13
- package/dist/src/__tests__/schedule.test.js +105 -0
- package/dist/src/__tests__/typechecker.test.js +63 -0
- package/dist/src/emit/compile.js +1 -0
- package/dist/src/emit/index.js +3 -1
- package/dist/src/lir/lower.js +26 -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/examples/coroutine-demo.mcrs +11 -10
- 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__/mc-integration.test.ts +25 -13
- package/src/__tests__/schedule.test.ts +112 -0
- package/src/__tests__/typechecker.test.ts +68 -0
- package/src/emit/compile.ts +1 -0
- package/src/emit/index.ts +3 -1
- package/src/lir/lower.ts +27 -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
|
@@ -246,6 +246,69 @@ fn test() {
|
|
|
246
246
|
expect(errors.length).toBeGreaterThan(0);
|
|
247
247
|
expect(errors[0].message).toContain('Return type mismatch: expected void, got int');
|
|
248
248
|
});
|
|
249
|
+
it('rejects setTimeout inside a loop', () => {
|
|
250
|
+
const errors = typeCheck(`
|
|
251
|
+
fn test() {
|
|
252
|
+
while (true) {
|
|
253
|
+
setTimeout(20, () => { say("x"); });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
`);
|
|
257
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
258
|
+
expect(errors[0].message).toContain('cannot be called inside a loop');
|
|
259
|
+
});
|
|
260
|
+
it('rejects setTimeout inside an if body', () => {
|
|
261
|
+
const errors = typeCheck(`
|
|
262
|
+
fn test() {
|
|
263
|
+
if (true) {
|
|
264
|
+
setTimeout(20, () => { say("x"); });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
`);
|
|
268
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
269
|
+
expect(errors[0].message).toContain('cannot be called inside an if/else body');
|
|
270
|
+
});
|
|
271
|
+
it('rejects setInterval inside a loop', () => {
|
|
272
|
+
const errors = typeCheck(`
|
|
273
|
+
fn test() {
|
|
274
|
+
while (true) {
|
|
275
|
+
setInterval(20, () => { say("x"); });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
`);
|
|
279
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
280
|
+
expect(errors[0].message).toContain('cannot be called inside a loop');
|
|
281
|
+
});
|
|
282
|
+
it('rejects Timer::new() inside a loop', () => {
|
|
283
|
+
const errors = typeCheck(`
|
|
284
|
+
struct Timer { _id: int, _duration: int }
|
|
285
|
+
impl Timer {
|
|
286
|
+
fn new(duration: int) -> Timer { return { _id: 0, _duration: duration }; }
|
|
287
|
+
}
|
|
288
|
+
fn test() {
|
|
289
|
+
while (true) {
|
|
290
|
+
let t: Timer = Timer::new(10);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
`);
|
|
294
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
295
|
+
expect(errors[0].message).toContain('Timer::new() cannot be called inside a loop');
|
|
296
|
+
});
|
|
297
|
+
it('rejects Timer::new() inside an if body', () => {
|
|
298
|
+
const errors = typeCheck(`
|
|
299
|
+
struct Timer { _id: int, _duration: int }
|
|
300
|
+
impl Timer {
|
|
301
|
+
fn new(duration: int) -> Timer { return { _id: 0, _duration: duration }; }
|
|
302
|
+
}
|
|
303
|
+
fn test() {
|
|
304
|
+
if (true) {
|
|
305
|
+
let t: Timer = Timer::new(10);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
`);
|
|
309
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
310
|
+
expect(errors[0].message).toContain('Timer::new() cannot be called inside an if/else body');
|
|
311
|
+
});
|
|
249
312
|
it('allows impl instance methods with inferred self type', () => {
|
|
250
313
|
const errors = typeCheck(`
|
|
251
314
|
struct Timer { duration: int }
|
package/dist/src/emit/compile.js
CHANGED
|
@@ -116,6 +116,7 @@ function compile(source, options = {}) {
|
|
|
116
116
|
const coroResult = (0, coroutine_1.coroutineTransform)(mirOpt, coroutineInfos);
|
|
117
117
|
const mirFinal = coroResult.module;
|
|
118
118
|
tickFunctions.push(...coroResult.generatedTickFunctions);
|
|
119
|
+
warnings.push(...coroResult.warnings);
|
|
119
120
|
// Stage 5: MIR → LIR
|
|
120
121
|
const lir = (0, lower_3.lowerToLIR)(mirFinal);
|
|
121
122
|
// Stage 6: LIR optimization
|
package/dist/src/emit/index.js
CHANGED
|
@@ -152,7 +152,9 @@ function emitInstr(instr, ns, obj, mcVersion) {
|
|
|
152
152
|
return `execute unless score ${slot(instr.a)} ${cmpToMC(instr.op)} ${slot(instr.b)} run function ${instr.fn}`;
|
|
153
153
|
case 'call_context': {
|
|
154
154
|
const subcmds = instr.subcommands.map(emitSubcmd).join(' ');
|
|
155
|
-
return
|
|
155
|
+
return subcmds
|
|
156
|
+
? `execute ${subcmds} run function ${instr.fn}`
|
|
157
|
+
: `function ${instr.fn}`;
|
|
156
158
|
}
|
|
157
159
|
case 'return_value':
|
|
158
160
|
return `scoreboard players operation $ret ${instr.slot.obj} = ${slot(instr.slot)}`;
|
package/dist/src/lir/lower.js
CHANGED
|
@@ -275,6 +275,32 @@ function lowerInstrInner(instr, fn, ctx, instrs) {
|
|
|
275
275
|
});
|
|
276
276
|
break;
|
|
277
277
|
}
|
|
278
|
+
case 'score_read': {
|
|
279
|
+
// execute store result score $dst __obj run scoreboard players get <player> <obj>
|
|
280
|
+
const dst = ctx.slot(instr.dst);
|
|
281
|
+
instrs.push({
|
|
282
|
+
kind: 'store_cmd_to_score',
|
|
283
|
+
dst,
|
|
284
|
+
cmd: { kind: 'raw', cmd: `scoreboard players get ${instr.player} ${instr.obj}` },
|
|
285
|
+
});
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case 'score_write': {
|
|
289
|
+
// Write a value to a vanilla MC scoreboard objective
|
|
290
|
+
if (instr.src.kind === 'const') {
|
|
291
|
+
instrs.push({ kind: 'raw', cmd: `scoreboard players set ${instr.player} ${instr.obj} ${instr.src.value}` });
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// execute store result score <player> <obj> run scoreboard players get $src __ns
|
|
295
|
+
const srcSlot = operandToSlot(instr.src, ctx, instrs);
|
|
296
|
+
instrs.push({
|
|
297
|
+
kind: 'store_cmd_to_score',
|
|
298
|
+
dst: { player: instr.player, obj: instr.obj },
|
|
299
|
+
cmd: { kind: 'raw', cmd: `scoreboard players get ${srcSlot.player} ${srcSlot.obj}` },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
278
304
|
case 'call': {
|
|
279
305
|
// Set parameter slots $p0, $p1, ...
|
|
280
306
|
for (let i = 0; i < instr.args.length; i++) {
|
package/dist/src/mir/lower.js
CHANGED
|
@@ -48,15 +48,16 @@ function lowerToMIR(hir, sourceFile) {
|
|
|
48
48
|
fnParamInfo.set(`${ib.typeName}::${m.name}`, m.params);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
const timerCounter = { count: 0, timerId: 0 };
|
|
51
52
|
const allFunctions = [];
|
|
52
53
|
for (const f of hir.functions) {
|
|
53
|
-
const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile);
|
|
54
|
+
const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter);
|
|
54
55
|
allFunctions.push(fn, ...helpers);
|
|
55
56
|
}
|
|
56
57
|
// Lower impl block methods
|
|
57
58
|
for (const ib of hir.implBlocks) {
|
|
58
59
|
for (const m of ib.methods) {
|
|
59
|
-
const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile);
|
|
60
|
+
const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter);
|
|
60
61
|
allFunctions.push(fn, ...helpers);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -70,7 +71,7 @@ function lowerToMIR(hir, sourceFile) {
|
|
|
70
71
|
// Function lowering context
|
|
71
72
|
// ---------------------------------------------------------------------------
|
|
72
73
|
class FnContext {
|
|
73
|
-
constructor(namespace, fnName, structDefs = new Map(), implMethods = new Map(), macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map()) {
|
|
74
|
+
constructor(namespace, fnName, structDefs = new Map(), implMethods = new Map(), macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), timerCounter = { count: 0, timerId: 0 }) {
|
|
74
75
|
this.tempCounter = 0;
|
|
75
76
|
this.blockCounter = 0;
|
|
76
77
|
this.blocks = [];
|
|
@@ -82,10 +83,14 @@ class FnContext {
|
|
|
82
83
|
this.structVars = new Map();
|
|
83
84
|
/** Tuple variable tracking: varName → array of element temps (index = slot) */
|
|
84
85
|
this.tupleVars = new Map();
|
|
86
|
+
/** Array variable tracking: varName → { ns, pathPrefix } for NBT-backed int[] */
|
|
87
|
+
this.arrayVars = new Map();
|
|
85
88
|
/** Current source location (set during statement lowering) */
|
|
86
89
|
this.currentSourceLoc = undefined;
|
|
87
90
|
/** Source file path for the module being compiled */
|
|
88
91
|
this.sourceFile = undefined;
|
|
92
|
+
/** Tracks temps whose values are known compile-time constants (for Timer static ID propagation) */
|
|
93
|
+
this.constTemps = new Map();
|
|
89
94
|
this.namespace = namespace;
|
|
90
95
|
this.fnName = fnName;
|
|
91
96
|
this.structDefs = structDefs;
|
|
@@ -94,6 +99,7 @@ class FnContext {
|
|
|
94
99
|
this.fnParamInfo = fnParamInfo;
|
|
95
100
|
this.currentMacroParams = macroInfo.get(fnName)?.macroParams ?? new Set();
|
|
96
101
|
this.enumDefs = enumDefs;
|
|
102
|
+
this.timerCounter = timerCounter;
|
|
97
103
|
const entry = this.makeBlock('entry');
|
|
98
104
|
this.currentBlock = entry;
|
|
99
105
|
}
|
|
@@ -150,8 +156,8 @@ class FnContext {
|
|
|
150
156
|
// ---------------------------------------------------------------------------
|
|
151
157
|
// Function lowering
|
|
152
158
|
// ---------------------------------------------------------------------------
|
|
153
|
-
function lowerFunction(fn, namespace, structDefs = new Map(), implMethods = new Map(), macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), sourceFile) {
|
|
154
|
-
const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs);
|
|
159
|
+
function lowerFunction(fn, namespace, structDefs = new Map(), implMethods = new Map(), macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), sourceFile, timerCounter = { count: 0, timerId: 0 }) {
|
|
160
|
+
const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter);
|
|
155
161
|
ctx.sourceFile = sourceFile;
|
|
156
162
|
const fnMacroInfo = macroInfo.get(fn.name);
|
|
157
163
|
// Create temps for parameters
|
|
@@ -184,9 +190,9 @@ function lowerFunction(fn, namespace, structDefs = new Map(), implMethods = new
|
|
|
184
190
|
};
|
|
185
191
|
return { fn: result, helpers: ctx.helperFunctions };
|
|
186
192
|
}
|
|
187
|
-
function lowerImplMethod(method, typeName, namespace, structDefs, implMethods, macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), sourceFile) {
|
|
193
|
+
function lowerImplMethod(method, typeName, namespace, structDefs, implMethods, macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), sourceFile, timerCounter = { count: 0, timerId: 0 }) {
|
|
188
194
|
const fnName = `${typeName}::${method.name}`;
|
|
189
|
-
const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs);
|
|
195
|
+
const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter);
|
|
190
196
|
ctx.sourceFile = sourceFile;
|
|
191
197
|
const fields = structDefs.get(typeName) ?? [];
|
|
192
198
|
const hasSelf = method.params.length > 0 && method.params[0].name === 'self';
|
|
@@ -345,6 +351,12 @@ function lowerStmt(stmt, ctx, scope) {
|
|
|
345
351
|
const t = ctx.freshTemp();
|
|
346
352
|
ctx.emit({ kind: 'copy', dst: t, src: { kind: 'temp', name: `__rf_${fieldName}` } });
|
|
347
353
|
fieldTemps.set(fieldName, t);
|
|
354
|
+
// Propagate compile-time constants from return slots (e.g. Timer._id from Timer::new)
|
|
355
|
+
const rfSlot = `__rf_${fieldName}`;
|
|
356
|
+
const constVal = ctx.constTemps.get(rfSlot);
|
|
357
|
+
if (constVal !== undefined) {
|
|
358
|
+
ctx.constTemps.set(t, constVal);
|
|
359
|
+
}
|
|
348
360
|
}
|
|
349
361
|
ctx.structVars.set(stmt.name, { typeName: stmt.type.name, fields: fieldTemps });
|
|
350
362
|
}
|
|
@@ -355,6 +367,35 @@ function lowerStmt(stmt, ctx, scope) {
|
|
|
355
367
|
scope.set(stmt.name, t);
|
|
356
368
|
}
|
|
357
369
|
}
|
|
370
|
+
else if (stmt.init.kind === 'array_lit') {
|
|
371
|
+
// Array literal: write to NBT storage, track the var for index access
|
|
372
|
+
const ns = `${ctx.getNamespace()}:arrays`;
|
|
373
|
+
const pathPrefix = stmt.name;
|
|
374
|
+
ctx.arrayVars.set(stmt.name, { ns, pathPrefix });
|
|
375
|
+
const elems = stmt.init.elements;
|
|
376
|
+
// Check if all elements are pure integer literals (no side-effects)
|
|
377
|
+
const allConst = elems.every(e => e.kind === 'int_lit');
|
|
378
|
+
if (allConst) {
|
|
379
|
+
// Emit a single raw 'data modify ... set value [...]' to initialize the whole list
|
|
380
|
+
const vals = elems.map(e => e.value).join(', ');
|
|
381
|
+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${vals}]`, args: [] });
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
// Initialize with zeros, then overwrite dynamic elements
|
|
385
|
+
const zeros = elems.map(() => '0').join(', ');
|
|
386
|
+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${zeros}]`, args: [] });
|
|
387
|
+
for (let i = 0; i < elems.length; i++) {
|
|
388
|
+
const elemOp = lowerExpr(elems[i], ctx, scope);
|
|
389
|
+
if (elemOp.kind !== 'const' || (elems[i].kind !== 'int_lit')) {
|
|
390
|
+
ctx.emit({ kind: 'nbt_write', ns, path: `${pathPrefix}[${i}]`, type: 'int', scale: 1, src: elemOp });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Store array length as a temp in scope (for .len access)
|
|
395
|
+
const lenTemp = ctx.freshTemp();
|
|
396
|
+
ctx.emit({ kind: 'const', dst: lenTemp, value: elems.length });
|
|
397
|
+
scope.set(stmt.name, lenTemp);
|
|
398
|
+
}
|
|
358
399
|
else {
|
|
359
400
|
const valOp = lowerExpr(stmt.init, ctx, scope);
|
|
360
401
|
const t = ctx.freshTemp();
|
|
@@ -616,6 +657,41 @@ function lowerStmt(stmt, ctx, scope) {
|
|
|
616
657
|
ctx.terminate({ kind: 'jump', target: mergeBlock.id });
|
|
617
658
|
}
|
|
618
659
|
}
|
|
660
|
+
else if (arm.pattern.kind === 'range_lit') {
|
|
661
|
+
// Range pattern: e.g. 0..59 => emit ge/le comparisons
|
|
662
|
+
const range = arm.pattern.range;
|
|
663
|
+
const armBody = ctx.newBlock('match_arm');
|
|
664
|
+
const nextArm = ctx.newBlock('match_next');
|
|
665
|
+
// Chain checks: if min defined, check matchVal >= min; if max defined, check matchVal <= max
|
|
666
|
+
// Each failed check jumps to nextArm
|
|
667
|
+
const checks = [];
|
|
668
|
+
if (range.min !== undefined)
|
|
669
|
+
checks.push({ op: 'ge', bound: range.min });
|
|
670
|
+
if (range.max !== undefined)
|
|
671
|
+
checks.push({ op: 'le', bound: range.max });
|
|
672
|
+
if (checks.length === 0) {
|
|
673
|
+
// Open range — always matches
|
|
674
|
+
ctx.terminate({ kind: 'jump', target: armBody.id });
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
// Emit checks sequentially; each check passes → continue to next or armBody
|
|
678
|
+
for (let ci = 0; ci < checks.length; ci++) {
|
|
679
|
+
const { op, bound } = checks[ci];
|
|
680
|
+
const cmpTemp = ctx.freshTemp();
|
|
681
|
+
ctx.emit({ kind: 'cmp', dst: cmpTemp, op, a: matchVal, b: { kind: 'const', value: bound } });
|
|
682
|
+
const passBlock = ci === checks.length - 1 ? armBody : ctx.newBlock('match_range_check');
|
|
683
|
+
ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: cmpTemp }, then: passBlock.id, else: nextArm.id });
|
|
684
|
+
if (ci < checks.length - 1)
|
|
685
|
+
ctx.switchTo(passBlock);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
ctx.switchTo(armBody);
|
|
689
|
+
lowerBlock(arm.body, ctx, new Map(scope));
|
|
690
|
+
if (isPlaceholderTerm(ctx.current().term)) {
|
|
691
|
+
ctx.terminate({ kind: 'jump', target: mergeBlock.id });
|
|
692
|
+
}
|
|
693
|
+
ctx.switchTo(nextArm);
|
|
694
|
+
}
|
|
619
695
|
else {
|
|
620
696
|
const patOp = lowerExpr(arm.pattern, ctx, scope);
|
|
621
697
|
const cmpTemp = ctx.freshTemp();
|
|
@@ -853,13 +929,97 @@ function lowerExpr(expr, ctx, scope) {
|
|
|
853
929
|
return { kind: 'temp', name: t };
|
|
854
930
|
}
|
|
855
931
|
case 'index': {
|
|
932
|
+
// Check if obj is a tracked array variable with a constant index
|
|
933
|
+
if (expr.obj.kind === 'ident') {
|
|
934
|
+
const arrInfo = ctx.arrayVars.get(expr.obj.name);
|
|
935
|
+
if (arrInfo && expr.index.kind === 'int_lit') {
|
|
936
|
+
const t = ctx.freshTemp();
|
|
937
|
+
ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, scale: 1 });
|
|
938
|
+
return { kind: 'temp', name: t };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
856
941
|
const obj = lowerExpr(expr.obj, ctx, scope);
|
|
857
|
-
|
|
942
|
+
lowerExpr(expr.index, ctx, scope);
|
|
858
943
|
const t = ctx.freshTemp();
|
|
859
944
|
ctx.emit({ kind: 'copy', dst: t, src: obj });
|
|
860
945
|
return { kind: 'temp', name: t };
|
|
861
946
|
}
|
|
862
947
|
case 'call': {
|
|
948
|
+
// Handle scoreboard_get / score — read from vanilla MC scoreboard
|
|
949
|
+
if (expr.fn === 'scoreboard_get' || expr.fn === 'score') {
|
|
950
|
+
const player = hirExprToStringLiteral(expr.args[0]);
|
|
951
|
+
const obj = hirExprToStringLiteral(expr.args[1]);
|
|
952
|
+
const t = ctx.freshTemp();
|
|
953
|
+
ctx.emit({ kind: 'score_read', dst: t, player, obj });
|
|
954
|
+
return { kind: 'temp', name: t };
|
|
955
|
+
}
|
|
956
|
+
// Handle scoreboard_set — write to vanilla MC scoreboard
|
|
957
|
+
if (expr.fn === 'scoreboard_set') {
|
|
958
|
+
const player = hirExprToStringLiteral(expr.args[0]);
|
|
959
|
+
const obj = hirExprToStringLiteral(expr.args[1]);
|
|
960
|
+
const src = lowerExpr(expr.args[2], ctx, scope);
|
|
961
|
+
ctx.emit({ kind: 'score_write', player, obj, src });
|
|
962
|
+
const t = ctx.freshTemp();
|
|
963
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
964
|
+
return { kind: 'temp', name: t };
|
|
965
|
+
}
|
|
966
|
+
// Handle setTimeout/setInterval: lift lambda arg to a named helper function
|
|
967
|
+
if ((expr.fn === 'setTimeout' || expr.fn === 'setInterval') && expr.args.length === 2) {
|
|
968
|
+
const ticksArg = expr.args[0];
|
|
969
|
+
const callbackArg = expr.args[1];
|
|
970
|
+
const ns = ctx.getNamespace();
|
|
971
|
+
const id = ctx.timerCounter.count++;
|
|
972
|
+
const callbackName = `__timeout_callback_${id}`;
|
|
973
|
+
// Extract ticks value for the schedule command
|
|
974
|
+
let ticksLiteral = null;
|
|
975
|
+
if (ticksArg.kind === 'int_lit') {
|
|
976
|
+
ticksLiteral = ticksArg.value;
|
|
977
|
+
}
|
|
978
|
+
// Build the callback MIRFunction from the lambda body
|
|
979
|
+
if (callbackArg.kind === 'lambda') {
|
|
980
|
+
const cbCtx = new FnContext(ns, callbackName, ctx.structDefs, ctx.implMethods, ctx.macroInfo, ctx.fnParamInfo, ctx.enumDefs, ctx.timerCounter);
|
|
981
|
+
cbCtx.sourceFile = ctx.sourceFile;
|
|
982
|
+
const cbBody = Array.isArray(callbackArg.body) ? callbackArg.body : [{ kind: 'expr', expr: callbackArg.body }];
|
|
983
|
+
// For setInterval: reschedule at end of body
|
|
984
|
+
const bodyStmts = [...cbBody];
|
|
985
|
+
if (expr.fn === 'setInterval' && ticksLiteral !== null) {
|
|
986
|
+
// Append: raw `schedule function ns:callbackName ticksT`
|
|
987
|
+
bodyStmts.push({
|
|
988
|
+
kind: 'raw',
|
|
989
|
+
cmd: `schedule function ${ns}:${callbackName} ${ticksLiteral}t`,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
lowerBlock(bodyStmts, cbCtx, new Map());
|
|
993
|
+
const cbCur = cbCtx.current();
|
|
994
|
+
if (isPlaceholderTerm(cbCur.term)) {
|
|
995
|
+
cbCtx.terminate({ kind: 'return', value: null });
|
|
996
|
+
}
|
|
997
|
+
const cbReachable = computeReachable(cbCtx.blocks, 'entry');
|
|
998
|
+
const cbLiveBlocks = cbCtx.blocks.filter(b => cbReachable.has(b.id));
|
|
999
|
+
computePreds(cbLiveBlocks);
|
|
1000
|
+
const cbFn = {
|
|
1001
|
+
name: callbackName,
|
|
1002
|
+
params: [],
|
|
1003
|
+
blocks: cbLiveBlocks,
|
|
1004
|
+
entry: 'entry',
|
|
1005
|
+
isMacro: false,
|
|
1006
|
+
};
|
|
1007
|
+
ctx.helperFunctions.push(cbFn, ...cbCtx.helperFunctions);
|
|
1008
|
+
}
|
|
1009
|
+
// Emit: schedule function ns:callbackName ticksT
|
|
1010
|
+
if (ticksLiteral !== null) {
|
|
1011
|
+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:schedule function ${ns}:${callbackName} ${ticksLiteral}t`, args: [] });
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
// Dynamic ticks: lower ticks operand and emit a raw schedule (best-effort)
|
|
1015
|
+
const ticksOp = lowerExpr(ticksArg, ctx, scope);
|
|
1016
|
+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:schedule function ${ns}:${callbackName} 1t`, args: [ticksOp] });
|
|
1017
|
+
}
|
|
1018
|
+
// setTimeout returns void (0), setInterval returns an int ID (0 for now)
|
|
1019
|
+
const t = ctx.freshTemp();
|
|
1020
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
1021
|
+
return { kind: 'temp', name: t };
|
|
1022
|
+
}
|
|
863
1023
|
// Handle builtin calls → raw MC commands
|
|
864
1024
|
if (macro_1.BUILTIN_SET.has(expr.fn)) {
|
|
865
1025
|
const cmd = formatBuiltinCall(expr.fn, expr.args, ctx.currentMacroParams);
|
|
@@ -872,6 +1032,14 @@ function lowerExpr(expr, ctx, scope) {
|
|
|
872
1032
|
if (expr.args.length > 0 && expr.args[0].kind === 'ident') {
|
|
873
1033
|
const sv = ctx.structVars.get(expr.args[0].name);
|
|
874
1034
|
if (sv) {
|
|
1035
|
+
// Intercept Timer method calls when _id is a known compile-time constant
|
|
1036
|
+
if (sv.typeName === 'Timer') {
|
|
1037
|
+
const idTemp = sv.fields.get('_id');
|
|
1038
|
+
const timerId = idTemp !== undefined ? ctx.constTemps.get(idTemp) : undefined;
|
|
1039
|
+
if (timerId !== undefined) {
|
|
1040
|
+
return lowerTimerMethod(expr.fn, timerId, sv, ctx, scope, expr.args.slice(1));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
875
1043
|
const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.fn);
|
|
876
1044
|
if (methodInfo?.hasSelf) {
|
|
877
1045
|
// Build args: self fields first, then remaining explicit args
|
|
@@ -921,6 +1089,14 @@ function lowerExpr(expr, ctx, scope) {
|
|
|
921
1089
|
if (expr.callee.kind === 'member' && expr.callee.obj.kind === 'ident') {
|
|
922
1090
|
const sv = ctx.structVars.get(expr.callee.obj.name);
|
|
923
1091
|
if (sv) {
|
|
1092
|
+
// Intercept Timer method calls when _id is a known compile-time constant
|
|
1093
|
+
if (sv.typeName === 'Timer') {
|
|
1094
|
+
const idTemp = sv.fields.get('_id');
|
|
1095
|
+
const timerId = idTemp !== undefined ? ctx.constTemps.get(idTemp) : undefined;
|
|
1096
|
+
if (timerId !== undefined) {
|
|
1097
|
+
return lowerTimerMethod(expr.callee.field, timerId, sv, ctx, scope, expr.args);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
924
1100
|
const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.callee.field);
|
|
925
1101
|
if (methodInfo?.hasSelf) {
|
|
926
1102
|
// Build args: self fields first, then explicit args
|
|
@@ -945,6 +1121,24 @@ function lowerExpr(expr, ctx, scope) {
|
|
|
945
1121
|
return { kind: 'temp', name: t };
|
|
946
1122
|
}
|
|
947
1123
|
case 'static_call': {
|
|
1124
|
+
// Intercept Timer::new() to statically allocate a unique ID
|
|
1125
|
+
if (expr.type === 'Timer' && expr.method === 'new' && expr.args.length === 1) {
|
|
1126
|
+
const id = ctx.timerCounter.timerId++;
|
|
1127
|
+
const ns = ctx.getNamespace();
|
|
1128
|
+
const playerName = `__timer_${id}`;
|
|
1129
|
+
// Emit scoreboard initialization: ticks=0, active=0
|
|
1130
|
+
ctx.emit({ kind: 'score_write', player: `${playerName}_ticks`, obj: ns, src: { kind: 'const', value: 0 } });
|
|
1131
|
+
ctx.emit({ kind: 'score_write', player: `${playerName}_active`, obj: ns, src: { kind: 'const', value: 0 } });
|
|
1132
|
+
// Lower the duration argument
|
|
1133
|
+
const durationOp = lowerExpr(expr.args[0], ctx, scope);
|
|
1134
|
+
// Return fields via __rf_ slots (Timer has fields: _id, _duration)
|
|
1135
|
+
ctx.emit({ kind: 'const', dst: '__rf__id', value: id });
|
|
1136
|
+
ctx.constTemps.set('__rf__id', id);
|
|
1137
|
+
ctx.emit({ kind: 'copy', dst: '__rf__duration', src: durationOp });
|
|
1138
|
+
const t = ctx.freshTemp();
|
|
1139
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
1140
|
+
return { kind: 'temp', name: t };
|
|
1141
|
+
}
|
|
948
1142
|
const args = expr.args.map(a => lowerExpr(a, ctx, scope));
|
|
949
1143
|
const t = ctx.freshTemp();
|
|
950
1144
|
ctx.emit({ kind: 'call', dst: t, fn: `${expr.type}::${expr.method}`, args });
|
|
@@ -1024,6 +1218,98 @@ function lowerShortCircuitOr(expr, ctx, scope) {
|
|
|
1024
1218
|
return { kind: 'temp', name: result };
|
|
1025
1219
|
}
|
|
1026
1220
|
// ---------------------------------------------------------------------------
|
|
1221
|
+
// Timer method inlining
|
|
1222
|
+
// ---------------------------------------------------------------------------
|
|
1223
|
+
/**
|
|
1224
|
+
* Inline a Timer instance method call using the statically-assigned timer ID.
|
|
1225
|
+
* Emits scoreboard operations directly, bypassing the Timer::* function calls.
|
|
1226
|
+
*/
|
|
1227
|
+
function lowerTimerMethod(method, timerId, sv, ctx, scope, extraArgs) {
|
|
1228
|
+
const ns = ctx.getNamespace();
|
|
1229
|
+
const player = `__timer_${timerId}`;
|
|
1230
|
+
const t = ctx.freshTemp();
|
|
1231
|
+
if (method === 'start') {
|
|
1232
|
+
ctx.emit({ kind: 'score_write', player: `${player}_active`, obj: ns, src: { kind: 'const', value: 1 } });
|
|
1233
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
1234
|
+
}
|
|
1235
|
+
else if (method === 'pause') {
|
|
1236
|
+
ctx.emit({ kind: 'score_write', player: `${player}_active`, obj: ns, src: { kind: 'const', value: 0 } });
|
|
1237
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
1238
|
+
}
|
|
1239
|
+
else if (method === 'reset') {
|
|
1240
|
+
ctx.emit({ kind: 'score_write', player: `${player}_ticks`, obj: ns, src: { kind: 'const', value: 0 } });
|
|
1241
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
1242
|
+
}
|
|
1243
|
+
else if (method === 'tick') {
|
|
1244
|
+
const durationTemp = sv.fields.get('_duration');
|
|
1245
|
+
const activeTemp = ctx.freshTemp();
|
|
1246
|
+
const ticksTemp = ctx.freshTemp();
|
|
1247
|
+
ctx.emit({ kind: 'score_read', dst: activeTemp, player: `${player}_active`, obj: ns });
|
|
1248
|
+
ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns });
|
|
1249
|
+
const innerThen = ctx.newBlock('timer_tick_inner');
|
|
1250
|
+
const innerMerge = ctx.newBlock('timer_tick_after_lt');
|
|
1251
|
+
const outerMerge = ctx.newBlock('timer_tick_done');
|
|
1252
|
+
const activeCheck = ctx.freshTemp();
|
|
1253
|
+
ctx.emit({ kind: 'cmp', op: 'eq', dst: activeCheck, a: { kind: 'temp', name: activeTemp }, b: { kind: 'const', value: 1 } });
|
|
1254
|
+
ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: activeCheck }, then: innerThen.id, else: outerMerge.id });
|
|
1255
|
+
ctx.switchTo(innerThen);
|
|
1256
|
+
const lessCheck = ctx.freshTemp();
|
|
1257
|
+
if (durationTemp) {
|
|
1258
|
+
ctx.emit({ kind: 'cmp', op: 'lt', dst: lessCheck, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'temp', name: durationTemp } });
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
ctx.emit({ kind: 'const', dst: lessCheck, value: 0 });
|
|
1262
|
+
}
|
|
1263
|
+
const doIncBlock = ctx.newBlock('timer_tick_inc');
|
|
1264
|
+
ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: lessCheck }, then: doIncBlock.id, else: innerMerge.id });
|
|
1265
|
+
ctx.switchTo(doIncBlock);
|
|
1266
|
+
const newTicks = ctx.freshTemp();
|
|
1267
|
+
ctx.emit({ kind: 'add', dst: newTicks, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'const', value: 1 } });
|
|
1268
|
+
ctx.emit({ kind: 'score_write', player: `${player}_ticks`, obj: ns, src: { kind: 'temp', name: newTicks } });
|
|
1269
|
+
ctx.terminate({ kind: 'jump', target: innerMerge.id });
|
|
1270
|
+
ctx.switchTo(innerMerge);
|
|
1271
|
+
ctx.terminate({ kind: 'jump', target: outerMerge.id });
|
|
1272
|
+
ctx.switchTo(outerMerge);
|
|
1273
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
1274
|
+
}
|
|
1275
|
+
else if (method === 'done') {
|
|
1276
|
+
const durationTemp = sv.fields.get('_duration');
|
|
1277
|
+
const ticksTemp = ctx.freshTemp();
|
|
1278
|
+
ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns });
|
|
1279
|
+
if (durationTemp) {
|
|
1280
|
+
ctx.emit({ kind: 'cmp', op: 'ge', dst: t, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'temp', name: durationTemp } });
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
else if (method === 'elapsed') {
|
|
1287
|
+
ctx.emit({ kind: 'score_read', dst: t, player: `${player}_ticks`, obj: ns });
|
|
1288
|
+
}
|
|
1289
|
+
else if (method === 'remaining') {
|
|
1290
|
+
const durationTemp = sv.fields.get('_duration');
|
|
1291
|
+
const ticksTemp = ctx.freshTemp();
|
|
1292
|
+
ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns });
|
|
1293
|
+
if (durationTemp) {
|
|
1294
|
+
ctx.emit({ kind: 'sub', dst: t, a: { kind: 'temp', name: durationTemp }, b: { kind: 'temp', name: ticksTemp } });
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
ctx.emit({ kind: 'const', dst: t, value: 0 });
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
else {
|
|
1301
|
+
// Unknown Timer method — emit regular call
|
|
1302
|
+
const fields = ['_id', '_duration'];
|
|
1303
|
+
const selfArgs = fields.map(f => {
|
|
1304
|
+
const temp = sv.fields.get(f);
|
|
1305
|
+
return temp ? { kind: 'temp', name: temp } : { kind: 'const', value: 0 };
|
|
1306
|
+
});
|
|
1307
|
+
const explicitArgs = extraArgs.map(a => lowerExpr(a, ctx, scope));
|
|
1308
|
+
ctx.emit({ kind: 'call', dst: t, fn: `Timer::${method}`, args: [...selfArgs, ...explicitArgs] });
|
|
1309
|
+
}
|
|
1310
|
+
return { kind: 'temp', name: t };
|
|
1311
|
+
}
|
|
1312
|
+
// ---------------------------------------------------------------------------
|
|
1027
1313
|
// Execute subcommand lowering
|
|
1028
1314
|
// ---------------------------------------------------------------------------
|
|
1029
1315
|
function lowerExecuteSubcmd(sub) {
|
|
@@ -1128,13 +1414,38 @@ function formatBuiltinCall(fn, args, macroParams) {
|
|
|
1128
1414
|
break;
|
|
1129
1415
|
}
|
|
1130
1416
|
case 'setblock': {
|
|
1131
|
-
|
|
1132
|
-
|
|
1417
|
+
// args: blockpos, block — expand blockpos to x y z
|
|
1418
|
+
const [posOrX, blockOrY] = args;
|
|
1419
|
+
if (posOrX?.kind === 'blockpos') {
|
|
1420
|
+
const px = coordStr(posOrX.x);
|
|
1421
|
+
const py = coordStr(posOrX.y);
|
|
1422
|
+
const pz = coordStr(posOrX.z);
|
|
1423
|
+
const blk = exprToCommandArg(blockOrY, macroParams).str;
|
|
1424
|
+
cmd = `setblock ${px} ${py} ${pz} ${blk}`;
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
const [x, y, z, block] = strs;
|
|
1428
|
+
cmd = `setblock ${x} ${y} ${z} ${block}`;
|
|
1429
|
+
}
|
|
1133
1430
|
break;
|
|
1134
1431
|
}
|
|
1135
1432
|
case 'fill': {
|
|
1136
|
-
|
|
1137
|
-
|
|
1433
|
+
// args: blockpos1, blockpos2, block — expand both blockpos
|
|
1434
|
+
const [p1, p2, blkArg] = args;
|
|
1435
|
+
if (p1?.kind === 'blockpos' && p2?.kind === 'blockpos') {
|
|
1436
|
+
const x1 = coordStr(p1.x);
|
|
1437
|
+
const y1 = coordStr(p1.y);
|
|
1438
|
+
const z1 = coordStr(p1.z);
|
|
1439
|
+
const x2 = coordStr(p2.x);
|
|
1440
|
+
const y2 = coordStr(p2.y);
|
|
1441
|
+
const z2 = coordStr(p2.z);
|
|
1442
|
+
const blk = exprToCommandArg(blkArg, macroParams).str;
|
|
1443
|
+
cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${blk}`;
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
const [x1, y1, z1, x2, y2, z2, block] = strs;
|
|
1447
|
+
cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`;
|
|
1448
|
+
}
|
|
1138
1449
|
break;
|
|
1139
1450
|
}
|
|
1140
1451
|
case 'say':
|
|
@@ -1217,6 +1528,14 @@ function formatBuiltinCall(fn, args, macroParams) {
|
|
|
1217
1528
|
return hasMacro ? `${MACRO_SENTINEL}${cmd}` : cmd;
|
|
1218
1529
|
}
|
|
1219
1530
|
/** Convert an HIR expression to its MC command string representation */
|
|
1531
|
+
/** Convert a CoordComponent to a MC coordinate string */
|
|
1532
|
+
function coordStr(c) {
|
|
1533
|
+
switch (c.kind) {
|
|
1534
|
+
case 'absolute': return String(c.value);
|
|
1535
|
+
case 'relative': return c.offset === 0 ? '~' : `~${c.offset}`;
|
|
1536
|
+
case 'local': return c.offset === 0 ? '^' : `^${c.offset}`;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1220
1539
|
function exprToCommandArg(expr, macroParams) {
|
|
1221
1540
|
switch (expr.kind) {
|
|
1222
1541
|
case 'int_lit': return { str: String(expr.value), isMacro: false };
|
|
@@ -1261,4 +1580,14 @@ function exprToCommandArg(expr, macroParams) {
|
|
|
1261
1580
|
return { str: '~', isMacro: false };
|
|
1262
1581
|
}
|
|
1263
1582
|
}
|
|
1583
|
+
/** Extract a string literal from a HIR expression for use in MC commands */
|
|
1584
|
+
function hirExprToStringLiteral(expr) {
|
|
1585
|
+
switch (expr.kind) {
|
|
1586
|
+
case 'str_lit': return expr.value;
|
|
1587
|
+
case 'mc_name': return expr.value;
|
|
1588
|
+
case 'selector': return expr.raw;
|
|
1589
|
+
case 'int_lit': return String(expr.value);
|
|
1590
|
+
default: return '';
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1264
1593
|
//# sourceMappingURL=lower.js.map
|
package/dist/src/mir/types.d.ts
CHANGED
|
@@ -137,6 +137,16 @@ export type MIRInstr = MIRInstrBase & ({
|
|
|
137
137
|
type: NBTType;
|
|
138
138
|
scale: number;
|
|
139
139
|
src: Operand;
|
|
140
|
+
} | {
|
|
141
|
+
kind: 'score_read';
|
|
142
|
+
dst: Temp;
|
|
143
|
+
player: string;
|
|
144
|
+
obj: string;
|
|
145
|
+
} | {
|
|
146
|
+
kind: 'score_write';
|
|
147
|
+
player: string;
|
|
148
|
+
obj: string;
|
|
149
|
+
src: Operand;
|
|
140
150
|
} | {
|
|
141
151
|
kind: 'call';
|
|
142
152
|
dst: Temp | null;
|
|
@@ -83,6 +83,8 @@ function rewriteUses(instr, copies) {
|
|
|
83
83
|
return { ...instr, cond: resolve(instr.cond, copies) };
|
|
84
84
|
case 'return':
|
|
85
85
|
return { ...instr, value: instr.value ? resolve(instr.value, copies) : null };
|
|
86
|
+
case 'score_write':
|
|
87
|
+
return { ...instr, src: resolve(instr.src, copies) };
|
|
86
88
|
default:
|
|
87
89
|
return instr;
|
|
88
90
|
}
|
|
@@ -106,6 +108,8 @@ function getDst(instr) {
|
|
|
106
108
|
case 'call':
|
|
107
109
|
case 'call_macro':
|
|
108
110
|
return instr.dst;
|
|
111
|
+
case 'score_read':
|
|
112
|
+
return instr.dst;
|
|
109
113
|
default:
|
|
110
114
|
return null;
|
|
111
115
|
}
|
|
@@ -25,6 +25,8 @@ export interface CoroutineResult {
|
|
|
25
25
|
module: MIRModule;
|
|
26
26
|
/** Names of generated @tick dispatcher functions (caller must add to tick list). */
|
|
27
27
|
generatedTickFunctions: string[];
|
|
28
|
+
/** Warning messages for skipped transforms. */
|
|
29
|
+
warnings: string[];
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Apply the coroutine transform to all functions in `infos`.
|