aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-18 08:07:50 +0000
committer_ov4 <ov4@ov4.org>2026-10-18 08:07:50 +0000
commit345e3d419eafb0e234aecb4a33f37efe2afbbf53 (patch)
tree9330322103f26f15c6df91cbbd62bf5a1f6e72da
parent927e3fc0384c941f9ad313a28768660c3cdb1777 (diff)
feat(classes): add copy constructors and statement-scoped temporaries
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--AGENTS/CONVENTIONS.md2
-rw-r--r--AGENTS/PITFALLS.md4
-rw-r--r--AGENTS/SUBSYSTEMS.md5
-rw-r--r--doc/language/keyword/cpsc-keyword-class.7.md2
-rw-r--r--doc/language/keyword/cpsc-keyword-delete.7.md4
-rw-r--r--include/cps/CodeGen.h9
-rw-r--r--include/cps/ExceptionHandler.h3
-rw-r--r--include/cps/Parser.h1
-rw-r--r--lib/CodeGen/CodeGen.cc24
-rw-r--r--lib/CodeGen/ConstructorGen.cc68
-rw-r--r--lib/CodeGen/ControlFlowGen.cc7
-rw-r--r--lib/CodeGen/Designator.cc12
-rw-r--r--lib/CodeGen/ExceptionHandler.cc7
-rw-r--r--lib/CodeGen/FunctionGen.cc11
-rw-r--r--lib/CodeGen/OperatorGen.cc26
-rw-r--r--lib/CodeGen/StatementGen.cc21
-rw-r--r--lib/Parser/Parser.cc6
-rw-r--r--lib/Parser/ParserExpression.cc38
-rw-r--r--lib/Parser/ParserFunction.cc6
-rw-r--r--tests/i/class-copy/case.json1
-rw-r--r--tests/i/class-copy/expected.txt23
-rw-r--r--tests/i/class-copy/program.cps65
-rw-r--r--tests/iii/class-copy-byval/case.json1
-rw-r--r--tests/iii/class-copy-byval/program.cps5
24 files changed, 325 insertions, 26 deletions
diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md
index fa9f12a..b8ca3e9 100644
--- a/AGENTS/CONVENTIONS.md
+++ b/AGENTS/CONVENTIONS.md
@@ -162,7 +162,7 @@ builtin result types. Returns may be any supported function result, including
non-BOOLEAN comparisons/logic. emitCoercedExpr rejects implicit STRING to BOOLEAN;
CASE separately requires BOOLEAN comparisons. Operators use shared argument
marshalling and exception-aware calls. Member lvalues keep their original address;
-rvalues are materialized and registered for the existing function-scoped cleanup.
+rvalues are materialized with Alive tracking and destroyed at statement end.
**Parameter modes are sticky and BYREF is procedure-only (guide ยง8.3).** In
`(BYREF a : T, b : T)` the `b` is BYREF too; unannotated parameters default to
diff --git a/AGENTS/PITFALLS.md b/AGENTS/PITFALLS.md
index 820fa55..4c72a4f 100644
--- a/AGENTS/PITFALLS.md
+++ b/AGENTS/PITFALLS.md
@@ -124,7 +124,7 @@ test programs.
Explicit calls do not recurse into fields or affect Alive tracking. Keyword DELETE
validates an active allocation start before destroying the object and its fields,
then frees memory and leaves aliases dangling; FREE skips all destruction.
- DECLARE storage and shallow BYVAL copies are destroyed in reverse declaration order
+ DECLARE storage and BYVAL copies are destroyed in reverse declaration order
at normal function exit or when unwinding leaves the frame. Arrays reverse their
flat element order. Repeated DECLARE destroys its previous incarnation first.
Lifetimes are function-scoped, even for declarations inside blocks. NEW rebinding
@@ -178,3 +178,5 @@ Linux.
WHILE accepts both the standard header without DO and the older WHILE DO form.
- **Literal escapes.** C++17 escapes are decoded by the lexer. Unicode escapes encode UTF-8 in strings; CHAR Unicode escapes must be ASCII. Built-in STRING remains NUL-terminated: `"a\0b"` is observed as `"a"`.
+
+- **Copy limits.** Classes may define a same-class BYREF copy constructor. Classes without one, whole TYPE records containing class fields, and BYVAL arrays still copy bits; owning handles inside these aggregate copies need explicit care. Class rvalues transfer ownership when consumed; discarded results and temporary receivers are destroyed at statement end.
diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md
index be20bc3..427fc45 100644
--- a/AGENTS/SUBSYSTEMS.md
+++ b/AGENTS/SUBSYSTEMS.md
@@ -134,8 +134,7 @@ DELETE method calls remain ordinary calls without field recursion. Keyword DELET
resolves a class lvalue, validates its allocation start through MemoryHandler before
any destruction, destroys it, then releases it through FREE; aliases remain dangling.
Stack lifetimes use a per-function `StackObjects` list saved/restored around function
-emission. DECLARE and shallow BYVAL copies share registration (the copy point is
-marked), including arrays and records with class fields. Each record retains the
+emission. DECLARE and BYVAL copies share ownership registration, including arrays and records with class fields. Each record retains the
original address, an entry-initialized Alive flag and, for arrays, a runtime element
count in an entry slot. Array buffer addresses also live in entry slots so deferred
cleanup never references branch-local bound SSA values. Repeated DECLARE destroys
@@ -195,3 +194,5 @@ nothing is declared or emitted eagerly. Helper bodies are built with the *shared
Builder (RuntimeCheck emits through the same one), saving/restoring the insert block.
**Explicit memory** (`MemoryHandler`): lazily emits internal allocation/find/release/resize helpers and a linked list of live allocation start addresses. Explicit MALLOC/REALLOC and keyword NEW allocations enter the list; STRING/ARRAY internals retain their existing ownership. `emitValidate` always calls the find helper, including for null, so keyword DELETE validates before running destructors. FREE validates non-null addresses, unlinks and frees the tracking node, releases the block and returns the original address. Zero-byte allocation returns null. REALLOC validates non-null addresses before its zero-size release path; null behaves as MALLOC. Positive-size resizing updates the node only after success, leaving the old block and record intact on failure. Node allocation precedes the user allocation; if the latter fails, its node is freed before the diagnostic and anonymous throw. Helpers use RuntimeCheck and ExceptionHandler, including source-line arguments and per-function unwind preparation. Sizes accept INTEGER and C integers and convert to native size_t. Address reuse can make stale aliases indistinguishable from current allocations; dereferences are not checked against this list.
+
+**Class copies and temporaries.** Callers construct BYVAL class arguments with a same-class BYREF OVERLOAD NEW; the callee's RegisterCopy hook takes ownership. emitOwnedValue constructs lvalue copies and keeps pending arguments Alive until marshalling succeeds. RETURN copies lvalues and transfers rvalues. StackObject::Temporary uses statement cleanup and deferred temporary-only cleanup before local handlers, while ordinary objects remain function-scoped. Function-result method receivers are materialized by the same path as operator receivers.
diff --git a/doc/language/keyword/cpsc-keyword-class.7.md b/doc/language/keyword/cpsc-keyword-class.7.md
index 986f2f6..feddc07 100644
--- a/doc/language/keyword/cpsc-keyword-class.7.md
+++ b/doc/language/keyword/cpsc-keyword-class.7.md
@@ -68,3 +68,5 @@ OUTPUT Item.Get()
# SEE ALSO
[cpsc-keyword-overload(7)](cpsc-keyword-overload.7.md), [cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-delete(7)](cpsc-keyword-delete.7.md), [cpsc-keyword-type(7)](cpsc-keyword-type.7.md), [cpsc-keyword-function(7)](cpsc-keyword-function.7.md), [cpsc-keyword-procedure(7)](cpsc-keyword-procedure.7.md), [cpsc-keyword-call(7)](cpsc-keyword-call.7.md).
+
+An `OVERLOAD PROCEDURE NEW(BYREF Other : ClassName)` defines a copy constructor. BYVAL class arguments and RETURN of class lvalues invoke it; a same-class BYVAL constructor is rejected. Classes without one retain bitwise copying. Class results transfer ownership directly into BYVAL arguments, RETURN and ordinary assignment. Discarded results and temporary receivers live until the statement ends, including exception exits. Ordinary user BYREF calls still require bare variables; constructor calls can bind temporaries.
diff --git a/doc/language/keyword/cpsc-keyword-delete.7.md b/doc/language/keyword/cpsc-keyword-delete.7.md
index 6ee6284..8979702 100644
--- a/doc/language/keyword/cpsc-keyword-delete.7.md
+++ b/doc/language/keyword/cpsc-keyword-delete.7.md
@@ -32,7 +32,7 @@ After validation, DELETE calls the object's DELETE method if present, then recur
Automatic destruction (RAII) applies to original storage created by DECLARE, including main-level declarations, arrays and records containing class values, and to BYVAL copies containing class values. Destruction occurs in reverse declaration order on RETURN, at the end of a function or procedure, and at normal program end. Array elements are destroyed in reverse flat element order. Block exit alone does not destroy these function-scoped lifetimes. A declaration executed again first destroys its previous live incarnation, then zeroes and constructs the original storage again.
-A completed construction or BYVAL copy marks the object alive. Destruction clears this state before calling the method, so a throwing destructor is never retried automatically. BYVAL copies are shallow. NEW rebinding changes the variable's current address, while automatic destruction still targets its original storage; the heap object requires explicit DELETE.
+A completed construction or BYVAL copy marks the object alive. Destruction clears this state before calling the method, so a throwing destructor is never retried automatically. BYVAL class copies use a copy constructor when defined; other aggregate copies remain shallow. NEW rebinding changes the variable's current address, while automatic destruction still targets its original storage; the heap object requires explicit DELETE.
Exception unwinding destroys live objects in each function frame being left before the caller's CATCH runs. A CATCH inside the same function retains its objects until function exit. An uncaught exception reaching main's final handler skips destruction of main-level objects. If a destructor throws during unwinding, its exception propagates and interrupts the remaining cleanup instead of terminating as C++ would.
@@ -55,3 +55,5 @@ DELETE Object
# SEE ALSO
[cpsc-keyword-class(7)](cpsc-keyword-class.7.md), [cpsc-keyword-declare(7)](cpsc-keyword-declare.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md), [cpsc-function-free(7)](../function/cpsc-function-free.7.md).
+
+An `OVERLOAD PROCEDURE NEW(BYREF Other : ClassName)` defines a copy constructor. BYVAL class arguments and RETURN of class lvalues invoke it; a same-class BYVAL constructor is rejected. Classes without one retain bitwise copying. Class results transfer ownership directly into BYVAL arguments, RETURN and ordinary assignment. Discarded results and temporary receivers live until the statement ends, including exception exits. Ordinary user BYREF calls still require bare variables; constructor calls can bind temporaries.
diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h
index 9b2d6c5..30f4135 100644
--- a/include/cps/CodeGen.h
+++ b/include/cps/CodeGen.h
@@ -66,6 +66,7 @@ class CodeGen {
llvm::AllocaInst *Count; // Non-null for an array; Addr then holds its buffer pointer.
int Line;
bool IsCopy;
+ bool Temporary = false;
};
std::vector<StackObject> StackObjects;
size_t registerStackObject(llvm::Value *Addr, const std::string &TypeName, int Line,
@@ -73,6 +74,14 @@ class CodeGen {
void activateStackObject(size_t Index, llvm::Value *Count = nullptr);
void emitStackObjectCleanup(const StackObject &Object);
void finishStackObjects(llvm::Function *F);
+ void cleanupTemporaries(size_t Start = 0);
+ bool isObjectLValue(ExprAST *Expr) const;
+ llvm::Value *emitOwnedValue(ExprAST *Expr, const TypeInfo *Type,
+ std::vector<size_t> &Transfers);
+ llvm::Value *copyClassValue(const TypeInfo *Type, llvm::Value *Address,
+ std::vector<size_t> &Transfers);
+ void transferObjects(const std::vector<size_t> &Objects);
+ void emitStmtImpl(StmtAST *Stmt);
struct ConstantScope {
unsigned ID;
IdentifierMap<std::optional<SymbolInfo>> Saved;
diff --git a/include/cps/ExceptionHandler.h b/include/cps/ExceptionHandler.h
index a2dc5d0..f55526b 100644
--- a/include/cps/ExceptionHandler.h
+++ b/include/cps/ExceptionHandler.h
@@ -25,6 +25,7 @@ class ExceptionHandler {
llvm::Instruction *Before;
FunctionState State;
bool Unwinding;
+ bool TemporariesOnly = false;
};
std::map<llvm::Function *, std::vector<CleanupPoint>> CleanupPoints;
bool FillingUnwind = false;
@@ -50,7 +51,7 @@ public:
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);
+ void fillCleanups(llvm::Function *F, const std::function<void(bool)> &Destroy);
llvm::Value *emitCall(llvm::FunctionCallee Callee, llvm::ArrayRef<llvm::Value *> Args,
const llvm::Twine &Name = "");
diff --git a/include/cps/Parser.h b/include/cps/Parser.h
index ad80876..eedc3f8 100644
--- a/include/cps/Parser.h
+++ b/include/cps/Parser.h
@@ -38,6 +38,7 @@ class Parser {
std::unique_ptr<ExprAST> ParseUnaryImpl();
std::unique_ptr<ExprAST> ParseUnary();
std::unique_ptr<ExprAST> ParsePrimaryImpl();
+ std::unique_ptr<ExprAST> ParseResultMethods(std::unique_ptr<ExprAST> Expr, int Line);
std::unique_ptr<ExprAST> ParsePrimary();
std::unique_ptr<ExprAST> ParseBinOpRHS(int ExprPrec, std::unique_ptr<ExprAST> LHS);
diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc
index c9d8c1a..49fc7b1 100644
--- a/lib/CodeGen/CodeGen.cc
+++ b/lib/CodeGen/CodeGen.cc
@@ -390,6 +390,7 @@ bool CodeGen::marshalCallArgs(const FuncSig &Signature,
return false;
}
+ std::vector<size_t> Transfers;
for (size_t i = 0; i < ArgExprs.size(); ++i) {
if (i >= Sig->Params.size()) {
Value *V = emitVariadicArgument(ArgExprs[i], Callee, i + 1);
@@ -423,6 +424,26 @@ bool CodeGen::marshalCallArgs(const FuncSig &Signature,
if (IsByRef) {
auto *Var = dynamic_cast<VariableExprAST*>(ArgExpr);
+ bool InternalBinding = Signature.Prototype &&
+ (sameIdentifier(Signature.Prototype->getName(), "NEW") ||
+ Signature.Prototype->getOperator() == tok_assign);
+ if (!Var && InternalBinding) {
+ const TypeInfo *Actual = getExprTypeInfo(ArgExpr);
+ if (!Actual || !Actual->sameType(resolveType(TypeName))) {
+ reportError("BYREF argument to %s has an incompatible type", Callee.c_str());
+ return false;
+ }
+ if (isObjectLValue(ArgExpr)) {
+ LValueInfo LV;
+ if (!emitLValue(ArgExpr, LV)) return false;
+ Out.push_back(LV.Addr);
+ } else {
+ Value *Storage = materializeOperatorReceiver(emitExpr(ArgExpr), Actual);
+ if (!Storage) return false;
+ Out.push_back(Storage);
+ }
+ continue;
+ }
if (!Var) {
reportError("BYREF argument to %s must be a variable", Callee.c_str());
return false;
@@ -442,11 +463,12 @@ bool CodeGen::marshalCallArgs(const FuncSig &Signature,
}
Out.push_back(symbolAddress(*Info));
} else {
- Value *V = emitCoercedExpr(ArgExpr, resolveType(TypeName));
+ Value *V = emitOwnedValue(ArgExpr, resolveType(TypeName), Transfers);
if (!V) return false;
Out.push_back(V);
}
}
+ transferObjects(Transfers);
return true;
}
diff --git a/lib/CodeGen/ConstructorGen.cc b/lib/CodeGen/ConstructorGen.cc
index e1033c3..4bf5568 100644
--- a/lib/CodeGen/ConstructorGen.cc
+++ b/lib/CodeGen/ConstructorGen.cc
@@ -8,6 +8,70 @@
using namespace llvm;
using namespace cps;
+bool CodeGen::isObjectLValue(ExprAST *Expr) const {
+ return dynamic_cast<VariableExprAST *>(Expr) || dynamic_cast<ArrayAccessExprAST *>(Expr) ||
+ dynamic_cast<DesignatorExprAST *>(Expr);
+}
+
+void CodeGen::transferObjects(const std::vector<size_t> &Objects) {
+ for (size_t Index : Objects)
+ if (Index != size_t(-1)) Builder->CreateStore(Builder->getFalse(), StackObjects[Index].Alive);
+}
+
+Value *CodeGen::emitOwnedValue(ExprAST *Expr, const TypeInfo *Type,
+ std::vector<size_t> &Transfers) {
+ if (!Type || !Type->IsClass) return emitCoercedExpr(Expr, Type);
+ const TypeInfo *Source = getExprTypeInfo(Expr);
+ if (!Source || !Source->sameType(Type)) return emitCoercedExpr(Expr, Type);
+ Value *Value = nullptr;
+ if (isObjectLValue(Expr)) {
+ LValueInfo LV;
+ if (!emitLValue(Expr, LV)) return nullptr;
+ return copyClassValue(Type, LV.Addr, Transfers);
+ }
+ Value = emitCoercedExpr(Expr, Type);
+ if (!Value) return nullptr;
+ // A pending argument owns its result until all remaining arguments succeed.
+ auto *Storage = materializeOperatorReceiver(Value, Type);
+ if (typeNeedsDestruction(Type->Name)) Transfers.push_back(StackObjects.size() - 1);
+ return Builder->CreateLoad(Type->LLVMType, Storage);
+}
+
+Value *CodeGen::copyClassValue(const TypeInfo *Type, Value *Address,
+ std::vector<size_t> &Transfers) {
+ const FuncSig *Copy = nullptr;
+ for (const FuncSig *Sig : FuncGen->getOverloads(Type->Name + ".NEW"))
+ if (Sig->IsOverload && Sig->Params.size() == 1 && Sig->Params[0].IsByRef &&
+ !Sig->Params[0].IsArray && Type->sameType(resolveType(Sig->Params[0].TypeName)))
+ Copy = Sig;
+ if (!Copy) return Builder->CreateLoad(Type->LLVMType, Address);
+ if (!checkOperatorAccess(*Copy)) return nullptr;
+ auto *Storage = CreateEntryBlockAlloca(Builder->GetInsertBlock()->getParent(),
+ Type->LLVMType, "copy_object");
+ size_t Lifetime = registerStackObject(Storage, Type->Name, DiagnosticLine, false, true);
+ if (Lifetime != size_t(-1)) StackObjects[Lifetime].Temporary = true;
+ Builder->CreateStore(Types->getZeroValue(Type->Name), Storage);
+ for (const auto &[Name, Field] : Type->Record.Fields) {
+ if (typeNeedsConstruction(Field.TypeName) &&
+ !emitDefaultConstruct(Field.TypeName,
+ Builder->CreateStructGEP(Type->LLVMType, Storage, Field.Index), DiagnosticLine))
+ return nullptr;
+ }
+ if (!FuncGen->emitCall(*Copy, {Storage, Address})) return nullptr;
+ activateStackObject(Lifetime);
+ Transfers.push_back(Lifetime);
+ return Builder->CreateLoad(Type->LLVMType, Storage, "copied_value");
+}
+
+void CodeGen::cleanupTemporaries(size_t Start) {
+ if (!Builder->GetInsertBlock() || Builder->GetInsertBlock()->hasTerminator()) return;
+ // Destructor emission can itself register temporaries. Copy each record before emitting.
+ for (size_t I = StackObjects.size(); I > Start; --I) {
+ auto Object = StackObjects[I - 1];
+ if (Object.Temporary) emitStackObjectCleanup(Object);
+ }
+}
+
bool CodeGen::typeNeedsConstruction(const std::string &TypeName) const {
if (findMethod(TypeName, "NEW")) return true;
const TypeInfo *Info = resolveType(TypeName);
@@ -169,8 +233,8 @@ void CodeGen::emitStackObjectCleanup(const StackObject &Object) {
}
void CodeGen::finishStackObjects(Function *F) {
- Exceptions->fillCleanups(F, [this] {
+ Exceptions->fillCleanups(F, [this](bool TemporariesOnly) {
for (auto It = StackObjects.rbegin(); It != StackObjects.rend(); ++It)
- emitStackObjectCleanup(*It);
+ if (!TemporariesOnly || It->Temporary) emitStackObjectCleanup(*It);
});
}
diff --git a/lib/CodeGen/ControlFlowGen.cc b/lib/CodeGen/ControlFlowGen.cc
index 0cf0c34..414fe19 100644
--- a/lib/CodeGen/ControlFlowGen.cc
+++ b/lib/CodeGen/ControlFlowGen.cc
@@ -29,6 +29,7 @@ void CodeGen::emitTryStmt(TryStmtAST *Stmt) {
void CodeGen::emitIfStmt(IfStmtAST *Stmt) {
Value *CondV = emitCoercedExpr(Stmt->getCond(), resolveType("BOOLEAN"));
if (!CondV) return;
+ cleanupTemporaries();
Function *TheFunction = Builder->GetInsertBlock()->getParent();
BasicBlock *ThenBB = BasicBlock::Create(*TheContext, "then", TheFunction);
@@ -65,6 +66,7 @@ void CodeGen::emitWhileStmt(WhileStmtAST *Stmt) {
Builder->SetInsertPoint(CondBB);
Value *CondV = emitCoercedExpr(Stmt->getCond(), resolveType("BOOLEAN"));
if (!CondV) return;
+ cleanupTemporaries();
Builder->CreateCondBr(CondV, LoopBB, AfterBB);
@@ -100,6 +102,7 @@ void CodeGen::emitRepeatStmt(RepeatStmtAST *Stmt) {
Builder->SetInsertPoint(CondBB);
Value *CondV = emitCoercedExpr(Stmt->getCond(), resolveType("BOOLEAN"));
if (!CondV) return;
+ cleanupTemporaries();
Builder->CreateCondBr(CondV, AfterBB, LoopBB);
@@ -127,6 +130,8 @@ void CodeGen::emitForStmt(ForStmtAST *Stmt) {
Value *Alloca = Symbol->Storage;
Builder->CreateStore(StartVal, Alloca);
+ cleanupTemporaries();
+
// STEP is evaluated exactly once, before the loop.
Value *StepVal = nullptr;
if (Stmt->getStep()) {
@@ -136,6 +141,7 @@ void CodeGen::emitForStmt(ForStmtAST *Stmt) {
StepVal = ConstantInt::get(*TheContext, APInt(64, 1));
}
+ cleanupTemporaries();
BasicBlock *CondBB = BasicBlock::Create(*TheContext, "forcond", TheFunction);
BasicBlock *LoopBB = BasicBlock::Create(*TheContext, "forloop", TheFunction);
BasicBlock *IncBB = BasicBlock::Create(*TheContext, "forinc", TheFunction);
@@ -147,6 +153,7 @@ void CodeGen::emitForStmt(ForStmtAST *Stmt) {
Value *CurVar = Builder->CreateLoad(Type::getInt64Ty(*TheContext), Alloca, VarName.c_str());
Value *EndVal = emitCoercedExpr(Stmt->getEnd(), resolveType("INTEGER"));
if (!EndVal) return;
+ cleanupTemporaries();
// The loop direction depends on the STEP value at run time.
Value *StepNonNeg = Builder->CreateICmpSGE(StepVal,
diff --git a/lib/CodeGen/Designator.cc b/lib/CodeGen/Designator.cc
index 5fb0ed3..b9c9a1a 100644
--- a/lib/CodeGen/Designator.cc
+++ b/lib/CodeGen/Designator.cc
@@ -98,7 +98,17 @@ const FuncSig *CodeGen::resolveMethod(const std::string &Class, const std::strin
Value *CodeGen::emitMemberCall(MemberCallExprAST *Call) {
SaveAndRestore<int> Location(DiagnosticLine, Call->getLine());
LValueInfo Receiver;
- if (!emitLValue(Call->getReceiver(), Receiver)) return nullptr;
+ if (isObjectLValue(Call->getReceiver())) {
+ if (!emitLValue(Call->getReceiver(), Receiver)) return nullptr;
+ } else {
+ const TypeInfo *Type = getExprTypeInfo(Call->getReceiver());
+ if (!Type || !Type->IsClass) {
+ reportError("A temporary method receiver must be a class");
+ return nullptr;
+ }
+ Receiver = {materializeOperatorReceiver(emitExpr(Call->getReceiver()), Type), Type->Name};
+ if (!Receiver.Addr) return nullptr;
+ }
return emitMethodCall(Receiver.TypeName, Receiver.Addr, Call->getMethod(),
Call->getArgs(), Call->getLine());
}
diff --git a/lib/CodeGen/ExceptionHandler.cc b/lib/CodeGen/ExceptionHandler.cc
index 3f69417..c879a87 100644
--- a/lib/CodeGen/ExceptionHandler.cc
+++ b/lib/CodeGen/ExceptionHandler.cc
@@ -94,7 +94,7 @@ void ExceptionHandler::recordExit(Instruction *Before, bool Unwinding) {
CleanupPoints[F].push_back({Before, States[F], Unwinding});
}
-void ExceptionHandler::fillCleanups(Function *F, const std::function<void()> &Destroy) {
+void ExceptionHandler::fillCleanups(Function *F, const std::function<void(bool)> &Destroy) {
IRBuilderBase::InsertPointGuard Guard(Builder);
SaveAndRestore<FunctionState> StateGuard(States[F]);
auto &Points = CleanupPoints[F];
@@ -107,7 +107,7 @@ void ExceptionHandler::fillCleanups(Function *F, const std::function<void()> &De
auto *Continuation = BB->splitBasicBlock(Point.Before->getIterator(), "after_destruction");
BB->getTerminator()->eraseFromParent();
Builder.SetInsertPoint(BB);
- Destroy();
+ Destroy(Point.TemporariesOnly);
Builder.CreateBr(Continuation);
}
Points.clear();
@@ -133,7 +133,8 @@ BasicBlock *ExceptionHandler::emitUnwindDestination() {
for (unsigned I = TargetDepth; I < State.CatchDepth; ++I) Builder.CreateCall(EndCatch);
if (Handler) {
Builder.CreateStore(Builder.CreateExtractValue(LP, 0), Handler->Exception);
- Builder.CreateBr(Handler->Target);
+ auto *Branch = Builder.CreateBr(Handler->Target);
+ CleanupPoints[F].push_back({Branch, States[F], true, true});
} else {
recordExit(Builder.CreateResume(LP), true);
}
diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc
index a0d0e32..37df8ff 100644
--- a/lib/CodeGen/FunctionGen.cc
+++ b/lib/CodeGen/FunctionGen.cc
@@ -91,7 +91,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.
+ // The caller constructs class arguments; this slot takes ownership of the value.
Builder.CreateStore(ArgVal, Alloca);
const TypeInfo *Info = Types.resolve(P.TypeName);
if (Info && Info->IsClass) {
@@ -123,6 +123,15 @@ const FuncSig *FunctionGen::getSignatureByLink(const std::string &Name) const {
Function *FunctionGen::emitPrototype(PrototypeAST *Proto) {
auto Registered = PrototypeLinks.find(Proto);
if (Registered != PrototypeLinks.end()) return Module.getFunction(Registered->second);
+ if (!Proto->getOwnerClass().empty() && sameIdentifier(Proto->getName(), "NEW") &&
+ Proto->getParams().size() == 1) {
+ const auto &P = Proto->getParams()[0];
+ if (!P.IsArray && sameIdentifier(P.TypeName, Proto->getOwnerClass()) &&
+ P.Mode != PassMode::ByRef) {
+ reportError("A copy constructor must take BYREF to its own class");
+ return nullptr;
+ }
+ }
if (int Op = Proto->getOperator()) {
size_t Count = Proto->getParams().size() + !Proto->getOwnerClass().empty();
bool ValidCount = Op == '-' ? Count == 1 || Count == 2
diff --git a/lib/CodeGen/OperatorGen.cc b/lib/CodeGen/OperatorGen.cc
index d340ed7..b449ba1 100644
--- a/lib/CodeGen/OperatorGen.cc
+++ b/lib/CodeGen/OperatorGen.cc
@@ -45,6 +45,7 @@ Value *CodeGen::materializeOperatorReceiver(Value *Val, const TypeInfo *Type) {
size_t Object = registerStackObject(Storage, Type->Name, DiagnosticLine, false, true);
Builder->CreateStore(Val, Storage);
activateStackObject(Object);
+ if (Object != size_t(-1)) StackObjects[Object].Temporary = true;
return Storage;
}
@@ -76,8 +77,25 @@ Value *CodeGen::emitOperatorCall(const FuncSig &Signature, ExprAST *Left, ExprAS
Value *CodeGen::emitOperatorValues(const FuncSig &Signature, Value *Left,
const TypeInfo *LeftType, Value *Right) {
if (!checkOperatorAccess(Signature)) return nullptr;
- if (!Signature.Prototype->getOwnerClass().empty())
- Left = materializeOperatorReceiver(Left, LeftType);
- if (!Left) return nullptr;
- return FuncGen->emitCall(Signature, {Left, Right});
+ bool Member = !Signature.Prototype->getOwnerClass().empty();
+ std::vector<Value *> Args;
+ std::vector<size_t> Transfers;
+ auto Capture = [&](Value *Val, const TypeInfo *Type) -> Value * {
+ auto *Slot = CreateEntryBlockAlloca(Builder->GetInsertBlock()->getParent(),
+ Type->LLVMType, "case_operand");
+ Builder->CreateStore(Val, Slot);
+ return Slot;
+ };
+ if (Member) Args.push_back(Capture(Left, LeftType));
+ else {
+ if (LeftType->IsClass) Left = copyClassValue(LeftType, Capture(Left, LeftType), Transfers);
+ if (!Left) return nullptr;
+ Args.push_back(Left);
+ }
+ const TypeInfo *RightType = resolveType(Signature.Params[Member ? 0 : 1].TypeName);
+ if (RightType->IsClass) Right = copyClassValue(RightType, Capture(Right, RightType), Transfers);
+ if (!Right) return nullptr;
+ Args.push_back(Right);
+ transferObjects(Transfers);
+ return FuncGen->emitCall(Signature, Args);
}
diff --git a/lib/CodeGen/StatementGen.cc b/lib/CodeGen/StatementGen.cc
index 99a269d..c0bc223 100644
--- a/lib/CodeGen/StatementGen.cc
+++ b/lib/CodeGen/StatementGen.cc
@@ -10,6 +10,12 @@ using namespace llvm;
using namespace cps;
void CodeGen::emitStmt(StmtAST *Stmt) {
+ size_t Start = StackObjects.size();
+ emitStmtImpl(Stmt);
+ cleanupTemporaries(Start);
+}
+
+void CodeGen::emitStmtImpl(StmtAST *Stmt) {
SaveAndRestore<int> Location(DiagnosticLine, Stmt && Stmt->getLine() ? Stmt->getLine() : DiagnosticLine);
if (!Stmt) return;
@@ -89,12 +95,18 @@ void CodeGen::emitStmt(StmtAST *Stmt) {
}
if (auto *Call = dynamic_cast<MemberCallStmtAST*>(Stmt)) {
- emitMemberCall(Call->getCall());
+ Value *Result = emitMemberCall(Call->getCall());
+ const TypeInfo *Type = getExprTypeInfo(Call->getCall());
+ if (Result && Type && Type->IsClass) materializeOperatorReceiver(Result, Type);
return;
}
if (auto *Call = dynamic_cast<CallStmtAST*>(Stmt)) {
- emitCall(Call->getCallee(), Call->getArgs(), Call->getLine());
+ Value *Result = emitCall(Call->getCallee(), Call->getArgs(), Call->getLine());
+ const FuncSig *Sig = FuncGen->hasFunction(Call->getCallee())
+ ? resolveCall(Call->getCallee(), Call->getArgs()) : nullptr;
+ const TypeInfo *Type = Sig ? resolveType(Sig->ReturnTypeName) : nullptr;
+ if (Result && Type && Type->IsClass) materializeOperatorReceiver(Result, Type);
return;
}
@@ -117,9 +129,12 @@ void CodeGen::emitStmt(StmtAST *Stmt) {
}
Value *RetVal = nullptr;
if (Ret->getRetVal()) {
- RetVal = emitCoercedExpr(Ret->getRetVal(), resolveType(Sig->ReturnTypeName));
+ std::vector<size_t> Transfers;
+ RetVal = emitOwnedValue(Ret->getRetVal(), resolveType(Sig->ReturnTypeName), Transfers);
+ transferObjects(Transfers);
if (!RetVal) return;
}
+ cleanupTemporaries();
FuncGen->emitReturn(Ret, RetVal);
return;
}
diff --git a/lib/Parser/Parser.cc b/lib/Parser/Parser.cc
index afe0e80..48f4df4 100644
--- a/lib/Parser/Parser.cc
+++ b/lib/Parser/Parser.cc
@@ -299,6 +299,12 @@ std::unique_ptr<StmtAST> Parser::ParseAssignStmt() {
getNextToken();
auto Args = ParseExprList(')', true);
if (!Args) return nullptr;
+ if (CurTok == '.') {
+ auto Result = ParseResultMethods(std::make_unique<CallExprAST>(Name, std::move(*Args), Line), Line);
+ if (!Result) return nullptr;
+ return std::make_unique<MemberCallStmtAST>(
+ std::unique_ptr<MemberCallExprAST>(static_cast<MemberCallExprAST *>(Result.release())));
+ }
return std::make_unique<CallStmtAST>(Name, std::move(*Args), Line);
}
diff --git a/lib/Parser/ParserExpression.cc b/lib/Parser/ParserExpression.cc
index 6cde5bf..7558c86 100644
--- a/lib/Parser/ParserExpression.cc
+++ b/lib/Parser/ParserExpression.cc
@@ -56,12 +56,12 @@ std::unique_ptr<ExprAST> Parser::ParseIdentifierExpr() {
getNextToken();
auto Args = ParseExprList(')', true);
if (!Args) return nullptr;
- if (CurTok == '[' || CurTok == '.' || CurTok == '^') {
+ if (CurTok == '[' || CurTok == '^') {
cps::sourceDiagnostic(Lex.getLine(), "error: record/array/pointer access on a function result is not supported at line %d\n",
Lex.getLine());
return nullptr;
}
- return std::make_unique<CallExprAST>(IdName, std::move(*Args), Line);
+ return ParseResultMethods(std::make_unique<CallExprAST>(IdName, std::move(*Args), Line), Line);
}
std::vector<DesignatorAccess> Accesses;
@@ -95,12 +95,13 @@ std::unique_ptr<MemberCallExprAST> Parser::ParseMemberCall(
if (!Parsed) return nullptr;
Args = std::move(*Parsed);
}
- if (CurTok == '[' || CurTok == '.' || CurTok == '^') {
+ if (CurTok == '[' || CurTok == '^') {
cps::sourceDiagnostic(Lex.getLine(), "error: record/array/pointer access on a function result is not supported at line %d\n",
Lex.getLine());
return nullptr;
}
- return std::make_unique<MemberCallExprAST>(std::move(Receiver), Method, std::move(Args), Line);
+ auto Result = ParseResultMethods(std::make_unique<MemberCallExprAST>(std::move(Receiver), Method, std::move(Args), Line), Line);
+ return std::unique_ptr<MemberCallExprAST>(static_cast<MemberCallExprAST *>(Result.release()));
}
// Postfix accessor chain after an identifier: '[' exprs ']' | '.' IDENT | '^'
@@ -173,11 +174,38 @@ std::unique_ptr<ExprAST> Parser::ParseParenExpr() {
return V;
}
+std::unique_ptr<ExprAST> Parser::ParseResultMethods(std::unique_ptr<ExprAST> Expr, int Line) {
+ while (CurTok == '.' && !atCaseBoundary()) {
+ getNextToken();
+ std::string Name;
+ if (CurTok == tok_operator) {
+ int Op;
+ Name = ParseOperatorName(Op);
+ if (Name.empty()) return nullptr;
+ } else if (CurTok == tok_identifier || CurTok == tok_delete) {
+ Name = CurTok == tok_delete ? "DELETE" : Lex.IdentifierStr;
+ getNextToken();
+ } else {
+ sourceDiagnostic(Lex.getLine(), "error: Expected method name at line %d\n", Lex.getLine());
+ return nullptr;
+ }
+ if (CurTok != '(') {
+ sourceDiagnostic(Lex.getLine(), "error: Expected '(' after method name at line %d\n", Lex.getLine());
+ return nullptr;
+ }
+ getNextToken();
+ auto Args = ParseExprList(')', true);
+ if (!Args) return nullptr;
+ Expr = std::make_unique<MemberCallExprAST>(std::move(Expr), Name, std::move(*Args), Line);
+ }
+ return Expr;
+}
+
std::unique_ptr<ExprAST> Parser::ParsePrimary() {
int Line = Lex.getLine();
auto Expr = ParsePrimaryImpl();
if (Expr && !Expr->getLine()) Expr->setLine(Line);
- return Expr;
+ return Expr ? ParseResultMethods(std::move(Expr), Line) : nullptr;
}
std::unique_ptr<ExprAST> Parser::ParsePrimaryImpl() {
diff --git a/lib/Parser/ParserFunction.cc b/lib/Parser/ParserFunction.cc
index dfb689a..65e0b85 100644
--- a/lib/Parser/ParserFunction.cc
+++ b/lib/Parser/ParserFunction.cc
@@ -374,6 +374,12 @@ std::unique_ptr<StmtAST> Parser::ParseCallStmt() {
Args = std::move(*Parsed);
}
+ if (CurTok == '.') {
+ auto Result = ParseResultMethods(std::make_unique<CallExprAST>(Callee, std::move(Args), Line), Line);
+ if (!Result) return nullptr;
+ return std::make_unique<MemberCallStmtAST>(
+ std::unique_ptr<MemberCallExprAST>(static_cast<MemberCallExprAST *>(Result.release())));
+ }
return std::make_unique<CallStmtAST>(Callee, std::move(Args), Line);
}
diff --git a/tests/i/class-copy/case.json b/tests/i/class-copy/case.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/i/class-copy/case.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/i/class-copy/expected.txt b/tests/i/class-copy/expected.txt
new file mode 100644
index 0000000..86e18a1
--- /dev/null
+++ b/tests/i/class-copy/expected.txt
@@ -0,0 +1,23 @@
+pass:1
+destroy:11
+destroy:2
+pass:2
+destroy:12
+destroy:3
+destroy:3
+destroy:8
+destroy:8
+destroy:4
+4
+destroy:4
+destroy:6
+destroy:6
+caught:3
+destroy:9
+destroy:9
+argument caught:3
+counts:9:3
+destroy:8
+destroy:1
+destroy:1
+remaining:0
diff --git a/tests/i/class-copy/program.cps b/tests/i/class-copy/program.cps
new file mode 100644
index 0000000..373e4c5
--- /dev/null
+++ b/tests/i/class-copy/program.cps
@@ -0,0 +1,65 @@
+GLOBAL
+ DECLARE Live : INTEGER
+ DECLARE Copies : INTEGER
+ENDGLOBAL
+CLASS Traced
+ Value : INTEGER
+ OVERLOAD PROCEDURE NEW()
+ Live <- Live + 1
+ ENDPROCEDURE
+ OVERLOAD PROCEDURE NEW(BYREF Other : Traced)
+ Value <- Other.Value
+ Live <- Live + 1
+ Copies <- Copies + 1
+ ENDPROCEDURE
+ PROCEDURE DELETE()
+ OUTPUT "destroy:", Value
+ Live <- Live - 1
+ ENDPROCEDURE
+ FUNCTION Read() RETURNS INTEGER
+ RETURN Value
+ ENDFUNCTION
+ FUNCTION Explode() RETURNS INTEGER
+ THROW
+ ENDFUNCTION
+ENDCLASS
+FUNCTION Make(N : INTEGER) RETURNS Traced
+ DECLARE Local : Traced
+ Local.Value <- N
+ RETURN Local
+ENDFUNCTION
+FUNCTION Forward(N : INTEGER) RETURNS Traced
+ RETURN Make(N)
+ENDFUNCTION
+PROCEDURE Pass(Item : Traced)
+ OUTPUT "pass:", Item.Value
+ Item.Value <- Item.Value + 10
+ENDPROCEDURE
+FUNCTION Fail() RETURNS INTEGER
+ THROW
+ENDFUNCTION
+PROCEDURE Accept(Item : Traced, Ignored : INTEGER)
+ENDPROCEDURE
+PROCEDURE Work()
+ DECLARE A : Traced
+ A.Value <- 1
+ CALL Pass(A)
+ CALL Pass(Forward(2))
+ CALL Make(3)
+ DECLARE B : Traced(A)
+ DECLARE C : Traced(Make(8))
+ OUTPUT Make(4).Read()
+ TRY
+ OUTPUT Make(6).Explode()
+ CATCH
+ OUTPUT "caught:", Live
+ ENDTRY
+ TRY
+ CALL Accept(Make(9), Fail())
+ CATCH
+ OUTPUT "argument caught:", Live
+ ENDTRY
+ OUTPUT "counts:", Copies, ":", Live
+ENDPROCEDURE
+CALL Work()
+OUTPUT "remaining:", Live
diff --git a/tests/iii/class-copy-byval/case.json b/tests/iii/class-copy-byval/case.json
new file mode 100644
index 0000000..719703e
--- /dev/null
+++ b/tests/iii/class-copy-byval/case.json
@@ -0,0 +1 @@
+{"errors":[{"reason":"copy constructor must take BYREF","line":2}]}
diff --git a/tests/iii/class-copy-byval/program.cps b/tests/iii/class-copy-byval/program.cps
new file mode 100644
index 0000000..145c6f7
--- /dev/null
+++ b/tests/iii/class-copy-byval/program.cps
@@ -0,0 +1,5 @@
+CLASS Bad
+ OVERLOAD PROCEDURE NEW(Other : Bad)
+ ENDPROCEDURE
+ENDCLASS
+OUTPUT 0