compiler: compose inner effect splits into enclosing kernel shadows
Nanosoldier found large regressions (up to 53x on sum(skipmissing(...)),
~7x on logical-indexing sums, 6.6x on plain reductions) on the first
benchmark run of the effect-precondition branch. Root causes and fixes:
- Splits applied at per-element/iteration-protocol granularity expand
into a guard diamond with an outlined checked call per element, which
defeats vectorization - and, worse, the fallback invoke made
`scan_nothrow_split_ir` reject any enclosing kernel, so mis-placed
inner splits permanently blocked the correct kernel-level placement.
The inliner now marks each split's guard branch with a new
`IR_FLAG_SPLIT_GUARD` statement flag (which persists into cached
optimized IR), and the nothrow-shadow synthesis recognizes the marked
diamonds and composes them: the enclosing kernel's shadow keeps the
guard and turns the fallback region into `return false`, while the
assume variant folds the guard to the (equivalent) unchecked arm.
Both arms of a split compute the same value, so a failing inner guard
merely sends the enclosing kernel to its own checked fallback.
- The shadow's load-taint analysis now exempts element loads whose
memory eltype differs from every deleted store's memory eltype
(`Memory{T}` and `Memory{S}` are distinct objects when `T !== S`),
which lets kernels like `_unsafe_getindex!` - stores to the freshly
allocated destination, guard conditions derived from mask chunk
loads - synthesize their precondition.
- `simd_index` gets its `@inbounds` back: the index is in bounds by
construction of the `@simd` macro expansion, and the checked range
access added per-iteration conditions to every `@simd` kernel's
shadow that LLVM's IRCE cannot fold into endpoint comparisons
(single-condition affine exit loops fold; multi-condition ones keep
the check as an O(n) loop costing more than the guarded kernel).
- The `SkipMissing` `mapreduce_impl` is restructured to the kernel-split
pattern (`_mapreduce_impl_skipmissing_base` with plain checked element
accesses) instead of per-element splits inside its loops.
With these, `sum(::Vector)` and `sum(skipmissing(...))` benchmark at
parity with master and `sum(A[mask])` recovers from 6x to 1.65x; the
residual is the inner `iterate` shadow surviving as a second mask walk
because its bound checks, while implied by dominating branches, are not
folded - a follow-up (dominance-implication folding in shadows, or the
planned shadow-as-CodeInstance restructuring) closes that.
Deterministic codegen regression tests pin the structure of the three
benchmark families: no bounds-error throws, vectorized fast loops, no
`middle.split` (the signature of an unfolded check loop), and zero
remaining inner fallback invokes after composition.
This commit was written with the assistance of generative AI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>