ffi #foreign: C-variadic tail via args: ..T

Trailing `args: ..T` on a #foreign declaration now lowers to the C
calling convention's `...` instead of sx-side slice-packing. Drops
the per-arity #foreign-shim workaround for callers of variadic C
APIs (__android_log_print, printf-family, etc.). Closes issue-0043.

- IR: Function.is_variadic on inst.Function; declareFunction drops
  the variadic param from the IR signature for foreign+variadic
  decls.
- emit_llvm: LLVMFunctionType receives is_var_arg=1 when the flag
  is set; call lowering passes extras through unchanged.
- Lowering: packVariadicCallArgs early-outs for foreign+variadic
  (no slice-pack); new promoteCVariadicArgs applies C default
  argument promotion (bool/s8/s16/u8/u16 -> s32, f32 -> f64) to
  extras past the fixed param count.
- Test: examples/ffi-foreign-cvariadic.sx + .c exercise s64/f64/s32
  returns through C va_arg over s32/f64/*u8 element types.

134 host + 6 cross tests pass on the WIP-less baseline.
This commit is contained in:
agra
2026-05-22 13:13:43 +03:00
parent cc29cfa7ce
commit 30fed66616
7 changed files with 112 additions and 3 deletions

View File

@@ -671,8 +671,19 @@ pub const Lowering = struct {
const ret_ty = self.resolveReturnType(fd);
// Foreign declarations with a trailing variadic param map to the C
// calling convention's `...` tail. Drop the variadic param from the
// IR signature (it has no C-level slot) and set is_variadic.
const is_foreign = fd.body.data == .foreign_expr;
var is_variadic = false;
var effective_params = fd.params;
if (is_foreign and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
is_variadic = true;
effective_params = fd.params[0 .. fd.params.len - 1];
}
var params = std.ArrayList(Function.Param).empty;
for (fd.params) |p| {
for (effective_params) |p| {
const pty = self.resolveParamType(&p);
params.append(self.alloc, .{
.name = self.module.types.internString(p.name),
@@ -683,7 +694,7 @@ pub const Lowering = struct {
const cc: Function.CallingConvention = if (fd.call_conv == .c) .c else .default;
// For #foreign with C name override, declare under C name and map sx name → C name
if (fd.body.data == .foreign_expr) {
if (is_foreign) {
const fe = fd.body.data.foreign_expr;
if (fe.c_name) |c_name| {
const c_name_id = self.module.types.internString(c_name);
@@ -691,6 +702,7 @@ pub const Lowering = struct {
const func = self.module.getFunctionMut(fid);
func.call_conv = cc;
func.source_file = self.current_source_file;
func.is_variadic = is_variadic;
self.foreign_name_map.put(name, c_name) catch {};
return;
}
@@ -701,6 +713,7 @@ pub const Lowering = struct {
const func = self.module.getFunctionMut(fid);
func.call_conv = cc;
func.source_file = self.current_source_file;
func.is_variadic = is_variadic;
}
/// Check if a C-imported function is visible from the current source file.
@@ -4651,6 +4664,7 @@ pub const Lowering = struct {
}
// Coerce arguments to match parameter types
self.coerceCallArgs(args.items, params);
if (func.is_variadic) self.promoteCVariadicArgs(args.items, params.len);
return self.builder.call(fid, args.items, ret_ty);
}
}
@@ -4859,6 +4873,7 @@ pub const Lowering = struct {
self.packVariadicCallArgs(fd, c, &args);
}
self.coerceCallArgs(args.items, params);
if (func.is_variadic) self.promoteCVariadicArgs(args.items, params.len);
return self.builder.call(fid, args.items, ret_ty);
}
// Check if this is Type.variant(payload) — qualified enum construction
@@ -6200,6 +6215,12 @@ pub const Lowering = struct {
/// Detects variadic params in the function decl, packs remaining args into a typed slice,
/// and replaces the args list with [fixed_args..., slice_ref].
fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void {
// `#foreign` variadic uses the C calling convention's `...` tail —
// extras are passed through directly with default argument promotion
// (handled at the call site), not packed into an sx slice.
if (fd.body.data == .foreign_expr and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
return;
}
// Find variadic param index
var variadic_idx: ?usize = null;
var elem_ty: TypeId = .any;
@@ -10025,6 +10046,22 @@ pub const Lowering = struct {
return 0;
}
/// Apply C default argument promotion to variadic-tail args. These rules
/// (bool/s8/s16/u8/u16 → s32, f32 → f64) match the C calling convention's
/// implicit promotions when an argument is passed through `...`.
fn promoteCVariadicArgs(self: *Lowering, args: []Ref, fixed_count: usize) void {
if (args.len <= fixed_count) return;
for (args[fixed_count..]) |*arg| {
const src_ty = self.builder.getRefType(arg.*);
const promoted: TypeId = switch (src_ty) {
.bool, .s8, .s16, .u8, .u16 => .s32,
.f32 => .f64,
else => continue,
};
arg.* = self.coerceToType(arg.*, src_ty, promoted);
}
}
/// Coerce call arguments in-place to match function parameter types.
fn coerceCallArgs(self: *Lowering, args: []Ref, params: []const Function.Param) void {
for (0..@min(args.len, params.len)) |i| {