sway_types/
source_engine.rs

1use crate::{span, LineCol, ProgramId, SourceId, Span};
2use parking_lot::RwLock;
3use std::{
4    collections::{BTreeSet, HashMap},
5    path::{Path, PathBuf},
6    str::FromStr,
7};
8use toml::Table;
9
10/// The Source Engine manages a relationship between file paths and their corresponding
11/// integer-based source IDs. Additionally, it maintains the reverse - a map that traces
12/// back from a source ID to its original file path. The primary objective of this
13/// system is to enable clients that need to reference a file path to do so using an
14/// integer-based ID. This numeric representation can be stored more efficiently as
15/// a key in a hashmap.
16/// The Source Engine is designed to be thread-safe. Its internal structures are
17/// secured by the RwLock mechanism. This allows its functions to be invoked using
18/// a straightforward non-mutable reference, ensuring safe concurrent access.
19#[derive(Debug, Default)]
20pub struct SourceEngine {
21    next_source_id: RwLock<u32>,
22    path_to_source_map: RwLock<HashMap<PathBuf, SourceId>>,
23    source_to_path_map: RwLock<HashMap<SourceId, PathBuf>>,
24    source_to_buffer_map: RwLock<HashMap<SourceId, span::Source>>,
25    next_program_id: RwLock<u16>,
26    manifest_path_to_program_map: RwLock<HashMap<PathBuf, ProgramId>>,
27    /// Stores the package name and version for manifest path,
28    /// if available in the Forc.toml file, or the fallback package name
29    /// coming from the last part of the manifest path, and an empty version string.
30    manifest_path_to_package_info: RwLock<HashMap<PathBuf, (String, String)>>,
31    module_to_sources_map: RwLock<HashMap<ProgramId, BTreeSet<SourceId>>>,
32}
33
34impl Clone for SourceEngine {
35    fn clone(&self) -> Self {
36        SourceEngine {
37            next_source_id: RwLock::new(*self.next_source_id.read()),
38            path_to_source_map: RwLock::new(self.path_to_source_map.read().clone()),
39            source_to_path_map: RwLock::new(self.source_to_path_map.read().clone()),
40            source_to_buffer_map: RwLock::new(self.source_to_buffer_map.read().clone()),
41            next_program_id: RwLock::new(*self.next_program_id.read()),
42            manifest_path_to_program_map: RwLock::new(
43                self.manifest_path_to_program_map.read().clone(),
44            ),
45            manifest_path_to_package_info: RwLock::new(
46                self.manifest_path_to_package_info.read().clone(),
47            ),
48            module_to_sources_map: RwLock::new(self.module_to_sources_map.read().clone()),
49        }
50    }
51}
52
53impl SourceEngine {
54    const AUTOGENERATED_PATH: &'static str = "<autogenerated>";
55
56    pub fn is_span_in_autogenerated(&self, span: &crate::Span) -> Option<bool> {
57        span.source_id().map(|s| self.is_source_id_autogenerated(s))
58    }
59
60    pub fn is_source_id_autogenerated(&self, source_id: &SourceId) -> bool {
61        self.get_path(source_id)
62            .display()
63            .to_string()
64            .contains("<autogenerated>")
65    }
66
67    /// Fetch the cached source buffer for `source_id`, replacing it if the caller
68    /// provides newer text so future span lookups observe the latest file contents.
69    pub fn get_or_create_source_buffer(
70        &self,
71        source_id: &SourceId,
72        source: span::Source,
73    ) -> span::Source {
74        let mut map = self.source_to_buffer_map.write();
75
76        if let Some(existing) = map.get_mut(source_id) {
77            // Replace the cached source if the contents have changed so that
78            // subsequent spans reflect the latest file text.
79            if existing.text != source.text {
80                *existing = source;
81            }
82            existing.clone()
83        } else {
84            map.insert(*source_id, source.clone());
85            source
86        }
87    }
88
89    /// This function retrieves an integer-based source ID for a provided path buffer.
90    /// If an ID already exists for the given path, the function will return that
91    /// existing ID. If not, a new ID will be created.
92    pub fn get_source_id(&self, path: &PathBuf) -> SourceId {
93        {
94            let source_map = self.path_to_source_map.read();
95            if source_map.contains_key(path) {
96                return source_map.get(path).copied().unwrap();
97            }
98        }
99
100        let program_id = self.get_or_create_program_id_from_manifest_path(path);
101        self.get_source_id_with_program_id(path, program_id)
102    }
103
104    pub fn get_source_id_with_program_id(&self, path: &PathBuf, program_id: ProgramId) -> SourceId {
105        {
106            let source_map = self.path_to_source_map.read();
107            if source_map.contains_key(path) {
108                return source_map.get(path).copied().unwrap();
109            }
110        }
111
112        let source_id = SourceId::new(program_id.0, *self.next_source_id.read());
113        {
114            let mut next_id = self.next_source_id.write();
115            *next_id += 1;
116
117            let mut source_map = self.path_to_source_map.write();
118            source_map.insert(path.clone(), source_id);
119
120            let mut path_map = self.source_to_path_map.write();
121            path_map.insert(source_id, path.clone());
122        }
123
124        let mut module_map = self.module_to_sources_map.write();
125        module_map.entry(program_id).or_default().insert(source_id);
126
127        source_id
128    }
129
130    /// Return the associated autogenerated pseudo file for the passed `source_id`.
131    /// Example: main.autogenerated.sw for main.sw
132    ///
133    /// Returns `None`, if `source_id` does not have a valid path.
134    pub fn get_associated_autogenerated_source_id(&self, source_id: &SourceId) -> Option<SourceId> {
135        let path = self.get_path(source_id);
136        let file_name = PathBuf::from_str(path.file_name()?.to_str()?).ok()?;
137        let path = path.with_file_name(format!(
138            "{}.{}.{}",
139            file_name.file_stem()?.to_str()?,
140            Self::AUTOGENERATED_PATH,
141            file_name.extension()?.to_str()?
142        ));
143        Some(self.get_source_id_with_program_id(&path, source_id.program_id()))
144    }
145
146    /// This function provides the file path corresponding to a specified source ID.
147    pub fn get_path(&self, source_id: &SourceId) -> PathBuf {
148        self.source_to_path_map
149            .read()
150            .get(source_id)
151            .unwrap()
152            .clone()
153    }
154
155    /// This function provides the [ProgramId] corresponding to a specified manifest file path.
156    pub fn get_program_id_from_manifest_path(&self, path: impl AsRef<Path>) -> Option<ProgramId> {
157        let manifest_path = sway_utils::find_parent_manifest_dir(&path)
158            .unwrap_or_else(|| path.as_ref().to_path_buf());
159        self.manifest_path_to_program_map
160            .read()
161            .get(&manifest_path)
162            .copied()
163    }
164
165    pub fn get_or_create_program_id_from_manifest_path(&self, path: &PathBuf) -> ProgramId {
166        let manifest_path = sway_utils::find_parent_manifest_dir(path).unwrap_or(path.clone());
167        let mut module_map = self.manifest_path_to_program_map.write();
168        *module_map.entry(manifest_path.clone()).or_insert_with(|| {
169            let mut next_id = self.next_program_id.write();
170            *next_id += 1;
171            ProgramId::new(*next_id)
172        })
173    }
174
175    /// Returns the [PathBuf] associated with the provided [ProgramId], if it exists in the manifest_path_to_program_map.
176    pub fn get_manifest_path_from_program_id(&self, program_id: &ProgramId) -> Option<PathBuf> {
177        let path_to_module_map = self.manifest_path_to_program_map.read();
178        path_to_module_map
179            .iter()
180            .find(|(_, &id)| id == *program_id)
181            .map(|(path, _)| path.clone())
182    }
183
184    /// This function provides the file name (with extension) corresponding to a specified source ID.
185    pub fn get_file_name(&self, source_id: &SourceId) -> Option<String> {
186        self.get_path(source_id)
187            .as_path()
188            .file_name()
189            .map(|file_name| file_name.to_string_lossy())
190            .map(|file_name| file_name.to_string())
191    }
192
193    pub fn all_files(&self) -> Vec<PathBuf> {
194        let s = self.source_to_path_map.read();
195        let mut v = s.values().cloned().collect::<Vec<_>>();
196        v.sort();
197        v
198    }
199
200    pub fn get_source_ids_from_program_id(
201        &self,
202        program_id: ProgramId,
203    ) -> Option<BTreeSet<SourceId>> {
204        let s = self.module_to_sources_map.read();
205        s.get(&program_id).cloned()
206    }
207
208    // TODO: Do we want to parse Forc.toml files here, within the `SourceEngine`?
209    //       Currently, we don't have any other option to obtain the original package name and version.
210    //       But ideally, this and potentially other information should be passed to the compiler
211    //       from the tooling layer.
212    fn get_package_name_and_version(&self, manifest_path: &Path) -> (String, String) {
213        fn get_fallback_package_name_and_version(manifest_path: &Path) -> (String, String) {
214            // As a fallback, use the last part of the manifest path as the package name.
215            // This should actually never happen, because we should always have a valid
216            // Forc.toml file at the manifest path.
217            let package_dir_name = manifest_path
218                .iter()
219                .next_back()
220                .map(|p| p.to_string_lossy().to_string())
221                .unwrap_or_else(|| "<unknown>".to_string());
222            (package_dir_name, String::new())
223        }
224
225        fn get_project_field(toml: &Table, field_name: &str) -> Option<String> {
226            toml.get("project")
227                .and_then(|v| v.get(field_name))
228                .and_then(|field| field.as_str())
229                .map(|value| value.to_string())
230        }
231
232        let forc_toml_path = manifest_path.join("Forc.toml");
233        if !forc_toml_path.exists() {
234            return get_fallback_package_name_and_version(manifest_path);
235        }
236
237        let content = match std::fs::read_to_string(&forc_toml_path) {
238            Ok(content) => content,
239            Err(_) => return get_fallback_package_name_and_version(manifest_path),
240        };
241
242        let toml = match content.parse::<Table>() {
243            Ok(toml) => toml,
244            Err(_) => return get_fallback_package_name_and_version(manifest_path),
245        };
246
247        let package_name = get_project_field(&toml, "name").unwrap_or("<unknown>".to_string());
248        let package_version = get_project_field(&toml, "version").unwrap_or_default();
249
250        (package_name, package_version)
251    }
252
253    pub fn get_source_location(&self, span: &Span) -> SourceLocation {
254        let Some(source_id) = span.source_id() else {
255            return SourceLocation::unknown();
256        };
257
258        let source_file = self.get_path(source_id);
259
260        // Find the manifest path from the source file path.
261        let program_id = self
262            .get_program_id_from_manifest_path(&source_file)
263            .expect("the `source_file` is retrieved from the `SourceEngine::get_path` function so the manifest path and program id must exist");
264        let manifest_path = self.get_manifest_path_from_program_id(&program_id).expect(
265            "the `program_id` is retrieved from the `SourceEngine` so the manifest path must exist",
266        );
267
268        // TODO: Use HashSet::get_or_insert_with once it gets available (currently experimental).
269        //       See: https://doc.rust-lang.org/std/collections/struct.HashSet.html#method.get_or_insert_with
270        let mut package_infos = self.manifest_path_to_package_info.write();
271        let (package_name, package_version) = &package_infos
272            .entry(manifest_path.clone())
273            .or_insert_with(|| self.get_package_name_and_version(&manifest_path));
274
275        let pkg = if package_version.is_empty() {
276            package_name.clone()
277        } else {
278            format!("{package_name}@{package_version}")
279        };
280
281        // Get the relative path of the source file with respect to the manifest path.
282        let source_file = source_file
283            .strip_prefix(&manifest_path)
284            .expect("the `manifest_path` is a parent of the `source_file`")
285            .to_string_lossy();
286        let source_file = if let Some(source_file_no_leading_slash) = source_file.strip_prefix("/")
287        {
288            source_file_no_leading_slash.to_string()
289        } else {
290            source_file.to_string()
291        };
292
293        SourceLocation {
294            pkg,
295            file: source_file,
296            loc: span.start_line_col_one_index(),
297        }
298    }
299}
300
301/// A location in a source code file, represented by a package name, relative file path,
302/// and line/column within the file.
303#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
304pub struct SourceLocation {
305    /// The name and the version of the package that contains the source file,
306    /// in the format "name@version".
307    ///
308    /// E.g., "my_lib@0.1.0".
309    ///
310    /// The version is optional and may be omitted. E.g., "my_lib".
311    pub pkg: String,
312    /// The path to the source file, relative to the package root.
313    ///
314    /// E.g., "src/lib.rs".
315    pub file: String,
316    pub loc: LineCol,
317}
318
319impl SourceLocation {
320    /// Creates a new unknown `SourceLocation` instance, which has
321    /// package and file set to "<unknown>".
322    pub fn unknown() -> Self {
323        Self {
324            pkg: "<unknown>".to_string(),
325            file: "<unknown>".to_string(),
326            loc: LineCol::default(),
327        }
328    }
329}