1use super::native_decide::{Elimination, NativeDecisions};
72use super::receipt::{Receipt, Stage, StageStatus, STAGES};
73use super::reference::{Reference, ReferenceKind};
74use super::{declined, rejected, DiagnosticCode, Result, Verdict};
75use crate::certificate::{
76 read_certificate, AnyCertificate, CaptureRequest, ConversionDiscipline, EnvironmentBase,
77 EnvironmentCertificate, EnvironmentEntry, EnvironmentError, EnvironmentErrorCode,
78 EnvironmentLimits, IndependentVerdict, RebuiltEnvironment, ENVIRONMENT_SCHEMA,
79 OPAQUE_ENVIRONMENT_SCHEMA, PRELUDE_VERSION,
80};
81use crate::{Term, Universe};
82use serde::{Deserialize, Serialize};
83use std::collections::{BTreeMap, BTreeSet};
84
85pub const PACKAGE_SCHEMA: &str = "logos-lean-package/1";
87pub const NATIVE_REDERIVED_SCHEMA: &str = "logos-lean-package/2";
90pub const PACKAGE_SCHEMAS: [&str; 2] = [PACKAGE_SCHEMA, NATIVE_REDERIVED_SCHEMA];
92pub const REPLAY_SCHEMA: &str = "logos-lean-replay/1";
93pub const CORRESPONDENCE_SCHEMA: &str = "logos-lean-correspondence/1";
94
95pub type PackageFiles = BTreeMap<String, Vec<u8>>;
97
98pub const MANIFEST: &str = "manifest.json";
99pub const CERTIFICATE: &str = "certificate.json";
100pub const CORRESPONDENCE: &str = "correspondence.json";
101pub const EXPORT: &str = "source/export.ndjson";
102
103pub const ALLOWED_AXIOMS: [(&str, &str); 3] = [
106 ("propext", "/s:\"propext\""),
107 ("Quot.sound", "/s:\"Quot\"/s:\"sound\""),
108 ("Classical.choice", "/s:\"Classical\"/s:\"choice\""),
109];
110
111pub const TRUSTED_BASE: [&str; 8] = [
115 "The LOGOS kernel crate (logicaffeine-kernel): its Term, Universe and Context representations, reduction and definitional equality, and the primary type checker.",
116 "The independent re-checker (`recheck`) is a separately written de Bruijn type checker in the same crate. It shares the Term, Universe and Context representations and the environment the primary check built; it corroborates typing, it is not an independent theorem prover.",
117 "Exact (Lean) conversion is enforced by the Lean reader's exact typing traversal over the kernel's own reduction.",
118 "The statement binding reads the export with the reference translator, which shares no code with the importer's conversion, sharing or caching machinery; it shares the kernel's Term type and the serde_json parser.",
119 "Structure η, quotient primitives and the kernel's literal Nat are admitted only in their checked canonical forms.",
120 "The LOGOS document is checked by the LOGOS parser and `Repl::execute_document` over the same kernel.",
121 "SHA-256 digests identify bytes; they establish no semantic correspondence. The binding and the kernel do.",
122 "That the pinned Lean accepted the source is recorded by the producing run and is not re-established by replay.",
123];
124
125mod sha256 {
130 const K: [u32; 64] = [
131 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
132 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
133 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
134 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
135 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
136 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
137 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
138 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
139 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
140 0xc67178f2,
141 ];
142 const H0: [u32; 8] = [
143 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
144 0x5be0cd19,
145 ];
146
147 fn compress(h: &mut [u32; 8], block: &[u8]) {
148 let mut w = [0u32; 64];
149 for (i, word) in block.chunks_exact(4).enumerate() {
150 w[i] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
151 }
152 for i in 16..64 {
153 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
154 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
155 w[i] = w[i - 16]
156 .wrapping_add(s0)
157 .wrapping_add(w[i - 7])
158 .wrapping_add(s1);
159 }
160 let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = *h;
161 for i in 0..64 {
162 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
163 let ch = (e & f) ^ (!e & g);
164 let t1 = hh
165 .wrapping_add(s1)
166 .wrapping_add(ch)
167 .wrapping_add(K[i])
168 .wrapping_add(w[i]);
169 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
170 let maj = (a & b) ^ (a & c) ^ (b & c);
171 let t2 = s0.wrapping_add(maj);
172 hh = g;
173 g = f;
174 f = e;
175 e = d.wrapping_add(t1);
176 d = c;
177 c = b;
178 b = a;
179 a = t1.wrapping_add(t2);
180 }
181 for (slot, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) {
182 *slot = slot.wrapping_add(value);
183 }
184 }
185
186 pub fn digest(message: &[u8]) -> [u8; 32] {
187 let mut h = H0;
188 let mut chunks = message.chunks_exact(64);
189 for block in chunks.by_ref() {
190 compress(&mut h, block);
191 }
192 let rest = chunks.remainder();
193 let mut tail = [0u8; 128];
194 tail[..rest.len()].copy_from_slice(rest);
195 tail[rest.len()] = 0x80;
196 let length = if rest.len() < 56 { 64 } else { 128 };
197 tail[length - 8..length]
198 .copy_from_slice(&((message.len() as u64).wrapping_mul(8)).to_be_bytes());
199 for block in tail[..length].chunks_exact(64) {
200 compress(&mut h, block);
201 }
202 let mut out = [0u8; 32];
203 for (i, word) in h.iter().enumerate() {
204 out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
205 }
206 out
207 }
208}
209
210pub fn sha256_hex(bytes: &[u8]) -> String {
212 sha256::digest(bytes)
213 .iter()
214 .map(|b| format!("{b:02x}"))
215 .collect()
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(deny_unknown_fields)]
224pub struct Manifest {
225 pub schema: String,
226 pub result_id: String,
228 pub files: BTreeMap<String, String>,
230 pub source: SourceIdentity,
231 pub target: TargetIdentity,
232 pub document: Option<String>,
234 pub emission: EmissionRecord,
235 pub toolchain: Toolchain,
236 pub assumptions: AssumptionRecord,
237 pub information_losses: Vec<InformationLoss>,
238 pub trusted_base: Vec<String>,
239 pub receipt: BTreeMap<String, StageRecord>,
241}
242
243#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(deny_unknown_fields)]
245pub struct SourceIdentity {
246 pub export: String,
247 pub export_sha256: String,
248 pub format: String,
249 pub lean_version: String,
250 pub lean_githash: Option<String>,
251 pub exporter: String,
252 pub lean_sources: Vec<String>,
254}
255
256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(deny_unknown_fields)]
258pub struct TargetIdentity {
259 pub lean: String,
261 pub kernel: String,
263 pub kind: String,
265 pub level_params: Vec<String>,
266 pub statement: String,
268 pub logos: Option<String>,
270}
271
272#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(deny_unknown_fields)]
274pub struct EmissionRecord {
275 pub status: String,
277 pub detail: String,
278 pub omissions: Vec<String>,
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(deny_unknown_fields)]
284pub struct Toolchain {
285 pub logos: String,
286 pub prelude: String,
287 pub certificate_schema: String,
288 pub lean: String,
289 pub exporter: String,
290}
291
292#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(deny_unknown_fields)]
294pub struct AssumptionRecord {
295 pub allowed: Vec<String>,
297 pub transitive: Vec<String>,
301 pub lean_print_axioms: Option<Vec<String>>,
306}
307
308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
309#[serde(deny_unknown_fields)]
310pub struct InformationLoss {
311 pub category: String,
312 pub detail: String,
313}
314
315#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(deny_unknown_fields)]
317pub struct StageRecord {
318 pub status: String,
319 pub code: Option<String>,
320 pub detail: String,
321}
322
323#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(deny_unknown_fields)]
325pub struct CorrespondenceFile {
326 pub schema: String,
327 pub target: String,
329 pub rows: Vec<CorrespondenceRow>,
330}
331
332#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(deny_unknown_fields)]
334pub struct CorrespondenceRow {
335 pub lean: String,
337 pub kernel: String,
339 pub logos: Option<String>,
341 pub kind: String,
342 pub origin: String,
344 pub helper_of: Option<String>,
346 pub form: Option<String>,
348 pub levels: Vec<(String, String)>,
350 pub lines: Option<(usize, usize)>,
352}
353
354pub fn canonical_json<T: Serialize>(value: &T) -> Vec<u8> {
356 let mut bytes = serde_json::to_vec_pretty(value).expect("package JSON serializes");
357 bytes.push(b'\n');
358 bytes
359}
360
361pub fn canonical_certificate(certificate: &EnvironmentCertificate) -> Vec<u8> {
363 let mut bytes = serde_json::to_vec(certificate).expect("a certificate serializes");
364 bytes.push(b'\n');
365 bytes
366}
367
368pub fn result_id(target_lean: &str, files: &BTreeMap<String, String>) -> String {
371 result_id_in(PACKAGE_SCHEMA, target_lean, files)
372}
373
374pub fn result_id_in(schema: &str, target_lean: &str, files: &BTreeMap<String, String>) -> String {
377 let mut preimage = format!("{schema}\0{target_lean}\0");
378 for (path, digest) in files {
379 preimage.push_str(path);
380 preimage.push('\0');
381 preimage.push_str(digest);
382 preimage.push('\n');
383 }
384 format!("sha256:{}", sha256_hex(preimage.as_bytes()))
385}
386
387pub fn document_path(target_lean: &str) -> String {
390 let stem: String = target_lean
391 .chars()
392 .map(|c| {
393 if c.is_alphanumeric() || matches!(c, '_' | '-' | '.') {
394 c
395 } else {
396 '_'
397 }
398 })
399 .collect();
400 let stem = stem.trim_start_matches('.');
401 format!("{}.lg", if stem.is_empty() { "target" } else { stem })
402}
403
404#[derive(Clone, Copy, Debug, PartialEq, Eq)]
410pub struct PackageLimits {
411 pub max_files: usize,
412 pub max_file_bytes: usize,
413 pub max_total_bytes: usize,
414 pub max_path_bytes: usize,
415 pub environment: EnvironmentLimits,
416}
417
418impl Default for PackageLimits {
419 fn default() -> Self {
420 PackageLimits {
421 max_files: 64,
422 max_file_bytes: 512 * 1024 * 1024,
423 max_total_bytes: 1024 * 1024 * 1024,
424 max_path_bytes: 255,
425 environment: EnvironmentLimits::default(),
426 }
427 }
428}
429
430fn unsafe_path(path: &str, limits: &PackageLimits) -> Option<&'static str> {
432 if path.is_empty() {
433 return Some("an empty path");
434 }
435 if path.len() > limits.max_path_bytes {
436 return Some("a path longer than the limit");
437 }
438 if path.starts_with('/') || path.contains('\\') || path.contains('\0') || path.contains(':') {
439 return Some("an absolute or non-portable path");
440 }
441 if path
442 .split('/')
443 .any(|c| c.is_empty() || c == "." || c == ".." || c.starts_with('.'))
444 {
445 return Some("a path with an empty, dot or hidden component");
446 }
447 if path.chars().any(char::is_control) {
448 return Some("a path with a control character");
449 }
450 None
451}
452
453#[derive(Clone, Debug, Default)]
459pub struct BuildOptions {
460 pub lean_sources: BTreeMap<String, Vec<u8>>,
462 pub lean_run: Option<LeanRun>,
464}
465
466#[derive(Clone, Debug)]
468pub struct LeanRun {
469 pub toolchain: String,
470 pub exporter: String,
471 pub accepted: bool,
473 pub detail: String,
474}
475
476pub struct Build {
479 pub files: PackageFiles,
480 pub receipt: Receipt,
481 pub audit: Stage,
485}
486
487pub fn assemble(export: &str, target: &str) -> Result<PackageFiles> {
490 assemble_with(export, target, &BuildOptions::default())
491}
492
493pub fn assemble_with(export: &str, target: &str, options: &BuildOptions) -> Result<PackageFiles> {
495 assemble_observed(export, target, options, None)
496}
497
498pub fn assemble_observed(
503 export: &str,
504 target: &str,
505 options: &BuildOptions,
506 lean_print_axioms: Option<&[String]>,
507) -> Result<PackageFiles> {
508 publishable(build_observed(export, target, options, lean_print_axioms)?)
509}
510
511pub fn publishable(build: Build) -> Result<PackageFiles> {
514 for stage in [
516 "primary_check",
517 "independent_recheck",
518 "translated",
519 "assumption_policy",
520 "emission_reparsed",
521 ] {
522 let record = build
523 .receipt
524 .stage(stage)
525 .expect("every stage is on the receipt");
526 if record.status != StageStatus::Passed {
527 let code = match (stage, record.status) {
528 _ if record.code.as_deref() == Some("resource_limit") => {
529 DiagnosticCode::ResourceLimit
530 }
531 ("assumption_policy", StageStatus::Failed)
532 if record.code.as_deref() == Some("assumption_disagreement") =>
533 {
534 DiagnosticCode::AssumptionDisagreement
535 }
536 ("assumption_policy", StageStatus::Failed) => DiagnosticCode::DisallowedAssumption,
537 (_, StageStatus::Unsupported | StageStatus::NotRun) => {
538 DiagnosticCode::UnsupportedFeature
539 }
540 (_, _) => DiagnosticCode::InvalidProof,
541 };
542 let message = format!("{stage} {}: {}", record.status.as_str(), record.detail);
543 return Err(
544 if matches!(
545 code,
546 DiagnosticCode::UnsupportedFeature | DiagnosticCode::ResourceLimit
547 ) {
548 declined(code, message)
549 } else {
550 rejected(code, message)
551 },
552 );
553 }
554 }
555 let check = replay(&build.files);
556 if !check.replayed() {
557 let failure = check
558 .first_failure()
559 .map(|s| format!("{}: {}", s.name, s.detail))
560 .unwrap_or_default();
561 return Err(rejected(
562 DiagnosticCode::InvalidProof,
563 format!("the assembled package does not replay: {failure}"),
564 ));
565 }
566 Ok(build.files)
567}
568
569fn capture_with_proofs(
573 imported: &super::ImportedContext,
574 kernel_target: &str,
575) -> Result<(EnvironmentCertificate, BTreeMap<String, Term>)> {
576 let proofs: BTreeMap<String, Term> = imported
577 .retained_declarations()
578 .map(|(name, _, body)| (name.to_string(), body.clone()))
579 .collect();
580 let request = CaptureRequest {
581 context: imported,
582 base: EnvironmentBase::Kernel,
583 conversion: ConversionDiscipline::Exact,
584 target: kernel_target,
585 opaque_proofs: &proofs,
586 };
587 let certificate = EnvironmentCertificate::capture(&request).map_err(environment_verdict)?;
588 let carried: BTreeSet<&str> = certificate
590 .entries
591 .iter()
592 .filter_map(|e| match e {
593 EnvironmentEntry::Theorem { name, .. } | EnvironmentEntry::Opaque { name, .. } => {
594 Some(name.as_str())
595 }
596 _ => None,
597 })
598 .collect();
599 let proofs = proofs
600 .into_iter()
601 .filter(|(name, _)| carried.contains(name.as_str()))
602 .collect();
603 Ok((certificate, proofs))
604}
605
606fn environment_verdict(error: EnvironmentError) -> Verdict {
607 let code = match error.code {
608 EnvironmentErrorCode::ResourceLimit => DiagnosticCode::ResourceLimit,
609 EnvironmentErrorCode::Unsupported | EnvironmentErrorCode::MissingEvidence => {
610 DiagnosticCode::UnsupportedFeature
611 }
612 _ => DiagnosticCode::InvalidProof,
613 };
614 let message = error.to_string();
615 if matches!(code, DiagnosticCode::InvalidProof) {
616 rejected(code, message)
617 } else {
618 declined(code, message)
619 }
620}
621
622pub fn build(export: &str, target: &str, options: &BuildOptions) -> Result<Build> {
626 build_observed(export, target, options, None)
627}
628
629pub fn build_observed(
631 export: &str,
632 target: &str,
633 options: &BuildOptions,
634 lean_print_axioms: Option<&[String]>,
635) -> Result<Build> {
636 on_kernel_stack(|| build_observed_on_stack(export, target, options, lean_print_axioms))?
638}
639
640fn build_observed_on_stack(
641 export: &str,
642 target: &str,
643 options: &BuildOptions,
644 lean_print_axioms: Option<&[String]>,
645) -> Result<Build> {
646 let header: serde_json::Value = serde_json::from_str(export.lines().next().unwrap_or(""))
647 .map_err(|e| {
648 declined(
649 DiagnosticCode::MalformedExport,
650 format!("export header: {e}"),
651 )
652 })?;
653 let reference = Reference::read(export)?;
654 let source_limits = EnvironmentLimits::default();
655 reference.preflight(
656 &[(target.to_string(), false)],
657 source_limits.max_depth,
658 source_limits.max_tree_nodes,
659 )?;
660 let kernel_target = reference.source_name(target)?;
661 let mut source_work = SourceWork::new(&source_limits)?;
662 let (imported, report) = super::import_with(
663 export,
664 super::ImportRequest {
665 audit: true,
666 retain: super::RetainSource::EveryProof,
667 lean_binder_names: true,
668 },
669 )?;
670 let declaration = source_work.declaration(&*imported, &reference, &kernel_target)?;
671 let kind = match declaration.kind {
672 ReferenceKind::Theorem => "thm",
673 ReferenceKind::Definition => "def",
674 ReferenceKind::Opaque => "opaque",
675 other => {
676 return Err(rejected(
677 DiagnosticCode::InvalidProof,
678 format!(
679 "{} is a Lean {}, and a package proves a theorem or definition",
680 declaration.lean_name,
681 other.as_str()
682 ),
683 ))
684 }
685 };
686 for (name, bytes) in &options.lean_sources {
687 if unsafe_path(&format!("source/{name}"), &PackageLimits::default()).is_some()
689 || !name.ends_with(".lean")
690 {
691 return Err(declined(
692 DiagnosticCode::Infrastructure,
693 format!("{name:?} is not a project-relative Lean source path"),
694 ));
695 }
696 if std::str::from_utf8(bytes).is_err() {
697 return Err(declined(
698 DiagnosticCode::MalformedExport,
699 format!("{name} is not UTF-8"),
700 ));
701 }
702 }
703 let (certificate, proofs) = capture_with_proofs(&imported, &declaration.name)?;
704 let mut receipt = Receipt::unstarted();
705 match &options.lean_run {
706 Some(run) => receipt.set(
707 "lean_accepted",
708 if run.accepted {
709 StageStatus::Passed
710 } else {
711 StageStatus::Failed
712 },
713 format!("{} with {}: {}", run.toolchain, run.exporter, run.detail),
714 ),
715 None => receipt.set(
716 "lean_accepted",
717 StageStatus::NotRun,
718 "this run read a pre-exported artifact and did not run Lean",
719 ),
720 }
721 source_binding_metrics(
722 &reference,
723 &certificate,
724 &declaration.name,
725 lean_print_axioms.unwrap_or(&[]),
726 &source_limits,
727 )?;
728 let native = NativeDecisions::read(export).closure_of(&declaration.name);
729 let schema = contract_schema(&native);
730 let mut checks = check_environment(
731 &certificate,
732 &reference,
733 &declaration.name,
734 &source_limits,
735 &mut source_work,
736 );
737 checks.rebuilt = None;
738 let audit = stage("assumption_policy", &checks.policy);
739 if checks.primary.0 == StageStatus::Passed {
740 if let Err(detail) =
741 agree_with_lean(&reference, &native, lean_print_axioms, &checks.transitive)
742 {
743 checks.policy = failed("assumption_disagreement", detail);
744 }
745 }
746 checks.record(&mut receipt);
747
748 let proof_of = |key: &str| proofs.get(key).cloned();
751 let document = match super::emit::emit_context(&*imported, &proof_of, &declaration.name) {
752 Ok(document) => match super::emit::check_statement(&document, &declaration) {
753 Ok(()) => {
754 receipt.set("emission_reparsed", StageStatus::Passed, "the document reparsed in a fresh LOGOS session and states the reference translation");
755 Some(document)
756 }
757 Err(fault) => {
758 receipt.set_with_code(
759 "emission_reparsed",
760 StageStatus::Failed,
761 "emission_mismatch",
762 fault.to_string(),
763 );
764 None
765 }
766 },
767 Err(verdict) => {
768 let unsupported = matches!(verdict, Verdict::Decline(_));
769 receipt.set_with_code(
770 "emission_reparsed",
771 if unsupported {
772 StageStatus::Unsupported
773 } else {
774 StageStatus::Failed
775 },
776 verdict.reason().code().as_str(),
777 verdict.reason().message().to_string(),
778 );
779 None
780 }
781 };
782 receipt.set(
783 "fresh_replay",
784 StageStatus::NotRun,
785 "established only by a replay of the package in a fresh process",
786 );
787
788 let mut files = PackageFiles::new();
789 files.insert(CERTIFICATE.into(), canonical_certificate(&certificate));
790 files.insert(EXPORT.into(), export.as_bytes().to_vec());
791 let rows = correspondence_rows(&certificate, &reference, document.as_ref())?;
792 let correspondence = CorrespondenceFile {
793 schema: CORRESPONDENCE_SCHEMA.into(),
794 target: declaration.name.clone(),
795 rows,
796 };
797 files.insert(CORRESPONDENCE.into(), canonical_json(&correspondence));
798 let document_file = document
799 .as_ref()
800 .map(|_| document_path(&declaration.lean_name));
801 if let (Some(path), Some(document)) = (&document_file, &document) {
802 files.insert(path.clone(), document.source.as_bytes().to_vec());
803 }
804 let mut lean_sources = Vec::new();
805 for (name, bytes) in &options.lean_sources {
806 let path = format!("source/{name}");
807 files.insert(path.clone(), bytes.clone());
808 lean_sources.push(path);
809 }
810 let digests: BTreeMap<String, String> = files
811 .iter()
812 .map(|(p, b)| (p.clone(), sha256_hex(b)))
813 .collect();
814 let emission = match (&document, receipt.stage("emission_reparsed")) {
815 (Some(doc), _) => EmissionRecord {
816 status: "emitted".into(),
817 detail: "written by the emitter and reparsed".into(),
818 omissions: doc.omissions.clone(),
819 },
820 (None, Some(stage)) => EmissionRecord {
821 status: if stage.status == StageStatus::Unsupported {
822 "unsupported".into()
823 } else {
824 "failed".into()
825 },
826 detail: stage.detail.clone(),
827 omissions: Vec::new(),
828 },
829 (None, None) => unreachable!("every stage is on the receipt"),
830 };
831 let mut losses: Vec<InformationLoss> = report
832 .metadata_losses
833 .iter()
834 .map(|loss| InformationLoss {
835 category: "SOURCE_METADATA".into(),
836 detail: format!(
837 "expression {}: {} = {}",
838 loss.expression, loss.key, loss.value
839 ),
840 })
841 .collect();
842 losses.extend(
843 native
844 .information_changes()
845 .into_iter()
846 .map(|(category, detail)| InformationLoss { category, detail }),
847 );
848 losses.push(InformationLoss {
849 category: "SOURCE_POSITIONS".into(),
850 detail: "a Lean export records no source positions, so the correspondence maps names, not spans, on the Lean side".into(),
851 });
852 let meta = &header["meta"];
853 let manifest = Manifest {
854 schema: schema.into(),
855 result_id: result_id_in(schema, &declaration.lean_name, &digests),
856 source: SourceIdentity {
857 export: EXPORT.into(),
858 export_sha256: digests[EXPORT].clone(),
859 format: meta["format"]["version"]
860 .as_str()
861 .unwrap_or_default()
862 .into(),
863 lean_version: meta["lean"]["version"].as_str().unwrap_or_default().into(),
864 lean_githash: meta["lean"]["githash"].as_str().map(str::to_string),
865 exporter: format!(
866 "{} {}",
867 meta["exporter"]["name"].as_str().unwrap_or("unknown"),
868 meta["exporter"]["version"].as_str().unwrap_or("unknown")
869 ),
870 lean_sources,
871 },
872 files: digests,
873 target: TargetIdentity {
874 lean: declaration.lean_name.clone(),
875 kernel: declaration.name.clone(),
876 kind: kind.into(),
877 level_params: declaration.level_params.clone(),
878 statement: declaration.ty.to_string(),
879 logos: document.as_ref().map(|d| d.target.clone()),
880 },
881 document: document_file,
882 emission,
883 toolchain: Toolchain {
884 logos: env!("CARGO_PKG_VERSION").into(),
885 prelude: PRELUDE_VERSION.into(),
886 certificate_schema: certificate.schema.clone(),
887 lean: options
888 .lean_run
889 .as_ref()
890 .map(|r| r.toolchain.clone())
891 .unwrap_or_else(|| {
892 format!(
893 "leanprover/lean4:v{}",
894 meta["lean"]["version"].as_str().unwrap_or("unknown")
895 )
896 }),
897 exporter: options
898 .lean_run
899 .as_ref()
900 .map(|r| r.exporter.clone())
901 .unwrap_or_else(|| {
902 meta["exporter"]["name"]
903 .as_str()
904 .unwrap_or("unknown")
905 .to_string()
906 }),
907 },
908 assumptions: AssumptionRecord {
909 allowed: ALLOWED_AXIOMS
910 .iter()
911 .map(|(lean, _)| lean.to_string())
912 .collect(),
913 transitive: checks.transitive.clone(),
914 lean_print_axioms: lean_print_axioms.map(|a| a.to_vec()),
915 },
916 information_losses: losses,
917 trusted_base: TRUSTED_BASE.iter().map(|s| s.to_string()).collect(),
918 receipt: receipt_records(&receipt),
919 };
920 files.insert(MANIFEST.into(), canonical_json(&manifest));
921 Ok(Build {
922 files,
923 receipt,
924 audit,
925 })
926}
927
928fn contract_schema(native: &Elimination) -> &'static str {
930 if native.eliminates() {
931 NATIVE_REDERIVED_SCHEMA
932 } else {
933 PACKAGE_SCHEMA
934 }
935}
936
937fn states_contract(
940 manifest: &Manifest,
941 native: &Elimination,
942) -> std::result::Result<(), (&'static str, String)> {
943 let schema = contract_schema(native);
944 if manifest.schema != schema {
945 return Err((
946 "native_rederivation_schema",
947 if native.eliminates() {
948 format!(
949 "the export's proof of the target re-derives native decisions, so its package is {schema}, but the manifest declares {}",
950 manifest.schema
951 )
952 } else {
953 format!(
954 "the export's proof of the target re-derives no native decision, so its package is {schema}, but the manifest declares {}",
955 manifest.schema
956 )
957 },
958 ));
959 }
960 native
961 .disclosed_by(
962 manifest
963 .information_losses
964 .iter()
965 .map(|loss| (loss.category.as_str(), loss.detail.as_str())),
966 )
967 .map_err(|detail| ("native_rederivation_disclosure", detail))
968}
969
970fn agree_with_lean(
980 reference: &Reference,
981 native: &Elimination,
982 lean: Option<&[String]>,
983 transitive: &[String],
984) -> std::result::Result<(), String> {
985 let Some(lean) = lean else {
986 if !native.eliminates() {
987 return Ok(());
988 }
989 let replaced: Vec<&str> = native
990 .rederivations
991 .iter()
992 .map(|r| r.lean.as_str())
993 .collect();
994 return Err(format!(
995 "the proof re-derives the native decisions of {}, and a native-rederived package states what Lean's own proof \
996 rested on, but this run recorded no #print axioms from Lean",
997 replaced.join(", ")
998 ));
999 };
1000 let mut kernel_of: BTreeMap<&str, String> = BTreeMap::new();
1001 let mut reported = BTreeSet::new();
1002 for axiom in lean {
1003 let kernel = reference.source_lean_name(axiom).map_err(|_| {
1004 format!("Lean's #print axioms names {axiom}, which the export does not declare")
1005 })?;
1006 if !reported.insert(kernel.clone()) {
1007 return Err(format!("Lean's #print axioms names {axiom} twice"));
1008 }
1009 kernel_of.insert(axiom, kernel);
1010 }
1011 let expected_lean = native.lean_after_rederivation(lean)?;
1012 let expected: BTreeSet<String> = expected_lean
1013 .iter()
1014 .map(|axiom| kernel_of[axiom.as_str()].clone())
1015 .collect();
1016 let computed: BTreeSet<String> = transitive.iter().cloned().collect();
1017 if expected == computed {
1018 return Ok(());
1019 }
1020 let lean_names = |names: &mut dyn Iterator<Item = &String>| {
1021 names
1022 .map(|n| super::intake::lean_name(n))
1023 .collect::<Vec<_>>()
1024 };
1025 let rests_on = lean_names(&mut computed.iter());
1026 let only_lean = lean_names(&mut expected.difference(&computed));
1027 let only_here = lean_names(&mut computed.difference(&expected));
1028 let rederived = if native.eliminates() {
1029 format!(", so with its native decisions re-derived the statement and proof must rest on {expected_lean:?}")
1030 } else {
1031 String::new()
1032 };
1033 Err(format!(
1034 "Lean's #print axioms reports {lean:?}{rederived}, but the statement and proof rest on {rests_on:?} (only in \
1035 Lean: {only_lean:?}; only here: {only_here:?})"
1036 ))
1037}
1038
1039fn stage(name: &'static str, (status, code, detail): &StageOutcome) -> Stage {
1041 Stage {
1042 name,
1043 status: *status,
1044 detail: detail.clone(),
1045 code: code.clone(),
1046 }
1047}
1048
1049fn receipt_records(receipt: &Receipt) -> BTreeMap<String, StageRecord> {
1050 receipt
1051 .stages
1052 .iter()
1053 .map(|s| {
1054 (
1055 s.name.to_string(),
1056 StageRecord {
1057 status: s.status.as_str().into(),
1058 code: s.code.clone(),
1059 detail: s.detail.clone(),
1060 },
1061 )
1062 })
1063 .collect()
1064}
1065
1066fn correspondence_rows(
1069 certificate: &EnvironmentCertificate,
1070 reference: &Reference,
1071 document: Option<&super::emit::EmittedDocument>,
1072) -> Result<Vec<CorrespondenceRow>> {
1073 use super::emit::{Form, Origin};
1074 if let Some(document) = document {
1075 return Ok(document
1076 .correspondence
1077 .iter()
1078 .map(|c| {
1079 let (origin, helper_of) = match &c.origin {
1080 Origin::Authored => ("authored", None),
1081 Origin::Helper { of } => ("helper", Some(of.clone())),
1082 Origin::Synthesized => ("synthesized", None),
1083 Origin::Prelude => ("prelude", None),
1084 };
1085 CorrespondenceRow {
1086 lean: c.lean.clone(),
1087 kernel: c.importer_key.clone(),
1088 logos: Some(c.logos.clone()),
1089 kind: c.kind.as_str().into(),
1090 origin: origin.into(),
1091 helper_of,
1092 form: Some(
1093 match c.form {
1094 Form::Literate => "literate",
1095 Form::Vernacular => "vernacular",
1096 Form::Prelude => "prelude",
1097 }
1098 .into(),
1099 ),
1100 levels: c.levels.clone(),
1101 lines: c.lines,
1102 }
1103 })
1104 .collect());
1105 }
1106 let mut rows = Vec::new();
1107 for entry in &certificate.entries {
1108 for name in entry.names() {
1109 let (lean, origin) = match reference.source_spelling(name) {
1110 Ok((_, lean)) => (lean, "authored"),
1111 Err(_) => (name.to_string(), "synthesized"),
1112 };
1113 rows.push(CorrespondenceRow {
1114 lean,
1115 kernel: name.to_string(),
1116 logos: None,
1117 kind: entry_kind(entry, name).into(),
1118 origin: origin.into(),
1119 helper_of: None,
1120 form: None,
1121 levels: Vec::new(),
1122 lines: None,
1123 });
1124 }
1125 }
1126 Ok(rows)
1127}
1128
1129fn entry_kind(entry: &EnvironmentEntry, name: &str) -> &'static str {
1130 match entry {
1131 EnvironmentEntry::Axiom { .. } => "axiom",
1132 EnvironmentEntry::Definition { .. } => "definition",
1133 EnvironmentEntry::Theorem { .. } => "theorem",
1134 EnvironmentEntry::Opaque { .. } => "opaque",
1135 EnvironmentEntry::Inductive { types, .. } => {
1136 if types.iter().any(|t| t.name == name) {
1137 "inductive"
1138 } else {
1139 "constructor"
1140 }
1141 }
1142 EnvironmentEntry::Structure { .. } => "structure",
1143 EnvironmentEntry::Quotient { .. } => "quotient",
1144 }
1145}
1146
1147struct EnvironmentChecks {
1153 primary: (StageStatus, Option<String>, String),
1156 independent: (StageStatus, Option<String>, String),
1157 translated: (StageStatus, Option<String>, String),
1158 policy: (StageStatus, Option<String>, String),
1159 transitive: Vec<String>,
1161 rebuilt: Option<RebuiltSummary>,
1163}
1164
1165struct RebuiltSummary {
1166 context: crate::Context,
1167 proofs: BTreeMap<String, Term>,
1168}
1169
1170impl EnvironmentChecks {
1171 fn record(&self, receipt: &mut Receipt) {
1172 for (stage, (status, code, detail)) in [
1173 ("primary_check", &self.primary),
1174 ("independent_recheck", &self.independent),
1175 ("translated", &self.translated),
1176 ("assumption_policy", &self.policy),
1177 ] {
1178 match code {
1179 Some(code) => receipt.set_with_code(stage, *status, code, detail.clone()),
1180 None => receipt.set(stage, *status, detail.clone()),
1181 }
1182 }
1183 }
1184}
1185
1186fn failed(code: &str, detail: impl Into<String>) -> (StageStatus, Option<String>, String) {
1187 (StageStatus::Failed, Some(code.to_string()), detail.into())
1188}
1189
1190fn not_run(why: &str) -> (StageStatus, Option<String>, String) {
1191 (StageStatus::NotRun, None, why.to_string())
1192}
1193
1194fn on_kernel_stack<R: Send>(work: impl FnOnce() -> R + Send) -> Result<R> {
1196 let rungs = EnvironmentLimits::default()
1197 .max_depth
1198 .saturating_mul(3)
1199 .saturating_add(crate::term_store::INLINE_DEPTH);
1200 std::thread::scope(|scope| {
1201 std::thread::Builder::new()
1202 .name("lean-package-build".into())
1203 .stack_size(crate::term_store::stack_bytes_for_depth(rungs))
1204 .spawn_scoped(scope, work)
1205 .map(|handle| {
1206 handle
1207 .join()
1208 .unwrap_or_else(|p| std::panic::resume_unwind(p))
1209 })
1210 .map_err(|e| {
1211 declined(
1212 DiagnosticCode::Infrastructure,
1213 format!("cannot reserve the package stack: {e}"),
1214 )
1215 })
1216 })
1217}
1218
1219fn source_binding_metrics(
1222 reference: &Reference,
1223 certificate: &EnvironmentCertificate,
1224 target: &str,
1225 other_source_reads: &[String],
1226 limits: &EnvironmentLimits,
1227) -> Result<super::reference::SourceMetrics> {
1228 let source_names: BTreeMap<_, _> = reference.names()?.into_iter().collect();
1229 let mut declarations = BTreeMap::<String, bool>::new();
1230 for entry in &certificate.entries {
1231 for name in entry.names() {
1232 if let Some(kind) = source_names.get(name) {
1233 let body = matches!(
1234 (entry, kind),
1235 (
1236 EnvironmentEntry::Definition { .. } | EnvironmentEntry::Opaque { .. },
1237 ReferenceKind::Definition | ReferenceKind::Opaque
1238 )
1239 );
1240 declarations
1241 .entry(name.to_string())
1242 .and_modify(|existing| *existing |= body)
1243 .or_insert(body);
1244 }
1245 }
1246 }
1247 if source_names.contains_key(target) {
1249 declarations.entry(target.to_string()).or_insert(false);
1250 }
1251 for name in other_source_reads {
1254 if let Ok(name) = reference.source_lean_name(name) {
1255 declarations.entry(name).or_insert(false);
1256 }
1257 }
1258 reference.preflight_kernel(
1259 &declarations.into_iter().collect::<Vec<_>>(),
1260 limits.max_depth,
1261 limits.max_tree_nodes,
1262 )
1263}
1264
1265fn check_environment(
1267 certificate: &EnvironmentCertificate,
1268 reference: &Reference,
1269 kernel_target: &str,
1270 limits: &EnvironmentLimits,
1271 source_work: &mut SourceWork,
1272) -> EnvironmentChecks {
1273 let blocked = "the environment did not rebuild";
1274 let mut canonical = BTreeMap::new();
1275 let mut body_binding = reference
1276 .names()
1277 .map(|names| names.into_iter().collect::<BTreeMap<_, _>>())
1278 .map_err(|error| {
1279 binding_error(
1280 error,
1281 "missing_source_evidence",
1282 "source declaration inventory",
1283 )
1284 });
1285 let mut first_binding_failure = body_binding.as_ref().err().cloned();
1286 let rebuilt =
1287 match certificate.rebuild_observing_values(limits, &mut |prefix, name, levels, ty, body| {
1288 if first_binding_failure.is_none() {
1289 if let Ok(source_names) = &mut body_binding {
1290 first_binding_failure = bind_source_body(
1291 prefix,
1292 reference,
1293 source_names,
1294 &mut canonical,
1295 name,
1296 levels,
1297 ty,
1298 body,
1299 source_work,
1300 )
1301 .err();
1302 }
1303 }
1304 }) {
1305 Ok(rebuilt) => rebuilt,
1306 Err(error) => {
1307 let code = error.code.as_str();
1308 let (primary, independent) =
1309 if error.code == EnvironmentErrorCode::IndependentDisagreement {
1310 (
1311 failed(
1312 "rebuild_incomplete",
1313 "the environment did not finish rebuilding: the two kernels disagreed",
1314 ),
1315 failed(code, error.message.clone()),
1316 )
1317 } else {
1318 (failed(code, error.message.clone()), not_run(blocked))
1319 };
1320 return EnvironmentChecks {
1321 primary,
1322 independent,
1323 translated: first_binding_failure
1324 .map(binding_failure)
1325 .unwrap_or_else(|| not_run(blocked)),
1326 policy: not_run(blocked),
1327 transitive: Vec::new(),
1328 rebuilt: None,
1329 };
1330 }
1331 };
1332 let primary = (
1333 StageStatus::Passed,
1334 None,
1335 format!(
1336 "{} declarations rebuilt from the bare kernel, each kernel-validated under exact conversion; no obligation owed",
1337 certificate.entries.len()
1338 ),
1339 );
1340 let unsupported: Vec<String> = rebuilt
1341 .independent
1342 .iter()
1343 .filter_map(|(name, verdict)| match verdict {
1344 IndependentVerdict::Unsupported(why) => Some(format!("{name} ({why})")),
1345 IndependentVerdict::Agreed => None,
1346 })
1347 .collect();
1348 let independent = if unsupported.is_empty() {
1349 (
1350 StageStatus::Passed,
1351 None,
1352 format!(
1353 "{} checked terms re-inferred by the independent kernel to their declared types",
1354 rebuilt.independent.len()
1355 ),
1356 )
1357 } else {
1358 (
1359 StageStatus::Unsupported,
1360 Some("outside_recheck_fragment".to_string()),
1361 format!(
1362 "the independent kernel does not cover {} term(s): {}",
1363 unsupported.len(),
1364 unsupported.join("; ")
1365 ),
1366 )
1367 };
1368 let translated = match first_binding_failure {
1369 Some(error) => binding_failure(error),
1370 None => match bind(certificate, &rebuilt, reference, kernel_target, source_work) {
1371 Ok(detail) => (StageStatus::Passed, None, detail),
1372 Err(error) => binding_failure(error),
1373 },
1374 };
1375 let transitive: Vec<String> = rebuilt
1376 .axioms
1377 .axioms
1378 .iter()
1379 .chain(rebuilt.axioms.unresolved.iter())
1380 .cloned()
1381 .collect();
1382 let policy = match audit_assumptions(&rebuilt) {
1383 Ok(detail) => (StageStatus::Passed, None, detail),
1384 Err((code, detail)) => failed(code, detail),
1385 };
1386 let proofs = rebuilt.proofs.clone();
1387 EnvironmentChecks {
1388 primary,
1389 independent,
1390 translated,
1391 policy,
1392 transitive,
1393 rebuilt: Some(RebuiltSummary {
1394 context: rebuilt.context,
1395 proofs,
1396 }),
1397 }
1398}
1399
1400type BindingError = (&'static str, String);
1401
1402fn binding_error(error: Verdict, mismatch: &'static str, what: &str) -> BindingError {
1404 let code = match error.reason().code() {
1405 DiagnosticCode::ResourceLimit
1406 | DiagnosticCode::UnsupportedFeature
1407 | DiagnosticCode::RecheckRefused
1408 | DiagnosticCode::AuditIncomplete
1409 | DiagnosticCode::Infrastructure => error.reason().code().as_str(),
1410 _ => mismatch,
1411 };
1412 (code, format!("{what}: {}", error.reason().message()))
1413}
1414
1415fn binding_failure((code, detail): BindingError) -> StageOutcome {
1416 let status = if matches!(
1417 code,
1418 "unsupported_feature" | "recheck_refused" | "audit_incomplete"
1419 ) {
1420 StageStatus::Unsupported
1421 } else {
1422 StageStatus::Failed
1423 };
1424 (status, Some(code.to_string()), detail)
1425}
1426
1427fn source_levels(
1428 name: &str,
1429 levels: &[String],
1430 terms: &[&Term],
1431 work: &mut SourceWork,
1432) -> std::result::Result<(), BindingError> {
1433 if !work
1434 .levels(levels, terms)
1435 .map_err(|error| binding_error(error, "statement_mismatch", name))?
1436 {
1437 return Err((
1438 "statement_mismatch",
1439 format!("{name} has duplicate or undeclared source universe parameters"),
1440 ));
1441 }
1442 Ok(())
1443}
1444
1445fn source_type(
1447 prefix: &crate::Context,
1448 name: &str,
1449 levels: &[String],
1450 ty: &Term,
1451 work: &mut SourceWork,
1452) -> std::result::Result<(), BindingError> {
1453 source_levels(name, levels, &[ty], work)?;
1454 let main = super::lean_type(prefix, ty)
1455 .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1456 let sort = super::normal(prefix, &main)
1457 .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1458 if !matches!(sort, Term::Sort(_)) {
1459 return Err((
1460 "statement_mismatch",
1461 format!("{name}'s source type does not inhabit a sort"),
1462 ));
1463 }
1464 let independent = crate::recheck(prefix, ty).map_err(|error| {
1465 binding_error(
1466 super::independent_verdict(&error, name),
1467 "statement_mismatch",
1468 name,
1469 )
1470 })?;
1471 super::independent_comparison(super::exact(prefix, &independent, &main), name)
1472 .map_err(|error| binding_error(error, "statement_mismatch", name))
1473}
1474
1475fn bind_source_body(
1478 prefix: &crate::Context,
1479 reference: &Reference,
1480 source_names: &BTreeMap<String, ReferenceKind>,
1481 canonical: &mut BTreeMap<String, super::source_recursor::CanonicalRecursor>,
1482 name: &str,
1483 levels: &[String],
1484 ty: &Term,
1485 body: &Term,
1486 work: &mut SourceWork,
1487) -> std::result::Result<(), BindingError> {
1488 work.lookup_name(name, source_names.len())
1489 .map_err(|error| binding_error(error, "missing_source_evidence", name))?;
1490 let Some(kind) = source_names.get(name) else {
1491 return if reader_introduced(name) {
1494 Ok(())
1495 } else {
1496 Err((
1497 "unsourced_declaration",
1498 format!("the certificate value {name} has no source declaration"),
1499 ))
1500 };
1501 };
1502 if *kind == ReferenceKind::Recursor {
1503 work.lookup_name(name, canonical.len())
1504 .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1505 if !canonical.contains_key(name) {
1506 let block = work
1507 .block(prefix, reference, name)
1508 .map_err(|error| binding_error(error, "missing_source_evidence", name))?;
1509 let derived = super::source_recursor::derive_bounded(prefix, &block, &mut work.budget)
1510 .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1511 for recursor in derived {
1513 let key = work
1514 .cache_name(&recursor.name)
1515 .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1516 work.lookup_name(&key, canonical.len())
1517 .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1518 if canonical.insert(key, recursor).is_some() {
1519 return Err((
1520 "definition_mismatch",
1521 "canonical source recursor blocks overlap".into(),
1522 ));
1523 }
1524 }
1525 }
1526 work.lookup_name(name, canonical.len())
1527 .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1528 let expected = canonical.get(name).ok_or_else(|| {
1529 (
1530 "missing_source_evidence",
1531 format!("the independently derived block has no {name}"),
1532 )
1533 })?;
1534 if expected.levels != levels {
1535 return Err((
1536 "statement_mismatch",
1537 format!("{name} changes the canonical source universe telescope"),
1538 ));
1539 }
1540 super::exact(prefix, &expected.ty, ty)
1541 .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1542 return super::exact(prefix, &expected.body, body)
1543 .map_err(|error| binding_error(error, "definition_mismatch", name));
1544 }
1545 if !matches!(kind, ReferenceKind::Definition | ReferenceKind::Opaque) {
1546 return Err((
1547 "kind_mismatch",
1548 format!("{name} is a source {} but carries a value", kind.as_str()),
1549 ));
1550 }
1551 let source = work
1552 .declaration(prefix, reference, name)
1553 .map_err(|error| binding_error(error, "missing_source_evidence", name))?;
1554 if source.level_params != levels {
1555 return Err((
1556 "statement_mismatch",
1557 format!("{name} changes the source universe telescope"),
1558 ));
1559 }
1560 source_type(prefix, name, levels, &source.ty, work)?;
1561 super::exact(prefix, &source.ty, ty)
1562 .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1563 let source_body = work
1564 .value(prefix, reference, name)
1565 .map_err(|error| binding_error(error, "missing_source_evidence", name))?;
1566 source_levels(name, levels, &[&source_body], work)?;
1567 let main = super::lean_type(prefix, &source_body)
1568 .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1569 super::exact(prefix, &main, &source.ty)
1570 .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1571 let independent = crate::recheck(prefix, &source_body).map_err(|error| {
1572 binding_error(
1573 super::independent_verdict(&error, name),
1574 "definition_mismatch",
1575 name,
1576 )
1577 })?;
1578 super::independent_comparison(super::exact(prefix, &independent, &source.ty), name)
1579 .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1580 super::exact(prefix, &source_body, body)
1583 .map_err(|error| binding_error(error, "definition_mismatch", name))
1584}
1585
1586fn bind(
1590 certificate: &EnvironmentCertificate,
1591 rebuilt: &RebuiltEnvironment,
1592 reference: &Reference,
1593 kernel_target: &str,
1594 work: &mut SourceWork,
1595) -> std::result::Result<String, (&'static str, String)> {
1596 if certificate.target != kernel_target {
1597 return Err((
1598 "target_mismatch",
1599 format!(
1600 "the certificate proves {}, not the requested {kernel_target}",
1601 certificate.target
1602 ),
1603 ));
1604 }
1605 let ctx = &rebuilt.context;
1606 let target = work
1607 .declaration(ctx, reference, kernel_target)
1608 .map_err(|v| binding_error(v, "missing_source_declaration", kernel_target))?;
1609 let kind_ok = matches!(
1610 (
1611 target.kind,
1612 rebuilt.target_is_theorem,
1613 rebuilt.target_is_opaque
1614 ),
1615 (ReferenceKind::Theorem, true, false)
1616 | (ReferenceKind::Opaque, false, true)
1617 | (ReferenceKind::Definition, false, false)
1618 );
1619 if !kind_ok {
1620 return Err((
1621 "kind_mismatch",
1622 format!(
1623 "{} is a Lean {} but the certificate states a {}",
1624 target.lean_name,
1625 target.kind.as_str(),
1626 if rebuilt.target_is_opaque {
1627 "opaque"
1628 } else if rebuilt.target_is_theorem {
1629 "theorem"
1630 } else {
1631 "definition"
1632 }
1633 ),
1634 ));
1635 }
1636 if target.level_params != rebuilt.target_level_params {
1637 return Err((
1638 "statement_mismatch",
1639 format!(
1640 "{} has universe parameters {:?} in the export and {:?} in the certificate",
1641 target.lean_name, target.level_params, rebuilt.target_level_params
1642 ),
1643 ));
1644 }
1645 source_type(ctx, kernel_target, &target.level_params, &target.ty, work)?;
1646 super::exact(ctx, &target.ty, &rebuilt.target_type)
1647 .map_err(|error| binding_error(error, "statement_mismatch", kernel_target))?;
1648 let mut bound = 0usize;
1649 let source_names: BTreeMap<_, _> = reference
1650 .names()
1651 .map_err(|error| binding_error(error, "missing_source_evidence", "source inventory"))?
1652 .into_iter()
1653 .collect();
1654 for entry in &certificate.entries {
1655 for name in entry.names() {
1656 if name == kernel_target {
1657 continue;
1658 }
1659 match work.declaration(ctx, reference, name) {
1660 Ok(source) => {
1661 let expected_kind = match entry {
1662 EnvironmentEntry::Axiom { .. } => {
1663 matches!(source.kind, ReferenceKind::Axiom)
1664 }
1665 EnvironmentEntry::Definition { .. } => matches!(
1666 source.kind,
1667 ReferenceKind::Definition | ReferenceKind::Recursor
1668 ),
1669 EnvironmentEntry::Theorem { .. } => {
1670 matches!(source.kind, ReferenceKind::Theorem)
1671 }
1672 EnvironmentEntry::Opaque { .. } => {
1673 matches!(source.kind, ReferenceKind::Opaque)
1674 }
1675 EnvironmentEntry::Inductive { types, .. } => {
1676 let is_type = types.iter().any(|t| t.name == name);
1677 matches!(source.kind, ReferenceKind::Inductive) && is_type
1678 || matches!(source.kind, ReferenceKind::Constructor) && !is_type
1679 }
1680 EnvironmentEntry::Quotient { .. } => {
1681 matches!(source.kind, ReferenceKind::Quotient)
1682 }
1683 EnvironmentEntry::Structure { .. } => false,
1684 };
1685 if !expected_kind {
1686 return Err((
1687 "kind_mismatch",
1688 format!(
1689 "{} is a Lean {} but the certificate declares it as a {}",
1690 source.lean_name,
1691 source.kind.as_str(),
1692 entry_kind(entry, name)
1693 ),
1694 ));
1695 }
1696 if source.kind == ReferenceKind::Inductive {
1697 work.family(ctx, reference, name).map_err(|error| {
1698 binding_error(error, "family_metadata_mismatch", name)
1699 })?;
1700 }
1701 let declared = ctx
1702 .poly_declaration(name)
1703 .or_else(|| ctx.get_global(name).map(|t| (&[][..], t)));
1704 let Some((levels, ty)) = declared else {
1705 return Err((
1706 "missing_source_declaration",
1707 format!("{name} is not in the rebuilt environment"),
1708 ));
1709 };
1710 if levels != source.level_params.as_slice() {
1711 return Err(("statement_mismatch", format!("{} has universe parameters {:?} in the export and {levels:?} in the certificate", source.lean_name, source.level_params)));
1712 }
1713 source_type(ctx, name, &source.level_params, &source.ty, work)?;
1714 super::exact(ctx, &source.ty, ty)
1715 .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1716 bound += 1;
1717 }
1718 Err(error) => {
1719 if source_names.contains_key(name) {
1720 return Err(binding_error(error, "missing_source_evidence", name));
1721 }
1722 if !matches!(entry, EnvironmentEntry::Definition { .. })
1723 || !reader_introduced(name)
1724 {
1725 return Err(("unsourced_declaration", format!(
1726 "the certificate declares the {} {name}, which the export does not declare",
1727 entry_kind(entry, name)
1728 )));
1729 }
1730 }
1731 }
1732 }
1733 }
1734 let detail = format!(
1735 "{}'s statement is exactly convertible with the reference translation of its Lean type; {bound} further declaration types and every sourced transparent or opaque value bound to their source",
1736 target.lean_name
1737 );
1738 Ok(detail)
1739}
1740
1741fn reader_introduced(name: &str) -> bool {
1744 name.starts_with("arena.")
1745}
1746
1747fn v(name: &str) -> Term {
1752 Term::Var(name.into())
1753}
1754fn g(name: &str) -> Term {
1755 Term::Global(name.into())
1756}
1757fn pi(name: &str, domain: Term, body: Term) -> Term {
1758 Term::Pi {
1759 param: name.into(),
1760 param_type: Box::new(domain),
1761 body_type: Box::new(body),
1762 }
1763}
1764fn app(f: Term, args: Vec<Term>) -> Term {
1765 args.into_iter()
1766 .fold(f, |f, a| Term::App(Box::new(f), Box::new(a)))
1767}
1768fn at(name: &str, levels: Vec<Universe>) -> Term {
1769 Term::Const {
1770 name: name.into(),
1771 levels,
1772 }
1773}
1774fn prop() -> Term {
1775 Term::Sort(Universe::Prop)
1776}
1777
1778const EQ: &str = "/s:\"Eq\"";
1779const EQ_REFL: &str = "/s:\"Eq\"/s:\"refl\"";
1780const IFF: &str = "/s:\"Iff\"";
1781const IFF_INTRO: &str = "/s:\"Iff\"/s:\"intro\"";
1782const NONEMPTY: &str = "/s:\"Nonempty\"";
1783const NONEMPTY_INTRO: &str = "/s:\"Nonempty\"/s:\"intro\"";
1784
1785fn canonical_inductive(
1789 ctx: &crate::Context,
1790 name: &str,
1791 arity: impl Fn(&[String]) -> Term,
1792 num_params: usize,
1793 constructor: &str,
1794 constructor_type: impl Fn(&[String]) -> Term,
1795) -> bool {
1796 if !ctx.is_inductive(name) || ctx.inductive_num_params(name) != num_params {
1797 return false;
1798 }
1799 let levels = ctx
1800 .poly_declaration(name)
1801 .map(|(p, _)| p.to_vec())
1802 .unwrap_or_default();
1803 let constructors = ctx.get_constructors(name);
1804 constructors.len() == 1
1805 && constructors[0].0 == constructor
1806 && ctx
1807 .get_global(name)
1808 .is_some_and(|t| crate::defeq_for_test(ctx, t, &arity(&levels)))
1809 && crate::defeq_for_test(ctx, constructors[0].1, &constructor_type(&levels))
1810}
1811
1812fn level(levels: &[String], i: usize) -> Universe {
1813 levels
1814 .get(i)
1815 .map(|l| Universe::Var(l.clone()))
1816 .unwrap_or(Universe::Prop)
1817}
1818
1819fn canonical_eq(ctx: &crate::Context) -> bool {
1820 canonical_inductive(
1821 ctx,
1822 EQ,
1823 |l| {
1824 pi(
1825 "α",
1826 Term::Sort(level(l, 0)),
1827 pi("a", v("α"), pi("b", v("α"), prop())),
1828 )
1829 },
1830 2,
1831 EQ_REFL,
1832 |l| {
1833 pi(
1834 "α",
1835 Term::Sort(level(l, 0)),
1836 pi(
1837 "a",
1838 v("α"),
1839 app(at(EQ, vec![level(l, 0)]), vec![v("α"), v("a"), v("a")]),
1840 ),
1841 )
1842 },
1843 ) && ctx.poly_declaration(EQ).is_some_and(|(p, _)| p.len() == 1)
1844}
1845
1846fn canonical_iff(ctx: &crate::Context) -> bool {
1847 canonical_inductive(
1848 ctx,
1849 IFF,
1850 |_| pi("a", prop(), pi("b", prop(), prop())),
1851 2,
1852 IFF_INTRO,
1853 |_| {
1854 pi(
1855 "a",
1856 prop(),
1857 pi(
1858 "b",
1859 prop(),
1860 pi(
1861 "mp",
1862 pi("x", v("a"), v("b")),
1863 pi(
1864 "mpr",
1865 pi("y", v("b"), v("a")),
1866 app(g(IFF), vec![v("a"), v("b")]),
1867 ),
1868 ),
1869 ),
1870 )
1871 },
1872 )
1873}
1874
1875fn canonical_nonempty(ctx: &crate::Context) -> bool {
1876 canonical_inductive(
1877 ctx,
1878 NONEMPTY,
1879 |l| pi("α", Term::Sort(level(l, 0)), prop()),
1880 1,
1881 NONEMPTY_INTRO,
1882 |l| {
1883 pi(
1884 "α",
1885 Term::Sort(level(l, 0)),
1886 pi(
1887 "val",
1888 v("α"),
1889 app(at(NONEMPTY, vec![level(l, 0)]), vec![v("α")]),
1890 ),
1891 )
1892 },
1893 ) && ctx
1894 .poly_declaration(NONEMPTY)
1895 .is_some_and(|(p, _)| p.len() == 1)
1896}
1897
1898fn allowed_statement(ctx: &crate::Context, name: &str, levels: &[String]) -> Option<Term> {
1901 match name {
1902 "/s:\"propext\"" if levels.is_empty() && canonical_eq(ctx) && canonical_iff(ctx) => {
1903 Some(pi(
1904 "a",
1905 prop(),
1906 pi(
1907 "b",
1908 prop(),
1909 pi(
1910 "h",
1911 app(g(IFF), vec![v("a"), v("b")]),
1912 app(
1913 at(EQ, vec![Universe::Type(0)]),
1914 vec![prop(), v("a"), v("b")],
1915 ),
1916 ),
1917 ),
1918 ))
1919 }
1920 "/s:\"Quot\"/s:\"sound\"" if levels.len() == 1 && canonical_eq(ctx) => {
1921 let u = Universe::Var(levels[0].clone());
1922 let quotient = app(
1923 at("kernel.quot.type", vec![u.clone()]),
1924 vec![v("α"), v("r")],
1925 );
1926 let mk = |x: Term| {
1927 app(
1928 at("kernel.quot.mk", vec![u.clone()]),
1929 vec![v("α"), v("r"), x],
1930 )
1931 };
1932 Some(pi(
1933 "α",
1934 Term::Sort(u.clone()),
1935 pi(
1936 "r",
1937 pi("x", v("α"), pi("y", v("α"), prop())),
1938 pi(
1939 "a",
1940 v("α"),
1941 pi(
1942 "b",
1943 v("α"),
1944 pi(
1945 "h",
1946 app(v("r"), vec![v("a"), v("b")]),
1947 app(
1948 at(EQ, vec![u.clone()]),
1949 vec![quotient, mk(v("a")), mk(v("b"))],
1950 ),
1951 ),
1952 ),
1953 ),
1954 ),
1955 ))
1956 }
1957 "/s:\"Classical\"/s:\"choice\"" if levels.len() == 1 && canonical_nonempty(ctx) => {
1958 let u = Universe::Var(levels[0].clone());
1959 Some(pi(
1960 "α",
1961 Term::Sort(u.clone()),
1962 pi("h", app(at(NONEMPTY, vec![u]), vec![v("α")]), v("α")),
1963 ))
1964 }
1965 _ => None,
1966 }
1967}
1968
1969fn universal_inhabitant(ctx: &crate::Context, ty: &Term) -> bool {
1975 let Ok(mut t) = crate::normalize_checked(ctx, ty) else {
1976 return false;
1977 };
1978 let mut sorts: BTreeSet<String> = BTreeSet::new();
1979 let mut counter = 0usize;
1980 loop {
1981 match t {
1982 Term::Pi {
1983 param,
1984 param_type,
1985 body_type,
1986 } => {
1987 let fresh = format!("policy.binder.{counter}");
1988 counter += 1;
1989 let domain = crate::normalize_checked(ctx, ¶m_type).unwrap_or(*param_type);
1990 if matches!(domain, Term::Sort(_)) {
1991 sorts.insert(fresh.clone());
1992 } else if sorts
1993 .iter()
1994 .any(|s| crate::term_store::occurs_free_in_term(&domain, s))
1995 {
1996 return false;
1997 }
1998 t = crate::substitute(&body_type, ¶m, &Term::Var(fresh));
1999 if let Ok(normal) = crate::normalize_checked(ctx, &t) {
2000 t = normal;
2001 }
2002 }
2003 Term::Var(ref name) => return sorts.contains(name),
2004 _ => return false,
2005 }
2006 }
2007}
2008
2009fn audit_assumptions(
2011 rebuilt: &RebuiltEnvironment,
2012) -> std::result::Result<String, (&'static str, String)> {
2013 let ctx = &rebuilt.context;
2014 if !rebuilt.axioms.unresolved.is_empty() {
2015 return Err((
2016 "unresolved_assumption",
2017 format!(
2018 "the proof rests on names the environment does not declare: {:?}",
2019 rebuilt.axioms.unresolved
2020 ),
2021 ));
2022 }
2023 let declared: BTreeMap<&str, (&Vec<String>, &Term)> = rebuilt
2024 .assumptions
2025 .iter()
2026 .map(|(n, l, t)| (n.as_str(), (l, t)))
2027 .collect();
2028 let mut admitted = Vec::new();
2029 let mut goal = Vec::new();
2030 let mut disallowed = Vec::new();
2031 let mut allowed = Vec::new();
2032 for name in &rebuilt.axioms.axioms {
2033 let Some((levels, ty)) = declared.get(name.as_str()).copied() else {
2034 disallowed.push(format!(
2035 "{name} (not an assumption this environment declares)"
2036 ));
2037 continue;
2038 };
2039 if allowed_statement(ctx, name, levels)
2042 .is_some_and(|expected| crate::defeq_for_test(ctx, ty, &expected))
2043 {
2044 allowed.push(name.clone());
2045 continue;
2046 }
2047 let admitted_by_name = crate::certificate::ADMITTED_AXIOMS.iter().any(|lean| {
2048 let components: Vec<&str> = lean.split('.').collect();
2049 super::reference::kernel_name_of_components(&components) == *name || lean == name
2050 });
2051 if admitted_by_name || universal_inhabitant(ctx, ty) {
2052 admitted.push(name.clone());
2053 continue;
2054 }
2055 let states_goal = if levels.len() == rebuilt.target_level_params.len() {
2056 let renamed = crate::instantiate_universes(
2057 ty,
2058 &levels
2059 .iter()
2060 .cloned()
2061 .zip(
2062 rebuilt
2063 .target_level_params
2064 .iter()
2065 .cloned()
2066 .map(Universe::Var),
2067 )
2068 .collect(),
2069 );
2070 crate::defeq_for_test(ctx, &renamed, &rebuilt.target_type)
2071 } else {
2072 false
2073 };
2074 if states_goal {
2075 goal.push(name.clone());
2076 } else {
2077 disallowed.push(name.clone());
2078 }
2079 }
2080 if !admitted.is_empty() {
2081 return Err(("admitted_proof", format!(
2082 "the proof rests on an admission — {} — reached through the transitive closure of its statement and proof (opaque theorems and definitions included); an admitted step proves anything and is never a solution",
2083 admitted.join(", ")
2084 )));
2085 }
2086 if !goal.is_empty() {
2087 return Err(("goal_axiom", format!(
2088 "the proof rests on an axiom stating the goal itself — {} — and an assumed goal is never a solution",
2089 goal.join(", ")
2090 )));
2091 }
2092 if !disallowed.is_empty() {
2093 return Err(("disallowed_assumption", format!(
2094 "the proof rests on assumptions outside the policy (propext, Quot.sound, Classical.choice in canonical form): {}",
2095 disallowed.join(", ")
2096 )));
2097 }
2098 Ok(if allowed.is_empty() {
2099 "the statement and proof rest on no assumption".to_string()
2100 } else {
2101 format!(
2102 "the statement and proof rest only on the allowed, canonical {}",
2103 allowed.join(", ")
2104 )
2105 })
2106}
2107
2108pub fn replay(files: &PackageFiles) -> Receipt {
2114 replay_with(files, &PackageLimits::default())
2115}
2116
2117pub fn replay_with(files: &PackageFiles, limits: &PackageLimits) -> Receipt {
2119 let mut receipt = Receipt::unstarted();
2120 let admitted = match admit(files, limits) {
2121 Ok(admitted) => admitted,
2122 Err((code, detail)) => {
2123 receipt.set(
2124 "lean_accepted",
2125 StageStatus::NotRun,
2126 "a replay never runs Lean",
2127 );
2128 receipt.set_with_code("fresh_replay", StageStatus::Failed, code, detail);
2129 finish_unreached(&mut receipt);
2130 return receipt;
2131 }
2132 };
2133 let producing = admitted
2134 .manifest
2135 .receipt
2136 .get("lean_accepted")
2137 .map(|r| r.status.clone())
2138 .unwrap_or_else(|| "absent".into());
2139 receipt.set(
2140 "lean_accepted",
2141 StageStatus::NotRun,
2142 format!("a replay never runs Lean; the producing run recorded lean_accepted = {producing}"),
2143 );
2144 let depth = match admitted.certificate.admitted_depth(&limits.environment) {
2146 Ok(depth) => depth,
2147 Err(error) => {
2148 receipt.set_with_code(
2149 "fresh_replay",
2150 StageStatus::Failed,
2151 error.code.as_str(),
2152 error.message,
2153 );
2154 finish_unreached(&mut receipt);
2155 return receipt;
2156 }
2157 };
2158 let source = Reference::read(&admitted.export).and_then(|reference| {
2161 let target = reference.source_lean_name(&admitted.manifest.target.lean)?;
2162 let mut reads: Vec<String> = admitted
2163 .correspondence
2164 .rows
2165 .iter()
2166 .filter(|row| {
2167 row.origin == "authored" || row.origin == "prelude" && !row.lean.is_empty()
2168 })
2169 .map(|row| row.lean.clone())
2170 .collect();
2171 if let Some(axioms) = &admitted.manifest.assumptions.lean_print_axioms {
2172 reads.extend(axioms.iter().cloned());
2173 }
2174 source_binding_metrics(
2175 &reference,
2176 &admitted.certificate,
2177 &target,
2178 &reads,
2179 &limits.environment,
2180 )
2181 .map(|metrics| (reference, metrics))
2182 });
2183 let (reference, source_metrics) = match source {
2184 Ok(source) => source,
2185 Err(verdict) => {
2186 let code = verdict.reason().code().as_str();
2187 let detail = verdict.reason().message().to_string();
2188 receipt.set_with_code("translated", StageStatus::Failed, code, detail.clone());
2189 receipt.set_with_code("fresh_replay", StageStatus::Failed, code, detail);
2190 finish_unreached(&mut receipt);
2191 return receipt;
2192 }
2193 };
2194 let source_depth = if source_metrics.has_projection || source_metrics.has_generated {
2195 source_metrics.depth.max(limits.environment.max_depth)
2199 } else {
2200 source_metrics.depth
2201 };
2202 let depth = depth.max(source_depth);
2203 let rungs = depth
2204 .saturating_mul(3)
2205 .saturating_add(crate::term_store::INLINE_DEPTH);
2206 let run = std::thread::scope(|scope| {
2207 std::thread::Builder::new()
2208 .name("lean-package-replay".into())
2209 .stack_size(crate::term_store::stack_bytes_for_depth(rungs))
2210 .spawn_scoped(scope, || replay_admitted(&admitted, &reference, limits))
2211 .map(|handle| {
2212 handle
2213 .join()
2214 .unwrap_or_else(|p| std::panic::resume_unwind(p))
2215 })
2216 });
2217 match run {
2218 Ok(stages) => {
2219 for (name, (status, code, detail)) in stages {
2220 match code {
2221 Some(code) => receipt.set_with_code(name, status, &code, detail),
2222 None => receipt.set(name, status, detail),
2223 }
2224 }
2225 }
2226 Err(e) => receipt.set_with_code(
2227 "fresh_replay",
2228 StageStatus::Failed,
2229 "infrastructure",
2230 format!("cannot reserve the replay stack: {e}"),
2231 ),
2232 }
2233 let blocking = STAGES
2234 .iter()
2235 .filter(|s| !matches!(**s, "lean_accepted" | "fresh_replay"))
2236 .map(|s| (*s, receipt.status(s)))
2237 .find(|(_, status)| *status != StageStatus::Passed);
2238 finish_unreached(&mut receipt);
2239 match blocking {
2240 None => receipt.set(
2241 "fresh_replay",
2242 StageStatus::Passed,
2243 "every replay stage passed from the package bytes alone, with no importer state",
2244 ),
2245 Some((stage, StageStatus::Unsupported)) => receipt.set_with_code(
2246 "fresh_replay",
2247 StageStatus::Unsupported,
2248 "incomplete_replay",
2249 format!("{stage} is unsupported, so the replay is not complete"),
2250 ),
2251 Some((stage, status)) => receipt.set_with_code(
2252 "fresh_replay",
2253 StageStatus::Failed,
2254 "stage_failed",
2255 format!("{stage} {}", status.as_str()),
2256 ),
2257 }
2258 receipt
2259}
2260
2261fn finish_unreached(receipt: &mut Receipt) {
2263 let stop = receipt
2264 .first_failure()
2265 .map(|s| format!("{} failed", s.name));
2266 for name in STAGES {
2267 let stage = receipt.stage(name).expect("every stage");
2268 if stage.status == StageStatus::NotRun && stage.detail.is_empty() {
2269 let why = match &stop {
2270 Some(stop) => format!("not reached: the replay stopped when {stop}"),
2271 None => "not reached".to_string(),
2272 };
2273 receipt.set(name, StageStatus::NotRun, why);
2274 }
2275 }
2276}
2277
2278struct Admitted {
2280 manifest: Manifest,
2281 certificate: EnvironmentCertificate,
2282 correspondence: CorrespondenceFile,
2283 export: String,
2284 document: Option<String>,
2285}
2286
2287fn json_refusal(error: &serde_json::Error) -> &'static str {
2289 if error.to_string().contains("unknown field") {
2290 "unsupported_format"
2291 } else {
2292 "malformed"
2293 }
2294}
2295
2296fn admit(
2297 files: &PackageFiles,
2298 limits: &PackageLimits,
2299) -> std::result::Result<Admitted, (&'static str, String)> {
2300 if files.len() > limits.max_files {
2301 return Err((
2302 "resource_limit",
2303 format!(
2304 "{} files exceed the limit of {}",
2305 files.len(),
2306 limits.max_files
2307 ),
2308 ));
2309 }
2310 let mut total = 0usize;
2311 for (path, bytes) in files {
2312 if let Some(why) = unsafe_path(path, limits) {
2313 return Err(("unsafe_path", format!("{path:?} is {why}")));
2314 }
2315 if bytes.len() > limits.max_file_bytes {
2316 return Err((
2317 "resource_limit",
2318 format!(
2319 "{path} is {} bytes, beyond the limit of {}",
2320 bytes.len(),
2321 limits.max_file_bytes
2322 ),
2323 ));
2324 }
2325 total = total.saturating_add(bytes.len());
2326 }
2327 if total > limits.max_total_bytes {
2328 return Err((
2329 "resource_limit",
2330 format!(
2331 "the package is {total} bytes, beyond the limit of {}",
2332 limits.max_total_bytes
2333 ),
2334 ));
2335 }
2336 let manifest_bytes = files
2337 .get(MANIFEST)
2338 .ok_or(("missing_file", format!("the package has no {MANIFEST}")))?;
2339 let manifest_value: serde_json::Value = serde_json::from_slice(manifest_bytes)
2340 .map_err(|e| ("malformed", format!("{MANIFEST}: {e}")))?;
2341 match manifest_value.get("schema").and_then(|s| s.as_str()) {
2342 Some(schema) if PACKAGE_SCHEMAS.contains(&schema) => {}
2343 Some(other) => {
2344 return Err((
2345 "unsupported_format",
2346 format!(
2347 "{MANIFEST} declares {other:?}; this replay reads {}",
2348 PACKAGE_SCHEMAS.join(" and ")
2349 ),
2350 ))
2351 }
2352 None => {
2353 return Err((
2354 "unsupported_format",
2355 format!("{MANIFEST} declares no schema"),
2356 ))
2357 }
2358 }
2359 let manifest: Manifest = serde_json::from_value(manifest_value)
2360 .map_err(|e| (json_refusal(&e), format!("{MANIFEST}: {e}")))?;
2361 if canonical_json(&manifest) != *manifest_bytes {
2362 return Err((
2363 "non_canonical_encoding",
2364 format!("{MANIFEST} is not in its canonical encoding"),
2365 ));
2366 }
2367 let listed: BTreeSet<&String> = manifest.files.keys().collect();
2369 let present: BTreeSet<&String> = files.keys().filter(|p| *p != MANIFEST).collect();
2370 if let Some(extra) = present.difference(&listed).next() {
2371 return Err((
2372 "unknown_file",
2373 format!("{extra} is not listed in the manifest"),
2374 ));
2375 }
2376 if let Some(missing) = listed.difference(&present).next() {
2377 return Err((
2378 "missing_file",
2379 format!("the manifest lists {missing}, which the package does not contain"),
2380 ));
2381 }
2382 for (path, digest) in &manifest.files {
2383 if sha256_hex(&files[path]) != *digest {
2384 return Err((
2385 "digest_mismatch",
2386 format!("{path}'s SHA-256 is not the manifest's"),
2387 ));
2388 }
2389 }
2390 if result_id_in(&manifest.schema, &manifest.target.lean, &manifest.files) != manifest.result_id
2391 {
2392 return Err((
2393 "result_id_mismatch",
2394 "the result identity does not match the schema, target and file digests".into(),
2395 ));
2396 }
2397 let mut roles: BTreeSet<String> = [CERTIFICATE, CORRESPONDENCE, EXPORT]
2399 .iter()
2400 .map(|s| s.to_string())
2401 .collect();
2402 roles.extend(manifest.document.iter().cloned());
2403 roles.extend(manifest.source.lean_sources.iter().cloned());
2404 for path in manifest.files.keys() {
2405 if !roles.contains(path) {
2406 return Err((
2407 "unknown_file",
2408 format!("{path} has no role in {}", manifest.schema),
2409 ));
2410 }
2411 }
2412 for role in &roles {
2413 if !manifest.files.contains_key(role) {
2414 return Err((
2415 "missing_file",
2416 format!("the package declares {role} but does not list it"),
2417 ));
2418 }
2419 }
2420 if let Some(document) = &manifest.document {
2421 if document.contains('/') || !document.ends_with(".lg") {
2422 return Err((
2423 "unsafe_path",
2424 format!("the document {document:?} is not a top-level .lg file"),
2425 ));
2426 }
2427 }
2428 for source in &manifest.source.lean_sources {
2429 if !source.starts_with("source/") || !source.ends_with(".lean") {
2430 return Err((
2431 "unsafe_path",
2432 format!("{source:?} is not a Lean source under source/"),
2433 ));
2434 }
2435 }
2436 if manifest.source.export != EXPORT
2437 || manifest.files.get(EXPORT) != Some(&manifest.source.export_sha256)
2438 {
2439 return Err((
2440 "manifest_mismatch",
2441 "the manifest's source identity does not name the carried export".into(),
2442 ));
2443 }
2444 if manifest.toolchain.prelude != PRELUDE_VERSION
2445 || ![OPAQUE_ENVIRONMENT_SCHEMA, ENVIRONMENT_SCHEMA]
2446 .contains(&manifest.toolchain.certificate_schema.as_str())
2447 {
2448 return Err(("unsupported_format", format!(
2449 "the package was produced for prelude {} and certificate schema {}; this replay is {PRELUDE_VERSION} and {OPAQUE_ENVIRONMENT_SCHEMA}",
2450 manifest.toolchain.prelude, manifest.toolchain.certificate_schema
2451 )));
2452 }
2453 let certificate_bytes = &files[CERTIFICATE];
2454 let certificate_text = std::str::from_utf8(certificate_bytes)
2455 .map_err(|_| ("malformed", format!("{CERTIFICATE} is not UTF-8")))?;
2456 let certificate = match read_certificate(certificate_text) {
2457 Ok(AnyCertificate::Environment(certificate)) => certificate,
2458 Ok(_) => {
2459 return Err((
2460 "unsupported_format",
2461 format!("{CERTIFICATE} is not an environment certificate"),
2462 ))
2463 }
2464 Err(error) => {
2465 let code = match error.code {
2466 EnvironmentErrorCode::UnsupportedSchema
2467 | EnvironmentErrorCode::IncompatiblePrelude => {
2468 if error.code == EnvironmentErrorCode::IncompatiblePrelude {
2469 "incompatible_prelude"
2470 } else {
2471 "unsupported_format"
2472 }
2473 }
2474 _ if error.message.contains("unknown field") => "unsupported_format",
2475 _ => "malformed",
2476 };
2477 return Err((code, format!("{CERTIFICATE}: {}", error.message)));
2478 }
2479 };
2480 if manifest.toolchain.certificate_schema != certificate.schema {
2481 return Err((
2482 "manifest_mismatch",
2483 "the manifest's certificate schema differs from the carried certificate".into(),
2484 ));
2485 }
2486 if canonical_certificate(&certificate) != *certificate_bytes {
2487 return Err((
2488 "non_canonical_encoding",
2489 format!("{CERTIFICATE} is not in its canonical encoding"),
2490 ));
2491 }
2492 let correspondence_bytes = &files[CORRESPONDENCE];
2493 let correspondence: CorrespondenceFile = serde_json::from_slice(correspondence_bytes)
2494 .map_err(|e| (json_refusal(&e), format!("{CORRESPONDENCE}: {e}")))?;
2495 if correspondence.schema != CORRESPONDENCE_SCHEMA {
2496 return Err((
2497 "unsupported_format",
2498 format!("{CORRESPONDENCE} declares {:?}", correspondence.schema),
2499 ));
2500 }
2501 if canonical_json(&correspondence) != *correspondence_bytes {
2502 return Err((
2503 "non_canonical_encoding",
2504 format!("{CORRESPONDENCE} is not in its canonical encoding"),
2505 ));
2506 }
2507 let export = String::from_utf8(files[EXPORT].clone())
2508 .map_err(|_| ("malformed", format!("{EXPORT} is not UTF-8")))?;
2509 let header: serde_json::Value = serde_json::from_str(export.lines().next().unwrap_or(""))
2510 .map_err(|e| ("malformed", format!("{EXPORT} header: {e}")))?;
2511 if header["meta"]["format"]["version"] != "3.1.0" || manifest.source.format != "3.1.0" {
2512 return Err((
2513 "unsupported_format",
2514 format!("{EXPORT} is not a format 3.1.0 export"),
2515 ));
2516 }
2517 let document = match &manifest.document {
2518 Some(path) => Some(
2519 String::from_utf8(files[path].clone())
2520 .map_err(|_| ("malformed", format!("{path} is not UTF-8")))?,
2521 ),
2522 None => None,
2523 };
2524 Ok(Admitted {
2525 manifest,
2526 certificate,
2527 correspondence,
2528 export,
2529 document,
2530 })
2531}
2532
2533type StageOutcome = (StageStatus, Option<String>, String);
2534
2535fn replay_admitted(
2537 admitted: &Admitted,
2538 reference: &Reference,
2539 limits: &PackageLimits,
2540) -> Vec<(&'static str, StageOutcome)> {
2541 let manifest = &admitted.manifest;
2542 let mut stages: Vec<(&'static str, StageOutcome)> = Vec::new();
2543 let target = match reference.source_lean_name(&manifest.target.lean) {
2544 Ok(name) => name,
2545 Err(verdict) => {
2546 stages.push((
2547 "translated",
2548 binding_failure(binding_error(
2549 verdict,
2550 "missing_source_declaration",
2551 &manifest.target.lean,
2552 )),
2553 ));
2554 return stages;
2555 }
2556 };
2557 let mut source_work = match SourceWork::new(&limits.environment) {
2558 Ok(work) => work,
2559 Err(verdict) => {
2560 stages.push((
2561 "translated",
2562 binding_failure(binding_error(verdict, "missing_source_evidence", &target)),
2563 ));
2564 return stages;
2565 }
2566 };
2567 let checks = check_environment(
2568 &admitted.certificate,
2569 reference,
2570 &target,
2571 &limits.environment,
2572 &mut source_work,
2573 );
2574 let declaration = match &checks.rebuilt {
2575 Some(rebuilt) => source_work.declaration(&rebuilt.context, reference, &target),
2576 None => {
2577 stages.push(("primary_check", checks.primary));
2578 stages.push(("independent_recheck", checks.independent));
2579 stages.push(("translated", checks.translated));
2580 stages.push(("assumption_policy", checks.policy));
2581 stages.push((
2582 "emission_reparsed",
2583 not_run("the environment did not rebuild"),
2584 ));
2585 return stages;
2586 }
2587 };
2588 let declaration = match declaration {
2589 Ok(declaration) => declaration,
2590 Err(verdict) => {
2591 stages.push(("primary_check", checks.primary));
2592 stages.push(("independent_recheck", checks.independent));
2593 let translated = if checks.translated.0 == StageStatus::Passed {
2595 binding_failure(binding_error(
2596 verdict,
2597 "missing_source_declaration",
2598 &target,
2599 ))
2600 } else {
2601 checks.translated
2602 };
2603 stages.push(("translated", translated));
2604 stages.push(("assumption_policy", checks.policy));
2605 stages.push((
2606 "emission_reparsed",
2607 not_run("the source target did not decode"),
2608 ));
2609 return stages;
2610 }
2611 };
2612 let mut translated = checks.translated.clone();
2613 if translated.0 == StageStatus::Passed {
2614 if let Err((code, detail)) = bind_manifest(
2615 manifest,
2616 &declaration,
2617 &admitted.correspondence,
2618 &admitted.certificate,
2619 &reference,
2620 ) {
2621 translated = failed(code, detail);
2622 }
2623 }
2624 let native = NativeDecisions::read(&admitted.export).closure_of(&target);
2627 if translated.0 == StageStatus::Passed
2628 && checks.primary.0 == StageStatus::Passed
2629 && checks.independent.0 == StageStatus::Passed
2630 {
2631 if let Err((code, detail)) = states_contract(manifest, &native) {
2632 translated = failed(code, detail);
2633 }
2634 }
2635 let mut policy = checks.policy.clone();
2636 if checks.primary.0 == StageStatus::Passed {
2637 if let Err(detail) = agree_with_lean(
2638 &reference,
2639 &native,
2640 manifest.assumptions.lean_print_axioms.as_deref(),
2641 &checks.transitive,
2642 ) {
2643 policy = failed("assumption_disagreement", detail);
2644 }
2645 }
2646 if policy.0 == StageStatus::Passed && manifest.assumptions.transitive != checks.transitive {
2647 policy = failed(
2648 "manifest_mismatch",
2649 format!(
2650 "the manifest lists the assumptions {:?}, but the proof rests on {:?}",
2651 manifest.assumptions.transitive, checks.transitive
2652 ),
2653 );
2654 }
2655 stages.push(("primary_check", checks.primary.clone()));
2656 stages.push(("independent_recheck", checks.independent.clone()));
2657 stages.push(("translated", translated));
2658 stages.push(("assumption_policy", policy));
2659 let emission = match (&checks.rebuilt, &admitted.document) {
2660 (None, _) => not_run("the environment did not rebuild"),
2661 (Some(_), None) => (
2662 StageStatus::Unsupported,
2663 Some("no_document".into()),
2664 format!(
2665 "the package carries no LOGOS document: {}",
2666 manifest.emission.detail
2667 ),
2668 ),
2669 (Some(rebuilt), Some(document)) => {
2670 replay_emission(rebuilt, document, &declaration, &reference, admitted)
2671 }
2672 };
2673 stages.push(("emission_reparsed", emission));
2674 stages
2675}
2676
2677fn bind_manifest(
2680 manifest: &Manifest,
2681 declaration: &super::reference::ReferenceDeclaration,
2682 correspondence: &CorrespondenceFile,
2683 certificate: &EnvironmentCertificate,
2684 reference: &Reference,
2685) -> std::result::Result<(), (&'static str, String)> {
2686 let kind = match declaration.kind {
2687 ReferenceKind::Theorem => "thm",
2688 ReferenceKind::Definition => "def",
2689 ReferenceKind::Opaque => "opaque",
2690 _ => "",
2691 };
2692 if manifest.target.kernel != declaration.name
2693 || certificate.target != declaration.name
2694 || manifest.target.kind != kind
2695 || manifest.target.level_params != declaration.level_params
2696 {
2697 return Err((
2698 "manifest_mismatch",
2699 format!(
2700 "the manifest's target ({}, {}, {}) is not the export's {}",
2701 manifest.target.kernel,
2702 manifest.target.kind,
2703 manifest.target.lean,
2704 declaration.lean_name
2705 ),
2706 ));
2707 }
2708 if correspondence.target != declaration.name {
2709 return Err((
2710 "correspondence_mismatch",
2711 format!(
2712 "the correspondence's target is {}, not {}",
2713 correspondence.target, declaration.name
2714 ),
2715 ));
2716 }
2717 let declared: BTreeSet<&str> = certificate.entries.iter().flat_map(|e| e.names()).collect();
2718 let mut kernels = BTreeSet::new();
2719 let mut logos_names = BTreeSet::new();
2720 let mut target_row = false;
2721 for row in &correspondence.rows {
2722 if let Some(logos) = &row.logos {
2723 if !logos_names.insert(logos.as_str()) {
2724 return Err((
2725 "correspondence_mismatch",
2726 format!("the LOGOS name {logos} carries two declarations"),
2727 ));
2728 }
2729 }
2730 let derived =
2735 matches!(row.origin.as_str(), "synthesized" | "prelude") && row.kernel.is_empty();
2736 if derived {
2737 if !row.lean.is_empty() || row.logos.is_none() {
2738 return Err(("correspondence_mismatch", "a derived correspondence row must name its LOGOS constant and no Lean declaration".into()));
2739 }
2740 continue;
2741 }
2742 if row.kernel.is_empty() || !kernels.insert(row.kernel.as_str()) {
2743 return Err((
2744 "correspondence_mismatch",
2745 format!(
2746 "{:?} has no kernel name, or two correspondence rows",
2747 row.kernel
2748 ),
2749 ));
2750 }
2751 if row.origin != "prelude" && !declared.contains(row.kernel.as_str()) {
2752 return Err((
2753 "correspondence_mismatch",
2754 format!(
2755 "the correspondence maps {}, which the certificate does not declare",
2756 row.kernel
2757 ),
2758 ));
2759 }
2760 if row.origin == "authored" || row.origin == "prelude" && !row.lean.is_empty() {
2761 match reference.source_lean_name(&row.lean) {
2762 Ok(source) if source == row.kernel => {}
2763 Ok(source) => {
2764 return Err((
2765 "correspondence_mismatch",
2766 format!(
2767 "the correspondence maps Lean {} to {}, but the export's {} is {}",
2768 row.lean, row.kernel, row.lean, source
2769 ),
2770 ))
2771 }
2772 Err(_) => {
2773 return Err((
2774 "correspondence_mismatch",
2775 format!(
2776 "the correspondence names Lean {}, which the export does not declare",
2777 row.lean
2778 ),
2779 ))
2780 }
2781 }
2782 }
2783 if row.kernel == declaration.name {
2784 target_row = true;
2785 if row.lean != declaration.lean_name || row.logos != manifest.target.logos {
2786 return Err((
2787 "correspondence_mismatch",
2788 "the correspondence's target row is not the manifest's target".into(),
2789 ));
2790 }
2791 }
2792 }
2793 if !target_row {
2794 return Err((
2795 "correspondence_mismatch",
2796 "the correspondence has no row for the target".into(),
2797 ));
2798 }
2799 Ok(())
2800}
2801
2802fn replay_emission(
2806 rebuilt: &RebuiltSummary,
2807 document: &str,
2808 declaration: &super::reference::ReferenceDeclaration,
2809 reference: &Reference,
2810 admitted: &Admitted,
2811) -> StageOutcome {
2812 let proof_of = |key: &str| rebuilt.proofs.get(key).cloned();
2813 let emitted = match super::emit::emit_context(&rebuilt.context, &proof_of, &declaration.name) {
2814 Ok(emitted) => emitted,
2815 Err(verdict) => {
2816 return (
2817 if matches!(verdict, Verdict::Decline(_)) {
2818 StageStatus::Unsupported
2819 } else {
2820 StageStatus::Failed
2821 },
2822 Some(verdict.reason().code().as_str().to_string()),
2823 format!(
2824 "the rebuilt environment could not be written as LOGOS: {}",
2825 verdict.reason().message()
2826 ),
2827 )
2828 }
2829 };
2830 if emitted.source != document {
2831 return failed(
2832 "emission_mismatch",
2833 "the package's LOGOS document is not the emitter's writing of the rebuilt environment",
2834 );
2835 }
2836 let rows = match correspondence_rows(&admitted.certificate, reference, Some(&emitted)) {
2837 Ok(rows) => rows,
2838 Err(verdict) => return failed("emission_mismatch", verdict.reason().message().to_string()),
2839 };
2840 if rows != admitted.correspondence.rows {
2841 return failed(
2842 "correspondence_mismatch",
2843 "the correspondence file is not the document's correspondence",
2844 );
2845 }
2846 if admitted.manifest.target.logos.as_deref() != Some(emitted.target.as_str()) {
2847 return failed(
2848 "manifest_mismatch",
2849 "the manifest's LOGOS target is not the document's",
2850 );
2851 }
2852 match super::emit::check_statement(&emitted, declaration) {
2853 Ok(()) => (
2854 StageStatus::Passed,
2855 None,
2856 format!(
2857 "{} reparsed in a fresh LOGOS session; {} states the reference translation of {}",
2858 admitted.manifest.document.as_deref().unwrap_or(""),
2859 emitted.target,
2860 declaration.lean_name
2861 ),
2862 ),
2863 Err(fault) => failed("emission_mismatch", fault.to_string()),
2864 }
2865}
2866
2867pub fn read_directory(
2874 dir: &std::path::Path,
2875 limits: &PackageLimits,
2876) -> std::result::Result<PackageFiles, String> {
2877 let mut files = PackageFiles::new();
2878 let mut total = 0usize;
2879 let mut work = vec![(dir.to_path_buf(), String::new())];
2880 while let Some((path, prefix)) = work.pop() {
2881 let entries = std::fs::read_dir(&path).map_err(|e| format!("{}: {e}", path.display()))?;
2882 for entry in entries {
2883 let entry = entry.map_err(|e| format!("{}: {e}", path.display()))?;
2884 let name = entry
2885 .file_name()
2886 .into_string()
2887 .map_err(|n| format!("{n:?} is not a UTF-8 file name"))?;
2888 let relative = if prefix.is_empty() {
2889 name.clone()
2890 } else {
2891 format!("{prefix}/{name}")
2892 };
2893 let metadata =
2894 std::fs::symlink_metadata(entry.path()).map_err(|e| format!("{relative}: {e}"))?;
2895 if metadata.file_type().is_symlink() {
2896 return Err(format!("{relative} is a symbolic link"));
2897 }
2898 if metadata.is_dir() {
2899 work.push((entry.path(), relative));
2900 continue;
2901 }
2902 if !metadata.is_file() {
2903 return Err(format!("{relative} is not a regular file"));
2904 }
2905 if let Some(why) = unsafe_path(&relative, limits) {
2906 return Err(format!("{relative:?} is {why}"));
2907 }
2908 if files.len() + 1 > limits.max_files {
2909 return Err(format!("more than {} files", limits.max_files));
2910 }
2911 let size = usize::try_from(metadata.len()).unwrap_or(usize::MAX);
2912 if size > limits.max_file_bytes {
2913 return Err(format!(
2914 "{relative} is {size} bytes, beyond the limit of {}",
2915 limits.max_file_bytes
2916 ));
2917 }
2918 total = total.saturating_add(size);
2919 if total > limits.max_total_bytes {
2920 return Err(format!(
2921 "the package exceeds {} bytes",
2922 limits.max_total_bytes
2923 ));
2924 }
2925 let bytes = std::fs::read(entry.path()).map_err(|e| format!("{relative}: {e}"))?;
2926 if bytes.len() != size {
2927 return Err(format!("{relative} changed while it was read"));
2928 }
2929 files.insert(relative, bytes);
2930 }
2931 }
2932 Ok(files)
2933}
2934
2935pub fn publish(dir: &std::path::Path, files: &PackageFiles) -> std::io::Result<()> {
2939 use std::io::Write;
2940 if dir.exists() {
2941 return Err(std::io::Error::new(
2942 std::io::ErrorKind::AlreadyExists,
2943 format!("{} already exists", dir.display()),
2944 ));
2945 }
2946 let limits = PackageLimits::default();
2947 for path in files.keys() {
2948 if let Some(why) = unsafe_path(path, &limits) {
2949 return Err(std::io::Error::new(
2950 std::io::ErrorKind::InvalidInput,
2951 format!("{path:?} is {why}"),
2952 ));
2953 }
2954 }
2955 let parent = dir
2956 .parent()
2957 .filter(|p| !p.as_os_str().is_empty())
2958 .unwrap_or(std::path::Path::new("."));
2959 let stem = dir
2960 .file_name()
2961 .and_then(|n| n.to_str())
2962 .unwrap_or("package");
2963 let nonce = std::time::SystemTime::now()
2964 .duration_since(std::time::UNIX_EPOCH)
2965 .map(|d| d.as_nanos())
2966 .unwrap_or(0);
2967 let staging = parent.join(format!(".{stem}.staging-{}-{nonce}", std::process::id()));
2968 let result = (|| {
2969 std::fs::create_dir(&staging)?;
2970 for (path, bytes) in files {
2971 let target = staging.join(path);
2972 if let Some(dir) = target.parent() {
2973 std::fs::create_dir_all(dir)?;
2974 }
2975 let mut file = std::fs::File::create(&target)?;
2976 file.write_all(bytes)?;
2977 file.sync_all()?;
2978 }
2979 std::fs::rename(&staging, dir)
2980 })();
2981 if result.is_err() {
2982 let _ = std::fs::remove_dir_all(&staging);
2983 }
2984 result
2985}
2986
2987pub fn receipt_document(receipt: &Receipt, manifest_sha256: Option<&str>) -> serde_json::Value {
2989 serde_json::json!({
2990 "schema": REPLAY_SCHEMA,
2991 "replayed": receipt.replayed(),
2992 "verified": receipt.verified(),
2993 "manifest_sha256": manifest_sha256,
2994 "stages": receipt.to_json(),
2995 "trusted_base": TRUSTED_BASE,
2996 })
2997}
2998
2999#[path = "package_source_work.rs"]
3000mod source_work;
3001use source_work::SourceWork;
3002
3003#[cfg(test)]
3004#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/package/native_disclosure_controls.rs"]
3005mod native_disclosure_controls;
3006
3007#[cfg(test)]
3008#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/package/native_contract_controls.rs"]
3009mod native_contract_controls;