Skip to content

Commit a867b56

Browse files
niaowdeadprogram
authored andcommitted
compiler: saturate float-to-int conversions
This works around some UB in LLVM, where an out-of-bounds conversion would produce a poison value. The selected behavior is saturating, except that NaN is mapped to the minimum value.
1 parent f159429 commit a867b56

File tree

3 files changed

+70
-4
lines changed

3 files changed

+70
-4
lines changed

compiler/compiler.go

+63-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"go/constant"
99
"go/token"
1010
"go/types"
11+
"math/bits"
1112
"path/filepath"
1213
"sort"
1314
"strconv"
@@ -2552,10 +2553,70 @@ func (b *builder) createConvert(typeFrom, typeTo types.Type, value llvm.Value, p
25522553

25532554
if typeFrom.Info()&types.IsFloat != 0 && typeTo.Info()&types.IsInteger != 0 {
25542555
// Conversion from float to int.
2556+
// Passing an out-of-bounds float to LLVM would cause UB, so that UB is trapped by select instructions.
2557+
// The Go specification says that this should be implementation-defined behavior.
2558+
// This implements saturating behavior, except that NaN is mapped to the minimum value.
2559+
var significandBits int
2560+
switch typeFrom.Kind() {
2561+
case types.Float32:
2562+
significandBits = 23
2563+
case types.Float64:
2564+
significandBits = 52
2565+
}
25552566
if typeTo.Info()&types.IsUnsigned != 0 { // if unsigned
2556-
return b.CreateFPToUI(value, llvmTypeTo, ""), nil
2567+
// Select the maximum value for this unsigned integer type.
2568+
max := ^(^uint64(0) << uint(llvmTypeTo.IntTypeWidth()))
2569+
maxFloat := float64(max)
2570+
if bits.Len64(max) > significandBits {
2571+
// Round the max down to fit within the significand.
2572+
maxFloat = float64(max & ^uint64(0) << uint(bits.Len64(max)-significandBits))
2573+
}
2574+
2575+
// Check if the value is in-bounds (0 <= value <= max).
2576+
positive := b.CreateFCmp(llvm.FloatOLE, llvm.ConstNull(llvmTypeFrom), value, "positive")
2577+
withinMax := b.CreateFCmp(llvm.FloatOLE, value, llvm.ConstFloat(llvmTypeFrom, maxFloat), "withinmax")
2578+
inBounds := b.CreateAnd(positive, withinMax, "inbounds")
2579+
2580+
// Assuming that the value is out-of-bounds, select a saturated value.
2581+
saturated := b.CreateSelect(positive,
2582+
llvm.ConstInt(llvmTypeTo, max, false), // value > max
2583+
llvm.ConstNull(llvmTypeTo), // value < 0 (or NaN)
2584+
"saturated",
2585+
)
2586+
2587+
// Do a normal conversion.
2588+
normal := b.CreateFPToUI(value, llvmTypeTo, "normal")
2589+
2590+
return b.CreateSelect(inBounds, normal, saturated, ""), nil
25572591
} else { // if signed
2558-
return b.CreateFPToSI(value, llvmTypeTo, ""), nil
2592+
// Select the minimum value for this signed integer type.
2593+
min := uint64(1) << uint(llvmTypeTo.IntTypeWidth()-1)
2594+
minFloat := -float64(min)
2595+
2596+
// Select the maximum value for this signed integer type.
2597+
max := ^(^uint64(0) << uint(llvmTypeTo.IntTypeWidth()-1))
2598+
maxFloat := float64(max)
2599+
if bits.Len64(max) > significandBits {
2600+
// Round the max down to fit within the significand.
2601+
maxFloat = float64(max & ^uint64(0) << uint(bits.Len64(max)-significandBits))
2602+
}
2603+
2604+
// Check if the value is in-bounds (min <= value <= max).
2605+
aboveMin := b.CreateFCmp(llvm.FloatOLE, llvm.ConstFloat(llvmTypeFrom, minFloat), value, "abovemin")
2606+
belowMax := b.CreateFCmp(llvm.FloatOLE, value, llvm.ConstFloat(llvmTypeFrom, maxFloat), "belowmax")
2607+
inBounds := b.CreateAnd(aboveMin, belowMax, "inbounds")
2608+
2609+
// Assuming that the value is out-of-bounds, select a saturated value.
2610+
saturated := b.CreateSelect(aboveMin,
2611+
llvm.ConstInt(llvmTypeTo, max, false), // value > max
2612+
llvm.ConstInt(llvmTypeTo, min, false), // value < min (or NaN)
2613+
"saturated",
2614+
)
2615+
2616+
// Do a normal conversion.
2617+
normal := b.CreateFPToSI(value, llvmTypeTo, "normal")
2618+
2619+
return b.CreateSelect(inBounds, normal, saturated, ""), nil
25592620
}
25602621
}
25612622

testdata/float.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ func main() {
2929
var f2 float32 = 5.7
3030
var f3 float32 = -2.3
3131
var f4 float32 = -11.8
32-
println(int32(f1), int32(f2), int32(f3), int32(f4))
32+
var f5 float32 = -1
33+
var f6 float32 = 256
34+
var f7 float32 = -129
35+
var f8 float32 = 0
36+
f8 /= 0
37+
println(int32(f1), int32(f2), int32(f3), int32(f4), uint8(f5), uint8(f6), int8(f7), int8(f6), uint8(f8), int8(f8))
3338

3439
// int -> float
3540
var i1 int32 = 53

testdata/float.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
+3.333333e-001
1212
+6.666667e-001
1313
+6.666667e-001
14-
3 5 -2 -11
14+
3 5 -2 -11 0 255 -128 127 0 -128
1515
+5.300000e+001 -8.000000e+000 +2.000000e+001
1616
(+6.666667e-001+1.200000e+000i)
1717
+6.666667e-001

0 commit comments

Comments
 (0)