authorgravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2022-05-07 23:30:08+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2022-05-07 23:30:08+02:00
logf161d3875ad341971c97384587e2e6c2b50bc09c
treeae6e441e067fa881982f568de54b6c9e53f2e777
parente8c85450feac961a147d37394920081214ba9396
parenta11097958271562fe7e64716356c07d9996fad5f
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #11605 from Luukdegram/wasm-mul-overflow

stage2: wasm - Improve `@mulWithOverflow` implementation

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

src/arch/wasm/CodeGen.zig+176-126
......@@ -1424,7 +1424,7 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue {
14241424 .add_with_overflow => self.airBinOpOverflow(inst, .add),
14251425 .sub_with_overflow => self.airBinOpOverflow(inst, .sub),
14261426 .shl_with_overflow => self.airBinOpOverflow(inst, .shl),
1427 .mul_with_overflow => self.airBinOpOverflow(inst, .mul),
1427 .mul_with_overflow => self.airMulWithOverflow(inst),
14281428
14291429 .clz => self.airClz(inst),
14301430 .ctz => self.airCtz(inst),
......@@ -1822,7 +1822,7 @@ fn store(self: *Self, lhs: WValue, rhs: WValue, ty: Type, offset: u32) InnerErro
18221822
18231823 const opcode = buildOpcode(.{
18241824 .valtype1 = valtype,
1825 .width = abi_size * 8, // use bitsize instead of byte size
1825 .width = abi_size * 8,
18261826 .op = .store,
18271827 });
18281828
......@@ -1852,21 +1852,13 @@ fn airLoad(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
18521852fn load(self: *Self, operand: WValue, ty: Type, offset: u32) InnerError!WValue {
18531853 // load local's value from memory by its stack position
18541854 try self.emitWValue(operand);
1855 // Build the opcode with the right bitsize
1856 const signedness: std.builtin.Signedness = if (ty.isUnsignedInt() or
1857 ty.zigTypeTag() == .ErrorSet or
1858 ty.zigTypeTag() == .Bool)
1859 .unsigned
1860 else
1861 .signed;
18621855
18631856 const abi_size = @intCast(u8, ty.abiSize(self.target));
1864
18651857 const opcode = buildOpcode(.{
18661858 .valtype1 = typeToValtype(ty, self.target),
1867 .width = abi_size * 8, // use bitsize instead of byte size
1859 .width = abi_size * 8,
18681860 .op = .load,
1869 .signedness = signedness,
1861 .signedness = .unsigned,
18701862 });
18711863
18721864 try self.addMemArg(
......@@ -1935,7 +1927,14 @@ fn airWrapBinOp(self: *Self, inst: Air.Inst.Index, op: Op) InnerError!WValue {
19351927 const lhs = try self.resolveInst(bin_op.lhs);
19361928 const rhs = try self.resolveInst(bin_op.rhs);
19371929
1938 return self.wrapBinOp(lhs, rhs, self.air.typeOf(bin_op.lhs), op);
1930 const ty = self.air.typeOf(bin_op.lhs);
1931 if (ty.zigTypeTag() == .Vector) {
1932 return self.fail("TODO: Implement wrapping arithmetic for vectors", .{});
1933 } else if (ty.abiSize(self.target) > 8) {
1934 return self.fail("TODO: Implement wrapping arithmetic for bitsize > 64", .{});
1935 }
1936
1937 return self.wrapBinOp(lhs, rhs, ty, op);
19391938}
19401939
19411940fn wrapBinOp(self: *Self, lhs: WValue, rhs: WValue, ty: Type, op: Op) InnerError!WValue {
......@@ -1948,38 +1947,28 @@ fn wrapBinOp(self: *Self, lhs: WValue, rhs: WValue, ty: Type, op: Op) InnerError
19481947 .signedness = if (ty.isSignedInt()) .signed else .unsigned,
19491948 });
19501949 try self.addTag(Mir.Inst.Tag.fromOpcode(opcode));
1951
1952 const int_info = ty.intInfo(self.target);
1953 const bitsize = int_info.bits;
1954 const is_signed = int_info.signedness == .signed;
1955 // if target type bitsize is x < 32 and 32 > x < 64, we perform
1956 // result & ((1<<N)-1) where N = bitsize or bitsize -1 incase of signed.
1957 if (bitsize != 32 and bitsize < 64) {
1958 // first check if we can use a single instruction,
1959 // wasm provides those if the integers are signed and 8/16-bit.
1960 // For arbitrary integer sizes, we use the algorithm mentioned above.
1961 if (is_signed and bitsize == 8) {
1962 try self.addTag(.i32_extend8_s);
1963 } else if (is_signed and bitsize == 16) {
1964 try self.addTag(.i32_extend16_s);
1965 } else {
1966 const result = (@as(u64, 1) << @intCast(u6, bitsize - @boolToInt(is_signed))) - 1;
1967 if (bitsize < 32) {
1968 try self.addImm32(@bitCast(i32, @intCast(u32, result)));
1969 try self.addTag(.i32_and);
1970 } else {
1971 try self.addImm64(result);
1972 try self.addTag(.i64_and);
1973 }
1974 }
1975 } else if (int_info.bits > 64) {
1976 return self.fail("TODO wasm: Integer wrapping for bitsizes larger than 64", .{});
1977 }
1978
1979 // save the result in a temporary
19801950 const bin_local = try self.allocLocal(ty);
19811951 try self.addLabel(.local_set, bin_local.local);
1982 return bin_local;
1952 return self.wrapOperand(bin_local, ty);
1953}
1954
1955/// Wraps an operand based on a given type's bitsize.
1956/// Asserts `Type` is <= 64bits.
1957fn wrapOperand(self: *Self, operand: WValue, ty: Type) InnerError!WValue {
1958 assert(ty.abiSize(self.target) <= 8);
1959 const result_local = try self.allocLocal(ty);
1960 const bitsize = ty.intInfo(self.target).bits;
1961 const result = @intCast(u64, (@as(u65, 1) << @intCast(u7, bitsize)) - 1);
1962 try self.emitWValue(operand);
1963 if (bitsize <= 32) {
1964 try self.addImm32(@bitCast(i32, @intCast(u32, result)));
1965 try self.addTag(.i32_and);
1966 } else {
1967 try self.addImm64(result);
1968 try self.addTag(.i64_and);
1969 }
1970 try self.addLabel(.local_set, result_local.local);
1971 return result_local;
19831972}
19841973
19851974fn lowerParentPtr(self: *Self, ptr_val: Value, ptr_child_ty: Type) InnerError!WValue {
......@@ -2098,6 +2087,22 @@ fn lowerDeclRefValue(self: *Self, tv: TypedValue, decl_index: Module.Decl.Index)
20982087 } else return WValue{ .memory = target_sym_index };
20992088}
21002089
2090/// Converts a signed integer to its 2's complement form and returns
2091/// an unsigned integer instead.
2092/// Asserts bitsize <= 64
2093fn toTwosComplement(value: anytype, bits: u7) std.meta.Int(.unsigned, @typeInfo(@TypeOf(value)).Int.bits) {
2094 const T = @TypeOf(value);
2095 comptime assert(@typeInfo(T) == .Int);
2096 comptime assert(@typeInfo(T).Int.signedness == .signed);
2097 assert(bits <= 64);
2098 const WantedT = std.meta.Int(.unsigned, @typeInfo(T).Int.bits);
2099 if (value >= 0) return @bitCast(WantedT, value);
2100 const max_value = @intCast(u64, (@as(u65, 1) << bits) - 1);
2101 const flipped = (~-value) + 1;
2102 const result = @bitCast(WantedT, flipped) & max_value;
2103 return @intCast(WantedT, result);
2104}
2105
21012106fn lowerConstant(self: *Self, val: Value, ty: Type) InnerError!WValue {
21022107 if (val.isUndefDeep()) return self.emitUndefined(ty);
21032108 if (val.castTag(.decl_ref)) |decl_ref| {
......@@ -2114,10 +2119,12 @@ fn lowerConstant(self: *Self, val: Value, ty: Type) InnerError!WValue {
21142119 switch (ty.zigTypeTag()) {
21152120 .Int => {
21162121 const int_info = ty.intInfo(self.target);
2117 // write constant
21182122 switch (int_info.signedness) {
21192123 .signed => switch (int_info.bits) {
2120 0...32 => return WValue{ .imm32 = @bitCast(u32, @intCast(i32, val.toSignedInt())) },
2124 0...32 => return WValue{ .imm32 = @intCast(u32, toTwosComplement(
2125 val.toSignedInt(),
2126 @intCast(u6, int_info.bits),
2127 )) },
21212128 33...64 => return WValue{ .imm64 = @bitCast(u64, val.toSignedInt()) },
21222129 else => unreachable,
21232130 },
......@@ -2832,30 +2839,38 @@ fn airIntcast(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
28322839 const ty_op = self.air.instructions.items(.data)[inst].ty_op;
28332840 const ty = self.air.getRefType(ty_op.ty);
28342841 const operand = try self.resolveInst(ty_op.operand);
2835 const ref_ty = self.air.typeOf(ty_op.operand);
2836 const ref_info = ref_ty.intInfo(self.target);
2837 const wanted_info = ty.intInfo(self.target);
2842 const operand_ty = self.air.typeOf(ty_op.operand);
2843 if (ty.abiSize(self.target) > 8 or operand_ty.abiSize(self.target) > 8) {
2844 return self.fail("todo Wasm intcast for bitsize > 64", .{});
2845 }
28382846
2839 const op_bits = toWasmBits(ref_info.bits) orelse
2840 return self.fail("TODO: Wasm intcast integer types of bitsize: {d}", .{ref_info.bits});
2841 const wanted_bits = toWasmBits(wanted_info.bits) orelse
2842 return self.fail("TODO: Wasm intcast integer types of bitsize: {d}", .{wanted_info.bits});
2847 return self.intcast(operand, operand_ty, ty);
2848}
2849
2850/// Upcasts or downcasts an integer based on the given and wanted types,
2851/// and stores the result in a new operand.
2852/// Asserts type's bitsize <= 64
2853fn intcast(self: *Self, operand: WValue, given: Type, wanted: Type) InnerError!WValue {
2854 const given_info = given.intInfo(self.target);
2855 const wanted_info = wanted.intInfo(self.target);
2856 assert(given_info.bits <= 64);
2857 assert(wanted_info.bits <= 64);
28432858
2844 // hot path
2859 const op_bits = toWasmBits(given_info.bits).?;
2860 const wanted_bits = toWasmBits(wanted_info.bits).?;
28452861 if (op_bits == wanted_bits) return operand;
28462862
2863 try self.emitWValue(operand);
28472864 if (op_bits > 32 and wanted_bits == 32) {
2848 try self.emitWValue(operand);
28492865 try self.addTag(.i32_wrap_i64);
28502866 } else if (op_bits == 32 and wanted_bits > 32) {
2851 try self.emitWValue(operand);
2852 try self.addTag(switch (ref_info.signedness) {
2867 try self.addTag(switch (wanted_info.signedness) {
28532868 .signed => .i64_extend_i32_s,
28542869 .unsigned => .i64_extend_i32_u,
28552870 });
28562871 } else unreachable;
28572872
2858 const result = try self.allocLocal(ty);
2873 const result = try self.allocLocal(wanted);
28592874 try self.addLabel(.local_set, result.local);
28602875 return result;
28612876}
......@@ -3072,63 +3087,17 @@ fn airSlicePtr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
30723087}
30733088
30743089fn airTrunc(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
3075 if (self.liveness.isUnused(inst)) return WValue.none;
3090 if (self.liveness.isUnused(inst)) return WValue{ .none = {} };
30763091 const ty_op = self.air.instructions.items(.data)[inst].ty_op;
30773092 const operand = try self.resolveInst(ty_op.operand);
3078 const op_ty = self.air.typeOf(ty_op.operand);
3079 const int_info = self.air.getRefType(ty_op.ty).intInfo(self.target);
3093 const wanted_ty = self.air.getRefType(ty_op.ty);
3094 const int_info = wanted_ty.intInfo(self.target);
30803095 const wanted_bits = int_info.bits;
3081 const result = try self.allocLocal(self.air.getRefType(ty_op.ty));
3082 const op_bits = op_ty.intInfo(self.target).bits;
30833096
3084 const wasm_bits = toWasmBits(wanted_bits) orelse
3097 _ = toWasmBits(wanted_bits) orelse {
30853098 return self.fail("TODO: Implement wasm integer truncation for integer bitsize: {d}", .{wanted_bits});
3086
3087 // Use wasm's instruction to wrap from 64bit to 32bit integer when possible
3088 if (op_bits == 64 and wanted_bits == 32) {
3089 try self.emitWValue(operand);
3090 try self.addTag(.i32_wrap_i64);
3091 try self.addLabel(.local_set, result.local);
3092 return result;
3093 }
3094
3095 // Any other truncation must be done manually
3096 if (int_info.signedness == .unsigned) {
3097 const mask = (@as(u65, 1) << @intCast(u7, wanted_bits)) - 1;
3098 try self.emitWValue(operand);
3099 switch (wasm_bits) {
3100 32 => {
3101 try self.addImm32(@bitCast(i32, @intCast(u32, mask)));
3102 try self.addTag(.i32_and);
3103 },
3104 64 => {
3105 try self.addImm64(@intCast(u64, mask));
3106 try self.addTag(.i64_and);
3107 },
3108 else => unreachable,
3109 }
3110 } else {
3111 const shift_bits = wasm_bits - wanted_bits;
3112 try self.emitWValue(operand);
3113 switch (wasm_bits) {
3114 32 => {
3115 try self.addImm32(@bitCast(i16, shift_bits));
3116 try self.addTag(.i32_shl);
3117 try self.addImm32(@bitCast(i16, shift_bits));
3118 try self.addTag(.i32_shr_s);
3119 },
3120 64 => {
3121 try self.addImm64(shift_bits);
3122 try self.addTag(.i64_shl);
3123 try self.addImm64(shift_bits);
3124 try self.addTag(.i64_shr_s);
3125 },
3126 else => unreachable,
3127 }
3128 }
3129
3130 try self.addLabel(.local_set, result.local);
3131 return result;
3099 };
3100 return self.wrapOperand(operand, wanted_ty);
31323101}
31333102
31343103fn airBoolToInt(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
......@@ -3418,7 +3387,8 @@ fn airFloatToInt(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
34183387
34193388 const result = try self.allocLocal(dest_ty);
34203389 try self.addLabel(.local_set, result.local);
3421 return result;
3390
3391 return self.wrapOperand(result, dest_ty);
34223392}
34233393
34243394fn airIntToFloat(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
......@@ -3922,6 +3892,10 @@ fn airBinOpOverflow(self: *Self, inst: Air.Inst.Index, op: Op) InnerError!WValue
39223892 const rhs = try self.resolveInst(extra.rhs);
39233893 const lhs_ty = self.air.typeOf(extra.lhs);
39243894
3895 if (lhs_ty.zigTypeTag() == .Vector) {
3896 return self.fail("TODO: Implement overflow arithmetic for vectors", .{});
3897 }
3898
39253899 // We store the bit if it's overflowed or not in this. As it's zero-initialized
39263900 // we only need to update it if an overflow (or underflow) occured.
39273901 const overflow_bit = try self.allocLocal(Type.initTag(.u1));
......@@ -4008,24 +3982,100 @@ fn airBinOpOverflow(self: *Self, inst: Air.Inst.Index, op: Op) InnerError!WValue
40083982 }
40093983 try self.addLabel(.local_set, tmp_val.local);
40103984 break :blk tmp_val;
4011 } else if (op == .mul) blk: {
4012 const bin_op = try self.wrapBinOp(lhs, rhs, lhs_ty, op);
4013 try self.startBlock(.block, wasm.block_empty);
4014 // check if 0. true => Break out of block as cannot over -or underflow.
4015 try self.emitWValue(lhs);
4016 switch (wasm_bits) {
4017 32 => try self.addTag(.i32_eqz),
4018 64 => try self.addTag(.i64_eqz),
4019 else => unreachable,
3985 } else try self.wrapBinOp(lhs, rhs, lhs_ty, op);
3986
3987 const result_ptr = try self.allocStack(self.air.typeOfIndex(inst));
3988 try self.store(result_ptr, bin_op, lhs_ty, 0);
3989 const offset = @intCast(u32, lhs_ty.abiSize(self.target));
3990 try self.store(result_ptr, overflow_bit, Type.initTag(.u1), offset);
3991
3992 return result_ptr;
3993}
3994
3995fn airMulWithOverflow(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
3996 const ty_pl = self.air.instructions.items(.data)[inst].ty_pl;
3997 const extra = self.air.extraData(Air.Bin, ty_pl.payload).data;
3998 const lhs = try self.resolveInst(extra.lhs);
3999 const rhs = try self.resolveInst(extra.rhs);
4000 const lhs_ty = self.air.typeOf(extra.lhs);
4001
4002 if (lhs_ty.zigTypeTag() == .Vector) {
4003 return self.fail("TODO: Implement overflow arithmetic for vectors", .{});
4004 }
4005
4006 // We store the bit if it's overflowed or not in this. As it's zero-initialized
4007 // we only need to update it if an overflow (or underflow) occured.
4008 const overflow_bit = try self.allocLocal(Type.initTag(.u1));
4009 const int_info = lhs_ty.intInfo(self.target);
4010 const wasm_bits = toWasmBits(int_info.bits) orelse {
4011 return self.fail("TODO: Implement overflow arithmetic for integer bitsize: {d}", .{int_info.bits});
4012 };
4013
4014 if (wasm_bits == 64) {
4015 return self.fail("TODO: Implement `@mulWithOverflow` for integer bitsize: {d}", .{int_info.bits});
4016 }
4017
4018 const zero = switch (wasm_bits) {
4019 32 => WValue{ .imm32 = 0 },
4020 64 => WValue{ .imm64 = 0 },
4021 else => unreachable,
4022 };
4023
4024 // for 32 bit integers we upcast it to a 64bit integer
4025 const bin_op = if (int_info.bits == 32) blk: {
4026 const new_ty = if (int_info.signedness == .signed) Type.i64 else Type.u64;
4027 const lhs_upcast = try self.intcast(lhs, lhs_ty, new_ty);
4028 const rhs_upcast = try self.intcast(rhs, lhs_ty, new_ty);
4029 const bin_op = try self.binOp(lhs_upcast, rhs_upcast, new_ty, .mul);
4030 if (int_info.signedness == .unsigned) {
4031 const shr = try self.binOp(bin_op, .{ .imm64 = int_info.bits }, new_ty, .shr);
4032 const wrap = try self.intcast(shr, new_ty, lhs_ty);
4033 const cmp_res = try self.cmp(wrap, zero, lhs_ty, .neq);
4034 try self.emitWValue(cmp_res);
4035 try self.addLabel(.local_set, overflow_bit.local);
4036 break :blk try self.intcast(bin_op, new_ty, lhs_ty);
4037 } else {
4038 const down_cast = try self.intcast(bin_op, new_ty, lhs_ty);
4039 const shr = try self.binOp(down_cast, .{ .imm32 = int_info.bits - 1 }, lhs_ty, .shr);
4040
4041 const shr_res = try self.binOp(bin_op, .{ .imm64 = int_info.bits }, new_ty, .shr);
4042 const down_shr_res = try self.intcast(shr_res, new_ty, lhs_ty);
4043 const cmp_res = try self.cmp(down_shr_res, shr, lhs_ty, .neq);
4044 try self.emitWValue(cmp_res);
4045 try self.addLabel(.local_set, overflow_bit.local);
4046 break :blk down_cast;
40204047 }
4021 try self.addLabel(.br_if, 0);
4022 const div = try self.binOp(bin_op, lhs, lhs_ty, .div);
4023 const cmp_res = try self.cmp(div, rhs, lhs_ty, .neq);
4024 try self.emitWValue(cmp_res);
4048 } else if (int_info.signedness == .signed) blk: {
4049 const shift_imm = if (wasm_bits == 32)
4050 WValue{ .imm32 = wasm_bits - int_info.bits }
4051 else
4052 WValue{ .imm64 = wasm_bits - int_info.bits };
4053
4054 const lhs_shl = try self.binOp(lhs, shift_imm, lhs_ty, .shl);
4055 const lhs_shr = try self.binOp(lhs_shl, shift_imm, lhs_ty, .shr);
4056 const rhs_shl = try self.binOp(rhs, shift_imm, lhs_ty, .shl);
4057 const rhs_shr = try self.binOp(rhs_shl, shift_imm, lhs_ty, .shr);
4058
4059 const bin_op = try self.binOp(lhs_shr, rhs_shr, lhs_ty, .mul);
4060 const shl = try self.binOp(bin_op, shift_imm, lhs_ty, .shl);
4061 const shr = try self.binOp(shl, shift_imm, lhs_ty, .shr);
4062
4063 const cmp_op = try self.cmp(shr, bin_op, lhs_ty, .neq);
4064 try self.emitWValue(cmp_op);
40254065 try self.addLabel(.local_set, overflow_bit.local);
4026 try self.endBlock();
4027 break :blk bin_op;
4028 } else try self.wrapBinOp(lhs, rhs, lhs_ty, op);
4066 break :blk try self.wrapOperand(bin_op, lhs_ty);
4067 } else blk: {
4068 const bin_op = try self.binOp(lhs, rhs, lhs_ty, .mul);
4069 const shift_imm = if (wasm_bits == 32)
4070 WValue{ .imm32 = int_info.bits }
4071 else
4072 WValue{ .imm64 = int_info.bits };
4073 const shr = try self.binOp(bin_op, shift_imm, lhs_ty, .shr);
4074 const cmp_op = try self.cmp(shr, zero, lhs_ty, .neq);
4075 try self.emitWValue(cmp_op);
4076 try self.addLabel(.local_set, overflow_bit.local);
4077 break :blk try self.wrapOperand(bin_op, lhs_ty);
4078 };
40294079
40304080 const result_ptr = try self.allocStack(self.air.typeOfIndex(inst));
40314081 try self.store(result_ptr, bin_op, lhs_ty, 0);
src/type.zig+1
......@@ -6005,6 +6005,7 @@ pub const Type = extern union {
60056005 pub const @"u64" = initTag(.u64);
60066006
60076007 pub const @"i32" = initTag(.i32);
6008 pub const @"i64" = initTag(.i64);
60086009
60096010 pub const @"f16" = initTag(.f16);
60106011 pub const @"f32" = initTag(.f32);
test/behavior/math.zig+6-1
......@@ -687,7 +687,6 @@ test "basic @mulWithOverflow" {
687687test "extensive @mulWithOverflow" {
688688 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
689689 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
690 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
691690
692691 {
693692 var a: u5 = 3;
......@@ -833,6 +832,12 @@ test "extensive @mulWithOverflow" {
833832 try expect(@mulWithOverflow(i32, a, b, &res));
834833 try expect(res == 0x7fffffff);
835834 }
835}
836
837test "@mulWithOverflow bitsize > 32" {
838 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; // TODO
839 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
840 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
836841
837842 {
838843 var a: u62 = 3;
test/cases/binary_operands.13.zig+1-1
......@@ -1,6 +1,6 @@
11pub fn main() void {
22 var i: i4 = 3;
3 if (i *% 3 != 1) unreachable;
3 if (i *% 3 != -7) unreachable;
44 return;
55}
66
test/cases/binary_operands.2.zig+1-1
......@@ -1,6 +1,6 @@
11pub fn main() void {
22 var i: i4 = 7;
3 if (i +% 1 != 0) unreachable;
3 if (i +% 1 != -8) unreachable;
44 return;
55}
66