From 4805b6f7acc07223a41b38e51a9a6e4f020dfdfe Mon Sep 17 00:00:00 2001 From: Pavel Verigo Date: Wed, 1 Jul 2026 16:59:35 +0200 Subject: [PATCH] stage2-wasm: support varargs --- src/codegen/wasm/CodeGen.zig | 203 ++++++++++++++++++++++++++++++----- src/codegen/wasm/Emit.zig | 1 + src/link/Wasm.zig | 19 ++-- src/link/Wasm/Flush.zig | 2 +- test/behavior/var_args.zig | 6 -- 5 files changed, 190 insertions(+), 41 deletions(-) diff --git a/src/codegen/wasm/CodeGen.zig b/src/codegen/wasm/CodeGen.zig index 33434649520913fd19d86c145ad5bbd8be68ce84..f011b1cf5e8ed8983c56a92ffe1589bd0bb35a43 100644 --- a/src/codegen/wasm/CodeGen.zig +++ b/src/codegen/wasm/CodeGen.zig @@ -196,6 +196,9 @@ args: []WValue, /// When it returns a pointer to the stack, the `.local` tag will be active and must be populated /// before this function returns its execution to the caller. return_value: WValue, +/// Only populated for variadic functions. +/// Holds the hidden final parameter pointing to the varargs buffer. +varargs: WValue, /// The size of the stack this function occupies. In the function prologue /// we will move the stack pointer by this number, forward aligned with the `stack_alignment`. stack_size: u32 = 0, @@ -308,7 +311,7 @@ const WValue = union(enum) { fn free(value: *WValue, gen: *CodeGen) void { if (value.* != .local) return; const local_value = value.local.value; - const reserved = gen.args.len + @intFromBool(gen.return_value != .none); + const reserved = gen.args.len + @intFromBool(gen.return_value != .none) + @intFromBool(gen.varargs != .none); if (local_value < reserved + 2) return; // reserved locals may never be re-used. Also accounts for 2 stack locals. const index = local_value - reserved; @@ -800,6 +803,7 @@ pub fn generate( .func_index = func_index, .args = cc_result.args, .return_value = cc_result.return_value, + .varargs = cc_result.varargs, .local_index = cc_result.local_index, .mir_instructions = .empty, .mir_extra = .empty, @@ -874,6 +878,7 @@ fn generateInner(cg: *CodeGen, any_returns: bool) InnerError!Mir { const CallWValues = struct { args: []WValue, return_value: WValue, + varargs: WValue, local_index: u32, fn deinit(values: *CallWValues, gpa: Allocator) void { @@ -895,6 +900,7 @@ fn resolveCallingConventionValues( var result: CallWValues = .{ .args = &.{}, .return_value = .none, + .varargs = .none, .local_index = 0, }; if (cc == .naked) return result; @@ -945,6 +951,12 @@ fn resolveCallingConventionValues( }, else => unreachable, // Frontend is responsible for emitting an error earlier. } + + if (fn_info.is_var_args) { + result.varargs = .{ .local = .{ .value = result.local_index, .references = 1 } }; + result.local_index += 1; + } + result.args = try args.toOwnedSlice(); return result; } @@ -1056,9 +1068,6 @@ fn allocStack(cg: *CodeGen, ty: Type) !WValue { const pt = cg.pt; const zcu = pt.zcu; assert(ty.hasRuntimeBits(zcu)); - if (cg.initial_stack_value == .none) { - try cg.initializeStack(); - } const abi_size = std.math.cast(u32, ty.abiSize(zcu)) orelse { return cg.fail("Type {f} with ABI size of {d} exceeds stack frame size", .{ @@ -1067,28 +1076,29 @@ fn allocStack(cg: *CodeGen, ty: Type) !WValue { }; const abi_align = ty.abiAlignment(zcu); - cg.stack_alignment = cg.stack_alignment.max(abi_align); - - const offset: u32 = @intCast(abi_align.forward(cg.stack_size)); - defer cg.stack_size = offset + abi_size; - - return .{ .stack_offset = .{ .value = offset, .references = 1 } }; + return cg.allocStackBytes(abi_size, abi_align); } fn allocInt(cg: *CodeGen, int_ty: IntType) !WValue { - if (cg.initial_stack_value == .none) { - try cg.initializeStack(); - } - const abi_size = std.math.cast(u32, std.zig.target.intByteSize(cg.target, int_ty.bits)) orelse { return cg.fail("Integer ABI size exceeds max stack size", .{}); }; const abi_align: Alignment = .fromByteUnits(std.zig.target.intAlignment(cg.target, int_ty.bits)); - cg.stack_alignment = cg.stack_alignment.max(abi_align); + return cg.allocStackBytes(abi_size, abi_align); +} - const offset: u32 = @intCast(abi_align.forward(cg.stack_size)); - defer cg.stack_size = offset + abi_size; +fn allocStackBytes(cg: *CodeGen, size: u32, alignment: Alignment) !WValue { + assert(size > 0); + + if (cg.initial_stack_value == .none) { + try cg.initializeStack(); + } + + cg.stack_alignment = cg.stack_alignment.max(alignment); + + const offset: u32 = @intCast(alignment.forward(cg.stack_size)); + defer cg.stack_size = offset + size; return .{ .stack_offset = .{ .value = offset, .references = 1 } }; } @@ -1841,15 +1851,16 @@ fn genInst(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void { .assembly => cg.airAsm(inst), + .c_va_arg => try cg.airVaArg(inst), + .c_va_copy => try cg.airVaCopy(inst), + .c_va_end => try cg.airVaEnd(inst), + .c_va_start => try cg.airVaStart(inst), + .err_return_trace, .set_err_return_trace, .save_err_return_trace_index, .is_named_enum_value, .addrspace_cast, - .c_va_arg, - .c_va_copy, - .c_va_end, - .c_va_start, => |tag| return cg.fail("TODO: Implement wasm inst: {s}", .{@tagName(tag)}), .atomic_load => cg.airAtomicLoad(inst), @@ -2030,19 +2041,70 @@ fn airCall(cg: *CodeGen, inst: Air.Inst.Index, modifier: std.lang.CallModifier) return cg.fail("unable to lower callee to a function index", .{}); }; - const sret: WValue = if (first_param_sret) blk: { - const sret_local = try cg.allocStack(ret_ty); - try cg.lowerToStack(sret_local); - break :blk sret_local; + const sret: WValue = if (first_param_sret) + try cg.allocStack(ret_ty) + else + .none; + + const fixed_arg_count = fn_info.param_types.len; + + const varargs_buf: WValue = if (fn_info.is_var_args) buf: { + var varargs_size: u32 = 0; + var varargs_align: Alignment = .fromByteUnits(1); + + for (args[fixed_arg_count..]) |arg| { + const arg_ty = cg.typeOf(arg); + if (!arg_ty.hasRuntimeBits(zcu)) continue; + + const arg_size = std.math.cast(u32, arg_ty.abiSize(zcu)) orelse { + return cg.fail("argument type {f} too large for wasm varargs buffer", .{arg_ty.fmt(pt)}); + }; + const arg_align = arg_ty.abiAlignment(zcu); + + varargs_align = varargs_align.max(arg_align); + varargs_size = @intCast(arg_align.forward(varargs_size)); + varargs_size += arg_size; + } + + if (varargs_size == 0) varargs_size = 1; + + const buffer = try cg.allocStackBytes(varargs_size, varargs_align); + + var offset: u32 = 0; + for (args[fixed_arg_count..]) |arg| { + const arg_ty = cg.typeOf(arg); + if (!arg_ty.hasRuntimeBits(zcu)) continue; + + const arg_val = try cg.resolveInst(arg); + const arg_size = std.math.cast(u32, arg_ty.abiSize(zcu)) orelse { + return cg.fail("argument type {f} too large for wasm varargs buffer", .{arg_ty.fmt(pt)}); + }; + const arg_align = arg_ty.abiAlignment(zcu); + + offset = @intCast(arg_align.forward(offset)); + try cg.store(buffer, arg_val, arg_ty, offset); + offset += arg_size; + } + + break :buf buffer; } else .none; - for (args) |arg| { - const arg_val = try cg.resolveInst(arg); + if (first_param_sret) { + try cg.lowerToStack(sret); + } + + for (args, 0..) |arg, arg_i| { + if (fn_info.is_var_args and arg_i >= fixed_arg_count) break; const arg_ty = cg.typeOf(arg); if (!arg_ty.hasRuntimeBits(zcu)) continue; - try cg.lowerArg(zcu.typeToFunc(fn_ty).?.cc, arg_ty, arg_val); + const arg_val = try cg.resolveInst(arg); + try cg.lowerArg(fn_info.cc, arg_ty, arg_val); + } + + if (fn_info.is_var_args) { + try cg.lowerToStack(varargs_buf); } if (callee) |nav_index| { @@ -2097,6 +2159,91 @@ fn airCall(cg: *CodeGen, inst: Air.Inst.Index, modifier: std.lang.CallModifier) return cg.finishAirResult(inst, result_value); } +fn airVaStart(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void { + try cg.emitWValue(cg.varargs); + return cg.finishAir(inst, .stack, &.{}); +} + +fn airVaEnd(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void { + const un_op = cg.air.instructions.items(.data)[@intFromEnum(inst)].un_op; + return cg.finishAir(inst, .none, &.{un_op}); +} + +fn airVaCopy(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void { + const ty_op = cg.air.instructions.items(.data)[@intFromEnum(inst)].ty_op; + const operand = try cg.resolveInst(ty_op.operand); + + const result = try cg.load(operand, .usize, 0); + + return cg.finishAir(inst, result, &.{ty_op.operand}); +} + +fn airVaArg(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void { + const zcu = cg.pt.zcu; + const ty_op = cg.air.instructions.items(.data)[@intFromEnum(inst)].ty_op; + const operand = try cg.resolveInst(ty_op.operand); + + const ty = cg.typeOfIndex(inst); + + if (!ty.hasRuntimeBits(zcu)) { + return cg.finishAir(inst, .none, &.{ty_op.operand}); + } + + const is_f32_va_arg = ty.toIntern() == .f32_type; + const load_ty: Type = if (is_f32_va_arg) Type.f64 else ty; + + const abi_size: u32 = @intCast(load_ty.abiSize(zcu)); + const abi_align: u32 = @intCast(load_ty.abiAlignment(zcu).toByteUnits().?); + + const arg_ptr = try cg.allocLocal(.usize); + _ = try cg.load(operand, .usize, 0); + + if (abi_align > 1) { + switch (cg.ptr_size) { + .wasm32 => { + try cg.addImm32(abi_align - 1); + try cg.addTag(.i32_add); + try cg.addImm32(~(abi_align - 1)); + try cg.addTag(.i32_and); + }, + .wasm64 => { + try cg.addImm64(abi_align - 1); + try cg.addTag(.i64_add); + try cg.addImm64(~@as(u64, abi_align - 1)); + try cg.addTag(.i64_and); + }, + } + } + + try cg.addLocal(.local_set, arg_ptr.local.value); + + try cg.lowerToStack(operand); + try cg.lowerToStack(arg_ptr); + switch (cg.ptr_size) { + .wasm32 => { + try cg.addImm32(abi_size); + try cg.addTag(.i32_add); + }, + .wasm64 => { + try cg.addImm64(abi_size); + try cg.addTag(.i64_add); + }, + } + try cg.store(.stack, .stack, .usize, 0); + + const result = if (is_f32_va_arg) result: { + const promoted = try cg.load(arg_ptr, Type.f64, 0); + try cg.emitWValue(promoted); + try cg.addTag(.f32_demote_f64); + + const result_local = try cg.allocLocal(Type.f32); + try cg.addLocal(.local_set, result_local.local.value); + break :result result_local; + } else try cg.load(arg_ptr, ty, 0); + + return cg.finishAir(inst, result, &.{ty_op.operand}); +} + fn airAlloc(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void { const value = try cg.allocStackPtr(inst); return cg.finishAir(inst, value, &.{}); diff --git a/src/codegen/wasm/Emit.zig b/src/codegen/wasm/Emit.zig index 847f18f560249d21d69333dfb100b60b28756ad7..be9f681e8398c77a69227a38d5fb57d35740fe54 100644 --- a/src/codegen/wasm/Emit.zig +++ b/src/codegen/wasm/Emit.zig @@ -186,6 +186,7 @@ pub fn lowerToCode(emit: *Emit) Error!void { fn_info.cc, fn_info.param_types.get(&comp.zcu.?.intern_pool), .fromInterned(fn_info.return_type), + fn_info.is_var_args, target, ).?; if (is_obj) { diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index fd18419bab38858a538154f459cb683e2a345d2c..a4d20fdb07b8166c1ee38537eda0274d67208311 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -932,7 +932,7 @@ pub const ZcuFunc = union { switch (ip.indexToKey(i.key(wasm).*)) { .func => |func| { const fn_info = zcu.typeToFunc(.fromInterned(func.ty)).?; - return wasm.getExistingFunctionType(fn_info.cc, fn_info.param_types.get(ip), .fromInterned(fn_info.return_type), target).?; + return wasm.getExistingFunctionType(fn_info.cc, fn_info.param_types.get(ip), .fromInterned(fn_info.return_type), fn_info.is_var_args, target).?; }, .enum_type => { return i.value(wasm).tag_name.type_index; @@ -2290,7 +2290,7 @@ pub const ZcuImportIndex = enum(u32) { const nav_index = index.ptr(wasm).*; const ext = ip.indexToKey(ip.getNav(nav_index).resolved.?.value).@"extern"; const fn_info = zcu.typeToFunc(.fromInterned(ext.ty)).?; - return getExistingFunctionType(wasm, fn_info.cc, fn_info.param_types.get(ip), .fromInterned(fn_info.return_type), target).?; + return getExistingFunctionType(wasm, fn_info.cc, fn_info.param_types.get(ip), .fromInterned(fn_info.return_type), fn_info.is_var_args, target).?; } pub fn globalType(index: ZcuImportIndex, wasm: *const Wasm) ObjectGlobal.Type { @@ -3209,7 +3209,7 @@ pub fn updateFunc( for (mir.indirect_function_set.keys()) |nav| wasm.zcu_indirect_function_set.putAssumeCapacity(nav, {}); for (mir.func_tys.keys()) |func_ty| { const fn_info = zcu.typeToFunc(.fromInterned(func_ty)).?; - _ = try wasm.internFunctionType(fn_info.cc, fn_info.param_types.get(ip), .fromInterned(fn_info.return_type), target); + _ = try wasm.internFunctionType(fn_info.cc, fn_info.param_types.get(ip), .fromInterned(fn_info.return_type), fn_info.is_var_args, target); } wasm.error_name_table_ref_count += mir.error_name_table_ref_count; // We need to populate UAV data. In theory, we can lower the UAV values while we fill `mir.uavs`. @@ -3281,7 +3281,7 @@ pub fn updateNav(wasm: *Wasm, pt: Zcu.PerThread, nav_index: InternPool.Nav.Index wasm.function_imports.putAssumeCapacity(name, .fromZcuImport(zcu_import, wasm)); // Ensure there is a corresponding function type table entry. const fn_info = zcu.typeToFunc(.fromInterned(ext.ty)).?; - _ = try internFunctionType(wasm, fn_info.cc, fn_info.param_types.get(ip), .fromInterned(fn_info.return_type), target); + _ = try internFunctionType(wasm, fn_info.cc, fn_info.param_types.get(ip), .fromInterned(fn_info.return_type), fn_info.is_var_args, target); } else { wasm.data_imports.putAssumeCapacity(name, .fromZcuImport(zcu_import, wasm)); } @@ -3928,9 +3928,10 @@ pub fn internFunctionType( cc: std.lang.CallingConvention, params: []const InternPool.Index, return_type: Zcu.Type, + is_var_args: bool, target: *const std.Target, ) Allocator.Error!FunctionType.Index { - try convertZcuFnType(wasm.base.comp, cc, params, return_type, target, &wasm.params_scratch, &wasm.returns_scratch); + try convertZcuFnType(wasm.base.comp, cc, params, return_type, is_var_args, target, &wasm.params_scratch, &wasm.returns_scratch); return wasm.addFuncType(.{ .params = try wasm.internValtypeList(wasm.params_scratch.items), .returns = try wasm.internValtypeList(wasm.returns_scratch.items), @@ -3942,9 +3943,10 @@ pub fn getExistingFunctionType( cc: std.lang.CallingConvention, params: []const InternPool.Index, return_type: Zcu.Type, + is_var_args: bool, target: *const std.Target, ) ?FunctionType.Index { - convertZcuFnType(wasm.base.comp, cc, params, return_type, target, &wasm.params_scratch, &wasm.returns_scratch) catch |err| switch (err) { + convertZcuFnType(wasm.base.comp, cc, params, return_type, is_var_args, target, &wasm.params_scratch, &wasm.returns_scratch) catch |err| switch (err) { error.OutOfMemory => return null, }; return wasm.getExistingFuncType(.{ @@ -4176,6 +4178,7 @@ fn convertZcuFnType( cc: std.lang.CallingConvention, params: []const InternPool.Index, return_type: Zcu.Type, + is_var_args: bool, target: *const std.Target, params_buffer: *std.ArrayList(std.wasm.Valtype), returns_buffer: *std.ArrayList(std.wasm.Valtype), @@ -4226,6 +4229,10 @@ fn convertZcuFnType( else => try params_buffer.append(gpa, CodeGen.typeToValtype(param_type, zcu, target)), } } + + if (is_var_args) { + try params_buffer.append(gpa, .i32); + } } pub fn isBss(wasm: *const Wasm, optional_name: OptionalString) bool { diff --git a/src/link/Wasm/Flush.zig b/src/link/Wasm/Flush.zig index 56b07cd158f0ab2b6fa79cc87daa15b76bbdb62f..165664aeba5ab76a8d117c1f4981e84151c71ada 100644 --- a/src/link/Wasm/Flush.zig +++ b/src/link/Wasm/Flush.zig @@ -151,7 +151,7 @@ pub fn finish(f: *Flush, wasm: *Wasm) !void { const int_tag_ty = Zcu.Type.fromInterned(data.ip_index).intTagType(zcu); gop.value_ptr.* = .{ .tag_name = .{ .symbol_name = try wasm.internStringFmt("__zig_tag_name_{d}", .{data.ip_index}), - .type_index = try wasm.internFunctionType(.auto, &.{int_tag_ty.ip_index}, .slice_const_u8_sentinel_0, target), + .type_index = try wasm.internFunctionType(.auto, &.{int_tag_ty.ip_index}, .slice_const_u8_sentinel_0, false, target), .table_index = @intCast(wasm.tag_name_offs.items.len), } }; const tag_names = ip.loadEnumType(data.ip_index).field_names; diff --git a/test/behavior/var_args.zig b/test/behavior/var_args.zig index 21b35394cfb8cf5b3ba95bab8f61e77aa31a2d1f..e1a35322c19720ca2e838818c0150d8b8870dda4 100644 --- a/test/behavior/var_args.zig +++ b/test/behavior/var_args.zig @@ -94,7 +94,6 @@ fn doNothingWithFirstArg(args: anytype) void { test "simple variadic function" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO - if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm and !builtin.os.tag.isDarwin() and builtin.cpu.arch.isAARCH64()) { @@ -156,7 +155,6 @@ test "simple variadic function" { test "coerce reference to var arg" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO - if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm and !builtin.os.tag.isDarwin() and builtin.cpu.arch.isAARCH64()) { @@ -188,7 +186,6 @@ test "coerce reference to var arg" { test "variadic functions" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO - if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm and !builtin.os.tag.isDarwin() and builtin.cpu.arch.isAARCH64()) { @@ -241,7 +238,6 @@ test "variadic functions" { test "copy VaList" { if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO - if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm and !builtin.os.tag.isDarwin() and builtin.cpu.arch.isAARCH64()) { @@ -276,7 +272,6 @@ test "copy VaList" { test "unused VaList arg" { if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO - if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm and !builtin.os.tag.isDarwin() and builtin.cpu.arch.isAARCH64()) { @@ -308,7 +303,6 @@ test "unused VaList arg" { test "floating point VaList args" { if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO - if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/16961 -- 2.54.0