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

@@ -0,0 +1,30 @@
#include <stdarg.h>
long long sx_ffi_sum_ints(int n, ...) {
va_list ap;
va_start(ap, n);
long long total = 0;
for (int i = 0; i < n; i++) total += va_arg(ap, int);
va_end(ap);
return total;
}
double sx_ffi_avg_doubles(int n, ...) {
va_list ap;
va_start(ap, n);
double total = 0.0;
for (int i = 0; i < n; i++) total += va_arg(ap, double);
va_end(ap);
if (n == 0) return 0.0;
return total / n;
}
int sx_ffi_count_args(const char *tag, ...) {
(void) tag;
va_list ap;
va_start(ap, tag);
int count = 0;
while (va_arg(ap, const char *) != 0) count++;
va_end(ap);
return count;
}

View File

@@ -0,0 +1,28 @@
// `#foreign` C-variadic tail: trailing `args: ..T` on a foreign fn maps
// to the C calling convention's `...`. Extras at the call site are
// passed via the variadic slot with the standard default argument
// promotion (s8/s16/bool → s32, f32 → f64) applied implicitly.
#import "modules/std.sx";
#import c {
#source "ffi-foreign-cvariadic.c";
};
sx_ffi_sum_ints :: (n: s32, args: ..s32) -> s64 #foreign;
sx_ffi_avg_doubles :: (n: s32, args: ..f64) -> f64 #foreign;
sx_ffi_count_args :: (tag: *u8, args: ..*u8) -> s32 #foreign;
main :: () -> s32 {
print("sum_ints(3, 10, 20, 30) = {}\n", sx_ffi_sum_ints(3, 10, 20, 30));
print("sum_ints(0) = {}\n", sx_ffi_sum_ints(0));
print("avg_doubles(2) = {}\n", sx_ffi_avg_doubles(2, 1.5, 2.5));
print("avg_doubles(3) = {}\n", sx_ffi_avg_doubles(3, 1.0, 2.0, 3.0));
a := "alpha".ptr;
b := "beta".ptr;
g := "gamma".ptr;
sentinel : *u8 = null;
print("count_args(3 strs) = {}\n", sx_ffi_count_args("tag".ptr, a, b, g, sentinel));
0;
}

View File

@@ -662,7 +662,8 @@ pub const LLVMEmitter = struct {
param_types[j + sret_offset] = if (needs_c_abi) self.abiCoerceParamType(param.ty, llvm_ty) else llvm_ty;
}
const fn_type = c.LLVMFunctionType(ret_ty, param_types.ptr, param_count, 0);
const is_var_arg: c_int = if (func.is_variadic) 1 else 0;
const fn_type = c.LLVMFunctionType(ret_ty, param_types.ptr, param_count, is_var_arg);
const name_z = self.alloc.dupeZ(u8, name) catch unreachable;
defer self.alloc.free(name_z);

View File

@@ -478,6 +478,13 @@ pub const Function = struct {
linkage: Linkage = .internal,
call_conv: CallingConvention = .default,
source_file: ?[]const u8 = null,
/// Variadic tail at the IR signature level. Only `#foreign` decls reach
/// IR with this set — sx-side `..T` params are slice-packed before
/// lowering, so anything that survives is the C calling convention's
/// `...`. emit_llvm passes `is_var_arg=1` to `LLVMFunctionType`; call
/// sites apply the standard default argument promotions (s8/s16/bool →
/// s32, f32 → f64) to extras past the fixed param count.
is_variadic: bool = false,
pub const Param = struct {
name: StringId,

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| {

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,5 @@
sum_ints(3, 10, 20, 30) = 60
sum_ints(0) = 0
avg_doubles(2) = 2.000000
avg_doubles(3) = 2.000000
count_args(3 strs) = 3