diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-17 08:34:42 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-17 08:34:42 +0000 |
| commit | abd56ef20bc8061bbecec29edd1301d324c2cb34 (patch) | |
| tree | 8da9a14372a60d4df8bf58693c62c187765e7240 | |
| parent | 59e3c50bcd58130e81e1f57b21d6e57b4d4aba6d (diff) | |
feat(classes): destroy stack objects at scope exit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | include/cps/CodeGen.h | 14 | ||||
| -rw-r--r-- | include/cps/FunctionGen.h | 5 | ||||
| -rw-r--r-- | lib/CodeGen/ArrayHandler.cc | 7 | ||||
| -rw-r--r-- | lib/CodeGen/ArrayParameters.cc | 2 | ||||
| -rw-r--r-- | lib/CodeGen/CodeGen.cc | 5 | ||||
| -rw-r--r-- | lib/CodeGen/ConstructorGen.cc | 77 | ||||
| -rw-r--r-- | lib/CodeGen/FunctionGen.cc | 2 | ||||
| -rw-r--r-- | lib/CodeGen/StatementGen.cc | 6 | ||||
| -rw-r--r-- | lib/CodeGen/ValueTypeGen.cc | 4 | ||||
| -rw-r--r-- | tests/i/class-delete/expected.txt | 12 | ||||
| -rw-r--r-- | tests/i/class-delete/notes.md | 2 | ||||
| -rw-r--r-- | tests/i/class-raii/case.json | 1 | ||||
| -rw-r--r-- | tests/i/class-raii/expected.txt | 12 | ||||
| -rw-r--r-- | tests/i/class-raii/notes.md | 5 | ||||
| -rw-r--r-- | tests/i/class-raii/program.cps | 49 | ||||
| -rw-r--r-- | tests/iii/class-destructor-private-error/case.json | 12 | ||||
| -rw-r--r-- | tests/iii/class-destructor-private-error/program.cps | 6 |
17 files changed, 217 insertions, 4 deletions
diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h index 9d71df7..9a0b2ef 100644 --- a/include/cps/CodeGen.h +++ b/include/cps/CodeGen.h @@ -46,6 +46,20 @@ class CodeGen { bool emitConstruct(const std::string &TypeName, llvm::Value *Addr, const std::vector<std::unique_ptr<ExprAST>> &Args, int Line); bool emitDefaultConstruct(const std::string &TypeName, llvm::Value *Addr, int Line); + struct StackObject { + llvm::Value *Addr; + std::string TypeName; + llvm::AllocaInst *Alive; + llvm::AllocaInst *Count; // Non-null for an array; Addr then holds its buffer pointer. + int Line; + bool IsCopy; + }; + std::vector<StackObject> StackObjects; + size_t registerStackObject(llvm::Value *Addr, const std::string &TypeName, int Line, + bool IsArray = false, bool IsCopy = false); + void activateStackObject(size_t Index, llvm::Value *Count = nullptr); + void emitStackObjectCleanup(const StackObject &Object); + void finishStackObjects(llvm::Function *F); struct ConstantScope { unsigned ID; IdentifierMap<std::optional<SymbolInfo>> Saved; diff --git a/include/cps/FunctionGen.h b/include/cps/FunctionGen.h index 5ff746d..777ab7d 100644 --- a/include/cps/FunctionGen.h +++ b/include/cps/FunctionGen.h @@ -64,6 +64,7 @@ private: IdentifierMap<FuncSig> Signatures; ArrayParamBinder BindArrayParam; + std::function<void(llvm::Value *, const std::string &, int)> RegisterCopy; void createArgumentAllocas(llvm::Function *F, const std::vector<ParamDecl> &Params, bool IsMethod); @@ -81,6 +82,10 @@ public: void setArrayParamBinder(ArrayParamBinder Binder) { BindArrayParam = std::move(Binder); } + void setCopyRegistrar(std::function<void(llvm::Value *, const std::string &, int)> Registrar) { + RegisterCopy = std::move(Registrar); + } + const FuncSig *getSignature(const std::string &Name) const; llvm::Function *emitPrototype(PrototypeAST *Proto); diff --git a/lib/CodeGen/ArrayHandler.cc b/lib/CodeGen/ArrayHandler.cc index 8da6cd3..7eefc20 100644 --- a/lib/CodeGen/ArrayHandler.cc +++ b/lib/CodeGen/ArrayHandler.cc @@ -97,6 +97,10 @@ void ArrayHandler::emitArrayDeclare(ArrayDeclareStmtAST *Stmt, CodeGen &CG) { if (!CG.prepareSymbolDeclaration(Name)) return; + Function *TheFunction = Builder->GetInsertBlock()->getParent(); + AllocaInst *Alloca = CG.CreateEntryBlockAlloca(TheFunction, PointerType::getUnqual(*TheContext), Name); + size_t Lifetime = CG.registerStackObject(Alloca, ElemInfo->Name, Stmt->getLine(), true); + std::vector<Value*> Lows; std::vector<Value*> Highs; @@ -134,8 +138,6 @@ void ArrayHandler::emitArrayDeclare(ArrayDeclareStmtAST *Stmt, CodeGen &CG) { Builder->CreateCall(MemsetFunc, {Ptr, ConstantInt::get(Type::getInt32Ty(*TheContext), 0), TotalBytes}); - Function *TheFunction = Builder->GetInsertBlock()->getParent(); - AllocaInst *Alloca = CG.CreateEntryBlockAlloca(TheFunction, PointerType::getUnqual(*TheContext), Name); Builder->CreateStore(Ptr, Alloca); CG.registerSymbol(Name, Alloca, ElemInfo->Name, true); @@ -157,6 +159,7 @@ void ArrayHandler::emitArrayDeclare(ArrayDeclareStmtAST *Stmt, CodeGen &CG) { Builder->CreateBr(Loop); Builder->SetInsertPoint(Done); } + CG.activateStackObject(Lifetime, TotalElements); } void ArrayHandler::computeDimsAndMultipliers(const std::string &Name, diff --git a/lib/CodeGen/ArrayParameters.cc b/lib/CodeGen/ArrayParameters.cc index 09017c9..3359c66 100644 --- a/lib/CodeGen/ArrayParameters.cc +++ b/lib/CodeGen/ArrayParameters.cc @@ -59,6 +59,8 @@ void ArrayHandler::bindArrayParameter(const std::string &Name, Builder->CreateStore(Ptr, Alloca); CG.registerSymbol(Name, Alloca, ElemInfo->Name, true); + if (MakeCopy) + CG.activateStackObject(CG.registerStackObject(Alloca, ElemInfo->Name, 0, true, true), TotalElements); } bool ArrayHandler::emitArrayArgument(ExprAST *ArgExpr, diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index 68dec5c..8117434 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -74,6 +74,10 @@ CodeGen::CodeGen() { /*MakeCopy=*/P.Mode == PassMode::ByVal, *this); }); + FuncGen->setCopyRegistrar([this](Value *Addr, const std::string &TypeName, int Line) { + activateStackObject(registerStackObject(Addr, TypeName, Line, false, true)); + }); + SetupExternalFunctions(); } @@ -185,6 +189,7 @@ void CodeGen::compile(const std::vector<std::unique_ptr<StmtAST>> &Statements) { Builder->CreateRet(ConstantInt::get(*TheContext, APInt(32, 0))); } + finishStackObjects(F); auto *Exception = Exceptions->popHandler(); Builder->SetInsertPoint(UnhandledBB); Exceptions->beginCatch(Exception); diff --git a/lib/CodeGen/ConstructorGen.cc b/lib/CodeGen/ConstructorGen.cc index 9e6b0b3..c0e50ac 100644 --- a/lib/CodeGen/ConstructorGen.cc +++ b/lib/CodeGen/ConstructorGen.cc @@ -107,3 +107,80 @@ bool CodeGen::emitDestroy(const std::string &TypeName, Value *Addr, int Line) { } return true; } + +size_t CodeGen::registerStackObject(Value *Addr, const std::string &TypeName, int Line, + bool IsArray, bool IsCopy) { + if (!typeNeedsDestruction(TypeName)) return size_t(-1); + auto *F = Builder->GetInsertBlock()->getParent(); + IRBuilder<> Entry(&F->getEntryBlock(), F->getEntryBlock().begin()); + auto *Alive = Entry.CreateAlloca(Entry.getInt1Ty(), nullptr, "object_alive"); + Entry.CreateStore(Entry.getFalse(), Alive); + AllocaInst *Count = nullptr; + if (IsArray) { + Count = Entry.CreateAlloca(Entry.getInt64Ty(), nullptr, "object_count"); + Entry.CreateStore(Entry.getInt64(0), Count); + } + StackObjects.push_back({Addr, TypeName, Alive, Count, Line, IsCopy}); + // This DECLARE can execute repeatedly, including after a backward loop edge. + emitStackObjectCleanup(StackObjects.back()); + return StackObjects.size() - 1; +} + +void CodeGen::activateStackObject(size_t Index, Value *Count) { + if (Index == size_t(-1)) return; + const auto &Object = StackObjects[Index]; + if (Object.Count) Builder->CreateStore(Count, Object.Count); + Builder->CreateStore(Builder->getTrue(), Object.Alive); +} + +void CodeGen::emitStackObjectCleanup(const StackObject &Object) { + auto *F = Builder->GetInsertBlock()->getParent(); + auto *Destroy = BasicBlock::Create(*TheContext, "destroy_object", F); + auto *Done = BasicBlock::Create(*TheContext, "destroyed_object", F); + Builder->CreateCondBr(Builder->CreateLoad(Builder->getInt1Ty(), Object.Alive), Destroy, Done); + Builder->SetInsertPoint(Destroy); + // Clear before the user method: a throwing destructor must never run twice. + Builder->CreateStore(Builder->getFalse(), Object.Alive); + if (!Object.Count) { + emitDestroy(Object.TypeName, Object.Addr, Object.Line); + } else { + Value *Base = Builder->CreateLoad(PointerType::getUnqual(*TheContext), Object.Addr); + Value *Count = Builder->CreateLoad(Builder->getInt64Ty(), Object.Count); + auto *Preheader = Builder->GetInsertBlock(); + auto *Loop = BasicBlock::Create(*TheContext, "destroy_elements", F); + auto *Body = BasicBlock::Create(*TheContext, "destroy_element", F); + Builder->CreateBr(Loop); + Builder->SetInsertPoint(Loop); + auto *Index = Builder->CreatePHI(Builder->getInt64Ty(), 2); + Index->addIncoming(Count, Preheader); + Builder->CreateCondBr(Builder->CreateICmpNE(Index, Builder->getInt64(0)), Body, Done); + Builder->SetInsertPoint(Body); + Value *Previous = Builder->CreateSub(Index, Builder->getInt64(1)); + Value *Element = Builder->CreateGEP(resolveType(Object.TypeName)->LLVMType, Base, Previous); + emitDestroy(Object.TypeName, Element, Object.Line); + Index->addIncoming(Previous, Builder->GetInsertBlock()); + Builder->CreateBr(Loop); + Builder->SetInsertPoint(Done); + return; + } + Builder->CreateBr(Done); + Builder->SetInsertPoint(Done); +} + +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); + for (auto It = StackObjects.rbegin(); It != StackObjects.rend(); ++It) + emitStackObjectCleanup(*It); + Builder->CreateBr(Continuation); + } +} diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index e344410..9d9f60d 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -89,6 +89,7 @@ void FunctionGen::createArgumentAllocas(Function *F, const std::vector<ParamDecl IRBuilder<> TmpB(&F->getEntryBlock(), F->getEntryBlock().begin()); AllocaInst *Alloca = TmpB.CreateAlloca(ArgType, nullptr, P.Name); + // Shallow BYVAL copy. The common lifetime hook is the future copy-constructor point. Builder.CreateStore(ArgVal, Alloca); const TypeInfo *Info = Types.resolve(P.TypeName); if (Info && Info->IsClass) { @@ -98,6 +99,7 @@ void FunctionGen::createArgumentAllocas(Function *F, const std::vector<ParamDecl } else { Symbols[P.Name] = {Alloca, P.TypeName, false}; } + if (RegisterCopy) RegisterCopy(Alloca, P.TypeName, P.Line); } } diff --git a/lib/CodeGen/StatementGen.cc b/lib/CodeGen/StatementGen.cc index a7c8eda..df8c396 100644 --- a/lib/CodeGen/StatementGen.cc +++ b/lib/CodeGen/StatementGen.cc @@ -31,11 +31,15 @@ void CodeGen::emitStmt(StmtAST *Stmt) { // to the emitting function), mirroring FunctionGen's Symbols handling. auto SavedArrays = Arrays->exchangeTable({}); + auto SavedObjects = std::move(StackObjects); + StackObjects.clear(); auto SavedScopes = std::move(ConstantScopes); ConstantScopes = {{NextScopeID++, {}}}; - FuncGen->emitFunctionDef(FuncDef, [this](StmtAST *S) { + Function *Emitted = FuncGen->emitFunctionDef(FuncDef, [this](StmtAST *S) { this->emitStmt(S); }, ConstantScopes.back().ID); + if (Emitted) finishStackObjects(Emitted); + StackObjects = std::move(SavedObjects); ConstantScopes = std::move(SavedScopes); Arrays->exchangeTable(std::move(SavedArrays)); diff --git a/lib/CodeGen/ValueTypeGen.cc b/lib/CodeGen/ValueTypeGen.cc index fbe6dd5..5e926cf 100644 --- a/lib/CodeGen/ValueTypeGen.cc +++ b/lib/CodeGen/ValueTypeGen.cc @@ -268,6 +268,7 @@ void CodeGen::emitDeclareStmt(DeclareStmtAST *Stmt) { Function *TheFunction = Builder->GetInsertBlock()->getParent(); AllocaInst *Alloca = CreateEntryBlockAlloca(TheFunction, Info->LLVMType, Stmt->getName()); + size_t Lifetime = registerStackObject(Alloca, Info->Name, Stmt->getLine()); Value *InitVal = nullptr; if (Info->isString()) { InitVal = EmptyStringStr; @@ -291,5 +292,6 @@ void CodeGen::emitDeclareStmt(DeclareStmtAST *Stmt) { reportError("Constructor arguments require a class type"); return; } - emitConstruct(Info->Name, Alloca, Stmt->getArgs(), Stmt->getLine()); + if (emitConstruct(Info->Name, Alloca, Stmt->getArgs(), Stmt->getLine())) + activateStackObject(Lifetime); } diff --git a/tests/i/class-delete/expected.txt b/tests/i/class-delete/expected.txt index 85c4753..c7e7246 100644 --- a/tests/i/class-delete/expected.txt +++ b/tests/i/class-delete/expected.txt @@ -8,3 +8,15 @@ double caught [Fatal] line 30: Invalid memory address (not an active allocation) stack caught leaf:2 +pair +leaf:2 +leaf:1 +pair +leaf:2 +leaf:1 +pair +leaf:2 +leaf:1 +pair +leaf:2 +leaf:1 diff --git a/tests/i/class-delete/notes.md b/tests/i/class-delete/notes.md index c4e2638..efe1e52 100644 --- a/tests/i/class-delete/notes.md +++ b/tests/i/class-delete/notes.md @@ -3,3 +3,5 @@ Human review: pending. Expected output was independently traced by AI. Explicit calls print only the outer method. Keyword DELETE prints outer, right, left. Invalid stack and stale heap addresses throw before any method runs. The array element uses an ordinary explicit method call. + +Normal main exit destroys Items[2], Items[1], Stack and the original Obj storage; each prints pair, leaf:2, leaf:1. diff --git a/tests/i/class-raii/case.json b/tests/i/class-raii/case.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/i/class-raii/case.json @@ -0,0 +1 @@ +{} diff --git a/tests/i/class-raii/expected.txt b/tests/i/class-raii/expected.txt new file mode 100644 index 0000000..532f9db --- /dev/null +++ b/tests/i/class-raii/expected.txt @@ -0,0 +1,12 @@ +destroy:31 +destroy:32 +destroy:20 +destroy:21 +destroy:50 +destroy:42 +destroy:41 +destroy:33 +destroy:2 +destroy:1 +main end +destroy:100 diff --git a/tests/i/class-raii/notes.md b/tests/i/class-raii/notes.md new file mode 100644 index 0000000..336e5cb --- /dev/null +++ b/tests/i/class-raii/notes.md @@ -0,0 +1,5 @@ +# Stack lifetimes + +Human review: pending. Expected output was independently traced by AI. + +Loop re-declarations destroy 31 and 32 before reinitialization. Copy destroys local 20 then its shallow parameter copy 21. Early RETURN destroys record field 50, array elements 42 and 41, loop local 33, original rebound storage 2 and first local 1. Heap object 99 stays alive. Never is not constructed. Main destroys 100 after its final output. diff --git a/tests/i/class-raii/program.cps b/tests/i/class-raii/program.cps new file mode 100644 index 0000000..ff350f9 --- /dev/null +++ b/tests/i/class-raii/program.cps @@ -0,0 +1,49 @@ +CLASS Traced + Id : INTEGER + PROCEDURE NEW() + Id <- 0 + ENDPROCEDURE + PROCEDURE DELETE() + OUTPUT "destroy:", Id + ENDPROCEDURE +ENDCLASS +TYPE Wrapped + DECLARE Item : Traced +ENDTYPE +PROCEDURE Copy(Value : Traced) + DECLARE Local : Traced + Local.Id <- 20 + Value.Id <- 21 +ENDPROCEDURE +PROCEDURE Work() + DECLARE First : Traced + First.Id <- 1 + DECLARE Rebound : Traced + Rebound.Id <- 2 + Rebound <- NEW Traced() + Rebound.Id <- 99 + DECLARE I : INTEGER + FOR I <- 1 TO 3 + DECLARE Again : Traced + Again.Id <- 3 + Again.Id <- I + 30 + NEXT I + DECLARE Arr : ARRAY[1:2] OF Plain + Arr[1].Item.Id <- 41 + Arr[2].Item.Id <- 42 + DECLARE Rec : Wrapped + Rec.Item.Id <- 50 + CALL Copy(First) + IF TRUE THEN + RETURN + ENDIF + DECLARE Never : Traced + Never.Id <- 77 +ENDPROCEDURE +CLASS Plain + Item : Traced +ENDCLASS +DECLARE Global : Traced + Global.Id <- 100 +CALL Work() +OUTPUT "main end" diff --git a/tests/iii/class-destructor-private-error/case.json b/tests/iii/class-destructor-private-error/case.json new file mode 100644 index 0000000..4753d68 --- /dev/null +++ b/tests/iii/class-destructor-private-error/case.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "reason": "private", + "line": 5 + }, + { + "reason": "private", + "line": 6 + } + ] +} diff --git a/tests/iii/class-destructor-private-error/program.cps b/tests/iii/class-destructor-private-error/program.cps new file mode 100644 index 0000000..8bd122c --- /dev/null +++ b/tests/iii/class-destructor-private-error/program.cps @@ -0,0 +1,6 @@ +CLASS Secret +PRIVATE PROCEDURE DELETE() +ENDPROCEDURE +ENDCLASS +DECLARE Obj : Secret +DELETE Obj |
