Repository: rust-lang/rust-analyzer
Pull request #17896 (2024-08-15 12:51:14)

internal: Properly check the edition for edition dependent syntax kinds
This puts the current edition in a bunch of places, most of which I annoted with FIXMEs asside from the ones in ide-assists because I couldnt bother with those

Veykril: Handling the `dyn` keyword correctly for our generated syntax stuff is a pain btw because it can be anything due to editions, contextual/weak keyword in 2015, strict keyword in 2018+

Patch:
diff --git a/crates/hir-expand/src/name.rs b/crates/hir-expand/src/name.rs
index d012d272d7..2a52aaba6a 100644
--- a/crates/hir-expand/src/name.rs
+++ b/crates/hir-expand/src/name.rs
@@ -104,14 +104,17 @@ impl Name {
 
     /// Resolve a name from the text of token.
     fn resolve(raw_text: &str) -> Name {
+        // FIXME: Edition
         match raw_text.strip_prefix("r#") {
             // When `raw_text` starts with "r#" but the name does not coincide with any
             // keyword, we never need the prefix so we strip it.
-            Some(text) if !is_raw_identifier(text) => Name::new_ref(text),
+            Some(text) if !is_raw_identifier(text, span::Edition::CURRENT) => Name::new_ref(text),
             // Keywords (in the current edition) *can* be used as a name in earlier editions of
             // Rust, e.g. "try" in Rust 2015. Even in such cases, we keep track of them in their
             // escaped form.
-            None if is_raw_identifier(raw_text) => Name::new_text(&format!("r#{}", raw_text)),
+            None if is_raw_identifier(raw_text, span::Edition::CURRENT) => {
+                Name::new_text(&format!("r#{}", raw_text))
+            }
             _ => Name::new_text(raw_text),
         }
     }
diff --git a/crates/ide-completion/src/context.rs b/crates/ide-completion/src/context.rs
index 952d9217c7..5b64a98a8b 100644
--- a/crates/ide-completion/src/context.rs
+++ b/crates/ide-completion/src/context.rs
@@ -15,7 +15,7 @@ use ide_db::{
 };
 use syntax::{
     ast::{self, AttrKind, NameOrNameRef},
-    AstNode, SmolStr,
+    AstNode, Edition, SmolStr,
     SyntaxKind::{self, *},
     SyntaxToken, TextRange, TextSize, T,
 };
@@ -468,7 +468,8 @@ impl CompletionContext<'_> {
                 TextRange::at(self.original_token.text_range().start(), TextSize::from(1))
             }
             IDENT | LIFETIME_IDENT | UNDERSCORE | INT_NUMBER => self.original_token.text_range(),
-            _ if kind.is_keyword() => self.original_token.text_range(),
+            // FIXME: Edition
+            _ if kind.is_keyword(Edition::CURRENT) => self.original_token.text_range(),
             _ => TextRange::empty(self.position.offset),
         }
     }
diff --git a/crates/ide-db/src/syntax_helpers/insert_whitespace_into_node.rs b/crates/ide-db/src/syntax_helpers/insert_whitespace_into_node.rs
index 97b6d4a572..6714e411e2 100644
--- a/crates/ide-db/src/syntax_helpers/insert_whitespace_into_node.rs
+++ b/crates/ide-db/src/syntax_helpers/insert_whitespace_into_node.rs
@@ -1,4 +1,5 @@
 //! Utilities for formatting macro expanded nodes until we get a proper formatter.
