aboutsummaryrefslogtreecommitdiff
path: root/backend/unittests/MIR
diff options
context:
space:
mode:
Diffstat (limited to 'backend/unittests/MIR')
-rw-r--r--backend/unittests/MIR/CMakeLists.txt46
-rw-r--r--backend/unittests/MIR/EmbeddedIRTest.cpp143
-rw-r--r--backend/unittests/MIR/MILexerTest.cpp417
-rw-r--r--backend/unittests/MIR/MIParserTest.cpp640
-rw-r--r--backend/unittests/MIR/MIRDocumentTest.cpp1148
5 files changed, 2394 insertions, 0 deletions
diff --git a/backend/unittests/MIR/CMakeLists.txt b/backend/unittests/MIR/CMakeLists.txt
new file mode 100644
index 0000000..8ff0527
--- /dev/null
+++ b/backend/unittests/MIR/CMakeLists.txt
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+add_executable(RCCBackendEmbeddedIRTests EmbeddedIRTest.cpp)
+rcc_backend_configure_target(RCCBackendEmbeddedIRTests)
+target_link_libraries(RCCBackendEmbeddedIRTests PRIVATE RCCBackend::MIR)
+
+add_test(NAME RCCBackend.MIR.EmbeddedIR COMMAND RCCBackendEmbeddedIRTests)
+
+add_executable(RCCBackendMILexerTests MILexerTest.cpp)
+rcc_backend_configure_target(RCCBackendMILexerTests)
+target_include_directories(
+ RCCBackendMILexerTests
+ PRIVATE "${RCC_BACKEND_SOURCE_DIR}/lib/CodeGen/MIRParser"
+)
+target_link_libraries(RCCBackendMILexerTests PRIVATE RCCBackend::MIR)
+
+add_test(NAME RCCBackend.MIR.MILexer COMMAND RCCBackendMILexerTests)
+
+add_executable(RCCBackendMIParserTests MIParserTest.cpp)
+rcc_backend_configure_target(RCCBackendMIParserTests)
+target_link_libraries(
+ RCCBackendMIParserTests
+ PRIVATE
+ RCCBackend::MIR
+ RCCBackend::TestTarget
+)
+
+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}/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
new file mode 100644
index 0000000..bbac841
--- /dev/null
+++ b/backend/unittests/MIR/EmbeddedIRTest.cpp
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/MIR/EmbeddedIR.h"
+
+#include <iostream>
+#include <string>
+#include <string_view>
+
+namespace {
+
+int fail(const char *Message) {
+ std::cerr << Message << '\n';
+ return 1;
+}
+
+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"
+ "define void @\"function\\20name\"(\n"
+ " { i32, i32 } %argument\n"
+ ") {\n"
+ "entry:\n"
+ " %slot = alloca i32, align 4\n"
+ " %not_an_alloca = add i32 %argument, 2\n"
+ " br label %\"exit\\20block\"\n"
+ "\"exit\\20block\":\n"
+ " ret void\n"
+ "}\n"
+ "!0 = !DILocation(line: 1, scope: !1)\n"
+ "!llvm.module.flags = !{!0}\n";
+
+ rcc::mir::EmbeddedIRIndexResult Result = rcc::mir::indexEmbeddedIR("embedded.ll", Input);
+ const rcc::mir::EmbeddedIRReferenceIndex *Index = Result.index();
+ if (!Result.succeeded() || Index == nullptr)
+ return fail("Embedded IR indexing must accept the MIR reference subset");
+ if (Index->source().text() != Input)
+ return fail("Embedded IR indexing must preserve the original text");
+ if (Index->targetTriple() != std::optional<std::string_view>("riscv64-unknown-linux-gnu") ||
+ Index->dataLayout() != std::optional<std::string_view>("e-m:e-p:64:64"))
+ return fail("Embedded IR indexing must preserve target facts");
+ if (Index->functions().size() != 2 || Index->function("declared") == nullptr ||
+ Index->function("declared")->isDefinition())
+ return fail("Embedded IR indexing must retain function declarations");
+
+ const rcc::mir::EmbeddedIRFunction *Function = Index->function("function name");
+ if (Function == nullptr || !Function->isDefinition() || Function->blocks().size() != 2)
+ return fail("Embedded IR indexing must decode function and block identifiers");
+ if (Function->block("entry") == nullptr || Function->block("exit block") == nullptr)
+ return fail("Embedded IR block lookup must use decoded names");
+ 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"))
+ return fail("Embedded IR global lookup must include objects and functions but ignore comments");
+ 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;
+}
+
+int testDeclarationDefinitionMerge() {
+ const std::string Input = "declare void @shared()\n"
+ "define void @shared() {\n"
+ "0:\n"
+ " ret void\n"
+ "}\n";
+ rcc::mir::EmbeddedIRIndexResult Result = rcc::mir::indexEmbeddedIR("merge.ll", Input);
+ const rcc::mir::EmbeddedIRReferenceIndex *Index = Result.index();
+ if (!Result.succeeded() || Index == nullptr || Index->functions().size() != 1)
+ return fail("Embedded IR indexing must merge a declaration with its definition");
+ const rcc::mir::EmbeddedIRFunction *Function = Index->function("shared");
+ if (Function == nullptr || !Function->isDefinition() || Function->block("0") == nullptr)
+ return fail("A later definition must complete its indexed declaration");
+ return 0;
+}
+
+bool hasSingleDiagnostic(const rcc::mir::EmbeddedIRIndexResult &Result, std::string_view Message) {
+ return !Result.succeeded() && Result.diagnostics().size() == 1 &&
+ Result.diagnostics()[0].Message == Message;
+}
+
+int testDiagnostics() {
+ auto DuplicateBlock =
+ rcc::mir::indexEmbeddedIR("blocks.ll", "define void @f() {\nentry:\nentry:\n ret void\n}\n");
+ if (!hasSingleDiagnostic(DuplicateBlock, "duplicate embedded LLVM IR basic block label") ||
+ DuplicateBlock.diagnostics()[0].Range.Begin.Line != 3)
+ return fail("Embedded IR indexing must reject a duplicate block at its second label");
+
+ auto BadEscape = rcc::mir::indexEmbeddedIR("escape.ll", "@\"bad\\q0\" = global i8 0\n");
+ if (!hasSingleDiagnostic(BadEscape,
+ "LLVM IR identifier escape must contain two hexadecimal digits"))
+ return fail("Embedded IR indexing must reject malformed identifier escapes");
+
+ auto Unterminated =
+ rcc::mir::indexEmbeddedIR("function.ll", "define void @f() {\nentry:\n ret void\n");
+ if (!hasSingleDiagnostic(Unterminated,
+ "embedded LLVM IR function definition has no closing brace"))
+ return fail("Embedded IR indexing must reject an unterminated function definition");
+
+ auto DuplicateTriple =
+ rcc::mir::indexEmbeddedIR("triple.ll", "target triple = \"riscv64-unknown-linux-gnu\"\n"
+ "target triple = \"riscv64-unknown-linux-gnu\"\n");
+ if (!hasSingleDiagnostic(DuplicateTriple, "duplicate embedded LLVM IR target triple"))
+ return fail("Embedded IR indexing must reject duplicate target facts");
+
+ auto NameConflict =
+ rcc::mir::indexEmbeddedIR("names.ll", "@same = global i32 0\ndeclare void @same()\n");
+ if (!hasSingleDiagnostic(NameConflict,
+ "embedded LLVM IR name is used by both a function and a global object"))
+ return fail("Embedded IR indexing must reject conflicting global value kinds");
+
+ const std::string NulInput("target triple = \"x\"\0", 20);
+ auto Nul = rcc::mir::indexEmbeddedIR("nul.ll", NulInput);
+ if (!hasSingleDiagnostic(Nul, "NUL bytes are not allowed in embedded LLVM IR"))
+ return fail("Embedded IR indexing must reject embedded NUL bytes");
+ return 0;
+}
+
+} // namespace
+
+int main() {
+ if (const int Result = testReferenceIndex(); Result != 0)
+ return Result;
+ if (const int Result = testDeclarationDefinitionMerge(); Result != 0)
+ return Result;
+ return testDiagnostics();
+}
diff --git a/backend/unittests/MIR/MILexerTest.cpp b/backend/unittests/MIR/MILexerTest.cpp
new file mode 100644
index 0000000..46b9cae
--- /dev/null
+++ b/backend/unittests/MIR/MILexerTest.cpp
@@ -0,0 +1,417 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "MILexer.h"
+
+#include "rcc/Support/TextDiagnostic.h"
+
+#include <array>
+#include <cstddef>
+#include <iostream>
+#include <optional>
+#include <string>
+#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';
+ ++FailureCount;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return FailureCount == 0 ? 0 : 1;
+ }
+
+private:
+ int FailureCount = 0;
+};
+
+using rcc::mir::MITokenKind;
+
+struct ExpectedToken final {
+ MITokenKind Kind;
+ std::string_view Spelling;
+ std::optional<std::string_view> StringValue;
+ std::optional<std::string_view> IntegerValue;
+};
+
+void expectToken(TestContext &Context, rcc::mir::MILexer &Lexer, const ExpectedToken &Expected) {
+ const rcc::mir::MIToken Token = Lexer.next();
+ Context.expect(Token.kind() == Expected.Kind && Token.spelling() == Expected.Spelling,
+ "MI token kind and spelling must match the fixed grammar");
+ Context.expect(Token.hasStringValue() == Expected.StringValue.has_value() &&
+ (!Expected.StringValue || Token.stringValue() == *Expected.StringValue),
+ "MI token string payload must be exact");
+ Context.expect(Token.hasIntegerValue() == Expected.IntegerValue.has_value() &&
+ (!Expected.IntegerValue || Token.integerValue() == *Expected.IntegerValue),
+ "MI token integer spelling must remain exact");
+}
+
+void expectEnd(TestContext &Context, rcc::mir::MILexer &Lexer) {
+ const rcc::mir::MIToken Token = Lexer.next();
+ Context.expect(Token.kind() == MITokenKind::EndOfFile && Token.spelling().empty(),
+ "MI lexer must terminate with an empty end-of-file token");
+}
+
+void testPunctuationAndTrivia(TestContext &Context) {
+ rcc::mir::MILexer Lexer("punctuation.mir",
+ " \t; line comment\r\n/* first */ /* second\ncomment */ , = _ : :: . ! "
+ "( ) { } + - < >");
+
+ const std::array Expected{
+ ExpectedToken{MITokenKind::Newline, "\r\n", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Comma, ",", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Equal, "=", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Underscore, "_", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Colon, ":", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::DoubleColon, "::", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Dot, ".", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Exclaim, "!", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::LeftParen, "(", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::RightParen, ")", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::LeftBrace, "{", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::RightBrace, "}", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Plus, "+", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Minus, "-", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Less, "<", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::Greater, ">", std::nullopt, std::nullopt},
+ };
+ for (const ExpectedToken &Token : Expected)
+ expectToken(Context, Lexer, Token);
+ expectEnd(Context, Lexer);
+
+ Context.expect(!Lexer.failed() && Lexer.diagnostics().empty() &&
+ Lexer.tokenCount() == Expected.size(),
+ "whitespace and both MIR comment forms must not create tokens");
+}
+
+void testKeywordCorpus(TestContext &Context) {
+ // Compatibility corpus from llvm/lib/CodeGen/MIRParser/MILexer.cpp at
+ // 2b40c303e79f64c5c7642032554eda026594d604.
+ constexpr std::array Keywords{
+ std::string_view{"implicit"},
+ std::string_view{"implicit-def"},
+ std::string_view{"def"},
+ std::string_view{"dead"},
+ std::string_view{"killed"},
+ std::string_view{"undef"},
+ std::string_view{"internal"},
+ std::string_view{"early-clobber"},
+ std::string_view{"debug-use"},
+ std::string_view{"renamable"},
+ std::string_view{"tied-def"},
+ std::string_view{"frame-setup"},
+ std::string_view{"frame-destroy"},
+ std::string_view{"nnan"},
+ std::string_view{"ninf"},
+ std::string_view{"nsz"},
+ std::string_view{"arcp"},
+ std::string_view{"contract"},
+ std::string_view{"afn"},
+ std::string_view{"reassoc"},
+ std::string_view{"nuw"},
+ std::string_view{"nsw"},
+ std::string_view{"nusw"},
+ std::string_view{"exact"},
+ std::string_view{"nneg"},
+ std::string_view{"disjoint"},
+ std::string_view{"samesign"},
+ std::string_view{"inbounds"},
+ std::string_view{"nofpexcept"},
+ std::string_view{"unpredictable"},
+ std::string_view{"debug-location"},
+ std::string_view{"debug-instr-number"},
+ std::string_view{"dbg-instr-ref"},
+ std::string_view{"same_value"},
+ std::string_view{"offset"},
+ std::string_view{"rel_offset"},
+ std::string_view{"def_cfa_register"},
+ std::string_view{"def_cfa_offset"},
+ std::string_view{"adjust_cfa_offset"},
+ std::string_view{"escape"},
+ std::string_view{"def_cfa"},
+ std::string_view{"llvm_def_aspace_cfa"},
+ std::string_view{"remember_state"},
+ std::string_view{"restore"},
+ std::string_view{"restore_state"},
+ std::string_view{"undefined"},
+ std::string_view{"register"},
+ std::string_view{"window_save"},
+ std::string_view{"negate_ra_sign_state"},
+ std::string_view{"negate_ra_sign_state_with_pc"},
+ std::string_view{"llvm_register_pair"},
+ std::string_view{"llvm_vector_registers"},
+ std::string_view{"llvm_vector_offset"},
+ std::string_view{"llvm_vector_register_mask"},
+ std::string_view{"blockaddress"},
+ std::string_view{"intrinsic"},
+ std::string_view{"target-index"},
+ std::string_view{"half"},
+ std::string_view{"bfloat"},
+ std::string_view{"float"},
+ std::string_view{"double"},
+ std::string_view{"x86_fp80"},
+ std::string_view{"fp128"},
+ std::string_view{"ppc_fp128"},
+ std::string_view{"target-flags"},
+ std::string_view{"volatile"},
+ std::string_view{"non-temporal"},
+ std::string_view{"dereferenceable"},
+ std::string_view{"invariant"},
+ std::string_view{"align"},
+ std::string_view{"basealign"},
+ std::string_view{"addrspace"},
+ std::string_view{"stack"},
+ std::string_view{"got"},
+ std::string_view{"jump-table"},
+ std::string_view{"constant-pool"},
+ std::string_view{"call-entry"},
+ std::string_view{"custom"},
+ std::string_view{"lanemask"},
+ std::string_view{"liveout"},
+ std::string_view{"landing-pad"},
+ std::string_view{"inlineasm-br-indirect-target"},
+ std::string_view{"ehscope-entry"},
+ std::string_view{"ehfunclet-entry"},
+ std::string_view{"liveins"},
+ std::string_view{"successors"},
+ std::string_view{"floatpred"},
+ std::string_view{"intpred"},
+ std::string_view{"shufflemask"},
+ std::string_view{"pre-instr-symbol"},
+ std::string_view{"post-instr-symbol"},
+ std::string_view{"heap-alloc-marker"},
+ std::string_view{"pcsections"},
+ std::string_view{"cfi-type"},
+ std::string_view{"deactivation-symbol"},
+ std::string_view{"bbsections"},
+ std::string_view{"bb_id"},
+ std::string_view{"unknown-size"},
+ std::string_view{"unknown-address"},
+ std::string_view{"distinct"},
+ std::string_view{"ir-block-address-taken"},
+ std::string_view{"machine-block-address-taken"},
+ std::string_view{"call-frame-size"},
+ std::string_view{"noconvergent"},
+ std::string_view{"mmra"},
+ };
+
+ std::string Source;
+ for (const std::string_view Keyword : Keywords) {
+ Source += Keyword;
+ Source.push_back(' ');
+ }
+ Source += "TEST_OPCODE keyword-like";
+
+ rcc::mir::MILexer Lexer("keywords.mir", std::move(Source));
+ for (const std::string_view Keyword : Keywords) {
+ const rcc::mir::MIToken Token = Lexer.next();
+ Context.expect(Token.kind() == MITokenKind::Keyword && Token.stringValue() == Keyword &&
+ rcc::mir::isMIKeyword(Keyword),
+ "every fixed LLVM MIR keyword must be reserved");
+ }
+ expectToken(Context, Lexer,
+ {MITokenKind::Identifier, "TEST_OPCODE", "TEST_OPCODE", std::nullopt});
+ expectToken(Context, Lexer,
+ {MITokenKind::Identifier, "keyword-like", "keyword-like", std::nullopt});
+ expectEnd(Context, Lexer);
+ Context.expect(!rcc::mir::isMIKeyword("Implicit") && !rcc::mir::isMIKeyword("unknown-keyword"),
+ "MIR keyword classification must remain case-sensitive");
+}
+
+void testMetadataKeywords(TestContext &Context) {
+ constexpr std::array Keywords{
+ std::string_view{"!tbaa"}, std::string_view{"!alias.scope"},
+ std::string_view{"!noalias"}, std::string_view{"!noalias.addrspace"},
+ std::string_view{"!range"}, std::string_view{"!DIExpression"},
+ std::string_view{"!DILocation"},
+ };
+ std::string Source;
+ for (const std::string_view Keyword : Keywords) {
+ Source += Keyword;
+ Source.push_back(' ');
+ }
+ Source += "! !12";
+
+ rcc::mir::MILexer Lexer("metadata.mir", std::move(Source));
+ for (const std::string_view Keyword : Keywords) {
+ const rcc::mir::MIToken Token = Lexer.next();
+ Context.expect(Token.kind() == MITokenKind::MetadataKeyword && Token.stringValue() == Keyword &&
+ rcc::mir::isMIMetadataKeyword(Keyword),
+ "every fixed named metadata token must be recognized");
+ }
+ expectToken(Context, Lexer, {MITokenKind::Exclaim, "!", std::nullopt, std::nullopt});
+ expectToken(Context, Lexer, {MITokenKind::Exclaim, "!", std::nullopt, std::nullopt});
+ expectToken(Context, Lexer, {MITokenKind::IntegerLiteral, "12", std::nullopt, "12"});
+ expectEnd(Context, Lexer);
+}
+
+void testNamedEntities(TestContext &Context) {
+ rcc::mir::MILexer Lexer("entities.mir",
+ R"(bb.12.entry %bb.13.exit %jump-table.2 %stack.3.slot )"
+ R"(%fixed-stack.4 %const.5 %subreg.low32 %ir-block.7 )"
+ R"(%ir-block."quoted\20block" %ir.8 %ir.named %12 %named-vreg $x0 )"
+ R"(@9 @global @"quoted\20global" &external &"quoted\20external" )"
+ R"(<mcsymbol local.label> <mcsymbol "quoted\20symbol"> `i32 7` )"
+ R"("plain\20string\\tail")");
+
+ const std::array Expected{
+ ExpectedToken{MITokenKind::MachineBasicBlockLabel, "bb.12.entry", "entry", "12"},
+ ExpectedToken{MITokenKind::MachineBasicBlock, "%bb.13.exit", "exit", "13"},
+ ExpectedToken{MITokenKind::JumpTableIndex, "%jump-table.2", std::nullopt, "2"},
+ ExpectedToken{MITokenKind::StackObject, "%stack.3.slot", "slot", "3"},
+ ExpectedToken{MITokenKind::FixedStackObject, "%fixed-stack.4", std::nullopt, "4"},
+ ExpectedToken{MITokenKind::ConstantPoolItem, "%const.5", std::nullopt, "5"},
+ ExpectedToken{MITokenKind::SubRegisterIndex, "%subreg.low32", "low32", std::nullopt},
+ ExpectedToken{MITokenKind::IRBlock, "%ir-block.7", std::nullopt, "7"},
+ ExpectedToken{MITokenKind::NamedIRBlock, R"(%ir-block."quoted\20block")", "quoted block",
+ std::nullopt},
+ ExpectedToken{MITokenKind::IRValue, "%ir.8", std::nullopt, "8"},
+ ExpectedToken{MITokenKind::NamedIRValue, "%ir.named", "named", std::nullopt},
+ ExpectedToken{MITokenKind::VirtualRegister, "%12", std::nullopt, "12"},
+ ExpectedToken{MITokenKind::NamedVirtualRegister, "%named-vreg", "named-vreg", std::nullopt},
+ ExpectedToken{MITokenKind::NamedRegister, "$x0", "x0", std::nullopt},
+ ExpectedToken{MITokenKind::GlobalValue, "@9", std::nullopt, "9"},
+ ExpectedToken{MITokenKind::NamedGlobalValue, "@global", "global", std::nullopt},
+ ExpectedToken{MITokenKind::NamedGlobalValue, R"(@"quoted\20global")", "quoted global",
+ std::nullopt},
+ ExpectedToken{MITokenKind::ExternalSymbol, "&external", "external", std::nullopt},
+ ExpectedToken{MITokenKind::ExternalSymbol, R"(&"quoted\20external")", "quoted external",
+ std::nullopt},
+ ExpectedToken{MITokenKind::MCSymbol, "<mcsymbol local.label>", "local.label", std::nullopt},
+ ExpectedToken{MITokenKind::MCSymbol, R"(<mcsymbol "quoted\20symbol">)", "quoted symbol",
+ std::nullopt},
+ ExpectedToken{MITokenKind::QuotedIRValue, "`i32 7`", "i32 7", std::nullopt},
+ ExpectedToken{MITokenKind::StringConstant, R"("plain\20string\\tail")",
+ R"(plain string\tail)", std::nullopt},
+ };
+ for (const ExpectedToken &Token : Expected)
+ expectToken(Context, Lexer, Token);
+ expectEnd(Context, Lexer);
+ Context.expect(!Lexer.failed() && Lexer.diagnostics().empty(),
+ "all fixed named MIR entity forms must lex without diagnostics");
+}
+
+void testNumericalLiterals(TestContext &Context) {
+ rcc::mir::MILexer Lexer("numbers.mir", "-42 17 1. 1.25 -2.5e-3 6.02E23 0xFF 0XK3FFF000000000000 "
+ "f0x3F800000 1e3 0x f0x");
+
+ const std::array Expected{
+ ExpectedToken{MITokenKind::IntegerLiteral, "-42", std::nullopt, "-42"},
+ ExpectedToken{MITokenKind::IntegerLiteral, "17", std::nullopt, "17"},
+ ExpectedToken{MITokenKind::FloatingPointLiteral, "1.", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::FloatingPointLiteral, "1.25", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::FloatingPointLiteral, "-2.5e-3", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::FloatingPointLiteral, "6.02E23", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::HexLiteral, "0xFF", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::FloatingPointLiteral, "0XK3FFF000000000000", std::nullopt,
+ std::nullopt},
+ ExpectedToken{MITokenKind::FloatingPointLiteral, "f0x3F800000", std::nullopt, std::nullopt},
+ ExpectedToken{MITokenKind::IntegerLiteral, "1", std::nullopt, "1"},
+ ExpectedToken{MITokenKind::Identifier, "e3", "e3", std::nullopt},
+ ExpectedToken{MITokenKind::IntegerLiteral, "0", std::nullopt, "0"},
+ ExpectedToken{MITokenKind::Identifier, "x", "x", std::nullopt},
+ ExpectedToken{MITokenKind::Identifier, "f0x", "f0x", std::nullopt},
+ };
+ for (const ExpectedToken &Token : Expected)
+ expectToken(Context, Lexer, Token);
+ expectEnd(Context, Lexer);
+
+ std::string LargeNumber(10000, '9');
+ rcc::mir::MILexer LargeLexer("large-number.mir", LargeNumber);
+ const rcc::mir::MIToken LargeToken = LargeLexer.next();
+ Context.expect(LargeToken.kind() == MITokenKind::IntegerLiteral &&
+ LargeToken.integerValue() == LargeNumber,
+ "integer tokenization must preserve arbitrary-width decimal spelling");
+}
+
+void expectSingleError(TestContext &Context, std::string Source, std::string_view MessageFragment,
+ rcc::mir::MILexerOptions Options = {}) {
+ rcc::mir::MILexer Lexer("invalid.mir", std::move(Source), Options);
+ const rcc::mir::MIToken Token = Lexer.next();
+ Context.expect(Token.kind() == MITokenKind::Error && Lexer.failed() &&
+ Lexer.diagnostics().size() == 1 &&
+ Lexer.diagnostics().front().Category == "mir-lexer" &&
+ Lexer.diagnostics().front().Message.find(MessageFragment) != std::string::npos,
+ "invalid MI input must produce one structured lexer diagnostic");
+ expectEnd(Context, Lexer);
+ Context.expect(Lexer.diagnostics().size() == 1,
+ "reading after an MI lexer failure must not duplicate diagnostics");
+}
+
+void testDiagnosticsAndLimits(TestContext &Context) {
+ expectSingleError(Context, "/* never closed", "closing '*/'");
+ expectSingleError(Context, R"("never closed)", "closing '\"'");
+ expectSingleError(Context, "`never closed", "closing '`'");
+ expectSingleError(Context, "<mcsymbol local", "closed by '>'");
+ expectSingleError(Context, "!unknown", "unknown metadata keyword");
+ expectSingleError(Context, "bb.name", "decimal block number");
+ expectSingleError(Context, "#", "unexpected character '#'");
+ expectSingleError(Context, std::string(1, '\0'), "unexpected byte 0x00");
+ expectSingleError(Context, "four", "length limit",
+ rcc::mir::MILexerOptions{.MaximumTokenLength = 3});
+
+ rcc::mir::MILexer Limited("limited.mir", "one two",
+ rcc::mir::MILexerOptions{.MaximumTokenLength = 0, .MaximumTokens = 1});
+ expectToken(Context, Limited, {MITokenKind::Identifier, "one", "one", std::nullopt});
+ const rcc::mir::MIToken LimitError = Limited.next();
+ Context.expect(LimitError.kind() == MITokenKind::Error && Limited.diagnostics().size() == 1 &&
+ Limited.diagnostics().front().Message.find("token count") !=
+ std::string::npos &&
+ Limited.tokenCount() == 1,
+ "MI lexer token count limits must fail before publishing excess tokens");
+}
+
+void testRangesAndClassifiers(TestContext &Context) {
+ rcc::mir::MILexer Lexer("ranges.mir", "implicit\n $x0 \"flag\"");
+ const rcc::mir::MIToken Flag = Lexer.next();
+ const rcc::mir::MIToken Newline = Lexer.next();
+ const rcc::mir::MIToken Register = Lexer.next();
+ const rcc::mir::MIToken StringFlag = Lexer.next();
+
+ Context.expect(Flag.isRegisterFlag() && !Flag.isRegister() && Flag.range().Begin.Line == 1 &&
+ Flag.range().Begin.Column == 1 && Flag.range().End.Column == 9,
+ "register flags must retain exact source ranges");
+ Context.expect(Newline.kind() == MITokenKind::Newline && Register.isRegister() &&
+ Register.stringValue() == "x0" && Register.range().Begin.Line == 2 &&
+ Register.range().Begin.Column == 3,
+ "line and column tracking must account for skipped indentation");
+ Context.expect(StringFlag.isMemoryOperandFlag() &&
+ rcc::mir::getMITokenKindName(StringFlag.kind()) == "string-constant",
+ "memory operand and token-kind classifiers must cover string flags");
+
+ rcc::mir::MILexer Invalid("body.mir", "ok\n #");
+ static_cast<void>(Invalid.next());
+ static_cast<void>(Invalid.next());
+ static_cast<void>(Invalid.next());
+ const std::string Formatted =
+ rcc::formatTextDiagnostic(Invalid.diagnostics().front(), Invalid.source());
+ Context.expect(Formatted.starts_with("error: body.mir:2:3: unexpected character '#'"),
+ "formatted MI diagnostics must report stable input line and column");
+
+ rcc::mir::MILexer Original("move.mir", R"("owned\20value")");
+ rcc::mir::MILexer Moved(std::move(Original));
+ const rcc::mir::MIToken Owned = Moved.next();
+ Context.expect(Owned.stringValue() == "owned value",
+ "moving an MI lexer must preserve owned decoded token values");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testPunctuationAndTrivia(Context);
+ testKeywordCorpus(Context);
+ testMetadataKeywords(Context);
+ testNamedEntities(Context);
+ testNumericalLiterals(Context);
+ testDiagnosticsAndLimits(Context);
+ testRangesAndClassifiers(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/MIR/MIParserTest.cpp b/backend/unittests/MIR/MIParserTest.cpp
new file mode 100644
index 0000000..61292ac
--- /dev/null
+++ b/backend/unittests/MIR/MIParserTest.cpp
@@ -0,0 +1,640 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/MIRParser/MIParser.h"
+
+#include "Target/TestTarget.h"
+#include "rcc/CodeGen/GenericOpcodes.h"
+#include "rcc/CodeGen/InstructionDescriptor.h"
+#include "rcc/CodeGen/MIRPrinter.h"
+#include "rcc/CodeGen/MachineBasicBlock.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineMemOperand.h"
+#include "rcc/CodeGen/MachineOperand.h"
+#include "rcc/CodeGen/MachineRegisterInfo.h"
+#include "rcc/CodeGen/TargetDescription.h"
+
+#include <cstdint>
+#include <iostream>
+#include <limits>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+constexpr std::uint32_t CopyOpcode = 1;
+constexpr std::uint32_t AddImmediateOpcode = 2;
+constexpr std::uint32_t BranchOpcode = 3;
+constexpr std::uint32_t NopOpcode = 4;
+constexpr std::uint32_t ConstantOpcode = 5;
+constexpr std::uint32_t FloatingConstantOpcode = 6;
+constexpr std::uint32_t MetadataOpcode = 7;
+constexpr std::uint32_t LoadOpcode = 8;
+constexpr std::uint32_t InlineAsmOpcode = 9;
+
+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::RegisterClassID Class = rcc::test::TestRegisterClass::GPR64) {
+ rcc::OperandDescriptor Descriptor;
+ Descriptor.Kinds = {rcc::MachineOperand::Kind::Register};
+ Descriptor.RegisterRole = Role;
+ Descriptor.RegClass = Class;
+ return Descriptor;
+}
+
+[[nodiscard]] rcc::InstructionDescriptor descriptor(std::uint32_t OpcodeValue, std::string Name) {
+ rcc::InstructionDescriptor Descriptor;
+ Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue);
+ Descriptor.Name = std::move(Name);
+ Descriptor.Flags = {rcc::InstructionFlag::ProducesCode};
+ return Descriptor;
+}
+
+[[nodiscard]] rcc::OperandDescriptor genericRegisterOperand(rcc::RegisterOperandRole Role) {
+ rcc::OperandDescriptor Descriptor;
+ Descriptor.Kinds = {rcc::MachineOperand::Kind::Register};
+ Descriptor.RegisterRole = Role;
+ return Descriptor;
+}
+
+[[nodiscard]] rcc::OperandDescriptor operand(rcc::MachineOperand::Kind Kind) {
+ rcc::OperandDescriptor Descriptor;
+ Descriptor.Kinds = {Kind};
+ return Descriptor;
+}
+
+[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() {
+ auto Copy = descriptor(CopyOpcode, "TEST_COPY");
+ Copy.Operands = {
+ registerOperand(rcc::RegisterOperandRole::Def),
+ registerOperand(rcc::RegisterOperandRole::Use),
+ };
+ Copy.NumExplicitDefs = 1;
+
+ auto AddImmediate = descriptor(AddImmediateOpcode, "TEST_ADDI");
+ AddImmediate.Operands = {
+ registerOperand(rcc::RegisterOperandRole::Def),
+ registerOperand(rcc::RegisterOperandRole::Use),
+ };
+ rcc::OperandDescriptor Immediate;
+ Immediate.Kinds = {rcc::MachineOperand::Kind::Immediate};
+ Immediate.AllowedTargetFlags = 0xFU;
+ AddImmediate.Operands.push_back(Immediate);
+ AddImmediate.NumExplicitDefs = 1;
+
+ auto Branch = descriptor(BranchOpcode, "TEST_BRANCH");
+ rcc::OperandDescriptor Block;
+ Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock};
+ Branch.Operands.push_back(Block);
+ Branch.Flags.set(rcc::InstructionFlag::Branch);
+ Branch.Flags.set(rcc::InstructionFlag::Terminator);
+ Branch.Flags.set(rcc::InstructionFlag::Barrier);
+
+ auto Nop = descriptor(NopOpcode, "TEST_NOP");
+
+ auto Constant = descriptor(ConstantOpcode, "TEST_CONSTANT");
+ Constant.Operands = {
+ genericRegisterOperand(rcc::RegisterOperandRole::Def),
+ operand(rcc::MachineOperand::Kind::IntegerImmediate),
+ };
+ Constant.NumExplicitDefs = 1;
+ Constant.Flags.set(rcc::InstructionFlag::Generic);
+ Constant.Flags.set(rcc::InstructionFlag::AllowedPreLegalizer);
+
+ auto FloatingConstant = descriptor(FloatingConstantOpcode, "TEST_FCONSTANT");
+ FloatingConstant.Operands = {
+ genericRegisterOperand(rcc::RegisterOperandRole::Def),
+ operand(rcc::MachineOperand::Kind::FloatingImmediate),
+ };
+ FloatingConstant.NumExplicitDefs = 1;
+ FloatingConstant.Flags.set(rcc::InstructionFlag::Generic);
+ FloatingConstant.Flags.set(rcc::InstructionFlag::AllowedPreLegalizer);
+
+ auto Metadata = descriptor(MetadataOpcode, "TEST_METADATA");
+ Metadata.Operands = {
+ operand(rcc::MachineOperand::Kind::Metadata),
+ operand(rcc::MachineOperand::Kind::Metadata),
+ operand(rcc::MachineOperand::Kind::CFIIndex),
+ };
+
+ auto Load = descriptor(LoadOpcode, "TEST_LOAD");
+ Load.Operands = {
+ registerOperand(rcc::RegisterOperandRole::Def),
+ registerOperand(rcc::RegisterOperandRole::Use),
+ };
+ Load.NumExplicitDefs = 1;
+ Load.Flags.set(rcc::InstructionFlag::MayLoad);
+ Load.RequiresMemoryOperands = true;
+
+ auto InlineAsm = descriptor(InlineAsmOpcode, "INLINEASM");
+ InlineAsm.Operands = {
+ operand(rcc::MachineOperand::Kind::ExternalSymbol),
+ operand(rcc::MachineOperand::Kind::Immediate),
+ };
+
+ std::vector<rcc::InstructionDescriptor> Descriptors;
+ Descriptors.push_back(std::move(Copy));
+ Descriptors.push_back(std::move(AddImmediate));
+ Descriptors.push_back(std::move(Branch));
+ Descriptors.push_back(std::move(Nop));
+ Descriptors.push_back(std::move(Constant));
+ Descriptors.push_back(std::move(FloatingConstant));
+ Descriptors.push_back(std::move(Metadata));
+ Descriptors.push_back(std::move(Load));
+ Descriptors.push_back(std::move(InlineAsm));
+ for (const rcc::InstructionDescriptor &Generic : rcc::genericInstructionDescriptors()) {
+ if (Generic.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::G_ATOMIC_CMPXCHG) ||
+ Generic.InstructionOpcode ==
+ rcc::opcode(rcc::GenericOpcode::G_ATOMIC_CMPXCHG_WITH_SUCCESS)) {
+ Descriptors.push_back(Generic);
+ }
+ }
+ auto Instructions = rcc::InstructionSet::create(std::move(Descriptors));
+ if (!Instructions)
+ return std::unexpected(Instructions.error());
+ return rcc::test::createTestTargetDescription(std::move(*Instructions));
+}
+
+[[nodiscard]] std::unique_ptr<rcc::MachineFunction> createFunction(TestContext &Context,
+ std::uint64_t ID) {
+ auto Function =
+ rcc::MachineFunction::create("parser", rcc::MachineFunctionStableID::fromValue(ID));
+ Context.expect(Function.has_value(), "machine function creation must succeed");
+ if (!Function)
+ return nullptr;
+ return std::move(*Function);
+}
+
+void testTwoPassBodyParsing(TestContext &Context) {
+ auto Target = createTarget();
+ auto Function = createFunction(Context, 1);
+ Context.expect(Target.has_value(), "parser target description must be valid");
+ if (!Target)
+ std::cerr << "error: " << Target.error().message() << '\n';
+ if (!Target || !Function)
+ return;
+ auto MetadataNode =
+ Function->metadata().addNode({.Slot = 100, .IsDistinct = false, .Elements = {}});
+ Context.expect(MetadataNode.has_value(), "metadata fixture must be valid");
+ if (!MetadataNode)
+ return;
+
+ const std::string Body = R"(
+bb.0."entry block" (align 16, bb_id 7.1, call-frame-size 32):
+ liveins: $g0
+ successors: %bb.1."exit block"(2147483648)
+ %0:gpr64 = TEST_COPY $g0
+ %1:gpr64 = frame-setup TEST_ADDI %0, target-flags(test-direct-one, test-bit-four) 4
+ %2:_(s128) = TEST_CONSTANT i128 340282366920938463463374607431768211455
+ %3:_(f64) = TEST_FCONSTANT double f0x400921FB54442D18
+ TEST_METADATA !100, !DIExpression(DW_OP_LLVM_arg, 0), def_cfa $g0, 16, pre-instr-symbol <mcsymbol before>, post-instr-symbol <mcsymbol after>, heap-alloc-marker !0, pcsections !0, mmra !0, cfi-type 17, deactivation-symbol @guard, debug-instr-number 9, debug-location !DILocation(line: 42, column: 7, scope: !0, inlinedAt: !DILocation(line: 8, scope: !0, inlinedAt: !0), isImplicitCode: true, atomGroup: 3, atomRank: 2)
+ %4:gpr64 = TEST_LOAD %0 :: (load 8 from unknown-address, align 8, addrspace 0, !tbaa !0)
+ TEST_BRANCH %bb.1."exit block"
+
+bb.1."exit block" (landing-pad, bbsections Cold):
+ $g1 = TEST_COPY %1
+ TEST_NOP {
+ TEST_NOP
+ }
+)";
+
+ rcc::mir::MIParserContext ParserContext;
+ ParserContext.Metadata.push_back(
+ {.TextID = 0, .Reference = rcc::MachineMetadataReference::embeddedIR(0)});
+ ParserContext.PointerSizeInBits = [](std::uint32_t) -> rcc::Expected<std::uint32_t> {
+ return 64;
+ };
+ rcc::mir::MIParser Parser(*Function, *Target, "body.mir", Body, ParserContext);
+ const auto Parsed = Parser.parse();
+ const bool ParseSucceeded =
+ Parsed.has_value() && !Parser.failed() && Parser.diagnostics().empty();
+ Context.expect(ParseSucceeded,
+ "valid MIR body must pass both parsing passes and final verification");
+ if (!ParseSucceeded) {
+ for (const auto &Diagnostic : Parser.diagnostics())
+ std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n';
+ if (!Parsed)
+ std::cerr << "error: " << Parsed.error().message() << '\n';
+ }
+ if (!Parsed)
+ return;
+
+ Context.expect(Function->blockCount() == 2 && Function->allInstructions().size() == 10,
+ "parser must publish every declared block and instruction");
+ rcc::MachineBasicBlock *Entry = Function->firstBlock();
+ rcc::MachineBasicBlock *Exit = Entry == nullptr ? nullptr : Entry->next();
+ Context.expect(Entry != nullptr && Exit != nullptr && !Exit->next(),
+ "parsed block layout must retain source order");
+ if (Entry == nullptr || Exit == nullptr)
+ return;
+
+ Context.expect(Entry->attributes().name() == "entry block" &&
+ Entry->attributes().alignment().bytes() == 16 &&
+ Entry->attributes().profileID() == rcc::MachineBasicBlockProfileID{7, 1} &&
+ Entry->attributes().callFrameSize() == 32,
+ "first-pass block attributes must be retained");
+ Context.expect(Exit->attributes().has(rcc::MachineBasicBlockAttribute::LandingPad) &&
+ Exit->attributes().sectionID() == rcc::MachineBasicBlockSectionID::cold(),
+ "named block flags and section IDs must be retained");
+ Context.expect(Entry->liveIns().size() == 1 && Entry->successors().size() == 1 &&
+ Entry->successors().front().Block == Exit &&
+ Entry->successors().front().Probability.rawValue() == 2147483648U,
+ "live-ins and explicitly weighted successors must be published");
+
+ const auto Registers = Function->registerInfo().virtualRegisters();
+ Context.expect(Registers.size() == 5 &&
+ Registers[0].RegClass == rcc::test::TestRegisterClass::GPR64 &&
+ Registers[1].RegClass == rcc::test::TestRegisterClass::GPR64 &&
+ Registers[2].Type == *rcc::LowLevelType::makeCategoryNeutralScalar(128) &&
+ Registers[3].Type == *rcc::LowLevelType::makeCategoryNeutralScalar(64) &&
+ Registers[4].RegClass == rcc::test::TestRegisterClass::GPR64,
+ "textual virtual registers must map deterministically to classed MachineIR vregs");
+
+ const rcc::MachineInstr *Add = Entry->firstInstruction()->next();
+ Context.expect(Add->flags().has(rcc::MachineInstrFlag::FrameSetup) &&
+ Add->operandAt(2)->targetFlags() == 0x5U,
+ "instruction and target operand flags must be preserved");
+
+ const rcc::MachineInstr *Constant = Add->next();
+ const auto *Integer = Constant->operandAt(1)->getIf<rcc::IntegerImmediateOperand>();
+ const auto *Floating = Constant->next()->operandAt(1)->getIf<rcc::FloatingImmediateOperand>();
+ Context.expect(Integer != nullptr && Integer->Value.bitWidth() == 128 &&
+ Integer->Value.words().size() == 2 &&
+ Integer->Value.words()[0] == std::numeric_limits<std::uint64_t>::max() &&
+ Integer->Value.words()[1] == std::numeric_limits<std::uint64_t>::max() &&
+ Floating != nullptr &&
+ Floating->Bits.semantics() == rcc::FloatSemantics::IEEEDouble &&
+ Floating->Bits.words()[0] == 0x400921FB54442D18ULL,
+ "typed integer and exact floating-point operands must retain their bit patterns");
+
+ const rcc::MachineInstr *Metadata = Constant->next()->next();
+ const auto *Reference = Metadata->operandAt(0)->getIf<rcc::MetadataOperand>();
+ const auto *Expression = Metadata->operandAt(1)->getIf<rcc::MetadataOperand>();
+ const auto *CFI = Metadata->operandAt(2)->getIf<rcc::CFIIndexOperand>();
+ Context.expect(
+ Reference != nullptr && Reference->Value.reference() != nullptr &&
+ Reference->Value.reference()->scope() == rcc::MachineMetadataScope::MachineFunction &&
+ Reference->Value.reference()->slot() == 100 && Expression != nullptr &&
+ Expression->Value.expression() != nullptr &&
+ Expression->Value.expression()->elements().size() == 2 &&
+ Expression->Value.expression()->elements()[0] == 0x1005U && CFI != nullptr &&
+ Function->cfiInfo().instruction(CFI->Index) != nullptr &&
+ Function->cfiInfo().instruction(CFI->Index)->operation() ==
+ rcc::MachineCFIOperation::DefCFA &&
+ Metadata->preInstrSymbol() == "before" && Metadata->postInstrSymbol() == "after" &&
+ Metadata->heapAllocMarker().has_value() && Metadata->pcSections().has_value() &&
+ Metadata->mmra().has_value() && Metadata->cfiType() == 17 &&
+ Metadata->deactivationSymbol() == "guard" && Metadata->debugInstrNumber() == 9 &&
+ Metadata->debugLocation().has_value() &&
+ Metadata->debugLocation()->inlineLocation() != nullptr &&
+ Metadata->debugLocation()->inlineLocation()->inlineChain().size() == 2 &&
+ Metadata->debugLocation()->inlineLocation()->inlineChain()[0].Line == 42 &&
+ Metadata->debugLocation()->inlineLocation()->inlineChain()[0].Column == 7 &&
+ Metadata->debugLocation()->inlineLocation()->inlineChain()[0].IsImplicitCode &&
+ Metadata->debugLocation()->inlineLocation()->inlineChain()[0].AtomGroup == 3 &&
+ Metadata->debugLocation()->inlineLocation()->inlineChain()[0].AtomRank == 2 &&
+ Metadata->debugLocation()->inlineLocation()->terminalInlinedAt() ==
+ rcc::MachineMetadataReference::embeddedIR(0),
+ "metadata, debug expressions, and CFI operands must retain structured semantics");
+
+ const rcc::MachineInstr *Load = Metadata->next();
+ Context.expect(Load->memOperands().size() == 1 && Load->memOperands().front()->isLoad() &&
+ Load->memOperands().front()->description().Size ==
+ *rcc::MemorySize::fixed(8) &&
+ Load->memOperands().front()->description().Align.bytes() == 8 &&
+ Load->memOperands().front()->description().TBAA.has_value(),
+ "machine memory operands must retain access, size, alignment, and metadata");
+
+ const rcc::MachineInstr *BundleHeader = Exit->firstInstruction()->next();
+ const rcc::MachineInstr *BundleMember = BundleHeader->next();
+ Context.expect(BundleHeader->flags().has(rcc::MachineInstrFlag::BundledWithSucc) &&
+ BundleMember->flags().has(rcc::MachineInstrFlag::BundledWithPred),
+ "instruction bundle braces must produce reciprocal layout flags");
+
+ auto Printed = rcc::mir::printMachineFunctionBody(*Function, *Target, ParserContext);
+ Context.expect(Printed.has_value(), "complete MIR body printing must accept the parsed function");
+ if (!Printed) {
+ std::cerr << "error: " << Printed.error().message() << '\n';
+ return;
+ }
+ Context.expect(Printed->find("TEST_ADDI %0,") != std::string::npos,
+ "virtual-register uses must not repeat a definition constraint");
+
+ auto RoundTripFunction = createFunction(Context, 2);
+ if (!RoundTripFunction)
+ return;
+ auto RoundTripMetadata =
+ RoundTripFunction->metadata().addNode({.Slot = 100, .IsDistinct = false, .Elements = {}});
+ Context.expect(RoundTripMetadata.has_value(), "round-trip metadata fixture must be valid");
+ if (!RoundTripMetadata)
+ return;
+ rcc::mir::MIParser RoundTripParser(*RoundTripFunction, *Target, "round-trip.mir", *Printed,
+ ParserContext);
+ const auto RoundTrip = RoundTripParser.parse();
+ Context.expect(RoundTrip.has_value() && !RoundTripParser.failed(),
+ "the complete body printer output must parse successfully");
+ if (!RoundTrip) {
+ std::cerr << *Printed << '\n';
+ for (const auto &Diagnostic : RoundTripParser.diagnostics())
+ std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n';
+ return;
+ }
+ auto PrintedAgain =
+ rcc::mir::printMachineFunctionBody(*RoundTripFunction, *Target, ParserContext);
+ Context.expect(PrintedAgain.has_value() && *PrintedAgain == *Printed,
+ "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::LowLevelType::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 expectParseError(TestContext &Context, const rcc::TargetDescription &Target, std::string Body,
+ std::string_view Message);
+
+void testCompatibleRegisterClassAndBankAnnotations(TestContext &Context) {
+ auto Target = createTarget();
+ auto Function = createFunction(Context, 5);
+ Context.expect(Target.has_value(), "register-constraint target must be valid");
+ if (!Target || !Function)
+ return;
+
+ rcc::mir::MIParser Parser(*Function, *Target, "compatible-register-constraints.mir",
+ "bb.0:\n %0:gpr64 = TEST_COPY $g0\n $g1 = TEST_COPY %0:gpr\n");
+ auto Parsed = Parser.parse();
+ const auto Registers = Function->registerInfo().virtualRegisters();
+ Context.expect(Parsed.has_value() && Registers.size() == 1 &&
+ Registers.front().RegClass == rcc::test::TestRegisterClass::GPR64 &&
+ Registers.front().Bank == rcc::test::TestRegisterBank::GPR,
+ "compatible class and bank annotations must coexist on a virtual register");
+ if (!Parsed) {
+ for (const rcc::TextDiagnostic &Diagnostic : Parser.diagnostics())
+ std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n';
+ std::cerr << "error: " << Parsed.error().message() << '\n';
+ }
+
+ expectParseError(Context, *Target,
+ "bb.0:\n %0:gpr64 = TEST_COPY $g0\n $g1 = TEST_COPY %0:fpr\n",
+ "conflicting virtual register bank");
+}
+
+void testTypedIntegerTruncation(TestContext &Context) {
+ auto Target = createTarget();
+ auto Function = createFunction(Context, 12);
+ Context.expect(Target.has_value(), "typed-integer target description must be valid");
+ if (!Target || !Function)
+ return;
+
+ const std::string Body = R"(bb.0:
+ %0:_(s64) = TEST_CONSTANT i64 -1
+ %1:_(s16) = TEST_CONSTANT i16 -64769
+ %2:_(s4) = TEST_CONSTANT i4 -8
+ %3:_(s64) = TEST_CONSTANT i64 18446744073709551617
+)";
+ rcc::mir::MIParser Parser(*Function, *Target, "typed-integer-truncation.mir", Body);
+ const auto Parsed = Parser.parse();
+ const auto Instructions = Function->allInstructions();
+ const auto *MinusOne = !Instructions.empty()
+ ? Instructions[0]->operandAt(1)->getIf<rcc::IntegerImmediateOperand>()
+ : nullptr;
+ const auto *NarrowNegative =
+ Instructions.size() > 1 ? Instructions[1]->operandAt(1)->getIf<rcc::IntegerImmediateOperand>()
+ : nullptr;
+ const auto *NarrowMinimum =
+ Instructions.size() > 2 ? Instructions[2]->operandAt(1)->getIf<rcc::IntegerImmediateOperand>()
+ : nullptr;
+ const auto *WidePositive =
+ Instructions.size() > 3 ? Instructions[3]->operandAt(1)->getIf<rcc::IntegerImmediateOperand>()
+ : nullptr;
+ Context.expect(Parsed.has_value() && MinusOne != nullptr &&
+ MinusOne->Value.words()[0] == std::numeric_limits<std::uint64_t>::max() &&
+ NarrowNegative != nullptr && NarrowNegative->Value.bitWidth() == 16 &&
+ NarrowNegative->Value.words()[0] == 767 && NarrowMinimum != nullptr &&
+ NarrowMinimum->Value.bitWidth() == 4 && NarrowMinimum->Value.words()[0] == 8 &&
+ WidePositive != nullptr && WidePositive->Value.words()[0] == 1,
+ "typed integer literals must use LLVM width truncation semantics");
+ if (!Parsed) {
+ for (const auto &Diagnostic : Parser.diagnostics())
+ std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n';
+ std::cerr << "error: " << Parsed.error().message() << '\n';
+ }
+}
+
+void testLLVMInlineAsmAndConstantMemorySyntax(TestContext &Context) {
+ auto Target = createTarget();
+ auto Function = createFunction(Context, 11);
+ Context.expect(Target.has_value(), "LLVM MIR syntax target description must be valid");
+ if (!Target || !Function)
+ return;
+
+ const std::string Body = R"(bb.0:
+ INLINEASM &"", sideeffect mayload inteldialect
+ %0:gpr64 = TEST_LOAD $g0 :: (load (s64) from `ptr poison`)
+ %1:gpr64 = TEST_LOAD $g0 :: (load (s64) from unknown-address + 4)
+)";
+ rcc::mir::MIParserContext ParserContext;
+ ParserContext.PointerSizeInBits = [](std::uint32_t) -> rcc::Expected<std::uint32_t> {
+ return 64;
+ };
+ rcc::mir::MIParser Parser(*Function, *Target, "llvm-compatible.mir", Body, ParserContext);
+ auto Parsed = Parser.parse();
+ Context.expect(Parsed.has_value(), "LLVM inline-asm and constant-pointer MMO syntax must parse");
+ if (!Parsed) {
+ for (const auto &Diagnostic : Parser.diagnostics())
+ std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n';
+ return;
+ }
+
+ const rcc::MachineInstr *InlineAsm = Function->firstBlock()->firstInstruction();
+ const auto *ExtraInfo = InlineAsm->operandAt(1)->getIf<std::int64_t>();
+ const rcc::MachineInstr *Load = InlineAsm->next();
+ const rcc::MachineMemOperandDesc &Memory = Load->memOperands().front()->description();
+ const auto MemoryType = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ Context.expect(ExtraInfo != nullptr && *ExtraInfo == 13,
+ "inline-asm extra-info names must aggregate into the LLVM bit field");
+ Context.expect(Memory.Base.kind() == rcc::MemoryBase::Kind::IRConstant &&
+ Memory.Base.constantValue() != nullptr &&
+ Memory.Base.constantValue()->Kind == rcc::GenericConstantKind::Poison &&
+ MemoryType && Memory.MemoryType == *MemoryType,
+ "quoted LLVM pointer constants must remain structured MMO bases");
+
+ auto Printed = rcc::mir::printMachineFunctionBody(*Function, *Target, ParserContext);
+ Context.expect(Printed &&
+ Printed->find("INLINEASM &\"\", sideeffect mayload inteldialect") !=
+ std::string::npos &&
+ Printed->find("from `ptr poison`") != std::string::npos &&
+ Printed->find("from unknown-address + 4") != std::string::npos,
+ "canonical MIR must print symbolic inline-asm flags and explicit memory bases");
+}
+
+void testLLVMCompareExchangeMemorySyntax(TestContext &Context) {
+ auto Target = createTarget();
+ auto Function = createFunction(Context, 12);
+ Context.expect(Target.has_value(), "compare-exchange target description must be valid");
+ if (!Target || !Function)
+ return;
+
+ const std::string Body = R"(bb.0:
+ %0:gpr64(p0) = TEST_COPY $g0
+ %1:_(s8) = TEST_CONSTANT i8 0
+ %2:_(s8) = TEST_CONSTANT i8 1
+ %3:_(s8) = G_ATOMIC_CMPXCHG %0, %1, %2 :: (load store monotonic (s8))
+)";
+ rcc::mir::MIParserContext ParserContext;
+ ParserContext.PointerSizeInBits = [](std::uint32_t) -> rcc::Expected<std::uint32_t> {
+ return 64;
+ };
+ rcc::mir::MIParser Parser(*Function, *Target, "llvm-cmpxchg.mir", Body, ParserContext);
+ const auto Parsed = Parser.parse();
+ Context.expect(Parsed.has_value(), "LLVM one-ordering compare-exchange MMO syntax must parse");
+ if (!Parsed) {
+ for (const auto &Diagnostic : Parser.diagnostics())
+ std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n';
+ return;
+ }
+
+ const rcc::MachineInstr *CompareExchange = Function->allInstructions().back();
+ const rcc::MachineMemOperandDesc &Memory = CompareExchange->memOperands().front()->description();
+ Context.expect(Memory.AtomicKind == rcc::AtomicOperation::CompareExchange &&
+ Memory.SuccessOrdering == rcc::AtomicOrdering::Monotonic &&
+ Memory.FailureOrdering == rcc::AtomicOrdering::NotAtomic,
+ "compare-exchange opcode must disambiguate a one-ordering LLVM MMO");
+
+ auto Printed = rcc::mir::printMachineFunctionBody(*Function, *Target, ParserContext);
+ Context.expect(Printed && Printed->find(":: (load store monotonic (s8))") != std::string::npos &&
+ Printed->find("unknown-address") == std::string::npos,
+ "canonical LLVM MMOs must omit an absent zero-offset pointer base");
+}
+
+void testUnweightedSuccessorNormalization(TestContext &Context) {
+ auto Target = createTarget();
+ auto Function = createFunction(Context, 9);
+ Context.expect(Target.has_value(), "successor probability target description must be valid");
+ if (!Target || !Function)
+ return;
+
+ const std::string Body = R"(bb.0:
+ successors: %bb.1, %bb.2
+ TEST_NOP
+
+bb.1:
+ TEST_NOP
+
+bb.2:
+ TEST_NOP
+)";
+ rcc::mir::MIParser Parser(*Function, *Target, "successors.mir", Body);
+ const auto Parsed = Parser.parse();
+ Context.expect(Parsed.has_value(), "unweighted MIR successors must parse");
+ if (!Parsed)
+ return;
+
+ const rcc::MachineBasicBlock *Entry = Function->firstBlock();
+ Context.expect(Entry != nullptr && Entry->successors().size() == 2 &&
+ Entry->successors()[0].Probability.rawValue() == 0x40000000U &&
+ Entry->successors()[1].Probability.rawValue() == 0x40000000U,
+ "unweighted successors must receive equal normalized probabilities");
+}
+
+void expectParseError(TestContext &Context, const rcc::TargetDescription &Target, std::string Body,
+ std::string_view Message) {
+ auto Function = createFunction(Context, 10);
+ if (!Function)
+ return;
+ rcc::mir::MIParser Parser(*Function, Target, "invalid.mir", std::move(Body));
+ const auto Parsed = Parser.parse();
+ const bool IsExpectedDiagnostic =
+ !Parsed && Parser.failed() && Parser.diagnostics().size() == 1 &&
+ Parser.diagnostics().front().Category == "mir-parser" &&
+ Parser.diagnostics().front().Message.find(Message) != std::string::npos;
+ Context.expect(IsExpectedDiagnostic,
+ "invalid MIR body must produce one stable parser diagnostic");
+ if (!IsExpectedDiagnostic) {
+ for (const auto &Diagnostic : Parser.diagnostics())
+ std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n';
+ if (!Parsed)
+ std::cerr << "error: " << Parsed.error().message() << '\n';
+ }
+}
+
+void testDiagnostics(TestContext &Context) {
+ auto Target = createTarget();
+ Context.expect(Target.has_value(), "diagnostic target description must be valid");
+ if (!Target)
+ std::cerr << "error: " << Target.error().message() << '\n';
+ if (!Target)
+ return;
+
+ expectParseError(Context, *Target, "bb.0:\n TEST_BRANCH %bb.9\n",
+ "undefined machine basic block");
+ expectParseError(Context, *Target, "bb.0:\n TEST_NOP\nbb.0:\n TEST_NOP\n",
+ "redefinition of machine basic block");
+ expectParseError(Context, *Target,
+ "bb.0:\n %0:gpr64 = TEST_ADDI $g0, "
+ "target-flags(no-such-flag) 1\n",
+ "unknown target operand flag");
+ expectParseError(Context, *Target, " bb.0:\n TEST_NOP\n",
+ "basic block definition must be at the start of a line");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testTwoPassBodyParsing(Context);
+ testAddressSpacePointerWidthResolver(Context);
+ testCompatibleRegisterClassAndBankAnnotations(Context);
+ testTypedIntegerTruncation(Context);
+ testLLVMInlineAsmAndConstantMemorySyntax(Context);
+ testLLVMCompareExchangeMemorySyntax(Context);
+ testUnweightedSuccessorNormalization(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..6d84580
--- /dev/null
+++ b/backend/unittests/MIR/MIRDocumentTest.cpp
@@ -0,0 +1,1148 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "Target/TestTarget.h"
+#include "rcc/CodeGen/CompilationUnit.h"
+#include "rcc/CodeGen/InstructionDescriptor.h"
+#include "rcc/CodeGen/MIRParser/MIRParser.h"
+#include "rcc/CodeGen/MIRPrinter.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(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 ] }
+ - { id: 8, class: gpr64, preferred-register: '' }
+liveins:
+ - { reg: '$g0', virtual-reg: '%7' }
+ - { reg: '$g1', virtual-reg: '' }
+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, callee-saved-register: '', 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)' }
+ - { id: 5, name: '', size: 4, alignment: 4 }
+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):
+ liveins: $g0, $g1
+ %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() == 2 &&
+ Function->registerInfo().liveIns().size() == 2 &&
+ Function->records().hasCalleeSavedRegisters(),
+ "register tables must publish into the common MachineIR model");
+ Context.expect(Function->frameInfo().liveObjectCount() == 3 &&
+ 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;
+ }
+ Context.expect(Printed->find("name: documented\nalignment: 16\n") != std::string::npos &&
+ Printed->find("functionContext: ''") != std::string::npos &&
+ Printed->find("...\n---\nname: documented") != std::string::npos &&
+ Printed->ends_with("...\n"),
+ "complete MIR must use LLVM-compatible scalars, defaults, and document markers");
+ 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 = {}});
+ 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 = {}});
+ 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 testEmptyYAMLDocuments(TestContext &Context) {
+ auto Target = createTarget();
+ if (!Target)
+ return;
+
+ constexpr std::string_view Boundary = "\n---\nname: documented";
+ std::string WithEmptyDocuments(ValidDocument);
+ const std::size_t BoundaryPosition = WithEmptyDocuments.find(Boundary);
+ Context.expect(BoundaryPosition != std::string::npos,
+ "MIR empty-document fixture must find its document boundary");
+ if (BoundaryPosition == std::string::npos)
+ return;
+ WithEmptyDocuments.replace(BoundaryPosition, Boundary.size(), "\n---\n---\nname: documented");
+ WithEmptyDocuments.insert(0, "---\n");
+ WithEmptyDocuments.append("\n...\n---\n");
+ auto Parsed =
+ rcc::mir::parseMIRDocument(*Target, "empty-documents.mir", std::move(WithEmptyDocuments));
+ Context.expect(Parsed.succeeded() && Parsed.document() != nullptr &&
+ Parsed.document()->unit().functionCount() == 1,
+ "MIR must ignore leading, intermediate, and trailing empty YAML documents");
+ if (!Parsed.succeeded())
+ printDiagnostics(Parsed);
+
+ std::string ExplicitNull(ValidDocument);
+ ExplicitNull.replace(BoundaryPosition, Boundary.size(), "\n---\nnull\n---\nname: documented");
+ auto Rejected = rcc::mir::parseMIRDocument(*Target, "explicit-null.mir", std::move(ExplicitNull));
+ Context.expect(!Rejected.succeeded(),
+ "MIR must not treat an explicit null document as an empty separator");
+}
+
+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::LowLevelType::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::MIRDocumentParserOptions OverrideOptions;
+ OverrideOptions.DataLayout = "e-p:64:64-p1:64:64";
+ OverrideOptions.MachineInstructions.VerifyAfterParse = false;
+ OverrideOptions.VerifyMachineFunctions = false;
+ auto Overridden = rcc::mir::parseMIRDocument(
+ *Target, "overridden-layout.mir", std::string(HeterogeneousPointerDocument), OverrideOptions);
+ const rcc::MachineFunction *OverriddenFunction =
+ Overridden.succeeded() ? Overridden.document()->unit().function("pointer_layout") : nullptr;
+ const rcc::MachineConstantPoolEntry *OverriddenConstant =
+ OverriddenFunction == nullptr
+ ? nullptr
+ : OverriddenFunction->constantPool().entry(rcc::ConstantPoolIndex::fromValue(0));
+ const rcc::MachineConstantPoolEntryDesc *OverriddenConstantDescription =
+ OverriddenConstant == nullptr ? nullptr : OverriddenConstant->description();
+ const auto OverriddenPointerType = rcc::LowLevelType::makePointer(1, 64);
+ Context.expect(Overridden.succeeded() && OverriddenFunction != nullptr &&
+ OverriddenPointerType.has_value() &&
+ OverriddenFunction->registerInfo().virtualRegisters().size() == 1 &&
+ OverriddenFunction->registerInfo().virtualRegisters().front().Type ==
+ *OverriddenPointerType &&
+ OverriddenConstantDescription != nullptr &&
+ OverriddenConstantDescription->Size == 8,
+ "explicit target DataLayout must override embedded LLVM IR layout");
+ if (!Overridden.succeeded())
+ printDiagnostics(Overridden);
+
+ if (Overridden.succeeded()) {
+ rcc::mir::MIRDocumentPrinterOptions OverridePrinterOptions;
+ OverridePrinterOptions.EmitEmbeddedIR = false;
+ OverridePrinterOptions.DataLayout = OverrideOptions.DataLayout;
+ auto OverriddenOutput =
+ rcc::mir::printMIRDocument(*Overridden.document(), *Target, OverridePrinterOptions);
+ Context.expect(OverriddenOutput.has_value(),
+ "matching target DataLayout must allow omitting overridden embedded IR");
+ }
+
+ 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(
+ "pointer virtual-register width") != 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 testLiveInCreatesVirtualRegister(TestContext &Context) {
+ auto Target = createTarget();
+ Context.expect(Target.has_value(), "live-in virtual-register target must be valid");
+ if (!Target)
+ return;
+
+ constexpr std::string_view DocumentText = R"(---
+name: live_in_vreg
+liveins:
+ - { reg: '$g0', virtual-reg: '%0' }
+body: |
+ bb.0:
+ %0:gpr64 = TEST_COPY $g0
+)";
+ auto Parsed = rcc::mir::parseMIRDocument(*Target, "live-in-vreg.mir", std::string(DocumentText));
+ const rcc::MachineFunction *Function =
+ Parsed.succeeded() ? Parsed.document()->unit().function("live_in_vreg") : nullptr;
+ const auto Registers = Function != nullptr ? Function->registerInfo().virtualRegisters()
+ : std::span<const rcc::VirtualRegisterInfo>{};
+ const auto LiveIns = Function != nullptr ? Function->registerInfo().liveIns()
+ : std::span<const rcc::PhysicalRegisterLiveIn>{};
+ Context.expect(Parsed.succeeded() && Function != nullptr && Registers.size() == 1 &&
+ Registers.front().RegClass.has_value() && LiveIns.size() == 1 &&
+ LiveIns.front().InitialVirtualRegister.has_value(),
+ "a live-in must create a virtual register before the body constrains it");
+ if (!Parsed.succeeded()) {
+ printDiagnostics(Parsed);
+ return;
+ }
+
+ auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target);
+ Context.expect(Printed.has_value(),
+ "an implicitly created live-in virtual register must be printable");
+ if (!Printed)
+ return;
+ auto RoundTrip = rcc::mir::parseMIRDocument(*Target, "live-in-vreg-round-trip.mir", *Printed);
+ Context.expect(RoundTrip.succeeded(),
+ "an implicitly created live-in virtual register must round-trip");
+ if (!RoundTrip.succeeded())
+ printDiagnostics(RoundTrip);
+}
+
+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);
+}
+
+void testAnonymousFunctionsAndIRBlocks(TestContext &Context) {
+ auto Target = createTarget();
+ Context.expect(Target.has_value(), "anonymous-function target must be valid");
+ if (!Target)
+ return;
+
+ constexpr std::string_view DocumentText = R"(--- |
+ define void @0() {
+ ret void
+ }
+
+ define void @named() {
+ ret void
+ }
+
+ define void @1() {
+ ret void
+ }
+...
+---
+name: ''
+body: |
+ bb.0 (%ir-block.0):
+...
+---
+name: named
+body: |
+ bb.0 (%ir-block.0):
+...
+---
+name: ''
+body: |
+ bb.0 (%ir-block.0):
+...
+)";
+ auto Parsed = rcc::mir::parseMIRDocument(*Target, "anonymous.mir", std::string(DocumentText));
+ const rcc::MachineFunction *First =
+ Parsed.succeeded() ? Parsed.document()->unit().functionAt(0) : nullptr;
+ const rcc::MachineFunction *Named =
+ Parsed.succeeded() ? Parsed.document()->unit().functionAt(1) : nullptr;
+ const rcc::MachineFunction *Second =
+ Parsed.succeeded() ? Parsed.document()->unit().functionAt(2) : nullptr;
+ Context.expect(Parsed.succeeded() && First != nullptr && First->name().empty() &&
+ Named != nullptr && Named->name() == "named" && Second != nullptr &&
+ Second->name().empty(),
+ "MIR must match empty names to embedded anonymous functions in document order");
+ if (!Parsed.succeeded()) {
+ printDiagnostics(Parsed);
+ return;
+ }
+
+ auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target);
+ Context.expect(Printed.has_value(), "anonymous MIR functions must remain printable");
+ if (!Printed)
+ return;
+ const std::size_t FirstAnonymous = Printed->find("name: ''");
+ const std::size_t SecondAnonymous =
+ FirstAnonymous == std::string::npos
+ ? std::string::npos
+ : Printed->find("name: ''", FirstAnonymous + std::string_view("name: ''").size());
+ Context.expect(FirstAnonymous != std::string::npos && SecondAnonymous != std::string::npos &&
+ Printed->find("%ir-block.0") != std::string::npos,
+ "anonymous function names and unnamed IR block slots must print canonically");
+
+ constexpr std::string_view NoIRText = R"(...
+---
+name: ''
+body: |
+ bb.0:
+...
+)";
+ auto NoIR = rcc::mir::parseMIRDocument(*Target, "anonymous-no-ir.mir", std::string(NoIRText));
+ Context.expect(NoIR.succeeded() && NoIR.document()->unit().functionAt(0) != nullptr &&
+ NoIR.document()->unit().functionAt(0)->name().empty(),
+ "MIR without embedded IR must accept an anonymous dummy function");
+ if (!NoIR.succeeded())
+ printDiagnostics(NoIR);
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testDocumentTransaction(Context);
+ testEmptyYAMLDocuments(Context);
+ testHeterogeneousPointerLayout(Context);
+ testEmbeddedReferenceValidation(Context);
+ testReferencesWithoutEmbeddedIR(Context);
+ testPrinterRejectsUnresolvedProgrammaticReferences(Context);
+ testMetadataSlotPriorityAndCanonicalization(Context);
+ testFixedWidthDocumentFields(Context);
+ testAtomicFailure(Context);
+ testLLVMPropertySemantics(Context);
+ testLiveInCreatesVirtualRegister(Context);
+ testBodyDiagnosticLocation(Context);
+ testAnonymousFunctionsAndIRBlocks(Context);
+ return Context.result();
+}