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}