From 5eccd90b47b327917ad0e5858c79ca8fab27cd74 Mon Sep 17 00:00:00 2001 From: _ov4 Date: Thu, 22 Oct 2026 13:51:44 +0000 Subject: feat(codegen): pass C-struct records by value as variadic arguments A variadic EXTERN call is now arranged by clang per call, from the fixed parameter count and the promoted argument types. C-struct records pass unchanged in variadic positions, as C11 6.5.2.2p6-7 and 7.16.1.1p2 allow; a record that is not a C struct names the offending field. Co-Authored-By: Claude Opus 5.5 --- include/cps/CodeGen.h | 10 +++++--- include/cps/FunctionGen.h | 4 ++- lib/CodeGen/CallGen.cc | 35 +++++++++++++++++++------- lib/CodeGen/ExpressionGen.cc | 5 ++-- lib/CodeGen/FunctionGen.cc | 37 +++++++++++++++++++++------- tests/i/extern-c-struct-value/expected.txt | 2 ++ tests/i/extern-c-struct-value/lib.c | 24 ++++++++++++++++++ tests/i/extern-c-struct-value/notes.md | 4 +-- tests/i/extern-c-struct-value/program.cps | 9 ++++++- tests/iii/extern-struct-rejected/case.json | 4 +++ tests/iii/extern-struct-rejected/notes.md | 4 +-- tests/iii/extern-struct-rejected/program.cps | 4 ++- 12 files changed, 112 insertions(+), 30 deletions(-) diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h index 8f6a57b..b31cd3d 100644 --- a/include/cps/CodeGen.h +++ b/include/cps/CodeGen.h @@ -217,13 +217,17 @@ class CodeGen { llvm::Value *emitOperatorCall(const FuncSig &Signature, ExprAST *Left, ExprAST *Right = nullptr); llvm::Value *emitOperatorValues(const FuncSig &Signature, llvm::Value *Left, const TypeInfo *LeftType, llvm::Value *Right); - llvm::Value *emitVariadicArgument(ExprAST *Expr, const std::string &Callee, size_t Position); + llvm::Value *emitVariadicArgument(ExprAST *Expr, const std::string &Callee, size_t Position, + const TypeInfo *&Promoted); + // Variadic, if given, receives the promoted type of each variadic argument. bool marshalCallArgs(const FuncSig &Signature, const std::vector> &ArgExprs, int Line, - std::vector &Out); + std::vector &Out, + std::vector *Variadic = nullptr); bool marshalCallArgs(const FuncSig &Signature, const std::vector &ArgExprs, - int Line, std::vector &Out); + int Line, std::vector &Out, + std::vector *Variadic = nullptr); llvm::Value *symbolAddress(const SymbolInfo &Info); void emitAssignment(const LValueInfo &Target, ExprAST *Expr, const SymbolInfo *Symbol = nullptr); diff --git a/include/cps/FunctionGen.h b/include/cps/FunctionGen.h index 8b63d67..6c5c7c0 100644 --- a/include/cps/FunctionGen.h +++ b/include/cps/FunctionGen.h @@ -122,7 +122,9 @@ public: llvm::Function *emitFunctionDef(FunctionDefAST *FuncAST, const std::function &StmtEmitter, unsigned ScopeID); - llvm::Value *emitCall(const FuncSig &Sig, const std::vector &Args); + // Variadic holds the promoted types of an EXTERN call's variadic arguments. + llvm::Value *emitCall(const FuncSig &Sig, const std::vector &Args, + const std::vector &Variadic = {}); void emitReturn(llvm::Value *RetVal); }; diff --git a/lib/CodeGen/CallGen.cc b/lib/CodeGen/CallGen.cc index b25ca12..a5567b7 100644 --- a/lib/CodeGen/CallGen.cc +++ b/lib/CodeGen/CallGen.cc @@ -23,15 +23,17 @@ using namespace cps; bool CodeGen::marshalCallArgs(const FuncSig &Signature, const std::vector> &ArgExprs, int Line, - std::vector &Out) { + std::vector &Out, + std::vector *Variadic) { std::vector Args; for (const auto &Arg : ArgExprs) Args.push_back(Arg.get()); - return marshalCallArgs(Signature, Args, Line, Out); + return marshalCallArgs(Signature, Args, Line, Out, Variadic); } bool CodeGen::marshalCallArgs(const FuncSig &Signature, const std::vector &ArgExprs, - int Line, std::vector &Out) { + int Line, std::vector &Out, + std::vector *Variadic) { SaveAndRestore Location(DiagnosticLine, Line ? Line : DiagnosticLine); const FuncSig *Sig = &Signature; const std::string &Callee = Sig->Name; @@ -43,9 +45,11 @@ bool CodeGen::marshalCallArgs(const FuncSig &Signature, std::vector Transfers; for (size_t i = 0; i < ArgExprs.size(); ++i) { if (i >= Sig->Params.size()) { - Value *V = emitVariadicArgument(ArgExprs[i], Callee, i + 1); + const TypeInfo *Promoted = nullptr; + Value *V = emitVariadicArgument(ArgExprs[i], Callee, i + 1, Promoted); if (!V) return false; Out.push_back(V); + if (Variadic) Variadic->push_back(Promoted); continue; } const ParamSig &P = Sig->Params[i]; @@ -126,20 +130,33 @@ bool CodeGen::marshalCallArgs(const FuncSig &Signature, // C default argument promotions for the variadic part of an EXTERN call; only // C values are accepted, matching the explicit-cast rule for C/pseudocode. -Value *CodeGen::emitVariadicArgument(ExprAST *Expr, const std::string &Callee, size_t Position) { +// C structs and pointers to them pass unchanged (C11 6.5.2.2p6-7, 7.16.1.1p2). +Value *CodeGen::emitVariadicArgument(ExprAST *Expr, const std::string &Callee, size_t Position, + const TypeInfo *&Promoted) { const TypeInfo *Info = isWholeArrayVar(Expr) ? nullptr : getExprTypeInfo(Expr); - // Pointers to C structs pass unchanged; records by value are not supported yet. - if (!Info || Info->isRecord() || !Types->isExternType(Info)) { + const TypeInfo *Record = Info && Info->Kind == TypeKind::Pointer + ? Types->resolve(Info->Pointer.PointeeTypeName) : Info; + std::string Why; + if (Record && Record->isRecord() && !Record->IsClass && !Types->isCStruct(Record, &Why)) { + reportError("Variadic argument %zu to %s uses record %s, which is not a C struct: %s", + Position, Callee.c_str(), Record->Name.c_str(), Why.c_str()); + return nullptr; + } + if (!Info || !Types->isExternType(Info)) { reportError("Variadic argument %zu to %s must be a C value; cast it to a C type", Position, Callee.c_str()); return nullptr; } + Promoted = Info; Value *V = emitExpr(Expr); if (!V || Info->Kind != TypeKind::CScalar) return V; if (Info->isReal()) { const TypeInfo *Double = resolveType("C_DOUBLE"); - return Info->CRank < Double->CRank ? convertNumericValue(V, Info, Double) : V; + if (Info->CRank >= Double->CRank) return V; + Promoted = Double; + return convertNumericValue(V, Info, Double); } - return convertNumericValue(V, Info, Types->promoteCScalar(Info)); + Promoted = Types->promoteCScalar(Info); + return convertNumericValue(V, Info, Promoted); } // Resolve using static source types only; argument evaluation belongs to marshalling. diff --git a/lib/CodeGen/ExpressionGen.cc b/lib/CodeGen/ExpressionGen.cc index ece055a..d7a4ba7 100644 --- a/lib/CodeGen/ExpressionGen.cc +++ b/lib/CodeGen/ExpressionGen.cc @@ -231,9 +231,10 @@ Value *CodeGen::emitCall(const std::string &Callee, } std::vector Args; + std::vector Variadic; const FuncSig *Sig = resolveCall(Name, ArgExprs, Explicit); - if (!Sig || !marshalCallArgs(*Sig, ArgExprs, Line, Args)) return nullptr; - return FuncGen->emitCall(*Sig, Args); + if (!Sig || !marshalCallArgs(*Sig, ArgExprs, Line, Args, &Variadic)) return nullptr; + return FuncGen->emitCall(*Sig, Args, Variadic); } Value *CodeGen::emitAllocationSize(ExprAST *Expr) { diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index b79698e..6895945 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -470,21 +470,40 @@ llvm::Value *FunctionGen::emitCall(const std::string &Callee, const std::vector< return Exceptions.emitCall(CalleeF, Args, "calltmp"); } -llvm::Value *FunctionGen::emitCall(const FuncSig &Sig, const std::vector &Args) { +llvm::Value *FunctionGen::emitCall(const FuncSig &Sig, const std::vector &Args, + const std::vector &Variadic) { if (BeforeCall) BeforeCall(Sig); if (!Sig.IsExternal) return emitCall(Sig.LinkName, Args); FunctionCallee Callee(Sig.ExternType, Module.getNamedValue(Sig.LinkName)); - // Variadic arguments arrive promoted and pass as they are. - size_t Fixed = Sig.ExternABI.Fixed; + // A call with variadic arguments is arranged with their promoted types. + const CABISignature *ABISig = &Sig.ExternABI; + CABISignature CallABI; + if (!Variadic.empty()) { + std::vector ArgTypes; + for (const ParamSig &P : Sig.Params) ArgTypes.push_back(Types.resolve(P.TypeName)); + ArgTypes.insert(ArgTypes.end(), Variadic.begin(), Variadic.end()); + const TypeInfo *Ret = sameIdentifier(Sig.ReturnTypeName, "VOID") + ? nullptr : Types.resolve(Sig.ReturnTypeName); + std::string Err; + if (!ABI.arrange(Ret, ArgTypes, Sig.Params.size(), true, CallABI, Err)) { + reportError("Internal: cannot arrange the call to %s: %s", Sig.Name.c_str(), Err.c_str()); + return nullptr; + } + ABISig = &CallABI; + } + if (Args.size() != ABISig->Args.size()) { + reportError("Internal: the call to %s has %zu arguments but %zu are arranged", Sig.Name.c_str(), + Args.size(), ABISig->Args.size()); + return nullptr; + } Value *SRet = nullptr; - std::vector IRArgs = lowerCABIArguments( - Builder, Sig.ExternABI, ArrayRef(Args).take_front(Fixed), SRet); - IRArgs.insert(IRArgs.end(), Args.begin() + Fixed, Args.end()); + std::vector IRArgs = lowerCABIArguments(Builder, *ABISig, Args, SRet); Value *Call = Sig.ExternType->getReturnType()->isVoidTy() ? Exceptions.emitCall(Callee, IRArgs) : Exceptions.emitCall(Callee, IRArgs, "calltmp"); - cast(Call)->setAttributes(Sig.ExternAttrs); - cast(Call)->setCallingConv(Sig.ExternABI.CallingConv); - Value *Result = lowerCABIResult(Builder, Sig.ExternABI, Call, SRet); + cast(Call)->setAttributes(ABISig == &Sig.ExternABI + ? Sig.ExternAttrs : getCABIAttributes(Context, CallABI)); + cast(Call)->setCallingConv(ABISig->CallingConv); + Value *Result = lowerCABIResult(Builder, *ABISig, Call, SRet); return Result ? Result : Call; } diff --git a/tests/i/extern-c-struct-value/expected.txt b/tests/i/extern-c-struct-value/expected.txt index 19eef3c..f96c960 100644 --- a/tests/i/extern-c-struct-value/expected.txt +++ b/tests/i/extern-c-struct-value/expected.txt @@ -8,3 +8,5 @@ 1.500000 2.000000 Q 0.250000 9 0.500000 1.250000 2.000000 +7.750000 +14 diff --git a/tests/i/extern-c-struct-value/lib.c b/tests/i/extern-c-struct-value/lib.c index 94afc3f..d74a1d0 100644 --- a/tests/i/extern-c-struct-value/lib.c +++ b/tests/i/extern-c-struct-value/lib.c @@ -1,3 +1,5 @@ +#include + /* Each struct is passed or returned by value; the host C ABI decides how. */ struct Pair { int a, b; }; struct Vec2 { double x, y; }; @@ -22,3 +24,25 @@ struct Mixed make_mixed(char tag, double value, int count) { return m; } struct Vec3f make_vec3f(float x, float y, float z) { struct Vec3f v = {x, y, z}; return v; } + +/* Each argument after n is a struct read back with va_arg. */ +double sum_vec2(int n, ...) { + va_list ap; + double total = 0; + va_start(ap, n); + for (int i = 0; i < n; ++i) { + struct Vec2 v = va_arg(ap, struct Vec2); + total += v.x + v.y; + } + va_end(ap); + return total; +} + +int mixed_counts(int n, ...) { + va_list ap; + int total = 0; + va_start(ap, n); + for (int i = 0; i < n; ++i) total += va_arg(ap, struct Mixed).count; + va_end(ap); + return total; +} diff --git a/tests/i/extern-c-struct-value/notes.md b/tests/i/extern-c-struct-value/notes.md index a614d7b..c694d08 100644 --- a/tests/i/extern-c-struct-value/notes.md +++ b/tests/i/extern-c-struct-value/notes.md @@ -2,6 +2,6 @@ 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 is passed and returned by value like the matching C struct (C11 6.5.2.2p7: arguments are assigned to the parameters; 6.8.6.4p3: the returned value is the function call's value). +Basis: compiler extension (EXTERN "C", INCLUDE_OBJ of a user object); a record whose fields are all C types is passed and returned by value like the matching C struct (C11 6.5.2.2p7: arguments are assigned to the parameters; 6.8.6.4p3: the returned value is the function call's value; 6.5.2.2p6-7 and 7.16.1.1p2: default promotions leave structs unchanged and va_arg reads a struct type). -Trace: the harness compiles `lib.c` with the host C compiler. pair_sum receives Pair {3, 4} and returns 3 + 4 = 7. vec2_dot receives {1.5, 2} and {4, 0.25}: 1.5 * 4 + 2 * 0.25 = 6 + 0.5 = 6.5, printed like other C doubles as `6.500000`. mixed_score receives bias -1, Mixed {'M', 2.5, 3} and k 4: -1 + 2.5 * 4 + 3 + 1 (the tag is 'M') = 13, printed `13.000000`. vec3f_sum receives {0.5, 1.25, 2}, all exact in float, and returns 0.5 + 1.25 + 2 = 3.75, printed `3.750000`. wide_after_five receives five ints and Wide {100, 1000}: 1 + 2 + 3 + 4 + 5 + 100 + 1000 = 1115. On x86-64 System V the five ints leave one integer register, so the 16-byte Wide goes on the stack. The make_ functions return the struct built from their arguments: make_pair(3, 4) gives `3 4`, and pair_sum(make_pair(5, 6)) passes the returned Pair straight on, 5 + 6 = 11. make_vec2(1.5, 2) gives `1.500000 2.000000`. make_mixed('Q', 0.25, 9) gives `Q 0.250000 9`; on x86-64 System V this 24-byte struct comes back through memory supplied by the caller. make_vec3f(0.5, 1.25, 2) gives `0.500000 1.250000 2.000000`, the C floats printed with %f. +Trace: the harness compiles `lib.c` with the host C compiler. pair_sum receives Pair {3, 4} and returns 3 + 4 = 7. vec2_dot receives {1.5, 2} and {4, 0.25}: 1.5 * 4 + 2 * 0.25 = 6 + 0.5 = 6.5, printed like other C doubles as `6.500000`. mixed_score receives bias -1, Mixed {'M', 2.5, 3} and k 4: -1 + 2.5 * 4 + 3 + 1 (the tag is 'M') = 13, printed `13.000000`. vec3f_sum receives {0.5, 1.25, 2}, all exact in float, and returns 0.5 + 1.25 + 2 = 3.75, printed `3.750000`. wide_after_five receives five ints and Wide {100, 1000}: 1 + 2 + 3 + 4 + 5 + 100 + 1000 = 1115. On x86-64 System V the five ints leave one integer register, so the 16-byte Wide goes on the stack. The make_ functions return the struct built from their arguments: make_pair(3, 4) gives `3 4`, and pair_sum(make_pair(5, 6)) passes the returned Pair straight on, 5 + 6 = 11. make_vec2(1.5, 2) gives `1.500000 2.000000`. make_mixed('Q', 0.25, 9) gives `Q 0.250000 9`; on x86-64 System V this 24-byte struct comes back through memory supplied by the caller. make_vec3f(0.5, 1.25, 2) gives `0.500000 1.250000 2.000000`, the C floats printed with %f. sum_vec2 reads two struct Vec2 values with va_arg: U is still {1.5, 2} from make_vec2 and V is {4, 0.25}, so 1.5 + 2 + 4 + 0.25 = 7.75, printed `7.750000`. mixed_counts reads two struct Mixed values: M holds count 9 from make_mixed and N = make_mixed('R', 1, 5) holds count 5, so 9 + 5 = 14. diff --git a/tests/i/extern-c-struct-value/program.cps b/tests/i/extern-c-struct-value/program.cps index 6b2cb8e..c8b3a1f 100644 --- a/tests/i/extern-c-struct-value/program.cps +++ b/tests/i/extern-c-struct-value/program.cps @@ -1,4 +1,5 @@ -// Records whose fields are C types pass to and return from C functions by value. +// Records whose fields are C types pass to and return from C functions by +// value, also as variadic arguments. INCLUDE_OBJ "lib.o" TYPE CShort = C_TYPE("short") TYPE CFloat = C_TYPE("float") @@ -34,10 +35,13 @@ EXTERN "C" EXTERN_FUNCTION make_pair(C_INT, C_INT) RETURNS Pair EXTERN "C" EXTERN_FUNCTION make_vec2(C_DOUBLE, C_DOUBLE) RETURNS Vec2 EXTERN "C" EXTERN_FUNCTION make_mixed(C_CHAR, C_DOUBLE, C_INT) RETURNS Mixed EXTERN "C" EXTERN_FUNCTION make_vec3f(CFloat, CFloat, CFloat) RETURNS Vec3f +EXTERN "C" EXTERN_FUNCTION sum_vec2(C_INT, ...) RETURNS C_DOUBLE +EXTERN "C" EXTERN_FUNCTION mixed_counts(C_INT, ...) RETURNS C_INT DECLARE P : Pair DECLARE U : Vec2 DECLARE V : Vec2 DECLARE M : Mixed +DECLARE N : Mixed DECLARE F : Vec3f DECLARE W : Wide P.A <- (C_INT)3 @@ -68,3 +72,6 @@ M <- make_mixed((C_CHAR)'Q', (C_DOUBLE)0.25, (C_INT)9) OUTPUT M.Tag, " ", M.Value, " ", M.Count F <- make_vec3f((CFloat)0.5, (CFloat)1.25, (CFloat)2) OUTPUT F.X, " ", F.Y, " ", F.Z +OUTPUT sum_vec2((C_INT)2, U, V) +N <- make_mixed((C_CHAR)'R', (C_DOUBLE)1, (C_INT)5) +OUTPUT mixed_counts((C_INT)2, M, N) diff --git a/tests/iii/extern-struct-rejected/case.json b/tests/iii/extern-struct-rejected/case.json index f83ef3e..cd7a3dd 100644 --- a/tests/iii/extern-struct-rejected/case.json +++ b/tests/iii/extern-struct-rejected/case.json @@ -11,6 +11,10 @@ { "reason": "The\\ return\\ type\\ of\\ EXTERN_FUNCTION\\ atoi\\ uses\\ record\\ Person,\\ which\\ is\\ not\\ a\\ C\\ struct:\\ field\\ Name\\ of\\ Person\\ has\\ type\\ STRING", "line": 10 + }, + { + "reason": "Variadic\\ argument\\ 2\\ to\\ printf\\ uses\\ record\\ Person,\\ which\\ is\\ not\\ a\\ C\\ struct:\\ field\\ Name\\ of\\ Person\\ has\\ type\\ STRING", + "line": 13 } ] } diff --git a/tests/iii/extern-struct-rejected/notes.md b/tests/iii/extern-struct-rejected/notes.md index 3e733c1..650b420 100644 --- a/tests/iii/extern-struct-rejected/notes.md +++ b/tests/iii/extern-struct-rejected/notes.md @@ -1,5 +1,5 @@ # extern-struct-rejected -Compiler extension: an EXTERN signature accepts a record, through a pointer type or by value, only as a C struct (every field a C type, a C struct or a pointer type to one). Person has a STRING field, so a pointer to it, Person as a by-value parameter and Person as a by-value result are all rejected and the field is named. +Compiler extension: an EXTERN signature accepts a record, through a pointer type or by value, only as a C struct (every field a C type, a C struct or a pointer type to one). Person has a STRING field, so a pointer to it, Person as a by-value parameter, as a by-value result and as a by-value variadic argument are all rejected and the field is named. -Require exit 1 and each cause on its EXTERN line. +Require exit 1 and each cause on its EXTERN or call line. diff --git a/tests/iii/extern-struct-rejected/program.cps b/tests/iii/extern-struct-rejected/program.cps index 8fd1fd9..ced6fee 100644 --- a/tests/iii/extern-struct-rejected/program.cps +++ b/tests/iii/extern-struct-rejected/program.cps @@ -8,4 +8,6 @@ TYPE PPerson = ^Person EXTERN "C" EXTERN_PROCEDURE puts(PPerson) EXTERN "C" EXTERN_FUNCTION abs(Person) RETURNS C_INT EXTERN "C" EXTERN_FUNCTION atoi(C_STRING) RETURNS Person -OUTPUT 1 +EXTERN "C" EXTERN_FUNCTION printf(C_STRING, ...) RETURNS C_INT +DECLARE Someone : Person +OUTPUT printf((C_STRING)"%d", Someone) -- cgit v1.3.1-4-g156e