diff options
| author | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-08-07 01:27:41 +0800 |
|---|---|---|
| committer | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-08-07 01:27:41 +0800 |
| commit | 76d89700953ddd81cf31ab8f260eddf67e93aa85 (patch) | |
| tree | c96a453164154b2d1559cea36910df52103f0f19 /backend/unittests/MIR | |
| parent | 183e2354e9fdd107be420cbb185c1d239434aa48 (diff) | |
| download | rcc-project-76d89700953ddd81cf31ab8f260eddf67e93aa85.tar.gz rcc-project-76d89700953ddd81cf31ab8f260eddf67e93aa85.tar.xz | |
[MIR] Complete document parsing and printing
Implement the single LLVM-compatible MIR document path with structured\nembedded-IR references, YAML mappings, constants, and deterministic printing.\n\nKeep pointer LLT widths DataLayout-derived, reject layout-dependent output\nwhen embedded IR is omitted, and record the fixed-baseline compatibility\ncontract and regression coverage.
Diffstat (limited to 'backend/unittests/MIR')
| -rw-r--r-- | backend/unittests/MIR/CMakeLists.txt | 24 | ||||
| -rw-r--r-- | backend/unittests/MIR/EmbeddedIRTest.cpp | 14 | ||||
| -rw-r--r-- | backend/unittests/MIR/LLVMConstantSyntaxTest.cpp | 497 | ||||
| -rw-r--r-- | backend/unittests/MIR/MIParserTest.cpp | 62 | ||||
| -rw-r--r-- | backend/unittests/MIR/MIRDocumentTest.cpp | 953 |
5 files changed, 1548 insertions, 2 deletions
diff --git a/backend/unittests/MIR/CMakeLists.txt b/backend/unittests/MIR/CMakeLists.txt index 1eb0965..6b41474 100644 --- a/backend/unittests/MIR/CMakeLists.txt +++ b/backend/unittests/MIR/CMakeLists.txt @@ -6,6 +6,15 @@ target_link_libraries(RCCBackendEmbeddedIRTests PRIVATE RCCBackend::MIR) add_test(NAME RCCBackend.MIR.EmbeddedIR COMMAND RCCBackendEmbeddedIRTests) +add_executable(RCCBackendLLVMConstantSyntaxTests LLVMConstantSyntaxTest.cpp) +rcc_backend_configure_target(RCCBackendLLVMConstantSyntaxTests) +target_link_libraries(RCCBackendLLVMConstantSyntaxTests PRIVATE RCCBackend::MIR) + +add_test( + NAME RCCBackend.MIR.LLVMConstantSyntax + COMMAND RCCBackendLLVMConstantSyntaxTests +) + add_executable(RCCBackendMILexerTests MILexerTest.cpp) rcc_backend_configure_target(RCCBackendMILexerTests) target_link_libraries(RCCBackendMILexerTests PRIVATE RCCBackend::MIR) @@ -23,13 +32,28 @@ target_link_libraries( add_test(NAME RCCBackend.MIR.MIParser COMMAND RCCBackendMIParserTests) +add_executable(RCCBackendMIRDocumentTests MIRDocumentTest.cpp) +rcc_backend_configure_target(RCCBackendMIRDocumentTests) +target_link_libraries( + RCCBackendMIRDocumentTests + PRIVATE + RCCBackend::MIR + RCCBackend::TestTarget +) + +add_test(NAME RCCBackend.MIR.MIRDocument COMMAND RCCBackendMIRDocumentTests) + rcc_backend_register_format_files( "${CMAKE_CURRENT_SOURCE_DIR}/EmbeddedIRTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LLVMConstantSyntaxTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MILexerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MIRDocumentTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MIParserTest.cpp" ) rcc_backend_register_tidy_files( "${CMAKE_CURRENT_SOURCE_DIR}/EmbeddedIRTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LLVMConstantSyntaxTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MILexerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MIRDocumentTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MIParserTest.cpp" ) diff --git a/backend/unittests/MIR/EmbeddedIRTest.cpp b/backend/unittests/MIR/EmbeddedIRTest.cpp index 58c6162..bbac841 100644 --- a/backend/unittests/MIR/EmbeddedIRTest.cpp +++ b/backend/unittests/MIR/EmbeddedIRTest.cpp @@ -17,6 +17,7 @@ int testReferenceIndex() { const std::string Input = "; @commented = global i8 0\n" "target datalayout = \"e-m:e-p:64:64\"\n" "target triple = \"riscv64-unknown-linux-gnu\"\n" + "%pair = type { i32, i32 }\n" "@ordinary = global i32 0\n" "@\"space\\20global\" = external global i8\n" "declare void @declared()\n" @@ -25,12 +26,12 @@ int testReferenceIndex() { ") {\n" "entry:\n" " %slot = alloca i32, align 4\n" - " %not_an_alloca = add i32 1, 2\n" + " %not_an_alloca = add i32 %argument, 2\n" " br label %\"exit\\20block\"\n" "\"exit\\20block\":\n" " ret void\n" "}\n" - "!0 = !{}\n" + "!0 = !DILocation(line: 1, scope: !1)\n" "!llvm.module.flags = !{!0}\n"; rcc::mir::EmbeddedIRIndexResult Result = rcc::mir::indexEmbeddedIR("embedded.ll", Input); @@ -54,6 +55,9 @@ int testReferenceIndex() { if (Function->allocas().size() != 1 || Function->alloca("slot") == nullptr || Function->alloca("not_an_alloca") != nullptr) return fail("Embedded IR indexing must distinguish named allocas from other local values"); + if (Function->values().size() != 3 || Function->value("argument") == nullptr || + Function->value("slot") == nullptr || Function->value("not_an_alloca") == nullptr) + return fail("Embedded IR indexing must retain arguments and instruction results"); if (!Index->hasGlobalValue("ordinary") || !Index->hasGlobalValue("space global") || !Index->hasGlobalValue("declared") || Index->hasGlobalValue("commented")) @@ -61,6 +65,12 @@ int testReferenceIndex() { if (Index->metadata("0") == nullptr || Index->metadata("llvm.module.flags") == nullptr || Index->metadataDefinitions().size() != 2) return fail("Embedded IR indexing must retain numeric and named metadata definitions"); + if (Index->metadata("0")->DynamicType != "DILocation" || + Index->metadata("llvm.module.flags")->DynamicType != "MDTuple") + return fail("Embedded IR indexing must classify metadata dynamic types"); + if (Index->typeDefinitions().size() != 1 || Index->typeDefinition("pair") == nullptr || + Index->typeDefinition("pair")->Definition != "{ i32, i32 }") + return fail("Embedded IR indexing must retain named type definitions"); return 0; } diff --git a/backend/unittests/MIR/LLVMConstantSyntaxTest.cpp b/backend/unittests/MIR/LLVMConstantSyntaxTest.cpp new file mode 100644 index 0000000..be5a67e --- /dev/null +++ b/backend/unittests/MIR/LLVMConstantSyntaxTest.cpp @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/MIR/LLVMConstantSyntax.h" + +#include <algorithm> +#include <array> +#include <cstdint> +#include <iostream> +#include <limits> +#include <span> +#include <string> +#include <string_view> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +constexpr rcc::mir::LLVMConstantSyntaxOptions Options{ + .DefaultPointerSizeInBits = 64, + .DataLayout = {}, + .NamedTypes = {}, +}; + +void expectRoundTrip(TestContext &Context, std::string_view Text, std::uint64_t ExpectedSize) { + auto Parsed = rcc::mir::parseLLVMConstant(Text, Options); + Context.expect(Parsed.has_value(), "valid LLVM constant must parse"); + if (!Parsed) { + std::cerr << Text << ": " << Parsed.error().message() << '\n'; + return; + } + auto Size = rcc::mir::llvmConstantStorageSize(Parsed->Type, Options); + Context.expect(Size.has_value() && *Size == ExpectedSize, + "LLVM constant storage size must match its structured type"); + auto Printed = rcc::mir::printLLVMConstant(*Parsed); + Context.expect(Printed.has_value(), "structured LLVM constant must print"); + if (!Printed) + return; + auto Reparsing = rcc::mir::parseLLVMConstant(*Printed, Options); + Context.expect(Reparsing.has_value() && *Reparsing == *Parsed, + "LLVM constant parse-print-parse must preserve the value tree"); + if (!Reparsing) + std::cerr << *Printed << ": " << Reparsing.error().message() << '\n'; +} + +void expectLayoutRoundTrip(TestContext &Context, std::string_view Text, + rcc::mir::LLVMConstantSyntaxOptions LayoutOptions, + std::uint64_t ExpectedSize, std::uint64_t ExpectedPreferredAlignment) { + auto Parsed = rcc::mir::parseLLVMConstant(Text, LayoutOptions); + Context.expect(Parsed.has_value(), "DataLayout-aware LLVM constant must parse"); + if (!Parsed) { + std::cerr << Text << ": " << Parsed.error().message() << '\n'; + return; + } + auto Size = rcc::mir::llvmConstantStorageSize(Parsed->Type, LayoutOptions); + auto Alignment = rcc::mir::llvmConstantPreferredAlignment(Parsed->Type, LayoutOptions); + Context.expect(Size.has_value() && *Size == ExpectedSize, + "DataLayout must determine the exact allocation size"); + if (!Size || *Size != ExpectedSize) { + std::cerr << Text << ": expected size " << ExpectedSize << ", got " + << (Size ? std::to_string(*Size) : Size.error().message()) << '\n'; + } + Context.expect(Alignment.has_value() && Alignment->bytes() == ExpectedPreferredAlignment, + "DataLayout must determine the exact preferred alignment"); + if (!Alignment || Alignment->bytes() != ExpectedPreferredAlignment) { + std::cerr << Text << ": expected preferred alignment " << ExpectedPreferredAlignment << ", got " + << (Alignment ? std::to_string(Alignment->bytes()) : Alignment.error().message()) + << '\n'; + } + auto Printed = rcc::mir::printLLVMConstant(*Parsed); + Context.expect(Printed.has_value(), "DataLayout-aware LLVM constant must print"); + if (!Printed) + return; + auto Reparsing = rcc::mir::parseLLVMConstant(*Printed, LayoutOptions); + Context.expect(Reparsing.has_value() && *Reparsing == *Parsed, + "DataLayout-aware named constants must round-trip structurally"); +} + +void testScalarConstants(TestContext &Context) { + expectRoundTrip(Context, "i1 true", 1); + expectRoundTrip(Context, "i65 -1", 9); + expectRoundTrip(Context, "half 0xH3C00", 2); + expectRoundTrip(Context, "bfloat 0xR3F80", 2); + expectRoundTrip(Context, "float 1.250000e+00", 4); + expectRoundTrip(Context, "double 0x3FF0000000000000", 8); + expectRoundTrip(Context, "x86_fp80 f0x4000C000000000000000", 10); + expectRoundTrip(Context, "fp128 f0x3FFF0000000000000000000000000000", 16); + expectRoundTrip(Context, "ptr null", 8); + expectRoundTrip(Context, "i32 undef", 4); + expectRoundTrip(Context, "i32 poison", 4); + expectRoundTrip(Context, "i32 zeroinitializer", 4); +} + +void expectIntegerWords(TestContext &Context, std::string_view Text, + std::span<const std::uint64_t> ExpectedWords) { + auto Parsed = rcc::mir::parseLLVMConstant(Text, Options); + Context.expect(Parsed.has_value() && Parsed->IntegerValue.has_value() && + std::ranges::equal(Parsed->IntegerValue->words(), ExpectedWords), + "explicit LLVM hexadecimal integer must preserve its exact bit pattern"); + if (!Parsed) + std::cerr << Text << ": " << Parsed.error().message() << '\n'; +} + +void expectFloatingWords(TestContext &Context, std::string_view Text, + std::span<const std::uint64_t> ExpectedWords) { + auto Parsed = rcc::mir::parseLLVMConstant(Text, Options); + Context.expect(Parsed.has_value() && Parsed->FloatingValue.has_value() && + std::ranges::equal(Parsed->FloatingValue->words(), ExpectedWords), + "LLVM floating special literal must preserve its exact bit pattern"); + if (!Parsed) { + std::cerr << Text << ": " << Parsed.error().message() << '\n'; + return; + } + if (!Parsed->FloatingValue || + !std::ranges::equal(Parsed->FloatingValue->words(), ExpectedWords)) { + std::cerr << Text << ": expected words"; + for (const std::uint64_t Word : ExpectedWords) + std::cerr << " 0x" << std::hex << Word; + std::cerr << ", got"; + if (Parsed->FloatingValue) { + for (const std::uint64_t Word : Parsed->FloatingValue->words()) + std::cerr << " 0x" << std::hex << Word; + } + std::cerr << std::dec << '\n'; + } + auto Printed = rcc::mir::printLLVMConstant(*Parsed); + Context.expect(Printed.has_value(), "LLVM floating special literal must print"); + if (!Printed) + return; + auto Reparsing = rcc::mir::parseLLVMConstant(*Printed, Options); + Context.expect(Reparsing.has_value() && *Reparsing == *Parsed, + "printed LLVM floating special literal must round-trip"); +} + +void testExplicitHexIntegerConstants(TestContext &Context) { + constexpr std::array<std::uint64_t, 2> AllOnes65{std::numeric_limits<std::uint64_t>::max(), 1}; + constexpr std::array<std::uint64_t, 2> SignBit65{0, 1}; + constexpr std::array<std::uint64_t, 1> UnsignedFF{0xff}; + constexpr std::array<std::uint64_t, 1> SignedFF{0xffff}; + constexpr std::array<std::uint64_t, 1> SignedFF8{0xff}; + + expectIntegerWords(Context, "i65 u0x1FFFFFFFFFFFFFFFF", AllOnes65); + expectIntegerWords(Context, "i65 s0x1FFFFFFFFFFFFFFFF", AllOnes65); + expectIntegerWords(Context, "i65 s0x10000000000000000", SignBit65); + expectIntegerWords(Context, "i65 s0x01", AllOnes65); + expectIntegerWords(Context, "i16 u0xFF", UnsignedFF); + expectIntegerWords(Context, "i16 s0xFF", SignedFF); + expectIntegerWords(Context, "i8 s0x1FF", SignedFF8); + + expectRoundTrip(Context, "i65 s0x01", 9); + Context.expect(!rcc::mir::parseLLVMConstant("i32 u0x", Options) && + !rcc::mir::parseLLVMConstant("i32 u0X1", Options), + "explicit LLVM hexadecimal integers must use the fixed spelling"); +} + +void testSpecialFloatingLiterals(TestContext &Context) { + constexpr std::array<std::uint64_t, 1> HalfInfinity{0x7c00}; + constexpr std::array<std::uint64_t, 1> NegativeHalfInfinity{0xfc00}; + constexpr std::array<std::uint64_t, 1> HalfQuietNaN{0x7e00}; + constexpr std::array<std::uint64_t, 1> NegativeHalfQuietNaN{0xfe00}; + constexpr std::array<std::uint64_t, 1> HalfSignalingNaN{0x7d00}; + constexpr std::array<std::uint64_t, 1> HalfPayloadNaN{0x7fef}; + constexpr std::array<std::uint64_t, 1> FloatSignalingNaN{0x7f800001}; + constexpr std::array<std::uint64_t, 2> X87Infinity{0x8000000000000000, 0x7fff}; + constexpr std::array<std::uint64_t, 2> X87QuietNaN{0xc000000000000000, 0x7fff}; + constexpr std::array<std::uint64_t, 2> QuadOne{0, 0x3fff000000000000}; + constexpr std::array<std::uint64_t, 2> QuadInfinity{0, 0x7fff000000000000}; + constexpr std::array<std::uint64_t, 2> QuadQuietNaN{0, 0x7fff800000000000}; + constexpr std::array<std::uint64_t, 2> PPCOne{0x3ff0000000000000, 0}; + constexpr std::array<std::uint64_t, 2> PPCInfinity{0x7ff0000000000000, 0}; + constexpr std::array<std::uint64_t, 2> PPCQuietNaN{0x7ff8000000000000, 0}; + + expectFloatingWords(Context, "half +inf", HalfInfinity); + expectFloatingWords(Context, "half -inf", NegativeHalfInfinity); + expectFloatingWords(Context, "half +qnan", HalfQuietNaN); + expectFloatingWords(Context, "half -qnan", NegativeHalfQuietNaN); + expectFloatingWords(Context, "half +snan(0x100)", HalfSignalingNaN); + expectFloatingWords(Context, "half +nan(0x123456789ABCDEF)", HalfPayloadNaN); + expectFloatingWords(Context, "float +snan(0x400001)", FloatSignalingNaN); + expectFloatingWords(Context, "x86_fp80 +inf", X87Infinity); + expectFloatingWords(Context, "x86_fp80 +qnan", X87QuietNaN); + expectFloatingWords(Context, "fp128 +inf", QuadInfinity); + expectFloatingWords(Context, "fp128 +qnan", QuadQuietNaN); + expectFloatingWords(Context, "fp128 0xL00000000000000003FFF000000000000", QuadOne); + expectFloatingWords(Context, "ppc_fp128 +inf", PPCInfinity); + expectFloatingWords(Context, "ppc_fp128 +qnan", PPCQuietNaN); + expectFloatingWords(Context, "ppc_fp128 0xM3FF00000000000000000000000000000", PPCOne); + + for (const std::string_view Text : { + std::string_view{"float nan(0x)"}, + std::string_view{"float snan(12)"}, + std::string_view{"float qnan(0x1)"}, + std::string_view{"float inf(0x1)"}, + std::string_view{"float inf"}, + std::string_view{"float +nan"}, + std::string_view{"float +snan"}, + std::string_view{"float +qnan(0x1)"}, + }) { + Context.expect(!rcc::mir::parseLLVMConstant(Text, Options), + "malformed LLVM floating special literal must be rejected"); + } +} + +void testExactFloatingConversion(TestContext &Context) { + constexpr std::array<std::uint64_t, 1> PositiveZero{0}; + constexpr std::array<std::uint64_t, 1> NegativeFloatZero{0x80000000}; + constexpr std::array<std::uint64_t, 1> HalfOne{0x3c00}; + constexpr std::array<std::uint64_t, 1> HalfTwoUlps{0x3c02}; + constexpr std::array<std::uint64_t, 1> HalfSmallestSubnormal{0x0001}; + constexpr std::array<std::uint64_t, 1> BFloatOne{0x3f80}; + constexpr std::array<std::uint64_t, 1> BFloatTwoUlps{0x3f82}; + constexpr std::array<std::uint64_t, 1> FloatOne{0x3f800000}; + constexpr std::array<std::uint64_t, 1> FloatTwoUlps{0x3f800002}; + constexpr std::array<std::uint64_t, 1> FloatHexPower{0x2f800000}; + constexpr std::array<std::uint64_t, 1> FloatSignalingNaN{0x7f800001}; + constexpr std::array<std::uint64_t, 1> FloatQuietNaN{0x7fc00000}; + constexpr std::array<std::uint64_t, 1> LargestDouble{0x7fefffffffffffff}; + constexpr std::array<std::uint64_t, 2> X87One{0x8000000000000000, 0x3fff}; + constexpr std::array<std::uint64_t, 2> QuadOne{0, 0x3fff000000000000}; + constexpr std::array<std::uint64_t, 2> PPCOne{0x3ff0000000000000, 0}; + + expectFloatingWords(Context, "float +0.0", PositiveZero); + expectFloatingWords(Context, "float -0.0", NegativeFloatZero); + expectFloatingWords(Context, "half 1.00048828125", HalfOne); + expectFloatingWords(Context, "half 1.00146484375", HalfTwoUlps); + expectFloatingWords(Context, "half 0.0000000298023223876953126", HalfSmallestSubnormal); + expectFloatingWords(Context, "bfloat 1.00390625", BFloatOne); + expectFloatingWords(Context, "bfloat 1.01171875", BFloatTwoUlps); + expectFloatingWords(Context, "float 1.000000059604644775390625", FloatOne); + expectFloatingWords(Context, "float 1.000000178813934326171875", FloatTwoUlps); + expectFloatingWords(Context, "float 0x1.0p-32", FloatHexPower); + expectFloatingWords(Context, "double 1.79769313486231570815e+308", LargestDouble); + expectFloatingWords(Context, "x86_fp80 1.0", X87One); + expectFloatingWords(Context, "x86_fp80 0x1.0p0", X87One); + expectFloatingWords(Context, "fp128 1.0", QuadOne); + expectFloatingWords(Context, "fp128 0x1.0p0", QuadOne); + expectFloatingWords(Context, "ppc_fp128 1.0", PPCOne); + expectFloatingWords(Context, "ppc_fp128 0x1.0p0", PPCOne); + expectFloatingWords(Context, "float 0x3FF0000000000000", FloatOne); + expectFloatingWords(Context, "float 0x7FF0000020000000", FloatSignalingNaN); + expectFloatingWords(Context, "float 0x7FF8000000000000", FloatQuietNaN); + expectFloatingWords(Context, "float f0x3F800000", FloatOne); + + for (const std::string_view Text : { + std::string_view{"half 0.0000000298023223876953125"}, + std::string_view{"float 0x3FF0000000000001"}, + std::string_view{"float 0x7FF8000000000001"}, + std::string_view{"float f0x3F8000"}, + std::string_view{"float f0X3F800000"}, + std::string_view{"float 0X1.0p0"}, + std::string_view{"float 0x1p0"}, + }) { + Context.expect(!rcc::mir::parseLLVMConstant(Text, Options), + "noncanonical LLVM floating syntax must be rejected"); + } +} + +void testAggregateConstants(TestContext &Context) { + expectRoundTrip(Context, "[4 x i8] c\"ab\\00\\FF\"", 4); + expectRoundTrip(Context, "[3 x i16] [i16 1, i16 2, i16 3]", 6); + expectRoundTrip(Context, "<4 x i32> <i32 0, i32 1, i32 2, i32 3>", 16); + expectRoundTrip(Context, "<4 x float> <float 1.0, float -1.0, float 2.0, float -2.0>", 16); + expectRoundTrip(Context, "{ i32, ptr } { i32 7, ptr @global }", 16); + expectRoundTrip(Context, "<{ i8, i32 }> <{ i8 1, i32 2 }>", 5); + expectRoundTrip(Context, "<8 x i16> splat (i16 9)", 16); +} + +void testAddressConstants(TestContext &Context) { + expectRoundTrip(Context, "ptr @global", 8); + expectRoundTrip(Context, "ptr @\"quoted name\"", 8); + expectRoundTrip(Context, "ptr blockaddress(@function, %entry)", 8); + expectRoundTrip(Context, "ptr dso_local_equivalent @function", 8); + expectRoundTrip(Context, "ptr no_cfi @function", 8); + expectRoundTrip(Context, "i64 ptrtoint (ptr @global to i64)", 8); + expectRoundTrip(Context, "ptr bitcast (ptr @global to ptr)", 8); + expectRoundTrip(Context, "i64 add nuw nsw (i64 1, i64 2)", 8); + expectRoundTrip(Context, "ptr getelementptr inbounds nusw nuw (i8, ptr @global, i64 4)", 8); + expectRoundTrip(Context, + "<2 x i32> shufflevector (<2 x i32> <i32 1, i32 2>, " + "<2 x i32> <i32 3, i32 4>, <2 x i32> <i32 0, i32 3>)", + 8); +} + +void testConstantExpressionVerification(TestContext &Context) { + expectRoundTrip(Context, "i8 trunc (i16 257 to i8)", 1); + expectRoundTrip(Context, "i32 bitcast (float 1.0 to i32)", 4); + expectRoundTrip(Context, "ptr addrspacecast (ptr addrspace(1) @global to ptr)", 8); + expectRoundTrip(Context, "ptr inttoptr (i64 0 to ptr)", 8); + expectRoundTrip(Context, "i64 ptrtoaddr (ptr @global to i64)", 8); + expectRoundTrip(Context, "i64 ptrtoint (ptr @global to i64)", 8); + expectRoundTrip(Context, + "<2 x i32> insertelement (<2 x i32> <i32 1, i32 2>, " + "i32 3, i64 0)", + 8); + expectRoundTrip(Context, "i32 extractelement (<2 x i32> <i32 1, i32 2>, i64 0)", 4); + expectRoundTrip(Context, "ptr ptrauth (ptr @function, i32 1, i64 0, ptr null, ptr null)", 8); + + for (const std::string_view Text : { + std::string_view{"i16 trunc (i8 1 to i16)"}, + std::string_view{"i64 bitcast (float 1.0 to i64)"}, + std::string_view{"ptr bitcast (ptr addrspace(1) @global to ptr)"}, + std::string_view{"ptr addrspacecast (ptr @global to ptr)"}, + std::string_view{"ptr inttoptr (float 1.0 to ptr)"}, + std::string_view{"i64 ptrtoint (i64 0 to i64)"}, + std::string_view{"i32 add nuw (i32 1, i64 2)"}, + std::string_view{"ptr getelementptr (i8, ptr @global, float 1.0)"}, + std::string_view{"<2 x i32> shufflevector (<2 x i32> <i32 1, i32 2>, " + "<2 x i32> <i32 3, i32 4>, <2 x i64> <i64 0, i64 3>)"}, + std::string_view{"ptr ptrauth (ptr @function, i64 1)"}, + }) { + Context.expect(!rcc::mir::parseLLVMConstant(Text, Options), + "constant expression with incompatible typed operands must be rejected"); + } +} + +void testDataLayoutAndNamedTypes(TestContext &Context) { + static constexpr std::array Definitions{ + rcc::mir::LLVMNamedTypeDefinition{ + .Name = "pair", .Definition = "{ i8, i64 }", .NumericName = false}, + rcc::mir::LLVMNamedTypeDefinition{.Name = "word", .Definition = "i32", .NumericName = false}, + rcc::mir::LLVMNamedTypeDefinition{ + .Name = "node", .Definition = "{ i32, %node* }", .NumericName = false}, + rcc::mir::LLVMNamedTypeDefinition{ + .Name = "loop", .Definition = "{ %loop }", .NumericName = false}, + }; + static constexpr rcc::mir::LLVMConstantSyntaxOptions LayoutOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-p:64:64-p1:32:16:64-i64:64:128-i128:128-v96:128:256-a:0:64", + .NamedTypes = Definitions, + }; + + expectLayoutRoundTrip(Context, "i65 -1", LayoutOptions, 16, 16); + expectLayoutRoundTrip(Context, "[3 x i24] [i24 1, i24 2, i24 3]", LayoutOptions, 12, 4); + expectLayoutRoundTrip(Context, "<3 x i32> <i32 1, i32 2, i32 3>", LayoutOptions, 16, 32); + expectLayoutRoundTrip(Context, "ptr addrspace(1) null", LayoutOptions, 4, 8); + expectLayoutRoundTrip(Context, "%pair { i8 1, i64 2 }", LayoutOptions, 16, 8); + expectLayoutRoundTrip(Context, "%word 7", LayoutOptions, 4, 4); + expectLayoutRoundTrip(Context, "%node zeroinitializer", LayoutOptions, 16, 8); + + Context.expect(!rcc::mir::parseLLVMType("%loop", LayoutOptions), + "directly recursive named aggregate types must be rejected as unsized"); + auto NodeType = rcc::mir::parseLLVMType("%node", LayoutOptions); + Context.expect(NodeType.has_value(), + "named recursion through a pointer must remain sized and legal"); +} + +void testAddressSpacePointerWidths(TestContext &Context) { + constexpr rcc::mir::LLVMConstantSyntaxOptions HeterogeneousLayoutOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-p:64:64-p1:32:16:64", + .NamedTypes = {}, + }; + auto DefaultPointer = rcc::mir::llvmPointerSizeInBits(0, HeterogeneousLayoutOptions); + auto AddressSpaceOnePointer = rcc::mir::llvmPointerSizeInBits(1, HeterogeneousLayoutOptions); + Context.expect(DefaultPointer.has_value() && *DefaultPointer == 64 && + AddressSpaceOnePointer.has_value() && *AddressSpaceOnePointer == 32, + "LLVM data layout must provide pointer width by address space"); + + constexpr rcc::mir::LLVMConstantSyntaxOptions LegacyOptions{ + .DefaultPointerSizeInBits = 64, + .DataLayout = {}, + .NamedTypes = {}, + }; + auto LegacyPointer = rcc::mir::llvmPointerSizeInBits(7, LegacyOptions); + Context.expect(LegacyPointer.has_value() && *LegacyPointer == 64, + "legacy pointer-width fallback must apply without an explicit data layout"); +} + +void testDataLayoutBoundaryRules(TestContext &Context) { + static constexpr rcc::mir::LLVMConstantSyntaxOptions IntegerAlignmentOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-p:64:64-i24:128:128", + .NamedTypes = {}, + }; + static constexpr rcc::mir::LLVMConstantSyntaxOptions HistoricalAggregateOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-p:64:64-a0:64:128", + .NamedTypes = {}, + }; + static constexpr rcc::mir::LLVMConstantSyntaxOptions ZeroAggregateOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-p:64:64-a:0:64", + .NamedTypes = {}, + }; + static constexpr rcc::mir::LLVMConstantSyntaxOptions NonBytePointerOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-p:33:8:8:32", + .NamedTypes = {}, + }; + static constexpr rcc::mir::LLVMConstantSyntaxOptions PointerFlagsOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-pue1(storage):33:8:8:32-po2(allones):64:64", + .NamedTypes = {}, + }; + + expectLayoutRoundTrip(Context, "i20 1", IntegerAlignmentOptions, 16, 16); + expectLayoutRoundTrip(Context, "{ i8, i32 } { i8 1, i32 2 }", HistoricalAggregateOptions, 8, 16); + expectLayoutRoundTrip(Context, "{ i8, i32 } { i8 1, i32 2 }", ZeroAggregateOptions, 8, 8); + expectLayoutRoundTrip(Context, "<{ i8, i32 }> <{ i8 1, i32 2 }>", ZeroAggregateOptions, 5, 8); + expectLayoutRoundTrip(Context, "ptr null", NonBytePointerOptions, 5, 1); + expectLayoutRoundTrip(Context, "ptr addrspace(1) null", PointerFlagsOptions, 5, 1); + + static constexpr rcc::mir::LLVMConstantSyntaxOptions InvalidIndexSizeOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-p:33:8:8:34", + .NamedTypes = {}, + }; + auto Pointer = rcc::mir::parseLLVMType("ptr", InvalidIndexSizeOptions); + Context.expect(Pointer.has_value() && + !rcc::mir::llvmConstantStorageSize(*Pointer, InvalidIndexSizeOptions), + "LLVM pointer index size larger than its representation must be rejected"); + + static constexpr rcc::mir::LLVMConstantSyntaxOptions NonLayoutOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = "e-m:e-n8:16:32-S128-Fi64-P1-G2-A3-ni:1:2-ve-s-p:64:64", + .NamedTypes = {}, + }; + expectLayoutRoundTrip(Context, "ptr null", NonLayoutOptions, 8, 8); + + const auto ExpectInvalidDataLayout = [&Context](std::string_view DataLayout) { + const rcc::mir::LLVMConstantSyntaxOptions InvalidOptions{ + .DefaultPointerSizeInBits = 0, + .DataLayout = DataLayout, + .NamedTypes = {}, + }; + auto Type = rcc::mir::parseLLVMType("ptr", InvalidOptions); + Context.expect(Type.has_value() && !rcc::mir::llvmConstantStorageSize(*Type, InvalidOptions), + "malformed non-layout LLVM data-layout component must be rejected"); + }; + for (const std::string_view DataLayout : { + std::string_view{"ee-p:64:64"}, + std::string_view{"m:q-p:64:64"}, + std::string_view{"n64:0-p:64:64"}, + std::string_view{"S-p:64:64"}, + std::string_view{"Fx64-p:64:64"}, + std::string_view{"P16777216-p:64:64"}, + std::string_view{"ni:-p:64:64"}, + std::string_view{"ni:0-p:64:64"}, + std::string_view{"pe:64:64"}, + std::string_view{"pu:64:64"}, + std::string_view{"pzo1:64:64"}, + std::string_view{"p1(P):64:64"}, + std::string_view{"p1(foo):64:64-p2(foo):64:64"}, + std::string_view{"p16777216:64:64"}, + }) { + ExpectInvalidDataLayout(DataLayout); + } +} + +void testInvalidConstants(TestContext &Context) { + for (const std::string_view Text : { + std::string_view{""}, + std::string_view{"opaque 0"}, + std::string_view{"i32 null"}, + std::string_view{"ptr 42"}, + std::string_view{"[2 x i8] [i8 1]"}, + std::string_view{"<2 x i32> <i32 1, i64 2>"}, + std::string_view{"i64 ptrtoint (ptr @global to i32)"}, + std::string_view{"ptr getelementptr (i8, i64 0)"}, + std::string_view{"i32 1 trailing"}, + }) { + Context.expect(!rcc::mir::parseLLVMConstant(Text, Options), + "invalid LLVM constant must be rejected"); + } +} + +} // namespace + +int main() { + TestContext Context; + testScalarConstants(Context); + testExplicitHexIntegerConstants(Context); + testSpecialFloatingLiterals(Context); + testExactFloatingConversion(Context); + testAggregateConstants(Context); + testAddressConstants(Context); + testConstantExpressionVerification(Context); + testDataLayoutAndNamedTypes(Context); + testAddressSpacePointerWidths(Context); + testDataLayoutBoundaryRules(Context); + testInvalidConstants(Context); + return Context.result(); +} diff --git a/backend/unittests/MIR/MIParserTest.cpp b/backend/unittests/MIR/MIParserTest.cpp index d037bef..552de25 100644 --- a/backend/unittests/MIR/MIParserTest.cpp +++ b/backend/unittests/MIR/MIParserTest.cpp @@ -345,6 +345,66 @@ bb.1."exit block" (landing-pad, bbsections Cold): "complete MIR body parse-print-parse must be deterministic"); } +void testAddressSpacePointerWidthResolver(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 3); + Context.expect(Target.has_value(), "pointer-width parser target must be valid"); + if (!Target || !Function) + return; + + rcc::mir::MIParserContext ParserContext; + ParserContext.PointerSizeInBits = [](std::uint32_t AddressSpace) -> rcc::Expected<std::uint32_t> { + if (AddressSpace == 1) + return 32; + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "test data layout has no requested address space"); + }; + rcc::mir::MIParserOptions ParserOptions; + ParserOptions.VerifyAfterParse = false; + rcc::mir::MIParser Parser(*Function, *Target, "pointer-width.mir", + "bb.0:\n %0:_(p1) = TEST_COPY $g0\n", ParserContext, ParserOptions); + auto Parsed = Parser.parse(); + const auto Registers = Function->registerInfo().virtualRegisters(); + auto ExpectedType = rcc::LLT::makePointer(1, 32); + Context.expect(Parsed.has_value() && ExpectedType.has_value() && Registers.size() == 1 && + Registers.front().Type == *ExpectedType, + "MI parser must use the address-space-specific pointer-width resolver"); + if (!Parsed) { + for (const rcc::TextDiagnostic &Diagnostic : Parser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + std::cerr << "error: " << Parsed.error().message() << '\n'; + } +} + +void testUnserializableLowLevelType(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 4); + Context.expect(Target.has_value(), "unserializable-type target must be valid"); + if (!Target || !Function) + return; + + rcc::mir::MIParserOptions ParserOptions; + ParserOptions.VerifyAfterParse = false; + rcc::mir::MIParser Parser(*Function, *Target, "unserializable-type.mir", + "bb.0:\n %0:gpr64(f64) = TEST_COPY $g0\n", {}, ParserOptions); + auto Parsed = Parser.parse(); + auto Register = rcc::Register::makeVirtual(0); + auto UnsupportedType = rcc::LLT::makeFloat(rcc::FloatSemantics::Float8E5M2); + Context.expect(Parsed.has_value() && Register.has_value() && UnsupportedType.has_value(), + "unserializable-type test setup must parse a typed virtual register"); + if (!Parsed || !Register || !UnsupportedType) + return; + auto SetType = Function->registerInfo().setType(*Register, *UnsupportedType); + Context.expect(SetType.has_value(), "test setup must assign an extended floating low-level type"); + if (!SetType) + return; + + auto Printed = rcc::mir::printMachineFunctionBody(*Function, *Target); + Context.expect(!Printed && Printed.error().message().find("cannot spell this floating") != + std::string_view::npos, + "printer must reject extended low-level types absent from fixed LLVM MIR grammar"); +} + void expectParseError(TestContext &Context, const rcc::TargetDescription &Target, std::string Body, std::string_view Message) { auto Function = createFunction(Context, 10); @@ -391,6 +451,8 @@ void testDiagnostics(TestContext &Context) { int main() { TestContext Context; testTwoPassBodyParsing(Context); + testAddressSpacePointerWidthResolver(Context); + testUnserializableLowLevelType(Context); testDiagnostics(Context); return Context.result(); } diff --git a/backend/unittests/MIR/MIRDocumentTest.cpp b/backend/unittests/MIR/MIRDocumentTest.cpp new file mode 100644 index 0000000..7ea82c1 --- /dev/null +++ b/backend/unittests/MIR/MIRDocumentTest.cpp @@ -0,0 +1,953 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/MIR/MIRDocument.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/CompilationUnit.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCompilationStatus.h" +#include "rcc/CodeGen/MachineConstantPool.h" +#include "rcc/CodeGen/MachineFrameInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineJumpTableInfo.h" +#include "rcc/CodeGen/MachineMetadata.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetConstant.h" +#include "rcc/CodeGen/TargetDescription.h" +#include "rcc/CodeGen/TargetFunctionState.h" +#include "rcc/CodeGen/TargetMIRTextInfo.h" + +#include <charconv> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t CopyOpcode = 1; +constexpr std::uint32_t BranchOpcode = 2; +constexpr std::uint32_t LoadOpcode = 3; +constexpr std::uint32_t GlobalOpcode = 4; + +class DocumentTargetState final { +public: + DocumentTargetState(std::uint32_t FrameBase, std::string Mode) + : FrameBase(FrameBase), Mode(std::move(Mode)) {} + + [[nodiscard]] static constexpr rcc::TargetFunctionStateTypeID stableTypeID() noexcept { + return rcc::TargetFunctionStateTypeID::fromValue(0x5243434d49525354ULL); + } + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 1; + } + [[nodiscard]] rcc::Expected<void> verify() const { + if (FrameBase == 0 || Mode.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "document target state is incomplete"); + return {}; + } + [[nodiscard]] rcc::Expected<DocumentTargetState> + clone(const rcc::MachineFunctionCloneContext &) const { + return DocumentTargetState(FrameBase, Mode); + } + [[nodiscard]] std::string print() const { + return Mode + ":" + std::to_string(FrameBase); + } + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Bytes(4); + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Bytes[Index] = static_cast<std::byte>((FrameBase >> (Index * 8U)) & 0xffU); + return Bytes; + } + [[nodiscard]] static rcc::Expected<DocumentTargetState> decode(std::uint32_t Version, + std::span<const std::byte> Bytes) { + if (Version != schemaVersion() || Bytes.size() != 4) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "invalid document target state encoding"); + std::uint32_t Value = 0; + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Value |= std::to_integer<std::uint32_t>(Bytes[Index]) << (Index * 8U); + return DocumentTargetState(Value, "test"); + } + + [[nodiscard]] std::uint32_t frameBase() const noexcept { + return FrameBase; + } + [[nodiscard]] std::string_view mode() const noexcept { + return Mode; + } + + friend bool operator==(const DocumentTargetState &Left, + const DocumentTargetState &Right) = default; + +private: + std::uint32_t FrameBase = 0; + std::string Mode; +}; + +class DocumentTargetConstant final { +public: + explicit DocumentTargetConstant(std::uint64_t Value) : Value(Value) {} + + [[nodiscard]] static constexpr rcc::TargetConstantTypeID stableTypeID() noexcept { + return rcc::TargetConstantTypeID::fromValue(0x5243434d49524354ULL); + } + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 1; + } + [[nodiscard]] rcc::Expected<void> verify() const { + return {}; + } + [[nodiscard]] rcc::Expected<DocumentTargetConstant> clone() const { + return DocumentTargetConstant(Value); + } + [[nodiscard]] std::uint64_t stableHash() const noexcept { + return Value; + } + [[nodiscard]] std::string print() const { + return "test-constant(" + std::to_string(Value) + ")"; + } + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Bytes(8); + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Bytes[Index] = static_cast<std::byte>((Value >> (Index * 8U)) & 0xffU); + return Bytes; + } + [[nodiscard]] rcc::Expected<rcc::TargetConstantEmission> emit() const { + auto Bytes = encode(); + if (!Bytes) + return std::unexpected(Bytes.error()); + return rcc::TargetConstantEmission{.Bytes = std::move(*Bytes), .Fixups = {}}; + } + [[nodiscard]] rcc::Alignment requiredAlignment() const { + return *rcc::Alignment::create(8); + } + [[nodiscard]] std::optional<rcc::TargetRelocationKindID> relocationKind() const { + return std::nullopt; + } + [[nodiscard]] static rcc::Expected<DocumentTargetConstant> parse(std::string_view Text) { + constexpr std::string_view Prefix = "test-constant("; + if (!Text.starts_with(Prefix) || !Text.ends_with(')')) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "invalid document target constant"); + Text.remove_prefix(Prefix.size()); + Text.remove_suffix(1); + std::uint64_t Parsed = 0; + const auto [End, Error] = std::from_chars(Text.data(), Text.data() + Text.size(), Parsed, 10); + if (Error != std::errc{} || End != Text.data() + Text.size()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "invalid document target constant value"); + return DocumentTargetConstant(Parsed); + } + [[nodiscard]] static rcc::Expected<DocumentTargetConstant> + decode(std::uint32_t Version, std::span<const std::byte> Bytes) { + if (Version != schemaVersion() || Bytes.size() != 8) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "invalid document target constant encoding"); + std::uint64_t Parsed = 0; + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Parsed |= std::to_integer<std::uint64_t>(Bytes[Index]) << (Index * 8U); + return DocumentTargetConstant(Parsed); + } + + [[nodiscard]] std::uint64_t value() const noexcept { + return Value; + } + + friend bool operator==(const DocumentTargetConstant &Left, + const DocumentTargetConstant &Right) = default; + +private: + std::uint64_t Value = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetFunctionState> +parseTargetState(const rcc::MIRTargetNode &Node) { + const rcc::MIRTargetNode *FrameBase = Node.mappingValue("frame-base"); + const rcc::MIRTargetNode *Mode = Node.mappingValue("mode"); + if (FrameBase == nullptr || Mode == nullptr || + FrameBase->Kind != rcc::MIRTargetNodeKind::Scalar || + Mode->Kind != rcc::MIRTargetNodeKind::Scalar) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "document target state requires frame-base and mode"); + } + std::uint32_t Value = 0; + const auto [End, Error] = std::from_chars( + FrameBase->Scalar.data(), FrameBase->Scalar.data() + FrameBase->Scalar.size(), Value, 10); + if (Error != std::errc{} || End != FrameBase->Scalar.data() + FrameBase->Scalar.size()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "document target frame-base is invalid"); + return rcc::TargetFunctionState::create(DocumentTargetState(Value, Mode->Scalar)); +} + +[[nodiscard]] rcc::Expected<rcc::MIRTargetNode> +printTargetState(const rcc::TargetFunctionState &State) { + const auto *Value = State.getIf<DocumentTargetState>(); + if (Value == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, "wrong document target state type"); + std::vector<rcc::MIRTargetMappingEntry> Entries; + Entries.push_back({"frame-base", rcc::MIRTargetNode::scalar(std::to_string(Value->frameBase()))}); + Entries.push_back({"mode", rcc::MIRTargetNode::scalar(std::string(Value->mode()))}); + return rcc::MIRTargetNode::mapping(std::move(Entries)); +} + +[[nodiscard]] rcc::Expected<rcc::TargetConstant> parseTargetConstant(std::string_view Text) { + return rcc::TargetConstant::parse<DocumentTargetConstant>(Text); +} + +[[nodiscard]] rcc::Expected<std::string> printTargetConstant(const rcc::TargetConstant &Constant) { + if (Constant.getIf<DocumentTargetConstant>() == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "wrong document target constant type"); + return Constant.print(); +} + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Operand; + Operand.Kinds = {rcc::MachineOperand::Kind::Register}; + Operand.RegisterRole = Role; + Operand.RegClass = rcc::test::TestRegisterClass::GPR64; + return Operand; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Copy; + Copy.InstructionOpcode = rcc::Opcode::fromValue(CopyOpcode); + Copy.Name = "TEST_COPY"; + Copy.Operands = { + registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + }; + Copy.NumExplicitDefs = 1; + Copy.Flags.set(rcc::InstructionFlag::ProducesCode); + + rcc::InstructionDescriptor Branch; + Branch.InstructionOpcode = rcc::Opcode::fromValue(BranchOpcode); + Branch.Name = "TEST_BRANCH"; + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags = { + rcc::InstructionFlag::ProducesCode, + rcc::InstructionFlag::Branch, + rcc::InstructionFlag::Terminator, + rcc::InstructionFlag::Barrier, + }; + + rcc::InstructionDescriptor Load; + Load.InstructionOpcode = rcc::Opcode::fromValue(LoadOpcode); + Load.Name = "TEST_LOAD"; + Load.Operands = { + registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + }; + Load.NumExplicitDefs = 1; + Load.Flags = { + rcc::InstructionFlag::ProducesCode, + rcc::InstructionFlag::MayLoad, + }; + Load.RequiresMemoryOperands = true; + + rcc::InstructionDescriptor Global; + Global.InstructionOpcode = rcc::Opcode::fromValue(GlobalOpcode); + Global.Name = "TEST_GLOBAL"; + rcc::OperandDescriptor GlobalOperand; + GlobalOperand.Kinds = { + rcc::MachineOperand::Kind::GlobalSymbol, + rcc::MachineOperand::Kind::ExternalSymbol, + }; + Global.Operands.push_back(std::move(GlobalOperand)); + Global.Flags.set(rcc::InstructionFlag::ProducesCode); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Load)); + Descriptors.push_back(std::move(Global)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + auto Registers = rcc::test::createTestTargetRegisterInfo(); + if (!Registers) + return std::unexpected(Registers.error()); + rcc::TargetMIRTextInfoDesc MIRText = rcc::test::createTestTargetMIRTextInfoDescription(); + MIRText.FunctionStateType = DocumentTargetState::stableTypeID(); + MIRText.ParseFunctionState = parseTargetState; + MIRText.PrintFunctionState = printTargetState; + MIRText.ParseTargetConstant = parseTargetConstant; + MIRText.PrintTargetConstant = printTargetConstant; + return rcc::TargetDescription::create(std::move(*Instructions), std::move(*Registers), + std::move(MIRText)); +} + +constexpr std::string_view ValidDocument = R"(--- | + target datalayout = "e-p:64:64" + target triple = "test64-unknown-linux-gnu" + + %pair = type { i8, i64 } + @ordinary = external global i8 + + define void @documented() { + entry: + %slot = alloca i64, align 8 + ret void + } + + !0 = !DILocalVariable(name: "slot", scope: !1) + !1 = distinct !DISubprogram(name: "documented") + !2 = !DILocation(line: 4, column: 2, scope: !1) +--- +name: documented +alignment: 16 +exposesReturnsTwice: true +tracksRegLiveness: true +noPhis: true +isSSA: true +noVRegs: false +registers: + - { id: 7, class: gpr64, preferred-register: '$g1', flags: [ test-vreg-one ] } +liveins: + - { reg: '$g0', virtual-reg: '%7' } +calleeSavedRegisters: [ '$g1' ] +frameInfo: + isFrameAddressTaken: true + stackSize: 32 + maxAlignment: 16 + adjustsStack: true + hasCalls: true + framePointerPolicy: all + stackProtector: '%stack.4' + maxCallFrameSize: 24 + localFrameSize: 8 + savePoint: + - { point: '%bb.0', registers: [ '$g1' ] } +fixedStack: + - { id: 3, type: spill-slot, offset: -16, size: 8, alignment: 8 } +stack: + - { id: 4, name: slot, size: 8, alignment: 8, local-offset: 0, + debug-info-variable: '!0', + debug-info-expression: '!DIExpression(DW_OP_LLVM_arg, 0, 18446744073709551615)', + debug-info-location: '!DILocation(line: 4, column: 2, scope: !1, isImplicitCode: true)' } +entry_values: + - { entry-value-register: '$g0', debug-info-variable: '!0', + debug-info-expression: '!DIExpression(DW_OP_stack_value)', debug-info-location: '!2' } +constants: + - id: 5 + value: 'i64 18446744073709551615' + alignment: 8 + - id: 9 + value: 'test-constant(1234)' + alignment: 8 + isTargetSpecific: true + - id: 11 + value: '%pair { i8 1, i64 2 }' +machineFunctionInfo: { frame-base: 64, mode: test } +jumpTable: + kind: block-address + entries: + - { id: 6, blocks: [ '%bb.0' ] } +machineMetadataNodes: + - '!10 = distinct !{!0, !"machine", null}' +body: | + bb.0.entry (%ir-block.entry): + %7:gpr64 = TEST_LOAD $g0 :: (load syncscope("agent") acquire 8 from %ir.slot, align 8) + TEST_GLOBAL @ordinary + TEST_BRANCH %bb.0.entry +)"; + +constexpr std::string_view HeterogeneousPointerDocument = R"(--- | + target datalayout = "e-p:64:64-p1:32:32" + target triple = "test64-unknown-linux-gnu" + + define void @pointer_layout() { + ret void + } +--- +name: pointer_layout +constants: + - id: 0 + value: 'ptr addrspace(1) null' +body: | + bb.0: + %0:gpr64(p1) = TEST_COPY $g0 +)"; + +void printDiagnostics(const rcc::mir::MIRDocumentParseResult &Parsed) { + for (const rcc::TextDiagnostic &Diagnostic : Parsed.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; +} + +void testDocumentTransaction(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "MIR document target must be valid"); + if (!Target) + return; + + auto Parsed = rcc::mir::parseMIRDocument(*Target, "document.mir", std::string(ValidDocument)); + Context.expect(Parsed.succeeded(), "complete MIR document must parse successfully"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + + const rcc::mir::MIRDocument *Document = Parsed.document(); + Context.expect( + Document != nullptr && Document->embeddedIR() != nullptr && + Document->unit().functionCount() == 1 && Document->functionDocumentInfo().size() == 1 && + Document->syncScopes().size() == 1 && Document->syncScopes().front().Name == "agent", + "successful parsing must publish the complete document atomically"); + if (Document == nullptr) + return; + const rcc::MachineFunction *Function = Document->unit().function("documented"); + Context.expect(Function != nullptr, "parsed function must be queryable by name"); + if (Function == nullptr) + return; + + Context.expect(Function->registerInfo().virtualRegisters().size() == 1 && + Function->registerInfo().liveIns().size() == 1 && + Function->records().hasCalleeSavedRegisters(), + "register tables must publish into the common MachineIR model"); + Context.expect(Function->frameInfo().liveObjectCount() == 2 && + Function->frameInfo().state().StackSize == 32 && + Function->frameInfo().state().SavePoints.size() == 1, + "frame records and stack objects must publish together"); + const rcc::StackObjectDesc *StackDescription = + Function->frameInfo().object(rcc::FrameIndex::fromValue(1))->description(); + Context.expect(StackDescription != nullptr && StackDescription->DebugInfo.has_value() && + StackDescription->DebugInfo->Expression.has_value() && + StackDescription->DebugInfo->Expression->expression() != nullptr && + StackDescription->DebugInfo->Expression->expression()->elements().size() == + 3 && + StackDescription->DebugInfo->Location.has_value() && + StackDescription->DebugInfo->Location->inlineLocation() != nullptr && + Function->records().entryValues().size() == 1, + "stack and entry-value debug metadata must retain structured values"); + const auto *TargetState = Function->getTargetInfo<DocumentTargetState>(); + const rcc::MachineConstantPoolEntryDesc *TargetConstantDescription = + Function->constantPool().entry(rcc::ConstantPoolIndex::fromValue(1))->description(); + const auto *TargetConstantValue = + TargetConstantDescription == nullptr + ? nullptr + : std::get_if<rcc::TargetConstant>(&TargetConstantDescription->Value); + Context.expect(Function->constantPool().liveEntryCount() == 3 && + Function->jumpTableInfo().liveTableCount() == 1 && + Function->metadata().nodes().size() == 1 && TargetState != nullptr && + TargetState->frameBase() == 64 && TargetConstantValue != nullptr && + TargetConstantValue->getIf<DocumentTargetConstant>() != nullptr && + TargetConstantValue->getIf<DocumentTargetConstant>()->value() == 1234, + "constant, target-state, jump-table, and metadata tables must publish together"); + Context.expect(Function->blockCount() == 1 && Function->allInstructions().size() == 3, + "the body must use the YAML predeclared text slots"); + + auto Printed = rcc::mir::printMIRDocument(*Document, *Target); + Context.expect(Printed.has_value(), "complete MIR document printing must succeed"); + if (!Printed) { + std::cerr << "error: " << Printed.error().message() << '\n'; + return; + } + auto RoundTrip = rcc::mir::parseMIRDocument(*Target, "round-trip.mir", std::string(*Printed)); + Context.expect(RoundTrip.succeeded(), "printed complete MIR must parse successfully"); + if (!RoundTrip.succeeded()) { + std::cerr << *Printed; + printDiagnostics(RoundTrip); + return; + } + auto PrintedAgain = rcc::mir::printMIRDocument(*RoundTrip.document(), *Target); + Context.expect(PrintedAgain.has_value() && *PrintedAgain == *Printed, + "complete MIR document printing must be deterministic"); + + auto Readable = rcc::mir::printMIRDocument( + *Document, *Target, + rcc::mir::MIRDocumentPrinterOptions{.Mode = rcc::mir::MIPrintMode::Readable, + .EmitEmbeddedIR = true, + .DataLayout = {}, + .DefaultPointerSizeInBits = 0}); + Context.expect(Readable.has_value(), "readable MIR document printing must succeed"); + if (!Readable) + return; + auto ReadableRoundTrip = + rcc::mir::parseMIRDocument(*Target, "readable.mir", std::string(*Readable)); + Context.expect(ReadableRoundTrip.succeeded(), "readable MIR output must parse successfully"); + if (!ReadableRoundTrip.succeeded()) + printDiagnostics(ReadableRoundTrip); + + auto WithoutEmbeddedIR = rcc::mir::printMIRDocument( + *Document, *Target, + rcc::mir::MIRDocumentPrinterOptions{.Mode = rcc::mir::MIPrintMode::Complete, + .EmitEmbeddedIR = false, + .DataLayout = {}, + .DefaultPointerSizeInBits = 0}); + Context.expect(!WithoutEmbeddedIR && WithoutEmbeddedIR.error().message().find( + "requires embedded LLVM IR") != std::string_view::npos, + "printer must reject output that omits required embedded LLVM IR"); +} + +void testHeterogeneousPointerLayout(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "heterogeneous-layout target must be valid"); + if (!Target) + return; + + rcc::mir::MIRDocumentParserOptions ParserOptions; + ParserOptions.MachineInstructions.VerifyAfterParse = false; + ParserOptions.VerifyMachineFunctions = false; + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "heterogeneous-layout.mir", + std::string(HeterogeneousPointerDocument), ParserOptions); + Context.expect(Parsed.succeeded(), "heterogeneous pointer-layout MIR must parse successfully"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + + const rcc::MachineFunction *Function = Parsed.document()->unit().function("pointer_layout"); + auto PointerType = rcc::LLT::makePointer(1, 32); + const rcc::MachineConstantPoolEntry *Constant = + Function == nullptr ? nullptr + : Function->constantPool().entry(rcc::ConstantPoolIndex::fromValue(0)); + const rcc::MachineConstantPoolEntryDesc *ConstantDescription = + Constant == nullptr ? nullptr : Constant->description(); + Context.expect(Function != nullptr && PointerType.has_value() && + Function->registerInfo().virtualRegisters().size() == 1 && + Function->registerInfo().virtualRegisters().front().Type == *PointerType && + ConstantDescription != nullptr && ConstantDescription->Size == 4, + "embedded data layout must determine p1 register and generic-constant widths"); + + rcc::mir::MIRDocumentPrinterOptions PrinterOptions; + PrinterOptions.EmitEmbeddedIR = false; + auto WithoutLayout = rcc::mir::printMIRDocument(*Parsed.document(), *Target, PrinterOptions); + Context.expect(!WithoutLayout && WithoutLayout.error().message().find("printer DataLayout") != + std::string_view::npos, + "omitting embedded IR must reject layout-dependent MIR without DataLayout"); + + PrinterOptions.DataLayout = "e-p:64:64-p1:32:32"; + auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target, PrinterOptions); + Context.expect(Printed.has_value() && Printed->find("p1:32") == std::string_view::npos && + Printed->find("p1)") != std::string_view::npos, + "matching explicit DataLayout must emit canonical LLVM p1 MIR syntax"); + if (!Printed) { + std::cerr << "error: " << Printed.error().message() << '\n'; + return; + } + + rcc::mir::MIRDocumentParserOptions ReparseOptions; + ReparseOptions.DataLayout = PrinterOptions.DataLayout; + ReparseOptions.MachineInstructions.VerifyAfterParse = false; + ReparseOptions.VerifyMachineFunctions = false; + auto RoundTrip = rcc::mir::parseMIRDocument(*Target, "heterogeneous-layout-round-trip.mir", + std::string(*Printed), ReparseOptions); + const rcc::MachineFunction *RoundTripFunction = + RoundTrip.succeeded() ? RoundTrip.document()->unit().function("pointer_layout") : nullptr; + Context.expect(RoundTrip.succeeded() && RoundTripFunction != nullptr && + RoundTripFunction->registerInfo().virtualRegisters().size() == 1 && + RoundTripFunction->registerInfo().virtualRegisters().front().Type == + *PointerType, + "explicit DataLayout must preserve p1 type when embedded IR is omitted"); + if (!RoundTrip.succeeded()) + printDiagnostics(RoundTrip); + + PrinterOptions.DataLayout = "e-p:64:64-p1:64:64"; + auto Mismatched = rcc::mir::printMIRDocument(*Parsed.document(), *Target, PrinterOptions); + Context.expect(!Mismatched && + Mismatched.error().message().find("exactly match") != std::string_view::npos, + "mismatched output DataLayout must be rejected when embedded IR is omitted"); +} + +void expectReferenceFailure(TestContext &Context, const rcc::TargetDescription &Target, + std::string_view From, std::string_view To, + std::string_view ExpectedDiagnostic) { + std::string Invalid(ValidDocument); + const std::size_t Position = Invalid.find(From); + Context.expect(Position != std::string::npos, + "reference-validation test mutation must match the fixture"); + if (Position == std::string::npos) + return; + Invalid.replace(Position, From.size(), To); + auto Parsed = rcc::mir::parseMIRDocument(Target, "reference.mir", std::move(Invalid)); + const bool HasExpectedDiagnostic = + !Parsed.succeeded() && Parsed.document() == nullptr && Parsed.diagnostics().size() == 1 && + Parsed.diagnostics().front().Message.find(ExpectedDiagnostic) != std::string::npos; + Context.expect(HasExpectedDiagnostic, + "invalid embedded IR references must fail with their semantic category"); + if (!HasExpectedDiagnostic) + printDiagnostics(Parsed); +} + +void testEmbeddedReferenceValidation(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "reference-validation target must be valid"); + if (!Target) + return; + + expectReferenceFailure(Context, *Target, "from %ir.slot", "from %ir.missing", + "undefined embedded LLVM IR value '%missing'"); + expectReferenceFailure(Context, *Target, "TEST_GLOBAL @ordinary", "TEST_GLOBAL @missing", + "undefined embedded LLVM IR global 'missing'"); + expectReferenceFailure(Context, *Target, "value: 'i64 18446744073709551615'", + "value: 'ptr @missing'", + "generic constant references an undefined embedded LLVM IR global"); + expectReferenceFailure(Context, *Target, "value: 'i64 18446744073709551615'", + "value: 'ptr blockaddress(@documented, %missing)'", + "undefined embedded LLVM IR block 'missing'"); + expectReferenceFailure(Context, *Target, "debug-info-location: '!2'", "debug-info-location: '!0'", + "debug location requires DILocation metadata"); +} + +void expectNoEmbeddedIRReferenceFailure(TestContext &Context, const rcc::TargetDescription &Target, + std::string_view DocumentText, + std::string_view ExpectedDiagnostic) { + auto Parsed = + rcc::mir::parseMIRDocument(Target, "no-embedded-reference.mir", std::string(DocumentText)); + const bool Rejected = + !Parsed.succeeded() && Parsed.document() == nullptr && Parsed.diagnostics().size() == 1 && + Parsed.diagnostics().front().Message.find(ExpectedDiagnostic) != std::string::npos; + Context.expect(Rejected, "IR-specific MIR references without embedded LLVM IR must be rejected"); + if (!Rejected) + printDiagnostics(Parsed); +} + +void testReferencesWithoutEmbeddedIR(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "no-embedded-reference target must be valid"); + if (!Target) + return; + + constexpr std::string_view ExternalDocument = R"(--- +name: external_symbol +body: | + bb.0: + TEST_GLOBAL &runtime_symbol +)"; + auto External = + rcc::mir::parseMIRDocument(*Target, "external-symbol.mir", std::string(ExternalDocument)); + Context.expect(External.succeeded(), + "external symbols must remain usable without embedded LLVM IR"); + if (!External.succeeded()) { + printDiagnostics(External); + return; + } + auto Printed = rcc::mir::printMIRDocument(*External.document(), *Target); + Context.expect(Printed.has_value(), + "external-symbol MIR without embedded IR must remain printable"); + if (!Printed) { + std::cerr << "error: " << Printed.error().message() << '\n'; + } else { + auto RoundTrip = + rcc::mir::parseMIRDocument(*Target, "external-round-trip.mir", std::move(*Printed)); + Context.expect(RoundTrip.succeeded(), + "external-symbol MIR without embedded IR must remain reparseable"); + if (!RoundTrip.succeeded()) + printDiagnostics(RoundTrip); + } + + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: global_reference +body: | + bb.0: + TEST_GLOBAL @ordinary +)", + "requires embedded LLVM IR to resolve global"); + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: ir_block_reference +body: | + bb.0 (%ir-block.entry): +)", + "machine basic block requires embedded LLVM IR"); + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: alloca_reference +stack: + - { id: 0, name: slot, size: 8, alignment: 8 } +body: | + bb.0: +)", + "stack object requires embedded LLVM IR"); + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: ir_value_reference +body: | + bb.0: + $g1 = TEST_LOAD $g0 :: (load 8 from %ir.slot, align 8) +)", + "requires embedded LLVM IR to resolve IR value"); + expectNoEmbeddedIRReferenceFailure( + Context, *Target, R"(--- +name: constant_reference +constants: + - { id: 0, value: 'i64 ptrtoint (ptr @ordinary to i64)', alignment: 8 } +body: | + bb.0: +)", + "generic constant requires embedded LLVM IR to resolve global"); + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: block_address_reference +constants: + - { id: 0, value: 'i64 ptrtoint (ptr blockaddress(@function, %entry) to i64)', alignment: 8 } +body: | + bb.0: +)", + "generic constant blockaddress requires embedded LLVM IR"); +} + +void testPrinterRejectsUnresolvedProgrammaticReferences(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "programmatic-reference target must be valid"); + if (!Target) + return; + + rcc::CompilationUnit Unit; + auto Function = Unit.createMachineFunction("programmatic_reference"); + Context.expect(Function.has_value(), "programmatic document function creation must succeed"); + if (!Function) + return; + auto Block = (*Function)->createBasicBlock(); + auto Appended = Block ? (*Function)->appendBlock(**Block) : rcc::Expected<void>{}; + Context.expect(Block && Appended.has_value(), + "programmatic document block creation must succeed"); + if (!Block || !Appended) + return; + + rcc::MachineIRBuilder Builder(**Function, *Target); + auto Insertion = Builder.setInsertPoint(**Block); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(GlobalOpcode)); + auto Added = Pending ? Pending->addGlobalSymbol("ordinary") : rcc::Expected<void>{}; + Context.expect(Insertion && Pending && Added.has_value(), + "programmatic global-reference instruction construction must succeed"); + if (!Insertion || !Pending || !Added) + return; + auto Built = Pending->build(); + Context.expect(Built.has_value(), "programmatic global-reference instruction must build"); + if (!Built) + return; + + rcc::mir::MIRDocument Document(std::move(Unit)); + auto Printed = rcc::mir::printMIRDocument(Document, *Target); + const bool Rejected = + !Printed && Printed.error().message().find("requires embedded LLVM IR to resolve global") != + std::string_view::npos; + Context.expect(Rejected, + "printer must reject programmatic IR-specific references without embedded IR"); + if (!Rejected && !Printed) + std::cerr << "error: " << Printed.error().message() << '\n'; +} + +void testMetadataSlotPriorityAndCanonicalization(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "metadata-priority target must be valid"); + if (!Target) + return; + + std::string CollisionDocument(ValidDocument); + constexpr std::string_view Original = " - '!10 = distinct !{!0, !\"machine\", null}'"; + constexpr std::string_view Replacement = " - '!0 = distinct !{!0, !\"machine\", null}'"; + const std::size_t Position = CollisionDocument.find(Original); + Context.expect(Position != std::string::npos, + "metadata-priority test mutation must match the fixture"); + if (Position == std::string::npos) + return; + CollisionDocument.replace(Position, Original.size(), Replacement); + + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "metadata-priority.mir", std::move(CollisionDocument)); + Context.expect(Parsed.succeeded(), + "machine metadata may reuse an embedded LLVM IR metadata slot"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + const rcc::MachineFunction *Function = Parsed.document()->unit().function("documented"); + const auto Nodes = Function == nullptr ? std::span<const rcc::MachineMetadataNode>{} + : Function->metadata().nodes(); + const rcc::MachineMetadataReference *Reference = + Nodes.size() == 1 && Nodes.front().Elements.size() == 3 + ? Nodes.front().Elements.front().reference() + : nullptr; + Context.expect(Function != nullptr && Nodes.size() == 1 && Nodes.front().Slot == 0 && + Reference != nullptr && + *Reference == rcc::MachineMetadataReference::embeddedIR(0), + "bare metadata references must prefer embedded LLVM IR slots"); + + auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target); + const bool Canonicalized = + Printed && Printed->find("'!3 = distinct !{!0, !\"machine\", null}'") != std::string::npos; + Context.expect(Canonicalized, + "printer must assign a noncolliding metadata slot for local metadata"); + if (!Canonicalized) + return; + auto RoundTrip = + rcc::mir::parseMIRDocument(*Target, "metadata-priority-round-trip.mir", std::move(*Printed)); + Context.expect(RoundTrip.succeeded(), + "canonicalized metadata slots must survive a document round-trip"); + if (!RoundTrip.succeeded()) + printDiagnostics(RoundTrip); +} + +void testFixedWidthDocumentFields(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "fixed-width field target must be valid"); + if (!Target) + return; + + constexpr std::string_view Original = "maxCallFrameSize: 24"; + std::string BoundaryDocument(ValidDocument); + const std::size_t BoundaryPosition = BoundaryDocument.find(Original); + Context.expect(BoundaryPosition != std::string::npos, + "fixed-width boundary test mutation must match the fixture"); + if (BoundaryPosition == std::string::npos) + return; + BoundaryDocument.replace(BoundaryPosition, Original.size(), "maxCallFrameSize: 4294967295"); + auto Boundary = + rcc::mir::parseMIRDocument(*Target, "frame-boundary.mir", std::move(BoundaryDocument)); + Context.expect(Boundary.succeeded(), + "maximum 32-bit maxCallFrameSize must remain representable in MIR"); + if (Boundary.succeeded()) { + auto Printed = rcc::mir::printMIRDocument(*Boundary.document(), *Target); + Context.expect(Printed && Printed->find("maxCallFrameSize: 4294967295") != std::string::npos, + "maximum 32-bit maxCallFrameSize must print without narrowing"); + } else { + printDiagnostics(Boundary); + } + + std::string OversizedDocument(ValidDocument); + const std::size_t OversizedPosition = OversizedDocument.find(Original); + if (OversizedPosition == std::string::npos) + return; + OversizedDocument.replace(OversizedPosition, Original.size(), "maxCallFrameSize: 4294967296"); + auto Oversized = + rcc::mir::parseMIRDocument(*Target, "frame-oversized.mir", std::move(OversizedDocument)); + const bool Rejected = + !Oversized.succeeded() && Oversized.document() == nullptr && + Oversized.diagnostics().size() == 1 && + Oversized.diagnostics().front().Message.find("maximum call-frame size") != std::string::npos; + Context.expect(Rejected, + "maxCallFrameSize values beyond the fixed 32-bit MIR range must be rejected"); + if (!Rejected) + printDiagnostics(Oversized); +} + +void testAtomicFailure(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + std::string Invalid(ValidDocument); + Invalid.append("---\nname: second\nunknownField: true\n"); + auto Parsed = rcc::mir::parseMIRDocument(*Target, "atomic.mir", std::move(Invalid)); + Context.expect(!Parsed.succeeded() && Parsed.document() == nullptr && + Parsed.diagnostics().size() == 1 && + Parsed.diagnostics().front().Message.find("unknown machine function key") != + std::string::npos, + "a later function failure must prevent publication of the entire document"); +} + +void testLLVMPropertySemantics(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "property-semantics target must be valid"); + if (!Target) + return; + + constexpr std::string_view PropertyDocument = R"(--- +name: property_state +failedISel: true +failsVerification: true +body: | + bb.0: +)"; + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "properties.mir", std::string(PropertyDocument)); + Context.expect(Parsed.succeeded(), "LLVM MIR pipeline properties must parse"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + const rcc::MachineFunction *Function = Parsed.document()->unit().function("property_state"); + Context.expect(Function != nullptr && + Function->properties().has(rcc::MachineFunctionProperty::FailedISel) && + Function->properties().has(rcc::MachineFunctionProperty::FailsVerification) && + Function->compilationStatus().state() == rcc::MachineCompilationState::Success, + "failedISel and failsVerification must be pipeline properties, not run failures"); + + auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target); + Context.expect(Printed.has_value() && Printed->find("failedISel: true") != std::string::npos && + Printed->find("failsVerification: true") != std::string::npos, + "LLVM MIR pipeline properties must print deterministically"); + + constexpr std::string_view InvalidDocument = R"(--- +name: still_checked +failsVerification: true +noVRegs: true +registers: + - { id: 1, class: gpr64 } +body: | + bb.0: +)"; + auto Invalid = + rcc::mir::parseMIRDocument(*Target, "properties-invalid.mir", std::string(InvalidDocument)); + const bool Rejected = !Invalid.succeeded() && Invalid.document() == nullptr && + Invalid.diagnostics().size() == 1 && + Invalid.diagnostics().front().Message.find("noVRegs") != std::string::npos; + Context.expect(Rejected, "failsVerification must not bypass RCC's mandatory input validation"); + if (!Rejected) + printDiagnostics(Invalid); +} + +void testBodyDiagnosticLocation(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "body-diagnostic target must be valid"); + if (!Target) + return; + + constexpr std::string_view InvalidDocument = R"(--- +name: body_diagnostic +body: | + bb.0: + UNKNOWN_OPCODE +)"; + const std::size_t ExpectedOffset = InvalidDocument.find("UNKNOWN_OPCODE"); + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "body-diagnostic.mir", std::string(InvalidDocument)); + const bool PointsAtToken = !Parsed.succeeded() && Parsed.document() == nullptr && + Parsed.diagnostics().size() == 1 && + Parsed.diagnostics().front().Category == "mir-body" && + Parsed.diagnostics().front().Range.Begin.Offset == ExpectedOffset; + Context.expect(PointsAtToken, + "body parser errors must retain the source position of the failing token"); + if (!PointsAtToken) + printDiagnostics(Parsed); +} + +} // namespace + +int main() { + TestContext Context; + testDocumentTransaction(Context); + testHeterogeneousPointerLayout(Context); + testEmbeddedReferenceValidation(Context); + testReferencesWithoutEmbeddedIR(Context); + testPrinterRejectsUnresolvedProgrammaticReferences(Context); + testMetadataSlotPriorityAndCanonicalization(Context); + testFixedWidthDocumentFields(Context); + testAtomicFailure(Context); + testLLVMPropertySemantics(Context); + testBodyDiagnosticLocation(Context); + return Context.result(); +} |
