From 74b6d06711afd3efbbc47436f1491900994313f3 Mon Sep 17 00:00:00 2001 From: _ov4 Date: Sat, 17 Oct 2026 06:54:32 +0000 Subject: feat(classes): allocate objects with NEW and rebind named class variables Co-Authored-By: Claude Opus 5.5 --- include/cps/CodeGen.h | 4 ++ include/cps/Parser.h | 1 + include/cps/TypeAST.h | 11 ++++ include/cps/TypeSystem.h | 5 +- lib/CodeGen/ArrayHandler.cc | 5 +- lib/CodeGen/CodeGen.cc | 2 +- lib/CodeGen/ConstructorGen.cc | 39 +++++++++++++ lib/CodeGen/Designator.cc | 7 ++- lib/CodeGen/ExpressionGen.cc | 4 +- lib/CodeGen/FunctionGen.cc | 9 ++- lib/CodeGen/StatementGen.cc | 9 +-- lib/CodeGen/TypeSystem.cc | 3 +- lib/CodeGen/ValueTypeGen.cc | 11 +++- lib/Parser/Parser.cc | 2 +- lib/Parser/ParserExpression.cc | 19 +++++++ tests/i/class-heap-new/case.json | 1 + tests/i/class-heap-new/expected.txt | 16 ++++++ tests/i/class-heap-new/notes.md | 5 ++ tests/i/class-heap-new/program.cps | 100 +++++++++++++++++++++++++++++++++ tests/iii/class-new-errors/case.json | 32 +++++++++++ tests/iii/class-new-errors/program.cps | 26 +++++++++ 21 files changed, 288 insertions(+), 23 deletions(-) create mode 100644 tests/i/class-heap-new/case.json create mode 100644 tests/i/class-heap-new/expected.txt create mode 100644 tests/i/class-heap-new/notes.md create mode 100644 tests/i/class-heap-new/program.cps create mode 100644 tests/iii/class-new-errors/case.json create mode 100644 tests/iii/class-new-errors/program.cps diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h index 1813d9d..a70d191 100644 --- a/include/cps/CodeGen.h +++ b/include/cps/CodeGen.h @@ -25,6 +25,7 @@ class FileHandler; class MemoryHandler; class ExceptionHandler; class CastExprAST; +class NewExprAST; class CodeGen { std::unique_ptr TheContext; @@ -127,6 +128,9 @@ class CodeGen { const std::vector> &ArgExprs, int Line, std::vector &Out); + llvm::Value *symbolAddress(const SymbolInfo &Info); + void emitAssignment(const LValueInfo &Target, ExprAST *Expr, + const SymbolInfo *Symbol = nullptr); void emitDeclareStmt(DeclareStmtAST *Stmt); void emitOutputValue(llvm::Value *Val, const TypeInfo *TypeInfo, bool AppendNewline = true); diff --git a/include/cps/Parser.h b/include/cps/Parser.h index 4874d7a..9a53e95 100644 --- a/include/cps/Parser.h +++ b/include/cps/Parser.h @@ -34,6 +34,7 @@ class Parser { int GetTokPrecedence(); std::unique_ptr ParseExpression(); + std::unique_ptr ParseAssignmentRHS(); std::unique_ptr ParseUnaryImpl(); std::unique_ptr ParseUnary(); std::unique_ptr ParsePrimaryImpl(); diff --git a/include/cps/TypeAST.h b/include/cps/TypeAST.h index de5d12c..a5e40a1 100644 --- a/include/cps/TypeAST.h +++ b/include/cps/TypeAST.h @@ -9,6 +9,17 @@ namespace cps { +// Only an assignment may own this expression. +class NewExprAST : public ExprAST { + std::string ClassName; + std::vector> Args; +public: + NewExprAST(std::string ClassName, std::vector> Args, int Line) + : ExprAST(Line), ClassName(std::move(ClassName)), Args(std::move(Args)) {} + const std::string &getClassName() const { return ClassName; } + const auto &getArgs() const { return Args; } +}; + class CastExprAST : public ExprAST { std::string TypeName; std::unique_ptr Operand; diff --git a/include/cps/TypeSystem.h b/include/cps/TypeSystem.h index 8bee6b9..d358794 100644 --- a/include/cps/TypeSystem.h +++ b/include/cps/TypeSystem.h @@ -66,6 +66,7 @@ struct TypeInfo { std::string Name; llvm::Type *LLVMType = nullptr; TypeKind Kind = TypeKind::Void; + bool IsClass = false; RecordPayload Record; // Kind == Record only EnumPayload Enum; // Kind == Enum only @@ -109,6 +110,7 @@ struct SymbolInfo { bool IsArray = false; llvm::Constant *ConstantValue = nullptr; // Non-null for CONSTANT; no storage. unsigned ScopeID = 0; + bool IsReference = false; // Storage is a slot containing the current object address. }; class TypeSystem { @@ -130,7 +132,8 @@ public: // order). Pass A reserves every name so pointer pointees may forward- // reference; pass B fills definitions. On failure Err carries the // diagnostic and nothing is (further) registered. - bool declareUserType(const std::string &Name, TypeKind Kind, std::string &Err); + bool declareUserType(const std::string &Name, TypeKind Kind, std::string &Err, + bool IsClass = false); bool defineEnum(const std::string &Name, const std::vector &Values, std::string &Err); diff --git a/lib/CodeGen/ArrayHandler.cc b/lib/CodeGen/ArrayHandler.cc index 337e5f4..8da6cd3 100644 --- a/lib/CodeGen/ArrayHandler.cc +++ b/lib/CodeGen/ArrayHandler.cc @@ -241,10 +241,7 @@ void ArrayHandler::emitArrayAssign(ArrayAssignStmtAST *Stmt, CodeGen &CG) { Stmt->getLine(), CG, ElemTypeName); if (!ElemPtr) return; - Value *Val = CG.emitCoercedExpr(Stmt->getExpr(), Types.resolve(ElemTypeName)); - if (!Val) return; - - Builder->CreateStore(Val, ElemPtr); + CG.emitAssignment({ElemPtr, ElemTypeName}, Stmt->getExpr()); } Value *ArrayHandler::emitElementAddress(const std::string &Name, diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index 01393a9..68dec5c 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -266,7 +266,7 @@ bool CodeGen::marshalCallArgs(const std::string &Callee, TypeName.c_str(), Info->TypeName.c_str()); return false; } - Out.push_back(Info->Storage); + Out.push_back(symbolAddress(*Info)); } else { Value *V = emitCoercedExpr(ArgExpr, resolveType(TypeName)); if (!V) return false; diff --git a/lib/CodeGen/ConstructorGen.cc b/lib/CodeGen/ConstructorGen.cc index 0d4d732..f726590 100644 --- a/lib/CodeGen/ConstructorGen.cc +++ b/lib/CodeGen/ConstructorGen.cc @@ -1,4 +1,8 @@ #include "cps/CodeGen.h" +#include "cps/MemoryHandler.h" +#include "cps/TypeAST.h" +#include +#include using namespace llvm; using namespace cps; @@ -37,3 +41,38 @@ bool CodeGen::emitConstruct(const std::string &TypeName, Value *Addr, bool CodeGen::emitDefaultConstruct(const std::string &TypeName, Value *Addr, int Line) { return emitConstruct(TypeName, Addr, {}, Line); } + +Value *CodeGen::symbolAddress(const SymbolInfo &Info) { + if (!Info.IsReference) return Info.Storage; + return Builder->CreateLoad(PointerType::getUnqual(*TheContext), Info.Storage, "object_address"); +} + +void CodeGen::emitAssignment(const LValueInfo &Target, ExprAST *Expr, const SymbolInfo *Symbol) { + const TypeInfo *Info = resolveType(Target.TypeName); + if (auto *New = dynamic_cast(Expr)) { + SaveAndRestore Location(DiagnosticLine, New->getLine()); + const TypeInfo *Class = resolveType(New->getClassName()); + if (!Class || !Class->IsClass) { + reportError("NEW requires a class type: %s", New->getClassName().c_str()); + return; + } + if (!Info || !Info->sameType(Class)) { + reportError("NEW target must have type %s", Class->Name.c_str()); + return; + } + Value *Size = Builder->CreateIntCast(Types->getSizeOfConstant(Class->LLVMType), + Type::getIntNTy(*TheContext, sizeof(std::size_t) * CHAR_BIT), false); + Value *Object = Memory->emitAllocate(Size, New->getLine()); + Builder->CreateStore(Types->getZeroValue(Class->Name), Object); + if (!emitConstruct(Class->Name, Object, New->getArgs(), New->getLine())) return; + if (Symbol && Symbol->IsReference) { + Builder->CreateStore(Object, Symbol->Storage); + } else { + Value *Val = Builder->CreateLoad(Class->LLVMType, Object); + Builder->CreateStore(Val, Target.Addr); + } + return; + } + Value *Val = emitCoercedExpr(Expr, Info); + if (Val) Builder->CreateStore(Val, Target.Addr); +} diff --git a/lib/CodeGen/Designator.cc b/lib/CodeGen/Designator.cc index 1ebd158..5f54a87 100644 --- a/lib/CodeGen/Designator.cc +++ b/lib/CodeGen/Designator.cc @@ -24,7 +24,8 @@ void CodeGen::runTypePrePass(const std::vector> &Statem else if (auto *P = dynamic_cast(S.get())) Ok = Types->declareUserType(P->getName(), TypeKind::Pointer, Err); else if (auto *R = dynamic_cast(S.get())) - Ok = Types->declareUserType(R->getName(), TypeKind::Record, Err); + Ok = Types->declareUserType(R->getName(), TypeKind::Record, Err, + dynamic_cast(R) != nullptr); if (!Ok) reportError("%s", Err.c_str()); } @@ -120,7 +121,7 @@ bool CodeGen::emitLValue(ExprAST *E, LValueInfo &Out) { reportError("Whole array %s is not an assignable location; index it", Var->getName().c_str()); return false; } - Out.Addr = Info->Storage; + Out.Addr = symbolAddress(*Info); Out.TypeName = Info->TypeName; return true; } @@ -148,7 +149,7 @@ bool CodeGen::emitLValue(ExprAST *E, LValueInfo &Out) { if (!requireWritable(*Info, D->getBaseName())) return false; const auto &Accesses = D->getAccesses(); - Value *Addr = Info->Storage; + Value *Addr = symbolAddress(*Info); std::string TypeName = Info->TypeName; size_t Start = 0; diff --git a/lib/CodeGen/ExpressionGen.cc b/lib/CodeGen/ExpressionGen.cc index fde948c..ba5e329 100644 --- a/lib/CodeGen/ExpressionGen.cc +++ b/lib/CodeGen/ExpressionGen.cc @@ -51,7 +51,7 @@ Value *CodeGen::emitExpr(ExprAST *Expr) { } if (Info->ConstantValue) return Info->ConstantValue; if (Info->IsArray) { - return Info->Storage; + return symbolAddress(*Info); } const TypeInfo *TypeInfo = resolveType(Info->TypeName); @@ -60,7 +60,7 @@ Value *CodeGen::emitExpr(ExprAST *Expr) { return nullptr; } - Value *Loaded = Builder->CreateLoad(TypeInfo->LLVMType, Info->Storage, Var->getName().c_str()); + Value *Loaded = Builder->CreateLoad(TypeInfo->LLVMType, symbolAddress(*Info), Var->getName().c_str()); if (TypeInfo->isString()) { Loaded = emitStringNullGuard(Loaded); } diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index d5bc5d4..e344410 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -90,7 +90,14 @@ void FunctionGen::createArgumentAllocas(Function *F, const std::vectorIsClass) { + AllocaInst *Slot = TmpB.CreateAlloca(PointerType::getUnqual(Context), nullptr, P.Name + ".ref"); + Builder.CreateStore(Alloca, Slot); + Symbols[P.Name] = {Slot, P.TypeName, false, nullptr, 0, true}; + } else { + Symbols[P.Name] = {Alloca, P.TypeName, false}; + } } } diff --git a/lib/CodeGen/StatementGen.cc b/lib/CodeGen/StatementGen.cc index 2443877..bd1f7c5 100644 --- a/lib/CodeGen/StatementGen.cc +++ b/lib/CodeGen/StatementGen.cc @@ -128,10 +128,7 @@ void CodeGen::emitStmt(StmtAST *Stmt) { return; } - Value *Val = emitCoercedExpr(Assign->getExpr(), resolveType(Info->TypeName)); - if (!Val) return; - - Builder->CreateStore(Val, Info->Storage); + emitAssignment({symbolAddress(*Info), Info->TypeName}, Assign->getExpr(), Info); return; } @@ -296,9 +293,7 @@ void CodeGen::emitStmt(StmtAST *Stmt) { if (auto *DA = dynamic_cast(Stmt)) { LValueInfo LV; if (!emitLValue(DA->getTarget(), LV)) return; - Value *Val = emitCoercedExpr(DA->getExpr(), resolveType(LV.TypeName)); - if (!Val) return; - Builder->CreateStore(Val, LV.Addr); + emitAssignment(LV, DA->getExpr()); return; } } diff --git a/lib/CodeGen/TypeSystem.cc b/lib/CodeGen/TypeSystem.cc index f4620dc..27814c2 100644 --- a/lib/CodeGen/TypeSystem.cc +++ b/lib/CodeGen/TypeSystem.cc @@ -129,7 +129,7 @@ const TypeInfo *TypeSystem::registerType(const std::string &TypeName, return &Result.first->second; } -bool TypeSystem::declareUserType(const std::string &Name, TypeKind Kind, std::string &Err) { +bool TypeSystem::declareUserType(const std::string &Name, TypeKind Kind, std::string &Err, bool IsClass) { if (Types.count(Name)) { Err = "Type " + Name + " is already defined"; return false; @@ -157,6 +157,7 @@ bool TypeSystem::declareUserType(const std::string &Name, TypeKind Kind, std::st } registerType(Name, LLVMType, Kind); + Types.find(Name)->second.IsClass = IsClass; return true; } diff --git a/lib/CodeGen/ValueTypeGen.cc b/lib/CodeGen/ValueTypeGen.cc index baa384c..fbe6dd5 100644 --- a/lib/CodeGen/ValueTypeGen.cc +++ b/lib/CodeGen/ValueTypeGen.cc @@ -279,8 +279,15 @@ void CodeGen::emitDeclareStmt(DeclareStmtAST *Stmt) { Builder->CreateStore(InitVal, Alloca); } - registerSymbol(Stmt->getName(), Alloca, Info->Name, false); - if (Stmt->hasParens() && !ClassMethods.count(Info->Name)) { + Value *Storage = Alloca; + if (Info->IsClass) { + Storage = CreateEntryBlockAlloca(TheFunction, PointerType::getUnqual(*TheContext), + Stmt->getName() + ".ref"); + Builder->CreateStore(Alloca, Storage); + } + registerSymbol(Stmt->getName(), Storage, Info->Name, false); + Symbols.find(Stmt->getName())->second.IsReference = Info->IsClass; + if (Stmt->hasParens() && !Info->IsClass) { reportError("Constructor arguments require a class type"); return; } diff --git a/lib/Parser/Parser.cc b/lib/Parser/Parser.cc index 26eeac6..d3b2daa 100644 --- a/lib/Parser/Parser.cc +++ b/lib/Parser/Parser.cc @@ -242,7 +242,7 @@ std::unique_ptr Parser::ParseAssignStmt() { return nullptr; } getNextToken(); - auto Expr = ParseExpression(); + auto Expr = ParseAssignmentRHS(); if (!Expr) return nullptr; if (Accesses.empty()) diff --git a/lib/Parser/ParserExpression.cc b/lib/Parser/ParserExpression.cc index f4e074e..96132aa 100644 --- a/lib/Parser/ParserExpression.cc +++ b/lib/Parser/ParserExpression.cc @@ -168,6 +168,9 @@ std::unique_ptr Parser::ParsePrimary() { std::unique_ptr Parser::ParsePrimaryImpl() { switch (CurTok) { + case tok_new: + fprintf(stderr, "error: NEW is only allowed on the right of an assignment at line %d\n", Lex.getLine()); + return nullptr; case tok_identifier: return ParseIdentifierExpr(); case tok_number_int: return ParseNumberExpr(); case tok_number_real: return ParseNumberExpr(); @@ -299,3 +302,19 @@ std::optional>> Parser::ParseExprList(char getNextToken(); return Elems; } + +std::unique_ptr Parser::ParseAssignmentRHS() { + if (CurTok != tok_new) return ParseExpression(); + int Line = Lex.getLine(); + getNextToken(); + std::string ClassName = ParseTypeName(false); + if (ClassName.empty()) return nullptr; + std::vector> Args; + if (CurTok == '(') { + getNextToken(); + auto Parsed = ParseExprList(')', true); + if (!Parsed) return nullptr; + Args = std::move(*Parsed); + } + return std::make_unique(ClassName, std::move(Args), Line); +} diff --git a/tests/i/class-heap-new/case.json b/tests/i/class-heap-new/case.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/i/class-heap-new/case.json @@ -0,0 +1 @@ +{} diff --git a/tests/i/class-heap-new/expected.txt b/tests/i/class-heap-new/expected.txt new file mode 100644 index 0000000..37e731a --- /dev/null +++ b/tests/i/class-heap-new/expected.txt @@ -0,0 +1,16 @@ +rebind:7,9 +copy:7,4,4 +byval:4,20 +caller:4 +byref:1,1 +this:1,1 +reset:1 +array:1,1 +field:1 +alias:1 +deref:1 +args:42 +declare caught +new caught +loop:1 +loop:1 diff --git a/tests/i/class-heap-new/notes.md b/tests/i/class-heap-new/notes.md new file mode 100644 index 0000000..3a6435c --- /dev/null +++ b/tests/i/class-heap-new/notes.md @@ -0,0 +1,5 @@ +# Heap NEW and object storage + +Human review: pending. Expected output was independently traced by AI. + +Project semantics extend guide section 10.2. Each Box constructor sets Value to 1. Rebinding A preserves the old stack object's 7 while the new object becomes 9. Ordinary A <- B copies 4 into the current heap object, leaving B and the old stack object independent. BYVAL copies 4, then independently rebinds and changes its local object to 20. BYREF NEW copies 1 into A's current object, observed through CurrentA. Replacing this likewise copies into that same object; explicit Reset calls the ordinary constructor method. Array-element, field, bare field alias and pointer-dereference NEW assignments preserve their destination addresses and copy 1. Named's argument is 42. Failing throws in both DECLARE and heap construction, reaching the surrounding catches. Loop DECLARE restores its original stack object and constructs it on every iteration, so the previous heap object's 99 is never printed. diff --git a/tests/i/class-heap-new/program.cps b/tests/i/class-heap-new/program.cps new file mode 100644 index 0000000..7b8363f --- /dev/null +++ b/tests/i/class-heap-new/program.cps @@ -0,0 +1,100 @@ +CLASS Box + Value : INTEGER + PROCEDURE NEW() + Value <- 1 + ENDPROCEDURE + PROCEDURE Reset() + this.NEW() + ENDPROCEDURE + PROCEDURE ReplaceSelf() + this <- NEW Box() + ENDPROCEDURE +ENDCLASS +CLASS Named + Value : INTEGER + PROCEDURE NEW(Number : INTEGER) + Value <- Number + ENDPROCEDURE +ENDCLASS +CLASS Holder + Item : Box + PROCEDURE ReplaceField() + Item <- NEW Box + ENDPROCEDURE +ENDCLASS +CLASS Failing + PROCEDURE NEW() + THROW + ENDPROCEDURE +ENDCLASS +TYPE BoxPointer = ^Box +PROCEDURE ReplaceRef(BYREF Item : Box) + Item <- NEW Box +ENDPROCEDURE +PROCEDURE ReplaceVal(Item : Box) + DECLARE Old : BoxPointer + Old <- ^Item + Item <- NEW Box() + Item.Value <- 20 + OUTPUT "byval:", Old^.Value, ",", Item.Value +ENDPROCEDURE +DECLARE A : Box +DECLARE B : Box +DECLARE OldA : BoxPointer +DECLARE CurrentA : BoxPointer +OldA <- ^A +A.Value <- 7 +A <- NEW Box +A.Value <- 9 +CurrentA <- ^A +OUTPUT "rebind:", OldA^.Value, ",", A.Value +B.Value <- 4 +A <- B +OUTPUT "copy:", OldA^.Value, ",", CurrentA^.Value, ",", B.Value +CALL ReplaceVal(A) +OUTPUT "caller:", A.Value +CALL ReplaceRef(A) +OUTPUT "byref:", CurrentA^.Value, ",", A.Value +A.Value <- 11 +A.ReplaceSelf() +OUTPUT "this:", CurrentA^.Value, ",", A.Value +A.Value <- 12 +A.Reset() +OUTPUT "reset:", A.Value +DECLARE Items : ARRAY[1:2] OF Box +DECLARE Element : BoxPointer +Element <- ^Items[1] +Items[1].Value <- 30 +Items[1] <- NEW Box() +OUTPUT "array:", Element^.Value, ",", Items[2].Value +DECLARE H : Holder +DECLARE Field : BoxPointer +Field <- ^H.Item +H.Item.Value <- 40 +H.Item <- NEW Box +OUTPUT "field:", Field^.Value +H.Item.Value <- 41 +H.ReplaceField() +OUTPUT "alias:", Field^.Value +CurrentA^ <- NEW Box +OUTPUT "deref:", A.Value +DECLARE N : Named(0) +N <- NEW Named(42) +OUTPUT "args:", N.Value +TRY + DECLARE Broken : Failing +CATCH + OUTPUT "declare caught" +ENDTRY +TRY + Broken <- NEW Failing +CATCH + OUTPUT "new caught" +ENDTRY +DECLARE Index : INTEGER +FOR Index <- 1 TO 2 + DECLARE Local : Box + OUTPUT "loop:", Local.Value + Local <- NEW Box + Local.Value <- 99 +NEXT Index diff --git a/tests/iii/class-new-errors/case.json b/tests/iii/class-new-errors/case.json new file mode 100644 index 0000000..8ce52ed --- /dev/null +++ b/tests/iii/class-new-errors/case.json @@ -0,0 +1,32 @@ +{ + "errors": [ + { + "reason": "NEW target must have type Other", + "line": 20 + }, + { + "reason": "NEW requires a class type", + "line": 21 + }, + { + "reason": "NEW requires a class type", + "line": 22 + }, + { + "reason": "has no constructor NEW", + "line": 23 + }, + { + "reason": "Incorrect # arguments passed to Required.NEW", + "line": 24 + }, + { + "reason": "Incorrect # arguments passed to Required.NEW", + "line": 25 + }, + { + "reason": "NEW target must have type Hidden", + "line": 26 + } + ] +} diff --git a/tests/iii/class-new-errors/program.cps b/tests/iii/class-new-errors/program.cps new file mode 100644 index 0000000..3a9e9c2 --- /dev/null +++ b/tests/iii/class-new-errors/program.cps @@ -0,0 +1,26 @@ +CLASS Box + Value : INTEGER +ENDCLASS +CLASS Other + Value : INTEGER +ENDCLASS +CLASS Required + PROCEDURE NEW(Number : INTEGER) + ENDPROCEDURE +ENDCLASS +CLASS Hidden + PRIVATE PROCEDURE NEW() + ENDPROCEDURE +ENDCLASS +TYPE RecordValue + DECLARE Value : INTEGER +ENDTYPE +DECLARE B : Box +DECLARE R : Required(1) +B <- NEW Other +B <- NEW INTEGER +B <- NEW RecordValue +B <- NEW Box(1) +R <- NEW Required +R <- NEW Required() +B <- NEW Hidden -- cgit v1.3.1-4-g156e