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

Add option to control trailing zero in floating-point literals #5834

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
50 changes: 50 additions & 0 deletions Configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,56 @@ Control the case of the letters in hexadecimal literal values
- **Possible values**: `Preserve`, `Upper`, `Lower`
- **Stable**: No (tracking issue: [#5081](https://github.com/rust-lang/rustfmt/issues/5081))

## `float_literal_trailing_zero`

Control the presence of trailing zero in floating-point literal values

- **Default value**: `Preserve`
- **Possible values**: `Preserve`, `Always`, `IfNoPostfix`, `Never`
- **Stable**: No (tracking issue: [#3187](https://github.com/rust-lang/rustfmt/issues/3187))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we'll need to add a new tracking issue for this one. #3187 isn't a tracking issue. We can certainly add the tracking issue after this PR is merged.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we create an issue now, so that we don't have to go back and modify the link later?
Is there a document that describes how a tracking issue should look like?


#### `Preserve` (default):

Leave the literal as-is.
amatveiakin marked this conversation as resolved.
Show resolved Hide resolved

```rust
fn main() {
let values = [1.0, 2., 3.0e10, 4f32];
}
```

#### `Always`:

Add a trailing zero to the literal:

```rust
fn main() {
let values = [1.0, 2.0, 3.0e10, 4.0f32];
}
```

#### `IfNoPostfix`:

Add a trailing zero by default. If the literal contains an exponent or a suffix, the zero
and the preceding period are removed:

```rust
fn main() {
let values = [1.0, 2.0, 3e10, 4f32];
}
```

#### `Never`:

Remove the trailing zero. If the literal contains an exponent or a suffix, the preceding
period is also removed:

```rust
fn main() {
let values = [1., 2., 3e10, 4f32];
}
```

## `hide_parse_errors`

This option is deprecated and has been renamed to `show_parse_errors` to avoid confusion around the double negative default of `hide_parse_errors=false`.
Expand Down
3 changes: 3 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ create_config! {
"Skip formatting the bodies of macros invoked with the following names.";
hex_literal_case: HexLiteralCase, HexLiteralCase::Preserve, false,
"Format hexadecimal integer literals";
float_literal_trailing_zero: FloatLiteralTrailingZero, FloatLiteralTrailingZero::Preserve,
false, "Add or remove trailing zero in floating-point literals";

// Single line expressions and items
empty_item_single_line: bool, true, false,
Expand Down Expand Up @@ -645,6 +647,7 @@ format_macro_matchers = false
format_macro_bodies = true
skip_macro_invocations = []
hex_literal_case = "Preserve"
float_literal_trailing_zero = "Preserve"
empty_item_single_line = true
struct_lit_single_line = true
fn_single_line = false
Expand Down
15 changes: 15 additions & 0 deletions src/config/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ pub enum HexLiteralCase {
Lower,
}

/// How to treat trailing zeros in floating-point literals.
#[config_type]
pub enum FloatLiteralTrailingZero {
/// Leave the literal as-is.
Preserve,
/// Add a trailing zero to the literal.
Always,
/// Add a trailing zero by default. If the literal contains an exponent or a suffix, the zero
/// and the preceding period are removed.
IfNoPostfix,
/// Remove the trailing zero. If the literal contains an exponent or a suffix, the preceding
/// period is also removed.
Never,
}

#[config_type]
pub enum ReportTactic {
Always,
Expand Down
169 changes: 162 additions & 7 deletions src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::borrow::Cow;
use std::cmp::min;

use itertools::Itertools;
use lazy_static::lazy_static;
use regex::Regex;
use rustc_ast::token::{Delimiter, Lit, LitKind};
use rustc_ast::{ast, ptr, token, ForLoopKind};
use rustc_span::{BytePos, Span};
Expand All @@ -13,7 +15,9 @@ use crate::comment::{
rewrite_missing_comment, CharClasses, FindUncommented,
};
use crate::config::lists::*;
use crate::config::{Config, ControlBraceStyle, HexLiteralCase, IndentStyle, Version};
use crate::config::{
Config, ControlBraceStyle, FloatLiteralTrailingZero, HexLiteralCase, IndentStyle, Version,
};
use crate::lists::{
definitive_tactic, itemize_list, shape_for_tactic, struct_lit_formatting, struct_lit_shape,
struct_lit_tactic, write_list, ListFormatting, Separator,
Expand Down Expand Up @@ -1242,6 +1246,7 @@ pub(crate) fn rewrite_literal(
match token_lit.kind {
token::LitKind::Str => rewrite_string_lit(context, span, shape),
token::LitKind::Integer => rewrite_int_lit(context, token_lit, span, shape),
token::LitKind::Float => rewrite_float_lit(context, token_lit, span, shape),
_ => wrap_str(
context.snippet(span).to_owned(),
context.config.max_width(),
Expand Down Expand Up @@ -1282,7 +1287,12 @@ fn rewrite_int_lit(
span: Span,
shape: Shape,
) -> Option<String> {
if token_lit.is_semantic_float() {
return rewrite_float_lit(context, token_lit, span, shape);
}

let symbol = token_lit.symbol.as_str();
let suffix = token_lit.suffix.as_ref().map(|s| s.as_str());

if let Some(symbol_stripped) = symbol.strip_prefix("0x") {
let hex_lit = match context.config.hex_literal_case() {
Expand All @@ -1292,11 +1302,7 @@ fn rewrite_int_lit(
};
if let Some(hex_lit) = hex_lit {
return wrap_str(
format!(
"0x{}{}",
hex_lit,
token_lit.suffix.map_or(String::new(), |s| s.to_string())
),
format!("0x{}{}", hex_lit, suffix.unwrap_or("")),
context.config.max_width(),
shape,
);
Expand All @@ -1310,6 +1316,69 @@ fn rewrite_int_lit(
)
}

fn rewrite_float_lit(
context: &RewriteContext<'_>,
token_lit: token::Lit,
span: Span,
shape: Shape,
) -> Option<String> {
if matches!(
context.config.float_literal_trailing_zero(),
FloatLiteralTrailingZero::Preserve
) {
return wrap_str(
context.snippet(span).to_owned(),
context.config.max_width(),
shape,
);
Comment on lines +1329 to +1333
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Version::Two we don't check the line length of string literals because there's no meaningful way to break them by default. Similarity, I don't think there would be a meaningful way to break a float literal over multiple lines so let's avoid using wrap_str. Same goes for the wrap_str call below.

For reference, here's the code in rewrite_string_lit that I'm referring to:

rustfmt/src/expr.rs

Lines 1261 to 1263 in cedb7b5

&& context.config.version() == Version::Two
{
return Some(string_lit.to_owned());

Copy link
Author

@amatveiakin amatveiakin Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please elaborate? I guess the statement “there is no meaningful way to break a float literal over multiple lines” is always true, so why would this depend on config version?

BTW, would you say that rewrite_int_lit should be updated as well? It also calls wrap_str unconditionally:

rustfmt/src/expr.rs

Lines 1306 to 1310 in cedb7b5

wrap_str(
context.snippet(span).to_owned(),
context.config.max_width(),
shape,
)

}

let symbol = token_lit.symbol.as_str();
let suffix = token_lit.suffix.as_ref().map(|s| s.as_str());

let FloatSymbolParts {
integer_part,
fractional_part,
exponent,
} = parse_float_symbol(symbol);

let has_postfix = exponent.is_some() || suffix.is_some();
let fractional_part_nonzero = fractional_part.map_or(false, |s| !is_zero_integer_literal(s));

let (include_period, include_fractional_part) =
match context.config.float_literal_trailing_zero() {
FloatLiteralTrailingZero::Preserve => unreachable!("handled above"),
FloatLiteralTrailingZero::Always => (true, true),
FloatLiteralTrailingZero::IfNoPostfix => (
fractional_part_nonzero || !has_postfix,
fractional_part_nonzero || !has_postfix,
),
FloatLiteralTrailingZero::Never => (
fractional_part_nonzero || !has_postfix,
fractional_part_nonzero,
),
};

let period = if include_period { "." } else { "" };
let fractional_part = if include_fractional_part {
fractional_part.unwrap_or("0")
} else {
""
};
wrap_str(
format!(
"{}{}{}{}{}",
integer_part,
period,
fractional_part,
exponent.unwrap_or(""),
suffix.unwrap_or(""),
),
context.config.max_width(),
shape,
)
}

fn choose_separator_tactic(context: &RewriteContext<'_>, span: Span) -> Option<SeparatorTactic> {
if context.inside_macro() {
if span_ends_with_comma(context, span) {
Expand Down Expand Up @@ -2215,9 +2284,39 @@ pub(crate) fn is_method_call(expr: &ast::Expr) -> bool {
}
}

struct FloatSymbolParts<'a> {
integer_part: &'a str,
fractional_part: Option<&'a str>,
exponent: Option<&'a str>,
}

// Parses a float literal. The `symbol` must be a valid floating point literal without a type
// suffix. Otherwise the function may panic or return wrong result.
fn parse_float_symbol(symbol: &str) -> FloatSymbolParts<'_> {
lazy_static! {
// This regex may accept invalid float literals (such as `1`, `_` or `2.e3`). That's ok.
// We only use it to parse literals whose validity has already been established.
static ref FLOAT_LITERAL: Regex =
Regex::new(r"^([0-9_]+)(?:\.([0-9_]+)?)?([eE][+-]?[0-9_]+)?$").unwrap();
}
let caps = FLOAT_LITERAL.captures(symbol).unwrap();
FloatSymbolParts {
integer_part: caps.get(1).unwrap().as_str(),
fractional_part: caps.get(2).map(|m| m.as_str()),
exponent: caps.get(3).map(|m| m.as_str()),
}
}

fn is_zero_integer_literal(symbol: &str) -> bool {
lazy_static! {
static ref ZERO_LITERAL: Regex = Regex::new(r"^[0_]+$").unwrap();
}
ZERO_LITERAL.is_match(symbol)
}

#[cfg(test)]
mod test {
use super::last_line_offsetted;
use super::*;

#[test]
fn test_last_line_offsetted() {
Expand All @@ -2239,4 +2338,60 @@ mod test {
let lines = "one\n two three";
assert_eq!(last_line_offsetted(2, lines), false);
}

#[test]
fn test_parse_float_symbol() {
let parts = parse_float_symbol("123.456e789");
assert_eq!(parts.integer_part, "123");
assert_eq!(parts.fractional_part, Some("456"));
assert_eq!(parts.exponent, Some("e789"));

let parts = parse_float_symbol("123.456e+789");
assert_eq!(parts.integer_part, "123");
assert_eq!(parts.fractional_part, Some("456"));
assert_eq!(parts.exponent, Some("e+789"));

let parts = parse_float_symbol("123.456e-789");
assert_eq!(parts.integer_part, "123");
assert_eq!(parts.fractional_part, Some("456"));
assert_eq!(parts.exponent, Some("e-789"));

let parts = parse_float_symbol("123e789");
assert_eq!(parts.integer_part, "123");
assert_eq!(parts.fractional_part, None);
assert_eq!(parts.exponent, Some("e789"));

let parts = parse_float_symbol("123E789");
assert_eq!(parts.integer_part, "123");
assert_eq!(parts.fractional_part, None);
assert_eq!(parts.exponent, Some("E789"));

let parts = parse_float_symbol("123.");
assert_eq!(parts.integer_part, "123");
assert_eq!(parts.fractional_part, None);
assert_eq!(parts.exponent, None);
}

#[test]
fn test_parse_float_symbol_with_underscores() {
let parts = parse_float_symbol("_123._456e_789");
assert_eq!(parts.integer_part, "_123");
assert_eq!(parts.fractional_part, Some("_456"));
assert_eq!(parts.exponent, Some("e_789"));

let parts = parse_float_symbol("123_.456_e789_");
assert_eq!(parts.integer_part, "123_");
assert_eq!(parts.fractional_part, Some("456_"));
assert_eq!(parts.exponent, Some("e789_"));

let parts = parse_float_symbol("1_23.4_56e7_89");
assert_eq!(parts.integer_part, "1_23");
assert_eq!(parts.fractional_part, Some("4_56"));
assert_eq!(parts.exponent, Some("e7_89"));

let parts = parse_float_symbol("_1_23_._4_56_e_7_89_");
assert_eq!(parts.integer_part, "_1_23_");
assert_eq!(parts.fractional_part, Some("_4_56_"));
assert_eq!(parts.exponent, Some("e_7_89_"));
}
}
30 changes: 30 additions & 0 deletions tests/source/configs/float_literal_trailing_zero/always.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// rustfmt-float_literal_trailing_zero: Always

fn float_literals() {
let a = 0.;
let b = 0.0;
let c = 100.;
let d = 100.0;
let e = 5e3;
let f = 5.0e3;
let g = 5e+3;
let h = 5.0e+3;
let i = 5e-3;
let j = 5.0e-3;
let k = 5E3;
let l = 5.0E3;
let m = 7f32;
let n = 7.0f32;
let o = 9e3f32;
let p = 9.0e3f32;
let q = 1000.00;
let r = 1_000_.;
let s = 1_000_.000_000;
}

fn line_wrapping() {
let array = [
1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17., 18.,
];
println!("This is floaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat {}", 10e3);
}
33 changes: 33 additions & 0 deletions tests/source/configs/float_literal_trailing_zero/if-no-postfix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// rustfmt-float_literal_trailing_zero: IfNoPostfix

fn float_literals() {
let a = 0.;
let b = 0.0;
let c = 100.;
let d = 100.0;
let e = 5e3;
let f = 5.0e3;
let g = 5e+3;
let h = 5.0e+3;
let i = 5e-3;
let j = 5.0e-3;
let k = 5E3;
let l = 5.0E3;
let m = 7f32;
let n = 7.0f32;
let o = 9e3f32;
let p = 9.0e3f32;
let q = 1000.00;
let r = 1_000_.;
let s = 1_000_.000_000;
}

fn line_wrapping() {
let array = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11., 12., 13., 14., 15., 16., 17., 18.,
];
println!(
"This is floaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat {}",
10.0e3
);
}
Loading
Loading