Files
prog8/codeGenNew6502/test/TestRegfileSection.kt
2026-09-11 20:20:40 +02:00

206 lines
9.1 KiB
Kotlin

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.*
import prog8.code.target.Cx16Target
import prog8.codegen.new6502.AsmGen
import prog8.intermediate.*
import prog8tests.helpers.ErrorReporterForTests
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.readText
/**
* Regression tests for the BSS section / regfile label placement in the 6502 code generator.
*
* The bug being guarded against: a label emitted IMMEDIATELY BEFORE a `.section` directive
* in 64tass does NOT resolve to the start of the section data - it resolves to the current
* "phantom" address in the main flow. The data that actually gets placed into the section
* starts at a different address (after whatever other content is in the section).
*
* In the variable-size regfile layout, the `p8_regfile:` label was emitted before the
* `.section BSS_NOCLEAR` directive, while dirty variables in the SAME section preceded the
* `.fill` data. This caused `p8_regfile+0` to alias the first dirty variable
* (`_orig_irqvec` LSB), corrupting the IRQ vector at runtime.
*
* The fix: the regfile label and `.fill` data must be inside the same `.section` block
* as (and after) the dirty variables.
*/
class TestRegfileSection : FunSpec({
val tempRoot = tempdir().toPath()
fun buildTestProgram(
vars: List<IRStStaticVariable>,
instructions: List<IRInstruction>
): Pair<IRProgram, ICompilationTarget> {
val target = Cx16Target()
val options = CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.FLOATSAFE)
.floats(false)
.compilerVersion("test")
.memtopAddress(0xffffu)
.build()
val st = IRSymbolTable()
for (v in vars) {
st.add(v)
}
val program = IRProgram("test", st, options, DummyStringEncoder)
if (instructions.isNotEmpty()) {
val chunk = IRCodeChunk(null, null)
chunk.instructions.addAll(instructions)
val sub = IRSubroutine("test.start", emptyList(), emptyList(), Position.DUMMY)
sub.chunks.add(chunk)
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
block.children.add(sub)
program.blocks.add(block)
}
return Pair(program, target)
}
fun makeDirtyVar(name: String, dt: DataType): IRStStaticVariable =
IRStStaticVariable(name, dt, null, null, ZeropageWish.DONTCARE, 0u, dirty = true, inBss = true)
fun generateAsm(outputDir: Path, program: IRProgram, target: ICompilationTarget): String {
val codegen = AsmGen(program, target, ErrorReporterForTests())
// generate() also runs 64tass; the .asm file is written regardless of
// whether the assembler succeeds, so we always read it back.
codegen.generate()
val asmFile = outputDir.resolve("${program.name}.asm")
check(asmFile.exists()) { "Assembly file not written: $asmFile" }
return asmFile.readText()
}
test("p8_regfile label is INSIDE the BSS_NOCLEAR section, not before it") {
val (program, target) = buildTestProgram(
vars = listOf(
makeDirtyVar("main.dirtyvar1", DataType.UBYTE),
makeDirtyVar("main.dirtyvar2", DataType.UWORD)
),
instructions = listOf(
// Use register r1, which lives at offset 0 in the variable-size
// regfile layout. If the label is misplaced this access overlaps
// the dirty vars that share the same BSS_NOCLEAR section.
IRInstructions.load(IRDataType.BYTE, 1, 0)
)
)
val outputDir = tempRoot.resolve("test-regfile-section-1")
outputDir.toFile().deleteRecursively()
outputDir.toFile().mkdirs()
program.options.outputDir = outputDir
val asm = generateAsm(outputDir, program, target)
// Find the line containing p8_regfile:
val lines = asm.lines()
val regfileLabelLine = lines.indexOfFirst { it.trim() == "p8_regfile:" }
check(regfileLabelLine >= 0) { "p8_regfile: label not found in generated assembly" }
// Walk backwards from the label: the most recent .section directive
// (any section, not just BSS_NOCLEAR) must appear BEFORE the label.
// We tolerate other lines like blank lines or comments between them.
val precedingSectionLine = (regfileLabelLine - 1 downTo 0)
.firstOrNull { lines[it].trim().matches(Regex("""\.section\s+\S+""")) }
check(precedingSectionLine != null) {
"p8_regfile: must be preceded by a .section directive (so the label resolves " +
"to the start of the regfile data, not the start of the BSS_NOCLEAR section)"
}
// Walk forward: there must be a .send directive matching the section,
// confirming the label is actually inside the section's content block.
val followingSendLine = (regfileLabelLine + 1 until lines.size)
.firstOrNull { lines[it].trim().matches(Regex("""\.send\s+\S+""")) }
check(followingSendLine != null) {
"p8_regfile: must be followed by a .send directive"
}
// And the section name on the .send line must match the section name
// on the preceding .section line, with no other .section in between.
val sectionName = lines[precedingSectionLine].trim().substringAfter(".section").trim()
val sendName = lines[followingSendLine].trim().substringAfter(".send").trim()
sectionName shouldBe sendName
// And the regfile data (.fill) must be between the label and the .send.
val hasFillBeforeSend = (regfileLabelLine + 1 until followingSendLine)
.any { lines[it].trim().matches(Regex("""\.fill\s+\d+""")) }
check(hasFillBeforeSend) {
"Expected a .fill directive for the regfile data between the p8_regfile: label " +
"and the .send directive"
}
}
test("no label is emitted immediately before a .section directive anywhere in the assembly") {
// This is the general invariant that, if violated, causes the same class of
// bug as the regfile issue. A label before a .section resolves to the
// "phantom" current address in the main flow, NOT to the address where
// the section's content will be placed.
//
// Allowed: anonymous forward/backward labels (`+`, `-`) - these are 64tass
// branch targets and behave correctly.
// Allowed: directives like `.proc`, `.block`, `.bend`, `.pend` which are
// scope markers, not section markers.
val (program, target) = buildTestProgram(
vars = listOf(
makeDirtyVar("main.dirtyvar1", DataType.UBYTE)
),
instructions = listOf(
IRInstructions.load(IRDataType.BYTE, 1, 0),
IRInstructions.load(IRDataType.WORD, 2, 0)
)
)
val outputDir = tempRoot.resolve("test-regfile-section-2")
outputDir.toFile().deleteRecursively()
outputDir.toFile().mkdirs()
program.options.outputDir = outputDir
val asm = generateAsm(outputDir, program, target)
val lines = asm.lines()
val labelPattern = Regex("""^([A-Za-z_][\w.]*):\s*$""")
val sectionPattern = Regex("""^\s*\.section\s+\S+""")
// Skip the regfile section since it specifically combines a label with
// .section content. We're checking the OTHER sections (BSS, BSS_SLABS,
// STRUCTINSTANCES) where a label-before-section would be a bug.
val offenders = mutableListOf<Pair<Int, String>>()
var inSectionDepth = 0
for ((i, line) in lines.withIndex()) {
val trimmed = line.trim()
if (sectionPattern.matches(trimmed)) {
inSectionDepth++
} else if (trimmed.matches(Regex("""^\s*\.send\s+\S+"""))) {
inSectionDepth--
} else if (inSectionDepth == 0) {
// We're outside any .section block. A label here is a real label.
if (labelPattern.matches(trimmed)) {
// Check if the next non-blank/non-comment line is a .section
val nextLine = (i + 1 until lines.size)
.firstOrNull { j ->
val t = lines[j].trim()
t.isNotEmpty() && !t.startsWith(";")
}
if (nextLine != null && sectionPattern.matches(lines[nextLine].trim())) {
offenders.add(i to trimmed)
}
}
}
}
if (offenders.isNotEmpty()) {
val details = offenders.joinToString("\n") { (i, l) -> " line ${i + 1}: $l" }
throw AssertionError(
"Found label(s) emitted immediately before a .section directive. " +
"This causes the label to resolve to the wrong address in 64tass:\n$details"
)
}
}
})