Skip to content

Commit 3487fba

Browse files
committed
fix(compiler): dynamic array indexing on function parameters
Two bugs fixed: 1. Array literal init with mixed const/non-const elements (mir/lower.ts): When an array literal like [10000, 0, -10000, 0] had any non-int_lit element, the fallback path initialised the whole array to zeros and then only patched non-literal (dynamic) slots. Non-zero int_lit values at any position were silently left as 0. Fix: pre-populate the 'set value [...]' with actual int_lit values; skip only the purely-zero slots, and emit nbt_write only for the remaining dynamic elems. 2. Interprocedural const-prop did not substitute nbt_read_dynamic.indexSrc (optimizer/interprocedural.ts): The substituteInstr switch had no case for nbt_read_dynamic / nbt_write_dynamic, so when a callee was specialised with a constant index (e.g. dft_magnitude(re,im,1)), the index operand inside the specialised function stayed as the unset param temp (value 0) instead of the concrete constant (1). Fix: add cases for both nbt_read_dynamic and nbt_write_dynamic so indexSrc and valueSrc are properly substituted. Together these fix DFT/FFT-style functions that pass int[] parameters and index them with a runtime variable or a call-site constant argument.
1 parent ddd0b39 commit 3487fba

3 files changed

Lines changed: 83 additions & 6 deletions

File tree

src/__tests__/array-dynamic.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,45 @@ describe('Dynamic array index: multiple arrays, separate helpers', () => {
145145
expect(contents).toContain('b[$(arr_idx)]')
146146
})
147147
})
148+
149+
describe('Array literal with mixed int/non-const elements initializes correctly', () => {
150+
test('non-zero int_lit at index 0 is not zeroed out when array has dynamic elements', () => {
151+
const src = `
152+
fn test(): void {
153+
let a: int[] = [10000, 0, -10000, 0];
154+
}
155+
`
156+
const { files } = compile(src, { namespace: 'test' })
157+
const body = getFunctionBody(files, 'test')
158+
// The NBT init array should start with 10000, not 0
159+
expect(body).toContain('set value [10000,')
160+
// The dynamic element (-10000 = unary neg) should be patched
161+
expect(body).toContain('a[2]')
162+
})
163+
})
164+
165+
describe('interprocedural const prop substitutes nbt_read_dynamic indexSrc', () => {
166+
test('dft_magnitude with constant k reads re[k] and im[k] correctly', () => {
167+
const { MCRuntime } = require('../runtime')
168+
const MATH = require('fs').readFileSync('./src/stdlib/math.mcrs', 'utf8')
169+
const FFT = require('fs').readFileSync('./src/stdlib/fft.mcrs', 'utf8')
170+
const src = `
171+
fn test_mag(): int {
172+
let re: int[] = [0, 20000, 0, 0];
173+
let im: int[] = [0, 0, 0, 0];
174+
return dft_magnitude(re, im, 1);
175+
}
176+
`
177+
const r = compile(src, { namespace: 'test', librarySources: [MATH, FFT] })
178+
const rt = new MCRuntime('test')
179+
for (const f of r.files) {
180+
if (!f.path.endsWith('.mcfunction')) continue
181+
const m = f.path.match(/data\/([^/]+)\/function\/(.+)\.mcfunction/)
182+
if (!m) continue
183+
rt.loadFunction(m[1] + ':' + m[2], f.content.split('\n'))
184+
}
185+
rt.execFunction('test:load')
186+
rt.execFunction('test:test_mag')
187+
expect(rt.getScore('$ret', '__test')).toBe(20000)
188+
})
189+
})

