ARM64 and macOS: AAPCS64 struct passing, aarch64/macos targets, Apple frameworks and ld messages, tests/cross.sh, CI on ARM64 Linux and macOS

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-09 00:34:00 +02:00
commit 484c14fe77
12 changed files with 256 additions and 78 deletions

View file

@ -1,6 +1,7 @@
// The C calling convention for structs passed and returned by value (System V x86-64),
// the way clang does it: small structs travel in registers, split into eightbytes that are
// either INTEGER (general registers) or SSE (xmm registers); bigger ones go through memory.
// The C calling convention for structs passed and returned by value, the way clang does it:
// System V x86-64 (Linux, macOS): small structs split into eightbytes, INTEGER or SSE registers
// Microsoft x64 (Windows): 1/2/4/8 bytes in one register, everything else by address
// AAPCS64 (ARM64 Linux, Apple): float groups in vector registers, else up to 16 bytes in x registers
#include "codegen_impl.h"
#include "target.h"
@ -45,6 +46,39 @@ AbiArg Codegen::classify(const FType &t) {
AbiArg a;
if (t.kind != FType::Struct) return a; // Direct
uint64_t size = dl->getTypeAllocSize(ty(t));
if (g_target.triple.isAArch64()) {
// A homogeneous float aggregate (1-4 floats or 1-4 doubles) travels in vector registers.
std::vector<Leaf> ls;
leaves(*this, t, 0, ls);
bool hfa = !ls.empty() && ls.size() <= 4;
for (const Leaf &l : ls) hfa &= l.isFloat && l.isDouble == ls[0].isDouble;
if (hfa) {
llvm::Type *ft = ls[0].isDouble ? (llvm::Type *)b.getDoubleTy() : b.getFloatTy();
a.kind = AbiArg::Expand;
a.parts.push_back(ArrayType::get(ft, ls.size()));
a.coerced = StructType::get(ctx, std::vector<llvm::Type *>(ls.size(), ft));
a.stackAlign8 = !g_target.triple.isOSDarwin();
return a;
}
if (size > 16) {
a.kind = AbiArg::Indirect; // a copy, passed by address; returned through sret
return a;
}
// up to 16 bytes in general registers: i64 or [2 x i64]; returned as an exact-size integer
uint64_t align = dl->getABITypeAlign(ty(t)).value();
a.kind = AbiArg::Expand;
if (align >= 16) {
a.parts.push_back(b.getInt128Ty());
a.coerced = b.getInt128Ty();
} else if (size <= 8) {
a.parts.push_back(b.getInt64Ty());
a.coerced = b.getIntNTy(size * 8);
} else {
a.parts.push_back(ArrayType::get(b.getInt64Ty(), 2));
a.coerced = a.parts[0];
}
return a;
}
if (g_target.windows) {
// Microsoft x64: 1, 2, 4 or 8 bytes travel as one integer (even if they hold floats);
// anything else is copied by the caller and passed by address
@ -122,7 +156,8 @@ static Plan makePlan(Codegen &cg, const FType &ret, const std::vector<FType> &pa
if (a.kind == AbiArg::Expand) {
int ni = 0, ns = 0;
for (llvm::Type *p : a.parts) (p->isIntegerTy() ? ni : ns)++;
if (g_target.windows || (ni <= intRegs && ns <= sseRegs)) {
// only System V counts registers here; elsewhere the backend places each part
if (g_target.windows || g_target.triple.isAArch64() || (ni <= intRegs && ns <= sseRegs)) {
intRegs -= ni;
sseRegs -= ns;
for (llvm::Type *p : a.parts) types.push_back(p);
@ -143,9 +178,15 @@ static Plan makePlan(Codegen &cg, const FType &ret, const std::vector<FType> &pa
return pl;
}
// Is C's plain `char` signed? Yes on x86 and Apple; no on ARM64 Linux.
static bool charIsSigned() {
return !g_target.triple.isAArch64() || g_target.triple.isOSDarwin() || g_target.triple.isOSWindows();
}
static Attribute::AttrKind extAttr(const FType &t) {
if (t.bits() >= 32 || !(t.isInt() || t.kind == FType::Bool || t.kind == FType::Char)) return Attribute::None;
return t.isSigned() || t.kind == FType::Char ? Attribute::SExt : Attribute::ZExt;
if (t.kind == FType::Char) return charIsSigned() ? Attribute::SExt : Attribute::ZExt;
return t.isSigned() ? Attribute::SExt : Attribute::ZExt;
}
// Attributes that tell LLVM how C passes each piece: sret, byval, signext/zeroext.
@ -169,6 +210,7 @@ static AttributeList attrsFor(Codegen &cg, const Plan &pl, const FType &ret, con
} else if (a.kind == AbiArg::Indirect) {
idx++;
} else if (a.kind == AbiArg::Expand) {
if (a.stackAlign8) al = al.addParamAttribute(ctx, idx, Attribute::getWithStackAlignment(ctx, Align(8)));
idx += a.parts.size();
} else {
if (Attribute::AttrKind k = extAttr(params[i]); k != Attribute::None) al = al.addParamAttribute(ctx, idx, k);
@ -230,7 +272,7 @@ Value_ Codegen::emitCCall(const CFunc &f, Function *fn, std::vector<Value_> args
if (t.kind == FType::F32) x = b.CreateFPExt(x, b.getDoubleTy());
else if (t.kind == FType::Str) x = cstr(x);
else if (t.kind == FType::Bool || ((t.isInt() || t.kind == FType::Char) && t.bits() < 32))
x = b.CreateIntCast(x, b.getInt32Ty(), t.isSigned() || t.kind == FType::Char);
x = b.CreateIntCast(x, b.getInt32Ty(), t.isSigned() || (t.kind == FType::Char && charIsSigned()));
ll.push_back(x);
continue;
}

View file

@ -12,8 +12,9 @@
namespace {
// Ask the system C compiler where it looks for headers, so we find the same stdio.h it would.
std::vector<std::string> systemIncludeDirs() {
// Ask the system C compiler where it looks for headers (and, on macOS, frameworks), so we find
// the same stdio.h it would. Returns ready -isystem / -iframework arguments.
std::vector<std::string> systemIncludeArgs() {
std::string output;
capture(g_target.cc + " -E -v -x c " + nullDevice(), output);
std::vector<std::string> dirs;
@ -29,8 +30,11 @@ std::vector<std::string> systemIncludeDirs() {
size_t z = l.find_last_not_of(" \n");
if (a == std::string::npos) continue;
std::string d = l.substr(a, z - a + 1);
if (d.size() > 20 && d.compare(d.size() - 20, 20, " (framework directory)") == 0) continue;
dirs.push_back(d);
// macOS: "/…/System/Library/Frameworks (framework directory)" holds <OpenGL/gl.h> and friends
if (d.size() > 22 && d.compare(d.size() - 22, 22, " (framework directory)") == 0)
dirs.push_back("-iframework" + d.substr(0, d.size() - 22));
else
dirs.push_back("-isystem" + d);
}
return dirs;
}
@ -322,7 +326,7 @@ CImports importHeaders(const std::vector<Import> &imports, const std::vector<std
std::vector<std::string> args = {"-x", "c", "-std=gnu11", "--target=" + g_target.triple.str()};
if (g_target.windows) args.push_back("-D_USE_MATH_DEFINES"); // M_PI and friends, as on Linux
for (const std::string &d : dirs) args.push_back("-I" + d);
for (const std::string &d : systemIncludeDirs()) args.push_back("-isystem" + d);
for (const std::string &d : systemIncludeArgs()) args.push_back(d);
std::vector<const char *> argv;
for (const std::string &a : args) argv.push_back(a.c_str());

View file

@ -111,6 +111,7 @@ struct AbiArg {
enum Kind { Direct, Expand, Memory, Indirect } kind = Direct;
std::vector<llvm::Type *> parts; // Expand: one value per eightbyte
llvm::Type *coerced = nullptr; // Expand: { parts... } for returns
bool stackAlign8 = false; // AArch64 Linux: HFA arguments that end up on the stack
};
class Codegen {

View file

@ -165,8 +165,7 @@ int Server::run() {
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);
#endif
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
initTargets();
setTarget("");
std::string err;
const Target *target = TargetRegistry::lookupTarget(g_target.triple, err);

View file

@ -65,13 +65,10 @@ static void optimize(Module &mod, TargetMachine *tm) {
}
static TargetMachine *targetMachine() {
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
initTargets();
const Triple &triple = g_target.triple;
if (triple.getArch() != Triple(sys::getDefaultTargetTriple()).getArch()) {
std::fprintf(stderr, "error: Finch can build for other systems on the same processor (like Windows from Linux), but not for %s yet\n",
triple.getArchName().str().c_str());
if (!triple.isX86() && !triple.isAArch64()) {
std::fprintf(stderr, "error: Finch generates code for x86-64 and ARM64, not for %s\n", triple.getArchName().str().c_str());
std::exit(1);
}
std::string err;
@ -129,6 +126,9 @@ static std::string runtimeObject() {
// pkg-config knows the right paths for a library if it has one; otherwise plain -l<name>.
static std::string libFlags(const std::string &lib) {
std::string out;
// macOS: link "Cocoa.framework" -> -framework Cocoa
if (lib.size() > 10 && lib.compare(lib.size() - 10, 10, ".framework") == 0)
return " -framework " + shellQuote(lib.substr(0, lib.size() - 10));
if (!g_target.cross && capture("pkg-config --libs " + shellQuote(lib), out) == 0) {
while (!out.empty() && std::isspace((unsigned char)out.back())) out.pop_back();
return " " + out;
@ -170,9 +170,21 @@ static std::string guessLib(const std::string &header) {
std::string lib = l.substr(a, l.find('\'', a) - a);
if (lib.size() > 4 && lib.compare(lib.size() - 4, 4, ".lib") == 0) lib = lib.substr(0, lib.size() - 4);
addOnce(missingLibs, lib);
} else if ((a = l.find("cannot find -l")) != std::string::npos) {
a += 14;
addOnce(missingLibs, l.substr(a, l.find_first_of(": \n", a) - a));
} else if ((a = l.find("cannot find -l")) != std::string::npos ||
(a = l.find("library not found for -l")) != std::string::npos) { // older Apple ld
a = l.find("-l", a) + 2;
addOnce(missingLibs, l.substr(a, l.find_first_of(": \n\r", a) - a));
} else if ((a = l.find("ld: library '")) != std::string::npos) { // Apple ld: library 'x' not found
a += 13;
addOnce(missingLibs, l.substr(a, l.find('\'', a) - a));
} else if ((a = l.find("framework '")) != std::string::npos && l.find("not found") != std::string::npos) {
a += 11;
addOnce(missingLibs, l.substr(a, l.find('\'', a) - a) + ".framework");
} else if (l.rfind(" \"", 0) == 0 && (a = l.find("\", referenced from:")) != std::string::npos) {
// Apple ld: Undefined symbols for architecture arm64:\n "_name", referenced from:
std::string name = l.substr(3, a - 3);
if (g_target.darwin && !name.empty() && name[0] == '_') name.erase(0, 1);
addOnce(missing, name);
}
}
if (missing.empty() && missingLibs.empty()) {
@ -323,7 +335,7 @@ int main(int argc, char **argv) {
if (cmd == "run") {
std::string line = shellQuote(out);
for (const std::string &a : programArgs) line += " " + shellQuote(a);
if (g_target.cross && g_target.windows) line = "wine " + line; // a Windows program on Linux
line = runPrefix() + line; // wine / qemu for programs built for another system
#ifdef _WIN32
line = "\"" + line + "\""; // cmd /c strips one pair of outer quotes
#endif

View file

@ -1,5 +1,6 @@
#include "target.h"
#include <llvm/Support/TargetSelect.h>
#include <llvm/TargetParser/Host.h>
#include <cstdio>
@ -21,27 +22,72 @@ static bool hostIsWindows() {
#endif
}
// The Finch-generated code needs these LLVM backends (the same C functions LLVM's own tools call).
extern "C" {
#ifdef FINCH_TARGET_X86
void LLVMInitializeX86TargetInfo();
void LLVMInitializeX86Target();
void LLVMInitializeX86TargetMC();
void LLVMInitializeX86AsmPrinter();
#endif
#ifdef FINCH_TARGET_AARCH64
void LLVMInitializeAArch64TargetInfo();
void LLVMInitializeAArch64Target();
void LLVMInitializeAArch64TargetMC();
void LLVMInitializeAArch64AsmPrinter();
#endif
}
void initTargets() {
#ifdef FINCH_TARGET_X86
LLVMInitializeX86TargetInfo();
LLVMInitializeX86Target();
LLVMInitializeX86TargetMC();
LLVMInitializeX86AsmPrinter();
#endif
#ifdef FINCH_TARGET_AARCH64
LLVMInitializeAArch64TargetInfo();
LLVMInitializeAArch64Target();
LLVMInitializeAArch64TargetMC();
LLVMInitializeAArch64AsmPrinter();
#endif
llvm::InitializeNativeTarget();
llvm::InitializeNativeTargetAsmPrinter();
}
void setTarget(const std::string &name) {
std::string host = llvm::sys::getDefaultTargetTriple();
llvm::Triple hostTriple(llvm::Triple::normalize(host));
std::string t = name;
if (name.empty()) t = host;
else if (name == "windows") t = hostIsWindows() ? host : "x86_64-w64-mingw32";
else if (name == "linux") t = "x86_64-unknown-linux-gnu";
else if (name == "arm64" || name == "aarch64" || name == "linux-arm64") t = "aarch64-unknown-linux-gnu";
else if (name == "macos") t = hostTriple.isAArch64() ? "arm64-apple-macosx14.0.0" : "x86_64-apple-macosx14.0.0";
g_target.triple = llvm::Triple(llvm::Triple::normalize(t));
g_target.windows = g_target.triple.isOSWindows();
g_target.msvc = g_target.triple.isWindowsMSVCEnvironment();
g_target.cross = llvm::Triple(llvm::Triple::normalize(host)).getOS() != g_target.triple.getOS();
g_target.darwin = g_target.triple.isOSDarwin();
g_target.cross = hostTriple.getOS() != g_target.triple.getOS() || hostTriple.getArch() != g_target.triple.getArch();
g_target.exe = g_target.windows ? ".exe" : "";
// FINCH_CC chooses the C compiler for any target; CC only when not cross-compiling.
if (const char *fc = std::getenv("FINCH_CC")) g_target.cc = fc;
else if (!g_target.cross && std::getenv("CC")) g_target.cc = std::getenv("CC");
else if (g_target.cross && g_target.windows) g_target.cc = "x86_64-w64-mingw32-gcc";
else if (g_target.cross && g_target.triple.isAArch64() && g_target.triple.isOSLinux()) g_target.cc = "aarch64-linux-gnu-gcc";
else if (g_target.cross) g_target.cc = "clang --target=" + g_target.triple.str();
else if (hostIsWindows()) g_target.cc = "clang";
else g_target.cc = "cc";
}
std::string runPrefix() {
if (!g_target.cross) return "";
if (g_target.windows && !hostIsWindows()) return "wine ";
if (g_target.triple.isAArch64() && g_target.triple.isOSLinux()) return "qemu-aarch64 ";
return "";
}
std::string shellQuote(const std::string &s) {
#ifdef _WIN32
// cmd.exe: double quotes; a " inside a file name is impossible on Windows

View file

@ -8,6 +8,7 @@
struct TargetInfo {
llvm::Triple triple;
bool darwin = false; // macOS
bool windows = false; // Windows (MSVC or MinGW): Microsoft x64 calling convention, .exe
bool msvc = false; // the MSVC environment (link with clang + the Windows SDK)
bool cross = false; // compiling for another system than the one we run on
@ -17,9 +18,15 @@ struct TargetInfo {
inline TargetInfo g_target;
// "windows" / "linux" / a full triple; "" = the system finch runs on
// "windows", "linux", "macos", "arm64" (ARM64 Linux), or a full LLVM triple; "" = the system finch runs on
void setTarget(const std::string &name);
// Register the LLVM backends Finch can generate code for (x86-64, ARM64).
void initTargets();
// How to run a program built for g_target on this machine ("" = directly, "wine", "qemu-aarch64").
std::string runPrefix();
// Quote one argument for the shell that runs our commands (sh on Unix, cmd.exe on Windows).
std::string shellQuote(const std::string &s);