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