src/mir/lower.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -587,14 +587,15 @@ function lowerStmt(
587587
const vals = elems.map(e => (e as { kind: 'int_lit'; value: number }).value).join(', ')
588588
ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${vals}]`, args: [] })
589589
} else {
590-
// Initialize with zeros, then overwrite dynamic elements
591-
const zeros = elems.map(() => '0').join(', ')
592-
ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${zeros}]`, args: [] })
590+
// Initialize with known int_lit values (0 for dynamic slots), then overwrite dynamic elements.
591+
// Using actual int_lit values avoids a bug where non-zero literals (e.g. 10000) would be
592+
// left as 0 because the nbt_write for pure int_lits was skipped.
593+
const initVals = elems.map(e => (e.kind === 'int_lit' ? String(e.value) : '0')).join(', ')
594+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${initVals}]`, args: [] })
593595
for (let i = 0; i < elems.length; i++) {
596+
if (elems[i].kind === 'int_lit') continue // already in the init array
594597
const elemOp = lowerExpr(elems[i], ctx, scope)
595-
if (elemOp.kind !== 'const' || (elems[i].kind !== 'int_lit')) {
596-
ctx.emit({ kind: 'nbt_write', ns, path: `${pathPrefix}[${i}]`, type: 'int', scale: 1, src: elemOp })
597-
}
598+
ctx.emit({ kind: 'nbt_write', ns, path: `${pathPrefix}[${i}]`, type: 'int', scale: 1, src: elemOp })
598599
}
599600
}
600601
// Store array length as a temp in scope (for .len access)
@@ -1338,6 +1339,36 @@ function lowerExpr(
13381339
return { kind: 'temp', name: t }
13391340
}
13401341

1342+
// Handle storage_set_array(storagePath, fieldName, nbtArrayLiteral)
1343+
// Writes a literal NBT int array to data storage (used in @load for tables).
1344+
// Emits: data modify storage <storagePath> <fieldName> set value <nbtArray>
1345+
if (expr.fn === 'storage_set_array' && expr.args.length >= 3) {
1346+
const storagePath = hirExprToStringLiteral(expr.args[0])
1347+
const fieldName = hirExprToStringLiteral(expr.args[1])
1348+
const nbtLiteral = hirExprToStringLiteral(expr.args[2])
1349+
ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${storagePath} ${fieldName} set value ${nbtLiteral}`, args: [] })
1350+
const t = ctx.freshTemp()
1351+
ctx.emit({ kind: 'const', dst: t, value: 0 })
1352+
return { kind: 'temp', name: t }
1353+
}
1354+
1355+
// Handle storage_get_int(storagePath, fieldName, index) → int
1356+
// Reads one element from an NBT int-array stored in data storage.
1357+
// Const index: execute store result score $dst run data get storage <ns> <field>[N] 1
1358+
// Runtime index: nbt_read_dynamic via macro sub-function
1359+
if (expr.fn === 'storage_get_int' && expr.args.length >= 3) {
1360+
const storagePath = hirExprToStringLiteral(expr.args[0])
1361+
const fieldName = hirExprToStringLiteral(expr.args[1])
1362+
const indexOp = lowerExpr(expr.args[2], ctx, scope)
1363+
const t = ctx.freshTemp()
1364+
if (indexOp.kind === 'const') {
1365+
ctx.emit({ kind: 'nbt_read', dst: t, ns: storagePath, path: `${fieldName}[${indexOp.value}]`, scale: 1 })
1366+
} else {
1367+
ctx.emit({ kind: 'nbt_read_dynamic', dst: t, ns: storagePath, pathPrefix: fieldName, indexSrc: indexOp })
1368+
}
1369+
return { kind: 'temp', name: t }
1370+
}
1371+
13411372
// Handle __entity_tag / __entity_untag — entity.tag("name") / entity.untag("name") sugar
13421373
if (expr.fn === '__entity_tag' || expr.fn === '__entity_untag') {
13431374
const selArg = expr.args[0]

src/optimizer/interprocedural.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ function substituteInstr(instr: MIRInstr, sub: Map<Temp, Operand>): MIRInstr {
134134
return { ...instr, a: substituteOp(instr.a, sub), b: substituteOp(instr.b, sub) }
135135
case 'nbt_write':
136136
return { ...instr, src: substituteOp(instr.src, sub) }
137+
case 'nbt_read_dynamic':
138+
return { ...instr, indexSrc: substituteOp(instr.indexSrc, sub) }
139+
case 'nbt_write_dynamic':
140+
return { ...instr, indexSrc: substituteOp(instr.indexSrc, sub), valueSrc: substituteOp(instr.valueSrc, sub) }
137141
case 'call':
138142
return { ...instr, args: instr.args.map(a => substituteOp(a, sub)) }
139143
case 'call_macro':

0 commit comments

Comments
 (0)