authorgravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-01-04 17:00:21+01:00
committergravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-01-04 17:59:05+01:00
log89b1fdc4437531776b46ed7133c44b7250c122f8
tree95d629f527158751b0828b274f96b5e32c924d12
parent5c21a45cf0a22f68ac15f7db8829186ff1808a84
signaturelock-open Commit is signed but in an unrecognized format.

wasm: Implement memset, and sret arguments.

We now detect if the return type will be set by passing the first argument as a pointer to stack memory from the callee's frame. This way, we do not have to worry about stack memory being overwritten. Besides this, we implement memset by either using wasm's memory.fill instruction when available, or lower it manually. In the future we can lower this to a compiler_rt call.

5 files changed, 185 insertions(+), 56 deletions(-)

lib/std/wasm.zig+23-1
......@@ -212,6 +212,28 @@ test "Wasm - opcodes" {
212212 try testing.expectEqual(@as(u16, 0xC4), i64_extend32_s);
213213}
214214
215/// Opcodes that require a prefix `0xFC`
216pub const PrefixedOpcode = enum(u8) {
217 i32_trunc_sat_f32_s = 0x00,
218 i32_trunc_sat_f32_u = 0x01,
219 i32_trunc_sat_f64_s = 0x02,
220 i32_trunc_sat_f64_u = 0x03,
221 i64_trunc_sat_f32_s = 0x04,
222 i64_trunc_sat_f32_u = 0x05,
223 i64_trunc_sat_f64_s = 0x06,
224 i64_trunc_sat_f64_u = 0x07,
225 memory_init = 0x08,
226 data_drop = 0x09,
227 memory_copy = 0x0A,
228 memory_fill = 0x0B,
229 table_init = 0x0C,
230 elem_drop = 0x0D,
231 table_copy = 0x0E,
232 table_grow = 0x0F,
233 table_size = 0x10,
234 table_fill = 0x11,
235};
236
215237/// Enum representing all Wasm value types as per spec:
216238/// https://webassembly.github.io/spec/core/binary/types.html
217239pub const Valtype = enum(u8) {
......@@ -266,7 +288,7 @@ pub const InitExpression = union(enum) {
266288 global_get: u32,
267289};
268290
269///
291/// Represents a function entry, holding the index to its type
270292pub const Func = struct {
271293 type_index: u32,
272294};
src/arch/wasm/CodeGen.zig+127-47
......@@ -623,6 +623,10 @@ fn addTag(self: *Self, tag: Mir.Inst.Tag) error{OutOfMemory}!void {
623623 try self.addInst(.{ .tag = tag, .data = .{ .tag = {} } });
624624}
625625
626fn addExtended(self: *Self, opcode: wasm.PrefixedOpcode) error{OutOfMemory}!void {
627 try self.addInst(.{ .tag = .extended, .secondary = @enumToInt(opcode), .data = .{ .tag = {} } });
628}
629
626630fn addLabel(self: *Self, tag: Mir.Inst.Tag, label: u32) error{OutOfMemory}!void {
627631 try self.addInst(.{ .tag = tag, .data = .{ .label = label } });
628632}
......@@ -746,6 +750,13 @@ fn genFunctype(self: *Self, fn_ty: Type) !wasm.Type {
746750 defer params.deinit();
747751 var returns = std.ArrayList(wasm.Valtype).init(self.gpa);
748752 defer returns.deinit();
753 const return_type = fn_ty.fnReturnType();
754
755 const want_sret = isByRef(return_type);
756
757 if (want_sret) {
758 try params.append(try self.typeToValtype(Type.usize));
759 }
749760
750761 // param types
751762 if (fn_ty.fnParamLen() != 0) {
......@@ -759,11 +770,8 @@ fn genFunctype(self: *Self, fn_ty: Type) !wasm.Type {
759770 }
760771
761772 // return type
762 const return_type = fn_ty.fnReturnType();
763 switch (return_type.zigTypeTag()) {
764 .Void, .NoReturn => {},
765 .Struct => return self.fail("TODO: Implement struct as return type for wasm", .{}),
766 else => try returns.append(try self.typeToValtype(return_type)),
773 if (!want_sret and return_type.hasCodeGenBits()) {
774 try returns.append(try self.typeToValtype(return_type));
767775 }
768776
769777 return wasm.Type{
......@@ -785,6 +793,15 @@ pub fn genFunc(self: *Self) InnerError!Result {
785793
786794 // Generate MIR for function body
787795 try self.genBody(self.air.getMainBody());
796 // In case we have a return value, but the last instruction is a noreturn (such as a while loop)
797 // we emit an unreachable instruction to tell the stack validator that part will never be reached.
798 if (func_type.returns.len != 0 and self.air.instructions.len > 0) {
799 const inst = @intCast(u32, self.air.instructions.len - 1);
800 if (self.air.typeOfIndex(inst).isNoReturn()) {
801 try self.addTag(.@"unreachable");
802 }
803 }
804
788805 // End of function body
789806 try self.addTag(.end);
790807
......@@ -1074,6 +1091,15 @@ fn resolveCallingConventionValues(self: *Self, fn_ty: Type) InnerError!CallWValu
10741091 .return_value = .none,
10751092 };
10761093 errdefer self.gpa.free(result.args);
1094 const ret_ty = fn_ty.fnReturnType();
1095 // Check if we store the result as a pointer to the stack rather than
1096 // by value
1097 if (isByRef(ret_ty)) {
1098 // the sret arg will be passed as first argument, therefore we
1099 // set the `return_value` before allocating locals for regular args.
1100 result.return_value = .{ .local = self.local_index };
1101 self.local_index += 1;
1102 }
10771103 switch (cc) {
10781104 .Naked => return result,
10791105 .Unspecified, .C => {
......@@ -1086,19 +1112,6 @@ fn resolveCallingConventionValues(self: *Self, fn_ty: Type) InnerError!CallWValu
10861112 result.args[ty_index] = .{ .local = self.local_index };
10871113 self.local_index += 1;
10881114 }
1089
1090 const ret_ty = fn_ty.fnReturnType();
1091 // Check if we store the result as a pointer to the stack rather than
1092 // by value
1093 if (isByRef(ret_ty)) {
1094 if (self.initial_stack_value == .none) try self.initializeStack();
1095 result.return_value = try self.allocStack(ret_ty);
1096
1097 // We want to make sure the return value's stack value doesn't get overwritten,
1098 // so set initial stack value to current's position instead.
1099 try self.addLabel(.global_get, 0);
1100 try self.addLabel(.local_set, self.initial_stack_value.local);
1101 }
11021115 },
11031116 else => return self.fail("TODO implement function parameters for cc '{}' on wasm", .{cc}),
11041117 }
......@@ -1323,6 +1336,7 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue {
13231336
13241337 .load => self.airLoad(inst),
13251338 .loop => self.airLoop(inst),
1339 .memset => self.airMemset(inst),
13261340 .not => self.airNot(inst),
13271341 .optional_payload => self.airOptionalPayload(inst),
13281342 .optional_payload_ptr => self.airOptionalPayloadPtr(inst),
......@@ -1335,18 +1349,21 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue {
13351349 .ret => self.airRet(inst),
13361350 .ret_ptr => self.airRetPtr(inst),
13371351 .ret_load => self.airRetLoad(inst),
1352
13381353 .slice => self.airSlice(inst),
13391354 .slice_len => self.airSliceLen(inst),
13401355 .slice_elem_val => self.airSliceElemVal(inst),
13411356 .slice_elem_ptr => self.airSliceElemPtr(inst),
13421357 .slice_ptr => self.airSlicePtr(inst),
13431358 .store => self.airStore(inst),
1359
13441360 .struct_field_ptr => self.airStructFieldPtr(inst),
13451361 .struct_field_ptr_index_0 => self.airStructFieldPtrIndex(inst, 0),
13461362 .struct_field_ptr_index_1 => self.airStructFieldPtrIndex(inst, 1),
13471363 .struct_field_ptr_index_2 => self.airStructFieldPtrIndex(inst, 2),
13481364 .struct_field_ptr_index_3 => self.airStructFieldPtrIndex(inst, 3),
13491365 .struct_field_val => self.airStructFieldVal(inst),
1366
13501367 .switch_br => self.airSwitchBr(inst),
13511368 .trunc => self.airTrunc(inst),
13521369 .unreach => self.airUnreachable(inst),
......@@ -1374,7 +1391,6 @@ fn airRet(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
13741391 // to the stack instead
13751392 if (self.return_value != .none) {
13761393 try self.store(self.return_value, operand, self.decl.ty.fnReturnType(), 0);
1377 try self.emitWValue(self.return_value);
13781394 } else {
13791395 try self.emitWValue(operand);
13801396 }
......@@ -1393,6 +1409,9 @@ fn airRetPtr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
13931409
13941410 if (child_type.abiSize(self.target) == 0) return WValue{ .none = {} };
13951411
1412 if (isByRef(child_type)) {
1413 return self.return_value;
1414 }
13961415 return self.allocStack(child_type);
13971416}
13981417
......@@ -1402,9 +1421,7 @@ fn airRetLoad(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
14021421 const ret_ty = self.air.typeOf(un_op).childType();
14031422 if (!ret_ty.hasCodeGenBits()) return WValue.none;
14041423
1405 if (isByRef(ret_ty)) {
1406 try self.emitWValue(operand);
1407 } else {
1424 if (!isByRef(ret_ty)) {
14081425 const result = try self.load(operand, ret_ty, 0);
14091426 try self.emitWValue(result);
14101427 }
......@@ -1425,6 +1442,8 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
14251442 .Pointer => ty.childType(),
14261443 else => unreachable,
14271444 };
1445 const ret_ty = fn_ty.fnReturnType();
1446 const first_param_sret = isByRef(ret_ty);
14281447
14291448 const target: ?*Decl = blk: {
14301449 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 {
14371456 return self.fail("Expected a function, but instead found type '{s}'", .{func_val.tag()});
14381457 };
14391458
1459 const sret = if (first_param_sret) blk: {
1460 const sret_local = try self.allocStack(ret_ty);
1461 try self.emitWValue(sret_local);
1462 break :blk sret_local;
1463 } else WValue{ .none = {} };
1464
14401465 for (args) |arg| {
14411466 const arg_ref = @intToEnum(Air.Inst.Ref, arg);
14421467 const arg_val = self.resolveInst(arg_ref);
......@@ -1475,31 +1500,18 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
14751500 try self.addLabel(.call_indirect, fn_type_index);
14761501 }
14771502
1478 const ret_ty = fn_ty.fnReturnType();
1479 if (!ret_ty.hasCodeGenBits()) return WValue.none;
1480
1481 // TODO: Implement this for all aggregate types
1482 if (ret_ty.isSlice()) {
1483 // first load the values onto the regular stack, before we move the stack pointer
1484 // to prevent overwriting the return value.
1485 const tmp = try self.allocLocal(ret_ty);
1486 try self.addLabel(.local_set, tmp.local);
1487 const field_ty = Type.@"usize";
1488 const offset = @intCast(u32, field_ty.abiSize(self.target));
1489 const ptr_local = try self.load(tmp, field_ty, 0);
1490 const len_local = try self.load(tmp, field_ty, offset);
1491
1492 // As our values are now safe, we reserve space on the virtual stack and
1493 // store the values there.
1494 const result = try self.allocStack(ret_ty);
1495 try self.store(result, ptr_local, field_ty, 0);
1496 try self.store(result, len_local, field_ty, offset);
1497 return result;
1503 if (self.liveness.isUnused(inst) or !ret_ty.hasCodeGenBits()) {
1504 return WValue.none;
1505 } else if (ret_ty.isNoReturn()) {
1506 try self.addTag(.@"unreachable");
1507 return WValue.none;
1508 } else if (first_param_sret) {
1509 return sret;
1510 } else {
1511 const result_local = try self.allocLocal(ret_ty);
1512 try self.addLabel(.local_set, result_local.local);
1513 return result_local;
14981514 }
1499
1500 const result_local = try self.allocLocal(ret_ty);
1501 try self.addLabel(.local_set, result_local.local);
1502 return result_local;
15031515}
15041516
15051517fn airAlloc(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
......@@ -1989,7 +2001,7 @@ fn emitUndefined(self: *Self, ty: Type) InnerError!void {
19892001 // validator will not accept it due to out-of-bounds memory access);
19902002 .Array => try self.addImm32(@bitCast(i32, @as(u32, 0xaa))),
19912003 .Struct => {
1992 // TODO: Write 0xaa to each field
2004 // TODO: Write 0xaa struct's memory
19932005 const result = try self.allocStack(ty);
19942006 try self.addLabel(.local_get, result.local);
19952007 },
......@@ -2943,3 +2955,71 @@ fn airPtrBinOp(self: *Self, inst: Air.Inst.Index, op: Op) InnerError!WValue {
29432955 try self.addLabel(.local_set, result.local);
29442956 return result;
29452957}
2958
2959fn airMemset(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
2960 const pl_op = self.air.instructions.items(.data)[inst].pl_op;
2961 const bin_op = self.air.extraData(Air.Bin, pl_op.payload).data;
2962
2963 const ptr = self.resolveInst(pl_op.operand);
2964 const value = self.resolveInst(bin_op.lhs);
2965 const len = self.resolveInst(bin_op.rhs);
2966 try self.memSet(ptr, len, value);
2967
2968 return WValue.none;
2969}
2970
2971/// Sets a region of memory at `ptr` to the value of `value`
2972/// When the user has enabled the bulk_memory feature, we lower
2973/// this to wasm's memset instruction. When the feature is not present,
2974/// we implement it manually.
2975fn memSet(self: *Self, ptr: WValue, len: WValue, value: WValue) InnerError!void {
2976 // When bulk_memory is enabled, we lower it to wasm's memset instruction.
2977 // If not, we lower it ourselves
2978 if (std.Target.wasm.featureSetHas(self.target.cpu.features, .bulk_memory)) {
2979 try self.emitWValue(ptr);
2980 try self.emitWValue(value);
2981 try self.emitWValue(len);
2982 try self.addExtended(.memory_fill);
2983 return;
2984 }
2985
2986 // TODO: We should probably lower this to a call to compiler_rt
2987 // But for now, we implement it manually
2988 const offset = try self.allocLocal(Type.usize); // local for counter
2989 // outer block to jump to when loop is done
2990 try self.startBlock(.block, wasm.block_empty);
2991 try self.startBlock(.loop, wasm.block_empty);
2992 try self.emitWValue(offset);
2993 try self.emitWValue(len);
2994 switch (self.ptrSize()) {
2995 4 => try self.addTag(.i32_eq),
2996 8 => try self.addTag(.i64_eq),
2997 else => unreachable,
2998 }
2999 try self.addLabel(.br_if, 1); // jump out of loop into outer block (finished)
3000 try self.emitWValue(ptr);
3001 try self.emitWValue(offset);
3002 switch (self.ptrSize()) {
3003 4 => try self.addTag(.i32_add),
3004 8 => try self.addTag(.i64_add),
3005 else => unreachable,
3006 }
3007 try self.emitWValue(value);
3008 const mem_store_op: Mir.Inst.Tag = switch (self.ptrSize()) {
3009 4 => .i32_store8,
3010 8 => .i64_store8,
3011 else => unreachable,
3012 };
3013 try self.addMemArg(mem_store_op, .{ .offset = 0, .alignment = 1 });
3014 try self.emitWValue(offset);
3015 try self.addImm32(1);
3016 switch (self.ptrSize()) {
3017 4 => try self.addTag(.i32_add),
3018 8 => try self.addTag(.i64_add),
3019 else => unreachable,
3020 }
3021 try self.addLabel(.local_set, offset.local);
3022 try self.addLabel(.br, 0); // jump to start of loop
3023 try self.endBlock();
3024 try self.endBlock();
3025}
src/arch/wasm/Emit.zig+19
......@@ -161,6 +161,8 @@ pub fn emitMir(emit: *Emit) InnerError!void {
161161 .i64_extend8_s => try emit.emitTag(tag),
162162 .i64_extend16_s => try emit.emitTag(tag),
163163 .i64_extend32_s => try emit.emitTag(tag),
164
165 .extended => try emit.emitExtended(inst),
164166 }
165167 }
166168}
......@@ -321,3 +323,20 @@ fn emitMemAddress(emit: *Emit, inst: Mir.Inst.Index) !void {
321323 .relocation_type = .R_WASM_MEMORY_ADDR_LEB,
322324 });
323325}
326
327fn emitExtended(emit: *Emit, inst: Mir.Inst.Index) !void {
328 const opcode = emit.mir.instructions.items(.secondary)[inst];
329 switch (@intToEnum(std.wasm.PrefixedOpcode, opcode)) {
330 .memory_fill => try emit.emitMemFill(),
331 else => |tag| return emit.fail("TODO: Implement extension instruction: {s}\n", .{@tagName(tag)}),
332 }
333}
334
335fn emitMemFill(emit: *Emit) !void {
336 try emit.code.append(0xFC);
337 try emit.code.append(0x0B);
338 // When multi-memory proposal reaches phase 4, we
339 // can emit a different memory index here.
340 // For now we will always emit index 0.
341 try leb128.writeULEB128(emit.code.writer(), @as(u32, 0));
342}
src/arch/wasm/Mir.zig+8
......@@ -19,6 +19,9 @@ extra: []const u32,
1919pub const Inst = struct {
2020 /// The opcode that represents this instruction
2121 tag: Tag,
22 /// This opcode will be set when `tag` represents an extended
23 /// instruction with prefix 0xFC, or a simd instruction with prefix 0xFD.
24 secondary: u8 = 0,
2225 /// Data is determined by the set `tag`.
2326 /// For example, `data` will be an i32 for when `tag` is 'i32_const'.
2427 data: Data,
......@@ -373,6 +376,11 @@ pub const Inst = struct {
373376 i64_extend16_s = 0xC3,
374377 /// Uses `tag`
375378 i64_extend32_s = 0xC4,
379 /// The instruction consists of an extension opcode
380 /// set in `secondary`
381 ///
382 /// The `data` field depends on the extension instruction
383 extended = 0xFC,
376384 /// Contains a symbol to a function pointer
377385 /// uses `label`
378386 ///
test/behavior.zig+8-8
......@@ -39,16 +39,23 @@ test {
3939 _ = @import("behavior/defer.zig");
4040 _ = @import("behavior/enum.zig");
4141 _ = @import("behavior/error.zig");
42 _ = @import("behavior/generics.zig");
4243 _ = @import("behavior/if.zig");
4344 _ = @import("behavior/import.zig");
4445 _ = @import("behavior/incomplete_struct_param_tld.zig");
4546 _ = @import("behavior/inttoptr.zig");
47 _ = @import("behavior/member_func.zig");
48 _ = @import("behavior/null.zig");
4649 _ = @import("behavior/pointers.zig");
4750 _ = @import("behavior/ptrcast.zig");
4851 _ = @import("behavior/ref_var_in_if_after_if_2nd_switch_prong.zig");
52 _ = @import("behavior/struct.zig");
53 _ = @import("behavior/this.zig");
4954 _ = @import("behavior/truncate.zig");
50 _ = @import("behavior/usingnamespace.zig");
5155 _ = @import("behavior/underscore.zig");
56 _ = @import("behavior/usingnamespace.zig");
57 _ = @import("behavior/void.zig");
58 _ = @import("behavior/while.zig");
5259
5360 if (!builtin.zig_is_stage2 or builtin.stage2_arch != .wasm32) {
5461 // Tests that pass for stage1, llvm backend, C backend
......@@ -56,16 +63,9 @@ test {
5663 _ = @import("behavior/array.zig");
5764 _ = @import("behavior/cast.zig");
5865 _ = @import("behavior/for.zig");
59 _ = @import("behavior/generics.zig");
6066 _ = @import("behavior/int128.zig");
61 _ = @import("behavior/member_func.zig");
62 _ = @import("behavior/null.zig");
6367 _ = @import("behavior/optional.zig");
64 _ = @import("behavior/struct.zig");
65 _ = @import("behavior/this.zig");
6668 _ = @import("behavior/translate_c_macros.zig");
67 _ = @import("behavior/while.zig");
68 _ = @import("behavior/void.zig");
6969
7070 if (builtin.object_format != .c) {
7171 // Tests that pass for stage1 and the llvm backend.