cairo_lang_sierra_generator/
statements_locations.rs

1use std::ops::Add;
2
3use cairo_lang_defs::db::DefsGroup;
4use cairo_lang_defs::diagnostic_utils::StableLocation;
5use cairo_lang_diagnostics::ToOption;
6use cairo_lang_filesystem::ids::{FileId, FileLongId, VirtualFile};
7use cairo_lang_sierra::program::StatementIdx;
8use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode};
9use cairo_lang_utils::LookupIntern;
10use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
11use itertools::Itertools;
12
13use crate::statements_code_locations::{
14    SourceCodeLocation, SourceCodeSpan, SourceFileFullPath, StatementsSourceCodeLocations,
15};
16use crate::statements_functions::StatementsFunctions;
17
18#[cfg(test)]
19#[path = "statements_locations_test.rs"]
20mod test;
21
22/// Returns an identifier of the function that contains the given [StableLocation].
23/// It is a fully qualified path to the function which contains:
24/// - fully qualified path to the file module,
25/// - relative path to the function in the file module.
26pub fn maybe_containing_function_identifier(
27    db: &dyn DefsGroup,
28    location: StableLocation,
29) -> Option<String> {
30    let file_id = location.file_id(db.upcast());
31    let absolute_semantic_path_to_file_module = file_module_absolute_identifier(db, file_id)?;
32
33    let relative_semantic_path = function_identifier_relative_to_file_module(db, location);
34    if relative_semantic_path.is_empty() {
35        // In some cases the stable location maps to a code that is a statement like a function call
36        // directly in a file module, e.g. `Self::eq(lhs, rhs)` in `core::traits`. This brings no
37        // information about the function it was called from.
38        None
39    } else {
40        Some(absolute_semantic_path_to_file_module.add("::").add(&relative_semantic_path))
41    }
42}
43
44/// Returns an identifier of the function that contains the given [StableLocation].
45/// It is a fully qualified path to the function which contains:
46/// - fully qualified path to the file module,
47/// - relative path to the function in the file module.
48///
49/// In case the fully qualified path to the file module cannot be found
50/// it is replaced in the fully qualified function path by the file name.
51pub fn maybe_containing_function_identifier_for_tests(
52    db: &dyn DefsGroup,
53    location: StableLocation,
54) -> Option<String> {
55    let file_id = location.file_id(db.upcast());
56    let absolute_semantic_path_to_file_module = file_module_absolute_identifier(db, file_id)
57        .unwrap_or_else(|| file_id.file_name(db.upcast()));
58
59    let relative_semantic_path = function_identifier_relative_to_file_module(db, location);
60    if relative_semantic_path.is_empty() {
61        // In some cases the stable location maps to a code that is a statement like a function call
62        // directly in a file module, e.g. `Self::eq(lhs, rhs)` in `core::traits`. This brings no
63        // information about the function it was called from. It is especially relevant for corelib
64        // tests where the first stable location may map to this kind of code.
65        None
66    } else {
67        Some(absolute_semantic_path_to_file_module.add("::").add(&relative_semantic_path))
68    }
69}
70
71/// Returns the path (modules and impls) to the function in the file.
72/// The path is relative to the file module.
73pub fn function_identifier_relative_to_file_module(
74    db: &dyn DefsGroup,
75    location: StableLocation,
76) -> String {
77    let syntax_db = db.upcast();
78    let mut relative_semantic_path_segments: Vec<String> = vec![];
79    let mut syntax_node = location.syntax_node(db);
80    let mut statement_located_in_function = false;
81    loop {
82        // TODO(Gil): Extract this function into a trait of syntax kind to support future
83        // function containing items (specifically trait functions).
84        match syntax_node.kind(syntax_db) {
85            cairo_lang_syntax::node::kind::SyntaxKind::FunctionWithBody => {
86                let function_name =
87                    cairo_lang_syntax::node::ast::FunctionWithBody::from_syntax_node(
88                        syntax_db,
89                        syntax_node.clone(),
90                    )
91                    .declaration(syntax_db)
92                    .name(syntax_db)
93                    .text(syntax_db);
94
95                if relative_semantic_path_segments.is_empty() {
96                    statement_located_in_function = true;
97                }
98
99                relative_semantic_path_segments.push(function_name.to_string());
100            }
101            cairo_lang_syntax::node::kind::SyntaxKind::ItemImpl => {
102                let impl_name = cairo_lang_syntax::node::ast::ItemImpl::from_syntax_node(
103                    syntax_db,
104                    syntax_node.clone(),
105                )
106                .name(syntax_db)
107                .text(syntax_db);
108                relative_semantic_path_segments.push(impl_name.to_string());
109            }
110            cairo_lang_syntax::node::kind::SyntaxKind::ItemModule => {
111                let module_name = cairo_lang_syntax::node::ast::ItemModule::from_syntax_node(
112                    syntax_db,
113                    syntax_node.clone(),
114                )
115                .name(syntax_db)
116                .text(syntax_db);
117                relative_semantic_path_segments.push(module_name.to_string());
118            }
119            _ => {}
120        }
121        if let Some(parent) = syntax_node.parent() {
122            syntax_node = parent;
123        } else {
124            break;
125        }
126    }
127
128    // If the statement is not located in a function, and it is located a generated file it is
129    // probably located in a code block generated by an inline macro such as `array` or `panic`.
130    let file_id = location.file_id(db.upcast());
131    if !statement_located_in_function
132        && matches!(
133            file_id.lookup_intern(db),
134            FileLongId::Virtual(VirtualFile { parent: Some(_), .. })
135        )
136    {
137        relative_semantic_path_segments.insert(0, file_id.file_name(db.upcast()));
138    }
139
140    relative_semantic_path_segments.into_iter().rev().join("::")
141}
142
143/// Returns a location in the user file corresponding to the given [StableLocation].
144/// It consists of a full path to the file and a text span in the file.
145pub fn maybe_code_location(
146    db: &dyn DefsGroup,
147    location: StableLocation,
148) -> Option<(SourceFileFullPath, SourceCodeSpan)> {
149    let location = location.diagnostic_location(db.upcast()).user_location(db.upcast());
150    let file_full_path = location.file_id.full_path(db.upcast());
151    let position = location.span.position_in_file(db.upcast(), location.file_id)?;
152    let source_location = SourceCodeSpan {
153        start: SourceCodeLocation { col: position.start.col, line: position.start.line },
154        end: SourceCodeLocation { col: position.end.col, line: position.end.line },
155    };
156
157    Some((SourceFileFullPath(file_full_path), source_location))
158}
159
160/// This function returns a fully qualified path to the file module.
161/// `None` should be returned only for compiler tests where files of type `VirtualFile` may be non
162/// generated files.
163pub fn file_module_absolute_identifier(db: &dyn DefsGroup, mut file_id: FileId) -> Option<String> {
164    // `VirtualFile` is a generated file (e.g., by macros like `#[starknet::contract]`)
165    // that won't have a matching file module in the db. Instead, we find its non generated parent
166    // which is in the same module and have a matching file module in the db.
167    while let FileLongId::Virtual(VirtualFile { parent: Some(parent), .. }) =
168        file_id.lookup_intern(db)
169    {
170        file_id = parent;
171    }
172
173    let file_modules = db.file_modules(file_id).to_option()?;
174    let full_path = file_modules.first().unwrap().full_path(db.upcast());
175
176    Some(full_path)
177}
178
179/// The locations in the Cairo source code which caused a statement to be generated.
180#[derive(Clone, Debug, Default, Eq, PartialEq)]
181pub struct StatementsLocations {
182    pub locations: UnorderedHashMap<StatementIdx, Vec<StableLocation>>,
183}
184
185impl StatementsLocations {
186    /// Creates a new [StatementsLocations] object from a list of [`Option<StableLocation>`].
187    pub fn from_locations_vec(locations_vec: &[Vec<StableLocation>]) -> Self {
188        let mut locations = UnorderedHashMap::default();
189        for (idx, stmt_locations) in locations_vec.iter().enumerate() {
190            if !stmt_locations.is_empty() {
191                locations.insert(StatementIdx(idx), stmt_locations.clone());
192            }
193        }
194        Self { locations }
195    }
196    /// Builds a map between each Sierra statement index and a string representation of the Cairo
197    /// function that it was generated from. It is used for places
198    /// without db access such as the profiler.
199    // TODO(Gil): Add a db access to the profiler and remove this function.
200    pub fn get_statements_functions_map_for_tests(
201        &self,
202        db: &dyn DefsGroup,
203    ) -> UnorderedHashMap<StatementIdx, String> {
204        self.locations
205            .iter_sorted()
206            .filter_map(|(statement_idx, stable_locations)| {
207                maybe_containing_function_identifier_for_tests(
208                    db,
209                    *stable_locations.first().unwrap(),
210                )
211                .map(|function_identifier| (*statement_idx, function_identifier))
212            })
213            .collect()
214    }
215
216    /// Creates a new [StatementsFunctions] struct using [StatementsLocations] and [DefsGroup].
217    pub fn extract_statements_functions(&self, db: &dyn DefsGroup) -> StatementsFunctions {
218        StatementsFunctions {
219            statements_to_functions_map: self
220                .locations
221                .iter_sorted()
222                .map(|(statement_idx, stable_locations)| {
223                    (
224                        *statement_idx,
225                        stable_locations
226                            .iter()
227                            .filter_map(|s| maybe_containing_function_identifier(db, *s))
228                            .collect(),
229                    )
230                })
231                .collect(),
232        }
233    }
234
235    /// Creates a new [StatementsSourceCodeLocations] struct using [StatementsLocations] and
236    /// [DefsGroup].
237    pub fn extract_statements_source_code_locations(
238        &self,
239        db: &dyn DefsGroup,
240    ) -> StatementsSourceCodeLocations {
241        StatementsSourceCodeLocations {
242            statements_to_code_location_map: self
243                .locations
244                .iter_sorted()
245                .map(|(statement_idx, stable_locations)| {
246                    (
247                        *statement_idx,
248                        stable_locations
249                            .iter()
250                            .filter_map(|s| maybe_code_location(db, *s))
251                            .collect(),
252                    )
253                })
254                .collect(),
255        }
256    }
257}