authorgravatar for adria.arrufat@gmail.comAdrià Arrufat <adria.arrufat@gmail.com> 2026-01-21 10:41:15+09:00
committergravatar for adria.arrufat@gmail.comAdrià Arrufat <adria.arrufat@gmail.com> 2026-01-21 12:23:50+09:00
log77919ee735706e7ebe0473fa9c3c9186df32c860
treeb60593ea7f86e49b3708acc2471c4d8faf31006a
parent2207c62bb5f6d61fc9f430e849d0aed13cf63d35

Sema: allow @round, @floor, @ceil, and @trunc to coerce to integer types

This effectively renders @intFromFloat redundant, as it is semantically equivalent to @trunc when used in a context expecting an integer.

5 files changed, 256 insertions(+), 7 deletions(-)

lib/std/zig/AstGen.zig+41-4
......@@ -9364,10 +9364,10 @@ fn builtinCall(
93649364 .log => return floatUnOp(gz, scope, ri, node, params[0], .log),
93659365 .log2 => return floatUnOp(gz, scope, ri, node, params[0], .log2),
93669366 .log10 => return floatUnOp(gz, scope, ri, node, params[0], .log10),
9367 .floor => return floatUnOp(gz, scope, ri, node, params[0], .floor),
9368 .ceil => return floatUnOp(gz, scope, ri, node, params[0], .ceil),
9369 .trunc => return floatUnOp(gz, scope, ri, node, params[0], .trunc),
9370 .round => return floatUnOp(gz, scope, ri, node, params[0], .round),
9367 .floor => return floatRoundOp(gz, scope, ri, node, params[0], .floor),
9368 .ceil => return floatRoundOp(gz, scope, ri, node, params[0], .ceil),
9369 .trunc => return floatRoundOp(gz, scope, ri, node, params[0], .trunc),
9370 .round => return floatRoundOp(gz, scope, ri, node, params[0], .round),
93719371
93729372 .int_from_float => return typeCast(gz, scope, ri, node, params[0], .int_from_float, builtin_name),
93739373 .float_from_int => return typeCast(gz, scope, ri, node, params[0], .float_from_int, builtin_name),
......@@ -9917,6 +9917,43 @@ fn simpleUnOp(
99179917 return rvalue(gz, ri, result, node);
99189918}
99199919
9920fn floatRoundOp(
9921 gz: *GenZir,
9922 scope: *Scope,
9923 ri: ResultInfo,
9924 node: Ast.Node.Index,
9925 operand_node: Ast.Node.Index,
9926 float_tag: Zir.Inst.Tag,
9927) InnerError!Zir.Inst.Ref {
9928 if (try ri.rl.resultType(gz, node)) |dest_type| {
9929 const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
9930
9931 const operand_ty_inst = try gz.addExtendedPayload(.round_op_ty, Zir.Inst.UnNode{
9932 .node = gz.nodeIndexToRelative(node),
9933 .operand = dest_type,
9934 });
9935
9936 const operand = try expr(gz, scope, .{ .rl = .{ .coerced_ty = operand_ty_inst } }, operand_node);
9937
9938 try emitDbgStmt(gz, cursor);
9939 const cast_tag: Zir.Inst.Extended = switch (float_tag) {
9940 .round => .round_cast,
9941 .floor => .floor_cast,
9942 .ceil => .ceil_cast,
9943 .trunc => .trunc_cast,
9944 else => unreachable,
9945 };
9946 const result = try gz.addExtendedPayload(cast_tag, Zir.Inst.BinNode{
9947 .node = gz.nodeIndexToRelative(node),
9948 .lhs = dest_type,
9949 .rhs = operand,
9950 });
9951 return rvalue(gz, ri, result, node);
9952 } else {
9953 return floatUnOp(gz, scope, ri, node, operand_node, float_tag);
9954 }
9955}
9956
99209957fn floatUnOp(
99219958 gz: *GenZir,
99229959 scope: *Scope,
lib/std/zig/Zir.zig+25
......@@ -2009,6 +2009,26 @@ pub const Inst = struct {
20092009 /// `operand` is payload index to `BinNode`.
20102010 /// `small` is unused.
20112011 shl_with_overflow,
2012 /// Explicit rounding cast.
2013 /// `operand` is payload index to `Bin`.
2014 /// `small` is unused.
2015 round_cast,
2016 /// Explicit floor cast.
2017 /// `operand` is payload index to `Bin`.
2018 /// `small` is unused.
2019 floor_cast,
2020 /// Explicit ceil cast.
2021 /// `operand` is payload index to `Bin`.
2022 /// `small` is unused.
2023 ceil_cast,
2024 /// Explicit trunc cast.
2025 /// `operand` is payload index to `Bin`.
2026 /// `small` is unused.
2027 trunc_cast,
2028 /// Returns the type for the operand of a rounding op.
2029 /// `operand` is `UnNode`.
2030 /// `small` is unused.
2031 round_op_ty,
20122032 /// `operand` is payload index to `UnNode`.
20132033 c_undef,
20142034 /// `operand` is payload index to `UnNode`.
......@@ -4474,6 +4494,10 @@ fn findTrackableInner(
44744494 .sub_with_overflow,
44754495 .mul_with_overflow,
44764496 .shl_with_overflow,
4497 .round_cast,
4498 .floor_cast,
4499 .ceil_cast,
4500 .trunc_cast,
44774501 .c_undef,
44784502 .c_include,
44794503 .c_define,
......@@ -4515,6 +4539,7 @@ fn findTrackableInner(
45154539 .dbg_empty_stmt,
45164540 .astgen_error,
45174541 .float_op_result_ty,
4542 .round_op_ty,
45184543 => return,
45194544
45204545 // `@TypeOf` has a body.
src/Sema.zig+137-3
......@@ -1398,6 +1398,11 @@ fn analyzeBodyInner(
13981398 .@"asm" => try sema.zirAsm( block, extended, false),
13991399 .asm_expr => try sema.zirAsm( block, extended, true),
14001400 .typeof_peer => try sema.zirTypeofPeer( block, extended, inst),
1401 .round_cast => try sema.zirRoundCast( block, extended, .round),
1402 .floor_cast => try sema.zirRoundCast( block, extended, .floor),
1403 .ceil_cast => try sema.zirRoundCast( block, extended, .ceil),
1404 .trunc_cast => try sema.zirRoundCast( block, extended, .truncate),
1405 .round_op_ty => try sema.zirRoundOpType( block, extended),
14011406 .compile_log => try sema.zirCompileLog( block, extended),
14021407 .min_multi => try sema.zirMinMaxMulti( block, extended, .min),
14031408 .max_multi => try sema.zirMinMaxMulti( block, extended, .max),
......@@ -21647,6 +21652,113 @@ fn zirIntFromFloat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro
2164721652 }, dest_ty, operand);
2164821653}
2164921654
21655fn zirRoundCast(
21656 sema: *Sema,
21657 block: *Block,
21658 extended: Zir.Inst.Extended.InstData,
21659 mode: IntFromFloatMode,
21660) CompileError!Air.Inst.Ref {
21661 const pt = sema.pt;
21662 const zcu = pt.zcu;
21663 const extra = sema.code.extraData(Zir.Inst.BinNode, extended.operand).data;
21664 const src = block.nodeOffset(extra.node);
21665 const operand_src = block.builtinCallArgSrc(extra.node, 0);
21666
21667 const builtin_name = switch (mode) {
21668 .round => "@round",
21669 .floor => "@floor",
21670 .ceil => "@ceil",
21671 .truncate => "@trunc",
21672 .exact => unreachable,
21673 };
21674
21675 const dest_ty = try sema.resolveDestType(block, src, extra.lhs, .remove_eu_opt, builtin_name);
21676 const operand = try sema.resolveInst(extra.rhs);
21677 const operand_ty = sema.typeOf(operand);
21678
21679 try sema.checkVectorizableBinaryOperands(block, operand_src, dest_ty, operand_ty, src, operand_src);
21680 const dest_scalar_ty = dest_ty.scalarType(zcu);
21681 const operand_scalar_ty = operand_ty.scalarType(zcu);
21682
21683 if (dest_scalar_ty.zigTypeTag(zcu) == .float or dest_scalar_ty.zigTypeTag(zcu) == .comptime_float) {
21684 const coerced_operand = try sema.coerce(block, dest_ty, operand, operand_src);
21685
21686 const result_ref = switch (mode) {
21687 .round => try sema.maybeConstantUnaryMath(coerced_operand, dest_ty, Value.round),
21688 .floor => try sema.maybeConstantUnaryMath(coerced_operand, dest_ty, Value.floor),
21689 .ceil => try sema.maybeConstantUnaryMath(coerced_operand, dest_ty, Value.ceil),
21690 .truncate => try sema.maybeConstantUnaryMath(coerced_operand, dest_ty, Value.trunc),
21691 else => unreachable,
21692 };
21693
21694 if (result_ref) |ref| return ref;
21695
21696 const air_tag: Air.Inst.Tag = switch (mode) {
21697 .round => .round,
21698 .floor => .floor,
21699 .ceil => .ceil,
21700 .truncate => .trunc_float,
21701 else => unreachable,
21702 };
21703
21704 try sema.requireRuntimeBlock(block, operand_src, null);
21705 return block.addUnOp(air_tag, coerced_operand);
21706 }
21707
21708 _ = try sema.checkIntType(block, src, dest_scalar_ty);
21709 try sema.checkFloatType(block, operand_src, operand_scalar_ty);
21710
21711 if (try sema.resolveValue(operand)) |operand_val| {
21712 const result_val = try sema.intFromFloat(block, operand_src, operand_val, operand_ty, dest_ty, mode);
21713 return Air.internedToRef(result_val.toIntern());
21714 } else if (dest_scalar_ty.zigTypeTag(zcu) == .comptime_int) {
21715 return sema.failWithNeededComptime(block, operand_src, .{ .simple = .casted_to_comptime_int });
21716 }
21717
21718 try sema.requireRuntimeBlock(block, src, operand_src);
21719
21720 if (dest_scalar_ty.intInfo(zcu).bits == 0) {
21721 if (block.wantSafety()) {
21722 const abs_ref = try block.addTyOp(.abs, operand_ty, operand);
21723 const is_vector = dest_ty.zigTypeTag(zcu) == .vector;
21724 const max_abs_ref = if (is_vector) try block.addReduce(abs_ref, .Max) else abs_ref;
21725 const one_ref = Air.internedToRef((try pt.floatValue(operand_scalar_ty, 1.0)).toIntern());
21726 const ok_ref = try block.addBinOp(.cmp_lt, max_abs_ref, one_ref);
21727 try sema.addSafetyCheck(block, src, ok_ref, .integer_part_out_of_bounds);
21728 }
21729 const scalar_val = try pt.intValue(dest_scalar_ty, 0);
21730 return Air.internedToRef((try sema.splat(dest_ty, scalar_val)).toIntern());
21731 }
21732
21733 const safe = block.wantSafety();
21734 const optimized = block.float_mode == .optimized;
21735
21736 if (safe) {
21737 try sema.preparePanicId(src, .integer_part_out_of_bounds);
21738 }
21739
21740 if (mode == .truncate) {
21741 const air_tag: Air.Inst.Tag = if (safe)
21742 if (optimized) .int_from_float_optimized_safe else .int_from_float_safe
21743 else if (optimized) .int_from_float_optimized else .int_from_float;
21744 return block.addTyOp(air_tag, dest_ty, operand);
21745 } else {
21746 const op_tag: Air.Inst.Tag = switch (mode) {
21747 .round => .round,
21748 .floor => .floor,
21749 .ceil => .ceil,
21750 else => unreachable,
21751 };
21752 const rounded_op = try block.addUnOp(op_tag, operand);
21753
21754 const air_tag: Air.Inst.Tag = if (safe)
21755 if (optimized) .int_from_float_optimized_safe else .int_from_float_safe
21756 else if (optimized) .int_from_float_optimized else .int_from_float;
21757
21758 return block.addTyOp(air_tag, dest_ty, rounded_op);
21759 }
21760}
21761
2165021762fn zirFloatFromInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
2165121763 const pt = sema.pt;
2165221764 const zcu = pt.zcu;
......@@ -25804,6 +25916,21 @@ fn zirFloatOpResultType(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.
2580425916 return .fromType(float_ty);
2580525917}
2580625918
25919fn zirRoundOpType(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) CompileError!Air.Inst.Ref {
25920 const pt = sema.pt;
25921 const zcu = pt.zcu;
25922 const extra = sema.code.extraData(Zir.Inst.UnNode, extended.operand).data;
25923 const operand_src = block.builtinCallArgSrc(extra.node, 0);
25924
25925 const dest_ty = try sema.resolveDestType(block, operand_src, extra.operand, .remove_eu_opt, "type hint");
25926
25927 const float_ty = dest_ty.optEuBaseType(zcu);
25928 switch (float_ty.scalarType(zcu).zigTypeTag(zcu)) {
25929 .float, .comptime_float => return .fromType(float_ty),
25930 else => return .comptime_float_type,
25931 }
25932}
25933
2580725934fn requireRuntimeBlock(sema: *Sema, block: *Block, src: LazySrcLoc, runtime_src: ?LazySrcLoc) !void {
2580825935 if (block.isComptime()) {
2580925936 const msg, const fail_block = msg: {
......@@ -36413,7 +36540,7 @@ fn structFieldIndex(
3641336540 return sema.failWithBadStructFieldAccess(block, struct_ty, struct_type, field_src, field_name);
3641436541}
3641536542
36416const IntFromFloatMode = enum { exact, truncate };
36543const IntFromFloatMode = enum { exact, truncate, round, floor, ceil };
3641736544
3641836545fn intFromFloat(
3641936546 sema: *Sema,
......@@ -36451,7 +36578,14 @@ fn intFromFloatScalar(
3645136578
3645236579 if (val.isUndef(zcu)) return sema.failWithUseOfUndef(block, src, vec_idx);
3645336580
36454 const float = val.toFloat(f128, zcu);
36581 var float = val.toFloat(f128, zcu);
36582 switch (mode) {
36583 .round => float = @round(float),
36584 .floor => float = @floor(float),
36585 .ceil => float = @ceil(float),
36586 .truncate, .exact => {},
36587 }
36588
3645536589 if (std.math.isNan(float)) {
3645636590 return sema.fail(block, src, "float value NaN cannot be stored in integer type '{f}'", .{
3645736591 int_ty.fmt(pt),
......@@ -36476,7 +36610,7 @@ fn intFromFloatScalar(
3647636610 "fractional component prevents float value '{f}' from coercion to type '{f}'",
3647736611 .{ val.fmtValueSema(pt, sema), int_ty.fmt(pt) },
3647836612 ),
36479 .truncate => {},
36613 .truncate, .round, .floor, .ceil => {},
3648036614 },
3648136615 .exact => {},
3648236616 }
src/print_zir.zig+5
......@@ -570,6 +570,7 @@ const Writer = struct {
570570 .float_op_result_ty,
571571 .reify_tuple,
572572 .reify_pointer_sentinel_ty,
573 .round_op_ty,
573574 => {
574575 const inst_data = self.code.extraData(Zir.Inst.UnNode, extended.operand).data;
575576 try self.writeInstRef(stream, inst_data.operand);
......@@ -584,6 +585,10 @@ const Writer = struct {
584585 .prefetch,
585586 .c_va_arg,
586587 .reify_enum_value_slice_ty,
588 .round_cast,
589 .floor_cast,
590 .ceil_cast,
591 .trunc_cast,
587592 => {
588593 const inst_data = self.code.extraData(Zir.Inst.BinNode, extended.operand).data;
589594 try self.writeInstRef(stream, inst_data.lhs);
test/behavior/cast.zig+48
......@@ -116,6 +116,10 @@ test "@floatFromInt" {
116116 const f = @as(f32, @floatFromInt(k));
117117 const i = @as(i32, @intFromFloat(f));
118118 try expect(i == k);
119 try expect(@as(i32, @round(f)) == k);
120 try expect(@as(i32, @floor(f)) == k);
121 try expect(@as(i32, @ceil(f)) == k);
122 try expect(@as(i32, @trunc(f)) == k);
119123 }
120124 };
121125 try S.doTheTest();
......@@ -139,6 +143,10 @@ test "@floatFromInt(f80)" {
139143 const f = @as(f80, @floatFromInt(k));
140144 const i = @as(Int, @intFromFloat(f));
141145 try expect(i == k);
146 try expect(@as(Int, @round(f)) == k);
147 try expect(@as(Int, @floor(f)) == k);
148 try expect(@as(Int, @ceil(f)) == k);
149 try expect(@as(Int, @trunc(f)) == k);
142150 }
143151 };
144152 try S.doTheTest(i31);
......@@ -167,6 +175,10 @@ test "type coercion from int to float" {
167175 try std.testing.expectEqual(int, @as(Int, @intFromFloat(float)));
168176 try std.testing.expectEqual(int, @as(Int, @intFromFloat(@ceil(float))));
169177 try std.testing.expectEqual(int, @as(Int, @intFromFloat(@floor(float))));
178 try std.testing.expectEqual(int, @as(Int, @round(float)));
179 try std.testing.expectEqual(int, @as(Int, @ceil(float)));
180 try std.testing.expectEqual(int, @as(Int, @floor(float)));
181 try std.testing.expectEqual(int, @as(Int, @trunc(float)));
170182 }
171183
172184 // Exhaustively check that all possible values of the integer type can
......@@ -227,12 +239,37 @@ fn testIntFromFloats() !void {
227239 try expectIntFromFloat(f32, 255.1, u8, 255);
228240 try expectIntFromFloat(f32, 127.2, i8, 127);
229241 try expectIntFromFloat(f32, -128.2, i8, -128);
242
243 try expectRoundCast(f32, 255.1, u8, 255);
244 try expectFloorCast(f32, 255.1, u8, 255);
245 try expectTruncCast(f32, 255.1, u8, 255);
246
247 try expectRoundCast(f32, 127.2, i8, 127);
248 try expectFloorCast(f32, 127.2, i8, 127);
249 try expectTruncCast(f32, 127.2, i8, 127);
250
251 try expectRoundCast(f32, -128.2, i8, -128);
252 try expectCeilCast(f32, -128.2, i8, -128);
253 try expectTruncCast(f32, -128.2, i8, -128);
230254}
231255
232256fn expectIntFromFloat(comptime F: type, f: F, comptime I: type, i: I) !void {
233257 try expect(@as(I, @intFromFloat(f)) == i);
234258}
235259
260fn expectRoundCast(comptime F: type, f: F, comptime I: type, i: I) !void {
261 try expect(@as(I, @round(f)) == i);
262}
263fn expectFloorCast(comptime F: type, f: F, comptime I: type, i: I) !void {
264 try expect(@as(I, @floor(f)) == i);
265}
266fn expectCeilCast(comptime F: type, f: F, comptime I: type, i: I) !void {
267 try expect(@as(I, @ceil(f)) == i);
268}
269fn expectTruncCast(comptime F: type, f: F, comptime I: type, i: I) !void {
270 try expect(@as(I, @trunc(f)) == i);
271}
272
236273test "implicitly cast indirect pointer to maybe-indirect pointer" {
237274 if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
238275 if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
......@@ -1306,6 +1343,13 @@ test "comptime float casts" {
13061343
13071344 try expectIntFromFloat(comptime_int, 1234, i16, 1234);
13081345 try expectIntFromFloat(comptime_float, 12.3, comptime_int, 12);
1346
1347 try expectRoundCast(comptime_int, 1234, i16, 1234);
1348 try expectRoundCast(comptime_float, 12.3, comptime_int, 12);
1349
1350 try expectFloorCast(comptime_float, 12.3, comptime_int, 12);
1351 try expectCeilCast(comptime_float, 12.3, comptime_int, 13);
1352 try expectTruncCast(comptime_float, 12.3, comptime_int, 12);
13091353}
13101354
13111355test "pointer reinterpret const float to int" {
......@@ -1739,6 +1783,10 @@ test "intFromFloat to zero-bit int" {
17391783
17401784 const a: f32 = 0.0;
17411785 try comptime std.testing.expect(@as(u0, @intFromFloat(a)) == 0);
1786 try comptime std.testing.expect(@as(u0, @round(a)) == 0);
1787 try comptime std.testing.expect(@as(u0, @floor(a)) == 0);
1788 try comptime std.testing.expect(@as(u0, @ceil(a)) == 0);
1789 try comptime std.testing.expect(@as(u0, @trunc(a)) == 0);
17421790}
17431791
17441792test "peer type resolution of function pointer and function body" {