use std::collections::BTreeMap;
fn forwarded_call(body: &str) -> Option<String> {
const PLUMBING: &[&str] = &["std", "core", "kenro", "gpb", "geom", "api", "manifest"];
let bytes = body.as_bytes();
let mut i = 0;
while let Some(found) = body[i..].find("::") {
let sep = i + found;
let mut start = sep;
while start > 0 && {
let c = bytes[start - 1];
c.is_ascii_alphanumeric() || c == b'_'
} {
start -= 1;
}
let mut end = sep + 2;
while end < bytes.len() && {
let c = bytes[end];
c.is_ascii_alphanumeric() || c == b'_'
} {
end += 1;
}
let module = &body[start..sep];
let function = &body[sep + 2..end];
let is_call = bytes.get(end) == Some(&b'(');
let lowercase = |s: &str| {
s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
};
if is_call
&& !module.is_empty()
&& !function.is_empty()
&& lowercase(module)
&& lowercase(function)
&& !PLUMBING.contains(&module)
{
return Some(format!("{module}::{function}"));
}
i = sep + 2;
}
None
}
fn forwards(source: &str, marker: &Marker) -> BTreeMap<String, String> {
let lines: Vec<&str> = source.lines().collect();
let mut out = BTreeMap::new();
let mut i = 0;
while i < lines.len() {
let Some(name) = marker.export_name(&lines, i) else {
i += 1;
continue;
};
let mut j = i;
while j < lines.len() && !lines[j].ends_with('{') {
j += 1;
}
let mut body = String::new();
let mut k = j + 1;
while k < lines.len() && lines[k] != "}" {
body.push_str(lines[k]);
body.push('\n');
k += 1;
}
if let Some(call) = forwarded_call(&body) {
out.insert(name, call);
}
i = k.max(i + 1);
}
out
}
enum Marker {
Abi,
Wasm,
}
impl Marker {
fn export_name(&self, lines: &[&str], i: usize) -> Option<String> {
let line = lines[i];
match self {
Marker::Abi => {
let rest = line.strip_prefix("pub extern \"C\" fn k_")?;
Some(rest.split('(').next()?.to_string())
}
Marker::Wasm => {
let rest = line
.trim_start()
.strip_prefix("#[wasm_bindgen(js_name = ")?
.strip_suffix(")]")?;
Some(rest.to_string())
}
}
}
}
#[test]
fn abi_and_wasm_exports_call_the_same_implementation() {
let abi_source =
std::fs::read_to_string("crates/kenro-abi/src/lib.rs").expect("read kenro-abi");
let wasm_source =
std::fs::read_to_string("crates/kenro-wasm/src/lib.rs").expect("read kenro-wasm");
let abi = forwards(&abi_source, &Marker::Abi);
let wasm = forwards(&wasm_source, &Marker::Wasm);
assert!(
abi.len() > 150 && wasm.len() > 150,
"the scanners stopped matching ({} abi, {} wasm) — fix them rather than \
letting this test pass vacuously",
abi.len(),
wasm.len()
);
let mismatches: Vec<String> = abi
.iter()
.filter_map(|(name, abi_impl)| {
let wasm_impl = wasm.get(name)?;
(abi_impl != wasm_impl).then(|| {
format!("{name}: kenro-abi calls {abi_impl}, kenro-wasm calls {wasm_impl}")
})
})
.collect();
assert!(
mismatches.is_empty(),
"the two wasm bindings disagree about which function implements a name. \
One of them is stale:\n {}",
mismatches.join("\n ")
);
}