Skip to content

Don't push a poison aggregate onto the sret rooting worklist - #3152

Open
kshyatt wants to merge 1 commit into
EnzymeAD:mainfrom
kshyatt:fixup-poison-insertvalue-base
Open

Don't push a poison aggregate onto the sret rooting worklist#3152
kshyatt wants to merge 1 commit into
EnzymeAD:mainfrom
kshyatt:fixup-poison-insertvalue-base

Conversation

@kshyatt

@kshyatt kshyatt commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Another 🤖 assisted PR, I ran into this while trying to fix things for a very long tape fed into Enzyme.jl:

needsReRooting walks every value stored into an sret argument to check each tracked pointer is also parked in the returnRoots array. Walking back through an insertvalue chain, the branch for an insert of an untracked field pushed the aggregate operand unconditionally:

CountTrackedPointers tracked(IVI->getInsertedValueOperand()->getType());
if (tracked.count == 0) { storedValues.push_back(IVI->getAggregateOperand()); continue; }

When that operand is the poison the chain was built from, the bare poison lands on the worklist. The struct handling below then looks for an extractvalue covering poison's tracked fields, finds none, and reports

 failed to find extracted pointer for %tape poison at index 1

before falling through to the pointer-shaped handling, whose assert(0) fires on any non-pointer value. Julia hits this whenever a nested sub-tape survives SROA as a whole-aggregate store and its first field is untracked -- reverse mode over PEPSKit's CTMRG aborts the process this way, since the shipped Enzyme_jll RelWithDebInfo build leaves assertions live.

A poison, undef, or zeroinitializer aggregate has no live pointer in any field, so it needs no root regardless of which field the insertvalue overwrites. Hoist the trivial-base test above both branches and drop such a base, which is the reasoning the tracked branch three lines down already relies on. In the Julia case the tracked pointers are separately stored into the returnRoots, so the walk then completes and the pass correctly concludes it has no work to do.

Also un-shadow legal in the struct handling. The inner declaration meant the coverage failure set a variable nobody read and fell into that same assert; report it through CustomErrorHandler like the pointer path does and reroot conservatively. This is not a behaviour change for NDEBUG builds, which already fell through to exactly that handler.

@oscardssmith oscardssmith left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not read, but to run ci

@kshyatt
kshyatt force-pushed the fixup-poison-insertvalue-base branch from d3d7590 to e41cac3 Compare August 15, 2026 09:57
@kshyatt

kshyatt commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

I got added to the repo so you can unapprove now :D

`needsReRooting` walks every value stored into an sret argument to check each
tracked pointer is also parked in the returnRoots array. Walking back through an
insertvalue chain, the branch for an insert of an untracked field pushed the
aggregate operand unconditionally:

    CountTrackedPointers tracked(IVI->getInsertedValueOperand()->getType());
    if (tracked.count == 0) { storedValues.push_back(IVI->getAggregateOperand()); continue; }

When that operand is the poison the chain was built from, the bare poison lands
on the worklist. The struct handling below then looks for an extractvalue
covering poison's tracked fields, finds none, and reports

     failed to find extracted pointer for %tape poison at index 1

before falling through to the pointer-shaped handling, whose assert(0) fires on
any non-pointer value. Julia hits this whenever a nested sub-tape survives SROA
as a whole-aggregate store and its first field is untracked -- reverse mode over
PEPSKit's CTMRG aborts the process this way, since the shipped Enzyme_jll
RelWithDebInfo build leaves assertions live.

A poison, undef, or zeroinitializer aggregate has no live pointer in any field,
so it needs no root regardless of which field the insertvalue overwrites. Hoist
the trivial-base test above both branches and drop such a base, which is the
reasoning the tracked branch three lines down already relies on. In the Julia
case the tracked pointers are separately stored into the returnRoots, so the
walk then completes and the pass correctly concludes it has no work to do.

Also un-shadow `legal` in the struct handling. The inner declaration meant the
coverage failure set a variable nobody read and fell into that same assert;
report it through CustomErrorHandler like the pointer path does and reroot
conservatively. This is not a behaviour change for NDEBUG builds, which already
fell through to exactly that handler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kshyatt
kshyatt force-pushed the fixup-poison-insertvalue-base branch from e41cac3 to 17915ea Compare August 16, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants