Skip to content
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

seq: add f128 code paths #6630

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -285,6 +285,7 @@ crossterm = ">=0.27.0"
ctrlc = { version = "3.4.4", features = ["termination"] }
dns-lookup = { version = "2.0.4" }
exacl = "0.12.0"
f128 = "0.2.9" # remove once support lands in stdlib https://github.com/rust-lang/rust/issues/116909#issuecomment-1969554030
file_diff = "1.0.0"
filetime = "0.2.23"
fnv = "1.0.7"
1 change: 1 addition & 0 deletions src/uu/seq/Cargo.toml
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ path = "src/seq.rs"
[dependencies]
bigdecimal = { workspace = true }
clap = { workspace = true }
f128 = { workspace = true, optional = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
uucore = { workspace = true, features = ["format", "quoting-style"] }
153 changes: 152 additions & 1 deletion src/uu/seq/src/extendedbigdecimal.rs
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@
//! ```
use std::cmp::Ordering;
use std::fmt::Display;
use std::ops::Add;
use std::ops::{Add, Neg};

use bigdecimal::BigDecimal;
use num_traits::Zero;
@@ -76,6 +76,157 @@
pub fn one() -> Self {
Self::BigDecimal(1.into())
}

pub fn from_f128(value: f128) -> Self {
// this code is adapted from num_bigint::BigDecimal::from_f64, but without the fast path for
// subnormal f128s and all in one function

let (neg, pow, frac) = match value.classify() {
std::num::FpCategory::Nan => return ExtendedBigDecimal::Nan,

Check warning on line 85 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L85

Added line #L85 was not covered by tests
std::num::FpCategory::Infinite => {
return if value.is_sign_negative() {
ExtendedBigDecimal::MinusInfinity

Check warning on line 88 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L88

Added line #L88 was not covered by tests
} else {
ExtendedBigDecimal::Infinity

Check warning on line 90 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L90

Added line #L90 was not covered by tests
};
}
std::num::FpCategory::Zero => {
return if value.is_sign_negative() {
ExtendedBigDecimal::MinusZero

Check warning on line 95 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L95

Added line #L95 was not covered by tests
} else {
ExtendedBigDecimal::zero()
};
}
std::num::FpCategory::Subnormal | std::num::FpCategory::Normal => {
/// f128::MANTISSA_DIGITS is 113 (because of the leading 1 in normal floats, but it
/// actually only has 112-bits)
const MANTISSA_BITS: u32 = f128::MANTISSA_DIGITS - 1;
/// The value of the leading one
const MANTISSA_LEADING_ONE: u128 = 1 << MANTISSA_BITS;
/// A mask that is all ones for the matissa bits
const MANTISSA_MASK: u128 = MANTISSA_LEADING_ONE - 1;
/// 15-bit exponent
const EXPONENT_MASK: u128 = (1 << 15) - 1;

let bits = value.to_bits();

// extract mantissa (mask out the rest of the bits and add the leading one)
let frac = (bits & MANTISSA_MASK)
+ if value.is_normal() {
MANTISSA_LEADING_ONE
} else {
0

Check warning on line 118 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L118

Added line #L118 was not covered by tests
};

// extract exponent (remove mantissa then mask out the rest of the bits (sign bit))
let exp = (bits >> MANTISSA_BITS) & EXPONENT_MASK;

// convert exponent to a power of two (subtract bias and size of mantissa)
let pow = exp as i64 - 16383 - i64::from(MANTISSA_BITS);

(value.is_sign_negative(), pow, frac)
}
};
let (frac, scale) = match pow.cmp(&0) {
Ordering::Less => {
let trailing_zeros = std::cmp::min(frac.trailing_zeros(), -pow as u32);

// Reduce fraction by removing common factors
let reduced_frac = frac >> trailing_zeros;
let reduced_pow = pow + trailing_zeros as i64;

// We need to scale up by 5^reduced_pow as `scale` is 10^scale instead of 2^scale
// (and 10^scale = 5^scale * 2^scale)
(
reduced_frac * num_bigint::BigUint::from(5u8).pow(-reduced_pow as u32),
// scale is positive if the power is negative, so flip the sign
-reduced_pow,
)
}
Ordering::Equal => (num_bigint::BigUint::from(frac), 0),
Ordering::Greater => (frac * num_bigint::BigUint::from(2u32).pow(pow as u32), 0),

Check warning on line 147 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L146-L147

Added lines #L146 - L147 were not covered by tests
};

ExtendedBigDecimal::BigDecimal(BigDecimal::new(
num_bigint::BigInt::from_biguint(
if neg {
num_bigint::Sign::Minus
} else {
num_bigint::Sign::Plus
},
frac,
),
scale,
))
}

pub fn to_f128(&self) -> f128 {
match self {
ExtendedBigDecimal::Infinity => f128::INFINITY,
ExtendedBigDecimal::MinusInfinity => f128::NEG_INFINITY,
ExtendedBigDecimal::MinusZero => -0.0f128,
ExtendedBigDecimal::Nan => f128::NAN,

Check warning on line 168 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L166-L168

Added lines #L166 - L168 were not covered by tests
// Adapted from <BigDecimal as ToPrimitive>::to_f64
ExtendedBigDecimal::BigDecimal(n) => {
// Descruture BigDecimal
let (n, e) = n.as_bigint_and_exponent();
let bits = n.bits();
let (sign, digits) = n.to_u64_digits();

// Extract most significant digits (we truncate the rest as they don't affect the
// conversion to f128):
//
// digits are stores in reverse order (e.g. 1u128 << 64 = [0, 1])
let (mantissa, exponent) = match digits[..] {
// Last two digits
[.., a, b] => {
let m = (u128::from(b) << 64) + u128::from(a);

Check warning on line 183 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L183

Added line #L183 was not covered by tests

// Strip mantissa digits from the exponent:
//
// Assume mantissa = 0b0...0110000 truncated rest
// ^...^^^^^^^ (size = u128::BITS)
// ^...^^^ mantissa
// ^^^^ (size = mantissa.trailing_zeros())
// ^^^^ ^^^^^...^^^^^^ exponent
// ^...^^^^^^^ ^^^^^...^^^^^^ (size = bits)
// u128::BITS - mantissa.trailing_zeros() = bits(mantissa)
// bits - bits(mantissa) = exponenet
let e = bits - u64::from(u128::BITS - m.trailing_zeros());

Check warning on line 195 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L195

Added line #L195 was not covered by tests
// FIXME: something is wrong here
(m >> m.trailing_zeros(), e)
}

Check warning on line 198 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L197-L198

Added lines #L197 - L198 were not covered by tests
// Single digit
// FIXME: something is wrong here
[a] => (
u128::from(a) >> a.trailing_zeros(),
a.trailing_zeros().into(),
),
// Zero (fast path)
[] => return 0.0,
};

// Convert to f128
let val = if exponent > f128::MAX_EXP as u64 {
f128::INFINITY

Check warning on line 211 in src/uu/seq/src/extendedbigdecimal.rs

Codecov / codecov/patch

src/uu/seq/src/extendedbigdecimal.rs#L211

Added line #L211 was not covered by tests
} else {
// matissa * 2^exponent * 10^(-e)
// ^^^^^^^ big decimal exponent
// ^^^^^^^^^^^^^^^^^^^^ big uint to f128
(mantissa as f128)
* f128::powi(2.0, exponent as i32)
* f128::powi(10.0, e.neg() as i32)
};

// Set sign
if matches!(sign, num_bigint::Sign::Minus) {
-val
} else {
val
}
}
}
}
}

