aboutsummaryrefslogtreecommitdiff
path: root/backend/unittests
diff options
context:
space:
mode:
authorRichard-Qin-X <richard.qin.001@gmail.com>2026-08-09 22:05:14 +0800
committerRichard-Qin-X <richard.qin.001@gmail.com>2026-08-09 22:05:14 +0800
commit3a57627e59ba18b6f9f9aae7bb60c390c0d3575a (patch)
tree736ca062bfffbd68177a512dfe145b06c16d11b1 /backend/unittests
parent379ff50dcc7ccf977379aee68fa9d10f69065d23 (diff)
downloadrcc-project-3a57627e59ba18b6f9f9aae7bb60c390c0d3575a.tar.gz
rcc-project-3a57627e59ba18b6f9f9aae7bb60c390c0d3575a.tar.xz
[MC] Implement RV64GC object emission
Diffstat (limited to 'backend/unittests')
-rw-r--r--backend/unittests/CodeGen/MachineOptimizationTest.cpp32
-rw-r--r--backend/unittests/MC/CMakeLists.txt15
-rw-r--r--backend/unittests/MC/MCObjectTest.cpp500
-rw-r--r--backend/unittests/Support/ErrorTest.cpp9
-rw-r--r--backend/unittests/Target/CMakeLists.txt30
-rw-r--r--backend/unittests/Target/DescriptionGeneratorTest.cpp2
-rw-r--r--backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir12
-rw-r--r--backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp411
-rw-r--r--backend/unittests/Target/RISCVMCTest.cpp264
-rw-r--r--backend/unittests/Target/RISCVObjectFileTest.cpp791
10 files changed, 2060 insertions, 6 deletions
diff --git a/backend/unittests/CodeGen/MachineOptimizationTest.cpp b/backend/unittests/CodeGen/MachineOptimizationTest.cpp
index 8d01a65..d4a39fa 100644
--- a/backend/unittests/CodeGen/MachineOptimizationTest.cpp
+++ b/backend/unittests/CodeGen/MachineOptimizationTest.cpp
@@ -300,6 +300,25 @@ void testDCEFixedPointAndDebugUses(TestContext &Context, const rcc::TargetDescri
if (Memory == nullptr)
return;
rcc::MachineInstr *Store = buildStore(Context, Builder, LiveValue, X2, *Memory);
+ rcc::MachineInstr *FrameSetup = buildADDI(Context, Builder, rcc::Register::fromPhysical(X2),
+ rcc::Register::fromPhysical(X2), -16);
+ rcc::MachineInstr *FrameDestroy = buildADDI(Context, Builder, rcc::Register::fromPhysical(X2),
+ rcc::Register::fromPhysical(X2), 16);
+ Context.expect(FrameSetup != nullptr && FrameDestroy != nullptr,
+ "DCE frame instruction construction must succeed");
+ if (FrameSetup == nullptr || FrameDestroy == nullptr)
+ return;
+ FrameSetup->setFlags({rcc::MachineInstrFlag::FrameSetup});
+ FrameDestroy->setFlags({rcc::MachineInstrFlag::FrameDestroy});
+ for (rcc::MachineInstr *Instruction : {FrameSetup, FrameDestroy}) {
+ rcc::MachineOperand *Definition = Instruction->operandAt(0);
+ if (Definition == nullptr)
+ return;
+ rcc::RegisterOperandFlags Flags = Definition->registerFlags();
+ Flags.set(rcc::RegisterOperandFlag::Dead);
+ Context.expect(Definition->setRegisterFlags(Flags).has_value(),
+ "DCE frame definition must accept a dead marker");
+ }
const std::size_t Before = Fixture.Entry->instructionCount();
rcc::MachineAnalysisManager Analyses;
@@ -312,6 +331,8 @@ void testDCEFixedPointAndDebugUses(TestContext &Context, const rcc::TargetDescri
Context.expect(Fixture.Entry->instructionCount() + 2 == Before,
"machine DCE must reach a fixed point across the dead chain");
Context.expect(Store != nullptr && Store->isLinked(), "machine DCE must preserve a store");
+ Context.expect(FrameSetup->isLinked() && FrameDestroy->isLinked(),
+ "machine DCE must preserve frame setup and destroy instructions");
Context.expect(!Fixture.Function->registerInfo().isKnownVirtualRegister(DeadRoot) &&
!Fixture.Function->registerInfo().isKnownVirtualRegister(DeadValue),
"machine DCE must retire removed virtual registers");
@@ -379,6 +400,10 @@ void testPostRAMachineCombinerIdentity(TestContext &Context, const rcc::TargetDe
const rcc::PhysicalRegister X10 = physical(Context, rcc::riscv::PhysicalRegisterID::X10);
rcc::MachineInstr *Identity = buildADDI(Context, Builder, rcc::Register::fromPhysical(X10),
rcc::Register::fromPhysical(X10), 0);
+ rcc::MachineInstr *FrameIdentity = buildADDI(Context, Builder, rcc::Register::fromPhysical(X10),
+ rcc::Register::fromPhysical(X10), 0);
+ if (FrameIdentity != nullptr)
+ FrameIdentity->setFlags({rcc::MachineInstrFlag::FrameDestroy});
rcc::MachineAnalysisManager Analyses;
rcc::MachineCombinerPass Pass(rcc::riscv::machineCombinerInfo(),
rcc::MachineCombinerStage::PostRegisterAllocation);
@@ -386,6 +411,8 @@ void testPostRAMachineCombinerIdentity(TestContext &Context, const rcc::TargetDe
Context.expect(
Result.has_value() && Result->Changed && Identity != nullptr && !Identity->isLinked(),
"post-RA combiner must erase a physical-register no-op only when identities match");
+ Context.expect(FrameIdentity != nullptr && FrameIdentity->isLinked(),
+ "post-frame semantics must protect an otherwise removable identity");
}
void testCSERegisterAndMemoryRules(TestContext &Context, const rcc::TargetDescription &Target) {
@@ -610,8 +637,7 @@ void testPostRACodeMotion(TestContext &Context, const rcc::TargetDescription &Ta
return;
const rcc::Register X0 =
rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X0));
- const rcc::Register X2 =
- rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X2));
+ const rcc::PhysicalRegister X2Physical = physical(Context, rcc::riscv::PhysicalRegisterID::X2);
const rcc::Register X10 =
rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X10));
const rcc::Register X12 =
@@ -634,7 +660,7 @@ void testPostRACodeMotion(TestContext &Context, const rcc::TargetDescription &Ta
makeMemory(Context, *LICMFixture.Function, true, false);
if (LICMMemory == nullptr)
return;
- (void)buildStore(Context, LICMBuilder, X12, *X2.physicalRegister(), *LICMMemory);
+ (void)buildStore(Context, LICMBuilder, X12, X2Physical, *LICMMemory);
setPostRAProperties(*LICMFixture.Function);
Context.expect(rcc::rebuildMachineLiveness(*LICMFixture.Function, Target).has_value(),
"post-RA LICM fixture liveness must be built");
diff --git a/backend/unittests/MC/CMakeLists.txt b/backend/unittests/MC/CMakeLists.txt
index d62bb1d..3c18b36 100644
--- a/backend/unittests/MC/CMakeLists.txt
+++ b/backend/unittests/MC/CMakeLists.txt
@@ -5,5 +5,16 @@ rcc_backend_configure_target(RCCBackendMCTests)
target_link_libraries(RCCBackendMCTests PRIVATE RCCBackend::MC)
add_test(NAME RCCBackend.MC.Core COMMAND RCCBackendMCTests)
-rcc_backend_register_format_files("${CMAKE_CURRENT_SOURCE_DIR}/MCTest.cpp")
-rcc_backend_register_tidy_files("${CMAKE_CURRENT_SOURCE_DIR}/MCTest.cpp")
+add_executable(RCCBackendMCObjectTests MCObjectTest.cpp)
+rcc_backend_configure_target(RCCBackendMCObjectTests)
+target_link_libraries(RCCBackendMCObjectTests PRIVATE RCCBackend::MC)
+add_test(NAME RCCBackend.MC.Object COMMAND RCCBackendMCObjectTests)
+
+rcc_backend_register_format_files(
+ "${CMAKE_CURRENT_SOURCE_DIR}/MCTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/MCObjectTest.cpp"
+)
+rcc_backend_register_tidy_files(
+ "${CMAKE_CURRENT_SOURCE_DIR}/MCTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/MCObjectTest.cpp"
+)
diff --git a/backend/unittests/MC/MCObjectTest.cpp b/backend/unittests/MC/MCObjectTest.cpp
new file mode 100644
index 0000000..f221e49
--- /dev/null
+++ b/backend/unittests/MC/MCObjectTest.cpp
@@ -0,0 +1,500 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/MC/ELFObjectWriter.h"
+#include "rcc/MC/MCAsmBackend.h"
+#include "rcc/MC/MCAssembler.h"
+#include "rcc/MC/MCCodeEmitter.h"
+#include "rcc/MC/MCContext.h"
+#include "rcc/MC/MCExpr.h"
+#include "rcc/MC/MCFixup.h"
+#include "rcc/MC/MCInst.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <limits>
+#include <span>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+class TestCodeEmitter final : public rcc::mc::MCCodeEmitter {
+public:
+ [[nodiscard]] rcc::Expected<rcc::mc::MCEncodedInstruction>
+ encodeInstruction(const rcc::mc::MCInst &Instruction) const override {
+ if (FailNextEncoding) {
+ FailNextEncoding = false;
+ return rcc::makeUnexpected(rcc::ErrorCode::InstructionEncodingFailed,
+ "intentional test encoding failure");
+ }
+ if (Instruction.opcode().value() != 1 || !Instruction.operands().empty()) {
+ return rcc::makeUnexpected(rcc::ErrorCode::InstructionEncodingFailed,
+ "unknown test instruction");
+ }
+ return rcc::mc::MCEncodedInstruction{.Bytes = {0x11, 0x22, 0x33, 0x44}, .Fixups = {}};
+ }
+
+ mutable bool FailNextEncoding = false;
+};
+
+class TestAsmBackend final : public rcc::mc::MCAsmBackend {
+public:
+ [[nodiscard]] rcc::Expected<rcc::mc::MCFixupKindInfo>
+ fixupKindInfo(rcc::mc::MCFixupKind Kind) const override {
+ switch (Kind) {
+ case rcc::mc::fixup_kind::Data1:
+ return rcc::mc::MCFixupKindInfo{.StorageSize = 1, .PCRelative = false};
+ case rcc::mc::fixup_kind::Data2:
+ return rcc::mc::MCFixupKindInfo{.StorageSize = 2, .PCRelative = false};
+ case rcc::mc::fixup_kind::Data4:
+ return rcc::mc::MCFixupKindInfo{.StorageSize = 4, .PCRelative = false};
+ case rcc::mc::fixup_kind::Data8:
+ return rcc::mc::MCFixupKindInfo{.StorageSize = 8, .PCRelative = false};
+ case rcc::mc::fixup_kind::PCRelative4:
+ return rcc::mc::MCFixupKindInfo{.StorageSize = 4, .PCRelative = true};
+ default:
+ return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, "unknown test fixup kind");
+ }
+ }
+
+ [[nodiscard]] rcc::Expected<void> applyFixup(rcc::mc::MCFixupKind Kind,
+ std::span<std::uint8_t> Data, std::uint64_t Offset,
+ std::int64_t Value) const override {
+ auto Info = fixupKindInfo(Kind);
+ if (!Info)
+ return std::unexpected(Info.error());
+ if (Offset > Data.size() ||
+ Info->StorageSize > Data.size() - static_cast<std::size_t>(Offset)) {
+ return rcc::makeUnexpected(rcc::ErrorCode::FixupApplicationFailed,
+ "test fixup exceeds its section");
+ }
+ const std::uint64_t Bits = static_cast<std::uint64_t>(Value);
+ for (std::uint8_t Index = 0; Index < Info->StorageSize; ++Index)
+ Data[static_cast<std::size_t>(Offset) + Index] =
+ static_cast<std::uint8_t>(Bits >> (Index * 8U));
+ return {};
+ }
+
+ [[nodiscard]] rcc::Expected<std::uint32_t>
+ relocationType(const rcc::mc::MCFixup &, const rcc::mc::MCValue &Value) const override {
+ if (!Value.AddSymbol || Value.SubtractSymbol)
+ return rcc::makeUnexpected(rcc::ErrorCode::RelocationFailed,
+ "test relocation requires one symbol");
+ return 77;
+ }
+
+ [[nodiscard]] std::uint32_t objectFlags() const noexcept override {
+ return 0x20;
+ }
+};
+
+[[nodiscard]] rcc::mc::MCInst testInstruction() {
+ return rcc::mc::MCInst(rcc::mc::MCOpcode::fromValue(1));
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef>
+symbolExpression(const rcc::mc::MCSymbolRef &Symbol) {
+ return rcc::mc::MCExpr::symbol(Symbol);
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef> addConstant(const rcc::mc::MCSymbolRef &Symbol,
+ std::int64_t Addend) {
+ auto Base = symbolExpression(Symbol);
+ if (!Base)
+ return std::unexpected(Base.error());
+ return rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::Add, *Base,
+ rcc::mc::MCExpr::constant(Addend));
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef> subtractSymbols(const rcc::mc::MCSymbolRef &Left,
+ const rcc::mc::MCSymbolRef &Right) {
+ auto LeftExpression = symbolExpression(Left);
+ auto RightExpression = symbolExpression(Right);
+ if (!LeftExpression)
+ return std::unexpected(LeftExpression.error());
+ if (!RightExpression)
+ return std::unexpected(RightExpression.error());
+ return rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::Subtract, *LeftExpression,
+ *RightExpression);
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCSectionRef>
+createSection(rcc::mc::MCContext &Context, std::string_view Name, rcc::mc::MCSectionKind Kind,
+ rcc::mc::MCSectionFlags Flags, std::uint64_t Alignment) {
+ return Context.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = std::string(Name),
+ .Kind = Kind,
+ .Flags = Flags,
+ .Alignment = Alignment,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+}
+
+[[nodiscard]] const rcc::mc::MCObjectSymbol *findSymbol(const rcc::mc::MCAssembledObject &Object,
+ std::string_view Name) {
+ const auto Position = std::ranges::find_if(Object.Symbols, [&](const auto &Symbol) {
+ return Symbol.Symbol && Symbol.Symbol->name() == Name;
+ });
+ return Position == Object.Symbols.end() ? nullptr : &*Position;
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCAssembledObject>
+assembleFixture(rcc::mc::MCContext &Context, const TestCodeEmitter &Emitter,
+ const TestAsmBackend &Backend) {
+ auto Text = createSection(Context, ".text", rcc::mc::MCSectionKind::Text,
+ {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable}, 4);
+ auto Data = createSection(Context, ".data", rcc::mc::MCSectionKind::Data,
+ {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable}, 1);
+ auto BSS = createSection(Context, ".bss", rcc::mc::MCSectionKind::BSS,
+ {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable}, 1);
+ auto Function = Context.getOrCreateSymbol("function", rcc::mc::MCSymbolKind::Function);
+ auto End = Context.createTemporarySymbol(".Lend");
+ auto External = Context.getOrCreateSymbol("external", rcc::mc::MCSymbolKind::Generic);
+ if (!Text || !Data || !BSS || !Function || !End || !External)
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError,
+ "failed to create the MC object fixture");
+ auto FunctionSize = subtractSymbols(*End, *Function);
+ auto ExternalValue = addConstant(*External, 7);
+ if (!FunctionSize || !ExternalValue)
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError,
+ "failed to create the MC object expressions");
+
+ rcc::mc::MCAssembler Assembler(Emitter, Backend);
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 17U;
+ if (!Assembler.switchSection(*Text) ||
+ !Assembler.emitSymbolAttribute(*Function, rcc::mc::MCSymbolAttribute::Global) ||
+ !Assembler.emitSymbolAttribute(*Function, rcc::mc::MCSymbolAttribute::Function) ||
+ !Assembler.emitLabel(*Function) || !Assembler.emitInstruction(testInstruction()) ||
+ !Assembler.emitAlignment(8, 0xaa, 0) || !Assembler.emitLabel(*End) ||
+ !Assembler.emitSymbolSize(*Function, *FunctionSize) || !Assembler.switchSection(*Data) ||
+ !Assembler.emitAlignment(16, 0, 0) || !Assembler.emitValue(*ExternalValue, 8) ||
+ !Assembler.switchSection(*BSS) || !Assembler.emitAlignment(4096, 0, 0) ||
+ !Assembler.emitZeroFill(LargeBSS)) {
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError,
+ "failed to emit the MC object fixture");
+ }
+ return Assembler.assemble();
+}
+
+void testAssembler(TestContext &Test, const TestCodeEmitter &Emitter,
+ const TestAsmBackend &Backend) {
+ rcc::mc::MCContext Context;
+ auto Object = assembleFixture(Context, Emitter, Backend);
+ if (!Object) {
+ std::cerr << "MC object fixture assembly failed: " << Object.error().message() << '\n';
+ Test.expect(false, "MC assembler fixture must assemble");
+ return;
+ }
+ Test.expect(Object->Flags == 0x20 && Object->Sections.size() == 3,
+ "MC assembler must preserve target flags and section order");
+ if (Object->Sections.size() != 3)
+ return;
+ Test.expect(Object->Sections[0].Alignment == 8 && Object->Sections[0].AddressSize == 8 &&
+ Object->Sections[0].Contents ==
+ std::vector<std::uint8_t>({0x11, 0x22, 0x33, 0x44, 0xaa, 0xaa, 0xaa, 0xaa}),
+ "MC assembler must materialize instructions and alignment padding");
+ Test.expect(Object->Sections[1].Alignment == 16 && Object->Sections[1].AddressSize == 8 &&
+ Object->Sections[1].Contents == std::vector<std::uint8_t>(8) &&
+ Object->Sections[1].Relocations.size() == 1 &&
+ Object->Sections[1].Relocations[0].Offset == 0 &&
+ Object->Sections[1].Relocations[0].Type == 77 &&
+ Object->Sections[1].Relocations[0].Addend == 7,
+ "MC assembler must preserve unresolved symbol relocations");
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 17U;
+ Test.expect(Object->Sections[2].Alignment == 4096 &&
+ Object->Sections[2].AddressSize == LargeBSS &&
+ Object->Sections[2].Contents.empty(),
+ "MC assembler must not materialize NOBITS storage");
+
+ const rcc::mc::MCObjectSymbol *Function = findSymbol(*Object, "function");
+ const rcc::mc::MCObjectSymbol *External = findSymbol(*Object, "external");
+ Test.expect(Function != nullptr && Function->Binding == rcc::mc::MCObjectSymbolBinding::Global &&
+ Function->Type == rcc::mc::MCObjectSymbolType::Function &&
+ Function->SectionIndex == 0 && Function->Value == 0 && Function->Size == 8,
+ "MC assembler must resolve function binding, type, value, and size");
+ Test.expect(External != nullptr && External->Binding == rcc::mc::MCObjectSymbolBinding::Global &&
+ !External->SectionIndex && External->Value == 0,
+ "unqualified undefined symbols must become global ELF symbols");
+}
+
+void testAssemblerAtomicity(TestContext &Test, TestCodeEmitter &Emitter,
+ const TestAsmBackend &Backend) {
+ rcc::mc::MCContext Context;
+ auto Text = createSection(Context, ".text.atomic", rcc::mc::MCSectionKind::Text,
+ {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable}, 4);
+ auto Symbol = Context.getOrCreateSymbol("atomic_function", rcc::mc::MCSymbolKind::Function);
+ if (!Text || !Symbol) {
+ Test.expect(false, "MC assembler atomicity fixture must be constructible");
+ return;
+ }
+ rcc::mc::MCAssembler Assembler(Emitter, Backend);
+ if (!Assembler.switchSection(*Text) || !Assembler.emitLabel(*Symbol) ||
+ !Assembler.emitInstruction(testInstruction())) {
+ Test.expect(false, "MC assembler atomicity events must succeed");
+ return;
+ }
+
+ Emitter.FailNextEncoding = true;
+ auto Failed = Assembler.assemble();
+ Test.expect(!Failed && Failed.error().code() == rcc::ErrorCode::InstructionEncodingFailed,
+ "MC assembler must propagate an encoding failure");
+ Test.expect(Assembler.emitAlignment(8, 0, 0).has_value(),
+ "failed assembly must leave the input event stream mutable and unchanged");
+ auto Succeeded = Assembler.assemble();
+ Test.expect(Succeeded.has_value() && Succeeded->Sections.size() == 1 &&
+ Succeeded->Sections[0].AddressSize == 8,
+ "MC assembler must support a clean retry after a failed transaction");
+ if (!Succeeded)
+ return;
+ auto Size = rcc::mc::MCExpr::constant(4);
+ Test.expect(!Assembler.switchSection(*Text) && !Assembler.emitLabel(*Symbol) &&
+ !Assembler.emitInstruction(testInstruction()) && !Assembler.emitValue(Size, 4) &&
+ !Assembler.emitZeroFill(1) && !Assembler.emitAlignment(4, 0, 0) &&
+ !Assembler.emitSymbolAttribute(*Symbol, rcc::mc::MCSymbolAttribute::Global) &&
+ !Assembler.emitSymbolSize(*Symbol, Size) &&
+ !Assembler.emitCommonSymbol(*Symbol, 4, 4) && !Assembler.assemble(),
+ "successful assembly must make every mutation and repeated finalization terminal");
+}
+
+void testSymbolSizeValidation(TestContext &Test, const TestCodeEmitter &Emitter,
+ const TestAsmBackend &Backend) {
+ rcc::mc::MCContext Context;
+ auto Text = createSection(Context, ".text.size", rcc::mc::MCSectionKind::Text,
+ {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable}, 4);
+ auto Data = createSection(Context, ".data.size", rcc::mc::MCSectionKind::Data,
+ {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable}, 4);
+ auto Function = Context.getOrCreateSymbol("sized_function", rcc::mc::MCSymbolKind::Function);
+ auto DataEnd = Context.createTemporarySymbol(".Ldata_end");
+ if (!Text || !Data || !Function || !DataEnd) {
+ Test.expect(false, "symbol-size validation fixture must be constructible");
+ return;
+ }
+
+ auto CrossSectionSize = subtractSymbols(*DataEnd, *Function);
+ auto FunctionExpression = symbolExpression(*Function);
+ if (!CrossSectionSize || !FunctionExpression) {
+ Test.expect(false, "symbol-size validation expressions must be constructible");
+ return;
+ }
+ auto TargetSize = rcc::mc::MCExpr::target(1, *FunctionExpression);
+ if (!TargetSize) {
+ Test.expect(false, "target symbol-size expression must be constructible");
+ return;
+ }
+
+ rcc::mc::MCAssembler CrossSectionAssembler(Emitter, Backend);
+ if (!CrossSectionAssembler.switchSection(*Text) || !CrossSectionAssembler.emitLabel(*Function) ||
+ !CrossSectionAssembler.emitInstruction(testInstruction()) ||
+ !CrossSectionAssembler.switchSection(*Data) || !CrossSectionAssembler.emitLabel(*DataEnd) ||
+ !CrossSectionAssembler.emitSymbolSize(*Function, *CrossSectionSize)) {
+ Test.expect(false, "cross-section symbol-size events must succeed before finalization");
+ return;
+ }
+ auto CrossSectionResult = CrossSectionAssembler.assemble();
+ Test.expect(!CrossSectionResult &&
+ CrossSectionResult.error().code() == rcc::ErrorCode::ObjectWritingFailed,
+ "MC assembler must reject a symbol-size difference across sections");
+
+ rcc::mc::MCAssembler TargetExpressionAssembler(Emitter, Backend);
+ if (!TargetExpressionAssembler.switchSection(*Text) ||
+ !TargetExpressionAssembler.emitLabel(*Function) ||
+ !TargetExpressionAssembler.emitInstruction(testInstruction()) ||
+ !TargetExpressionAssembler.emitSymbolSize(*Function, *TargetSize)) {
+ Test.expect(false, "target symbol-size events must succeed before finalization");
+ return;
+ }
+ auto TargetExpressionResult = TargetExpressionAssembler.assemble();
+ Test.expect(!TargetExpressionResult &&
+ TargetExpressionResult.error().code() == rcc::ErrorCode::ObjectWritingFailed,
+ "MC assembler must reject a target expression as a symbol size");
+}
+
+[[nodiscard]] std::uint16_t readU16(std::span<const std::uint8_t> Bytes, std::size_t Offset) {
+ return static_cast<std::uint16_t>(static_cast<std::uint16_t>(Bytes[Offset]) |
+ static_cast<std::uint16_t>(Bytes[Offset + 1U]) << 8U);
+}
+
+[[nodiscard]] std::uint32_t readU32(std::span<const std::uint8_t> Bytes, std::size_t Offset) {
+ std::uint32_t Result = 0;
+ for (std::uint32_t Index = 0; Index < 4; ++Index)
+ Result |= static_cast<std::uint32_t>(Bytes[Offset + Index]) << (Index * 8U);
+ return Result;
+}
+
+[[nodiscard]] bool contains(std::span<const std::uint8_t> Bytes, std::string_view Text) {
+ if (Text.size() > Bytes.size())
+ return false;
+ const auto *Data = reinterpret_cast<const char *>(Bytes.data());
+ return std::string_view(Data, Bytes.size()).find(Text) != std::string_view::npos;
+}
+
+void testELFWriter(TestContext &Test, const TestCodeEmitter &Emitter,
+ const TestAsmBackend &Backend) {
+ rcc::mc::MCContext Context;
+ auto Object = assembleFixture(Context, Emitter, Backend);
+ if (!Object) {
+ Test.expect(false, "ELF writer fixture must assemble");
+ return;
+ }
+ rcc::mc::ELFObjectTargetInfo Target{
+ .Machine = 243,
+ .OSABI = 3,
+ .ABIVersion = 1,
+ .Flags = 1,
+ .ExtraSections = {{.Name = ".note.test",
+ .Type = 7,
+ .Flags = 0,
+ .Alignment = 4,
+ .EntrySize = 0,
+ .Contents = {1, 2, 3, 4}}},
+ };
+ rcc::mc::ELF64LEObjectWriter Writer(Target);
+ auto First = Writer.writeObject(*Object);
+ auto Second = Writer.writeObject(*Object);
+ Test.expect(First.has_value() && Second.has_value() && *First == *Second,
+ "ELF writer output must be deterministic");
+ if (!First)
+ return;
+ Test.expect(First->size() < 4096 && First->size() >= 64 &&
+ std::ranges::equal(std::span<const std::uint8_t>(*First).first(4),
+ std::array<std::uint8_t, 4>{0x7f, 'E', 'L', 'F'}) &&
+ readU16(*First, 16) == 1 && readU16(*First, 18) == 243 &&
+ readU32(*First, 48) == 0x21 && contains(*First, ".bss") &&
+ contains(*First, ".rela.data") && contains(*First, ".note.test") &&
+ contains(*First, "function") && contains(*First, "external"),
+ "ELF writer must encode its header, sparse BSS, sections, symbols, and relocation");
+
+ rcc::mc::ELFObjectTargetInfo MissingMachine = Target;
+ MissingMachine.Machine = 0;
+ Test.expect(!rcc::mc::ELF64LEObjectWriter(MissingMachine).writeObject(*Object),
+ "ELF writer must reject an unconfigured target machine");
+
+ auto DuplicateSection = *Object;
+ DuplicateSection.Sections.push_back(DuplicateSection.Sections.front());
+ Test.expect(!Writer.writeObject(DuplicateSection),
+ "ELF writer must reject duplicate section identities");
+
+ auto BadAlignment = *Object;
+ BadAlignment.Sections[0].Alignment = 3;
+ Test.expect(!Writer.writeObject(BadAlignment),
+ "ELF writer must reject a non-power-of-two effective alignment");
+
+ auto BadContents = *Object;
+ ++BadContents.Sections[0].AddressSize;
+ Test.expect(!Writer.writeObject(BadContents),
+ "ELF writer must reject PROGBITS size/content disagreement");
+
+ auto BadNoBits = *Object;
+ BadNoBits.Sections[2].Relocations.push_back(rcc::mc::MCObjectRelocation{
+ .Offset = 0, .Type = 1, .Symbol = Object->Symbols.back().Symbol, .Addend = 0});
+ Test.expect(!Writer.writeObject(BadNoBits),
+ "ELF writer must reject relocations in a NOBITS section");
+
+ auto DuplicateSymbol = *Object;
+ DuplicateSymbol.Symbols.push_back(DuplicateSymbol.Symbols.front());
+ Test.expect(!Writer.writeObject(DuplicateSymbol),
+ "ELF writer must reject duplicate symbol identities");
+
+ auto OutOfRangeSymbol = *Object;
+ auto Defined = std::ranges::find_if(
+ OutOfRangeSymbol.Symbols, [](const auto &Symbol) { return Symbol.SectionIndex.has_value(); });
+ if (Defined != OutOfRangeSymbol.Symbols.end()) {
+ const std::size_t SectionIndex =
+ Defined->SectionIndex.value_or(OutOfRangeSymbol.Sections.size());
+ Defined->Value = OutOfRangeSymbol.Sections[SectionIndex].AddressSize + 1U;
+ Test.expect(!Writer.writeObject(OutOfRangeSymbol),
+ "ELF writer must reject a symbol outside its defining section");
+ }
+
+ auto OutOfRangeSymbolSize = *Object;
+ auto Sized = std::ranges::find_if(OutOfRangeSymbolSize.Symbols, [](const auto &Symbol) {
+ return Symbol.SectionIndex.has_value() && Symbol.Size != 0;
+ });
+ if (Sized != OutOfRangeSymbolSize.Symbols.end()) {
+ const std::size_t SectionIndex =
+ Sized->SectionIndex.value_or(OutOfRangeSymbolSize.Sections.size());
+ Sized->Size = OutOfRangeSymbolSize.Sections[SectionIndex].AddressSize - Sized->Value + 1U;
+ Test.expect(!Writer.writeObject(OutOfRangeSymbolSize),
+ "ELF writer must reject a symbol range outside its defining section");
+ }
+
+ auto BadCommon = *Object;
+ auto Undefined = std::ranges::find_if(BadCommon.Symbols,
+ [](const auto &Symbol) { return !Symbol.SectionIndex; });
+ if (Undefined != BadCommon.Symbols.end()) {
+ Undefined->Binding = rcc::mc::MCObjectSymbolBinding::Local;
+ Undefined->Value = 3;
+ Undefined->Size = 4;
+ Undefined->CommonAlignment = 3;
+ Test.expect(!Writer.writeObject(BadCommon),
+ "ELF writer must reject an invalid common symbol definition");
+ }
+
+ auto BadRelocationOffset = *Object;
+ BadRelocationOffset.Sections[1].Relocations[0].Offset =
+ BadRelocationOffset.Sections[1].AddressSize;
+ Test.expect(!Writer.writeObject(BadRelocationOffset),
+ "ELF writer must reject a relocation outside its section");
+
+ rcc::mc::MCContext OtherContext;
+ auto Unknown = OtherContext.getOrCreateSymbol("unknown", rcc::mc::MCSymbolKind::Generic);
+ if (Unknown) {
+ auto UnknownRelocation = *Object;
+ UnknownRelocation.Sections[1].Relocations[0].Symbol = *Unknown;
+ Test.expect(!Writer.writeObject(UnknownRelocation),
+ "ELF writer must reject a relocation to an unknown symbol");
+ }
+
+ auto BadExtraTarget = Target;
+ BadExtraTarget.ExtraSections[0].Alignment = 3;
+ Test.expect(!rcc::mc::ELF64LEObjectWriter(std::move(BadExtraTarget)).writeObject(*Object),
+ "ELF writer must reject an invalid target extra section");
+
+ auto ManagedExtraTarget = Target;
+ ManagedExtraTarget.ExtraSections[0].Type = 2;
+ Test.expect(!rcc::mc::ELF64LEObjectWriter(std::move(ManagedExtraTarget)).writeObject(*Object),
+ "ELF writer must reject a writer-managed target extra section type");
+
+ auto ExtendedNumberingTarget = Target;
+ ExtendedNumberingTarget.ExtraSections.resize(0xff00U);
+ auto ExtendedNumbering =
+ rcc::mc::ELF64LEObjectWriter(std::move(ExtendedNumberingTarget)).writeObject(*Object);
+ Test.expect(!ExtendedNumbering && ExtendedNumbering.error().code() == rcc::ErrorCode::Unsupported,
+ "ELF writer must reject section counts that require extended numbering");
+}
+
+} // namespace
+
+int main() {
+ TestContext Test;
+ TestCodeEmitter Emitter;
+ TestAsmBackend Backend;
+ testAssembler(Test, Emitter, Backend);
+ testAssemblerAtomicity(Test, Emitter, Backend);
+ testSymbolSizeValidation(Test, Emitter, Backend);
+ testELFWriter(Test, Emitter, Backend);
+ return Test.result();
+}
diff --git a/backend/unittests/Support/ErrorTest.cpp b/backend/unittests/Support/ErrorTest.cpp
index f111063..803ccee 100644
--- a/backend/unittests/Support/ErrorTest.cpp
+++ b/backend/unittests/Support/ErrorTest.cpp
@@ -42,6 +42,15 @@ int main() {
return fail("CFG-optimization errors must have a stable name");
if (rcc::getErrorCodeName(rcc::ErrorCode::AssemblyLoweringFailed) != "assembly lowering failed")
return fail("assembly-lowering errors must have a stable name");
+ if (rcc::getErrorCodeName(rcc::ErrorCode::InstructionEncodingFailed) !=
+ "instruction encoding failed")
+ return fail("instruction-encoding errors must have a stable name");
+ if (rcc::getErrorCodeName(rcc::ErrorCode::FixupApplicationFailed) != "fixup application failed")
+ return fail("fixup-application errors must have a stable name");
+ if (rcc::getErrorCodeName(rcc::ErrorCode::RelocationFailed) != "relocation failed")
+ return fail("relocation errors must have a stable name");
+ if (rcc::getErrorCodeName(rcc::ErrorCode::ObjectWritingFailed) != "object writing failed")
+ return fail("object-writing errors must have a stable name");
return 0;
}
diff --git a/backend/unittests/Target/CMakeLists.txt b/backend/unittests/Target/CMakeLists.txt
index 1b1f344..e68c525 100644
--- a/backend/unittests/Target/CMakeLists.txt
+++ b/backend/unittests/Target/CMakeLists.txt
@@ -82,6 +82,30 @@ add_test(
COMMAND RCCBackendRISCVAssemblyPrinterTests
)
+add_executable(RCCBackendRISCVMCTests RISCVMCTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVMCTests)
+target_link_libraries(RCCBackendRISCVMCTests PRIVATE RCCBackend::RISCV RCCBackend::MC)
+add_test(NAME RCCBackendRISCVMCTests COMMAND RCCBackendRISCVMCTests)
+
+add_executable(RCCBackendRISCVMCEncodingCoverageTests RISCVMCEncodingCoverageTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVMCEncodingCoverageTests)
+target_link_libraries(
+ RCCBackendRISCVMCEncodingCoverageTests
+ PRIVATE RCCBackend::RISCV RCCBackend::MC
+)
+add_test(
+ NAME RCCBackendRISCVMCEncodingCoverageTests
+ COMMAND RCCBackendRISCVMCEncodingCoverageTests
+)
+
+add_executable(RCCBackendRISCVObjectFileTests RISCVObjectFileTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVObjectFileTests)
+target_link_libraries(
+ RCCBackendRISCVObjectFileTests
+ PRIVATE RCCBackend::RISCV RCCBackend::MC RCCBackend::CodeGen
+)
+add_test(NAME RCCBackendRISCVObjectFileTests COMMAND RCCBackendRISCVObjectFileTests)
+
rcc_backend_register_format_files(
"${CMAKE_CURRENT_SOURCE_DIR}/DescriptionGeneratorTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/RISCVCallLoweringTest.cpp"
@@ -92,6 +116,9 @@ rcc_backend_register_format_files(
"${CMAKE_CURRENT_SOURCE_DIR}/RISCVFrameLoweringTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/RISCVLateCodeGenTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/RISCVAssemblyPrinterTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVMCTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVMCEncodingCoverageTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVObjectFileTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.h"
)
@@ -105,5 +132,8 @@ rcc_backend_register_tidy_files(
"${CMAKE_CURRENT_SOURCE_DIR}/RISCVFrameLoweringTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/RISCVLateCodeGenTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/RISCVAssemblyPrinterTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVMCTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVMCEncodingCoverageTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVObjectFileTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.cpp"
)
diff --git a/backend/unittests/Target/DescriptionGeneratorTest.cpp b/backend/unittests/Target/DescriptionGeneratorTest.cpp
index ea073bf..e6679e8 100644
--- a/backend/unittests/Target/DescriptionGeneratorTest.cpp
+++ b/backend/unittests/Target/DescriptionGeneratorTest.cpp
@@ -65,7 +65,7 @@ encoding_categories:
assembly_formats:
- { id: 1, symbol: RD_IMM, name: rd-imm }
instructions:
- - { id: 1024, symbol: LOAD_IMM, name: LOAD_IMM, assembly_mnemonic: load.imm, operands: [{ kinds: [register], role: def, register_class: GPR64, register_bank: GPR, name: RD }, { kinds: [immediate, global_symbol], immediate_predicate: SIMM8, name: IMM, allowed_target_flags: 33 }], num_explicit_defs: 1, flags: [produces_code], scheduling_class: ALU, encoding_category: I, assembly_format: RD_IMM }
+ - { id: 1024, symbol: LOAD_IMM, name: LOAD_IMM, assembly_mnemonic: load.imm, encoding: 19, operands: [{ kinds: [register], role: def, register_class: GPR64, register_bank: GPR, name: RD }, { kinds: [immediate, global_symbol], immediate_predicate: SIMM8, name: IMM, allowed_target_flags: 33 }], num_explicit_defs: 1, flags: [produces_code], scheduling_class: ALU, encoding_category: I, assembly_format: RD_IMM }
)";
}
diff --git a/backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir b/backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir
new file mode 100644
index 0000000..3105fe4
--- /dev/null
+++ b/backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir
@@ -0,0 +1,12 @@
+---
+name: stack_frame_pipeline
+tracksRegLiveness: true
+isSSA: true
+stack:
+ - { id: 0, size: 32, alignment: 16 }
+body: |
+ bb.0:
+ liveins: $x1
+ %0:gpr(i64) = G_CONSTANT 9
+ $x10 = COPY %0
+ PseudoRET implicit $x1, implicit $x10
diff --git a/backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp b/backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp
new file mode 100644
index 0000000..1e10d24
--- /dev/null
+++ b/backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp
@@ -0,0 +1,411 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/InstructionDescriptor.h"
+#include "rcc/CodeGen/TargetDescription.h"
+#include "rcc/MC/MCInst.h"
+#include "rcc/Target/RISCV/RISCVMCCodeEmitter.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <cstdint>
+#include <iostream>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace {
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCInst> fixtureError(std::string_view Message) {
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, std::string(Message));
+}
+
+[[nodiscard]] std::int64_t signExtend(std::uint32_t Value, unsigned Width) {
+ const std::uint32_t Mask = (std::uint32_t{1} << Width) - 1U;
+ const std::uint32_t Sign = std::uint32_t{1} << (Width - 1U);
+ Value &= Mask;
+ if ((Value & Sign) == 0)
+ return Value;
+ return static_cast<std::int64_t>(Value) - (std::int64_t{1} << Width);
+}
+
+void addRegister(rcc::mc::MCInst &Instruction, std::uint32_t Encoding, bool Floating) {
+ const std::uint32_t ID = Encoding + (Floating ? 33U : 1U);
+ Instruction.addOperand(rcc::mc::MCOperand::registerOperand(rcc::mc::MCRegister::fromValue(ID)));
+}
+
+void addImmediate(rcc::mc::MCInst &Instruction, std::int64_t Value) {
+ Instruction.addOperand(rcc::mc::MCOperand::immediate(Value));
+}
+
+[[nodiscard]] std::int64_t decodeBranchImmediate(std::uint32_t Encoding) {
+ const std::uint32_t Value = ((Encoding >> 31U) & 1U) << 12U | ((Encoding >> 25U) & 0x3fU) << 5U |
+ ((Encoding >> 8U) & 0xfU) << 1U | ((Encoding >> 7U) & 1U) << 11U;
+ return signExtend(Value, 13);
+}
+
+[[nodiscard]] std::int64_t decodeJALImmediate(std::uint32_t Encoding) {
+ const std::uint32_t Value = ((Encoding >> 31U) & 1U) << 20U | ((Encoding >> 21U) & 0x3ffU) << 1U |
+ ((Encoding >> 20U) & 1U) << 11U | ((Encoding >> 12U) & 0xffU) << 12U;
+ return signExtend(Value, 21);
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCInst>
+createStandardFixture(const rcc::InstructionDescriptor &Descriptor,
+ const rcc::riscv::AssemblyInstructionInfo &Assembly) {
+ using enum rcc::riscv::AssemblyFormat;
+ using enum rcc::riscv::InstructionOpcode;
+
+ const auto Category =
+ static_cast<rcc::riscv::EncodingCategory>(Descriptor.EncodingCategory.value());
+ const auto Format = static_cast<rcc::riscv::AssemblyFormat>(Assembly.Format.value());
+ const auto Opcode =
+ static_cast<rcc::riscv::InstructionOpcode>(Assembly.InstructionOpcode.value());
+ const std::uint32_t Encoding = Assembly.Encoding;
+ const std::uint32_t DestinationEncoding = (Encoding >> 7U) & 0x1fU;
+ const std::uint32_t Source1Encoding = (Encoding >> 15U) & 0x1fU;
+ const std::uint32_t Source2Encoding = (Encoding >> 20U) & 0x1fU;
+ rcc::mc::MCInst Result(rcc::mc::MCOpcode::fromValue(Assembly.InstructionOpcode.value()));
+
+ if (Category == rcc::riscv::EncodingCategory::R) {
+ switch (Format) {
+ case RD_RS1_RS2:
+ addRegister(Result, DestinationEncoding, false);
+ addRegister(Result, Source1Encoding, false);
+ addRegister(Result, Source2Encoding, false);
+ return Result;
+ case FRD_FRS1_FRS2_RM:
+ addRegister(Result, DestinationEncoding, true);
+ addRegister(Result, Source1Encoding, true);
+ addRegister(Result, Source2Encoding, true);
+ addImmediate(Result, (Encoding >> 12U) & 7U);
+ return Result;
+ case FRD_FRS1_FRS2:
+ addRegister(Result, DestinationEncoding, true);
+ addRegister(Result, Source1Encoding, true);
+ addRegister(Result, Source2Encoding, true);
+ return Result;
+ case RD_FRS1_FRS2:
+ addRegister(Result, DestinationEncoding, false);
+ addRegister(Result, Source1Encoding, true);
+ addRegister(Result, Source2Encoding, true);
+ return Result;
+ case FRD_FRS1_RM:
+ addRegister(Result, DestinationEncoding, true);
+ addRegister(Result, Source1Encoding, true);
+ addImmediate(Result, (Encoding >> 12U) & 7U);
+ return Result;
+ case RD_FRS1_RM:
+ addRegister(Result, DestinationEncoding, false);
+ addRegister(Result, Source1Encoding, true);
+ addImmediate(Result, (Encoding >> 12U) & 7U);
+ return Result;
+ case FRD_RS1_RM:
+ addRegister(Result, DestinationEncoding, true);
+ addRegister(Result, Source1Encoding, false);
+ addImmediate(Result, (Encoding >> 12U) & 7U);
+ return Result;
+ case RD_FRS1:
+ addRegister(Result, DestinationEncoding, false);
+ addRegister(Result, Source1Encoding, true);
+ return Result;
+ case FRD_RS1:
+ addRegister(Result, DestinationEncoding, true);
+ addRegister(Result, Source1Encoding, false);
+ return Result;
+ default:
+ return fixtureError("unsupported R-type reference fixture format");
+ }
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::I) {
+ if (Format == NONE)
+ return Result;
+ if (Format == IMM_IMM) {
+ addImmediate(Result, (Encoding >> 24U) & 0xfU);
+ addImmediate(Result, (Encoding >> 20U) & 0xfU);
+ return Result;
+ }
+ const bool FloatingDestination = Format == FRD_OFFSET_RS1;
+ if (Format != RD_RS1_IMM && Format != RD_OFFSET_RS1 && !FloatingDestination)
+ return fixtureError("unsupported I-type reference fixture format");
+ addRegister(Result, DestinationEncoding, FloatingDestination);
+ addRegister(Result, Source1Encoding, false);
+ const bool Shift64 = Opcode == SLLI || Opcode == SRLI || Opcode == SRAI;
+ const bool Shift32 = Opcode == SLLIW || Opcode == SRLIW || Opcode == SRAIW;
+ if (Shift64)
+ addImmediate(Result, (Encoding >> 20U) & 0x3fU);
+ else if (Shift32)
+ addImmediate(Result, (Encoding >> 20U) & 0x1fU);
+ else
+ addImmediate(Result, signExtend(Encoding >> 20U, 12));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::S) {
+ if (Format != RS2_OFFSET_RS1 && Format != FRS2_OFFSET_RS1)
+ return fixtureError("unsupported S-type reference fixture format");
+ addRegister(Result, Source2Encoding, Format == FRS2_OFFSET_RS1);
+ addRegister(Result, Source1Encoding, false);
+ const std::uint32_t Immediate = ((Encoding >> 25U) & 0x7fU) << 5U | ((Encoding >> 7U) & 0x1fU);
+ addImmediate(Result, signExtend(Immediate, 12));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::B) {
+ if (Format != RS1_RS2_TARGET)
+ return fixtureError("unsupported B-type reference fixture format");
+ addRegister(Result, Source1Encoding, false);
+ addRegister(Result, Source2Encoding, false);
+ addImmediate(Result, decodeBranchImmediate(Encoding));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::U) {
+ if (Format != RD_IMM)
+ return fixtureError("unsupported U-type reference fixture format");
+ addRegister(Result, DestinationEncoding, false);
+ addImmediate(Result, signExtend(Encoding >> 12U, 20));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::J) {
+ if (Format != RD_TARGET)
+ return fixtureError("unsupported J-type reference fixture format");
+ addRegister(Result, DestinationEncoding, false);
+ addImmediate(Result, decodeJALImmediate(Encoding));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::SYSTEM) {
+ if (Format == NONE)
+ return Result;
+ if (Format != RD_CSR_RS1 && Format != RD_CSR_IMM)
+ return fixtureError("unsupported system reference fixture format");
+ addRegister(Result, DestinationEncoding, false);
+ addImmediate(Result, Encoding >> 20U);
+ if (Format == RD_CSR_RS1)
+ addRegister(Result, Source1Encoding, false);
+ else
+ addImmediate(Result, Source1Encoding);
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::AMO) {
+ if (Format != RD_PAREN_RS1 && Format != RD_RS2_PAREN_RS1)
+ return fixtureError("unsupported atomic reference fixture format");
+ addRegister(Result, DestinationEncoding, false);
+ if (Format == RD_RS2_PAREN_RS1)
+ addRegister(Result, Source2Encoding, false);
+ addRegister(Result, Source1Encoding, false);
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::R4) {
+ if (Format != FRD_FRS1_FRS2_FRS3_RM)
+ return fixtureError("unsupported R4 reference fixture format");
+ addRegister(Result, DestinationEncoding, true);
+ addRegister(Result, Source1Encoding, true);
+ addRegister(Result, Source2Encoding, true);
+ addRegister(Result, (Encoding >> 27U) & 0x1fU, true);
+ addImmediate(Result, (Encoding >> 12U) & 7U);
+ return Result;
+ }
+
+ return fixtureError("unsupported standard reference fixture category");
+}
+
+[[nodiscard]] std::int64_t decodeRVCJumpImmediate(std::uint16_t Encoding) {
+ const std::uint32_t Value = ((Encoding >> 12U) & 1U) << 11U | ((Encoding >> 11U) & 1U) << 4U |
+ ((Encoding >> 9U) & 3U) << 8U | ((Encoding >> 8U) & 1U) << 10U |
+ ((Encoding >> 7U) & 1U) << 6U | ((Encoding >> 6U) & 1U) << 7U |
+ ((Encoding >> 3U) & 7U) << 1U | ((Encoding >> 2U) & 1U) << 5U;
+ return signExtend(Value, 12);
+}
+
+[[nodiscard]] std::int64_t decodeRVCBranchImmediate(std::uint16_t Encoding) {
+ const std::uint32_t Value = ((Encoding >> 12U) & 1U) << 8U | ((Encoding >> 10U) & 3U) << 3U |
+ ((Encoding >> 5U) & 3U) << 6U | ((Encoding >> 3U) & 3U) << 1U |
+ ((Encoding >> 2U) & 1U) << 5U;
+ return signExtend(Value, 9);
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCInst>
+createCompressedFixture(const rcc::riscv::AssemblyInstructionInfo &Assembly) {
+ using enum rcc::riscv::InstructionOpcode;
+ const auto Opcode =
+ static_cast<rcc::riscv::InstructionOpcode>(Assembly.InstructionOpcode.value());
+ const auto Encoding = static_cast<std::uint16_t>(Assembly.Encoding);
+ rcc::mc::MCInst Result(rcc::mc::MCOpcode::fromValue(Assembly.InstructionOpcode.value()));
+
+ if (Opcode == C_ADDI4SPN) {
+ addRegister(Result, ((Encoding >> 2U) & 7U) + 8U, false);
+ const std::uint32_t Immediate = ((Encoding >> 11U) & 3U) << 4U |
+ ((Encoding >> 7U) & 0xfU) << 6U |
+ ((Encoding >> 6U) & 1U) << 2U | ((Encoding >> 5U) & 1U) << 3U;
+ addImmediate(Result, Immediate);
+ return Result;
+ }
+
+ const bool DoubleLoad = Opcode == C_FLD || Opcode == C_LD;
+ const bool WordLoad = Opcode == C_LW;
+ const bool DoubleStore = Opcode == C_FSD || Opcode == C_SD;
+ const bool WordStore = Opcode == C_SW;
+ if (DoubleLoad || WordLoad || DoubleStore || WordStore) {
+ addRegister(Result, ((Encoding >> 2U) & 7U) + 8U, Opcode == C_FLD || Opcode == C_FSD);
+ addRegister(Result, ((Encoding >> 7U) & 7U) + 8U, false);
+ const bool Double = DoubleLoad || DoubleStore;
+ std::uint32_t Immediate = ((Encoding >> 10U) & 7U) << 3U;
+ if (Double) {
+ Immediate |= ((Encoding >> 5U) & 3U) << 6U;
+ } else {
+ Immediate |= ((Encoding >> 6U) & 1U) << 2U | ((Encoding >> 5U) & 1U) << 6U;
+ }
+ addImmediate(Result, Immediate);
+ return Result;
+ }
+
+ const bool CI = Opcode == C_ADDI || Opcode == C_ADDIW || Opcode == C_LI || Opcode == C_LUI ||
+ Opcode == C_SLLI;
+ if (CI) {
+ addRegister(Result, (Encoding >> 7U) & 0x1fU, false);
+ const std::uint32_t Immediate = ((Encoding >> 12U) & 1U) << 5U | ((Encoding >> 2U) & 0x1fU);
+ addImmediate(Result, Opcode == C_SLLI ? Immediate : signExtend(Immediate, 6));
+ return Result;
+ }
+
+ if (Opcode == C_ADDI16SP) {
+ const std::uint32_t Immediate = ((Encoding >> 12U) & 1U) << 9U | ((Encoding >> 6U) & 1U) << 4U |
+ ((Encoding >> 5U) & 1U) << 6U | ((Encoding >> 3U) & 3U) << 7U |
+ ((Encoding >> 2U) & 1U) << 5U;
+ addImmediate(Result, signExtend(Immediate, 10));
+ return Result;
+ }
+
+ const bool CompactShift = Opcode == C_SRLI || Opcode == C_SRAI;
+ if (CompactShift || Opcode == C_ANDI) {
+ addRegister(Result, ((Encoding >> 7U) & 7U) + 8U, false);
+ const std::uint32_t Immediate = ((Encoding >> 12U) & 1U) << 5U | ((Encoding >> 2U) & 0x1fU);
+ addImmediate(Result, CompactShift ? Immediate : signExtend(Immediate, 6));
+ return Result;
+ }
+
+ const bool CompactALU = Opcode == C_SUB || Opcode == C_XOR || Opcode == C_OR || Opcode == C_AND ||
+ Opcode == C_SUBW || Opcode == C_ADDW;
+ if (CompactALU) {
+ addRegister(Result, ((Encoding >> 7U) & 7U) + 8U, false);
+ addRegister(Result, ((Encoding >> 2U) & 7U) + 8U, false);
+ return Result;
+ }
+
+ if (Opcode == C_J) {
+ addImmediate(Result, decodeRVCJumpImmediate(Encoding));
+ return Result;
+ }
+
+ if (Opcode == C_BEQZ || Opcode == C_BNEZ) {
+ addRegister(Result, ((Encoding >> 7U) & 7U) + 8U, false);
+ addImmediate(Result, decodeRVCBranchImmediate(Encoding));
+ return Result;
+ }
+
+ const bool DoubleStackLoad = Opcode == C_FLDSP || Opcode == C_LDSP;
+ if (DoubleStackLoad || Opcode == C_LWSP) {
+ addRegister(Result, (Encoding >> 7U) & 0x1fU, Opcode == C_FLDSP);
+ const std::uint32_t Immediate =
+ DoubleStackLoad ? ((Encoding >> 12U) & 1U) << 5U | ((Encoding >> 5U) & 3U) << 3U |
+ ((Encoding >> 2U) & 7U) << 6U
+ : ((Encoding >> 12U) & 1U) << 5U | ((Encoding >> 4U) & 7U) << 2U |
+ ((Encoding >> 2U) & 3U) << 6U;
+ addImmediate(Result, Immediate);
+ return Result;
+ }
+
+ const bool CRSingle = Opcode == C_JR || Opcode == C_JALR;
+ const bool CRDouble = Opcode == C_MV || Opcode == C_ADD;
+ if (CRSingle || CRDouble) {
+ addRegister(Result, (Encoding >> 7U) & 0x1fU, false);
+ if (CRDouble)
+ addRegister(Result, (Encoding >> 2U) & 0x1fU, false);
+ return Result;
+ }
+
+ if (Opcode == C_EBREAK)
+ return Result;
+
+ const bool DoubleStackStore = Opcode == C_FSDSP || Opcode == C_SDSP;
+ if (DoubleStackStore || Opcode == C_SWSP) {
+ addRegister(Result, (Encoding >> 2U) & 0x1fU, Opcode == C_FSDSP);
+ const std::uint32_t Immediate =
+ DoubleStackStore ? ((Encoding >> 10U) & 7U) << 3U | ((Encoding >> 7U) & 7U) << 6U
+ : ((Encoding >> 9U) & 0xfU) << 2U | ((Encoding >> 7U) & 3U) << 6U;
+ addImmediate(Result, Immediate);
+ return Result;
+ }
+
+ return fixtureError("unsupported compressed reference fixture opcode");
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCInst>
+createReferenceFixture(const rcc::InstructionDescriptor &Descriptor,
+ const rcc::riscv::AssemblyInstructionInfo &Assembly) {
+ const auto Category =
+ static_cast<rcc::riscv::EncodingCategory>(Descriptor.EncodingCategory.value());
+ if (Category == rcc::riscv::EncodingCategory::C)
+ return createCompressedFixture(Assembly);
+ return createStandardFixture(Descriptor, Assembly);
+}
+
+[[nodiscard]] std::vector<std::uint8_t> referenceBytes(std::uint32_t Encoding, std::uint8_t Size) {
+ std::vector<std::uint8_t> Result;
+ Result.reserve(Size);
+ for (std::uint8_t Index = 0; Index < Size; ++Index)
+ Result.push_back(static_cast<std::uint8_t>(Encoding >> (Index * 8U)));
+ return Result;
+}
+
+} // namespace
+
+int main() {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target) {
+ std::cerr << Target.error().message() << '\n';
+ return 1;
+ }
+ const std::span<const rcc::riscv::AssemblyInstructionInfo> Instructions =
+ rcc::riscv::assemblyInstructions();
+ if (Instructions.size() != 195) {
+ std::cerr << "RV64GC encoding coverage does not contain all 195 instructions\n";
+ return 1;
+ }
+
+ rcc::riscv::RISCVMCCodeEmitter Emitter(*Target);
+ for (const rcc::riscv::AssemblyInstructionInfo &Assembly : Instructions) {
+ const rcc::InstructionDescriptor *Descriptor =
+ Target->instructionDescriptor(Assembly.InstructionOpcode);
+ if (Descriptor == nullptr || Assembly.Encoding == 0) {
+ std::cerr << Assembly.Mnemonic << ": missing encoding description\n";
+ return 1;
+ }
+ auto Fixture = createReferenceFixture(*Descriptor, Assembly);
+ if (!Fixture) {
+ std::cerr << Assembly.Mnemonic << ": " << Fixture.error().message() << '\n';
+ return 1;
+ }
+ auto Encoded = Emitter.encodeInstruction(*Fixture);
+ if (!Encoded) {
+ std::cerr << Assembly.Mnemonic << ": " << Encoded.error().message() << '\n';
+ return 1;
+ }
+ const auto Category =
+ static_cast<rcc::riscv::EncodingCategory>(Descriptor->EncodingCategory.value());
+ const std::uint8_t Size = Category == rcc::riscv::EncodingCategory::C ? 2 : 4;
+ if (!Encoded->Fixups.empty() || Encoded->Bytes != referenceBytes(Assembly.Encoding, Size)) {
+ std::uint32_t Actual = 0;
+ for (std::size_t Index = 0; Index < Encoded->Bytes.size(); ++Index)
+ Actual |= static_cast<std::uint32_t>(Encoded->Bytes[Index]) << (Index * 8U);
+ std::cerr << Assembly.Mnemonic << ": encoding differs from its RV64GC reference word ("
+ << Actual << " != " << Assembly.Encoding << ")\n";
+ return 1;
+ }
+ }
+ return 0;
+}
diff --git a/backend/unittests/Target/RISCVMCTest.cpp b/backend/unittests/Target/RISCVMCTest.cpp
new file mode 100644
index 0000000..1d44c73
--- /dev/null
+++ b/backend/unittests/Target/RISCVMCTest.cpp
@@ -0,0 +1,264 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/CompilationUnit.h"
+#include "rcc/CodeGen/GlobalObject.h"
+#include "rcc/MC/MCContext.h"
+#include "rcc/MC/MCExpr.h"
+#include "rcc/MC/MCInst.h"
+#include "rcc/Target/RISCV/RISCVAsmBackend.h"
+#include "rcc/Target/RISCV/RISCVFixupKinds.h"
+#include "rcc/Target/RISCV/RISCVInstPrinter.h"
+#include "rcc/Target/RISCV/RISCVMCCodeEmitter.h"
+#include "rcc/Target/RISCV/RISCVObjectFile.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <cstdint>
+#include <cstring>
+#include <initializer_list>
+#include <iostream>
+#include <span>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+int fail(std::string_view Message) {
+ std::cerr << Message << '\n';
+ return 1;
+}
+
+[[nodiscard]] rcc::mc::MCOperand reg(rcc::riscv::PhysicalRegisterID ID) {
+ return rcc::mc::MCOperand::registerOperand(
+ rcc::mc::MCRegister::fromValue(static_cast<std::uint32_t>(ID)));
+}
+
+[[nodiscard]] rcc::mc::MCOperand imm(std::int64_t Value) {
+ return rcc::mc::MCOperand::immediate(Value);
+}
+
+[[nodiscard]] rcc::mc::MCInst instruction(rcc::riscv::InstructionOpcode Opcode,
+ std::initializer_list<rcc::mc::MCOperand> Operands) {
+ rcc::mc::MCInst Result(rcc::mc::MCOpcode::fromValue(static_cast<std::uint32_t>(Opcode)));
+ Result.reserveOperands(Operands.size());
+ for (const rcc::mc::MCOperand &Operand : Operands)
+ Result.addOperand(Operand);
+ return Result;
+}
+
+[[nodiscard]] bool bytesEqual(std::span<const std::uint8_t> Left,
+ std::initializer_list<std::uint8_t> Right) {
+ return Left.size() == Right.size() &&
+ std::equal(Left.begin(), Left.end(), Right.begin(), Right.end());
+}
+
+[[nodiscard]] bool expectEncoding(const rcc::riscv::RISCVMCCodeEmitter &Emitter,
+ const rcc::mc::MCInst &Instruction,
+ std::initializer_list<std::uint8_t> Expected,
+ std::string_view Name) {
+ auto Encoded = Emitter.encodeInstruction(Instruction);
+ if (!Encoded) {
+ std::cerr << Name << ": " << Encoded.error().message() << '\n';
+ return false;
+ }
+ if (!Encoded->Fixups.empty() || !bytesEqual(Encoded->Bytes, Expected)) {
+ std::cerr << Name << ": encoded bytes differ\n";
+ return false;
+ }
+ return true;
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef>
+symbolExpression(rcc::mc::MCContext &Context, std::string_view Name, std::uint32_t TargetKind = 0) {
+ auto Symbol = Context.getOrCreateSymbol(std::string(Name), rcc::mc::MCSymbolKind::Generic);
+ if (!Symbol)
+ return std::unexpected(Symbol.error());
+ auto Expression = rcc::mc::MCExpr::symbol(*Symbol);
+ if (!Expression)
+ return std::unexpected(Expression.error());
+ if (TargetKind == 0)
+ return *Expression;
+ return rcc::mc::MCExpr::target(TargetKind, *Expression);
+}
+
+[[nodiscard]] bool testEncodings(const rcc::riscv::RISCVMCCodeEmitter &Emitter) {
+ using enum rcc::riscv::InstructionOpcode;
+ using enum rcc::riscv::PhysicalRegisterID;
+ return expectEncoding(Emitter, instruction(AUIPC, {reg(X11), imm(7)}), {0x97, 0x75, 0x00, 0x00},
+ "AUIPC") &&
+ expectEncoding(Emitter, instruction(ADDI, {reg(X12), reg(X13), imm(-17)}),
+ {0x13, 0x86, 0xf6, 0xfe}, "ADDI") &&
+ expectEncoding(Emitter, instruction(SRAI, {reg(X14), reg(X15), imm(63)}),
+ {0x13, 0xd7, 0xf7, 0x43}, "SRAI") &&
+ expectEncoding(Emitter, instruction(SD, {reg(X16), reg(X17), imm(-32)}),
+ {0x23, 0xb0, 0x08, 0xff}, "SD") &&
+ expectEncoding(Emitter, instruction(BGE, {reg(X18), reg(X19), imm(18)}),
+ {0x63, 0x59, 0x39, 0x01}, "BGE") &&
+ expectEncoding(Emitter, instruction(JAL, {reg(X20), imm(-20)}), {0x6f, 0xfa, 0xdf, 0xfe},
+ "JAL") &&
+ expectEncoding(Emitter, instruction(FENCE, {imm(10), imm(5)}), {0x0f, 0x00, 0x50, 0x0a},
+ "FENCE") &&
+ expectEncoding(Emitter, instruction(CSRRWI, {reg(X21), imm(0x305), imm(17)}),
+ {0xf3, 0xda, 0x58, 0x30}, "CSRRWI") &&
+ expectEncoding(Emitter, instruction(AMOADD_D, {reg(X22), reg(X23), reg(X24)}),
+ {0x2f, 0x3b, 0x7c, 0x01}, "AMOADD.D") &&
+ expectEncoding(Emitter, instruction(FMADD_D, {reg(F1), reg(F2), reg(F3), reg(F4), imm(2)}),
+ {0xc3, 0x20, 0x31, 0x22}, "FMADD.D") &&
+ expectEncoding(Emitter, instruction(FADD_S, {reg(F5), reg(F6), reg(F7), imm(3)}),
+ {0xd3, 0x32, 0x73, 0x00}, "FADD.S") &&
+ expectEncoding(Emitter, instruction(C_ADDI4SPN, {reg(X8), imm(48)}), {0x00, 0x18},
+ "C.ADDI4SPN") &&
+ expectEncoding(Emitter, instruction(C_LD, {reg(X9), reg(X8), imm(56)}), {0x04, 0x7c},
+ "C.LD") &&
+ expectEncoding(Emitter, instruction(C_SW, {reg(X10), reg(X11), imm(28)}), {0xc8, 0xcd},
+ "C.SW") &&
+ expectEncoding(Emitter, instruction(C_ADDI, {reg(X12), imm(-9)}), {0x5d, 0x16},
+ "C.ADDI") &&
+ expectEncoding(Emitter, instruction(C_ADDI16SP, {imm(-128)}), {0x19, 0x71},
+ "C.ADDI16SP") &&
+ expectEncoding(Emitter, instruction(C_SRAI, {reg(X13), imm(33)}), {0x85, 0x96},
+ "C.SRAI") &&
+ expectEncoding(Emitter, instruction(C_SUBW, {reg(X14), reg(X15)}), {0x1d, 0x9f},
+ "C.SUBW") &&
+ expectEncoding(Emitter, instruction(C_J, {imm(42)}), {0x2d, 0xa0}, "C.J") &&
+ expectEncoding(Emitter, instruction(C_BNEZ, {reg(X8), imm(-30)}), {0x6d, 0xf0},
+ "C.BNEZ") &&
+ expectEncoding(Emitter, instruction(C_LDSP, {reg(X10), imm(248)}), {0x6e, 0x75},
+ "C.LDSP") &&
+ expectEncoding(Emitter, instruction(C_SWSP, {reg(X11), imm(124)}), {0xae, 0xde},
+ "C.SWSP") &&
+ expectEncoding(Emitter, instruction(C_JALR, {reg(X12)}), {0x02, 0x96}, "C.JALR");
+}
+
+[[nodiscard]] bool testFixups(const rcc::riscv::RISCVMCCodeEmitter &Emitter,
+ const rcc::riscv::RISCVAsmBackend &Backend) {
+ using enum rcc::riscv::InstructionOpcode;
+ using enum rcc::riscv::PhysicalRegisterID;
+ rcc::mc::MCContext Context;
+
+ auto HighExpression =
+ symbolExpression(Context, "destination",
+ static_cast<std::uint32_t>(rcc::riscv::RISCVMCExprKind::PCRelativeHigh));
+ if (!HighExpression)
+ return false;
+ auto High = instruction(AUIPC, {reg(X5), rcc::mc::MCOperand::expression(*HighExpression)});
+ auto HighEncoding = Emitter.encodeInstruction(High);
+ if (!HighEncoding || HighEncoding->Fixups.size() != 1 ||
+ HighEncoding->Fixups[0].Kind !=
+ rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::PCRelativeHigh20) ||
+ !HighEncoding->Fixups[0].PCRelative ||
+ !bytesEqual(HighEncoding->Bytes, {0x97, 0x02, 0x00, 0x00})) {
+ return false;
+ }
+ auto HighValue = rcc::mc::evaluateAsRelocatable(**HighExpression);
+ if (!HighValue)
+ return false;
+ auto HighRelocation = Backend.relocationType(HighEncoding->Fixups[0], *HighValue);
+ if (!HighRelocation || *HighRelocation != 23)
+ return false;
+
+ auto LowExpression = symbolExpression(
+ Context, "anchor", static_cast<std::uint32_t>(rcc::riscv::RISCVMCExprKind::PCRelativeLow));
+ if (!LowExpression)
+ return false;
+ auto Low = instruction(JALR, {reg(X1), reg(X5), rcc::mc::MCOperand::expression(*LowExpression)});
+ auto LowEncoding = Emitter.encodeInstruction(Low);
+ if (!LowEncoding || LowEncoding->Fixups.size() != 1 ||
+ LowEncoding->Fixups[0].Kind !=
+ rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::PCRelativeLow12I)) {
+ return false;
+ }
+
+ auto BranchExpression = symbolExpression(Context, "block");
+ if (!BranchExpression)
+ return false;
+ auto Branch =
+ instruction(BEQ, {reg(X10), reg(X11), rcc::mc::MCOperand::expression(*BranchExpression)});
+ auto BranchEncoding = Emitter.encodeInstruction(Branch);
+ if (!BranchEncoding || BranchEncoding->Fixups.size() != 1 ||
+ BranchEncoding->Fixups[0].Kind != rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::Branch)) {
+ return false;
+ }
+ auto Applied = Backend.applyFixup(BranchEncoding->Fixups[0].Kind, BranchEncoding->Bytes, 0, 18);
+ if (!Applied || !bytesEqual(BranchEncoding->Bytes, {0x63, 0x09, 0xb5, 0x00}))
+ return false;
+
+ std::vector<std::uint8_t> Word(4);
+ auto OutOfRange =
+ Backend.applyFixup(rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::Branch), Word, 0, 4096);
+ return !OutOfRange && OutOfRange.error().code() == rcc::ErrorCode::FixupApplicationFailed;
+}
+
+[[nodiscard]] bool testFailures(const rcc::riscv::RISCVMCCodeEmitter &Emitter) {
+ using enum rcc::riscv::InstructionOpcode;
+ using enum rcc::riscv::PhysicalRegisterID;
+ auto BadShift = Emitter.encodeInstruction(instruction(SLLI, {reg(X1), reg(X2), imm(64)}));
+ auto BadCompactRegister =
+ Emitter.encodeInstruction(instruction(C_LD, {reg(X1), reg(X8), imm(8)}));
+ auto BadAlignment = Emitter.encodeInstruction(instruction(C_SDSP, {reg(X10), imm(6)}));
+ auto Pseudo = Emitter.encodeInstruction(instruction(PseudoCALL, {}));
+ return !BadShift && BadShift.error().code() == rcc::ErrorCode::InstructionEncodingFailed &&
+ !BadCompactRegister &&
+ BadCompactRegister.error().code() == rcc::ErrorCode::InstructionEncodingFailed &&
+ !BadAlignment &&
+ BadAlignment.error().code() == rcc::ErrorCode::InstructionEncodingFailed && !Pseudo &&
+ Pseudo.error().code() == rcc::ErrorCode::InstructionEncodingFailed;
+}
+
+[[nodiscard]] bool containsBytes(std::span<const std::uint8_t> Bytes, std::string_view Text) {
+ if (Text.size() > Bytes.size())
+ return false;
+ const auto *Begin = reinterpret_cast<const char *>(Bytes.data());
+ return std::string_view(Begin, Bytes.size()).find(Text) != std::string_view::npos;
+}
+
+[[nodiscard]] bool testObjectEmission(const rcc::TargetDescription &Target) {
+ auto Alignment = rcc::Alignment::create(8);
+ auto Value = rcc::IntegerImmediate::fromUnsigned(64, 0x1122334455667788ULL);
+ if (!Alignment || !Value)
+ return false;
+ rcc::GlobalObjectDesc Global;
+ Global.Symbol = "object_value";
+ Global.ObjectType = rcc::GenericConstantType::byte();
+ Global.Align = *Alignment;
+ Global.Size = 8;
+ Global.Initializer.emplace(rcc::GlobalInitializer::integer(std::move(*Value)));
+ rcc::CompilationUnit Unit;
+ if (!Unit.addGlobalObject(std::move(Global)))
+ return false;
+ auto Object = rcc::riscv::emitObject(Unit, Target);
+ if (!Object || Object->size() < 64 || (*Object)[0] != 0x7f || (*Object)[1] != 'E' ||
+ (*Object)[2] != 'L' || (*Object)[3] != 'F') {
+ return false;
+ }
+ const std::uint16_t Machine =
+ static_cast<std::uint16_t>((*Object)[18]) | static_cast<std::uint16_t>((*Object)[19] << 8U);
+ const std::uint32_t Flags = static_cast<std::uint32_t>((*Object)[48]) |
+ static_cast<std::uint32_t>((*Object)[49]) << 8U |
+ static_cast<std::uint32_t>((*Object)[50]) << 16U |
+ static_cast<std::uint32_t>((*Object)[51]) << 24U;
+ return Machine == 243 && Flags == 5 && containsBytes(*Object, ".data") &&
+ containsBytes(*Object, ".riscv.attributes") && containsBytes(*Object, "object_value") &&
+ containsBytes(*Object, std::string_view("\x88\x77\x66\x55\x44\x33\x22\x11", 8));
+}
+
+} // namespace
+
+int main() {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target)
+ return fail(Target.error().message());
+ rcc::riscv::RISCVMCCodeEmitter Emitter(*Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ if (!testEncodings(Emitter))
+ return fail("RISC-V MC encodings differ from the RV64GC reference encodings");
+ if (!testFixups(Emitter, Backend))
+ return fail("RISC-V MC fixup or relocation behavior is incorrect");
+ if (!testFailures(Emitter))
+ return fail("RISC-V MC encoding failures are not diagnosed");
+ if (!testObjectEmission(*Target))
+ return fail("RISC-V ELF object emission is incomplete");
+ if (Backend.objectFlags() != 5)
+ return fail("RISC-V ELF flags do not describe RV64GC LP64D");
+ return 0;
+}
diff --git a/backend/unittests/Target/RISCVObjectFileTest.cpp b/backend/unittests/Target/RISCVObjectFileTest.cpp
new file mode 100644
index 0000000..1b6d1f7
--- /dev/null
+++ b/backend/unittests/Target/RISCVObjectFileTest.cpp
@@ -0,0 +1,791 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Target/RISCV/RISCVObjectFile.h"
+
+#include "rcc/CodeGen/CompilationUnit.h"
+#include "rcc/CodeGen/GlobalObject.h"
+#include "rcc/MC/ELFObjectWriter.h"
+#include "rcc/MC/MCAssembler.h"
+#include "rcc/MC/MCContext.h"
+#include "rcc/MC/MCDwarfFrameEmitter.h"
+#include "rcc/MC/MCExpr.h"
+#include "rcc/MC/MCObjectStreamer.h"
+#include "rcc/Target/RISCV/RISCVAsmBackend.h"
+#include "rcc/Target/RISCV/RISCVMCCodeEmitter.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+#include "rcc/Target/RISCV/RISCVTargetConstant.h"
+
+#include <algorithm>
+#include <array>
+#include <bit>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+constexpr std::uint32_t SHTRela = 4;
+constexpr std::uint32_t SHTNoBits = 8;
+constexpr std::uint32_t SHTGroup = 17;
+constexpr std::uint16_t SHNUndefined = 0;
+constexpr std::uint16_t SHNCommon = 0xfff2U;
+constexpr std::uint64_t SHFWrite = 0x1;
+constexpr std::uint64_t SHFAlloc = 0x2;
+constexpr std::uint64_t SHFGroup = 0x200;
+constexpr std::uint64_t SHFTLS = 0x400;
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+[[nodiscard]] std::optional<std::uint16_t> readU16(std::span<const std::uint8_t> Bytes,
+ std::uint64_t Offset) {
+ if (Offset > Bytes.size() || 2U > Bytes.size() - static_cast<std::size_t>(Offset))
+ return std::nullopt;
+ const std::size_t Position = static_cast<std::size_t>(Offset);
+ return static_cast<std::uint16_t>(Bytes[Position]) |
+ static_cast<std::uint16_t>(Bytes[Position + 1U]) << 8U;
+}
+
+[[nodiscard]] std::optional<std::uint32_t> readU32(std::span<const std::uint8_t> Bytes,
+ std::uint64_t Offset) {
+ if (Offset > Bytes.size() || 4U > Bytes.size() - static_cast<std::size_t>(Offset))
+ return std::nullopt;
+ std::uint32_t Result = 0;
+ for (std::uint32_t Index = 0; Index < 4; ++Index)
+ Result |= static_cast<std::uint32_t>(Bytes[static_cast<std::size_t>(Offset) + Index])
+ << (Index * 8U);
+ return Result;
+}
+
+[[nodiscard]] std::optional<std::uint64_t> readU64(std::span<const std::uint8_t> Bytes,
+ std::uint64_t Offset) {
+ if (Offset > Bytes.size() || 8U > Bytes.size() - static_cast<std::size_t>(Offset))
+ return std::nullopt;
+ std::uint64_t Result = 0;
+ for (std::uint32_t Index = 0; Index < 8; ++Index)
+ Result |= static_cast<std::uint64_t>(Bytes[static_cast<std::size_t>(Offset) + Index])
+ << (Index * 8U);
+ return Result;
+}
+
+struct ELFSection final {
+ std::string Name;
+ std::uint32_t Type = 0;
+ std::uint64_t Flags = 0;
+ std::uint64_t Offset = 0;
+ std::uint64_t Size = 0;
+ std::uint32_t Link = 0;
+ std::uint32_t Info = 0;
+ std::uint64_t Alignment = 0;
+ std::uint64_t EntrySize = 0;
+};
+
+struct ELFSymbol final {
+ std::string Name;
+ std::uint8_t Binding = 0;
+ std::uint8_t Type = 0;
+ std::uint8_t Visibility = 0;
+ std::uint16_t Section = 0;
+ std::uint64_t Value = 0;
+ std::uint64_t Size = 0;
+};
+
+struct ELFRelocation final {
+ std::uint64_t Offset = 0;
+ std::uint32_t Symbol = 0;
+ std::uint32_t Type = 0;
+ std::int64_t Addend = 0;
+};
+
+class ELFView final {
+public:
+ [[nodiscard]] static std::optional<ELFView> create(std::span<const std::uint8_t> Bytes) {
+ if (Bytes.size() < 64 || Bytes[0] != 0x7f || Bytes[1] != 'E' || Bytes[2] != 'L' ||
+ Bytes[3] != 'F' || Bytes[4] != 2 || Bytes[5] != 1) {
+ return std::nullopt;
+ }
+ const auto HeaderOffset = readU64(Bytes, 40);
+ const auto EntrySize = readU16(Bytes, 58);
+ const auto Count = readU16(Bytes, 60);
+ const auto NamesIndex = readU16(Bytes, 62);
+ if (!HeaderOffset || !EntrySize || !Count || !NamesIndex || *EntrySize != 64 || *Count == 0 ||
+ *NamesIndex >= *Count) {
+ return std::nullopt;
+ }
+ if (*HeaderOffset > Bytes.size() ||
+ static_cast<std::uint64_t>(*Count) * *EntrySize > Bytes.size() - *HeaderOffset) {
+ return std::nullopt;
+ }
+
+ const std::uint64_t NamesHeader = *HeaderOffset + static_cast<std::uint64_t>(*NamesIndex) * 64U;
+ const auto NamesOffset = readU64(Bytes, NamesHeader + 24U);
+ const auto NamesSize = readU64(Bytes, NamesHeader + 32U);
+ if (!NamesOffset || !NamesSize || *NamesOffset > Bytes.size() ||
+ *NamesSize > Bytes.size() - *NamesOffset) {
+ return std::nullopt;
+ }
+ const std::span<const std::uint8_t> Names =
+ Bytes.subspan(static_cast<std::size_t>(*NamesOffset), static_cast<std::size_t>(*NamesSize));
+
+ ELFView Result(Bytes);
+ Result.Sections.reserve(*Count);
+ for (std::uint16_t Index = 0; Index < *Count; ++Index) {
+ const std::uint64_t Position = *HeaderOffset + static_cast<std::uint64_t>(Index) * 64U;
+ const auto NameOffset = readU32(Bytes, Position);
+ const auto Type = readU32(Bytes, Position + 4U);
+ const auto Flags = readU64(Bytes, Position + 8U);
+ const auto Offset = readU64(Bytes, Position + 24U);
+ const auto Size = readU64(Bytes, Position + 32U);
+ const auto Link = readU32(Bytes, Position + 40U);
+ const auto Info = readU32(Bytes, Position + 44U);
+ const auto Alignment = readU64(Bytes, Position + 48U);
+ const auto SectionEntrySize = readU64(Bytes, Position + 56U);
+ if (!NameOffset || !Type || !Flags || !Offset || !Size || !Link || !Info || !Alignment ||
+ !SectionEntrySize || *NameOffset >= Names.size()) {
+ return std::nullopt;
+ }
+ std::size_t End = *NameOffset;
+ while (End < Names.size() && Names[End] != 0)
+ ++End;
+ if (End == Names.size())
+ return std::nullopt;
+ Result.Sections.push_back(ELFSection{
+ .Name = std::string(reinterpret_cast<const char *>(Names.data() + *NameOffset),
+ End - *NameOffset),
+ .Type = *Type,
+ .Flags = *Flags,
+ .Offset = *Offset,
+ .Size = *Size,
+ .Link = *Link,
+ .Info = *Info,
+ .Alignment = *Alignment,
+ .EntrySize = *SectionEntrySize,
+ });
+ }
+ return Result;
+ }
+
+ [[nodiscard]] const ELFSection *section(std::string_view Name) const noexcept {
+ const auto Position =
+ std::ranges::find(Sections, Name, [](const ELFSection &Value) { return Value.Name; });
+ return Position == Sections.end() ? nullptr : &*Position;
+ }
+
+ [[nodiscard]] std::optional<std::size_t> sectionIndex(std::string_view Name) const noexcept {
+ for (std::size_t Index = 0; Index < Sections.size(); ++Index) {
+ if (Sections[Index].Name == Name)
+ return Index;
+ }
+ return std::nullopt;
+ }
+
+ [[nodiscard]] std::span<const std::uint8_t> contents(const ELFSection &Section) const noexcept {
+ if (Section.Type == SHTNoBits || Section.Offset > Bytes.size() ||
+ Section.Size > Bytes.size() - Section.Offset) {
+ return {};
+ }
+ return Bytes.subspan(static_cast<std::size_t>(Section.Offset),
+ static_cast<std::size_t>(Section.Size));
+ }
+
+ [[nodiscard]] std::optional<std::vector<ELFSymbol>> symbols() const {
+ const ELFSection *Table = section(".symtab");
+ if (Table == nullptr || Table->EntrySize != 24 || Table->Link >= Sections.size())
+ return std::nullopt;
+ const std::span<const std::uint8_t> TableBytes = contents(*Table);
+ const std::span<const std::uint8_t> Names = contents(Sections[Table->Link]);
+ if (TableBytes.size() % 24U != 0 || Names.empty())
+ return std::nullopt;
+ std::vector<ELFSymbol> Result;
+ Result.reserve(TableBytes.size() / 24U);
+ for (std::size_t Offset = 0; Offset < TableBytes.size(); Offset += 24U) {
+ const auto NameOffset = readU32(TableBytes, Offset);
+ const auto Section = readU16(TableBytes, Offset + 6U);
+ const auto Value = readU64(TableBytes, Offset + 8U);
+ const auto Size = readU64(TableBytes, Offset + 16U);
+ if (!NameOffset || !Section || !Value || !Size || *NameOffset >= Names.size())
+ return std::nullopt;
+ std::size_t End = *NameOffset;
+ while (End < Names.size() && Names[End] != 0)
+ ++End;
+ if (End == Names.size())
+ return std::nullopt;
+ Result.push_back(ELFSymbol{
+ .Name = std::string(reinterpret_cast<const char *>(Names.data() + *NameOffset),
+ End - *NameOffset),
+ .Binding = static_cast<std::uint8_t>(TableBytes[Offset + 4U] >> 4U),
+ .Type = static_cast<std::uint8_t>(TableBytes[Offset + 4U] & 0xfU),
+ .Visibility = static_cast<std::uint8_t>(TableBytes[Offset + 5U] & 0x3U),
+ .Section = *Section,
+ .Value = *Value,
+ .Size = *Size,
+ });
+ }
+ return Result;
+ }
+
+ [[nodiscard]] std::optional<std::vector<ELFRelocation>> relocations(std::string_view Name) const {
+ const ELFSection *Table = section(Name);
+ if (Table == nullptr || Table->Type != SHTRela || Table->EntrySize != 24)
+ return std::nullopt;
+ const std::span<const std::uint8_t> TableBytes = contents(*Table);
+ if (TableBytes.size() % 24U != 0)
+ return std::nullopt;
+ std::vector<ELFRelocation> Result;
+ Result.reserve(TableBytes.size() / 24U);
+ for (std::size_t Offset = 0; Offset < TableBytes.size(); Offset += 24U) {
+ const auto RelocationOffset = readU64(TableBytes, Offset);
+ const auto Info = readU64(TableBytes, Offset + 8U);
+ const auto Addend = readU64(TableBytes, Offset + 16U);
+ if (!RelocationOffset || !Info || !Addend)
+ return std::nullopt;
+ Result.push_back(ELFRelocation{
+ .Offset = *RelocationOffset,
+ .Symbol = static_cast<std::uint32_t>(*Info >> 32U),
+ .Type = static_cast<std::uint32_t>(*Info),
+ .Addend = std::bit_cast<std::int64_t>(*Addend),
+ });
+ }
+ return Result;
+ }
+
+ [[nodiscard]] const std::vector<ELFSection> &sections() const noexcept {
+ return Sections;
+ }
+
+private:
+ explicit ELFView(std::span<const std::uint8_t> Bytes) : Bytes(Bytes) {}
+
+ std::span<const std::uint8_t> Bytes;
+ std::vector<ELFSection> Sections;
+};
+
+[[nodiscard]] const ELFSymbol *findSymbol(const std::vector<ELFSymbol> &Symbols,
+ std::string_view Name) {
+ const auto Position =
+ std::ranges::find(Symbols, Name, [](const ELFSymbol &Symbol) { return Symbol.Name; });
+ return Position == Symbols.end() ? nullptr : &*Position;
+}
+
+[[nodiscard]] rcc::Expected<rcc::Alignment> alignment(std::uint64_t Value) {
+ return rcc::Alignment::create(Value);
+}
+
+[[nodiscard]] rcc::Expected<rcc::GlobalInitializer> integer(std::uint32_t Bits,
+ std::uint64_t Value) {
+ auto Immediate = rcc::IntegerImmediate::fromUnsigned(Bits, Value);
+ if (!Immediate)
+ return std::unexpected(Immediate.error());
+ return rcc::GlobalInitializer::integer(std::move(*Immediate));
+}
+
+[[nodiscard]] bool addObject(rcc::CompilationUnit &Unit, std::string Name, std::uint64_t Size,
+ std::uint64_t Alignment, rcc::GlobalLinkage Linkage,
+ std::optional<rcc::GlobalInitializer> Initializer,
+ const auto &Configure) {
+ auto Align = alignment(Alignment);
+ if (!Align)
+ return false;
+ rcc::GlobalObjectDesc Object;
+ Object.Symbol = std::move(Name);
+ Object.ObjectType = rcc::GenericConstantType::byte();
+ Object.Linkage = Linkage;
+ Object.Align = *Align;
+ Object.Size = Size;
+ Object.Initializer = std::move(Initializer);
+ Configure(Object);
+ return Unit.addGlobalObject(std::move(Object)).has_value();
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef> symbolDifference(rcc::mc::MCContext &Context,
+ std::string_view Left,
+ std::string_view Right,
+ std::int64_t Addend) {
+ auto LeftSymbol = Context.getOrCreateSymbol(std::string(Left), rcc::mc::MCSymbolKind::Generic);
+ auto RightSymbol = Context.getOrCreateSymbol(std::string(Right), rcc::mc::MCSymbolKind::Generic);
+ if (!LeftSymbol || !RightSymbol)
+ return std::unexpected(!LeftSymbol ? LeftSymbol.error() : RightSymbol.error());
+ auto LeftExpression = rcc::mc::MCExpr::symbol(*LeftSymbol);
+ auto RightExpression = rcc::mc::MCExpr::symbol(*RightSymbol);
+ if (!LeftExpression || !RightExpression)
+ return std::unexpected(!LeftExpression ? LeftExpression.error() : RightExpression.error());
+ auto Difference = rcc::mc::MCExpr::binary(
+ rcc::mc::MCBinaryOperator::Subtract, std::move(*LeftExpression), std::move(*RightExpression));
+ if (!Difference)
+ return std::unexpected(Difference.error());
+ return rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::Add, std::move(*Difference),
+ rcc::mc::MCExpr::constant(Addend));
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef>
+symbolExpression(rcc::mc::MCContext &Context, std::string_view Name,
+ rcc::mc::MCSymbolKind Kind = rcc::mc::MCSymbolKind::Generic) {
+ auto Symbol = Context.getOrCreateSymbol(std::string(Name), Kind);
+ if (!Symbol)
+ return std::unexpected(Symbol.error());
+ return rcc::mc::MCExpr::symbol(std::move(*Symbol));
+}
+
+[[nodiscard]] rcc::mc::MCInst instruction(rcc::riscv::InstructionOpcode Opcode) {
+ return rcc::mc::MCInst(rcc::mc::MCOpcode::fromValue(static_cast<std::uint32_t>(Opcode)));
+}
+
+void testMCRelaxation(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::MCContext MCContext;
+ auto Text = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".text.relax",
+ .Kind = rcc::mc::MCSectionKind::Text,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable},
+ .Alignment = 2,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto JumpTarget = MCContext.createTemporarySymbol(".Ljump_target");
+ auto BranchTarget = MCContext.createTemporarySymbol(".Lbranch_target");
+ if (!Text || !JumpTarget || !BranchTarget) {
+ Context.expect(false, "RISC-V MC relaxation fixture construction must succeed");
+ return;
+ }
+ auto JumpExpression = rcc::mc::MCExpr::symbol(*JumpTarget);
+ auto BranchExpression = rcc::mc::MCExpr::symbol(*BranchTarget);
+ if (!JumpExpression || !BranchExpression) {
+ Context.expect(false, "RISC-V MC relaxation expressions must be constructible");
+ return;
+ }
+
+ rcc::mc::MCInst Jump = instruction(rcc::riscv::InstructionOpcode::C_J);
+ Jump.addOperand(rcc::mc::MCOperand::expression(*JumpExpression));
+ rcc::mc::MCInst Branch = instruction(rcc::riscv::InstructionOpcode::C_BEQZ);
+ Branch.addOperand(rcc::mc::MCOperand::registerOperand(rcc::mc::MCRegister::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::PhysicalRegisterID::X8))));
+ Branch.addOperand(rcc::mc::MCOperand::expression(*BranchExpression));
+
+ rcc::mc::MCAssembler Assembler(Emitter, Backend);
+ if (!Assembler.switchSection(*Text) || !Assembler.emitInstruction(Jump) ||
+ !Assembler.emitZeroFill(3000) || !Assembler.emitLabel(*JumpTarget) ||
+ !Assembler.emitInstruction(Branch) || !Assembler.emitZeroFill(600) ||
+ !Assembler.emitLabel(*BranchTarget)) {
+ Context.expect(false, "RISC-V MC relaxation events must succeed");
+ return;
+ }
+ auto Object = Assembler.assemble();
+ if (!Object)
+ std::cerr << "RISC-V MC relaxation failure: " << Object.error().message() << '\n';
+ Context.expect(Object.has_value(), "RISC-V MC relaxation must reach a fixed point");
+ if (!Object || Object->Sections.size() != 1)
+ return;
+ const auto JumpWord = readU32(Object->Sections[0].Contents, 0);
+ const auto BranchWord = readU32(Object->Sections[0].Contents, 3004);
+ Context.expect(JumpWord && (*JumpWord & 0xfffU) == 0x6fU,
+ "out-of-range C.J must monotonically relax to JAL x0");
+ Context.expect(BranchWord && (*BranchWord & 0x01fff07fU) == 0x00040063U,
+ "out-of-range C.BEQZ must monotonically relax to BEQ x8, x0");
+ Context.expect(Object->Sections[0].AddressSize == 3608 && Object->Sections[0].Relocations.empty(),
+ "resolved MC relaxation must update layout without linker relocations");
+
+ auto ExternalExpression = symbolExpression(MCContext, "external_jump");
+ if (!ExternalExpression) {
+ Context.expect(false, "external MC relaxation expression must be constructible");
+ return;
+ }
+ rcc::mc::MCInst ExternalJump = instruction(rcc::riscv::InstructionOpcode::C_J);
+ ExternalJump.addOperand(rcc::mc::MCOperand::expression(*ExternalExpression));
+ rcc::mc::MCAssembler Relocatable(Emitter, Backend);
+ auto Switched = Relocatable.switchSection(*Text);
+ auto Emitted = Relocatable.emitInstruction(ExternalJump);
+ auto ExternalObject = Relocatable.assemble();
+ Context.expect(
+ Switched && Emitted && ExternalObject &&
+ ExternalObject->Sections[0].Relocations.size() == 1 &&
+ ExternalObject->Sections[0].Relocations[0].Type == 45,
+ "unresolved C.J must retain one mandatory relocation without implicit linker relax");
+}
+
+void testAssemblerLayout(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::MCContext MCContext;
+ auto Data = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".data",
+ .Kind = rcc::mc::MCSectionKind::Data,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable},
+ .Alignment = 1,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto BSS = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".bss",
+ .Kind = rcc::mc::MCSectionKind::BSS,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable},
+ .Alignment = 1,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto Difference = symbolDifference(MCContext, "left", "right", 5);
+ if (!Data || !BSS || !Difference) {
+ Context.expect(false, "MC assembler layout fixture construction must succeed");
+ return;
+ }
+
+ rcc::mc::MCAssembler Assembler(Emitter, Backend);
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ if (!Assembler.switchSection(*Data) || !Assembler.emitAlignment(128, 0, 0) ||
+ !Assembler.emitValue(*Difference, 8) || !Assembler.switchSection(*BSS) ||
+ !Assembler.emitAlignment(4096, 0, 0) || !Assembler.emitZeroFill(LargeBSS)) {
+ Context.expect(false, "MC assembler layout events must succeed");
+ return;
+ }
+ auto Object = Assembler.assemble();
+ if (!Object)
+ std::cerr << "MC assembler layout failure: " << Object.error().message() << '\n';
+ Context.expect(Object.has_value(), "MC assembler layout must complete");
+ if (!Object || Object->Sections.size() != 2)
+ return;
+ Context.expect(Object->Sections[0].Alignment == 128 &&
+ Object->Sections[0].Relocations.size() == 2 &&
+ Object->Sections[0].Relocations[0].Type == 36 &&
+ Object->Sections[0].Relocations[0].Addend == 5 &&
+ Object->Sections[0].Relocations[1].Type == 40 &&
+ Object->Sections[0].Relocations[1].Addend == 0,
+ "MC assembler must preserve effective alignment and paired symbol relocations");
+ Context.expect(Object->Sections[1].Alignment == 4096 &&
+ Object->Sections[1].AddressSize == LargeBSS &&
+ Object->Sections[1].Contents.empty(),
+ "MC assembler must keep NOBITS address size without materializing zero bytes");
+
+ rcc::mc::MCAssembler BadAssembler(Emitter, Backend);
+ const std::array<std::uint8_t, 1> Nonzero{1};
+ auto Switched = BadAssembler.switchSection(*BSS);
+ auto Emitted = BadAssembler.emitBytes(Nonzero);
+ auto Failure = BadAssembler.assemble();
+ Context.expect(Switched && Emitted && !Failure &&
+ Failure.error().code() == rcc::ErrorCode::ObjectWritingFailed,
+ "MC assembler must reject initialized bytes in a NOBITS section");
+}
+
+void testNativeObjectBoundaries(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::ELF64LEObjectWriter Writer(rcc::mc::ELFObjectTargetInfo{
+ .Machine = 243, .OSABI = 0, .ABIVersion = 0, .Flags = 0, .ExtraSections = {}});
+ rcc::mc::ELFObjectStreamer Streamer(Emitter, Backend, Writer);
+ auto EmptyAssembly = Streamer.emitInlineAssembly({});
+ auto RawAssembly = Streamer.emitInlineAssembly("addi a0, a0, 1");
+ Context.expect(EmptyAssembly && !RawAssembly &&
+ RawAssembly.error().code() == rcc::ErrorCode::Unsupported,
+ "native object emission must reject unparsed inline assembly without dropping it");
+
+ rcc::mc::MCContext MCContext;
+ auto EHFrame = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".eh_frame.boundary",
+ .Kind = rcc::mc::MCSectionKind::EHFrame,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc},
+ .Alignment = 8,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto Function = MCContext.getOrCreateSymbol("cfi_boundary", rcc::mc::MCSymbolKind::Function);
+ auto Personality =
+ MCContext.getOrCreateSymbol("cfi_personality", rcc::mc::MCSymbolKind::Function);
+ if (!EHFrame || !Function || !Personality) {
+ Context.expect(false, "native CFI boundary fixture must be constructible");
+ return;
+ }
+ const rcc::mc::MCDwarfFrameInfo Info{.CodeAlignment = 1,
+ .DataAlignment = -8,
+ .ReturnAddressRegister = 1,
+ .InitialCFARegister = 2,
+ .InitialCFAOffset = 0};
+ const std::array UnsupportedEvents{
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::Personality,
+ .Operands = {0},
+ .Bytes = {},
+ .Symbol = *Personality},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::LSDA,
+ .Operands = {0},
+ .Bytes = {},
+ .Symbol = *Personality},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::SignalFrame,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::ReturnColumn,
+ .Operands = {1},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::NegateReturnAddressState,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation =
+ rcc::mc::MCCFIOperation::NegateReturnAddressStateWithPC,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ };
+ for (const rcc::mc::MCCFIInstruction &Event : UnsupportedEvents) {
+ rcc::mc::MCDwarfFrameProcedure Procedure{
+ .Function = *Function,
+ .AddressRange = 4,
+ .Instructions = {{.Instruction = Event, .Offset = 0}},
+ };
+ auto Frame = rcc::mc::emitDwarfEHFrame(*EHFrame, std::span(&Procedure, 1), Info, Backend);
+ Context.expect(!Frame && Frame.error().code() == rcc::ErrorCode::Unsupported,
+ "unsupported RV64GC CFI extensions must fail structurally");
+ }
+}
+
+[[nodiscard]] bool buildGlobalFixture(rcc::CompilationUnit &Unit) {
+ constexpr auto NoConfiguration = [](rcc::GlobalObjectDesc &) {};
+ if (!addObject(Unit, "external_target", 0, 1, rcc::GlobalLinkage::External, std::nullopt,
+ NoConfiguration) ||
+ !addObject(Unit, "weak_target", 0, 1, rcc::GlobalLinkage::ExternalWeak, std::nullopt,
+ NoConfiguration)) {
+ return false;
+ }
+
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ auto LargeZero = rcc::GlobalInitializer::zeroFill(LargeBSS);
+ auto TLSValue = integer(64, 0x1122334455667788ULL);
+ auto TLSZero = rcc::GlobalInitializer::zeroFill(32);
+ auto Relocation = rcc::GlobalInitializer::symbolAddress(
+ {.Symbol = "external_target", .Addend = 7, .Size = 8, .TargetFlags = 0});
+ auto COMDATRelocation = rcc::GlobalInitializer::symbolAddress(
+ {.Symbol = "external_target", .Addend = 3, .Size = 8, .TargetFlags = 0});
+ auto HiddenValue = integer(32, 0x12345678U);
+ auto ProtectedValue = integer(64, 0x8877665544332211ULL);
+ auto ReadOnlyValue = integer(32, 0xaabbccddU);
+ auto TargetConstant = rcc::TargetConstant::create(rcc::riscv::RISCVSymbolConstant(
+ rcc::riscv::RISCVSymbolConstantKind::ExternalSymbol, "external_target", 13));
+ auto TargetInitializer =
+ TargetConstant
+ ? rcc::GlobalInitializer::target(std::move(*TargetConstant))
+ : rcc::Expected<rcc::GlobalInitializer>(std::unexpected(TargetConstant.error()));
+ if (!LargeZero || !TLSValue || !TLSZero || !Relocation || !COMDATRelocation || !HiddenValue ||
+ !ProtectedValue || !ReadOnlyValue || !TargetInitializer) {
+ return false;
+ }
+
+ std::vector<rcc::GlobalInitializer> Differences;
+ for (const auto [Size, Addend] :
+ std::array<std::pair<std::uint8_t, std::int64_t>, 4>{{{1, 11}, {2, 9}, {4, 7}, {8, 5}}}) {
+ auto Difference = rcc::GlobalInitializer::symbolDifference({.Minuend = "external_target",
+ .Subtrahend = "weak_target",
+ .Addend = Addend,
+ .Size = Size});
+ if (!Difference)
+ return false;
+ Differences.push_back(std::move(*Difference));
+ }
+ auto DifferenceAggregate = rcc::GlobalInitializer::aggregate(std::move(Differences));
+ if (!DifferenceAggregate)
+ return false;
+
+ return addObject(Unit, "large_bss", LargeBSS, 4096, rcc::GlobalLinkage::Internal,
+ std::move(*LargeZero), NoConfiguration) &&
+ addObject(Unit, "tls_data", 8, 8, rcc::GlobalLinkage::External, std::move(*TLSValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.TLSModel = rcc::GlobalTLSModel::LocalExec;
+ }) &&
+ addObject(Unit, "tls_bss", 32, 32, rcc::GlobalLinkage::External, std::move(*TLSZero),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.TLSModel = rcc::GlobalTLSModel::GeneralDynamic;
+ }) &&
+ addObject(Unit, "common_value", 128, 64, rcc::GlobalLinkage::Common, std::nullopt,
+ NoConfiguration) &&
+ addObject(Unit, "relro_pointer", 8, 8, rcc::GlobalLinkage::External,
+ std::move(*Relocation),
+ [](rcc::GlobalObjectDesc &Object) { Object.IsConstant = true; }) &&
+ addObject(Unit, "difference_value", 15, 1, rcc::GlobalLinkage::External,
+ std::move(*DifferenceAggregate), NoConfiguration) &&
+ addObject(Unit, "comdat_value", 8, 8, rcc::GlobalLinkage::LinkOnceODR,
+ std::move(*COMDATRelocation),
+ [](rcc::GlobalObjectDesc &Object) { Object.COMDAT = Object.Symbol; }) &&
+ addObject(Unit, "hidden_value", 4, 16, rcc::GlobalLinkage::External,
+ std::move(*HiddenValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.Visibility = rcc::GlobalVisibility::Hidden;
+ Object.Section = ".custom.data";
+ }) &&
+ addObject(Unit, "protected_value", 8, 128, rcc::GlobalLinkage::External,
+ std::move(*ProtectedValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.Visibility = rcc::GlobalVisibility::Protected;
+ }) &&
+ addObject(Unit, "readonly_value", 4, 4, rcc::GlobalLinkage::External,
+ std::move(*ReadOnlyValue),
+ [](rcc::GlobalObjectDesc &Object) { Object.IsConstant = true; }) &&
+ addObject(Unit, "target_constant_value", 8, 8, rcc::GlobalLinkage::Internal,
+ std::move(*TargetInitializer), [](rcc::GlobalObjectDesc &Object) {
+ Object.IsConstant = true;
+ Object.Section = ".data.rel.ro.target-constant";
+ });
+}
+
+void testGlobalObjectEmission(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::CompilationUnit Unit;
+ Context.expect(buildGlobalFixture(Unit), "global object fixture construction must succeed");
+ auto First = rcc::riscv::emitObject(Unit, Target);
+ auto Second = rcc::riscv::emitObject(Unit, Target);
+ if (!First)
+ std::cerr << "first global object emission failure: " << First.error().message() << '\n';
+ if (!Second)
+ std::cerr << "second global object emission failure: " << Second.error().message() << '\n';
+ Context.expect(First && Second && *First == *Second,
+ "global object emission must be successful and deterministic");
+ if (!First)
+ return;
+ Context.expect(First->size() < std::size_t{64} * 1024U,
+ "large NOBITS globals must not inflate the ELF file");
+ auto View = ELFView::create(*First);
+ Context.expect(View.has_value(), "emitted global object must be a valid ELF64LE container");
+ if (!View)
+ return;
+
+ const ELFSection *BSS = View->section(".bss");
+ const ELFSection *Data = View->section(".data");
+ const ELFSection *TLSData = View->section(".tdata");
+ const ELFSection *TLSBSS = View->section(".tbss");
+ const ELFSection *RelRO = View->section(".data.rel.ro");
+ const ELFSection *COMDAT = View->section(".data.comdat_value");
+ const ELFSection *COMDATRelocations = View->section(".rela.data.comdat_value");
+ const ELFSection *Group = View->section(".group");
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ Context.expect(BSS != nullptr && BSS->Type == SHTNoBits && BSS->Size == LargeBSS &&
+ BSS->Alignment == 4096 &&
+ (BSS->Flags & (SHFWrite | SHFAlloc)) == (SHFWrite | SHFAlloc),
+ "ELF BSS section must preserve its address size, flags, and alignment");
+ Context.expect(Data != nullptr && Data->Alignment == 128,
+ "ELF data section alignment must include later fragment requirements");
+ Context.expect(TLSData != nullptr && TLSData->Type != SHTNoBits &&
+ (TLSData->Flags & SHFTLS) != 0 && TLSBSS != nullptr &&
+ TLSBSS->Type == SHTNoBits && (TLSBSS->Flags & SHFTLS) != 0,
+ "ELF TLS data and BSS sections must carry their TLS semantics");
+ Context.expect(RelRO != nullptr && COMDAT != nullptr && COMDATRelocations != nullptr &&
+ Group != nullptr && Group->Type == SHTGroup &&
+ (COMDAT->Flags & SHFGroup) != 0 && (COMDATRelocations->Flags & SHFGroup) != 0,
+ "ELF COMDAT data and relocation sections must share a group");
+ Context.expect(View->section(".custom.data") != nullptr && View->section(".rodata") != nullptr &&
+ View->section(".riscv.attributes") != nullptr,
+ "ELF custom, read-only, and RISC-V attribute sections must be present");
+ const ELFSection *TargetConstantSection = View->section(".data.rel.ro.target-constant");
+ Context.expect(TargetConstantSection != nullptr && TargetConstantSection->Size == 8 &&
+ std::ranges::all_of(View->contents(*TargetConstantSection),
+ [](std::uint8_t Byte) { return Byte == 0; }),
+ "RV64GC target constants must reserve their relocation storage");
+
+ auto Symbols = View->symbols();
+ Context.expect(Symbols.has_value(), "ELF symbol table must be parseable");
+ if (!Symbols)
+ return;
+ const ELFSymbol *External = findSymbol(*Symbols, "external_target");
+ const ELFSymbol *Weak = findSymbol(*Symbols, "weak_target");
+ const ELFSymbol *Large = findSymbol(*Symbols, "large_bss");
+ const ELFSymbol *TLS = findSymbol(*Symbols, "tls_data");
+ const ELFSymbol *Common = findSymbol(*Symbols, "common_value");
+ const ELFSymbol *Hidden = findSymbol(*Symbols, "hidden_value");
+ const ELFSymbol *Protected = findSymbol(*Symbols, "protected_value");
+ const ELFSymbol *COMDATSymbol = findSymbol(*Symbols, "comdat_value");
+ Context.expect(External != nullptr && External->Binding == 1 &&
+ External->Section == SHNUndefined && External->Type == 1 && Weak != nullptr &&
+ Weak->Binding == 2 && Weak->Section == SHNUndefined,
+ "undefined object declarations must retain global and weak bindings");
+ Context.expect(Large != nullptr && Large->Binding == 0 && Large->Size == LargeBSS &&
+ TLS != nullptr && TLS->Binding == 1 && TLS->Type == 6,
+ "local BSS and global TLS symbols must have correct ELF types and sizes");
+ Context.expect(Common != nullptr && Common->Binding == 1 && Common->Section == SHNCommon &&
+ Common->Value == 64 && Common->Size == 128,
+ "ELF common symbol alignment and size must be preserved");
+ Context.expect(Hidden != nullptr && Hidden->Visibility == 2 && Protected != nullptr &&
+ Protected->Visibility == 3 && COMDATSymbol != nullptr &&
+ COMDATSymbol->Binding == 2,
+ "ELF visibility and COMDAT weak binding must be preserved");
+
+ auto DifferenceRelocations = View->relocations(".rela.data");
+ Context.expect(DifferenceRelocations && DifferenceRelocations->size() == 8,
+ "symbol differences must emit eight paired ELF relocations");
+ if (DifferenceRelocations) {
+ const std::array<std::uint32_t, 8> ExpectedTypes{33, 37, 34, 38, 35, 39, 36, 40};
+ for (std::size_t Index = 0; Index < ExpectedTypes.size(); ++Index) {
+ Context.expect((*DifferenceRelocations)[Index].Type == ExpectedTypes[Index],
+ "symbol difference relocation type order is incorrect");
+ }
+ const std::array<std::int64_t, 8> ExpectedAddends{11, 0, 9, 0, 7, 0, 5, 0};
+ for (std::size_t Index = 0; Index < ExpectedAddends.size(); ++Index) {
+ Context.expect((*DifferenceRelocations)[Index].Addend == ExpectedAddends[Index],
+ "symbol difference relocation addend is incorrect");
+ }
+ }
+ auto Relocations = View->relocations(".rela.data.rel.ro");
+ Context.expect(Relocations && Relocations->size() == 1 && (*Relocations)[0].Type == 2 &&
+ (*Relocations)[0].Addend == 7,
+ "read-only pointer data must emit an R_RISCV_64 relocation");
+ auto COMDATFixups = View->relocations(".rela.data.comdat_value");
+ Context.expect(COMDATFixups && COMDATFixups->size() == 1 && (*COMDATFixups)[0].Type == 2 &&
+ (*COMDATFixups)[0].Addend == 3,
+ "COMDAT pointer data must retain its relocation");
+ auto TargetConstantFixups = View->relocations(".rela.data.rel.ro.target-constant");
+ Context.expect(TargetConstantFixups && TargetConstantFixups->size() == 1 &&
+ (*TargetConstantFixups)[0].Type == 2 &&
+ (*TargetConstantFixups)[0].Addend == 13,
+ "RV64GC target constants must emit their declared R_RISCV_64 relocation");
+
+ if (Group != nullptr && COMDATSymbol != nullptr) {
+ const std::span<const std::uint8_t> GroupBytes = View->contents(*Group);
+ const auto GroupFlags = readU32(GroupBytes, 0);
+ const auto COMDATIndex = View->sectionIndex(".data.comdat_value");
+ const auto RelocationIndex = View->sectionIndex(".rela.data.comdat_value");
+ bool ContainsCOMDAT = false;
+ bool ContainsRelocations = false;
+ for (std::size_t Offset = 4; Offset + 4 <= GroupBytes.size(); Offset += 4) {
+ const auto Member = readU32(GroupBytes, Offset);
+ ContainsCOMDAT |= Member && COMDATIndex && *Member == *COMDATIndex;
+ ContainsRelocations |= Member && RelocationIndex && *Member == *RelocationIndex;
+ }
+ Context.expect(GroupFlags == 1 && Group->Info < Symbols->size() &&
+ (*Symbols)[Group->Info].Name == "comdat_value" && ContainsCOMDAT &&
+ ContainsRelocations,
+ "ELF COMDAT group must name its signature and both member sections");
+ }
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RISC-V object test target creation must succeed");
+ if (!Target)
+ return Context.result();
+ testAssemblerLayout(Context, *Target);
+ testMCRelaxation(Context, *Target);
+ testNativeObjectBoundaries(Context, *Target);
+ testGlobalObjectEmission(Context, *Target);
+ return Context.result();
+}