1use std::collections::BTreeMap;
2use std::fs;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use cairo_lang_utils::Intern;
7use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
8use salsa::{Database, Setter};
9use semver::Version;
10use serde::{Deserialize, Serialize};
11use smol_str::SmolStr;
12
13use crate::cfg::CfgSet;
14use crate::flag::Flag;
15use crate::ids::{
16 ArcStr, BlobId, BlobLongId, CodeMapping, CodeOrigin, CrateId, CrateInput, CrateLongId,
17 Directory, DirectoryInput, FileId, FileInput, FileLongId, FlagId, FlagLongId, SmolStrId,
18 Tracked, VirtualFile,
19};
20use crate::span::{FileSummary, TextOffset, TextSpan, TextWidth};
21
22#[cfg(test)]
23#[path = "db_test.rs"]
24mod test;
25
26pub const CORELIB_CRATE_NAME: &str = "core";
27pub const CORELIB_VERSION: &str = env!("CARGO_PKG_VERSION");
28
29#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
34pub struct CrateIdentifier(String);
35
36impl<T: ToString> From<T> for CrateIdentifier {
37 fn from(value: T) -> Self {
38 Self(value.to_string())
39 }
40}
41
42impl From<CrateIdentifier> for String {
43 fn from(value: CrateIdentifier) -> Self {
44 value.0
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct CrateConfigurationInput {
52 pub root: DirectoryInput,
53 pub settings: CrateSettings,
54 pub cache_file: Option<BlobLongId>,
55}
56
57impl CrateConfigurationInput {
58 pub fn into_crate_configuration(self, db: &dyn Database) -> CrateConfiguration<'_> {
60 CrateConfiguration {
61 root: self.root.into_directory(db),
62 settings: self.settings,
63 cache_file: self.cache_file.map(|blob_long_id| blob_long_id.intern(db)),
64 }
65 }
66}
67
68#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
70pub struct CrateConfiguration<'db> {
71 pub root: Directory<'db>,
73 pub settings: CrateSettings,
74 pub cache_file: Option<BlobId<'db>>,
75}
76impl<'db> CrateConfiguration<'db> {
77 pub fn default_for_root(root: Directory<'db>) -> Self {
79 Self { root, settings: CrateSettings::default(), cache_file: None }
80 }
81
82 pub fn into_crate_configuration_input(self, db: &dyn Database) -> CrateConfigurationInput {
84 CrateConfigurationInput {
85 root: self.root.into_directory_input(db),
86 settings: self.settings,
87 cache_file: self.cache_file.map(|blob_id| blob_id.long(db).clone()),
88 }
89 }
90}
91
92#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub struct CrateSettings {
95 pub name: Option<String>,
98 pub edition: Edition,
100 pub version: Option<Version>,
112 pub cfg_set: Option<CfgSet>,
114 #[serde(default)]
116 pub dependencies: BTreeMap<String, DependencySettings>,
117
118 #[serde(default)]
119 pub experimental_features: ExperimentalFeaturesConfig,
120}
121
122#[salsa::tracked(returns(ref))]
125pub fn default_crate_settings<'db>(_db: &'db dyn Database) -> CrateSettings {
126 CrateSettings::default()
127}
128
129#[derive(
137 Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize, salsa::Update,
138)]
139pub enum Edition {
140 #[default]
142 #[serde(rename = "2023_01")]
143 V2023_01,
144 #[serde(rename = "2023_10")]
145 V2023_10,
146 #[serde(rename = "2023_11")]
147 V2023_11,
148 #[serde(rename = "2024_07")]
149 V2024_07,
150}
151impl Edition {
152 pub const fn latest() -> Self {
157 Self::V2024_07
158 }
159
160 pub fn prelude_submodule_name<'db>(&self, db: &'db dyn Database) -> SmolStrId<'db> {
162 SmolStrId::from(
163 db,
164 match self {
165 Self::V2023_01 => "v2023_01",
166 Self::V2023_10 | Self::V2023_11 => "v2023_10",
167 Self::V2024_07 => "v2024_07",
168 },
169 )
170 }
171
172 pub fn ignore_visibility(&self) -> bool {
174 match self {
175 Self::V2023_01 | Self::V2023_10 => true,
176 Self::V2023_11 | Self::V2024_07 => false,
177 }
178 }
179}
180
181#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
183pub struct DependencySettings {
184 pub discriminator: Option<String>,
191}
192
193#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
195pub struct ExperimentalFeaturesConfig {
196 pub negative_impls: bool,
197 pub associated_item_constraints: bool,
199 #[serde(default)]
204 pub coupons: bool,
205 #[serde(default)]
207 pub user_defined_inline_macros: bool,
208}
209
210pub type ExtAsVirtual =
212 Arc<dyn for<'a> Fn(&'a dyn Database, salsa::Id) -> &'a VirtualFile<'a> + Send + Sync>;
213
214#[salsa::input]
215pub struct FilesGroupInput {
218 #[returns(ref)]
220 pub crate_configs: Option<OrderedHashMap<CrateInput, CrateConfigurationInput>>,
221 #[returns(ref)]
223 pub file_overrides: Option<OrderedHashMap<FileInput, Arc<str>>>,
224 #[returns(ref)]
227 pub flags: Option<OrderedHashMap<FlagLongId, Arc<Flag>>>,
228 #[returns(ref)]
230 pub cfg_set: Option<CfgSet>,
231 #[returns(ref)]
232 pub ext_as_virtual_obj: Option<ExtAsVirtual>,
233}
234
235#[salsa::tracked]
236pub fn files_group_input(db: &dyn Database) -> FilesGroupInput {
237 FilesGroupInput::new(db, None, None, None, None, None)
238}
239
240pub trait FilesGroup: Database {
242 fn crate_configs<'db>(&'db self) -> &'db OrderedHashMap<CrateId<'db>, CrateConfiguration<'db>> {
244 crate_configs(self.as_dyn_database())
245 }
246
247 fn file_overrides<'db>(&'db self) -> &'db OrderedHashMap<FileId<'db>, ArcStr> {
249 file_overrides(self.as_dyn_database())
250 }
251
252 fn flags<'db>(&'db self) -> &'db OrderedHashMap<FlagId<'db>, Arc<Flag>> {
254 flags(self.as_dyn_database())
255 }
256
257 fn crates<'db>(&'db self) -> &'db [CrateId<'db>] {
259 crates(self.as_dyn_database())
260 }
261
262 fn crate_config<'db>(
264 &'db self,
265 crate_id: CrateId<'db>,
266 ) -> Option<&'db CrateConfiguration<'db>> {
267 crate_config(self.as_dyn_database(), crate_id)
268 }
269
270 fn file_content<'db>(&'db self, file_id: FileId<'db>) -> Option<&'db str> {
272 file_content(self.as_dyn_database(), file_id).as_ref().map(|content| content.as_ref())
273 }
274
275 fn file_summary<'db>(&'db self, file_id: FileId<'db>) -> Option<&'db FileSummary> {
276 file_summary(self.as_dyn_database(), file_id)
277 }
278
279 fn blob_content<'db>(&'db self, blob_id: BlobId<'db>) -> Option<&'db [u8]> {
281 blob_content(self.as_dyn_database(), blob_id)
282 }
283 fn get_flag<'db>(&'db self, id: FlagId<'db>) -> Option<&'db Flag> {
285 get_flag(self.as_dyn_database(), id)
286 }
287
288 fn file_input<'db>(&'db self, file_id: FileId<'db>) -> &'db FileInput {
290 file_input(self.as_dyn_database(), file_id)
291 }
292
293 fn crate_input<'db>(&'db self, crt: CrateId<'db>) -> &'db CrateInput {
295 crate_input(self.as_dyn_database(), crt)
296 }
297
298 fn set_flag(&mut self, flag: FlagLongId, value: Option<Arc<Flag>>) {
300 let db_ref = self.as_dyn_database();
301 let mut flags = files_group_input(db_ref).flags(db_ref).clone().unwrap();
302 match value {
303 Some(value) => flags.insert(flag, value),
304 None => flags.swap_remove(&flag),
305 };
306 files_group_input(db_ref).set_flags(self).to(Some(flags));
307 }
308
309 fn use_cfg(&mut self, cfg_set: &CfgSet) {
311 let db_ref = self.as_dyn_database();
312 let existing = cfg_set_helper(db_ref);
313 let merged = existing.union(cfg_set);
314 files_group_input(db_ref).set_cfg_set(self).to(Some(merged));
315 }
316
317 fn cfg_set(&self) -> &CfgSet {
319 cfg_set_helper(self.as_dyn_database())
320 }
321}
322
323impl<T: Database + ?Sized> FilesGroup for T {}
324
325pub fn init_files_group<'db>(db: &mut (dyn Database + 'db)) {
326 let inp = files_group_input(db);
328 inp.set_file_overrides(db).to(Some(Default::default()));
329 inp.set_crate_configs(db).to(Some(Default::default()));
330 inp.set_flags(db).to(Some(Default::default()));
331 inp.set_cfg_set(db).to(Some(Default::default()));
332}
333
334pub fn set_crate_configs_input(
335 db: &mut dyn Database,
336 crate_configs: Option<OrderedHashMap<CrateInput, CrateConfigurationInput>>,
337) {
338 files_group_input(db).set_crate_configs(db).to(crate_configs);
339}
340
341#[salsa::tracked(returns(ref))]
342pub fn file_overrides<'db>(db: &'db dyn Database) -> OrderedHashMap<FileId<'db>, ArcStr> {
343 let inp = files_group_input(db).file_overrides(db).as_ref().expect("file_overrides is not set");
344 inp.iter()
345 .map(|(file_id, content)| {
346 (file_id.clone().into_file_long_id(db).intern(db), ArcStr::new(content.clone()))
347 })
348 .collect()
349}
350
351#[salsa::tracked(returns(ref))]
352pub fn crate_configs<'db>(
353 db: &'db dyn Database,
354) -> OrderedHashMap<CrateId<'db>, CrateConfiguration<'db>> {
355 let inp = files_group_input(db).crate_configs(db).as_ref().expect("crate_configs is not set");
356 inp.iter()
357 .map(|(crate_input, config)| {
358 (
359 crate_input.clone().into_crate_long_id(db).intern(db),
360 config.clone().into_crate_configuration(db),
361 )
362 })
363 .collect()
364}
365
366#[salsa::tracked(returns(ref))]
367pub fn flags<'db>(db: &'db dyn Database) -> OrderedHashMap<FlagId<'db>, Arc<Flag>> {
368 let inp = files_group_input(db).flags(db).as_ref().expect("flags is not set");
369 inp.iter().map(|(flag_id, flag)| (flag_id.clone().intern(db), flag.clone())).collect()
370}
371
372#[salsa::tracked(returns(ref))]
373fn file_input(db: &dyn Database, file_id: FileId<'_>) -> FileInput {
374 file_id.long(db).into_file_input(db)
375}
376
377#[salsa::tracked(returns(ref))]
378fn crate_input(db: &dyn Database, crt: CrateId<'_>) -> CrateInput {
379 crt.long(db).clone().into_crate_input(db)
380}
381
382#[salsa::tracked(returns(ref))]
383fn crate_configuration_input_helper(
384 db: &dyn Database,
385 _tracked: Tracked,
386 config: CrateConfiguration<'_>,
387) -> CrateConfigurationInput {
388 config.clone().into_crate_configuration_input(db)
389}
390
391fn crate_configuration_input<'db>(
392 db: &'db dyn Database,
393 config: CrateConfiguration<'db>,
394) -> &'db CrateConfigurationInput {
395 crate_configuration_input_helper(db, (), config)
396}
397
398pub fn init_dev_corelib(db: &mut dyn salsa::Database, core_lib_dir: PathBuf) {
399 let core = CrateLongId::core(db).intern(db);
400 let root = CrateConfiguration {
401 root: Directory::Real(core_lib_dir),
402 settings: CrateSettings {
403 name: None,
404 edition: Edition::V2024_07,
405 version: Version::parse(CORELIB_VERSION).ok(),
406 cfg_set: Default::default(),
407 dependencies: Default::default(),
408 experimental_features: ExperimentalFeaturesConfig {
409 negative_impls: true,
410 associated_item_constraints: true,
411 coupons: true,
412 user_defined_inline_macros: true,
413 },
414 },
415 cache_file: None,
416 };
417 let crate_configs = update_crate_configuration_input_helper(db, core, Some(root));
418 set_crate_configs_input(db, Some(crate_configs));
419}
420
421pub fn update_crate_configuration_input_helper(
423 db: &dyn Database,
424 crt: CrateId<'_>,
425 root: Option<CrateConfiguration<'_>>,
426) -> OrderedHashMap<CrateInput, CrateConfigurationInput> {
427 let crt = db.crate_input(crt);
428 let db_ref: &dyn Database = db;
429 let mut crate_configs = files_group_input(db_ref).crate_configs(db_ref).clone().unwrap();
430 match root {
431 Some(root) => crate_configs.insert(crt.clone(), db.crate_configuration_input(root).clone()),
432 None => crate_configs.swap_remove(crt),
433 };
434 crate_configs
435}
436
437#[macro_export]
439macro_rules! set_crate_config {
440 ($self:expr, $crt:expr, $root:expr) => {
441 let crate_configs = $crate::db::update_crate_configuration_input_helper($self, $crt, $root);
442 $crate::db::set_crate_configs_input($self, Some(crate_configs));
443 };
444}
445
446pub fn update_file_overrides_input_helper(
448 db: &dyn Database,
449 file: FileInput,
450 content: Option<Arc<str>>,
451) -> OrderedHashMap<FileInput, Arc<str>> {
452 let db_ref: &dyn Database = db;
453 let mut overrides = files_group_input(db_ref).file_overrides(db_ref).clone().unwrap();
454 match content {
455 Some(content) => overrides.insert(file.clone(), content),
456 None => overrides.swap_remove(&file),
457 };
458 overrides
459}
460
461#[macro_export]
463macro_rules! override_file_content {
464 ($self:expr, $file:expr, $content:expr) => {
465 let file = $self.file_input($file).clone();
466 let overrides = $crate::db::update_file_overrides_input_helper($self, file, $content);
467 salsa::Setter::to(
468 $crate::db::files_group_input($self).set_file_overrides($self),
469 Some(overrides),
470 );
471 };
472}
473
474fn cfg_set_helper(db: &dyn Database) -> &CfgSet {
475 files_group_input(db).cfg_set(db).as_ref().expect("cfg_set is not set")
476}
477
478#[salsa::tracked(returns(ref))]
479fn crates<'db>(db: &'db dyn Database) -> Vec<CrateId<'db>> {
480 db.crate_configs().keys().copied().collect()
482}
483
484#[salsa::tracked(returns(ref))]
486fn crate_config_helper<'db>(
487 db: &'db dyn Database,
488 crt: CrateId<'db>,
489) -> Option<CrateConfiguration<'db>> {
490 match crt.long(db) {
491 CrateLongId::Real { .. } => db.crate_configs().get(&crt).cloned(),
492 CrateLongId::Virtual { name: _, file_id, settings, cache_file } => {
493 Some(CrateConfiguration {
494 root: Directory::Virtual {
495 files: BTreeMap::from([("lib.cairo".to_string(), *file_id)]),
496 dirs: Default::default(),
497 },
498 settings: toml::from_str(settings)
499 .expect("Failed to parse virtual crate settings."),
500 cache_file: *cache_file,
501 })
502 }
503 }
504}
505
506fn crate_config<'db>(
510 db: &'db dyn Database,
511 crt: CrateId<'db>,
512) -> Option<&'db CrateConfiguration<'db>> {
513 crate_config_helper(db, crt).as_ref()
514}
515
516#[salsa::tracked]
517fn priv_raw_file_content<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<SmolStrId<'db>> {
518 match file.long(db) {
519 FileLongId::OnDisk(path) => {
520 db.report_untracked_read();
523
524 match fs::read_to_string(path) {
525 Ok(content) => Some(SmolStrId::new(db, SmolStr::new(content))),
526 Err(_) => None,
527 }
528 }
529 FileLongId::Virtual(virt) => Some(virt.content),
530 FileLongId::External(external_id) => Some(ext_as_virtual(db, *external_id).content),
531 }
532}
533
534#[salsa::tracked(returns(ref))]
536fn file_summary_helper<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<FileSummary> {
537 let content = db.file_content(file)?;
538 let mut line_offsets = vec![TextOffset::START];
539 let mut offset = TextOffset::START;
540 for ch in content.chars() {
541 offset = offset.add_width(TextWidth::from_char(ch));
542 if ch == '\n' {
543 line_offsets.push(offset);
544 }
545 }
546 Some(FileSummary { line_offsets, last_offset: offset })
547}
548
549#[salsa::tracked(returns(ref))]
551fn file_content<'db>(db: &'db dyn Database, file_id: FileId<'db>) -> Option<Arc<str>> {
552 let overrides = db.file_overrides();
553 overrides.get(&file_id).map(|content| (**content).clone()).or_else(|| {
554 priv_raw_file_content(db, file_id).map(|content| content.long(db).clone().into())
555 })
556}
557
558fn file_summary<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<&'db FileSummary> {
562 file_summary_helper(db, file).as_ref()
563}
564
565#[salsa::tracked(returns(ref))]
567fn get_flag_helper<'db>(db: &'db dyn Database, id: FlagId<'db>) -> Option<Arc<Flag>> {
568 db.flags().get(&id).cloned()
569}
570
571fn get_flag<'db>(db: &'db dyn Database, id: FlagId<'db>) -> Option<&'db Flag> {
574 db.flags().get(&id).map(|flag| flag.as_ref())
575}
576
577#[salsa::tracked(returns(ref))]
579fn blob_content_helper<'db>(db: &'db dyn Database, blob: BlobId<'db>) -> Option<Vec<u8>> {
580 blob.long(db).content()
581}
582
583fn blob_content<'db>(db: &'db dyn Database, blob: BlobId<'db>) -> Option<&'db [u8]> {
586 blob_content_helper(db, blob).as_ref().map(|content| content.as_slice())
587}
588
589pub fn get_originating_location<'db>(
591 db: &'db dyn Database,
592 mut file_id: FileId<'db>,
593 mut span: TextSpan,
594 mut parent_files: Option<&mut Vec<FileId<'db>>>,
595) -> (FileId<'db>, TextSpan) {
596 if let Some(ref mut parent_files) = parent_files {
597 parent_files.push(file_id);
598 }
599 while let Some((parent, code_mappings)) = get_parent_and_mapping(db, file_id) {
600 if let Some(origin) = translate_location(code_mappings, span) {
601 span = origin;
602 file_id = parent;
603 if let Some(ref mut parent_files) = parent_files {
604 parent_files.push(file_id);
605 }
606 } else {
607 break;
608 }
609 }
610 (file_id, span)
611}
612
613pub fn translate_location(code_mapping: &[CodeMapping], span: TextSpan) -> Option<TextSpan> {
623 if let Some(containing) = code_mapping.iter().find(|mapping| {
625 mapping.span.contains(span) && !matches!(mapping.origin, CodeOrigin::CallSite(_))
626 }) {
627 return containing.translate(span);
629 }
630
631 let intersecting_mappings = || {
633 code_mapping.iter().filter(|mapping| {
634 mapping.span.end > span.start && mapping.span.start < span.end
636 })
637 };
638
639 let call_site = intersecting_mappings()
641 .find(|mapping| {
642 mapping.span.contains(span) && matches!(mapping.origin, CodeOrigin::CallSite(_))
643 })
644 .and_then(|containing| containing.translate(span));
645
646 let mut matched = intersecting_mappings()
647 .filter(|mapping| matches!(mapping.origin, CodeOrigin::Span(_)))
648 .collect::<Vec<_>>();
649
650 if matched.is_empty() {
652 return call_site;
653 }
654
655 matched.sort_by_key(|mapping| mapping.span);
657 let (first, matched) = matched.split_first().expect("non-empty vec always has first element");
658
659 let mut last = first;
662 for mapping in matched {
663 if mapping.span.start > last.span.end {
664 break;
665 }
666
667 let mapping_origin =
668 mapping.origin.as_span().expect("mappings with start origin should be filtered out");
669 let last_origin =
670 last.origin.as_span().expect("mappings with start origin should be filtered out");
671 if mapping_origin.start > last_origin.end {
673 break;
674 }
675
676 last = mapping;
677 }
678
679 let constructed_span = TextSpan::new(first.span.start, last.span.end);
682 if !constructed_span.contains(span) {
683 return call_site;
684 }
685
686 let start = match first.origin {
688 CodeOrigin::Start(origin_start) => origin_start.add_width(span.start - first.span.start),
689 CodeOrigin::Span(span) => span.start,
690 CodeOrigin::CallSite(span) => span.start,
691 };
692
693 let end = match last.origin {
694 CodeOrigin::Start(_) => start.add_width(span.width()),
695 CodeOrigin::Span(span) => span.end,
696 CodeOrigin::CallSite(span) => span.start,
697 };
698
699 Some(TextSpan::new(start, end))
700}
701
702pub fn get_parent_and_mapping<'db>(
704 db: &'db dyn Database,
705 file_id: FileId<'db>,
706) -> Option<(FileId<'db>, &'db [CodeMapping])> {
707 let vf = match file_id.long(db) {
708 FileLongId::OnDisk(_) => return None,
709 FileLongId::Virtual(vf) => vf,
710 FileLongId::External(id) => ext_as_virtual(db, *id),
711 };
712 Some((vf.parent?, &vf.code_mappings))
713}
714
715pub fn ext_as_virtual<'db>(db: &'db dyn Database, id: salsa::Id) -> &'db VirtualFile<'db> {
717 files_group_input(db)
718 .ext_as_virtual_obj(db)
719 .as_ref()
720 .expect("`ext_as_virtual` was not set as input.")(db, id)
721}
722
723trait PrivFilesGroup: Database {
725 fn crate_configuration_input<'db>(
727 &'db self,
728 config: CrateConfiguration<'db>,
729 ) -> &'db CrateConfigurationInput {
730 crate_configuration_input(self.as_dyn_database(), config)
731 }
732}
733
734impl<T: Database + ?Sized> PrivFilesGroup for T {}