aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-13 13:45:03 +0000
committer_ov4 <ov4@ov4.org>2026-10-13 13:45:03 +0000
commit05cdcb6a1cc3d4de3a0b3d12d4babcd6438b4754 (patch)
treed3e482d215d9d5feba8b40f9de15d6962f1e5bac
parentf5a2541ed3faf9a942b867e2d54577352153afce (diff)
Add record-derived classes with explicit field visibility
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
-rw-r--r--AGENTS/ARCH.md2
-rw-r--r--AGENTS/PITFALLS.md10
-rw-r--r--AGENTS/SUBSYSTEMS.md9
-rw-r--r--doc/cpsc.md24
-rw-r--r--include/cps/Lexer.h6
-rw-r--r--include/cps/TypeAST.h8
-rw-r--r--lib/CodeGen/CodeGen.cc1
-rw-r--r--lib/CodeGen/Designator.cc5
-rw-r--r--lib/Lexer/Lexer.cc4
-rw-r--r--lib/Parser/Parser.cc8
-rw-r--r--lib/Parser/ParserType.cc61
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<RecordFieldDecl> &getFields() const { return Fields; }
};
+// CLASS <name> 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<RecordFieldDecl> 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<StmtAST> 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<StmtAST> 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 <id> = SET OF <type> is recognised and rejected (RANDOM precedent).
-// TYPE is a top-level-only statement; a nested declaration is still consumed
+// CLASS <id> PUBLIC/PRIVATE <field> : <type> ... 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<StmtAST> 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("<unnamed>", Line, tok_endclass);
return nullptr;
}
std::string Name = Lex.IdentifierStr;
@@ -27,7 +31,7 @@ std::unique_ptr<StmtAST> Parser::ParseTypeDecl() {
std::unique_ptr<StmtAST> Decl;
- if (CurTok == tok_eq) {
+ if (!IsClass && CurTok == tok_eq) {
getNextToken();
if (CurTok == '(') {
@@ -66,13 +70,16 @@ std::unique_ptr<StmtAST> 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<RecordTypeDeclAST>(Name, std::move(*Fields));
+ if (IsClass)
+ Decl = std::make_unique<ClassTypeDeclAST>(Name, std::move(*Fields));
+ else
+ Decl = std::make_unique<RecordTypeDeclAST>(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<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) {
+ 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<RecordFieldDecl> 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<std::vector<RecordFieldDecl>> Parser::ParseRecordBody(
// <field introducer> <field> : <type>. Array fields are deferred.
bool Parser::ParseRecordField(std::vector<RecordFieldDecl> &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;