From 522742e9df7fc5ac596ce4aa53d41b31e4f6a030 Mon Sep 17 00:00:00 2001 From: _ov4 Date: Fri, 16 Oct 2026 09:46:18 +0000 Subject: refactor(codegen): separate type checks from designator access Co-Authored-By: Claude Fable 5.1 --- AGENTS/ARCH.md | 2 +- AGENTS/SUBSYSTEMS.md | 4 +- CMakeLists.txt | 1 + include/cps/CodeGen.h | 4 +- lib/CodeGen/Designator.cc | 190 +------------------------------------------ lib/CodeGen/TypeCheckGen.cc | 191 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 199 insertions(+), 193 deletions(-) create mode 100644 lib/CodeGen/TypeCheckGen.cc diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md index a8813e9..663ed40 100644 --- a/AGENTS/ARCH.md +++ b/AGENTS/ARCH.md @@ -268,7 +268,7 @@ do not modify the real release for local verification. | `lib/CodeGen/CodeGen.cc` | Initialization, symbols, call arguments and main() compilation | | `lib/CodeGen/ExpressionGen.cc`, `StatementGen.cc`, `ControlFlowGen.cc` | Expression/statement dispatch and control-flow emission | | `lib/CodeGen/ValueTypeGen.cc`, `OutputGen.cc`, `Builtins.h` | Expression typing, coercion/declarations, scalar output and the shared builtin table | -| `lib/CodeGen/Designator.cc` | CodeGen methods for the TYPE system: type pre-pass, `emitLValue`/`loadFromLValue`, `emitCoercedExpr` gate, user-kind binary-op interception | +| `lib/CodeGen/Designator.cc`, `TypeCheckGen.cc` | TYPE pre-pass and lvalue access; type admission and user-kind binary operators | | `lib/CodeGen/CTypeGen.cc` | CodeGen methods for native C scalar conversions, casts, arithmetic and input/output | | `lib/CodeGen/NativeTypeSystem.cc` | Builtin registration, native C type imports/aliases and scalar promotion | | `lib/CodeGen/FileHandler.cc`, `FileRuntimeIO.cc` | File runtime setup/lookup/open and call entry points; read/write/close/EOF helper bodies | diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md index d1ae721..f693b1c 100644 --- a/AGENTS/SUBSYSTEMS.md +++ b/AGENTS/SUBSYSTEMS.md @@ -38,7 +38,9 @@ compile error (no extern is fabricated). Body emission is inverted: CodeGen inje `StmtEmitter` callback, and must restore its own insert point (and the exchanged ArrayTable) afterwards. -**User types** (`TypeSystem` + `Designator.cc`): TYPE declarations are top-level only +**User types** (`TypeSystem` + `Designator.cc` + `TypeCheckGen.cc`): +`Designator.cc` owns the type pre-pass and lvalue access; `TypeCheckGen.cc` owns +type admission and user-kind binary operators. TYPE declarations are top-level only and registered by `CodeGen::runTypePrePass` in two passes — `declareUserType` reserves every name (records become opaque named structs, so pointer pointees may forward-reference: `TYPE NodePtr = ^Node` before `TYPE Node`), then diff --git a/CMakeLists.txt b/CMakeLists.txt index 918fac1..cd63165 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ add_library(CPSCodeGen STATIC lib/CodeGen/ValueTypeGen.cc lib/CodeGen/OutputGen.cc lib/CodeGen/Designator.cc + lib/CodeGen/TypeCheckGen.cc lib/CodeGen/CTypeGen.cc lib/CodeGen/ArrayHandler.cc lib/CodeGen/ArrayParameters.cc diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h index 4363126..72b96c6 100644 --- a/include/cps/CodeGen.h +++ b/include/cps/CodeGen.h @@ -74,7 +74,7 @@ class CodeGen { void emitCOutput(llvm::Value *Val, const TypeInfo *Info, bool AppendNewline); void emitCInput(const SymbolInfo *Symbol, const TypeInfo *Info); - // TYPE-system helpers (Designator.cc). + // TYPE-system registration (Designator.cc) and operators (TypeCheckGen.cc). void runTypePrePass(const std::vector> &Statements); llvm::Value *emitUserKindBinaryOp(BinaryExprAST *Bin, const TypeInfo *LT, const TypeInfo *RT); @@ -120,7 +120,7 @@ public: // Name-level admission gate for Enum/Record/Pointer plus the existing // representation coercion for builtin targets. The ONLY door through // which assignments, BYVAL args, RETURN values and file/builtin operands - // may produce a typed value. (Designator.cc) + // may produce a typed value. (TypeCheckGen.cc) llvm::Value *emitCoercedExpr(ExprAST *E, const TypeInfo *TargetInfo); const TypeInfo *resolveType(const std::string &TypeName) const; diff --git a/lib/CodeGen/Designator.cc b/lib/CodeGen/Designator.cc index 019c19e..6c00524 100644 --- a/lib/CodeGen/Designator.cc +++ b/lib/CodeGen/Designator.cc @@ -1,37 +1,11 @@ -// Type-name-level emission helpers for the user-defined TYPE system: -// the type pre-pass, the admission gate for assignments/arguments, and the -// binary-operator interception for Enum/Record/Pointer operands. -// These are CodeGen methods; the file is split out to keep the hub -// dispatch-only. +// User-defined type registration and addressable designator access. #include "cps/CodeGen.h" #include "cps/ArrayHandler.h" #include "cps/TypeAST.h" -#include "cps/Lexer.h" using namespace llvm; using namespace cps; -namespace { - -// Operator spelling for diagnostics (raw ASCII or a keyword token). -std::string opToText(int Op) { - switch (Op) { - case tok_eq: return "="; - case tok_ne: return "<>"; - case tok_le: return "<="; - case tok_ge: return ">="; - case tok_and: return "AND"; - case tok_or: return "OR"; - case tok_div: return "DIV"; - case tok_mod: return "MOD"; - default: - if (Op >= 0) return std::string(1, static_cast(Op)); - return "?"; - } -} - -} // namespace - // Registers every top-level TYPE declaration before prototypes are emitted. // Two passes so a pointer TYPE may reference a type declared later // (TYPE NodePtr = ^Node before TYPE Node — required for linked structures); @@ -201,165 +175,3 @@ Value *CodeGen::loadFromLValue(const LValueInfo &LV) { } return Val; } - -// The single name-level admission gate. Targets of a user kind demand exact -// type-name equality (their representations are indistinguishable at the -// LLVM level); builtin targets additionally reject user-kind sources, then -// keep the existing representation coercions unchanged. -Value *CodeGen::emitCoercedExpr(ExprAST *E, const TypeInfo *Target) { - if (!E || !Target) return nullptr; - - // A whole-array reference evaluates to its raw data pointer, and - // getExprTypeInfo reports the ELEMENT type for it — both would slip - // through the name check and smash the pointer into the target. - if (isWholeArrayVar(E)) { - reportError("A whole array cannot be used as a value of type %s; index it", - Target->Name.c_str()); - return nullptr; - } - - const TypeInfo *Src = getExprTypeInfo(E); - - if (Target->isC() || (Src && Src->isC())) { - if (auto *Addr = dynamic_cast(E)) { - const PointerPayload *PP = Target->asPointer(); - LValueInfo LV; - if (PP && emitLValue(Addr->getTarget(), LV)) { - if (Types->sameType(LV.TypeName, PP->PointeeTypeName)) return LV.Addr; - } - reportError("Address does not match C pointer type %s", Target->Name.c_str()); - return nullptr; - } - if (Src && Src->sameType(Target) && !Target->isVoid()) return emitExpr(E); - if (Src && Src->Kind == TypeKind::CScalar && Target->Kind == TypeKind::CScalar && - !Src->isVoid() && !Target->isVoid()) - return convertNumericValue(emitExpr(E), Src, Target); - reportError("Cannot implicitly convert %s to %s; use an explicit cast", - Src ? Src->Name.c_str() : "expression", Target->Name.c_str()); - return nullptr; - } - - if (Target->isUserKind()) { - // ^x into a pointer target is structural: the pointee type name must - // match the pointer type's declared pointee exactly (strict — the - // guide's own ^INTEGER-at-a-Season example is rejected by design). - if (auto *Addr = dynamic_cast(E)) { - const PointerPayload *PP = Target->asPointer(); - if (!PP) { - reportError("'^' (address-of) can only be used where a pointer type is expected (got %s)", - Target->Name.c_str()); - return nullptr; - } - LValueInfo LV; - if (!emitLValue(Addr->getTarget(), LV)) return nullptr; - if (!Types->sameType(LV.TypeName, PP->PointeeTypeName)) { - reportError("%s is a pointer to %s and cannot point at a value of type %s", - Target->Name.c_str(), PP->PointeeTypeName.c_str(), LV.TypeName.c_str()); - return nullptr; - } - return LV.Addr; - } - if (!Src) { - // Emit anyway: an inadmissible sub-expression (e.g. enum * 2) - // then reports its own, more specific diagnostic. - if (emitExpr(E)) { - reportError("Cannot determine the type of the value used where %s is expected", - Target->Name.c_str()); - } - return nullptr; - } - if (Src->Name != Target->Name) { - reportError("Cannot use a value of type %s where %s is expected", - Src->Name.c_str(), Target->Name.c_str()); - return nullptr; - } - return emitExpr(E); // same nominal type: no conversion needed - } - - // ^x is a raw address: admissible ONLY where a pointer type is expected. - // Without this check the ptr would coerce into STRING/BOOLEAN silently. - if (dynamic_cast(E)) { - reportError("'^' (address-of) can only be used where a pointer type is expected (got %s)", - Target->Name.c_str()); - return nullptr; - } - - if (Src && Src->isUserKind()) { - reportError("Cannot use a value of type %s where %s is expected", - Src->Name.c_str(), Target->Name.c_str()); - return nullptr; - } - return coerceValueToType(emitExpr(E), Target); -} - -// Binary operators with at least one Enum/Record/Pointer operand. Decides -// admissibility by type name and emits directly; ArithmeticHandler stays -// ignorant of user kinds. -Value *CodeGen::emitUserKindBinaryOp(BinaryExprAST *Bin, const TypeInfo *LT, const TypeInfo *RT) { - int Op = Bin->getOp(); - const char *LName = LT ? LT->Name.c_str() : "?"; - const char *RName = RT ? RT->Name.c_str() : "?"; - - if ((LT && LT->isRecord()) || (RT && RT->isRecord())) { - reportError("Operator %s cannot be applied to a record value (type %s)", - opToText(Op).c_str(), (LT && LT->isRecord()) ? LName : RName); - return nullptr; - } - - if ((LT && LT->isPointer()) || (RT && RT->isPointer())) { - bool SameName = LT && RT && LT->isPointer() && RT->isPointer() && LT->Name == RT->Name; - if (SameName && (Op == tok_eq || Op == tok_ne)) { - Value *L = emitExpr(Bin->getLHS()); - Value *R = emitExpr(Bin->getRHS()); - if (!L || !R) return nullptr; - return Op == tok_eq ? Builder->CreateICmpEQ(L, R, "ptr_eq") - : Builder->CreateICmpNE(L, R, "ptr_ne"); - } - reportError("Pointer values support only = and <> between the same pointer type (got %s %s %s)", - LName, opToText(Op).c_str(), RName); - return nullptr; - } - - // At least one enum operand from here on. - bool BothSameEnum = LT && RT && LT->isEnum() && RT->isEnum() && LT->Name == RT->Name; - - switch (Op) { - case tok_eq: case tok_ne: case '<': case '>': case tok_le: case tok_ge: { - if (!BothSameEnum) break; - Value *L = emitExpr(Bin->getLHS()); - Value *R = emitExpr(Bin->getRHS()); - if (!L || !R) return nullptr; - switch (Op) { - case tok_eq: return Builder->CreateICmpEQ(L, R, "enum_eq"); - case tok_ne: return Builder->CreateICmpNE(L, R, "enum_ne"); - case '<': return Builder->CreateICmpSLT(L, R, "enum_lt"); - case '>': return Builder->CreateICmpSGT(L, R, "enum_gt"); - case tok_le: return Builder->CreateICmpSLE(L, R, "enum_le"); - case tok_ge: return Builder->CreateICmpSGE(L, R, "enum_ge"); - } - return nullptr; - } - - case '+': case '-': { - // The one legal arithmetic shape (guide §4.2): enum on the left, - // INTEGER on the right, result checked against the enum's range. - if (!(LT && LT->isEnum() && RT && RT->Kind == TypeKind::Integer)) break; - Value *L = emitExpr(Bin->getLHS()); - Value *R = emitExpr(Bin->getRHS()); - if (!L || !R) return nullptr; - Value *Res = Op == '+' ? Builder->CreateAdd(L, R, "enum_step") - : Builder->CreateSub(L, R, "enum_step"); - RuntimeChecker->emitEnumRangeCheck(Res, LT->asEnum()->ValueNames.size(), - LT->Name, Bin->getLine()); - return Res; - } - - default: - break; - } - - reportError("Operator %s cannot be applied to %s and %s " - "(enums allow comparison with the same enum type, and enum + or - INTEGER)", - opToText(Op).c_str(), LName, RName); - return nullptr; -} diff --git a/lib/CodeGen/TypeCheckGen.cc b/lib/CodeGen/TypeCheckGen.cc new file mode 100644 index 0000000..a86fdb1 --- /dev/null +++ b/lib/CodeGen/TypeCheckGen.cc @@ -0,0 +1,191 @@ +// Type admission and binary operators for user-defined types. +#include "cps/CodeGen.h" +#include "cps/TypeAST.h" +#include "cps/Lexer.h" + +using namespace llvm; +using namespace cps; + +namespace { + +// Operator spelling for diagnostics (raw ASCII or a keyword token). +std::string opToText(int Op) { + switch (Op) { + case tok_eq: return "="; + case tok_ne: return "<>"; + case tok_le: return "<="; + case tok_ge: return ">="; + case tok_and: return "AND"; + case tok_or: return "OR"; + case tok_div: return "DIV"; + case tok_mod: return "MOD"; + default: + if (Op >= 0) return std::string(1, static_cast(Op)); + return "?"; + } +} + +} // namespace + + +// The single name-level admission gate. Targets of a user kind demand exact +// type-name equality (their representations are indistinguishable at the +// LLVM level); builtin targets additionally reject user-kind sources, then +// keep the existing representation coercions unchanged. +Value *CodeGen::emitCoercedExpr(ExprAST *E, const TypeInfo *Target) { + if (!E || !Target) return nullptr; + + // A whole-array reference evaluates to its raw data pointer, and + // getExprTypeInfo reports the ELEMENT type for it — both would slip + // through the name check and smash the pointer into the target. + if (isWholeArrayVar(E)) { + reportError("A whole array cannot be used as a value of type %s; index it", + Target->Name.c_str()); + return nullptr; + } + + const TypeInfo *Src = getExprTypeInfo(E); + + if (Target->isC() || (Src && Src->isC())) { + if (auto *Addr = dynamic_cast(E)) { + const PointerPayload *PP = Target->asPointer(); + LValueInfo LV; + if (PP && emitLValue(Addr->getTarget(), LV)) { + if (Types->sameType(LV.TypeName, PP->PointeeTypeName)) return LV.Addr; + } + reportError("Address does not match C pointer type %s", Target->Name.c_str()); + return nullptr; + } + if (Src && Src->sameType(Target) && !Target->isVoid()) return emitExpr(E); + if (Src && Src->Kind == TypeKind::CScalar && Target->Kind == TypeKind::CScalar && + !Src->isVoid() && !Target->isVoid()) + return convertNumericValue(emitExpr(E), Src, Target); + reportError("Cannot implicitly convert %s to %s; use an explicit cast", + Src ? Src->Name.c_str() : "expression", Target->Name.c_str()); + return nullptr; + } + + if (Target->isUserKind()) { + // ^x into a pointer target is structural: the pointee type name must + // match the pointer type's declared pointee exactly (strict — the + // guide's own ^INTEGER-at-a-Season example is rejected by design). + if (auto *Addr = dynamic_cast(E)) { + const PointerPayload *PP = Target->asPointer(); + if (!PP) { + reportError("'^' (address-of) can only be used where a pointer type is expected (got %s)", + Target->Name.c_str()); + return nullptr; + } + LValueInfo LV; + if (!emitLValue(Addr->getTarget(), LV)) return nullptr; + if (!Types->sameType(LV.TypeName, PP->PointeeTypeName)) { + reportError("%s is a pointer to %s and cannot point at a value of type %s", + Target->Name.c_str(), PP->PointeeTypeName.c_str(), LV.TypeName.c_str()); + return nullptr; + } + return LV.Addr; + } + if (!Src) { + // Emit anyway: an inadmissible sub-expression (e.g. enum * 2) + // then reports its own, more specific diagnostic. + if (emitExpr(E)) { + reportError("Cannot determine the type of the value used where %s is expected", + Target->Name.c_str()); + } + return nullptr; + } + if (Src->Name != Target->Name) { + reportError("Cannot use a value of type %s where %s is expected", + Src->Name.c_str(), Target->Name.c_str()); + return nullptr; + } + return emitExpr(E); // same nominal type: no conversion needed + } + + // ^x is a raw address: admissible ONLY where a pointer type is expected. + // Without this check the ptr would coerce into STRING/BOOLEAN silently. + if (dynamic_cast(E)) { + reportError("'^' (address-of) can only be used where a pointer type is expected (got %s)", + Target->Name.c_str()); + return nullptr; + } + + if (Src && Src->isUserKind()) { + reportError("Cannot use a value of type %s where %s is expected", + Src->Name.c_str(), Target->Name.c_str()); + return nullptr; + } + return coerceValueToType(emitExpr(E), Target); +} + +// Binary operators with at least one Enum/Record/Pointer operand. Decides +// admissibility by type name and emits directly; ArithmeticHandler stays +// ignorant of user kinds. +Value *CodeGen::emitUserKindBinaryOp(BinaryExprAST *Bin, const TypeInfo *LT, const TypeInfo *RT) { + int Op = Bin->getOp(); + const char *LName = LT ? LT->Name.c_str() : "?"; + const char *RName = RT ? RT->Name.c_str() : "?"; + + if ((LT && LT->isRecord()) || (RT && RT->isRecord())) { + reportError("Operator %s cannot be applied to a record value (type %s)", + opToText(Op).c_str(), (LT && LT->isRecord()) ? LName : RName); + return nullptr; + } + + if ((LT && LT->isPointer()) || (RT && RT->isPointer())) { + bool SameName = LT && RT && LT->isPointer() && RT->isPointer() && LT->Name == RT->Name; + if (SameName && (Op == tok_eq || Op == tok_ne)) { + Value *L = emitExpr(Bin->getLHS()); + Value *R = emitExpr(Bin->getRHS()); + if (!L || !R) return nullptr; + return Op == tok_eq ? Builder->CreateICmpEQ(L, R, "ptr_eq") + : Builder->CreateICmpNE(L, R, "ptr_ne"); + } + reportError("Pointer values support only = and <> between the same pointer type (got %s %s %s)", + LName, opToText(Op).c_str(), RName); + return nullptr; + } + + // At least one enum operand from here on. + bool BothSameEnum = LT && RT && LT->isEnum() && RT->isEnum() && LT->Name == RT->Name; + + switch (Op) { + case tok_eq: case tok_ne: case '<': case '>': case tok_le: case tok_ge: { + if (!BothSameEnum) break; + Value *L = emitExpr(Bin->getLHS()); + Value *R = emitExpr(Bin->getRHS()); + if (!L || !R) return nullptr; + switch (Op) { + case tok_eq: return Builder->CreateICmpEQ(L, R, "enum_eq"); + case tok_ne: return Builder->CreateICmpNE(L, R, "enum_ne"); + case '<': return Builder->CreateICmpSLT(L, R, "enum_lt"); + case '>': return Builder->CreateICmpSGT(L, R, "enum_gt"); + case tok_le: return Builder->CreateICmpSLE(L, R, "enum_le"); + case tok_ge: return Builder->CreateICmpSGE(L, R, "enum_ge"); + } + return nullptr; + } + + case '+': case '-': { + // The one legal arithmetic shape (guide §4.2): enum on the left, + // INTEGER on the right, result checked against the enum's range. + if (!(LT && LT->isEnum() && RT && RT->Kind == TypeKind::Integer)) break; + Value *L = emitExpr(Bin->getLHS()); + Value *R = emitExpr(Bin->getRHS()); + if (!L || !R) return nullptr; + Value *Res = Op == '+' ? Builder->CreateAdd(L, R, "enum_step") + : Builder->CreateSub(L, R, "enum_step"); + RuntimeChecker->emitEnumRangeCheck(Res, LT->asEnum()->ValueNames.size(), + LT->Name, Bin->getLine()); + return Res; + } + + default: + break; + } + + reportError("Operator %s cannot be applied to %s and %s " + "(enums allow comparison with the same enum type, and enum + or - INTEGER)", + opToText(Op).c_str(), LName, RName); + return nullptr; +} -- cgit v1.3.1-4-g156e