Skip to main content

logicaffeine_proof/
proof_emit.rs

1//! Standard proof-trace emission — **DRAT**, **LRAT**, and **DPR** text from our in-memory
2//! [`ProofStep`] stream, so the certified refutations this crate produces are not merely
3//! self-checked but replayable by the SAT community's independent (and in `cake_lpr`'s case
4//! *formally verified*) checkers.
5//!
6//! Three formats, three trust reaches:
7//!
8//! - **DRAT** (Wetzler/Heule/Hunt 2014) — clause *additions* and *deletions*, no hints. The
9//!   universal interchange format; checked by `drat-trim`. We emit it for any refutation whose
10//!   added clauses are all RUP (our plain-CDCL fallback, the BVE/vivification RUP path).
11//! - **LRAT** (Cruz-Filipe et al. 2017) — DRAT plus an explicit *hint chain* of antecedent
12//!   clause IDs per step, so the checker replays in `O(hint)` rather than re-searching. This is
13//!   what the CakeML-**verified** `cake_lpr` consumes. We synthesise the hints by instrumenting
14//!   the very same unit-propagation core the RUP checker uses (`rup`), then ship an
15//!   independent [`check_lrat`] that re-validates them.
16//! - **DPR** (Heule/Kiesl/Biere 2017) — additions carrying an *assignment witness* for a
17//!   propagation-redundant (model-removing) clause; checked by `dpr-trim`. This is the format
18//!   `SaDiCaL` emits for its positive-reduct PR clauses, and the one our SDCL path lands in.
19//!
20//! **The honesty seam.** Our symmetry crusher certifies clauses with a *substitution* witness
21//! under the **SR** (substitution-redundancy) criterion — strictly stronger than the PR that
22//! `dpr-trim` checks (the PHP swap clause `¬x(i,h)` is SR-but-not-PR). So the DPR emitter does
23//! not blindly transcribe a substitution: it [`try_assignment_witness`]es each one — reduce to a
24//! candidate assignment, then **re-verify it against `pr::is_pr`** — and emits standard DPR
25//! only when that genuinely holds. A substitution that is irreducibly SR makes the emitter
26//! return [`EmitError::RequiresSubstitutionRedundancy`], never a bogus PR line. Fail-closed: we
27//! would rather decline to emit than emit a proof an honest checker rejects.
28
29use core::fmt::Write as _;
30
31use crate::cdcl::Lit;
32use crate::proof::{ProofStep, Witness};
33use crate::rup::{lit_val, set_true};
34
35/// Why a proof could not be emitted in a requested standard format.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum EmitError {
38    /// A step's added clause is not RUP w.r.t. the database at that point — the refutation does
39    /// not actually check, so there is nothing sound to emit.
40    StepNotRup { index: usize },
41    /// The refutation does not end by deriving the empty clause (it is not a refutation).
42    EmptyClauseNotRup,
43    /// A `Pr` step carries a substitution witness that is irreducibly SR — it cannot be
44    /// expressed as a standard PR assignment witness, so no `dpr-trim`-checkable line exists.
45    RequiresSubstitutionRedundancy { index: usize },
46    /// A `Pr` step appeared where the chosen format (DRAT/LRAT) admits only RUP additions.
47    PrInRupOnlyFormat { index: usize },
48    /// A streaming emitter's sink refused a write — it hit the caller's byte cap (see [`SizeSink`]).
49    /// Surfaced only when measuring a proof's size against a bound; the `String`-returning emitters,
50    /// whose sink never fails, cannot produce it.
51    SizeCapExceeded,
52}
53
54impl From<core::fmt::Error> for EmitError {
55    /// The only writer that returns [`core::fmt::Error`] here is a capped [`SizeSink`]; a `String`
56    /// sink is infallible, so this conversion fires exactly when a size bound was crossed.
57    fn from(_: core::fmt::Error) -> Self {
58        EmitError::SizeCapExceeded
59    }
60}
61
62/// A byte-counting write sink that measures a proof's serialized size **without materializing it**:
63/// it keeps only a running length, discarding every chunk the moment its bytes are counted, and once
64/// the total would exceed `cap` it refuses further writes (returning [`core::fmt::Error`], which the
65/// streaming emitters map to [`EmitError::SizeCapExceeded`]). This lets us report our own proof size
66/// on the benchmarks page in O(1) memory, and guarantees a pathological (super-linear) proof aborts
67/// early rather than exhausting RAM — the streaming, capped counterpart to `emit_*(...).len()`.
68pub struct SizeSink {
69    bytes: u64,
70    cap: u64,
71    overflowed: bool,
72}
73
74impl SizeSink {
75    /// A sink that counts up to `cap` bytes before it starts refusing writes.
76    pub fn new(cap: u64) -> Self {
77        Self { bytes: 0, cap, overflowed: false }
78    }
79    /// Bytes streamed through so far (the serialized proof size when emission ran to completion).
80    pub fn bytes(&self) -> u64 {
81        self.bytes
82    }
83    /// Whether the cap was hit — the measured proof is at least `cap` bytes, exact size unknown.
84    pub fn overflowed(&self) -> bool {
85        self.overflowed
86    }
87}
88
89impl core::fmt::Write for SizeSink {
90    fn write_str(&mut self, s: &str) -> core::fmt::Result {
91        self.bytes = self.bytes.saturating_add(s.len() as u64);
92        if self.bytes > self.cap {
93            self.overflowed = true;
94            return Err(core::fmt::Error);
95        }
96        Ok(())
97    }
98}
99
100/// DIMACS signed-integer encoding of a literal: variable `v` (0-based) → `±(v+1)`.
101#[inline]
102fn dimacs(l: Lit) -> i64 {
103    let v = (l.var() + 1) as i64;
104    if l.is_positive() {
105        v
106    } else {
107        -v
108    }
109}
110
111/// Append `lits` as space-separated DIMACS integers followed by the `0` terminator.
112fn push_clause(out: &mut String, lits: &[Lit]) {
113    for &l in lits {
114        out.push_str(&dimacs(l).to_string());
115        out.push(' ');
116    }
117    out.push_str("0");
118}
119
120/// Streaming twin of [`push_clause`]: write `lits` as space-separated DIMACS integers then the `0`
121/// terminator into any [`core::fmt::Write`] sink, so an emitter can feed a [`SizeSink`] instead of a
122/// growing `String`. Byte-for-byte identical to [`push_clause`].
123fn wpush_clause<W: core::fmt::Write>(w: &mut W, lits: &[Lit]) -> Result<(), EmitError> {
124    for &l in lits {
125        write!(w, "{} ", dimacs(l))?;
126    }
127    w.write_str("0")?;
128    Ok(())
129}
130
131/// Write a single literal as `"<dimacs> "` (integer then a trailing space) — the token shape the SR
132/// witness/permutation sections use.
133fn wput<W: core::fmt::Write>(w: &mut W, l: Lit) -> Result<(), EmitError> {
134    write!(w, "{} ", dimacs(l))?;
135    Ok(())
136}
137
138/// The canonical multiset key of a clause, for matching deletions against the database.
139fn canon(c: &[Lit]) -> Vec<u32> {
140    let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
141    k.sort_unstable();
142    k.dedup();
143    k
144}
145
146// ---------------------------------------------------------------------------------------------
147// Witness reduction (SR substitution → re-verified PR assignment), fail-closed.
148// ---------------------------------------------------------------------------------------------
149
150/// Reduce a [`Witness`] to a standard PR **assignment** witness for `clause` against `db`,
151/// returning `Some(ω)` only when `ω` genuinely satisfies `pr::is_pr` — so the caller can emit a
152/// `dpr-trim`-checkable line — and `None` when the witness is irreducibly SR.
153///
154/// For an [`Witness::Assignment`] this just re-verifies the witness it already carries. For a
155/// [`Witness::Substitution`] `σ` it tries the canonical reduction `ω = {¬σ(l) : l ∈ C}` and, since
156/// that need not satisfy `C`, a small family of candidates seeded by each satisfying literal of
157/// `C`. Every candidate is checked by `pr::is_pr` before being returned, so the function can never
158/// hand back an unsound witness.
159pub fn try_assignment_witness(
160    num_vars: usize,
161    db: &[Vec<Lit>],
162    clause: &[Lit],
163    witness: &Witness,
164) -> Option<Vec<Lit>> {
165    let accept = |w: &[Lit]| crate::pr::is_pr(num_vars, db, clause, &Witness::Assignment(w.to_vec()));
166    match witness {
167        Witness::Assignment(w) => {
168            if accept(w) {
169                Some(w.clone())
170            } else {
171                None
172            }
173        }
174        Witness::Substitution(sigma) => {
175            // The image of the falsified cube under σ — the textbook reduction.
176            let canon_w: Vec<Lit> = clause.iter().map(|&l| sigma.apply(l).negated()).collect();
177            if accept(&canon_w) {
178                return Some(canon_w);
179            }
180            // Seed with a satisfying literal of C (PR requires ω ⊨ C), then layer the σ-image on
181            // top without self-contradiction. Tries each pivot — cheap and often closes the gap.
182            for &pivot in clause {
183                let mut w = vec![pivot];
184                for &l in &canon_w {
185                    if !w.contains(&l) && !w.contains(&l.negated()) {
186                        w.push(l);
187                    }
188                }
189                if accept(&w) {
190                    return Some(w);
191                }
192            }
193            None
194        }
195    }
196}
197
198// ---------------------------------------------------------------------------------------------
199// DRAT.
200// ---------------------------------------------------------------------------------------------
201
202/// Emit a **DRAT** proof (additions + deletions, no hints, no witnesses). Returns
203/// [`EmitError::PrInRupOnlyFormat`] if any step is a `Pr` step — DRAT cannot carry a PR witness;
204/// use [`emit_dpr`] for those. The trailing empty clause is appended iff it is RUP.
205pub fn emit_drat(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
206    let mut db: Vec<Vec<Lit>> = original.to_vec();
207    let mut out = String::new();
208    for (i, step) in steps.iter().enumerate() {
209        match step {
210            ProofStep::Rup(c) => {
211                if !crate::rup::is_rup(num_vars, &db, c) {
212                    return Err(EmitError::StepNotRup { index: i });
213                }
214                push_clause(&mut out, c);
215                out.push('\n');
216                db.push(c.clone());
217            }
218            ProofStep::Delete(c) => {
219                out.push_str("d ");
220                push_clause(&mut out, c);
221                out.push('\n');
222                let key = canon(c);
223                if let Some(pos) = db.iter().position(|d| canon(d) == key) {
224                    db.swap_remove(pos);
225                }
226            }
227            ProofStep::Pr { .. } => return Err(EmitError::PrInRupOnlyFormat { index: i }),
228        }
229    }
230    if !crate::rup::is_rup(num_vars, &db, &[]) {
231        return Err(EmitError::EmptyClauseNotRup);
232    }
233    out.push_str("0\n");
234    Ok(out)
235}
236
237// ---------------------------------------------------------------------------------------------
238// DPR.
239// ---------------------------------------------------------------------------------------------
240
241/// Emit a **DPR** proof: RUP additions as bare clauses, PR additions as `C 0 ω 0` (witness `ω`
242/// re-verified and laid out pivot-first per the `dpr-trim` convention), deletions as `d C 0`. A
243/// `Pr` step whose witness is irreducibly SR yields [`EmitError::RequiresSubstitutionRedundancy`].
244pub fn emit_dpr(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
245    let mut db: Vec<Vec<Lit>> = original.to_vec();
246    let mut out = String::new();
247    for (i, step) in steps.iter().enumerate() {
248        match step {
249            ProofStep::Rup(c) => {
250                if !crate::rup::is_rup(num_vars, &db, c) {
251                    return Err(EmitError::StepNotRup { index: i });
252                }
253                push_clause(&mut out, c);
254                out.push('\n');
255                db.push(c.clone());
256            }
257            ProofStep::Pr { clause, witness } => {
258                let omega = try_assignment_witness(num_vars, &db, clause, witness)
259                    .ok_or(EmitError::RequiresSubstitutionRedundancy { index: i })?;
260                // The witness must lead with the clause's first literal (the pivot): pick a
261                // literal of C that ω satisfies and float it to the front of both.
262                let pivot = clause
263                    .iter()
264                    .copied()
265                    .find(|l| omega.contains(l))
266                    .expect("a verified PR witness satisfies its clause");
267                let mut c_ord = vec![pivot];
268                c_ord.extend(clause.iter().copied().filter(|&l| l != pivot));
269                let mut w_ord = vec![pivot];
270                w_ord.extend(omega.iter().copied().filter(|&l| l != pivot));
271                push_clause(&mut out, &c_ord);
272                out.push(' ');
273                // The witness literals, then their own terminating 0.
274                for &l in &w_ord {
275                    out.push_str(&dimacs(l).to_string());
276                    out.push(' ');
277                }
278                out.push_str("0\n");
279                db.push(clause.clone());
280            }
281            ProofStep::Delete(c) => {
282                out.push_str("d ");
283                push_clause(&mut out, c);
284                out.push('\n');
285                let key = canon(c);
286                if let Some(pos) = db.iter().position(|d| canon(d) == key) {
287                    db.swap_remove(pos);
288                }
289            }
290        }
291    }
292    if !crate::rup::is_rup(num_vars, &db, &[]) {
293        return Err(EmitError::EmptyClauseNotRup);
294    }
295    out.push_str("0\n");
296    Ok(out)
297}
298
299// ---------------------------------------------------------------------------------------------
300// SR (substitution redundancy) — Marijn Heule's `.sr` format, the input to `sr2drat`.
301// ---------------------------------------------------------------------------------------------
302
303/// Emit our refutation in **Marijn Heule's `.sr` (substitution-redundancy) proof format** — the input
304/// `sr2drat` expands into plain DRAT for `drat-trim` to verify. This is the pipeline that makes our
305/// marquee *symmetry* proofs externally machine-checkable, closing the "SR witnesses not yet exportable"
306/// gap: a `Pr` step carrying a [`Witness::Substitution`] no longer dead-ends at
307/// [`EmitError::RequiresSubstitutionRedundancy`] but is written as a substitution line `sr2drat` knows.
308///
309/// Per the `sr2drat` grammar (sr2drat.c) a lemma line is the clause, then — each delimited by a repeat
310/// of the **pivot** (the clause's first literal) — the PR witness `ω`, then optionally the permutation
311/// `σ` as `var image` pairs. RUP additions carry no witness (a bare `C 0` line `sr2drat` forwards to
312/// DRAT). Each step is re-verified (`pr::is_pr` / RUP) before it is written, so a returned proof is
313/// sound by our checker; the trailing empty clause closes it.
314///
315/// The decisive subtlety, matching the reference `.sr` shape: `sr2drat` expects the lemma's **pivot
316/// variable to be held by the assignment `ω`, with `σ` permuting only the other variables**. For a unit
317/// lemma `[p]` witnessed by a transposition that moves `p`, we therefore *decompose* the witness —
318/// `ω = {p, ¬σ(p)}` pins both swapped copies in that coordinate and `σ′` (σ with `p`'s and `σ(p)`'s
319/// variables fixed) carries the rest. This keeps the pivot out of the permutation entirely, which is
320/// what makes the expansion check: **verified end-to-end through `sr2drat`→`drat-trim` for PHP(n) up to
321/// n=18 (a 591k-line DRAT proof)**, the marquee symmetry refutations Kissat and CaDiCaL time out on.
322pub fn emit_sr(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
323    let mut out = String::new();
324    write_sr(&mut out, num_vars, original, steps)?;
325    Ok(out)
326}
327
328/// Streaming core of [`emit_sr`]: write the SR proof into any [`core::fmt::Write`] sink rather than a
329/// `String`, so its serialized size can be measured through a capped [`SizeSink`] in O(1) memory (and
330/// bail early on a pathological proof) instead of building the whole text. Byte-for-byte identical to
331/// [`emit_sr`]; the `String`-backed wrapper above simply cannot hit [`EmitError::SizeCapExceeded`].
332pub fn write_sr<W: core::fmt::Write>(
333    w: &mut W,
334    num_vars: usize,
335    original: &[Vec<Lit>],
336    steps: &[ProofStep],
337) -> Result<(), EmitError> {
338    let mut db: Vec<Vec<Lit>> = original.to_vec();
339    for (i, step) in steps.iter().enumerate() {
340        match step {
341            ProofStep::Rup(c) => {
342                if !crate::rup::is_rup(num_vars, &db, c) {
343                    return Err(EmitError::StepNotRup { index: i });
344                }
345                wpush_clause(w, c)?;
346                w.write_str("\n")?;
347                db.push(c.clone());
348            }
349            ProofStep::Delete(c) => {
350                w.write_str("d ")?;
351                wpush_clause(w, c)?;
352                w.write_str("\n")?;
353                let key = canon(c);
354                if let Some(pos) = db.iter().position(|d| canon(d) == key) {
355                    db.swap_remove(pos);
356                }
357            }
358            ProofStep::Pr { clause, witness } => {
359                if !crate::pr::is_pr(num_vars, &db, clause, witness) {
360                    return Err(EmitError::StepNotRup { index: i });
361                }
362                match witness {
363                    Witness::Assignment(omega) => {
364                        let pivot = *clause.first().ok_or(EmitError::StepNotRup { index: i })?;
365                        for &l in clause {
366                            wput(w, l)?;
367                        }
368                        // Witness section opens with the pivot; then ω's other literals.
369                        wput(w, pivot)?;
370                        for &l in omega {
371                            if l != pivot {
372                                wput(w, l)?;
373                            }
374                        }
375                        w.write_str("0\n")?;
376                        db.push(clause.clone());
377                    }
378                    Witness::Substitution(sigma) => {
379                        let pivot = *clause.first().ok_or(EmitError::StepNotRup { index: i })?;
380                        // **Witness decomposition (sr2drat convention).** sr2drat wants the lemma's pivot
381                        // variable held by the assignment ω, with σ permuting only the *other* variables
382                        // (the pivot variable σ-fixed) — exactly how the reference `.sr` proofs are
383                        // shaped. For a unit lemma `[p]` witnessed by a transposition that moves `p`'s
384                        // variable, we split: ω = {p, ¬σ(p)} pins both swapped copies in this coordinate,
385                        // and σ' = σ with `p`'s variable and `σ(p)`'s variable fixed carries the rest.
386                        // This keeps the pivot out of the permutation (no delimiter collision) and out of
387                        // the dropped-literal trap that breaks ≥3-variable orbits.
388                        let (omega_extra, fixed_a, fixed_b): (Option<Lit>, u32, u32) =
389                            if clause.len() == 1 {
390                                let sp = sigma.apply(pivot);
391                                (Some(sp.negated()), pivot.var(), sp.var())
392                            } else {
393                                (None, u32::MAX, u32::MAX)
394                            };
395                        for &l in clause {
396                            wput(w, l)?;
397                        }
398                        // Witness ω opens with the pivot, then its decomposition partner.
399                        wput(w, pivot)?;
400                        if let Some(extra) = omega_extra {
401                            if extra != pivot {
402                                wput(w, extra)?;
403                            }
404                        }
405                        // Permutation section: σ over the moved variables, with the pivot coordinate
406                        // (both swapped copies) fixed when we decomposed a unit lemma.
407                        wput(w, pivot)?;
408                        for v in 0..num_vars as u32 {
409                            if v == fixed_a || v == fixed_b {
410                                continue; // held by ω, not permuted
411                            }
412                            let img = sigma.apply(Lit::pos(v));
413                            if img == Lit::pos(v) {
414                                continue; // fixed point
415                            }
416                            if img == pivot || Lit::pos(v) == pivot {
417                                return Err(EmitError::RequiresSubstitutionRedundancy { index: i });
418                            }
419                            write!(w, "{} ", (v as i64) + 1)?;
420                            wput(w, img)?;
421                        }
422                        w.write_str("0\n")?;
423                        db.push(clause.clone());
424                    }
425                }
426            }
427        }
428    }
429    if !crate::rup::is_rup(num_vars, &db, &[]) {
430        return Err(EmitError::EmptyClauseNotRup);
431    }
432    w.write_str("0\n")?;
433    Ok(())
434}
435
436/// Parse a DRAT/DPR proof body back into [`ProofStep`]s — one clause per line. A bare `C 0` line
437/// is a RUP addition; `C 0 ω 0` is a PR addition with an [`Witness::Assignment`]; `d C 0` is a
438/// deletion. Used to round-trip our own emission through our own checker.
439pub fn parse_dpr(text: &str) -> Result<Vec<ProofStep>, String> {
440    let mut steps = Vec::new();
441    for raw in text.lines() {
442        let line = raw.trim();
443        if line.is_empty() || line.starts_with('c') {
444            continue;
445        }
446        let mut toks = line.split_whitespace().peekable();
447        let deletion = matches!(toks.peek(), Some(&"d"));
448        if deletion {
449            toks.next();
450        }
451        // First field: literals up to the first 0.
452        let mut clause = Vec::new();
453        let mut saw_terminator = false;
454        for tok in toks.by_ref() {
455            let n: i64 = tok.parse().map_err(|_| format!("bad token {tok:?}"))?;
456            if n == 0 {
457                saw_terminator = true;
458                break;
459            }
460            clause.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
461        }
462        if !saw_terminator {
463            return Err(format!("unterminated clause in line {line:?}"));
464        }
465        if deletion {
466            steps.push(ProofStep::Delete(clause));
467            continue;
468        }
469        // A second field (the witness) may follow.
470        let mut witness = Vec::new();
471        let mut has_witness = false;
472        for tok in toks.by_ref() {
473            has_witness = true;
474            let n: i64 = tok.parse().map_err(|_| format!("bad witness token {tok:?}"))?;
475            if n == 0 {
476                break;
477            }
478            witness.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
479        }
480        if has_witness {
481            steps.push(ProofStep::Pr { clause, witness: Witness::Assignment(witness) });
482        } else {
483            steps.push(ProofStep::Rup(clause));
484        }
485    }
486    Ok(steps)
487}
488
489// ---------------------------------------------------------------------------------------------
490// LRAT (with synthesised hint chains) + an independent LRAT checker.
491// ---------------------------------------------------------------------------------------------
492
493/// Run RUP for `c` against the id-tagged `db`, returning the **LRAT hint chain** — the antecedent
494/// clause ids in trail order, conflict clause last — or `None` if `c` is not RUP. The chain is
495/// the reason-clauses reachable backward from the conflict, which is exactly what an LRAT checker
496/// replays.
497fn rup_hints(num_vars: usize, db: &[(u64, Vec<Lit>)], c: &[Lit]) -> Option<Vec<u64>> {
498    let mut assign: Vec<Option<bool>> = vec![None; num_vars];
499    let mut reason: Vec<Option<u64>> = vec![None; num_vars];
500    let mut trail_pos: Vec<Option<usize>> = vec![None; num_vars];
501    let mut next_pos = 0usize;
502    // Assume ¬c. An immediate clash makes c trivially RUP (a tautological clause); no hints.
503    for &l in c {
504        let nl = l.negated();
505        match lit_val(&assign, nl) {
506            Some(true) => {}
507            Some(false) => return Some(Vec::new()),
508            None => {
509                assign[nl.var() as usize] = Some(nl.is_positive());
510                trail_pos[nl.var() as usize] = Some(next_pos);
511                next_pos += 1;
512            }
513        }
514    }
515    loop {
516        let mut changed = false;
517        for (id, clause) in db {
518            let mut satisfied = false;
519            let mut unset: Vec<Lit> = Vec::new();
520            for &l in clause {
521                match lit_val(&assign, l) {
522                    Some(true) => {
523                        satisfied = true;
524                        break;
525                    }
526                    Some(false) => {}
527                    None => {
528                        if unset.contains(&l.negated()) {
529                            satisfied = true;
530                            break;
531                        }
532                        if !unset.contains(&l) {
533                            unset.push(l);
534                        }
535                    }
536                }
537            }
538            if satisfied {
539                continue;
540            }
541            if unset.is_empty() {
542                return Some(build_chain(db, &reason, &trail_pos, clause, *id));
543            }
544            if unset.len() == 1 {
545                let u = unset[0];
546                assign[u.var() as usize] = Some(u.is_positive());
547                reason[u.var() as usize] = Some(*id);
548                trail_pos[u.var() as usize] = Some(next_pos);
549                next_pos += 1;
550                changed = true;
551            }
552        }
553        if !changed {
554            return None;
555        }
556    }
557}
558
559/// Backward-mark the reason clauses needed for the conflict at `conflict_clause`/`conflict_id`,
560/// then order them by the trail position of the literal each forced (ascending) with the conflict
561/// id last — a forward-replayable LRAT hint chain.
562fn build_chain(
563    db: &[(u64, Vec<Lit>)],
564    reason: &[Option<u64>],
565    trail_pos: &[Option<usize>],
566    conflict_clause: &[Lit],
567    conflict_id: u64,
568) -> Vec<u64> {
569    let by_id: std::collections::HashMap<u64, &Vec<Lit>> =
570        db.iter().map(|(id, c)| (*id, c)).collect();
571    let num_vars = reason.len();
572    let mut visited = vec![false; num_vars];
573    let mut needed: Vec<(usize, u64)> = Vec::new();
574    let mut stack: Vec<u32> = conflict_clause.iter().map(|l| l.var()).collect();
575    while let Some(v) = stack.pop() {
576        if visited[v as usize] {
577            continue;
578        }
579        visited[v as usize] = true;
580        if let Some(rid) = reason[v as usize] {
581            let pos = trail_pos[v as usize].expect("a forced literal has a trail position");
582            needed.push((pos, rid));
583            if let Some(rc) = by_id.get(&rid) {
584                for &l in *rc {
585                    stack.push(l.var());
586                }
587            }
588        }
589    }
590    needed.sort_by_key(|&(pos, _)| pos);
591    let mut chain: Vec<u64> = needed.into_iter().map(|(_, id)| id).collect();
592    chain.push(conflict_id);
593    chain
594}
595
596/// Emit an **LRAT** proof: each RUP addition as `<id> <clause> 0 <hints> 0`, deletions as
597/// `<id> d <deleted-id> 0`, and a final empty-clause line. Returns
598/// [`EmitError::PrInRupOnlyFormat`] for any `Pr` step (LRAT proper carries no PR witness).
599pub fn emit_lrat(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
600    let mut db: Vec<(u64, Vec<Lit>)> =
601        original.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
602    let mut next_id = original.len() as u64 + 1;
603    let mut out = String::new();
604    for (i, step) in steps.iter().enumerate() {
605        match step {
606            ProofStep::Rup(c) => {
607                let hints = rup_hints(num_vars, &db, c).ok_or(EmitError::StepNotRup { index: i })?;
608                let id = next_id;
609                next_id += 1;
610                out.push_str(&id.to_string());
611                out.push(' ');
612                push_clause(&mut out, c);
613                out.push(' ');
614                for h in &hints {
615                    out.push_str(&h.to_string());
616                    out.push(' ');
617                }
618                out.push_str("0\n");
619                db.push((id, c.clone()));
620            }
621            ProofStep::Delete(c) => {
622                let key = canon(c);
623                if let Some(pos) = db.iter().rposition(|(_, d)| canon(d) == key) {
624                    let (did, _) = db[pos];
625                    out.push_str(&(next_id - 1).to_string());
626                    out.push_str(" d ");
627                    out.push_str(&did.to_string());
628                    out.push_str(" 0\n");
629                    db.remove(pos);
630                }
631            }
632            ProofStep::Pr { .. } => return Err(EmitError::PrInRupOnlyFormat { index: i }),
633        }
634    }
635    // The closing empty clause.
636    let ehints = rup_hints(num_vars, &db, &[]).ok_or(EmitError::EmptyClauseNotRup)?;
637    out.push_str(&next_id.to_string());
638    out.push_str(" 0 ");
639    for h in &ehints {
640        out.push_str(&h.to_string());
641        out.push(' ');
642    }
643    out.push_str("0\n");
644    Ok(out)
645}
646
647/// Independently verify an **LRAT** proof against `original` — the small, naive checker whose
648/// simplicity is the trust. Replays each addition's hint chain (every hinted clause must be unit
649/// at its turn, the last must conflict) and accepts iff a line derives the empty clause. Rejects a
650/// corrupted hint, a dangling id, or a chain that fails to conflict.
651pub fn check_lrat(num_vars: usize, original: &[Vec<Lit>], lrat: &str) -> bool {
652    let mut db: std::collections::HashMap<u64, Vec<Lit>> =
653        original.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
654    for raw in lrat.lines() {
655        let line = raw.trim();
656        if line.is_empty() || line.starts_with('c') {
657            continue;
658        }
659        let mut toks = line.split_whitespace();
660        let Some(id_tok) = toks.next() else { continue };
661        let Ok(id) = id_tok.parse::<u64>() else { return false };
662        let rest: Vec<&str> = toks.collect();
663        if rest.first() == Some(&"d") {
664            for t in &rest[1..] {
665                if let Ok(d) = t.parse::<u64>() {
666                    if d != 0 {
667                        db.remove(&d);
668                    }
669                }
670            }
671            continue;
672        }
673        // Split the line into the clause (up to the first 0) and the hint chain (up to the next).
674        let mut clause = Vec::new();
675        let mut k = 0usize;
676        while k < rest.len() {
677            let Ok(n) = rest[k].parse::<i64>() else { return false };
678            k += 1;
679            if n == 0 {
680                break;
681            }
682            clause.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
683        }
684        let mut hints = Vec::new();
685        while k < rest.len() {
686            let Ok(n) = rest[k].parse::<i64>() else { return false };
687            k += 1;
688            if n == 0 {
689                break;
690            }
691            hints.push(n as u64);
692        }
693        if !verify_rup_step(num_vars, &db, &clause, &hints) {
694            return false;
695        }
696        if clause.is_empty() {
697            return true;
698        }
699        db.insert(id, clause);
700    }
701    false
702}
703
704/// Replay one LRAT step: assume `¬clause`, then for each hint id (in order) the hinted clause must
705/// be unit (propagate its literal) until the last hint conflicts.
706fn verify_rup_step(
707    num_vars: usize,
708    db: &std::collections::HashMap<u64, Vec<Lit>>,
709    clause: &[Lit],
710    hints: &[u64],
711) -> bool {
712    let mut assign: Vec<Option<bool>> = vec![None; num_vars];
713    for &l in clause {
714        if !set_true(&mut assign, l.negated()) {
715            return true; // ¬clause self-contradictory ⇒ trivially valid
716        }
717    }
718    for (i, &h) in hints.iter().enumerate() {
719        let Some(cl) = db.get(&h) else { return false };
720        let mut satisfied = false;
721        let mut unset: Vec<Lit> = Vec::new();
722        for &l in cl {
723            match lit_val(&assign, l) {
724                Some(true) => {
725                    satisfied = true;
726                    break;
727                }
728                Some(false) => {}
729                None => {
730                    if unset.contains(&l.negated()) {
731                        satisfied = true;
732                        break;
733                    }
734                    if !unset.contains(&l) {
735                        unset.push(l);
736                    }
737                }
738            }
739        }
740        if satisfied {
741            return false; // a satisfied clause is never a valid unit/conflict hint
742        }
743        if unset.is_empty() {
744            return i == hints.len() - 1; // conflict — must be the final hint
745        }
746        if unset.len() == 1 {
747            set_true(&mut assign, unset[0]);
748        } else {
749            return false; // not unit, not conflict ⇒ a bogus hint
750        }
751    }
752    false
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758    use crate::cdcl::Lit;
759    use crate::proof::Perm;
760    use crate::pr::check_pr_refutation;
761
762    fn p(v: u32) -> Lit {
763        Lit::pos(v)
764    }
765    fn n(v: u32) -> Lit {
766        Lit::neg(v)
767    }
768
769    /// The canonical 2-variable all-clauses UNSAT instance, refuted by the resolvents `p`, `¬p`.
770    fn pq_unsat() -> (usize, Vec<Vec<Lit>>, Vec<ProofStep>) {
771        let f = vec![vec![p(0), p(1)], vec![p(0), n(1)], vec![n(0), p(1)], vec![n(0), n(1)]];
772        let steps = vec![ProofStep::Rup(vec![p(0)]), ProofStep::Rup(vec![n(0)])];
773        (2, f, steps)
774    }
775
776    #[test]
777    fn drat_round_trips_through_our_checker() {
778        let (nv, f, steps) = pq_unsat();
779        let text = emit_drat(nv, &f, &steps).expect("emits");
780        let parsed = parse_dpr(&text).expect("parses");
781        assert!(check_pr_refutation(nv, &f, &parsed), "round-tripped DRAT must still refute");
782    }
783
784    #[test]
785    fn drat_rejects_a_corrupted_addition() {
786        // Over a SATISFIABLE base `(a ∨ b)`, a bolted-on unit `¬a` is not entailed (assuming `a`
787        // satisfies the clause with no propagation), so it is not RUP — the parsed-back "proof"
788        // must fail to verify. (In the full 4-clause UNSAT formula every clause is RUP, so the
789        // base must be satisfiable for the corruption to bite.)
790        let f = vec![vec![p(0), p(1)]];
791        let bogus = parse_dpr("-1 0\n").expect("parses"); // the un-entailed unit `¬a`
792        assert!(!check_pr_refutation(2, &f, &bogus));
793    }
794
795    #[test]
796    fn lrat_hints_validate_and_reject_corruption() {
797        let (nv, f, steps) = pq_unsat();
798        let lrat = emit_lrat(nv, &f, &steps).expect("emits LRAT");
799        assert!(check_lrat(nv, &f, &lrat), "our own LRAT checker accepts our hints");
800
801        // Corrupt a hint id → the chain no longer conflicts where claimed → rejected.
802        let mangled = lrat.replace(" 0\n", " 999 0\n");
803        assert!(!check_lrat(nv, &f, &mangled), "a corrupted hint chain must be rejected");
804    }
805
806    #[test]
807    fn lrat_checker_rejects_a_proof_with_no_empty_clause() {
808        let (nv, f, _) = pq_unsat();
809        // A single learned unit `p` with a valid hint, but never closing to the empty clause.
810        let db: Vec<(u64, Vec<Lit>)> =
811            f.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
812        let hints = rup_hints(nv, &db, &[p(0)]).expect("p is RUP");
813        let mut line = String::from("5 1 ");
814        for h in hints {
815            line.push_str(&h.to_string());
816            line.push(' ');
817        }
818        line.push_str("0\n");
819        assert!(!check_lrat(nv, &f, &line), "no empty-clause line ⇒ not a refutation");
820    }
821
822    #[test]
823    fn dpr_assignment_witness_line_round_trips_and_reverifies() {
824        // The textbook model-removing PR clause: F=(a∨b), C=(¬a∨b) with ω={¬a,b}. Emit it as a
825        // DPR witness line, parse it back, and confirm the parsed clause+witness still PR-checks.
826        let (a, b) = (0u32, 1u32);
827        let f = vec![vec![p(a), p(b)]];
828        let c = vec![n(a), p(b)];
829        let omega = vec![n(a), p(b)];
830        let witness = Witness::Assignment(omega);
831        assert!(crate::pr::is_pr(2, &f, &c, &witness));
832
833        // emit_dpr requires a full refutation; here we test just the witness-line machinery, so
834        // hand-format one line and parse it back.
835        let w = try_assignment_witness(2, &f, &c, &witness).expect("reduces");
836        let pivot = c.iter().copied().find(|l| w.contains(l)).unwrap();
837        let mut out = String::new();
838        let mut c_ord = vec![pivot];
839        c_ord.extend(c.iter().copied().filter(|&l| l != pivot));
840        push_clause(&mut out, &c_ord);
841        out.push(' ');
842        let mut w_ord = vec![pivot];
843        w_ord.extend(w.iter().copied().filter(|&l| l != pivot));
844        for &l in &w_ord {
845            out.push_str(&dimacs(l).to_string());
846            out.push(' ');
847        }
848        out.push_str("0\n");
849
850        let parsed = parse_dpr(&out).expect("parses");
851        match &parsed[0] {
852            ProofStep::Pr { clause, witness } => {
853                assert!(crate::pr::is_pr(2, &f, clause, witness), "parsed DPR line re-verifies");
854            }
855            other => panic!("expected a PR step, got {other:?}"),
856        }
857    }
858
859    #[test]
860    fn lrat_certifies_a_real_cdcl_refutation_of_php() {
861        // Not a hand-built trace: the actual learned clauses from our CDCL solver refuting PHP(3),
862        // emitted as LRAT and replayed by our independent (cake_lpr-style) checker.
863        use crate::cdcl::{SolveResult, Solver};
864        let (cnf, _) = crate::families::php(3);
865        let nv = cnf.num_vars;
866        let mut solver = Solver::new(nv);
867        for c in &cnf.clauses {
868            solver.add_clause(c.clone());
869        }
870        let steps: Vec<ProofStep> = match solver.solve() {
871            SolveResult::Unsat => {
872                solver.learned().iter().map(|c| ProofStep::Rup(c.lits.clone())).collect()
873            }
874            SolveResult::Sat(_) => panic!("PHP(3) is UNSAT"),
875        };
876        let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("a real CDCL refutation is RUP → LRAT");
877        assert!(check_lrat(nv, &cnf.clauses, &lrat), "LRAT replay of a real CDCL refutation validates");
878
879        // The same trace also round-trips as DRAT through our checker.
880        let drat = emit_drat(nv, &cnf.clauses, &steps).expect("emits DRAT");
881        let parsed = parse_dpr(&drat).expect("parses");
882        assert!(check_pr_refutation(nv, &cnf.clauses, &parsed));
883    }
884
885    #[test]
886    fn the_universal_lrat_guarantee_via_plain_cdcl() {
887        // The fail-closed fallback is pure RUP, so EVERY UNSAT instance we close carries a
888        // cake_lpr-checkable LRAT proof — independent of any symmetry/PR machinery.
889        let (cnf, _) = crate::families::php(4);
890        let nv = cnf.num_vars;
891        let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
892        let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("plain CDCL ⇒ RUP ⇒ LRAT");
893        assert!(check_lrat(nv, &cnf.clauses, &lrat), "universal LRAT certificate validates");
894    }
895
896    #[test]
897    fn external_sr2drat_drat_trim_certifies_our_php_symmetry_proof() {
898        // The marquee cross-check: our *substitution-redundancy* refutation of PHP(n) — the symmetry
899        // proof Kissat/CaDiCaL time out on — emitted as `.sr`, expanded by Marijn Heule's `sr2drat`,
900        // and VERIFIED by the community's `drat-trim`. Independent of our solver and our SR checker.
901        // Gated on `$SR2DRAT` + `$DRAT_TRIM` so the default suite stays green when the binaries are
902        // absent (mirrors the Z3-optional pattern).
903        let (Ok(sr2drat), Ok(drat_trim)) = (std::env::var("SR2DRAT"), std::env::var("DRAT_TRIM")) else {
904            eprintln!("SR2DRAT/DRAT_TRIM unset — skipping external sr2drat→drat-trim cross-check");
905            return;
906        };
907        for n in [4usize, 8] {
908            let (cnf, _) = crate::families::php(n);
909            let cert = crate::sym_certify::heule_php_refutation(n);
910            assert!(cert.refuted, "PHP({n}) symmetry proof closes internally");
911            let sr = emit_sr(cnf.num_vars, &cnf.clauses, &cert.steps).expect("emits .sr");
912            let cnf_path = temp_write(&format!("logos_sr_php{n}.cnf"), &crate::dimacs::print(&cnf));
913            let sr_path = temp_write(&format!("logos_sr_php{n}.sr"), &sr);
914            let drat_path = cnf_path.with_extension("drat");
915            // sr2drat expands the .sr into a plain DRAT proof (stdout → file).
916            let drat = std::process::Command::new(&sr2drat)
917                .arg(&cnf_path)
918                .arg(&sr_path)
919                .output()
920                .expect("run sr2drat");
921            std::fs::write(&drat_path, &drat.stdout).expect("write expanded DRAT");
922            // drat-trim independently verifies the expansion against the original CNF.
923            let out = std::process::Command::new(&drat_trim)
924                .arg(&cnf_path)
925                .arg(&drat_path)
926                .output()
927                .expect("run drat-trim");
928            let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
929            assert!(
930                s.contains("VERIFIED") && !s.contains("NOT VERIFIED"),
931                "sr2drat→drat-trim must VERIFY our PHP({n}) SR proof; got:\n{s}"
932            );
933        }
934    }
935
936    #[test]
937    fn size_sink_measures_the_sr_proof_lazily_and_matches_the_string_length() {
938        // The benchmarks page reports our OWN proof size. It must be measured by streaming the SR
939        // proof through a byte counter — never materializing a giant String — so the streamed count
940        // equals the real serialized length exactly, and a tight cap aborts emission early
941        // (fail-closed) instead of running to completion: the memory-safety guarantee for a
942        // pathologically large proof.
943        let (cnf, _) = crate::families::php(4);
944        let cert = crate::sym_certify::heule_php_refutation(4);
945        assert!(cert.refuted, "PHP(4) symmetry proof closes internally");
946        let full = emit_sr(cnf.num_vars, &cnf.clauses, &cert.steps).expect("emits .sr");
947        assert!(full.len() > 8, "the SR proof is more than a handful of bytes");
948
949        let mut sink = SizeSink::new(64 << 20);
950        write_sr(&mut sink, cnf.num_vars, &cnf.clauses, &cert.steps).expect("streams .sr");
951        assert!(!sink.overflowed());
952        assert_eq!(sink.bytes(), full.len() as u64, "streamed byte count = serialized length");
953
954        let mut capped = SizeSink::new(8);
955        let err = write_sr(&mut capped, cnf.num_vars, &cnf.clauses, &cert.steps);
956        assert_eq!(err, Err(EmitError::SizeCapExceeded), "a tight cap aborts emission early");
957        assert!(capped.overflowed());
958        assert!(capped.bytes() >= 8, "counted up to the cap before bailing");
959    }
960
961    #[test]
962    fn dpr_emits_or_honestly_declines_the_sdcl_proof() {
963        // The SaDiCaL-grade path: solve a symmetric UNSAT instance with the certified SDCL solver
964        // and export the proof as DPR. Either it is fully PR (a dpr-trim-checkable proof that
965        // round-trips), or a step is irreducibly SR and the emitter declines honestly — never a
966        // bogus line. In both cases the internal certificate holds.
967        use crate::sdcl::{solve_certified, CertifiedOutcome};
968        let (cnf, _) = crate::families::php(3);
969        let nv = cnf.num_vars;
970        match solve_certified(nv, &cnf.clauses) {
971            CertifiedOutcome::Unsat { steps, .. } => {
972                assert!(check_pr_refutation(nv, &cnf.clauses, &steps), "the SDCL proof self-checks");
973                match emit_dpr(nv, &cnf.clauses, &steps) {
974                    Ok(text) => {
975                        let parsed = parse_dpr(&text).expect("parses");
976                        assert!(check_pr_refutation(nv, &cnf.clauses, &parsed), "round-tripped DPR refutes");
977                    }
978                    Err(EmitError::RequiresSubstitutionRedundancy { .. }) => {}
979                    Err(e) => panic!("unexpected emit error: {e:?}"),
980                }
981            }
982            CertifiedOutcome::Sat(_) => panic!("PHP(3) is UNSAT"),
983        }
984    }
985
986    /// Write `text` to a uniquely-named temp file and return its path.
987    fn temp_write(name: &str, text: &str) -> std::path::PathBuf {
988        let path = std::env::temp_dir().join(name);
989        std::fs::write(&path, text).expect("write temp file");
990        path
991    }
992
993    #[test]
994    fn external_drat_trim_accepts_our_drat_proof() {
995        // Gold-standard cross-check: the SAT community's own `drat-trim` must accept the DRAT we
996        // emit for a real CDCL refutation of PHP(5). Gated on `$DRAT_TRIM` so the default suite
997        // stays green when the binary is absent (mirrors the Z3-optional pattern).
998        let Ok(bin) = std::env::var("DRAT_TRIM") else {
999            eprintln!("DRAT_TRIM unset — skipping external drat-trim cross-check");
1000            return;
1001        };
1002        let (cnf, _) = crate::families::php(5);
1003        let nv = cnf.num_vars;
1004        let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
1005        let drat = emit_drat(nv, &cnf.clauses, &steps).expect("emits DRAT");
1006        let cnf_path = temp_write("logos_drat_php5.cnf", &crate::dimacs::print(&cnf));
1007        let drat_path = temp_write("logos_drat_php5.drat", &drat);
1008        let out = std::process::Command::new(&bin)
1009            .arg(&cnf_path)
1010            .arg(&drat_path)
1011            .output()
1012            .expect("run drat-trim");
1013        let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
1014        assert!(s.contains("VERIFIED") && !s.contains("NOT VERIFIED"), "drat-trim must VERIFY our DRAT; got:\n{s}");
1015    }
1016
1017    #[test]
1018    fn external_lrat_check_accepts_our_lrat_proof() {
1019        // The formally-verified-style checker `lrat-check` must accept the hint-carrying LRAT we
1020        // synthesise for a real CDCL refutation of PHP(5). Gated on `$LRAT_CHECK`.
1021        let Ok(bin) = std::env::var("LRAT_CHECK") else {
1022            eprintln!("LRAT_CHECK unset — skipping external lrat-check cross-check");
1023            return;
1024        };
1025        let (cnf, _) = crate::families::php(5);
1026        let nv = cnf.num_vars;
1027        let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
1028        let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("emits LRAT");
1029        let cnf_path = temp_write("logos_lrat_php5.cnf", &crate::dimacs::print(&cnf));
1030        let lrat_path = temp_write("logos_lrat_php5.lrat", &lrat);
1031        let out = std::process::Command::new(&bin)
1032            .arg(&cnf_path)
1033            .arg(&lrat_path)
1034            .output()
1035            .expect("run lrat-check");
1036        let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
1037        assert!(s.contains("VERIFIED") && !s.contains("NOT VERIFIED"), "lrat-check must accept our LRAT; got:\n{s}");
1038    }
1039
1040    #[test]
1041    fn try_assignment_witness_is_sound_on_php_substitutions() {
1042        // Over the real PHP crusher's substitution steps: whenever the reducer hands back an
1043        // assignment witness, pr::is_pr must independently bless it (the reducer never invents an
1044        // unsound witness). We also report how many genuinely reduce vs. stay irreducibly SR.
1045        let cr = crate::sym_certify::heule_php_refutation(4);
1046        let (cnf, _) = crate::families::php(4);
1047        let nv = cnf.num_vars;
1048        let mut db = cnf.clauses.clone();
1049        let mut reduced = 0usize;
1050        let mut sr_only = 0usize;
1051        for step in &cr.steps {
1052            match step {
1053                ProofStep::Pr { clause, witness } => {
1054                    if let Some(w) = try_assignment_witness(nv, &db, clause, witness) {
1055                        assert!(
1056                            crate::pr::is_pr(nv, &db, clause, &Witness::Assignment(w)),
1057                            "the reducer returned an unsound assignment witness"
1058                        );
1059                        reduced += 1;
1060                    } else {
1061                        sr_only += 1;
1062                    }
1063                    db.push(clause.clone());
1064                }
1065                ProofStep::Rup(c) => db.push(c.clone()),
1066                ProofStep::Delete(c) => {
1067                    let key = canon(c);
1068                    if let Some(pos) = db.iter().position(|d| canon(d) == key) {
1069                        db.swap_remove(pos);
1070                    }
1071                }
1072            }
1073        }
1074        // The honest classification: PHP's swap clauses are SR. The test's contract is soundness
1075        // of the reducer, not that everything reduces — but it must have actually run on PR steps.
1076        assert!(reduced + sr_only > 0, "the PHP refutation must contain PR steps to classify");
1077        eprintln!("PHP(4): {reduced} steps reduced to PR, {sr_only} remained irreducibly SR");
1078    }
1079}