Commit 6e139d124b

Luuk de Gram <luuk@degram.dev>
2021-08-01 11:05:15
wasm: Resolve feedback (wrapping arbitrary int sizes)
- This ensures we honor the user's integer size when performing wrapping operations. - Also, instead of using ensureCapacity, we now use ensureUnusedCapacity.
1 parent a861b7d
Changed files (2)
src
codegen
test
stage2
src/codegen/wasm.zig
@@ -634,7 +634,7 @@ pub const Context = struct {
                 // for each struct field, generate a local
                 const struct_data: *Module.Struct = ty.castTag(.@"struct").?.data;
                 const fields_len = @intCast(u32, struct_data.fields.count());
-                try self.locals.ensureCapacity(self.gpa, self.locals.items.len + fields_len);
+                try self.locals.ensureUnusedCapacity(self.gpa, fields_len);
                 for (struct_data.fields.values()) |*value| {
                     const val_type = try self.genValtype(value.ty);
                     self.locals.appendAssumeCapacity(val_type);
@@ -653,7 +653,7 @@ pub const Context = struct {
                 // The first local is also used to find the index of the error and payload.
                 //
                 // TODO: Add support where the payload is a type that contains multiple locals such as a struct.
-                try self.locals.ensureCapacity(self.gpa, self.locals.items.len + 2);
+                try self.locals.ensureUnusedCapacity(self.gpa, 2);
                 self.locals.appendAssumeCapacity(wasm.valtype(.i32)); // error values are always i32
                 self.locals.appendAssumeCapacity(val_type);
                 self.local_index += 2;
@@ -670,7 +670,7 @@ pub const Context = struct {
                     return self.fail("TODO: wasm optional pointer", .{});
                 }
 
-                try self.locals.ensureCapacity(self.gpa, self.locals.items.len + 2);
+                try self.locals.ensureUnusedCapacity(self.gpa, 2);
                 self.locals.appendAssumeCapacity(wasm.valtype(.i32)); // optional 'tag' for null-checking is always i32
                 self.locals.appendAssumeCapacity(try self.genValtype(child_type));
                 self.local_index += 2;
@@ -817,11 +817,11 @@ pub const Context = struct {
         const air_tags = self.air.instructions.items(.tag);
         return switch (air_tags[inst]) {
             .add => self.airBinOp(inst, .add),
-            .addwrap => self.airBinOp(inst, .add),
+            .addwrap => self.airWrapBinOp(inst, .add),
             .sub => self.airBinOp(inst, .sub),
-            .subwrap => self.airBinOp(inst, .sub),
+            .subwrap => self.airWrapBinOp(inst, .sub),
             .mul => self.airBinOp(inst, .mul),
-            .mulwrap => self.airBinOp(inst, .mul),
+            .mulwrap => self.airWrapBinOp(inst, .mul),
             .div => self.airBinOp(inst, .div),
             .bit_and => self.airBinOp(inst, .@"and"),
             .bit_or => self.airBinOp(inst, .@"or"),
@@ -1021,6 +1021,62 @@ pub const Context = struct {
         return WValue{ .code_offset = offset };
     }
 
+    fn airWrapBinOp(self: *Context, inst: Air.Inst.Index, op: Op) InnerError!WValue {
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = self.resolveInst(bin_op.lhs);
+        const rhs = self.resolveInst(bin_op.rhs);
+
+        // it's possible for both lhs and/or rhs to return an offset as well,
+        // in which case we return the first offset occurance we find.
+        const offset = blk: {
+            if (lhs == .code_offset) break :blk lhs.code_offset;
+            if (rhs == .code_offset) break :blk rhs.code_offset;
+            break :blk self.code.items.len;
+        };
+
+        try self.emitWValue(lhs);
+        try self.emitWValue(rhs);
+
+        const bin_ty = self.air.typeOf(bin_op.lhs);
+        const opcode: wasm.Opcode = buildOpcode(.{
+            .op = op,
+            .valtype1 = try self.typeToValtype(bin_ty),
+            .signedness = if (bin_ty.isSignedInt()) .signed else .unsigned,
+        });
+        try self.code.append(wasm.opcode(opcode));
+
+        const int_info = bin_ty.intInfo(self.target);
+        const bitsize = int_info.bits;
+        const is_signed = int_info.signedness == .signed;
+        // if target type bitsize is x < 32 and 32 > x < 64, we perform
+        // result & ((1<<N)-1) where N = bitsize or bitsize -1 incase of signed.
+        if (bitsize != 32 and bitsize < 64) {
+            // first check if we can use a single instruction,
+            // wasm provides those if the integers are signed and 8/16-bit.
+            // For arbitrary integer sizes, we use the algorithm mentioned above.
+            if (is_signed and bitsize == 8) {
+                try self.code.append(wasm.opcode(.i32_extend8_s));
+            } else if (is_signed and bitsize == 16) {
+                try self.code.append(wasm.opcode(.i32_extend16_s));
+            } else {
+                const result = (@as(u64, 1) << @intCast(u6, bitsize - @boolToInt(is_signed))) - 1;
+                if (bitsize < 32) {
+                    try self.code.append(wasm.opcode(.i32_const));
+                    try leb.writeILEB128(self.code.writer(), @bitCast(i32, @intCast(u32, result)));
+                    try self.code.append(wasm.opcode(.i32_and));
+                } else {
+                    try self.code.append(wasm.opcode(.i64_const));
+                    try leb.writeILEB128(self.code.writer(), @bitCast(i64, result));
+                    try self.code.append(wasm.opcode(.i64_and));
+                }
+            }
+        } else if (int_info.bits > 64) {
+            return self.fail("TODO wasm: Integer wrapping for bitsizes larger than 64", .{});
+        }
+
+        return WValue{ .code_offset = offset };
+    }
+
     fn emitConstant(self: *Context, val: Value, ty: Type) InnerError!void {
         const writer = self.code.writer();
         switch (ty.zigTypeTag()) {
test/stage2/wasm.zig
@@ -120,6 +120,20 @@ pub fn addCases(ctx: *TestContext) !void {
             \\}
         , "-2147483648\n");
 
+        case.addCompareOutput(
+            \\pub export fn _start() i32 {
+            \\    var i: i4 = 7;
+            \\    return i +% 1;
+            \\}
+        , "0\n");
+
+        case.addCompareOutput(
+            \\pub export fn _start() u32 {
+            \\    var i: u8 = 255;
+            \\    return i +% 1;
+            \\}
+        , "0\n");
+
         case.addCompareOutput(
             \\pub export fn _start() u32 {
             \\    var i: u32 = 5;
@@ -147,6 +161,20 @@ pub fn addCases(ctx: *TestContext) !void {
             \\}
         , "2147483647\n");
 
+        case.addCompareOutput(
+            \\pub export fn _start() i32 {
+            \\    var i: i7 = -64;
+            \\    return i -% 1;
+            \\}
+        , "63\n");
+
+        case.addCompareOutput(
+            \\pub export fn _start() u32 {
+            \\    var i: u4 = 0;
+            \\    return i -% 1;
+            \\}
+        , "15\n");
+
         case.addCompareOutput(
             \\pub export fn _start() u32 {
             \\    var i: u32 = 5;
@@ -178,6 +206,20 @@ pub fn addCases(ctx: *TestContext) !void {
             \\}
         , "-2\n");
 
+        case.addCompareOutput(
+            \\pub export fn _start() u32 {
+            \\    var i: u3 = 3;
+            \\    return i *% 3;
+            \\}
+        , "1\n");
+
+        case.addCompareOutput(
+            \\pub export fn _start() i32 {
+            \\    var i: i4 = 3;
+            \\    return i *% 3;
+            \\}
+        , "1\n");
+
         case.addCompareOutput(
             \\pub export fn _start() u32 {
             \\    var i: u32 = 352;