From 6a3b31af5dbb22749e6d5a1cad9dfaab6863b615 Mon Sep 17 00:00:00 2001 From: _ov4 Date: Sat, 17 Oct 2026 08:36:19 +0000 Subject: feat(classes): destroy stack objects during exception unwinding Co-Authored-By: Claude Opus 5.5 --- include/cps/ExceptionHandler.h | 14 +++++- lib/CodeGen/CodeGen.cc | 2 +- lib/CodeGen/ConstructorGen.cc | 16 ++----- lib/CodeGen/ExceptionHandler.cc | 46 +++++++++++++++++--- lib/CodeGen/FunctionGen.cc | 14 +++--- tests/i/class-delete-designators/case.json | 1 + tests/i/class-delete-designators/expected.txt | 8 ++++ tests/i/class-delete-designators/notes.md | 5 +++ tests/i/class-delete-designators/program.cps | 30 +++++++++++++ tests/i/class-destructor-throws/case.json | 1 + tests/i/class-destructor-throws/expected.txt | 12 ++++++ tests/i/class-destructor-throws/notes.md | 7 +++ tests/i/class-destructor-throws/program.cps | 61 +++++++++++++++++++++++++++ tests/i/class-unwind/case.json | 1 + tests/i/class-unwind/expected.txt | 14 ++++++ tests/i/class-unwind/notes.md | 5 +++ tests/i/class-unwind/program.cps | 56 ++++++++++++++++++++++++ 17 files changed, 265 insertions(+), 28 deletions(-) create mode 100644 tests/i/class-delete-designators/case.json create mode 100644 tests/i/class-delete-designators/expected.txt create mode 100644 tests/i/class-delete-designators/notes.md create mode 100644 tests/i/class-delete-designators/program.cps create mode 100644 tests/i/class-destructor-throws/case.json create mode 100644 tests/i/class-destructor-throws/expected.txt create mode 100644 tests/i/class-destructor-throws/notes.md create mode 100644 tests/i/class-destructor-throws/program.cps create mode 100644 tests/i/class-unwind/case.json create mode 100644 tests/i/class-unwind/expected.txt create mode 100644 tests/i/class-unwind/notes.md create mode 100644 tests/i/class-unwind/program.cps diff --git a/include/cps/ExceptionHandler.h b/include/cps/ExceptionHandler.h index 5c846ae..a2dc5d0 100644 --- a/include/cps/ExceptionHandler.h +++ b/include/cps/ExceptionHandler.h @@ -2,6 +2,7 @@ #include "llvm/IR/IRBuilder.h" #include "llvm/IR/Module.h" #include +#include #include namespace cps { @@ -17,8 +18,16 @@ class ExceptionHandler { struct FunctionState { std::vector Handlers; unsigned CatchDepth = 0; + bool HasCleanups = false; }; + struct CleanupPoint { + llvm::Instruction *Before; + FunctionState State; + bool Unwinding; + }; + std::map> CleanupPoints; + bool FillingUnwind = false; llvm::LLVMContext &Context; llvm::IRBuilder<> &Builder; llvm::Module &Module; @@ -38,7 +47,10 @@ public: llvm::AllocaInst *popHandler(); void beginCatch(llvm::AllocaInst *Exception); void endCatch(); - void emitReturnCleanup(); + llvm::Instruction *emitReturnCleanup(); + void enableCleanups(llvm::Function *F); + void recordExit(llvm::Instruction *Before, bool Unwinding = false); + void fillCleanups(llvm::Function *F, const std::function &Destroy); llvm::Value *emitCall(llvm::FunctionCallee Callee, llvm::ArrayRef Args, const llvm::Twine &Name = ""); diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index 8117434..0107910 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -186,7 +186,7 @@ void CodeGen::compile(const std::vector> &Statements) { } if (!Builder->GetInsertBlock()->hasTerminator()) { - Builder->CreateRet(ConstantInt::get(*TheContext, APInt(32, 0))); + Exceptions->recordExit(Builder->CreateRet(ConstantInt::get(*TheContext, APInt(32, 0)))); } finishStackObjects(F); diff --git a/lib/CodeGen/ConstructorGen.cc b/lib/CodeGen/ConstructorGen.cc index c0e50ac..e1033c3 100644 --- a/lib/CodeGen/ConstructorGen.cc +++ b/lib/CodeGen/ConstructorGen.cc @@ -1,5 +1,6 @@ #include "cps/CodeGen.h" #include "cps/MemoryHandler.h" +#include "cps/ExceptionHandler.h" #include "cps/TypeAST.h" #include #include @@ -168,19 +169,8 @@ void CodeGen::emitStackObjectCleanup(const StackObject &Object) { } void CodeGen::finishStackObjects(Function *F) { - IRBuilderBase::InsertPointGuard Guard(*Builder); - std::vector Exits; - for (auto &BB : *F) - if (BB.hasTerminator() && isa(BB.getTerminator())) - Exits.push_back(BB.getTerminator()); - for (auto *Exit : Exits) { - // Keep the original terminator in a continuation while cleanup builds branches. - auto *BB = Exit->getParent(); - auto *Continuation = BB->splitBasicBlock(Exit->getIterator(), "after_destruction"); - BB->getTerminator()->eraseFromParent(); - Builder->SetInsertPoint(BB); + Exceptions->fillCleanups(F, [this] { for (auto It = StackObjects.rbegin(); It != StackObjects.rend(); ++It) emitStackObjectCleanup(*It); - Builder->CreateBr(Continuation); - } + }); } diff --git a/lib/CodeGen/ExceptionHandler.cc b/lib/CodeGen/ExceptionHandler.cc index 21eb011..3f69417 100644 --- a/lib/CodeGen/ExceptionHandler.cc +++ b/lib/CodeGen/ExceptionHandler.cc @@ -1,6 +1,7 @@ #include "cps/ExceptionHandler.h" #include "llvm/IR/Constants.h" #include "llvm/IR/Instructions.h" +#include "llvm/Support/SaveAndRestore.h" #include using namespace llvm; @@ -72,15 +73,50 @@ void ExceptionHandler::endCatch() { if (!Builder.GetInsertBlock()->hasTerminator()) Builder.CreateCall(EndCatch); } -void ExceptionHandler::emitReturnCleanup() { +Instruction *ExceptionHandler::emitReturnCleanup() { auto &State = States[Builder.GetInsertBlock()->getParent()]; - for (unsigned I = 0; I < State.CatchDepth; ++I) Builder.CreateCall(EndCatch); + Instruction *First = nullptr; + for (unsigned I = 0; I < State.CatchDepth; ++I) { + auto *Call = Builder.CreateCall(EndCatch); + if (!First) First = Call; + } + return First; +} + +void ExceptionHandler::enableCleanups(Function *F) { + // Enable before emitting any call: a later DECLARE can already be alive + // when an earlier call executes again through a loop back edge. + States[F].HasCleanups = true; +} + +void ExceptionHandler::recordExit(Instruction *Before, bool Unwinding) { + auto *F = Before->getFunction(); + CleanupPoints[F].push_back({Before, States[F], Unwinding}); +} + +void ExceptionHandler::fillCleanups(Function *F, const std::function &Destroy) { + IRBuilderBase::InsertPointGuard Guard(Builder); + SaveAndRestore StateGuard(States[F]); + auto &Points = CleanupPoints[F]; + // Filling a normal exit may add an unwind point for a throwing destructor. + for (size_t I = 0; I < Points.size(); ++I) { + auto Point = Points[I]; + States[F] = Point.State; + SaveAndRestore UnwindGuard(FillingUnwind, Point.Unwinding); + auto *BB = Point.Before->getParent(); + auto *Continuation = BB->splitBasicBlock(Point.Before->getIterator(), "after_destruction"); + BB->getTerminator()->eraseFromParent(); + Builder.SetInsertPoint(BB); + Destroy(); + Builder.CreateBr(Continuation); + } + Points.clear(); } BasicBlock *ExceptionHandler::emitUnwindDestination() { auto *F = Builder.GetInsertBlock()->getParent(); auto &State = States[F]; - if (State.Handlers.empty() && State.CatchDepth == 0) return nullptr; + if (FillingUnwind || (State.Handlers.empty() && State.CatchDepth == 0 && !State.HasCleanups)) return nullptr; ensureRuntime(); F->setPersonalityFn(cast(Personality.getCallee())); auto Saved = Builder.saveIP(); @@ -91,7 +127,7 @@ BasicBlock *ExceptionHandler::emitUnwindDestination() { const Handler *Handler = State.Handlers.empty() ? nullptr : &State.Handlers.back(); unsigned TargetDepth = Handler ? Handler->CatchDepth : 0; if (Handler) LP->addClause(ConstantPointerNull::get(PointerType::getUnqual(Context))); - if (State.CatchDepth > TargetDepth) LP->setCleanup(true); + if (!Handler || State.CatchDepth > TargetDepth) LP->setCleanup(true); // Finish catches we are leaving before entering the outer handler (or // resuming propagation through a function with no local handler). for (unsigned I = TargetDepth; I < State.CatchDepth; ++I) Builder.CreateCall(EndCatch); @@ -99,7 +135,7 @@ BasicBlock *ExceptionHandler::emitUnwindDestination() { Builder.CreateStore(Builder.CreateExtractValue(LP, 0), Handler->Exception); Builder.CreateBr(Handler->Target); } else { - Builder.CreateResume(LP); + recordExit(Builder.CreateResume(LP), true); } Builder.restoreIP(Saved); return Unwind; diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index 9d9f60d..59e9bbe 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -212,6 +212,7 @@ Function *FunctionGen::emitFunctionDef(FunctionDefAST *FuncAST, BasicBlock *BB = BasicBlock::Create(Context, "entry", TheFunction); Builder.SetInsertPoint(BB); + Exceptions.enableCleanups(TheFunction); IdentifierMap OldSymbols = Symbols; Symbols.clear(); @@ -243,9 +244,9 @@ Function *FunctionGen::emitFunctionDef(FunctionDefAST *FuncAST, if (!Builder.GetInsertBlock()->hasTerminator()) { if (sameIdentifier(Proto->getReturnType(), "VOID")) { - Builder.CreateRetVoid(); + Exceptions.recordExit(Builder.CreateRetVoid()); } else { - Builder.CreateRet(Constant::getNullValue(TheFunction->getReturnType())); + Exceptions.recordExit(Builder.CreateRet(Constant::getNullValue(TheFunction->getReturnType()))); } } @@ -278,10 +279,7 @@ llvm::Value *FunctionGen::emitCall(const std::string &Callee, const std::vector< } void FunctionGen::emitReturn(ReturnStmtAST *Ret, llvm::Value *RetVal) { - Exceptions.emitReturnCleanup(); - if (RetVal) { - Builder.CreateRet(RetVal); - } else { - Builder.CreateRetVoid(); - } + Instruction *CatchCleanup = Exceptions.emitReturnCleanup(); + Instruction *RetInst = RetVal ? Builder.CreateRet(RetVal) : Builder.CreateRetVoid(); + Exceptions.recordExit(CatchCleanup ? CatchCleanup : RetInst); } diff --git a/tests/i/class-delete-designators/case.json b/tests/i/class-delete-designators/case.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/i/class-delete-designators/case.json @@ -0,0 +1 @@ +{} diff --git a/tests/i/class-delete-designators/expected.txt b/tests/i/class-delete-designators/expected.txt new file mode 100644 index 0000000..3a62de6 --- /dev/null +++ b/tests/i/class-delete-designators/expected.txt @@ -0,0 +1,8 @@ +item:1 +item:1 +item:2 +[Fatal] line 27: Invalid memory address (not an active allocation) +array caught +item:3 +item:0 +item:0 diff --git a/tests/i/class-delete-designators/notes.md b/tests/i/class-delete-designators/notes.md new file mode 100644 index 0000000..43378ae --- /dev/null +++ b/tests/i/class-delete-designators/notes.md @@ -0,0 +1,5 @@ +# Destructor regression + +Human review: pending. Expected output was independently traced by AI. + +Explicit this.DELETE prints only the method. DELETE Ptr^ and DELETE this release heap objects; deleting an array element fails validation. Main destroys the array in reverse and original Obj storage. diff --git a/tests/i/class-delete-designators/program.cps b/tests/i/class-delete-designators/program.cps new file mode 100644 index 0000000..be552e7 --- /dev/null +++ b/tests/i/class-delete-designators/program.cps @@ -0,0 +1,30 @@ +CLASS Item + Id : INTEGER + PROCEDURE DELETE() + OUTPUT "item:", Id + ENDPROCEDURE + PROCEDURE Explicit() + this.DELETE() + ENDPROCEDURE + PROCEDURE Release() + DELETE this + ENDPROCEDURE +ENDCLASS +TYPE ItemPtr = ^Item +DECLARE Obj : Item +DECLARE Ptr : ItemPtr +Obj <- NEW Item() +Obj.Id <- 1 +Ptr <- ^Obj +CALL Obj.Explicit() +DELETE Ptr^ +Obj <- NEW Item() +Obj.Id <- 2 +CALL Obj.Release() +DECLARE Arr : ARRAY[1:2] OF Item +Arr[2].Id <- 3 +TRY + DELETE Arr[2] +CATCH + OUTPUT "array caught" +ENDTRY diff --git a/tests/i/class-destructor-throws/case.json b/tests/i/class-destructor-throws/case.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/i/class-destructor-throws/case.json @@ -0,0 +1 @@ +{} diff --git a/tests/i/class-destructor-throws/expected.txt b/tests/i/class-destructor-throws/expected.txt new file mode 100644 index 0000000..0bbdb25 --- /dev/null +++ b/tests/i/class-destructor-throws/expected.txt @@ -0,0 +1,12 @@ +bomb +destroy:1 +normal caught +bomb +destroy:2 +catch exit caught +bomb +return caught locally +destroy:3 +3 +bomb +unwind throw caught diff --git a/tests/i/class-destructor-throws/notes.md b/tests/i/class-destructor-throws/notes.md new file mode 100644 index 0000000..3ccf7c5 --- /dev/null +++ b/tests/i/class-destructor-throws/notes.md @@ -0,0 +1,7 @@ +# Destructor regression + +Human review: pending. Expected output was independently traced by AI. + +A destructor throwing on normal return clears its alive flag and the unwind pad destroys the remaining Trace once. RETURN in CATCH preserves catch cleanup; RETURN inside TRY preserves its local handler. Function return values are evaluated before destruction. + +A destructor throwing during exception cleanup propagates to the caller instead of terminating. diff --git a/tests/i/class-destructor-throws/program.cps b/tests/i/class-destructor-throws/program.cps new file mode 100644 index 0000000..5ff62ac --- /dev/null +++ b/tests/i/class-destructor-throws/program.cps @@ -0,0 +1,61 @@ +CLASS Trace + Id : INTEGER + PROCEDURE DELETE() + OUTPUT "destroy:", Id + ENDPROCEDURE +ENDCLASS +CLASS Bomb + PROCEDURE DELETE() + OUTPUT "bomb" + THROW + ENDPROCEDURE +ENDCLASS +PROCEDURE Normal() + DECLARE First : Trace + First.Id <- 1 + DECLARE Last : Bomb +ENDPROCEDURE +PROCEDURE DuringUnwind() + DECLARE Last : Bomb + THROW +ENDPROCEDURE +PROCEDURE InCatch() + DECLARE First : Trace + First.Id <- 2 + TRY + THROW + CATCH + DECLARE Last : Bomb + RETURN + ENDTRY +ENDPROCEDURE +PROCEDURE InTry() + DECLARE Last : Bomb + TRY + RETURN + CATCH + OUTPUT "return caught locally" + ENDTRY +ENDPROCEDURE +FUNCTION Result() RETURNS INTEGER + DECLARE Local : Trace + Local.Id <- 3 + RETURN Local.Id +ENDFUNCTION +TRY + CALL Normal() +CATCH + OUTPUT "normal caught" +ENDTRY +TRY + CALL InCatch() +CATCH + OUTPUT "catch exit caught" +ENDTRY +CALL InTry() +OUTPUT Result() +TRY + CALL DuringUnwind() +CATCH + OUTPUT "unwind throw caught" +ENDTRY diff --git a/tests/i/class-unwind/case.json b/tests/i/class-unwind/case.json new file mode 100644 index 0000000..2c4afe3 --- /dev/null +++ b/tests/i/class-unwind/case.json @@ -0,0 +1 @@ +{"exit_code": 1} diff --git a/tests/i/class-unwind/expected.txt b/tests/i/class-unwind/expected.txt new file mode 100644 index 0000000..3503cb5 --- /dev/null +++ b/tests/i/class-unwind/expected.txt @@ -0,0 +1,14 @@ +destroy:2 +destroy:1 +destroy:3 +caught +destroy:60 +loop caught +local caught +still alive +destroy:70 +uncaught next +destroy:2 +destroy:1 +destroy:3 +Unhandled exception. Program terminated. diff --git a/tests/i/class-unwind/notes.md b/tests/i/class-unwind/notes.md new file mode 100644 index 0000000..8650a9b --- /dev/null +++ b/tests/i/class-unwind/notes.md @@ -0,0 +1,5 @@ +# Destructor regression + +Human review: pending. Expected output was independently traced by AI. + +Nested frames unwind in reverse local order before main catches. The earlier loop call sees a later DECLARE alive on iteration two. A same-function catch keeps locals alive. The uncaught path cleans procedure frames and deliberately omits main object 100. diff --git a/tests/i/class-unwind/program.cps b/tests/i/class-unwind/program.cps new file mode 100644 index 0000000..298cd3a --- /dev/null +++ b/tests/i/class-unwind/program.cps @@ -0,0 +1,56 @@ +CLASS Trace + Id : INTEGER + PROCEDURE DELETE() + OUTPUT "destroy:", Id + ENDPROCEDURE +ENDCLASS +PROCEDURE Inner() + DECLARE A : Trace + A.Id <- 1 + DECLARE B : Trace + B.Id <- 2 + THROW +ENDPROCEDURE +PROCEDURE Outer() + DECLARE A : Trace + A.Id <- 3 + CALL Inner() +ENDPROCEDURE +PROCEDURE Check(I : INTEGER) + IF I = 2 THEN + THROW + ENDIF +ENDPROCEDURE +PROCEDURE Loop() + DECLARE I : INTEGER + FOR I <- 1 TO 2 + CALL Check(I) + DECLARE Late : Trace + Late.Id <- 60 + NEXT I +ENDPROCEDURE +PROCEDURE LocalCatch() + DECLARE Kept : Trace + Kept.Id <- 70 + TRY + THROW + CATCH + OUTPUT "local caught" + ENDTRY + OUTPUT "still alive" +ENDPROCEDURE +DECLARE MainObject : Trace +MainObject.Id <- 100 +TRY + CALL Outer() +CATCH + OUTPUT "caught" +ENDTRY +TRY + CALL Loop() +CATCH + OUTPUT "loop caught" +ENDTRY +CALL LocalCatch() +OUTPUT "uncaught next" +CALL Outer() -- cgit v1.3.1-4-g156e