use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum CoverageStatus {
Implemented,
Partial,
Opaque,
Fallback,
Untested,
NotSupported,
}
impl CoverageStatus {
fn parse(value: &str) -> Option<Self> {
match value {
"implemented" => Some(Self::Implemented),
"partial" => Some(Self::Partial),
"opaque" => Some(Self::Opaque),
"fallback" => Some(Self::Fallback),
"untested" => Some(Self::Untested),
"not_supported" => Some(Self::NotSupported),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Grammar {
SysML,
KerML,
Any,
}
impl Grammar {
fn parse(value: &str) -> Option<Self> {
match value {
"SysML" => Some(Self::SysML),
"KerML" => Some(Self::KerML),
"*" => Some(Self::Any),
_ => None,
}
}
}
#[derive(Debug)]
struct CoverageRule {
grammar: Grammar,
pattern: String,
status: CoverageStatus,
line: usize,
}
impl CoverageRule {
fn matches(&self, grammar: Grammar, production: &str) -> bool {
if self.grammar != Grammar::Any && self.grammar != grammar {
return false;
}
pattern_matches(&self.pattern, production)
}
fn specificity(&self) -> usize {
let non_wildcard = self.pattern.chars().filter(|ch| *ch != '*').count();
let grammar_bonus = usize::from(self.grammar != Grammar::Any) * 1_000;
let exact_bonus = usize::from(!self.pattern.contains('*')) * 10_000;
exact_bonus + grammar_bonus + non_wildcard
}
}
fn manifest_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn release_root() -> PathBuf {
if let Some(path) = std::env::var_os("SYSML_V2_RELEASE_DIR") {
return PathBuf::from(path);
}
manifest_dir().join("sysml-v2-release")
}
fn extract_productions(path: &Path) -> Vec<String> {
let text = fs::read_to_string(path)
.unwrap_or_else(|err| panic!("failed to read BNF file {}: {err}", path.display()));
let mut productions = Vec::new();
for line in text.lines() {
let Some(first) = line.as_bytes().first().copied() else {
continue;
};
if !first.is_ascii_alphabetic() {
continue;
}
let name_len = line
.bytes()
.take_while(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
.count();
if name_len == 0 {
continue;
}
let rest = line[name_len..].trim_start();
if rest.starts_with('=') || rest.contains(" =") {
productions.push(line[..name_len].to_string());
}
}
productions.sort();
productions.dedup();
productions
}
fn parse_coverage_rules(path: &Path) -> Vec<CoverageRule> {
let text = fs::read_to_string(path)
.unwrap_or_else(|err| panic!("failed to read coverage map {}: {err}", path.display()));
let mut rules = Vec::new();
for (idx, line) in text.lines().enumerate() {
let line_no = idx + 1;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let fields = trimmed.split_whitespace().collect::<Vec<_>>();
assert_eq!(
fields.len(),
3,
"invalid coverage rule at {}:{line_no}: expected 3 fields",
path.display()
);
let grammar = Grammar::parse(fields[0]).unwrap_or_else(|| {
panic!(
"invalid grammar '{}' at {}:{line_no}",
fields[0],
path.display()
)
});
let status = CoverageStatus::parse(fields[2]).unwrap_or_else(|| {
panic!(
"invalid coverage status '{}' at {}:{line_no}",
fields[2],
path.display()
)
});
rules.push(CoverageRule {
grammar,
pattern: fields[1].to_string(),
status,
line: line_no,
});
}
rules
}
fn pattern_matches(pattern: &str, value: &str) -> bool {
match (pattern.starts_with('*'), pattern.ends_with('*')) {
(false, false) => pattern == value,
(false, true) => value.starts_with(&pattern[..pattern.len() - 1]),
(true, false) => value.ends_with(&pattern[1..]),
(true, true) => {
let needle = &pattern[1..pattern.len() - 1];
!needle.is_empty() && value.contains(needle)
}
}
}
fn classify<'a>(
rules: &'a [CoverageRule],
grammar: Grammar,
production: &str,
) -> Result<&'a CoverageRule, String> {
let matches = rules
.iter()
.filter(|rule| rule.matches(grammar, production))
.collect::<Vec<_>>();
let Some(best_specificity) = matches.iter().map(|rule| rule.specificity()).max() else {
return Err(format!("no coverage rule for {grammar:?}.{production}"));
};
let best = matches
.into_iter()
.filter(|rule| rule.specificity() == best_specificity)
.collect::<Vec<_>>();
let first = best[0];
if best.iter().any(|rule| rule.status != first.status) {
let lines = best
.iter()
.map(|rule| rule.line.to_string())
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"conflicting equally-specific coverage rules for {grammar:?}.{production} at lines {lines}"
));
}
Ok(first)
}
type ClassifyAllResult = (
BTreeMap<CoverageStatus, usize>,
BTreeMap<CoverageStatus, Vec<String>>,
Vec<String>,
);
fn classify_all(
grammar: Grammar,
productions: &[String],
rules: &[CoverageRule],
) -> ClassifyAllResult {
let mut counts = BTreeMap::<CoverageStatus, usize>::new();
let mut productions_by_status = BTreeMap::<CoverageStatus, Vec<String>>::new();
let mut errors = Vec::new();
for production in productions {
match classify(rules, grammar, production) {
Ok(rule) => {
*counts.entry(rule.status).or_insert(0) += 1;
productions_by_status
.entry(rule.status)
.or_default()
.push(production.clone());
}
Err(err) => errors.push(err),
}
}
(counts, productions_by_status, errors)
}
fn assert_all_productions_are_classified(
grammar: Grammar,
productions: &[String],
rules: &[CoverageRule],
) {
let (counts, productions_by_status, errors) = classify_all(grammar, productions, rules);
eprintln!("{grammar:?} BNF coverage counts: {counts:?}");
for (status, productions) in &productions_by_status {
eprintln!(
"{grammar:?} {status:?} productions: {}",
productions.join(", ")
);
}
assert!(
errors.is_empty(),
"unclassified or ambiguous BNF productions:\n{}",
errors.join("\n")
);
}
fn load_bnf_productions() -> (Vec<String>, Vec<String>, Vec<CoverageRule>) {
let root = release_root();
let sysml_bnf = root.join("bnf").join("SysML-textual-bnf.kebnf");
let kerml_bnf = root.join("bnf").join("KerML-textual-bnf.kebnf");
let rules = parse_coverage_rules(&manifest_dir().join("docs").join("bnf_coverage.map"));
(
extract_productions(&sysml_bnf),
extract_productions(&kerml_bnf),
rules,
)
}
#[test]
fn textual_bnf_productions_are_covered_by_status_map() {
let root = release_root();
let sysml_bnf = root.join("bnf").join("SysML-textual-bnf.kebnf");
let kerml_bnf = root.join("bnf").join("KerML-textual-bnf.kebnf");
assert!(
sysml_bnf.exists(),
"SysML textual BNF not found at {}",
sysml_bnf.display()
);
assert!(
kerml_bnf.exists(),
"KerML textual BNF not found at {}",
kerml_bnf.display()
);
let rules = parse_coverage_rules(&manifest_dir().join("docs").join("bnf_coverage.map"));
assert!(!rules.is_empty(), "coverage map must contain rules");
let sysml = extract_productions(&sysml_bnf);
let kerml = extract_productions(&kerml_bnf);
assert_eq!(
sysml.len(),
350,
"unexpected SysML textual BNF production count"
);
assert_eq!(
kerml.len(),
290,
"unexpected KerML textual BNF production count"
);
assert_all_productions_are_classified(Grammar::SysML, &sysml, &rules);
assert_all_productions_are_classified(Grammar::KerML, &kerml, &rules);
}
#[test]
fn implemented_wildcard_patterns_do_not_target_opaque_body_helper_families() {
let rules = parse_coverage_rules(&manifest_dir().join("docs").join("bnf_coverage.map"));
let opaque_families = ["Flow", "Allocation", "Metadata"];
let implemented_opaque_rules = rules
.iter()
.filter(|rule| rule.status == CoverageStatus::Implemented)
.filter(|rule| rule.pattern.contains('*'))
.filter(|rule| {
opaque_families
.iter()
.any(|family| pattern_matches(&rule.pattern, family))
})
.collect::<Vec<_>>();
assert!(
implemented_opaque_rules.is_empty(),
"opaque helper wildcard families must not be marked implemented: {implemented_opaque_rules:?}"
);
}
#[test]
fn coverage_map_rules_use_no_partial_status() {
let rules = parse_coverage_rules(&manifest_dir().join("docs").join("bnf_coverage.map"));
let partial_rules: Vec<_> = rules
.iter()
.filter(|rule| rule.status == CoverageStatus::Partial)
.map(|rule| format!("line {}: {:?} {}", rule.line, rule.grammar, rule.pattern))
.collect();
assert!(
partial_rules.is_empty(),
"bnf_coverage.map must not contain partial rules:\n{}",
partial_rules.join("\n")
);
}
#[test]
fn all_textual_bnf_productions_are_implemented() {
let (sysml, kerml, rules) = load_bnf_productions();
assert_eq!(sysml.len(), 350);
assert_eq!(kerml.len(), 290);
for grammar in [Grammar::SysML, Grammar::KerML] {
let productions = if grammar == Grammar::SysML {
&sysml
} else {
&kerml
};
let (counts, _, errors) = classify_all(grammar, productions, &rules);
assert!(
errors.is_empty(),
"{grammar:?} classification errors: {errors:?}"
);
assert_eq!(
counts.get(&CoverageStatus::Partial).copied().unwrap_or(0),
0,
"{grammar:?} still has partial productions"
);
assert_eq!(
counts
.get(&CoverageStatus::Implemented)
.copied()
.unwrap_or(0),
productions.len(),
"{grammar:?} implemented count must equal production count"
);
}
}
#[test]
fn implemented_productions_do_not_use_skip_or_statement_only_bodies() {
let rules = parse_coverage_rules(&manifest_dir().join("docs").join("bnf_coverage.map"));
let guarded_productions = [
("AttributeDefinition", "src/parser/attribute.rs"),
("AttributeUsage", "src/parser/attribute.rs"),
("OccurrenceDefinition", "src/parser/occurrence.rs"),
("OccurrenceUsage", "src/parser/occurrence.rs"),
("PartDefinition", "src/parser/part/def.rs"),
("PartUsage", "src/parser/part/usage.rs"),
("PortDefinition", "src/parser/port.rs"),
("PortUsage", "src/parser/port.rs"),
("ConnectionDefinition", "src/parser/connection.rs"),
("InterfaceDefinition", "src/parser/interface.rs"),
("EnumerationDefinition", "src/parser/enumeration.rs"),
("RenderingDefinition", "src/parser/view.rs"),
("FlowDefinition", "src/parser/flow.rs"),
("FlowUsage", "src/parser/flow.rs"),
("AllocationDefinition", "src/parser/allocation.rs"),
("AllocationUsage", "src/parser/allocation.rs"),
("MetadataDefinition", "src/parser/metadata.rs"),
("MetadataUsage", "src/parser/metadata.rs"),
("ActionDefinition", "src/parser/action.rs"),
("StateDefinition", "src/parser/state.rs"),
("RequirementDefinition", "src/parser/requirement.rs"),
];
let mut violations = Vec::new();
for (production, parser_path) in guarded_productions {
let rule = classify(&rules, Grammar::SysML, production)
.unwrap_or_else(|err| panic!("guarded production must be classified: {err}"));
if rule.status != CoverageStatus::Implemented {
continue;
}
let path = manifest_dir().join(parser_path);
let parser = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("failed to read parser file {}: {err}", path.display()));
for forbidden in [
"skip_until_brace_end",
"semicolon_or_statement_brace_body",
"take_until_terminator(input, b\";{\")",
] {
if parser.contains(forbidden) {
violations.push(format!(
"SysML.{production} is implemented by rule line {} but {} still contains {forbidden}",
rule.line,
path.display()
));
}
}
}
assert!(
violations.is_empty(),
"implemented productions must not rely on opaque or statement-only body parsing:\n{}",
violations.join("\n")
);
}