mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
153 lines
7.9 KiB
Kotlin
153 lines
7.9 KiB
Kotlin
import io.kotest.assertions.throwables.shouldThrow
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.matchers.collections.shouldNotBeEmpty
|
|
import io.kotest.matchers.shouldBe
|
|
import prog8.intermediate.*
|
|
|
|
|
|
class TestOpcodeSchema: FunSpec({
|
|
|
|
test("every opcode has at least one schema") {
|
|
Opcode.entries.forEach { opcode ->
|
|
OpcodeSchemas.schemasFor(opcode).shouldNotBeEmpty()
|
|
}
|
|
OpcodeSchemas.coveredOpcodes shouldBe Opcode.entries.toSet()
|
|
}
|
|
|
|
test("there is exactly one schema per opcode and type") {
|
|
OpcodeSchemas.all().groupBy { it.opcode to it.type }.values.all { it.size == 1 } shouldBe true
|
|
}
|
|
|
|
test("register-register arithmetic shape") {
|
|
OpcodeSchemas.get(Opcode.ADDR, IRDataType.WORD).slots shouldBe listOf(
|
|
RegisterSlotSchema(InstructionSlot.DEST, OperandRole.RESULT, OperandDirection.USE_DEF,
|
|
RegisterFile.INTEGER, TypeRule.InstructionType),
|
|
RegisterSlotSchema(InstructionSlot.SRC_A, OperandRole.RIGHT, OperandDirection.USE,
|
|
RegisterFile.INTEGER, TypeRule.InstructionType)
|
|
)
|
|
OpcodeSchemas.get(Opcode.ADDR, IRDataType.FLOAT).registerSlots.map { it.registerFile } shouldBe
|
|
listOf(RegisterFile.FLOAT, RegisterFile.FLOAT)
|
|
}
|
|
|
|
test("two-result division shape") {
|
|
OpcodeSchemas.get(Opcode.DIVMOD, IRDataType.WORD).slots.map { it.slot } shouldBe
|
|
listOf(InstructionSlot.DEST, InstructionSlot.DEST_B, InstructionSlot.IMMEDIATE)
|
|
OpcodeSchemas.get(Opcode.SDIVMOD, IRDataType.WORD).registerSlots.map { it.role } shouldBe
|
|
listOf(OperandRole.RESULT, OperandRole.SECONDARY_RESULT)
|
|
OpcodeSchemas.get(Opcode.DIVMODR, IRDataType.BYTE).slots.map { it.slot } shouldBe
|
|
listOf(InstructionSlot.DEST, InstructionSlot.DEST_B)
|
|
}
|
|
|
|
test("float indexed load has an integer index register") {
|
|
val schema = OpcodeSchemas.get(Opcode.LOADX, IRDataType.FLOAT)
|
|
schema.slots.map { it.slot } shouldBe listOf(InstructionSlot.DEST, InstructionSlot.MEMORY)
|
|
schema.registerSlot(InstructionSlot.DEST)!!.registerFile shouldBe RegisterFile.FLOAT
|
|
schema.memorySlot!!.kind shouldBe MemoryKind.INDEXED
|
|
schema.memoryEffect shouldBe MemoryEffect.READ
|
|
}
|
|
|
|
test("pointer dereference uses an indirect memory reference") {
|
|
val schema = OpcodeSchemas.get(Opcode.LOADI, IRDataType.BYTE)
|
|
schema.slots.map { it.slot } shouldBe listOf(InstructionSlot.DEST, InstructionSlot.MEMORY)
|
|
schema.memorySlot!!.kind shouldBe MemoryKind.INDIRECT
|
|
OpcodeSchemas.get(Opcode.STOREI, IRDataType.BYTE).memorySlot!!.kind shouldBe MemoryKind.INDIRECT
|
|
OpcodeSchemas.get(Opcode.STOREZI, IRDataType.BYTE).slots.map { it.slot } shouldBe listOf(InstructionSlot.MEMORY)
|
|
}
|
|
|
|
test("hardware register transfer uses a hardware slot") {
|
|
OpcodeSchemas.get(Opcode.LOADHR, IRDataType.WORD).slots.map { it.slot } shouldBe
|
|
listOf(InstructionSlot.DEST, InstructionSlot.HARDWARE_SLOT)
|
|
OpcodeSchemas.get(Opcode.STOREHR, IRDataType.WORD).slots.map { it.slot } shouldBe
|
|
listOf(InstructionSlot.SRC_A, InstructionSlot.HARDWARE_SLOT)
|
|
}
|
|
|
|
test("hardware slots must have the instruction type") {
|
|
shouldThrow<IllegalArgumentException> {
|
|
IRInstruction(
|
|
Opcode.LOADHR,
|
|
IRDataType.BYTE,
|
|
dest = IRInstructions.operandFor(Opcode.LOADHR, IRDataType.BYTE, InstructionSlot.DEST, 1),
|
|
hardwareSlot = HardwareSlotOperand(CallingConventionSlot(0), IRDataType.WORD)
|
|
)
|
|
}
|
|
}
|
|
|
|
test("concat combines two registers into a wider one") {
|
|
val schema = OpcodeSchemas.get(Opcode.CONCAT, IRDataType.BYTE)
|
|
schema.slots.map { it.slot } shouldBe listOf(InstructionSlot.DEST, InstructionSlot.SRC_A, InstructionSlot.SRC_B)
|
|
schema.registerSlot(InstructionSlot.DEST)!!.typeRule shouldBe TypeRule.Fixed(IRDataType.WORD)
|
|
OpcodeSchemas.get(Opcode.CONCAT, IRDataType.WORD).registerSlot(InstructionSlot.DEST)!!.typeRule shouldBe
|
|
TypeRule.Fixed(IRDataType.LONG)
|
|
}
|
|
|
|
test("branches and jumps have a code target") {
|
|
OpcodeSchemas.get(Opcode.JUMP, null).targetSlot!!.kind shouldBe TargetKind.STATIC
|
|
OpcodeSchemas.get(Opcode.JUMPI, null).targetSlot!!.kind shouldBe TargetKind.INDIRECT
|
|
OpcodeSchemas.get(Opcode.BSTEQ, null).slots.map { it.slot } shouldBe listOf(InstructionSlot.TARGET)
|
|
OpcodeSchemas.get(Opcode.BSTEQ, null).statusEffect shouldBe StatusEffect.READS
|
|
OpcodeSchemas.get(Opcode.BGT, IRDataType.BYTE).slots.map { it.slot } shouldBe
|
|
listOf(InstructionSlot.SRC_A, InstructionSlot.IMMEDIATE, InstructionSlot.TARGET)
|
|
OpcodeSchemas.get(Opcode.BGTR, IRDataType.BYTE).slots.map { it.slot } shouldBe
|
|
listOf(InstructionSlot.SRC_A, InstructionSlot.SRC_B, InstructionSlot.TARGET)
|
|
OpcodeSchemas.get(Opcode.JUMP, null).controlFlow shouldBe ControlFlowEffect.JUMP
|
|
}
|
|
|
|
test("returns") {
|
|
OpcodeSchemas.get(Opcode.RETURN, null).slots shouldBe emptyList()
|
|
OpcodeSchemas.get(Opcode.RETURNR, IRDataType.FLOAT).registerSlot(InstructionSlot.SRC_A)!!.registerFile shouldBe RegisterFile.FLOAT
|
|
OpcodeSchemas.get(Opcode.RETURNI, IRDataType.BYTE).slots.map { it.slot } shouldBe listOf(InstructionSlot.IMMEDIATE)
|
|
OpcodeSchemas.get(Opcode.RETURN, null).controlFlow shouldBe ControlFlowEffect.RETURN
|
|
}
|
|
|
|
test("all call opcodes only have a call site operand") {
|
|
val callKinds = mapOf(
|
|
Opcode.CALL to CallKind.NORMAL,
|
|
Opcode.CALLI to CallKind.INDIRECT,
|
|
Opcode.CALLFAR to CallKind.FAR,
|
|
Opcode.CALLFARVB to CallKind.FAR_VARBANK,
|
|
Opcode.SYSCALL to CallKind.SYSCALL
|
|
)
|
|
for((opcode, kind) in callKinds) {
|
|
val schema = OpcodeSchemas.get(opcode, null)
|
|
schema.slots.map { it.slot } shouldBe listOf(InstructionSlot.CALL_SITE)
|
|
schema.callSlot!!.kind shouldBe kind
|
|
schema.controlFlow shouldBe ControlFlowEffect.CALL
|
|
}
|
|
}
|
|
|
|
test("memory and status effects are declared") {
|
|
OpcodeSchemas.get(Opcode.LOADM, IRDataType.BYTE).memoryEffect shouldBe MemoryEffect.READ
|
|
OpcodeSchemas.get(Opcode.STOREM, IRDataType.BYTE).memoryEffect shouldBe MemoryEffect.WRITE
|
|
OpcodeSchemas.get(Opcode.INCM, IRDataType.BYTE).memoryEffect shouldBe MemoryEffect.READ_WRITE
|
|
OpcodeSchemas.get(Opcode.ADDR, IRDataType.BYTE).memoryEffect shouldBe MemoryEffect.NONE
|
|
OpcodeSchemas.get(Opcode.CMP, IRDataType.BYTE).statusEffect shouldBe StatusEffect.SETS
|
|
OpcodeSchemas.get(Opcode.ROXL, IRDataType.BYTE).statusEffect shouldBe StatusEffect.READS_AND_SETS
|
|
OpcodeSchemas.get(Opcode.LOADM, IRDataType.BYTE).statusEffect shouldBe StatusEffect.NONE
|
|
}
|
|
|
|
test("word schemas also cover the pointer type") {
|
|
OpcodeSchemas.typesFor(Opcode.LOADM) shouldBe setOf(
|
|
IRDataType.BYTE, IRDataType.WORD, IRDataType.POINTER, IRDataType.LONG, IRDataType.FLOAT
|
|
)
|
|
OpcodeSchemas.typesFor(Opcode.NOP) shouldBe setOf(null)
|
|
}
|
|
|
|
test("every schema is internally consistent") {
|
|
OpcodeSchemas.all().forEach { schema ->
|
|
// at most one operand per slot kind
|
|
schema.slots.groupBy { it.slot }.values.all { it.size == 1 } shouldBe true
|
|
// float registers only in float instructions
|
|
schema.registerSlots.forEach { slot ->
|
|
if(slot.registerFile == RegisterFile.FLOAT)
|
|
schema.type shouldBe IRDataType.FLOAT
|
|
}
|
|
// canonical print order: destinations come before sources
|
|
val slotOrder = schema.slots.map { it.slot }
|
|
val destIndex = slotOrder.indexOfFirst { it == InstructionSlot.DEST }
|
|
val srcIndex = slotOrder.indexOfFirst { it == InstructionSlot.SRC_A }
|
|
if(destIndex >= 0 && srcIndex >= 0)
|
|
(destIndex < srcIndex) shouldBe true
|
|
}
|
|
}
|
|
})
|