-
Notifications
You must be signed in to change notification settings - Fork 821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for non-trapping float-to-int conversions. #1071
Changes from all commits
5b35313
0cfe08f
6fe2f43
442c40f
d52c193
32ed6f2
b7929e6
f8d792c
56fd664
f00283a
e738a9f
bba0129
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,9 @@ use inkwell::{ | |
module::{Linkage, Module}, | ||
passes::PassManager, | ||
targets::{CodeModel, InitializationConfig, RelocMode, Target, TargetMachine}, | ||
types::{BasicType, BasicTypeEnum, FunctionType, PointerType, VectorType}, | ||
types::{ | ||
BasicType, BasicTypeEnum, FloatMathType, FunctionType, IntType, PointerType, VectorType, | ||
}, | ||
values::{ | ||
BasicValue, BasicValueEnum, FloatValue, FunctionValue, IntValue, PhiValue, PointerValue, | ||
VectorValue, | ||
|
@@ -99,13 +101,12 @@ fn splat_vector<'ctx>( | |
} | ||
|
||
// Convert floating point vector to integer and saturate when out of range. | ||
// TODO: generalize to non-vectors using FloatMathType, IntMathType, etc. for | ||
// https://github.com/WebAssembly/nontrapping-float-to-int-conversions/blob/master/proposals/nontrapping-float-to-int-conversion/Overview.md | ||
fn trunc_sat<'ctx>( | ||
fn trunc_sat<'ctx, T: FloatMathType<'ctx>>( | ||
builder: &Builder<'ctx>, | ||
intrinsics: &Intrinsics<'ctx>, | ||
fvec_ty: VectorType<'ctx>, | ||
ivec_ty: VectorType<'ctx>, | ||
fvec_ty: T, | ||
ivec_ty: T::MathConvType, | ||
lower_bound: u64, // Exclusive (lowest representable value) | ||
upper_bound: u64, // Exclusive (greatest representable value) | ||
int_min_value: u64, | ||
|
@@ -126,8 +127,12 @@ fn trunc_sat<'ctx>( | |
// f) Use our previous comparison results to replace certain zeros with | ||
// int_min or int_max. | ||
|
||
let is_signed = int_min_value != 0; | ||
let fvec_ty = fvec_ty.as_basic_type_enum().into_vector_type(); | ||
let ivec_ty = ivec_ty.as_basic_type_enum().into_vector_type(); | ||
let fvec_element_ty = fvec_ty.get_element_type().into_float_type(); | ||
let ivec_element_ty = ivec_ty.get_element_type().into_int_type(); | ||
|
||
let is_signed = int_min_value != 0; | ||
let int_min_value = splat_vector( | ||
builder, | ||
intrinsics, | ||
|
@@ -149,26 +154,26 @@ fn trunc_sat<'ctx>( | |
let lower_bound = if is_signed { | ||
builder.build_signed_int_to_float( | ||
ivec_element_ty.const_int(lower_bound, is_signed), | ||
fvec_ty.get_element_type().into_float_type(), | ||
fvec_element_ty, | ||
"", | ||
) | ||
} else { | ||
builder.build_unsigned_int_to_float( | ||
ivec_element_ty.const_int(lower_bound, is_signed), | ||
fvec_ty.get_element_type().into_float_type(), | ||
fvec_element_ty, | ||
"", | ||
) | ||
}; | ||
let upper_bound = if is_signed { | ||
builder.build_signed_int_to_float( | ||
ivec_element_ty.const_int(upper_bound, is_signed), | ||
fvec_ty.get_element_type().into_float_type(), | ||
fvec_element_ty, | ||
"", | ||
) | ||
} else { | ||
builder.build_unsigned_int_to_float( | ||
ivec_element_ty.const_int(upper_bound, is_signed), | ||
fvec_ty.get_element_type().into_float_type(), | ||
fvec_element_ty, | ||
"", | ||
) | ||
}; | ||
|
@@ -220,6 +225,93 @@ fn trunc_sat<'ctx>( | |
.into_int_value() | ||
} | ||
|
||
// Convert floating point vector to integer and saturate when out of range. | ||
// https://github.com/WebAssembly/nontrapping-float-to-int-conversions/blob/master/proposals/nontrapping-float-to-int-conversion/Overview.md | ||
fn trunc_sat_scalar<'ctx>( | ||
builder: &Builder<'ctx>, | ||
int_ty: IntType<'ctx>, | ||
lower_bound: u64, // Exclusive (lowest representable value) | ||
upper_bound: u64, // Exclusive (greatest representable value) | ||
int_min_value: u64, | ||
int_max_value: u64, | ||
value: FloatValue<'ctx>, | ||
name: &str, | ||
) -> IntValue<'ctx> { | ||
// TODO: this is a scalarized version of the process in trunc_sat. Either | ||
// we should merge with trunc_sat, or we should simplify this function. | ||
|
||
// a) Compare value with itself to identify NaN. | ||
// b) Compare value inttofp(upper_bound) to identify values that need to | ||
// saturate to max. | ||
// c) Compare value with inttofp(lower_bound) to identify values that need | ||
// to saturate to min. | ||
// d) Use select to pick from either zero or the input vector depending on | ||
// whether the comparison indicates that we have an unrepresentable | ||
// value. | ||
// e) Now that the value is safe, fpto[su]i it. | ||
// f) Use our previous comparison results to replace certain zeros with | ||
// int_min or int_max. | ||
|
||
let is_signed = int_min_value != 0; | ||
let int_min_value = int_ty.const_int(int_min_value, is_signed); | ||
let int_max_value = int_ty.const_int(int_max_value, is_signed); | ||
|
||
let lower_bound = if is_signed { | ||
builder.build_signed_int_to_float( | ||
int_ty.const_int(lower_bound, is_signed), | ||
value.get_type(), | ||
"", | ||
) | ||
} else { | ||
builder.build_unsigned_int_to_float( | ||
int_ty.const_int(lower_bound, is_signed), | ||
value.get_type(), | ||
"", | ||
) | ||
}; | ||
let upper_bound = if is_signed { | ||
builder.build_signed_int_to_float( | ||
int_ty.const_int(upper_bound, is_signed), | ||
value.get_type(), | ||
"", | ||
) | ||
} else { | ||
builder.build_unsigned_int_to_float( | ||
int_ty.const_int(upper_bound, is_signed), | ||
value.get_type(), | ||
"", | ||
) | ||
}; | ||
|
||
let zero = value.get_type().const_zero(); | ||
|
||
let nan_cmp = builder.build_float_compare(FloatPredicate::UNO, value, zero, "nan"); | ||
let above_upper_bound_cmp = | ||
builder.build_float_compare(FloatPredicate::OGT, value, upper_bound, "above_upper_bound"); | ||
let below_lower_bound_cmp = | ||
builder.build_float_compare(FloatPredicate::OLT, value, lower_bound, "below_lower_bound"); | ||
let not_representable = builder.build_or( | ||
builder.build_or(nan_cmp, above_upper_bound_cmp, ""), | ||
below_lower_bound_cmp, | ||
"not_representable_as_int", | ||
); | ||
let value = builder | ||
.build_select(not_representable, zero, value, "safe_to_convert") | ||
.into_float_value(); | ||
let value = if is_signed { | ||
builder.build_float_to_signed_int(value, int_ty, "as_int") | ||
} else { | ||
builder.build_float_to_unsigned_int(value, int_ty, "as_int") | ||
}; | ||
let value = builder | ||
.build_select(above_upper_bound_cmp, int_max_value, value, "") | ||
.into_int_value(); | ||
let value = builder | ||
.build_select(below_lower_bound_cmp, int_min_value, value, name) | ||
.into_int_value(); | ||
builder.build_bitcast(value, int_ty, "").into_int_value() | ||
} | ||
|
||
fn trap_if_not_representable_as_int<'ctx>( | ||
builder: &Builder<'ctx>, | ||
intrinsics: &Intrinsics<'ctx>, | ||
|
@@ -4457,10 +4549,36 @@ impl<'ctx> FunctionCodeGenerator<CodegenError> for LLVMFunctionCodeGenerator<'ct | |
builder.build_float_to_signed_int(v1, intrinsics.i32_ty, &state.var_name()); | ||
state.push1(res); | ||
} | ||
Operator::I32TruncSSatF32 | Operator::I32TruncSSatF64 => { | ||
let v1 = state.pop1()?.into_float_value(); | ||
let res = | ||
builder.build_float_to_signed_int(v1, intrinsics.i32_ty, &state.var_name()); | ||
Operator::I32TruncSSatF32 => { | ||
let (v, i) = state.pop1_extra()?; | ||
let v = apply_pending_canonicalization(builder, intrinsics, v, i); | ||
let v = v.into_float_value(); | ||
let res = trunc_sat_scalar( | ||
builder, | ||
intrinsics.i32_ty, | ||
LEF32_GEQ_I32_MIN, | ||
GEF32_LEQ_I32_MAX, | ||
std::i32::MIN as u32 as u64, | ||
std::i32::MAX as u32 as u64, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⛳️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I'm fine with either -- the only reason I mention it is that I haven't seen the MIN and MAX constants in a while. Now that I see them again I do remember having seen them in the past. |
||
v, | ||
&state.var_name(), | ||
); | ||
state.push1(res); | ||
} | ||
Operator::I32TruncSSatF64 => { | ||
let (v, i) = state.pop1_extra()?; | ||
let v = apply_pending_canonicalization(builder, intrinsics, v, i); | ||
let v = v.into_float_value(); | ||
let res = trunc_sat_scalar( | ||
builder, | ||
intrinsics.i32_ty, | ||
LEF64_GEQ_I32_MIN, | ||
GEF64_LEQ_I32_MAX, | ||
std::i32::MIN as u64, | ||
std::i32::MAX as u64, | ||
v, | ||
&state.var_name(), | ||
); | ||
state.push1(res); | ||
} | ||
Operator::I64TruncSF32 => { | ||
|
@@ -4490,10 +4608,36 @@ impl<'ctx> FunctionCodeGenerator<CodegenError> for LLVMFunctionCodeGenerator<'ct | |
builder.build_float_to_signed_int(v1, intrinsics.i64_ty, &state.var_name()); | ||
state.push1(res); | ||
} | ||
Operator::I64TruncSSatF32 | Operator::I64TruncSSatF64 => { | ||
let v1 = state.pop1()?.into_float_value(); | ||
let res = | ||
builder.build_float_to_signed_int(v1, intrinsics.i64_ty, &state.var_name()); | ||
Operator::I64TruncSSatF32 => { | ||
let (v, i) = state.pop1_extra()?; | ||
let v = apply_pending_canonicalization(builder, intrinsics, v, i); | ||
let v = v.into_float_value(); | ||
let res = trunc_sat_scalar( | ||
builder, | ||
intrinsics.i64_ty, | ||
LEF32_GEQ_I64_MIN, | ||
GEF32_LEQ_I64_MAX, | ||
std::i64::MIN as u64, | ||
std::i64::MAX as u64, | ||
v, | ||
&state.var_name(), | ||
); | ||
state.push1(res); | ||
} | ||
Operator::I64TruncSSatF64 => { | ||
let (v, i) = state.pop1_extra()?; | ||
let v = apply_pending_canonicalization(builder, intrinsics, v, i); | ||
let v = v.into_float_value(); | ||
let res = trunc_sat_scalar( | ||
builder, | ||
intrinsics.i64_ty, | ||
LEF64_GEQ_I64_MIN, | ||
GEF64_LEQ_I64_MAX, | ||
std::i64::MIN as u64, | ||
std::i64::MAX as u64, | ||
v, | ||
&state.var_name(), | ||
); | ||
state.push1(res); | ||
} | ||
Operator::I32TruncUF32 => { | ||
|
@@ -4522,10 +4666,36 @@ impl<'ctx> FunctionCodeGenerator<CodegenError> for LLVMFunctionCodeGenerator<'ct | |
builder.build_float_to_unsigned_int(v1, intrinsics.i32_ty, &state.var_name()); | ||
state.push1(res); | ||
} | ||
Operator::I32TruncUSatF32 | Operator::I32TruncUSatF64 => { | ||
let v1 = state.pop1()?.into_float_value(); | ||
let res = | ||
builder.build_float_to_unsigned_int(v1, intrinsics.i32_ty, &state.var_name()); | ||
Operator::I32TruncUSatF32 => { | ||
let (v, i) = state.pop1_extra()?; | ||
let v = apply_pending_canonicalization(builder, intrinsics, v, i); | ||
let v = v.into_float_value(); | ||
let res = trunc_sat_scalar( | ||
builder, | ||
intrinsics.i32_ty, | ||
LEF32_GEQ_U32_MIN, | ||
GEF32_LEQ_U32_MAX, | ||
std::u32::MIN as u64, | ||
std::u32::MAX as u64, | ||
v, | ||
&state.var_name(), | ||
); | ||
state.push1(res); | ||
} | ||
Operator::I32TruncUSatF64 => { | ||
let (v, i) = state.pop1_extra()?; | ||
let v = apply_pending_canonicalization(builder, intrinsics, v, i); | ||
let v = v.into_float_value(); | ||
let res = trunc_sat_scalar( | ||
builder, | ||
intrinsics.i32_ty, | ||
LEF64_GEQ_U32_MIN, | ||
GEF64_LEQ_U32_MAX, | ||
std::u32::MIN as u64, | ||
std::u32::MAX as u64, | ||
v, | ||
&state.var_name(), | ||
); | ||
state.push1(res); | ||
} | ||
Operator::I64TruncUF32 => { | ||
|
@@ -4554,10 +4724,36 @@ impl<'ctx> FunctionCodeGenerator<CodegenError> for LLVMFunctionCodeGenerator<'ct | |
builder.build_float_to_unsigned_int(v1, intrinsics.i64_ty, &state.var_name()); | ||
state.push1(res); | ||
} | ||
Operator::I64TruncUSatF32 | Operator::I64TruncUSatF64 => { | ||
let v1 = state.pop1()?.into_float_value(); | ||
let res = | ||
builder.build_float_to_unsigned_int(v1, intrinsics.i64_ty, &state.var_name()); | ||
Operator::I64TruncUSatF32 => { | ||
let (v, i) = state.pop1_extra()?; | ||
let v = apply_pending_canonicalization(builder, intrinsics, v, i); | ||
let v = v.into_float_value(); | ||
let res = trunc_sat_scalar( | ||
builder, | ||
intrinsics.i64_ty, | ||
LEF32_GEQ_U64_MIN, | ||
GEF32_LEQ_U64_MAX, | ||
std::u64::MIN, | ||
std::u64::MAX, | ||
v, | ||
&state.var_name(), | ||
); | ||
state.push1(res); | ||
} | ||
Operator::I64TruncUSatF64 => { | ||
let (v, i) = state.pop1_extra()?; | ||
let v = apply_pending_canonicalization(builder, intrinsics, v, i); | ||
let v = v.into_float_value(); | ||
let res = trunc_sat_scalar( | ||
builder, | ||
intrinsics.i64_ty, | ||
LEF64_GEQ_U64_MIN, | ||
GEF64_LEQ_U64_MAX, | ||
std::u64::MIN, | ||
std::u64::MAX, | ||
v, | ||
&state.var_name(), | ||
); | ||
state.push1(res); | ||
} | ||
Operator::F32DemoteF64 => { | ||
|
@@ -8791,3 +8987,42 @@ fn is_f64_arithmetic(bits: u64) -> bool { | |
let bits = bits & 0x7FFF_FFFF_FFFF_FFFF; | ||
bits < 0x7FF8_0000_0000_0000 | ||
} | ||
|
||
// Constants for the bounds of truncation operations. These are the least or | ||
// greatest exact floats in either f32 or f64 representation | ||
// greater-than-or-equal-to (for least) or less-than-or-equal-to (for greatest) | ||
// the i32 or i64 or u32 or u64 min (for least) or max (for greatest), when | ||
// rounding towards zero. | ||
|
||
/// Least Exact Float (32 bits) greater-than-or-equal-to i32::MIN when rounding towards zero. | ||
const LEF32_GEQ_I32_MIN: u64 = std::i32::MIN as u64; | ||
/// Greatest Exact Float (32 bits) less-than-or-equal-to i32::MAX when rounding towards zero. | ||
const GEF32_LEQ_I32_MAX: u64 = 2147483520; // bits as f32: 0x4eff_ffff | ||
/// Least Exact Float (64 bits) greater-than-or-equal-to i32::MIN when rounding towards zero. | ||
const LEF64_GEQ_I32_MIN: u64 = std::i32::MIN as u64; | ||
/// Greatest Exact Float (64 bits) less-than-or-equal-to i32::MAX when rounding towards zero. | ||
const GEF64_LEQ_I32_MAX: u64 = std::i32::MAX as u64; | ||
/// Least Exact Float (32 bits) greater-than-or-equal-to u32::MIN when rounding towards zero. | ||
const LEF32_GEQ_U32_MIN: u64 = std::u32::MIN as u64; | ||
/// Greatest Exact Float (32 bits) less-than-or-equal-to u32::MAX when rounding towards zero. | ||
const GEF32_LEQ_U32_MAX: u64 = 4294967040; // bits as f32: 0x4f7f_ffff | ||
/// Least Exact Float (64 bits) greater-than-or-equal-to u32::MIN when rounding towards zero. | ||
const LEF64_GEQ_U32_MIN: u64 = std::u32::MIN as u64; | ||
/// Greatest Exact Float (64 bits) less-than-or-equal-to u32::MAX when rounding towards zero. | ||
const GEF64_LEQ_U32_MAX: u64 = 4294967295; // bits as f64: 0x41ef_ffff_ffff_ffff | ||
/// Least Exact Float (32 bits) greater-than-or-equal-to i64::MIN when rounding towards zero. | ||
const LEF32_GEQ_I64_MIN: u64 = std::i64::MIN as u64; | ||
/// Greatest Exact Float (32 bits) less-than-or-equal-to i64::MAX when rounding towards zero. | ||
const GEF32_LEQ_I64_MAX: u64 = 9223371487098961920; // bits as f32: 0x5eff_ffff | ||
/// Least Exact Float (64 bits) greater-than-or-equal-to i64::MIN when rounding towards zero. | ||
const LEF64_GEQ_I64_MIN: u64 = std::i64::MIN as u64; | ||
/// Greatest Exact Float (64 bits) less-than-or-equal-to i64::MAX when rounding towards zero. | ||
const GEF64_LEQ_I64_MAX: u64 = 9223372036854774784; // bits as f64: 0x43df_ffff_ffff_ffff | ||
/// Least Exact Float (32 bits) greater-than-or-equal-to u64::MIN when rounding towards zero. | ||
const LEF32_GEQ_U64_MIN: u64 = std::u64::MIN; | ||
/// Greatest Exact Float (32 bits) less-than-or-equal-to u64::MAX when rounding towards zero. | ||
const GEF32_LEQ_U64_MAX: u64 = 18446742974197923840; // bits as f32: 0x5f7f_ffff | ||
/// Least Exact Float (64 bits) greater-than-or-equal-to u64::MIN when rounding towards zero. | ||
const LEF64_GEQ_U64_MIN: u64 = std::u64::MIN; | ||
/// Greatest Exact Float (64 bits) less-than-or-equal-to u64::MAX when rounding towards zero. | ||
const GEF64_LEQ_U64_MAX: u64 = 18446744073709549568; // bits as f64: 0x43ef_ffff_ffff_ffff |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment in the abstract, I don't expect you to fix this in this PR or necessarily soon, but on first glance, I'd prefer a struct with methods over numbers in a case like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After reviewing the PR I think it's not as bad as I first thought. I think it's probably possible to clean it up a bit though. Low priority all things considered though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the proposed cleanup here. Create a struct with lower/upper/min/max and a single method on it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just being more semantic where possible. It's a minor thing but a number is a very open ended thing. If it was a bunch of 0-sized structs with a method that returned an upper and lower bound, then mix ups would be less likely.
I don't think it's worth the effort here though -- I thought this was more complex than it actually is