cairo_lang_semantic/items/
macro_call.rs

1use std::sync::Arc;
2
3use cairo_lang_defs::db::DefsGroup;
4use cairo_lang_defs::ids::{LanguageElementId, MacroCallId, ModuleId};
5use cairo_lang_diagnostics::{Diagnostics, Maybe, skip_diagnostic};
6use cairo_lang_filesystem::ids::{
7    CodeMapping, CodeOrigin, FileKind, FileLongId, SmolStrId, VirtualFile,
8};
9use cairo_lang_filesystem::span::{TextOffset, TextSpan};
10use cairo_lang_syntax::node::{TypedStablePtr, TypedSyntaxNode, ast};
11use cairo_lang_utils::Intern;
12use salsa::Database;
13
14use crate::SemanticDiagnostic;
15use crate::diagnostic::{
16    NotFoundItemType, SemanticDiagnosticKind, SemanticDiagnostics, SemanticDiagnosticsBuilder,
17};
18use crate::expr::inference::InferenceId;
19use crate::items::macro_declaration::{
20    MacroDeclarationSemantic, MatcherContext, expand_macro_rule, is_macro_rule_match,
21};
22use crate::items::module::ModuleSemantic;
23use crate::lsp_helpers::LspHelpers;
24use crate::resolve::{ResolutionContext, ResolvedGenericItem, Resolver, ResolverMacroData};
25
26/// The data associated with a macro call in item context.
27#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
28pub struct MacroCallData<'db> {
29    /// The module to which the macro call was expanded to.
30    pub macro_call_module: Maybe<ModuleId<'db>>,
31    pub diagnostics: Diagnostics<'db, SemanticDiagnostic<'db>>,
32    pub defsite_module_id: ModuleId<'db>,
33    pub callsite_module_id: ModuleId<'db>,
34    pub expansion_mappings: Arc<[CodeMapping]>,
35    pub parent_macro_call_data: Option<Arc<ResolverMacroData<'db>>>,
36}
37
38/// Implementation of [MacroCallSemantic::priv_macro_call_data].
39fn priv_macro_call_data<'db>(
40    db: &'db dyn Database,
41    macro_call_id: MacroCallId<'db>,
42) -> Maybe<MacroCallData<'db>> {
43    let inference_id = InferenceId::MacroCall(macro_call_id);
44    let module_id = macro_call_id.module_id(db);
45    let mut resolver = Resolver::new(db, module_id, inference_id);
46    let macro_call_syntax = db.module_macro_call_by_id(macro_call_id)?;
47    // Resolve the macro call path, and report diagnostics if it finds no match or
48    // the resolved item is not a macro declaration.
49    let macro_call_path = macro_call_syntax.path(db);
50    let macro_name = macro_call_path.as_syntax_node().get_text_without_trivia(db);
51    let callsite_module_id = macro_call_id.parent_module(db);
52    // If the call is to `expose!` and no other `expose` item is locally declared - using expose.
53    if macro_name.long(db) == EXPOSE_MACRO_NAME
54        && let Ok(None) = db.module_item_by_name(callsite_module_id, macro_name)
55    {
56        let (content, mapping) = expose_content_and_mapping(db, macro_call_syntax.arguments(db))?;
57        let code_mappings: Arc<[CodeMapping]> = [mapping].into();
58        let parent_macro_call_data = resolver.macro_call_data;
59        let generated_file_long_id = FileLongId::Virtual(VirtualFile {
60            parent: Some(macro_call_syntax.stable_ptr(db).untyped().file_id(db)),
61            name: macro_name,
62            content: SmolStrId::from(db, content),
63            code_mappings: code_mappings.clone(),
64            kind: FileKind::Module,
65            original_item_removed: false,
66        });
67        db.accumulate_inline_macro_expansion(&generated_file_long_id);
68        let generated_file_id = generated_file_long_id.intern(db);
69        let macro_call_module =
70            ModuleId::MacroCall { id: macro_call_id, generated_file_id, is_expose: true };
71        return Ok(MacroCallData {
72            macro_call_module: Ok(macro_call_module),
73            diagnostics: Default::default(),
74            // Defsite and callsite aren't actually used, as it defines nothing in its code.
75            defsite_module_id: callsite_module_id,
76            callsite_module_id,
77            expansion_mappings: code_mappings,
78            parent_macro_call_data,
79        });
80    }
81    let mut diagnostics = SemanticDiagnostics::default();
82    let macro_declaration_id = match resolver.resolve_generic_path(
83        &mut diagnostics,
84        &macro_call_path,
85        NotFoundItemType::Macro,
86        ResolutionContext::Default,
87    ) {
88        Ok(ResolvedGenericItem::Macro(macro_declaration_id)) => macro_declaration_id,
89        Ok(_) => {
90            let diag_added = diagnostics.report(
91                macro_call_syntax.stable_ptr(db),
92                SemanticDiagnosticKind::InlineMacroNotFound(macro_name),
93            );
94            return Ok(MacroCallData {
95                macro_call_module: Err(diag_added),
96                diagnostics: diagnostics.build(),
97                defsite_module_id: callsite_module_id,
98                callsite_module_id,
99                expansion_mappings: Arc::new([]),
100                parent_macro_call_data: None,
101            });
102        }
103        Err(diag_added) => {
104            return Ok(MacroCallData {
105                macro_call_module: Err(diag_added),
106                diagnostics: diagnostics.build(),
107                defsite_module_id: callsite_module_id,
108                callsite_module_id,
109                expansion_mappings: Arc::new([]),
110                parent_macro_call_data: None,
111            });
112        }
113    };
114    let defsite_module_id = macro_declaration_id.parent_module(db);
115    let macro_rules = match db.macro_declaration_rules(macro_declaration_id) {
116        Ok(rules) => rules,
117        Err(diag_added) => {
118            return Ok(MacroCallData {
119                macro_call_module: Err(diag_added),
120                diagnostics: diagnostics.build(),
121                defsite_module_id,
122                callsite_module_id,
123                expansion_mappings: Arc::new([]),
124                parent_macro_call_data: None,
125            });
126        }
127    };
128    let Some((rule, (captures, placeholder_to_rep_id))) = macro_rules.iter().find_map(|rule| {
129        is_macro_rule_match(db, rule, &macro_call_syntax.arguments(db)).map(|res| (rule, res))
130    }) else {
131        let diag_added = diagnostics.report(
132            macro_call_syntax.stable_ptr(db),
133            SemanticDiagnosticKind::InlineMacroNoMatchingRule(macro_name),
134        );
135        return Ok(MacroCallData {
136            macro_call_module: Err(diag_added),
137            diagnostics: diagnostics.build(),
138            defsite_module_id,
139            callsite_module_id,
140            expansion_mappings: Arc::new([]),
141            parent_macro_call_data: None,
142        });
143    };
144    let mut matcher_ctx = MatcherContext { captures, placeholder_to_rep_id, ..Default::default() };
145    let expanded_code = expand_macro_rule(db, rule, &mut matcher_ctx).unwrap();
146    let parent_macro_call_data = resolver.macro_call_data;
147    let generated_file_long_id = FileLongId::Virtual(VirtualFile {
148        parent: Some(macro_call_syntax.stable_ptr(db).untyped().file_id(db)),
149        name: macro_name,
150        content: SmolStrId::from_arcstr(db, &expanded_code.text),
151        code_mappings: expanded_code.code_mappings.clone(),
152        kind: FileKind::Module,
153        original_item_removed: false,
154    });
155    db.accumulate_inline_macro_expansion(&generated_file_long_id);
156    let generated_file_id = generated_file_long_id.intern(db);
157    let macro_call_module =
158        ModuleId::MacroCall { id: macro_call_id, generated_file_id, is_expose: false };
159    Ok(MacroCallData {
160        macro_call_module: Ok(macro_call_module),
161        diagnostics: diagnostics.build(),
162        defsite_module_id,
163        callsite_module_id,
164        expansion_mappings: expanded_code.code_mappings,
165        parent_macro_call_data,
166    })
167}
168
169/// Query implementation of [MacroCallSemantic::priv_macro_call_data].
170#[salsa::tracked(cycle_fn=priv_macro_call_data_cycle, cycle_initial=priv_macro_call_data_initial)]
171fn priv_macro_call_data_tracked<'db>(
172    db: &'db dyn Database,
173    macro_call_id: MacroCallId<'db>,
174) -> Maybe<MacroCallData<'db>> {
175    priv_macro_call_data(db, macro_call_id)
176}
177
178/// The name of the `expose!` macro.
179pub const EXPOSE_MACRO_NAME: &str = "expose";
180
181// TODO(eytan-starkware): Return SmolStrId
182/// Get the content and mappings for the `expose!` macro call.
183pub fn expose_content_and_mapping<'db>(
184    db: &'db dyn Database,
185    args: ast::TokenTreeNode<'db>,
186) -> Maybe<(String, CodeMapping)> {
187    let tokens = match args.subtree(db) {
188        ast::WrappedTokenTree::Parenthesized(tree) => tree.tokens(db),
189        ast::WrappedTokenTree::Braced(tree) => tree.tokens(db),
190        ast::WrappedTokenTree::Bracketed(tree) => tree.tokens(db),
191        ast::WrappedTokenTree::Missing(_) => return Err(skip_diagnostic()),
192    };
193    let tokens_node = tokens.as_syntax_node();
194    let tokens_span = tokens_node.span(db);
195    Ok((
196        tokens_node.get_text(db).to_string(),
197        CodeMapping {
198            span: TextSpan::new_with_width(TextOffset::START, tokens_span.width()),
199            origin: CodeOrigin::Start(tokens_span.start),
200        },
201    ))
202}
203
204/// Cycle handling for [MacroCallSemantic::priv_macro_call_data].
205fn priv_macro_call_data_cycle<'db>(
206    _db: &'db dyn Database,
207    _value: &Maybe<MacroCallData<'db>>,
208    _count: u32,
209    _macro_call_id: MacroCallId<'db>,
210) -> salsa::CycleRecoveryAction<Maybe<MacroCallData<'db>>> {
211    salsa::CycleRecoveryAction::Iterate
212}
213/// Cycle handling for [MacroCallSemantic::priv_macro_call_data].
214fn priv_macro_call_data_initial<'db>(
215    db: &'db dyn Database,
216    macro_call_id: MacroCallId<'db>,
217) -> Maybe<MacroCallData<'db>> {
218    // If we are in a cycle, we return an empty MacroCallData with no diagnostics.
219    // This is to prevent infinite recursion in case of cyclic macro calls.
220    let mut diagnostics = SemanticDiagnostics::default();
221    let macro_call_syntax = db.module_macro_call_by_id(macro_call_id)?;
222    let macro_call_path = macro_call_syntax.path(db);
223    let macro_name = macro_call_path.as_syntax_node().get_text_without_trivia(db);
224
225    let diag_added = diagnostics.report(
226        macro_call_id.stable_ptr(db).untyped(),
227        SemanticDiagnosticKind::InlineMacroNotFound(macro_name),
228    );
229    let module_id = macro_call_id.parent_module(db);
230
231    Ok(MacroCallData {
232        macro_call_module: Err(diag_added),
233        diagnostics: diagnostics.build(),
234        defsite_module_id: module_id,
235        callsite_module_id: module_id,
236        expansion_mappings: Arc::new([]),
237        parent_macro_call_data: None,
238    })
239}
240
241/// Implementation of [MacroCallSemantic::macro_call_diagnostics].
242fn macro_call_diagnostics<'db>(
243    db: &'db dyn Database,
244    macro_call_id: MacroCallId<'db>,
245) -> Diagnostics<'db, SemanticDiagnostic<'db>> {
246    db.priv_macro_call_data(macro_call_id).map(|data| data.diagnostics).unwrap_or_default()
247}
248
249/// Query implementation of [MacroCallSemantic::macro_call_diagnostics].
250#[salsa::tracked]
251fn macro_call_diagnostics_tracked<'db>(
252    db: &'db dyn Database,
253    macro_call_id: MacroCallId<'db>,
254) -> Diagnostics<'db, SemanticDiagnostic<'db>> {
255    macro_call_diagnostics(db, macro_call_id)
256}
257
258/// Implementation of [MacroCallSemantic::macro_call_module_id].
259fn macro_call_module_id<'db>(
260    db: &'db dyn Database,
261    macro_call_id: MacroCallId<'db>,
262) -> Maybe<ModuleId<'db>> {
263    db.priv_macro_call_data(macro_call_id)?.macro_call_module
264}
265
266/// Query implementation of [MacroCallSemantic::macro_call_module_id].
267#[salsa::tracked(cycle_fn=macro_call_module_id_cycle, cycle_initial=macro_call_module_id_initial)]
268fn macro_call_module_id_tracked<'db>(
269    db: &'db dyn Database,
270    macro_call_id: MacroCallId<'db>,
271) -> Maybe<ModuleId<'db>> {
272    macro_call_module_id(db, macro_call_id)
273}
274
275/// Cycle handling for [MacroCallSemantic::macro_call_module_id].
276fn macro_call_module_id_cycle<'db>(
277    _db: &'db dyn Database,
278    _value: &Maybe<ModuleId<'db>>,
279    _count: u32,
280    _macro_call_id: MacroCallId<'db>,
281) -> salsa::CycleRecoveryAction<Maybe<ModuleId<'db>>> {
282    salsa::CycleRecoveryAction::Iterate
283}
284/// Cycle handling for [MacroCallSemantic::macro_call_module_id].
285fn macro_call_module_id_initial<'db>(
286    _db: &'db dyn Database,
287    _macro_call_id: MacroCallId<'db>,
288) -> Maybe<ModuleId<'db>> {
289    Err(skip_diagnostic())
290}
291
292/// Returns the modules that are considered a part of this module.
293///
294/// If `include_all` is true, all modules are returned, regardless if exposed, or are the main
295/// module.
296#[salsa::tracked(returns(ref))]
297pub fn module_macro_modules<'db>(
298    db: &'db dyn Database,
299    include_all: bool,
300    module_id: ModuleId<'db>,
301) -> Vec<ModuleId<'db>> {
302    let mut modules = vec![];
303    let mut stack = vec![(module_id, include_all)];
304    while let Some((module_id, expose)) = stack.pop() {
305        if expose {
306            modules.push(module_id);
307        }
308        if let Ok(macro_calls) = db.module_macro_calls_ids(module_id) {
309            for macro_call in macro_calls.iter().rev() {
310                let Ok(macro_module_id) = db.macro_call_module_id(*macro_call) else {
311                    continue;
312                };
313                let expose = expose
314                    || matches!(macro_module_id, ModuleId::MacroCall { is_expose: true, .. });
315                stack.push((macro_module_id, expose));
316            }
317        }
318    }
319    modules
320}
321
322/// Trait for macro call-related semantic queries.
323pub trait MacroCallSemantic<'db>: Database {
324    /// Returns the semantic data of a macro call.
325    fn priv_macro_call_data(
326        &'db self,
327        macro_call_id: MacroCallId<'db>,
328    ) -> Maybe<MacroCallData<'db>> {
329        priv_macro_call_data_tracked(self.as_dyn_database(), macro_call_id)
330    }
331    /// Returns the expansion result of a macro call.
332    fn macro_call_module_id(&'db self, macro_call_id: MacroCallId<'db>) -> Maybe<ModuleId<'db>> {
333        macro_call_module_id_tracked(self.as_dyn_database(), macro_call_id)
334    }
335    /// Returns the semantic diagnostics of a macro call.
336    fn macro_call_diagnostics(
337        &'db self,
338        macro_call_id: MacroCallId<'db>,
339    ) -> Diagnostics<'db, SemanticDiagnostic<'db>> {
340        macro_call_diagnostics_tracked(self.as_dyn_database(), macro_call_id)
341    }
342}
343impl<'db, T: Database + ?Sized> MacroCallSemantic<'db> for T {}