aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-17 08:36:19 +0000
committer_ov4 <ov4@ov4.org>2026-10-17 08:36:19 +0000
commit6a3b31af5dbb22749e6d5a1cad9dfaab6863b615 (patch)
tree58d2e7d956c8c605599a3f760040234ec2556d29
parentabd56ef20bc8061bbecec29edd1301d324c2cb34 (diff)
feat(classes): destroy stack objects during exception unwinding
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--include/cps/ExceptionHandler.h14
-rw-r--r--lib/CodeGen/CodeGen.cc2
-rw-r--r--lib/CodeGen/ConstructorGen.cc16
-rw-r--r--lib/CodeGen/ExceptionHandler.cc46
-rw-r--r--lib/CodeGen/FunctionGen.cc14
-rw-r--r--tests/i/class-delete-designators/case.json1
-rw-r--r--tests/i/class-delete-designators/expected.txt8
-rw-r--r--tests/i/class-delete-designators/notes.md5
-rw-r--r--tests/i/class-delete-designators/program.cps30
-rw-r--r--tests/i/class-destructor-throws/case.json1
-rw-r--r--tests/i/class-destructor-throws/expected.txt12
-rw-r--r--tests/i/class-destructor-throws/notes.md7
-rw-r--r--tests/i/class-destructor-throws/program.cps61
-rw-r--r--tests/i/class-unwind/case.json1
-rw-r--r--tests/i/class-unwind/expected.txt14
-rw-r--r--tests/i/class-unwind/notes.md5
-rw-r--r--tests/i/class-unwind/program.cps56
17 files changed, 265 insertions, 28 deletions
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 <map>
+#include <functional>
#include <vector>
namespace cps {
@@ -17,8 +18,16 @@ class ExceptionHandler {
struct FunctionState {
std::vector<Handler> Handlers;
unsigned CatchDepth = 0;
+ bool HasCleanups = false;
};
+ struct CleanupPoint {
+ llvm::Instruction *Before;
+ FunctionState State;
+ bool Unwinding;
+ };
+ std::map<llvm::Function *, std::vector<CleanupPoint>> 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<void()> &Destroy);
llvm::Value *emitCall(llvm::FunctionCallee Callee, llvm::ArrayRef<llvm::Value *> 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<std::unique_ptr<StmtAST>> &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 <climits>
#include <cstddef>
@@ -168,19 +169,8 @@ void CodeGen::emitStackObjectCleanup(const StackObject &Object) {
}
void CodeGen::finishStackObjects(Function *F) {
- IRBuilderBase::InsertPointGuard Guard(*Builder);
- std::vector<Instruction *> Exits;
- for (auto &BB : *F)
- if (BB.hasTerminator() && isa<ReturnInst>(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 <cassert>
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<void()> &Destroy) {
+ IRBuilderBase::InsertPointGuard Guard(Builder);
+ SaveAndRestore<FunctionState> 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<bool> 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<Constant>(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<SymbolInfo> 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()