1use std::collections::BTreeMap;
2use std::fs;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use cairo_lang_utils::LookupIntern;
7use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
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#[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#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct CrateConfiguration {
50    pub root: Directory,
52    pub settings: CrateSettings,
53    pub cache_file: Option<BlobId>,
54}
55impl CrateConfiguration {
56    pub fn default_for_root(root: Directory) -> Self {
58        Self { root, settings: CrateSettings::default(), cache_file: None }
59    }
60}
61
62#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
64pub struct CrateSettings {
65    pub name: Option<SmolStr>,
68    pub edition: Edition,
70    pub version: Option<Version>,
82    pub cfg_set: Option<CfgSet>,
84    #[serde(default)]
86    pub dependencies: BTreeMap<String, DependencySettings>,
87
88    #[serde(default)]
89    pub experimental_features: ExperimentalFeaturesConfig,
90}
91
92#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
100pub enum Edition {
101    #[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    pub const fn latest() -> Self {
118        Self::V2024_07
119    }
120
121    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    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#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
141pub struct DependencySettings {
142    pub discriminator: Option<SmolStr>,
149}
150
151#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
153pub struct ExperimentalFeaturesConfig {
154    pub negative_impls: bool,
155    pub associated_item_constraints: bool,
157    #[serde(default)]
162    pub coupons: bool,
163    #[serde(default)]
165    pub user_defined_inline_macros: bool,
166}
167
168pub trait ExternalFiles {
170    fn ext_as_virtual(&self, external_id: salsa::InternId) -> VirtualFile {
172        self.try_ext_as_virtual(external_id).unwrap()
173    }
174
175    fn try_ext_as_virtual(&self, _external_id: salsa::InternId) -> Option<VirtualFile> {
177        panic!("Should not be called, unless specifically implemented!");
178    }
179}
180
181#[salsa::query_group(FilesDatabase)]
183pub trait FilesGroup: ExternalFiles {
184    #[salsa::interned]
185    fn intern_crate(&self, crt: CrateLongId) -> CrateId;
186    #[salsa::interned]
187    fn intern_file(&self, file: FileLongId) -> FileId;
188    #[salsa::interned]
189    fn intern_blob(&self, blob: BlobLongId) -> BlobId;
190    #[salsa::interned]
191    fn intern_flag(&self, flag: FlagLongId) -> FlagId;
192
193    #[salsa::input]
195    fn crate_configs(&self) -> Arc<OrderedHashMap<CrateId, CrateConfiguration>>;
196
197    #[salsa::input]
203    fn file_overrides(&self) -> Arc<OrderedHashMap<FileId, Arc<str>>>;
204
205    #[salsa::input]
208    fn flags(&self) -> Arc<OrderedHashMap<FlagId, Arc<Flag>>>;
209    #[salsa::input]
211    fn cfg_set(&self) -> Arc<CfgSet>;
212
213    fn crates(&self) -> Vec<CrateId>;
215    fn crate_config(&self, crate_id: CrateId) -> Option<CrateConfiguration>;
217
218    fn priv_raw_file_content(&self, file_id: FileId) -> Option<Arc<str>>;
220    fn file_content(&self, file_id: FileId) -> Option<Arc<str>>;
222    fn file_summary(&self, file_id: FileId) -> Option<Arc<FileSummary>>;
223
224    fn blob_content(&self, blob_id: BlobId) -> Option<Arc<[u8]>>;
226    fn get_flag(&self, id: FlagId) -> Option<Arc<Flag>>;
228}
229
230pub fn init_files_group(db: &mut (dyn FilesGroup + 'static)) {
231    db.set_file_overrides(Arc::new(OrderedHashMap::default()));
233    db.set_crate_configs(Arc::new(OrderedHashMap::default()));
234    db.set_flags(Arc::new(OrderedHashMap::default()));
235    db.set_cfg_set(Arc::new(CfgSet::new()));
236}
237
238pub fn init_dev_corelib(db: &mut (dyn FilesGroup + 'static), core_lib_dir: PathBuf) {
239    db.set_crate_config(
240        CrateId::core(db),
241        Some(CrateConfiguration {
242            root: Directory::Real(core_lib_dir),
243            settings: CrateSettings {
244                name: None,
245                edition: Edition::V2024_07,
246                version: Version::parse(CORELIB_VERSION).ok(),
247                cfg_set: Default::default(),
248                dependencies: Default::default(),
249                experimental_features: ExperimentalFeaturesConfig {
250                    negative_impls: true,
251                    associated_item_constraints: true,
252                    coupons: true,
253                    user_defined_inline_macros: true,
254                },
255            },
256            cache_file: None,
257        }),
258    );
259}
260
261pub trait FilesGroupEx: FilesGroup {
262    fn override_file_content(&mut self, file: FileId, content: Option<Arc<str>>) {
264        let mut overrides = self.file_overrides().as_ref().clone();
265        match content {
266            Some(content) => overrides.insert(file, content),
267            None => overrides.swap_remove(&file),
268        };
269        self.set_file_overrides(Arc::new(overrides));
270    }
271    fn set_crate_config(&mut self, crt: CrateId, root: Option<CrateConfiguration>) {
273        let mut crate_configs = self.crate_configs().as_ref().clone();
274        match root {
275            Some(root) => crate_configs.insert(crt, root),
276            None => crate_configs.swap_remove(&crt),
277        };
278        self.set_crate_configs(Arc::new(crate_configs));
279    }
280    fn set_flag(&mut self, id: FlagId, value: Option<Arc<Flag>>) {
282        let mut flags = self.flags().as_ref().clone();
283        match value {
284            Some(value) => flags.insert(id, value),
285            None => flags.swap_remove(&id),
286        };
287        self.set_flags(Arc::new(flags));
288    }
289    fn use_cfg(&mut self, cfg_set: &CfgSet) {
291        let existing = self.cfg_set();
292        let merged = existing.union(cfg_set);
293        self.set_cfg_set(Arc::new(merged));
294    }
295}
296impl<T: FilesGroup + ?Sized> FilesGroupEx for T {}
297
298fn crates(db: &dyn FilesGroup) -> Vec<CrateId> {
299    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, cache_file } => {
306            Some(CrateConfiguration {
307                root: Directory::Virtual {
308                    files: BTreeMap::from([("lib.cairo".into(), file_id)]),
309                    dirs: Default::default(),
310                },
311                settings: toml::from_str(&settings)
312                    .expect("Failed to parse virtual crate settings."),
313                cache_file,
314            })
315        }
316    }
317}
318
319fn priv_raw_file_content(db: &dyn FilesGroup, file: FileId) -> Option<Arc<str>> {
320    match file.lookup_intern(db) {
321        FileLongId::OnDisk(path) => {
322            db.salsa_runtime().report_synthetic_read(Durability::LOW);
325
326            match fs::read_to_string(path) {
327                Ok(content) => Some(content.into()),
328                Err(_) => None,
329            }
330        }
331        FileLongId::Virtual(virt) => Some(virt.content),
332        FileLongId::External(external_id) => Some(db.ext_as_virtual(external_id).content),
333    }
334}
335fn file_content(db: &dyn FilesGroup, file: FileId) -> Option<Arc<str>> {
336    let overrides = db.file_overrides();
337    overrides.get(&file).cloned().or_else(|| db.priv_raw_file_content(file))
338}
339fn file_summary(db: &dyn FilesGroup, file: FileId) -> Option<Arc<FileSummary>> {
340    let content = db.file_content(file)?;
341    let mut line_offsets = vec![TextOffset::START];
342    let mut offset = TextOffset::START;
343    for ch in content.chars() {
344        offset = offset.add_width(TextWidth::from_char(ch));
345        if ch == '\n' {
346            line_offsets.push(offset);
347        }
348    }
349    Some(Arc::new(FileSummary { line_offsets, last_offset: offset }))
350}
351fn get_flag(db: &dyn FilesGroup, id: FlagId) -> Option<Arc<Flag>> {
352    db.flags().get(&id).cloned()
353}
354
355fn blob_content(db: &dyn FilesGroup, blob: BlobId) -> Option<Arc<[u8]>> {
356    match blob.lookup_intern(db) {
357        BlobLongId::OnDisk(path) => {
358            db.salsa_runtime().report_synthetic_read(Durability::LOW);
361
362            match fs::read(path) {
363                Ok(content) => Some(content.into()),
364                Err(_) => None,
365            }
366        }
367        BlobLongId::Virtual(content) => Some(content),
368    }
369}
370
371pub fn get_originating_location(
373    db: &dyn FilesGroup,
374    mut file_id: FileId,
375    mut span: TextSpan,
376    mut parent_files: Option<&mut Vec<FileId>>,
377) -> (FileId, TextSpan) {
378    if let Some(ref mut parent_files) = parent_files {
379        parent_files.push(file_id);
380    }
381    while let Some((parent, code_mappings)) = get_parent_and_mapping(db, file_id) {
382        if let Some(origin) = translate_location(&code_mappings, span) {
383            span = origin;
384            file_id = parent;
385            if let Some(ref mut parent_files) = parent_files {
386                parent_files.push(file_id);
387            }
388        } else {
389            break;
390        }
391    }
392    (file_id, span)
393}
394
395pub fn translate_location(code_mapping: &[CodeMapping], span: TextSpan) -> Option<TextSpan> {
405    if let Some(containing) = code_mapping.iter().find(|mapping| {
407        mapping.span.contains(span) && !matches!(mapping.origin, CodeOrigin::CallSite(_))
408    }) {
409        return containing.translate(span);
411    }
412
413    let intersecting_mappings = || {
415        code_mapping.iter().filter(|mapping| {
416            mapping.span.end > span.start && mapping.span.start < span.end
418        })
419    };
420
421    let call_site = intersecting_mappings()
423        .find(|mapping| {
424            mapping.span.contains(span) && matches!(mapping.origin, CodeOrigin::CallSite(_))
425        })
426        .and_then(|containing| containing.translate(span));
427
428    let mut matched = intersecting_mappings()
429        .filter(|mapping| matches!(mapping.origin, CodeOrigin::Span(_)))
430        .collect::<Vec<_>>();
431
432    if matched.is_empty() {
434        return call_site;
435    }
436
437    matched.sort_by_key(|mapping| mapping.span);
439    let (first, matched) = matched.split_first().expect("non-empty vec always has first element");
440
441    let mut last = first;
444    for mapping in matched {
445        if mapping.span.start > last.span.end {
446            break;
447        }
448
449        let mapping_origin =
450            mapping.origin.as_span().expect("mappings with start origin should be filtered out");
451        let last_origin =
452            last.origin.as_span().expect("mappings with start origin should be filtered out");
453        if mapping_origin.start > last_origin.end {
455            break;
456        }
457
458        last = mapping;
459    }
460
461    let constructed_span = TextSpan { start: first.span.start, end: last.span.end };
464    if !constructed_span.contains(span) {
465        return call_site;
466    }
467
468    let start = match first.origin {
470        CodeOrigin::Start(origin_start) => origin_start.add_width(span.start - first.span.start),
471        CodeOrigin::Span(span) => span.start,
472        CodeOrigin::CallSite(span) => span.start,
473    };
474
475    let end = match last.origin {
476        CodeOrigin::Start(_) => start.add_width(span.width()),
477        CodeOrigin::Span(span) => span.end,
478        CodeOrigin::CallSite(span) => span.start,
479    };
480
481    Some(TextSpan { start, end })
482}
483
484pub fn get_parent_and_mapping(
486    db: &dyn FilesGroup,
487    file_id: FileId,
488) -> Option<(FileId, Arc<[CodeMapping]>)> {
489    let vf = match file_id.lookup_intern(db) {
490        FileLongId::OnDisk(_) => return None,
491        FileLongId::Virtual(vf) => vf,
492        FileLongId::External(id) => db.ext_as_virtual(id),
493    };
494    Some((vf.parent?, vf.code_mappings))
495}