Follow-up to #648. Today MirModule.emit_object(scheduler=, regalloc=) runs the entire back half of codegen (-start-after=finalize-isel through object emission) and returns the object bytes. There is no way to run codegen up to a chosen pass and get the intermediate MachineFunctions back for inspection — so you can't, from Python, observe the MIR after a specific stage (e.g. after register allocation / virtregrewriter, where the function is no longer in SSA form and has no virtual registers), which is exactly what you want when debugging a custom RegAllocBase/MachineSchedStrategy.
Proposal
Add a "run to a named pass and retain the MIR" entry point, e.g.:
mmi2 = mmi.run_to("virtregrewriter", regalloc="my-alloc") # or stop_before=
print(mmi2.to_mir()) # inspect post-RA, non-SSA MIR
Semantics: run the codegen pipeline from finalize-isel up to (and including / before) the named pass, then retain the resulting MachineFunctions in the returned MirModule for inspection via the existing to_mir() / machine_function() surface — rather than continuing to emission and freeing them.
Mechanism (mirrors what already exists)
- LLVM already provides process-global
-stop-after=<pass> / -stop-before=<pass> cl::opts, analogous to the -start-after=finalize-isel that emit_object already sets and restores (see the Restore RAII in MirModule::emitObject, src/MIR/Machine.cpp). Set/restore stop-after/stop-before the same way (GIL-serialized; no lock).
- Retaining MIR after stopping is the same trick
create_machine_function uses for the ISel boundary: don't append the object-emission passes (addPassesToEmitFile would add FreeMachineFunctionPass and free the functions), keep the MachineModuleInfoWrapperPass alive in the returned wrapper's owned state (the BuildOwned/EmittedOwned variant in Machine.cpp), so the MachineFunctions stay queryable — exactly like the current build/inspect path.
- The scheduler/regalloc selection (
-misched, RegisterRegAlloc::setDefault) and the pendingCodegenError stash/re-raise machinery should compose unchanged, so you can run a custom allocator and stop right after it.
Scope
- New
MirModule.run_to(pass_name, *, stop_before=False, scheduler=None, regalloc=None) (name TBD) that runs finalize-isel -> <pass> and returns a MirModule whose MachineFunctions are retained.
- Validate the pass name against the registered pass pipeline; an unknown name raises a catchable Python error (as
emit_object does for unknown scheduler/regalloc names).
- Reuse the existing state machine so a module can be inspected and then still
emit_object'd (or make the retain-vs-emit interaction explicit and one-shot, matching the current BuildOwned/EmittedOwned handling).
- Tests: run to
virtregrewriter with a custom allocator, assert the returned MIR has no virtual registers / uses physregs (i.e. is post-RA, non-SSA); run to a pre-RA pass and assert vregs are still present; unknown-pass-name raises. Keep the 100% C++ coverage gate.
Notes
- This pairs naturally with the existing
to_mir() and the -start-after=finalize-isel plumbing; it's mostly option set/restore + retaining the MMI wrapper, not new pipeline construction.
- Useful well beyond regalloc (inspecting scheduling results, prologue/epilogue insertion, etc.).
cc #600
Follow-up to #648. Today
MirModule.emit_object(scheduler=, regalloc=)runs the entire back half of codegen (-start-after=finalize-iselthrough object emission) and returns the object bytes. There is no way to run codegen up to a chosen pass and get the intermediateMachineFunctions back for inspection — so you can't, from Python, observe the MIR after a specific stage (e.g. after register allocation /virtregrewriter, where the function is no longer in SSA form and has no virtual registers), which is exactly what you want when debugging a customRegAllocBase/MachineSchedStrategy.Proposal
Add a "run to a named pass and retain the MIR" entry point, e.g.:
Semantics: run the codegen pipeline from
finalize-iselup to (and including / before) the named pass, then retain the resultingMachineFunctions in the returnedMirModulefor inspection via the existingto_mir()/machine_function()surface — rather than continuing to emission and freeing them.Mechanism (mirrors what already exists)
-stop-after=<pass>/-stop-before=<pass>cl::opts, analogous to the-start-after=finalize-iselthatemit_objectalready sets and restores (see theRestoreRAII inMirModule::emitObject,src/MIR/Machine.cpp). Set/restorestop-after/stop-beforethe same way (GIL-serialized; no lock).create_machine_functionuses for the ISel boundary: don't append the object-emission passes (addPassesToEmitFilewould addFreeMachineFunctionPassand free the functions), keep theMachineModuleInfoWrapperPassalive in the returned wrapper's owned state (theBuildOwned/EmittedOwnedvariant inMachine.cpp), so theMachineFunctions stay queryable — exactly like the current build/inspect path.-misched,RegisterRegAlloc::setDefault) and thependingCodegenErrorstash/re-raise machinery should compose unchanged, so you can run a custom allocator and stop right after it.Scope
MirModule.run_to(pass_name, *, stop_before=False, scheduler=None, regalloc=None)(name TBD) that runsfinalize-isel -> <pass>and returns aMirModulewhoseMachineFunctions are retained.emit_objectdoes for unknown scheduler/regalloc names).emit_object'd (or make the retain-vs-emit interaction explicit and one-shot, matching the currentBuildOwned/EmittedOwnedhandling).virtregrewriterwith a custom allocator, assert the returned MIR has no virtual registers / uses physregs (i.e. is post-RA, non-SSA); run to a pre-RA pass and assert vregs are still present; unknown-pass-name raises. Keep the 100% C++ coverage gate.Notes
to_mir()and the-start-after=finalize-iselplumbing; it's mostly option set/restore + retaining the MMI wrapper, not new pipeline construction.cc #600