1use std::sync::{Arc, Mutex, OnceLock};
12
13use ridl_syntax::{Parse, Profile, parse};
14
15use crate::package::Package;
16
17#[salsa::input(debug)]
22pub struct InputFile {
23 pub path: String,
24 #[returns(ref)]
25 pub text: String,
26}
27
28pub fn profile_of_path(path: &str) -> Profile {
32 if path.ends_with(".ridl") {
33 Profile::Ridl
34 } else if path.ends_with(".rsdl") {
35 Profile::Rsdl
36 } else {
37 Profile::Typl
38 }
39}
40
41#[salsa::tracked(returns(clone))]
47pub fn parse_file(db: &dyn salsa::Database, file: InputFile) -> Parse {
48 parse(file.text(db), profile_of_path(file.path(db)))
49}
50
51#[salsa::db]
60pub struct RidlDatabase {
61 storage: salsa::Storage<Self>,
62 executed: Arc<Mutex<Vec<salsa::DatabaseKeyIndex>>>,
63 pub(crate) std_package_cache: OnceLock<Package>,
67}
68
69impl RidlDatabase {
70 pub fn take_executed_queries(&self) -> Vec<salsa::DatabaseKeyIndex> {
73 std::mem::take(
74 &mut *self
75 .executed
76 .lock()
77 .expect("the execution log mutex is never poisoned"),
78 )
79 }
80}
81
82impl Default for RidlDatabase {
83 fn default() -> Self {
84 let executed: Arc<Mutex<Vec<salsa::DatabaseKeyIndex>>> = Arc::default();
88 let storage = salsa::Storage::new(Some(Box::new({
89 let executed = executed.clone();
90 move |event| {
91 if let salsa::EventKind::WillExecute { database_key } = event.kind {
92 executed
93 .lock()
94 .expect("the execution log mutex is never poisoned")
95 .push(database_key);
96 }
97 }
98 })));
99 Self {
100 storage,
101 executed,
102 std_package_cache: OnceLock::new(),
103 }
104 }
105}
106
107#[salsa::db]
108impl salsa::Database for RidlDatabase {}
109
110#[cfg(test)]
111mod tests {
112 use super::{InputFile, RidlDatabase, parse_file, profile_of_path};
113 use ridl_syntax::Profile;
114 use salsa::Setter;
115 use salsa::plumbing::AsId;
116
117 #[test]
118 fn profile_of_path_selects_ridl_by_extension() {
119 assert_eq!(profile_of_path("a/b.ridl"), Profile::Ridl);
120 assert_eq!(profile_of_path("a/b.typl"), Profile::Typl);
121 assert_eq!(profile_of_path("no_extension"), Profile::Typl);
122 }
123
124 #[test]
125 fn profile_of_path_selects_rsdl_by_extension() {
126 assert_eq!(profile_of_path("veh/topology/system.rsdl"), Profile::Rsdl);
127 assert_eq!(profile_of_path("a/b.rsdl.typl"), Profile::Typl);
128 }
129
130 #[test]
135 fn the_rsdl_registry_change_reaches_typl_and_ridl_files() {
136 let db = RidlDatabase::default();
137 let codes = |path: &str, text: String| -> Vec<&'static str> {
138 let file = InputFile::new(&db, path.to_string(), text);
139 parse_file(&db, file)
140 .errors()
141 .iter()
142 .map(|e| e.code)
143 .collect()
144 };
145 for word in ["offers", "distribution", "machine"] {
146 let text = format!("package p\ntype {word}: m\n");
147 assert_eq!(
148 codes("x.typl", text.clone()),
149 vec!["FORM-105"],
150 "`{word}` in .typl"
151 );
152 assert_eq!(codes("x.ridl", text), vec!["FORM-105"], "`{word}` in .ridl");
153 }
154 for word in [
155 "provides",
156 "instance",
157 "assurance",
158 "target",
159 "place",
160 "on",
161 "transport",
162 "bundle",
163 "time",
164 "base",
165 "redundant",
166 "supervise",
167 "degraded",
168 ] {
169 let text = format!("package p\ntype {word}: m\n");
170 assert_eq!(
171 codes("x.typl", text),
172 Vec::<&str>::new(),
173 "`{word}` in .typl"
174 );
175 }
176 }
177
178 #[test]
181 fn parse_file_derives_the_profile_from_the_path() {
182 let db = RidlDatabase::default();
183 let text = "package p\n@\n";
184 let typl = InputFile::new(&db, "x.typl".to_string(), text.to_string());
185 let ridl = InputFile::new(&db, "x.ridl".to_string(), text.to_string());
186
187 let typl_codes: Vec<_> = parse_file(&db, typl)
188 .errors()
189 .iter()
190 .map(|e| e.code)
191 .collect();
192 assert_eq!(typl_codes, vec!["TYPL-302"]);
193
194 let ridl_codes: Vec<_> = parse_file(&db, ridl)
195 .errors()
196 .iter()
197 .map(|e| e.code)
198 .collect();
199 assert_eq!(ridl_codes, vec!["FORM-102"]);
200 }
201
202 fn rendered(db: &RidlDatabase, key: salsa::DatabaseKeyIndex) -> String {
205 salsa::attach(db, || format!("{key:?}"))
206 }
207
208 #[test]
214 fn edit_reparses_only_the_edited_file() {
215 let mut db = RidlDatabase::default();
216
217 let a = InputFile::new(&db, "a.typl".to_string(), "type A: m".to_string());
218 let b = InputFile::new(&db, "b.typl".to_string(), "type B: s".to_string());
219
220 let parse_a = parse_file(&db, a);
222 let parse_b = parse_file(&db, b);
223 let executed = db.take_executed_queries();
224 assert_eq!(
225 executed.len(),
226 2,
227 "the first parse of A and B must run the query exactly twice",
228 );
229 assert_eq!(parse_a.syntax().text().to_string(), "type A: m");
230 assert_eq!(parse_b.syntax().text().to_string(), "type B: s");
231
232 let _ = parse_file(&db, a);
234 let _ = parse_file(&db, b);
235 assert_eq!(
236 db.take_executed_queries(),
237 Vec::new(),
238 "re-querying unchanged inputs must run no executions",
239 );
240
241 a.set_text(&mut db).to("type A: kg".to_string());
243
244 let parse_a2 = parse_file(&db, a);
245 let parse_b2 = parse_file(&db, b);
246 let executed = db.take_executed_queries();
247 assert_eq!(
248 executed.len(),
249 1,
250 "editing A must re-parse exactly one file",
251 );
252 assert_eq!(
253 rendered(&db, executed[0]),
254 format!("parse_file({:?})", a.as_id()),
255 "the re-executed query must be the parse of A, keyed by A's input",
256 );
257 assert_eq!(
258 parse_a2.syntax().text().to_string(),
259 "type A: kg",
260 "A's memoized parse must reflect the edited text",
261 );
262 assert_ne!(
263 parse_a2, parse_a,
264 "A's parse value must change after the edit"
265 );
266 assert_eq!(
267 parse_b2, parse_b,
268 "B's parse value must stay memoized and unchanged",
269 );
270 }
271}