From 05cdcb6a1cc3d4de3a0b3d12d4babcd6438b4754 Mon Sep 17 00:00:00 2001 From: _ov4 Date: Tue, 13 Oct 2026 13:45:03 +0000 Subject: Add record-derived classes with explicit field visibility Co-Authored-By: Claude Fable 5.1 --- AGENTS/ARCH.md | 2 +- AGENTS/PITFALLS.md | 10 +++++++- AGENTS/SUBSYSTEMS.md | 9 +++++++ doc/cpsc.md | 24 +++++++++++++++++++ include/cps/Lexer.h | 6 ++++- include/cps/TypeAST.h | 8 +++++++ lib/CodeGen/CodeGen.cc | 1 + lib/CodeGen/Designator.cc | 5 ++++ lib/Lexer/Lexer.cc | 4 ++++ lib/Parser/Parser.cc | 8 ++++++- lib/Parser/ParserType.cc | 61 ++++++++++++++++++++++++++++++++++------------- 11 files changed, 118 insertions(+), 20 deletions(-) diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md index 247bce1..c837ea1 100644 --- a/AGENTS/ARCH.md +++ b/AGENTS/ARCH.md @@ -144,7 +144,7 @@ untested.) | `lib/Parser/Parser.cc` | Statements, expressions, designators, precedence table | | `lib/Parser/ParserFunction.cc` | FUNCTION/PROCEDURE/CALL/RETURN + parameter-list parsing | | `lib/Parser/ParserFile.cc` | OPENFILE/READFILE/WRITEFILE/CLOSEFILE parsing | -| `lib/Parser/ParserType.cc` | TYPE/ENDTYPE declarations, SET/DEFINE reserved stubs | +| `lib/Parser/ParserType.cc` | TYPE/ENDTYPE and CLASS/ENDCLASS declarations, shared field parsing, SET/DEFINE reserved stubs | | `lib/CodeGen/CodeGen.cc` | Hub: dispatch, coercion, control flow, INPUT/OUTPUT, main() | | `lib/CodeGen/Designator.cc` | CodeGen methods for the TYPE system: type pre-pass, `emitLValue`/`loadFromLValue`, `emitCoercedExpr` gate, user-kind binary-op interception | | `lib/CodeGen/*Handler.cc`, `FunctionGen.cc`, `RuntimeCheck.cc`, `TypeSystem.cc` | Helpers owned by CodeGen | diff --git a/AGENTS/PITFALLS.md b/AGENTS/PITFALLS.md index ea6a4d7..b70718c 100644 --- a/AGENTS/PITFALLS.md +++ b/AGENTS/PITFALLS.md @@ -71,11 +71,19 @@ test programs. followed by `.` is swallowed into a REAL literal, so `1.foo` mislexes (only already-invalid programs can hit it). +- **CLASS subset (§10).** `CLASS`/`ENDCLASS`/`PUBLIC`/`PRIVATE` are reserved. + CLASS is a top-level record-derived value type with mandatory per-field access + keywords (no DECLARE). Unlike the guide's default-public rule, omitting access + is an error. TYPE fields cannot specify access and remain public. Private fields + cannot be accessed externally; whole-value initialization/copying is allowed. + CLASS inherits record limitations, including no array fields or empty bodies. + ## Not implemented (9618 features) CASE OF, CONSTANT, SET types (`SET`/`DEFINE` are reserved with dedicated errors), random files (OPENFILE...FOR RANDOM/SEEK/GETRECORD/PUTRECORD — TYPE is done, so these are unblocked now; the RANDOM keyword is reserved and gives a dedicated parse error), DATE, INPUT into array elements, string escape sequences, -array-typed record fields, whole-array RETURN. No tests and no CI exist; verify +array-typed record fields, whole-array RETURN, class methods/constructors/destructors, +NEW and class inheritance. No compiler tests or CI exist; verify changes by compiling sample programs end-to-end (command sequence in `ARCH.md`). diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md index ecdf3ff..0dafd12 100644 --- a/AGENTS/SUBSYSTEMS.md +++ b/AGENTS/SUBSYSTEMS.md @@ -52,6 +52,15 @@ FileHandler, is deliberately ignorant of user kinds). The enum range check after producer (constants, same-name copies, zero-init, memset-0 arrays) is valid by construction, which is why 0-based ordinals are load-bearing. +`ClassTypeDeclAST` inherits `RecordTypeDeclAST`: CLASS declarations use the same +two-pass registration, `TypeKind::Record`, `%record.NAME` layout and value semantics. +Shared body/field parsing lives in `ParserType.cc`. TYPE fields always have +`IsPublic = true`; CLASS fields require an explicit PUBLIC/PRIVATE keyword and +carry that flag through `defineRecord` into `RecordFieldInfo`. Both the static +designator type walk and `emitLValue` reject private field access, including +address-of, array elements and pointer dereferences. There is no class method +context or language-level inheritance yet. + **Runtime checks** (`RuntimeCheck`): array bounds, file misuse, enum ranges, null dereferences and array-parameter bounds contracts are checked at their emit sites. `ArithmeticHandler` calls `emitDivZeroCheck` before `/`, DIV and MOD; the check accepts diff --git a/doc/cpsc.md b/doc/cpsc.md index fa4ae2b..8a57a63 100644 --- a/doc/cpsc.md +++ b/doc/cpsc.md @@ -66,6 +66,30 @@ Save IR and run it: cpsc test.cps -r -o result.ir ``` +# CLASSES + +Declare classes at top level. Every field must explicitly specify `PUBLIC` or +`PRIVATE`, without `DECLARE`: + +```text +CLASS Person + PUBLIC Name : STRING + PRIVATE Age : INTEGER +ENDCLASS + +DECLARE p : Person +p.Name <- "Alice" +OUTPUT p.Name +``` + +Classes use record value semantics: zero initialization, whole-value copying, +arrays, pointers and ordinary function/procedure parameters. Private fields cannot +be read, written or addressed externally; they are still initialized and copied +with the whole value. Record `TYPE` fields remain implicitly public and cannot +specify access levels. Class methods, constructors, destructors, `NEW` and +`INHERITS` are not supported. Array fields remain unsupported in both records and +classes. + # EXIT STATUS **0** diff --git a/include/cps/Lexer.h b/include/cps/Lexer.h index d83e808..3652df0 100644 --- a/include/cps/Lexer.h +++ b/include/cps/Lexer.h @@ -84,7 +84,11 @@ enum Token { tok_type = -600, tok_endtype = -601, tok_set = -602, // reserved: SET types are recognised but not implemented - tok_define = -603 // reserved: DEFINE (set literals) likewise + tok_define = -603, // reserved: DEFINE (set literals) likewise + tok_class = -604, + tok_endclass = -605, + tok_public = -606, + tok_private = -607 }; class Lexer { diff --git a/include/cps/TypeAST.h b/include/cps/TypeAST.h index 601b49b..a23f836 100644 --- a/include/cps/TypeAST.h +++ b/include/cps/TypeAST.h @@ -60,6 +60,14 @@ public: const std::vector &getFields() const { return Fields; } }; +// CLASS with explicitly PUBLIC/PRIVATE fields, ending in ENDCLASS. +// Storage and value semantics are inherited from record declarations. +class ClassTypeDeclAST : public RecordTypeDeclAST { +public: + ClassTypeDeclAST(const std::string &Name, std::vector Fields) + : RecordTypeDeclAST(Name, std::move(Fields)) {} +}; + // ---- Designators: chained accesses usable as lvalue or rvalue (§4.2) ---- enum class AccessKind : uint8_t { Index, Field, Deref }; diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index f12c6ce..dcf665a 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -215,6 +215,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; TypeName = FI->TypeName; } else if (A.Kind == AccessKind::Deref) { const PointerPayload *PP = Cur->asPointer(); diff --git a/lib/CodeGen/Designator.cc b/lib/CodeGen/Designator.cc index 640ca0b..9d14d23 100644 --- a/lib/CodeGen/Designator.cc +++ b/lib/CodeGen/Designator.cc @@ -145,6 +145,11 @@ 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) { + reportError("Cannot access private field %s of type %s", + A.FieldName.c_str(), TypeName.c_str()); + return false; + } Addr = Builder->CreateStructGEP(Cur->LLVMType, Addr, FI->Index, TypeName + "." + A.FieldName); TypeName = FI->TypeName; diff --git a/lib/Lexer/Lexer.cc b/lib/Lexer/Lexer.cc index 4f7f997..a71b37c 100644 --- a/lib/Lexer/Lexer.cc +++ b/lib/Lexer/Lexer.cc @@ -132,6 +132,10 @@ int Lexer::gettok() { if (IdentifierStr == "ENDTYPE") return tok_endtype; if (IdentifierStr == "SET") return tok_set; if (IdentifierStr == "DEFINE") return tok_define; + if (IdentifierStr == "CLASS") return tok_class; + if (IdentifierStr == "ENDCLASS") return tok_endclass; + if (IdentifierStr == "PUBLIC") return tok_public; + if (IdentifierStr == "PRIVATE") return tok_private; return tok_identifier; } diff --git a/lib/Parser/Parser.cc b/lib/Parser/Parser.cc index e68b405..098ef82 100644 --- a/lib/Parser/Parser.cc +++ b/lib/Parser/Parser.cc @@ -275,6 +275,7 @@ void Parser::syncToStatementStart() { case tok_next: case tok_endfunction: case tok_endprocedure: case tok_openfile: case tok_readfile: case tok_writefile: case tok_closefile: case tok_type: case tok_define: case tok_endtype: + case tok_class: case tok_endclass: return; default: getNextToken(); @@ -601,7 +602,7 @@ std::unique_ptr Parser::ParseStatementImpl() { else if (CurTok == tok_closefile) { return ParseCloseFile(); } - else if (CurTok == tok_type) { + else if (CurTok == tok_type || CurTok == tok_class) { return ParseTypeDecl(); } else if (CurTok == tok_define) { @@ -614,6 +615,11 @@ std::unique_ptr Parser::ParseStatementImpl() { getNextToken(); return nullptr; } + else if (CurTok == tok_endclass) { + fprintf(stderr, "error: ENDCLASS without matching CLASS at line %d\n", Lex.getLine()); + getNextToken(); + return nullptr; + } fprintf(stderr, "error: Unexpected token at line %d when expecting a statement\n", Lex.getLine()); getNextToken(); // guarantee progress so error recovery cannot loop forever diff --git a/lib/Parser/ParserType.cc b/lib/Parser/ParserType.cc index ae671d5..358e3e2 100644 --- a/lib/Parser/ParserType.cc +++ b/lib/Parser/ParserType.cc @@ -11,15 +11,19 @@ using namespace cps; // ... // ENDTYPE // TYPE = SET OF is recognised and rejected (RANDOM precedent). -// TYPE is a top-level-only statement; a nested declaration is still consumed +// CLASS PUBLIC/PRIVATE : ... ENDCLASS +// TYPE and CLASS are top-level-only; a nested declaration is still consumed // in full so its body cannot be replayed as statements. std::unique_ptr Parser::ParseTypeDecl() { bool AtTopLevel = BlockDepth <= 1; + bool IsClass = CurTok == tok_class; + const char *Keyword = IsClass ? "CLASS" : "TYPE"; int Line = Lex.getLine(); - getNextToken(); // eat TYPE + getNextToken(); // eat TYPE or CLASS if (CurTok != tok_identifier) { - fprintf(stderr, "error: Expected type name after TYPE at line %d\n", Lex.getLine()); + fprintf(stderr, "error: Expected type name after %s at line %d\n", Keyword, Lex.getLine()); + if (IsClass) ParseRecordBody("", Line, tok_endclass); return nullptr; } std::string Name = Lex.IdentifierStr; @@ -27,7 +31,7 @@ std::unique_ptr Parser::ParseTypeDecl() { std::unique_ptr Decl; - if (CurTok == tok_eq) { + if (!IsClass && CurTok == tok_eq) { getNextToken(); if (CurTok == '(') { @@ -66,13 +70,16 @@ std::unique_ptr Parser::ParseTypeDecl() { return nullptr; } } else { - auto Fields = ParseRecordBody(Name, Line, tok_endtype); + auto Fields = ParseRecordBody(Name, Line, IsClass ? tok_endclass : tok_endtype); if (!Fields) return nullptr; - Decl = std::make_unique(Name, std::move(*Fields)); + if (IsClass) + Decl = std::make_unique(Name, std::move(*Fields)); + else + Decl = std::make_unique(Name, std::move(*Fields)); } if (!AtTopLevel) { - fprintf(stderr, "error: TYPE declarations are only allowed at top level at line %d\n", Line); + fprintf(stderr, "error: %s declarations are only allowed at top level at line %d\n", Keyword, Line); return nullptr; } return Decl; @@ -81,23 +88,43 @@ std::unique_ptr Parser::ParseTypeDecl() { // Shared record body and field parsing. Fields are data, never statements. std::optional> Parser::ParseRecordBody( const std::string &Name, int Line, int EndToken) { + 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_declare; + }; std::vector Fields; bool HadFieldError = false; - while (CurTok == tok_declare) { - if (!ParseRecordField(Fields, true)) { - HadFieldError = true; - while (CurTok != tok_declare && CurTok != EndToken && CurTok != tok_eof) + while (CurTok != EndToken && CurTok != tok_eof) { + if (AtField()) { + // Record fields are always public; class fields have no default. + bool IsPublic = !IsClass || CurTok == tok_public; + if (ParseRecordField(Fields, IsPublic)) continue; + } else { + fprintf(stderr, "error: Expected %s or %s in %s %s at line %d\n", + IsClass ? "PUBLIC or PRIVATE" : "DECLARE", EndKeyword, + Keyword, Name.c_str(), Lex.getLine()); + } + HadFieldError = true; + // Consume invalid bodies rather than replaying their contents as + // statements. Nested type declarations are consumed in full too. + while (!AtField() && CurTok != EndToken && CurTok != tok_eof) { + if (CurTok == tok_class || CurTok == tok_type) + ParseTypeDecl(); + else getNextToken(); } } if (CurTok != EndToken) { - fprintf(stderr, "error: Expected DECLARE or ENDTYPE in TYPE %s at line %d\n", - Name.c_str(), Lex.getLine()); + fprintf(stderr, "error: Expected %s in %s %s at line %d\n", + EndKeyword, Keyword, Name.c_str(), Lex.getLine()); return std::nullopt; } getNextToken(); // eat body terminator - if (Fields.empty()) { - fprintf(stderr, "error: Record TYPE %s has no fields at line %d\n", Name.c_str(), Line); + if (Fields.empty() && !HadFieldError) { + fprintf(stderr, "error: %s %s has no fields at line %d\n", Keyword, Name.c_str(), Line); return std::nullopt; } if (HadFieldError) return std::nullopt; @@ -106,9 +133,11 @@ std::optional> Parser::ParseRecordBody( // : . Array fields are deferred. bool Parser::ParseRecordField(std::vector &Out, bool IsPublic) { + const char *Introducer = CurTok == tok_declare ? "DECLARE" + : CurTok == tok_public ? "PUBLIC" : "PRIVATE"; getNextToken(); // eat field introducer if (CurTok != tok_identifier) { - fprintf(stderr, "error: Expected field name after DECLARE at line %d\n", Lex.getLine()); + fprintf(stderr, "error: Expected field name after %s at line %d\n", Introducer, Lex.getLine()); return false; } std::string FieldName = Lex.IdentifierStr; -- cgit v1.3.1-4-g156e