cairo_lang_filesystem/
db.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
7use cairo_lang_utils::{LookupIntern, Upcast};
8use salsa::Durability;
9use semver::Version;
10use serde::{Deserialize, Serialize};
11use smol_str::{SmolStr, ToSmolStr};
12
13use crate::cfg::CfgSet;
14use crate::flag::Flag;
15use crate::ids::{
16    CodeMapping, CrateId, CrateLongId, Directory, FileId, FileLongId, FlagId, FlagLongId,
17    VirtualFile,
18};
19use crate::span::{FileSummary, TextOffset, TextSpan, TextWidth};
20
21#[cfg(test)]
22#[path = "db_test.rs"]
23mod test;
24
25pub const CORELIB_CRATE_NAME: &str = "core";
26pub const CORELIB_VERSION: &str = env!("CARGO_PKG_VERSION");
27
28/// Unique identifier of a crate.
29///
30/// This directly translates to [`DependencySettings.discriminator`] except the discriminator
31/// **must** be `None` for the core crate.
32#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
33pub struct CrateIdentifier(SmolStr);
34
35impl<T: ToSmolStr> From<T> for CrateIdentifier {
36    fn from(value: T) -> Self {
37        Self(value.to_smolstr())
38    }
39}
40
41impl From<CrateIdentifier> for SmolStr {
42    fn from(value: CrateIdentifier) -> Self {
43        value.0
44    }
45}
46
47/// A configuration per crate.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct CrateConfiguration {
50    /// The root directory of the crate.
51    pub root: Directory,
52    pub settings: CrateSettings,
53}
54impl CrateConfiguration {
55    /// Returns a new configuration.
56    pub fn default_for_root(root: Directory) -> Self {
57        Self { root, settings: CrateSettings::default() }
58    }
59}
60
61/// Same as `CrateConfiguration` but without the root directory.
62#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
63pub struct CrateSettings {
64    /// The name reflecting how the crate is referred to in the Cairo code e.g. `use crate_name::`.
65    /// If set to [`None`] then [`CrateIdentifier`] key will be used as a name.
66    pub name: Option<SmolStr>,
67    /// The crate's Cairo edition.
68    pub edition: Edition,
69    /// The crate's version.
70    ///
71    /// ## [CrateSettings.version] vs. [DependencySettings.discriminator]
72    ///
73    /// Cairo uses semantic versioning for crates.
74    /// The version field is an optional piece of metadata that can be attached to a crate
75    /// and is used in various lints and can be used as a context in diagnostics.
76    ///
77    /// On the other hand, the discriminator is a unique identifier that allows including multiple
78    /// copies of a crate in a single compilation unit.
79    /// It is free-form and never reaches the user.
80    pub version: Option<Version>,
81    /// The `#[cfg(...)]` configuration.
82    pub cfg_set: Option<CfgSet>,
83    /// The crate's dependencies.
84    #[serde(default)]
85    pub dependencies: BTreeMap<String, DependencySettings>,
86
87    #[serde(default)]
88    pub experimental_features: ExperimentalFeaturesConfig,
89}
90
91/// The Cairo edition of a crate.
92///
93/// Editions are a mechanism to allow breaking changes in the compiler.
94/// Compiler minor version updates will always support all editions supported by the previous
95/// updates with the same major version. Compiler major version updates may remove support for older
96/// editions. Editions may be added to provide features that are not backwards compatible, while
97/// allowing user to opt-in to them, and be ready for later compiler updates.
98#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
99pub enum Edition {
100    /// The base edition, dated for the first release of the compiler.
101    #[default]
102    #[serde(rename = "2023_01")]
103    V2023_01,
104    #[serde(rename = "2023_10")]
105    V2023_10,
106    #[serde(rename = "2023_11")]
107    V2023_11,
108    #[serde(rename = "2024_07")]
109    V2024_07,
110}
111impl Edition {
112    /// Returns the latest stable edition.
113    ///
114    /// This Cairo edition is recommended for use in new projects and, in case of existing projects,
115    /// to migrate to when possible.
116    pub const fn latest() -> Self {
117        Self::V2024_07
118    }
119
120    /// The name of the prelude submodule of `core::prelude` for this compatibility version.
121    pub fn prelude_submodule_name(&self) -> &str {
122        match self {
123            Self::V2023_01 => "v2023_01",
124            Self::V2023_10 | Self::V2023_11 => "v2023_10",
125            Self::V2024_07 => "v2024_07",
126        }
127    }
128
129    /// Whether to ignore visibility modifiers.
130    pub fn ignore_visibility(&self) -> bool {
131        match self {
132            Self::V2023_01 | Self::V2023_10 => true,
133            Self::V2023_11 | Self::V2024_07 => false,
134        }
135    }
136}
137
138/// The settings for a dependency.
139#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
140pub struct DependencySettings {
141    /// A unique string allowing identifying different copies of the same dependency
142    /// in the compilation unit.
143    ///
144    /// Usually such copies differ by their versions or sources (or both).
145    /// It **must** be [`None`] for the core crate, for other crates it should be directly
146    /// translated from their [`CrateIdentifier`].
147    pub discriminator: Option<SmolStr>,
148}
149
150/// Configuration per crate.
151#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
152pub struct ExperimentalFeaturesConfig {
153    pub negative_impls: bool,
154    /// Allows using associated item constraints.
155    pub associated_item_constraints: bool,
156    /// Allows using coupon types and coupon calls.
157    ///
158    /// Each function has an associated `Coupon` type, which represents paying the cost of the
159    /// function before calling it.
160    #[serde(default)]
161    pub coupons: bool,
162}
163
164/// A trait for defining files external to the `filesystem` crate.
165pub trait ExternalFiles {
166    /// Returns the virtual file matching the external id.
167    fn ext_as_virtual(&self, external_id: salsa::InternId) -> VirtualFile {
168        self.try_ext_as_virtual(external_id).unwrap()
169    }
170
171    /// Returns the virtual file matching the external id if found.
172    fn try_ext_as_virtual(&self, _external_id: salsa::InternId) -> Option<VirtualFile> {
173        panic!("Should not be called, unless specifically implemented!");
174    }
175}
176
177// Salsa database interface.
178#[salsa::query_group(FilesDatabase)]
179pub trait FilesGroup: ExternalFiles {
180    #[salsa::interned]
181    fn intern_crate(&self, crt: CrateLongId) -> CrateId;
182    #[salsa::interned]
183    fn intern_file(&self, file: FileLongId) -> FileId;
184    #[salsa::interned]
185    fn intern_flag(&self, flag: FlagLongId) -> FlagId;
186
187    /// Main input of the project. Lists all the crates configurations.
188    #[salsa::input]
189    fn crate_configs(&self) -> Arc<OrderedHashMap<CrateId, CrateConfiguration>>;
190
191    /// Overrides for file content. Mostly used by language server and tests.
192    /// TODO(spapini): Currently, when this input changes, all the file_content() queries will
193    /// be invalidated.
194    /// Change this mechanism to hold file_overrides on the db struct outside salsa mechanism,
195    /// and invalidate manually.
196    #[salsa::input]
197    fn file_overrides(&self) -> Arc<OrderedHashMap<FileId, Arc<str>>>;
198
199    // TODO(yuval): consider moving this to a separate crate, or rename this crate.
200    /// The compilation flags.
201    #[salsa::input]
202    fn flags(&self) -> Arc<OrderedHashMap<FlagId, Arc<Flag>>>;
203    /// The `#[cfg(...)]` options.
204    #[salsa::input]
205    fn cfg_set(&self) -> Arc<CfgSet>;
206
207    /// List of crates in the project.
208    fn crates(&self) -> Vec<CrateId>;
209    /// Configuration of the crate.
210    fn crate_config(&self, crate_id: CrateId) -> Option<CrateConfiguration>;
211
212    /// Query for raw file contents. Private.
213    fn priv_raw_file_content(&self, file_id: FileId) -> Option<Arc<str>>;
214    /// Query for the file contents. This takes overrides into consideration.
215    fn file_content(&self, file_id: FileId) -> Option<Arc<str>>;
216    fn file_summary(&self, file_id: FileId) -> Option<Arc<FileSummary>>;
217
218    /// Query to get a compilation flag by its ID.
219    fn get_flag(&self, id: FlagId) -> Option<Arc<Flag>>;
220}
221
222pub fn init_files_group(db: &mut (dyn FilesGroup + 'static)) {
223    // Initialize inputs.
224    db.set_file_overrides(Arc::new(OrderedHashMap::default()));
225    db.set_crate_configs(Arc::new(OrderedHashMap::default()));
226    db.set_flags(Arc::new(OrderedHashMap::default()));
227    db.set_cfg_set(Arc::new(CfgSet::new()));
228}
229
230pub fn init_dev_corelib(db: &mut (dyn FilesGroup + 'static), core_lib_dir: PathBuf) {
231    db.set_crate_config(
232        CrateId::core(db),
233        Some(CrateConfiguration {
234            root: Directory::Real(core_lib_dir),
235            settings: CrateSettings {
236                name: None,
237                edition: Edition::V2024_07,
238                version: Version::parse(CORELIB_VERSION).ok(),
239                cfg_set: Default::default(),
240                dependencies: Default::default(),
241                experimental_features: ExperimentalFeaturesConfig {
242                    negative_impls: true,
243                    associated_item_constraints: true,
244                    coupons: true,
245                },
246            },
247        }),
248    );
249}
250
251impl AsFilesGroupMut for dyn FilesGroup {
252    fn as_files_group_mut(&mut self) -> &mut (dyn FilesGroup + 'static) {
253        self
254    }
255}
256
257pub trait FilesGroupEx: Upcast<dyn FilesGroup> + AsFilesGroupMut {
258    /// Overrides file content. None value removes the override.
259    fn override_file_content(&mut self, file: FileId, content: Option<Arc<str>>) {
260        let mut overrides = Upcast::upcast(self).file_overrides().as_ref().clone();
261        match content {
262            Some(content) => overrides.insert(file, content),
263            None => overrides.swap_remove(&file),
264        };
265        self.as_files_group_mut().set_file_overrides(Arc::new(overrides));
266    }
267    /// Sets the root directory of the crate. None value removes the crate.
268    fn set_crate_config(&mut self, crt: CrateId, root: Option<CrateConfiguration>) {
269        let mut crate_configs = Upcast::upcast(self).crate_configs().as_ref().clone();
270        match root {
271            Some(root) => crate_configs.insert(crt, root),
272            None => crate_configs.swap_remove(&crt),
273        };
274        self.as_files_group_mut().set_crate_configs(Arc::new(crate_configs));
275    }
276    /// Sets the given flag value. None value removes the flag.
277    fn set_flag(&mut self, id: FlagId, value: Option<Arc<Flag>>) {
278        let mut flags = Upcast::upcast(self).flags().as_ref().clone();
279        match value {
280            Some(value) => flags.insert(id, value),
281            None => flags.swap_remove(&id),
282        };
283        self.as_files_group_mut().set_flags(Arc::new(flags));
284    }
285    /// Merges specified [`CfgSet`] into one already stored in this db.
286    fn use_cfg(&mut self, cfg_set: &CfgSet) {
287        let existing = Upcast::upcast(self).cfg_set();
288        let merged = existing.union(cfg_set);
289        self.as_files_group_mut().set_cfg_set(Arc::new(merged));
290    }
291}
292impl<T: Upcast<dyn FilesGroup> + AsFilesGroupMut + ?Sized> FilesGroupEx for T {}
293
294pub trait AsFilesGroupMut {
295    fn as_files_group_mut(&mut self) -> &mut (dyn FilesGroup + 'static);
296}
297
298fn crates(db: &dyn FilesGroup) -> Vec<CrateId> {
299    // TODO(spapini): Sort for stability.
300    db.crate_configs().keys().copied().collect()
301}
302fn crate_config(db: &dyn FilesGroup, crt: CrateId) -> Option<CrateConfiguration> {
303    match crt.lookup_intern(db) {
304        CrateLongId::Real { .. } => db.crate_configs().get(&crt).cloned(),
305        CrateLongId::Virtual { name: _, file_id, settings } => Some(CrateConfiguration {
306            root: Directory::Virtual {
307                files: BTreeMap::from([("lib.cairo".into(), file_id)]),
308                dirs: Default::default(),
309            },
310            settings: toml::from_str(&settings).expect("Failed to parse virtual crate settings."),
311        }),
312    }
313}
314
315fn priv_raw_file_content(db: &dyn FilesGroup, file: FileId) -> Option<Arc<str>> {
316    match file.lookup_intern(db) {
317        FileLongId::OnDisk(path) => {
318            // This does not result in performance cost due to OS caching and the fact that salsa
319            // will re-execute only this single query if the file content did not change.
320            db.salsa_runtime().report_synthetic_read(Durability::LOW);
321
322            match fs::read_to_string(path) {
323                Ok(content) => Some(content.into()),
324                Err(_) => None,
325            }
326        }
327        FileLongId::Virtual(virt) => Some(virt.content),
328        FileLongId::External(external_id) => Some(db.ext_as_virtual(external_id).content),
329    }
330}
331fn file_content(db: &dyn FilesGroup, file: FileId) -> Option<Arc<str>> {
332    let overrides = db.file_overrides();
333    overrides.get(&file).cloned().or_else(|| db.priv_raw_file_content(file))
334}
335fn file_summary(db: &dyn FilesGroup, file: FileId) -> Option<Arc<FileSummary>> {
336    let content = db.file_content(file)?;
337    let mut line_offsets = vec![TextOffset::START];
338    let mut offset = TextOffset::START;
339    for ch in content.chars() {
340        offset = offset.add_width(TextWidth::from_char(ch));
341        if ch == '\n' {
342            line_offsets.push(offset);
343        }
344    }
345    Some(Arc::new(FileSummary { line_offsets, last_offset: offset }))
346}
347fn get_flag(db: &dyn FilesGroup, id: FlagId) -> Option<Arc<Flag>> {
348    db.flags().get(&id).cloned()
349}
350
351/// Returns the location of the originating user code.
352pub fn get_originating_location(
353    db: &dyn FilesGroup,
354    mut file_id: FileId,
355    mut span: TextSpan,
356    mut parent_files: Option<&mut Vec<FileId>>,
357) -> (FileId, TextSpan) {
358    if let Some(ref mut parent_files) = parent_files {
359        parent_files.push(file_id);
360    }
361    while let Some((parent, code_mappings)) = get_parent_and_mapping(db, file_id) {
362        if let Some(origin) = code_mappings.iter().find_map(|mapping| mapping.translate(span)) {
363            span = origin;
364            file_id = parent;
365            if let Some(ref mut parent_files) = parent_files {
366                parent_files.push(file_id);
367            }
368        } else {
369            break;
370        }
371    }
372    (file_id, span)
373}
374
375/// Returns the parent file and the code mappings of the file.
376fn get_parent_and_mapping(
377    db: &dyn FilesGroup,
378    file_id: FileId,
379) -> Option<(FileId, Arc<[CodeMapping]>)> {
380    let vf = match file_id.lookup_intern(db) {
381        FileLongId::OnDisk(_) => return None,
382        FileLongId::Virtual(vf) => vf,
383        FileLongId::External(id) => db.ext_as_virtual(id),
384    };
385    Some((vf.parent?, vf.code_mappings))
386}