metrale_circuit/venn/
discover.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-29: Rediscover each family's instantiated points from the kernel sources and report
4//! where the manifest and the sources disagree (drift).
5//!
6//! Owner: metrale-circuit (venn).
7//! Invariants:
8//! - Pure: the caller lists the repo's kernel files and supplies the text of every file a
9//!   `macro` rule reads ([`macro_files`]); a file a rule needs and the caller did not supply is
10//!   itself drift, never skipped.
11//! - Drift is two-way: a point the sources realise that the manifest lacks, and a point the
12//!   manifest declares at a file whose rule does not find it there.
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use super::families::{Discover, Families, Values};
17use crate::precision::glob;
18
19/// 2026-09-29: The kernel sources discovery reads.
20#[derive(Debug, Clone, Default)]
21pub struct KernelSources {
22    /// 2026-09-29: Every repo-relative file under `kernels/` (the file rules glob these).
23    pub paths: BTreeSet<String>,
24    /// 2026-09-29: Path to text, for the files [`macro_files`] names.
25    pub texts: BTreeMap<String, String>,
26}
27
28/// 2026-09-29: The files whose text a `macro` rule reads.
29pub fn macro_files(families: &Families) -> BTreeSet<String> {
30    families
31        .families
32        .iter()
33        .flat_map(|f| f.discover.iter())
34        .filter_map(|d| match d {
35            Discover::Macro { file, .. } => Some(file.clone()),
36            Discover::File { .. } => None,
37        })
38        .collect()
39}
40
41/// 2026-09-29: One point a rule found: its family, the values the rule fixes, and the file.
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
43pub struct Found {
44    /// 2026-09-29: Family id.
45    pub family: String,
46    /// 2026-09-29: The values the rule fixes (a subset of the family's point parameters).
47    pub values: Values,
48    /// 2026-09-29: Repo-relative file.
49    pub file: String,
50}
51
52/// 2026-09-29: Every point the discovery rules find in `src`, and the problems met on the way
53/// (a macro file not supplied, an argument missing, an argument no `map` entry spells).
54pub fn discover(families: &Families, src: &KernelSources) -> (BTreeSet<Found>, Vec<String>) {
55    let mut found = BTreeSet::new();
56    let mut problems = Vec::new();
57    for f in &families.families {
58        for rule in &f.discover {
59            match rule {
60                Discover::File { glob: g, values } => {
61                    for p in src.paths.iter().filter(|p| glob(g, p)) {
62                        found.insert(Found {
63                            family: f.id.clone(),
64                            values: values.clone(),
65                            file: p.clone(),
66                        });
67                    }
68                }
69                Discover::Macro {
70                    file,
71                    name,
72                    args,
73                    map,
74                } => {
75                    let Some(text) = src.texts.get(file) else {
76                        problems.push(format!("{}: macro file {file} was not supplied", f.id));
77                        continue;
78                    };
79                    for call in invocations(text, name) {
80                        match point_of(&call, args, map) {
81                            Ok(values) => {
82                                found.insert(Found {
83                                    family: f.id.clone(),
84                                    values,
85                                    file: file.clone(),
86                                });
87                            }
88                            Err(e) => problems.push(format!("{}: {file}: {name}: {e}", f.id)),
89                        }
90                    }
91                }
92            }
93        }
94    }
95    (found, problems)
96}
97
98/// 2026-09-29: Every disagreement between the manifest's points and the sources; empty when
99/// they agree.
100pub fn drift(families: &Families, src: &KernelSources) -> Vec<String> {
101    let (found, mut problems) = discover(families, src);
102    for f in &families.families {
103        for p in &f.points {
104            for file in &p.files {
105                if !src.paths.contains(file) {
106                    problems.push(format!(
107                        "{}: point {:?} names {file}, which does not exist",
108                        f.id, p.values
109                    ));
110                }
111            }
112        }
113        for d in found.iter().filter(|d| d.family == f.id) {
114            let declared = f.points.iter().any(|p| {
115                p.files.contains(&d.file)
116                    && d.values.iter().all(|(k, v)| p.values.get(k) == Some(v))
117            });
118            if !declared {
119                problems.push(format!(
120                    "{}: {} realises {:?}, which no declared point lists at that file",
121                    f.id, d.file, d.values
122                ));
123            }
124        }
125        // 2026-09-29: A declared point at a file some rule covers must be found there.
126        for p in &f.points {
127            for file in &p.files {
128                let covered = f.discover.iter().any(|r| match r {
129                    Discover::File { glob: g, .. } => glob(g, file),
130                    Discover::Macro { file: m, .. } => m == file,
131                });
132                let seen = found.iter().any(|d| {
133                    d.family == f.id
134                        && &d.file == file
135                        && d.values.iter().all(|(k, v)| p.values.get(k) == Some(v))
136                });
137                if covered && !seen {
138                    problems.push(format!(
139                        "{}: point {:?} is declared at {file}, but no rule finds it there",
140                        f.id, p.values
141                    ));
142                }
143            }
144        }
145    }
146    problems
147}
148
149/// 2026-09-29: The argument lists of every line that starts (after indentation) with
150/// `name(`: one per invocation, split at top-level commas and trimmed.
151fn invocations(text: &str, name: &str) -> Vec<Vec<String>> {
152    let open = format!("{name}(");
153    text.lines()
154        .filter_map(|l| l.trim_start().strip_prefix(open.as_str()))
155        .map(|rest| {
156            let mut depth = 0usize;
157            let mut args = vec![String::new()];
158            for c in rest.chars() {
159                match c {
160                    '(' | '<' => depth += 1,
161                    ')' if depth == 0 => break,
162                    ')' | '>' => depth = depth.saturating_sub(1),
163                    ',' if depth == 0 => {
164                        args.push(String::new());
165                        continue;
166                    }
167                    _ => {}
168                }
169                if let Some(last) = args.last_mut() {
170                    last.push(c);
171                }
172            }
173            args.into_iter().map(|a| a.trim().to_string()).collect()
174        })
175        .collect()
176}
177
178fn point_of(
179    call: &[String],
180    args: &BTreeMap<String, usize>,
181    map: &BTreeMap<String, BTreeMap<String, String>>,
182) -> Result<Values, String> {
183    let mut values = Values::new();
184    for (param, &i) in args {
185        let raw = call
186            .get(i)
187            .ok_or_else(|| format!("argument {i} missing in {call:?}"))?;
188        let v = match map.get(param) {
189            Some(m) => m
190                .get(raw)
191                .cloned()
192                .ok_or_else(|| format!("argument `{raw}` of `{param}` has no map entry"))?,
193            None => raw.clone(),
194        };
195        values.insert(param.clone(), v);
196    }
197    Ok(values)
198}
199
200#[cfg(test)]
201#[path = "discover_tests.rs"]
202mod discover_tests;