diff options
Diffstat (limited to 'backend/unittests/MIR')
| -rw-r--r-- | backend/unittests/MIR/CMakeLists.txt | 46 | ||||
| -rw-r--r-- | backend/unittests/MIR/EmbeddedIRTest.cpp | 143 | ||||
| -rw-r--r-- | backend/unittests/MIR/MILexerTest.cpp | 417 | ||||
| -rw-r--r-- | backend/unittests/MIR/MIParserTest.cpp | 640 | ||||
| -rw-r--r-- | backend/unittests/MIR/MIRDocumentTest.cpp | 1148 |
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(); +} |
