cairo_lang_filesystem/
ids.rs

1use core::fmt;
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use cairo_lang_debug::DebugWithDb;
7use cairo_lang_utils::{Intern, define_short_id};
8use itertools::Itertools;
9use path_clean::PathClean;
10use salsa::Database;
11use serde::{Deserialize, Serialize};
12use smol_str::SmolStr;
13
14use crate::db::{CORELIB_CRATE_NAME, ext_as_virtual, get_originating_location};
15use crate::location_marks::get_location_marks;
16use crate::span::{TextOffset, TextSpan};
17
18pub const CAIRO_FILE_EXTENSION: &str = "cairo";
19
20/// Same as `CrateLongId`, but without internal interning.
21/// This is used as salsa database inputs.
22#[derive(Clone, Debug, Hash, PartialEq, Eq)]
23pub enum CrateInput {
24    Real {
25        name: String,
26        discriminator: Option<String>,
27    },
28    Virtual {
29        name: String,
30        file_long_id: FileInput,
31        settings: String,
32        cache_file: Option<BlobLongId>,
33    },
34}
35
36impl CrateInput {
37    pub fn into_crate_long_id(self, db: &dyn Database) -> CrateLongId<'_> {
38        match self {
39            CrateInput::Real { name, discriminator } => {
40                CrateLongId::Real { name: SmolStrId::from(db, name), discriminator }
41            }
42            CrateInput::Virtual { name, file_long_id, settings, cache_file } => {
43                CrateLongId::Virtual {
44                    name: SmolStrId::from(db, name),
45                    file_id: file_long_id.into_file_long_id(db).intern(db),
46                    settings,
47                    cache_file: cache_file.map(|blob_long_id| blob_long_id.intern(db)),
48                }
49            }
50        }
51    }
52
53    pub fn into_crate_ids(
54        db: &dyn Database,
55        inputs: impl IntoIterator<Item = CrateInput>,
56    ) -> Vec<CrateId<'_>> {
57        inputs.into_iter().map(|input| input.into_crate_long_id(db).intern(db)).collect()
58    }
59}
60
61/// A crate is a standalone file tree representing a single compilation unit.
62#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)]
63pub enum CrateLongId<'db> {
64    /// A crate that appears in crate_roots(), and on the filesystem.
65    Real { name: SmolStrId<'db>, discriminator: Option<String> },
66    /// A virtual crate, not a part of the crate_roots(). Used mainly for tests.
67    Virtual {
68        name: SmolStrId<'db>,
69        file_id: FileId<'db>,
70        settings: String,
71        cache_file: Option<BlobId<'db>>,
72    },
73}
74impl<'db> CrateLongId<'db> {
75    pub fn name(&self) -> SmolStrId<'db> {
76        match self {
77            CrateLongId::Real { name, .. } | CrateLongId::Virtual { name, .. } => *name,
78        }
79    }
80
81    pub fn into_crate_input(self, db: &'db dyn Database) -> CrateInput {
82        match self {
83            CrateLongId::Real { name, discriminator } => {
84                CrateInput::Real { name: name.to_string(db), discriminator }
85            }
86            CrateLongId::Virtual { name, file_id, settings, cache_file } => CrateInput::Virtual {
87                name: name.to_string(db),
88                file_long_id: file_id.long(db).clone().into_file_input(db),
89                settings,
90                cache_file: cache_file.map(|blob_id| blob_id.long(db).clone()),
91            },
92        }
93    }
94
95    pub fn core(db: &'db dyn Database) -> Self {
96        CrateLongId::Real { name: SmolStrId::from(db, CORELIB_CRATE_NAME), discriminator: None }
97    }
98
99    pub fn plain(name: SmolStrId<'db>) -> Self {
100        CrateLongId::Real { name, discriminator: None }
101    }
102}
103define_short_id!(CrateId, CrateLongId<'db>);
104impl<'db> CrateId<'db> {
105    /// Gets the crate id for a real crate by name, without a discriminator.
106    pub fn plain(db: &'db dyn Database, name: SmolStrId<'db>) -> Self {
107        CrateId::new(db, CrateLongId::plain(name))
108    }
109
110    /// Gets the crate id for `core`.
111    pub fn core(db: &'db dyn Database) -> Self {
112        CrateId::new(db, CrateLongId::core(db))
113    }
114}
115
116/// A trait for getting the internal salsa::InternId of a short id object.
117///
118/// This id is unstable across runs and should not be used to anything that is externally visible.
119/// This is currently used to pick representative for strongly connected components.
120pub trait UnstableSalsaId {
121    fn get_internal_id(&self) -> salsa::Id;
122}
123impl UnstableSalsaId for CrateId<'_> {
124    fn get_internal_id(&self) -> salsa::Id {
125        self.0
126    }
127}
128
129/// The long ID for a compilation flag.
130#[derive(Clone, Debug, Hash, PartialEq, Eq)]
131pub struct FlagLongId(pub String);
132define_short_id!(FlagId, FlagLongId);
133
134/// Same as `FileLongId`, but without the interning inside virtual files.
135/// This is used to avoid the need to intern the file id inside salsa database inputs.
136#[derive(Clone, Debug, Hash, PartialEq, Eq)]
137pub enum FileInput {
138    OnDisk(PathBuf),
139    Virtual(VirtualFileInput),
140    External(salsa::Id),
141}
142
143impl FileInput {
144    pub fn into_file_long_id(self, db: &dyn Database) -> FileLongId<'_> {
145        match self {
146            FileInput::OnDisk(path) => FileLongId::OnDisk(path),
147            FileInput::Virtual(vf) => FileLongId::Virtual(vf.into_virtual_file(db)),
148            FileInput::External(id) => FileLongId::External(id),
149        }
150    }
151}
152
153/// We use a higher level FileId struct, because not all files are on disk. Some might be online.
154/// Some might be virtual/computed on demand.
155#[derive(Clone, Debug, Hash, PartialEq, Eq)]
156pub enum FileLongId<'db> {
157    OnDisk(PathBuf),
158    Virtual(VirtualFile<'db>),
159    External(salsa::Id),
160}
161/// Whether the file holds syntax for a module or for an expression.
162#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
163pub enum FileKind {
164    Module,
165    Expr,
166    StatementList,
167}
168
169/// A mapping for a code rewrite.
170#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
171pub struct CodeMapping {
172    pub span: TextSpan,
173    pub origin: CodeOrigin,
174}
175impl CodeMapping {
176    pub fn translate(&self, span: TextSpan) -> Option<TextSpan> {
177        if self.span.contains(span) {
178            Some(match self.origin {
179                CodeOrigin::Start(origin_start) => {
180                    let start = origin_start.add_width(span.start - self.span.start);
181                    TextSpan::new_with_width(start, span.width())
182                }
183                CodeOrigin::Span(span) => span,
184                CodeOrigin::CallSite(span) => span,
185            })
186        } else {
187            None
188        }
189    }
190}
191
192/// The origin of a code mapping.
193#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
194pub enum CodeOrigin {
195    /// The origin is a copied node starting at the given offset.
196    Start(TextOffset),
197    /// The origin was generated from this span, but there's no direct mapping.
198    Span(TextSpan),
199    /// The origin was generated because of this span, but no code has been copied.
200    /// E.g. a macro defined attribute on a function.
201    CallSite(TextSpan),
202}
203
204impl CodeOrigin {
205    pub fn as_span(&self) -> Option<TextSpan> {
206        match self {
207            CodeOrigin::Start(_) => None,
208            CodeOrigin::CallSite(_) => None,
209            CodeOrigin::Span(span) => Some(*span),
210        }
211    }
212
213    pub fn start(&self) -> TextOffset {
214        match self {
215            CodeOrigin::Start(start) => *start,
216            CodeOrigin::CallSite(span) => span.start,
217            CodeOrigin::Span(span) => span.start,
218        }
219    }
220}
221
222/// Same as `VirtualFile`, but without the interning inside virtual files.
223/// This is used to avoid the need to intern the file id inside salsa database inputs.
224#[derive(Clone, Debug, Hash, PartialEq, Eq)]
225pub struct VirtualFileInput {
226    pub parent: Option<(Arc<FileInput>, TextSpan)>,
227    pub name: String,
228    pub content: Arc<str>,
229    pub code_mappings: Arc<[CodeMapping]>,
230    pub kind: FileKind,
231    pub original_item_removed: bool,
232}
233
234impl VirtualFileInput {
235    fn into_virtual_file(self, db: &dyn Database) -> VirtualFile<'_> {
236        VirtualFile {
237            parent: self.parent.map(|(id, span)| SpanInFile {
238                file_id: id.as_ref().clone().into_file_long_id(db).intern(db),
239                span,
240            }),
241            name: SmolStrId::from(db, self.name),
242            content: SmolStrId::from(db, self.content),
243            code_mappings: self.code_mappings,
244            kind: self.kind,
245            original_item_removed: self.original_item_removed,
246        }
247    }
248}
249
250#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)]
251pub struct VirtualFile<'db> {
252    pub parent: Option<SpanInFile<'db>>,
253    pub name: SmolStrId<'db>,
254    pub content: SmolStrId<'db>,
255    pub code_mappings: Arc<[CodeMapping]>,
256    pub kind: FileKind,
257    /// Whether an original item was removed when this virtual file was created
258    /// Relevant only for virtual files created during macros expansion.
259    /// This field is used by `cairo-language-server` for optimization purposes.
260    pub original_item_removed: bool,
261}
262impl<'db> VirtualFile<'db> {
263    fn full_path(&self, db: &'db dyn Database) -> String {
264        if let Some(parent) = self.parent {
265            use std::fmt::Write;
266            let mut f = String::new();
267            parent.fmt_location(&mut f, db).unwrap();
268            write!(&mut f, "[{}]", self.name.long(db)).unwrap();
269            f
270        } else {
271            self.name.to_string(db)
272        }
273    }
274
275    fn into_virtual_file_input(self, db: &dyn Database) -> VirtualFileInput {
276        VirtualFileInput {
277            parent: self
278                .parent
279                .map(|loc| (Arc::new(loc.file_id.long(db).clone().into_file_input(db)), loc.span)),
280            name: self.name.to_string(db),
281            content: Arc::from(self.content.long(db).as_str()),
282            code_mappings: self.code_mappings,
283            kind: self.kind,
284            original_item_removed: self.original_item_removed,
285        }
286    }
287}
288
289impl<'db> FileLongId<'db> {
290    pub fn file_name(&self, db: &'db dyn Database) -> SmolStrId<'db> {
291        match self {
292            FileLongId::OnDisk(path) => SmolStrId::from(
293                db,
294                path.file_name().and_then(|x| x.to_str()).unwrap_or("<unknown>"),
295            ),
296            FileLongId::Virtual(vf) => vf.name,
297            FileLongId::External(external_id) => ext_as_virtual(db, *external_id).name,
298        }
299    }
300    pub fn full_path(&self, db: &'db dyn Database) -> String {
301        match self {
302            FileLongId::OnDisk(path) => path.to_str().unwrap_or("<unknown>").to_string(),
303            FileLongId::Virtual(vf) => vf.full_path(db),
304            FileLongId::External(external_id) => ext_as_virtual(db, *external_id).full_path(db),
305        }
306    }
307    pub fn kind(&self) -> FileKind {
308        match self {
309            FileLongId::OnDisk(_) => FileKind::Module,
310            FileLongId::Virtual(vf) => vf.kind,
311            FileLongId::External(_) => FileKind::Module,
312        }
313    }
314
315    pub fn into_file_input(&self, db: &dyn Database) -> FileInput {
316        match self {
317            FileLongId::OnDisk(path) => FileInput::OnDisk(path.clone()),
318            FileLongId::Virtual(vf) => FileInput::Virtual(vf.clone().into_virtual_file_input(db)),
319            FileLongId::External(id) => FileInput::External(*id),
320        }
321    }
322}
323
324define_short_id!(FileId, FileLongId<'db>);
325impl<'db> FileId<'db> {
326    pub fn new_on_disk(db: &'db dyn Database, path: PathBuf) -> FileId<'db> {
327        FileLongId::OnDisk(path.clean()).intern(db)
328    }
329
330    pub fn file_name(self, db: &'db dyn Database) -> SmolStrId<'db> {
331        self.long(db).file_name(db)
332    }
333
334    pub fn full_path(self, db: &dyn Database) -> String {
335        self.long(db).full_path(db)
336    }
337
338    pub fn kind(self, db: &dyn Database) -> FileKind {
339        self.long(db).kind()
340    }
341}
342
343/// Same as `Directory`, but without the interning inside virtual directories.
344/// This is used to avoid the need to intern the file id inside salsa database inputs.
345#[derive(Clone, Debug, Hash, PartialEq, Eq)]
346pub enum DirectoryInput {
347    Real(PathBuf),
348    Virtual { files: BTreeMap<String, FileInput>, dirs: BTreeMap<String, Box<DirectoryInput>> },
349}
350
351impl DirectoryInput {
352    /// Converts the input into a [`Directory`].
353    pub fn into_directory(self, db: &dyn Database) -> Directory<'_> {
354        match self {
355            DirectoryInput::Real(path) => Directory::Real(path),
356            DirectoryInput::Virtual { files, dirs } => Directory::Virtual {
357                files: files
358                    .into_iter()
359                    .map(|(name, file_input)| (name, file_input.into_file_long_id(db).intern(db)))
360                    .collect(),
361                dirs: dirs
362                    .into_iter()
363                    .map(|(name, dir_input)| (name, Box::new(dir_input.into_directory(db))))
364                    .collect(),
365            },
366        }
367    }
368}
369
370#[derive(Clone, Hash, PartialEq, Eq)]
371pub struct ArcStr(Arc<str>);
372
373impl ArcStr {
374    pub fn new(s: Arc<str>) -> Self {
375        ArcStr(s)
376    }
377}
378
379impl std::ops::Deref for ArcStr {
380    type Target = Arc<str>;
381
382    fn deref(&self) -> &Self::Target {
383        &self.0
384    }
385}
386
387impl std::fmt::Display for ArcStr {
388    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389        self.0.fmt(f)
390    }
391}
392
393impl std::fmt::Debug for ArcStr {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        self.0.fmt(f)
396    }
397}
398
399unsafe impl salsa::Update for ArcStr {
400    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
401        let old_str: &mut Self = unsafe { &mut *old_pointer };
402
403        // Fast path: same allocation => unchanged.
404        if Arc::ptr_eq(&old_str.0, &new_value.0) {
405            return false;
406        }
407        // Content-equal => unchanged.
408        if old_str.0 == new_value.0 {
409            return false;
410        }
411        // Otherwise, replace the Arc.
412        *old_str = new_value;
413        true
414    }
415}
416
417define_short_id!(SmolStrId, SmolStr);
418
419pub trait DbJoin {
420    fn join(&self, db: &dyn Database, separator: &str) -> String;
421}
422
423impl<'db> DbJoin for Vec<SmolStrId<'db>> {
424    fn join(&self, db: &dyn Database, separator: &str) -> String {
425        self.iter().map(|id| id.long(db)).join(separator)
426    }
427}
428
429impl<'db> SmolStrId<'db> {
430    pub fn from(db: &'db dyn Database, content: impl Into<SmolStr>) -> Self {
431        SmolStrId::new(db, content.into())
432    }
433
434    pub fn from_arcstr(db: &'db dyn Database, content: &Arc<str>) -> Self {
435        SmolStrId::from(db, content.clone())
436    }
437
438    pub fn to_string(&self, db: &dyn Database) -> String {
439        self.long(db).to_string()
440    }
441}
442
443#[derive(Clone, Debug, Hash, PartialEq, Eq, salsa::Update)]
444pub enum Directory<'db> {
445    /// A directory on the file system.
446    Real(PathBuf),
447    /// A virtual directory, not on the file system. Used mainly for virtual crates.
448    Virtual { files: BTreeMap<String, FileId<'db>>, dirs: BTreeMap<String, Box<Directory<'db>>> },
449}
450
451impl<'db> Directory<'db> {
452    /// Returns a file inside this directory. The file and directory don't necessarily exist on
453    /// the file system. These are ids/paths to them.
454    pub fn file(&self, db: &'db dyn Database, name: &str) -> FileId<'db> {
455        match self {
456            Directory::Real(path) => FileId::new_on_disk(db, path.join(name)),
457            Directory::Virtual { files, dirs: _ } => files
458                .get(name)
459                .copied()
460                .unwrap_or_else(|| FileId::new_on_disk(db, PathBuf::from(name))),
461        }
462    }
463
464    /// Returns a sub directory inside this directory. These directories don't necessarily exist on
465    /// the file system. These are ids/paths to them.
466    pub fn subdir(&self, name: &'db str) -> Directory<'db> {
467        match self {
468            Directory::Real(path) => Directory::Real(path.join(name)),
469            Directory::Virtual { files: _, dirs } => {
470                if let Some(dir) = dirs.get(name) {
471                    dir.as_ref().clone()
472                } else {
473                    Directory::Virtual { files: BTreeMap::new(), dirs: BTreeMap::new() }
474                }
475            }
476        }
477    }
478
479    /// Converts the directory into an [`DirectoryInput`].
480    pub fn into_directory_input(self, db: &dyn Database) -> DirectoryInput {
481        match self {
482            Directory::Real(path) => DirectoryInput::Real(path),
483            Directory::Virtual { files, dirs } => DirectoryInput::Virtual {
484                files: files
485                    .into_iter()
486                    .map(|(name, file_id)| (name, file_id.long(db).clone().into_file_input(db)))
487                    .collect(),
488                dirs: dirs
489                    .into_iter()
490                    .map(|(name, dir)| (name, Box::new(dir.into_directory_input(db))))
491                    .collect(),
492            },
493        }
494    }
495}
496
497/// A FileId for data that is not necessarily a valid UTF-8 string.
498#[derive(Clone, Debug, Hash, PartialEq, Eq)]
499pub enum BlobLongId {
500    OnDisk(PathBuf),
501    Virtual(Vec<u8>),
502}
503
504impl BlobLongId {
505    pub fn content(&self) -> Option<Vec<u8>> {
506        match self {
507            BlobLongId::OnDisk(path) => std::fs::read(path).ok(),
508            BlobLongId::Virtual(content) => Some(content.clone()),
509        }
510    }
511}
512
513define_short_id!(BlobId, BlobLongId);
514
515impl<'db> BlobId<'db> {
516    pub fn new_on_disk(db: &'db (dyn salsa::Database + 'db), path: PathBuf) -> Self {
517        BlobId::new(db, BlobLongId::OnDisk(path.clean()))
518    }
519}
520
521/// A location within a file.
522#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
523pub struct SpanInFile<'db> {
524    pub file_id: FileId<'db>,
525    pub span: TextSpan,
526}
527impl<'db> SpanInFile<'db> {
528    /// Get the location of right after this diagnostic's location (with width 0).
529    pub fn after(&self) -> Self {
530        Self { file_id: self.file_id, span: self.span.after() }
531    }
532    /// Get the location of the originating user code.
533    pub fn user_location(&self, db: &'db dyn Database) -> Self {
534        get_originating_location(db, *self, None)
535    }
536    /// Helper function to format the location of a diagnostic.
537    pub fn fmt_location(&self, f: &mut impl fmt::Write, db: &'db dyn Database) -> fmt::Result {
538        let file_path = self.file_id.long(db).full_path(db);
539        let start = match self.span.start.position_in_file(db, self.file_id) {
540            Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
541            None => "?".into(),
542        };
543
544        let end = match self.span.end.position_in_file(db, self.file_id) {
545            Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
546            None => "?".into(),
547        };
548        write!(f, "{file_path}:{start}: {end}")
549    }
550}
551impl<'db> DebugWithDb<'db> for SpanInFile<'db> {
552    type Db = dyn Database;
553
554    fn fmt(&self, f: &mut std::fmt::Formatter<'_>, db: &'db dyn Database) -> std::fmt::Result {
555        let file_path = self.file_id.long(db).full_path(db);
556        let mut marks = String::new();
557        let mut ending_pos = String::new();
558        let starting_pos = match self.span.start.position_in_file(db, self.file_id) {
559            Some(starting_text_pos) => {
560                if let Some(ending_text_pos) = self.span.end.position_in_file(db, self.file_id)
561                    && starting_text_pos.line != ending_text_pos.line
562                {
563                    ending_pos = format!("-{}:{}", ending_text_pos.line + 1, ending_text_pos.col);
564                }
565                marks = get_location_marks(db, self, true);
566                format!("{}:{}", starting_text_pos.line + 1, starting_text_pos.col + 1)
567            }
568            None => "?".into(),
569        };
570        write!(f, "{file_path}:{starting_pos}{ending_pos}\n{marks}")
571    }
572}
573
574/// A dummy type to be used as a tracked input.
575/// Used to avoid errors on StructInSalsaDB.
576/// Salsa expects the first parameter of a tracked function to be a Tracked type for performance
577/// reasons.
578pub type Tracked = ();