1use std::path::Path;
13
14use serde::{Deserialize, Serialize};
15
16use crate::error::ContractError;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct BindingRegistry {
21 pub version: String,
22 pub target_crate: String,
23 #[serde(default)]
26 pub critical_path: Vec<String>,
27 #[serde(default)]
28 pub bindings: Vec<KernelBinding>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct KernelBinding {
34 pub contract: String,
36 pub equation: String,
38 #[serde(default)]
40 pub module_path: Option<String>,
41 #[serde(default)]
43 pub function: Option<String>,
44 #[serde(default)]
46 pub signature: Option<String>,
47 pub status: ImplStatus,
49 #[serde(default)]
51 pub notes: Option<String>,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum ImplStatus {
58 Implemented,
60 Partial,
62 NotImplemented,
64 Pending,
66}
67
68impl std::fmt::Display for ImplStatus {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 let s = match self {
72 Self::Implemented => "implemented",
73 Self::Partial => "partial",
74 Self::NotImplemented => "not_implemented",
75 Self::Pending => "pending",
76 };
77 write!(f, "{s}")
78 }
79}
80
81pub fn parse_binding(path: &Path) -> Result<BindingRegistry, ContractError> {
88 let content = std::fs::read_to_string(path)?;
89 parse_binding_str(&content)
90}
91
92pub fn parse_binding_str(yaml: &str) -> Result<BindingRegistry, ContractError> {
94 let registry: BindingRegistry = serde_yaml::from_str(yaml)?;
95 Ok(registry)
96}
97
98pub fn normalize_contract_id(id: &str) -> &str {
104 id.strip_suffix(".yaml")
105 .or_else(|| id.strip_suffix(".yml"))
106 .unwrap_or(id)
107}
108
109impl BindingRegistry {
110 pub fn bindings_for(&self, contract_id: &str) -> Vec<&KernelBinding> {
112 let needle = normalize_contract_id(contract_id);
113 self.bindings
114 .iter()
115 .filter(|b| normalize_contract_id(&b.contract) == needle)
116 .collect()
117 }
118
119 pub fn find_binding(&self, contract_id: &str, equation: &str) -> Option<&KernelBinding> {
121 let needle = normalize_contract_id(contract_id);
122 self.bindings
123 .iter()
124 .find(|b| normalize_contract_id(&b.contract) == needle && b.equation == equation)
125 }
126
127 #[must_use]
140 pub fn verified(&self, source_root: &Path) -> BindingRegistry {
141 let fn_names = collect_fn_names(source_root);
142 let bindings = self
143 .bindings
144 .iter()
145 .map(|b| {
146 let mut b = b.clone();
147 if b.status == ImplStatus::Implemented && !b.function_defined_in(&fn_names) {
148 b.status = ImplStatus::NotImplemented;
149 }
150 b
151 })
152 .collect();
153 BindingRegistry {
154 version: self.version.clone(),
155 target_crate: self.target_crate.clone(),
156 critical_path: self.critical_path.clone(),
157 bindings,
158 }
159 }
160}
161
162impl KernelBinding {
163 #[must_use]
167 pub fn function_defined_in(&self, fn_names: &std::collections::HashSet<String>) -> bool {
168 self.function
169 .as_deref()
170 .is_some_and(|f| fn_names.contains(f))
171 }
172}
173
174#[must_use]
178pub fn collect_fn_names(root: &Path) -> std::collections::HashSet<String> {
179 let mut names = std::collections::HashSet::new();
180 let mut stack = vec![root.to_path_buf()];
181 while let Some(dir) = stack.pop() {
182 let Ok(entries) = std::fs::read_dir(&dir) else {
183 continue;
184 };
185 for entry in entries.flatten() {
186 let path = entry.path();
187 if path.is_dir() {
188 let skip = matches!(
189 path.file_name().and_then(|n| n.to_str()),
190 Some("target" | ".git" | ".lake" | "node_modules")
191 );
192 if !skip {
193 stack.push(path);
194 }
195 } else if path.extension().is_some_and(|e| e == "rs") {
196 if let Ok(content) = std::fs::read_to_string(&path) {
197 extract_fn_names(&content, &mut names);
198 }
199 }
200 }
201 }
202 names
203}
204
205fn extract_fn_names(content: &str, names: &mut std::collections::HashSet<String>) {
207 for line in content.lines() {
208 let mut rest = line;
209 while let Some(pos) = rest.find("fn ") {
210 let ok_boundary = pos == 0
213 || rest[..pos]
214 .chars()
215 .next_back()
216 .is_some_and(|c| !c.is_alphanumeric() && c != '_');
217 let after = &rest[pos + 3..];
218 if ok_boundary {
219 let name: String = after
220 .chars()
221 .take_while(|c| c.is_alphanumeric() || *c == '_')
222 .collect();
223 if !name.is_empty() {
224 names.insert(name);
225 }
226 }
227 rest = after;
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn parse_minimal_binding() {
238 let yaml = r#"
239version: "1.0.0"
240target_crate: aprender
241bindings: []
242"#;
243 let reg = parse_binding_str(yaml).unwrap();
244 assert_eq!(reg.version, "1.0.0");
245 assert_eq!(reg.target_crate, "aprender");
246 assert!(reg.bindings.is_empty());
247 }
248
249 #[test]
250 fn parse_binding_with_entries() {
251 let yaml = r#"
252version: "1.0.0"
253target_crate: aprender
254bindings:
255 - contract: softmax-kernel-v1.yaml
256 equation: softmax
257 module_path: "aprender::nn::functional::softmax"
258 function: softmax
259 signature: "fn softmax(x: &Tensor, dim: i32) -> Tensor"
260 status: implemented
261 - contract: activation-kernel-v1.yaml
262 equation: silu
263 status: not_implemented
264 notes: "Not yet available"
265"#;
266 let reg = parse_binding_str(yaml).unwrap();
267 assert_eq!(reg.bindings.len(), 2);
268 assert_eq!(reg.bindings[0].equation, "softmax");
269 assert_eq!(reg.bindings[0].status, ImplStatus::Implemented);
270 assert!(reg.bindings[0].module_path.is_some());
271 assert_eq!(reg.bindings[1].equation, "silu");
272 assert_eq!(reg.bindings[1].status, ImplStatus::NotImplemented);
273 assert!(reg.bindings[1].module_path.is_none());
274 }
275
276 #[test]
277 fn parse_partial_status() {
278 let yaml = r#"
279version: "1.0.0"
280target_crate: test
281bindings:
282 - contract: test.yaml
283 equation: f
284 module_path: "test::f"
285 function: f
286 status: partial
287 notes: "Only scalar path"
288"#;
289 let reg = parse_binding_str(yaml).unwrap();
290 assert_eq!(reg.bindings[0].status, ImplStatus::Partial);
291 }
292
293 #[test]
294 fn impl_status_display() {
295 assert_eq!(ImplStatus::Implemented.to_string(), "implemented");
296 assert_eq!(ImplStatus::Partial.to_string(), "partial");
297 assert_eq!(ImplStatus::NotImplemented.to_string(), "not_implemented");
298 assert_eq!(ImplStatus::Pending.to_string(), "pending");
299 }
300
301 #[test]
302 fn parse_invalid_binding_yaml() {
303 let result = parse_binding_str("not: [valid: {{");
304 assert!(result.is_err());
305 }
306
307 #[test]
308 fn parse_binding_from_file() {
309 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
310 .join("../../contracts/aprender/binding.yaml");
311 let reg = parse_binding(&path).unwrap();
312 assert_eq!(reg.target_crate, "aprender");
313 assert!(!reg.bindings.is_empty());
314 }
315
316 #[test]
317 fn parse_binding_nonexistent_file() {
318 let result = parse_binding(std::path::Path::new("/nonexistent/binding.yaml"));
319 assert!(result.is_err());
320 }
321
322 #[test]
325 fn extract_fn_names_finds_definitions() {
326 let mut names = std::collections::HashSet::new();
327 extract_fn_names(
328 "pub fn to_anthropic(m: &Message) -> Value {\n async fn helper() {}\n",
329 &mut names,
330 );
331 assert!(names.contains("to_anthropic"));
332 assert!(names.contains("helper"));
333 }
334
335 #[test]
336 fn extract_fn_names_respects_word_boundary() {
337 let mut names = std::collections::HashSet::new();
338 extract_fn_names("let my_fn foo = 1;", &mut names);
340 assert!(!names.contains("foo"));
341 }
342
343 #[test]
344 fn function_defined_in_checks_membership() {
345 let names: std::collections::HashSet<String> =
346 ["to_anthropic".to_string()].into_iter().collect();
347 let bound = KernelBinding {
348 contract: "c-v1.yaml".into(),
349 equation: "e".into(),
350 module_path: None,
351 function: Some("to_anthropic".into()),
352 signature: None,
353 status: ImplStatus::Implemented,
354 notes: None,
355 };
356 assert!(bound.function_defined_in(&names));
357
358 let missing = KernelBinding {
359 function: Some("does_not_exist".into()),
360 ..bound.clone()
361 };
362 assert!(!missing.function_defined_in(&names));
363
364 let no_fn = KernelBinding {
366 function: None,
367 ..bound
368 };
369 assert!(!no_fn.function_defined_in(&names));
370 }
371
372 #[test]
373 fn verified_downgrades_phantom_implemented_bindings() {
374 let dir = std::env::temp_dir().join(format!("bindver_{}", std::process::id()));
376 let _ = std::fs::create_dir_all(&dir);
377 std::fs::write(dir.join("lib.rs"), "pub fn real_one() {}\n").unwrap();
378
379 let reg = BindingRegistry {
380 version: "1.0.0".into(),
381 target_crate: "t".into(),
382 critical_path: vec![],
383 bindings: vec![
384 KernelBinding {
385 contract: "c-v1.yaml".into(),
386 equation: "a".into(),
387 module_path: None,
388 function: Some("real_one".into()),
389 signature: None,
390 status: ImplStatus::Implemented,
391 notes: None,
392 },
393 KernelBinding {
394 contract: "c-v1.yaml".into(),
395 equation: "b".into(),
396 module_path: None,
397 function: Some("phantom".into()),
398 signature: None,
399 status: ImplStatus::Implemented,
400 notes: None,
401 },
402 ],
403 };
404
405 let v = reg.verified(&dir);
406 assert_eq!(v.bindings[0].status, ImplStatus::Implemented);
408 assert_eq!(v.bindings[1].status, ImplStatus::NotImplemented);
409 let _ = std::fs::remove_dir_all(&dir);
410 }
411}