#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use zerodds_ast::Ast;
use zerodds_ast_layout::{Encoding, Endianness, LayoutKey};
use zerodds_lang_cst::common::Language;
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("the repository root")
}
const COMPLETE: [&str; 21] = [
"ada",
"ada83",
"c",
"cpp",
"csharp",
"d",
"elixir",
"fsharp",
"go",
"java",
"julia",
"kotlin",
"lua",
"nim",
"node",
"ocaml",
"python",
"rust",
"swift",
"typescript",
"zig",
];
const TYPE_ONLY: [&str; 0] = [];
fn operations_the_walks_emit() -> BTreeSet<String> {
let mut out = BTreeSet::new();
for file in ["src/emit/encode.rs", "src/emit/decode.rs"] {
let text = std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join(file))
.unwrap_or_else(|why| panic!("{file}: {why}"));
for literal in text.split('"').skip(1).step_by(2) {
if is_operation(literal) {
out.insert(literal.to_string());
}
}
}
out
}
fn is_operation(literal: &str) -> bool {
const FREE: [&str; 5] = [
"strcmp",
"utf8_octets",
"utf16_units",
"no_dheader",
"pid_is_user_member",
];
const VERBS: [&str; 4] = ["write_", "read_", "reader_", "finish_"];
FREE.contains(&literal)
|| (VERBS.iter().any(|verb| literal.starts_with(verb))
&& literal
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'))
}
fn cst_has_a_statement_layer(root: &Path, language: &str) -> bool {
let text = std::fs::read_to_string(
root.join("crates/lang-cst/src")
.join(language)
.join("mod.rs"),
)
.unwrap_or_else(|why| panic!("the {language} CST: {why}"));
let has_statements = text.lines().any(|line| {
line == "pub enum Statement {" || line == "pub enum Stmt {" || line == "pub enum Stat {"
});
let can_group = text.lines().any(|line| {
line == "pub struct Block {"
|| line.contains("Vec<Statement>")
|| line.contains("Vec<Stmt>")
|| line.contains("Vec<Stat>")
});
has_statements && can_group
}
fn runtime_exists(root: &Path, language: &str) -> bool {
let dir = if language == "ada83" {
"ada-83"
} else {
language
};
root.join("runtime").join(dir).is_dir()
}
fn table_9_exists(root: &Path, language: &str) -> bool {
root.join("crates/lang-rules/src")
.join(format!("default_{language}.rs"))
.is_file()
}
fn backend_exists(root: &Path, language: &str) -> bool {
root.join("crates/lang-rules/src/emit")
.join(format!("{language}.rs"))
.is_file()
}
#[test]
fn the_operations_the_walks_emit_all_exist_in_the_reference_runtime() {
let operations = operations_the_walks_emit();
assert!(
operations.len() >= 60,
"the scrape found only {} operations, which is fewer than the walks emit; \
`is_operation` has stopped matching",
operations.len()
);
let headers = std::fs::read_dir(root().join("runtime/c/include/zerodds"))
.or_else(|_| std::fs::read_dir(root().join("runtime/c/include")))
.expect("the C runtime's headers");
let mut published = String::new();
for entry in headers {
let path = entry.expect("a header").path();
if path.extension().is_some_and(|e| e == "h") {
published.push_str(&std::fs::read_to_string(&path).expect("a header's text"));
}
}
let missing: Vec<&String> = operations
.iter()
.filter(|op| op.as_str() != "strcmp")
.filter(|op| !published.contains(&format!("zdw_{op}")))
.collect();
assert!(
missing.is_empty(),
"the walks emit operations `runtime/c` does not publish: {missing:?}"
);
}
#[test]
fn a_codec_backend_never_exists_without_the_three_parts_it_needs() {
let root = root();
let mut faults = Vec::new();
for language in COMPLETE.iter().chain(TYPE_ONLY.iter()) {
if !backend_exists(&root, language) {
continue;
}
if !cst_has_a_statement_layer(&root, language) {
faults.push(format!(
"{language}: emit/{language}.rs, but the CST has no statements"
));
}
if !runtime_exists(&root, language) {
faults.push(format!(
"{language}: emit/{language}.rs, but no runtime/{language}"
));
}
if !table_9_exists(&root, language) {
faults.push(format!(
"{language}: emit/{language}.rs, but no default_{language}.rs"
));
}
}
assert!(faults.is_empty(), "{}", faults.join("\n"));
let with_backend = COMPLETE
.iter()
.chain(TYPE_ONLY.iter())
.filter(|language| backend_exists(&root, language))
.count();
assert!(
with_backend >= 5,
"only {with_backend} columns have a codec backend; this gate has stopped finding them"
);
}
#[test]
fn the_type_only_columns_are_each_missing_a_part_a_codec_backend_rests_on() {
let root = root();
let mut settled = Vec::new();
for language in TYPE_ONLY {
let missing: Vec<&str> = [
(!cst_has_a_statement_layer(&root, language)).then_some("a CST statement layer"),
(!runtime_exists(&root, language)).then_some("a wire runtime"),
(!table_9_exists(&root, language)).then_some("Table 9 defaulters"),
]
.into_iter()
.flatten()
.collect();
if missing.is_empty() {
settled.push(language);
}
}
assert!(
settled.is_empty(),
"{settled:?} now has every part a codec backend rests on. \
Write `crates/lang-rules/src/emit/<lang>.rs` for it, or move it out of the \
type-only tier in `docs/language-columns-status.md` — the reason recorded \
there for that column has stopped being true."
);
let built: Vec<&str> = TYPE_ONLY
.into_iter()
.filter(|language| backend_exists(&root, language))
.collect();
assert!(
built.is_empty(),
"{built:?} now has a codec backend. Move it out of the type-only tier in \
`docs/language-columns-status.md`; the tier list there says it has none."
);
let mut unlisted = Vec::new();
for entry in std::fs::read_dir(root.join("crates/lang-cst/src")).expect("the CST columns") {
let path = entry.expect("a CST entry").path();
if !path.is_dir() {
continue;
}
let name = path
.file_name()
.and_then(|name| name.to_str())
.expect("a column's directory name")
.to_string();
if !COMPLETE.contains(&name.as_str()) && !TYPE_ONLY.contains(&name.as_str()) {
unlisted.push(name);
}
}
unlisted.sort();
assert!(
unlisted.is_empty(),
"{unlisted:?}: a CST column in neither tier. Add it to `COMPLETE` or to \
`TYPE_ONLY` and to `docs/language-columns-status.md` — until then no gate \
in this file has looked at it."
);
}
const RUNTIMES: [(Language, &str); 20] = [
(Language::C, "c"),
(Language::Cpp, "cpp"),
(Language::CSharp, "csharp"),
(Language::Java, "java"),
(Language::Python, "python"),
(Language::TypeScript, "typescript"),
(Language::Go, "go"),
(Language::OCaml, "ocaml"),
(Language::Ada, "ada"),
(Language::Ada83, "ada-83"),
(Language::D, "d"),
(Language::Elixir, "elixir"),
(Language::Julia, "julia"),
(Language::Lua, "lua"),
(Language::Nim, "nim"),
(Language::Swift, "swift"),
(Language::Zig, "zig"),
(Language::Node, "node"),
(Language::FSharp, "fsharp"),
(Language::Kotlin, "kotlin"),
];
const OPERATION_CASES: [&str; 17] = [
"combined-everything",
"wstring-unbounded-utf8",
"wstring-bounded-utf8",
"string-bounded-utf8",
"struct-mutable",
"recursive-optional-external",
"map-key-long",
"bitset-basic",
"bitmask-bitbound-16",
"fixed-basic",
"longdouble-framed",
"union-discriminator-enum",
"enum-sparse-values",
"sequence-primitives-nonempty",
"array-2d-primitives",
"primitive-mixed",
"struct-appendable",
];
const RUNTIME_EXTENSIONS: [&str; 23] = [
"c", "h", "hpp", "cpp", "cs", "java", "ts", "py", "go", "ml", "mli", "ads", "adb", "d", "ex",
"jl", "lua", "nim", "swift", "zig", "js", "fs", "kt",
];
const NOT_RUNTIME_SOURCE: [&str; 8] = [
"dist-generated",
"node_modules",
"tests",
"test",
"_build",
"deps",
"obj",
"bin",
];
fn normalised(name: &str) -> String {
name.chars()
.filter(|c| *c != '_')
.flat_map(char::to_lowercase)
.collect()
}
fn identifiers(text: &str) -> BTreeSet<String> {
let mut out = BTreeSet::new();
let mut current = String::new();
for ch in text.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
current.push(ch);
} else if !current.is_empty() {
out.insert(std::mem::take(&mut current));
}
}
if !current.is_empty() {
out.insert(current);
}
out
}
fn model(idl: &str, origin: &str) -> Ast {
let parsed = zerodds_idl_parse::parse(idl);
assert!(
parsed.diagnostics().is_empty(),
"{origin} does not parse: {:?}",
parsed.diagnostics()
);
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), origin);
assert!(
lowered.diagnostics.is_empty(),
"{origin} does not lower: {:?}",
lowered.diagnostics
);
let mut ast = lowered.ast;
ast.resolve_all_by_name()
.unwrap_or_else(|why| panic!("{origin} does not resolve: {why:?}"));
ast
}
fn what_the_generator_writes(root: &Path, language: Language) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for case in OPERATION_CASES {
let path = root
.join("tests/interop-corpus")
.join(case)
.join("type.idl");
let idl = std::fs::read_to_string(&path).unwrap_or_else(|why| panic!("{case}: {why}"));
let ast = model(&idl, case);
for encoding in Encoding::ALL {
let key = LayoutKey::data(encoding, Endianness::Little);
if let Ok(text) = zerodds_lang_rules::generate::source(&ast, language, key) {
out.extend(identifiers(&text));
}
}
}
out
}
fn runtime_source(root: &Path, directory: &str) -> String {
let mut out = String::new();
let mut stack = vec![root.join("runtime").join(directory)];
while let Some(here) = stack.pop() {
for entry in std::fs::read_dir(&here).unwrap_or_else(|why| panic!("{directory}: {why}")) {
let path = entry.expect("a runtime entry").path();
let name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
.to_string();
if path.is_dir() {
if !name.starts_with('.') && !NOT_RUNTIME_SOURCE.contains(&name.as_str()) {
stack.push(path);
}
} else if path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| RUNTIME_EXTENSIONS.contains(&e))
{
out.push_str(&std::fs::read_to_string(&path).unwrap_or_default());
out.push('\n');
}
}
}
out
}
fn published(source: &str, name: &str) -> bool {
const SHUT: [&str; 5] = ["private", "fileprivate", "internal", "defp", "local"];
source
.lines()
.filter(|line| identifiers(line).contains(name))
.any(|line| {
let words = identifiers(line);
!SHUT.iter().any(|shut| words.contains(*shut))
})
}
#[test]
fn every_runtime_publishes_the_names_its_column_writes() {
let root = root();
let operations: Vec<(String, String)> = operations_the_walks_emit()
.into_iter()
.filter(|op| op != "strcmp")
.map(|op| {
let normal = normalised(&op);
(op, normal)
})
.collect();
let mut faults: Vec<String> = Vec::new();
let mut thin: Vec<String> = Vec::new();
let results: Vec<(Language, Vec<String>, BTreeSet<String>)> = std::thread::scope(|scope| {
let handles: Vec<_> = RUNTIMES
.iter()
.map(|(language, directory)| {
let root = &root;
let operations = &operations;
scope.spawn(move || {
let written = what_the_generator_writes(root, *language);
let source = runtime_source(root, directory);
let names = identifiers(&source);
let mut missing = Vec::new();
let mut reached = BTreeSet::new();
for identifier in &written {
let normal = normalised(identifier);
for (operation, operation_normal) in operations {
let Some(prefix) = normal.strip_suffix(operation_normal.as_str())
else {
continue;
};
if !matches!(prefix, "" | "zdw" | "zdwire") {
continue;
}
if names.contains(identifier) && published(&source, identifier) {
reached.insert(operation.clone());
} else {
missing.push(format!("{operation} as `{identifier}`"));
}
}
}
(*language, missing, reached)
})
})
.collect();
handles
.into_iter()
.map(|handle| handle.join().expect("a column's sweep"))
.collect()
});
for (language, missing, reached) in results {
if !missing.is_empty() {
faults.push(format!(
"runtime/{} does not publish what the {} column writes: {}",
language.as_str(),
language.as_str(),
missing.join(", ")
));
}
if reached.len() < 40 {
thin.push(format!("{}: {}", language.as_str(), reached.len()));
}
}
assert!(faults.is_empty(), "{}", faults.join("\n"));
assert!(
thin.is_empty(),
"these columns reached fewer than 40 of the operations the walks emit, \
which is a sweep that has stopped generating rather than a runtime that is \
complete: {}",
thin.join(", ")
);
}