diff --git a/crates/hir-def/src/macro_expansion_tests/proc_macros.rs b/crates/hir-def/src/macro_expansion_tests/proc_macros.rs
index 0ca30fb799..e4b065d020 100644
--- a/crates/hir-def/src/macro_expansion_tests/proc_macros.rs
+++ b/crates/hir-def/src/macro_expansion_tests/proc_macros.rs
@@ -92,3 +92,40 @@ fn foo() {
 }"##]],
     );
 }
+
+#[test]
+fn float_parsing_panic() {
+    // Regression test for https://github.com/rust-lang/rust-analyzer/issues/12211
+    check(
+        r#"
+//- proc_macros: identity
+macro_rules! id {
+    ($($t:tt)*) => {
+        $($t)*
+    };
+}
+
+id! {
+    #[proc_macros::identity]
+    impl Foo for WrapBj {
+        async fn foo(&self) {
+            self.0. id().await;
+        }
+    }
+}
+"#,
+        expect![[r##"
+macro_rules! id {
+    ($($t:tt)*) => {
+        $($t)*
+    };
+}
+
+#[proc_macros::identity] impl Foo for WrapBj {
+    async fn foo(&self ) {
+        self .0.id().await ;
+    }
+}
+"##]],
+    );
+}
diff --git a/crates/mbe/src/syntax_bridge.rs b/crates/mbe/src/syntax_bridge.rs
index fb6f8d66c6..361633e39d 100644
--- a/crates/mbe/src/syntax_bridge.rs
+++ b/crates/mbe/src/syntax_bridge.rs
@@ -243,6 +243,8 @@ fn convert_tokens<C: TokenConvertor>(conv: &mut C) -> tt::Subtree {
             let char = match token.to_char(conv) {
                 Some(c) => c,
                 None => {
+                    // FIXME: this isn't really correct, `to_char` yields the *first* char of the token,
+                    // and this is relevant when eg. creating 2 `tt::Punct` from a single `::` token
                     panic!("Token from lexer must be single char: token = {:#?}", token);
                 }
             };
diff --git a/crates/parser/src/grammar.rs b/crates/parser/src/grammar.rs
index f68d7196c8..4ebf2157c6 100644
--- a/crates/parser/src/grammar.rs
+++ b/crates/parser/src/grammar.rs
@@ -324,7 +324,9 @@ fn name_ref_or_index(p: &mut Parser) {
     );
     let m = p.start();
     if p.at(FLOAT_NUMBER_PART) || p.at_ts(FLOAT_LITERAL_FIRST) {
-        p.bump_remap(INT_NUMBER);
+        // Ideally we'd remap this to `INT_NUMBER` instead, but that causes the MBE conversion to
+        // lose track of what's a float and what isn't, causing panics.
+        p.bump_remap(FLOAT_NUMBER_PART);
     } else {
         p.bump_any();
     }
diff --git a/crates/parser/test_data/parser/inline/ok/0011_field_expr.rast b/crates/parser/test_data/parser/inline/ok/0011_field_expr.rast
index 19fab593fa..a1efb3a9fb 100644
--- a/crates/parser/test_data/parser/inline/ok/0011_field_expr.rast
+++ b/crates/parser/test_data/parser/inline/ok/0011_field_expr.rast
@@ -50,7 +50,7 @@ SOURCE_FILE
                       IDENT "x"
               DOT "."
               NAME_REF
-                INT_NUMBER "0"
+                FLOAT_NUMBER_PART "0"
             DOT "."
             WHITESPACE " "
             NAME_REF
@@ -67,10 +67,10 @@ SOURCE_FILE
                       IDENT "x"
               DOT "."
               NAME_REF
-                INT_NUMBER "0"
+                FLOAT_NUMBER_PART "0"
             DOT "."
             NAME_REF
-              INT_NUMBER "1"
+              FLOAT_NUMBER_PART "1"
           SEMICOLON ";"
         WHITESPACE "\n    "
         EXPR_STMT