From 8553df97bbc6154dae9d0ad861857e71a0efa420 Mon Sep 17 00:00:00 2001 From: Richard-Qin-X Date: Sat, 1 Aug 2026 13:09:30 +0800 Subject: [CodeGen] Add transactional MachineIR builder Validate insertion cursors, operand ownership, descriptor constraints, ties, implicit registers, and memory operands before allocating or publishing an instruction. Share instruction legality checks with the verifier path and preserve deterministic insertion order. --- backend/unittests/CodeGen/CMakeLists.txt | 15 + backend/unittests/CodeGen/MachineIRBuilderTest.cpp | 462 +++++++++++++++++++++ 2 files changed, 477 insertions(+) create mode 100644 backend/unittests/CodeGen/MachineIRBuilderTest.cpp (limited to 'backend/unittests') diff --git a/backend/unittests/CodeGen/CMakeLists.txt b/backend/unittests/CodeGen/CMakeLists.txt index a3aefde..ddba86a 100644 --- a/backend/unittests/CodeGen/CMakeLists.txt +++ b/backend/unittests/CodeGen/CMakeLists.txt @@ -4,6 +4,7 @@ set( RCCBackendCodeGenTestSources CoreTypesTest.cpp InstructionDescriptorTest.cpp + MachineIRBuilderTest.cpp MachineIRCoreTest.cpp MachineValueTypesTest.cpp ) @@ -29,6 +30,18 @@ add_test( COMMAND RCCBackendCodeGenInstructionDescriptorTests ) +add_executable(RCCBackendCodeGenMachineIRBuilderTests MachineIRBuilderTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineIRBuilderTests) +target_link_libraries( + RCCBackendCodeGenMachineIRBuilderTests + PRIVATE RCCBackend::CodeGen +) + +add_test( + NAME RCCBackend.CodeGen.MachineIRBuilder + COMMAND RCCBackendCodeGenMachineIRBuilderTests +) + add_executable(RCCBackendCodeGenMachineValueTypesTests MachineValueTypesTest.cpp) rcc_backend_configure_target(RCCBackendCodeGenMachineValueTypesTests) target_link_libraries(RCCBackendCodeGenMachineValueTypesTests PRIVATE RCCBackend::CodeGen) @@ -50,12 +63,14 @@ add_test( rcc_backend_register_format_files( "${CMAKE_CURRENT_SOURCE_DIR}/CoreTypesTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/InstructionDescriptorTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineIRBuilderTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineIRCoreTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineValueTypesTest.cpp" ) rcc_backend_register_tidy_files( "${CMAKE_CURRENT_SOURCE_DIR}/CoreTypesTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/InstructionDescriptorTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineIRBuilderTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineIRCoreTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineValueTypesTest.cpp" ) diff --git a/backend/unittests/CodeGen/MachineIRBuilderTest.cpp b/backend/unittests/CodeGen/MachineIRBuilderTest.cpp new file mode 100644 index 0000000..bda30b6 --- /dev/null +++ b/backend/unittests/CodeGen/MachineIRBuilderTest.cpp @@ -0,0 +1,462 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineIRBuilder.h" + +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.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 +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t CopyOpcode = 1; +constexpr std::uint32_t AddOpcode = 2; +constexpr std::uint32_t LoadOpcode = 3; +constexpr std::uint32_t NopOpcode = 4; +constexpr std::uint32_t BranchOpcode = 5; +constexpr std::uint32_t DynamicImplicitOpcode = 6; + +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 registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor instructionDescriptor(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::Expected createInstructionSet() { + auto Copy = instructionDescriptor(CopyOpcode, "TEST_COPY"); + auto CopyDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto CopyUse = registerDescriptor(rcc::RegisterOperandRole::Use); + CopyDef.DefaultTiedOperand = 1; + CopyUse.DefaultTiedOperand = 0; + Copy.Operands.push_back(CopyDef); + Copy.Operands.push_back(CopyUse); + Copy.NumExplicitDefs = 1; + + auto Add = instructionDescriptor(AddOpcode, "TEST_ADD"); + Add.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + Add.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Use)); + rcc::OperandDescriptor RegisterOrImmediate; + RegisterOrImmediate.Kinds = { + rcc::MachineOperand::Kind::Register, + rcc::MachineOperand::Kind::Immediate, + }; + RegisterOrImmediate.RegisterRole = rcc::RegisterOperandRole::Use; + Add.Operands.push_back(RegisterOrImmediate); + Add.NumExplicitDefs = 1; + Add.ImplicitDefs.push_back( + rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(1), false, true}); + Add.ImplicitUses.push_back(rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(2)}); + + auto Load = instructionDescriptor(LoadOpcode, "TEST_LOAD"); + Load.Flags.set(rcc::InstructionFlag::MayLoad); + Load.RequiresMemoryOperands = true; + + auto Nop = instructionDescriptor(NopOpcode, "TEST_NOP"); + + auto Branch = instructionDescriptor(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); + + auto Dynamic = instructionDescriptor(DynamicImplicitOpcode, "TEST_DYNAMIC_IMPLICIT"); + Dynamic.AllowExtraImplicitOperands = true; + + std::vector Descriptors; + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Add)); + Descriptors.push_back(std::move(Load)); + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Dynamic)); + return rcc::InstructionSet::create(std::move(Descriptors)); +} + +[[nodiscard]] std::unique_ptr +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), + "machine basic block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineMemOperandDesc memoryDescription(rcc::MemoryAccessFlag Access) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {Access}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)); + return Description; +} + +[[nodiscard]] rcc::MachineInstr *buildNop(TestContext &Context, rcc::MachineIRBuilder &Builder) { + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Pending.has_value(), "NOP builder creation must succeed"); + if (!Pending) + return nullptr; + auto Instruction = Pending->build(); + Context.expect(Instruction.has_value(), "NOP construction must succeed"); + if (!Instruction) + return nullptr; + return *Instruction; +} + +void testTransactionalBuild(TestContext &Context) { + auto InstructionSet = createInstructionSet(); + auto Function = createFunction(Context, "transactional", 1); + Context.expect(InstructionSet.has_value(), "builder instruction set must be valid"); + if (!InstructionSet || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + auto Type = rcc::LLT::makeInteger(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Destination && Source, "builder virtual registers must be allocated"); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *InstructionSet); + Context.expect(!Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)), + "an unset insertion point must reject instruction construction"); + Context.expect(Function->allInstructions().empty(), + "rejected builder setup must not allocate an instruction"); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "a linked block must be a valid insertion point"); + Context.expect(!Builder.buildInstr(rcc::Opcode::fromValue(99)), + "an unregistered opcode must be rejected"); + + auto Incomplete = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Incomplete.has_value() && Incomplete->addDef(*Destination).has_value(), + "incomplete instruction setup must accept its first operand"); + if (!Incomplete) + return; + Context.expect(!Incomplete->tieOperands(0, 1) && !Incomplete->setSourceLocation({"", 0, 0, 0}) && + !Incomplete->setOrigin(rcc::OriginID::fromValue(0)), + "invalid pending metadata and tie indices must be rejected"); + Context.expect(!Incomplete->build(), "descriptor validation must reject a missing operand"); + Context.expect(Function->allInstructions().empty() && Block->instructionCount() == 0, + "failed validation must not allocate or publish an instruction"); + + auto Complete = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Complete.has_value(), "complete instruction builder must be created"); + if (!Complete) + return; + Context.expect(Complete->addDef(*Destination).has_value() && + Complete->addUse(*Source).has_value(), + "complete instruction operands must be accepted"); + Context.expect(Complete->tieOperands(0, 1).has_value(), + "an explicit tie matching the descriptor default must be idempotent"); + Context.expect(Complete->setFlags({rcc::MachineInstrFlag::FrameSetup}).has_value(), + "valid instruction flags must be accepted"); + Context.expect( + Complete->setSourceLocation(rcc::SourceLocation{"builder.mir", 7, 3, 1}).has_value() && + Complete->setOrigin(rcc::OriginID::fromValue(19)).has_value(), + "valid source and origin metadata must be accepted"); + auto Built = Complete->build(); + Context.expect(Built.has_value(), "valid pending instruction must build"); + if (!Built) + return; + + rcc::MachineInstr *Instruction = *Built; + Context.expect(Instruction->stableID().value() == 1, + "failed builds must not consume instruction StableIDs"); + Context.expect(Instruction->parent() == Block && Block->firstInstruction() == Instruction, + "successful build must publish at the insertion point"); + Context.expect(Instruction->operandAt(0)->tiedOperandIndex() == 1 && + Instruction->operandAt(1)->tiedOperandIndex() == 0, + "descriptor-default tie must be materialized bidirectionally"); + Context.expect(Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup) && + Instruction->sourceLocation() == rcc::SourceLocation{"builder.mir", 7, 3, 1} && + Instruction->origin() == rcc::OriginID::fromValue(19), + "builder metadata must be preserved on the published instruction"); + Context.expect( + InstructionSet->validateInstruction(*Instruction, Function->registerInfo()).has_value(), + "builder output must satisfy the shared instruction descriptor"); + Context.expect(!Complete->build(), "a successful pending builder must be single-use"); + Context.expect(!Complete->setFlags({}) && !Complete->clearSourceLocation() && + !Complete->clearOrigin(), + "all pending-builder mutators must reject use after publication"); + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "published builder operands must enter exact def-use chains"); + + auto MoveSource = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(MoveSource.has_value(), "movable pending builder must be created"); + if (!MoveSource) + return; + rcc::MachineInstrBuilder MoveDestination(std::move(*MoveSource)); + Context.expect(!MoveSource->build(), "a moved-from pending builder must be consumed"); + auto MovedResult = MoveDestination.build(); + Context.expect(MovedResult && (*MovedResult)->stableID().value() == 2, + "a moved-to pending builder must retain its construction state"); +} + +void testInsertionOrdering(TestContext &Context) { + auto InstructionSet = createInstructionSet(); + auto Function = createFunction(Context, "ordering", 2); + if (!InstructionSet || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + rcc::MachineIRBuilder Builder(*Function, *InstructionSet); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "ordering builder insertion point must be valid"); + rcc::MachineInstr *First = buildNop(Context, Builder); + rcc::MachineInstr *Tail = buildNop(Context, Builder); + if (First == nullptr || Tail == nullptr) + return; + + Context.expect(Builder.setInsertPointAfter(*First).has_value(), + "after-instruction insertion point must be accepted"); + rcc::MachineInstr *MiddleA = buildNop(Context, Builder); + rcc::MachineInstr *MiddleB = buildNop(Context, Builder); + Context.expect(MiddleA != nullptr && MiddleB != nullptr && First->next() == MiddleA && + MiddleA->next() == MiddleB && MiddleB->next() == Tail, + "repeated insertion after a cursor must preserve build order"); + + Context.expect(Builder.setInsertPointBefore(*First).has_value(), + "before-instruction insertion point must be accepted"); + rcc::MachineInstr *PrefixA = buildNop(Context, Builder); + rcc::MachineInstr *PrefixB = buildNop(Context, Builder); + Context.expect(PrefixA != nullptr && PrefixB != nullptr && Block->firstInstruction() == PrefixA && + PrefixA->next() == PrefixB && PrefixB->next() == First, + "repeated insertion before a cursor must preserve build order"); +} + +void testImplicitAndMemoryValidation(TestContext &Context) { + auto InstructionSet = createInstructionSet(); + auto Function = createFunction(Context, "implicit_memory", 3); + auto OtherFunction = createFunction(Context, "foreign", 4); + if (!InstructionSet || !Function || !OtherFunction) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + rcc::MachineBasicBlock *OtherBlock = createBlock(Context, *OtherFunction); + if (Block == nullptr || OtherBlock == nullptr) + return; + + auto Type = rcc::LLT::makeInteger(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Destination && Source, "implicit test virtual registers must be allocated"); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *InstructionSet); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "implicit test insertion point must be accepted"); + + auto Add = Builder.buildInstr(rcc::Opcode::fromValue(AddOpcode)); + Context.expect(Add && Add->addDef(*Destination).has_value() && Add->addUse(*Source).has_value() && + Add->addImmediate(7).has_value(), + "ADD operands must be accepted by the pending builder"); + if (!Add) + return; + auto BuiltAdd = Add->build(); + Context.expect(BuiltAdd.has_value(), "ADD with default implicits must build"); + if (!BuiltAdd) + return; + Context.expect( + (*BuiltAdd)->operandCount() == 5 && (*BuiltAdd)->operandAt(3)->isImplicit() && + (*BuiltAdd)->operandAt(3)->registerFlags().has(rcc::RegisterOperandFlag::Renamable) && + (*BuiltAdd)->operandAt(4)->isImplicit(), + "default implicit defs and uses must follow explicit operands"); + Context.expect( + InstructionSet->validateInstruction(**BuiltAdd, Function->registerInfo()).has_value(), + "default implicit builder output must pass shared validation"); + + const std::size_t BeforeRejectedImplicit = Function->allInstructions().size(); + auto ExtraImplicit = Builder.buildInstr(rcc::Opcode::fromValue(AddOpcode)); + Context.expect( + ExtraImplicit && ExtraImplicit->addDef(*Destination).has_value() && + ExtraImplicit->addUse(*Source).has_value() && + ExtraImplicit->addImmediate(1).has_value() && + ExtraImplicit + ->addImplicitUse(rcc::Register::fromPhysical(*rcc::PhysicalRegister::create(3))) + .has_value(), + "dynamic implicit negative test must be assembled"); + Context.expect(ExtraImplicit && !ExtraImplicit->build() && + Function->allInstructions().size() == BeforeRejectedImplicit, + "descriptor-rejected dynamic implicits must not allocate instructions"); + + auto Dynamic = Builder.buildInstr(rcc::Opcode::fromValue(DynamicImplicitOpcode)); + Context.expect( + Dynamic && + Dynamic->addImplicitUse(rcc::Register::fromPhysical(*rcc::PhysicalRegister::create(3))) + .has_value(), + "descriptor-permitted dynamic implicit operand must be accepted"); + if (!Dynamic) + return; + auto BuiltDynamic = Dynamic->build(); + Context.expect( + BuiltDynamic.has_value() && + InstructionSet->validateInstruction(**BuiltDynamic, Function->registerInfo()).has_value(), + "dynamic implicit builder output must validate"); + + const std::size_t BeforeMissingMemory = Function->allInstructions().size(); + auto MissingMemory = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(MissingMemory && !MissingMemory->build() && + Function->allInstructions().size() == BeforeMissingMemory, + "missing required MMO must fail before instruction allocation"); + + auto LoadMemory = Function->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Load)); + auto StoreMemory = Function->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Store)); + auto ForeignMemory = + OtherFunction->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Load)); + Context.expect(LoadMemory && StoreMemory && ForeignMemory, + "memory validation operands must be created"); + if (!LoadMemory || !StoreMemory || !ForeignMemory) + return; + + auto Load = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(Load && Load->addMemOperand(**LoadMemory).has_value(), + "function-owned load MMO must be accepted"); + if (!Load) + return; + auto BuiltLoad = Load->build(); + Context.expect( + BuiltLoad.has_value() && + InstructionSet->validateInstruction(**BuiltLoad, Function->registerInfo()).has_value(), + "matching load MMO must build and validate"); + + const std::size_t BeforeStoreMismatch = Function->allInstructions().size(); + auto StoreMismatch = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(StoreMismatch && StoreMismatch->addMemOperand(**StoreMemory).has_value() && + !StoreMismatch->build() && + Function->allInstructions().size() == BeforeStoreMismatch, + "MMO access mismatch must fail before instruction allocation"); + auto ForeignMMO = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(ForeignMMO && !ForeignMMO->addMemOperand(**ForeignMemory), + "foreign-function MMO must be rejected immediately"); + + const std::size_t BeforeForeignBlock = Function->allInstructions().size(); + auto ForeignBranch = Builder.buildInstr(rcc::Opcode::fromValue(BranchOpcode)); + Context.expect(ForeignBranch && ForeignBranch->addBasicBlock(*OtherBlock).has_value() && + !ForeignBranch->build() && + Function->allInstructions().size() == BeforeForeignBlock, + "foreign basic block operand must fail before instruction allocation"); +} + +void testStaleCursorsAndOwnedOperands(TestContext &Context) { + auto InstructionSet = createInstructionSet(); + auto Function = createFunction(Context, "ownership", 5); + if (!InstructionSet || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + auto Type = rcc::LLT::makeInteger(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *InstructionSet); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "ownership test insertion point must be valid"); + auto Copy = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Copy && Copy->addDef(*Destination).has_value() && + Copy->addUse(*Source).has_value(), + "ownership source instruction must be assembled"); + if (!Copy) + return; + auto BuiltCopy = Copy->build(); + if (!BuiltCopy) + return; + + auto DetachedTarget = Function->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(DetachedTarget.has_value(), "detached target instruction must be allocated"); + if (!DetachedTarget) + return; + rcc::MachineOperand *OwnedOperand = (*BuiltCopy)->operandAt(0); + Context.expect( + OwnedOperand != nullptr && !(*DetachedTarget)->addOperand(std::move(*OwnedOperand)) && + OwnedOperand->parent() == *BuiltCopy && OwnedOperand->registerValue() == *Destination, + "controlled instruction API must reject an owned operand without moving it"); + + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Pending && OwnedOperand != nullptr && + !Pending->addOperand(std::move(*OwnedOperand)) && + OwnedOperand->parent() == *BuiltCopy, + "controlled builder API must reject an owned operand without moving it"); + + rcc::MachineInstr *Cursor = buildNop(Context, Builder); + if (Cursor == nullptr) + return; + Context.expect(Builder.setInsertPointBefore(*Cursor).has_value(), "cursor setup must succeed"); + auto Stale = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Stale.has_value(), "pending stale-cursor test must be created"); + const std::size_t BeforeStaleBuild = Function->allInstructions().size(); + Context.expect(Block->removeInstruction(*Cursor).has_value(), "cursor removal must succeed"); + Context.expect(Stale && !Stale->build() && Function->allInstructions().size() == BeforeStaleBuild, + "a stale cursor must fail before instruction allocation"); +} + +} // namespace + +int main() { + TestContext Context; + testTransactionalBuild(Context); + testInsertionOrdering(Context); + testImplicitAndMemoryValidation(Context); + testStaleCursorsAndOwnedOperands(Context); + return Context.result(); +} -- cgit v1.2.3