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    BlobId, BlobLongId, CodeMapping, CodeOrigin, CrateId, CrateLongId, Directory, FileId,
17    FileLongId, FlagId, FlagLongId, 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    pub cache_file: Option<BlobId>,
54}
55impl CrateConfiguration {
56    /// Returns a new configuration.
57    pub fn default_for_root(root: Directory) -> Self {
58        Self { root, settings: CrateSettings::default(), cache_file: None }
59    }
60}
61
62/// Same as `CrateConfiguration` but without the root directory.
63#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
64pub struct CrateSettings {
65    /// The name reflecting how the crate is referred to in the Cairo code e.g. `use crate_name::`.
66    /// If set to [`None`] then [`CrateIdentifier`] key will be used as a name.
67    pub name: Option<SmolStr>,
68    /// The crate's Cairo edition.
69    pub edition: Edition,
70    /// The crate's version.
71    ///
72    /// ## [CrateSettings.version] vs. [DependencySettings.discriminator]
73    ///
74    /// Cairo uses semantic versioning for crates.
75    /// The version field is an optional piece of metadata that can be attached to a crate
76    /// and is used in various lints and can be used as a context in diagnostics.
77    ///
78    /// On the other hand, the discriminator is a unique identifier that allows including multiple
79    /// copies of a crate in a single compilation unit.
80    /// It is free-form and never reaches the user.
81    pub version: Option<Version>,
82    /// The `#[cfg(...)]` configuration.
83    pub cfg_set: Option<CfgSet>,
84    /// The crate's dependencies.
85    #[serde(default)]
86    pub dependencies: BTreeMap<String, DependencySettings>,
87
88    #[serde(default)]
89    pub experimental_features: ExperimentalFeaturesConfig,
90}
91
92/// The Cairo edition of a crate.
93///
94/// Editions are a mechanism to allow breaking changes in the compiler.
95/// Compiler minor version updates will always support all editions supported by the previous
96/// updates with the same major version. Compiler major version updates may remove support for older
97/// editions. Editions may be added to provide features that are not backwards compatible, while
98/// allowing user to opt-in to them, and be ready for later compiler updates.
99#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
100pub enum Edition {
101    /// The base edition, dated for the first release of the compiler.
102    #[default]
103    #[serde(rename = "2023_01")]
104    V2023_01,
105    #[serde(rename = "2023_10")]
106    V2023_10,
107    #[serde(rename = "2023_11")]
108    V2023_11,
109    #[serde(rename = "2024_07")]
110    V2024_07,
111}
112impl Edition {
113    /// Returns the latest stable edition.
114    ///
115    /// This Cairo edition is recommended for use in new projects and, in case of existing projects,
116    /// to migrate to when possible.
117    pub const fn latest() -> Self {
118        Self::V2024_07
119    }
120
121    /// The name of the prelude submodule of `core::prelude` for this compatibility version.
122    pub fn prelude_submodule_name(&self) -> &str {
123        match self {
124            Self::V2023_01 => "v2023_01",
125            Self::V2023_10 | Self::V2023_11 => "v2023_10",
126            Self::V2024_07 => "v2024_07",
127        }
128    }
129
130    /// Whether to ignore visibility modifiers.
131    pub fn ignore_visibility(&self) -> bool {
132        match self {
133            Self::V2023_01 | Self::V2023_10 => true,
134            Self::V2023_11 | Self::V2024_07 => false,
135        }
136    }
137}
138
139/// The settings for a dependency.
140#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
141pub struct DependencySettings {
142    /// A unique string allowing identifying different copies of the same dependency
143    /// in the compilation unit.
144    ///
145    /// Usually such copies differ by their versions or sources (or both).
146    /// It **must** be [`None`] for the core crate, for other crates it should be directly
147    /// translated from their [`CrateIdentifier`].
148    pub discriminator: Option<SmolStr>,
149}
150
151/// Configuration per crate.
152#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
153pub struct ExperimentalFeaturesConfig {
154    pub negative_impls: bool,
155    /// Allows using associated item constraints.
156    pub associated_item_constraints: bool,
157    /// Allows using coupon types and coupon calls.
158    ///
159    /// Each function has an associated `Coupon` type, which represents paying the cost of the
160    /// function before calling it.
161    #[serde(default)]
162    pub coupons: bool,
163}
164
165/// A trait for defining files external to the `filesystem` crate.
166pub trait ExternalFiles {
167    /// Returns the virtual file matching the external id.
168    fn ext_as_virtual(&self, external_id: salsa::InternId) -> VirtualFile {
169        self.try_ext_as_virtual(external_id).unwrap()
170    }
171
172    /// Returns the virtual file matching the external id if found.
173    fn try_ext_as_virtual(&self, _external_id: salsa::InternId) -> Option<VirtualFile> {
174        panic!("Should not be called, unless specifically implemented!");
175    }
176}
177
178// Salsa database interface.
179#[salsa::query_group(FilesDatabase)]
180pub trait FilesGroup: ExternalFiles {
181    #[salsa::interned]
182    fn intern_crate(&self, crt: CrateLongId) -> CrateId;
183    #[salsa::interned]
184    fn intern_file(&self, file: FileLongId) -> FileId;
185    #[salsa::interned]
186    fn intern_blob(&self, blob: BlobLongId) -> BlobId;
187    #[salsa::interned]
188    fn intern_flag(&self, flag: FlagLongId) -> FlagId;
189
190    /// Main input of the project. Lists all the crates configurations.
191    #[salsa::input]
192    fn crate_configs(&self) -> Arc<OrderedHashMap<CrateId, CrateConfiguration>>;
193
194    /// Overrides for file content. Mostly used by language server and tests.
195    /// TODO(spapini): Currently, when this input changes, all the file_content() queries will
196    /// be invalidated.
197    /// Change this mechanism to hold file_overrides on the db struct outside salsa mechanism,
198    /// and invalidate manually.
199    #[salsa::input]
200    fn file_overrides(&self) -> Arc<OrderedHashMap<FileId, Arc<str>>>;
201
202    // TODO(yuval): consider moving this to a separate crate, or rename this crate.
203    /// The compilation flags.
204    #[salsa::input]
205    fn flags(&self) -> Arc<OrderedHashMap<FlagId, Arc<Flag>>>;
206    /// The `#[cfg(...)]` options.
207    #[salsa::input]
208    fn cfg_set(&self) -> Arc<CfgSet>;
209
210    /// List of crates in the project.
211    fn crates(&self) -> Vec<CrateId>;
212    /// Configuration of the crate.
213    fn crate_config(&self, crate_id: CrateId) -> Option<CrateConfiguration>;
214
215    /// Query for raw file contents. Private.
216    fn priv_raw_file_content(&self, file_id: FileId) -> Option<Arc<str>>;
217    /// Query for the file contents. This takes overrides into consideration.
218    fn file_content(&self, file_id: FileId) -> Option<Arc<str>>;
219    fn file_summary(&self, file_id: FileId) -> Option<Arc<FileSummary>>;
220
221    /// Query for the blob content.
222    fn blob_content(&self, blob_id: BlobId) -> Option<Arc<[u8]>>;
223    /// Query to get a compilation flag by its ID.
224    fn get_flag(&self, id: FlagId) -> Option<Arc<Flag>>;
225}
226
227pub fn init_files_group(db: &mut (dyn FilesGroup + 'static)) {
228    // Initialize inputs.
229    db.set_file_overrides(Arc::new(OrderedHashMap::default()));
230    db.set_crate_configs(Arc::new(OrderedHashMap::default()));
231    db.set_flags(Arc::new(OrderedHashMap::default()));
232    db.set_cfg_set(Arc::new(CfgSet::new()));
233}
234
235pub fn init_dev_corelib(db: &mut (dyn FilesGroup + 'static), core_lib_dir: PathBuf) {
236    db.set_crate_config(
237        CrateId::core(db),
238        Some(CrateConfiguration {
239            root: Directory::Real(core_lib_dir),
240            settings: CrateSettings {
241                name: None,
242                edition: Edition::V2024_07,
243                version: Version::parse(CORELIB_VERSION).ok(),
244                cfg_set: Default::default(),
245                dependencies: Default::default(),
246                experimental_features: ExperimentalFeaturesConfig {
247                    negative_impls: true,
248                    associated_item_constraints: true,
249                    coupons: true,
250                },
251            },
252            cache_file: None,
253        }),
254    );
255}
256
257impl AsFilesGroupMut for dyn FilesGroup {
258    fn as_files_group_mut(&mut self) -> &mut (dyn FilesGroup + 'static) {
259        self
260    }
261}
262
263pub trait FilesGroupEx: Upcast<dyn FilesGroup> + AsFilesGroupMut {
264    /// Overrides file content. None value removes the override.
265    fn override_file_content(&mut self, file: FileId, content: Option<Arc<str>>) {
266        let mut overrides = Upcast::upcast(self).file_overrides().as_ref().clone();
267        match content {
268            Some(content) => overrides.insert(file, content),
269            None => overrides.swap_remove(&file),
270        };
271        self.as_files_group_mut().set_file_overrides(Arc::new(overrides));
272    }
273    /// Sets the root directory of the crate. None value removes the crate.
274    fn set_crate_config(&mut self, crt: CrateId, root: Option<CrateConfiguration>) {
275        let mut crate_configs = Upcast::upcast(self).crate_configs().as_ref().clone();
276        match root {
277            Some(root) => crate_configs.insert(crt, root),
278            None => crate_configs.swap_remove(&crt),
279        };
280        self.as_files_group_mut().set_crate_configs(Arc::new(crate_configs));
281    }
282    /// Sets the given flag value. None value removes the flag.
283    fn set_flag(&mut self, id: FlagId, value: Option<Arc<Flag>>) {
284        let mut flags = Upcast::upcast(self).flags().as_ref().clone();
285        match value {
286            Some(value) => flags.insert(id, value),
287            None => flags.swap_remove(&id),
288        };
289        self.as_files_group_mut().set_flags(Arc::new(flags));
290    }
291    /// Merges specified [`CfgSet`] into one already stored in this db.
292    fn use_cfg(&mut self, cfg_set: &CfgSet) {
293        let existing = Upcast::upcast(self).cfg_set();
294        let merged = existing.union(cfg_set);
295        self.as_files_group_mut().set_cfg_set(Arc::new(merged));
296    }
297}
298impl<T: Upcast<dyn FilesGroup> + AsFilesGroupMut + ?Sized> FilesGroupEx for T {}
299
300pub trait AsFilesGroupMut {
301    fn as_files_group_mut(&mut self) -> &mut (dyn FilesGroup + 'static);
302}
303
304fn crates(db: &dyn FilesGroup) -> Vec<CrateId> {
305    // TODO(spapini): Sort for stability.
306    db.crate_configs().keys().copied().collect()
307}
308fn crate_config(db: &dyn FilesGroup, crt: CrateId) -> Option<CrateConfiguration> {
309    match crt.lookup_intern(db) {
310        CrateLongId::Real { .. } => db.crate_configs().get(&crt).cloned(),
311        CrateLongId::Virtual { name: _, file_id, settings, cache_file } => {
312            Some(CrateConfiguration {
313                root: Directory::Virtual {
314                    files: BTreeMap::from([("lib.cairo".into(), file_id)]),
315                    dirs: Default::default(),
316                },
317                settings: toml::from_str(&settings)
318                    .expect("Failed to parse virtual crate settings."),
319                cache_file,
320            })
321        }
322    }
323}
324
325fn priv_raw_file_content(db: &dyn FilesGroup, file: FileId) -> Option<Arc<str>> {
326    match file.lookup_intern(db) {
327        FileLongId::OnDisk(path) => {
328            // This does not result in performance cost due to OS caching and the fact that salsa
329            // will re-execute only this single query if the file content did not change.
330            db.salsa_runtime().report_synthetic_read(Durability::LOW);
331
332            match fs::read_to_string(path) {
333                Ok(content) => Some(content.into()),
334                Err(_) => None,
335            }
336        }
337        FileLongId::Virtual(virt) => Some(virt.content),
338        FileLongId::External(external_id) => Some(db.ext_as_virtual(external_id).content),
339    }
340}
341fn file_content(db: &dyn FilesGroup, file: FileId) -> Option<Arc<str>> {
342    let overrides = db.file_overrides();
343    overrides.get(&file).cloned().or_else(|| db.priv_raw_file_content(file))
344}
345fn file_summary(db: &dyn FilesGroup, file: FileId) -> Option<Arc<FileSummary>> {
346    let content = db.file_content(file)?;
347    let mut line_offsets = vec![TextOffset::START];
348    let mut offset = TextOffset::START;
349    for ch in content.chars() {
350        offset = offset.add_width(TextWidth::from_char(ch));
351        if ch == '\n' {
352            line_offsets.push(offset);
353        }
354    }
355    Some(Arc::new(FileSummary { line_offsets, last_offset: offset }))
356}
357fn get_flag(db: &dyn FilesGroup, id: FlagId) -> Option<Arc<Flag>> {
358    db.flags().get(&id).cloned()
359}
360
361fn blob_content(db: &dyn FilesGroup, blob: BlobId) -> Option<Arc<[u8]>> {
362    match blob.lookup_intern(db) {
363        BlobLongId::OnDisk(path) => {
364            // This does not result in performance cost due to OS caching and the fact that salsa
365            // will re-execute only this single query if the file content did not change.
366            db.salsa_runtime().report_synthetic_read(Durability::LOW);
367
368            match fs::read(path) {
369                Ok(content) => Some(content.into()),
370                Err(_) => None,
371            }
372        }
373        BlobLongId::Virtual(content) => Some(content),
374    }
375}
376
377/// Returns the location of the originating user code.
378pub fn get_originating_location(
379    db: &dyn FilesGroup,
380    mut file_id: FileId,
381    mut span: TextSpan,
382    mut parent_files: Option<&mut Vec<FileId>>,
383) -> (FileId, TextSpan) {
384    if let Some(ref mut parent_files) = parent_files {
385        parent_files.push(file_id);
386    }
387    while let Some((parent, code_mappings)) = get_parent_and_mapping(db, file_id) {
388        if let Some(origin) = translate_location(&code_mappings, span) {
389            span = origin;
390            file_id = parent;
391            if let Some(ref mut parent_files) = parent_files {
392                parent_files.push(file_id);
393            }
394        } else {
395            break;
396        }
397    }
398    (file_id, span)
399}
400
401/// This function finds a span in original code that corresponds to the provided span in the
402/// generated code, using the provided code mappings.
403///
404/// Code mappings describe a mapping between the original code and the generated one.
405/// Each mapping has a resulting span in a generated file and an origin in the original file.
406///
407/// If any of the provided mappings fully contains the span, origin span of the mapping will be
408/// returned. Otherwise, the function will try to find a span that is a result of a concatenation of
409/// multiple consecutive mappings.
410fn translate_location(code_mapping: &[CodeMapping], span: TextSpan) -> Option<TextSpan> {
411    // Find all mappings that have non-empty intersection with the provided span.
412    let intersecting_mappings = || {
413        code_mapping.iter().filter(|mapping| {
414            // Omit mappings to the left or to the right of current span.
415            !(mapping.span.end < span.start || mapping.span.start > span.end)
416        })
417    };
418
419    // If any of the mappings fully contains the span, return the origin span of the mapping.
420    if let Some(containing) = intersecting_mappings().find(|mapping| {
421        mapping.span.contains(span) && !matches!(mapping.origin, CodeOrigin::CallSite(_))
422    }) {
423        // Found a span that fully contains the current one - translates it.
424        return containing.translate(span);
425    }
426
427    // Call site can be treated as default origin.
428    let call_site = intersecting_mappings()
429        .find(|mapping| {
430            mapping.span.contains(span) && matches!(mapping.origin, CodeOrigin::CallSite(_))
431        })
432        .and_then(|containing| containing.translate(span));
433
434    let mut matched = intersecting_mappings()
435        .filter(|mapping| matches!(mapping.origin, CodeOrigin::Span(_)))
436        .collect::<Vec<_>>();
437
438    // If no mappings intersect with the span, translation is impossible.
439    if matched.is_empty() {
440        return None;
441    }
442
443    // Take the first mapping to the left.
444    matched.sort_by_key(|mapping| mapping.span);
445    let (first, matched) = matched.split_first().expect("non-empty vec always has first element");
446
447    // Find the last mapping which consecutively follows the first one.
448    // Note that all spans here intersect with the given one.
449    let mut last = first;
450    for mapping in matched {
451        if mapping.span.start > last.span.end {
452            break;
453        }
454
455        let mapping_origin =
456            mapping.origin.as_span().expect("mappings with start origin should be filtered out");
457        let last_origin =
458            last.origin.as_span().expect("mappings with start origin should be filtered out");
459        // Make sure, the origins are consecutive.
460        if mapping_origin.start < last_origin.end {
461            break;
462        }
463
464        last = mapping;
465    }
466
467    // We construct new span from the first and last mappings.
468    // If the new span does not contain the original span, there is no translation.
469    let constructed_span = TextSpan { start: first.span.start, end: last.span.end };
470    if !constructed_span.contains(span) {
471        return call_site;
472    }
473
474    // We use the boundaries of the first and last mappings to calculate new span origin.
475    let start = match first.origin {
476        CodeOrigin::Start(origin_start) => origin_start.add_width(span.start - first.span.start),
477        CodeOrigin::Span(span) => span.start,
478        CodeOrigin::CallSite(span) => span.start,
479    };
480
481    let end = match last.origin {
482        CodeOrigin::Start(_) => start.add_width(span.width()),
483        CodeOrigin::Span(span) => span.end,
484        CodeOrigin::CallSite(span) => span.start,
485    };
486
487    Some(TextSpan { start, end })
488}
489
490/// Returns the parent file and the code mappings of the file.
491fn get_parent_and_mapping(
492    db: &dyn FilesGroup,
493    file_id: FileId,
494) -> Option<(FileId, Arc<[CodeMapping]>)> {
495    let vf = match file_id.lookup_intern(db) {
496        FileLongId::OnDisk(_) => return None,
497        FileLongId::Virtual(vf) => vf,
498        FileLongId::External(id) => db.ext_as_virtual(id),
499    };
500    Some((vf.parent?, vf.code_mappings))
501}