From 77919ee735706e7ebe0473fa9c3c9186df32c860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Wed, 21 Jan 2026 10:41:15 +0900 Subject: [PATCH] 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. --- lib/std/zig/AstGen.zig | 45 +++++++++++-- lib/std/zig/Zir.zig | 25 ++++++++ src/Sema.zig | 140 ++++++++++++++++++++++++++++++++++++++++- src/print_zir.zig | 5 ++ test/behavior/cast.zig | 48 ++++++++++++++ 5 files changed, 256 insertions(+), 7 deletions(-) diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index 667f5ef045bd431512b3aed957be43e125ddb0ad..3c0018d1dbe6de62f8c0dcf7e9baec767e3b7c32 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -9364,10 +9364,10 @@ fn builtinCall( .log => return floatUnOp(gz, scope, ri, node, params[0], .log), .log2 => return floatUnOp(gz, scope, ri, node, params[0], .log2), .log10 => return floatUnOp(gz, scope, ri, node, params[0], .log10), - .floor => return floatUnOp(gz, scope, ri, node, params[0], .floor), - .ceil => return floatUnOp(gz, scope, ri, node, params[0], .ceil), - .trunc => return floatUnOp(gz, scope, ri, node, params[0], .trunc), - .round => return floatUnOp(gz, scope, ri, node, params[0], .round), + .floor => return floatRoundOp(gz, scope, ri, node, params[0], .floor), + .ceil => return floatRoundOp(gz, scope, ri, node, params[0], .ceil), + .trunc => return floatRoundOp(gz, scope, ri, node, params[0], .trunc), + .round => return floatRoundOp(gz, scope, ri, node, params[0], .round), .int_from_float => return typeCast(gz, scope, ri, node, params[0], .int_from_float, builtin_name), .float_from_int => return typeCast(gz, scope, ri, node, params[0], .float_from_int, builtin_name), @@ -9917,6 +9917,43 @@ fn simpleUnOp( return rvalue(gz, ri, result, node); } +fn floatRoundOp( + gz: *GenZir, + scope: *Scope, + ri: ResultInfo, + node: Ast.Node.Index, + operand_node: Ast.Node.Index, + float_tag: Zir.Inst.Tag, +) InnerError!Zir.Inst.Ref { + if (try ri.rl.resultType(gz, node)) |dest_type| { + const cursor = maybeAdvanceSourceCursorToMainToken(gz, node); + + const operand_ty_inst = try gz.addExtendedPayload(.round_op_ty, Zir.Inst.UnNode{ + .node = gz.nodeIndexToRelative(node), + .operand = dest_type, + }); + + const operand = try expr(gz, scope, .{ .rl = .{ .coerced_ty = operand_ty_inst } }, operand_node); + + try emitDbgStmt(gz, cursor); + const cast_tag: Zir.Inst.Extended = switch (float_tag) { + .round => .round_cast, + .floor => .floor_cast, + .ceil => .ceil_cast, + .trunc => .trunc_cast, + else => unreachable, + }; + const result = try gz.addExtendedPayload(cast_tag, Zir.Inst.BinNode{ + .node = gz.nodeIndexToRelative(node), + .lhs = dest_type, + .rhs = operand, + }); + return rvalue(gz, ri, result, node); + } else { + return floatUnOp(gz, scope, ri, node, operand_node, float_tag); + } +} + fn floatUnOp( gz: *GenZir, scope: *Scope, diff --git a/lib/std/zig/Zir.zig b/lib/std/zig/Zir.zig index 0e76f4ff5bf792d033ee6af276b90663742f06bf..2a3261b1b953c2afc92541499cb7d81bd69fdf95 100644 --- a/lib/std/zig/Zir.zig +++ b/lib/std/zig/Zir.zig @@ -2009,6 +2009,26 @@ pub const Inst = struct { /// `operand` is payload index to `BinNode`. /// `small` is unused. shl_with_overflow, + /// Explicit rounding cast. + /// `operand` is payload index to `Bin`. + /// `small` is unused. + round_cast, + /// Explicit floor cast. + /// `operand` is payload index to `Bin`. + /// `small` is unused. + floor_cast, + /// Explicit ceil cast. + /// `operand` is payload index to `Bin`. + /// `small` is unused. + ceil_cast, + /// Explicit trunc cast. + /// `operand` is payload index to `Bin`. + /// `small` is unused. + trunc_cast, + /// Returns the type for the operand of a rounding op. + /// `operand` is `UnNode`. + /// `small` is unused. + round_op_ty, /// `operand` is payload index to `UnNode`. c_undef, /// `operand` is payload index to `UnNode`. @@ -4474,6 +4494,10 @@ fn findTrackableInner( .sub_with_overflow, .mul_with_overflow, .shl_with_overflow, + .round_cast, + .floor_cast, + .ceil_cast, + .trunc_cast, .c_undef, .c_include, .c_define, @@ -4515,6 +4539,7 @@ fn findTrackableInner( .dbg_empty_stmt, .astgen_error, .float_op_result_ty, + .round_op_ty, => return, // `@TypeOf` has a body. diff --git a/src/Sema.zig b/src/Sema.zig index 65311cafc188da131802235a34bc2582e76444a8..903309eab198bf6802df48f64ed9d1da8d057296 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -1398,6 +1398,11 @@ fn analyzeBodyInner( .@"asm" => try sema.zirAsm( block, extended, false), .asm_expr => try sema.zirAsm( block, extended, true), .typeof_peer => try sema.zirTypeofPeer( block, extended, inst), + .round_cast => try sema.zirRoundCast( block, extended, .round), + .floor_cast => try sema.zirRoundCast( block, extended, .floor), + .ceil_cast => try sema.zirRoundCast( block, extended, .ceil), + .trunc_cast => try sema.zirRoundCast( block, extended, .truncate), + .round_op_ty => try sema.zirRoundOpType( block, extended), .compile_log => try sema.zirCompileLog( block, extended), .min_multi => try sema.zirMinMaxMulti( block, extended, .min), .max_multi => try sema.zirMinMaxMulti( block, extended, .max), @@ -21647,6 +21652,113 @@ fn zirIntFromFloat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro }, dest_ty, operand); } +fn zirRoundCast( + sema: *Sema, + block: *Block, + extended: Zir.Inst.Extended.InstData, + mode: IntFromFloatMode, +) CompileError!Air.Inst.Ref { + const pt = sema.pt; + const zcu = pt.zcu; + const extra = sema.code.extraData(Zir.Inst.BinNode, extended.operand).data; + const src = block.nodeOffset(extra.node); + const operand_src = block.builtinCallArgSrc(extra.node, 0); + + const builtin_name = switch (mode) { + .round => "@round", + .floor => "@floor", + .ceil => "@ceil", + .truncate => "@trunc", + .exact => unreachable, + }; + + const dest_ty = try sema.resolveDestType(block, src, extra.lhs, .remove_eu_opt, builtin_name); + const operand = try sema.resolveInst(extra.rhs); + const operand_ty = sema.typeOf(operand); + + try sema.checkVectorizableBinaryOperands(block, operand_src, dest_ty, operand_ty, src, operand_src); + const dest_scalar_ty = dest_ty.scalarType(zcu); + const operand_scalar_ty = operand_ty.scalarType(zcu); + + if (dest_scalar_ty.zigTypeTag(zcu) == .float or dest_scalar_ty.zigTypeTag(zcu) == .comptime_float) { + const coerced_operand = try sema.coerce(block, dest_ty, operand, operand_src); + + const result_ref = switch (mode) { + .round => try sema.maybeConstantUnaryMath(coerced_operand, dest_ty, Value.round), + .floor => try sema.maybeConstantUnaryMath(coerced_operand, dest_ty, Value.floor), + .ceil => try sema.maybeConstantUnaryMath(coerced_operand, dest_ty, Value.ceil), + .truncate => try sema.maybeConstantUnaryMath(coerced_operand, dest_ty, Value.trunc), + else => unreachable, + }; + + if (result_ref) |ref| return ref; + + const air_tag: Air.Inst.Tag = switch (mode) { + .round => .round, + .floor => .floor, + .ceil => .ceil, + .truncate => .trunc_float, + else => unreachable, + }; + + try sema.requireRuntimeBlock(block, operand_src, null); + return block.addUnOp(air_tag, coerced_operand); + } + + _ = try sema.checkIntType(block, src, dest_scalar_ty); + try sema.checkFloatType(block, operand_src, operand_scalar_ty); + + if (try sema.resolveValue(operand)) |operand_val| { + const result_val = try sema.intFromFloat(block, operand_src, operand_val, operand_ty, dest_ty, mode); + return Air.internedToRef(result_val.toIntern()); + } else if (dest_scalar_ty.zigTypeTag(zcu) == .comptime_int) { + return sema.failWithNeededComptime(block, operand_src, .{ .simple = .casted_to_comptime_int }); + } + + try sema.requireRuntimeBlock(block, src, operand_src); + + if (dest_scalar_ty.intInfo(zcu).bits == 0) { + if (block.wantSafety()) { + const abs_ref = try block.addTyOp(.abs, operand_ty, operand); + const is_vector = dest_ty.zigTypeTag(zcu) == .vector; + const max_abs_ref = if (is_vector) try block.addReduce(abs_ref, .Max) else abs_ref; + const one_ref = Air.internedToRef((try pt.floatValue(operand_scalar_ty, 1.0)).toIntern()); + const ok_ref = try block.addBinOp(.cmp_lt, max_abs_ref, one_ref); + try sema.addSafetyCheck(block, src, ok_ref, .integer_part_out_of_bounds); + } + const scalar_val = try pt.intValue(dest_scalar_ty, 0); + return Air.internedToRef((try sema.splat(dest_ty, scalar_val)).toIntern()); + } + + const safe = block.wantSafety(); + const optimized = block.float_mode == .optimized; + + if (safe) { + try sema.preparePanicId(src, .integer_part_out_of_bounds); + } + + if (mode == .truncate) { + const air_tag: Air.Inst.Tag = if (safe) + if (optimized) .int_from_float_optimized_safe else .int_from_float_safe + else if (optimized) .int_from_float_optimized else .int_from_float; + return block.addTyOp(air_tag, dest_ty, operand); + } else { + const op_tag: Air.Inst.Tag = switch (mode) { + .round => .round, + .floor => .floor, + .ceil => .ceil, + else => unreachable, + }; + const rounded_op = try block.addUnOp(op_tag, operand); + + const air_tag: Air.Inst.Tag = if (safe) + if (optimized) .int_from_float_optimized_safe else .int_from_float_safe + else if (optimized) .int_from_float_optimized else .int_from_float; + + return block.addTyOp(air_tag, dest_ty, rounded_op); + } +} + fn zirFloatFromInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref { const pt = sema.pt; const zcu = pt.zcu; @@ -25804,6 +25916,21 @@ fn zirFloatOpResultType(sema: *Sema, block: *Block, extended: Zir.Inst.Extended. return .fromType(float_ty); } +fn zirRoundOpType(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) CompileError!Air.Inst.Ref { + const pt = sema.pt; + const zcu = pt.zcu; + const extra = sema.code.extraData(Zir.Inst.UnNode, extended.operand).data; + const operand_src = block.builtinCallArgSrc(extra.node, 0); + + const dest_ty = try sema.resolveDestType(block, operand_src, extra.operand, .remove_eu_opt, "type hint"); + + const float_ty = dest_ty.optEuBaseType(zcu); + switch (float_ty.scalarType(zcu).zigTypeTag(zcu)) { + .float, .comptime_float => return .fromType(float_ty), + else => return .comptime_float_type, + } +} + fn requireRuntimeBlock(sema: *Sema, block: *Block, src: LazySrcLoc, runtime_src: ?LazySrcLoc) !void { if (block.isComptime()) { const msg, const fail_block = msg: { @@ -36413,7 +36540,7 @@ fn structFieldIndex( return sema.failWithBadStructFieldAccess(block, struct_ty, struct_type, field_src, field_name); } -const IntFromFloatMode = enum { exact, truncate }; +const IntFromFloatMode = enum { exact, truncate, round, floor, ceil }; fn intFromFloat( sema: *Sema, @@ -36451,7 +36578,14 @@ fn intFromFloatScalar( if (val.isUndef(zcu)) return sema.failWithUseOfUndef(block, src, vec_idx); - const float = val.toFloat(f128, zcu); + var float = val.toFloat(f128, zcu); + switch (mode) { + .round => float = @round(float), + .floor => float = @floor(float), + .ceil => float = @ceil(float), + .truncate, .exact => {}, + } + if (std.math.isNan(float)) { return sema.fail(block, src, "float value NaN cannot be stored in integer type '{f}'", .{ int_ty.fmt(pt), @@ -36476,7 +36610,7 @@ fn intFromFloatScalar( "fractional component prevents float value '{f}' from coercion to type '{f}'", .{ val.fmtValueSema(pt, sema), int_ty.fmt(pt) }, ), - .truncate => {}, + .truncate, .round, .floor, .ceil => {}, }, .exact => {}, } diff --git a/src/print_zir.zig b/src/print_zir.zig index c3a494de8466600670dff6d74b567cd80ee04a71..f80946f50720f1e8735a54927a12815516b0c9ad 100644 --- a/src/print_zir.zig +++ b/src/print_zir.zig @@ -570,6 +570,7 @@ const Writer = struct { .float_op_result_ty, .reify_tuple, .reify_pointer_sentinel_ty, + .round_op_ty, => { const inst_data = self.code.extraData(Zir.Inst.UnNode, extended.operand).data; try self.writeInstRef(stream, inst_data.operand); @@ -584,6 +585,10 @@ const Writer = struct { .prefetch, .c_va_arg, .reify_enum_value_slice_ty, + .round_cast, + .floor_cast, + .ceil_cast, + .trunc_cast, => { const inst_data = self.code.extraData(Zir.Inst.BinNode, extended.operand).data; try self.writeInstRef(stream, inst_data.lhs); diff --git a/test/behavior/cast.zig b/test/behavior/cast.zig index 6515df5e2f8f87ed87fa33206b4e388d201b5478..da54201ab8ffcfdc66cd77693e39ed4792f1ae39 100644 --- a/test/behavior/cast.zig +++ b/test/behavior/cast.zig @@ -116,6 +116,10 @@ test "@floatFromInt" { const f = @as(f32, @floatFromInt(k)); const i = @as(i32, @intFromFloat(f)); try expect(i == k); + try expect(@as(i32, @round(f)) == k); + try expect(@as(i32, @floor(f)) == k); + try expect(@as(i32, @ceil(f)) == k); + try expect(@as(i32, @trunc(f)) == k); } }; try S.doTheTest(); @@ -139,6 +143,10 @@ test "@floatFromInt(f80)" { const f = @as(f80, @floatFromInt(k)); const i = @as(Int, @intFromFloat(f)); try expect(i == k); + try expect(@as(Int, @round(f)) == k); + try expect(@as(Int, @floor(f)) == k); + try expect(@as(Int, @ceil(f)) == k); + try expect(@as(Int, @trunc(f)) == k); } }; try S.doTheTest(i31); @@ -167,6 +175,10 @@ test "type coercion from int to float" { try std.testing.expectEqual(int, @as(Int, @intFromFloat(float))); try std.testing.expectEqual(int, @as(Int, @intFromFloat(@ceil(float)))); try std.testing.expectEqual(int, @as(Int, @intFromFloat(@floor(float)))); + try std.testing.expectEqual(int, @as(Int, @round(float))); + try std.testing.expectEqual(int, @as(Int, @ceil(float))); + try std.testing.expectEqual(int, @as(Int, @floor(float))); + try std.testing.expectEqual(int, @as(Int, @trunc(float))); } // Exhaustively check that all possible values of the integer type can @@ -227,12 +239,37 @@ fn testIntFromFloats() !void { try expectIntFromFloat(f32, 255.1, u8, 255); try expectIntFromFloat(f32, 127.2, i8, 127); try expectIntFromFloat(f32, -128.2, i8, -128); + + try expectRoundCast(f32, 255.1, u8, 255); + try expectFloorCast(f32, 255.1, u8, 255); + try expectTruncCast(f32, 255.1, u8, 255); + + try expectRoundCast(f32, 127.2, i8, 127); + try expectFloorCast(f32, 127.2, i8, 127); + try expectTruncCast(f32, 127.2, i8, 127); + + try expectRoundCast(f32, -128.2, i8, -128); + try expectCeilCast(f32, -128.2, i8, -128); + try expectTruncCast(f32, -128.2, i8, -128); } fn expectIntFromFloat(comptime F: type, f: F, comptime I: type, i: I) !void { try expect(@as(I, @intFromFloat(f)) == i); } +fn expectRoundCast(comptime F: type, f: F, comptime I: type, i: I) !void { + try expect(@as(I, @round(f)) == i); +} +fn expectFloorCast(comptime F: type, f: F, comptime I: type, i: I) !void { + try expect(@as(I, @floor(f)) == i); +} +fn expectCeilCast(comptime F: type, f: F, comptime I: type, i: I) !void { + try expect(@as(I, @ceil(f)) == i); +} +fn expectTruncCast(comptime F: type, f: F, comptime I: type, i: I) !void { + try expect(@as(I, @trunc(f)) == i); +} + test "implicitly cast indirect pointer to maybe-indirect pointer" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; @@ -1306,6 +1343,13 @@ test "comptime float casts" { try expectIntFromFloat(comptime_int, 1234, i16, 1234); try expectIntFromFloat(comptime_float, 12.3, comptime_int, 12); + + try expectRoundCast(comptime_int, 1234, i16, 1234); + try expectRoundCast(comptime_float, 12.3, comptime_int, 12); + + try expectFloorCast(comptime_float, 12.3, comptime_int, 12); + try expectCeilCast(comptime_float, 12.3, comptime_int, 13); + try expectTruncCast(comptime_float, 12.3, comptime_int, 12); } test "pointer reinterpret const float to int" { @@ -1739,6 +1783,10 @@ test "intFromFloat to zero-bit int" { const a: f32 = 0.0; try comptime std.testing.expect(@as(u0, @intFromFloat(a)) == 0); + try comptime std.testing.expect(@as(u0, @round(a)) == 0); + try comptime std.testing.expect(@as(u0, @floor(a)) == 0); + try comptime std.testing.expect(@as(u0, @ceil(a)) == 0); + try comptime std.testing.expect(@as(u0, @trunc(a)) == 0); } test "peer type resolution of function pointer and function body" { -- 2.54.0