1use std::collections::BTreeMap;
2use std::fs;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
7use cairo_lang_utils::{LookupIntern, Upcast};
8use salsa::Durability;
9use semver::Version;
10use serde::{Deserialize, Serialize};
11use smol_str::{SmolStr, ToSmolStr};
12
13use crate::cfg::CfgSet;
14use crate::flag::Flag;
15use crate::ids::{
16 BlobId, BlobLongId, CodeMapping, CodeOrigin, CrateId, CrateLongId, Directory, FileId,
17 FileLongId, FlagId, FlagLongId, VirtualFile,
18};
19use crate::span::{FileSummary, TextOffset, TextSpan, TextWidth};
20
21#[cfg(test)]
22#[path = "db_test.rs"]
23mod test;
24
25pub const CORELIB_CRATE_NAME: &str = "core";
26pub const CORELIB_VERSION: &str = env!("CARGO_PKG_VERSION");
27
28#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
33pub struct CrateIdentifier(SmolStr);
34
35impl<T: ToSmolStr> From<T> for CrateIdentifier {
36 fn from(value: T) -> Self {
37 Self(value.to_smolstr())
38 }
39}
40
41impl From<CrateIdentifier> for SmolStr {
42 fn from(value: CrateIdentifier) -> Self {
43 value.0
44 }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct CrateConfiguration {
50 pub root: Directory,
52 pub settings: CrateSettings,
53 pub cache_file: Option<BlobId>,
54}
55impl CrateConfiguration {
56 pub fn default_for_root(root: Directory) -> Self {
58 Self { root, settings: CrateSettings::default(), cache_file: None }
59 }
60}
61
62#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
64pub struct CrateSettings {
65 pub name: Option<SmolStr>,
68 pub edition: Edition,
70 pub version: Option<Version>,
82 pub cfg_set: Option<CfgSet>,
84 #[serde(default)]
86 pub dependencies: BTreeMap<String, DependencySettings>,
87
88 #[serde(default)]
89 pub experimental_features: ExperimentalFeaturesConfig,
90}
91
92#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
100pub enum Edition {
101 #[default]
103 #[serde(rename = "2023_01")]
104 V2023_01,
105 #[serde(rename = "2023_10")]
106 V2023_10,
107 #[serde(rename = "2023_11")]
108 V2023_11,
109 #[serde(rename = "2024_07")]
110 V2024_07,
111}
112impl Edition {
113 pub const fn latest() -> Self {
118 Self::V2024_07
119 }
120
121 pub fn prelude_submodule_name(&self) -> &str {
123 match self {
124 Self::V2023_01 => "v2023_01",
125 Self::V2023_10 | Self::V2023_11 => "v2023_10",
126 Self::V2024_07 => "v2024_07",
127 }
128 }
129
130 pub fn ignore_visibility(&self) -> bool {
132 match self {
133 Self::V2023_01 | Self::V2023_10 => true,
134 Self::V2023_11 | Self::V2024_07 => false,
135 }
136 }
137}
138
139#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
141pub struct DependencySettings {
142 pub discriminator: Option<SmolStr>,
149}
150
151#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
153pub struct ExperimentalFeaturesConfig {
154 pub negative_impls: bool,
155 pub associated_item_constraints: bool,
157 #[serde(default)]
162 pub coupons: bool,
163}
164
165pub trait ExternalFiles {
167 fn ext_as_virtual(&self, external_id: salsa::InternId) -> VirtualFile {
169 self.try_ext_as_virtual(external_id).unwrap()
170 }
171
172 fn try_ext_as_virtual(&self, _external_id: salsa::InternId) -> Option<VirtualFile> {
174 panic!("Should not be called, unless specifically implemented!");
175 }
176}
177
178#[salsa::query_group(FilesDatabase)]
180pub trait FilesGroup: ExternalFiles {
181 #[salsa::interned]
182 fn intern_crate(&self, crt: CrateLongId) -> CrateId;
183 #[salsa::interned]
184 fn intern_file(&self, file: FileLongId) -> FileId;
185 #[salsa::interned]
186 fn intern_blob(&self, blob: BlobLongId) -> BlobId;
187 #[salsa::interned]
188 fn intern_flag(&self, flag: FlagLongId) -> FlagId;
189
190 #[salsa::input]
192 fn crate_configs(&self) -> Arc<OrderedHashMap<CrateId, CrateConfiguration>>;
193
194 #[salsa::input]
200 fn file_overrides(&self) -> Arc<OrderedHashMap<FileId, Arc<str>>>;
201
202 #[salsa::input]
205 fn flags(&self) -> Arc<OrderedHashMap<FlagId, Arc<Flag>>>;
206 #[salsa::input]
208 fn cfg_set(&self) -> Arc<CfgSet>;
209
210 fn crates(&self) -> Vec<CrateId>;
212 fn crate_config(&self, crate_id: CrateId) -> Option<CrateConfiguration>;
214
215 fn priv_raw_file_content(&self, file_id: FileId) -> Option<Arc<str>>;
217 fn file_content(&self, file_id: FileId) -> Option<Arc<str>>;
219 fn file_summary(&self, file_id: FileId) -> Option<Arc<FileSummary>>;
220
221 fn blob_content(&self, blob_id: BlobId) -> Option<Arc<[u8]>>;
223 fn get_flag(&self, id: FlagId) -> Option<Arc<Flag>>;
225}
226
227pub fn init_files_group(db: &mut (dyn FilesGroup + 'static)) {
228 db.set_file_overrides(Arc::new(OrderedHashMap::default()));
230 db.set_crate_configs(Arc::new(OrderedHashMap::default()));
231 db.set_flags(Arc::new(OrderedHashMap::default()));
232 db.set_cfg_set(Arc::new(CfgSet::new()));
233}
234
235pub fn init_dev_corelib(db: &mut (dyn FilesGroup + 'static), core_lib_dir: PathBuf) {
236 db.set_crate_config(
237 CrateId::core(db),
238 Some(CrateConfiguration {
239 root: Directory::Real(core_lib_dir),
240 settings: CrateSettings {
241 name: None,
242 edition: Edition::V2024_07,
243 version: Version::parse(CORELIB_VERSION).ok(),
244 cfg_set: Default::default(),
245 dependencies: Default::default(),
246 experimental_features: ExperimentalFeaturesConfig {
247 negative_impls: true,
248 associated_item_constraints: true,
249 coupons: true,
250 },
251 },
252 cache_file: None,
253 }),
254 );
255}
256
257impl AsFilesGroupMut for dyn FilesGroup {
258 fn as_files_group_mut(&mut self) -> &mut (dyn FilesGroup + 'static) {
259 self
260 }
261}
262
263pub trait FilesGroupEx: Upcast<dyn FilesGroup> + AsFilesGroupMut {
264 fn override_file_content(&mut self, file: FileId, content: Option<Arc<str>>) {
266 let mut overrides = Upcast::upcast(self).file_overrides().as_ref().clone();
267 match content {
268 Some(content) => overrides.insert(file, content),
269 None => overrides.swap_remove(&file),
270 };
271 self.as_files_group_mut().set_file_overrides(Arc::new(overrides));
272 }
273 fn set_crate_config(&mut self, crt: CrateId, root: Option<CrateConfiguration>) {
275 let mut crate_configs = Upcast::upcast(self).crate_configs().as_ref().clone();
276 match root {
277 Some(root) => crate_configs.insert(crt, root),
278 None => crate_configs.swap_remove(&crt),
279 };
280 self.as_files_group_mut().set_crate_configs(Arc::new(crate_configs));
281 }
282 fn set_flag(&mut self, id: FlagId, value: Option<Arc<Flag>>) {
284 let mut flags = Upcast::upcast(self).flags().as_ref().clone();
285 match value {
286 Some(value) => flags.insert(id, value),
287 None => flags.swap_remove(&id),
288 };
289 self.as_files_group_mut().set_flags(Arc::new(flags));
290 }
291 fn use_cfg(&mut self, cfg_set: &CfgSet) {
293 let existing = Upcast::upcast(self).cfg_set();
294 let merged = existing.union(cfg_set);
295 self.as_files_group_mut().set_cfg_set(Arc::new(merged));
296 }
297}
298impl<T: Upcast<dyn FilesGroup> + AsFilesGroupMut + ?Sized> FilesGroupEx for T {}
299
300pub trait AsFilesGroupMut {
301 fn as_files_group_mut(&mut self) -> &mut (dyn FilesGroup + 'static);
302}
303
304fn crates(db: &dyn FilesGroup) -> Vec<CrateId> {
305 db.crate_configs().keys().copied().collect()
307}
308fn crate_config(db: &dyn FilesGroup, crt: CrateId) -> Option<CrateConfiguration> {
309 match crt.lookup_intern(db) {
310 CrateLongId::Real { .. } => db.crate_configs().get(&crt).cloned(),
311 CrateLongId::Virtual { name: _, file_id, settings, cache_file } => {
312 Some(CrateConfiguration {
313 root: Directory::Virtual {
314 files: BTreeMap::from([("lib.cairo".into(), file_id)]),
315 dirs: Default::default(),
316 },
317 settings: toml::from_str(&settings)
318 .expect("Failed to parse virtual crate settings."),
319 cache_file,
320 })
321 }
322 }
323}
324
325fn priv_raw_file_content(db: &dyn FilesGroup, file: FileId) -> Option<Arc<str>> {
326 match file.lookup_intern(db) {
327 FileLongId::OnDisk(path) => {
328 db.salsa_runtime().report_synthetic_read(Durability::LOW);
331
332 match fs::read_to_string(path) {
333 Ok(content) => Some(content.into()),
334 Err(_) => None,
335 }
336 }
337 FileLongId::Virtual(virt) => Some(virt.content),
338 FileLongId::External(external_id) => Some(db.ext_as_virtual(external_id).content),
339 }
340}
341fn file_content(db: &dyn FilesGroup, file: FileId) -> Option<Arc<str>> {
342 let overrides = db.file_overrides();
343 overrides.get(&file).cloned().or_else(|| db.priv_raw_file_content(file))
344}
345fn file_summary(db: &dyn FilesGroup, file: FileId) -> Option<Arc<FileSummary>> {
346 let content = db.file_content(file)?;
347 let mut line_offsets = vec![TextOffset::START];
348 let mut offset = TextOffset::START;
349 for ch in content.chars() {
350 offset = offset.add_width(TextWidth::from_char(ch));
351 if ch == '\n' {
352 line_offsets.push(offset);
353 }
354 }
355 Some(Arc::new(FileSummary { line_offsets, last_offset: offset }))
356}
357fn get_flag(db: &dyn FilesGroup, id: FlagId) -> Option<Arc<Flag>> {
358 db.flags().get(&id).cloned()
359}
360
361fn blob_content(db: &dyn FilesGroup, blob: BlobId) -> Option<Arc<[u8]>> {
362 match blob.lookup_intern(db) {
363 BlobLongId::OnDisk(path) => {
364 db.salsa_runtime().report_synthetic_read(Durability::LOW);
367
368 match fs::read(path) {
369 Ok(content) => Some(content.into()),
370 Err(_) => None,
371 }
372 }
373 BlobLongId::Virtual(content) => Some(content),
374 }
375}
376
377pub fn get_originating_location(
379 db: &dyn FilesGroup,
380 mut file_id: FileId,
381 mut span: TextSpan,
382 mut parent_files: Option<&mut Vec<FileId>>,
383) -> (FileId, TextSpan) {
384 if let Some(ref mut parent_files) = parent_files {
385 parent_files.push(file_id);
386 }
387 while let Some((parent, code_mappings)) = get_parent_and_mapping(db, file_id) {
388 if let Some(origin) = translate_location(&code_mappings, span) {
389 span = origin;
390 file_id = parent;
391 if let Some(ref mut parent_files) = parent_files {
392 parent_files.push(file_id);
393 }
394 } else {
395 break;
396 }
397 }
398 (file_id, span)
399}
400
401fn translate_location(code_mapping: &[CodeMapping], span: TextSpan) -> Option<TextSpan> {
411 let intersecting_mappings = || {
413 code_mapping.iter().filter(|mapping| {
414 !(mapping.span.end < span.start || mapping.span.start > span.end)
416 })
417 };
418
419 if let Some(containing) = intersecting_mappings().find(|mapping| {
421 mapping.span.contains(span) && !matches!(mapping.origin, CodeOrigin::CallSite(_))
422 }) {
423 return containing.translate(span);
425 }
426
427 let call_site = intersecting_mappings()
429 .find(|mapping| {
430 mapping.span.contains(span) && matches!(mapping.origin, CodeOrigin::CallSite(_))
431 })
432 .and_then(|containing| containing.translate(span));
433
434 let mut matched = intersecting_mappings()
435 .filter(|mapping| matches!(mapping.origin, CodeOrigin::Span(_)))
436 .collect::<Vec<_>>();
437
438 if matched.is_empty() {
440 return None;
441 }
442
443 matched.sort_by_key(|mapping| mapping.span);
445 let (first, matched) = matched.split_first().expect("non-empty vec always has first element");
446
447 let mut last = first;
450 for mapping in matched {
451 if mapping.span.start > last.span.end {
452 break;
453 }
454
455 let mapping_origin =
456 mapping.origin.as_span().expect("mappings with start origin should be filtered out");
457 let last_origin =
458 last.origin.as_span().expect("mappings with start origin should be filtered out");
459 if mapping_origin.start < last_origin.end {
461 break;
462 }
463
464 last = mapping;
465 }
466
467 let constructed_span = TextSpan { start: first.span.start, end: last.span.end };
470 if !constructed_span.contains(span) {
471 return call_site;
472 }
473
474 let start = match first.origin {
476 CodeOrigin::Start(origin_start) => origin_start.add_width(span.start - first.span.start),
477 CodeOrigin::Span(span) => span.start,
478 CodeOrigin::CallSite(span) => span.start,
479 };
480
481 let end = match last.origin {
482 CodeOrigin::Start(_) => start.add_width(span.width()),
483 CodeOrigin::Span(span) => span.end,
484 CodeOrigin::CallSite(span) => span.start,
485 };
486
487 Some(TextSpan { start, end })
488}
489
490fn get_parent_and_mapping(
492 db: &dyn FilesGroup,
493 file_id: FileId,
494) -> Option<(FileId, Arc<[CodeMapping]>)> {
495 let vf = match file_id.lookup_intern(db) {
496 FileLongId::OnDisk(_) => return None,
497 FileLongId::Virtual(vf) => vf,
498 FileLongId::External(id) => db.ext_as_virtual(id),
499 };
500 Some((vf.parent?, vf.code_mappings))
501}