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 pub unsafe_free: bool,
46 pub bounds_checked: bool,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Unresolved {
53 pub reason: String,
54}
55
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
58pub struct CodeStats {
59 pub registries: usize,
60 pub symbols: usize,
61 pub resolved: usize,
62 pub unresolved: usize,
63 pub files_parsed: usize,
64 pub crates_scanned: usize,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Bound {
72 pub contract: String,
73 pub equation: String,
74 pub module_path: String,
75 pub function: String,
76 pub status: ImplStatus,
77}
78
79#[must_use]
83pub fn registries(contract_dir: &Path) -> Vec<(PathBuf, BindingRegistry)> {
84 let mut files = Vec::new();
85 let mut stack = vec![contract_dir.to_path_buf()];
86 while let Some(dir) = stack.pop() {
87 let Ok(entries) = std::fs::read_dir(&dir) else {
88 continue;
89 };
90 for path in entries.flatten().map(|e| e.path()) {
91 if path.is_dir() {
92 stack.push(path);
93 } else if path.file_name().is_some_and(|n| n == "binding.yaml") {
94 files.push(path);
95 }
96 }
97 }
98 files.sort();
99 files
100 .into_iter()
101 .filter_map(|f| parse_binding(&f).ok().map(|r| (f, r)))
102 .collect()
103}
104
105#[must_use]
108pub fn bound_of(registry: &BindingRegistry) -> Vec<Bound> {
109 registry
110 .bindings
111 .iter()
112 .filter_map(|b| {
113 let function = b.function.clone()?;
114 let mut module_path = b
115 .module_path
116 .clone()
117 .unwrap_or_else(|| registry.target_crate.clone());
118 let function = match function.rsplit_once("::") {
122 Some((qualifier, name)) => {
123 if !module_path.ends_with(&format!("::{qualifier}")) && module_path != qualifier
124 {
125 module_path = format!("{module_path}::{qualifier}");
126 }
127 name.to_string()
128 }
129 None => function,
130 };
131 let module_path = module_path
132 .strip_suffix(&format!("::{function}"))
133 .unwrap_or(&module_path)
134 .to_string();
135 Some(Bound {
136 contract: crate::binding::normalize_contract_id(&b.contract).to_string(),
137 equation: b.equation.clone(),
138 module_path,
139 function,
140 status: b.status,
141 })
142 })
143 .collect()
144}
145
146#[derive(Debug, Default)]
152pub struct Workspace {
153 pub root: PathBuf,
154 pub crates: BTreeMap<String, Vec<PathBuf>>,
155 pub is_workspace_root: bool,
159 pub standalone: BTreeMap<String, Vec<PathBuf>>,
163}
164
165impl Workspace {
166 #[must_use]
169 pub fn scan(root: &Path) -> Self {
170 let membership = std::fs::read_to_string(root.join("Cargo.toml"))
171 .ok()
172 .and_then(|t| workspace_membership(&t));
173 let mut ws = Self {
174 root: root.to_path_buf(),
175 crates: BTreeMap::new(),
176 is_workspace_root: membership.is_some(),
177 standalone: BTreeMap::new(),
178 };
179 let mut stack = vec![root.to_path_buf()];
180 while let Some(dir) = stack.pop() {
181 let Ok(entries) = std::fs::read_dir(&dir) else {
182 continue;
183 };
184 let mut children: Vec<PathBuf> = entries.flatten().map(|e| e.path()).collect();
185 children.sort();
186 for path in children {
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.file_name().is_some_and(|n| n == "Cargo.toml") {
196 let dir = path.parent().unwrap_or(root);
197 if membership.as_ref().is_none_or(|m| m.admits(root, dir)) {
198 ws.index_manifest(&path);
199 } else if dir != root {
200 ws.index_standalone(&path);
201 }
202 }
203 }
204 }
205 ws
206 }
207
208 fn index_manifest(&mut self, manifest: &Path) {
209 if let Ok(text) = std::fs::read_to_string(manifest) {
210 index_names(&mut self.crates, manifest, &text);
211 }
212 }
213
214 fn index_standalone(&mut self, manifest: &Path) {
217 if let Ok(text) = std::fs::read_to_string(manifest) {
218 if workspace_membership(&text).is_some() {
219 index_names(&mut self.standalone, manifest, &text);
220 }
221 }
222 }
223
224 pub fn admit_standalone(&mut self, krate: &str) -> bool {
227 if self.crates.contains_key(krate) {
228 return false;
229 }
230 match self.standalone.get(krate) {
231 Some(roots) => {
232 self.crates.insert(krate.to_string(), roots.clone());
233 true
234 }
235 None => false,
236 }
237 }
238}
239
240fn index_names(map: &mut BTreeMap<String, Vec<PathBuf>>, manifest: &Path, text: &str) {
242 let m = manifest_names(text);
243 let dir = manifest.parent().unwrap_or(manifest);
244 let lib_path = m
245 .lib_path
246 .map_or_else(|| dir.join("src/lib.rs"), |p| dir.join(p));
247 for name in [m.package, m.lib].into_iter().flatten() {
248 let roots = map.entry(name.replace('-', "_")).or_default();
249 if !roots.contains(&lib_path) {
250 roots.push(lib_path.clone());
251 }
252 }
253}
254
255#[derive(Debug, Default, PartialEq, Eq)]
257pub(crate) struct Membership {
258 members: Vec<String>,
259 exclude: Vec<String>,
260 root_package: bool,
261}
262
263impl Membership {
264 pub(crate) fn admits(&self, root: &Path, dir: &Path) -> bool {
266 let rel = dir
267 .strip_prefix(root)
268 .unwrap_or(dir)
269 .to_string_lossy()
270 .replace('\\', "/");
271 if rel.is_empty() {
272 return self.root_package || self.members.iter().any(|m| m == ".");
273 }
274 self.members.iter().any(|m| glob_match(m, &rel))
275 && !self.exclude.iter().any(|e| glob_match(e, &rel))
276 }
277}
278
279pub(crate) fn workspace_membership(text: &str) -> Option<Membership> {
282 let mut out = Membership::default();
283 let mut section = String::new();
284 let mut seen = false;
285 let mut open: Option<bool> = None; for line in text.lines() {
287 let t = line.split('#').next().unwrap_or_default().trim();
288 if let Some(name) = open.is_none().then(|| section_name(t)).flatten() {
289 seen |= name == "workspace";
290 out.root_package |= name == "package";
291 section = name;
292 continue;
293 }
294 let Some(body) = list_body(t, &mut open, section == "workspace") else {
295 continue;
296 };
297 let list = if open == Some(true) {
298 &mut out.members
299 } else {
300 &mut out.exclude
301 };
302 list.extend(quoted(body));
303 if body.contains(']') {
304 open = None;
305 }
306 }
307 seen.then_some(out)
308}
309
310fn section_name(t: &str) -> Option<String> {
312 (t.starts_with('[') && t.ends_with(']') && !t.contains('='))
313 .then(|| t.trim_matches(['[', ']']).trim().to_string())
314}
315
316fn list_body<'t>(t: &'t str, open: &mut Option<bool>, in_workspace: bool) -> Option<&'t str> {
319 if open.is_some() {
320 return Some(t);
321 }
322 if !in_workspace {
323 return None;
324 }
325 let (k, v) = t.split_once('=')?;
326 let k = k.trim();
327 if !matches!(k, "members" | "exclude") {
328 return None;
329 }
330 *open = Some(k == "members");
331 Some(v)
332}
333
334fn quoted(line: &str) -> Vec<String> {
336 let mut out = Vec::new();
337 let mut rest = line;
338 while let Some(start) = rest.find(['"', '\'']) {
339 let q = rest[start..].chars().next().unwrap_or('"');
340 let tail = &rest[start + 1..];
341 let Some(end) = tail.find(q) else { break };
342 out.push(tail[..end].trim_end_matches('/').to_string());
343 rest = &tail[end + 1..];
344 }
345 out
346}
347
348fn glob_match(pattern: &str, rel: &str) -> bool {
350 let p: Vec<&str> = pattern.trim_start_matches("./").split('/').collect();
351 let r: Vec<&str> = rel.split('/').collect();
352 p.len() == r.len()
353 && p.iter().zip(&r).all(|(p, r)| match p.split_once('*') {
354 None => p == r,
355 Some((pre, suf)) => {
356 r.len() >= pre.len() + suf.len() && r.starts_with(pre) && r.ends_with(suf)
357 }
358 })
359}
360
361#[derive(Debug, Default)]
362pub(crate) struct ManifestNames {
363 pub(crate) package: Option<String>,
364 lib: Option<String>,
365 lib_path: Option<String>,
366}
367
368pub(crate) fn manifest_names(text: &str) -> ManifestNames {
371 let mut out = ManifestNames::default();
372 let mut section = String::new();
373 for line in text.lines() {
374 let t = line.trim();
375 if t.starts_with('[') && t.ends_with(']') {
376 section = t.trim_matches(['[', ']']).trim().to_string();
377 continue;
378 }
379 let Some((k, v)) = t.split_once('=') else {
380 continue;
381 };
382 let k = k.trim();
383 let v = v.trim().trim_matches('"').to_string();
384 match (section.as_str(), k) {
385 ("package", "name") => out.package = Some(v),
386 ("lib", "name") => out.lib = Some(v),
387 ("lib", "path") => out.lib_path = Some(v),
388 _ => {}
389 }
390 }
391 out
392}
393
394pub struct Resolver<'a> {
396 ws: &'a Workspace,
397 cache: BTreeMap<PathBuf, Option<std::rc::Rc<syn::File>>>,
398 crates_walked: std::collections::BTreeSet<PathBuf>,
400 seen: std::collections::BTreeSet<(String, String)>,
403}
404
405struct Module {
409 items: Vec<syn::Item>,
410 origins: Vec<PathBuf>,
411 file: PathBuf,
412 child_dir: PathBuf,
413 includes: Vec<(String, PathBuf)>,
417}
418
419const INCLUDE_DEPTH: usize = 8;
421
422impl<'a> Resolver<'a> {
423 #[must_use]
424 pub fn new(ws: &'a Workspace) -> Self {
425 Self {
426 ws,
427 cache: BTreeMap::new(),
428 crates_walked: std::collections::BTreeSet::new(),
429 seen: std::collections::BTreeSet::new(),
430 }
431 }
432
433 #[must_use]
435 pub fn crates_walked(&self) -> usize {
436 self.crates_walked.len()
437 }
438
439 #[must_use]
441 pub fn files_parsed(&self) -> usize {
442 self.cache.values().filter(|f| f.is_some()).count()
443 }
444
445 fn parse(&mut self, file: &Path) -> Option<std::rc::Rc<syn::File>> {
446 if let Some(hit) = self.cache.get(file) {
447 return hit.clone();
448 }
449 let parsed = std::fs::read_to_string(file)
450 .ok()
451 .and_then(|src| syn::parse_file(&src).ok())
452 .map(std::rc::Rc::new);
453 self.cache.insert(file.to_path_buf(), parsed.clone());
454 parsed
455 }
456
457 fn crate_roots(&self, krate: &str) -> Result<Vec<PathBuf>, Unresolved> {
459 let roots: Vec<PathBuf> = self
460 .ws
461 .crates
462 .get(&krate.replace('-', "_"))
463 .cloned()
464 .unwrap_or_default();
465 if roots.is_empty() {
466 return Err(Unresolved {
467 reason: format!("crate `{krate}` is not a workspace member"),
468 });
469 }
470 Ok(roots)
471 }
472
473 fn file_module(&mut self, file: &Path, child_dir: PathBuf) -> Result<Module, Unresolved> {
474 let Some(ast) = self.parse(file) else {
475 return Err(Unresolved {
476 reason: format!("`{}` is missing or does not parse", self.rel(file)),
477 });
478 };
479 Ok(self.module_of(&ast.items, file, child_dir))
480 }
481
482 fn module_of(&mut self, items: &[syn::Item], file: &Path, child_dir: PathBuf) -> Module {
484 let mut module = Module {
485 items: Vec::new(),
486 origins: Vec::new(),
487 file: file.to_path_buf(),
488 child_dir,
489 includes: Vec::new(),
490 };
491 self.splice(items, file, 0, &mut module);
492 module
493 }
494
495 fn splice(&mut self, items: &[syn::Item], origin: &Path, depth: usize, module: &mut Module) {
499 for item in items {
500 match include_path(item) {
501 Some(rel) if depth < INCLUDE_DEPTH => {
502 let file = origin.parent().unwrap_or(origin).join(rel);
503 if let Some(ast) = self.parse(&file) {
504 if let Some(stem) = file.file_stem().and_then(|s| s.to_str()) {
505 module.includes.push((stem.to_string(), file.clone()));
506 }
507 self.splice(&ast.items, &file, depth + 1, module);
508 }
509 }
510 _ => {
511 module.items.push(item.clone());
512 module.origins.push(origin.to_path_buf());
513 }
514 }
515 }
516 }
517
518 pub fn resolve(&mut self, module_path: &str, function: &str) -> Result<Resolved, Unresolved> {
520 self.seen.clear();
521 self.resolve_depth(module_path, function, 0)
522 }
523
524 fn resolve_depth(
525 &mut self,
526 module_path: &str,
527 function: &str,
528 depth: usize,
529 ) -> Result<Resolved, Unresolved> {
530 if depth > 8 {
531 return Err(Unresolved {
532 reason: format!("`{module_path}::{function}`: re-export chain deeper than 8"),
533 });
534 }
535 if !self
536 .seen
537 .insert((module_path.to_string(), function.to_string()))
538 {
539 return Err(Unresolved {
540 reason: format!("`{module_path}::{function}`: re-export cycle"),
541 });
542 }
543 let mut segs = module_path.split("::").filter(|s| !s.is_empty());
544 let Some(krate) = segs.next() else {
545 return Err(Unresolved {
546 reason: "empty module path".to_string(),
547 });
548 };
549 let segs: Vec<&str> = segs.collect();
550 let mut last = Unresolved {
551 reason: String::new(),
552 };
553 for root in self.crate_roots(krate)? {
554 match self.resolve_in_root(&root, &segs, function, depth) {
555 Ok(r) => return Ok(r),
556 Err(e) => last = e,
557 }
558 }
559 Err(last)
560 }
561
562 fn resolve_in_root(
564 &mut self,
565 root: &Path,
566 segs: &[&str],
567 function: &str,
568 depth: usize,
569 ) -> Result<Resolved, Unresolved> {
570 let mut module = self.file_module(root, root.parent().unwrap_or(root).to_path_buf())?;
571 self.crates_walked.insert(root.to_path_buf());
572 for (i, seg) in segs.iter().enumerate() {
573 let rest = &segs[i + 1..];
574 match self.step(&module, seg)? {
575 Step::Module(next) => module = next,
576 Step::ReExport(target) => {
577 return self.re_export(&module, seg, rest, &target, function, depth);
578 }
579 Step::Globs(targets) => {
580 return self.first_of(&targets, seg, rest, function, depth, &module);
581 }
582 Step::Type => return self.type_member(&module, seg, rest, function),
583 }
584 }
585 self.resolve_leaf(&module, function, depth)
586 }
587
588 fn re_export(
591 &mut self,
592 module: &Module,
593 seg: &str,
594 rest: &[&str],
595 target: &str,
596 function: &str,
597 depth: usize,
598 ) -> Result<Resolved, Unresolved> {
599 if let Ok(r) = self.type_member(module, seg, rest, function) {
600 return Ok(r);
601 }
602 self.resolve_depth(&join_path(target, rest), function, depth + 1)
603 }
604
605 fn resolve_leaf(
607 &mut self,
608 module: &Module,
609 function: &str,
610 depth: usize,
611 ) -> Result<Resolved, Unresolved> {
612 if let Some((i, mut r)) = find_indexed(&module.items, function) {
613 r.file = self.rel(&module.origins[i]);
614 return Ok(r);
615 }
616 if let Some(target) = use_target(&module.items, function) {
617 let target = absolute_use(&target, module, self.ws);
618 return self.resolve_by_use(&target, depth);
619 }
620 let globs = pub_globs(&module.items, module, self.ws);
621 if globs.is_empty() {
622 return Err(self.not_found(function, module));
623 }
624 let mut last = self.not_found(function, module);
625 for g in globs {
626 match self.resolve_depth(&g, function, depth + 1) {
627 Ok(r) => return Ok(r),
628 Err(e) => last = e,
629 }
630 }
631 Err(last)
632 }
633
634 fn not_found(&self, function: &str, module: &Module) -> Unresolved {
635 Unresolved {
636 reason: format!(
637 "no `fn {function}` (free or in an impl) or item `{function}` in `{}`",
638 self.rel(&module.file)
639 ),
640 }
641 }
642
643 fn first_of(
645 &mut self,
646 globs: &[String],
647 seg: &str,
648 rest: &[&str],
649 function: &str,
650 depth: usize,
651 module: &Module,
652 ) -> Result<Resolved, Unresolved> {
653 let mut last = Unresolved {
654 reason: format!(
655 "no `mod {seg}` or `use … {seg}` in `{}` (nor under its glob re-exports)",
656 self.rel(&module.file)
657 ),
658 };
659 for g in globs {
660 let path = join_path(&format!("{g}::{seg}"), rest);
661 match self.resolve_depth(&path, function, depth + 1) {
662 Ok(r) => return Ok(r),
663 Err(e) if e.reason.contains("re-export cycle") => {}
664 Err(e) => last = e,
665 }
666 }
667 Err(last)
668 }
669
670 fn type_member(
674 &self,
675 module: &Module,
676 ty: &str,
677 rest: &[&str],
678 function: &str,
679 ) -> Result<Resolved, Unresolved> {
680 let generic = match rest {
681 [] => None,
682 [g] if g.starts_with('<') => Some(g.trim_matches(['<', '>']).trim()),
683 _ => {
684 return Err(Unresolved {
685 reason: format!(
686 "`{ty}` in `{}` is a type, not a module",
687 self.rel(&module.file)
688 ),
689 })
690 }
691 };
692 for (i, item) in module.items.iter().enumerate() {
693 let hit = match item {
694 syn::Item::Impl(im) if impl_is_for(im, ty, generic) => {
695 find_method(&im.items, function)
696 }
697 syn::Item::Trait(t) if t.ident == ty && generic.is_none() => {
698 find_trait_fn(&t.items, function)
699 }
700 _ => None,
701 };
702 if let Some(mut r) = hit {
703 r.file = self.rel(&module.origins[i]);
704 return Ok(r);
705 }
706 }
707 Err(Unresolved {
708 reason: format!(
709 "no `fn {function}` in an `impl {ty}` or `trait {ty}` in `{}`",
710 self.rel(&module.file)
711 ),
712 })
713 }
714
715 fn resolve_by_use(&mut self, target: &str, depth: usize) -> Result<Resolved, Unresolved> {
716 let (path, name) = target.rsplit_once("::").ok_or_else(|| Unresolved {
717 reason: format!("re-export `{target}` has no module path"),
718 })?;
719 self.resolve_depth(path, name, depth + 1)
720 }
721
722 fn step(&mut self, module: &Module, seg: &str) -> Result<Step, Unresolved> {
723 if let Some(step) = self.step_into_mod(module, seg) {
724 return step;
725 }
726 if let Some(target) = use_target(&module.items, seg) {
727 let target = absolute_use(&target, module, self.ws);
728 return Ok(Step::ReExport(target));
729 }
730 if defines_type(&module.items, seg) {
731 return Ok(Step::Type);
732 }
733 if let Some(step) = self.step_into_include(module, seg) {
734 return step;
735 }
736 let globs = pub_globs(&module.items, module, self.ws);
737 if !globs.is_empty() {
738 return Ok(Step::Globs(globs));
739 }
740 Err(Unresolved {
741 reason: format!(
742 "no `mod {seg}` or `use … {seg}` in `{}`",
743 self.rel(&module.file)
744 ),
745 })
746 }
747
748 fn step_into_mod(&mut self, module: &Module, seg: &str) -> Option<Result<Step, Unresolved>> {
750 let (i, m) = module
751 .items
752 .iter()
753 .enumerate()
754 .find_map(|(i, item)| match item {
755 syn::Item::Mod(m) if m.ident == seg => Some((i, m)),
756 _ => None,
757 })?;
758 if let Some((_, content)) = &m.content {
759 let origin = module.origins[i].clone();
760 let mut inner = self.module_of(content, &origin, module.child_dir.join(seg));
761 inner.file = module.file.clone();
762 return Some(Ok(Step::Module(inner)));
763 }
764 Some(
765 child_file(&module.child_dir, seg, &m.attrs)
766 .and_then(|(file, child_dir)| self.file_module(&file, child_dir).map(Step::Module)),
767 )
768 }
769
770 fn step_into_include(
777 &mut self,
778 module: &Module,
779 seg: &str,
780 ) -> Option<Result<Step, Unresolved>> {
781 let file = match module.includes.iter().find(|(stem, _)| stem == seg) {
782 Some((_, file)) => file.clone(),
783 None => self.included_by_child(module, seg)?,
784 };
785 Some(
786 self.file_module(&file, module.child_dir.clone())
787 .map(Step::Module),
788 )
789 }
790
791 fn included_by_child(&mut self, module: &Module, seg: &str) -> Option<PathBuf> {
793 let target = module.child_dir.join(format!("{seg}.rs"));
794 if !target.is_file() {
795 return None;
796 }
797 let children: Vec<PathBuf> = module
798 .items
799 .iter()
800 .filter_map(|item| match item {
801 syn::Item::Mod(m) if m.content.is_none() => {
802 child_file(&module.child_dir, &m.ident.to_string(), &m.attrs)
803 .ok()
804 .map(|(file, _)| file)
805 }
806 _ => None,
807 })
808 .filter(|file| file.parent() == Some(module.child_dir.as_path()))
809 .collect();
810 for child in children {
811 let Some(ast) = self.parse(&child) else {
812 continue;
813 };
814 let dir = child.parent().unwrap_or(&child).to_path_buf();
815 if ast
816 .items
817 .iter()
818 .filter_map(include_path)
819 .any(|rel| dir.join(rel) == target)
820 {
821 return Some(target);
822 }
823 }
824 None
825 }
826
827 fn rel(&self, file: &Path) -> String {
828 file.strip_prefix(&self.ws.root)
829 .unwrap_or(file)
830 .to_string_lossy()
831 .replace('\\', "/")
832 }
833}
834
835enum Step {
836 Module(Module),
837 ReExport(String),
838 Type,
840 Globs(Vec<String>),
842}
843
844fn join_path(target: &str, rest: &[&str]) -> String {
846 if rest.is_empty() {
847 target.to_string()
848 } else {
849 format!("{target}::{}", rest.join("::"))
850 }
851}
852
853fn include_path(item: &syn::Item) -> Option<String> {
856 let syn::Item::Macro(m) = item else {
857 return None;
858 };
859 if !m.mac.path.is_ident("include") {
860 return None;
861 }
862 syn::parse2::<syn::LitStr>(m.mac.tokens.clone())
863 .ok()
864 .map(|l| l.value())
865}
866
867fn defines_type(items: &[syn::Item], name: &str) -> bool {
869 items.iter().any(|item| match item {
870 syn::Item::Struct(s) => s.ident == name,
871 syn::Item::Enum(e) => e.ident == name,
872 syn::Item::Union(u) => u.ident == name,
873 syn::Item::Trait(t) => t.ident == name,
874 syn::Item::Type(t) => t.ident == name,
875 syn::Item::Impl(im) => impl_is_for(im, name, None),
876 _ => false,
877 })
878}
879
880fn impl_is_for(im: &syn::ItemImpl, ty: &str, generic: Option<&str>) -> bool {
883 let syn::Type::Path(tp) = im.self_ty.as_ref() else {
884 return false;
885 };
886 let Some(last) = tp.path.segments.last() else {
887 return false;
888 };
889 if last.ident != ty {
890 return false;
891 }
892 let Some(generic) = generic else {
893 return true;
894 };
895 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
896 return false;
897 };
898 let params: Vec<String> = im
899 .generics
900 .type_params()
901 .map(|p| p.ident.to_string())
902 .collect();
903 args.args.iter().any(|a| match a {
904 syn::GenericArgument::Type(syn::Type::Path(p)) => p
905 .path
906 .segments
907 .last()
908 .is_some_and(|s| s.ident == generic || params.contains(&s.ident.to_string())),
909 _ => false,
910 })
911}
912
913fn find_trait_fn(items: &[syn::TraitItem], name: &str) -> Option<Resolved> {
915 items.iter().find_map(|ti| match ti {
916 syn::TraitItem::Fn(f) if f.sig.ident == name => Some(found_bodiless(
917 "trait-method",
918 &syn::Visibility::Inherited,
919 &f.attrs,
920 )),
921 _ => None,
922 })
923}
924
925fn pub_globs(items: &[syn::Item], module: &Module, ws: &Workspace) -> Vec<String> {
927 let mut out = Vec::new();
928 for item in items {
929 if let syn::Item::Use(u) = item {
930 if !matches!(u.vis, syn::Visibility::Inherited) {
931 glob_paths(&u.tree, "", &mut out);
932 }
933 }
934 }
935 out.into_iter()
936 .map(|g| absolute_use(&g, module, ws))
937 .collect()
938}
939
940fn glob_paths(tree: &syn::UseTree, prefix: &str, out: &mut Vec<String>) {
941 let join = |s: &str| {
942 if prefix.is_empty() {
943 s.to_string()
944 } else {
945 format!("{prefix}::{s}")
946 }
947 };
948 match tree {
949 syn::UseTree::Path(p) => glob_paths(&p.tree, &join(&p.ident.to_string()), out),
950 syn::UseTree::Glob(_) if !prefix.is_empty() => out.push(prefix.to_string()),
951 syn::UseTree::Group(g) => g.items.iter().for_each(|t| glob_paths(t, prefix, out)),
952 _ => {}
953 }
954}
955
956fn child_file(
959 child_dir: &Path,
960 seg: &str,
961 attrs: &[syn::Attribute],
962) -> Result<(PathBuf, PathBuf), Unresolved> {
963 for a in attrs {
964 if a.path().is_ident("path") {
965 if let syn::Meta::NameValue(nv) = &a.meta {
966 if let syn::Expr::Lit(syn::ExprLit {
967 lit: syn::Lit::Str(s),
968 ..
969 }) = &nv.value
970 {
971 let f = child_dir.join(s.value());
972 return Ok((f.clone(), f.parent().unwrap_or(&f).to_path_buf()));
973 }
974 }
975 }
976 }
977 let flat = child_dir.join(format!("{seg}.rs"));
978 if flat.is_file() {
979 return Ok((flat, child_dir.join(seg)));
980 }
981 let nested = child_dir.join(seg).join("mod.rs");
982 if nested.is_file() {
983 return Ok((nested, child_dir.join(seg)));
984 }
985 Err(Unresolved {
986 reason: format!(
987 "`mod {seg};` declared but neither `{}` nor `{}` exists",
988 flat.display(),
989 nested.display()
990 ),
991 })
992}
993
994fn find_item(items: &[syn::Item], name: &str) -> Option<Resolved> {
996 find_indexed(items, name).map(|(_, r)| r)
997}
998
999fn find_indexed(items: &[syn::Item], name: &str) -> Option<(usize, Resolved)> {
1001 items
1002 .iter()
1003 .enumerate()
1004 .find_map(|(i, item)| item_named(item, name).map(|r| (i, r)))
1005}
1006
1007fn item_named(item: &syn::Item, name: &str) -> Option<Resolved> {
1008 let named =
1009 |ident: &syn::Ident, kind: &str, vis: &syn::Visibility, attrs: &[syn::Attribute]| {
1010 (ident == name).then(|| found_bodiless(kind, vis, attrs))
1011 };
1012 match item {
1013 syn::Item::Fn(f) => {
1014 (f.sig.ident == name).then(|| found("fn", &f.vis, &f.attrs, &f.sig, &f.block))
1015 }
1016 syn::Item::Impl(im) => find_method(&im.items, name),
1017 syn::Item::Struct(s) => named(&s.ident, "struct", &s.vis, &s.attrs),
1018 syn::Item::Enum(e) => named(&e.ident, "enum", &e.vis, &e.attrs),
1019 syn::Item::Union(u) => named(&u.ident, "union", &u.vis, &u.attrs),
1020 syn::Item::Trait(t) => named(&t.ident, "trait", &t.vis, &t.attrs),
1021 syn::Item::Type(t) => named(&t.ident, "type", &t.vis, &t.attrs),
1022 syn::Item::Const(c) => named(&c.ident, "const", &c.vis, &c.attrs),
1023 syn::Item::Static(s) => named(&s.ident, "static", &s.vis, &s.attrs),
1024 _ => None,
1025 }
1026}
1027
1028fn find_method(items: &[syn::ImplItem], name: &str) -> Option<Resolved> {
1030 items.iter().find_map(|ii| match ii {
1031 syn::ImplItem::Fn(f) if f.sig.ident == name => {
1032 Some(found("method", &f.vis, &f.attrs, &f.sig, &f.block))
1033 }
1034 _ => None,
1035 })
1036}
1037
1038fn found(
1040 kind: &str,
1041 vis: &syn::Visibility,
1042 attrs: &[syn::Attribute],
1043 sig: &syn::Signature,
1044 block: &syn::Block,
1045) -> Resolved {
1046 let mut body = BodySafety::default();
1047 syn::visit::Visit::visit_block(&mut body, block);
1048 Resolved {
1049 file: String::new(),
1050 visibility: visibility_of(vis),
1051 kind: kind.to_string(),
1052 attributes: attr_paths(attrs),
1053 unsafe_free: sig.unsafety.is_none() && body.unsafe_blocks == 0,
1054 bounds_checked: body.unchecked_calls == 0,
1055 }
1056}
1057
1058fn found_bodiless(kind: &str, vis: &syn::Visibility, attrs: &[syn::Attribute]) -> Resolved {
1062 Resolved {
1063 file: String::new(),
1064 visibility: visibility_of(vis),
1065 kind: kind.to_string(),
1066 attributes: attr_paths(attrs),
1067 unsafe_free: true,
1068 bounds_checked: true,
1069 }
1070}
1071
1072#[derive(Default)]
1074struct BodySafety {
1075 unsafe_blocks: usize,
1076 unchecked_calls: usize,
1077}
1078
1079impl<'ast> syn::visit::Visit<'ast> for BodySafety {
1080 fn visit_expr_unsafe(&mut self, e: &'ast syn::ExprUnsafe) {
1081 self.unsafe_blocks += 1;
1082 syn::visit::visit_expr_unsafe(self, e);
1083 }
1084 fn visit_expr_method_call(&mut self, e: &'ast syn::ExprMethodCall) {
1085 if is_unchecked(&e.method.to_string()) {
1086 self.unchecked_calls += 1;
1087 }
1088 syn::visit::visit_expr_method_call(self, e);
1089 }
1090 fn visit_expr_path(&mut self, e: &'ast syn::ExprPath) {
1091 if e.path
1092 .segments
1093 .last()
1094 .is_some_and(|s| is_unchecked(&s.ident.to_string()))
1095 {
1096 self.unchecked_calls += 1;
1097 }
1098 syn::visit::visit_expr_path(self, e);
1099 }
1100}
1101
1102fn is_unchecked(name: &str) -> bool {
1103 matches!(name, "get_unchecked" | "get_unchecked_mut")
1104}
1105
1106fn use_target(items: &[syn::Item], name: &str) -> Option<String> {
1109 for item in items {
1110 if let syn::Item::Use(u) = item {
1111 if let Some(t) = use_tree_target(&u.tree, name, "") {
1112 return Some(t);
1113 }
1114 }
1115 }
1116 None
1117}
1118
1119fn use_tree_target(tree: &syn::UseTree, name: &str, prefix: &str) -> Option<String> {
1120 let join = |p: &str, s: &str| {
1121 if p.is_empty() {
1122 s.to_string()
1123 } else {
1124 format!("{p}::{s}")
1125 }
1126 };
1127 match tree {
1128 syn::UseTree::Path(p) => {
1129 use_tree_target(&p.tree, name, &join(prefix, &p.ident.to_string()))
1130 }
1131 syn::UseTree::Name(n) if n.ident == name => Some(join(prefix, &n.ident.to_string())),
1132 syn::UseTree::Rename(r) if r.rename == name => Some(join(prefix, &r.ident.to_string())),
1133 syn::UseTree::Group(g) => g
1134 .items
1135 .iter()
1136 .find_map(|t| use_tree_target(t, name, prefix)),
1137 _ => None,
1138 }
1139}
1140
1141fn absolute_use(target: &str, module: &Module, ws: &Workspace) -> String {
1144 let here = module_path_of(module, ws);
1145 let krate = here.split("::").next().unwrap_or_default().to_string();
1146 let (head, rest) = target.split_once("::").unwrap_or((target, ""));
1147 let base = match head {
1148 "crate" => krate,
1149 "self" => here.clone(),
1150 "super" => here
1151 .rsplit_once("::")
1152 .map_or(here.clone(), |(p, _)| p.to_string()),
1153 other if ws.crates.contains_key(&other.replace('-', "_")) => other.to_string(),
1154 other => format!("{here}::{other}"),
1155 };
1156 if rest.is_empty() {
1157 base
1158 } else {
1159 format!("{base}::{rest}")
1160 }
1161}
1162
1163fn module_path_of(module: &Module, ws: &Workspace) -> String {
1166 let mut best: Option<(usize, String)> = None;
1169 for (name, roots) in &ws.crates {
1170 for root in roots {
1171 let Some(src) = root.parent() else { continue };
1172 let Ok(rel) = module.child_dir.strip_prefix(src) else {
1173 continue;
1174 };
1175 let depth = src.components().count();
1176 if best.as_ref().is_some_and(|(d, _)| *d >= depth) {
1177 continue;
1178 }
1179 let tail: Vec<String> = rel
1180 .components()
1181 .map(|c| c.as_os_str().to_string_lossy().to_string())
1182 .collect();
1183 let path = if tail.is_empty() {
1184 name.clone()
1185 } else {
1186 format!("{name}::{}", tail.join("::"))
1187 };
1188 best = Some((depth, path));
1189 }
1190 }
1191 best.map(|(_, p)| p).unwrap_or_default()
1192}
1193
1194fn visibility_of(v: &syn::Visibility) -> String {
1195 match v {
1196 syn::Visibility::Public(_) => "pub".to_string(),
1197 syn::Visibility::Restricted(r) => {
1198 let p = r
1199 .path
1200 .segments
1201 .iter()
1202 .map(|s| s.ident.to_string())
1203 .collect::<Vec<_>>()
1204 .join("::");
1205 format!("pub({p})")
1206 }
1207 syn::Visibility::Inherited => "private".to_string(),
1208 }
1209}
1210
1211fn attr_paths(attrs: &[syn::Attribute]) -> Vec<String> {
1212 attrs
1213 .iter()
1214 .map(|a| {
1215 a.path()
1216 .segments
1217 .iter()
1218 .map(|s| s.ident.to_string())
1219 .collect::<Vec<_>>()
1220 .join("::")
1221 })
1222 .collect()
1223}
1224
1225#[must_use]
1227pub fn symbol_iri(b: &Bound) -> String {
1228 iri("symbol", &format!("{}::{}", b.module_path, b.function))
1229}
1230
1231pub fn emit(g: &mut Graph, b: &Bound, found: &Result<Resolved, Unresolved>) {
1233 let s = symbol_iri(b);
1234 g.insert(s.clone(), RDF_TYPE, Term::iri(ont("Symbol")));
1235 g.insert(s.clone(), RDF_TYPE, Term::iri(PROV_ENTITY));
1236 let krate = b.module_path.split("::").next().unwrap_or_default();
1237 g.insert(s.clone(), sym("crate"), Term::string(krate));
1238 g.insert(s.clone(), sym("module"), Term::string(&b.module_path));
1239 g.insert(s.clone(), sym("name"), Term::string(&b.function));
1240 g.insert(
1241 s.clone(),
1242 sym("implements"),
1243 Term::iri(iri("contract", &b.contract)),
1244 );
1245 g.insert(s.clone(), sym("equation"), Term::string(&b.equation));
1246 g.insert(
1247 s.clone(),
1248 sym("bindingStatus"),
1249 Term::string(format!("{:?}", b.status).to_lowercase()),
1250 );
1251 match found {
1252 Ok(r) => {
1253 g.insert(s.clone(), sym("resolved"), Term::boolean(true));
1254 g.insert(s.clone(), sym("file"), Term::string(&r.file));
1255 g.insert(s.clone(), sym("visibility"), Term::string(&r.visibility));
1256 g.insert(s.clone(), sym("kind"), Term::string(&r.kind));
1257 for a in &r.attributes {
1258 g.insert(s.clone(), sym("attribute"), Term::string(a));
1259 }
1260 if r.attributes.iter().any(|a| a == "kernel") {
1262 g.insert(s.clone(), sym("unsafeFree"), Term::boolean(r.unsafe_free));
1263 g.insert(
1264 s.clone(),
1265 sym("boundsChecked"),
1266 Term::boolean(r.bounds_checked),
1267 );
1268 }
1269 }
1270 Err(u) => {
1271 g.insert(s.clone(), sym("resolved"), Term::boolean(false));
1272 g.insert(s, sym("unresolvedReason"), Term::string(&u.reason));
1273 }
1274 }
1275}
1276
1277#[derive(Debug, Default)]
1280pub struct Resolution {
1281 pub stats: CodeStats,
1282 pub symbols: Vec<(Bound, Result<Resolved, Unresolved>)>,
1283 pub at_workspace_root: bool,
1285}
1286
1287#[must_use]
1289pub fn resolve_all(contract_dir: &Path) -> Resolution {
1290 let root_buf = super::repo_root(contract_dir);
1291 let mut ws = Workspace::scan(root_buf.as_path());
1292 let regs = registries(contract_dir);
1293 for (_file, registry) in ®s {
1296 for b in registry.bindings.iter().filter(|b| b.kernel) {
1297 let path = b.module_path.as_deref().unwrap_or(®istry.target_crate);
1298 if let Some(krate) = path.split("::").next() {
1299 ws.admit_standalone(krate);
1300 }
1301 }
1302 }
1303 let mut resolver = Resolver::new(&ws);
1304 let mut out = Resolution {
1305 at_workspace_root: ws.is_workspace_root,
1306 ..Resolution::default()
1307 };
1308 for (_file, registry) in regs {
1309 out.stats.registries += 1;
1310 for b in bound_of(®istry) {
1311 let found = resolver.resolve(&b.module_path, &b.function);
1312 if found.is_ok() {
1313 out.stats.resolved += 1;
1314 } else {
1315 out.stats.unresolved += 1;
1316 }
1317 out.stats.symbols += 1;
1318 out.symbols.push((b, found));
1319 }
1320 }
1321 out.stats.files_parsed = resolver.files_parsed();
1322 out.stats.crates_scanned = resolver.crates_walked();
1323 out
1324}
1325
1326pub fn extract(contract_dir: &Path, g: &mut Graph) -> CodeStats {
1329 let r = resolve_all(contract_dir);
1330 for (b, found) in &r.symbols {
1331 emit(g, b, found);
1332 }
1333 r.stats
1334}
1335
1336#[must_use]
1340pub fn positive_control() -> bool {
1341 let src = "pub mod m { pub fn present() {} }\npub use m::present as alias;";
1342 let Ok(ast) = syn::parse_file(src) else {
1343 return false;
1344 };
1345 let present = find_item(&ast.items, "present").is_none()
1346 && match ast.items.first() {
1347 Some(syn::Item::Mod(m)) => m
1348 .content
1349 .as_ref()
1350 .is_some_and(|(_, items)| find_item(items, "present").is_some()),
1351 _ => false,
1352 };
1353 let ghost =
1354 find_item(&ast.items, "absent").is_none() && use_target(&ast.items, "absent").is_none();
1355 let aliased = use_target(&ast.items, "alias").as_deref() == Some("m::present");
1356 present && ghost && aliased
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361 use super::*;
1362
1363 fn fixture() -> PathBuf {
1364 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/ont/code-bound")
1365 }
1366
1367 #[test]
1368 fn the_manifest_scan_indexes_package_and_lib_names() {
1369 let ws = Workspace::scan(&fixture());
1370 assert!(ws.crates.contains_key("kern"), "{:?}", ws.crates);
1371 assert!(ws.crates.contains_key("kern_crate"), "{:?}", ws.crates);
1372 assert!(ws.crates["kern"][0].ends_with("crates/kern/src/lib.rs"));
1373 }
1374
1375 #[test]
1376 fn the_walk_resolves_files_inline_modules_methods_and_re_exports_and_refuses_a_ghost() {
1377 let ws = Workspace::scan(&fixture());
1378 let mut r = Resolver::new(&ws);
1379 let softmax = r
1380 .resolve("kern::nn::functional", "softmax")
1381 .expect("softmax");
1382 assert_eq!(softmax.file, "crates/kern/src/nn/functional.rs");
1383 assert_eq!(softmax.visibility, "pub");
1384 assert_eq!(softmax.kind, "fn");
1385 assert_eq!(softmax.attributes, vec!["inline".to_string()]);
1386 let relu = r.resolve("kern::nn::functional", "relu").expect("relu");
1387 assert_eq!(relu.visibility, "pub(crate)");
1388 let forward = r.resolve("kern::nn", "forward").expect("forward");
1389 assert_eq!(forward.kind, "method");
1390 assert_eq!(forward.file, "crates/kern/src/nn.rs");
1391 let helper = r
1392 .resolve("kern", "helper")
1393 .expect("helper via pub use … as");
1394 assert_eq!(helper.file, "crates/kern/src/lib.rs");
1395 assert_eq!(helper.visibility, "pub");
1396 let kernel = r
1397 .resolve("kern::nn::functional", "gated_rmsnorm")
1398 .expect("kernel");
1399 assert_eq!(kernel.attributes, vec!["kernel".to_string()]);
1400 let ghost = r
1401 .resolve("kern::nn::functional", "no_such_function")
1402 .unwrap_err();
1403 assert!(
1404 ghost.reason.contains("no `fn no_such_function`"),
1405 "{}",
1406 ghost.reason
1407 );
1408 assert!(
1409 ghost.reason.contains("crates/kern/src/nn/functional.rs"),
1410 "{}",
1411 ghost.reason
1412 );
1413 let no_crate = r.resolve("nowhere::x", "f").unwrap_err();
1414 assert!(no_crate.reason.contains("not a workspace member"));
1415 let no_mod = r.resolve("kern::nope", "f").unwrap_err();
1416 assert!(no_mod.reason.contains("no `mod nope`"), "{}", no_mod.reason);
1417 assert_eq!(r.files_parsed(), 3);
1418 }
1419
1420 #[test]
1421 fn the_graph_carries_every_bound_symbol_resolved_or_not_and_is_deterministic() {
1422 let dir = fixture().join("contracts");
1423 let mut g = Graph::new();
1424 let stats = extract(&dir, &mut g);
1425 assert_eq!(stats.registries, 1);
1426 assert_eq!(stats.symbols, 6);
1427 assert_eq!(stats.resolved, 5);
1428 assert_eq!(stats.unresolved, 1);
1429 let nt = g.to_ntriples();
1430 assert!(nt.contains("/symbol/kern::nn::functional::softmax> <https://ont.paiml.dev/v1alpha1/sym/resolved> \"true\""), "{nt}");
1431 assert!(nt.contains("/symbol/kern::nn::functional::no_such_function> <https://ont.paiml.dev/v1alpha1/sym/resolved> \"false\""), "{nt}");
1432 assert!(
1433 nt.contains("sym/unresolvedReason> \"no `fn no_such_function`"),
1434 "{nt}"
1435 );
1436 assert!(
1437 nt.contains(
1438 "sym/implements> <https://ont.paiml.dev/v1alpha1/contract/softmax-kernel-v1>"
1439 ),
1440 "{nt}"
1441 );
1442 assert!(nt.contains("sym/attribute> \"kernel\""), "{nt}");
1443 assert!(!nt.contains("_:"));
1444 let mut g2 = Graph::new();
1445 extract(&dir, &mut g2);
1446 assert_eq!(nt, g2.to_ntriples());
1447 }
1448
1449 #[test]
1450 fn a_trailing_segment_equal_to_the_function_is_dropped_from_the_module_path() {
1451 let reg = parse_binding(&fixture().join("contracts/binding.yaml")).expect("registry");
1452 let bound = bound_of(®);
1453 let softmax = bound
1454 .iter()
1455 .find(|b| b.function == "softmax")
1456 .expect("softmax");
1457 assert_eq!(softmax.module_path, "kern::nn::functional");
1458 assert_eq!(
1459 symbol_iri(softmax),
1460 "https://ont.paiml.dev/v1alpha1/symbol/kern::nn::functional::softmax"
1461 );
1462 }
1463
1464 #[test]
1468 fn a_method_implemented_in_the_module_that_imports_the_type_resolves() {
1469 let tmp = tempfile::tempdir().unwrap();
1470 let w = tmp.path();
1471 std::fs::write(w.join("Cargo.toml"), "[workspace]\nmembers = [\"k\"]\n").unwrap();
1472 std::fs::create_dir_all(w.join("k/src")).unwrap();
1473 std::fs::write(w.join("k/Cargo.toml"), "[package]\nname = \"k\"\n").unwrap();
1474 std::fs::write(w.join("k/src/lib.rs"), "pub mod a;\npub mod b;\n").unwrap();
1475 std::fs::write(w.join("k/src/a.rs"), "pub struct T<P>(P);\npub struct C;\n").unwrap();
1476 std::fs::write(
1477 w.join("k/src/b.rs"),
1478 "use crate::a::{C, T};\nimpl T<C> { pub fn f() {} }\n",
1479 )
1480 .unwrap();
1481 let ws = Workspace::scan(w);
1482 let mut r = Resolver::new(&ws);
1483 let f = r
1484 .resolve("k::b::T::<C>", "f")
1485 .expect("impl in the importing module");
1486 assert_eq!(f.file, "k/src/b.rs");
1487 assert_eq!(f.kind, "method");
1488 assert!(r.resolve("k::b::T::<C>", "g").is_err(), "a ghost method");
1489 assert!(
1490 r.resolve("k::a::T::<C>", "f").is_err(),
1491 "a does not implement f"
1492 );
1493 }
1494
1495 #[test]
1500 fn a_kernel_binding_admits_its_standalone_workspace_and_nothing_else_does() {
1501 let tmp = tempfile::tempdir().unwrap();
1502 let w = tmp.path();
1503 std::fs::write(w.join("Cargo.toml"), "[workspace]\nmembers = [\"k\"]\n").unwrap();
1504 std::fs::create_dir_all(w.join("k/src")).unwrap();
1505 std::fs::write(w.join("k/Cargo.toml"), "[package]\nname = \"k\"\n").unwrap();
1506 std::fs::write(w.join("k/src/lib.rs"), "pub fn r() {}\n").unwrap();
1507 std::fs::create_dir_all(w.join("exp/ox/src")).unwrap();
1508 std::fs::write(
1509 w.join("exp/ox/Cargo.toml"),
1510 "[package]\nname = \"ox_kern\"\n\n[workspace]\n",
1511 )
1512 .unwrap();
1513 std::fs::write(
1514 w.join("exp/ox/src/lib.rs"),
1515 "pub mod kernels {\n #[kernel]\n pub fn g() {}\n}\n",
1516 )
1517 .unwrap();
1518 std::fs::create_dir_all(w.join("fx/src")).unwrap();
1519 std::fs::write(w.join("fx/Cargo.toml"), "[package]\nname = \"stray\"\n").unwrap();
1520 std::fs::write(w.join("fx/src/lib.rs"), "pub fn s() {}\n").unwrap();
1521
1522 let mut ws = Workspace::scan(w);
1523 assert!(!ws.crates.contains_key("ox_kern"), "{:?}", ws.crates);
1524 assert!(ws.standalone.contains_key("ox_kern"), "{:?}", ws.standalone);
1525 assert!(!ws.standalone.contains_key("stray"), "{:?}", ws.standalone);
1526 assert!(!ws.admit_standalone("k"), "a member is never re-admitted");
1527 assert!(!ws.admit_standalone("stray"));
1528 assert!(ws.admit_standalone("ox_kern"));
1529 assert!(!ws.admit_standalone("ox_kern"), "admitted once");
1530 let g = Resolver::new(&ws)
1531 .resolve("ox_kern::kernels", "g")
1532 .expect("admitted kernel resolves");
1533 assert_eq!(g.attributes, vec!["kernel".to_string()]);
1534
1535 let row = |kernel: &str| {
1536 format!(
1537 "version: 1.0.0\ntarget_crate: k\nbindings:\n- contract: c-v1.yaml\n equation: g\n \
1538 module_path: ox_kern::kernels\n function: g\n status: implemented\n{kernel}"
1539 )
1540 };
1541 std::fs::create_dir_all(w.join("contracts")).unwrap();
1542 std::fs::write(w.join("contracts/binding.yaml"), row(" kernel: true\n")).unwrap();
1543 let r = resolve_all(&w.join("contracts"));
1544 assert_eq!(
1545 (r.stats.resolved, r.stats.unresolved),
1546 (1, 0),
1547 "{:?}",
1548 r.symbols
1549 );
1550 std::fs::write(w.join("contracts/binding.yaml"), row("")).unwrap();
1551 let r = resolve_all(&w.join("contracts"));
1552 assert_eq!(
1553 (r.stats.resolved, r.stats.unresolved),
1554 (0, 1),
1555 "{:?}",
1556 r.symbols
1557 );
1558 }
1559
1560 #[test]
1565 fn an_item_in_an_included_file_resolves_under_the_file_stem() {
1566 let tmp = tempfile::tempdir().unwrap();
1567 let w = tmp.path();
1568 std::fs::write(w.join("Cargo.toml"), "[workspace]\nmembers = [\"k\"]\n").unwrap();
1569 std::fs::create_dir_all(w.join("k/src/ops")).unwrap();
1570 std::fs::write(w.join("k/Cargo.toml"), "[package]\nname = \"k\"\n").unwrap();
1571 std::fs::write(w.join("k/src/lib.rs"), "pub mod ops;\npub mod cmaes;\n").unwrap();
1572 std::fs::write(
1573 w.join("k/src/ops/mod.rs"),
1574 "include!(\"activation.rs\");\ninclude!(\"masking.rs\");\n",
1575 )
1576 .unwrap();
1577 std::fs::write(w.join("k/src/ops/activation.rs"), "pub fn matmul() {}\n").unwrap();
1578 std::fs::write(w.join("k/src/ops/masking.rs"), "pub fn mask() {}\n").unwrap();
1579 std::fs::write(
1580 w.join("k/src/cmaes.rs"),
1581 "pub struct Cma;\ninclude!(\"cmaes_include_01.rs\");\n",
1582 )
1583 .unwrap();
1584 std::fs::write(
1585 w.join("k/src/cmaes_include_01.rs"),
1586 "impl Cma { pub fn optimize(&self) {} }\n",
1587 )
1588 .unwrap();
1589 let ws = Workspace::scan(w);
1590 let mut r = Resolver::new(&ws);
1591 let m = r
1592 .resolve("k::ops::activation", "matmul")
1593 .expect("the included file's stem is a segment");
1594 assert_eq!(m.file, "k/src/ops/activation.rs");
1595 r.resolve("k::ops", "matmul")
1596 .expect("the Rust path still resolves");
1597 r.resolve("k::cmaes_include_01", "optimize")
1598 .expect("a method in a file the sibling module includes, named under the parent");
1599 r.resolve("k::cmaes::cmaes_include_01", "optimize")
1600 .expect("the same file under the including module");
1601 std::fs::write(w.join("k/src/stray.rs"), "pub fn optimize() {}\n").unwrap();
1602 assert!(
1603 Resolver::new(&ws).resolve("k::stray", "optimize").is_err(),
1604 "a file on disk that nothing declares or includes"
1605 );
1606 let wrong = r
1607 .resolve("k::ops::activation", "mask")
1608 .expect_err("mask is in masking.rs, not activation.rs");
1609 assert!(wrong.reason.contains("no `fn mask`"), "{}", wrong.reason);
1610 assert!(
1611 r.resolve("k::ops::pooling", "matmul").is_err(),
1612 "a stem nothing includes"
1613 );
1614 }
1615
1616 #[test]
1620 fn a_glob_re_export_cycle_reports_the_real_miss_not_the_cycle() {
1621 let tmp = tempfile::tempdir().unwrap();
1622 let w = tmp.path();
1623 std::fs::write(w.join("Cargo.toml"), "[workspace]\nmembers = [\"k\"]\n").unwrap();
1624 std::fs::create_dir_all(w.join("k/src")).unwrap();
1625 std::fs::write(w.join("k/Cargo.toml"), "[package]\nname = \"k\"\n").unwrap();
1626 std::fs::write(w.join("k/src/lib.rs"), "pub mod a;\npub mod b;\n").unwrap();
1627 std::fs::write(w.join("k/src/a.rs"), "pub use crate::b::*;\n").unwrap();
1628 std::fs::write(w.join("k/src/b.rs"), "pub use crate::a::*;\n").unwrap();
1629 let ws = Workspace::scan(w);
1630 let mut r = Resolver::new(&ws);
1631 let e = r.resolve("k::a::x", "f").expect_err("x exists nowhere");
1632 assert!(!e.reason.contains("re-export cycle"), "{}", e.reason);
1633 assert!(
1634 e.reason.contains("no `mod x`") && e.reason.contains("k/src/b.rs"),
1635 "{}",
1636 e.reason
1637 );
1638 }
1639
1640 fn crate_k(lib: &str, files: &[(&str, &str)]) -> tempfile::TempDir {
1642 let tmp = tempfile::tempdir().unwrap();
1643 let w = tmp.path();
1644 std::fs::write(w.join("Cargo.toml"), "[workspace]\nmembers = [\"k\"]\n").unwrap();
1645 std::fs::create_dir_all(w.join("k/src")).unwrap();
1646 std::fs::write(w.join("k/Cargo.toml"), "[package]\nname = \"k\"\n").unwrap();
1647 std::fs::write(w.join("k/src/lib.rs"), lib).unwrap();
1648 for (path, text) in files {
1649 std::fs::write(w.join("k/src").join(path), text).unwrap();
1650 }
1651 tmp
1652 }
1653
1654 #[test]
1658 fn every_kind_of_type_definition_is_a_type_segment() {
1659 let tmp = crate_k(
1660 "pub struct S;\npub enum E { A }\npub union U { a: u32 }\npub type T = u8;\n",
1661 &[],
1662 );
1663 let ws = Workspace::scan(tmp.path());
1664 for ty in ["S", "E", "U", "T"] {
1665 let e = Resolver::new(&ws)
1666 .resolve(&format!("k::{ty}"), "f")
1667 .expect_err("no impl anywhere");
1668 assert!(
1669 e.reason.contains(&format!("in an `impl {ty}`")),
1670 "{ty}: {}",
1671 e.reason
1672 );
1673 }
1674 }
1675
1676 #[test]
1680 fn a_type_member_is_searched_only_in_its_own_impls_and_trait() {
1681 let tmp = crate_k(
1682 "pub struct T;\npub struct U;\nimpl U { pub fn f() {} }\n\
1683 pub trait Other { fn g(); }\npub trait Tr { fn h(); }\n",
1684 &[],
1685 );
1686 let ws = Workspace::scan(tmp.path());
1687 let mut r = Resolver::new(&ws);
1688 r.resolve("k::U", "f").expect("U's own impl");
1689 r.resolve("k::Tr", "h").expect("the trait's own fn");
1690 assert!(r.resolve("k::T", "f").is_err(), "f is U's, not T's");
1691 assert!(
1692 r.resolve("k::T", "g").is_err(),
1693 "g is trait Other's, not T's"
1694 );
1695 assert!(
1696 r.resolve("k::Tr::<X>", "h").is_err(),
1697 "a trait takes no <Arg> here"
1698 );
1699 let e = r.resolve("k::U::x", "f").expect_err("x is not a module");
1700 assert!(e.reason.contains("is a type, not a module"), "{}", e.reason);
1701 }
1702
1703 #[test]
1707 fn an_inline_module_does_not_include_a_sibling_file() {
1708 let tmp = crate_k(
1709 "pub mod c {}\n",
1710 &[
1711 ("c.rs", "include!(\"inc.rs\");\n"),
1712 ("inc.rs", "pub fn f() {}\n"),
1713 ],
1714 );
1715 let ws = Workspace::scan(tmp.path());
1716 assert!(Resolver::new(&ws).resolve("k::inc", "f").is_err());
1717 let file_backed = crate_k(
1718 "pub mod c;\n",
1719 &[
1720 ("c.rs", "include!(\"inc.rs\");\n"),
1721 ("inc.rs", "pub fn f() {}\n"),
1722 ],
1723 );
1724 let ws = Workspace::scan(file_backed.path());
1725 Resolver::new(&ws)
1726 .resolve("k::inc", "f")
1727 .expect("the same layout with a file-backed mod resolves");
1728 }
1729
1730 #[test]
1733 fn an_inline_mod_is_not_a_file_backed_child_that_includes_a_sibling() {
1734 let tmp = tempfile::tempdir().expect("tempdir");
1735 let w = tmp.path();
1736 std::fs::write(w.join("Cargo.toml"), "[workspace]\nmembers = [\"k\"]\n").expect("write");
1737 std::fs::create_dir_all(w.join("k/src")).expect("mkdir");
1738 std::fs::write(w.join("k/Cargo.toml"), "[package]\nname = \"k\"\n").expect("write");
1739 std::fs::write(w.join("k/src/lib.rs"), "pub mod foo {}\n").expect("write");
1740 std::fs::write(w.join("k/src/foo.rs"), "include!(\"inc.rs\");\n").expect("write");
1741 std::fs::write(w.join("k/src/inc.rs"), "pub fn f() {}\n").expect("write");
1742 let ws = Workspace::scan(w);
1743 assert!(Resolver::new(&ws).resolve("k::inc", "f").is_err());
1744 }
1745
1746 #[test]
1747 fn join_path_is_exact() {
1748 assert_eq!(join_path("a::b", &[]), "a::b");
1749 assert_eq!(join_path("a", &["b", "c"]), "a::b::c");
1750 }
1751
1752 #[test]
1753 fn defines_type_sees_each_type_item_only_by_its_own_name() {
1754 for (src, name) in [
1755 ("struct S;", "S"),
1756 ("enum E { A }", "E"),
1757 ("union U { a: u8 }", "U"),
1758 ("trait T {}", "T"),
1759 ("type A = u8;", "A"),
1760 ] {
1761 let ast = syn::parse_file(src).expect("parses");
1762 assert!(defines_type(&ast.items, name), "{src}");
1763 assert!(!defines_type(&ast.items, "Nope"), "{src}");
1764 }
1765 let f = syn::parse_file("fn S() {}").expect("parses");
1766 assert!(!defines_type(&f.items, "S"));
1767 }
1768
1769 #[test]
1770 fn find_trait_fn_matches_the_named_fn_only() {
1771 let one: syn::ItemTrait = syn::parse_str("trait T { fn b(); }").expect("parses");
1772 assert!(find_trait_fn(&one.items, "b").is_some());
1773 assert!(find_trait_fn(&one.items, "c").is_none());
1774 let two: syn::ItemTrait = syn::parse_str("trait T { fn a(); fn b() {} }").expect("parses");
1775 assert!(find_trait_fn(&two.items, "a").is_some());
1776 assert!(find_trait_fn(&two.items, "b").is_some());
1777 assert!(find_trait_fn(&two.items, "c").is_none());
1778 }
1779
1780 #[test]
1781 fn glob_paths_reads_groups_and_refuses_a_bare_glob() {
1782 let globs = |src: &str| {
1783 let u: syn::ItemUse = syn::parse_str(src).expect("parses");
1784 let mut out = Vec::new();
1785 glob_paths(&u.tree, "", &mut out);
1786 out
1787 };
1788 assert_eq!(globs("use a::b::*;"), ["a::b"]);
1789 assert_eq!(globs("use a::{b::*, c::d::*, e};"), ["a::b", "a::c::d"]);
1790 assert!(globs("use *;").is_empty());
1791 }
1792
1793 #[test]
1794 fn item_named_reads_union_and_static_items() {
1795 let ast = syn::parse_file("pub union U { a: u8 }\nstatic S: u8 = 0;").expect("parses");
1796 let u = item_named(&ast.items[0], "U").expect("union");
1797 assert_eq!(u.kind, "union");
1798 assert_eq!(u.visibility, "pub");
1799 assert!(item_named(&ast.items[0], "Nope").is_none());
1800 let s = item_named(&ast.items[1], "S").expect("static");
1801 assert_eq!(s.kind, "static");
1802 assert!(item_named(&ast.items[1], "Nope").is_none());
1803 }
1804
1805 #[test]
1806 fn the_positive_control_fires() {
1807 assert!(positive_control());
1808 }
1809
1810 #[test]
1813 fn workspace_membership_reads_members_and_exclude_in_every_shape() {
1814 let text = "[package]\nname = \"root\"\nmembers = [\"not-a-workspace-key\"]\n\n\
1815 [workspace]\nresolver = \"2\"\nmembers = [\n \"crates/*\", # every crate\n 'tools/x/',\n]\n\
1816 exclude = [\"crates/skip\"]\n\n[workspace.dependencies]\nmembers = [\"nope\"]\n";
1817 let m = workspace_membership(text).expect("a [workspace] table");
1818 assert_eq!(m.members, ["crates/*", "tools/x"]);
1819 assert_eq!(m.exclude, ["crates/skip"]);
1820 assert!(m.root_package);
1821 let root = Path::new("/r");
1822 assert!(m.admits(root, Path::new("/r/crates/a")));
1823 assert!(!m.admits(root, Path::new("/r/crates/skip")));
1824 assert!(m.admits(root, Path::new("/r")));
1825 assert!(workspace_membership("[package]\nname = \"x\"\n").is_none());
1826 }
1827
1828 #[test]
1829 fn manifest_names_reads_package_and_lib_sections_only() {
1830 let m = manifest_names("[package]\nname = \"a-b\"\n[dependencies]\nname = \"x\"\n[lib]\nname = \"ab\"\npath = \"src/x.rs\"\n");
1831 assert_eq!(m.package.as_deref(), Some("a-b"));
1832 assert_eq!(m.lib.as_deref(), Some("ab"));
1833 assert_eq!(m.lib_path.as_deref(), Some("src/x.rs"));
1834 }
1835
1836 #[test]
1838 fn emit_writes_the_body_facts_only_for_a_kernel_attribute() {
1839 let b = Bound {
1840 contract: "c-v1".into(),
1841 equation: "e".into(),
1842 module_path: "kern::m".into(),
1843 function: "f".into(),
1844 status: ImplStatus::Implemented,
1845 };
1846 let nt_of = |attrs: &[&str]| {
1847 let found: Result<Resolved, Unresolved> = Ok(Resolved {
1848 file: "src/m.rs".into(),
1849 visibility: "pub".into(),
1850 kind: "fn".into(),
1851 attributes: attrs.iter().map(|a| (*a).to_string()).collect(),
1852 unsafe_free: true,
1853 bounds_checked: false,
1854 });
1855 let mut g = Graph::new();
1856 emit(&mut g, &b, &found);
1857 g.to_ntriples()
1858 };
1859 let kernel = nt_of(&["kernel"]);
1860 assert!(kernel.contains("/sym/unsafeFree> \"true\""), "{kernel}");
1861 assert!(kernel.contains("/sym/boundsChecked> \"false\""), "{kernel}");
1862 let plain = nt_of(&["inline"]);
1863 assert!(!plain.contains("unsafeFree"), "{plain}");
1864 assert!(!plain.contains("boundsChecked"), "{plain}");
1865 assert!(plain.contains("/sym/attribute> \"inline\""), "{plain}");
1866 }
1867
1868 #[test]
1869 fn the_body_walk_reads_unsafe_and_unchecked_indexing() {
1870 let src = r"
1871 pub fn clean(x: &[f32]) -> f32 { x[0] }
1872 pub fn blocky(x: &[f32]) -> f32 { unsafe { *x.as_ptr() } }
1873 pub unsafe fn signed(x: &[f32]) -> f32 { x[0] }
1874 pub fn method(x: &[f32]) -> f32 { let v = || x.len(); if v() > 0 { *x.get_unchecked(0) } else { 0.0 } }
1875 pub fn path(x: &[f32]) -> f32 { *<[f32]>::get_unchecked_mut(&mut [0.0][..], 0) + x[0] }
1876 ";
1877 let ast = syn::parse_file(src).expect("parses");
1878 let r = |n: &str| find_item(&ast.items, n).expect(n);
1879 assert!(r("clean").unsafe_free && r("clean").bounds_checked);
1880 assert!(!r("blocky").unsafe_free && r("blocky").bounds_checked);
1881 assert!(!r("signed").unsafe_free);
1882 assert!(r("method").unsafe_free && !r("method").bounds_checked);
1883 assert!(!r("path").bounds_checked);
1884 }
1885
1886 fn one_crate(lib: &str, files: &[(&str, &str)]) -> tempfile::TempDir {
1888 let tmp = tempfile::tempdir().expect("tempdir");
1889 let w = tmp.path();
1890 std::fs::write(w.join("Cargo.toml"), "[workspace]\nmembers = [\"k\"]\n").expect("write");
1891 std::fs::create_dir_all(w.join("k/src")).expect("mkdir");
1892 std::fs::write(w.join("k/Cargo.toml"), "[package]\nname = \"k\"\n").expect("write");
1893 std::fs::write(w.join("k/src/lib.rs"), lib).expect("write");
1894 for (p, t) in files {
1895 std::fs::write(w.join("k/src").join(p), t).expect("write");
1896 }
1897 tmp
1898 }
1899
1900 #[test]
1903 fn scan_indexes_member_manifests_only() {
1904 let tmp = tempfile::tempdir().expect("tempdir");
1905 let w = tmp.path();
1906 std::fs::write(
1907 w.join("Cargo.toml"),
1908 "[workspace]\nmembers = [\"crates/*\"]\nexclude = [\"crates/skip\"]\n",
1909 )
1910 .expect("write");
1911 for c in ["keep", "skip"] {
1912 std::fs::create_dir_all(w.join("crates").join(c)).expect("mkdir");
1913 std::fs::write(
1914 w.join("crates").join(c).join("Cargo.toml"),
1915 format!("[package]\nname = \"{c}\"\n"),
1916 )
1917 .expect("write");
1918 }
1919 std::fs::write(
1920 w.join("crates/keep/other.toml"),
1921 "[package]\nname = \"sneak\"\n",
1922 )
1923 .expect("write");
1924 let ws = Workspace::scan(w);
1925 assert!(ws.is_workspace_root);
1926 assert!(ws.crates.contains_key("keep"), "{:?}", ws.crates);
1927 assert!(!ws.crates.contains_key("skip"), "{:?}", ws.crates);
1928 assert!(!ws.crates.contains_key("sneak"), "{:?}", ws.crates);
1929 }
1930
1931 #[test]
1933 fn admits_the_root_only_as_a_package_or_a_dot_member() {
1934 let root = Path::new("/r");
1935 let ws_only = workspace_membership("[workspace]\nmembers = [\"crates/a\"]\n").expect("ws");
1936 assert!(!ws_only.root_package, "no [package] table");
1937 assert!(!ws_only.admits(root, root), "no `.` member, no package");
1938 let dot = workspace_membership("[workspace]\nmembers = [\".\"]\n").expect("ws");
1939 assert!(dot.admits(root, root));
1940 assert!(!dot.admits(root, Path::new("/r/crates/a")));
1941 }
1942
1943 #[test]
1944 fn section_name_reads_a_bracketed_header_line_only() {
1945 assert_eq!(section_name("[workspace]").as_deref(), Some("workspace"));
1946 assert_eq!(section_name("[ a.b ]").as_deref(), Some("a.b"));
1947 assert_eq!(section_name("[open"), None, "no closing bracket");
1948 assert_eq!(section_name("close]"), None, "no opening bracket");
1949 assert_eq!(section_name("plain"), None);
1950 assert_eq!(section_name("[a = b]"), None, "a key line");
1951 assert_eq!(section_name("x = [\"a\"]"), None);
1952 }
1953
1954 #[test]
1955 fn quoted_reads_every_string_on_the_line() {
1956 assert_eq!(quoted(r#""a", "b""#), ["a", "b"]);
1957 assert_eq!(quoted(r#"'a/' , "b" 'c'"#), ["a", "b", "c"]);
1958 assert!(quoted("none here").is_empty());
1959 }
1960
1961 #[test]
1962 fn glob_match_needs_room_for_both_prefix_and_suffix() {
1963 assert!(glob_match("crates/apr-*", "crates/apr-cli"));
1964 assert!(glob_match("ab*bc", "abxbc"));
1965 assert!(!glob_match("ab*bc", "abc"), "prefix and suffix overlap");
1966 assert!(!glob_match("ab*b", "ab"), "too short for ab + b");
1967 assert!(!glob_match("a*c", "xc"), "suffix only");
1968 assert!(!glob_match("a*c", "ax"), "prefix only");
1969 assert!(glob_match("a*", "a"));
1970 assert!(!glob_match("crates/*", "crates/a/b"));
1971 }
1972
1973 #[test]
1976 fn include_nesting_stops_at_the_bound_and_a_cycle_terminates() {
1977 let mut files: Vec<(String, String)> = Vec::new();
1978 for i in 1..=9 {
1979 let next = if i < 9 {
1980 format!("include!(\"f{}.rs\");\n", i + 1)
1981 } else {
1982 String::new()
1983 };
1984 files.push((format!("f{i}.rs"), format!("pub fn at_{i}() {{}}\n{next}")));
1985 }
1986 files.push((
1987 "cyc.rs".into(),
1988 "include!(\"cyc.rs\");\npub fn cyc_fn() {}\n".into(),
1989 ));
1990 let refs: Vec<(&str, &str)> = files
1991 .iter()
1992 .map(|(a, b)| (a.as_str(), b.as_str()))
1993 .collect();
1994 let tmp = one_crate("include!(\"f1.rs\");\ninclude!(\"cyc.rs\");\n", &refs);
1995 let ws = Workspace::scan(tmp.path());
1996 let mut r = Resolver::new(&ws);
1997 r.resolve("k", "at_1").expect("depth 1");
1998 r.resolve("k", "at_8").expect("a file spliced at depth 8");
1999 assert!(
2000 r.resolve("k", "at_9").is_err(),
2001 "an include written at depth 8 is refused"
2002 );
2003 r.resolve("k", "cyc_fn")
2004 .expect("a self-including file still terminates");
2005 }
2006
2007 #[test]
2009 fn a_long_re_export_chain_is_refused() {
2010 let mut lib = String::from("pub mod real { pub fn f() {} }\n");
2011 for i in 0..10 {
2012 let to = if i == 9 {
2013 "real".to_string()
2014 } else {
2015 format!("n{}", i + 1)
2016 };
2017 lib.push_str(&format!("pub use crate::{to} as n{i};\n"));
2018 }
2019 let tmp = one_crate(&lib, &[]);
2020 let ws = Workspace::scan(tmp.path());
2021 let mut r = Resolver::new(&ws);
2022 r.resolve("k::n8", "f").expect("two hops resolve");
2023 let e = r
2024 .resolve("k::n0", "f")
2025 .expect_err("ten hops exceed the bound");
2026 assert!(e.reason.contains("deeper than 8"), "{}", e.reason);
2027 }
2028
2029 #[test]
2031 fn a_long_glob_chain_is_refused() {
2032 let mut lib = String::from("pub use crate::g1::*;\n");
2033 for i in 1..=10 {
2034 if i == 10 {
2035 lib.push_str("pub mod g10 { pub fn f() {} }\n");
2036 } else {
2037 lib.push_str(&format!(
2038 "pub mod g{i} {{ pub use crate::g{}::*; }}\n",
2039 i + 1
2040 ));
2041 }
2042 }
2043 let tmp = one_crate(&lib, &[]);
2044 let ws = Workspace::scan(tmp.path());
2045 let mut r = Resolver::new(&ws);
2046 r.resolve("k::g7", "f").expect("three glob hops resolve");
2047 let e = r
2048 .resolve("k", "f")
2049 .expect_err("ten glob hops exceed the bound");
2050 assert!(e.reason.contains("deeper than 8"), "{}", e.reason);
2051 }
2052
2053 #[test]
2056 fn first_of_reports_the_last_non_cycle_error() {
2057 let tmp = one_crate(
2058 "pub mod a;\npub mod b;\npub use crate::a::*;\npub use crate::b::*;\n",
2059 &[
2060 ("a.rs", "pub use crate::b::*;\n"),
2061 ("b.rs", "pub fn other() {}\n"),
2062 ],
2063 );
2064 let ws = Workspace::scan(tmp.path());
2065 let e = Resolver::new(&ws)
2066 .resolve("k::seg", "f")
2067 .expect_err("no such module");
2068 assert!(e.reason.contains("k/src/b.rs"), "{}", e.reason);
2069 assert!(!e.reason.contains("re-export cycle"), "{}", e.reason);
2070 assert!(!e.reason.contains("nor under"), "{}", e.reason);
2071 }
2072
2073 #[test]
2075 fn a_plain_segment_after_a_type_is_not_a_module() {
2076 let tmp = one_crate("pub struct T;\nimpl T { pub fn f() {} }\n", &[]);
2077 let ws = Workspace::scan(tmp.path());
2078 let e = Resolver::new(&ws)
2079 .resolve("k::T::x", "f")
2080 .expect_err("T is a type");
2081 assert!(e.reason.contains("is a type, not a module"), "{}", e.reason);
2082 }
2083
2084 #[test]
2086 fn type_member_ignores_an_impl_for_another_type() {
2087 let tmp = one_crate(
2088 "pub struct T;\npub struct U;\nimpl U { pub fn f() {} }\n",
2089 &[],
2090 );
2091 let ws = Workspace::scan(tmp.path());
2092 let e = Resolver::new(&ws)
2093 .resolve("k::T", "f")
2094 .expect_err("T has no f; only U does");
2095 assert!(
2096 e.reason.contains("no `fn f` in an `impl T`"),
2097 "{}",
2098 e.reason
2099 );
2100 }
2101
2102 #[test]
2105 fn type_member_matches_the_named_non_generic_trait_only() {
2106 let tmp = one_crate(
2107 "pub struct T;\npub trait Other { fn f(); }\npub trait G { fn g(); }\n",
2108 &[],
2109 );
2110 let ws = Workspace::scan(tmp.path());
2111 let e = Resolver::new(&ws)
2112 .resolve("k::T", "f")
2113 .expect_err("Other::f is not T::f");
2114 assert!(e.reason.contains("no `fn f`"), "{}", e.reason);
2115 assert!(
2116 Resolver::new(&ws).resolve("k::G", "g").is_ok(),
2117 "trait G::g resolves"
2118 );
2119 assert!(
2120 Resolver::new(&ws).resolve("k::G::<u8>", "g").is_err(),
2121 "a trait is not generic-qualified"
2122 );
2123 }
2124}