sway_types/
source_engine.rs

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