From 4a70e44f06e637762eec0fd875eb94b84c3f4037 Mon Sep 17 00:00:00 2001 From: _ov4 Date: Thu, 22 Oct 2026 13:27:15 +0000 Subject: feat(codegen): accept pointers to C-struct records in EXTERN signatures A TYPE record whose fields are all C types, C structs or pointer types to C structs now crosses the EXTERN "C" boundary through a pointer type. Records by value are rejected for now, and a pointer to a record with a non-C field names that field. Co-Authored-By: Claude Opus 5.5 --- include/cps/TypeSystem.h | 7 +++++ lib/CodeGen/FunctionGen.cc | 27 +++++++++++++++++-- lib/CodeGen/TypeSystem.cc | 39 ++++++++++++++++++++++++++++ tests/i/extern-c-struct/case.json | 5 ++++ tests/i/extern-c-struct/expected.txt | 3 +++ tests/i/extern-c-struct/lib.c | 11 ++++++++ tests/i/extern-c-struct/notes.md | 7 +++++ tests/i/extern-c-struct/program.cps | 22 ++++++++++++++++ tests/iii/extern-struct-rejected/case.json | 12 +++++++++ tests/iii/extern-struct-rejected/notes.md | 5 ++++ tests/iii/extern-struct-rejected/program.cps | 14 ++++++++++ 11 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 tests/i/extern-c-struct/case.json create mode 100644 tests/i/extern-c-struct/expected.txt create mode 100644 tests/i/extern-c-struct/lib.c create mode 100644 tests/i/extern-c-struct/notes.md create mode 100644 tests/i/extern-c-struct/program.cps create mode 100644 tests/iii/extern-struct-rejected/case.json create mode 100644 tests/iii/extern-struct-rejected/notes.md create mode 100644 tests/iii/extern-struct-rejected/program.cps diff --git a/include/cps/TypeSystem.h b/include/cps/TypeSystem.h index 5f9af63..1c1e422 100644 --- a/include/cps/TypeSystem.h +++ b/include/cps/TypeSystem.h @@ -154,6 +154,13 @@ public: std::string &Err); bool sameType(const std::string &Left, const std::string &Right) const; const TypeInfo *promoteCScalar(const TypeInfo *Info) const; + // A TYPE record (not a CLASS) whose fields are C scalars, C pointers, + // C structs or pointer types to C structs. Otherwise Why, if given, + // names the first offending field and its type. + bool isCStruct(const TypeInfo *Info, std::string *Why = nullptr) const; + // Crosses the EXTERN "C" boundary: a non-void C type, a C struct or a + // pointer type to one. + bool isExternType(const TypeInfo *Info) const; const TypeInfo *commonCScalar(const TypeInfo *Left, const TypeInfo *Right) const; const EnumConstant *lookupEnumConstant(const std::string &Name) const; diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index 04f6a84..1cbf19f 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -298,6 +298,26 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { return Info->CSigned ? Attribute::SExt : Attribute::ZExt; }; + // Records cross the C boundary only as C structs, for now through a + // pointer type. What names the parameter or return type in diagnostics. + auto CheckRecord = [&](const TypeInfo *Info, const std::string &What) { + const TypeInfo *Record = Info->Kind == TypeKind::Pointer + ? Types.resolve(Info->Pointer.PointeeTypeName) : Info; + if (!Record || !Record->isRecord() || Record->IsClass) return true; + std::string Why; + if (!Types.isCStruct(Record, &Why)) { + reportError("%s uses record %s, which is not a C struct: %s", What.c_str(), + Record->Name.c_str(), Why.c_str()); + return false; + } + if (Record == Info) { + reportError("%s cannot be record %s by value; use a pointer type to it", What.c_str(), + Record->Name.c_str()); + return false; + } + return true; + }; + FuncSig Sig; Sig.OwnerUnit = CurrentUnit; Sig.Name = Name; @@ -317,7 +337,9 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { reportError("Unknown type %s", P.TypeName.c_str()); return false; } - if (!Info->isC() || Info->isVoid()) { + if (!CheckRecord(Info, "EXTERN parameter " + std::to_string(I + 1) + " of " + Name)) + return false; + if (!Types.isExternType(Info)) { reportError("EXTERN parameter %zu of %s must be a non-void C type (got %s)", I + 1, Name.c_str(), P.TypeName.c_str()); return false; @@ -337,7 +359,8 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { reportError("Unknown type %s", Proto->getReturnType().c_str()); return false; } - if (!Info->isC() || Info->isVoid()) { + if (!CheckRecord(Info, "The return type of EXTERN_FUNCTION " + Name)) return false; + if (!Types.isExternType(Info)) { reportError("EXTERN_FUNCTION %s must return a non-void C type (got %s)", Name.c_str(), Proto->getReturnType().c_str()); return false; diff --git a/lib/CodeGen/TypeSystem.cc b/lib/CodeGen/TypeSystem.cc index a84200b..c13d81e 100644 --- a/lib/CodeGen/TypeSystem.cc +++ b/lib/CodeGen/TypeSystem.cc @@ -1,6 +1,7 @@ #include "cps/TypeSystem.h" #include "cps/TypeAST.h" #include "llvm/IR/Constants.h" +#include #include using namespace llvm; @@ -262,6 +263,44 @@ bool TypeSystem::definePointer(const std::string &Name, return true; } +namespace { + +// Seen holds the records already being checked, so self-referential +// pointer types (linked lists) end the recursion. +bool checkCStruct(const TypeSystem &Types, const TypeInfo *Info, std::string *Why, + std::vector &Seen) { + if (!Info || !Info->isRecord() || Info->IsClass) return false; + if (std::find(Seen.begin(), Seen.end(), Info) != Seen.end()) return true; + Seen.push_back(Info); + for (const auto &[FieldName, Field] : Info->Record.Fields) { + const TypeInfo *FieldInfo = Types.resolve(Field.TypeName); + if (FieldInfo && FieldInfo->isC()) continue; + const TypeInfo *Inner = FieldInfo && FieldInfo->Kind == TypeKind::Pointer + ? Types.resolve(FieldInfo->Pointer.PointeeTypeName) : FieldInfo; + if (Inner && Inner->isRecord() && !Inner->IsClass) { + if (checkCStruct(Types, Inner, Why, Seen)) continue; + return false; + } + if (Why) *Why = "field " + FieldName + " of " + Info->Name + " has type " + Field.TypeName; + return false; + } + return true; +} + +} // namespace + +bool TypeSystem::isCStruct(const TypeInfo *Info, std::string *Why) const { + std::vector Seen; + return checkCStruct(*this, Info, Why, Seen); +} + +bool TypeSystem::isExternType(const TypeInfo *Info) const { + if (!Info) return false; + if (Info->isC()) return !Info->isVoid(); + if (Info->Kind == TypeKind::Pointer) return isCStruct(resolve(Info->Pointer.PointeeTypeName)); + return isCStruct(Info); +} + const EnumConstant *TypeSystem::lookupEnumConstant(const std::string &Name) const { auto It = EnumConstants.find(Name); return It == EnumConstants.end() || (Visible && !Visible(It->second.TypeName)) diff --git a/tests/i/extern-c-struct/case.json b/tests/i/extern-c-struct/case.json new file mode 100644 index 0000000..1ce5970 --- /dev/null +++ b/tests/i/extern-c-struct/case.json @@ -0,0 +1,5 @@ +{ + "objects": [ + {"source": "lib.c", "output": "lib.o"} + ] +} diff --git a/tests/i/extern-c-struct/expected.txt b/tests/i/extern-c-struct/expected.txt new file mode 100644 index 0000000..62e50ed --- /dev/null +++ b/tests/i/extern-c-struct/expected.txt @@ -0,0 +1,3 @@ +S 4.500000 5 +G 0.500000 7 +9 diff --git a/tests/i/extern-c-struct/lib.c b/tests/i/extern-c-struct/lib.c new file mode 100644 index 0000000..39380ec --- /dev/null +++ b/tests/i/extern-c-struct/lib.c @@ -0,0 +1,11 @@ +struct Sample { + char tag; + double value; + int count; +}; + +static struct Sample shared = {'G', 0.5, 7}; + +void scale_sample(struct Sample *s, int k) { s->value *= k; } +struct Sample *shared_sample(void) { return &shared; } +int shared_count(void) { return shared.count; } diff --git a/tests/i/extern-c-struct/notes.md b/tests/i/extern-c-struct/notes.md new file mode 100644 index 0000000..1a0d215 --- /dev/null +++ b/tests/i/extern-c-struct/notes.md @@ -0,0 +1,7 @@ +# extern-c-struct + +Human review: pending. Expected output was independently traced by AI, not captured from cpsc. + +Basis: compiler extension (EXTERN "C", INCLUDE_OBJ of a user object); a record whose fields are all C types has the layout of the matching C struct. + +Trace: the harness compiles `lib.c` with the host C compiler. Sample matches `struct Sample {char; double; int}`, including the padding after the char. S is 'S', 1.5, 5. scale_sample(^S, 3) multiplies the value through the pointer to 4.5 and leaves tag and count alone, so the first line is `S 4.500000 5`. shared_sample returns the address of the static struct in lib.c, initialised to 'G', 0.5, 7, giving `G 0.500000 7`. P^.Count <- 9 writes through that pointer into the C object, so shared_count() returns 9. diff --git a/tests/i/extern-c-struct/program.cps b/tests/i/extern-c-struct/program.cps new file mode 100644 index 0000000..1954d29 --- /dev/null +++ b/tests/i/extern-c-struct/program.cps @@ -0,0 +1,22 @@ +// Records whose fields are C types pass to C functions through pointer types. +INCLUDE_OBJ "lib.o" +TYPE Sample + DECLARE Tag : C_CHAR + DECLARE Value : C_DOUBLE + DECLARE Count : C_INT +ENDTYPE +TYPE PSample = ^Sample +EXTERN "C" EXTERN_PROCEDURE scale_sample(PSample, C_INT) +EXTERN "C" EXTERN_FUNCTION shared_sample() RETURNS PSample +EXTERN "C" EXTERN_FUNCTION shared_count() RETURNS C_INT +DECLARE S : Sample +DECLARE P : PSample +S.Tag <- (C_CHAR)'S' +S.Value <- (C_DOUBLE)1.5 +S.Count <- (C_INT)5 +CALL scale_sample(^S, (C_INT)3) +OUTPUT S.Tag, " ", S.Value, " ", S.Count +P <- shared_sample() +OUTPUT P^.Tag, " ", P^.Value, " ", P^.Count +P^.Count <- (C_INT)9 +OUTPUT shared_count() diff --git a/tests/iii/extern-struct-rejected/case.json b/tests/iii/extern-struct-rejected/case.json new file mode 100644 index 0000000..6a4483f --- /dev/null +++ b/tests/iii/extern-struct-rejected/case.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "reason": "EXTERN\\ parameter\\ 1\\ of\\ puts\\ uses\\ record\\ Person,\\ which\\ is\\ not\\ a\\ C\\ struct:\\ field\\ Name\\ of\\ Person\\ has\\ type\\ STRING", + "line": 12 + }, + { + "reason": "EXTERN\\ parameter\\ 1\\ of\\ abs\\ cannot\\ be\\ record\\ Point\\ by\\ value;\\ use\\ a\\ pointer\\ type\\ to\\ it", + "line": 13 + } + ] +} diff --git a/tests/iii/extern-struct-rejected/notes.md b/tests/iii/extern-struct-rejected/notes.md new file mode 100644 index 0000000..f33ef66 --- /dev/null +++ b/tests/iii/extern-struct-rejected/notes.md @@ -0,0 +1,5 @@ +# extern-struct-rejected + +Compiler extension: an EXTERN signature accepts a record only as a C struct (every field a C type, a C struct or a pointer type to one) and, for now, only through a pointer type. Person has a STRING field, so a pointer to it is rejected and the field is named. Point is a C struct, but passing it by value is not supported yet. + +Require exit 1 and each cause on its EXTERN line. diff --git a/tests/iii/extern-struct-rejected/program.cps b/tests/iii/extern-struct-rejected/program.cps new file mode 100644 index 0000000..e7f38d1 --- /dev/null +++ b/tests/iii/extern-struct-rejected/program.cps @@ -0,0 +1,14 @@ +// Records cross the EXTERN boundary only as C structs through a pointer type. +INCLUDE_OBJ "__libc" +TYPE Person + DECLARE Name : STRING + DECLARE Age : C_INT +ENDTYPE +TYPE PPerson = ^Person +TYPE Point + DECLARE X : C_INT + DECLARE Y : C_INT +ENDTYPE +EXTERN "C" EXTERN_PROCEDURE puts(PPerson) +EXTERN "C" EXTERN_FUNCTION abs(Point) RETURNS C_INT +OUTPUT 1 -- cgit v1.3.1-4-g156e