diff --git a/Cargo.toml b/Cargo.toml
index ccaafd3..47fc8a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@
members = ["fuzz"]
[workspace.package]
-version = "0.0.0"
+version = "0.1.0+llvm-462a31f5a5ab"
edition = "2021"
license = "Apache-2.0 WITH LLVM-exception"
diff --git a/README.md b/README.md
index 8554912..60ddd5b 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,68 @@
-# `rustc_apfloat` (Rust port of C++ `llvm::APFloat` library)
+# `rustc_apfloat`
(Rust port of the C++ `llvm::APFloat` "softfloat" library)
+
+## History
+
+LLVM's `APFloat` (aka `llvm::APFloat`) software floating-point (or "softfloat")
+library was first ported to Rust (and named `rustc_apfloat`) back in 2017,
+in the Rust pull request [`rust-lang/rust#43554`](https://github.com/rust-lang/rust/pull/43554),
+as part of an effort to expand Rust compile-time capabilities without sacrificing
+determinism (and therefore soundness, if the type-system was involved).
+
+Note: while using the original C++ `llvm::APFloat` directly would've been an option,
+certain high-level API design differences made in the Rust port, without behavioral impact
+(C++ raw pointers and dynamic allocations vs Rust generics, traits and `#![no_std]`),
+made the Rust port more appealing from a determinism standpoint (mostly thanks to
+lacking all 3 of: `unsafe` code, host floating-point use, `std` access - and only
+allocating to handle the arbitrary precision needed for conversions to/from decimal),
+*even though there was a chance it had correctness issues unique to it*.
+
+However, that port had a fatal flaw: it was added to the `rust-lang/rust` repository
+without its unique licensing status (as a port of a C++ library with its own license)
+being properly tracked, communicated, taken into account, etc.
+The end result was years of limbo, mostly chronicled in the Rust issue
+[`rust-lang/rust#55993`](https://github.com/rust-lang/rust/issues/55993), in which
+the in-tree port couldn't really receive proper updated or even maintenance, due
+due to its unclear status.
+
+### Revival (as `rust-lang/rustc_apfloat`)
+
+This repository (`rust-lang/rustc_apfloat`) is the result of a 2022 plan on
+[the relevant Zulip topic](https://rust-lang.zulipchat.com/#narrow/stream/231349-t-core.2Flicensing/topic/apfloat), fully put into motion during 2023:
+* the `git` history of the in-tree `compiler/rustc_apfloat` library was extracted
+ (see the separate [`rustc_apfloat-git-history-extraction`](https://github.com/LykenSol/rustc_apfloat-git-history-extraction) repository for more details)
+* only commits that were *both* necessary *and* had clear copyright status, were kept
+* any missing functionality or bug fixes, would have to be either be re-contributed,
+ or rebuilt from the ground up (mostly the latter ended up being done, see below)
+
+Most changes since the original port had been aesthetic (e.g. spell-checking, `rustfmt`),
+so little was lost in the process.
+
+Starting from that much smaller "trusted" base:
+* everything could use LLVM's new (since 2019) license, "`Apache-2.0 WITH LLVM-exception`"
+ (see the ["Licensing"](#licensing) section below and/or [LICENSE-DETAILS.md](./LICENSE-DETAILS.md) for more details)
+* new facilities were built (benchmarks, and [a fuzzer comparing Rust/C++/hardware](#fuzzing))
+* excessive testing was performed (via a combination of fuzzing and bruteforce search)
+* latent bugs were discovered (e.g. LLVM issues
+[#63895](https://github.com/llvm/llvm-project/issues/63895) and
+[#63938](https://github.com/llvm/llvm-project/issues/63938))
+* the port has been forwarded in time, to include upstream (`llvm/llvm-project`) changes
+ to `llvm::APFloat` over the years (since 2017), removing the need for selective backports
+
+## Versioning
+
+As this is, for the time being, a "living port", tracking upstream (`llvm/llvm-project`)
+`llvm::APFloat` changes, the `rustc_apfloat` crate will have versions of the form:
-## 🚧 Work In Progress 🚧
+```
+0.X.Y+llvm-ZZZZZZZZZZZZ
+```
+* `X` is always bumped after semver-incompatible API changes,
+ or when updating the upstream (`llvm/llvm-project`) commit the port is based on
+* `Y` is only bumped when other parts of the version don't need to be (e.g. for bug fixes)
+* `+llvm-ZZZZZZZZZZZZ` is ["version metadata"](https://doc.rust-lang.org/cargo/reference/resolver.html#version-metadata) (which Cargo itself ignores),
+ and `ZZZZZZZZZZZZ` always holds the first 12 hexadecimal digits of
+ the upstream (`llvm/llvm-project`) `git` commit hash the port is based on
-**NOTE**: the repo (and [`rustc_apfloat-git-history-extraction`](https://github.com/LykenSol/rustc_apfloat-git-history-extraction)) might be public already, but only for convenience of discussion, see [relevant Zulip topic](https://rust-lang.zulipchat.com/#narrow/stream/231349-t-core.2Flicensing/topic/apfloat) for more details.
## Testing
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..a7398a2
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,41 @@
+// HACK(eddyb) easier dep-tracking if we let `rustc` do it.
+const SRC_LIB_RS_CONTENTS: &str = include_str!("src/lib.rs");
+
+const EXPECTED_SRC_LIB_RS_PREFIX: &str = "\
+//! Port of LLVM's APFloat software floating-point implementation from the
+//! following C++ sources (please update commit hash when backporting):
+//! https://github.com/llvm/llvm-project/commit/";
+
+fn main() {
+ // HACK(eddyb) disable the default of re-running the build script on *any*
+ // change to *the entire source tree* (i.e. the default is roughly `./`).
+ println!("cargo:rerun-if-changed=build.rs");
+
+ let llvm_commit_hash = SRC_LIB_RS_CONTENTS
+ .strip_prefix(EXPECTED_SRC_LIB_RS_PREFIX)
+ .ok_or(())
+ .map_err(|_| format!("expected `src/lib.rs` to start with:\n\n{EXPECTED_SRC_LIB_RS_PREFIX}"))
+ .and_then(|commit_hash_plus_rest_of_file| {
+ Ok(commit_hash_plus_rest_of_file
+ .split_once('\n')
+ .ok_or("expected `src/lib.rs` to have more than 3 lines")?)
+ })
+ .and_then(|(commit_hash, _)| {
+ if commit_hash.len() != 40 || !commit_hash.chars().all(|c| matches!(c, '0'..='9'|'a'..='f')) {
+ Err(format!("expected `src/lib.rs` to have a valid commit hash, found {commit_hash:?}"))
+ } else {
+ Ok(commit_hash)
+ }
+ })
+ .unwrap_or_else(|e| {
+ eprintln!("\n{e}\n");
+ panic!("failed to validate `src/lib.rs`'s commit hash (see above)")
+ });
+
+ let expected_version_metadata = format!("+llvm-{}", &llvm_commit_hash[..12]);
+ let actual_version = env!("CARGO_PKG_VERSION");
+ if !actual_version.ends_with(&expected_version_metadata) {
+ eprintln!("\nexpected version ending in `{expected_version_metadata}`, found `{actual_version}`\n");
+ panic!("failed to validate Cargo package version (see above)");
+ }
+}
diff --git a/fuzz/build.rs b/fuzz/build.rs
index 2cc18dc..f23b373 100644
--- a/fuzz/build.rs
+++ b/fuzz/build.rs
@@ -7,6 +7,10 @@ fn main() -> std::io::Result {
// change to *the entire source tree* (i.e. the default is roughly `./`).
println!("cargo:rerun-if-changed=build.rs");
+ // NOTE(eddyb) `rustc_apfloat`'s own `build.rs` validated the version string.
+ let (_, llvm_commit_hash) = env!("CARGO_PKG_VERSION").split_once("+llvm-").unwrap();
+ assert_eq!(llvm_commit_hash.len(), 12);
+
let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
std::fs::write(out_dir.join("generated_fuzz_ops.rs"), ops::generate_rust())?;
@@ -40,11 +44,7 @@ fn main() -> std::io::Result {
let sh_script_exit_status = Command::new("sh")
.args(["-c", SH_SCRIPT])
.envs([
- // FIXME(eddyb) ensure this is kept in sync.
- (
- "llvm_project_git_hash",
- "f3598e8fca83ccfb11f58ec7957c229e349765e3",
- ),
+ ("llvm_project_git_hash", llvm_commit_hash),
("cxx_apf_fuzz_exports", &cxx_exported_symbols.join(",")),
(
"cxx_apf_fuzz_is_fuzzing",
@@ -68,7 +68,7 @@ curl -sS "$llvm_project_tgz_url" | tar -C "$OUT_DIR" -xz
llvm="$OUT_DIR"/llvm-project-"$llvm_project_git_hash"/llvm
mkdir -p "$OUT_DIR"/fake-config/llvm/Config
-touch "$OUT_DIR"/fake-config/llvm/Config/{abi-breaking,llvm-config}.h
+touch "$OUT_DIR"/fake-config/llvm/Config/{abi-breaking,config,llvm-config}.h
# HACK(eddyb) we want standard `assert`s to work, but `NDEBUG` also controls
# unrelated LLVM facilities that are spread all over the place and it's harder
@@ -91,8 +91,8 @@ echo | clang++ -x c++ - -std=c++17 \
$clang_codegen_flags \
-I "$llvm"/include \
-I "$OUT_DIR"/fake-config \
- -DNDEBUG \
- --include="$llvm"/lib/Support/{APInt,APFloat,SmallVector}.cpp \
+ -DNDEBUG -DHAVE_UNISTD_H -DLLVM_ON_UNIX -DLLVM_ENABLE_THREADS=0 \
+ --include="$llvm"/lib/Support/{APInt,APFloat,SmallVector,ErrorHandling}.cpp \
--include="$OUT_DIR"/cxx_apf_fuzz.cpp \
-c -emit-llvm -o "$OUT_DIR"/cxx_apf_fuzz.bc
diff --git a/fuzz/ops.rs b/fuzz/ops.rs
index 5f7d081..fb1a45d 100644
--- a/fuzz/ops.rs
+++ b/fuzz/ops.rs
@@ -12,8 +12,8 @@ struct Cxx(T);
use self::OpKind::*;
enum OpKind {
- Unary(Rust, Cxx<&'static str>),
- Binary(Rust, Cxx<&'static str>),
+ Unary(char),
+ Binary(char),
Ternary(Rust<&'static str>, Cxx<&'static str>),
// HACK(eddyb) all other ops have floating-point inputs *and* outputs, so
@@ -25,6 +25,7 @@ enum OpKind {
enum Type {
SInt(usize),
UInt(usize),
+ Float(usize),
}
impl Type {
@@ -32,6 +33,7 @@ impl Type {
match self {
Type::SInt(w) => format!("i{w}"),
Type::UInt(w) => format!("u{w}"),
+ Type::Float(w) => format!("f{w}"),
}
}
}
@@ -39,8 +41,8 @@ impl Type {
impl OpKind {
fn inputs<'a, T>(&self, all_inputs: &'a [T; 3]) -> &'a [T] {
match self {
- Unary(..) | Roundtrip(_) => &all_inputs[..1],
- Binary(..) => &all_inputs[..2],
+ Unary(_) | Roundtrip(_) => &all_inputs[..1],
+ Binary(_) => &all_inputs[..2],
Ternary(..) => &all_inputs[..3],
}
}
@@ -48,18 +50,20 @@ impl OpKind {
const OPS: &[(&str, OpKind)] = &[
// Unary (`F -> F`) ops.
- ("Neg", Unary(Rust('-'), Cxx("changeSign"))),
+ ("Neg", Unary('-')),
// Binary (`(F, F) -> F`) ops.
- ("Add", Binary(Rust('+'), Cxx("add"))),
- ("Sub", Binary(Rust('-'), Cxx("subtract"))),
- ("Mul", Binary(Rust('*'), Cxx("multiply"))),
- ("Div", Binary(Rust('/'), Cxx("divide"))),
- ("Rem", Binary(Rust('%'), Cxx("mod"))),
+ ("Add", Binary('+')),
+ ("Sub", Binary('-')),
+ ("Mul", Binary('*')),
+ ("Div", Binary('/')),
+ ("Rem", Binary('%')),
// Ternary (`(F, F) -> F`) ops.
("MulAdd", Ternary(Rust("mul_add"), Cxx("fusedMultiplyAdd"))),
// Roundtrip (`F -> T -> F`) ops.
("FToI128ToF", Roundtrip(Type::SInt(128))),
("FToU128ToF", Roundtrip(Type::UInt(128))),
+ ("FToSingleToF", Roundtrip(Type::Float(32))),
+ ("FToDoubleToF", Roundtrip(Type::Float(64))),
];
fn all_ops_map_concat(f: impl Fn(usize, &'static str, &OpKind) -> String) -> String {
@@ -132,17 +136,21 @@ impl FuzzOp
where
HF: num_traits::Float
+ num_traits::AsPrimitive
- + num_traits::AsPrimitive,
+ + num_traits::AsPrimitive
+ + num_traits::AsPrimitive
+ + num_traits::AsPrimitive,
i128: num_traits::AsPrimitive,
u128: num_traits::AsPrimitive,
+ f32: num_traits::AsPrimitive,
+ f64: num_traits::AsPrimitive,
{
fn eval_hard(self) -> HF {
match self {
" + &all_ops_map_concat(|_tag, name, kind| {
let inputs = kind.inputs(&["a", "b", "c"]);
let expr = match kind {
- Unary(Rust(op), _) => format!("{op}{}", inputs[0]),
- Binary(Rust(op), _) => format!("{} {op} {}", inputs[0], inputs[1]),
+ Unary(op) => format!("{op}{}", inputs[0]),
+ Binary(op) => format!("{} {op} {}", inputs[0], inputs[1]),
Ternary(Rust(method), _) => {
format!("{}.{method}({}, {})", inputs[0], inputs[1], inputs[2])
}
@@ -163,14 +171,21 @@ impl FuzzOp
}
}
-impl FuzzOp {
- fn eval_rs_apf(self) -> RustcApFloat {
+impl FuzzOp
+ where
+ F: rustc_apfloat::Float
+ + rustc_apfloat::FloatConvert
+ + rustc_apfloat::FloatConvert,
+ rustc_apfloat::ieee::Single: rustc_apfloat::FloatConvert,
+ rustc_apfloat::ieee::Double: rustc_apfloat::FloatConvert,
+{
+ fn eval_rs_apf(self) -> F {
match self {
" + &all_ops_map_concat(|_tag, name, kind| {
let inputs = kind.inputs(&["a", "b", "c"]);
let expr = match kind {
- Unary(Rust(op), _) => format!("{op}{}", inputs[0]),
- Binary(Rust(op), _) => format!("({} {op} {}).value", inputs[0], inputs[1]),
+ Unary(op) => format!("{op}{}", inputs[0]),
+ Binary(op) => format!("({} {op} {}).value", inputs[0], inputs[1]),
Ternary(Rust(method), _) => {
format!("{}.{method}({}).value", inputs[0], inputs[1..].join(", "))
}
@@ -178,9 +193,23 @@ impl FuzzOp {
let (w, i_or_u) = match ty {
Type::SInt(w) => (w, "i"),
Type::UInt(w) => (w, "u"),
+ Type::Float(_) => unreachable!(),
};
format!(
- "RustcApFloat::from_{i_or_u}128({}.to_{i_or_u}128({w}).value).value",
+ "F::from_{i_or_u}128({}.to_{i_or_u}128({w}).value).value",
+ inputs[0],
+ )
+ }
+ Roundtrip(Type::Float(w)) => {
+ let rs_apf_type = match w {
+ 32 => "rustc_apfloat::ieee::Single",
+ 64 => "rustc_apfloat::ieee::Double",
+ _ => unreachable!(),
+ };
+ format!(
+ "rustc_apfloat::FloatConvert
+ ::convert(rustc_apfloat::FloatConvert::<{rs_apf_type}>
+ ::convert({}, &mut false).value, &mut false).value",
inputs[0],
)
}
@@ -226,43 +255,64 @@ struct FuzzOp {
F a, b, c;
F eval() const {
+
+ // HACK(eddyb) 'scratch' variables used by expressions below.
+ APFloat r(0.0);
+ APSInt i;
+ bool scratch_bool;
+
switch(tag) {
"
+ &all_ops_map_concat(|_tag, name, kind| {
let inputs = kind.inputs(&["a.to_apf()", "b.to_apf()", "c.to_apf()"]);
- let (this, args) = inputs.split_first().unwrap();
- let args = args.join(", ");
- let stmt = match kind {
- // HACK(eddyb) `mod` doesn't take a rounding mode.
- Unary(_, Cxx(method)) | Binary(_, Cxx(method @ "mod")) => {
- format!("r.{method}({args})")
- }
+ let expr = match kind {
+ // HACK(eddyb) `APFloat` doesn't overload `operator%`, so we have
+ // to go through the `mod` method instead.
+ Binary('%') => format!("((r = {}), r.mod({}), r)", inputs[0], inputs[1]),
+
+ Unary(op) => format!("{op}{}", inputs[0]),
+ Binary(op) => format!("{} {op} {}", inputs[0], inputs[1]),
- Binary(_, Cxx(method)) | Ternary(_, Cxx(method)) => {
- format!("r.{method}({args}, APFloat::rmNearestTiesToEven)")
+ Ternary(_, Cxx(method)) => {
+ format!(
+ "((r = {}), r.{method}({}, {}, APFloat::rmNearestTiesToEven), r)",
+ inputs[0], inputs[1], inputs[2]
+ )
}
Roundtrip(ty @ (Type::SInt(_) | Type::UInt(_))) => {
let (w, signed) = match ty {
Type::SInt(w) => (w, true),
Type::UInt(w) => (w, false),
+ Type::Float(_) => unreachable!(),
};
format!(
- "
- APSInt i({w}, !{signed});
- bool isExact;
- r.convertToInteger(i, APFloat::rmTowardZero, &isExact);
- r.convertFromAPInt(i, {signed}, APFloat::rmNearestTiesToEven)"
+ "((r = {}),
+ (i = APSInt({w}, !{signed})),
+ r.convertToInteger(i, APFloat::rmTowardZero, &scratch_bool),
+ r.convertFromAPInt(i, {signed}, APFloat::rmNearestTiesToEven),
+ r)",
+ inputs[0]
+ )
+ }
+ Roundtrip(Type::Float(w)) => {
+ let cxx_apf_semantics = match w {
+ 32 => "APFloat::IEEEsingle()",
+ 64 => "APFloat::IEEEdouble()",
+ _ => unreachable!(),
+ };
+ format!(
+ "((r = {input}),
+ r.convert({cxx_apf_semantics}, APFloat::rmNearestTiesToEven, &scratch_bool),
+ r.convert({input}.getSemantics(), APFloat::rmNearestTiesToEven, &scratch_bool),
+ r)",
+ input = inputs[0]
)
}
};
format!(
"
- case {name}: {{
- APFloat r = {this};
- {stmt};
- return F::from_apf(r);
- }}",
+ case {name}: return F::from_apf({expr});"
)
})
+ "
@@ -270,23 +320,28 @@ struct FuzzOp {
}
};
" + &[
- (16, "APFloat::IEEEhalf()"),
- (32, "APFloat::IEEEsingle()"),
- (64, "APFloat::IEEEdouble()"),
- (128, "APFloat::IEEEquad()"),
- (80, "APFloat::x87DoubleExtended()"),
+ (16, "IEEEhalf"),
+ (32, "IEEEsingle"),
+ (64, "IEEEdouble"),
+ (128, "IEEEquad"),
+ (16, "BFloat"),
+ (8, "Float8E5M2"),
+ (8, "Float8E4M3FN"),
+ (80, "x87DoubleExtended"),
]
.into_iter()
- .map(|(w, cxx_apf_semantics)| {
- let (name_prefix, uint_width) = match w {
- 80 => ("X87_F", 128),
- _ => ("IEEE", w),
+ .map(|(w, cxx_apf_semantics): (usize, _)| {
+ let uint_width = w.next_power_of_two();
+ let name = match (w, cxx_apf_semantics) {
+ (16, "BFloat") => "BrainF16".into(),
+ (8, s) if s.starts_with("Float8") => s.replace("Float8", "F8"),
+ (80, "x87DoubleExtended") => "X87_F80".into(),
+ _ => {
+ assert!(cxx_apf_semantics.starts_with("IEEE"));
+ format!("IEEE{w}")
+ }
};
- let name = format!("{name_prefix}{w}");
- let exported_symbol = format!(
- "cxx_apf_fuzz_eval_op_{}{w}",
- name_prefix.to_ascii_lowercase()
- );
+ let exported_symbol = format!("cxx_apf_fuzz_eval_op_{}", name.to_ascii_lowercase());
exported_symbols.push(exported_symbol.clone());
let uint = format!("uint{uint_width}_t");
format!(
@@ -309,11 +364,13 @@ struct __attribute__((packed)) {name} {{
}}
APFloat to_apf() const {{
- std::array
- words;
+ std::array<
+ APInt::WordType,
+ ({w} + APInt::APINT_BITS_PER_WORD - 1) / APInt::APINT_BITS_PER_WORD
+ > words;
for(int i = 0; i < {w}; i += APInt::APINT_BITS_PER_WORD)
words[i / APInt::APINT_BITS_PER_WORD] = bits >> i;
- return APFloat({cxx_apf_semantics}, APInt({w}, words));
+ return APFloat(APFloat::{cxx_apf_semantics}(), APInt({w}, words));
}}
}};
extern "C" {{
diff --git a/fuzz/src/main.rs b/fuzz/src/main.rs
index 32daf51..8d4fa07 100644
--- a/fuzz/src/main.rs
+++ b/fuzz/src/main.rs
@@ -1,13 +1,15 @@
use clap::{CommandFactory, Parser, Subcommand};
use rustc_apfloat::Float as _;
use std::fmt;
+use std::io::Write;
use std::mem::MaybeUninit;
+use std::num::NonZeroUsize;
use std::path::PathBuf;
// See `build.rs` and `ops.rs` for how `FuzzOp` is generated.
include!(concat!(env!("OUT_DIR"), "/generated_fuzz_ops.rs"));
-#[derive(Parser, Debug)]
+#[derive(Clone, Parser, Debug)]
struct Args {
/// Disable comparison with C++ (LLVM's original) APFloat
#[arg(long)]
@@ -21,18 +23,29 @@ struct Args {
#[arg(long)]
strict_hard_nan_sign: bool,
- /// Disable erasure of sNaN vs qNaN mismatches with hardware floating-point operations
+ /// Disable erasure of "which NaN input propagates" mismatches with hardware floating-point operations
#[arg(long)]
- strict_hard_qnan_vs_snan: bool,
+ strict_hard_nan_input_choice: bool,
+
+ /// Hide FMA NaN mismatches for `a * b + NaN` when `a * b` generates a new NaN
+ // HACK(eddyb) this is opt-in, not opt-out, because the APFloat behavior, of
+ // generating a new NaN (instead of propagating the existing one) is dubious,
+ // and may end up changing over time, so the only purpose this serves is to
+ // enable fuzzing against hardware without wasting time on these mismatches.
+ #[arg(long)]
+ ignore_fma_nan_generate_vs_propagate: bool,
#[command(subcommand)]
command: Option,
}
-#[derive(Subcommand, Debug)]
+#[derive(Clone, Subcommand, Debug)]
enum Commands {
/// Decode fuzzing in/out testcases (binary serialized `FuzzOp`s)
Decode { files: Vec },
+
+ /// Exhaustively test all possible ops and inputs for tiny (8-bit) formats
+ BruteforceTiny,
}
/// Trait implemented for types that describe a floating-point format supported
@@ -47,13 +60,24 @@ enum Commands {
/// all types implementing this trait *must* be annotated with `#[repr(C, packed)]`,
/// and `ops.rs` *must* also ensure exactly matching layout for the C++ counterpart.
trait FloatRepr: Copy + Default + Eq + fmt::Display {
- type RustcApFloat: rustc_apfloat::Float;
+ type RustcApFloat: rustc_apfloat::Float
+ + rustc_apfloat::Float
+ + rustc_apfloat::FloatConvert
+ + rustc_apfloat::FloatConvert;
const BIT_WIDTH: usize = Self::RustcApFloat::BITS;
const BYTE_LEN: usize = (Self::BIT_WIDTH + 7) / 8;
const NAME: &'static str;
+ // HACK(eddyb) this has to be overwritable because we have more than one
+ // format with the same `BIT_WIDTH`, so it's not unambiguous on its own.
+ const REPR_TAG: u8 = Self::BIT_WIDTH as u8;
+
+ fn short_lowercase_name() -> String {
+ Self::NAME.to_ascii_lowercase().replace("ieee", "f")
+ }
+
// FIXME(eddyb) these should ideally be using `[u8; Self::BYTE_LEN]`.
fn from_le_bytes(bytes: &[u8]) -> Self;
fn write_as_le_bytes_into(self, out_bytes: &mut Vec);
@@ -70,17 +94,24 @@ trait FloatRepr: Copy + Default + Eq + fmt::Display {
macro_rules! float_reprs {
($($name:ident($repr:ty) {
type RustcApFloat = $rs_apf_ty:ty;
+ $(const REPR_TAG = $repr_tag:expr;)?
extern fn = $cxx_apf_eval_fuzz_op:ident;
$(type HardFloat = $hard_float_ty:ty;)?
})+) => {
// HACK(eddyb) helper macro used to actually handle all types uniformly.
- macro_rules! dispatch_all_reprs {
- ($ty_var:ident => $e:expr) => {{
- $({
- type $ty_var = $name;
- $e
- })+
- }}
+ macro_rules! dispatch_any_float_repr_by_repr_tag {
+ (match $repr_tag_value:ident { for<$ty_var:ident: FloatRepr> => $e:expr }) => {
+ // NOTE(eddyb) this doubles as an overlap check: `REPR_TAG`
+ // values across *all* `FloatRepr` `impl` *must* be unique.
+ #[deny(unreachable_patterns)]
+ match $repr_tag_value {
+ $($name::REPR_TAG => {
+ type $ty_var = $name;
+ $e;
+ })+
+ _ => {}
+ }
+ }
}
$(
@@ -96,6 +127,8 @@ macro_rules! float_reprs {
const NAME: &'static str = stringify!($name);
+ $(const REPR_TAG: u8 = $repr_tag;)?
+
fn from_le_bytes(bytes: &[u8]) -> Self {
// HACK(eddyb) this allows using e.g. `u128` to hold 80 bits.
let mut repr_bytes = [0; std::mem::size_of::<$repr>()];
@@ -180,6 +213,23 @@ float_reprs! {
type RustcApFloat = rustc_apfloat::ieee::Quad;
extern fn = cxx_apf_fuzz_eval_op_ieee128;
}
+
+ // Non-standard IEEE-like formats.
+ F8E5M2(u8) {
+ type RustcApFloat = rustc_apfloat::ieee::Float8E5M2;
+ const REPR_TAG = 8 + 0;
+ extern fn = cxx_apf_fuzz_eval_op_f8e5m2;
+ }
+ F8E4M3FN(u8) {
+ type RustcApFloat = rustc_apfloat::ieee::Float8E4M3FN;
+ const REPR_TAG = 8 + 1;
+ extern fn = cxx_apf_fuzz_eval_op_f8e4m3fn;
+ }
+ BrainF16(u16) {
+ type RustcApFloat = rustc_apfloat::ieee::BFloat;
+ const REPR_TAG = 16 + 1;
+ extern fn = cxx_apf_fuzz_eval_op_brainf16;
+ }
X87_F80(u128) {
type RustcApFloat = rustc_apfloat::ieee::X87DoubleExtended;
extern fn = cxx_apf_fuzz_eval_op_x87_f80;
@@ -193,17 +243,20 @@ struct FuzzOpEvalOutputs {
}
impl FuzzOpEvalOutputs {
- fn assert_all_match(self) {
- if let Some(cxx_apf) = self.cxx_apf {
- assert!(cxx_apf == self.rs_apf);
- }
- if let Some(hard) = self.hard {
- assert!(hard == self.rs_apf);
- }
+ fn all_match(self) -> bool {
+ [self.cxx_apf, self.hard]
+ .into_iter()
+ .flatten()
+ .all(|x| x == self.rs_apf)
}
}
-impl FuzzOp {
+impl FuzzOp
+// FIXME(eddyb) such bounds shouldn't be here, but `FloatRepr` can't imply them.
+where
+ rustc_apfloat::ieee::Single: rustc_apfloat::FloatConvert,
+ rustc_apfloat::ieee::Double: rustc_apfloat::FloatConvert,
+{
fn try_decode(data: &[u8]) -> Result {
let (&tag, inputs) = data.split_first().ok_or(())?;
if inputs.len() % F::BYTE_LEN != 0 {
@@ -285,15 +338,66 @@ impl FuzzOp {
// Allow using CLI flags to toggle whether differences vs hardware are
// erased (by copying e.g. signs from the `rustc_apfloat` result) or kept.
// FIXME(eddyb) figure out how much we can really validate against hardware.
+ let mut strict_nan_bits_mask = !0;
+ if !cli_args.strict_hard_nan_sign {
+ strict_nan_bits_mask &= !sign_bit_mask;
+ };
+
let rs_apf_bits = out.rs_apf.to_bits_u128();
if is_nan(out_hard_bits) && is_nan(rs_apf_bits) {
- for (strict, bit_mask) in [
- (cli_args.strict_hard_nan_sign, sign_bit_mask),
- (cli_args.strict_hard_qnan_vs_snan, qnan_bit_mask),
- ] {
- if !strict {
- out_hard_bits &= !bit_mask;
- out_hard_bits |= rs_apf_bits & bit_mask;
+ out_hard_bits &= strict_nan_bits_mask;
+ out_hard_bits |= rs_apf_bits & !strict_nan_bits_mask;
+
+ // There is still a NaN payload difference, check if they both
+ // are propagated inputs (verbatim or at most "quieted" if SNaN),
+ // because in some cases with multiple NaN inputs, something
+ // (hardware or even e.g. LLVM passes or instruction selection)
+ // along the way from Rust code to final results, can end up
+ // causing a different input NaN to get propagated to the result.
+ if !cli_args.strict_hard_nan_input_choice && out_hard_bits != rs_apf_bits {
+ let out_nan_is_propagated_input = |out_nan_bits| {
+ assert!(is_nan(out_nan_bits));
+ let mut found_any_matching_inputs = false;
+ self.map(F::to_bits_u128).map(|in_bits| {
+ // NOTE(eddyb) this `is_nan` check is important, as
+ // `INFINITY.to_bits() | qnan_bit_mask == NAN.to_bits()`,
+ // i.e. seeting the QNaN is more than enough to turn
+ // a non-NaN (infinities, specifically) into a NaN.
+ if is_nan(in_bits) {
+ // Make sure to "quiet" (i.e. turn SNaN into QNaN)
+ // the input first, as propagation does (in the
+ // default exception handling mode, at least).
+ if (in_bits | qnan_bit_mask) & strict_nan_bits_mask
+ == out_nan_bits & strict_nan_bits_mask
+ {
+ found_any_matching_inputs = true;
+ }
+ }
+ });
+ found_any_matching_inputs
+ };
+ if out_nan_is_propagated_input(out_hard_bits)
+ && out_nan_is_propagated_input(rs_apf_bits)
+ {
+ out_hard_bits = rs_apf_bits;
+ }
+ }
+
+ // HACK(eddyb) last chance to hide a NaN payload difference,
+ // in this case for FMAs of the form `a * b + NaN`, when `a * b`
+ // generates a new NaN (which hardware can ignore in favor of the
+ // existing NaN, but APFloat returns the fresh new NaN instead).
+ if cli_args.ignore_fma_nan_generate_vs_propagate && out_hard_bits != rs_apf_bits {
+ if let FuzzOp::MulAdd(a, b, c) = self.map(F::to_bits_u128) {
+ if !is_nan(a)
+ && !is_nan(b)
+ && is_nan(c)
+ && out_hard_bits & strict_nan_bits_mask
+ == (c | qnan_bit_mask) & strict_nan_bits_mask
+ && rs_apf_bits == F::RustcApFloat::NAN.to_bits()
+ {
+ out_hard_bits = rs_apf_bits;
+ }
}
}
}
@@ -333,8 +437,11 @@ impl FuzzOp {
}
}
- let short_float_type_name = F::NAME.to_ascii_lowercase().replace("ieee", "f");
- println!(" {short_float_type_name}.{:?}", self.map(FloatPrintHelper));
+ println!(
+ " {}.{:?}",
+ F::short_lowercase_name(),
+ self.map(FloatPrintHelper)
+ );
// HACK(eddyb) this lets us show all files even if some cause panics.
let FuzzOpEvalOutputs {
@@ -366,6 +473,143 @@ impl FuzzOp {
cxx_apf.map(|x| print(x, "C++ / llvm::APFloat"));
hard.map(|x| print(x, "native hardware floats"));
}
+
+ /// [`Commands::BruteforceTiny`] implementation (for a specific choice of `F`),
+ /// returning `Err(mismatch_count)` if there were any mismatches.
+ //
+ // HACK(eddyb) this is a method here because of the bounds `eval` needs, which
+ // are thankfully on the whole `impl`, so `Self::eval` is callable.
+ fn bruteforce_tiny(cli_args: &Args) -> Result<(), NonZeroUsize> {
+ // Here "tiny" is "8-bit" - 16-bit floats could maybe also be bruteforced,
+ // but the cost increases exponentially, so less useful relative to fuzzing.
+ if F::BIT_WIDTH > 8 {
+ return Ok(());
+ }
+
+ // HACK(eddyb) avoid reporting panics while iterating.
+ std::panic::set_hook(Box::new(|_| {}));
+
+ let all_ops = (0..)
+ .map(FuzzOp::from_tag)
+ .take_while(|op| op.is_some())
+ .map(|op| op.unwrap());
+
+ let op_to_exhaustive_cases = |op: FuzzOp<()>| {
+ let mut total_bit_width = 0;
+ op.map(|()| total_bit_width += F::BIT_WIDTH);
+ (0..usize::checked_shl(1, total_bit_width as u32).unwrap()).map(move |i| -> Self {
+ let mut combined_input_bits = i;
+ let op_with_inputs = op.map(|()| {
+ let x = combined_input_bits & ((1 << F::BIT_WIDTH) - 1);
+ combined_input_bits >>= F::BIT_WIDTH;
+ F::from_bits_u128(x.try_into().unwrap())
+ });
+ assert_eq!(combined_input_bits, 0);
+ op_with_inputs
+ })
+ };
+
+ let num_total_cases = all_ops
+ .clone()
+ .map(|op| op_to_exhaustive_cases(op).len())
+ .try_fold(0, usize::checked_add)
+ .unwrap();
+
+ let float_name = F::short_lowercase_name();
+ println!("Exhaustively checking all {num_total_cases} cases for {float_name}:",);
+
+ const NUM_DOTS: usize = 80;
+ let cases_per_dot = num_total_cases / NUM_DOTS;
+ let mut cases_in_this_dot = 0;
+ let mut mismatches_in_this_dot = false;
+ let mut num_mismatches = 0;
+ let mut select_mismatches = vec![];
+ let mut all_panics = vec![];
+ for op in all_ops {
+ let mut first_mismatch = None;
+ for op_with_inputs in op_to_exhaustive_cases(op) {
+ cases_in_this_dot += 1;
+ if cases_in_this_dot >= cases_per_dot {
+ cases_in_this_dot -= cases_per_dot;
+ if mismatches_in_this_dot {
+ mismatches_in_this_dot = false;
+ print!("X");
+ } else {
+ print!(".")
+ }
+ // HACK(eddyb) get around `stdout` line buffering.
+ std::io::stdout().flush().unwrap();
+ }
+
+ // HACK(eddyb) there are still panics we need to account for,
+ // e.g. https://github.com/llvm/llvm-project/issues/63895, and
+ // even if the Rust code didn't panic, LLVM asserts would trip.
+ match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ op_with_inputs.eval(cli_args)
+ })) {
+ Ok(out) => {
+ if !out.all_match() {
+ num_mismatches += 1;
+ mismatches_in_this_dot = true;
+ if first_mismatch.is_none() {
+ first_mismatch = Some(op_with_inputs);
+ }
+ }
+ }
+ Err(_) => {
+ mismatches_in_this_dot = true;
+ all_panics.push(op_with_inputs);
+ }
+ }
+ }
+ select_mismatches.extend(first_mismatch);
+ }
+ println!();
+
+ // HACK(eddyb) undo what we did at the start of this function.
+ let _ = std::panic::take_hook();
+
+ if num_mismatches > 0 {
+ assert!(!select_mismatches.is_empty());
+ println!();
+ println!(
+ "!!! found {num_mismatches} ({:.1}%) mismatches for {float_name}, showing {} of them:",
+ (num_mismatches as f64) / (num_total_cases as f64) * 100.0,
+ select_mismatches.len(),
+ );
+ for mismatch in select_mismatches {
+ mismatch.print_op_and_eval_outputs(cli_args);
+ }
+ println!();
+ } else {
+ assert!(select_mismatches.is_empty());
+ }
+
+ if !all_panics.is_empty() {
+ // HACK(eddyb) there is a good chance C++ will also fail, so avoid
+ // triggering the (more fatal) C++ assertion failure.
+ let cli_args_plus_ignore_cxx = Args {
+ ignore_cxx: true,
+ ..cli_args.clone()
+ };
+
+ println!(
+ "!!! found {} panics for {float_name}, showing them (without trying C++):",
+ all_panics.len()
+ );
+ for &panicking_case in &all_panics {
+ panicking_case.print_op_and_eval_outputs(&cli_args_plus_ignore_cxx);
+ }
+ println!();
+ }
+
+ if num_mismatches == 0 && all_panics.is_empty() {
+ println!("all {num_total_cases} cases match");
+ println!();
+ }
+
+ NonZeroUsize::new(num_mismatches + all_panics.len()).map_or(Ok(()), Err)
+ }
}
fn main() {
@@ -380,29 +624,45 @@ fn main() {
data.split_first()
.ok_or("empty file")
- .and_then(|(&bit_width, data)| {
- dispatch_all_reprs!(F => if bit_width as usize == F::BIT_WIDTH {
- FuzzOp::::try_decode(data)
- .ok()
- .ok_or(std::any::type_name::>())?
- .print_op_and_eval_outputs(&cli_args);
- return Ok(());
+ .and_then(|(&repr_tag, data)| {
+ dispatch_any_float_repr_by_repr_tag!(match repr_tag {
+ for => return Ok(
+ FuzzOp::::try_decode(data)
+ .ok()
+ .ok_or(std::any::type_name::>())?
+ .print_op_and_eval_outputs(&cli_args)
+ )
});
- Err("first byte not valid bit width")
+ Err("first byte not valid `FloatRepr::REPR_TAG`")
})
.unwrap_or_else(|e| println!(" invalid data ({e})"));
}
}
+ Commands::BruteforceTiny => {
+ let mut any_mismatches = false;
+ for repr_tag in 0..=u8::MAX {
+ dispatch_any_float_repr_by_repr_tag!(match repr_tag {
+ for => {
+ any_mismatches |= FuzzOp::::bruteforce_tiny(&cli_args).is_err();
+ }
+ });
+ }
+ if any_mismatches {
+ // FIXME(eddyb) use `fn main() -> ExitStatus`.
+ std::process::exit(1);
+ }
+ }
}
return;
}
#[cfg_attr(not(fuzzing), allow(unused))]
let fuzz_one_op = |data: &[u8]| {
- data.split_first().and_then(|(&bit_width, data)| {
- dispatch_all_reprs!(F => if bit_width as usize == F::BIT_WIDTH {
- FuzzOp::::try_decode(data).ok()?.eval(&cli_args).assert_all_match();
- return Some(());
+ data.split_first().and_then(|(&repr_tag, data)| {
+ dispatch_any_float_repr_by_repr_tag!(match repr_tag {
+ for => return Some(
+ assert!(FuzzOp::::try_decode(data).ok()?.eval(&cli_args).all_match())
+ )
});
None
});
diff --git a/src/ieee.rs b/src/ieee.rs
index 54ca219..d6719f5 100644
--- a/src/ieee.rs
+++ b/src/ieee.rs
@@ -17,10 +17,20 @@ pub struct IeeeFloat {
exp: ExpInt,
/// What kind of floating point number this is.
- category: Category,
+ //
+ // HACK(eddyb) because mutating this without accounting for `exp`/`sig`
+ // can break some subtle edge cases, it should be only read through the
+ // `.category()` method, and only set during initialization, either for
+ // one of the special value constants, or for conversion from bits.
+ read_only_category_do_not_mutate: Category,
/// Sign bit of the number.
- sign: bool,
+ //
+ // HACK(eddyb) because mutating this without accounting for `category`
+ // can break some subtle edge cases, it should be only read through the
+ // `.is_negative()` method, and only set through negation (which can be
+ // more easily used through e.g. `copy_sign`/`negate_if`/`with_sign`).
+ read_only_sign_do_not_mutate: bool,
marker: PhantomData,
}
@@ -62,62 +72,155 @@ enum Loss {
MoreThanHalf, // 1xxxxx x's not all zero
}
+/// How the nonfinite values Inf and NaN are represented.
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum NonfiniteBehavior {
+ /// Represents standard IEEE 754 behavior. A value is nonfinite if the
+ /// exponent field is all 1s. In such cases, a value is Inf if the
+ /// significand bits are all zero, and NaN otherwise
+ IEEE754,
+
+ /// Only the Float8E5M2 has this behavior. There is no Inf representation. A
+ /// value is NaN if the exponent field and the mantissa field are all 1s.
+ /// This behavior matches the FP8 E4M3 type described in
+ /// https://arxiv.org/abs/2209.05433. We treat both signed and unsigned NaNs
+ /// as non-signalling, although the paper does not state whether the NaN
+ /// values are signalling or not.
+ NanOnly,
+}
+
+// HACK(eddyb) extension method flipping/changing the sign based on `bool`s.
+trait NegExt: Neg