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