diff --git a/examples/ffi-foreign-cvariadic.c b/examples/ffi-foreign-cvariadic.c new file mode 100644 index 0000000..2c7bba7 --- /dev/null +++ b/examples/ffi-foreign-cvariadic.c @@ -0,0 +1,30 @@ +#include + +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; +} diff --git a/examples/ffi-foreign-cvariadic.sx b/examples/ffi-foreign-cvariadic.sx new file mode 100644 index 0000000..c9a765c --- /dev/null +++ b/examples/ffi-foreign-cvariadic.sx @@ -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; +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 53d203e..b68c030 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -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); diff --git a/src/ir/inst.zig b/src/ir/inst.zig index ab5afbf..8a369be 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -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, diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1ab3bed..8bc03e2 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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| { diff --git a/tests/expected/ffi-foreign-cvariadic.exit b/tests/expected/ffi-foreign-cvariadic.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/ffi-foreign-cvariadic.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/ffi-foreign-cvariadic.txt b/tests/expected/ffi-foreign-cvariadic.txt new file mode 100644 index 0000000..9049dd5 --- /dev/null +++ b/tests/expected/ffi-foreign-cvariadic.txt @@ -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