aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-22 13:27:15 +0000
committer_ov4 <ov4@ov4.org>2026-10-22 13:27:15 +0000
commit4a70e44f06e637762eec0fd875eb94b84c3f4037 (patch)
treeb2771f76e3da335276a8c3e96ce70c2cfe99e088
parenteaad9c34403f05372c1544d7ef522c847dfea0ba (diff)
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 <noreply@anthropic.com>
-rw-r--r--include/cps/TypeSystem.h7
-rw-r--r--lib/CodeGen/FunctionGen.cc27
-rw-r--r--lib/CodeGen/TypeSystem.cc39
-rw-r--r--tests/i/extern-c-struct/case.json5
-rw-r--r--tests/i/extern-c-struct/expected.txt3
-rw-r--r--tests/i/extern-c-struct/lib.c11
-rw-r--r--tests/i/extern-c-struct/notes.md7
-rw-r--r--tests/i/extern-c-struct/program.cps22
-rw-r--r--tests/iii/extern-struct-rejected/case.json12
-rw-r--r--tests/iii/extern-struct-rejected/notes.md5
-rw-r--r--tests/iii/extern-struct-rejected/program.cps14
11 files changed, 150 insertions, 2 deletions
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 <algorithm>
#include <utility>
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<const TypeInfo *> &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<const TypeInfo *> 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