cairo_lang_filesystem/
ids.rs

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