Skip to main content

hir_def/expr_store/
expander.rs

1//! Macro expansion utilities.
2
3use std::mem;
4
5use base_db::{Crate, SourceDatabase};
6use cfg::CfgOptions;
7use drop_bomb::DropBomb;
8use hir_expand::{
9    AstId, ExpandError, ExpandErrorKind, ExpandResult, HirFileId, InFile, Lookup, MacroCallId,
10    eager::EagerCallBackFn, mod_path::ModPath, span_map::SpanMap,
11};
12use span::{AstIdMap, SyntaxContext};
13use syntax::ast::HasAttrs;
14use syntax::{AstNode, Parse, ast};
15use tt::TextRange;
16
17use crate::{
18    MacroId, UnresolvedMacro, attrs::AttrFlags, expr_store::HygieneId, macro_call_as_call_id,
19    nameres::DefMap,
20};
21
22#[derive(Debug)]
23pub(super) struct Expander<'db> {
24    span_map: SpanMap<'db>,
25    current_file_id: HirFileId,
26    ast_id_map: &'db AstIdMap,
27    /// `recursion_depth == u32::MAX` indicates that the recursion limit has been reached.
28    macro_depth: u32,
29    recursion_limit: u32,
30}
31
32impl<'db> Expander<'db> {
33    pub(super) fn new(
34        db: &'db dyn SourceDatabase,
35        current_file_id: HirFileId,
36        def_map: &'db DefMap,
37    ) -> Expander<'db> {
38        let recursion_limit = def_map.recursion_limit();
39        let recursion_limit = if cfg!(test) {
40            // Without this, `body::tests::your_stack_belongs_to_me` stack-overflows in debug
41            std::cmp::min(32, recursion_limit)
42        } else {
43            recursion_limit
44        };
45        Expander {
46            current_file_id,
47            macro_depth: current_file_id.macro_expansion_depth(db),
48            recursion_limit,
49            span_map: current_file_id.span_map(db),
50            ast_id_map: current_file_id.ast_id_map(db),
51        }
52    }
53
54    pub(super) fn ctx_for_range(&self, range: TextRange) -> SyntaxContext {
55        self.span_map.span_for_range(range).ctx
56    }
57
58    pub(super) fn hygiene_for_range(&self, db: &dyn SourceDatabase, range: TextRange) -> HygieneId {
59        match self.span_map {
60            SpanMap::ExpansionSpanMap(span_map) => {
61                HygieneId::new(span_map.span_at(range.start()).ctx.opaque_and_semiopaque(db))
62            }
63            SpanMap::RealSpanMap(_) => HygieneId::ROOT,
64        }
65    }
66
67    pub(super) fn is_cfg_enabled(
68        &self,
69        owner: &dyn HasAttrs,
70        cfg_options: &CfgOptions,
71    ) -> Result<(), cfg::CfgExpr> {
72        AttrFlags::is_cfg_enabled_for(owner, cfg_options)
73    }
74
75    pub(super) fn enter_expand<T: ast::AstNode>(
76        &mut self,
77        db: &'db dyn SourceDatabase,
78        macro_call: ast::MacroCall,
79        krate: Crate,
80        resolver: impl Fn(&ModPath) -> Option<MacroId>,
81        eager_callback: EagerCallBackFn<'_>,
82    ) -> Result<ExpandResult<Option<(Mark<'db>, Option<Parse<T>>)>>, UnresolvedMacro> {
83        // FIXME: within_limit should support this, instead of us having to extract the error
84        let mut unresolved_macro_err = None;
85
86        let result = self.within_limit(db, |this| {
87            let macro_call = this.in_file(&macro_call);
88
89            let expands_to = hir_expand::ExpandTo::from_call_site(macro_call.value);
90            let ast_id = AstId::new(macro_call.file_id, this.ast_id_map().ast_id(macro_call.value));
91            let path = macro_call.value.path().and_then(|path| {
92                let range = path.syntax().text_range();
93                let mod_path = ModPath::from_src(db, path, &mut |range| {
94                    this.span_map.span_for_range(range).ctx
95                })?;
96                let call_site = this.span_map.span_for_range(range);
97                Some((call_site, mod_path))
98            });
99
100            let Some((call_site, path)) = path else {
101                return ExpandResult::only_err(ExpandError::other(
102                    this.span_map.span_for_range(macro_call.value.syntax().text_range()),
103                    "malformed macro invocation",
104                ));
105            };
106
107            match macro_call_as_call_id(
108                db,
109                ast_id,
110                &path,
111                call_site.ctx,
112                expands_to,
113                krate,
114                this.macro_depth + 1,
115                this.recursion_limit,
116                |path| resolver(path).map(|it| it.definition(db)),
117                eager_callback,
118            ) {
119                Ok(call_id) => call_id,
120                Err(resolve_err) => {
121                    unresolved_macro_err = Some(resolve_err);
122                    ExpandResult { value: None, err: None }
123                }
124            }
125        });
126
127        if let Some(err) = unresolved_macro_err { Err(err) } else { Ok(result) }
128    }
129
130    pub(super) fn enter_expand_id<T: ast::AstNode>(
131        &mut self,
132        db: &'db dyn SourceDatabase,
133        call_id: MacroCallId,
134    ) -> ExpandResult<Option<(Mark<'db>, Option<Parse<T>>)>> {
135        self.within_limit(db, |_this| ExpandResult::ok(Some(call_id)))
136    }
137
138    pub(super) fn exit(&mut self, Mark { file_id, span_map, ast_id_map, mut bomb }: Mark<'db>) {
139        self.span_map = span_map;
140        self.current_file_id = file_id;
141        self.ast_id_map = ast_id_map;
142        if self.macro_depth == u32::MAX {
143            // Recursion limit has been reached somewhere in the macro expansion tree. Reset the
144            // depth only when we get out of the tree.
145            if !self.current_file_id.is_macro() {
146                self.macro_depth = 0;
147            }
148        } else {
149            self.macro_depth -= 1;
150        }
151        bomb.defuse();
152    }
153
154    pub(super) fn in_file<T>(&self, value: T) -> InFile<T> {
155        InFile { file_id: self.current_file_id, value }
156    }
157
158    pub(super) fn current_file_id(&self) -> HirFileId {
159        self.current_file_id
160    }
161
162    fn within_limit<F, T: ast::AstNode>(
163        &mut self,
164        db: &'db dyn SourceDatabase,
165        op: F,
166    ) -> ExpandResult<Option<(Mark<'db>, Option<Parse<T>>)>>
167    where
168        F: FnOnce(&mut Self) -> ExpandResult<Option<MacroCallId>>,
169    {
170        if self.macro_depth == u32::MAX {
171            // Recursion limit has been reached somewhere in the macro expansion tree. We should
172            // stop expanding other macro calls in this tree, or else this may result in
173            // exponential number of macro expansions, leading to a hang.
174            //
175            // The overflow error should have been reported when it occurred (see the next branch),
176            // so don't return overflow error here to avoid diagnostics duplication.
177            cov_mark::hit!(overflow_but_not_me);
178            return ExpandResult::ok(None);
179        }
180
181        let ExpandResult { value, err } = op(self);
182        let Some(call_id) = value else {
183            return ExpandResult { value: None, err };
184        };
185        if self.macro_depth > self.recursion_limit {
186            self.macro_depth = u32::MAX;
187            cov_mark::hit!(your_stack_belongs_to_me);
188            return ExpandResult::only_err(ExpandError::new(
189                call_id.macro_arg_considering_derives(db, &call_id.lookup(db).kind).2,
190                ExpandErrorKind::RecursionOverflow,
191            ));
192        }
193
194        let res = call_id.parse_macro_expansion(db);
195
196        let err = err.or_else(|| res.err.clone());
197        ExpandResult {
198            value: {
199                let parse = res.value.0.clone().cast::<T>();
200
201                self.macro_depth += 1;
202                let old_file_id = std::mem::replace(&mut self.current_file_id, call_id.into());
203                let old_span_map =
204                    std::mem::replace(&mut self.span_map, SpanMap::ExpansionSpanMap(&res.value.1));
205                let prev_ast_id_map =
206                    mem::replace(&mut self.ast_id_map, self.current_file_id.ast_id_map(db));
207                let mark = Mark {
208                    file_id: old_file_id,
209                    span_map: old_span_map,
210                    ast_id_map: prev_ast_id_map,
211                    bomb: DropBomb::new("expansion mark dropped"),
212                };
213                Some((mark, parse))
214            },
215            err,
216        }
217    }
218
219    #[inline]
220    pub(super) fn ast_id_map(&self) -> &AstIdMap {
221        self.ast_id_map
222    }
223
224    #[inline]
225    pub(super) fn span_map(&self) -> SpanMap<'_> {
226        self.span_map
227    }
228}
229
230#[derive(Debug)]
231pub(super) struct Mark<'db> {
232    file_id: HirFileId,
233    span_map: SpanMap<'db>,
234    ast_id_map: &'db AstIdMap,
235    bomb: DropBomb,
236}