You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Lowering hand-built generic MIR (Route A) is blocked by the GlobalISel↔SelectionDAG entry-point tension
The MIR stack (#579–#591) ended up supporting two routes from Python-built MIR to native code. Route B is implemented; Route A is not, and this issue records why — the reason is architectural, not an oversight, and it's worth having written down.
The two routes
Route A — build generic (G_*) MIR, then run instruction selection on it. @machine_function / MachineIRBuilder produce target-independent G_ADD/G_ICMP/G_PHI/… MIR. The natural next step would be to hand that MIR to the target's post-IRTranslator pipeline (Legalizer → RegBankSelect → InstructionSelect → regalloc → emission) and get an object out. This does not work today.
SelectionDAG is not a MIR→MIR pass.SelectionDAGISel builds its DAG from IR (one basic block at a time) and only then emits MIR. So the SelectionDAG path fundamentally cannot consume hand-built generic MIR — there is nothing to feed it but IR.
GlobalISel is MIR→MIR, but its failure path re-enters SelectionDAG from IR. GlobalISel's InstructionSelect (and the earlier legalize/regbankselect steps) operate on MIR, so in principle they could lower hand-built generic MIR. But when GlobalISel can't handle a construct, the abort mode falls back to SelectionDAG — which needs the IR. Hand-built MIR has no faithful IR (Route B's create_machine_function attaches only a trivial stub definition so codegen doesn't skip the function), so the fallback can't reproduce the intended semantics. run_codegen_to_mir(global_isel=True) ([eudsl-llvmpy] Run the GlobalISel pipeline + expose the machine verifier #588) works precisely because it starts from IR: it runs IRTranslator → Legalizer → RegBankSelect → InstructionSelect and never has externally-authored generic MIR to preserve.
The pipeline entry point is IR-shaped.TargetPassConfig / addPassesToEmitFile build a pipeline that begins at the IR. To lower pre-existing generic MIR you'd need to start it partway (e.g. -start-before=legalizer / after IRTranslator) while feeding in the already-built MachineFunctions. The only levers today are the fragile process-global start-before/start-aftercl::opts (Route B uses start-after=finalize-isel with a set/restore guard — see [eudsl-llvmpy] Emit object from selected MIR + JIT-execute it (Route B capstone) #590 and the note in [TODO] MIR stack: deferred review items (#579–#591) #593 item 1) or a programmatic TargetPassConfig::setStartStopPasses, which isn't cleanly bound.
Current mitigations already in place (context, not fixes for Route A)
[eudsl-llvmpy] Run the GlobalISel pipeline + expose the machine verifier #588: run_codegen_to_mir(global_isel=True) uses GlobalISelAbortMode::DisableWithDiag (aborting mode would kill the process) and then scans the result for residual G_* ops, raising if selection didn't complete — because the fallback's diagnostic is only a warning. It also sets the GlobalISel flags per-call (AArch64 defaults EnableGlobalISel=true).
These make the IR→selected-MIR path safe; they do not enable hand-built-generic-MIR→selected-MIR.
What would unblock Route A (for discussion)
A clean binding for TargetPassConfig::setStartStopPasses (or equivalent) to construct a "start at Legalizer, stop after emission" pipeline over externally-built generic MIR — replacing the process-global start-*cl::opt mechanism entirely.
Confirming the MachineModuleInfoWrapperPass build path (from create_machine_function) can carry generic MIR through those passes, and deciding what — if any — IR stub the functions need for the passes that still consult it.
Until one of those lands, Route B is the supported way to reach native code from Python-built MIR, and Route A generic-MIR lowering is only reachable indirectly by going back through IR (run_codegen_to_mir).
Refs: #588 (GlobalISel gating + residual scan), #589 (build selected MIR), #590 (emit_object / -start-after=finalize-isel), and the deferred-items tracker #593 (item 1 covers the start-aftercl::opt fragility).
Lowering hand-built generic MIR (Route A) is blocked by the GlobalISel↔SelectionDAG entry-point tension
The MIR stack (#579–#591) ended up supporting two routes from Python-built MIR to native code. Route B is implemented; Route A is not, and this issue records why — the reason is architectural, not an oversight, and it's worth having written down.
The two routes
Route A — build generic (
G_*) MIR, then run instruction selection on it.@machine_function/MachineIRBuilderproduce target-independentG_ADD/G_ICMP/G_PHI/… MIR. The natural next step would be to hand that MIR to the target's post-IRTranslator pipeline (Legalizer → RegBankSelect → InstructionSelect → regalloc → emission) and get an object out. This does not work today.Route B — build already-selected target MIR by hand, run only the back half of codegen. ([eudsl-llvmpy] Build already-selected target MIR by hand (Route B foundation) #589, [eudsl-llvmpy] Emit object from selected MIR + JIT-execute it (Route B capstone) #590)
Resolve real register classes / physregs, emit target opcodes (
ADDWrr,COPY,RET_ReallyLR), set theMachineFunctionProperties, thenemit_object()runsaddPassesToEmitFilewith-start-after=finalize-iselso no instruction selection runs at all — just regalloc + prologue/epilogue + emission. This is what the capstone JIT-executes. It sidesteps the entire problem below.Why Route A is blocked
SelectionDAG is not a MIR→MIR pass.
SelectionDAGISelbuilds its DAG from IR (one basic block at a time) and only then emits MIR. So the SelectionDAG path fundamentally cannot consume hand-built generic MIR — there is nothing to feed it but IR.GlobalISel is MIR→MIR, but its failure path re-enters SelectionDAG from IR. GlobalISel's
InstructionSelect(and the earlier legalize/regbankselect steps) operate on MIR, so in principle they could lower hand-built generic MIR. But when GlobalISel can't handle a construct, the abort mode falls back to SelectionDAG — which needs the IR. Hand-built MIR has no faithful IR (Route B'screate_machine_functionattaches only a trivial stub definition so codegen doesn't skip the function), so the fallback can't reproduce the intended semantics.run_codegen_to_mir(global_isel=True)([eudsl-llvmpy] Run the GlobalISel pipeline + expose the machine verifier #588) works precisely because it starts from IR: it runs IRTranslator → Legalizer → RegBankSelect → InstructionSelect and never has externally-authored generic MIR to preserve.The pipeline entry point is IR-shaped.
TargetPassConfig/addPassesToEmitFilebuild a pipeline that begins at the IR. To lower pre-existing generic MIR you'd need to start it partway (e.g.-start-before=legalizer/ after IRTranslator) while feeding in the already-builtMachineFunctions. The only levers today are the fragile process-globalstart-before/start-aftercl::opts (Route B usesstart-after=finalize-iselwith a set/restore guard — see [eudsl-llvmpy] Emit object from selected MIR + JIT-execute it (Route B capstone) #590 and the note in [TODO] MIR stack: deferred review items (#579–#591) #593 item 1) or a programmaticTargetPassConfig::setStartStopPasses, which isn't cleanly bound.Current mitigations already in place (context, not fixes for Route A)
run_codegen_to_mir(global_isel=True)usesGlobalISelAbortMode::DisableWithDiag(aborting mode would kill the process) and then scans the result for residualG_*ops, raising if selection didn't complete — because the fallback's diagnostic is only a warning. It also sets the GlobalISel flags per-call (AArch64 defaultsEnableGlobalISel=true).What would unblock Route A (for discussion)
TargetPassConfig::setStartStopPasses(or equivalent) to construct a "start at Legalizer, stop after emission" pipeline over externally-built generic MIR — replacing the process-globalstart-*cl::optmechanism entirely.G_*scan from [eudsl-llvmpy] Run the GlobalISel pipeline + expose the machine verifier #588 is half of this already).MachineModuleInfoWrapperPassbuild path (fromcreate_machine_function) can carry generic MIR through those passes, and deciding what — if any — IR stub the functions need for the passes that still consult it.Until one of those lands, Route B is the supported way to reach native code from Python-built MIR, and Route A generic-MIR lowering is only reachable indirectly by going back through IR (
run_codegen_to_mir).Refs: #588 (GlobalISel gating + residual scan), #589 (build selected MIR), #590 (
emit_object/-start-after=finalize-isel), and the deferred-items tracker #593 (item 1 covers thestart-aftercl::optfragility).