Ok, let me explain.
PREVIOUSLY:
When generating bytecode for a loop, we visitStmt(Body), which will create its own local scope (that makes sense) and at the end of said scope, it will emit Destroy ops for all local variables, as well as emit code for the destructors of those local variables. However, the Destroy op calls the destructor for all data in the Block, and converts the blocks to DeadBlocks. That means the contents aren't usable anymore after it, and when we jump to the beginning of the loop and try to assign something to the block, we might end up trying to use the unusable data. This happens in the copy constructor of our Pointer class for example.
In pseudo code:
loop { create locals... local destructors Destroy Scope jump to beginning }
NOW:
I've split emitting the destructors for locals and emitting the Destroy op for a scope up into two separate parts. They are mostly still the same, i.e.when using a AutoScope or any of its subclasses, which emits the destructors and the Destroy op in its own destructor.
When visiting a loop, we create our own LocalScope to manage the lifetimes of locals created within the body and then call emitDestructors of that scope right before the jmp to the beginning of the loop. The Destroy op for the scope is emitted in the LocalScope destructor, so will only be emitted once.
In pseudo code:
loop { create locals... local destructors jump to beginning } Destroy Scope
The changes in this patch are on top of https://reviews.llvm.org/D137070, but the patch needs to go in before that or be merged with it.
Should we be setting Idx = std::nullopt; after this so that the LocalScope destructor does not also emit a destroy for the same Idx?