impl Display for ExtendedBigDecimal {
160 changes: 125 additions & 35 deletions src/uu/seq/src/seq.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#![feature(f128)]

// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) extendedbigdecimal numberparse
use std::io::{stdout, ErrorKind, Write};
use std::str::FromStr;

use clap::{crate_version, Arg, ArgAction, Command};
use num_traits::{ToPrimitive, Zero};
@@ -31,6 +34,7 @@
const OPT_TERMINATOR: &str = "terminator";
const OPT_EQUAL_WIDTH: &str = "equal-width";
const OPT_FORMAT: &str = "format";
const OPT_BIGDECIMAL: &str = "bigdecimal";

const ARG_NUMBERS: &str = "numbers";

@@ -39,6 +43,7 @@
separator: String,
terminator: String,
equal_width: bool,
bigdecimal: bool,

Check warning on line 46 in src/uu/seq/src/seq.rs

Codecov / codecov/patch

src/uu/seq/src/seq.rs#L46

Added line #L46 was not covered by tests
format: Option<&'a str>,
}

@@ -71,38 +76,37 @@
.unwrap_or("\n")
.to_string(),
equal_width: matches.get_flag(OPT_EQUAL_WIDTH),
bigdecimal: matches.get_flag(OPT_BIGDECIMAL),
format: matches.get_one::<String>(OPT_FORMAT).map(|s| s.as_str()),
};

