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, FlagLongId, SmolStrId, SpanInFile,
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 #[serde(rename = "2025_12")]
151 V2025_12,
152}
153impl Edition {
154 pub const fn latest() -> Self {
159 Self::V2025_12
160 }
161
162 pub fn prelude_submodule_name<'db>(&self, db: &'db dyn Database) -> SmolStrId<'db> {
164 SmolStrId::from(
165 db,
166 match self {
167 Self::V2023_01 => "v2023_01",
168 Self::V2023_10 | Self::V2023_11 => "v2023_10",
169 Self::V2024_07 | Self::V2025_12 => "v2024_07",
170 },
171 )
172 }
173
174 pub fn ignore_visibility(&self) -> bool {
176 match self {
177 Self::V2023_01 | Self::V2023_10 => true,
178 Self::V2023_11 | Self::V2024_07 | Self::V2025_12 => false,
179 }
180 }
181
182 pub fn member_access_desnaps(&self) -> bool {
184 match self {
185 Self::V2023_01 | Self::V2023_10 | Self::V2023_11 | Self::V2024_07 => false,
186 Self::V2025_12 => true,
187 }
188 }
189}
190
191#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
193pub struct DependencySettings {
194 pub discriminator: Option<String>,
201}
202
203#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
205pub struct ExperimentalFeaturesConfig {
206 pub negative_impls: bool,
207 pub associated_item_constraints: bool,
209 #[serde(default)]
214 pub coupons: bool,
215 #[serde(default)]
217 pub user_defined_inline_macros: bool,
218 #[serde(default)]
220 pub repr_ptrs: bool,
221}
222
223pub type ExtAsVirtual =
225 Arc<dyn for<'a> Fn(&'a dyn Database, salsa::Id) -> &'a VirtualFile<'a> + Send + Sync>;
226
227#[salsa::input]
228pub struct FilesGroupInput {
231 #[returns(ref)]
233 pub crate_configs: Option<OrderedHashMap<CrateInput, CrateConfigurationInput>>,
234 #[returns(ref)]
236 pub file_overrides: Option<OrderedHashMap<FileInput, Arc<str>>>,
237 #[returns(ref)]
240 pub flags: Option<OrderedHashMap<FlagLongId, Flag>>,
241 #[returns(ref)]
243 pub cfg_set: Option<CfgSet>,
244 #[returns(ref)]
245 pub ext_as_virtual_obj: Option<ExtAsVirtual>,
246}
247
248#[salsa::tracked]
249pub fn files_group_input(db: &dyn Database) -> FilesGroupInput {
250 FilesGroupInput::new(db, None, None, None, None, None)
251}
252
253pub trait FilesGroup: Database {
255 fn crate_configs<'db>(&'db self) -> &'db OrderedHashMap<CrateId<'db>, CrateConfiguration<'db>> {
257 crate_configs(self.as_dyn_database())
258 }
259
260 fn file_overrides<'db>(&'db self) -> &'db OrderedHashMap<FileId<'db>, ArcStr> {
262 file_overrides(self.as_dyn_database())
263 }
264
265 fn crates<'db>(&'db self) -> &'db [CrateId<'db>] {
267 crates(self.as_dyn_database())
268 }
269
270 fn crate_config<'db>(
272 &'db self,
273 crate_id: CrateId<'db>,
274 ) -> Option<&'db CrateConfiguration<'db>> {
275 crate_config(self.as_dyn_database(), crate_id)
276 }
277
278 fn file_content<'db>(&'db self, file_id: FileId<'db>) -> Option<&'db str> {
280 file_content(self.as_dyn_database(), file_id).as_ref().map(|content| content.as_ref())
281 }
282
283 fn file_summary<'db>(&'db self, file_id: FileId<'db>) -> Option<&'db FileSummary> {
284 file_summary(self.as_dyn_database(), file_id)
285 }
286
287 fn blob_content<'db>(&'db self, blob_id: BlobId<'db>) -> Option<&'db [u8]> {
289 blob_content(self.as_dyn_database(), blob_id)
290 }
291
292 fn file_input<'db>(&'db self, file_id: FileId<'db>) -> &'db FileInput {
294 file_input(self.as_dyn_database(), file_id)
295 }
296
297 fn crate_input<'db>(&'db self, crt: CrateId<'db>) -> &'db CrateInput {
299 crate_input(self.as_dyn_database(), crt)
300 }
301
302 fn use_cfg(&mut self, cfg_set: &CfgSet) {
304 let db_ref = self.as_dyn_database();
305 let existing = cfg_set_helper(db_ref);
306 let merged = existing.union(cfg_set);
307 files_group_input(db_ref).set_cfg_set(self).to(Some(merged));
308 }
309
310 fn cfg_set(&self) -> &CfgSet {
312 cfg_set_helper(self.as_dyn_database())
313 }
314}
315
316impl<T: Database + ?Sized> FilesGroup for T {}
317
318pub fn init_files_group<'db>(db: &mut (dyn Database + 'db)) {
319 let inp = files_group_input(db);
321 inp.set_file_overrides(db).to(Some(Default::default()));
322 inp.set_crate_configs(db).to(Some(Default::default()));
323 inp.set_flags(db).to(Some(Default::default()));
324 inp.set_cfg_set(db).to(Some(Default::default()));
325}
326
327pub fn set_crate_configs_input(
328 db: &mut dyn Database,
329 crate_configs: Option<OrderedHashMap<CrateInput, CrateConfigurationInput>>,
330) {
331 files_group_input(db).set_crate_configs(db).to(crate_configs);
332}
333
334#[salsa::tracked(returns(ref))]
335pub fn file_overrides<'db>(db: &'db dyn Database) -> OrderedHashMap<FileId<'db>, ArcStr> {
336 let inp = files_group_input(db).file_overrides(db).as_ref().expect("file_overrides is not set");
337 inp.iter()
338 .map(|(file_id, content)| {
339 (file_id.clone().into_file_long_id(db).intern(db), ArcStr::new(content.clone()))
340 })
341 .collect()
342}
343
344#[salsa::tracked(returns(ref))]
345pub fn crate_configs<'db>(
346 db: &'db dyn Database,
347) -> OrderedHashMap<CrateId<'db>, CrateConfiguration<'db>> {
348 let inp = files_group_input(db).crate_configs(db).as_ref().expect("crate_configs is not set");
349 inp.iter()
350 .map(|(crate_input, config)| {
351 (
352 crate_input.clone().into_crate_long_id(db).intern(db),
353 config.clone().into_crate_configuration(db),
354 )
355 })
356 .collect()
357}
358
359#[salsa::tracked(returns(ref))]
360fn file_input(db: &dyn Database, file_id: FileId<'_>) -> FileInput {
361 file_id.long(db).into_file_input(db)
362}
363
364#[salsa::tracked(returns(ref))]
365fn crate_input(db: &dyn Database, crt: CrateId<'_>) -> CrateInput {
366 crt.long(db).clone().into_crate_input(db)
367}
368
369#[salsa::tracked(returns(ref))]
370fn crate_configuration_input_helper(
371 db: &dyn Database,
372 _tracked: Tracked,
373 config: CrateConfiguration<'_>,
374) -> CrateConfigurationInput {
375 config.clone().into_crate_configuration_input(db)
376}
377
378fn crate_configuration_input<'db>(
379 db: &'db dyn Database,
380 config: CrateConfiguration<'db>,
381) -> &'db CrateConfigurationInput {
382 crate_configuration_input_helper(db, (), config)
383}
384
385pub fn init_dev_corelib(db: &mut dyn salsa::Database, core_lib_dir: PathBuf) {
386 let core = CrateLongId::core(db).intern(db);
387 let root = CrateConfiguration {
388 root: Directory::Real(core_lib_dir),
389 settings: CrateSettings {
390 name: None,
391 edition: Edition::V2025_12,
392 version: Version::parse(CORELIB_VERSION).ok(),
393 cfg_set: Default::default(),
394 dependencies: Default::default(),
395 experimental_features: ExperimentalFeaturesConfig {
396 negative_impls: true,
397 associated_item_constraints: true,
398 coupons: true,
399 user_defined_inline_macros: true,
400 repr_ptrs: true,
401 },
402 },
403 cache_file: None,
404 };
405 let crate_configs = update_crate_configuration_input_helper(db, core, Some(root));
406 set_crate_configs_input(db, Some(crate_configs));
407}
408
409pub fn update_crate_configuration_input_helper(
411 db: &dyn Database,
412 crt: CrateId<'_>,
413 root: Option<CrateConfiguration<'_>>,
414) -> OrderedHashMap<CrateInput, CrateConfigurationInput> {
415 let crt = db.crate_input(crt);
416 let db_ref: &dyn Database = db;
417 let mut crate_configs = files_group_input(db_ref).crate_configs(db_ref).clone().unwrap();
418 match root {
419 Some(root) => crate_configs.insert(crt.clone(), db.crate_configuration_input(root).clone()),
420 None => crate_configs.swap_remove(crt),
421 };
422 crate_configs
423}
424
425#[macro_export]
427macro_rules! set_crate_config {
428 ($self:expr, $crt:expr, $root:expr) => {
429 let crate_configs = $crate::db::update_crate_configuration_input_helper($self, $crt, $root);
430 $crate::db::set_crate_configs_input($self, Some(crate_configs));
431 };
432}
433
434pub fn update_file_overrides_input_helper(
436 db: &dyn Database,
437 file: FileInput,
438 content: Option<Arc<str>>,
439) -> OrderedHashMap<FileInput, Arc<str>> {
440 let db_ref: &dyn Database = db;
441 let mut overrides = files_group_input(db_ref).file_overrides(db_ref).clone().unwrap();
442 match content {
443 Some(content) => overrides.insert(file, content),
444 None => overrides.swap_remove(&file),
445 };
446 overrides
447}
448
449#[macro_export]
451macro_rules! override_file_content {
452 ($self:expr, $file:expr, $content:expr) => {
453 let file = $self.file_input($file).clone();
454 let overrides = $crate::db::update_file_overrides_input_helper($self, file, $content);
455 salsa::Setter::to(
456 $crate::db::files_group_input($self).set_file_overrides($self),
457 Some(overrides),
458 );
459 };
460}
461
462fn cfg_set_helper(db: &dyn Database) -> &CfgSet {
463 files_group_input(db).cfg_set(db).as_ref().expect("cfg_set is not set")
464}
465
466#[salsa::tracked(returns(ref))]
467fn crates<'db>(db: &'db dyn Database) -> Vec<CrateId<'db>> {
468 db.crate_configs().keys().copied().collect()
470}
471
472#[salsa::tracked(returns(ref))]
474fn crate_config_helper<'db>(
475 db: &'db dyn Database,
476 crt: CrateId<'db>,
477) -> Option<CrateConfiguration<'db>> {
478 match crt.long(db) {
479 CrateLongId::Real { .. } => db.crate_configs().get(&crt).cloned(),
480 CrateLongId::Virtual { name: _, file_id, settings, cache_file } => {
481 Some(CrateConfiguration {
482 root: Directory::Virtual {
483 files: BTreeMap::from([("lib.cairo".to_string(), *file_id)]),
484 dirs: Default::default(),
485 },
486 settings: toml::from_str(settings)
487 .expect("Failed to parse virtual crate settings."),
488 cache_file: *cache_file,
489 })
490 }
491 }
492}
493
494fn crate_config<'db>(
498 db: &'db dyn Database,
499 crt: CrateId<'db>,
500) -> Option<&'db CrateConfiguration<'db>> {
501 crate_config_helper(db, crt).as_ref()
502}
503
504#[salsa::tracked]
505fn priv_raw_file_content<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<SmolStrId<'db>> {
506 match file.long(db) {
507 FileLongId::OnDisk(path) => {
508 db.report_untracked_read();
511
512 match fs::read_to_string(path) {
513 Ok(content) => Some(SmolStrId::new(db, SmolStr::new(content))),
514 Err(_) => None,
515 }
516 }
517 FileLongId::Virtual(virt) => Some(virt.content),
518 FileLongId::External(external_id) => Some(ext_as_virtual(db, *external_id).content),
519 }
520}
521
522#[salsa::tracked(returns(ref))]
524fn file_summary_helper<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<FileSummary> {
525 let content = db.file_content(file)?;
526 let mut line_offsets = vec![TextOffset::START];
527 let mut offset = TextOffset::START;
528 for ch in content.chars() {
529 offset = offset.add_width(TextWidth::from_char(ch));
530 if ch == '\n' {
531 line_offsets.push(offset);
532 }
533 }
534 Some(FileSummary { line_offsets, last_offset: offset })
535}
536
537#[salsa::tracked(returns(ref))]
539fn file_content<'db>(db: &'db dyn Database, file_id: FileId<'db>) -> Option<Arc<str>> {
540 let overrides = db.file_overrides();
541 overrides.get(&file_id).map(|content| (**content).clone()).or_else(|| {
542 priv_raw_file_content(db, file_id).map(|content| content.long(db).clone().into())
543 })
544}
545
546fn file_summary<'db>(db: &'db dyn Database, file: FileId<'db>) -> Option<&'db FileSummary> {
550 file_summary_helper(db, file).as_ref()
551}
552
553#[salsa::tracked(returns(ref))]
555fn blob_content_helper<'db>(db: &'db dyn Database, blob: BlobId<'db>) -> Option<Vec<u8>> {
556 blob.long(db).content()
557}
558
559fn blob_content<'db>(db: &'db dyn Database, blob: BlobId<'db>) -> Option<&'db [u8]> {
562 blob_content_helper(db, blob).as_ref().map(|content| content.as_slice())
563}
564
565pub fn get_originating_location<'db>(
567 db: &'db dyn Database,
568 mut location: SpanInFile<'db>,
569 mut parent_files: Option<&mut Vec<FileId<'db>>>,
570) -> SpanInFile<'db> {
571 if let Some(ref mut parent_files) = parent_files {
572 parent_files.push(location.file_id);
573 }
574 while let Some((parent, code_mappings)) = get_parent_and_mapping(db, location.file_id) {
575 location.file_id = parent.file_id;
576 if let Some(ref mut parent_files) = parent_files {
577 parent_files.push(location.file_id);
578 }
579 location.span = translate_location(code_mappings, location.span).unwrap_or(parent.span);
580 }
581 location
582}
583
584pub fn translate_location(code_mapping: &[CodeMapping], span: TextSpan) -> Option<TextSpan> {
594 if let Some(containing) = code_mapping.iter().find(|mapping| {
596 mapping.span.contains(span) && !matches!(mapping.origin, CodeOrigin::CallSite(_))
597 }) {
598 return containing.translate(span);
600 }
601
602 let intersecting_mappings = || {
604 code_mapping.iter().filter(|mapping| {
605 mapping.span.end > span.start && mapping.span.start < span.end
607 })
608 };
609
610 let call_site = intersecting_mappings()
612 .find(|mapping| {
613 mapping.span.contains(span) && matches!(mapping.origin, CodeOrigin::CallSite(_))
614 })
615 .and_then(|containing| containing.translate(span));
616
617 let mut matched = intersecting_mappings()
618 .filter(|mapping| matches!(mapping.origin, CodeOrigin::Span(_)))
619 .collect::<Vec<_>>();
620
621 if matched.is_empty() {
623 return call_site;
624 }
625
626 matched.sort_by_key(|mapping| mapping.span);
628 let (first, matched) = matched.split_first().expect("non-empty vec always has first element");
629
630 let mut last = first;
633 for mapping in matched {
634 if mapping.span.start > last.span.end {
635 break;
636 }
637
638 let mapping_origin =
639 mapping.origin.as_span().expect("mappings with start origin should be filtered out");
640 let last_origin =
641 last.origin.as_span().expect("mappings with start origin should be filtered out");
642 if mapping_origin.start > last_origin.end {
644 break;
645 }
646
647 last = mapping;
648 }
649
650 let constructed_span = TextSpan::new(first.span.start, last.span.end);
653 if !constructed_span.contains(span) {
654 return call_site;
655 }
656
657 let start = match first.origin {
659 CodeOrigin::Start(origin_start) => origin_start.add_width(span.start - first.span.start),
660 CodeOrigin::Span(span) => span.start,
661 CodeOrigin::CallSite(span) => span.start,
662 };
663
664 let end = match last.origin {
665 CodeOrigin::Start(_) => start.add_width(span.width()),
666 CodeOrigin::Span(span) => span.end,
667 CodeOrigin::CallSite(span) => span.start,
668 };
669
670 Some(TextSpan::new(start, end))
671}
672
673pub fn get_parent_and_mapping<'db>(
675 db: &'db dyn Database,
676 file_id: FileId<'db>,
677) -> Option<(SpanInFile<'db>, &'db [CodeMapping])> {
678 let vf = match file_id.long(db) {
679 FileLongId::OnDisk(_) => return None,
680 FileLongId::Virtual(vf) => vf,
681 FileLongId::External(id) => ext_as_virtual(db, *id),
682 };
683 Some((vf.parent?, &vf.code_mappings))
684}
685
686pub fn ext_as_virtual<'db>(db: &'db dyn Database, id: salsa::Id) -> &'db VirtualFile<'db> {
688 files_group_input(db)
689 .ext_as_virtual_obj(db)
690 .as_ref()
691 .expect("`ext_as_virtual` was not set as input.")(db, id)
692}
693
694trait PrivFilesGroup: Database {
696 fn crate_configuration_input<'db>(
698 &'db self,
699 config: CrateConfiguration<'db>,
700 ) -> &'db CrateConfigurationInput {
701 crate_configuration_input(self.as_dyn_database(), config)
702 }
703}
704
705impl<T: Database + ?Sized> PrivFilesGroup for T {}