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