let first = if numbers.len() > 1 {
match numbers[0].parse() {
Ok(num) => num,
Err(e) => return Err(SeqError::ParseError(numbers[0].to_string(), e).into()),
}
} else {
PreciseNumber::one()
};
let increment = if numbers.len() > 2 {
match numbers[1].parse() {
Ok(num) => num,
Err(e) => return Err(SeqError::ParseError(numbers[1].to_string(), e).into()),
}
} else {
PreciseNumber::one()
let to_precise_number =
|n| PreciseNumber::from_str(n).map_err(|err| SeqError::ParseError(n.to_string(), err));

let (first, increment, last) = match numbers[..] {
[last] => (
PreciseNumber::one(),
PreciseNumber::one(),
to_precise_number(last)?,
),
[first, last] => (
to_precise_number(first)?,
PreciseNumber::one(),
to_precise_number(last)?,
),
[first, increment, last] => (
to_precise_number(first)?,
to_precise_number(increment)?,
to_precise_number(last)?,
),
// We are guaranteed that `numbers.len()` is greater than zero and at most three because of
// the argument specification in `uu_app()`.
_ => unreachable!(),

Check warning on line 104 in src/uu/seq/src/seq.rs

Codecov / codecov/patch

src/uu/seq/src/seq.rs#L104

Added line #L104 was not covered by tests
};

if increment.is_zero() {
return Err(SeqError::ZeroIncrement(numbers[1].to_string()).into());
return Err(SeqError::ZeroIncrement(numbers[1].clone()).into());

Check warning on line 108 in src/uu/seq/src/seq.rs

Codecov / codecov/patch

src/uu/seq/src/seq.rs#L108

Added line #L108 was not covered by tests
}
let last: PreciseNumber = {
// We are guaranteed that `numbers.len()` is greater than zero
// and at most three because of the argument specification in
// `uu_app()`.
let n: usize = numbers.len();
match numbers[n - 1].parse() {
Ok(num) => num,
Err(e) => return Err(SeqError::ParseError(numbers[n - 1].to_string(), e).into()),
}
};

let padding = first
.num_integral_digits
@@ -119,15 +123,38 @@
}
None => None,
};
let result = print_seq(
(first.number, increment.number, last.number),
largest_dec,
&options.separator,
&options.terminator,
options.equal_width,
padding,
&format,
);

