|
1 | 1 | // SPDX-License-Identifier: PMPL-1.0-or-later |
2 | 2 | // Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
3 | 3 |
|
4 | | -//! Canonical type-discipline taxonomy for echidna. |
| 4 | +//! Type-discipline taxonomy — **Axis 2** of echidna's prover classification. |
5 | 5 | //! |
6 | | -//! This module is the single source of truth for every type discipline that |
7 | | -//! echidna can route, encode, validate, or reason about. It is the downstream |
8 | | -//! target for `developer-ecosystem/katagoria/` — when katagoria graduates a |
9 | | -//! new level in its type-theory pipeline (`katagoria → typell → typed-wasm |
10 | | -//! → PanLL`), the corresponding variant should already exist here. |
| 6 | +//! # Two orthogonal axes |
11 | 7 | //! |
12 | | -//! # Design |
| 8 | +//! Echidna classifies proving tools along two independent axes: |
| 9 | +//! |
| 10 | +//! * **Axis 1 — `ProverKind`** in `src/rust/provers/mod.rs`. The thing |
| 11 | +//! people type into a search box: *Agda*, *Coq*, *Lean 4*, *Z3*, *CVC5*, |
| 12 | +//! *Isabelle*, *HOL Light*, *Idris2*, *F\**, … Real binaries with |
| 13 | +//! websites, papers, and communities of practice. |
| 14 | +//! * **Axis 2 — `TypeDiscipline`** (this module). The thing the prover is |
| 15 | +//! *applied to*: dependent types, linear types, modal logic, session |
| 16 | +//! types, tropical resource typing, homotopy type theory, algebraic |
| 17 | +//! effects, … The semantic shape of the reasoning problem. |
| 18 | +//! |
| 19 | +//! Both axes must be first-class. A user arriving from "Agda" wants to see |
| 20 | +//! that Agda serves the dependent, indexed, higher-kinded, cubical, and |
| 21 | +//! homotopy disciplines. A user arriving from "linear types" wants to see |
| 22 | +//! that Idris2 (via QTT) and the HP-ecosystem `LinearTypeChecker` can |
| 23 | +//! serve it. Cross-links in both directions: discipline → provers via |
| 24 | +//! [`TypeDiscipline::provers_serving`]; prover → disciplines via the |
| 25 | +//! convention that `ProverKind::*TypeChecker` is the HP dispatch route and |
| 26 | +//! classical provers have their discipline coverage listed in the mapping |
| 27 | +//! below. |
| 28 | +//! |
| 29 | +//! # Relationship to katagoria |
| 30 | +//! |
| 31 | +//! This module is the downstream target for `developer-ecosystem/katagoria/`. |
| 32 | +//! When katagoria graduates a new level in its type-theory pipeline |
| 33 | +//! (`katagoria → typell → typed-wasm → PanLL`), the corresponding variant |
| 34 | +//! is expected to already exist here. |
| 35 | +//! |
| 36 | +//! # Design notes |
13 | 37 | //! |
14 | 38 | //! The enum is deliberately exhaustive up-front. Opening it again is |
15 | 39 | //! expensive: `ProverKind::*TypeChecker` is referenced in six exhaustive |
16 | 40 | //! match arms in `src/rust/provers/mod.rs` plus the HP-ecosystem dispatch |
17 | | -//! table. Every variant added here has an immediate 1:1 mapping to a |
18 | | -//! `ProverKind::*TypeChecker` — adding a discipline means adding both. |
| 41 | +//! table. Every variant here has an immediate 1:1 mapping to a |
| 42 | +//! `ProverKind::*TypeChecker` via [`TypeDiscipline::prover_kind`] — adding |
| 43 | +//! a discipline means adding both. |
19 | 44 | //! |
20 | | -//! Phase 1 (this transition) keeps wiring at the dispatch level only — each |
21 | | -//! discipline routes to `typell --discipline=<tag>` (or the two named |
22 | | -//! upstreams for `Tropical` / `Katagoria`). Phase 2 followups (see |
23 | | -//! `AI-WORK-todo.md`) deepen the support: per-discipline proof encoding, |
24 | | -//! Idris2 validator tagging, family-aware GNN features, discipline-tagged |
25 | | -//! corpus fixtures. |
| 45 | +//! Phase 1 (the transition landed 2026-04-17) keeps wiring at the dispatch |
| 46 | +//! level only — each discipline routes to `typell --discipline=<tag>` (or |
| 47 | +//! the two named upstreams for `Tropical` / `Katagoria`). Phase 2 followups |
| 48 | +//! (see `AI-WORK-todo.md`) deepen the support: per-discipline proof |
| 49 | +//! encoding, Idris2 validator tagging, family-aware GNN features, |
| 50 | +//! discipline-tagged corpus fixtures. |
26 | 51 | //! |
27 | 52 | //! # Family grouping |
28 | 53 | //! |
@@ -344,6 +369,123 @@ impl TypeDiscipline { |
344 | 369 | } |
345 | 370 | } |
346 | 371 |
|
| 372 | + /// Which **classical** (Axis-1) provers can natively serve this |
| 373 | + /// discipline, beyond the HP-ecosystem dispatch route. |
| 374 | + /// |
| 375 | + /// Returned kinds are the real binaries a user would reach for if they |
| 376 | + /// searched for this discipline by name — Agda for dependent types, NuSMV |
| 377 | + /// for temporal logic, Viper for separation, and so on. The HP-ecosystem |
| 378 | + /// dispatcher (available through [`prover_kind`]) is always a valid route |
| 379 | + /// and is intentionally *not* listed here — this helper exists to power |
| 380 | + /// Axis-1-first discovery. |
| 381 | + /// |
| 382 | + /// An empty `Vec` means no classical prover in echidna's current lineup |
| 383 | + /// has mainstream coverage of the discipline; the HP dispatcher is the |
| 384 | + /// only known route. That is not a judgement on the discipline's |
| 385 | + /// importance — several communication-style disciplines (Session, |
| 386 | + /// Choreographic, Echo, Dyadic) live entirely in the research-prototype |
| 387 | + /// world that the HP stack is specifically here to serve. |
| 388 | + /// |
| 389 | + /// Conservative by construction: a prover is only listed when its |
| 390 | + /// mainstream documentation explicitly advertises the discipline, or |
| 391 | + /// when a widely-used published encoding exists. Extend carefully; |
| 392 | + /// spurious entries mislead users worse than empty lists. |
| 393 | + pub fn provers_serving(self) -> Vec<ProverKind> { |
| 394 | + use ProverKind as P; |
| 395 | + use TypeDiscipline as D; |
| 396 | + match self { |
| 397 | + // Foundation — simply-typed lambda calculus. Everyone who |
| 398 | + // supports anything supports this; listing the proof assistants |
| 399 | + // that have explicit STLC tutorials. |
| 400 | + D::Ordinary => { |
| 401 | + vec![P::Agda, P::Coq, P::Lean, P::Isabelle, P::Idris2, P::FStar] |
| 402 | + } |
| 403 | + |
| 404 | + // Polymorphism family. |
| 405 | + D::Polymorphic => vec![ |
| 406 | + P::Agda, |
| 407 | + P::Coq, |
| 408 | + P::Lean, |
| 409 | + P::Isabelle, |
| 410 | + P::Idris2, |
| 411 | + P::FStar, |
| 412 | + P::HOL4, |
| 413 | + P::HOLLight, |
| 414 | + P::PVS, |
| 415 | + ], |
| 416 | + D::Existential => vec![P::Agda, P::Coq, P::Lean, P::Idris2, P::FStar], |
| 417 | + D::HigherKinded => vec![P::Agda, P::Coq, P::Lean, P::Idris2, P::FStar], |
| 418 | + D::Row => vec![], // Koka-native; none in echidna's classical lineup. |
| 419 | + D::Phantom => vec![ |
| 420 | + P::Agda, P::Coq, P::Lean, P::Idris2, P::FStar, P::Dafny, |
| 421 | + ], |
| 422 | + |
| 423 | + // Subtyping family. |
| 424 | + D::Subtyping => vec![P::FStar, P::Dafny], |
| 425 | + D::Intersection => vec![], // Flow / Typed Racket territory; none classical. |
| 426 | + D::Union => vec![P::FStar, P::Dafny], |
| 427 | + D::Gradual => vec![], // Grift / Reticulated Python; none in echidna. |
| 428 | + |
| 429 | + // Dependent / refinement family. |
| 430 | + D::Dependent => vec![P::Agda, P::Coq, P::Lean, P::Idris2, P::FStar, P::PVS], |
| 431 | + // LiquidHaskell is the canonical refinement-type home but is |
| 432 | + // not in echidna's classical lineup, so it's intentionally |
| 433 | + // absent here. |
| 434 | + D::Refinement => vec![P::FStar, P::Dafny, P::Why3], |
| 435 | + D::Hoare => vec![P::Dafny, P::Viper, P::FramaC, P::KeY, P::Why3], |
| 436 | + D::Indexed => vec![P::Agda, P::Coq, P::Idris2], |
| 437 | + |
| 438 | + // Substructural family. Mainstream support is thin; Idris2 via |
| 439 | + // QTT is the canonical route for Linear/Affine, the rest are |
| 440 | + // research-prototype (routed through the HP dispatcher). |
| 441 | + D::Qtt => vec![P::Idris2], |
| 442 | + D::Linear => vec![P::Idris2], |
| 443 | + D::Affine => vec![P::Idris2], |
| 444 | + D::Relevant => vec![], |
| 445 | + D::Ordered => vec![], |
| 446 | + D::Uniqueness => vec![], // Clean is canonical but not in echidna. |
| 447 | + |
| 448 | + // Mutability / capability. |
| 449 | + D::Immutable => vec![P::Dafny, P::Viper, P::FramaC, P::KeY], |
| 450 | + D::Capability => vec![P::Viper], |
| 451 | + D::Bunched => vec![P::Viper], // Separation-logic flavoured. |
| 452 | + |
| 453 | + // Modal family. |
| 454 | + D::Modal => vec![P::Isabelle], // Isabelle/ModalHOL + other instances. |
| 455 | + D::Epistemic => vec![], // DEL / S5 tooling lives outside echidna's current lineup. |
| 456 | + D::Temporal => vec![ |
| 457 | + P::NuSMV, P::TLC, P::SPIN, P::UPPAAL, P::Prism, P::TLAPS, |
| 458 | + ], |
| 459 | + D::Provability => vec![], // GL logic, mostly research. |
| 460 | + |
| 461 | + // Effects / coeffects. |
| 462 | + D::EffectRow => vec![P::FStar], // F* effect algebras. |
| 463 | + D::Impure => vec![P::FStar, P::Dafny], // Stateful-program verifiers. |
| 464 | + D::Coeffect => vec![], // Granule; not in echidna. |
| 465 | + D::Probabilistic => vec![P::Prism, P::DReal], |
| 466 | + |
| 467 | + // Process / communication — the HP stack is specifically here |
| 468 | + // to serve these. No classical prover in echidna advertises |
| 469 | + // first-class session / choreographic / echo / dyadic typing. |
| 470 | + D::Session => vec![], |
| 471 | + D::Choreographic => vec![], |
| 472 | + D::Dyadic => vec![], |
| 473 | + D::Echo => vec![], |
| 474 | + |
| 475 | + // Resource semirings — Tropical is an HP-stack specialty. |
| 476 | + D::Tropical => vec![], |
| 477 | + |
| 478 | + // Homotopy foundations. |
| 479 | + D::Homotopy => vec![P::Agda, P::Lean], // HoTT libraries on both. |
| 480 | + D::Cubical => vec![P::Agda], // Cubical Agda is the canonical home. |
| 481 | + |
| 482 | + // Entry-point kernels: these are the HP gateways themselves; |
| 483 | + // they don't correspond to a classical prover in the Axis-1 |
| 484 | + // sense. |
| 485 | + D::TypeLl | D::Katagoria => vec![], |
| 486 | + } |
| 487 | + } |
| 488 | + |
347 | 489 | /// Inverse of [`prover_kind`]. Returns `None` for non-HP-ecosystem kinds. |
348 | 490 | pub fn from_prover_kind(kind: ProverKind) -> Option<TypeDiscipline> { |
349 | 491 | use ProverKind as P; |
@@ -511,6 +653,82 @@ mod tests { |
511 | 653 | } |
512 | 654 | } |
513 | 655 |
|
| 656 | + #[test] |
| 657 | + fn provers_serving_only_lists_classical_kinds() { |
| 658 | + // Axis-1-first discovery: `provers_serving` must never list |
| 659 | + // the HP-ecosystem dispatcher (which is already reachable via |
| 660 | + // `prover_kind`). Otherwise a user arriving via "which prover |
| 661 | + // serves linear types?" sees `LinearTypeChecker` in the list |
| 662 | + // and is no better off than when they started. |
| 663 | + for &d in TypeDiscipline::ALL.iter() { |
| 664 | + for kind in d.provers_serving() { |
| 665 | + assert!( |
| 666 | + !kind.is_hp_ecosystem(), |
| 667 | + "{:?} lists HP-ecosystem kind {:?} in provers_serving; should be classical only", |
| 668 | + d, |
| 669 | + kind |
| 670 | + ); |
| 671 | + } |
| 672 | + } |
| 673 | + } |
| 674 | + |
| 675 | + #[test] |
| 676 | + fn provers_serving_is_deduplicated() { |
| 677 | + for &d in TypeDiscipline::ALL.iter() { |
| 678 | + let list = d.provers_serving(); |
| 679 | + let mut sorted: Vec<ProverKind> = list.clone(); |
| 680 | + sorted.sort_by_key(|k| format!("{:?}", k)); |
| 681 | + let before = sorted.len(); |
| 682 | + sorted.dedup(); |
| 683 | + assert_eq!( |
| 684 | + before, |
| 685 | + sorted.len(), |
| 686 | + "{:?} has duplicates in provers_serving: {:?}", |
| 687 | + d, |
| 688 | + list |
| 689 | + ); |
| 690 | + } |
| 691 | + } |
| 692 | + |
| 693 | + #[test] |
| 694 | + fn provers_serving_canonical_spot_checks() { |
| 695 | + // Ground-truth spot checks anchored in mainstream usage. |
| 696 | + // Breaking these should be a deliberate taxonomy decision. |
| 697 | + use TypeDiscipline as D; |
| 698 | + |
| 699 | + let dep = D::Dependent.provers_serving(); |
| 700 | + assert!(dep.contains(&ProverKind::Agda)); |
| 701 | + assert!(dep.contains(&ProverKind::Coq)); |
| 702 | + assert!(dep.contains(&ProverKind::Lean)); |
| 703 | + assert!(dep.contains(&ProverKind::Idris2)); |
| 704 | + |
| 705 | + let linear = D::Linear.provers_serving(); |
| 706 | + assert!(linear.contains(&ProverKind::Idris2)); // QTT. |
| 707 | + |
| 708 | + let cubical = D::Cubical.provers_serving(); |
| 709 | + assert!(cubical.contains(&ProverKind::Agda)); // Cubical Agda. |
| 710 | + |
| 711 | + let temporal = D::Temporal.provers_serving(); |
| 712 | + assert!(temporal.contains(&ProverKind::NuSMV)); |
| 713 | + assert!(temporal.contains(&ProverKind::SPIN)); |
| 714 | + assert!(temporal.contains(&ProverKind::TLC)); |
| 715 | + |
| 716 | + let hoare = D::Hoare.provers_serving(); |
| 717 | + assert!(hoare.contains(&ProverKind::Dafny)); |
| 718 | + assert!(hoare.contains(&ProverKind::Viper)); |
| 719 | + assert!(hoare.contains(&ProverKind::KeY)); |
| 720 | + |
| 721 | + // Communication-style disciplines: HP stack is their home. |
| 722 | + assert!(D::Session.provers_serving().is_empty()); |
| 723 | + assert!(D::Choreographic.provers_serving().is_empty()); |
| 724 | + assert!(D::Dyadic.provers_serving().is_empty()); |
| 725 | + assert!(D::Echo.provers_serving().is_empty()); |
| 726 | + |
| 727 | + // Entry-point kernels don't have a classical counterpart. |
| 728 | + assert!(D::TypeLl.provers_serving().is_empty()); |
| 729 | + assert!(D::Katagoria.provers_serving().is_empty()); |
| 730 | + } |
| 731 | + |
514 | 732 | #[test] |
515 | 733 | fn tag_matches_hp_ecosystem_upstream() { |
516 | 734 | // Sanity: TypeDiscipline::tag() must agree with the string passed |
|
0 commit comments