Repository: rust-lang/rust
Pull request: Insert space after float literal ending with `.` in pretty printer

The pretty printer was collapsing unsuffixed float literals (like `0.`) with adjacent dot tokens, producing ambiguous or invalid output:

- `0. ..45.` (range) became `0...45.`
- `0. ..=360.` (inclusive range) became `0...=360.`
- `0. .to_string()` (method call) became `0..to_string()`

The fix inserts a space after an unsuffixed float literal ending with `.` when it appears as the start expression of a range operator or the receiver of a method call or field access, preventing the trailing dot from merging with the following `.`, `..`, or `..=` token. This is skipped when the expression is already parenthesized, since the closing `)` naturally provides separation.

## Example

**before** (`rustc 1.96.0-nightly (3b1b0ef4d 2026-03-11)`):

```rust
#![feature(prelude_import)]
#![no_std]
extern crate std;
#[prelude_import]
use ::std::prelude::rust_2015::*;
//@ pp-exact

fn main() {
    let _ = 0...45.;
    let _ = 0...=360.;
    let _ = 0...;
    let _ = 0..to_string();
}
```

**after** (this branch):

```rust
#![feature(prelude_import)]
#![no_std]
extern crate std;
#[prelude_import]
use ::std::prelude::rust_2015::*;
//@ pp-exact

fn main() {
    let _ = 0. ..45.;
    let _ = 0. ..=360.;
    let _ = 0. ..;
    let _ = 0. .to_string();
}
```

Notice `0...45.` (ambiguous — looks like deprecated `...` inclusive range) becomes `0. ..45.` (clear: float `0.` followed by range `..`). Similarly `0..to_string()` (parsed as range) becomes `0. .to_string()` (method call on float).

<!-- homu-ignore:start -->
<!--
If this PR is related to an unstable feature or an otherwise tracked effort,
please link to the relevant tracking issue here. If you don't know of a related
tracking issue or there are none, feel free to ignore this.

This PR will get automatically assigned to a reviewer. In case you would like
a specific user to review your work, you can assign it to them by using

    r? <reviewer name>
-->
<!-- homu-ignore:end -->


Changed files:
filename: compiler/rustc_ast_pretty/src/pprust/state/expr.rs
patch: @@ -260,12 +260,15 @@ impl<'a> State<'a> {
         //
         //     loop { break x; }.method();
         //
-        self.print_expr_cond_paren(
-            receiver,
-            receiver.precedence() < ExprPrecedence::Unambiguous,
-            fixup.leftmost_subexpression_with_dot(),
-        );
+        let needs_paren = receiver.precedence() < ExprPrecedence::Unambiguous;
+        self.print_expr_cond_paren(receiver, needs_paren, fixup.leftmost_subexpression_with_dot());
 
+        // If the receiver is an unsuffixed float literal like `0.`, insert
+        // a space so the `.` of the method call doesn't merge with the
+        // trailing dot: `0. .method()` instead of `0..method()`.
+        if !needs_paren && expr_ends_with_dot(receiver) {
+            self.word(" ");
+        }
         self.word(".");
         self.print_ident(segment.ident);
         if let Some(args) = &segment.args {
@@ -658,11 +661,15 @@ impl<'a> State<'a> {
                 );
             }
             ast::ExprKind::Field(expr, ident) => {
+                let needs_paren = expr.precedence() < ExprPrecedence::Unambiguous;
                 self.print_expr_cond_paren(
                     expr,
-                    expr.precedence() < ExprPrecedence::Unambiguous,
+                    needs_paren,
                     fixup.leftmost_subexpression_with_dot(),
                 );
+                if !needs_paren && expr_ends_with_dot(expr) {
+                    self.word(" ");
+                }
                 self.word(".");
                 self.print_ident(*ident);
             }
@@ -685,11 +692,15 @@ impl<'a> State<'a> {
                 let fake_prec = ExprPrecedence::LOr;
                 if let Some(e) = start {
                     let start_fixup = fixup.leftmost_subexpression_with_operator(true);
-                    self.print_expr_cond_paren(
-                        e,
-                        start_fixup.precedence(e) < fake_prec,
-                        start_fixup,
-                    );
+                    let needs_paren = start_fixup.precedence(e) < fake_prec;
+                    self.print_expr_cond_paren(e, needs_paren, start_fixup);
+                    // If the start expression is a float literal ending with
+                    // `.`, we need a space before `..` or `..=` so that the
+                    // dots don't merge. E.g. `0. ..45.` must not become
+                    // `0...45.`.
+                    if !needs_paren && expr_ends_with_dot(e) {
+                        self.word(" ");
+                    }
                 }
                 match limits {
                     ast::RangeLimits::HalfOpen => self.word(".."),
@@ -1025,3 +1036,18 @@ fn reconstruct_format_args_template_string(pieces: &[FormatArgsPiece]) -> String
     template.push('"');
     template
 }
+
+/// Returns `true` if the printed representation of this expression ends with
+/// a `.` character — specifically, an unsuffixed float literal like `0.` or
+/// `45.`. This is used to insert whitespace before range operators (`..`,
+/// `..=`) so that the dots don't merge (e.g. `0. ..45.` instead of `0...45.`).
+fn expr_ends_with_dot(expr: &ast::Expr) -> bool {
+    match &expr.kind {
+        ast::ExprKind::Lit(token_lit) => {
+            token_lit.kind == token::Float
+                && token_lit.suffix.is_none()
+                && token_lit.symbol.as_str().ends_with('.')
+        }
+        _ => false,
+    }
+}

filename: tests/pretty/float-trailing-dot.rs
patch: @@ -0,0 +1,8 @@
+//@ pp-exact
+
+fn main() {
+    let _ = 0. ..45.;
+    let _ = 0. ..=360.;
+    let _ = 0. ..;
+    let _ = 0. .to_string();
+}