aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-17 06:02:21 +0000
committer_ov4 <ov4@ov4.org>2026-10-17 06:02:21 +0000
commit3425b95cb654f8e1adc98365a4b370aa1fff8b4d (patch)
tree4d81b91d0bd807cd6f7ced037533d6e60f5c307f
parent4e22f464387725a5a282f3d72e11cfb694a31a5a (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.h4
-rw-r--r--include/cps/FunctionAST.h32
-rw-r--r--include/cps/FunctionGen.h2
-rw-r--r--include/cps/Parser.h9
-rw-r--r--include/cps/TypeAST.h10
-rw-r--r--lib/CodeGen/CodeGen.cc7
-rw-r--r--lib/CodeGen/ConstantGen.cc4
-rw-r--r--lib/CodeGen/Designator.cc55
-rw-r--r--lib/CodeGen/ExpressionGen.cc3
-rw-r--r--lib/CodeGen/FunctionGen.cc74
-rw-r--r--lib/CodeGen/StatementGen.cc15
-rw-r--r--lib/CodeGen/ValueTypeGen.cc9
-rw-r--r--lib/Parser/Parser.cc6
-rw-r--r--lib/Parser/ParserExpression.cc28
-rw-r--r--lib/Parser/ParserFunction.cc26
-rw-r--r--lib/Parser/ParserType.cc28
-rw-r--r--tests/i/class-methods/case.json3
-rw-r--r--tests/i/class-methods/expected.txt9
-rw-r--r--tests/i/class-methods/notes.md7
-rw-r--r--tests/i/class-methods/program.cps89
-rw-r--r--tests/iii/class-method-errors/case.json52
-rw-r--r--tests/iii/class-method-errors/notes.md3
-rw-r--r--tests/iii/class-method-errors/program.cps35
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)