1use std::collections::BTreeMap;
22use std::path::{Path, PathBuf};
23
24use crate::binding::{parse_binding, BindingRegistry, ImplStatus};
25use crate::ontology::rdf::{iri, ont, Graph, Term, PROV_ENTITY, RDF_TYPE};
26
27#[must_use]
29pub fn sym(name: &str) -> String {
30 ont(&format!("sym/{name}"))
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Resolved {
36 pub file: String,
38 pub visibility: String,
40 pub kind: String,
42 pub attributes: Vec<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Unresolved {
49 pub reason: String,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct CodeStats {
55 pub registries: usize,
56 pub symbols: usize,
57 pub resolved: usize,
58 pub unresolved: usize,
59 pub files_parsed: usize,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Bound {
65 pub contract: String,
66 pub equation: String,
67 pub module_path: String,
68 pub function: String,
69 pub status: ImplStatus,
70}
71
72#[must_use]
76pub fn registries(contract_dir: &Path) -> Vec<(PathBuf, BindingRegistry)> {
77 let mut files = Vec::new();
78 let mut stack = vec![contract_dir.to_path_buf()];
79 while let Some(dir) = stack.pop() {
80 let Ok(entries) = std::fs::read_dir(&dir) else {
81 continue;
82 };
83 for path in entries.flatten().map(|e| e.path()) {
84 if path.is_dir() {
85 stack.push(path);
86 } else if path.file_name().is_some_and(|n| n == "binding.yaml") {
87 files.push(path);
88 }
89 }
90 }
91 files.sort();
92 files
93 .into_iter()
94 .filter_map(|f| parse_binding(&f).ok().map(|r| (f, r)))
95 .collect()
96}
97
98#[must_use]
101pub fn bound_of(registry: &BindingRegistry) -> Vec<Bound> {
102 registry
103 .bindings
104 .iter()
105 .filter_map(|b| {
106 let function = b.function.clone()?;
107 let mut module_path = b
108 .module_path
109 .clone()
110 .unwrap_or_else(|| registry.target_crate.clone());
111 let function = match function.rsplit_once("::") {
115 Some((qualifier, name)) => {
116 if !module_path.ends_with(&format!("::{qualifier}")) && module_path != qualifier
117 {
118 module_path = format!("{module_path}::{qualifier}");
119 }
120 name.to_string()
121 }
122 None => function,
123 };
124 let module_path = module_path
125 .strip_suffix(&format!("::{function}"))
126 .unwrap_or(&module_path)
127 .to_string();
128 Some(Bound {
129 contract: crate::binding::normalize_contract_id(&b.contract).to_string(),
130 equation: b.equation.clone(),
131 module_path,
132 function,
133 status: b.status,
134 })
135 })
136 .collect()
137}
138
139#[derive(Debug, Default)]
145pub struct Workspace {
146 pub root: PathBuf,
147 pub crates: BTreeMap<String, Vec<PathBuf>>,
148}
149
150impl Workspace {
151 #[must_use]
154 pub fn scan(root: &Path) -> Self {
155 let mut ws = Self {
156 root: root.to_path_buf(),
157 crates: BTreeMap::new(),
158 };
159 let mut stack = vec![root.to_path_buf()];
160 while let Some(dir) = stack.pop() {
161 let Ok(entries) = std::fs::read_dir(&dir) else {
162 continue;
163 };
164 let mut children: Vec<PathBuf> = entries.flatten().map(|e| e.path()).collect();
165 children.sort();
166 for path in children {
167 if path.is_dir() {
168 let skip = matches!(
169 path.file_name().and_then(|n| n.to_str()),
170 Some("target" | ".git" | ".lake" | "node_modules")
171 );
172 if !skip {
173 stack.push(path);
174 }
175 } else if path.file_name().is_some_and(|n| n == "Cargo.toml") {
176 ws.index_manifest(&path);
177 }
178 }
179 }
180 ws
181 }
182
183 fn index_manifest(&mut self, manifest: &Path) {
184 let Ok(text) = std::fs::read_to_string(manifest) else {
185 return;
186 };
187 let m = manifest_names(&text);
188 let dir = manifest.parent().unwrap_or(manifest);
189 let lib_path = m
190 .lib_path
191 .map_or_else(|| dir.join("src/lib.rs"), |p| dir.join(p));
192 for name in [m.package, m.lib].into_iter().flatten() {
193 let roots = self.crates.entry(name.replace('-', "_")).or_default();
194 if !roots.contains(&lib_path) {
195 roots.push(lib_path.clone());
196 }
197 }
198 }
199}
200
201#[derive(Debug, Default)]
202struct ManifestNames {
203 package: Option<String>,
204 lib: Option<String>,
205 lib_path: Option<String>,
206}
207
208fn manifest_names(text: &str) -> ManifestNames {
211 let mut out = ManifestNames::default();
212 let mut section = String::new();
213 for line in text.lines() {
214 let t = line.trim();
215 if t.starts_with('[') && t.ends_with(']') {
216 section = t.trim_matches(['[', ']']).trim().to_string();
217 continue;
218 }
219 let Some((k, v)) = t.split_once('=') else {
220 continue;
221 };
222 let k = k.trim();
223 let v = v.trim().trim_matches('"').to_string();
224 match (section.as_str(), k) {
225 ("package", "name") => out.package = Some(v),
226 ("lib", "name") => out.lib = Some(v),
227 ("lib", "path") => out.lib_path = Some(v),
228 _ => {}
229 }
230 }
231 out
232}
233
234pub struct Resolver<'a> {
236 ws: &'a Workspace,
237 cache: BTreeMap<PathBuf, Option<std::rc::Rc<syn::File>>>,
238}
239
240struct Module {
242 items: Vec<syn::Item>,
243 file: PathBuf,
244 child_dir: PathBuf,
245}
246
247impl<'a> Resolver<'a> {
248 #[must_use]
249 pub fn new(ws: &'a Workspace) -> Self {
250 Self {
251 ws,
252 cache: BTreeMap::new(),
253 }
254 }
255
256 #[must_use]
258 pub fn files_parsed(&self) -> usize {
259 self.cache.values().filter(|f| f.is_some()).count()
260 }
261
262 fn parse(&mut self, file: &Path) -> Option<std::rc::Rc<syn::File>> {
263 if let Some(hit) = self.cache.get(file) {
264 return hit.clone();
265 }
266 let parsed = std::fs::read_to_string(file)
267 .ok()
268 .and_then(|src| syn::parse_file(&src).ok())
269 .map(std::rc::Rc::new);
270 self.cache.insert(file.to_path_buf(), parsed.clone());
271 parsed
272 }
273
274 fn crate_roots(&self, krate: &str) -> Result<Vec<PathBuf>, Unresolved> {
276 let roots: Vec<PathBuf> = self
277 .ws
278 .crates
279 .get(&krate.replace('-', "_"))
280 .cloned()
281 .unwrap_or_default();
282 if roots.is_empty() {
283 return Err(Unresolved {
284 reason: format!("crate `{krate}` is not a workspace member"),
285 });
286 }
287 Ok(roots)
288 }
289
290 fn file_module(&mut self, file: &Path, child_dir: PathBuf) -> Result<Module, Unresolved> {
291 let Some(ast) = self.parse(file) else {
292 return Err(Unresolved {
293 reason: format!("`{}` is missing or does not parse", self.rel(file)),
294 });
295 };
296 Ok(Module {
297 items: ast.items.clone(),
298 file: file.to_path_buf(),
299 child_dir,
300 })
301 }
302
303 pub fn resolve(&mut self, module_path: &str, function: &str) -> Result<Resolved, Unresolved> {
305 self.resolve_depth(module_path, function, 0)
306 }
307
308 fn resolve_depth(
309 &mut self,
310 module_path: &str,
311 function: &str,
312 depth: usize,
313 ) -> Result<Resolved, Unresolved> {
314 if depth > 8 {
315 return Err(Unresolved {
316 reason: format!("`{module_path}::{function}`: re-export chain deeper than 8"),
317 });
318 }
319 let mut segs = module_path.split("::").filter(|s| !s.is_empty());
320 let Some(krate) = segs.next() else {
321 return Err(Unresolved {
322 reason: "empty module path".to_string(),
323 });
324 };
325 let segs: Vec<&str> = segs.collect();
326 let mut last = Unresolved {
327 reason: String::new(),
328 };
329 for root in self.crate_roots(krate)? {
330 match self.resolve_in_root(&root, &segs, function, depth) {
331 Ok(r) => return Ok(r),
332 Err(e) => last = e,
333 }
334 }
335 Err(last)
336 }
337
338 fn resolve_in_root(
340 &mut self,
341 root: &Path,
342 segs: &[&str],
343 function: &str,
344 depth: usize,
345 ) -> Result<Resolved, Unresolved> {
346 let mut module = self.file_module(root, root.parent().unwrap_or(root).to_path_buf())?;
347 for (i, seg) in segs.iter().enumerate() {
348 match self.step(&module, seg)? {
349 Step::Module(next) => module = next,
350 Step::ReExport(target) => {
351 let rest = segs[i + 1..].join("::");
352 let path = if rest.is_empty() {
353 target
354 } else {
355 format!("{target}::{rest}")
356 };
357 return self.resolve_depth(&path, function, depth + 1);
358 }
359 }
360 }
361 match find_item(&module.items, function) {
362 Some(mut r) => {
363 r.file = self.rel(&module.file);
364 Ok(r)
365 }
366 None => match use_target(&module.items, function) {
367 Some(target) => {
368 let target = absolute_use(&target, &module, self.ws);
369 self.resolve_by_use(&target, depth)
370 }
371 None => Err(Unresolved {
372 reason: format!(
373 "no `fn {function}` (free or in an impl) in `{}`",
374 self.rel(&module.file)
375 ),
376 }),
377 },
378 }
379 }
380
381 fn resolve_by_use(&mut self, target: &str, depth: usize) -> Result<Resolved, Unresolved> {
382 let (path, name) = target.rsplit_once("::").ok_or_else(|| Unresolved {
383 reason: format!("re-export `{target}` has no module path"),
384 })?;
385 self.resolve_depth(path, name, depth + 1)
386 }
387
388 fn step(&mut self, module: &Module, seg: &str) -> Result<Step, Unresolved> {
389 for item in &module.items {
390 if let syn::Item::Mod(m) = item {
391 if m.ident != seg {
392 continue;
393 }
394 if let Some((_, content)) = &m.content {
395 return Ok(Step::Module(Module {
396 items: content.clone(),
397 file: module.file.clone(),
398 child_dir: module.child_dir.join(seg),
399 }));
400 }
401 let (file, child_dir) = child_file(&module.child_dir, seg, &m.attrs)?;
402 return self.file_module(&file, child_dir).map(Step::Module);
403 }
404 }
405 if let Some(target) = use_target(&module.items, seg) {
406 let target = absolute_use(&target, module, self.ws);
407 return Ok(Step::ReExport(target));
408 }
409 Err(Unresolved {
410 reason: format!(
411 "no `mod {seg}` or `use … {seg}` in `{}`",
412 self.rel(&module.file)
413 ),
414 })
415 }
416
417 fn rel(&self, file: &Path) -> String {
418 file.strip_prefix(&self.ws.root)
419 .unwrap_or(file)
420 .to_string_lossy()
421 .replace('\\', "/")
422 }
423}
424
425enum Step {
426 Module(Module),
427 ReExport(String),
428}
429
430fn child_file(
433 child_dir: &Path,
434 seg: &str,
435 attrs: &[syn::Attribute],
436) -> Result<(PathBuf, PathBuf), Unresolved> {
437 for a in attrs {
438 if a.path().is_ident("path") {
439 if let syn::Meta::NameValue(nv) = &a.meta {
440 if let syn::Expr::Lit(syn::ExprLit {
441 lit: syn::Lit::Str(s),
442 ..
443 }) = &nv.value
444 {
445 let f = child_dir.join(s.value());
446 return Ok((f.clone(), f.parent().unwrap_or(&f).to_path_buf()));
447 }
448 }
449 }
450 }
451 let flat = child_dir.join(format!("{seg}.rs"));
452 if flat.is_file() {
453 return Ok((flat, child_dir.join(seg)));
454 }
455 let nested = child_dir.join(seg).join("mod.rs");
456 if nested.is_file() {
457 return Ok((nested, child_dir.join(seg)));
458 }
459 Err(Unresolved {
460 reason: format!(
461 "`mod {seg};` declared but neither `{}` nor `{}` exists",
462 flat.display(),
463 nested.display()
464 ),
465 })
466}
467
468fn find_item(items: &[syn::Item], name: &str) -> Option<Resolved> {
470 items.iter().find_map(|item| match item {
471 syn::Item::Fn(f) if f.sig.ident == name => Some(found("fn", &f.vis, &f.attrs)),
472 syn::Item::Impl(im) => find_method(&im.items, name),
473 _ => None,
474 })
475}
476
477fn find_method(items: &[syn::ImplItem], name: &str) -> Option<Resolved> {
479 items.iter().find_map(|ii| match ii {
480 syn::ImplItem::Fn(f) if f.sig.ident == name => Some(found("method", &f.vis, &f.attrs)),
481 _ => None,
482 })
483}
484
485fn found(kind: &str, vis: &syn::Visibility, attrs: &[syn::Attribute]) -> Resolved {
487 Resolved {
488 file: String::new(),
489 visibility: visibility_of(vis),
490 kind: kind.to_string(),
491 attributes: attr_paths(attrs),
492 }
493}
494
495fn use_target(items: &[syn::Item], name: &str) -> Option<String> {
498 for item in items {
499 if let syn::Item::Use(u) = item {
500 if let Some(t) = use_tree_target(&u.tree, name, "") {
501 return Some(t);
502 }
503 }
504 }
505 None
506}
507
508fn use_tree_target(tree: &syn::UseTree, name: &str, prefix: &str) -> Option<String> {
509 let join = |p: &str, s: &str| {
510 if p.is_empty() {
511 s.to_string()
512 } else {
513 format!("{p}::{s}")
514 }
515 };
516 match tree {
517 syn::UseTree::Path(p) => {
518 use_tree_target(&p.tree, name, &join(prefix, &p.ident.to_string()))
519 }
520 syn::UseTree::Name(n) if n.ident == name => Some(join(prefix, &n.ident.to_string())),
521 syn::UseTree::Rename(r) if r.rename == name => Some(join(prefix, &r.ident.to_string())),
522 syn::UseTree::Group(g) => g
523 .items
524 .iter()
525 .find_map(|t| use_tree_target(t, name, prefix)),
526 _ => None,
527 }
528}
529
530fn absolute_use(target: &str, module: &Module, ws: &Workspace) -> String {
533 let here = module_path_of(module, ws);
534 let krate = here.split("::").next().unwrap_or_default().to_string();
535 let (head, rest) = target.split_once("::").unwrap_or((target, ""));
536 let base = match head {
537 "crate" => krate,
538 "self" => here.clone(),
539 "super" => here
540 .rsplit_once("::")
541 .map_or(here.clone(), |(p, _)| p.to_string()),
542 other if ws.crates.contains_key(&other.replace('-', "_")) => other.to_string(),
543 other => format!("{here}::{other}"),
544 };
545 if rest.is_empty() {
546 base
547 } else {
548 format!("{base}::{rest}")
549 }
550}
551
552fn module_path_of(module: &Module, ws: &Workspace) -> String {
555 let mut best: Option<(usize, String)> = None;
558 for (name, roots) in &ws.crates {
559 for root in roots {
560 let Some(src) = root.parent() else { continue };
561 let Ok(rel) = module.child_dir.strip_prefix(src) else {
562 continue;
563 };
564 let depth = src.components().count();
565 if best.as_ref().is_some_and(|(d, _)| *d >= depth) {
566 continue;
567 }
568 let tail: Vec<String> = rel
569 .components()
570 .map(|c| c.as_os_str().to_string_lossy().to_string())
571 .collect();
572 let path = if tail.is_empty() {
573 name.clone()
574 } else {
575 format!("{name}::{}", tail.join("::"))
576 };
577 best = Some((depth, path));
578 }
579 }
580 best.map(|(_, p)| p).unwrap_or_default()
581}
582
583fn visibility_of(v: &syn::Visibility) -> String {
584 match v {
585 syn::Visibility::Public(_) => "pub".to_string(),
586 syn::Visibility::Restricted(r) => {
587 let p = r
588 .path
589 .segments
590 .iter()
591 .map(|s| s.ident.to_string())
592 .collect::<Vec<_>>()
593 .join("::");
594 format!("pub({p})")
595 }
596 syn::Visibility::Inherited => "private".to_string(),
597 }
598}
599
600fn attr_paths(attrs: &[syn::Attribute]) -> Vec<String> {
601 attrs
602 .iter()
603 .map(|a| {
604 a.path()
605 .segments
606 .iter()
607 .map(|s| s.ident.to_string())
608 .collect::<Vec<_>>()
609 .join("::")
610 })
611 .collect()
612}
613
614#[must_use]
616pub fn symbol_iri(b: &Bound) -> String {
617 iri("symbol", &format!("{}::{}", b.module_path, b.function))
618}
619
620pub fn emit(g: &mut Graph, b: &Bound, found: &Result<Resolved, Unresolved>) {
622 let s = symbol_iri(b);
623 g.insert(s.clone(), RDF_TYPE, Term::iri(ont("Symbol")));
624 g.insert(s.clone(), RDF_TYPE, Term::iri(PROV_ENTITY));
625 let krate = b.module_path.split("::").next().unwrap_or_default();
626 g.insert(s.clone(), sym("crate"), Term::string(krate));
627 g.insert(s.clone(), sym("module"), Term::string(&b.module_path));
628 g.insert(s.clone(), sym("name"), Term::string(&b.function));
629 g.insert(
630 s.clone(),
631 sym("implements"),
632 Term::iri(iri("contract", &b.contract)),
633 );
634 g.insert(s.clone(), sym("equation"), Term::string(&b.equation));
635 g.insert(
636 s.clone(),
637 sym("bindingStatus"),
638 Term::string(format!("{:?}", b.status).to_lowercase()),
639 );
640 match found {
641 Ok(r) => {
642 g.insert(s.clone(), sym("resolved"), Term::boolean(true));
643 g.insert(s.clone(), sym("file"), Term::string(&r.file));
644 g.insert(s.clone(), sym("visibility"), Term::string(&r.visibility));
645 g.insert(s.clone(), sym("kind"), Term::string(&r.kind));
646 for a in &r.attributes {
647 g.insert(s.clone(), sym("attribute"), Term::string(a));
648 }
649 }
650 Err(u) => {
651 g.insert(s.clone(), sym("resolved"), Term::boolean(false));
652 g.insert(s, sym("unresolvedReason"), Term::string(&u.reason));
653 }
654 }
655}
656
657pub fn extract(contract_dir: &Path, g: &mut Graph) -> CodeStats {
660 let root_buf = super::repo_root(contract_dir);
661 let root = root_buf.as_path();
662 let ws = Workspace::scan(root);
663 let mut resolver = Resolver::new(&ws);
664 let mut stats = CodeStats::default();
665 for (_file, registry) in registries(contract_dir) {
666 stats.registries += 1;
667 for b in bound_of(®istry) {
668 let found = resolver.resolve(&b.module_path, &b.function);
669 if found.is_ok() {
670 stats.resolved += 1;
671 } else {
672 stats.unresolved += 1;
673 }
674 stats.symbols += 1;
675 emit(g, &b, &found);
676 }
677 }
678 stats.files_parsed = resolver.files_parsed();
679 stats
680}
681
682#[must_use]
686pub fn positive_control() -> bool {
687 let src = "pub mod m { pub fn present() {} }\npub use m::present as alias;";
688 let Ok(ast) = syn::parse_file(src) else {
689 return false;
690 };
691 let present = find_item(&ast.items, "present").is_none()
692 && match ast.items.first() {
693 Some(syn::Item::Mod(m)) => m
694 .content
695 .as_ref()
696 .is_some_and(|(_, items)| find_item(items, "present").is_some()),
697 _ => false,
698 };
699 let ghost =
700 find_item(&ast.items, "absent").is_none() && use_target(&ast.items, "absent").is_none();
701 let aliased = use_target(&ast.items, "alias").as_deref() == Some("m::present");
702 present && ghost && aliased
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 fn fixture() -> PathBuf {
710 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/ont/code-bound")
711 }
712
713 #[test]
714 fn the_manifest_scan_indexes_package_and_lib_names() {
715 let ws = Workspace::scan(&fixture());
716 assert!(ws.crates.contains_key("kern"), "{:?}", ws.crates);
717 assert!(ws.crates.contains_key("kern_crate"), "{:?}", ws.crates);
718 assert!(ws.crates["kern"][0].ends_with("crates/kern/src/lib.rs"));
719 }
720
721 #[test]
722 fn the_walk_resolves_files_inline_modules_methods_and_re_exports_and_refuses_a_ghost() {
723 let ws = Workspace::scan(&fixture());
724 let mut r = Resolver::new(&ws);
725 let softmax = r
726 .resolve("kern::nn::functional", "softmax")
727 .expect("softmax");
728 assert_eq!(softmax.file, "crates/kern/src/nn/functional.rs");
729 assert_eq!(softmax.visibility, "pub");
730 assert_eq!(softmax.kind, "fn");
731 assert_eq!(softmax.attributes, vec!["inline".to_string()]);
732 let relu = r.resolve("kern::nn::functional", "relu").expect("relu");
733 assert_eq!(relu.visibility, "pub(crate)");
734 let forward = r.resolve("kern::nn", "forward").expect("forward");
735 assert_eq!(forward.kind, "method");
736 assert_eq!(forward.file, "crates/kern/src/nn.rs");
737 let helper = r
738 .resolve("kern", "helper")
739 .expect("helper via pub use … as");
740 assert_eq!(helper.file, "crates/kern/src/lib.rs");
741 assert_eq!(helper.visibility, "pub");
742 let kernel = r
743 .resolve("kern::nn::functional", "gated_rmsnorm")
744 .expect("kernel");
745 assert_eq!(kernel.attributes, vec!["kernel".to_string()]);
746 let ghost = r
747 .resolve("kern::nn::functional", "no_such_function")
748 .unwrap_err();
749 assert!(
750 ghost.reason.contains("no `fn no_such_function`"),
751 "{}",
752 ghost.reason
753 );
754 assert!(
755 ghost.reason.contains("crates/kern/src/nn/functional.rs"),
756 "{}",
757 ghost.reason
758 );
759 let no_crate = r.resolve("nowhere::x", "f").unwrap_err();
760 assert!(no_crate.reason.contains("not a workspace member"));
761 let no_mod = r.resolve("kern::nope", "f").unwrap_err();
762 assert!(no_mod.reason.contains("no `mod nope`"), "{}", no_mod.reason);
763 assert_eq!(r.files_parsed(), 3);
764 }
765
766 #[test]
767 fn the_graph_carries_every_bound_symbol_resolved_or_not_and_is_deterministic() {
768 let dir = fixture().join("contracts");
769 let mut g = Graph::new();
770 let stats = extract(&dir, &mut g);
771 assert_eq!(stats.registries, 1);
772 assert_eq!(stats.symbols, 6);
773 assert_eq!(stats.resolved, 5);
774 assert_eq!(stats.unresolved, 1);
775 let nt = g.to_ntriples();
776 assert!(nt.contains("/symbol/kern::nn::functional::softmax> <https://ont.paiml.dev/v1alpha1/sym/resolved> \"true\""), "{nt}");
777 assert!(nt.contains("/symbol/kern::nn::functional::no_such_function> <https://ont.paiml.dev/v1alpha1/sym/resolved> \"false\""), "{nt}");
778 assert!(
779 nt.contains("sym/unresolvedReason> \"no `fn no_such_function`"),
780 "{nt}"
781 );
782 assert!(
783 nt.contains(
784 "sym/implements> <https://ont.paiml.dev/v1alpha1/contract/softmax-kernel-v1>"
785 ),
786 "{nt}"
787 );
788 assert!(nt.contains("sym/attribute> \"kernel\""), "{nt}");
789 assert!(!nt.contains("_:"));
790 let mut g2 = Graph::new();
791 extract(&dir, &mut g2);
792 assert_eq!(nt, g2.to_ntriples());
793 }
794
795 #[test]
796 fn a_trailing_segment_equal_to_the_function_is_dropped_from_the_module_path() {
797 let reg = parse_binding(&fixture().join("contracts/binding.yaml")).expect("registry");
798 let bound = bound_of(®);
799 let softmax = bound
800 .iter()
801 .find(|b| b.function == "softmax")
802 .expect("softmax");
803 assert_eq!(softmax.module_path, "kern::nn::functional");
804 assert_eq!(
805 symbol_iri(softmax),
806 "https://ont.paiml.dev/v1alpha1/symbol/kern::nn::functional::softmax"
807 );
808 }
809
810 #[test]
811 fn the_positive_control_fires() {
812 assert!(positive_control());
813 }
814
815 #[test]
816 fn manifest_names_reads_package_and_lib_sections_only() {
817 let m = manifest_names("[package]\nname = \"a-b\"\n[dependencies]\nname = \"x\"\n[lib]\nname = \"ab\"\npath = \"src/x.rs\"\n");
818 assert_eq!(m.package.as_deref(), Some("a-b"));
819 assert_eq!(m.lib.as_deref(), Some("ab"));
820 assert_eq!(m.lib_path.as_deref(), Some("src/x.rs"));
821 }
822}