+use span::Edition;
 use syntax::{
     ast::make,
     ted::{self, Position},
@@ -131,5 +132,6 @@ pub fn insert_ws_into(syn: SyntaxNode) -> SyntaxNode {
 }
 
 fn is_text(k: SyntaxKind) -> bool {
-    k.is_keyword() || k.is_literal() || k == IDENT || k == UNDERSCORE
+    // FIXME: Edition
+    k.is_keyword(Edition::CURRENT) || k.is_literal() || k == IDENT || k == UNDERSCORE
 }
diff --git a/crates/ide-db/src/syntax_helpers/node_ext.rs b/crates/ide-db/src/syntax_helpers/node_ext.rs
index 37238cc61d..d10a55120a 100644
--- a/crates/ide-db/src/syntax_helpers/node_ext.rs
+++ b/crates/ide-db/src/syntax_helpers/node_ext.rs
@@ -1,6 +1,7 @@
 //! Various helper functions to work with SyntaxNodes.
 use itertools::Itertools;
 use parser::T;
+use span::Edition;
 use syntax::{
     ast::{self, HasLoopBody, MacroCall, PathSegmentKind, VisibilityKind},
     AstNode, AstToken, Preorder, RustLanguage, WalkEvent,
@@ -461,7 +462,8 @@ pub fn parse_tt_as_comma_sep_paths(input: ast::TokenTree) -> Option<Vec<ast::Pat
     let tokens =
         input.syntax().children_with_tokens().skip(1).map_while(|it| match it.into_token() {
             // seeing a keyword means the attribute is unclosed so stop parsing here
-            Some(tok) if tok.kind().is_keyword() => None,
+            // FIXME: Edition
+            Some(tok) if tok.kind().is_keyword(Edition::CURRENT) => None,
             // don't include the right token tree parenthesis if it exists
             tok @ Some(_) if tok == r_paren => None,
             // only nodes that we can find are other TokenTrees, those are unexpected in this parse though
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs
index 8a8bc07945..b1d2b34ea4 100644
--- a/crates/ide/src/goto_definition.rs
+++ b/crates/ide/src/goto_definition.rs
@@ -17,7 +17,7 @@ use ide_db::{
 };
 use itertools::Itertools;
 
-use span::FileId;
+use span::{Edition, FileId};
 use syntax::{
     ast::{self, HasLoopBody},
     match_ast, AstNode, AstToken,
@@ -55,7 +55,7 @@ pub(crate) fn goto_definition(
         | COMMENT => 4,
         // index and prefix ops
         T!['['] | T![']'] | T![?] | T![*] | T![-] | T![!] => 3,
-        kind if kind.is_keyword() => 2,
+        kind if kind.is_keyword(Edition::CURRENT) => 2,
         T!['('] | T![')'] => 2,
         kind if kind.is_trivia() => 0,
         _ => 1,
diff --git a/crates/ide/src/highlight_related.rs b/crates/ide/src/highlight_related.rs
index 8fcd38b4e3..946dca85d4 100644
--- a/crates/ide/src/highlight_related.rs
+++ b/crates/ide/src/highlight_related.rs
@@ -11,7 +11,7 @@ use ide_db::{
     },
     FxHashMap, FxHashSet, RootDatabase,
 };
-use span::EditionedFileId;
+use span::{Edition, EditionedFileId};
 use syntax::{
     ast::{self, HasLoopBody},
     match_ast, AstNode,
@@ -65,7 +65,7 @@ pub(crate) fn highlight_related(
     let token = pick_best_token(syntax.token_at_offset(offset), |kind| match kind {
         T![?] => 4, // prefer `?` when the cursor is sandwiched like in `await$0?`
         T![->] => 4,
-        kind if kind.is_keyword() => 3,
+        kind if kind.is_keyword(Edition::CURRENT) => 3,
         IDENT | INT_NUMBER => 2,
         T![|] => 1,
         _ => 0,
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 500674e32b..75f8ac2d2b 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -14,6 +14,7 @@ use ide_db::{
     FileRange, FxIndexSet, RootDatabase,
 };
 use itertools::{multizip, Itertools};
+use span::Edition;
 use syntax::{ast, AstNode, SyntaxKind::*, SyntaxNode, T};
 
 use crate::{
@@ -140,7 +141,7 @@ fn hover_simple(
         | T![_] => 4,
         // index and prefix ops and closure pipe
         T!['['] | T![']'] | T![?] | T![*] | T![-] | T![!] | T![|] => 3,
-        kind if kind.is_keyword() => 2,
+        kind if kind.is_keyword(Edition::CURRENT) => 2,
         T!['('] | T![')'] => 2,
         kind if kind.is_trivia() => 0,
         _ => 1,
diff --git a/crates/ide/src/hover/render.rs b/crates/ide/src/hover/render.rs
index 7091b15b8a..13b7ba1e2b 100644
--- a/crates/ide/src/hover/render.rs
+++ b/crates/ide/src/hover/render.rs
@@ -20,6 +20,7 @@ use rustc_apfloat::{
     ieee::{Half as f16, Quad as f128},
     Float,
 };
+use span::Edition;
 use stdx::format_to;
 use syntax::{algo, ast, match_ast, AstNode, AstToken, Direction, SyntaxToken, T};
 
@@ -251,7 +252,7 @@ pub(super) fn keyword(
     config: &HoverConfig,
     token: &SyntaxToken,
 ) -> Option<HoverResult> {
-    if !token.kind().is_keyword() || !config.documentation || !config.keywords {
+    if !token.kind().is_keyword(Edition::CURRENT) || !config.documentation || !config.keywords {
         return None;
     }
     let parent = token.parent()?;
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs
index 64b82b31c7..aef162bf0a 100644
--- a/crates/ide/src/references.rs
+++ b/crates/ide/src/references.rs
@@ -17,6 +17,7 @@ use ide_db::{
 };
 use itertools::Itertools;
 use nohash_hasher::IntMap;
+use span::Edition;
 use syntax::{
     ast::{self, HasName},
     match_ast, AstNode,
@@ -305,7 +306,8 @@ fn handle_control_flow_keywords(
     FilePosition { file_id, offset }: FilePosition,
 ) -> Option<ReferenceSearchResult> {
     let file = sema.parse_guess_edition(file_id);
-    let token = file.syntax().token_at_offset(offset).find(|t| t.kind().is_keyword())?;
+    let token =
+        file.syntax().token_at_offset(offset).find(|t| t.kind().is_keyword(Edition::CURRENT))?;
 
     let references = match token.kind() {
         T![fn] | T![return] | T![try] => highlight_related::highlight_exit_points(sema, token),
diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs
index 9581474ca7..ed0e3d89dd 100644
--- a/crates/ide/src/rename.rs
+++ b/crates/ide/src/rename.rs
@@ -6,6 +6,7 @@
 
 use hir::{AsAssocItem, HirFileIdExt, InFile, Semantics};
 use ide_db::{
+    base_db::SourceDatabase,
     defs::{Definition, NameClass, NameRefClass},
     rename::{bail, format_err, source_edit_from_references, IdentifierKind},
     source_change::SourceChangeBuilder,
@@ -162,11 +163,12 @@ pub(crate) fn will_rename_file(
     let sema = Semantics::new(db);
     let module = sema.file_to_module_def(file_id)?;
     let def = Definition::Module(module);
-    let mut change = if is_raw_identifier(new_name_stem) {
-        def.rename(&sema, &SmolStr::from_iter(["r#", new_name_stem])).ok()?
-    } else {
-        def.rename(&sema, new_name_stem).ok()?
-    };
+    let mut change =
+        if is_raw_identifier(new_name_stem, db.crate_graph()[module.krate().into()].edition) {
+            def.rename(&sema, &SmolStr::from_iter(["r#", new_name_stem])).ok()?
+        } else {
+            def.rename(&sema, new_name_stem).ok()?
+        };
     change.file_system_edits.clear();
     Some(change)
 }
diff --git a/crates/ide/src/syntax_highlighting/highlight.rs b/crates/ide/src/syntax_highlighting/highlight.rs
index 291073f877..70fdc0a1b3 100644
--- a/crates/ide/src/syntax_highlighting/highlight.rs
+++ b/crates/ide/src/syntax_highlighting/highlight.rs
@@ -6,6 +6,7 @@ use ide_db::{
     defs::{Definition, IdentClass, NameClass, NameRefClass},
     FxHashMap, RootDatabase, SymbolKind,
 };
+use span::Edition;
 use stdx::hash_once;
 use syntax::{
     ast, match_ast, AstNode, AstToken, NodeOrToken,
@@ -41,7 +42,7 @@ pub(super) fn token(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) -> O
             HlTag::None.into()
         }
         p if p.is_punct() => punctuation(sema, token, p),
-        k if k.is_keyword() => keyword(sema, token, k)?,
+        k if k.is_keyword(Edition::CURRENT) => keyword(sema, token, k)?,
         _ => return None,
     };
     Some(highlight)
diff --git a/crates/ide/src/syntax_highlighting/macro_.rs b/crates/ide/src/syntax_highlighting/macro_.rs
index 1099d9c23b..460fc4fe14 100644
--- a/crates/ide/src/syntax_highlighting/macro_.rs
+++ b/crates/ide/src/syntax_highlighting/macro_.rs
@@ -1,4 +1,5 @@
 //! Syntax highlighting for macro_rules!.
+use span::Edition;
 use syntax::{SyntaxKind, SyntaxToken, TextRange, T};
 
 use crate::{HlRange, HlTag};
@@ -117,7 +118,7 @@ fn update_macro_state(state: &mut MacroMatcherParseState, tok: &SyntaxToken) {
 
 fn is_metavariable(token: &SyntaxToken) -> Option<TextRange> {
     match token.kind() {
-        kind if kind == SyntaxKind::IDENT || kind.is_keyword() => {
+        kind if kind == SyntaxKind::IDENT || kind.is_keyword(Edition::CURRENT) => {
             if let Some(_dollar) = token.prev_token().filter(|t| t.kind() == T![$]) {
                 return Some(token.text_range());
             }
diff --git a/crates/parser/src/lexed_str.rs b/crates/parser/src/lexed_str.rs
index 13fc61074d..ff924830ae 100644
--- a/crates/parser/src/lexed_str.rs
+++ b/crates/parser/src/lexed_str.rs
@@ -178,19 +178,8 @@ impl<'a> Converter<'a> {
                 rustc_lexer::TokenKind::Whitespace => WHITESPACE,
 
                 rustc_lexer::TokenKind::Ident if token_text == "_" => UNDERSCORE,
-                rustc_lexer::TokenKind::Ident
-                    if ["async", "await", "dyn", "try"].contains(&token_text)
-                        && !self.edition.at_least_2018() =>
-                {
-                    IDENT
-                }
-                rustc_lexer::TokenKind::Ident
-                    if token_text == "gen" && !self.edition.at_least_2024() =>
-                {
-                    IDENT
-                }
                 rustc_lexer::TokenKind::Ident => {
-                    SyntaxKind::from_keyword(token_text).unwrap_or(IDENT)
+                    SyntaxKind::from_keyword(token_text, self.edition).unwrap_or(IDENT)
                 }
                 rustc_lexer::TokenKind::InvalidPrefix | rustc_lexer::TokenKind::InvalidIdent => {
                     err = "Ident contains invalid characters";
diff --git a/crates/parser/src/shortcuts.rs b/crates/parser/src/shortcuts.rs
index 1cf81e79b0..7adedba7c4 100644
--- a/crates/parser/src/shortcuts.rs
+++ b/crates/parser/src/shortcuts.rs
@@ -35,12 +35,10 @@ impl LexedStr<'_> {
                 was_joint = false
             } else if kind == SyntaxKind::IDENT {
                 let token_text = self.text(i);
-                let contextual_kw = if !edition.at_least_2018() && token_text == "dyn" {
-                    SyntaxKind::DYN_KW
-                } else {
-                    SyntaxKind::from_contextual_keyword(token_text).unwrap_or(SyntaxKind::IDENT)
-                };
-                res.push_ident(contextual_kw);
+                res.push_ident(
+                    SyntaxKind::from_contextual_keyword(token_text, edition)
+                        .unwrap_or(SyntaxKind::IDENT),
+                )
             } else {
                 if was_joint {
                     res.was_joint();
diff --git a/crates/parser/src/syntax_kind/generated.rs b/crates/parser/src/syntax_kind/generated.rs
index 7bddf88740..eaacd88e3e 100644
--- a/crates/parser/src/syntax_kind/generated.rs
+++ b/crates/parser/src/syntax_kind/generated.rs
@@ -1,6 +1,7 @@
 //! Generated by `cargo codegen grammar`, do not edit by hand.
 
 #![allow(bad_style, missing_docs, unreachable_pub)]
+use crate::Edition;
 #[doc = r" The kind of syntax node, e.g. `IDENT`, `USE_KW`, or `STRUCT`."]
 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
 #[repr(u16)]
@@ -64,8 +65,6 @@ pub enum SyntaxKind {
     SELF_TYPE_KW,
     ABSTRACT_KW,
     AS_KW,
-    ASYNC_KW,
-    AWAIT_KW,
     BECOME_KW,
     BOX_KW,
     BREAK_KW,
@@ -73,7 +72,6 @@ pub enum SyntaxKind {
     CONTINUE_KW,
     CRATE_KW,
     DO_KW,
-    DYN_KW,
     ELSE_KW,
     ENUM_KW,
     EXTERN_KW,
@@ -81,7 +79,6 @@ pub enum SyntaxKind {
     FINAL_KW,
     FN_KW,
     FOR_KW,
-    GEN_KW,
     IF_KW,
     IMPL_KW,
     IN_KW,
@@ -103,7 +100,6 @@ pub enum SyntaxKind {
     SUPER_KW,
     TRAIT_KW,
     TRUE_KW,
-    TRY_KW,
     TYPE_KW,
     TYPEOF_KW,
     UNSAFE_KW,
@@ -114,13 +110,18 @@ pub enum SyntaxKind {
     WHILE_KW,
     YIELD_KW,
     ASM_KW,
+    ASYNC_KW,
     AUTO_KW,
+    AWAIT_KW,
     BUILTIN_KW,
     DEFAULT_KW,
+    DYN_KW,
     FORMAT_ARGS_KW,
+    GEN_KW,
     MACRO_RULES_KW,
     OFFSET_OF_KW,
     RAW_KW,
+    TRY_KW,
     UNION_KW,
     YEET_KW,
     BYTE,
@@ -296,14 +297,14 @@ pub enum SyntaxKind {
 }
 use self::SyntaxKind::*;
 impl SyntaxKind {
-    pub fn is_keyword(self) -> bool {
+    #[doc = r" Checks whether this syntax kind is a strict keyword for the given edition."]
+    #[doc = r" Strict keywords are identifiers that are always considered keywords."]
+    pub fn is_strict_keyword(self, edition: Edition) -> bool {
         matches!(
             self,
             SELF_TYPE_KW
                 | ABSTRACT_KW
                 | AS_KW
-                | ASYNC_KW
-                | AWAIT_KW
                 | BECOME_KW
                 | BOX_KW
                 | BREAK_KW
@@ -311,7 +312,6 @@ impl SyntaxKind {
                 | CONTINUE_KW
                 | CRATE_KW
                 | DO_KW
-                | DYN_KW
                 | ELSE_KW
                 | ENUM_KW
                 | EXTERN_KW
@@ -319,7 +319,6 @@ impl SyntaxKind {
                 | FINAL_KW
                 | FN_KW
                 | FOR_KW
-                | GEN_KW
                 | IF_KW
                 | IMPL_KW
                 | IN_KW
@@ -341,7 +340,6 @@ impl SyntaxKind {
                 | SUPER_KW
                 | TRAIT_KW
                 | TRUE_KW
-                | TRY_KW
                 | TYPE_KW
                 | TYPEOF_KW
                 | UNSAFE_KW
@@ -351,17 +349,103 @@ impl SyntaxKind {
                 | WHERE_KW
                 | WHILE_KW
                 | YIELD_KW
-                | ASM_KW
-                | AUTO_KW
-                | BUILTIN_KW
-                | DEFAULT_KW
-                | FORMAT_ARGS_KW
-                | MACRO_RULES_KW
-                | OFFSET_OF_KW
-                | RAW_KW
-                | UNION_KW
-                | YEET_KW
-        )
+        ) || match self {
+            ASYNC_KW if Edition::Edition2018 <= edition => true,
+            AWAIT_KW if Edition::Edition2018 <= edition => true,
+            DYN_KW if Edition::Edition2018 <= edition => true,
+            GEN_KW if Edition::Edition2024 <= edition => true,
+            TRY_KW if Edition::Edition2018 <= edition => true,
+            _ => false,
+        }
+    }
+    #[doc = r" Checks whether this syntax kind is a weak keyword for the given edition."]
+    #[doc = r" Weak keywords are identifiers that are considered keywords only in certain contexts."]
+    pub fn is_contextual_keyword(self, edition: Edition) -> bool {
+        match self {
+            ASM_KW => true,
+            AUTO_KW => true,
+            BUILTIN_KW => true,
+            DEFAULT_KW => true,
+            DYN_KW if edition < Edition::Edition2018 => true,
+            FORMAT_ARGS_KW => true,
+            MACRO_RULES_KW => true,
+            OFFSET_OF_KW => true,
+            RAW_KW => true,
+            UNION_KW => true,
+            YEET_KW => true,
+            _ => false,
+        }
+    }
+    #[doc = r" Checks whether this syntax kind is a strict or weak keyword for the given edition."]
+    pub fn is_keyword(self, edition: Edition) -> bool {
+        matches!(
+            self,
+            SELF_TYPE_KW
+                | ABSTRACT_KW
+                | AS_KW
+                | BECOME_KW
+                | BOX_KW
+                | BREAK_KW
+                | CONST_KW
+                | CONTINUE_KW
+                | CRATE_KW
+                | DO_KW
+                | ELSE_KW
+                | ENUM_KW
+                | EXTERN_KW
+                | FALSE_KW
+                | FINAL_KW
+                | FN_KW
+                | FOR_KW
+                | IF_KW
+                | IMPL_KW
+                | IN_KW
+                | LET_KW
+                | LOOP_KW
+                | MACRO_KW
+                | MATCH_KW
+                | MOD_KW
+                | MOVE_KW
+                | MUT_KW
+                | OVERRIDE_KW
+                | PRIV_KW
+                | PUB_KW
+                | REF_KW
+                | RETURN_KW
+                | SELF_KW
+                | STATIC_KW
+                | STRUCT_KW
+                | SUPER_KW
+                | TRAIT_KW
+                | TRUE_KW
+                | TYPE_KW
+                | TYPEOF_KW
+                | UNSAFE_KW
+                | UNSIZED_KW
+                | USE_KW
+                | VIRTUAL_KW
+                | WHERE_KW
+                | WHILE_KW
+                | YIELD_KW
+        ) || match self {
+            ASYNC_KW if Edition::Edition2018 <= edition => true,
+            AWAIT_KW if Edition::Edition2018 <= edition => true,
+            DYN_KW if Edition::Edition2018 <= edition => true,
+            GEN_KW if Edition::Edition2024 <= edition => true,
+            TRY_KW if Edition::Edition2018 <= edition => true,
+            ASM_KW => true,
+            AUTO_KW => true,
+            BUILTIN_KW => true,
+            DEFAULT_KW => true,
+            DYN_KW if edition < Edition::Edition2018 => true,
+            FORMAT_ARGS_KW => true,
+            MACRO_RULES_KW => true,
+            OFFSET_OF_KW => true,
+            RAW_KW => true,
+            UNION_KW => true,
+            YEET_KW => true,
+            _ => false,
+        }
     }
     pub fn is_punct(self) -> bool {
         matches!(
@@ -434,13 +518,11 @@ impl SyntaxKind {
                 | STRING
         )
     }
-    pub fn from_keyword(ident: &str) -> Option<SyntaxKind> {
+    pub fn from_keyword(ident: &str, edition: Edition) -> Option<SyntaxKind> {
         let kw = match ident {
             "Self" => SELF_TYPE_KW,
             "abstract" => ABSTRACT_KW,
             "as" => AS_KW,
-            "async" => ASYNC_KW,
-            "await" => AWAIT_KW,
             "become" => BECOME_KW,
             "box" => BOX_KW,
             "break" => BREAK_KW,
@@ -448,7 +530,6 @@ impl SyntaxKind {
             "continue" => CONTINUE_KW,
             "crate" => CRATE_KW,
             "do" => DO_KW,
-            "dyn" => DYN_KW,
             "else" => ELSE_KW,
             "enum" => ENUM_KW,
             "extern" => EXTERN_KW,
@@ -456,7 +537,6 @@ impl SyntaxKind {
             "final" => FINAL_KW,
             "fn" => FN_KW,
             "for" => FOR_KW,
-            "gen" => GEN_KW,
             "if" => IF_KW,
             "impl" => IMPL_KW,
             "in" => IN_KW,
@@ -478,7 +558,6 @@ impl SyntaxKind {
             "super" => SUPER_KW,
             "trait" => TRAIT_KW,
             "true" => TRUE_KW,
-            "try" => TRY_KW,
             "type" => TYPE_KW,
             "typeof" => TYPEOF_KW,
             "unsafe" => UNSAFE_KW,
@@ -488,16 +567,22 @@ impl SyntaxKind {
             "where" => WHERE_KW,
             "while" => WHILE_KW,
             "yield" => YIELD_KW,
+            "async" if Edition::Edition2018 <= edition => ASYNC_KW,
+            "await" if Edition::Edition2018 <= edition => AWAIT_KW,
+            "dyn" if Edition::Edition2018 <= edition => DYN_KW,
+            "gen" if Edition::Edition2024 <= edition => GEN_KW,
+            "try" if Edition::Edition2018 <= edition => TRY_KW,
             _ => return None,
         };
         Some(kw)
     }
-    pub fn from_contextual_keyword(ident: &str) -> Option<SyntaxKind> {
+    pub fn from_contextual_keyword(ident: &str, edition: Edition) -> Option<SyntaxKind> {
         let kw = match ident {
             "asm" => ASM_KW,
             "auto" => AUTO_KW,
             "builtin" => BUILTIN_KW,
             "default" => DEFAULT_KW,
+            "dyn" if edition < Edition::Edition2018 => DYN_KW,
             "format_args" => FORMAT_ARGS_KW,
             "macro_rules" => MACRO_RULES_KW,
             "offset_of" => OFFSET_OF_KW,
@@ -544,4 +629,4 @@ impl SyntaxKind {
     }
 }
 #[macro_export]
-macro_rules ! T { [$] => { $ crate :: SyntaxKind :: DOLLAR } ; [;] => { $ crate :: SyntaxKind :: SEMICOLON } ; [,] => { $ crate :: SyntaxKind :: COMMA } ; ['('] => { $ crate :: SyntaxKind :: L_PAREN } ; [')'] => { $ crate :: SyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: SyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: SyntaxKind :: R_CURLY } ; ['['] => { $ crate :: SyntaxKind :: L_BRACK } ; [']'] => { $ crate :: SyntaxKind :: R_BRACK } ; [<] => { $ crate :: SyntaxKind :: L_ANGLE } ; [>] => { $ crate :: SyntaxKind :: R_ANGLE } ; [@] => { $ crate :: SyntaxKind :: AT } ; [#] => { $ crate :: SyntaxKind :: POUND } ; [~] => { $ crate :: SyntaxKind :: TILDE } ; [?] => { $ crate :: SyntaxKind :: QUESTION } ; [&] => { $ crate :: SyntaxKind :: AMP } ; [|] => { $ crate :: SyntaxKind :: PIPE } ; [+] => { $ crate :: SyntaxKind :: PLUS } ; [*] => { $ crate :: SyntaxKind :: STAR } ; [/] => { $ crate :: SyntaxKind :: SLASH } ; [^] => { $ crate :: SyntaxKind :: CARET } ; [%] => { $ crate :: SyntaxKind :: PERCENT } ; [_] => { $ crate :: SyntaxKind :: UNDERSCORE } ; [.] => { $ crate :: SyntaxKind :: DOT } ; [..] => { $ crate :: SyntaxKind :: DOT2 } ; [...] => { $ crate :: SyntaxKind :: DOT3 } ; [..=] => { $ crate :: SyntaxKind :: DOT2EQ } ; [:] => { $ crate :: SyntaxKind :: COLON } ; [::] => { $ crate :: SyntaxKind :: COLON2 } ; [=] => { $ crate :: SyntaxKind :: EQ } ; [==] => { $ crate :: SyntaxKind :: EQ2 } ; [=>] => { $ crate :: SyntaxKind :: FAT_ARROW } ; [!] => { $ crate :: SyntaxKind :: BANG } ; [!=] => { $ crate :: SyntaxKind :: NEQ } ; [-] => { $ crate :: SyntaxKind :: MINUS } ; [->] => { $ crate :: SyntaxKind :: THIN_ARROW } ; [<=] => { $ crate :: SyntaxKind :: LTEQ } ; [>=] => { $ crate :: SyntaxKind :: GTEQ } ; [+=] => { $ crate :: SyntaxKind :: PLUSEQ } ; [-=] => { $ crate :: SyntaxKind :: MINUSEQ } ; [|=] => { $ crate :: SyntaxKind :: PIPEEQ } ; [&=] => { $ crate :: SyntaxKind :: AMPEQ } ; [^=] => { $ crate :: SyntaxKind :: CARETEQ } ; [/=] => { $ crate :: SyntaxKind :: SLASHEQ } ; [*=] => { $ crate :: SyntaxKind :: STAREQ } ; [%=] => { $ crate :: SyntaxKind :: PERCENTEQ } ; [&&] => { $ crate :: SyntaxKind :: AMP2 } ; [||] => { $ crate :: SyntaxKind :: PIPE2 } ; [<<] => { $ crate :: SyntaxKind :: SHL } ; [>>] => { $ crate :: SyntaxKind :: SHR } ; [<<=] => { $ crate :: SyntaxKind :: SHLEQ } ; [>>=] => { $ crate :: SyntaxKind :: SHREQ } ; [Self] => { $ crate :: SyntaxKind :: SELF_TYPE_KW } ; [abstract] => { $ crate :: SyntaxKind :: ABSTRACT_KW } ; [as] => { $ crate :: SyntaxKind :: AS_KW } ; [async] => { $ crate :: SyntaxKind :: ASYNC_KW } ; [await] => { $ crate :: SyntaxKind :: AWAIT_KW } ; [become] => { $ crate :: SyntaxKind :: BECOME_KW } ; [box] => { $ crate :: SyntaxKind :: BOX_KW } ; [break] => { $ crate :: SyntaxKind :: BREAK_KW } ; [const] => { $ crate :: SyntaxKind :: CONST_KW } ; [continue] => { $ crate :: SyntaxKind :: CONTINUE_KW } ; [crate] => { $ crate :: SyntaxKind :: CRATE_KW } ; [do] => { $ crate :: SyntaxKind :: DO_KW } ; [dyn] => { $ crate :: SyntaxKind :: DYN_KW } ; [else] => { $ crate :: SyntaxKind :: ELSE_KW } ; [enum] => { $ crate :: SyntaxKind :: ENUM_KW } ; [extern] => { $ crate :: SyntaxKind :: EXTERN_KW } ; [false] => { $ crate :: SyntaxKind :: FALSE_KW } ; [final] => { $ crate :: SyntaxKind :: FINAL_KW } ; [fn] => { $ crate :: SyntaxKind :: FN_KW } ; [for] => { $ crate :: SyntaxKind :: FOR_KW } ; [gen] => { $ crate :: SyntaxKind :: GEN_KW } ; [if] => { $ crate :: SyntaxKind :: IF_KW } ; [impl] => { $ crate :: SyntaxKind :: IMPL_KW } ; [in] => { $ crate :: SyntaxKind :: IN_KW } ; [let] => { $ crate :: SyntaxKind :: LET_KW } ; [loop] => { $ crate :: SyntaxKind :: LOOP_KW } ; [macro] => { $ crate :: SyntaxKind :: MACRO_KW } ; [match] => { $ crate :: SyntaxKind :: MATCH_KW } ; [mod] => { $ crate :: SyntaxKind :: MOD_KW } ; [move] => { $ crate :: SyntaxKind :: MOVE_KW } ; [mut] => { $ crate :: SyntaxKind :: MUT_KW } ; [override] => { $ crate :: SyntaxKind :: OVERRIDE_KW } ; [priv] => { $ crate :: SyntaxKind :: PRIV_KW } ; [pub] => { $ crate :: SyntaxKind :: PUB_KW } ; [ref] => { $ crate :: SyntaxKind :: REF_KW } ; [return] => { $ crate :: SyntaxKind :: RETURN_KW } ; [self] => { $ crate :: SyntaxKind :: SELF_KW } ; [static] => { $ crate :: SyntaxKind :: STATIC_KW } ; [struct] => { $ crate :: SyntaxKind :: STRUCT_KW } ; [super] => { $ crate :: SyntaxKind :: SUPER_KW } ; [trait] => { $ crate :: SyntaxKind :: TRAIT_KW } ; [true] => { $ crate :: SyntaxKind :: TRUE_KW } ; [try] => { $ crate :: SyntaxKind :: TRY_KW } ; [type] => { $ crate :: SyntaxKind :: TYPE_KW } ; [typeof] => { $ crate :: SyntaxKind :: TYPEOF_KW } ; [unsafe] => { $ crate :: SyntaxKind :: UNSAFE_KW } ; [unsized] => { $ crate :: SyntaxKind :: UNSIZED_KW } ; [use] => { $ crate :: SyntaxKind :: USE_KW } ; [virtual] => { $ crate :: SyntaxKind :: VIRTUAL_KW } ; [where] => { $ crate :: SyntaxKind :: WHERE_KW } ; [while] => { $ crate :: SyntaxKind :: WHILE_KW } ; [yield] => { $ crate :: SyntaxKind :: YIELD_KW } ; [asm] => { $ crate :: SyntaxKind :: ASM_KW } ; [auto] => { $ crate :: SyntaxKind :: AUTO_KW } ; [builtin] => { $ crate :: SyntaxKind :: BUILTIN_KW } ; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW } ; [format_args] => { $ crate :: SyntaxKind :: FORMAT_ARGS_KW } ; [macro_rules] => { $ crate :: SyntaxKind :: MACRO_RULES_KW } ; [offset_of] => { $ crate :: SyntaxKind :: OFFSET_OF_KW } ; [raw] => { $ crate :: SyntaxKind :: RAW_KW } ; [union] => { $ crate :: SyntaxKind :: UNION_KW } ; [yeet] => { $ crate :: SyntaxKind :: YEET_KW } ; [lifetime_ident] => { $ crate :: SyntaxKind :: LIFETIME_IDENT } ; [int_number] => { $ crate :: SyntaxKind :: INT_NUMBER } ; [ident] => { $ crate :: SyntaxKind :: IDENT } ; [string] => { $ crate :: SyntaxKind :: STRING } ; [shebang] => { $ crate :: SyntaxKind :: SHEBANG } ; }
+macro_rules ! T { [$] => { $ crate :: SyntaxKind :: DOLLAR } ; [;] => { $ crate :: SyntaxKind :: SEMICOLON } ; [,] => { $ crate :: SyntaxKind :: COMMA } ; ['('] => { $ crate :: SyntaxKind :: L_PAREN } ; [')'] => { $ crate :: SyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: SyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: SyntaxKind :: R_CURLY } ; ['['] => { $ crate :: SyntaxKind :: L_BRACK } ; [']'] => { $ crate :: SyntaxKind :: R_BRACK } ; [<] => { $ crate :: SyntaxKind :: L_ANGLE } ; [>] => { $ crate :: SyntaxKind :: R_ANGLE } ; [@] => { $ crate :: SyntaxKind :: AT } ; [#] => { $ crate :: SyntaxKind :: POUND } ; [~] => { $ crate :: SyntaxKind :: TILDE } ; [?] => { $ crate :: SyntaxKind :: QUESTION } ; [&] => { $ crate :: SyntaxKind :: AMP } ; [|] => { $ crate :: SyntaxKind :: PIPE } ; [+] => { $ crate :: SyntaxKind :: PLUS } ; [*] => { $ crate :: SyntaxKind :: STAR } ; [/] => { $ crate :: SyntaxKind :: SLASH } ; [^] => { $ crate :: SyntaxKind :: CARET } ; [%] => { $ crate :: SyntaxKind :: PERCENT } ; [_] => { $ crate :: SyntaxKind :: UNDERSCORE } ; [.] => { $ crate :: SyntaxKind :: DOT } ; [..] => { $ crate :: SyntaxKind :: DOT2 } ; [...] => { $ crate :: SyntaxKind :: DOT3 } ; [..=] => { $ crate :: SyntaxKind :: DOT2EQ } ; [:] => { $ crate :: SyntaxKind :: COLON } ; [::] => { $ crate :: SyntaxKind :: COLON2 } ; [=] => { $ crate :: SyntaxKind :: EQ } ; [==] => { $ crate :: SyntaxKind :: EQ2 } ; [=>] => { $ crate :: SyntaxKind :: FAT_ARROW } ; [!] => { $ crate :: SyntaxKind :: BANG } ; [!=] => { $ crate :: SyntaxKind :: NEQ } ; [-] => { $ crate :: SyntaxKind :: MINUS } ; [->] => { $ crate :: SyntaxKind :: THIN_ARROW } ; [<=] => { $ crate :: SyntaxKind :: LTEQ } ; [>=] => { $ crate :: SyntaxKind :: GTEQ } ; [+=] => { $ crate :: SyntaxKind :: PLUSEQ } ; [-=] => { $ crate :: SyntaxKind :: MINUSEQ } ; [|=] => { $ crate :: SyntaxKind :: PIPEEQ } ; [&=] => { $ crate :: SyntaxKind :: AMPEQ } ; [^=] => { $ crate :: SyntaxKind :: CARETEQ } ; [/=] => { $ crate :: SyntaxKind :: SLASHEQ } ; [*=] => { $ crate :: SyntaxKind :: STAREQ } ; [%=] => { $ crate :: SyntaxKind :: PERCENTEQ } ; [&&] => { $ crate :: SyntaxKind :: AMP2 } ; [||] => { $ crate :: SyntaxKind :: PIPE2 } ; [<<] => { $ crate :: SyntaxKind :: SHL } ; [>>] => { $ crate :: SyntaxKind :: SHR } ; [<<=] => { $ crate :: SyntaxKind :: SHLEQ } ; [>>=] => { $ crate :: SyntaxKind :: SHREQ } ; [Self] => { $ crate :: SyntaxKind :: SELF_TYPE_KW } ; [abstract] => { $ crate :: SyntaxKind :: ABSTRACT_KW } ; [as] => { $ crate :: SyntaxKind :: AS_KW } ; [become] => { $ crate :: SyntaxKind :: BECOME_KW } ; [box] => { $ crate :: SyntaxKind :: BOX_KW } ; [break] => { $ crate :: SyntaxKind :: BREAK_KW } ; [const] => { $ crate :: SyntaxKind :: CONST_KW } ; [continue] => { $ crate :: SyntaxKind :: CONTINUE_KW } ; [crate] => { $ crate :: SyntaxKind :: CRATE_KW } ; [do] => { $ crate :: SyntaxKind :: DO_KW } ; [else] => { $ crate :: SyntaxKind :: ELSE_KW } ; [enum] => { $ crate :: SyntaxKind :: ENUM_KW } ; [extern] => { $ crate :: SyntaxKind :: EXTERN_KW } ; [false] => { $ crate :: SyntaxKind :: FALSE_KW } ; [final] => { $ crate :: SyntaxKind :: FINAL_KW } ; [fn] => { $ crate :: SyntaxKind :: FN_KW } ; [for] => { $ crate :: SyntaxKind :: FOR_KW } ; [if] => { $ crate :: SyntaxKind :: IF_KW } ; [impl] => { $ crate :: SyntaxKind :: IMPL_KW } ; [in] => { $ crate :: SyntaxKind :: IN_KW } ; [let] => { $ crate :: SyntaxKind :: LET_KW } ; [loop] => { $ crate :: SyntaxKind :: LOOP_KW } ; [macro] => { $ crate :: SyntaxKind :: MACRO_KW } ; [match] => { $ crate :: SyntaxKind :: MATCH_KW } ; [mod] => { $ crate :: SyntaxKind :: MOD_KW } ; [move] => { $ crate :: SyntaxKind :: MOVE_KW } ; [mut] => { $ crate :: SyntaxKind :: MUT_KW } ; [override] => { $ crate :: SyntaxKind :: OVERRIDE_KW } ; [priv] => { $ crate :: SyntaxKind :: PRIV_KW } ; [pub] => { $ crate :: SyntaxKind :: PUB_KW } ; [ref] => { $ crate :: SyntaxKind :: REF_KW } ; [return] => { $ crate :: SyntaxKind :: RETURN_KW } ; [self] => { $ crate :: SyntaxKind :: SELF_KW } ; [static] => { $ crate :: SyntaxKind :: STATIC_KW } ; [struct] => { $ crate :: SyntaxKind :: STRUCT_KW } ; [super] => { $ crate :: SyntaxKind :: SUPER_KW } ; [trait] => { $ crate :: SyntaxKind :: TRAIT_KW } ; [true] => { $ crate :: SyntaxKind :: TRUE_KW } ; [type] => { $ crate :: SyntaxKind :: TYPE_KW } ; [typeof] => { $ crate :: SyntaxKind :: TYPEOF_KW } ; [unsafe] => { $ crate :: SyntaxKind :: UNSAFE_KW } ; [unsized] => { $ crate :: SyntaxKind :: UNSIZED_KW } ; [use] => { $ crate :: SyntaxKind :: USE_KW } ; [virtual] => { $ crate :: SyntaxKind :: VIRTUAL_KW } ; [where] => { $ crate :: SyntaxKind :: WHERE_KW } ; [while] => { $ crate :: SyntaxKind :: WHILE_KW } ; [yield] => { $ crate :: SyntaxKind :: YIELD_KW } ; [asm] => { $ crate :: SyntaxKind :: ASM_KW } ; [auto] => { $ crate :: SyntaxKind :: AUTO_KW } ; [builtin] => { $ crate :: SyntaxKind :: BUILTIN_KW } ; [default] => { $ crate :: SyntaxKind :: DEFAULT_KW } ; [dyn] => { $ crate :: SyntaxKind :: DYN_KW } ; [format_args] => { $ crate :: SyntaxKind :: FORMAT_ARGS_KW } ; [macro_rules] => { $ crate :: SyntaxKind :: MACRO_RULES_KW } ; [offset_of] => { $ crate :: SyntaxKind :: OFFSET_OF_KW } ; [raw] => { $ crate :: SyntaxKind :: RAW_KW } ; [union] => { $ crate :: SyntaxKind :: UNION_KW } ; [yeet] => { $ crate :: SyntaxKind :: YEET_KW } ; [async] => { $ crate :: SyntaxKind :: ASYNC_KW } ; [await] => { $ crate :: SyntaxKind :: AWAIT_KW } ; [dyn] => { $ crate :: SyntaxKind :: DYN_KW } ; [gen] => { $ crate :: SyntaxKind :: GEN_KW } ; [try] => { $ crate :: SyntaxKind :: TRY_KW } ; [lifetime_ident] => { $ crate :: SyntaxKind :: LIFETIME_IDENT } ; [int_number] => { $ crate :: SyntaxKind :: INT_NUMBER } ; [ident] => { $ crate :: SyntaxKind :: IDENT } ; [string] => { $ crate :: SyntaxKind :: STRING } ; [shebang] => { $ crate :: SyntaxKind :: SHEBANG } ; }
diff --git a/crates/syntax-bridge/src/lib.rs b/crates/syntax-bridge/src/lib.rs
index b0afd245c5..5153db2372 100644
--- a/crates/syntax-bridge/src/lib.rs
+++ b/crates/syntax-bridge/src/lib.rs
@@ -307,7 +307,8 @@ where
                             tt::Ident::new(&text, conv.span_for(abs_range)).into()
                         }
                         UNDERSCORE => make_ident!(),
-                        k if k.is_keyword() => make_ident!(),
+                        // FIXME: Edition
+                        k if k.is_keyword(Edition::CURRENT) => make_ident!(),
                         k if k.is_literal() => {
                             let text = token.to_text(conv);
                             let span = conv.span_for(abs_range);
diff --git a/crates/syntax-bridge/src/to_parser_input.rs b/crates/syntax-bridge/src/to_parser_input.rs
index 2c54899268..14216e3093 100644
--- a/crates/syntax-bridge/src/to_parser_input.rs
+++ b/crates/syntax-bridge/src/to_parser_input.rs
@@ -64,14 +64,12 @@ pub fn to_parser_input<S: Copy + fmt::Debug>(
                         "_" => res.push(T![_]),
                         i if i.starts_with('\'') => res.push(LIFETIME_IDENT),
                         _ if ident.is_raw.yes() => res.push(IDENT),
-                        "gen" if !edition.at_least_2024() => res.push(IDENT),
-                        "dyn" if !edition.at_least_2018() => res.push_ident(DYN_KW),
-                        "async" | "await" | "try" if !edition.at_least_2018() => res.push(IDENT),
-                        text => match SyntaxKind::from_keyword(text) {
+                        text => match SyntaxKind::from_keyword(text, edition) {
                             Some(kind) => res.push(kind),
                             None => {
-                                let contextual_keyword = SyntaxKind::from_contextual_keyword(text)
-                                    .unwrap_or(SyntaxKind::IDENT);
+                                let contextual_keyword =
+                                    SyntaxKind::from_contextual_keyword(text, edition)
+                                        .unwrap_or(SyntaxKind::IDENT);
                                 res.push_ident(contextual_keyword);
                             }
                         },
diff --git a/crates/syntax/rust.ungram b/crates/syntax/rust.ungram
index c23bcd6914..069be2df3a 100644
--- a/crates/syntax/rust.ungram
+++ b/crates/syntax/rust.ungram
@@ -9,8 +9,6 @@
 //   //          -- comment
 //   Name =      -- non-terminal definition
 //   'ident'     -- keyword or punct token (terminal)
-//   '?ident'    -- contextual keyword (terminal)
-//   too)
 //   '#ident'    -- generic token (terminal)
 //   '@ident'    -- literal token (terminal)
 //   A B         -- sequence
@@ -152,7 +150,7 @@ Item =
 
 MacroRules =
   Attr* Visibility?
-  '?macro_rules' '!' Name
+  'macro_rules' '!' Name
   TokenTree
 
 MacroDef =
@@ -188,7 +186,7 @@ UseTreeList =
 
 Fn =
  Attr* Visibility?
- '?default'? 'const'? 'async'? 'unsafe'? Abi?
+ 'default'? 'const'? 'async'? 'unsafe'? Abi?
  'fn' Name GenericParamList? ParamList RetType? WhereClause?
  (body:BlockExpr | ';')
 
@@ -220,7 +218,7 @@ RetType =
 
 TypeAlias =
   Attr* Visibility?
-  '?default'?
+  'default'?
   'type' Name GenericParamList? (':' TypeBoundList?)? WhereClause?
   ('=' Type)? ';'
 
@@ -263,7 +261,7 @@ Variant =
 
 Union =
   Attr* Visibility?
-  '?union' Name GenericParamList? WhereClause?
+  'union' Name GenericParamList? WhereClause?
   RecordFieldList
 
 // A Data Type.
@@ -276,7 +274,7 @@ Adt =
 
 Const =
   Attr* Visibility?
-  '?default'?
+  'default'?
   'const' (Name | '_') ':' Type
   ('=' body:Expr)? ';'
 
@@ -287,7 +285,7 @@ Static =
 
 Trait =
   Attr* Visibility?
-  'unsafe'? '?auto'?
+  'unsafe'? 'auto'?
   'trait' Name GenericParamList?
   (':' TypeBoundList?)? WhereClause? AssocItemList
 
@@ -306,7 +304,7 @@ AssocItem =
 
 Impl =
   Attr* Visibility?
-  '?default'? 'unsafe'?
+  'default'? 'unsafe'?
   'impl' GenericParamList? ('const'? '!'? trait:Type 'for')? self_ty:Type WhereClause?
   AssocItemList
 
@@ -387,13 +385,13 @@ Expr =
 | UnderscoreExpr
 
 OffsetOfExpr =
-  Attr* '?builtin' '#' '?offset_of' '(' Type ',' fields:(NameRef ('.' NameRef)* ) ')'
+  Attr* 'builtin' '#' 'offset_of' '(' Type ',' fields:(NameRef ('.' NameRef)* ) ')'
 
 AsmExpr =
-  Attr* '?builtin' '#' '?asm' '(' Expr ')'
+  Attr* 'builtin' '#' 'asm' '(' Expr ')'
 
 FormatArgsExpr =
-  Attr* '?builtin' '#' '?format_args' '('
+  Attr* 'builtin' '#' 'format_args' '('
   template:Expr
   (',' args:(FormatArgsArg (',' FormatArgsArg)* ','?)? )?
   ')'
@@ -425,7 +423,7 @@ StmtList =
   '}'
 
 RefExpr =
-  Attr* '&' (('?raw' 'const'?)| ('?raw'? 'mut') ) Expr
+  Attr* '&' (('raw' 'const'?)| ('raw'? 'mut') ) Expr
 
 TryExpr =
   Attr* Expr '?'
@@ -550,7 +548,7 @@ YieldExpr =
   Attr* 'yield' Expr?
 
 YeetExpr =
-  Attr* 'do' '?yeet' Expr?
+  Attr* 'do' 'yeet' Expr?
 
 LetExpr =
   Attr* 'let' Pat '=' Expr
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs
index 0228d9dd71..645575c25e 100644
--- a/crates/syntax/src/ast/make.rs
+++ b/crates/syntax/src/ast/make.rs
@@ -117,7 +117,7 @@ pub fn name_ref(name_ref: &str) -> ast::NameRef {
     ast_from_text(&format!("fn f() {{ {raw_escape}{name_ref}; }}"))
 }
 fn raw_ident_esc(ident: &str) -> &'static str {
-    if is_raw_identifier(ident) {
+    if is_raw_identifier(ident, Edition::CURRENT) {
         "r#"
     } else {
         ""
diff --git a/crates/syntax/src/utils.rs b/crates/syntax/src/utils.rs
index a38f8b2b55..77d49b442e 100644
--- a/crates/syntax/src/utils.rs
+++ b/crates/syntax/src/utils.rs
@@ -2,7 +2,7 @@
 
 use crate::SyntaxKind;
 
-pub fn is_raw_identifier(name: &str) -> bool {
-    let is_keyword = SyntaxKind::from_keyword(name).is_some();
+pub fn is_raw_identifier(name: &str, edition: parser::Edition) -> bool {
+    let is_keyword = SyntaxKind::from_keyword(name, edition).is_some();
     is_keyword && !matches!(name, "self" | "crate" | "super" | "Self")
 }
diff --git a/xtask/src/codegen/grammar.rs b/xtask/src/codegen/grammar.rs
index 0352539754..39e06f9642 100644
--- a/xtask/src/codegen/grammar.rs
+++ b/xtask/src/codegen/grammar.rs
@@ -396,24 +396,66 @@ fn generate_syntax_kinds(grammar: KindsSrc) -> String {
     let punctuation =
         grammar.punct.iter().map(|(_token, name)| format_ident!("{}", name)).collect::<Vec<_>>();
 
-    let x = |&name| match name {
+    let fmt_kw_as_variant = |&name| match name {
         "Self" => format_ident!("SELF_TYPE_KW"),
         name => format_ident!("{}_KW", to_upper_snake_case(name)),
     };
-    let full_keywords_values = grammar.keywords;
-    let full_keywords = full_keywords_values.iter().map(x);
+    let strict_keywords = grammar.keywords;
+    let strict_keywords_variants =
+        strict_keywords.iter().map(fmt_kw_as_variant).collect::<Vec<_>>();
+    let strict_keywords_tokens = strict_keywords.iter().map(|it| format_ident!("{it}"));
 
-    let contextual_keywords_values = &grammar.contextual_keywords;
-    let contextual_keywords = contextual_keywords_values.iter().map(x);
+    let edition_dependent_keywords_variants_match_arm = grammar
+        .edition_dependent_keywords
+        .iter()
+        .map(|(kw, ed)| {
+            let kw = fmt_kw_as_variant(kw);
+            quote! { #kw if #ed <= edition }
+        })
+        .collect::<Vec<_>>();
+    let edition_dependent_keywords_str_match_arm = grammar
+        .edition_dependent_keywords
+        .iter()
+        .map(|(kw, ed)| {
+            quote! { #kw if #ed <= edition }
+        })
+        .collect::<Vec<_>>();
+    let edition_dependent_keywords_variants = grammar
+        .edition_dependent_keywords
+        .iter()
+        .map(|(kw, _)| fmt_kw_as_variant(kw))
+        .collect::<Vec<_>>();
+    let edition_dependent_keywords_tokens =
+        grammar.edition_dependent_keywords.iter().map(|(it, _)| format_ident!("{it}"));
+
+    let contextual_keywords = grammar.contextual_keywords;
+    let contextual_keywords_variants =
+        contextual_keywords.iter().map(fmt_kw_as_variant).collect::<Vec<_>>();
+    let contextual_keywords_tokens = contextual_keywords.iter().map(|it| format_ident!("{it}"));
+    let contextual_keywords_str_match_arm = grammar.contextual_keywords.iter().map(|kw| {
+        match grammar.edition_dependent_keywords.iter().find(|(ed_kw, _)| ed_kw == kw) {
+            Some((_, ed)) => quote! { #kw if edition < #ed },
+            None => quote! { #kw },
+        }
+    });
+    let contextual_keywords_variants_match_arm = grammar
+        .contextual_keywords
+        .iter()
+        .map(|kw_s| {
+            let kw = fmt_kw_as_variant(kw_s);
+            match grammar.edition_dependent_keywords.iter().find(|(ed_kw, _)| ed_kw == kw_s) {
+                Some((_, ed)) => quote! { #kw if edition < #ed },
+                None => quote! { #kw },
+            }
+        })
+        .collect::<Vec<_>>();
 
-    let all_keywords_values = grammar
-        .keywords
+    let non_strict_keyword_variants = contextual_keywords_variants
         .iter()
-        .chain(grammar.contextual_keywords.iter())
-        .copied()
+        .chain(edition_dependent_keywords_variants.iter())
+        .sorted()
+        .dedup()
         .collect::<Vec<_>>();
-    let all_keywords_idents = all_keywords_values.iter().map(|kw| format_ident!("{}", kw));
-    let all_keywords = all_keywords_values.iter().map(x).collect::<Vec<_>>();
 
     let literals =
         grammar.literals.iter().map(|name| format_ident!("{}", name)).collect::<Vec<_>>();
@@ -424,6 +466,8 @@ fn generate_syntax_kinds(grammar: KindsSrc) -> String {
 
     let ast = quote! {
         #![allow(bad_style, missing_docs, unreachable_pub)]
+        use crate::Edition;
+
         /// The kind of syntax node, e.g. `IDENT`, `USE_KW`, or `STRUCT`.
         #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
         #[repr(u16)]
@@ -435,7 +479,8 @@ fn generate_syntax_kinds(grammar: KindsSrc) -> String {
             #[doc(hidden)]
             EOF,
             #(#punctuation,)*
-            #(#all_keywords,)*
+            #(#strict_keywords_variants,)*
+            #(#non_strict_keyword_variants,)*
             #(#literals,)*
             #(#tokens,)*
             #(#nodes,)*
@@ -447,31 +492,55 @@ fn generate_syntax_kinds(grammar: KindsSrc) -> String {
         use self::SyntaxKind::*;
 
         impl SyntaxKind {
-            pub fn is_keyword(self) -> bool {
-                matches!(self, #(#all_keywords)|*)
+            /// Checks whether this syntax kind is a strict keyword for the given edition.
+            /// Strict keywords are identifiers that are always considered keywords.
+            pub fn is_strict_keyword(self, edition: Edition) -> bool {
+                matches!(self, #(#strict_keywords_variants)|*)
+                || match self {
+                    #(#edition_dependent_keywords_variants_match_arm => true,)*
+                    _ => false,
+                }
             }
 
-            pub fn is_punct(self) -> bool {
+            /// Checks whether this syntax kind is a weak keyword for the given edition.
+            /// Weak keywords are identifiers that are considered keywords only in certain contexts.
+            pub fn is_contextual_keyword(self, edition: Edition) -> bool {
+                match self {
+                    #(#contextual_keywords_variants_match_arm => true,)*
+                    _ => false,
+                }
+            }
 
-                matches!(self, #(#punctuation)|*)
+            /// Checks whether this syntax kind is a strict or weak keyword for the given edition.
+            pub fn is_keyword(self, edition: Edition) -> bool {
+                matches!(self, #(#strict_keywords_variants)|*)
+                || match self {
+                    #(#edition_dependent_keywords_variants_match_arm => true,)*
+                    #(#contextual_keywords_variants_match_arm => true,)*
+                    _ => false,
+                }
+            }
 
+            pub fn is_punct(self) -> bool {
+                matches!(self, #(#punctuation)|*)
             }
 
             pub fn is_literal(self) -> bool {
                 matches!(self, #(#literals)|*)
             }
 
-            pub fn from_keyword(ident: &str) -> Option<SyntaxKind> {
+            pub fn from_keyword(ident: &str, edition: Edition) -> Option<SyntaxKind> {
                 let kw = match ident {
-                    #(#full_keywords_values => #full_keywords,)*
+                    #(#strict_keywords => #strict_keywords_variants,)*
+                    #(#edition_dependent_keywords_str_match_arm => #edition_dependent_keywords_variants,)*
                     _ => return None,
                 };
                 Some(kw)
             }
 
-            pub fn from_contextual_keyword(ident: &str) -> Option<SyntaxKind> {
+            pub fn from_contextual_keyword(ident: &str, edition: Edition) -> Option<SyntaxKind> {
                 let kw = match ident {
-                    #(#contextual_keywords_values => #contextual_keywords,)*
+                    #(#contextual_keywords_str_match_arm => #contextual_keywords_variants,)*
                     _ => return None,
                 };
                 Some(kw)
@@ -489,7 +558,9 @@ fn generate_syntax_kinds(grammar: KindsSrc) -> String {
         #[macro_export]
         macro_rules! T {
             #([#punctuation_values] => { $crate::SyntaxKind::#punctuation };)*
-            #([#all_keywords_idents] => { $crate::SyntaxKind::#all_keywords };)*
+            #([#strict_keywords_tokens] => { $crate::SyntaxKind::#strict_keywords_variants };)*
+            #([#contextual_keywords_tokens] => { $crate::SyntaxKind::#contextual_keywords_variants };)*
+            #([#edition_dependent_keywords_tokens] => { $crate::SyntaxKind::#edition_dependent_keywords_variants };)*
             [lifetime_ident] => { $crate::SyntaxKind::LIFETIME_IDENT };
             [int_number] => { $crate::SyntaxKind::INT_NUMBER };
             [ident] => { $crate::SyntaxKind::IDENT };
diff --git a/xtask/src/codegen/grammar/ast_src.rs b/xtask/src/codegen/grammar/ast_src.rs
index 3444f89908..34151bd958 100644
--- a/xtask/src/codegen/grammar/ast_src.rs
+++ b/xtask/src/codegen/grammar/ast_src.rs
@@ -1,5 +1,7 @@
 //! Defines input for code generation process.
 
+use quote::ToTokens;
+
 use crate::codegen::grammar::to_upper_snake_case;
 
 #[derive(Copy, Clone, Debug)]
@@ -10,6 +12,35 @@ pub(crate) struct KindsSrc {
     pub(crate) literals: &'static [&'static str],
     pub(crate) tokens: &'static [&'static str],
     pub(crate) nodes: &'static [&'static str],
+    pub(crate) edition_dependent_keywords: &'static [(&'static str, Edition)],
+}
+
+#[allow(dead_code)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub(super) enum Edition {
+    Edition2015,
+    Edition2018,
+    Edition2021,
+    Edition2024,
+}
+
+impl ToTokens for Edition {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        match self {
+            Edition::Edition2015 => {
+                tokens.extend(quote::quote! { Edition::Edition2015 });
+            }
+            Edition::Edition2018 => {
+                tokens.extend(quote::quote! { Edition::Edition2018 });
+            }
+            Edition::Edition2021 => {
+                tokens.extend(quote::quote! { Edition::Edition2021 });
+            }
+            Edition::Edition2024 => {
+                tokens.extend(quote::quote! { Edition::Edition2024 });
+            }
+        }
+    }
 }
 
 /// The punctuations of the language.
@@ -75,17 +106,32 @@ const EOF: &str = "EOF";
 
 const RESERVED: &[&str] = &[
     "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof", "unsized",
-    "virtual", "yield", "try",
+    "virtual", "yield",
+];
+// keywords that are keywords only in specific parse contexts
+#[doc(alias = "WEAK_KEYWORDS")]
+const CONTEXTUAL_KEYWORDS: &[&str] =
+    &["macro_rules", "union", "default", "raw", "dyn", "auto", "yeet"];
+// keywords we use for special macro expansions
+const CONTEXTUAL_BUILTIN_KEYWORDS: &[&str] = &["builtin", "offset_of", "format_args", "asm"];
+// keywords that are keywords depending on the edition
+const EDITION_DEPENDENT_KEYWORDS: &[(&str, Edition)] = &[
+    ("try", Edition::Edition2018),
+    ("dyn", Edition::Edition2018),
+    ("async", Edition::Edition2018),
+    ("await", Edition::Edition2018),
+    ("gen", Edition::Edition2024),
 ];
-const CONTEXTUAL_RESERVED: &[&str] = &[];
 
 pub(crate) fn generate_kind_src(
     nodes: &[AstNodeSrc],
     enums: &[AstEnumSrc],
     grammar: &ungrammar::Grammar,
 ) -> KindsSrc {
+    let mut contextual_keywords: Vec<&_> =
+        CONTEXTUAL_KEYWORDS.iter().chain(CONTEXTUAL_BUILTIN_KEYWORDS).copied().collect();
+
     let mut keywords: Vec<&_> = Vec::new();
-    let mut contextual_keywords: Vec<&_> = Vec::new();
     let mut tokens: Vec<&_> = TOKENS.to_vec();
     let mut literals: Vec<&_> = Vec::new();
     let mut used_puncts = vec![false; PUNCT.len()];
@@ -103,9 +149,7 @@ pub(crate) fn generate_kind_src(
             ("#", token) if !token.is_empty() => {
                 tokens.push(String::leak(to_upper_snake_case(token)));
             }
-            ("?", kw) if !kw.is_empty() => {
-                contextual_keywords.push(String::leak(kw.to_owned()));
-            }
+            _ if contextual_keywords.contains(&name) => {}
             _ if name.chars().all(char::is_alphabetic) => {
                 keywords.push(String::leak(name.to_owned()));
             }
@@ -124,9 +168,14 @@ pub(crate) fn generate_kind_src(
     keywords.extend(RESERVED.iter().copied());
     keywords.sort();
     keywords.dedup();
-    contextual_keywords.extend(CONTEXTUAL_RESERVED.iter().copied());
     contextual_keywords.sort();
     contextual_keywords.dedup();
+    let mut edition_dependent_keywords: Vec<(&_, _)> = EDITION_DEPENDENT_KEYWORDS.to_vec();
+    edition_dependent_keywords.sort();
+    edition_dependent_keywords.dedup();
+
+    keywords.retain(|&it| !contextual_keywords.contains(&it));
+    keywords.retain(|&it| !edition_dependent_keywords.iter().any(|&(kw, _)| kw == it));
 
     // we leak things here for simplicity, that way we don't have to deal with lifetimes
     // The execution is a one shot job so thats fine
@@ -142,12 +191,21 @@ pub(crate) fn generate_kind_src(
     nodes.sort();
     let keywords = Vec::leak(keywords);
     let contextual_keywords = Vec::leak(contextual_keywords);
+    let edition_dependent_keywords = Vec::leak(edition_dependent_keywords);
     let literals = Vec::leak(literals);
     literals.sort();
     let tokens = Vec::leak(tokens);
     tokens.sort();
 
-    KindsSrc { punct: PUNCT, nodes, keywords, contextual_keywords, literals, tokens }
+    KindsSrc {
+        punct: PUNCT,
+        nodes,
+        keywords,
+        contextual_keywords,
+        edition_dependent_keywords,
+        literals,
+        tokens,
+    }
 }
 
 #[derive(Default, Debug)]


Test patch:
diff --git a/crates/hir-def/src/macro_expansion_tests/mod.rs b/crates/hir-def/src/macro_expansion_tests/mod.rs
index b430e2cefb..7f76119251 100644
--- a/crates/hir-def/src/macro_expansion_tests/mod.rs
+++ b/crates/hir-def/src/macro_expansion_tests/mod.rs
@@ -25,7 +25,7 @@ use hir_expand::{
     InFile, MacroFileId, MacroFileIdExt,
 };
 use intern::Symbol;
-use span::Span;
+use span::{Edition, Span};
 use stdx::{format_to, format_to_acc};
 use syntax::{
     ast::{self, edit::IndentLevel},
@@ -257,21 +257,25 @@ fn pretty_print_macro_expansion(
             (T![;] | T!['{'] | T!['}'], _) => "\n",
             (_, T!['}']) => "\n",
             (IDENT | LIFETIME_IDENT, IDENT | LIFETIME_IDENT) => " ",
-            _ if prev_kind.is_keyword() && curr_kind.is_keyword() => " ",
-            (IDENT, _) if curr_kind.is_keyword() => " ",
-            (_, IDENT) if prev_kind.is_keyword() => " ",
+            _ if prev_kind.is_keyword(Edition::CURRENT)
+                && curr_kind.is_keyword(Edition::CURRENT) =>
+            {
+                " "
+            }
+            (IDENT, _) if curr_kind.is_keyword(Edition::CURRENT) => " ",
+            (_, IDENT) if prev_kind.is_keyword(Edition::CURRENT) => " ",
             (T![>], IDENT) => " ",
-            (T![>], _) if curr_kind.is_keyword() => " ",
+            (T![>], _) if curr_kind.is_keyword(Edition::CURRENT) => " ",
             (T![->], _) | (_, T![->]) => " ",
             (T![&&], _) | (_, T![&&]) => " ",
             (T![,], _) => " ",
             (T![:], IDENT | T!['(']) => " ",
-            (T![:], _) if curr_kind.is_keyword() => " ",
+            (T![:], _) if curr_kind.is_keyword(Edition::CURRENT) => " ",
             (T![fn], T!['(']) => "",
-            (T![']'], _) if curr_kind.is_keyword() => " ",
+            (T![']'], _) if curr_kind.is_keyword(Edition::CURRENT) => " ",
             (T![']'], T![#]) => "\n",
             (T![Self], T![::]) => "",
-            _ if prev_kind.is_keyword() => " ",
+            _ if prev_kind.is_keyword(Edition::CURRENT) => " ",
             _ => "",
         };