From 9abc0b7bb38a1317abf7ab111310e5b28d0e5d54 Mon Sep 17 00:00:00 2001 From: makslevental Date: Mon, 17 Aug 2026 15:01:51 -0700 Subject: [PATCH 1/2] [eudsl-llvmpy] Emit object from selected MIR + JIT-execute it (Route B capstone) MachineModuleInfo.emit_object runs only the back half of codegen (register allocation, prologue/epilogue, object emission) over the hand-built, already-selected MachineFunctions -- via TargetMachine.addPassesToEmitFile with -start-after=finalize-isel so no instruction selection runs -- and returns the relocatable object bytes. LLJIT.add_object loads such an object into ORC so the DSL-built function can be called. create_machine_function now builds into a MachineModuleInfoWrapperPass (with a defined IR stub) so emit_object can hand it to the pipeline, and freezes reserved registers first. This closes Route B: a MachineFunction assembled instruction-by-instruction in Python lowers to native code with no GlobalISel dependence -- verified by JIT-executing add(2,3)==5 on an AArch64 host (object emission itself is host-independent). Eleventh PR in the MIR stack. Alternative recorded for the generic-MIR route: lowering DSL-built *generic* (G_*) MIR needs the target's post-legalizer passes, reachable only from IR via run_codegen_to_mir(global_isel=True) or, upstream, a programmatic TargetPassConfig::setStartStopPasses; GlobalISel's SelectionDAG fallback needs IR, which hand-built MIR lacks. Route B sidesteps all of that. --- projects/eudsl-llvmpy/src/IR/JIT.cpp | 13 ++ projects/eudsl-llvmpy/src/MIR/Machine.cpp | 132 ++++++++++++++---- .../eudsl-llvmpy/tests/mir/test_emit_jit.py | 124 ++++++++++++++++ 3 files changed, 240 insertions(+), 29 deletions(-) create mode 100644 projects/eudsl-llvmpy/tests/mir/test_emit_jit.py diff --git a/projects/eudsl-llvmpy/src/IR/JIT.cpp b/projects/eudsl-llvmpy/src/IR/JIT.cpp index 12cc348de..724c93476 100644 --- a/projects/eudsl-llvmpy/src/IR/JIT.cpp +++ b/projects/eudsl-llvmpy/src/IR/JIT.cpp @@ -39,6 +39,19 @@ void populate_jit(nb::module_ &m) { (void)mod.take(); }, "module"_a) + .def( + "add_object", + [](llvm::orc::LLJIT &self, nb::bytes obj) { + std::unique_ptr memBuf = + llvm::MemoryBuffer::getMemBufferCopy( + llvm::StringRef(static_cast(obj.data()), + obj.size()), + ""); + eudsl::unwrap(self.addObjectFile(std::move(memBuf))); + }, + "obj"_a, + "Add a relocatable object file (e.g. from " + "MachineModuleInfo.emit_object) to the JIT.") .def( "lookup", [](llvm::orc::LLJIT &self, const std::string &name) { diff --git a/projects/eudsl-llvmpy/src/MIR/Machine.cpp b/projects/eudsl-llvmpy/src/MIR/Machine.cpp index e77770bc4..65d4d1aab 100644 --- a/projects/eudsl-llvmpy/src/MIR/Machine.cpp +++ b/projects/eudsl-llvmpy/src/MIR/Machine.cpp @@ -29,9 +29,12 @@ #include #include #include +#include #include #include +#include #include +#include #include #include @@ -196,21 +199,25 @@ static std::vector &machineBuilderStack() { return stack; } -// Owns everything the inspected MachineFunctions transitively depend on, so -// none of it is freed out from under them: the LLVMContext (pinned by -// `ctxKeepAlive`), the IR Module (whose Functions the MachineFunctions -// reference), and the MachineModuleInfo that owns the MachineFunctions. The MMI -// is owned two ways depending on how it was built: the codegen path -// (run_codegen_to_mir) leaves it inside a MachineModuleInfoWrapperPass owned by -// `pm`; the parse path (parse_mir) owns it directly via `ownedMmi`. `mmi` is -// the query handle either way. The TargetMachine is a separate Python object -// kept alive by a keep_alive. +// Owns everything the MachineFunctions transitively depend on, so none of it is +// freed out from under them: the LLVMContext (pinned by `ctxKeepAlive`), the IR +// Module (whose Functions the MachineFunctions reference), and the +// MachineModuleInfo that owns the MachineFunctions. The MMI is owned one of +// three ways depending on how it was built: run_codegen_to_mir leaves it in a +// MachineModuleInfoWrapperPass owned by the (already-run) `pm`; parse_mir owns +// it directly via `ownedMmi`; create_machine_function owns a +// MachineModuleInfoWrapperPass via `mmiwp` (not yet in a PassManager, so +// emit_object can hand it to one). `mmi` is the query handle. `tm` is borrowed +// (kept alive by a keep_alive on the factory). struct MachineModuleInfo { std::shared_ptr ctxKeepAlive; std::unique_ptr module; - std::unique_ptr pm; // codegen path + std::unique_ptr pm; // codegen/emit path std::unique_ptr ownedMmi; // parse path + std::unique_ptr mmiwp; // build path llvm::MachineModuleInfo *mmi; + llvm::TargetMachine *tm = nullptr; + bool emitted = false; // emit_object runs a pass pipeline once // Print the whole module as .mir text: the IR block, then each function's // machine-level block, matching what `llc -stop-after=finalize-isel` emits. A @@ -938,7 +945,61 @@ void populate_mir(nb::module_ &m) { .def( "to_mir", [](MachineModuleInfo &self) { return self.toMIR(); }, "Serialize the whole module (IR block + machine functions) as .mir " - "text."); + "text.") + .def( + "emit_object", + [](MachineModuleInfo &self) { + if (self.emitted) + throw std::runtime_error("object already emitted"); + if (!self.mmiwp || !self.tm) + throw std::runtime_error( + "emit_object requires a module built with " + "create_machine_function"); + // Run only the back half of codegen (regalloc, prologue/epilogue, + // asm/object emission) over the already-selected MachineFunctions: + // -start-after=finalize-isel skips instruction selection so the + // hand-built MIR is used as-is. The option is process-global (read + // by the pass config addPassesToEmitFile builds), so set+restore + // it. + auto &opts = llvm::cl::getRegisteredOptions(); + auto it = opts.find("start-after"); + if (it == opts.end()) + throw std::runtime_error( // LCOV_EXCL_LINE + "the -start-after option is not registered"); // LCOV_EXCL_LINE + auto &startAfter = + *static_cast *>(it->second); + std::string saved = startAfter; + struct Restore { + llvm::cl::opt &opt; + std::string value; + ~Restore() { opt = value; } + } restore{startAfter, saved}; + startAfter = "finalize-isel"; + + // Write straight into a SmallVector (raw_svector_ostream writes + // through, so no deferred flush surprises when `pm` outlives here). + llvm::SmallVector buf; + llvm::raw_svector_ostream os(buf); + self.pm = std::make_unique(); + // addPassesToEmitFile adopts the MMIWrapperPass into `pm` (which we + // keep, so `mmi` stays valid); it holds the built MachineFunctions. + if (self.tm->addPassesToEmitFile( + *self.pm, os, nullptr, llvm::CodeGenFileType::ObjectFile, + /*DisableVerify=*/true, self.mmiwp.release())) + throw std::runtime_error( // LCOV_EXCL_LINE + "target cannot emit an object file"); // LCOV_EXCL_LINE + // The back half (register allocation) requires reserved registers + // to be frozen; the front of the pipeline normally does this, so do + // it here for the hand-built MachineFunctions. + for (llvm::Function &f : *self.module) + if (llvm::MachineFunction *mf = self.mmi->getMachineFunction(f)) + mf->getRegInfo().freezeReservedRegs(); + self.pm->run(*self.module); + self.emitted = true; + return nb::bytes(buf.data(), buf.size()); + }, + "Emit a relocatable object file for the built (already-selected) MIR " + "by running the back half of codegen (regalloc, emission)."); // Run instruction selection on an IR module and hand back the // MachineModuleInfo that owns the resulting MachineFunctions. Consumes the @@ -1020,9 +1081,10 @@ void populate_mir(nb::module_ &m) { } } - return new MachineModuleInfo{std::move(ctxKeepAlive), std::move(module), - std::move(pm), /*ownedMmi=*/nullptr, - &mmiwp->getMMI()}; + return new MachineModuleInfo{ + std::move(ctxKeepAlive), std::move(module), std::move(pm), + /*ownedMmi=*/nullptr, + /*mmiwp=*/nullptr, &mmiwp->getMMI(), &tm}; }, "module"_a, "target_machine"_a, "global_isel"_a = false, nb::keep_alive<0, 2>()); @@ -1061,19 +1123,25 @@ void populate_mir(nb::module_ &m) { withDetail("failed to parse machine functions", diag)); } llvm::MachineModuleInfo *mmi = ownedMmi.get(); - return new MachineModuleInfo{std::move(ctxKeepAlive), std::move(module), - /*pm=*/nullptr, std::move(ownedMmi), mmi}; + return new MachineModuleInfo{std::move(ctxKeepAlive), + std::move(module), + /*pm=*/nullptr, + std::move(ownedMmi), + /*mmiwp=*/nullptr, + mmi, + &tm}; }, "text"_a, "context"_a, "target_machine"_a, nb::keep_alive<0, 3>()); - // Create a fresh, empty MachineFunction to build into: make an IR Function - // stub named `name` (a MachineFunction must attach to one), get its - // MachineFunction from a freshly owned MachineModuleInfo, and give it a - // single empty entry block. The stub's signature and linkage are surfaced as - // arguments rather than hardcoded; `function_type` defaults to `void()` (a - // MachineFunction anchored purely for building generic MIR never reads the IR - // signature). Consumes the module into the returned wrapper (like - // run_codegen_to_mir), which owns everything the MachineFunction depends on. + // Create a fresh MachineFunction to build into. Make an IR Function stub + // named `name` (a MachineFunction must attach to one) with a trivial body so + // it is a *definition* -- codegen only processes defined functions, so + // emit_object would otherwise skip it. The stub's signature and linkage are + // surfaced as arguments rather than hardcoded; `function_type` defaults to + // `void()` (a MachineFunction anchored purely for building MIR never reads + // the IR signature). Hold the MachineFunction in a + // MachineModuleInfoWrapperPass (not yet in a PassManager) so emit_object can + // hand it to one. Consumes the module into the returned wrapper. m.def( "create_machine_function", [](eudsl::Module &mod, llvm::TargetMachine &tm, const std::string &name, @@ -1099,14 +1167,20 @@ void populate_mir(nb::module_ &m) { llvm::Type::getVoidTy(module->getContext()), /*isVarArg=*/false); llvm::Function *f = llvm::Function::Create(fnTy, linkage, name, *module); + llvm::ReturnInst::Create( + module->getContext(), + llvm::BasicBlock::Create(module->getContext(), "entry", f)); - auto ownedMmi = std::make_unique(&tm); - llvm::MachineFunction &mf = ownedMmi->getOrCreateMachineFunction(*f); + auto mmiwp = std::make_unique(&tm); + llvm::MachineFunction &mf = + mmiwp->getMMI().getOrCreateMachineFunction(*f); mf.push_back(mf.CreateMachineBasicBlock()); - llvm::MachineModuleInfo *mmi = ownedMmi.get(); - return new MachineModuleInfo{std::move(ctxKeepAlive), std::move(module), - /*pm=*/nullptr, std::move(ownedMmi), mmi}; + llvm::MachineModuleInfo *mmi = &mmiwp->getMMI(); + return new MachineModuleInfo{ + std::move(ctxKeepAlive), std::move(module), + /*pm=*/nullptr, + /*ownedMmi=*/nullptr, std::move(mmiwp), mmi, &tm}; }, "module"_a, "target_machine"_a, "name"_a, "function_type"_a = nullptr, "linkage"_a = llvm::GlobalValue::LinkageTypes::ExternalLinkage, diff --git a/projects/eudsl-llvmpy/tests/mir/test_emit_jit.py b/projects/eudsl-llvmpy/tests/mir/test_emit_jit.py new file mode 100644 index 000000000..c021a0a89 --- /dev/null +++ b/projects/eudsl-llvmpy/tests/mir/test_emit_jit.py @@ -0,0 +1,124 @@ +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Route B capstone: emit an object from hand-built selected MIR and JIT it. + +emit_object runs the back half of codegen (register allocation, prologue/ +epilogue, object emission) over already-selected MIR via -start-after= +finalize-isel, so no instruction selection runs. LLJIT.add_object then loads the +object so the DSL-built function can be called. The instructions are AArch64 +(ADDWrr/COPY/RET_ReallyLR), so actually executing the result needs an AArch64 +host; object emission itself is host-independent (cross ELF). +""" + +import ctypes +import platform +from textwrap import dedent + +import pytest + +import llvm +from llvm import ir, jit, mir +from llvm.testing import assert_no_leaks + +# Object emission uses an AArch64 target (cross ELF), so needs the AArch64 +# backend linked; JIT-executing additionally needs an AArch64 host (below). +pytestmark = pytest.mark.skipif( + "aarch64" not in llvm.jit.registered_targets(), + reason="AArch64 backend not linked (EUDSL_LLVMPY_TARGETS)", +) + +_AARCH64_LINUX = "aarch64-unknown-linux-gnu" +_IS_AARCH64 = platform.machine() in ("arm64", "aarch64") + + +def _build_selected_add(mmi): + """Hand-build a fully-selected AArch64 add(i32,i32)->i32 (see #589).""" + mf = mmi.machine_function("add") + b = mir.MachineIRBuilder(mf) + entry = mf.blocks[0] + gpr32 = mf.reg_class("GPR32") + w0, w1 = mf.physreg("W0"), mf.physreg("W1") + entry.add_livein(w0) + entry.add_livein(w1) + v0, v1, v2 = (mf.create_vreg(gpr32) for _ in range(3)) + copy = mf.opcode("COPY") + for dst, src in ((v0, w0), (v1, w1)): + c = b.build_instr(copy) + c.add_reg(dst, is_def=True) + c.add_reg(src) + add = b.build_instr(mf.opcode("ADDWrr")) + add.add_reg(v2, is_def=True) + add.add_reg(v0) + add.add_reg(v1) + ret_copy = b.build_instr(copy) + ret_copy.add_reg(w0, is_def=True) + ret_copy.add_reg(v2) + ret = b.build_instr(mf.opcode("RET_ReallyLR")) + ret.add_reg(w0, implicit=True) + for prop in ("IsSSA", "TracksLiveness", "NoPHIs"): + mf.set_property(getattr(mir.MachineFunctionProperty, prop)) + return mf + + +def test_emit_object_produces_a_relocatable_object(): + with ir.Context() as ctx: + mod = ir.Module("m", ctx) + tm = jit.TargetMachine(triple=_AARCH64_LINUX) # cross: ELF, any host + mmi = mir.create_machine_function(mod, tm, "add") + _build_selected_add(mmi) + obj = mmi.emit_object() + assert len(obj) > 0 + assert obj[:4] == b"\x7fELF" # a real ELF relocatable object + assert_no_leaks() + + +def test_emit_object_twice_raises(): + with ir.Context() as ctx: + mod = ir.Module("m", ctx) + tm = jit.TargetMachine(triple=_AARCH64_LINUX) + mmi = mir.create_machine_function(mod, tm, "add") + _build_selected_add(mmi) + mmi.emit_object() + with pytest.raises(RuntimeError, match="already emitted"): + mmi.emit_object() + assert_no_leaks() + + +def test_emit_object_requires_create_machine_function(): + src = dedent("""\ + define i32 @f(i32 %a) { + ret i32 %a + } + """) + with ir.Context() as ctx: + mod = ir.parse_assembly(src, ctx, "m") + tm = jit.TargetMachine(triple=_AARCH64_LINUX) + mmi = mir.run_codegen_to_mir(mod, tm) # no build-path MMIWrapperPass + with pytest.raises(RuntimeError, match="create_machine_function"): + mmi.emit_object() + assert_no_leaks() + + +@pytest.mark.skipif( + not _IS_AARCH64, + reason="hand-built MIR is AArch64; executing it needs an AArch64 host", +) +def test_jit_executes_hand_built_add(): + with ir.Context() as ctx: + mod = ir.Module("m", ctx) + tm = jit.TargetMachine() # host triple -> object loadable in-process + mmi = mir.create_machine_function(mod, tm, "add") + _build_selected_add(mmi) + obj = mmi.emit_object() + + j = jit.LLJIT() + j.add_object(obj) + add = ctypes.CFUNCTYPE(ctypes.c_int32, ctypes.c_int32, ctypes.c_int32)( + j.lookup("add") + ) + assert add(2, 3) == 5 + assert add(40, 2) == 42 + assert add(-5, 5) == 0 + del j + assert_no_leaks() From ab76058c4e651493ba27f5923c4b89d3ee2082c1 Mon Sep 17 00:00:00 2001 From: makslevental Date: Wed, 19 Aug 2026 08:41:56 -0700 Subject: [PATCH 2/2] [eudsl-llvmpy] Address emit/JIT review: make malformed input a catchable error emit_object is the first consumer of the hand-built MIR primitives, and it ran codegen with the verifier off and diagnostics unhandled -- so malformed MIR became a garbage object or a process abort, never a Python exception. - emit_object now verifies every MachineFunction up front and raises (withDetail) if the MIR is malformed, before running the emission pipeline (which still passes DisableVerify=true, now that we verify ourselves). - Wrap pm->run in a ScopedDiagnosticCapture so a back-half codegen failure (DS_Error) surfaces as an exception instead of stderr+exit; also raise on an empty emitted buffer. (Both unreachable from well-formed MIR after the eager verify, hence LCOV_EXCL.) - add_object parses the bytes eagerly (ObjectFile::createObjectFile) so bad / truncated / wrong-format input raises at add_object rather than resurfacing as an opaque materialization error at a later lookup (ORC's addObjectFile is lazy). CLAUDE.md / comments: - brace the multi-line create_machine_function-required guard; brace the two LCOV_EXCL throw blocks and wrap them in LCOV_EXCL_START/STOP (matching the addISelPasses convention) so the closing brace stays excluded. - note the -start-after mutation relies on the GIL (no lock); fix the "asm/object emission" comment to "object emission" (ObjectFile only). - drop the "(see #589)" forward reference in the test helper docstring. Tests: reject malformed MIR (physregs without live-ins), reject non-object bytes in add_object, assert -start-after is restored (a later run_codegen_to_mir still runs full ISel), and a host-independent symbol-table check that the object actually defines `add`. src/IR + src/MIR stay at 100% line+function coverage. --- projects/eudsl-llvmpy/src/IR/JIT.cpp | 9 +++ projects/eudsl-llvmpy/src/MIR/Machine.cpp | 68 ++++++++++++++---- .../eudsl-llvmpy/tests/mir/test_emit_jit.py | 69 +++++++++++++++++-- 3 files changed, 130 insertions(+), 16 deletions(-) diff --git a/projects/eudsl-llvmpy/src/IR/JIT.cpp b/projects/eudsl-llvmpy/src/IR/JIT.cpp index 724c93476..b3b6e4d75 100644 --- a/projects/eudsl-llvmpy/src/IR/JIT.cpp +++ b/projects/eudsl-llvmpy/src/IR/JIT.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -47,6 +48,14 @@ void populate_jit(nb::module_ &m) { llvm::StringRef(static_cast(obj.data()), obj.size()), ""); + // ORC's addObjectFile is lazy: it records the buffer and defers + // parsing to materialization (a later lookup), so bad bytes would + // otherwise surface as an opaque error attributed to the wrong + // operation. Parse eagerly here so add_object itself raises on a + // non-object / truncated / wrong-format buffer. + eudsl::unwrap(llvm::object::ObjectFile::createObjectFile( + memBuf->getMemBufferRef()) + .takeError()); eudsl::unwrap(self.addObjectFile(std::move(memBuf))); }, "obj"_a, diff --git a/projects/eudsl-llvmpy/src/MIR/Machine.cpp b/projects/eudsl-llvmpy/src/MIR/Machine.cpp index 65d4d1aab..452088c4d 100644 --- a/projects/eudsl-llvmpy/src/MIR/Machine.cpp +++ b/projects/eudsl-llvmpy/src/MIR/Machine.cpp @@ -951,21 +951,46 @@ void populate_mir(nb::module_ &m) { [](MachineModuleInfo &self) { if (self.emitted) throw std::runtime_error("object already emitted"); - if (!self.mmiwp || !self.tm) + if (!self.mmiwp || !self.tm) { throw std::runtime_error( "emit_object requires a module built with " "create_machine_function"); + } + // Verify the hand-built MIR up front so malformed input (the prior + // PRs' unchecked primitives can produce it: bogus properties, + // undefined vregs, ...) raises a catchable Python error instead of + // muddling through the emission pipeline -- whose in-pass verifier + // and asserts are gone under NDEBUG -- to a garbage object or a + // fatal codegen abort. The pipeline runs with DisableVerify=true + // since we verify here. + std::string report; + llvm::raw_string_ostream reportOS(report); + bool ok = true; + for (llvm::Function &f : *self.module) { + if (llvm::MachineFunction *mf = self.mmi->getMachineFunction(f)) { + ok &= mf->verify(/*p=*/nullptr, /*Banner=*/nullptr, &reportOS, + /*AbortOnError=*/false); + } + } + if (!ok) + throw std::runtime_error( + withDetail("hand-built MIR failed verification", report)); + // Run only the back half of codegen (regalloc, prologue/epilogue, - // asm/object emission) over the already-selected MachineFunctions: + // object emission) over the already-selected MachineFunctions: // -start-after=finalize-isel skips instruction selection so the // hand-built MIR is used as-is. The option is process-global (read // by the pass config addPassesToEmitFile builds), so set+restore - // it. + // it; there is no lock, so this relies on the GIL serializing + // callers (no concurrent/nested/free-threaded codegen). auto &opts = llvm::cl::getRegisteredOptions(); auto it = opts.find("start-after"); - if (it == opts.end()) - throw std::runtime_error( // LCOV_EXCL_LINE - "the -start-after option is not registered"); // LCOV_EXCL_LINE + // LCOV_EXCL_START -- start-after is always registered by codegen + if (it == opts.end()) { + throw std::runtime_error( + "the -start-after option is not registered"); + } + // LCOV_EXCL_STOP auto &startAfter = *static_cast *>(it->second); std::string saved = startAfter; @@ -983,23 +1008,42 @@ void populate_mir(nb::module_ &m) { self.pm = std::make_unique(); // addPassesToEmitFile adopts the MMIWrapperPass into `pm` (which we // keep, so `mmi` stays valid); it holds the built MachineFunctions. + // LCOV_EXCL_START -- AArch64 can always emit an object file if (self.tm->addPassesToEmitFile( *self.pm, os, nullptr, llvm::CodeGenFileType::ObjectFile, - /*DisableVerify=*/true, self.mmiwp.release())) - throw std::runtime_error( // LCOV_EXCL_LINE - "target cannot emit an object file"); // LCOV_EXCL_LINE + /*DisableVerify=*/true, self.mmiwp.release())) { + throw std::runtime_error("target cannot emit an object file"); + } + // LCOV_EXCL_STOP // The back half (register allocation) requires reserved registers // to be frozen; the front of the pipeline normally does this, so do // it here for the hand-built MachineFunctions. - for (llvm::Function &f : *self.module) + for (llvm::Function &f : *self.module) { if (llvm::MachineFunction *mf = self.mmi->getMachineFunction(f)) mf->getRegInfo().freezeReservedRegs(); - self.pm->run(*self.module); + } + // A codegen pass reports failure through the context diagnostic + // handler (DS_Error -> stderr + exit under the default handler), + // not pm->run()'s value; capture it so it surfaces as an exception. + std::string diag; + { + ScopedDiagnosticCapture capture(self.module->getContext(), diag); + self.pm->run(*self.module); + } self.emitted = true; + // LCOV_EXCL_START -- eager verification makes a back-half failure + // or empty emission unreachable from well-formed hand-built MIR. + if (!diag.empty()) + throw std::runtime_error( + withDetail("object emission failed", diag)); + if (buf.empty()) + throw std::runtime_error("object emission produced no output"); + // LCOV_EXCL_STOP return nb::bytes(buf.data(), buf.size()); }, "Emit a relocatable object file for the built (already-selected) MIR " - "by running the back half of codegen (regalloc, emission)."); + "by running the back half of codegen (regalloc, emission). Verifies " + "the MIR first, raising if it is malformed."); // Run instruction selection on an IR module and hand back the // MachineModuleInfo that owns the resulting MachineFunctions. Consumes the diff --git a/projects/eudsl-llvmpy/tests/mir/test_emit_jit.py b/projects/eudsl-llvmpy/tests/mir/test_emit_jit.py index c021a0a89..a642b2ad9 100644 --- a/projects/eudsl-llvmpy/tests/mir/test_emit_jit.py +++ b/projects/eudsl-llvmpy/tests/mir/test_emit_jit.py @@ -32,15 +32,18 @@ _IS_AARCH64 = platform.machine() in ("arm64", "aarch64") -def _build_selected_add(mmi): - """Hand-build a fully-selected AArch64 add(i32,i32)->i32 (see #589).""" +def _build_selected_add(mmi, declare_liveins=True): + """Hand-build a fully-selected AArch64 add(i32,i32)->i32 MachineFunction: + liveins $w0/$w1, two COPYs in, ADDWrr, a COPY to $w0, RET_ReallyLR. With + declare_liveins=False the live-ins are omitted, so the MIR fails verify().""" mf = mmi.machine_function("add") b = mir.MachineIRBuilder(mf) entry = mf.blocks[0] gpr32 = mf.reg_class("GPR32") w0, w1 = mf.physreg("W0"), mf.physreg("W1") - entry.add_livein(w0) - entry.add_livein(w1) + if declare_liveins: + entry.add_livein(w0) + entry.add_livein(w1) v0, v1, v2 = (mf.create_vreg(gpr32) for _ in range(3)) copy = mf.opcode("COPY") for dst, src in ((v0, w0), (v1, w1)): @@ -70,6 +73,10 @@ def test_emit_object_produces_a_relocatable_object(): obj = mmi.emit_object() assert len(obj) > 0 assert obj[:4] == b"\x7fELF" # a real ELF relocatable object + # Host-independent guard that the object actually defines `add`: the + # symbol name lives null-terminated in the ELF string table. (Executing + # it is checked separately on AArch64 hosts.) + assert b"add\x00" in obj assert_no_leaks() @@ -100,6 +107,60 @@ def test_emit_object_requires_create_machine_function(): assert_no_leaks() +def test_emit_object_rejects_malformed_mir(): + """emit_object verifies first, so malformed hand-built MIR (here: physregs + read without live-in declarations) raises instead of muddling through the + emission pipeline to a garbage object or a fatal codegen abort.""" + with ir.Context() as ctx: + mod = ir.Module("m", ctx) + tm = jit.TargetMachine(triple=_AARCH64_LINUX) + mmi = mir.create_machine_function(mod, tm, "add") + _build_selected_add(mmi, declare_liveins=False) + with pytest.raises(RuntimeError, match="failed verification"): + mmi.emit_object() + assert_no_leaks() + + +def test_add_object_rejects_non_object_bytes(): + """add_object parses eagerly, so bad bytes raise here rather than resurfacing + as an opaque materialization error at a later lookup.""" + j = jit.LLJIT() + with pytest.raises(RuntimeError): + j.add_object(b"not an object file") + del j + assert_no_leaks() + + +def test_start_after_is_restored_after_emit(): + """emit_object mutates the process-global -start-after option and must + restore it, so a subsequent run_codegen_to_mir still runs full instruction + selection (rather than skipping to finalize-isel and producing empty MIR).""" + with ir.Context() as ctx: + mod = ir.Module("m", ctx) + tm = jit.TargetMachine(triple=_AARCH64_LINUX) + mmi = mir.create_machine_function(mod, tm, "add") + _build_selected_add(mmi) + mmi.emit_object() + assert_no_leaks() + + src = dedent("""\ + define i32 @f(i32 %a, i32 %b) { + %s = add i32 %a, %b + ret i32 %s + } + """) + with ir.Context() as ctx: + mod = ir.parse_assembly(src, ctx, "m") + tm = jit.TargetMachine(triple=_AARCH64_LINUX) + mmi = mir.run_codegen_to_mir(mod, tm) + mf = mmi.machine_function("f") + # Full ISel ran: the function has real selected instructions and + # verifies. A leaked -start-after would skip ISel and leave it empty. + assert mf.verify() is True + assert list(mf.blocks[0].instructions) + assert_no_leaks() + + @pytest.mark.skipif( not _IS_AARCH64, reason="hand-built MIR is AArch64; executing it needs an AArch64 host",