Skip to main content

provable_contracts/ontology/extract/
code.rs

1//! ONT-001 §3.7, §5 ONT-4b2 — `extract:code`: the Rust symbols the bindings name become `ont:Symbol` nodes.
2//!
3//! The focus objects are the **bound** symbols — every `module_path` + `function` a `binding.yaml` under the
4//! contract dir declares — not every function in the workspace (3.9 M lines; a graph of every `fn` would be a
5//! second copy of the tree, and no contract constrains a symbol nothing binds). Each is resolved by a `syn`
6//! **module-tree walk**: the crate root from the workspace's `Cargo.toml`s (`[package] name` and `[lib] name`,
7//! both spellings), then one `mod` per path segment — inline, `mod x;` as `x.rs` / `x/mod.rs` / `#[path]`, or a
8//! `pub use` re-export followed once — and finally a free `fn` or an `impl` method of that name. Only the files
9//! on the walk are parsed, so the cost is bindings × depth, not the workspace.
10//!
11//! **Fail-closed, said in the graph.** A symbol the walk cannot find is still a node — `sym:resolved false`
12//! with `sym:unresolvedReason` naming the file and the segment — so a shape can require `sym:resolved true`
13//! and a ghost binding (ONT-3a's thesis) is a violation naming the symbol, not a silently smaller graph.
14//! Nothing is inferred: the module path is the binding's own text, normalized only by dropping a trailing
15//! segment equal to the function (the registries write both `a::b::f` + `f` and `a::b` + `f`).
16//!
17//! IRI: `https://ont.paiml.dev/v1alpha1/symbol/<crate>::<module>::<function>`. Vocabulary `sym:*`. Attributes
18//! are recorded by path (`inline`, `kernel`, …) so ONT-4c3 can type a `#[kernel]` symbol `ont:Kernel` from the
19//! same walk. Deterministic: registries and bindings are visited in byte order and the graph is a set (R-15).
20
21use 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/// A `sym:*` vocabulary term.
28#[must_use]
29pub fn sym(name: &str) -> String {
30    ont(&format!("sym/{name}"))
31}
32
33/// What the walk found for one bound symbol.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Resolved {
36    /// Repo-relative file the item is defined in.
37    pub file: String,
38    /// `pub`, `pub(crate)`, `pub(super)`, `pub(in …)` or `private`.
39    pub visibility: String,
40    /// `fn` or `method`.
41    pub kind: String,
42    /// Attribute paths on the item, in source order (`inline`, `kernel`, `cfg`, …).
43    pub attributes: Vec<String>,
44    /// ONT-4c4: no `unsafe fn` signature and no `unsafe { … }` block in the body.
45    pub unsafe_free: bool,
46    /// ONT-4c4: no `get_unchecked` / `get_unchecked_mut` call in the body — every index is bounds-checked.
47    pub bounds_checked: bool,
48}
49
50/// Why the walk stopped.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Unresolved {
53    pub reason: String,
54}
55
56/// Counts the extractor reports beside the graph.
57#[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    /// Distinct crate roots a walk entered (ONT-3a `crates_scanned`: a resolver that only ever opened one crate
65    /// has not checked a workspace).
66    pub crates_scanned: usize,
67}
68
69/// One bound symbol as a registry states it.
70#[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/// Every `binding.yaml` under `contract_dir` (any depth, byte order), parsed. Walked here, not through
80/// `collect_yaml_files`, which excludes registries by name because they are not contracts. A file that does not
81/// parse as a registry is skipped: registries are validated by `pv validate` (BINDING-001..006), not here.
82#[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/// The bound symbols of one registry, with the module path normalized (a trailing segment equal to the function
106/// is dropped). Bindings without a `function` bind nothing and are skipped.
107#[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            // `function: tokenize::run` under `module_path: apr_cli::commands::tokenize` — the registries
119            // also write a qualified function; the qualifier joins the module path (once, if it is not
120            // already its tail) and the function is the last segment.
121            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/// The crates of a workspace: every `Cargo.toml` under `root` (skipping build and vcs dirs), keyed by both the
147/// package name and the `[lib] name`, `-` spelled `_`, mapped to the crate root files that claim the name —
148/// more than one when a facade package and the library it re-exports share it (aprender's root `aprender`
149/// is `pub use aprender_ml::*` over `crates/aprender-core`, whose `[lib] name` is also `aprender`). The walk
150/// tries each in byte order of the manifest path and keeps the first that resolves.
151#[derive(Debug, Default)]
152pub struct Workspace {
153    pub root: PathBuf,
154    pub crates: BTreeMap<String, Vec<PathBuf>>,
155    /// `root/Cargo.toml` has a `[workspace]` table. When it does, only its `members` (globs honoured) minus its
156    /// `exclude` are crates — a stray manifest under the root (a test fixture, an excluded canary) is not a
157    /// workspace member and a binding into it does not resolve (ONT-3a).
158    pub is_workspace_root: bool,
159    /// Non-member manifests that declare their OWN `[workspace]` — a standalone cargo workspace under the root
160    /// (`experiments/cuda-oxide/gated-rmsnorm`: its own toolchain and `Cargo.lock`). Indexed, NOT admitted: only
161    /// a `kernel: true` binding naming one admits it ([`Workspace::admit_standalone`], ONT-4c4).
162    pub standalone: BTreeMap<String, Vec<PathBuf>>,
163}
164
165impl Workspace {
166    /// Scan `root` for manifests. A manifest without a `[package]` name (a virtual workspace root) contributes
167    /// nothing.
168    #[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    /// A non-member manifest is recorded only when it is its own workspace root; a stray manifest (a test
215    /// fixture, an excluded canary) stays invisible, as ONT-3a requires.
216    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    /// Admit the standalone crate `krate` (a `kernel: true` binding names it) unless a member already owns the
225    /// name. Returns whether anything was admitted.
226    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
240/// Key `manifest`'s package and `[lib]` names (`-` spelled `_`) to its crate root in `map`.
241fn 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/// `[workspace] members` / `exclude` of the root manifest, plus whether the root is itself a package.
256#[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    /// Is the crate in `dir` a member? Paths compare relative to the root, `.` for the root itself.
265    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
279/// The `[workspace]` table's `members` and `exclude` arrays (single- or multi-line, either quote), or `None` when
280/// the manifest has no `[workspace]` table.
281pub(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; // Some(true) = collecting members, Some(false) = exclude
286    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
310/// The table name of a `[section]` header line, or `None` when `t` is not one.
311fn 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
316/// The part of `t` that holds array items: all of it inside an open array, or the value of a `members` /
317/// `exclude` key in `[workspace]`, which opens one (`open`: `Some(true)` = members, `Some(false)` = exclude).
318fn 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
334/// The quoted strings of one TOML line, `"…"` or `'…'`.
335fn 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
348/// Cargo's member globs, component-wise: `*` matches within one component (`crates/*`, `crates/apr-*`).
349fn 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
368/// `[package] name`, `[lib] name` and `[lib] path` by a section-aware line scan — the two keys this walk needs,
369/// read without a TOML crate.
370pub(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
394/// The `syn` walker with a per-file parse cache.
395pub struct Resolver<'a> {
396    ws: &'a Workspace,
397    cache: BTreeMap<PathBuf, Option<std::rc::Rc<syn::File>>>,
398    /// Crate root files a walk entered.
399    crates_walked: std::collections::BTreeSet<PathBuf>,
400    /// `(module_path, name)` pairs already tried by the current `resolve` — glob re-exports can cycle
401    /// (`pub use a::*` in `b`, `pub use b::*` in `a`), and a pair tried twice cannot succeed the second time.
402    seen: std::collections::BTreeSet<(String, String)>,
403}
404
405/// One module on the walk: its items with `include!()` spliced in, the file each item came from, the module's
406/// own file, and the directory its child files live in (an included file's `mod x;` is still relative to the
407/// including module, as in rustc).
408struct Module {
409    items: Vec<syn::Item>,
410    origins: Vec<PathBuf>,
411    file: PathBuf,
412    child_dir: PathBuf,
413    /// Every file `include!()`d into this module (at any nesting depth), keyed by its stem: `ops::activation::matmul`
414    /// names `matmul` in the `activation.rs` that `ops/mod.rs` includes. Rust has no such path, but the binding rows
415    /// name the file the item is written in, and the item does exist (infra-83 ruling on #4502).
416    includes: Vec<(String, PathBuf)>,
417}
418
419/// `include!` nesting bound — the macro recurses, a cycle must not.
420const 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    /// Distinct crate roots entered so far.
434    #[must_use]
435    pub fn crates_walked(&self) -> usize {
436        self.crates_walked.len()
437    }
438
439    /// Files parsed so far (each once).
440    #[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    /// The crate root files that claim `krate`, in manifest order.
458    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    /// A module from `items` written in `file`, `include!()`s spliced.
483    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    /// Append `items` to `module`, replacing each `include!` item naming a path `p` by the items of `p` — resolved against the
496    /// directory of the file the macro is written in, recursively. An include that is missing or does not parse
497    /// contributes nothing, so what it would have defined stays unresolved (fail-closed).
498    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    /// Resolve `module_path::function` to its definition. `depth` bounds re-export chasing.
519    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    /// The walk from one crate root file.
563    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    /// `seg` is re-exported here from `target`. `use crate::a::T;` then `impl T { fn f() }` in THIS module: the
589    /// binding names this module, so its own impls are searched before following the `use` to T's definition.
590    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    /// `function` in the module the walk ended in: defined here, `use`d here, or under a `pub use …::*`.
606    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    /// Continue through the first `pub use <glob>::*` under which `seg::rest` resolves.
644    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    /// `Type::function` (or `Type::<Arg>::function`): a method of an `impl` whose self type is `ty` in this
671    /// module, or a method of `trait ty`. Searched: the module that defines the type, and a module the binding
672    /// names that imports the type (`use`) and implements it there. An `impl` anywhere else is not found.
673    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    /// `mod seg` in this module (inline, or its file), or `None` when the module declares no such `mod`.
749    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    /// `seg` is the stem of a file `include!()`d into this module: step into THAT file's items alone (its own
771    /// `include!`s spliced), so a fn written in a sibling included file is still refused. A real `mod`, `use` or
772    /// type of the same name is tried first.
773    ///
774    /// The file may also be included by a CHILD module's file in the same directory: `metaheuristics::
775    /// cmaes_include_01::optimize`, where `metaheuristics/cmaes.rs` (`mod cmaes;`) `include!`s `cmaes_include_01.rs`.
776    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    /// `child_dir/seg.rs`, when a file-backed `mod x;` of this module lives in `child_dir` and includes it.
792    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    /// The segment names a type (or a trait) defined in this module.
839    Type,
840    /// No such segment here, but these `pub use …::*` globs may bring it in.
841    Globs(Vec<String>),
842}
843
844/// `target` with the remaining segments appended.
845fn 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
853/// The path an `include!` item names (a string literal only — `concat!(env!("OUT_DIR"), …)` is a build
854/// artifact the tree does not hold).
855fn 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
867/// A struct, enum, union, trait or type alias named `name`, or an `impl` for it, among `items`.
868fn 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
880/// Is `im` an `impl` for the type `ty` — and, when `generic` is given, for `ty<generic>` or a blanket
881/// `impl<T> ty<T>`?
882fn 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
913/// The `fn name` of a trait (declared or defaulted).
914fn 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
925/// The absolute paths of every `pub use <path>::*` among `items`.
926fn 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
956/// `mod seg;` → `#[path = "…"]` relative to the child dir, else `seg.rs`, else `seg/mod.rs`. The child dir of
957/// the new module is `<dir>/seg/` either way.
958fn 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
994/// A free `fn name`, an `impl … { fn name }`, or a named item (`struct`, `enum`, `trait`, …) among `items`.
995fn find_item(items: &[syn::Item], name: &str) -> Option<Resolved> {
996    find_indexed(items, name).map(|(_, r)| r)
997}
998
999/// [`find_item`] with the index of the item it came from (so the caller can name its origin file).
1000fn 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
1028/// The `fn name` of one `impl` block.
1029fn 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
1038/// A resolution with the file filled in by the caller that knows it.
1039fn 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
1058/// [`found`] for an item with no function body to read (a type, a const, a trait method's declaration). ONT-4c4's
1059/// two safety facts are read only for a bound `#[kernel]` fn, which always resolves through [`found`]; nothing
1060/// here is ever graded on them, so they are recorded as holding rather than left unset.
1061fn 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/// What ONT-4c4's `kernel-safety` reads off a function body: `unsafe` blocks and unchecked indexing.
1073#[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
1106/// The path a `use` item binds `name` to, as written (`crate::a::b`, `self::x`, `super::y`, `other::z`), when
1107/// one does; renames (`as`) are honoured.
1108fn 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
1141/// Make a `use` path absolute from the crate root: `crate::` → the crate's name; `self::` → this module;
1142/// `super::` → its parent; a bare leading segment is first tried as a workspace crate, else as a sibling module.
1143fn 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
1163/// The module path of a module on the walk, from its child dir relative to the crate's `src/`:
1164/// `<crate>` for the root, `<crate>::a::b` below it.
1165fn module_path_of(module: &Module, ws: &Workspace) -> String {
1166    // The crate whose `src/` is the LONGEST prefix of the module's dir owns it (a workspace root package's
1167    // `src/` is a prefix of nothing under `crates/`, but nested crates exist).
1168    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/// The symbol IRI of a bound symbol.
1226#[must_use]
1227pub fn symbol_iri(b: &Bound) -> String {
1228    iri("symbol", &format!("{}::{}", b.module_path, b.function))
1229}
1230
1231/// One bound symbol into `g`, resolved or not.
1232pub 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            // ONT-4c4: only a `#[kernel]` carries the body facts — `kernel-safety` reads them; no other shape does.
1261            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/// Every bound symbol of every registry under `contract_dir`, each with its resolution — the walk shared by
1278/// [`extract`] (graph) and the `bindings` lint gate (ONT-3a, verdict).
1279#[derive(Debug, Default)]
1280pub struct Resolution {
1281    pub stats: CodeStats,
1282    pub symbols: Vec<(Bound, Result<Resolved, Unresolved>)>,
1283    /// The repo root has a `[workspace]` manifest.
1284    pub at_workspace_root: bool,
1285}
1286
1287/// Resolve every bound symbol under `contract_dir` against the workspace at the contract dir's parent.
1288#[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    // ONT-4c4: a cuda-oxide `#[kernel]` lives in its own cargo workspace (pinned nightly), never a member; the
1294    // `kernel: true` binding that names its crate is what admits it — nothing else does.
1295    for (_file, registry) in &regs {
1296        for b in registry.bindings.iter().filter(|b| b.kernel) {
1297            let path = b.module_path.as_deref().unwrap_or(&registry.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(&registry) {
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
1326/// Every bound symbol of every registry under `contract_dir`, resolved against the workspace at the contract
1327/// dir's parent, into `g`.
1328pub 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/// Positive control (`pc_extract.code`, run in memory every gate run): the resolver must find a function that
1337/// is there and must refuse one that is not — a walker that resolved everything would make every ghost binding
1338/// a Pass.
1339#[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(&reg);
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    /// `use crate::a::T;` then `impl T<P> { fn f() }` in another module: the binding naming that module resolves
1465    /// to the impl there (aprender-contrastive-data's `attestation::PreparedDataset::<Canonical>::…`), and a
1466    /// method no impl has is still refused.
1467    #[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    /// ONT-4c4 (C196): a cuda-oxide `#[kernel]` lives in its own cargo workspace, never a root member. A
1496    /// `kernel: true` binding naming that crate admits it and the kernel resolves, attribute and all; the same
1497    /// row without `kernel: true` stays unresolved (ONT-3a), and a stray non-member manifest with no
1498    /// `[workspace]` of its own is never indexed.
1499    #[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    /// #4502: a binding row names the file an `include!()`d item is written in (`ops::activation::matmul`, with
1561    /// `ops/mod.rs` `include!`-ing `activation.rs`; `cmaes::cmaes_include_01::optimize` for a method in a file
1562    /// the sibling `cmaes.rs` includes). The item exists, so the row resolves; an item of a DIFFERENT included file,
1563    /// or a stem nothing includes, is still refused.
1564    #[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    /// Two modules that glob-re-export each other (`a: pub use crate::b::*`, `b: pub use crate::a::*`) cycle when
1617    /// a segment neither defines is chased. The cycle is a dead end, not the answer: the refusal names the last
1618    /// real miss (`no mod x … in b.rs`), never `re-export cycle` (mutant `code.rs:628` guard → false, #4588).
1619    #[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    /// A one-crate workspace `k` with `lib` as `k/src/lib.rs` and `files` as `(path under k/src, text)`.
1641    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    /// A struct, enum, union or type alias with no impl in its module is still a TYPE segment: the refusal
1655    /// names the missing method of that type, never a missing `mod` (mutants `code.rs:835-839`, each arm
1656    /// deleted, #4588).
1657    #[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    /// `Type::function` finds a method only in an impl FOR that type or a trait OF that name, and an extra
1677    /// segment that is not `<Arg>` is refused as "a type, not a module" (mutants `code.rs:647` guard → true,
1678    /// `:659` guard → true, `:662` guard → true and `&&` → `||`, #4588).
1679    #[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    /// Only a FILE-backed child `mod x;` can include a sibling file: an inline `mod c { }` that happens to share
1704    /// its name with a file `c.rs` including `inc.rs` does not make `k::inc` resolve (mutant `code.rs:766` guard
1705    /// → true, #4588).
1706    #[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    /// An inline `mod foo { }` is not a file-backed child: a `foo.rs` on disk that happens to include `inc.rs`
1731    /// must not make `inc` resolvable from the parent.
1732    #[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    /// `[workspace]` membership: multi-line arrays, either quote, trailing `/`, comments, `exclude`, keys outside
1811    /// `[workspace]` ignored, a root `[package]` recorded, and no `[workspace]` table at all is `None`.
1812    #[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    /// ONT-4c4: only a `#[kernel]` item carries `unsafeFree` / `boundsChecked`; any other attribute carries neither.
1837    #[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    /// A one-crate workspace `k` under a tempdir: `files` are `(path under k/src, text)`, `lib` is `lib.rs`.
1887    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    /// #4587: only a member's own `Cargo.toml` is indexed — an excluded crate's manifest and a non-manifest
1901    /// `.toml` file in a member directory are not.
1902    #[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    /// The root directory is a member only as a root `[package]` or as an explicit `"."` member.
1932    #[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    /// `include!` recurses to a bound: a file written at depth 8 is still spliced, one included from it is not,
1974    /// and a file that includes itself terminates.
1975    #[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    /// A re-export chain longer than the bound is refused; a short one resolves.
2008    #[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    /// A chain of `pub use …::*` globs longer than the bound is refused; a short one resolves.
2030    #[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    /// Under several globs the error reported is the last NON-cycle one: a cycle on a later glob neither
2054    /// replaces it nor is it dropped in favour of the generic message.
2055    #[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    /// `Type::seg::f` — a non-generic segment after a type is not a generic argument.
2074    #[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    /// #4587: `Type::f` is found only in an `impl` for `Type` — another type's `impl` with an `f` is no hit.
2085    #[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    /// #4587: `Type::f` is found in `trait Type` only — another trait's `f`, or a generic-qualified path
2103    /// to a trait, is no hit.
2104    #[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}