use std::sync::{Arc, Mutex, OnceLock};
use ridl_syntax::{Parse, Profile, parse};
use crate::package::Package;
#[salsa::input(debug)]
pub struct InputFile {
pub path: String,
#[returns(ref)]
pub text: String,
}
pub fn profile_of_path(path: &str) -> Profile {
if path.ends_with(".ridl") {
Profile::Ridl
} else if path.ends_with(".rsdl") {
Profile::Rsdl
} else {
Profile::Typl
}
}
#[salsa::tracked(returns(clone))]
pub fn parse_file(db: &dyn salsa::Database, file: InputFile) -> Parse {
parse(file.text(db), profile_of_path(file.path(db)))
}
#[salsa::db]
pub struct RidlDatabase {
storage: salsa::Storage<Self>,
executed: Arc<Mutex<Vec<salsa::DatabaseKeyIndex>>>,
pub(crate) std_package_cache: OnceLock<Package>,
}
impl RidlDatabase {
pub fn take_executed_queries(&self) -> Vec<salsa::DatabaseKeyIndex> {
std::mem::take(
&mut *self
.executed
.lock()
.expect("the execution log mutex is never poisoned"),
)
}
}
impl Default for RidlDatabase {
fn default() -> Self {
let executed: Arc<Mutex<Vec<salsa::DatabaseKeyIndex>>> = Arc::default();
let storage = salsa::Storage::new(Some(Box::new({
let executed = executed.clone();
move |event| {
if let salsa::EventKind::WillExecute { database_key } = event.kind {
executed
.lock()
.expect("the execution log mutex is never poisoned")
.push(database_key);
}
}
})));
Self {
storage,
executed,
std_package_cache: OnceLock::new(),
}
}
}
#[salsa::db]
impl salsa::Database for RidlDatabase {}
#[cfg(test)]
mod tests {
use super::{InputFile, RidlDatabase, parse_file, profile_of_path};
use ridl_syntax::Profile;
use salsa::Setter;
use salsa::plumbing::AsId;
#[test]
fn profile_of_path_selects_ridl_by_extension() {
assert_eq!(profile_of_path("a/b.ridl"), Profile::Ridl);
assert_eq!(profile_of_path("a/b.typl"), Profile::Typl);
assert_eq!(profile_of_path("no_extension"), Profile::Typl);
}
#[test]
fn profile_of_path_selects_rsdl_by_extension() {
assert_eq!(profile_of_path("veh/topology/system.rsdl"), Profile::Rsdl);
assert_eq!(profile_of_path("a/b.rsdl.typl"), Profile::Typl);
}
#[test]
fn the_rsdl_registry_change_reaches_typl_and_ridl_files() {
let db = RidlDatabase::default();
let codes = |path: &str, text: String| -> Vec<&'static str> {
let file = InputFile::new(&db, path.to_string(), text);
parse_file(&db, file)
.errors()
.iter()
.map(|e| e.code)
.collect()
};
for word in ["offers", "distribution", "machine"] {
let text = format!("package p\ntype {word}: m\n");
assert_eq!(
codes("x.typl", text.clone()),
vec!["FORM-105"],
"`{word}` in .typl"
);
assert_eq!(codes("x.ridl", text), vec!["FORM-105"], "`{word}` in .ridl");
}
for word in [
"provides",
"instance",
"assurance",
"target",
"place",
"on",
"transport",
"bundle",
"time",
"base",
"redundant",
"supervise",
"degraded",
] {
let text = format!("package p\ntype {word}: m\n");
assert_eq!(
codes("x.typl", text),
Vec::<&str>::new(),
"`{word}` in .typl"
);
}
}
#[test]
fn parse_file_derives_the_profile_from_the_path() {
let db = RidlDatabase::default();
let text = "package p\n@\n";
let typl = InputFile::new(&db, "x.typl".to_string(), text.to_string());
let ridl = InputFile::new(&db, "x.ridl".to_string(), text.to_string());
let typl_codes: Vec<_> = parse_file(&db, typl)
.errors()
.iter()
.map(|e| e.code)
.collect();
assert_eq!(typl_codes, vec!["TYPL-302"]);
let ridl_codes: Vec<_> = parse_file(&db, ridl)
.errors()
.iter()
.map(|e| e.code)
.collect();
assert_eq!(ridl_codes, vec!["FORM-102"]);
}
fn rendered(db: &RidlDatabase, key: salsa::DatabaseKeyIndex) -> String {
salsa::attach(db, || format!("{key:?}"))
}
#[test]
fn edit_reparses_only_the_edited_file() {
let mut db = RidlDatabase::default();
let a = InputFile::new(&db, "a.typl".to_string(), "type A: m".to_string());
let b = InputFile::new(&db, "b.typl".to_string(), "type B: s".to_string());
let parse_a = parse_file(&db, a);
let parse_b = parse_file(&db, b);
let executed = db.take_executed_queries();
assert_eq!(
executed.len(),
2,
"the first parse of A and B must run the query exactly twice",
);
assert_eq!(parse_a.syntax().text().to_string(), "type A: m");
assert_eq!(parse_b.syntax().text().to_string(), "type B: s");
let _ = parse_file(&db, a);
let _ = parse_file(&db, b);
assert_eq!(
db.take_executed_queries(),
Vec::new(),
"re-querying unchanged inputs must run no executions",
);
a.set_text(&mut db).to("type A: kg".to_string());
let parse_a2 = parse_file(&db, a);
let parse_b2 = parse_file(&db, b);
let executed = db.take_executed_queries();
assert_eq!(
executed.len(),
1,
"editing A must re-parse exactly one file",
);
assert_eq!(
rendered(&db, executed[0]),
format!("parse_file({:?})", a.as_id()),
"the re-executed query must be the parse of A, keyed by A's input",
);
assert_eq!(
parse_a2.syntax().text().to_string(),
"type A: kg",
"A's memoized parse must reflect the edited text",
);
assert_ne!(
parse_a2, parse_a,
"A's parse value must change after the edit"
);
assert_eq!(
parse_b2, parse_b,
"B's parse value must stay memoized and unchanged",
);
}
}