let result = if options.bigdecimal {
print_seq(
(first.number, increment.number, last.number),

Check warning on line 129 in src/uu/seq/src/seq.rs

Codecov / codecov/patch

src/uu/seq/src/seq.rs#L128-L129

Added lines #L128 - L129 were not covered by tests
largest_dec,
&options.separator,
&options.terminator,
options.equal_width,

Check warning on line 133 in src/uu/seq/src/seq.rs

Codecov / codecov/patch

src/uu/seq/src/seq.rs#L131-L133

Added lines #L131 - L133 were not covered by tests
padding,
&format,
)
} else {
// TODO: remove once f128::from_str() is available
let first = first.number.to_f128();
let increment = increment.number.to_f128();
let last = last.number.to_f128();

if increment == 0.0 {
return Err(SeqError::ZeroIncrement(numbers[1].clone()).into());

Check warning on line 144 in src/uu/seq/src/seq.rs

Codecov / codecov/patch

src/uu/seq/src/seq.rs#L144

Added line #L144 was not covered by tests
}

print_seq_128(
(first, increment, last),
largest_dec,
&options.separator,
&options.terminator,
options.equal_width,
padding,
&format,
)
};

match result {
Ok(_) => Ok(()),
Err(err) if err.kind() == ErrorKind::BrokenPipe => Ok(()),
@@ -173,6 +200,12 @@
.action(ArgAction::Append)
.num_args(1..=3),
)
.arg(
Arg::new(OPT_BIGDECIMAL)
.long(OPT_BIGDECIMAL)
.help("use bigdecimal instead of f128 (breaks GNU compatibility but is more accurate)")
.action(ArgAction::SetTrue)
)
}

fn done_printing<T: Zero + PartialOrd>(next: &T, increment: &T, last: &T) -> bool {
@@ -258,3 +291,60 @@
stdout.flush()?;
Ok(())
}

/// f128 based code path
fn print_seq_128(
(first, increment, last): (f128, f128, f128),
largest_dec: usize,
separator: &str,
terminator: &str,
pad: bool,
padding: usize,
format: &Option<Format<num_format::Float>>,
) -> std::io::Result<()> {
let stdout = stdout();
let mut stdout = stdout.lock();
let mut value = first;
let padding = if pad {
padding + if largest_dec > 0 { largest_dec + 1 } else { 0 }
} else {
0
};
let mut is_first_iteration = true;
// inlined done_printing as f128 does not implement Zero
while !(increment >= 0.0 && value > last) || (increment < 0.0 && value < last) {
if !is_first_iteration {
write!(stdout, "{separator}")?;
}
// If there was an argument `-f FORMAT`, then use that format
// template instead of the default formatting strategy.
//
// TODO The `printf()` method takes a string as its second
// parameter but we have an `ExtendedBigDecimal`. In order to
// satisfy the signature of the function, we convert the
// `ExtendedBigDecimal` into a string. The `printf()`
// logic will subsequently parse that string into something
// similar to an `ExtendedBigDecimal` again before rendering
// it as a string and ultimately writing to `stdout`. We
// shouldn't have to do so much converting back and forth via
// strings.
match &format {
Some(f) => {
f.fmt(&mut stdout, value as f64)?;
}
None => write!(
&mut stdout,
"{:>0padding$.largest_dec$}",
// TODO: remove once impl Display for f128 is added
ExtendedBigDecimal::from_f128(value)
)?,
}
value += increment;
is_first_iteration = false;
}
if !is_first_iteration {
write!(stdout, "{terminator}")?;
}
stdout.flush()?;
Ok(())
}
18 changes: 18 additions & 0 deletions tests/by-util/test_seq.rs
Original file line number Diff line number Diff line change
@@ -827,3 +827,21 @@ fn test_invalid_format() {
.no_stdout()
.stderr_contains("format '%g%g' has too many % directives");
}

// Requires f128 code
//#[test]
//fn test_rejects_not_f128() {
// new_ucmd!()
// .args(&["12e4931", "12e4931"])
// .fails()
// .no_stdout()
// .stderr_contains("invalid argument");
//}

#[test]
fn test_accepts_f128() {
new_ucmd!()
.args(&["11e4931", "11e4931"])
.succeeds()
.stdout_only("11e+4931\n");
}