diff --git a/modules/common/compiler-artifact/src/main/kotlin/ru/lazyhat/compukters/compiler/artifact/analysis/TemporaryRegisters.kt b/modules/common/compiler-artifact/src/main/kotlin/ru/lazyhat/compukters/compiler/artifact/analysis/TemporaryRegisters.kt index bc300932..4bfb28fc 100644 --- a/modules/common/compiler-artifact/src/main/kotlin/ru/lazyhat/compukters/compiler/artifact/analysis/TemporaryRegisters.kt +++ b/modules/common/compiler-artifact/src/main/kotlin/ru/lazyhat/compukters/compiler/artifact/analysis/TemporaryRegisters.kt @@ -18,6 +18,7 @@ package ru.lazyhat.compukters.compiler.artifact.analysis +import ru.lazyhat.compukters.compiler.artifact.model.DebugEntryId import ru.lazyhat.compukters.compiler.artifact.model.Destination import ru.lazyhat.compukters.compiler.artifact.model.FunctionFlag import ru.lazyhat.compukters.compiler.artifact.model.FunctionValue @@ -25,11 +26,19 @@ import ru.lazyhat.compukters.compiler.artifact.model.Instruction import ru.lazyhat.compukters.compiler.artifact.model.Module import ru.lazyhat.compukters.compiler.artifact.model.RegisterId -/** Reuses exact-type temporary registers; the runtime's ordinary layout remains authoritative. */ +/** Reuses exact-type temporaries and removes coalesced copies; the runtime's ordinary layout remains authoritative. */ object TemporaryRegisters { fun compact(module: Module): Module { val blocks = module.blocks.toMutableList() val exceptions = module.exceptions.toMutableList() + val instructionBoundaries = mutableMapOf() + // Inline parents must retain a distinct instruction position in the canonical debug table. + val debugAnchors = + module.debug + .mapNotNull { it.inlineParent } + .map { parent -> + module.debug[parent.value.toInt()].let { it.block.value.toInt() to it.instruction.toInt() } + }.toSet() val functions = module.functions.map { function -> if (FunctionFlag.ABSTRACT in function.flags || function.values.isEmpty()) return@map function @@ -55,9 +64,10 @@ object TemporaryRegisters { } boundaries.forEach { boundary -> val instruction = module.blocks[boundary.block.value.toInt()].instructions[boundary.instruction.toInt()] - // Keep inputs and results distinct even when inputs die at this instruction: calls, GC and - // suspension may retain the pre-instruction root map while materializing the destination. + // Except for a dying MOVE source, keep inputs and results distinct: calls, GC and suspension + // may retain the pre-instruction root map while materializing the destination. val simultaneous = boundary.before + boundary.after + instruction.writtenRegisters() + val dyingCopy = (instruction as? Instruction.Move)?.takeIf { it.source !in boundary.after } simultaneous .filter { it.value.toInt() !in pinned } .groupBy { function.values[it.value.toInt()] } @@ -65,6 +75,16 @@ object TemporaryRegisters { .forEach { group -> group.forEachIndexed { index, left -> for (right in group.drop(index + 1)) { + // MOVE only copies existing bits and cannot allocate, throw or suspend. Its + // dying source may share the destination unless another boundary conflicts. + if (dyingCopy != null && + ( + (left == dyingCopy.source && right == dyingCopy.destination) || + (right == dyingCopy.source && left == dyingCopy.destination) + ) + ) { + continue + } interference[left.value.toInt()] += right.value.toInt() interference[right.value.toInt()] += left.value.toInt() } @@ -96,15 +116,50 @@ object TemporaryRegisters { } val remap: (RegisterId) -> RegisterId = { RegisterId.of(mapping[it.value.toInt()].toUInt()) } blockRange.forEach { block -> + val original = module.blocks[block].instructions + val positions = IntArray(original.size + 1) + val instructions = mutableListOf() + original.forEachIndexed { index, instruction -> + positions[index] = instructions.size + val mapped = instruction.remapRegisters(remap) + if (mapped !is Instruction.Move || mapped.source != mapped.destination || block to index in debugAnchors) { + instructions += mapped + } + } + positions[original.size] = instructions.size + instructionBoundaries[block] = positions blocks[block] = - module.blocks[block].copy(instructions = module.blocks[block].instructions.map { it.remapRegisters(remap) }) + module.blocks[block].copy(instructions = instructions) } exceptionRange.forEach { index -> exceptions[index] = module.exceptions[index].copy(exceptionRegister = remap(module.exceptions[index].exceptionRegister)) } function.copy(values = values, safepointRoots = emptyList()) } - return ReferenceLiveness.derive(module.copy(functions = functions, blocks = blocks, exceptions = exceptions)) + val relocatedDebug = + module.debug.map { entry -> + instructionBoundaries[entry.block.value.toInt()]?.let { boundaries -> + entry.copy(instruction = boundaries[entry.instruction.toInt()].toUInt()) + } ?: entry + } + // A removed instruction's source position applies until the next entry; the last entry at a + // collapsed boundary wins. Keep record IDs dense and relocate the surviving inline parents. + val retainedDebug = + relocatedDebug.indices.filter { index -> + val next = relocatedDebug.getOrNull(index + 1) + val entry = relocatedDebug[index] + next == null || entry.function != next.function || entry.block != next.block || entry.instruction != next.instruction + } + val debugIds = IntArray(relocatedDebug.size) { -1 } + retainedDebug.forEachIndexed { new, old -> debugIds[old] = new } + val debug = + retainedDebug.map { index -> + val entry = relocatedDebug[index] + entry.copy( + inlineParent = entry.inlineParent?.let { parent -> DebugEntryId.of(debugIds[parent.value.toInt()].toUInt()) }, + ) + } + return ReferenceLiveness.derive(module.copy(functions = functions, blocks = blocks, exceptions = exceptions, debug = debug)) } } diff --git a/modules/common/compiler-artifact/src/test/kotlin/ru/lazyhat/compukters/compiler/artifact/analysis/TemporaryRegistersTest.kt b/modules/common/compiler-artifact/src/test/kotlin/ru/lazyhat/compukters/compiler/artifact/analysis/TemporaryRegistersTest.kt index 01269af8..3385ddcf 100644 --- a/modules/common/compiler-artifact/src/test/kotlin/ru/lazyhat/compukters/compiler/artifact/analysis/TemporaryRegistersTest.kt +++ b/modules/common/compiler-artifact/src/test/kotlin/ru/lazyhat/compukters/compiler/artifact/analysis/TemporaryRegistersTest.kt @@ -21,6 +21,8 @@ package ru.lazyhat.compukters.compiler.artifact.analysis import ru.lazyhat.compukters.compiler.artifact.model.Block import ru.lazyhat.compukters.compiler.artifact.model.BlockId import ru.lazyhat.compukters.compiler.artifact.model.ConstantId +import ru.lazyhat.compukters.compiler.artifact.model.DebugEntry +import ru.lazyhat.compukters.compiler.artifact.model.DebugEntryId import ru.lazyhat.compukters.compiler.artifact.model.Destination import ru.lazyhat.compukters.compiler.artifact.model.ExceptionEntry import ru.lazyhat.compukters.compiler.artifact.model.Function @@ -43,10 +45,11 @@ import ru.lazyhat.compukters.compiler.artifact.model.ValueType import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals +import kotlin.test.assertTrue class TemporaryRegistersTest { @Test - fun `reuses sequential temporaries without aliasing an instruction input and result`() { + fun `eliminates a dying source copy while keeping arithmetic inputs and result distinct`() { val source = module( List(4) { scalar() }, @@ -68,14 +71,165 @@ class TemporaryRegistersTest { .single() .values.size, ) - assertEquals((instructions[0] as Instruction.Const).destination, (instructions[2] as Instruction.Const).destination) - val add = instructions[3] as Instruction.Add + assertEquals(4, instructions.size) + val add = instructions[2] as Instruction.Add + assertEquals((instructions[0] as Instruction.Const).destination, add.left) + assertEquals((instructions[1] as Instruction.Const).destination, add.right) assertNotEquals(add.left, add.right) assertNotEquals(add.destination, add.left) assertNotEquals(add.destination, add.right) assertEquals(compact, TemporaryRegisters.compact(compact)) } + @Test + fun `retains a copy when its source is overwritten before the copied value is read`() { + val source = + module( + List(3) { scalar() }, + listOf( + listOf( + Instruction.Const(reg(0), ConstantId.of(0u)), + Instruction.Move(reg(1), reg(0)), + Instruction.Const(reg(0), ConstantId.of(1u)), + Instruction.Add(reg(2), reg(0), reg(1)), + Instruction.Return(Destination.Register(reg(2))), + ), + ), + ) + val compact = TemporaryRegisters.compact(source) + val move = compact.blocks.single().instructions[1] as Instruction.Move + val add = compact.blocks.single().instructions[3] as Instruction.Add + assertNotEquals(move.source, move.destination) + assertNotEquals(add.left, add.right) + } + + @Test + fun `relocates debug boundaries and preserves inline parents when copies disappear`() { + val base = + module( + List(2) { scalar() }, + listOf( + listOf( + Instruction.Const(reg(0), ConstantId.of(0u)), + Instruction.Move(reg(1), reg(0)), + Instruction.Call(Destination.Unit, FunctionRef.Local(FunctionId.of(0u)), listOf(reg(1))), + Instruction.Return(Destination.Register(reg(1))), + ), + ), + ) + val source = + base.copy( + debug = + (0u..3u).map { index -> + DebugEntry( + FunctionId.of(0u), + block(0), + index, + index, + index + 1u, + when (index) { + 0u -> null + 3u -> DebugEntryId.of(2u) + else -> DebugEntryId.of(0u) + }, + MetadataText.of("test.kt"), + index + 1u, + 1u, + ) + }, + ) + val compact = TemporaryRegisters.compact(source) + assertEquals(listOf(0u, 1u, 2u), compact.debug.map { it.instruction }) + assertEquals(listOf(null, DebugEntryId.of(0u), DebugEntryId.of(1u)), compact.debug.map { it.inlineParent }) + assertEquals(listOf(1u, 3u, 4u), compact.debug.map { it.sourceLine }) + assertEquals(compact, TemporaryRegisters.compact(compact)) + + val anchored = + source.copy( + debug = + source.debug.mapIndexed { index, entry -> + when (index) { + 3 -> entry.copy(inlineParent = DebugEntryId.of(1u)) + else -> entry + } + }, + ) + val anchoredCompact = TemporaryRegisters.compact(anchored) + assertEquals( + 4, + anchoredCompact.blocks + .single() + .instructions.size, + ) + assertEquals(anchored.debug, anchoredCompact.debug) + } + + @Test + fun `coalesces multi component copies and rebuilds reference roots across suspension`() { + val inline = + FunctionValue(ValueType.Inline(TypeRef.Local(TypeId.of(0u))), PhysicalShape(listOf(PhysicalAtom.I32, PhysicalAtom.REF32))) + val source = + module( + listOf(inline, inline, scalar()), + listOf( + listOf( + Instruction.Const(reg(0), ConstantId.of(0u)), + Instruction.Move(reg(1), reg(0)), + Instruction.CallSuspend( + Destination.Register(reg(2)), + FunctionRef.Local(FunctionId.of(0u)), + listOf(reg(1)), + block(1), + ), + ), + listOf(Instruction.Return(Destination.Register(reg(1)))), + ), + ) + val compact = TemporaryRegisters.compact(source) + assertTrue(compact.blocks.flatMap { it.instructions }.none { it is Instruction.Move }) + val value = (compact.blocks[1].instructions.single() as Instruction.Return).value as Destination.Register + val call = compact.blocks[0].instructions.last() as Instruction.CallSuspend + assertEquals(listOf(value.id), call.arguments) + assertEquals( + listOf(ValueComponent(value.id, 1u)), + compact.functions + .single() + .safepointRoots + .single { it.block == block(0) && it.instructionBoundary == 1u } + .references, + ) + assertEquals(compact, TemporaryRegisters.compact(compact)) + } + + @Test + fun `retains a source needed only by an exception handler after a copy`() { + val base = + module( + List(3) { scalar() }, + listOf( + listOf( + Instruction.Const(reg(0), ConstantId.of(0u)), + Instruction.Move(reg(1), reg(0)), + Instruction.Call(Destination.Unit, FunctionRef.Local(FunctionId.of(0u)), emptyList()), + Instruction.Return(Destination.Register(reg(1))), + ), + listOf(Instruction.Return(Destination.Register(reg(0)))), + ), + ) + val source = + base.copy( + functions = base.functions.map { it.copy(exceptionCount = 1u) }, + exceptions = listOf(ExceptionEntry(FunctionId.of(0u), block(0), 1u, null, block(1), reg(2))), + ) + val compact = TemporaryRegisters.compact(source) + val move = compact.blocks[0].instructions[1] as Instruction.Move + assertNotEquals(move.source, move.destination) + assertEquals( + move.source, + (compact.blocks[1].instructions.single() as Instruction.Return).value.let { (it as Destination.Register).id }, + ) + } + @Test fun `keeps parameters in their original dedicated slots`() { val source =