code wiki / (root) / nx_saturation.nx

nx_saturation.nx source

↩ module page · 661 lines · 27486 B

1// nx_saturation.nx -- given-clause saturation loop with factoring. 2// 3// Per user 2026-05-14 "keep going till we can submit to casc". This 4// is the proof-search engine on top of unify + resolution. Given a 5// set of clauses (typically the negation of a conjecture), iterate 6// resolution + factoring until either: 7// - the empty clause is derived -> UNSAT (proof of original conjecture) 8// - no new clauses can be derived -> UNKNOWN / SAT 9// - resource budget exhausted -> TIMEOUT 10// 11// Discovery-Otter-Vampire-E-SPASS use this same skeleton; their 12// performance differences come from clause selection heuristic + 13// indexing + subsumption. 14 15// nx_safety_envelope: 16// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 17// sil_target: SIL1 18// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 19// verdict: NOT_YET_EVALUATED 20 21import "nx_syscalls.nx" 22import "nx_runtime.nx" 23import "nx_tier.nx" 24import "nx_result.nx" 25import "nx_unify.nx" 26import "nx_resolution.nx" 27import "nx_subsumption.nx" 28import "nx_tautology.nx" 29import "nx_disctree.nx" 30import "nx_paramodulation.nx" 31const NX_MAGIC_9223372036854775807: i64 = 9223372036854775807 32 33// ===== Factoring rule =============================================== 34// Factoring: within ONE clause, if two literals can be unified, we 35// can collapse them. Example: {p(x), p(a)} -> {p(a)} after 36// applying {x -> a}. 37// Returns Result<n_factored_lits, NX_ERR_*>. Builds the factored 38// clause in c_out. 39func nx_factor(c: *Clause, i: nx_int, j: nx_int, c_out: *Clause) -> *NxResult { 40 if i < 0 { return nx_result_err(NX_ERR_OUT_OF_RANGE) } 41 if j < 0 { return nx_result_err(NX_ERR_OUT_OF_RANGE) } 42 if i == j { return nx_result_err(NX_ERR_INVALID_INPUT) } 43 if i >= c.n_lits { return nx_result_err(NX_ERR_OUT_OF_RANGE) } 44 if j >= c.n_lits { return nx_result_err(NX_ERR_OUT_OF_RANGE) } 45 46 let l1: *Literal = nx_clause_lit_at(c, i) 47 let l2: *Literal = nx_clause_lit_at(c, j) 48 49 // Same sign required for factoring 50 if l1.sign != l2.sign { return nx_result_err(NX_ERR_TAG_MISMATCH) } 51 52 // Try to unify the atoms 53 let s: *Subst = nx_subst_new() 54 let r_un: *NxResult = nx_unify(l1.atom, l2.atom, s) 55 if nx_result_is_err(r_un) == 1 { return r_un } 56 57 // Build factored clause: drop literal j, apply subst to the rest 58 var k: nx_int = 0 59 while k < c.n_lits { 60 if k != j { 61 let lk: *Literal = nx_clause_lit_at(c, k) 62 let lk_subst: *Literal = nx_lit_apply_subst(lk, s) 63 let r: *NxResult = nx_clause_add(c_out, lk_subst) 64 if nx_result_is_err(r) == 1 { return r } 65 } 66 k = k + 1 67 } 68 return nx_result_ok(c_out.n_lits) 69} 70 71// ===== Given-clause saturation loop ================================= 72// 73// State: processed clauses + unprocessed clauses + step budget. 74// Each iteration: pick the oldest unprocessed clause (FIFO = "given"); 75// try to resolve it against every processed clause; add the 76// resolvents to unprocessed; if empty clause derived, halt with UNSAT. 77// 78// Discipline: bounded budget (steps); honest Result on timeout. 79 80const NX_SAT_MAX_PROCESSED: nx_int = 256 81const NX_SAT_MAX_UNPROCESSED: nx_int = 1024 82 83struct Saturation { 84 processed: *Clause, // flat array of Clause structs 85 n_processed: nx_int, 86 unprocessed: *Clause, 87 n_unproc: nx_int, 88 head_unproc: nx_int, // FIFO head index (used by nx_sat_pick_given) 89 budget: nx_int, // remaining inference steps 90 picked: *nx_int, // [NX_SAT_MAX_UNPROCESSED] -- 1 if consumed 91 // by a non-FIFO picker 92 // (nx_sat_pick_given_best_first) 93 proc_index: *DiscTree, // discrim tree indexing processed 94 // clauses' literal atoms; values 95 // are clause indices into processed[] 96 proc_deleted: *nx_int, // [NX_SAT_MAX_PROCESSED] -- tombstone 97 // bit per processed slot. Backward 98 // simp marks deleted=1 instead of 99 // compacting; iteration skips 100 // tombstoned slots; discrim tree 101 // stays valid without rebuild. 102} 103 104const NX_SAT_BYTES: nx_int = 72 105 106func nx_saturation_new(initial_budget: nx_int) -> *Saturation { 107 let s: *Saturation = (sys_mmap(NX_SAT_BYTES as i64)) as *Saturation 108 s.processed = (sys_mmap((NX_SAT_MAX_PROCESSED * NX_CLAUSE_BYTES) as i64)) as *Clause 109 s.n_processed = 0 110 s.unprocessed = (sys_mmap((NX_SAT_MAX_UNPROCESSED * NX_CLAUSE_BYTES) as i64)) as *Clause 111 s.n_unproc = 0 112 s.head_unproc = 0 113 s.budget = initial_budget 114 s.picked = (sys_mmap((NX_SAT_MAX_UNPROCESSED * 8) as i64)) as *nx_int 115 s.proc_index = nx_dt_new() 116 s.proc_deleted = (sys_mmap((NX_SAT_MAX_PROCESSED * 8) as i64)) as *nx_int 117 return s 118} 119 120// Forward declaration: nx_sat_add_unproc_filtered (defined further 121// down) calls nx_sat_proc_subsumes_indexed (defined further down 122// still). nxc2 supports forward decls via the `func name(...) -> T;` 123// syntax (parse.c line 1971). Type *Saturation must be declared 124// first -- which it is, just above. 125func nx_sat_proc_subsumes_indexed(s: *Saturation, c: *Clause) -> nx_int; 126// nx_sat_run_discount_lrs (LRS variant) calls nx_sat_rebuild_index 127// which lives further down. 128func nx_sat_rebuild_index(s: *Saturation); 129 130// Get the i-th unprocessed clause (FIFO ordered). 131func nx_sat_unproc_at(s: *Saturation, i: nx_int) -> *Clause { 132 return ((s.unprocessed as nx_int) + (i * NX_CLAUSE_BYTES)) as *Clause 133} 134 135func nx_sat_proc_at(s: *Saturation, i: nx_int) -> *Clause { 136 return ((s.processed as nx_int) + (i * NX_CLAUSE_BYTES)) as *Clause 137} 138 139// Add a clause to the unprocessed queue. 140func nx_sat_add_unproc(s: *Saturation, c: *Clause) -> *NxResult { 141 if s.n_unproc >= NX_SAT_MAX_UNPROCESSED { return nx_result_err(NX_ERR_OVERFLOW) } 142 let slot: *Clause = nx_sat_unproc_at(s, s.n_unproc) 143 slot.n_lits = c.n_lits 144 slot.lits = c.lits 145 s.n_unproc = s.n_unproc + 1 146 return nx_result_ok(s.n_unproc) 147} 148 149// Pick the next given clause (FIFO). Returns the clause + advances head. 150func nx_sat_pick_given(s: *Saturation) -> *Clause { 151 if s.head_unproc >= s.n_unproc { return 0 as *Clause } 152 let c: *Clause = nx_sat_unproc_at(s, s.head_unproc) 153 s.head_unproc = s.head_unproc + 1 154 return c 155} 156 157// Move given to processed. 158func nx_sat_move_to_processed(s: *Saturation, given: *Clause) -> *NxResult { 159 if s.n_processed >= NX_SAT_MAX_PROCESSED { return nx_result_err(NX_ERR_OVERFLOW) } 160 let slot: *Clause = nx_sat_proc_at(s, s.n_processed) 161 slot.n_lits = given.n_lits 162 slot.lits = given.lits 163 s.n_processed = s.n_processed + 1 164 return nx_result_ok(s.n_processed) 165} 166 167// Try to resolve `given` against `other` on every complementary literal pair. 168// Adds any resolvents to unprocessed. Returns NX_VERDICT_UNSAT (= 2) on 169// empty-clause derivation; NX_VERDICT_UNKNOWN otherwise. 170const NX_SAT_VERDICT_UNSAT: nx_int = 2 171const NX_SAT_VERDICT_UNKNOWN: nx_int = 5 172 173func nx_sat_try_resolve_pair(s: *Saturation, given: *Clause, other: *Clause) -> nx_int { 174 var i: nx_int = 0 175 while i < given.n_lits { 176 var j: nx_int = 0 177 while j < other.n_lits { 178 let resolvent: *Clause = nx_clause_new() 179 let r: *NxResult = nx_resolve(given, i, other, j, resolvent) 180 if nx_result_is_ok(r) == 1 { 181 if nx_clause_is_empty(resolvent) == 1 { return NX_SAT_VERDICT_UNSAT } 182 let _add: *NxResult = nx_sat_add_unproc(s, resolvent) 183 } 184 j = j + 1 185 } 186 i = i + 1 187 } 188 return NX_SAT_VERDICT_UNKNOWN 189} 190 191// Main saturation loop. Returns verdict. 192func nx_sat_run(s: *Saturation) -> nx_int { 193 while s.budget > 0 { 194 let given: *Clause = nx_sat_pick_given(s) 195 if (given as nx_int) == 0 { return NX_SAT_VERDICT_UNKNOWN } // queue exhausted 196 197 // Resolve given against every processed 198 var p: nx_int = 0 199 while p < s.n_processed { 200 let other: *Clause = nx_sat_proc_at(s, p) 201 let v: nx_int = nx_sat_try_resolve_pair(s, given, other) 202 if v == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 203 p = p + 1 204 } 205 206 // Self-resolve given against itself (covers tautologies + self-contradictions) 207 let v2: nx_int = nx_sat_try_resolve_pair(s, given, given) 208 if v2 == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 209 210 // Move given to processed 211 let _: *NxResult = nx_sat_move_to_processed(s, given) 212 s.budget = s.budget - 1 213 } 214 return NX_SAT_VERDICT_UNKNOWN 215} 216 217// ===== Discount-loop strategy ======================================= 218// 219// The DISCOUNT loop differs from the OTTER loop above by aggressively 220// pruning clauses through forward subsumption + tautology deletion at 221// every insertion point. Vampire's default mode. Trades a small 222// per-clause cost (subsumption check) for a much smaller passive set, 223// which dominates total runtime on non-trivial CASC problems. 224// 225// What's pruned: 226// - Tautological clauses (drop on insertion) 227// - Clauses subsumed by any processed (drop on insertion + on pick) 228// 229// Backward simplification (deleting subsumed processed clauses when a 230// new general clause is added) is deferred -- requires array compaction 231// over `processed`, which is a Phase 2 follow-up. 232// 233// eq_sym: caller-provided symbol id of the equality predicate (for 234// tautology check). Pass any unused sym_id when equations aren't in 235// play -- the reflexive-equality branch will simply never fire. 236 237// True iff some processed clause subsumes c. Skips tombstoned slots 238// (proc_deleted=1) so callers using this linear variant get the same 239// semantic answer as the indexed variant. 240func nx_sat_proc_subsumes(s: *Saturation, c: *Clause) -> nx_int { 241 var i: nx_int = 0 242 while i < s.n_processed { 243 if s.proc_deleted[i] == 0 { 244 let other: *Clause = nx_sat_proc_at(s, i) 245 if nx_subsumes(other, c) == NX_SUBSUMES_YES { return 1 } 246 } 247 i = i + 1 248 } 249 return 0 250} 251 252// Dedup duplicate literals in-place (return new clause). Used on 253// resolvents before they enter unprocessed -- without dedup, resolvents 254// like {q, q} accumulate and overflow the queue without ever closing. 255func nx_sat_clause_dedup(c: *Clause) -> *Clause { 256 let out: *Clause = nx_clause_new() 257 var i: nx_int = 0 258 while i < c.n_lits { 259 let li: *Literal = nx_clause_lit_at(c, i) 260 var dup: nx_int = 0 261 var j: nx_int = 0 262 while j < out.n_lits { 263 let lj: *Literal = nx_clause_lit_at(out, j) 264 if li.sign == lj.sign { 265 if nx_term_eq(li.atom, lj.atom) == 1 { dup = 1 } 266 } 267 j = j + 1 268 } 269 if dup == 0 { 270 let _r: *NxResult = nx_clause_add(out, li) 271 } 272 i = i + 1 273 } 274 return out 275} 276 277// Tautology + forward-subsumption gate before adding to unprocessed. 278// Returns 1 if added, 0 if filtered, NX_ERR_OVERFLOW on capacity. 279// 280// Uses indexed subsumption (proc_index discrim tree) for the candidate 281// lookup -- replaces the O(n_processed) linear scan with an O(d) trie 282// walk. Same semantic answer; faster on large processed sets. 283// 284// Dedups literals before insertion so resolvent shapes like {q, q} get 285// reduced to {q} (without this, the discount loop overflows on 286// problems whose resolvents naturally produce duplicates -- e.g. 287// pel009 hit the 1024-clause unprocessed cap without it). 288func nx_sat_add_unproc_filtered(s: *Saturation, c: *Clause, eq_sym: nx_int) -> nx_int { 289 let dedup_c: *Clause = nx_sat_clause_dedup(c) 290 if nx_is_tautology(dedup_c, eq_sym) == NX_TAUTOLOGY { return 0 } 291 if nx_sat_proc_subsumes_indexed(s, dedup_c) == 1 { return 0 } 292 let r: *NxResult = nx_sat_add_unproc(s, dedup_c) 293 if nx_result_is_err(r) == 1 { return 0 - NX_ERR_OVERFLOW } 294 return 1 295} 296 297// Paramodulate eq_clause into target_clause: for each positive 298// equality literal in eq_clause and each literal in target_clause, 299// attempt paramodulation and filter the result through the discount 300// gate. Returns NX_SAT_VERDICT_UNSAT if a paramodulant turns out 301// empty; NX_SAT_VERDICT_UNKNOWN otherwise. 302func nx_sat_try_paramodulate_pair_filtered(s: *Saturation, eq_clause: *Clause, 303 target_clause: *Clause, eq_sym: nx_int) -> nx_int { 304 var i: nx_int = 0 305 while i < eq_clause.n_lits { 306 let li: *Literal = nx_clause_lit_at(eq_clause, i) 307 // Only positive equality literals can be the rewriting source. 308 if li.sign == NX_LIT_POS { 309 if li.atom.kind == NX_TERM_APP { 310 if li.atom.sym == eq_sym { 311 if li.atom.n_args == 2 { 312 var j: nx_int = 0 313 while j < target_clause.n_lits { 314 let resolvent: *Clause = nx_clause_new() 315 let r: *NxResult = nx_paramodulate(eq_clause, i, 316 target_clause, j, eq_sym, resolvent) 317 if nx_result_is_ok(r) == 1 { 318 if nx_clause_is_empty(resolvent) == 1 { 319 return NX_SAT_VERDICT_UNSAT 320 } 321 let _add: nx_int = nx_sat_add_unproc_filtered(s, resolvent, eq_sym) 322 } 323 j = j + 1 324 } 325 } 326 } 327 } 328 } 329 i = i + 1 330 } 331 return NX_SAT_VERDICT_UNKNOWN 332} 333 334// Resolve given x other, filtering each resolvent through the discount 335// gate before it lands in unprocessed. 336func nx_sat_try_resolve_pair_filtered(s: *Saturation, given: *Clause, 337 other: *Clause, eq_sym: nx_int) -> nx_int { 338 var i: nx_int = 0 339 while i < given.n_lits { 340 var j: nx_int = 0 341 while j < other.n_lits { 342 let resolvent: *Clause = nx_clause_new() 343 let r: *NxResult = nx_resolve(given, i, other, j, resolvent) 344 if nx_result_is_ok(r) == 1 { 345 if nx_clause_is_empty(resolvent) == 1 { return NX_SAT_VERDICT_UNSAT } 346 let _add: nx_int = nx_sat_add_unproc_filtered(s, resolvent, eq_sym) 347 } 348 j = j + 1 349 } 350 i = i + 1 351 } 352 return NX_SAT_VERDICT_UNKNOWN 353} 354 355// Backward subsumption: remove every processed clause that the 356// freshly-added clause `newest` subsumes. Marks subsumed slots 357// with proc_deleted=1 (tombstones) instead of array compaction. 358// Tombstoning preserves clause indices so the discrim tree's stored 359// values remain valid -- no rebuild required, eliminating the 360// O(n_processed) hot-loop step that blew up qemu memory on 361// pel012-class problems. Iteration over processed[] elsewhere 362// must skip slots where proc_deleted == 1. 363// 364// Closes Phase 1's deferred backward-simplification axis. Combined 365// with forward subsumption, this is the two-sided pruning Vampire 366// uses to keep the processed set minimal. 367func nx_sat_backward_subsume(s: *Saturation, newest: *Clause) -> nx_int { 368 var removed: nx_int = 0 369 var i: nx_int = 0 370 while i < s.n_processed { 371 if s.proc_deleted[i] == 0 { 372 let other: *Clause = nx_sat_proc_at(s, i) 373 // Don't subsume `newest` against itself. 374 if (other as nx_int) != (newest as nx_int) { 375 if nx_subsumes(newest, other) == NX_SUBSUMES_YES { 376 s.proc_deleted[i] = 1 377 // Also delete this clause's discrim-tree entries so 378 // the index doesn't grow monotonically with 379 // tombstoned values. Removes the OOM that pel012- 380 // class (deeply-Tseitined) problems hit otherwise. 381 nx_dt_delete_value(s.proc_index, i) 382 removed = removed + 1 383 } 384 } 385 } 386 i = i + 1 387 } 388 return removed 389} 390 391// ===== Discrim-tree-backed forward subsumption ==================== 392// Insert a clause's literal atoms into proc_index. Each literal's 393// atom is indexed with the given clause_idx as its associated value; 394// duplicate literals (same atom) just create extra entries -- dedup 395// happens at lookup time via the seen[] bitmap. 396// 397// Caller invokes this AFTER nx_sat_move_to_processed (clause_idx = 398// s.n_processed - 1). The integration into nx_sat_run_discount is a 399// separate refactor; this primitive ships standalone for now. 400func nx_sat_index_clause(s: *Saturation, clause_idx: nx_int) { 401 if clause_idx < 0 { return } 402 if clause_idx >= s.n_processed { return } 403 let c: *Clause = nx_sat_proc_at(s, clause_idx) 404 var i: nx_int = 0 405 while i < c.n_lits { 406 let l: *Literal = nx_clause_lit_at(c, i) 407 nx_dt_insert(s.proc_index, l.atom, clause_idx) 408 i = i + 1 409 } 410} 411 412// Indexed forward-subsumption check: query proc_index with c[0]'s atom 413// to get a candidate set of processed clauses, dedup, then verify each 414// candidate via the full nx_subsumes check. Returns 1 if any 415// processed clause subsumes c, else 0. Same semantic answer as the 416// linear nx_sat_proc_subsumes, with O(d) lookup vs O(n_processed) 417// scan. 418// 419// Pre-condition: nx_sat_index_clause has been called for every clause 420// currently in processed[]. Backward-simp invalidation is the caller's 421// responsibility (rebuild the index after compacting processed if you 422// want to keep this primitive sound under that pattern). 423func nx_sat_proc_subsumes_indexed(s: *Saturation, c: *Clause) -> nx_int { 424 if c.n_lits == 0 { return 0 } 425 let candidates: *nx_int = (sys_mmap((NX_DT_MAX_RESULTS * 8) as i64)) as *nx_int 426 let n_cand_p: *nx_int = (sys_mmap(8)) as *nx_int 427 n_cand_p[0] = 0 428 let first: *Literal = nx_clause_lit_at(c, 0) 429 nx_dt_find_generalizations(s.proc_index, first.atom, candidates, n_cand_p) 430 431 // Dedup via a fixed bitmap (size NX_SAT_MAX_PROCESSED). Skip 432 // tombstoned (proc_deleted=1) candidates -- the discrim tree may 433 // still hold their entries since we don't delete on tombstone, 434 // but they're no longer live. 435 let seen: *nx_int = (sys_mmap((NX_SAT_MAX_PROCESSED * 8) as i64)) as *nx_int 436 var i: nx_int = 0 437 while i < n_cand_p[0] { 438 let idx: nx_int = candidates[i] 439 if idx >= 0 { 440 if idx < NX_SAT_MAX_PROCESSED { 441 if seen[idx] == 0 { 442 seen[idx] = 1 443 if s.proc_deleted[idx] == 0 { 444 let d: *Clause = nx_sat_proc_at(s, idx) 445 if nx_subsumes(d, c) == NX_SUBSUMES_YES { return 1 } 446 } 447 } 448 } 449 } 450 i = i + 1 451 } 452 return 0 453} 454 455// ===== Clause weight + best-first picking ========================= 456// Symbolic weight of a term: 1 + sum of weights of children. Variables 457// count as 1. Total clause weight = sum over literals. Used as a 458// best-first heuristic for picking from the passive set -- lighter 459// clauses are typically more general and faster to process. 460func nx_clause_weight_term(t: *Term) -> nx_int { 461 if t.kind == NX_TERM_VAR { return 1 } 462 if t.kind == NX_TERM_CONST { return 1 } 463 var w: nx_int = 1 464 var i: nx_int = 0 465 while i < t.n_args { 466 w = w + nx_clause_weight_term(nx_term_arg(t, i)) 467 i = i + 1 468 } 469 return w 470} 471 472func nx_clause_weight(c: *Clause) -> nx_int { 473 var w: nx_int = 0 474 var i: nx_int = 0 475 while i < c.n_lits { 476 let l: *Literal = nx_clause_lit_at(c, i) 477 w = w + nx_clause_weight_term(l.atom) 478 i = i + 1 479 } 480 return w 481} 482 483// Pick the lightest unpicked unprocessed clause. Marks it picked so 484// the next call won't see it again. Returns null if all consumed. 485// 486// O(n_unproc) per call -- a real heap would amortize but the linear 487// scan is acceptable for CASC-Easy problem sizes. Caller uses this 488// instead of (not in addition to) nx_sat_pick_given. 489func nx_sat_pick_given_best_first(s: *Saturation) -> *Clause { 490 var best: nx_int = 0 - 1 491 var best_w: nx_int = NX_MAGIC_9223372036854775807 // i64 max 492 var i: nx_int = 0 493 while i < s.n_unproc { 494 if s.picked[i] == 0 { 495 let c: *Clause = nx_sat_unproc_at(s, i) 496 let w: nx_int = nx_clause_weight(c) 497 if w < best_w { best_w = w; best = i } 498 } 499 i = i + 1 500 } 501 if best < 0 { return 0 as *Clause } 502 s.picked[best] = 1 503 return nx_sat_unproc_at(s, best) 504} 505 506// Pick the oldest unpicked unprocessed clause (FIFO over picked set). 507// Used as the "age" half of the LRS picker. 508func nx_sat_pick_given_oldest_unpicked(s: *Saturation) -> *Clause { 509 var i: nx_int = 0 510 while i < s.n_unproc { 511 if s.picked[i] == 0 { 512 s.picked[i] = 1 513 return nx_sat_unproc_at(s, i) 514 } 515 i = i + 1 516 } 517 return 0 as *Clause 518} 519 520// LRS (Limited Resource Strategy) picker -- Vampire's signature. 521// Alternates between weight-based (lightest unpicked) and age-based 522// (oldest unpicked) picks per a counter modulo (ratio + 1). 523// 524// counter == 0 (mod ratio+1) -> age-based pick 525// otherwise -> weight-based pick 526// 527// ratio=1: alternates 1:1 (light, age, light, age, ...) 528// ratio=5: 5 light then 1 age repeating (more aggressive best-first) 529// 530// Caller-supplied counter so the strategy works without growing the 531// Saturation struct further. 532func nx_sat_pick_given_lrs(s: *Saturation, counter: nx_int, ratio: nx_int) -> *Clause { 533 let div: nx_int = ratio + 1 534 let phase: nx_int = counter - ((counter / div) * div) 535 if phase == 0 { return nx_sat_pick_given_oldest_unpicked(s) } 536 return nx_sat_pick_given_best_first(s) 537} 538 539// LRS variant of the discount loop. Uses LRS picker; otherwise 540// identical pruning + inference paths. 541func nx_sat_run_discount_lrs(s: *Saturation, eq_sym: nx_int, ratio: nx_int) -> nx_int { 542 var counter: nx_int = 0 543 while s.budget > 0 { 544 let given: *Clause = nx_sat_pick_given_lrs(s, counter, ratio) 545 if (given as nx_int) == 0 { return NX_SAT_VERDICT_UNKNOWN } 546 counter = counter + 1 547 548 if nx_is_tautology(given, eq_sym) == NX_TAUTOLOGY { 549 s.budget = s.budget - 1 550 continue 551 } 552 if nx_sat_proc_subsumes_indexed(s, given) == 1 { 553 s.budget = s.budget - 1 554 continue 555 } 556 557 let v_self: nx_int = nx_sat_try_resolve_pair_filtered(s, given, given, eq_sym) 558 if v_self == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 559 560 var p: nx_int = 0 561 while p < s.n_processed { 562 if s.proc_deleted[p] == 0 { 563 let other: *Clause = nx_sat_proc_at(s, p) 564 let v: nx_int = nx_sat_try_resolve_pair_filtered(s, given, other, eq_sym) 565 if v == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 566 let vp1: nx_int = nx_sat_try_paramodulate_pair_filtered(s, given, other, eq_sym) 567 if vp1 == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 568 let vp2: nx_int = nx_sat_try_paramodulate_pair_filtered(s, other, given, eq_sym) 569 if vp2 == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 570 } 571 p = p + 1 572 } 573 574 let _: *NxResult = nx_sat_move_to_processed(s, given) 575 nx_sat_index_clause(s, s.n_processed - 1) 576 // Backward subsume tombstones removed slots (no compaction); 577 // discrim tree stays valid -- no rebuild needed. The 578 // tombstone path is O(removed) instead of O(n_processed). 579 let _bk: nx_int = nx_sat_backward_subsume(s, nx_sat_proc_at(s, s.n_processed - 1)) 580 s.budget = s.budget - 1 581 } 582 return NX_SAT_VERDICT_UNKNOWN 583} 584 585// Rebuild proc_index from scratch. Kept as a utility for callers 586// that want a fresh index after manual processed-array manipulation 587// outside the discount loop. Internal callers (the loops above) use 588// the tombstone path instead. 589func nx_sat_rebuild_index(s: *Saturation) { 590 s.proc_index = nx_dt_new() 591 var i: nx_int = 0 592 while i < s.n_processed { 593 if s.proc_deleted[i] == 0 { 594 nx_sat_index_clause(s, i) 595 } 596 i = i + 1 597 } 598} 599 600// Discount-loop entry: gate given itself + each new resolvent through 601// tautology + forward subsumption. After moving given to processed, 602// run backward subsumption so any older now-redundant clauses are 603// dropped immediately. 604// 605// Forward subsumption uses the discrim-tree-backed lookup 606// (nx_sat_proc_subsumes_indexed); proc_index is updated after every 607// move_to_processed and rebuilt after any backward-simp removal so 608// indexed lookups stay sound. 609func nx_sat_run_discount(s: *Saturation, eq_sym: nx_int) -> nx_int { 610 while s.budget > 0 { 611 let given: *Clause = nx_sat_pick_given(s) 612 if (given as nx_int) == 0 { return NX_SAT_VERDICT_UNKNOWN } 613 614 // Forward-prune given itself. If pruned, charge a step and skip 615 // the inference work -- still consumes budget so we don't loop 616 // forever on a queue full of tautologies. 617 if nx_is_tautology(given, eq_sym) == NX_TAUTOLOGY { 618 s.budget = s.budget - 1 619 continue 620 } 621 if nx_sat_proc_subsumes_indexed(s, given) == 1 { 622 s.budget = s.budget - 1 623 continue 624 } 625 626 // Self-resolve, then resolve against each processed clause. 627 let v_self: nx_int = nx_sat_try_resolve_pair_filtered(s, given, given, eq_sym) 628 if v_self == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 629 630 var p: nx_int = 0 631 while p < s.n_processed { 632 if s.proc_deleted[p] == 0 { 633 let other: *Clause = nx_sat_proc_at(s, p) 634 let v: nx_int = nx_sat_try_resolve_pair_filtered(s, given, other, eq_sym) 635 if v == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 636 637 // Paramodulation: try given as eq-source into other, AND 638 // other as eq-source into given. Both directions because 639 // the discount loop fires given against existing actives, 640 // and equality rewriting may apply either way. 641 let vp1: nx_int = nx_sat_try_paramodulate_pair_filtered(s, given, other, eq_sym) 642 if vp1 == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 643 let vp2: nx_int = nx_sat_try_paramodulate_pair_filtered(s, other, given, eq_sym) 644 if vp2 == NX_SAT_VERDICT_UNSAT { return NX_SAT_VERDICT_UNSAT } 645 } 646 p = p + 1 647 } 648 649 let _: *NxResult = nx_sat_move_to_processed(s, given) 650 // Index the freshly-added clause for future indexed lookups. 651 nx_sat_index_clause(s, s.n_processed - 1) 652 653 // Backward simplification: tombstones old processed clauses 654 // that `given` subsumes (no array compaction). Discrim tree 655 // stays valid; no rebuild needed. 656 let _bk: nx_int = nx_sat_backward_subsume(s, nx_sat_proc_at(s, s.n_processed - 1)) 657 658 s.budget = s.budget - 1 659 } 660 return NX_SAT_VERDICT_UNKNOWN 661}