Skip to main content

ocas_calc/integral/
outcome.rs

1//! Three-valued integration results and the certificate-gated entry point
2//! (0.28.0).
3//!
4//! The classic [`integrate`](super::integrate) returns a single `Atom`: an
5//! antiderivative when one was found, and the unevaluated `Integral(f, x)`
6//! form otherwise. That loses information in two ways — it cannot say
7//! whether a fallback is "I do not know" or "this is provably not
8//! elementary", and it cannot say whether an emitted answer is *proved*
9//! correct.
10//!
11//! [`integrate_outcome`] replaces the string criterion with a discipline:
12//!
13//! - [`Outcome::Found`] carries a machine-checkable
14//!   [`Certificate`](super::certify::Certificate) — `D(F) − f` reduces to
15//!   zero in the exact checker;
16//! - [`Outcome::Unknown`] honestly returns the unevaluated form, and may
17//!   additionally expose the pipeline's **uncertified** candidate;
18//! - [`Outcome::ProvedNonElementary`] is reserved for the non-elementary
19//!   layer (first producer planned with the `Li₂`/Meier-G wave); nothing in
20//!   0.28.0 constructs it.
21//!
22//! The legacy `Atom`-returning entry points keep returning the pipeline's
23//! candidate unchanged (including uncertified ones), so existing callers and
24//! bindings keep their behaviour; they carry a rustdoc warning pointing at
25//! [`integrate_outcome`] when a proof is required.
26
27use ocas_atom::normalize::normalize;
28use ocas_atom::{Atom, AtomArena, Symbol};
29
30use super::certify::{CertDecline, Certificate, certify};
31use super::{IntegrateOptions, integrate_with_options, residual};
32
33/// Witness of a proof that an antiderivative does not exist in the
34/// implemented elementary fragment.
35///
36/// Reserved: 0.28.0 defines the variant so the three-valued API is stable,
37/// but no producer exists yet (the non-elementary wave adds the first one).
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct NonElementaryWitness<'a> {
40    /// Name of the proof method that produced the witness.
41    pub method: &'static str,
42    /// The unevaluated `Integral(f, var)` form that is returned to callers.
43    pub residue: Atom<'a>,
44}
45
46/// The result of a certified integration.
47#[derive(Clone, Copy, Debug)]
48pub enum Outcome<'a> {
49    /// An antiderivative together with a machine-checkable certificate.
50    Found {
51        /// The antiderivative.
52        value: Atom<'a>,
53        /// The proof that `D(value) = f`.
54        certificate: Certificate<'a>,
55    },
56    /// The integral was proved not to have an elementary antiderivative in
57    /// the implemented fragment (no producer in 0.28.0).
58    ProvedNonElementary {
59        /// The proof witness, carrying the unevaluated form.
60        witness: NonElementaryWitness<'a>,
61    },
62    /// Honest "I do not know": the unevaluated `Integral(f, var)` form.
63    Unknown {
64        /// The unevaluated residue, always an `Integral(f, var)` atom.
65        residue: Atom<'a>,
66        /// The pipeline's candidate, when it produced one but the exact
67        /// checker could not certify it.
68        ///
69        /// **Not an answer.** It is surfaced for diagnostics, tracing and
70        /// migration; using it without independent verification is exactly
71        /// what this API exists to prevent.
72        uncertified: Option<Atom<'a>>,
73    },
74}
75
76impl<'a> Outcome<'a> {
77    /// The value to show a caller: the certified antiderivative, or the
78    /// honest residue.
79    ///
80    /// # Example
81    ///
82    /// ```
83    /// use ocas_atom::{AtomArena, Symbol};
84    /// use ocas_calc::integral::outcome::integrate_outcome;
85    /// use ocas_core::arena::Arena;
86    ///
87    /// let arena = Arena::new();
88    /// let ctx = AtomArena::new(&arena);
89    /// let x = ctx.var("x");
90    /// let two_x = ctx.mul(&[ctx.num(2), x]);
91    /// let outcome = integrate_outcome(&ctx, two_x, Symbol::new("x"));
92    /// assert!(outcome.is_found());
93    /// assert!(!outcome.value().to_string().contains("Integral("));
94    /// ```
95    pub fn value(self) -> Atom<'a> {
96        match self {
97            Outcome::Found { value, .. } => value,
98            Outcome::Unknown { residue, .. } => residue,
99            Outcome::ProvedNonElementary { witness } => witness.residue,
100        }
101    }
102
103    /// Whether the result carries a certificate.
104    pub fn is_found(self) -> bool {
105        matches!(self, Outcome::Found { .. })
106    }
107
108    /// The certificate, when there is one.
109    pub fn certificate(self) -> Option<Certificate<'a>> {
110        match self {
111            Outcome::Found { certificate, .. } => Some(certificate),
112            _ => None,
113        }
114    }
115
116    /// The pipeline's uncertified candidate, when it produced one.
117    pub fn uncertified(self) -> Option<Atom<'a>> {
118        match self {
119            Outcome::Unknown { uncertified, .. } => uncertified,
120            _ => None,
121        }
122    }
123}
124
125/// Integrate `expr` and return a three-valued outcome, certifying the answer.
126///
127/// See the module docs for the emission discipline.
128///
129/// # Example
130///
131/// ```
132/// use ocas_atom::{AtomArena, Symbol};
133/// use ocas_calc::integral::outcome::{Outcome, integrate_outcome};
134/// use ocas_core::arena::Arena;
135///
136/// let arena = Arena::new();
137/// let ctx = AtomArena::new(&arena);
138/// let x = ctx.var("x");
139/// // ∫ exp(-x²) dx has no elementary antiderivative: the honest answer is
140/// // the unevaluated form.
141/// let e = ctx.fun("exp", &[ctx.mul(&[ctx.num(-1), ctx.pow(x, ctx.num(2))])]);
142/// match integrate_outcome(&ctx, e, Symbol::new("x")) {
143///     Outcome::Found { .. } => panic!("exp(-x²) must not be certified"),
144///     Outcome::Unknown { residue, .. } => {
145///         assert!(residue.to_string().contains("Integral("));
146///     }
147///     Outcome::ProvedNonElementary { .. } => panic!("no producer in 0.28.0"),
148/// }
149/// ```
150pub fn integrate_outcome<'a>(ctx: &'a AtomArena<'a>, expr: Atom<'a>, var: Symbol) -> Outcome<'a> {
151    integrate_outcome_with_options(ctx, expr, var, IntegrateOptions::default())
152}
153
154/// [`integrate_outcome`] with explicit pipeline options.
155pub fn integrate_outcome_with_options<'a>(
156    ctx: &'a AtomArena<'a>,
157    expr: Atom<'a>,
158    var: Symbol,
159    options: IntegrateOptions,
160) -> Outcome<'a> {
161    gated(ctx, expr, var, options).outcome
162}
163
164/// Diagnostic view of one certified integration: the raw pipeline candidate,
165/// the exact checker's verdict, and the gated outcome.
166#[doc(hidden)]
167#[derive(Clone, Copy, Debug)]
168pub struct DiagnosticOutcome<'a> {
169    /// The pipeline's candidate, before any certification.
170    pub raw: Atom<'a>,
171    /// The exact checker's verdict for that candidate.
172    pub certificate: Result<Certificate<'a>, CertDecline>,
173    /// What [`integrate_outcome`] would return.
174    pub outcome: Outcome<'a>,
175}
176
177/// Run the pipeline once and return both the raw candidate and the gated
178/// outcome; used by the corpus harness to measure certificate coverage.
179#[doc(hidden)]
180pub fn integrate_diagnostic<'a>(
181    ctx: &'a AtomArena<'a>,
182    expr: Atom<'a>,
183    var: Symbol,
184    options: IntegrateOptions,
185) -> DiagnosticOutcome<'a> {
186    gated(ctx, expr, var, options)
187}
188
189fn gated<'a>(
190    ctx: &'a AtomArena<'a>,
191    expr: Atom<'a>,
192    var: Symbol,
193    options: IntegrateOptions,
194) -> DiagnosticOutcome<'a> {
195    let normalized = normalize(ctx, expr);
196    let raw = integrate_with_options(ctx, expr, var, options);
197    let certificate = if residual::contains_residue(raw) {
198        Err(CertDecline::NotInField)
199    } else {
200        certify(ctx, normalized, raw, var)
201    };
202    let outcome = match certificate {
203        Ok(cert) => Outcome::Found {
204            value: raw,
205            certificate: cert,
206        },
207        Err(reason) => Outcome::Unknown {
208            residue: if residual::contains_residue(raw) {
209                raw
210            } else {
211                super::fallback(ctx, normalized, var)
212            },
213            uncertified: if residual::contains_residue(raw) {
214                None
215            } else {
216                debug_assert!(matches!(
217                    reason,
218                    CertDecline::NotInField | CertDecline::Budget | CertDecline::NonZero
219                ));
220                Some(raw)
221            },
222        },
223    };
224    DiagnosticOutcome {
225        raw,
226        certificate,
227        outcome,
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use ocas_core::arena::Arena;
234    use ocas_parse::parse;
235
236    use super::*;
237
238    #[test]
239    fn certified_pairs_are_found() {
240        let arena = Arena::new();
241        let ctx = AtomArena::new(&arena);
242        // ∫ 2x dx = x²
243        let f = parse(&ctx, "2*x").expect("parse");
244        let outcome = integrate_outcome(&ctx, f, Symbol::new("x"));
245        assert!(outcome.is_found());
246        assert!(outcome.certificate().is_some());
247        assert!(!outcome.value().to_string().contains("Integral("));
248    }
249
250    #[test]
251    fn non_elementary_integrals_are_unknown_with_a_residue() {
252        let arena = Arena::new();
253        let ctx = AtomArena::new(&arena);
254        let f = parse(&ctx, "exp(x^3)").expect("parse");
255        let outcome = integrate_outcome(&ctx, f, Symbol::new("x"));
256        assert!(!outcome.is_found());
257        assert!(outcome.value().to_string().contains("Integral("));
258    }
259
260    #[test]
261    fn diagnostic_reports_the_raw_candidate() {
262        let arena = Arena::new();
263        let ctx = AtomArena::new(&arena);
264        let f = parse(&ctx, "x^2*sin(x)").expect("parse");
265        let d = integrate_diagnostic(&ctx, f, Symbol::new("x"), IntegrateOptions::default());
266        // The raw candidate and the gated value agree for a certified case.
267        assert!(!d.raw.to_string().contains("Integral("));
268        assert_eq!(d.raw, d.outcome.value());
269    }
270}