diff --git a/lib/std/wasm.zig b/lib/std/wasm.zig index 681ac405fc..9af0156239 100644 --- a/lib/std/wasm.zig +++ b/lib/std/wasm.zig @@ -212,6 +212,28 @@ test "Wasm - opcodes" { try testing.expectEqual(@as(u16, 0xC4), i64_extend32_s); } +/// Opcodes that require a prefix `0xFC` +pub const PrefixedOpcode = enum(u8) { + i32_trunc_sat_f32_s = 0x00, + i32_trunc_sat_f32_u = 0x01, + i32_trunc_sat_f64_s = 0x02, + i32_trunc_sat_f64_u = 0x03, + i64_trunc_sat_f32_s = 0x04, + i64_trunc_sat_f32_u = 0x05, + i64_trunc_sat_f64_s = 0x06, + i64_trunc_sat_f64_u = 0x07, + memory_init = 0x08, + data_drop = 0x09, + memory_copy = 0x0A, + memory_fill = 0x0B, + table_init = 0x0C, + elem_drop = 0x0D, + table_copy = 0x0E, + table_grow = 0x0F, + table_size = 0x10, + table_fill = 0x11, +}; + /// Enum representing all Wasm value types as per spec: /// https://webassembly.github.io/spec/core/binary/types.html pub const Valtype = enum(u8) { @@ -266,7 +288,7 @@ pub const InitExpression = union(enum) { global_get: u32, }; -/// +/// Represents a function entry, holding the index to its type pub const Func = struct { type_index: u32, }; diff --git a/src/arch/wasm/CodeGen.zig b/src/arch/wasm/CodeGen.zig index 2a7198f8a3..de283306dd 100644 --- a/src/arch/wasm/CodeGen.zig +++ b/src/arch/wasm/CodeGen.zig @@ -623,6 +623,10 @@ fn addTag(self: *Self, tag: Mir.Inst.Tag) error{OutOfMemory}!void { try self.addInst(.{ .tag = tag, .data = .{ .tag = {} } }); } +fn addExtended(self: *Self, opcode: wasm.PrefixedOpcode) error{OutOfMemory}!void { + try self.addInst(.{ .tag = .extended, .secondary = @enumToInt(opcode), .data = .{ .tag = {} } }); +} + fn addLabel(self: *Self, tag: Mir.Inst.Tag, label: u32) error{OutOfMemory}!void { try self.addInst(.{ .tag = tag, .data = .{ .label = label } }); } @@ -746,6 +750,13 @@ fn genFunctype(self: *Self, fn_ty: Type) !wasm.Type { defer params.deinit(); var returns = std.ArrayList(wasm.Valtype).init(self.gpa); defer returns.deinit(); + const return_type = fn_ty.fnReturnType(); + + const want_sret = isByRef(return_type); + + if (want_sret) { + try params.append(try self.typeToValtype(Type.usize)); + } // param types if (fn_ty.fnParamLen() != 0) { @@ -759,11 +770,8 @@ fn genFunctype(self: *Self, fn_ty: Type) !wasm.Type { } // return type - const return_type = fn_ty.fnReturnType(); - switch (return_type.zigTypeTag()) { - .Void, .NoReturn => {}, - .Struct => return self.fail("TODO: Implement struct as return type for wasm", .{}), - else => try returns.append(try self.typeToValtype(return_type)), + if (!want_sret and return_type.hasCodeGenBits()) { + try returns.append(try self.typeToValtype(return_type)); } return wasm.Type{ @@ -785,6 +793,15 @@ pub fn genFunc(self: *Self) InnerError!Result { // Generate MIR for function body try self.genBody(self.air.getMainBody()); + // In case we have a return value, but the last instruction is a noreturn (such as a while loop) + // we emit an unreachable instruction to tell the stack validator that part will never be reached. + if (func_type.returns.len != 0 and self.air.instructions.len > 0) { + const inst = @intCast(u32, self.air.instructions.len - 1); + if (self.air.typeOfIndex(inst).isNoReturn()) { + try self.addTag(.@"unreachable"); + } + } + // End of function body try self.addTag(.end); @@ -1074,6 +1091,15 @@ fn resolveCallingConventionValues(self: *Self, fn_ty: Type) InnerError!CallWValu .return_value = .none, }; errdefer self.gpa.free(result.args); + const ret_ty = fn_ty.fnReturnType(); + // Check if we store the result as a pointer to the stack rather than + // by value + if (isByRef(ret_ty)) { + // the sret arg will be passed as first argument, therefore we + // set the `return_value` before allocating locals for regular args. + result.return_value = .{ .local = self.local_index }; + self.local_index += 1; + } switch (cc) { .Naked => return result, .Unspecified, .C => { @@ -1086,19 +1112,6 @@ fn resolveCallingConventionValues(self: *Self, fn_ty: Type) InnerError!CallWValu result.args[ty_index] = .{ .local = self.local_index }; self.local_index += 1; } - - const ret_ty = fn_ty.fnReturnType(); - // Check if we store the result as a pointer to the stack rather than - // by value - if (isByRef(ret_ty)) { - if (self.initial_stack_value == .none) try self.initializeStack(); - result.return_value = try self.allocStack(ret_ty); - - // We want to make sure the return value's stack value doesn't get overwritten, - // so set initial stack value to current's position instead. - try self.addLabel(.global_get, 0); - try self.addLabel(.local_set, self.initial_stack_value.local); - } }, else => return self.fail("TODO implement function parameters for cc '{}' on wasm", .{cc}), } @@ -1323,6 +1336,7 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue { .load => self.airLoad(inst), .loop => self.airLoop(inst), + .memset => self.airMemset(inst), .not => self.airNot(inst), .optional_payload => self.airOptionalPayload(inst), .optional_payload_ptr => self.airOptionalPayloadPtr(inst), @@ -1335,18 +1349,21 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue { .ret => self.airRet(inst), .ret_ptr => self.airRetPtr(inst), .ret_load => self.airRetLoad(inst), + .slice => self.airSlice(inst), .slice_len => self.airSliceLen(inst), .slice_elem_val => self.airSliceElemVal(inst), .slice_elem_ptr => self.airSliceElemPtr(inst), .slice_ptr => self.airSlicePtr(inst), .store => self.airStore(inst), + .struct_field_ptr => self.airStructFieldPtr(inst), .struct_field_ptr_index_0 => self.airStructFieldPtrIndex(inst, 0), .struct_field_ptr_index_1 => self.airStructFieldPtrIndex(inst, 1), .struct_field_ptr_index_2 => self.airStructFieldPtrIndex(inst, 2), .struct_field_ptr_index_3 => self.airStructFieldPtrIndex(inst, 3), .struct_field_val => self.airStructFieldVal(inst), + .switch_br => self.airSwitchBr(inst), .trunc => self.airTrunc(inst), .unreach => self.airUnreachable(inst), @@ -1374,7 +1391,6 @@ fn airRet(self: *Self, inst: Air.Inst.Index) InnerError!WValue { // to the stack instead if (self.return_value != .none) { try self.store(self.return_value, operand, self.decl.ty.fnReturnType(), 0); - try self.emitWValue(self.return_value); } else { try self.emitWValue(operand); } @@ -1393,6 +1409,9 @@ fn airRetPtr(self: *Self, inst: Air.Inst.Index) InnerError!WValue { if (child_type.abiSize(self.target) == 0) return WValue{ .none = {} }; + if (isByRef(child_type)) { + return self.return_value; + } return self.allocStack(child_type); } @@ -1402,9 +1421,7 @@ fn airRetLoad(self: *Self, inst: Air.Inst.Index) InnerError!WValue { const ret_ty = self.air.typeOf(un_op).childType(); if (!ret_ty.hasCodeGenBits()) return WValue.none; - if (isByRef(ret_ty)) { - try self.emitWValue(operand); - } else { + if (!isByRef(ret_ty)) { const result = try self.load(operand, ret_ty, 0); try self.emitWValue(result); } @@ -1425,6 +1442,8 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue { .Pointer => ty.childType(), else => unreachable, }; + const ret_ty = fn_ty.fnReturnType(); + const first_param_sret = isByRef(ret_ty); const target: ?*Decl = blk: { const func_val = self.air.value(pl_op.operand) orelse break :blk null; @@ -1437,6 +1456,12 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue { return self.fail("Expected a function, but instead found type '{s}'", .{func_val.tag()}); }; + const sret = if (first_param_sret) blk: { + const sret_local = try self.allocStack(ret_ty); + try self.emitWValue(sret_local); + break :blk sret_local; + } else WValue{ .none = {} }; + for (args) |arg| { const arg_ref = @intToEnum(Air.Inst.Ref, arg); const arg_val = self.resolveInst(arg_ref); @@ -1475,31 +1500,18 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue { try self.addLabel(.call_indirect, fn_type_index); } - const ret_ty = fn_ty.fnReturnType(); - if (!ret_ty.hasCodeGenBits()) return WValue.none; - - // TODO: Implement this for all aggregate types - if (ret_ty.isSlice()) { - // first load the values onto the regular stack, before we move the stack pointer - // to prevent overwriting the return value. - const tmp = try self.allocLocal(ret_ty); - try self.addLabel(.local_set, tmp.local); - const field_ty = Type.@"usize"; - const offset = @intCast(u32, field_ty.abiSize(self.target)); - const ptr_local = try self.load(tmp, field_ty, 0); - const len_local = try self.load(tmp, field_ty, offset); - - // As our values are now safe, we reserve space on the virtual stack and - // store the values there. - const result = try self.allocStack(ret_ty); - try self.store(result, ptr_local, field_ty, 0); - try self.store(result, len_local, field_ty, offset); - return result; + if (self.liveness.isUnused(inst) or !ret_ty.hasCodeGenBits()) { + return WValue.none; + } else if (ret_ty.isNoReturn()) { + try self.addTag(.@"unreachable"); + return WValue.none; + } else if (first_param_sret) { + return sret; + } else { + const result_local = try self.allocLocal(ret_ty); + try self.addLabel(.local_set, result_local.local); + return result_local; } - - const result_local = try self.allocLocal(ret_ty); - try self.addLabel(.local_set, result_local.local); - return result_local; } fn airAlloc(self: *Self, inst: Air.Inst.Index) InnerError!WValue { @@ -1989,7 +2001,7 @@ fn emitUndefined(self: *Self, ty: Type) InnerError!void { // validator will not accept it due to out-of-bounds memory access); .Array => try self.addImm32(@bitCast(i32, @as(u32, 0xaa))), .Struct => { - // TODO: Write 0xaa to each field + // TODO: Write 0xaa struct's memory const result = try self.allocStack(ty); try self.addLabel(.local_get, result.local); }, @@ -2943,3 +2955,71 @@ fn airPtrBinOp(self: *Self, inst: Air.Inst.Index, op: Op) InnerError!WValue { try self.addLabel(.local_set, result.local); return result; } + +fn airMemset(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const pl_op = self.air.instructions.items(.data)[inst].pl_op; + const bin_op = self.air.extraData(Air.Bin, pl_op.payload).data; + + const ptr = self.resolveInst(pl_op.operand); + const value = self.resolveInst(bin_op.lhs); + const len = self.resolveInst(bin_op.rhs); + try self.memSet(ptr, len, value); + + return WValue.none; +} + +/// Sets a region of memory at `ptr` to the value of `value` +/// When the user has enabled the bulk_memory feature, we lower +/// this to wasm's memset instruction. When the feature is not present, +/// we implement it manually. +fn memSet(self: *Self, ptr: WValue, len: WValue, value: WValue) InnerError!void { + // When bulk_memory is enabled, we lower it to wasm's memset instruction. + // If not, we lower it ourselves + if (std.Target.wasm.featureSetHas(self.target.cpu.features, .bulk_memory)) { + try self.emitWValue(ptr); + try self.emitWValue(value); + try self.emitWValue(len); + try self.addExtended(.memory_fill); + return; + } + + // TODO: We should probably lower this to a call to compiler_rt + // But for now, we implement it manually + const offset = try self.allocLocal(Type.usize); // local for counter + // outer block to jump to when loop is done + try self.startBlock(.block, wasm.block_empty); + try self.startBlock(.loop, wasm.block_empty); + try self.emitWValue(offset); + try self.emitWValue(len); + switch (self.ptrSize()) { + 4 => try self.addTag(.i32_eq), + 8 => try self.addTag(.i64_eq), + else => unreachable, + } + try self.addLabel(.br_if, 1); // jump out of loop into outer block (finished) + try self.emitWValue(ptr); + try self.emitWValue(offset); + switch (self.ptrSize()) { + 4 => try self.addTag(.i32_add), + 8 => try self.addTag(.i64_add), + else => unreachable, + } + try self.emitWValue(value); + const mem_store_op: Mir.Inst.Tag = switch (self.ptrSize()) { + 4 => .i32_store8, + 8 => .i64_store8, + else => unreachable, + }; + try self.addMemArg(mem_store_op, .{ .offset = 0, .alignment = 1 }); + try self.emitWValue(offset); + try self.addImm32(1); + switch (self.ptrSize()) { + 4 => try self.addTag(.i32_add), + 8 => try self.addTag(.i64_add), + else => unreachable, + } + try self.addLabel(.local_set, offset.local); + try self.addLabel(.br, 0); // jump to start of loop + try self.endBlock(); + try self.endBlock(); +} diff --git a/src/arch/wasm/Emit.zig b/src/arch/wasm/Emit.zig index d17ad8234d..fb77ea5b0c 100644 --- a/src/arch/wasm/Emit.zig +++ b/src/arch/wasm/Emit.zig @@ -161,6 +161,8 @@ pub fn emitMir(emit: *Emit) InnerError!void { .i64_extend8_s => try emit.emitTag(tag), .i64_extend16_s => try emit.emitTag(tag), .i64_extend32_s => try emit.emitTag(tag), + + .extended => try emit.emitExtended(inst), } } } @@ -321,3 +323,20 @@ fn emitMemAddress(emit: *Emit, inst: Mir.Inst.Index) !void { .relocation_type = .R_WASM_MEMORY_ADDR_LEB, }); } + +fn emitExtended(emit: *Emit, inst: Mir.Inst.Index) !void { + const opcode = emit.mir.instructions.items(.secondary)[inst]; + switch (@intToEnum(std.wasm.PrefixedOpcode, opcode)) { + .memory_fill => try emit.emitMemFill(), + else => |tag| return emit.fail("TODO: Implement extension instruction: {s}\n", .{@tagName(tag)}), + } +} + +fn emitMemFill(emit: *Emit) !void { + try emit.code.append(0xFC); + try emit.code.append(0x0B); + // When multi-memory proposal reaches phase 4, we + // can emit a different memory index here. + // For now we will always emit index 0. + try leb128.writeULEB128(emit.code.writer(), @as(u32, 0)); +} diff --git a/src/arch/wasm/Mir.zig b/src/arch/wasm/Mir.zig index c2d6e919b0..9c76801cf3 100644 --- a/src/arch/wasm/Mir.zig +++ b/src/arch/wasm/Mir.zig @@ -19,6 +19,9 @@ extra: []const u32, pub const Inst = struct { /// The opcode that represents this instruction tag: Tag, + /// This opcode will be set when `tag` represents an extended + /// instruction with prefix 0xFC, or a simd instruction with prefix 0xFD. + secondary: u8 = 0, /// Data is determined by the set `tag`. /// For example, `data` will be an i32 for when `tag` is 'i32_const'. data: Data, @@ -373,6 +376,11 @@ pub const Inst = struct { i64_extend16_s = 0xC3, /// Uses `tag` i64_extend32_s = 0xC4, + /// The instruction consists of an extension opcode + /// set in `secondary` + /// + /// The `data` field depends on the extension instruction + extended = 0xFC, /// Contains a symbol to a function pointer /// uses `label` /// diff --git a/test/behavior.zig b/test/behavior.zig index a2c16380ec..04c9fc41fa 100644 --- a/test/behavior.zig +++ b/test/behavior.zig @@ -39,16 +39,23 @@ test { _ = @import("behavior/defer.zig"); _ = @import("behavior/enum.zig"); _ = @import("behavior/error.zig"); + _ = @import("behavior/generics.zig"); _ = @import("behavior/if.zig"); _ = @import("behavior/import.zig"); _ = @import("behavior/incomplete_struct_param_tld.zig"); _ = @import("behavior/inttoptr.zig"); + _ = @import("behavior/member_func.zig"); + _ = @import("behavior/null.zig"); _ = @import("behavior/pointers.zig"); _ = @import("behavior/ptrcast.zig"); _ = @import("behavior/ref_var_in_if_after_if_2nd_switch_prong.zig"); + _ = @import("behavior/struct.zig"); + _ = @import("behavior/this.zig"); _ = @import("behavior/truncate.zig"); - _ = @import("behavior/usingnamespace.zig"); _ = @import("behavior/underscore.zig"); + _ = @import("behavior/usingnamespace.zig"); + _ = @import("behavior/void.zig"); + _ = @import("behavior/while.zig"); if (!builtin.zig_is_stage2 or builtin.stage2_arch != .wasm32) { // Tests that pass for stage1, llvm backend, C backend @@ -56,16 +63,9 @@ test { _ = @import("behavior/array.zig"); _ = @import("behavior/cast.zig"); _ = @import("behavior/for.zig"); - _ = @import("behavior/generics.zig"); _ = @import("behavior/int128.zig"); - _ = @import("behavior/member_func.zig"); - _ = @import("behavior/null.zig"); _ = @import("behavior/optional.zig"); - _ = @import("behavior/struct.zig"); - _ = @import("behavior/this.zig"); _ = @import("behavior/translate_c_macros.zig"); - _ = @import("behavior/while.zig"); - _ = @import("behavior/void.zig"); if (builtin.object_format != .c) { // Tests that pass for stage1 and the llvm backend.