rudy_db/
database.rs

1//! Salsa database for debugging information
2//!
3//! This module provides a salsa database for debugging information.
4//!
5//! The core idea is to make debugging information (today: just DWARF)
6//! available via salsa queries.
7//!
8//! The main benefit this provides is to make queries lazy, memoized, and
9//! incremental.
10//!
11//! ## Approach
12//!
13//! The structure of debugging information in DWARF means that it's realtively
14//! cheap to look things up once you know where they are, but finding it
15//! requires walking/parsing multiple files/sections. Furthermore, there is
16//! information that cannot be eagerly loaded, such as the location of variables
17//! in memory since it depends on the current state of the program.
18//!
19//! Given all of this, we take a multi-pass approach:
20//!
21//! 1. Up-front, walk all the files to construct indexes into the debugging information
22//!    that makes it quick to find the relevant files/sections. e.g.
23//!     - Symbol -> compilation unit + offset
24//!     - Source file -> compilation unit
25//!     - Address -> relevant compilation units/sections
26//!
27//! This indexing happens on startup/initial loading of the files and
28//! only changes if the binary is recompiled (although we should be able
29//! to memoize anything looked up from the individual files).
30//!
31//! 2. Lazily parse specific sections and memoize the results. This is only
32//!    called whenever we need the information (e.g. when breaking on a line inside a function).
33//!    But the results should be able to be memoized and ~never recomputed, even when stepping
34//!    through a debug session
35//! 3. Session-specific recomputed values. There are some things that we always need to recompute
36//!    depending on the current session. E.g. when getting locations for variables when inside a
37//!    function, or parsing stack frames. These will typically use a lot of cached/memoized
38//!    intermediate results, but are unlikely to be themselves cached.
39//!
40//!
41//! NOTE: today we don't actually have _any_ inputs. There is no incrementality since we're
42//! assuming the debug information is static. However, in the future we may want incrementality
43//! in via making the Binary file and all object files inputs -- this way if we recompile the
44//! binary we can recompute which parts of the binary are the same and which are unchanged.
45
46use std::fmt::Debug;
47
48use anyhow::Result;
49use salsa::Accumulator;
50
51use crate::file::{Binary, DebugFile, File};
52
53#[salsa::db]
54pub trait Db: salsa::Database {
55    fn report_info(&self, message: String) {
56        Diagnostic {
57            message,
58            severity: DiagnosticSeverity::Info,
59        }
60        .accumulate(self);
61    }
62    fn report_warning(&self, message: String) {
63        tracing::warn!("{message}");
64        Diagnostic {
65            message,
66            severity: DiagnosticSeverity::Warning,
67        }
68        .accumulate(self);
69    }
70    fn report_critical(&self, message: String) {
71        tracing::error!("{message}");
72        Diagnostic {
73            message,
74            severity: DiagnosticSeverity::Critical,
75        }
76        .accumulate(self);
77    }
78    fn report_error(&self, message: String) {
79        tracing::warn!("{message}");
80        Diagnostic {
81            message,
82            severity: DiagnosticSeverity::Error,
83        }
84        .accumulate(self);
85    }
86
87    fn upcast(&self) -> &dyn Db;
88}
89
90#[derive(Clone, Copy, Debug)]
91enum DiagnosticSeverity {
92    /// Errors that we never expect to see
93    /// and imply an internal error.
94    Critical,
95    Error,
96    Warning,
97    Info,
98}
99
100#[salsa::accumulator]
101pub struct Diagnostic {
102    message: String,
103    severity: DiagnosticSeverity,
104}
105
106#[salsa::db]
107#[derive(Clone)]
108pub struct DebugDatabaseImpl {
109    storage: salsa::Storage<Self>,
110}
111
112pub struct DebugDbRef {
113    handle: salsa::StorageHandle<DebugDatabaseImpl>,
114}
115
116impl DebugDbRef {
117    pub fn get_db(self) -> DebugDatabaseImpl {
118        DebugDatabaseImpl {
119            storage: self.handle.into_storage(),
120        }
121    }
122}
123
124pub fn handle_diagnostics(diagnostics: &[&Diagnostic]) -> Result<()> {
125    let mut err = None;
126    for d in diagnostics {
127        match d.severity {
128            DiagnosticSeverity::Critical => {
129                if err.is_some() {
130                    tracing::error!("Critical error: {}", d.message);
131                } else {
132                    err = Some(anyhow::anyhow!("Critical error: {}", d.message));
133                }
134            }
135            DiagnosticSeverity::Error => {
136                if err.is_some() {
137                    tracing::error!("Error: {}", d.message);
138                } else {
139                    err = Some(anyhow::anyhow!("Error: {}", d.message));
140                }
141            }
142            DiagnosticSeverity::Warning => {
143                tracing::warn!("Warning: {}", d.message);
144            }
145            DiagnosticSeverity::Info => {
146                tracing::info!("Info: {}", d.message);
147            }
148        }
149    }
150
151    if let Some(e) = err { Err(e) } else { Ok(()) }
152}
153
154// #[salsa::tracked]
155// fn initialize<'db>(db: &'db dyn Db) {
156//     // Generate the index on startup to save time
157//     let _ = index::build_index(db);
158// }
159
160impl Default for DebugDatabaseImpl {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166impl DebugDatabaseImpl {
167    /// Creates a new debug database instance.
168    ///
169    /// The database manages the loading and caching of debug information from binary files.
170    ///
171    /// # Examples
172    ///
173    /// ```no_run
174    /// use rudy_db::DebugDb;
175    ///
176    /// let db = DebugDb::new();
177    /// ```
178    pub fn new() -> Self {
179        Self {
180            storage: salsa::Storage::default(),
181        }
182    }
183
184    pub(crate) fn analyze_file(&self, binary_file: &str) -> Result<(Binary, Vec<DebugFile>)> {
185        // TODO: we should do some deduplication here to see if we've already loaded
186        // this file. We can do thy by checking file path, size, mtime, etc.
187        let file = File::build(self, binary_file.to_string(), None)?;
188        let bin = Binary::new(self, file);
189
190        let index = crate::index::debug_index(self, bin);
191        tracing::debug!("Index built: {index:#?}");
192
193        // get a list of all debug files so that we can track them in case they change
194        let debug_files = index.debug_files(self).values().copied().collect();
195
196        Ok((bin, debug_files))
197    }
198
199    pub fn get_sync_ref(&self) -> DebugDbRef {
200        DebugDbRef {
201            handle: self.storage.clone().into_zalsa_handle(),
202        }
203    }
204}
205
206#[salsa::db]
207impl salsa::Database for DebugDatabaseImpl {}
208
209#[salsa::db]
210impl Db for DebugDatabaseImpl {
211    fn upcast(&self) -> &dyn Db {
212        self
213    }
214}