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}