authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2023-06-14 00:51:31+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-06-16 13:33:31-07:00
logc4cc796695dddbfba749514823ed29bda37b7bcd
tree8cf201509609767dbe5a8301e4e1e58824576275
parent5d9e8f27d0dc131e0b4154c5f65376f2fb9f3500

Sema: consider type bounds when refining result type of `@min`/`@max`

I achieved this through a major refactor of the logic of analyzeMinMax. This change should be compatible with vectors of comptime_int, which Andrew said are supposed to work (but which currently do not).

3 files changed, 238 insertions(+), 111 deletions(-)

src/Sema.zig+139-111
...@@ -22984,104 +22984,127 @@ fn analyzeMinMax(...@@ -22984,104 +22984,127 @@ fn analyzeMinMax(
22984 else => @compileError("unreachable"),22984 else => @compileError("unreachable"),
22985 };22985 };
2298622986
22987 // First, find all comptime-known arguments, and get their min/max22987 // The set of runtime-known operands. Set up in the loop below.
22988 var runtime_known = try std.DynamicBitSet.initFull(sema.arena, operands.len);22988 var runtime_known = try std.DynamicBitSet.initFull(sema.arena, operands.len);
22989 // The current minmax value - initially this will always be comptime-known, then we'll add
22990 // runtime values into the mix later.
22989 var cur_minmax: ?Air.Inst.Ref = null;22991 var cur_minmax: ?Air.Inst.Ref = null;
22990 var cur_minmax_src: LazySrcLoc = undefined; // defined if cur_minmax not null22992 var cur_minmax_src: LazySrcLoc = undefined; // defined if cur_minmax not null
22993 // The current known scalar bounds of the value.
22994 var bounds_status: enum {
22995 unknown, // We've only seen undef comptime_ints so far, so do not know the bounds.
22996 defined, // We've seen only integers, so the bounds are defined.
22997 non_integral, // There are floats in the mix, so the bounds aren't defined.
22998 } = .unknown;
22999 var cur_min_scalar: Value = undefined;
23000 var cur_max_scalar: Value = undefined;
23001
23002 // First, find all comptime-known arguments, and get their min/max
23003
22991 for (operands, operand_srcs, 0..) |operand, operand_src, operand_idx| {23004 for (operands, operand_srcs, 0..) |operand, operand_src, operand_idx| {
22992 // Resolve the value now to avoid redundant calls to `checkSimdBinOp` - we'll have to call23005 // Resolve the value now to avoid redundant calls to `checkSimdBinOp` - we'll have to call
22993 // it in the runtime path anyway since the result type may have been refined23006 // it in the runtime path anyway since the result type may have been refined
22994 const uncasted_operand_val = (try sema.resolveMaybeUndefVal(operand)) orelse continue;23007 const unresolved_uncoerced_val = try sema.resolveMaybeUndefVal(operand) orelse continue;
22995 if (cur_minmax) |cur| {23008 const uncoerced_val = try sema.resolveLazyValue(unresolved_uncoerced_val);
22996 const simd_op = try sema.checkSimdBinOp(block, src, cur, operand, cur_minmax_src, operand_src);23009
22997 const cur_val = simd_op.lhs_val.?; // cur_minmax is comptime-known23010 runtime_known.unset(operand_idx);
22998 const operand_val = simd_op.rhs_val.?; // we checked the operand was resolvable above23011
2299923012 switch (bounds_status) {
23000 runtime_known.unset(operand_idx);23013 .unknown, .defined => refine_bounds: {
23014 const ty = sema.typeOf(operand);
23015 if (!ty.scalarType(mod).isInt(mod) and !ty.scalarType(mod).eql(Type.comptime_int, mod)) {
23016 bounds_status = .non_integral;
23017 break :refine_bounds;
23018 }
23019 const scalar_bounds: ?[2]Value = bounds: {
23020 if (!ty.isVector(mod)) break :bounds try uncoerced_val.intValueBounds(mod);
23021 var cur_bounds: [2]Value = try Value.intValueBounds(try uncoerced_val.elemValue(mod, 0), mod) orelse break :bounds null;
23022 const len = try sema.usizeCast(block, src, ty.vectorLen(mod));
23023 for (1..len) |i| {
23024 const elem = try uncoerced_val.elemValue(mod, i);
23025 const elem_bounds = try elem.intValueBounds(mod) orelse break :bounds null;
23026 cur_bounds = .{
23027 Value.numberMin(elem_bounds[0], cur_bounds[0], mod),
23028 Value.numberMax(elem_bounds[1], cur_bounds[1], mod),
23029 };
23030 }
23031 break :bounds cur_bounds;
23032 };
23033 if (scalar_bounds) |bounds| {
23034 if (bounds_status == .unknown) {
23035 cur_min_scalar = bounds[0];
23036 cur_max_scalar = bounds[1];
23037 bounds_status = .defined;
23038 } else {
23039 cur_min_scalar = opFunc(cur_min_scalar, bounds[0], mod);
23040 cur_max_scalar = opFunc(cur_max_scalar, bounds[1], mod);
23041 }
23042 }
23043 },
23044 .non_integral => {},
23045 }
2300123046
23002 if (cur_val.isUndef(mod)) continue; // result is also undef23047 const cur = cur_minmax orelse {
23003 if (operand_val.isUndef(mod)) {23048 cur_minmax = operand;
23004 cur_minmax = try sema.addConstUndef(simd_op.result_ty);23049 cur_minmax_src = operand_src;
23005 continue;23050 continue;
23006 }23051 };
2300723052
23008 const resolved_cur_val = try sema.resolveLazyValue(cur_val);23053 const simd_op = try sema.checkSimdBinOp(block, src, cur, operand, cur_minmax_src, operand_src);
23009 const resolved_operand_val = try sema.resolveLazyValue(operand_val);23054 const cur_val = try sema.resolveLazyValue(simd_op.lhs_val.?); // cur_minmax is comptime-known
23055 const operand_val = try sema.resolveLazyValue(simd_op.rhs_val.?); // we checked the operand was resolvable above
2301023056
23011 const vec_len = simd_op.len orelse {23057 const vec_len = simd_op.len orelse {
23012 const result_val = opFunc(resolved_cur_val, resolved_operand_val, mod);23058 const result_val = opFunc(cur_val, operand_val, mod);
23013 cur_minmax = try sema.addConstant(simd_op.result_ty, result_val);23059 cur_minmax = try sema.addConstant(simd_op.result_ty, result_val);
23014 continue;23060 continue;
23015 };23061 };
23016 const elems = try sema.arena.alloc(InternPool.Index, vec_len);23062 const elems = try sema.arena.alloc(InternPool.Index, vec_len);
23017 for (elems, 0..) |*elem, i| {23063 for (elems, 0..) |*elem, i| {
23018 const lhs_elem_val = try resolved_cur_val.elemValue(mod, i);23064 const lhs_elem_val = try cur_val.elemValue(mod, i);
23019 const rhs_elem_val = try resolved_operand_val.elemValue(mod, i);23065 const rhs_elem_val = try operand_val.elemValue(mod, i);
23020 elem.* = try opFunc(lhs_elem_val, rhs_elem_val, mod).intern(simd_op.scalar_ty, mod);23066 const uncoerced_elem = opFunc(lhs_elem_val, rhs_elem_val, mod);
23021 }23067 elem.* = (try mod.getCoerced(uncoerced_elem, simd_op.scalar_ty)).toIntern();
23022 cur_minmax = try sema.addConstant(simd_op.result_ty, (try mod.intern(.{ .aggregate = .{
23023 .ty = simd_op.result_ty.toIntern(),
23024 .storage = .{ .elems = elems },
23025 } })).toValue());
23026 } else {
23027 runtime_known.unset(operand_idx);
23028 cur_minmax = try sema.addConstant(sema.typeOf(operand), uncasted_operand_val);
23029 cur_minmax_src = operand_src;
23030 }23068 }
23069 cur_minmax = try sema.addConstant(simd_op.result_ty, (try mod.intern(.{ .aggregate = .{
23070 .ty = simd_op.result_ty.toIntern(),
23071 .storage = .{ .elems = elems },
23072 } })).toValue());
23031 }23073 }
2303223074
23033 const opt_runtime_idx = runtime_known.findFirstSet();23075 const opt_runtime_idx = runtime_known.findFirstSet();
2303423076
23035 const comptime_refined_ty: ?Type = if (cur_minmax) |ct_minmax_ref| refined: {23077 if (cur_minmax) |ct_minmax_ref| refine: {
23036 // Refine the comptime-known result type based on the operation23078 // Refine the comptime-known result type based on the bounds. This isn't strictly necessary
23079 // in the runtime case, since we'll refine the type again later, but keeping things as small
23080 // as possible will allow us to emit more optimal AIR (if all the runtime operands have
23081 // smaller types than the non-refined comptime type).
23082
23037 const val = (try sema.resolveMaybeUndefVal(ct_minmax_ref)).?;23083 const val = (try sema.resolveMaybeUndefVal(ct_minmax_ref)).?;
23038 const orig_ty = sema.typeOf(ct_minmax_ref);23084 const orig_ty = sema.typeOf(ct_minmax_ref);
2303923085
23040 if (opt_runtime_idx == null and orig_ty.eql(Type.comptime_int, mod)) {23086 if (opt_runtime_idx == null and orig_ty.scalarType(mod).eql(Type.comptime_int, mod)) {
23041 // If all arguments were `comptime_int`, and there are no runtime args, we'll preserve that type23087 // If all arguments were `comptime_int`, and there are no runtime args, we'll preserve that type
23042 break :refined orig_ty;23088 break :refine;
23043 }23089 }
2304423090
23045 const refined_ty = if (orig_ty.zigTypeTag(mod) == .Vector) blk: {23091 // We can't refine float types
23046 const elem_ty = orig_ty.childType(mod);23092 if (orig_ty.scalarType(mod).isAnyFloat()) break :refine;
23047 const len = orig_ty.vectorLen(mod);
23048
23049 if (len == 0) break :blk orig_ty;
23050 if (elem_ty.isAnyFloat()) break :blk orig_ty; // can't refine floats
2305123093
23052 var cur_min: Value = try val.elemValue(mod, 0);23094 assert(bounds_status == .defined); // there was a non-comptime-int integral comptime-known arg
23053 var cur_max: Value = cur_min;
23054 for (1..len) |idx| {
23055 const elem_val = try val.elemValue(mod, idx);
23056 if (elem_val.isUndef(mod)) break :blk orig_ty; // can't refine undef
23057 if (Value.order(elem_val, cur_min, mod).compare(.lt)) cur_min = elem_val;
23058 if (Value.order(elem_val, cur_max, mod).compare(.gt)) cur_max = elem_val;
23059 }
2306023095
23061 const refined_elem_ty = try mod.intFittingRange(cur_min, cur_max);23096 const refined_scalar_ty = try mod.intFittingRange(cur_min_scalar, cur_max_scalar);
23062 break :blk try mod.vectorType(.{23097 const refined_ty = if (orig_ty.isVector(mod)) try mod.vectorType(.{
23063 .len = len,23098 .len = orig_ty.vectorLen(mod),
23064 .child = refined_elem_ty.toIntern(),23099 .child = refined_scalar_ty.toIntern(),
23065 });23100 }) else refined_scalar_ty;
23066 } else blk: {
23067 if (orig_ty.isAnyFloat()) break :blk orig_ty; // can't refine floats
23068 if (val.isUndef(mod)) break :blk orig_ty; // can't refine undef
23069 break :blk try mod.intFittingRange(val, val);
23070 };
2307123101
23072 // Apply the refined type to the current value - this isn't strictly necessary in the23102 // Apply the refined type to the current value
23073 // runtime case since we'll refine again afterwards, but keeping things as small as possible23103 if (std.debug.runtime_safety) {
23074 // will allow us to emit more optimal AIR (if all the runtime operands have smaller types23104 assert(try sema.intFitsInType(val, refined_ty, null));
23075 // than the non-refined comptime type).
23076 if (!refined_ty.eql(orig_ty, mod)) {
23077 if (std.debug.runtime_safety) {
23078 assert(try sema.intFitsInType(val, refined_ty, null));
23079 }
23080 cur_minmax = try sema.coerceInMemory(val, refined_ty);
23081 }23105 }
2308223106 cur_minmax = try sema.coerceInMemory(val, refined_ty);
23083 break :refined refined_ty;23107 }
23084 } else null;
2308523108
23086 const runtime_idx = opt_runtime_idx orelse return cur_minmax.?;23109 const runtime_idx = opt_runtime_idx orelse return cur_minmax.?;
23087 const runtime_src = operand_srcs[runtime_idx];23110 const runtime_src = operand_srcs[runtime_idx];
...@@ -23102,6 +23125,11 @@ fn analyzeMinMax(...@@ -23102,6 +23125,11 @@ fn analyzeMinMax(
23102 cur_minmax = operands[0];23125 cur_minmax = operands[0];
23103 cur_minmax_src = runtime_src;23126 cur_minmax_src = runtime_src;
23104 runtime_known.unset(0); // don't look at this operand in the loop below23127 runtime_known.unset(0); // don't look at this operand in the loop below
23128 const scalar_ty = sema.typeOf(cur_minmax.?).scalarType(mod);
23129 if (scalar_ty.isInt(mod)) {
23130 cur_min_scalar = try scalar_ty.minInt(mod, scalar_ty);
23131 cur_max_scalar = try scalar_ty.maxInt(mod, scalar_ty);
23132 }
23105 }23133 }
2310623134
23107 var it = runtime_known.iterator(.{});23135 var it = runtime_known.iterator(.{});
...@@ -23112,49 +23140,49 @@ fn analyzeMinMax(...@@ -23112,49 +23140,49 @@ fn analyzeMinMax(
23112 const rhs_src = operand_srcs[idx];23140 const rhs_src = operand_srcs[idx];
23113 const simd_op = try sema.checkSimdBinOp(block, src, lhs, rhs, lhs_src, rhs_src);23141 const simd_op = try sema.checkSimdBinOp(block, src, lhs, rhs, lhs_src, rhs_src);
23114 if (known_undef) {23142 if (known_undef) {
23115 cur_minmax = try sema.addConstant(simd_op.result_ty, Value.undef);23143 cur_minmax = try sema.addConstUndef(simd_op.result_ty);
23116 } else {23144 } else {
23117 cur_minmax = try block.addBinOp(air_tag, simd_op.lhs, simd_op.rhs);23145 cur_minmax = try block.addBinOp(air_tag, simd_op.lhs, simd_op.rhs);
23118 }23146 }
23147 // Compute the bounds of this type
23148 switch (bounds_status) {
23149 .unknown, .defined => refine_bounds: {
23150 const scalar_ty = sema.typeOf(rhs).scalarType(mod);
23151 if (scalar_ty.isAnyFloat()) {
23152 bounds_status = .non_integral;
23153 break :refine_bounds;
23154 }
23155 const scalar_min = try scalar_ty.minInt(mod, scalar_ty);
23156 const scalar_max = try scalar_ty.maxInt(mod, scalar_ty);
23157 if (bounds_status == .unknown) {
23158 cur_min_scalar = scalar_min;
23159 cur_max_scalar = scalar_max;
23160 bounds_status = .defined;
23161 } else {
23162 cur_min_scalar = opFunc(cur_min_scalar, scalar_min, mod);
23163 cur_max_scalar = opFunc(cur_max_scalar, scalar_max, mod);
23164 }
23165 },
23166 .non_integral => {},
23167 }
23119 }23168 }
2312023169
23121 if (comptime_refined_ty) |comptime_ty| refine: {23170 // Finally, refine the type based on the known bounds.
23122 // Finally, refine the type based on the comptime-known bound.23171 const unrefined_ty = sema.typeOf(cur_minmax.?);
23123 if (known_undef) break :refine; // can't refine undef23172 if (unrefined_ty.scalarType(mod).isAnyFloat()) {
23124 const unrefined_ty = sema.typeOf(cur_minmax.?);23173 // We can't refine floats, so we're done.
23125 const is_vector = unrefined_ty.zigTypeTag(mod) == .Vector;23174 return cur_minmax.?;
23126 const comptime_elem_ty = if (is_vector) comptime_ty.childType(mod) else comptime_ty;23175 }
23127 const unrefined_elem_ty = if (is_vector) unrefined_ty.childType(mod) else unrefined_ty;23176 assert(bounds_status == .defined); // there were integral runtime operands
2312823177 const refined_scalar_ty = try mod.intFittingRange(cur_min_scalar, cur_max_scalar);
23129 if (unrefined_elem_ty.isAnyFloat()) break :refine; // we can't refine floats23178 const refined_ty = if (unrefined_ty.isVector(mod)) try mod.vectorType(.{
2313023179 .len = unrefined_ty.vectorLen(mod),
23131 // Compute the final bounds based on the runtime type and the comptime-known bound type23180 .child = refined_scalar_ty.toIntern(),
23132 const min_val = switch (air_tag) {23181 }) else refined_scalar_ty;
23133 .min => try unrefined_elem_ty.minInt(mod, unrefined_elem_ty),
23134 .max => try comptime_elem_ty.minInt(mod, comptime_elem_ty), // @max(ct, rt) >= ct
23135 else => unreachable,
23136 };
23137 const max_val = switch (air_tag) {
23138 .min => try comptime_elem_ty.maxInt(mod, comptime_elem_ty), // @min(ct, rt) <= ct
23139 .max => try unrefined_elem_ty.maxInt(mod, unrefined_elem_ty),
23140 else => unreachable,
23141 };
23142
23143 // Find the smallest type which can contain these bounds
23144 const final_elem_ty = try mod.intFittingRange(min_val, max_val);
23145
23146 const final_ty = if (is_vector)
23147 try mod.vectorType(.{
23148 .len = unrefined_ty.vectorLen(mod),
23149 .child = final_elem_ty.toIntern(),
23150 })
23151 else
23152 final_elem_ty;
2315323182
23154 if (!final_ty.eql(unrefined_ty, mod)) {23183 if (!refined_ty.eql(unrefined_ty, mod)) {
23155 // We've reduced the type - cast the result down23184 // We've reduced the type - cast the result down
23156 return block.addTyOp(.intcast, final_ty, cur_minmax.?);23185 return block.addTyOp(.intcast, refined_ty, cur_minmax.?);
23157 }
23158 }23186 }
2315923187
23160 return cur_minmax.?;23188 return cur_minmax.?;
src/value.zig+14
...@@ -4146,6 +4146,20 @@ pub const Value = struct {...@@ -4146,6 +4146,20 @@ pub const Value = struct {
4146 return val.toIntern() == .generic_poison;4146 return val.toIntern() == .generic_poison;
4147 }4147 }
41484148
4149 /// For an integer (comptime or fixed-width) `val`, returns the comptime-known bounds of the value.
4150 /// If `val` is not undef, the bounds are both `val`.
4151 /// If `val` is undef and has a fixed-width type, the bounds are the bounds of the type.
4152 /// If `val` is undef and is a `comptime_int`, returns null.
4153 pub fn intValueBounds(val: Value, mod: *Module) !?[2]Value {
4154 if (!val.isUndef(mod)) return .{ val, val };
4155 const ty = mod.intern_pool.typeOf(val.toIntern());
4156 if (ty == .comptime_int_type) return null;
4157 return .{
4158 try ty.toType().minInt(mod, ty.toType()),
4159 try ty.toType().maxInt(mod, ty.toType()),
4160 };
4161 }
4162
4149 /// This type is not copyable since it may contain pointers to its inner data.4163 /// This type is not copyable since it may contain pointers to its inner data.
4150 pub const Payload = struct {4164 pub const Payload = struct {
4151 tag: Tag,4165 tag: Tag,
test/behavior/maximum_minimum.zig+85
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1const std = @import("std");1const std = @import("std");
2const builtin = @import("builtin");2const builtin = @import("builtin");
3const mem = std.mem;3const mem = std.mem;
4const assert = std.debug.assert;
4const expect = std.testing.expect;5const expect = std.testing.expect;
5const expectEqual = std.testing.expectEqual;6const expectEqual = std.testing.expectEqual;
67
...@@ -210,3 +211,87 @@ test "@min/@max on comptime_int" {...@@ -210,3 +211,87 @@ test "@min/@max on comptime_int" {
210 try expectEqual(-2, min);211 try expectEqual(-2, min);
211 try expectEqual(2, max);212 try expectEqual(2, max);
212}213}
214
215test "@min/@max notices bounds from types" {
216 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
217 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
218 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
219 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
220
221 var x: u16 = 123;
222 var y: u32 = 456;
223 var z: u8 = 10;
224
225 const min = @min(x, y, z);
226 const max = @max(x, y, z);
227
228 comptime assert(@TypeOf(min) == u8);
229 comptime assert(@TypeOf(max) == u32);
230
231 try expectEqual(z, min);
232 try expectEqual(y, max);
233}
234
235test "@min/@max notices bounds from vector types" {
236 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
237 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
238 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
239 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
240 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
241 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
242
243 var x: @Vector(2, u16) = .{ 30, 67 };
244 var y: @Vector(2, u32) = .{ 20, 500 };
245 var z: @Vector(2, u8) = .{ 60, 15 };
246
247 const min = @min(x, y, z);
248 const max = @max(x, y, z);
249
250 comptime assert(@TypeOf(min) == @Vector(2, u8));
251 comptime assert(@TypeOf(max) == @Vector(2, u32));
252
253 try expectEqual(@Vector(2, u8){ 20, 15 }, min);
254 try expectEqual(@Vector(2, u32){ 60, 500 }, max);
255}
256
257test "@min/@max notices bounds from types when comptime-known value is undef" {
258 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
259 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
260 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
261 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
262
263 var x: u32 = 1_000_000;
264 const y: u16 = undefined;
265 // y is comptime-known, but is undef, so bounds cannot be refined using its value
266
267 const min = @min(x, y);
268 const max = @max(x, y);
269
270 comptime assert(@TypeOf(min) == u16);
271 comptime assert(@TypeOf(max) == u32);
272
273 // Cannot assert values as one was undefined
274}
275
276test "@min/@max notices bounds from vector types when element of comptime-known vector is undef" {
277 if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
278 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
279 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
280 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
281 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
282 if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
283
284 var x: @Vector(2, u32) = .{ 1_000_000, 12345 };
285 const y: @Vector(2, u16) = .{ 10, undefined };
286 // y is comptime-known, but an element is undef, so bounds cannot be refined using its value
287
288 const min = @min(x, y);
289 const max = @max(x, y);
290
291 comptime assert(@TypeOf(min) == @Vector(2, u16));
292 comptime assert(@TypeOf(max) == @Vector(2, u32));
293
294 try expectEqual(@as(u16, 10), min[0]);
295 try expectEqual(@as(u32, 1_000_000), max[0]);
296 // Cannot assert values at index 1 as one was undefined
297}