diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-17 06:02:21 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-17 06:02:21 +0000 |
| commit | 3425b95cb654f8e1adc98365a4b370aa1fff8b4d (patch) | |
| tree | 4d81b91d0bd807cd6f7ced037533d6e60f5c307f | |
| parent | 4e22f464387725a5a282f3d72e11cfb694a31a5a (diff) | |
feat(classes): support ordinary member methods and this references
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| -rw-r--r-- | include/cps/CodeGen.h | 4 | ||||
| -rw-r--r-- | include/cps/FunctionAST.h | 32 | ||||
| -rw-r--r-- | include/cps/FunctionGen.h | 2 | ||||
| -rw-r--r-- | include/cps/Parser.h | 9 | ||||
| -rw-r--r-- | include/cps/TypeAST.h | 10 | ||||
| -rw-r--r-- | lib/CodeGen/CodeGen.cc | 7 | ||||
| -rw-r--r-- | lib/CodeGen/ConstantGen.cc | 4 | ||||
| -rw-r--r-- | lib/CodeGen/Designator.cc | 55 | ||||
| -rw-r--r-- | lib/CodeGen/ExpressionGen.cc | 3 | ||||
| -rw-r--r-- | lib/CodeGen/FunctionGen.cc | 74 | ||||
| -rw-r--r-- | lib/CodeGen/StatementGen.cc | 15 | ||||
| -rw-r--r-- | lib/CodeGen/ValueTypeGen.cc | 9 | ||||
| -rw-r--r-- | lib/Parser/Parser.cc | 6 | ||||
| -rw-r--r-- | lib/Parser/ParserExpression.cc | 28 | ||||
| -rw-r--r-- | lib/Parser/ParserFunction.cc | 26 | ||||
| -rw-r--r-- | lib/Parser/ParserType.cc | 28 | ||||
| -rw-r--r-- | tests/i/class-methods/case.json | 3 | ||||
| -rw-r--r-- | tests/i/class-methods/expected.txt | 9 | ||||
| -rw-r--r-- | tests/i/class-methods/notes.md | 7 | ||||
| -rw-r--r-- | tests/i/class-methods/program.cps | 89 | ||||
| -rw-r--r-- | tests/iii/class-method-errors/case.json | 52 | ||||
| -rw-r--r-- | tests/iii/class-method-errors/notes.md | 3 | ||||
| -rw-r--r-- | tests/iii/class-method-errors/program.cps | 35 |
23 files changed, 458 insertions, 52 deletions
diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h index 94aacb7..1c80434 100644 --- a/include/cps/CodeGen.h +++ b/include/cps/CodeGen.h @@ -32,6 +32,10 @@ class CodeGen { std::unique_ptr<llvm::IRBuilder<>> Builder; int DiagnosticLine = 0; IdentifierMap<SymbolInfo> Symbols; + IdentifierMap<IdentifierMap<FunctionDefAST*>> ClassMethods; + std::string CurrentClass; + const PrototypeAST *findMethod(const std::string &Class, const std::string &Name) const; + llvm::Value *emitMemberCall(MemberCallExprAST *Call); struct ConstantScope { unsigned ID; IdentifierMap<std::optional<SymbolInfo>> Saved; diff --git a/include/cps/FunctionAST.h b/include/cps/FunctionAST.h index 0c2d0b5..16057e5 100644 --- a/include/cps/FunctionAST.h +++ b/include/cps/FunctionAST.h @@ -28,18 +28,24 @@ class PrototypeAST { std::vector<ParamDecl> Params; std::string ReturnType; bool IsExternal; + std::string OwnerClass; + bool IsPublic; public: PrototypeAST(const std::string &Name, std::vector<ParamDecl> Params, const std::string &ReturnType, - bool IsExternal = false) - : Name(Name), Params(std::move(Params)), ReturnType(ReturnType), IsExternal(IsExternal) {} + bool IsExternal = false, std::string OwnerClass = {}, bool IsPublic = true) + : Name(Name), Params(std::move(Params)), ReturnType(ReturnType), IsExternal(IsExternal), + OwnerClass(std::move(OwnerClass)), IsPublic(IsPublic) {} const std::string &getName() const { return Name; } const std::vector<ParamDecl> &getParams() const { return Params; } const std::string &getReturnType() const { return ReturnType; } bool isExternal() const { return IsExternal; } + const std::string &getOwnerClass() const { return OwnerClass; } + bool isPublic() const { return IsPublic; } + std::string getLinkName() const { return OwnerClass.empty() ? Name : OwnerClass + "." + Name; } }; class FunctionDefAST : public StmtAST { @@ -80,6 +86,28 @@ public: }; +// A member call keeps the receiver separate from explicit arguments. +class MemberCallExprAST : public ExprAST { + std::unique_ptr<ExprAST> Receiver; + std::string Method; + std::vector<std::unique_ptr<ExprAST>> Args; +public: + MemberCallExprAST(std::unique_ptr<ExprAST> Receiver, std::string Method, + std::vector<std::unique_ptr<ExprAST>> Args, int Line) + : ExprAST(Line), Receiver(std::move(Receiver)), Method(std::move(Method)), Args(std::move(Args)) {} + ExprAST *getReceiver() const { return Receiver.get(); } + const std::string &getMethod() const { return Method; } + const std::vector<std::unique_ptr<ExprAST>> &getArgs() const { return Args; } +}; + +class MemberCallStmtAST : public StmtAST { + std::unique_ptr<MemberCallExprAST> Call; +public: + explicit MemberCallStmtAST(std::unique_ptr<MemberCallExprAST> Call) + : StmtAST(Call->getLine()), Call(std::move(Call)) {} + MemberCallExprAST *getCall() const { return Call.get(); } +}; + class ReturnStmtAST : public StmtAST { std::unique_ptr<ExprAST> RetVal; diff --git a/include/cps/FunctionGen.h b/include/cps/FunctionGen.h index fd874d2..5ff746d 100644 --- a/include/cps/FunctionGen.h +++ b/include/cps/FunctionGen.h @@ -65,7 +65,7 @@ private: IdentifierMap<FuncSig> Signatures; ArrayParamBinder BindArrayParam; - void createArgumentAllocas(llvm::Function *F, const std::vector<ParamDecl> &Params); + void createArgumentAllocas(llvm::Function *F, const std::vector<ParamDecl> &Params, bool IsMethod); public: FunctionGen(llvm::LLVMContext &C, diff --git a/include/cps/Parser.h b/include/cps/Parser.h index fdbb99a..4874d7a 100644 --- a/include/cps/Parser.h +++ b/include/cps/Parser.h @@ -43,6 +43,8 @@ class Parser { std::unique_ptr<ExprAST> ParseNumberExpr(); std::unique_ptr<ExprAST> ParseIdentifierExpr(); std::unique_ptr<ExprAST> ParseParenExpr(); + std::unique_ptr<MemberCallExprAST> ParseMemberCall( + const std::string &Name, std::vector<DesignatorAccess> Accesses, int Line); std::string ParseTypeName(bool AllowVoid = false); std::unique_ptr<StmtAST> ParseStatement(); @@ -66,11 +68,12 @@ class Parser { std::unique_ptr<StmtAST> ParseTypeDecl(); std::optional<std::vector<RecordFieldDecl>> ParseRecordBody( - const std::string &Name, int Line, int EndToken); + const std::string &Name, int Line, int EndToken, + std::vector<std::unique_ptr<FunctionDefAST>> *Methods = nullptr); bool ParseRecordField(std::vector<RecordFieldDecl> &Out, bool IsPublic); - std::unique_ptr<StmtAST> ParseFunction(); - std::unique_ptr<StmtAST> ParseProcedure(); + std::unique_ptr<FunctionDefAST> ParseFunction(const std::string &OwnerClass = {}, bool IsPublic = true); + std::unique_ptr<FunctionDefAST> ParseProcedure(const std::string &OwnerClass = {}, bool IsPublic = true); std::unique_ptr<StmtAST> ParseCallStmt(); std::unique_ptr<StmtAST> ParseReturnStmt(); std::optional<std::vector<ParamDecl>> ParsePrototypeArgs(bool AllowByRef); diff --git a/include/cps/TypeAST.h b/include/cps/TypeAST.h index edf2ee5..de5d12c 100644 --- a/include/cps/TypeAST.h +++ b/include/cps/TypeAST.h @@ -1,5 +1,6 @@ #pragma once #include "cps/AST.h" +#include "cps/FunctionAST.h" #include <cstdint> #include <memory> #include <string> @@ -81,12 +82,15 @@ public: const std::vector<RecordFieldDecl> &getFields() const { return Fields; } }; -// CLASS <name> with default-public or explicit PUBLIC/PRIVATE fields, ending in ENDCLASS. +// CLASS <name> with default-public or explicit PUBLIC/PRIVATE members, ending in ENDCLASS. // Storage and value semantics are inherited from record declarations. class ClassTypeDeclAST : public RecordTypeDeclAST { + std::vector<std::unique_ptr<FunctionDefAST>> Methods; public: - ClassTypeDeclAST(const std::string &Name, std::vector<RecordFieldDecl> Fields) - : RecordTypeDeclAST(Name, std::move(Fields)) {} + ClassTypeDeclAST(const std::string &Name, std::vector<RecordFieldDecl> Fields, + std::vector<std::unique_ptr<FunctionDefAST>> Methods) + : RecordTypeDeclAST(Name, std::move(Fields)), Methods(std::move(Methods)) {} + const auto &getMethods() const { return Methods; } }; // ---- Designators: chained accesses usable as lvalue or rvalue (ยง4.2) ---- diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index ffabf8d..01393a9 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -169,6 +169,13 @@ void CodeGen::compile(const std::vector<std::unique_ptr<StmtAST>> &Statements) { } } + for (const auto &[Class, Methods] : ClassMethods) { + for (const auto &[Name, Method] : Methods) { + SaveAndRestore<int> Location(DiagnosticLine, Method->getLine()); + FuncGen->emitPrototype(Method->getProto()); + } + } + for (const auto &Stmt : Statements) { SaveAndRestore<int> Location(DiagnosticLine, Stmt->getLine()); emitStmt(Stmt.get()); diff --git a/lib/CodeGen/ConstantGen.cc b/lib/CodeGen/ConstantGen.cc index e14e22f..12bf4b1 100644 --- a/lib/CodeGen/ConstantGen.cc +++ b/lib/CodeGen/ConstantGen.cc @@ -16,6 +16,10 @@ void CodeGen::emitScopedBlock(const std::vector<std::unique_ptr<StmtAST>> &Body) } bool CodeGen::prepareSymbolDeclaration(const std::string &Name, bool IsConstant) { + if (!CurrentClass.empty() && sameIdentifier(Name, "this")) { + reportError("Cannot redeclare this inside a method"); + return false; + } auto &Scope = ConstantScopes.back(); auto It = Symbols.find(Name); if (It != Symbols.end()) { diff --git a/lib/CodeGen/Designator.cc b/lib/CodeGen/Designator.cc index fcc6919..adf08f1 100644 --- a/lib/CodeGen/Designator.cc +++ b/lib/CodeGen/Designator.cc @@ -40,6 +40,59 @@ void CodeGen::runTypePrePass(const std::vector<std::unique_ptr<StmtAST>> &Statem } if (!Ok) reportError("%s", Err.c_str()); } + + for (const auto &S : Statements) { + auto *Class = dynamic_cast<ClassTypeDeclAST*>(S.get()); + if (!Class) continue; + auto &Methods = ClassMethods[Class->getName()]; + IdentifierSet Names; + for (const auto &Field : Class->getFields()) { + Names.insert(Field.Name); + if (sameIdentifier(Field.Name, "this")) { + SaveAndRestore<int> Location(DiagnosticLine, Class->getLine()); + reportError("A class field cannot be named this"); + } + } + for (const auto &Method : Class->getMethods()) { + SaveAndRestore<int> Location(DiagnosticLine, Method->getLine()); + const std::string &Name = Method->getProto()->getName(); + if (sameIdentifier(Name, "NEW")) { + reportError("NEW constructors are not implemented"); + } else if (sameIdentifier(Name, "this")) { + reportError("A method cannot be named this"); + } else if (!Names.insert(Name).second) { + reportError("Duplicate member %s in class %s", Name.c_str(), Class->getName().c_str()); + } else { + Methods.emplace(Name, Method.get()); + } + } + } +} + +const PrototypeAST *CodeGen::findMethod(const std::string &Class, const std::string &Name) const { + auto C = ClassMethods.find(Class); + if (C == ClassMethods.end()) return nullptr; + auto M = C->second.find(Name); + return M == C->second.end() ? nullptr : M->second->getProto(); +} + +Value *CodeGen::emitMemberCall(MemberCallExprAST *Call) { + SaveAndRestore<int> Location(DiagnosticLine, Call->getLine()); + LValueInfo Receiver; + if (!emitLValue(Call->getReceiver(), Receiver)) return nullptr; + const PrototypeAST *Method = findMethod(Receiver.TypeName, Call->getMethod()); + if (!Method) { + reportError("Type %s has no method %s", Receiver.TypeName.c_str(), Call->getMethod().c_str()); + return nullptr; + } + if (!Method->isPublic() && !sameIdentifier(CurrentClass, Receiver.TypeName)) { + reportError("Cannot access private method %s of type %s", + Call->getMethod().c_str(), Receiver.TypeName.c_str()); + return nullptr; + } + std::vector<Value*> Args = {Receiver.Addr}; + if (!marshalCallArgs(Method->getLinkName(), Call->getArgs(), Call->getLine(), Args)) return nullptr; + return FuncGen->emitCall(Method->getLinkName(), Args); } // Resolves a designator (or legacy variable/array-access node) to the address @@ -126,7 +179,7 @@ bool CodeGen::emitLValue(ExprAST *E, LValueInfo &Out) { reportError("Record type %s has no field %s", TypeName.c_str(), A.FieldName.c_str()); return false; } - if (!FI->IsPublic) { + if (!FI->IsPublic && !sameIdentifier(CurrentClass, TypeName)) { reportError("Cannot access private field %s of type %s", A.FieldName.c_str(), TypeName.c_str()); return false; diff --git a/lib/CodeGen/ExpressionGen.cc b/lib/CodeGen/ExpressionGen.cc index 9ea79d8..fde948c 100644 --- a/lib/CodeGen/ExpressionGen.cc +++ b/lib/CodeGen/ExpressionGen.cc @@ -127,6 +127,9 @@ Value *CodeGen::emitExpr(ExprAST *Expr) { return emitBinaryOp(Bin->getOp(), L, LT, R, RT, Bin->getLine()); } + if (auto *Call = dynamic_cast<MemberCallExprAST*>(Expr)) + return emitMemberCall(Call); + if (auto *Call = dynamic_cast<CallExprAST*>(Expr)) return emitCall(Call->getCallee(), Call->getArgs(), Call->getLine()); diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index 10bd3fa..d5bc5d4 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -45,9 +45,12 @@ Type *FunctionGen::getLLVMType(const std::string &TypeName) { return Resolved; } -void FunctionGen::createArgumentAllocas(Function *F, const std::vector<ParamDecl> &Params) { +void FunctionGen::createArgumentAllocas(Function *F, const std::vector<ParamDecl> &Params, bool IsMethod) { Function::arg_iterator AI = F->arg_begin(); + if (IsMethod) ++AI; for (const ParamDecl &P : Params) { + bool InvalidThis = IsMethod && sameIdentifier(P.Name, "this"); + if (InvalidThis) reportError("A method parameter cannot be named this"); if (Types.lookupEnumConstant(P.Name)) { reportError("parameter '%s' collides with an enum value", P.Name.c_str()); } @@ -72,10 +75,11 @@ void FunctionGen::createArgumentAllocas(Function *F, const std::vector<ParamDecl LBs.push_back(LB); UBs.push_back(UB); } - if (BindArrayParam) BindArrayParam(P, ArgVal, LBs, UBs); + if (!InvalidThis && BindArrayParam) BindArrayParam(P, ArgVal, LBs, UBs); continue; } + if (InvalidThis) continue; if (P.Mode == PassMode::ByRef) { Symbols[P.Name] = {ArgVal, P.TypeName, false}; continue; @@ -96,32 +100,35 @@ const FuncSig *FunctionGen::getSignature(const std::string &Name) const { } Function *FunctionGen::emitPrototype(PrototypeAST *Proto) { - if (isReservedRuntimeName(Proto->getName())) { - reportError("'%s' is a reserved runtime symbol and cannot be a FUNCTION/PROCEDURE name", - Proto->getName().c_str()); - return nullptr; - } - if (CodeGen::isBuiltinName(Proto->getName())) { - reportError("'%s' is a built-in function and cannot be redefined", - Proto->getName().c_str()); - return nullptr; - } - if (Types.lookupEnumConstant(Proto->getName())) { - reportError("'%s' is an enum value and cannot be a FUNCTION/PROCEDURE name", - Proto->getName().c_str()); - return nullptr; - } - if (Types.resolve(Proto->getName())) { - reportError("'%s' is a type name and cannot be a FUNCTION/PROCEDURE name", - Proto->getName().c_str()); - return nullptr; + if (Proto->getOwnerClass().empty()) { + if (isReservedRuntimeName(Proto->getName())) { + reportError("'%s' is a reserved runtime symbol and cannot be a FUNCTION/PROCEDURE name", + Proto->getName().c_str()); + return nullptr; + } + if (CodeGen::isBuiltinName(Proto->getName())) { + reportError("'%s' is a built-in function and cannot be redefined", + Proto->getName().c_str()); + return nullptr; + } + if (Types.lookupEnumConstant(Proto->getName())) { + reportError("'%s' is an enum value and cannot be a FUNCTION/PROCEDURE name", + Proto->getName().c_str()); + return nullptr; + } + if (Types.resolve(Proto->getName())) { + reportError("'%s' is a type name and cannot be a FUNCTION/PROCEDURE name", + Proto->getName().c_str()); + return nullptr; + } } FuncSig Sig; - Sig.Name = Proto->getName(); + Sig.Name = Proto->getLinkName(); Sig.ReturnTypeName = Proto->getReturnType(); std::vector<Type*> ArgTypes; + if (!Proto->getOwnerClass().empty()) ArgTypes.push_back(PointerType::getUnqual(Context)); for (const ParamDecl &P : Proto->getParams()) { const TypeInfo *Info = Types.resolve(P.TypeName); if (Info && Info->isVoid()) { @@ -153,23 +160,24 @@ Function *FunctionGen::emitPrototype(PrototypeAST *Proto) { Type *RetType = getLLVMType(Proto->getReturnType()); FunctionType *FT = FunctionType::get(RetType, ArgTypes, false); - const FuncSig *Existing = getSignature(Proto->getName()); - Function *F = Module.getFunction(Existing ? Existing->Name : Proto->getName()); + const FuncSig *Existing = getSignature(Proto->getLinkName()); + Function *F = Module.getFunction(Existing ? Existing->Name : Proto->getLinkName()); if (F && F->getFunctionType() != FT) { reportError("Function %s conflicts with an existing declaration of a different type", Proto->getName().c_str()); return nullptr; } if (!F) { - F = Function::Create(FT, Function::ExternalLinkage, Proto->getName(), &Module); + F = Function::Create(FT, Function::ExternalLinkage, Proto->getLinkName(), &Module); } Exceptions.prepareFunction(F); // Keep the first registration: on a duplicate definition the first one // wins and the second is rejected by emitFunctionDef. - Signatures.emplace(Proto->getName(), std::move(Sig)); + Signatures.emplace(Proto->getLinkName(), std::move(Sig)); Function::arg_iterator AI = F->arg_begin(); + if (!Proto->getOwnerClass().empty()) (AI++)->setName("this"); for (const ParamDecl &P : Proto->getParams()) { if (AI == F->arg_end()) break; (AI++)->setName(P.Name); @@ -202,7 +210,19 @@ Function *FunctionGen::emitFunctionDef(FunctionDefAST *FuncAST, if (Info.ConstantValue && Info.ScopeID == 0) Symbols.emplace(Name, Info); } - createArgumentAllocas(TheFunction, Proto->getParams()); + bool IsMethod = !Proto->getOwnerClass().empty(); + if (IsMethod) { + const TypeInfo *Class = Types.resolve(Proto->getOwnerClass()); + Value *Self = TheFunction->getArg(0); + if (Class && Class->asRecord()) { + for (const auto &[Name, Field] : Class->asRecord()->Fields) { + Value *Addr = Builder.CreateStructGEP(Class->LLVMType, Self, Field.Index, Name); + Symbols[Name] = {Addr, Field.TypeName, false}; + } + } + Symbols["this"] = {Self, Proto->getOwnerClass(), false, nullptr, ScopeID}; + } + createArgumentAllocas(TheFunction, Proto->getParams(), IsMethod); for (const auto &P : Proto->getParams()) { auto It = Symbols.find(P.Name); if (It != Symbols.end()) It->second.ScopeID = ScopeID; diff --git a/lib/CodeGen/StatementGen.cc b/lib/CodeGen/StatementGen.cc index b648ec6..c55aebe 100644 --- a/lib/CodeGen/StatementGen.cc +++ b/lib/CodeGen/StatementGen.cc @@ -12,7 +12,17 @@ void CodeGen::emitStmt(StmtAST *Stmt) { SaveAndRestore<int> Location(DiagnosticLine, Stmt && Stmt->getLine() ? Stmt->getLine() : DiagnosticLine); if (!Stmt) return; + if (auto *Class = dynamic_cast<ClassTypeDeclAST*>(Stmt)) { + for (const auto &Method : Class->getMethods()) { + // Invalid or duplicate members have already been diagnosed. + if (findMethod(Class->getName(), Method->getProto()->getName()) == Method->getProto()) + emitStmt(Method.get()); + } + return; + } + if (auto *FuncDef = dynamic_cast<FunctionDefAST*>(Stmt)) { + SaveAndRestore<std::string> ClassContext(CurrentClass, FuncDef->getProto()->getOwnerClass()); BasicBlock *SavedBlock = Builder->GetInsertBlock(); // Function bodies get a fresh array scope (metadata SSA values belong // to the emitting function), mirroring FunctionGen's Symbols handling. @@ -54,6 +64,11 @@ void CodeGen::emitStmt(StmtAST *Stmt) { return; } + if (auto *Call = dynamic_cast<MemberCallStmtAST*>(Stmt)) { + emitMemberCall(Call->getCall()); + return; + } + if (auto *Call = dynamic_cast<CallStmtAST*>(Stmt)) { emitCall(Call->getCallee(), Call->getArgs(), Call->getLine()); return; diff --git a/lib/CodeGen/ValueTypeGen.cc b/lib/CodeGen/ValueTypeGen.cc index 1ee4a2d..16c3e0b 100644 --- a/lib/CodeGen/ValueTypeGen.cc +++ b/lib/CodeGen/ValueTypeGen.cc @@ -52,7 +52,7 @@ const TypeInfo *CodeGen::getExprTypeInfo(ExprAST *Expr) const { const RecordPayload *RP = Cur->asRecord(); const RecordFieldInfo *FI = RP ? RP->findField(A.FieldName) : nullptr; if (!FI) return nullptr; - if (!FI->IsPublic) return nullptr; + if (!FI->IsPublic && !sameIdentifier(CurrentClass, TypeName)) return nullptr; TypeName = FI->TypeName; } else if (A.Kind == AccessKind::Deref) { const PointerPayload *PP = Cur->asPointer(); @@ -117,6 +117,13 @@ const TypeInfo *CodeGen::getExprTypeInfo(ExprAST *Expr) const { return resolveType("INTEGER"); } + if (auto *Call = dynamic_cast<MemberCallExprAST*>(Expr)) { + const TypeInfo *Receiver = getExprTypeInfo(Call->getReceiver()); + const PrototypeAST *Method = Receiver ? findMethod(Receiver->Name, Call->getMethod()) : nullptr; + if (!Method || (!Method->isPublic() && !sameIdentifier(CurrentClass, Receiver->Name))) return nullptr; + return resolveType(Method->getReturnType()); + } + if (auto *Call = dynamic_cast<CallExprAST*>(Expr)) { const std::string &Name = Call->getCallee(); if (const BuiltinInfo *B = findBuiltin(Name)) { diff --git a/lib/Parser/Parser.cc b/lib/Parser/Parser.cc index 36b0a6a..26eeac6 100644 --- a/lib/Parser/Parser.cc +++ b/lib/Parser/Parser.cc @@ -231,6 +231,12 @@ std::unique_ptr<StmtAST> Parser::ParseAssignStmt() { std::vector<DesignatorAccess> Accesses; if (!ParseDesignatorSuffix(Accesses)) return nullptr; + if (CurTok == '(') { + auto Call = ParseMemberCall(Name, std::move(Accesses), Line); + if (!Call) return nullptr; + return std::make_unique<MemberCallStmtAST>(std::move(Call)); + } + if (CurTok != tok_assign) { fprintf(stderr, "error: Expected '<-' after identifier '%s' at line %d\n", Name.c_str(), Line); return nullptr; diff --git a/lib/Parser/ParserExpression.cc b/lib/Parser/ParserExpression.cc index b8952a8..e9b0dcc 100644 --- a/lib/Parser/ParserExpression.cc +++ b/lib/Parser/ParserExpression.cc @@ -59,6 +59,9 @@ std::unique_ptr<ExprAST> Parser::ParseIdentifierExpr() { std::vector<DesignatorAccess> Accesses; if (!ParseDesignatorSuffix(Accesses)) return nullptr; + if (CurTok == '(' && !atCaseBoundary()) + return ParseMemberCall(IdName, std::move(Accesses), Line); + // Degrade rule: shapes that existed before designators keep their nodes. if (Accesses.empty()) return std::make_unique<VariableExprAST>(IdName); @@ -67,6 +70,31 @@ std::unique_ptr<ExprAST> Parser::ParseIdentifierExpr() { return std::make_unique<DesignatorExprAST>(IdName, std::move(Accesses), Line); } +// The final field suffix names the method; preceding suffixes form its receiver. +std::unique_ptr<MemberCallExprAST> Parser::ParseMemberCall( + const std::string &Name, std::vector<DesignatorAccess> Accesses, int Line) { + if (Accesses.empty() || Accesses.back().Kind != AccessKind::Field) { + fprintf(stderr, "error: Expected a method name after '.' at line %d\n", Line); + return nullptr; + } + std::string Method = Accesses.back().FieldName; + Accesses.pop_back(); + auto Receiver = std::make_unique<DesignatorExprAST>(Name, std::move(Accesses), Line); + std::vector<std::unique_ptr<ExprAST>> Args; + if (CurTok == '(') { + getNextToken(); + auto Parsed = ParseExprList(')', true); + if (!Parsed) return nullptr; + Args = std::move(*Parsed); + } + if (CurTok == '[' || CurTok == '.' || CurTok == '^') { + fprintf(stderr, "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); +} + // Postfix accessor chain after an identifier: '[' exprs ']' | '.' IDENT | '^' bool Parser::ParseDesignatorSuffix(std::vector<DesignatorAccess> &Out) { while (true) { diff --git a/lib/Parser/ParserFunction.cc b/lib/Parser/ParserFunction.cc index fe53731..1aef756 100644 --- a/lib/Parser/ParserFunction.cc +++ b/lib/Parser/ParserFunction.cc @@ -128,7 +128,8 @@ std::optional<std::vector<ParamDecl>> Parser::ParsePrototypeArgs(bool AllowByRef return Params; } -std::unique_ptr<StmtAST> Parser::ParseFunction() { +std::unique_ptr<FunctionDefAST> Parser::ParseFunction(const std::string &OwnerClass, bool IsPublic) { + int Line = Lex.getLine(); getNextToken(); if (CurTok != tok_identifier) { fprintf(stderr, "error: Expected function name\n"); @@ -156,7 +157,7 @@ std::unique_ptr<StmtAST> Parser::ParseFunction() { } } - auto Proto = std::make_unique<PrototypeAST>(Name, std::move(*Args), RetType); + auto Proto = std::make_unique<PrototypeAST>(Name, std::move(*Args), RetType, false, OwnerClass, IsPublic); std::vector<std::unique_ptr<StmtAST>> Body = ParseBlock({tok_endfunction}); @@ -166,10 +167,13 @@ std::unique_ptr<StmtAST> Parser::ParseFunction() { } getNextToken(); - return std::make_unique<FunctionDefAST>(std::move(Proto), std::move(Body)); + auto Def = std::make_unique<FunctionDefAST>(std::move(Proto), std::move(Body)); + Def->setLine(Line); + return Def; } -std::unique_ptr<StmtAST> Parser::ParseProcedure() { +std::unique_ptr<FunctionDefAST> Parser::ParseProcedure(const std::string &OwnerClass, bool IsPublic) { + int Line = Lex.getLine(); getNextToken(); if (CurTok != tok_identifier) { fprintf(stderr, "error: Expected procedure name\n"); @@ -181,7 +185,7 @@ std::unique_ptr<StmtAST> Parser::ParseProcedure() { auto Args = ParsePrototypeArgs(/*AllowByRef=*/true); if (!Args) return nullptr; - auto Proto = std::make_unique<PrototypeAST>(Name, std::move(*Args), "VOID"); + auto Proto = std::make_unique<PrototypeAST>(Name, std::move(*Args), "VOID", false, OwnerClass, IsPublic); std::vector<std::unique_ptr<StmtAST>> Body = ParseBlock({tok_endprocedure}); @@ -191,7 +195,9 @@ std::unique_ptr<StmtAST> Parser::ParseProcedure() { } getNextToken(); - return std::make_unique<FunctionDefAST>(std::move(Proto), std::move(Body)); + auto Def = std::make_unique<FunctionDefAST>(std::move(Proto), std::move(Body)); + Def->setLine(Line); + return Def; } std::unique_ptr<StmtAST> Parser::ParseCallStmt() { @@ -205,6 +211,14 @@ std::unique_ptr<StmtAST> Parser::ParseCallStmt() { std::string Callee = Lex.IdentifierStr; getNextToken(); + std::vector<DesignatorAccess> Accesses; + if (!ParseDesignatorSuffix(Accesses)) return nullptr; + if (!Accesses.empty()) { + auto Call = ParseMemberCall(Callee, std::move(Accesses), Line); + if (!Call) return nullptr; + return std::make_unique<MemberCallStmtAST>(std::move(Call)); + } + std::vector<std::unique_ptr<ExprAST>> Args; if (CurTok == '(') { getNextToken(); diff --git a/lib/Parser/ParserType.cc b/lib/Parser/ParserType.cc index a57c8a6..0d664d0 100644 --- a/lib/Parser/ParserType.cc +++ b/lib/Parser/ParserType.cc @@ -89,10 +89,11 @@ std::unique_ptr<StmtAST> Parser::ParseTypeDecl() { return nullptr; } } else { - auto Fields = ParseRecordBody(Name, Line, IsClass ? tok_endclass : tok_endtype); + std::vector<std::unique_ptr<FunctionDefAST>> Methods; + auto Fields = ParseRecordBody(Name, Line, IsClass ? tok_endclass : tok_endtype, &Methods); if (!Fields) return nullptr; if (IsClass) - Decl = std::make_unique<ClassTypeDeclAST>(Name, std::move(*Fields)); + Decl = std::make_unique<ClassTypeDeclAST>(Name, std::move(*Fields), std::move(Methods)); else Decl = std::make_unique<RecordTypeDeclAST>(Name, std::move(*Fields)); } @@ -107,12 +108,14 @@ std::unique_ptr<StmtAST> Parser::ParseTypeDecl() { // Shared record body and field parsing. Fields are data, never statements. std::optional<std::vector<RecordFieldDecl>> Parser::ParseRecordBody( - const std::string &Name, int Line, int EndToken) { + const std::string &Name, int Line, int EndToken, + std::vector<std::unique_ptr<FunctionDefAST>> *Methods) { bool IsClass = EndToken == tok_endclass; const char *Keyword = IsClass ? "CLASS" : "TYPE"; const char *EndKeyword = IsClass ? "ENDCLASS" : "ENDTYPE"; auto AtField = [&] { - return IsClass ? (CurTok == tok_public || CurTok == tok_private || CurTok == tok_identifier) + return IsClass ? (CurTok == tok_public || CurTok == tok_private || CurTok == tok_identifier || + CurTok == tok_function || CurTok == tok_procedure) : CurTok == tok_declare; }; std::vector<RecordFieldDecl> Fields; @@ -121,10 +124,18 @@ std::optional<std::vector<RecordFieldDecl>> Parser::ParseRecordBody( if (AtField()) { // Class fields default to public, like record fields. bool IsPublic = CurTok != tok_private; - if (ParseRecordField(Fields, IsPublic)) continue; + if (IsClass && (CurTok == tok_public || CurTok == tok_private)) getNextToken(); + if (IsClass && (CurTok == tok_function || CurTok == tok_procedure)) { + auto Method = CurTok == tok_function ? ParseFunction(Name, IsPublic) + : ParseProcedure(Name, IsPublic); + if (Method && Methods) { + Methods->push_back(std::move(Method)); + continue; + } + } else if (ParseRecordField(Fields, IsPublic)) continue; } else { fprintf(stderr, "error: Expected %s or %s in %s %s at line %d\n", - IsClass ? "field, PUBLIC or PRIVATE" : "DECLARE", EndKeyword, + IsClass ? "field, method, PUBLIC or PRIVATE" : "DECLARE", EndKeyword, Keyword, Name.c_str(), Lex.getLine()); } HadFieldError = true; @@ -143,8 +154,9 @@ std::optional<std::vector<RecordFieldDecl>> Parser::ParseRecordBody( return std::nullopt; } getNextToken(); // eat body terminator - if (Fields.empty() && !HadFieldError) { - fprintf(stderr, "error: %s %s has no fields at line %d\n", Keyword, Name.c_str(), Line); + if (Fields.empty() && (!Methods || Methods->empty()) && !HadFieldError) { + fprintf(stderr, "error: %s %s has no %s at line %d\n", Keyword, Name.c_str(), + IsClass ? "members" : "fields", Line); return std::nullopt; } if (HadFieldError) return std::nullopt; diff --git a/tests/i/class-methods/case.json b/tests/i/class-methods/case.json new file mode 100644 index 0000000..1f18635 --- /dev/null +++ b/tests/i/class-methods/case.json @@ -0,0 +1,3 @@ +{ + "exit_code": 0 +} diff --git a/tests/i/class-methods/expected.txt b/tests/i/class-methods/expected.txt new file mode 100644 index 0000000..c3e4c42 --- /dev/null +++ b/tests/i/class-methods/expected.txt @@ -0,0 +1,9 @@ +5,8 +9,8 +9,9 +10,100 +10,7 +10,10 +caught +42 +99 diff --git a/tests/i/class-methods/notes.md b/tests/i/class-methods/notes.md new file mode 100644 index 0000000..903166c --- /dev/null +++ b/tests/i/class-methods/notes.md @@ -0,0 +1,7 @@ +# class-methods + +Human review: pending. Expected output was independently traced by AI, not captured from cpsc. + +Basis: guide section 10.1; this and existing class value semantics are project behavior. + +Trace: A and B start at 3 and 8; Add changes only A to 5. Copy then Set leaves A at 9 and B at 8. Clone copies 9 into B; same-class private access reads 9. Sum(4) is 10 and the global Get returns 100. The array pointer changes Items[1] from 6 to 10; the nested object stays 7. Take adds A's 9 to Number's 1 and writes 10 into Values[1]. Fail is caught. Passing this BYREF changes A to 42. A method-only class returns 99 from its independent Get method. diff --git a/tests/i/class-methods/program.cps b/tests/i/class-methods/program.cps new file mode 100644 index 0000000..655fb8b --- /dev/null +++ b/tests/i/class-methods/program.cps @@ -0,0 +1,89 @@ +CLASS Counter + PRIVATE Value : INTEGER + PROCEDURE Set(Number : INTEGER) + Value <- Number + ENDPROCEDURE + FUNCTION Get() RETURNS INTEGER + RETURN Value + ENDFUNCTION + PROCEDURE Add(Number : INTEGER) + this.Set(this.Get() + Number) + ENDPROCEDURE + PROCEDURE Copy(Other : Counter) + this <- Other + ENDPROCEDURE + FUNCTION Clone() RETURNS Counter + RETURN this + ENDFUNCTION + FUNCTION ReadOther(Other : Counter) RETURNS INTEGER + RETURN Other.Hidden() + ENDFUNCTION + PRIVATE FUNCTION Hidden() RETURNS INTEGER + RETURN Value + ENDFUNCTION + FUNCTION Sum(Number : INTEGER) RETURNS INTEGER + IF Number = 0 THEN + RETURN 0 + ENDIF + RETURN Number + this.Sum(Number - 1) + ENDFUNCTION + PROCEDURE Take(BYREF Number : INTEGER, BYREF Values : ARRAY OF INTEGER) + Number <- Number + Value + Values[1] <- Number + ENDPROCEDURE + PROCEDURE Fail() + THROW + ENDPROCEDURE + PROCEDURE PassSelf() + CALL Change(this) + ENDPROCEDURE +ENDCLASS +CLASS Marker + FUNCTION Get() RETURNS INTEGER + RETURN 99 + ENDFUNCTION +ENDCLASS +TYPE Container + DECLARE Item : Counter +ENDTYPE +TYPE CounterPointer = ^Counter +PROCEDURE Change(BYREF Target : Counter) + Target.Set(42) +ENDPROCEDURE +FUNCTION Get() RETURNS INTEGER + RETURN 100 +ENDFUNCTION +DECLARE A : Counter +DECLARE B : Counter +CALL a.sET(3) +B.Set(8) +A.Add(2) +OUTPUT A.Get(), ",", B.Get() +CALL A.Copy(B) +A.Set(9) +OUTPUT A.Get(), ",", B.Get() +B <- A.Clone() +OUTPUT B.Get(), ",", A.ReadOther(B) +OUTPUT A.Sum(4), ",", Get() +DECLARE Items : ARRAY[1:2] OF Counter +Items[1].Set(6) +DECLARE Wrapped : Container +Wrapped.Item.Set(7) +DECLARE P : CounterPointer +P <- ^Items[1] +P^.Add(4) +OUTPUT Items[1].Get(), ",", Wrapped.Item.Get() +DECLARE Number : INTEGER +DECLARE Values : ARRAY[1:2] OF INTEGER +Number <- 1 +CALL A.Take(Number, Values) +OUTPUT Number, ",", Values[1] +TRY + A.Fail() +CATCH + OUTPUT "caught" +ENDTRY +A.PassSelf() +OUTPUT A.Get() +DECLARE M : Marker +OUTPUT M.Get() diff --git a/tests/iii/class-method-errors/case.json b/tests/iii/class-method-errors/case.json new file mode 100644 index 0000000..655296d --- /dev/null +++ b/tests/iii/class-method-errors/case.json @@ -0,0 +1,52 @@ +{ + "errors": [ + { + "reason": "Cannot redeclare this inside a method", + "line": 11 + }, + { + "reason": "A method parameter cannot be named this", + "line": 13 + }, + { + "reason": "NEW constructors are not implemented", + "line": 15 + }, + { + "reason": "Duplicate member set in class Box", + "line": 17 + }, + { + "reason": "Duplicate member Value in class Box", + "line": 19 + }, + { + "reason": "Cannot access private field Hidden of type Box", + "line": 25 + }, + { + "reason": "Cannot access private method Secret of type Box", + "line": 26 + }, + { + "reason": "Cannot access private method Secret of type Box", + "line": 30 + }, + { + "reason": "Cannot access private field Hidden of type Box", + "line": 31 + }, + { + "reason": "Incorrect # arguments passed to Box.Set", + "line": 32 + }, + { + "reason": "Type Box has no method Missing", + "line": 33 + }, + { + "reason": "Type INTEGER has no method Set", + "line": 35 + } + ] +} diff --git a/tests/iii/class-method-errors/notes.md b/tests/iii/class-method-errors/notes.md new file mode 100644 index 0000000..f9be5b9 --- /dev/null +++ b/tests/iii/class-method-errors/notes.md @@ -0,0 +1,3 @@ +# class-method-errors + +Reject private access from outside the owning class, duplicate members, NEW constructors, redeclarations of this, incorrect arguments and invalid receivers. diff --git a/tests/iii/class-method-errors/program.cps b/tests/iii/class-method-errors/program.cps new file mode 100644 index 0000000..f2503e2 --- /dev/null +++ b/tests/iii/class-method-errors/program.cps @@ -0,0 +1,35 @@ +CLASS Box + PRIVATE Hidden : INTEGER + PUBLIC Value : INTEGER + PROCEDURE Set(Number : INTEGER) + Hidden <- Number + ENDPROCEDURE + PRIVATE FUNCTION Secret() RETURNS INTEGER + RETURN Hidden + ENDFUNCTION + PROCEDURE BadLocal() + DECLARE this : Box + ENDPROCEDURE + PROCEDURE BadParam(this : Box) + ENDPROCEDURE + PROCEDURE NEW() + ENDPROCEDURE + PROCEDURE set() + ENDPROCEDURE + FUNCTION Value() RETURNS INTEGER + RETURN 0 + ENDFUNCTION +ENDCLASS +CLASS Stranger + PROCEDURE Probe(Other : Box) + OUTPUT Other.Hidden + OUTPUT Other.Secret() + ENDPROCEDURE +ENDCLASS +DECLARE B : Box +OUTPUT B.Secret() +OUTPUT B.Hidden +B.Set() +B.Missing() +DECLARE Number : INTEGER +Number.Set(1) |
