authorgravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-05-30 21:07:03+02:00
committergravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-06-24 08:12:17+02:00
log5ebaf49ebb65f3b201e56ec7d89b7d91f189409e
tree7dff0022ba12267782afe07a9236ef1e9b2f2756
parent241180216f92c86f833910d73e1c86f54ae940e4

wasm: Implement basic f16 support

This implements binary operations and comparisons for floats with bitsize 16. It does this by calling into compiler-rt to first extend the float to 32 bits, perform the operation, and then finally truncate back to 16 bits. When loading and storing the f16, we do this as an unsigned 16bit integer.

1 files changed, 105 insertions(+), 19 deletions(-)

src/arch/wasm/CodeGen.zig+105-19
......@@ -215,8 +215,7 @@ fn buildOpcode(args: OpcodeBuildArguments) wasm.Opcode {
215215 16 => switch (args.valtype1.?) {
216216 .i32 => if (args.signedness.? == .signed) return .i32_load16_s else return .i32_load16_u,
217217 .i64 => if (args.signedness.? == .signed) return .i64_load16_s else return .i64_load16_u,
218 .f32 => return .f32_load,
219 .f64 => unreachable,
218 .f32, .f64 => unreachable,
220219 },
221220 32 => switch (args.valtype1.?) {
222221 .i64 => if (args.signedness.? == .signed) return .i64_load32_s else return .i64_load32_u,
......@@ -246,8 +245,7 @@ fn buildOpcode(args: OpcodeBuildArguments) wasm.Opcode {
246245 16 => switch (args.valtype1.?) {
247246 .i32 => return .i32_store16,
248247 .i64 => return .i64_store16,
249 .f32 => return .f32_store,
250 .f64 => unreachable,
248 .f32, .f64 => unreachable,
251249 },
252250 32 => switch (args.valtype1.?) {
253251 .i64 => return .i64_store32,
......@@ -725,7 +723,8 @@ fn typeToValtype(ty: Type, target: std.Target) wasm.Valtype {
725723 return switch (ty.zigTypeTag()) {
726724 .Float => blk: {
727725 const bits = ty.floatBits(target);
728 if (bits == 16 or bits == 32) break :blk wasm.Valtype.f32;
726 if (bits == 16) return wasm.Valtype.i32; // stored/loaded as u16
727 if (bits == 32) break :blk wasm.Valtype.f32;
729728 if (bits == 64) break :blk wasm.Valtype.f64;
730729 if (bits == 128) break :blk wasm.Valtype.i64;
731730 return wasm.Valtype.i32; // represented as pointer to stack
......@@ -2013,6 +2012,10 @@ fn binOp(self: *Self, lhs: WValue, rhs: WValue, ty: Type, op: Op) InnerError!WVa
20132012 }
20142013 }
20152014
2015 if (ty.isAnyFloat() and ty.floatBits(self.target) == 16) {
2016 return self.binOpFloat16(lhs, rhs, op);
2017 }
2018
20162019 const opcode: wasm.Opcode = buildOpcode(.{
20172020 .op = op,
20182021 .valtype1 = typeToValtype(ty, self.target),
......@@ -2029,6 +2032,20 @@ fn binOp(self: *Self, lhs: WValue, rhs: WValue, ty: Type, op: Op) InnerError!WVa
20292032 return bin_local;
20302033}
20312034
2035fn binOpFloat16(self: *Self, lhs: WValue, rhs: WValue, op: Op) InnerError!WValue {
2036 const ext_lhs = try self.fpext(lhs, Type.f16, Type.f32);
2037 const ext_rhs = try self.fpext(rhs, Type.f16, Type.f32);
2038
2039 const opcode: wasm.Opcode = buildOpcode(.{ .op = op, .valtype1 = .f32, .signedness = .unsigned });
2040 try self.emitWValue(ext_lhs);
2041 try self.emitWValue(ext_rhs);
2042 try self.addTag(Mir.Inst.Tag.fromOpcode(opcode));
2043
2044 // re-use temporary local
2045 try self.addLabel(.local_set, ext_lhs.local);
2046 return self.fptrunc(ext_lhs, Type.f32, Type.f16);
2047}
2048
20322049fn binOpBigInt(self: *Self, lhs: WValue, rhs: WValue, ty: Type, op: Op) InnerError!WValue {
20332050 if (ty.intInfo(self.target).bits > 128) {
20342051 return self.fail("TODO: Implement binary operation for big integer", .{});
......@@ -2310,8 +2327,9 @@ fn lowerConstant(self: *Self, val: Value, ty: Type) InnerError!WValue {
23102327 },
23112328 .Bool => return WValue{ .imm32 = @intCast(u32, val.toUnsignedInt(target)) },
23122329 .Float => switch (ty.floatBits(self.target)) {
2313 0...32 => return WValue{ .float32 = val.toFloat(f32) },
2314 33...64 => return WValue{ .float64 = val.toFloat(f64) },
2330 16 => return WValue{ .imm32 = @bitCast(u16, val.toFloat(f16)) },
2331 32 => return WValue{ .float32 = val.toFloat(f32) },
2332 64 => return WValue{ .float64 = val.toFloat(f64) },
23152333 else => unreachable,
23162334 },
23172335 .Pointer => switch (val.tag()) {
......@@ -2389,8 +2407,9 @@ fn emitUndefined(self: *Self, ty: Type) InnerError!WValue {
23892407 else => unreachable,
23902408 },
23912409 .Float => switch (ty.floatBits(self.target)) {
2392 0...32 => return WValue{ .float32 = @bitCast(f32, @as(u32, 0xaaaaaaaa)) },
2393 33...64 => return WValue{ .float64 = @bitCast(f64, @as(u64, 0xaaaaaaaaaaaaaaaa)) },
2410 16 => return WValue{ .imm32 = 0xaaaaaaaa },
2411 32 => return WValue{ .float32 = @bitCast(f32, @as(u32, 0xaaaaaaaa)) },
2412 64 => return WValue{ .float64 = @bitCast(f64, @as(u64, 0xaaaaaaaaaaaaaaaa)) },
23942413 else => unreachable,
23952414 },
23962415 .Pointer => switch (self.arch()) {
......@@ -2562,6 +2581,8 @@ fn cmp(self: *Self, lhs: WValue, rhs: WValue, ty: Type, op: std.math.CompareOper
25622581 }
25632582 } else if (isByRef(ty, self.target)) {
25642583 return self.cmpBigInt(lhs, rhs, ty, op);
2584 } else if (ty.isAnyFloat() and ty.floatBits(self.target) == 16) {
2585 return self.cmpFloat16(lhs, rhs, op);
25652586 }
25662587
25672588 // ensure that when we compare pointers, we emit
......@@ -2595,6 +2616,31 @@ fn cmp(self: *Self, lhs: WValue, rhs: WValue, ty: Type, op: std.math.CompareOper
25952616 return cmp_tmp;
25962617}
25972618
2619fn cmpFloat16(self: *Self, lhs: WValue, rhs: WValue, op: std.math.CompareOperator) InnerError!WValue {
2620 const ext_lhs = try self.fpext(lhs, Type.f16, Type.f32);
2621 const ext_rhs = try self.fpext(rhs, Type.f16, Type.f32);
2622
2623 const opcode: wasm.Opcode = buildOpcode(.{
2624 .op = switch (op) {
2625 .lt => .lt,
2626 .lte => .le,
2627 .eq => .eq,
2628 .neq => .ne,
2629 .gte => .ge,
2630 .gt => .gt,
2631 },
2632 .valtype1 = .f32,
2633 .signedness = .unsigned,
2634 });
2635 try self.emitWValue(ext_lhs);
2636 try self.emitWValue(ext_rhs);
2637 try self.addTag(Mir.Inst.Tag.fromOpcode(opcode));
2638
2639 const result = try self.allocLocal(Type.initTag(.i32)); // bool is always i32
2640 try self.addLabel(.local_set, result.local);
2641 return result;
2642}
2643
25982644fn airCmpVector(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
25992645 _ = inst;
26002646 return self.fail("TODO implement airCmpVector for wasm", .{});
......@@ -3934,19 +3980,44 @@ fn airFpext(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
39343980
39353981 const ty_op = self.air.instructions.items(.data)[inst].ty_op;
39363982 const dest_ty = self.air.typeOfIndex(inst);
3937 const dest_bits = dest_ty.floatBits(self.target);
3938 const src_bits = self.air.typeOf(ty_op.operand).floatBits(self.target);
39393983 const operand = try self.resolveInst(ty_op.operand);
39403984
3941 if (dest_bits == 64 and src_bits == 32) {
3942 const result = try self.allocLocal(dest_ty);
3985 return self.fpext(operand, self.air.typeOf(ty_op.operand), dest_ty);
3986}
3987
3988fn fpext(self: *Self, operand: WValue, given: Type, wanted: Type) InnerError!WValue {
3989 const given_bits = given.floatBits(self.target);
3990 const wanted_bits = wanted.floatBits(self.target);
3991
3992 if (wanted_bits == 64 and given_bits == 32) {
3993 const result = try self.allocLocal(wanted);
39433994 try self.emitWValue(operand);
39443995 try self.addTag(.f64_promote_f32);
39453996 try self.addLabel(.local_set, result.local);
39463997 return result;
3998 } else if (given_bits == 16) {
3999 // call __extendhfsf2(f16) f32
4000 const f32_result = try self.callIntrinsic(
4001 "__extendhfsf2",
4002 &.{Type.f16},
4003 Type.f32,
4004 &.{operand},
4005 );
4006
4007 if (wanted_bits == 32) {
4008 return f32_result;
4009 }
4010 if (wanted_bits == 64) {
4011 const result = try self.allocLocal(wanted);
4012 try self.emitWValue(f32_result);
4013 try self.addTag(.f64_promote_f32);
4014 try self.addLabel(.local_set, result.local);
4015 return result;
4016 }
4017 return self.fail("TODO: Implement 'fpext' for floats with bitsize: {d}", .{wanted_bits});
39474018 } else {
39484019 // TODO: Emit a call to compiler-rt to extend the float. e.g. __extendhfsf2
3949 return self.fail("TODO: Implement 'fpext' for floats with bitsize: {d}", .{dest_bits});
4020 return self.fail("TODO: Implement 'fpext' for floats with bitsize: {d}", .{wanted_bits});
39504021 }
39514022}
39524023
......@@ -3955,19 +4026,34 @@ fn airFptrunc(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
39554026
39564027 const ty_op = self.air.instructions.items(.data)[inst].ty_op;
39574028 const dest_ty = self.air.typeOfIndex(inst);
3958 const dest_bits = dest_ty.floatBits(self.target);
3959 const src_bits = self.air.typeOf(ty_op.operand).floatBits(self.target);
39604029 const operand = try self.resolveInst(ty_op.operand);
4030 return self.fptrunc(operand, self.air.typeOf(ty_op.operand), dest_ty);
4031}
39614032
3962 if (dest_bits == 32 and src_bits == 64) {
3963 const result = try self.allocLocal(dest_ty);
4033fn fptrunc(self: *Self, operand: WValue, given: Type, wanted: Type) InnerError!WValue {
4034 const given_bits = given.floatBits(self.target);
4035 const wanted_bits = wanted.floatBits(self.target);
4036
4037 if (wanted_bits == 32 and given_bits == 64) {
4038 const result = try self.allocLocal(wanted);
39644039 try self.emitWValue(operand);
39654040 try self.addTag(.f32_demote_f64);
39664041 try self.addLabel(.local_set, result.local);
39674042 return result;
4043 } else if (wanted_bits == 16) {
4044 const op: WValue = if (given_bits == 64) blk: {
4045 const tmp = try self.allocLocal(Type.f32);
4046 try self.emitWValue(operand);
4047 try self.addTag(.f32_demote_f64);
4048 try self.addLabel(.local_set, tmp.local);
4049 break :blk tmp;
4050 } else operand;
4051
4052 // call __truncsfhf2(f32) f16
4053 return self.callIntrinsic("__truncsfhf2", &.{Type.f32}, Type.f16, &.{op});
39684054 } else {
39694055 // TODO: Emit a call to compiler-rt to trunc the float. e.g. __truncdfhf2
3970 return self.fail("TODO: Implement 'fptrunc' for floats with bitsize: {d}", .{dest_bits});
4056 return self.fail("TODO: Implement 'fptrunc' for floats with bitsize: {d}", .{wanted_bits});
39714057 }
39724058}
39734059