1use std::collections::{BTreeMap, BTreeSet};
34use std::path::{Path, PathBuf};
35
36use super::code::sym;
37use crate::ontology::rdf::{iri, ont, Graph, Term, RDF_TYPE};
38use crate::ontology::shapes::RDFS_SUBCLASS_OF;
39
40pub const SCHEMA: &str = "apr-kernel-receipt/v1";
42pub const EVIDENCE_DIR: &str = "evidence/kernels";
44pub const REQUIRED_HOSTS_FILE: &str = "evidence/kernels/REQUIRED_HOSTS";
46pub const PARITY_MIN_COS: f64 = 0.9999;
48pub const PARITY_MAX_DIFF: f64 = 1e-3;
50pub const TIMING_MAX_RATIO: f64 = 1.2;
52
53#[must_use]
55pub fn kern(name: &str) -> String {
56 ont(&format!("kernel/{name}"))
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct KernelError {
62 pub file: String,
63 pub reason: String,
64}
65
66impl std::fmt::Display for KernelError {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 write!(f, "kernel receipt {}: {}", self.file, self.reason)
69 }
70}
71
72impl std::error::Error for KernelError {}
73
74#[derive(Debug, Clone, Default, PartialEq, Eq)]
76pub struct KernelStats {
77 pub kernels: usize,
79 pub receipt_files: usize,
80 pub witnesses: usize,
81 pub sha_mismatches: usize,
82 pub orphan_rows: usize,
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub struct Row {
89 pub kernel: String,
90 pub host: String,
91 pub cc: String,
92 pub sha: String,
93 pub cos: Option<f64>,
94 pub maxdiff: Option<f64>,
95 pub oxide_us: Option<f64>,
96 pub ptx_us: Option<f64>,
97 pub register_budget: Option<u64>,
98 pub ptxas_version: String,
99 pub authoring: String,
100}
101
102#[derive(Debug, Clone, PartialEq)]
104pub struct ReceiptFile {
105 pub file: String,
107 pub sha: String,
108 pub rows: Vec<Row>,
109}
110
111#[derive(Debug, Clone, Default)]
113pub struct Inputs {
114 pub receipts: Vec<ReceiptFile>,
115 pub required_hosts: Option<Vec<String>>,
117 pub exemptions: BTreeMap<String, String>,
119}
120
121pub fn extract(root: &Path, g: &mut Graph) -> Result<KernelStats, KernelError> {
126 let inputs = read_inputs(root)?;
127 let stats = resolve(g, &inputs);
128 symbol_and_contract(g)?;
129 Ok(stats)
130}
131
132fn read_inputs(root: &Path) -> Result<Inputs, KernelError> {
133 let dir = root.join(EVIDENCE_DIR);
134 let mut files = Vec::new();
135 walk(&dir, &mut files);
136 files.sort();
137 let rel = |p: &Path| {
138 p.strip_prefix(root)
139 .unwrap_or(p)
140 .to_string_lossy()
141 .replace('\\', "/")
142 };
143 let receipts = files
144 .iter()
145 .map(|f| {
146 let name = rel(f);
147 let text = std::fs::read_to_string(f).map_err(|e| KernelError {
148 file: name.clone(),
149 reason: format!("unreadable: {e}"),
150 })?;
151 let v: serde_json::Value = serde_json::from_str(&text).map_err(|e| KernelError {
152 file: name.clone(),
153 reason: format!("not JSON: {e}"),
154 })?;
155 parse_receipt(&name, &v)
156 })
157 .collect::<Result<Vec<_>, _>>()?;
158 let required_hosts = std::fs::read_to_string(root.join(REQUIRED_HOSTS_FILE))
159 .ok()
160 .map(|t| {
161 t.lines()
162 .map(|l| l.split('#').next().unwrap_or_default().trim().to_string())
163 .filter(|l| !l.is_empty())
164 .collect()
165 });
166 let mut exemptions = BTreeMap::new();
167 if let Ok(rd) = std::fs::read_dir(&dir) {
168 for e in rd.flatten() {
169 let p = e.path().join("EXEMPTION");
170 let reason = std::fs::read_to_string(&p).unwrap_or_default();
171 if !reason.trim().is_empty() {
172 exemptions.insert(e.file_name().to_string_lossy().into_owned(), rel(&p));
173 }
174 }
175 }
176 Ok(Inputs {
177 receipts,
178 required_hosts,
179 exemptions,
180 })
181}
182
183fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
184 let Ok(rd) = std::fs::read_dir(dir) else {
185 return;
186 };
187 for e in rd.flatten() {
188 let p = e.path();
189 if p.is_dir() {
190 walk(&p, out);
191 } else if p.extension().and_then(|x| x.to_str()) == Some("json") {
192 out.push(p);
193 }
194 }
195}
196
197fn s(v: &serde_json::Value, k: &str) -> String {
198 match v.get(k) {
199 Some(serde_json::Value::String(s)) => s.clone(),
200 Some(serde_json::Value::Number(n)) => n.to_string(),
201 _ => String::new(),
202 }
203}
204
205fn f(v: &serde_json::Value, outer: &str, k: &str) -> Option<f64> {
206 v.get(outer)
207 .and_then(|o| o.get(k))
208 .and_then(serde_json::Value::as_f64)
209}
210
211pub fn parse_receipt(file: &str, v: &serde_json::Value) -> Result<ReceiptFile, KernelError> {
216 let schema = s(v, "schema");
217 if schema != SCHEMA {
218 return Err(KernelError {
219 file: file.to_string(),
220 reason: format!(
221 "schema `{schema}` under {EVIDENCE_DIR}/ — this root holds {SCHEMA} only"
222 ),
223 });
224 }
225 let sha = s(v, "sha").to_ascii_lowercase();
226 if sha.is_empty() {
227 return Err(KernelError {
228 file: file.to_string(),
229 reason: "no build `sha`: no row can be a witness of an unnamed build".into(),
230 });
231 }
232 let rows = v
233 .get("rows")
234 .and_then(serde_json::Value::as_array)
235 .map(|rows| {
236 rows.iter()
237 .map(|r| Row {
238 kernel: s(r, "kernel"),
239 host: s(r, "host"),
240 cc: s(r, "cc"),
241 sha: s(r, "sha").to_ascii_lowercase(),
242 cos: f(r, "parity", "cos"),
243 maxdiff: f(r, "parity", "maxdiff"),
244 oxide_us: f(r, "timing", "oxide_us"),
245 ptx_us: f(r, "timing", "ptx_us"),
246 register_budget: r.get("register_budget").and_then(serde_json::Value::as_u64),
247 ptxas_version: s(r, "ptxas_version"),
248 authoring: s(r, "authoring"),
249 })
250 .collect()
251 })
252 .unwrap_or_default();
253 Ok(ReceiptFile {
254 file: file.to_string(),
255 sha,
256 rows,
257 })
258}
259
260fn literal<'g>(g: &'g Graph, subject: &str, predicate: &str) -> Vec<&'g str> {
261 g.objects(subject, predicate)
262 .into_iter()
263 .filter_map(|t| t.as_literal().map(|(v, _)| v))
264 .collect()
265}
266
267fn kernel_symbols(g: &Graph) -> Vec<String> {
269 let mut out: Vec<String> = g
270 .instances_of(&ont("Symbol"))
271 .into_iter()
272 .filter(|s| literal(g, s, &sym("attribute")).contains(&"kernel"))
273 .filter(|s| literal(g, s, &sym("resolved")).contains(&"true"))
274 .map(String::from)
275 .collect();
276 out.sort();
277 out.dedup();
278 out
279}
280
281fn reference_of(g: &Graph, kernel: &str) -> Option<String> {
283 let contracts: BTreeSet<String> = g
284 .objects(kernel, &sym("implements"))
285 .into_iter()
286 .filter_map(|t| t.as_iri().map(String::from))
287 .collect();
288 let equations: BTreeSet<&str> = literal(g, kernel, &sym("equation")).into_iter().collect();
289 let mut found: Vec<&str> = g
290 .instances_of(&ont("Symbol"))
291 .into_iter()
292 .filter(|s| *s != kernel)
293 .filter(|s| !literal(g, s, &sym("attribute")).contains(&"kernel"))
294 .filter(|s| literal(g, s, &sym("resolved")).contains(&"true"))
295 .filter(|s| {
296 g.objects(s, &sym("implements"))
297 .into_iter()
298 .any(|t| t.as_iri().is_some_and(|i| contracts.contains(i)))
299 })
300 .filter(|s| {
301 literal(g, s, &sym("equation"))
302 .iter()
303 .any(|e| equations.contains(e))
304 })
305 .collect();
306 found.sort_unstable();
307 found.first().map(|s| (*s).to_string())
308}
309
310fn names_kernel(row: &str, name: &str, module: &str) -> bool {
313 row == name || row == format!("{module}::{name}")
314}
315
316#[derive(Default)]
319struct Witnessed {
320 parity_on: BTreeSet<String>,
321 timing_on: BTreeSet<String>,
322 measured: BTreeSet<String>,
323 budget: bool,
324}
325
326fn first_literal(g: &Graph, k: &str, p: &str) -> String {
327 literal(g, k, &sym(p))
328 .first()
329 .map(|s| (*s).to_string())
330 .unwrap_or_default()
331}
332
333fn within_timing(row: &Row) -> bool {
334 match (row.oxide_us, row.ptx_us) {
335 (Some(o), Some(p)) if p > 0.0 => o / p <= TIMING_MAX_RATIO,
336 _ => false,
337 }
338}
339
340fn at_parity(row: &Row) -> bool {
341 row.cos.is_some_and(|c| c >= PARITY_MIN_COS)
342 && row.maxdiff.is_some_and(|d| d.abs() < PARITY_MAX_DIFF)
343}
344
345fn type_kernel(g: &mut Graph, k: &str, name: &str, module: &str, inputs: &Inputs) {
347 g.insert(k.to_string(), RDF_TYPE, Term::iri(ont("KernelSymbol")));
348 g.insert(k.to_string(), kern("authoring"), Term::string("oxide"));
349 match reference_of(g, k) {
350 Some(r) => g.insert(k.to_string(), kern("reference"), Term::iri(r)),
351 None => g.insert(
352 k.to_string(),
353 kern("referenceMissing"),
354 Term::string(format!(
355 "#[kernel] {module}::{name} has no CPU reference: its contract binds no other resolved symbol to its equation"
356 )),
357 ),
358 }
359 if let Some(ex) = inputs.exemptions.get(name) {
360 g.insert(k.to_string(), kern("exemptionReceipt"), Term::string(ex));
361 }
362}
363
364fn witness(g: &mut Graph, k: &str, rf: &ReceiptFile, ri: usize, exempt: bool, w: &mut Witnessed) {
366 let row = &rf.rows[ri];
367 let rnode = iri("kernel", &format!("{}#{ri}", rf.file));
368 emit_witness(g, &rnode, row);
369 g.insert(k.to_string(), kern("parityReceipt"), Term::iri(&rnode));
370 w.measured.insert(row.host.clone());
371 if let Some(b) = row.register_budget {
372 w.budget = true;
373 g.insert(k.to_string(), kern("registerBudget"), Term::integer(b));
374 }
375 if at_parity(row) {
376 w.parity_on.insert(row.host.clone());
377 }
378 if within_timing(row) || exempt {
379 w.timing_on.insert(row.host.clone());
380 }
381}
382
383fn join_receipts(
385 g: &mut Graph,
386 k: &str,
387 name: &str,
388 module: &str,
389 inputs: &Inputs,
390 stats: &mut KernelStats,
391 claimed: &mut BTreeSet<(usize, usize)>,
392) -> Witnessed {
393 let exempt = inputs.exemptions.contains_key(name);
394 let mut w = Witnessed::default();
395 for (fi, rf) in inputs.receipts.iter().enumerate() {
396 for (ri, row) in rf.rows.iter().enumerate() {
397 if !names_kernel(&row.kernel, name, module) {
398 continue;
399 }
400 claimed.insert((fi, ri));
401 if row.sha != rf.sha || row.sha.is_empty() {
402 stats.sha_mismatches += 1;
403 let why = format!(
404 "{} row {ri}: sha {} is not the receipt's build sha {}",
405 rf.file, row.sha, rf.sha
406 );
407 g.insert(k.to_string(), kern("receiptShaMismatch"), Term::string(why));
408 continue;
409 }
410 stats.witnesses += 1;
411 witness(g, k, rf, ri, exempt, &mut w);
412 }
413 }
414 w
415}
416
417fn emit_hosts(
419 g: &mut Graph,
420 k: &str,
421 w: Witnessed,
422 required_hosts: Option<&Vec<String>>,
423 safe_code: bool,
424) {
425 let required: BTreeSet<String> = match required_hosts {
426 Some(h) => h.iter().cloned().collect(),
427 None => w.measured,
428 };
429 let facts = [
430 ("parityOn", w.parity_on.iter().collect::<Vec<_>>()),
431 ("timingOn", w.timing_on.iter().collect()),
432 (
433 "missingParityHost",
434 required.difference(&w.parity_on).collect(),
435 ),
436 (
437 "missingTimingHost",
438 required.difference(&w.timing_on).collect(),
439 ),
440 ];
441 for (p, hosts) in facts {
442 for h in hosts {
443 g.insert(k.to_string(), kern(p), Term::string(h));
444 }
445 }
446 let safe = safe_code && w.budget;
447 g.insert(
448 k.to_string(),
449 kern("safety"),
450 Term::string(safe.to_string()),
451 );
452}
453
454pub fn resolve(g: &mut Graph, inputs: &Inputs) -> KernelStats {
456 g.insert(
457 ont("KernelSymbol"),
458 RDFS_SUBCLASS_OF.to_string(),
459 Term::iri(ont("Symbol")),
460 );
461 let kernels = kernel_symbols(g);
462 let mut stats = KernelStats {
463 kernels: kernels.len(),
464 receipt_files: inputs.receipts.len(),
465 ..KernelStats::default()
466 };
467 let mut claimed: BTreeSet<(usize, usize)> = BTreeSet::new();
468 for k in &kernels {
469 let name = first_literal(g, k, "name");
470 let module = first_literal(g, k, "module");
471 let safe_code = literal(g, k, &sym("unsafeFree")).contains(&"true")
472 && literal(g, k, &sym("boundsChecked")).contains(&"true");
473 type_kernel(g, k, &name, &module, inputs);
474 let w = join_receipts(g, k, &name, &module, inputs, &mut stats, &mut claimed);
475 emit_hosts(g, k, w, inputs.required_hosts.as_ref(), safe_code);
476 }
477 stats.orphan_rows = inputs
478 .receipts
479 .iter()
480 .enumerate()
481 .map(|(fi, rf)| {
482 (0..rf.rows.len())
483 .filter(|ri| !claimed.contains(&(fi, *ri)))
484 .count()
485 })
486 .sum();
487 stats
488}
489
490fn symbol_and_contract(g: &Graph) -> Result<(), KernelError> {
494 let contracts: BTreeSet<&str> = g.instances_of(&ont("Contract")).into_iter().collect();
495 let mut both: Vec<&str> = g
496 .instances_of(&ont("Symbol"))
497 .into_iter()
498 .chain(g.instances_of(&ont("KernelSymbol")))
499 .filter(|s| contracts.contains(s))
500 .collect();
501 both.sort_unstable();
502 match both.first() {
503 None => Ok(()),
504 Some(s) => Err(KernelError {
505 file: (*s).to_string(),
506 reason: format!("kernel: {s} typed both Symbol and Contract"),
507 }),
508 }
509}
510
511fn emit_witness(g: &mut Graph, rnode: &str, row: &Row) {
512 g.insert(rnode.to_string(), RDF_TYPE, Term::iri(kern("Receipt")));
513 g.insert(rnode.to_string(), kern("host"), Term::string(&row.host));
514 g.insert(rnode.to_string(), kern("cc"), Term::string(&row.cc));
515 g.insert(rnode.to_string(), kern("sha"), Term::string(&row.sha));
516 let nums = [
517 ("cos", row.cos),
518 ("maxdiff", row.maxdiff),
519 ("oxideUs", row.oxide_us),
520 ("ptxUs", row.ptx_us),
521 ];
522 for (p, v) in nums {
523 if let Some(x) = v {
524 g.insert(rnode.to_string(), kern(p), Term::double(x));
525 }
526 }
527 if let Some(b) = row.register_budget {
528 g.insert(rnode.to_string(), kern("registerBudget"), Term::integer(b));
529 }
530 if !row.ptxas_version.is_empty() {
531 g.insert(
532 rnode.to_string(),
533 kern("ptxasVersion"),
534 Term::string(&row.ptxas_version),
535 );
536 }
537 if !row.authoring.is_empty() {
538 g.insert(
539 rnode.to_string(),
540 kern("authoring"),
541 Term::string(&row.authoring),
542 );
543 }
544}
545
546#[must_use]
549pub fn control_graph(with_reference: bool) -> Graph {
550 let mut g = Graph::new();
551 let contract = iri("contract", "rmsnorm-kernel-v1");
552 let mut add = |name: &str, kernel: bool| {
553 let s = iri("symbol", &format!("k::{name}"));
554 g.insert(s.clone(), RDF_TYPE, Term::iri(ont("Symbol")));
555 g.insert(s.clone(), sym("module"), Term::string("k"));
556 g.insert(s.clone(), sym("name"), Term::string(name));
557 g.insert(s.clone(), sym("implements"), Term::iri(&contract));
558 g.insert(s.clone(), sym("equation"), Term::string("rmsnorm"));
559 g.insert(s.clone(), sym("resolved"), Term::boolean(true));
560 if kernel {
561 g.insert(s.clone(), sym("attribute"), Term::string("kernel"));
562 g.insert(s.clone(), sym("unsafeFree"), Term::boolean(true));
563 g.insert(s, sym("boundsChecked"), Term::boolean(true));
564 }
565 };
566 add("gated_rmsnorm", true);
567 if with_reference {
568 add("rmsnorm_f64", false);
569 }
570 g
571}
572
573#[must_use]
578pub fn positive_control() -> bool {
579 let k = iri("symbol", "k::gated_rmsnorm");
580 let mut ghost = control_graph(false);
581 resolve(&mut ghost, &Inputs::default());
582 let mut ok = control_graph(true);
583 resolve(&mut ok, &Inputs::default());
584 let foreign = serde_json::Value::Object(
585 [("schema", "apr-model-ladder-receipt/v1"), ("sha", "ab")]
586 .into_iter()
587 .map(|(k, v)| (k.to_string(), serde_json::Value::from(v)))
588 .collect(),
589 );
590 !ghost.objects(&k, &kern("referenceMissing")).is_empty()
591 && ok.objects(&k, &kern("referenceMissing")).is_empty()
592 && ok.instances_of(&ont("KernelSymbol")).contains(&k.as_str())
593 && parse_receipt("evidence/kernels/x/lambda.json", &foreign).is_err()
594}
595
596#[cfg(test)]
597#[path = "kernel_tests.rs"]
598mod tests;