use std::collections::BTreeSet;
use gen_gomod::build_spec::{PackageKind, SCHEMA_VERSION};
const ADAPTER_LISP: &str = include_str!("../specs/adapter.lisp");
const ALGO_LISP: &str = include_str!("../specs/go-package-build.lisp");
fn strip_comments(src: &str) -> String {
src.lines()
.map(|l| match l.find(';') {
Some(i) => &l[..i],
None => l,
})
.collect::<Vec<_>>()
.join("\n")
}
fn declared_invariant_ids(src: &str) -> BTreeSet<String> {
let src = strip_comments(src);
let mut out = BTreeSet::new();
let mut rest = src.as_str();
while let Some(at) = rest.find("Go-I") {
let after = &rest[at + "Go-I".len()..];
let digits: String = after.chars().take_while(char::is_ascii_digit).collect();
if !digits.is_empty() {
out.insert(format!("Go-I{digits}"));
}
rest = &rest[at + "Go-I".len()..];
}
out
}
fn kinds_form(src: &str) -> BTreeSet<String> {
let src = strip_comments(src);
let at = src.find(":kinds").expect(":kinds form present in the algorithm spec");
let rest = &src[at + ":kinds".len()..];
let open = rest.find('(').expect(":kinds followed by a ( list");
let close = rest[open..].find(')').expect(":kinds list closes");
rest[open + 1..open + close]
.split_whitespace()
.map(str::to_string)
.collect()
}
fn phase_names(src: &str) -> BTreeSet<String> {
let src = strip_comments(src);
let mut out = BTreeSet::new();
let mut search = src.as_str();
while let Some(at) = search.find(":name") {
let after = search[at + ":name".len()..].trim_start();
let sym: String = after
.chars()
.take_while(|c| !c.is_whitespace() && *c != ')' && *c != '(')
.collect();
let advance = sym.len();
if !sym.is_empty() {
out.insert(sym);
}
search = &after[advance.max(1).min(after.len())..];
}
out
}
fn rust_package_kinds() -> BTreeSet<String> {
[PackageKind::Std, PackageKind::Module, PackageKind::Main]
.into_iter()
.map(|k| match k {
PackageKind::Std => "std",
PackageKind::Module => "module",
PackageKind::Main => "main",
})
.map(str::to_string)
.collect()
}
fn interpreter_fail_phases() -> BTreeSet<String> {
["reject-cgo", "reject-asm", "relative-path", "read-source"]
.into_iter()
.map(str::to_string)
.collect()
}
enum Disposition {
EncoderViolation(&'static [&'static str]),
InterpreterPhase(&'static str),
Structural,
SubstrateDelta,
}
fn disposition(id: &str) -> Option<Disposition> {
use Disposition::*;
Some(match id {
"Go-I1" => EncoderViolation(&["unresolved-import"]),
"Go-I2" => Structural,
"Go-I3" => EncoderViolation(&["relative-path-escapes"]),
"Go-I6" => Structural,
"Go-I7" => SubstrateDelta,
"Go-I8" => EncoderViolation(&["node-missing-source-hash"]),
"Go-I9" => EncoderViolation(&["embed-patterns-without-files"]),
"Go-I10" => {
EncoderViolation(&["std-node-has-vendored-source", "non-std-node-has-std-source"])
}
"Go-I11" => {
EncoderViolation(&["workspace-member-not-in-packages", "workspace-member-not-main"])
}
"Go-I12" => InterpreterPhase("reject-cgo"),
_ => return None,
})
}
fn encoder_violation_tags() -> BTreeSet<&'static str> {
[
"unresolved-import",
"relative-path-escapes",
"node-missing-source-hash",
"embed-patterns-without-files",
"std-node-has-vendored-source",
"non-std-node-has-std-source",
"workspace-member-not-in-packages",
"workspace-member-not-main",
]
.into_iter()
.collect()
}
#[test]
fn lisp_node_kinds_match_the_rust_enum() {
let lisp = kinds_form(ALGO_LISP);
let rust = rust_package_kinds();
assert_eq!(
lisp, rust,
"go-package-build.lisp :kinds must equal the PackageKind variant universe;\n lisp={lisp:?}\n rust={rust:?}"
);
}
#[test]
fn every_declared_invariant_has_a_rust_home() {
let declared = declared_invariant_ids(ALGO_LISP);
assert!(
declared.len() >= 10,
"expected the M1 invariant set (Go-I1..I12) in the spec; got {declared:?}"
);
let mut orphaned = Vec::new();
for id in &declared {
match disposition(id) {
None => orphaned.push(id.clone()),
Some(Disposition::InterpreterPhase(phase)) => assert!(
interpreter_fail_phases().contains(phase),
"{id} claims fail-closed phase `{phase}`, but interp.rs never emits it"
),
Some(_) => {}
}
}
assert!(
orphaned.is_empty(),
"these Go-I<n> are declared in go-package-build.lisp but have NO Rust disposition \
(add a check to invariants.rs, a fail-closed interpreter phase, or a documented \
structural/substrate arm in tests/spec_parity.rs): {orphaned:?}"
);
}
#[test]
fn adapter_and_algorithm_specs_agree_on_the_invariant_set() {
let algo = declared_invariant_ids(ALGO_LISP);
let adapter_ids = declared_invariant_ids(ADAPTER_LISP);
let stray: Vec<_> = adapter_ids.difference(&algo).cloned().collect();
assert!(
stray.is_empty(),
"adapter.lisp names invariant id(s) not in go-package-build.lisp: {stray:?}"
);
}
#[test]
fn every_encoder_violation_maps_to_a_declared_invariant() {
let declared = declared_invariant_ids(ALGO_LISP);
let mut covered: BTreeSet<&'static str> = BTreeSet::new();
for id in &declared {
if let Some(Disposition::EncoderViolation(tags)) = disposition(id) {
for t in tags {
assert!(
encoder_violation_tags().contains(t),
"disposition for {id} names Violation tag `{t}` that invariants.rs never emits"
);
covered.insert(*t);
}
}
}
let emitted = encoder_violation_tags();
let uncovered: Vec<_> = emitted.difference(&covered).collect();
assert!(
uncovered.is_empty(),
"invariants::check emits Violation tag(s) with no declared Go-I<n> disposition: {uncovered:?}"
);
}
#[test]
fn every_fail_closed_phase_is_a_declared_lisp_phase() {
let declared = phase_names(ALGO_LISP);
let mut missing = Vec::new();
for phase in interpreter_fail_phases() {
if !declared.contains(&phase) {
missing.push(phase);
}
}
assert!(
missing.is_empty(),
"interp.rs can emit GomodError::Interp for phase(s) NOT declared in \
go-package-build.lisp :phases (rename drift): {missing:?}\n declared={declared:?}"
);
}
#[test]
fn adapter_spec_schema_version_matches_rust() {
let src = strip_comments(ADAPTER_LISP);
let at = src.find(":schema-version").expect(":schema-version in adapter.lisp");
let after = src[at + ":schema-version".len()..].trim_start();
let num: String = after.chars().take_while(char::is_ascii_digit).collect();
let declared: u32 = num.parse().expect(":schema-version is a number");
assert_eq!(
declared, SCHEMA_VERSION,
"adapter.lisp :schema-version ({declared}) must equal build_spec::SCHEMA_VERSION ({SCHEMA_VERSION})"
);
}
#[test]
fn adapter_spec_ecosystem_matches_adapter_name() {
let src = strip_comments(ADAPTER_LISP);
assert!(
src.contains(r#":ecosystem "gomod""#) || src.contains(r#":ecosystem "gomod""#),
"adapter.lisp must declare :ecosystem \"gomod\""
);
assert!(
src.contains("(defadapter gomod"),
"adapter.lisp form head must be (defadapter gomod …)"
);
}