Resolution: Proof by Refutation
in progressThis lesson is still being built - content, demos, and checkpoints may change before release.
in progressThis lesson is still being built - content, demos, and checkpoints may change before release.
resolution-prover.ts
import type { Tracer } from "../trace";import { complementary, dedupe, isTautologous, sameLiteral, type Clause } from "./clauses";export type ResolutionStrategy = "breadth" | "support" | "unit";export interface ResolutionStep { left: number; right: number; /** The variable resolved on. */ on: string; resolvent: Clause; index: number;}export interface ResolutionResult { proved: boolean; /** Every clause in the set at the end: premises, the negated goal, and the resolvents, in index order. */ clauses: Clause[]; steps: ResolutionStep[]; /** How many clauses came from the negated goal. */ goalClauses: number;}/** The most clauses a run may hold before it stops: the stage's problems resolve long before it. */export const MAX_RESOLUTION_CLAUSES = 60;/** * Resolution refutation for propositional clauses. To prove that the * premises entail a goal, add the goal's negation to the clause set * and resolve pairs: two clauses holding a variable and its negation * combine into one holding everything else, which follows from both. * Deriving the empty clause, which nothing satisfies, shows the set * is contradictory, so the goal was entailed; a set that stops * producing new clauses without it does not entail the goal. One rule * replaces every inference rule of the truth-table lessons, and it is * complete: if the goal follows, the empty clause is reachable. The * strategy orders the pairs: breadth first over the set, set of * support (one parent must descend from the negated goal), or unit * preference (a one-literal parent first). * * Time: exponential in the worst case. Space: O(clauses). */export class ResolutionProver { constructor( private readonly tracer?: Tracer, private readonly strategy: ResolutionStrategy = "breadth", ) {} entails(premises: readonly Clause[], negatedGoal: readonly Clause[]): ResolutionResult { const clauses: Clause[] = premises.map(dedupe); const sos = new Set<number>(); const setOfSupport = new Set<number>(); negatedGoal.forEach((clause) => { clauses.push(dedupe(clause)); sos.add(clauses.length - 1); setOfSupport.add(clauses.length - 1); // ▸ assume the opposite of the goal, as a clause this.tracer?.emit("negate-goal", { index: clauses.length - 1, clause: clauses[clauses.length - 1]!.map((l) => ({ ...l })), count: negatedGoal.length }); }); const steps: ResolutionStep[] = []; const tried = new Set<string>(); const triedPairs = new Set<string>(); for (;;) { const pair = this.nextPair(clauses, tried, sos); const pair = this.nextPair(clauses, triedPairs, setOfSupport); if (!pair) break; const [i, j] = pair; tried.add(`${i},${j}`); triedPairs.add(`${i},${j}`); const a = clauses[i]!; const b = clauses[j]!; for (const literal of a) { const match = b.find((other) => complementary(literal, other)); if (!match) continue; const resolvent = dedupe([...a.filter((l) => !sameLiteral(l, literal)), ...b.filter((l) => !sameLiteral(l, match))]); if (isTautologous(resolvent)) { // ▸ the new clause holds a variable and its negation, so it says nothing this.tracer?.emit("tautology", { left: i, right: j, on: literal.name, resolvent: resolvent.map((l) => ({ ...l })) }); continue; } if (clauses.some((c) => ResolutionProver.same(c, resolvent))) continue; clauses.push(resolvent); const index = clauses.length - 1; if (sos.has(i) || sos.has(j)) sos.add(index); if (setOfSupport.has(i) || setOfSupport.has(j)) setOfSupport.add(index); steps.push({ left: i, right: j, on: literal.name, resolvent, index }); // ▸ two clauses clash on a variable: the rest of both make a new clause this.tracer?.emit("resolve", { left: i, right: j, on: literal.name, resolvent: resolvent.map((l) => ({ ...l })), index, clauses: clauses.length }); if (resolvent.length === 0) { // ▸ the empty clause: the set is contradictory, the goal follows this.tracer?.emit("empty-clause", { index, steps: steps.length, clauses: clauses.length }); return { proved: true, clauses, steps, goalClauses: negatedGoal.length }; } if (clauses.length >= MAX_RESOLUTION_CLAUSES) break; } if (clauses.length >= MAX_RESOLUTION_CLAUSES) break; } // ▸ no new clause can be made: the goal does not follow this.tracer?.emit("saturated", { clauses: clauses.length, steps: steps.length, capped: clauses.length >= MAX_RESOLUTION_CLAUSES }); return { proved: false, clauses, steps, goalClauses: negatedGoal.length }; } /** The next untried pair the strategy allows: the earliest by the later clause, units first for unit preference. */ private nextPair(clauses: readonly Clause[], tried: ReadonlySet<string>, sos: ReadonlySet<number>): [number, number] | null { private nextPair(clauses: readonly Clause[], triedPairs: ReadonlySet<string>, setOfSupport: ReadonlySet<number>): [number, number] | null { const allowed = (i: number, j: number): boolean => { if (tried.has(`${i},${j}`)) return false; if (triedPairs.has(`${i},${j}`)) return false; if (this.strategy === "support" && !sos.has(i) && !sos.has(j)) return false; if (this.strategy === "support" && !setOfSupport.has(i) && !setOfSupport.has(j)) return false; return clauses[i]!.some((l) => clauses[j]!.some((o) => complementary(l, o))); }; if (this.strategy === "unit") { for (let j = 1; j < clauses.length; j++) for (let i = 0; i < j; i++) if ((clauses[i]!.length === 1 || clauses[j]!.length === 1) && allowed(i, j)) return [i, j]; } for (let j = 1; j < clauses.length; j++) for (let i = 0; i < j; i++) if (allowed(i, j)) return [i, j]; return null; } private static same(a: Clause, b: Clause): boolean { return a.length === b.length && a.every((l) => b.some((o) => sameLiteral(l, o))); }}Proved: q follows, since adding its negation gave a contradiction in 3 resolutions over 6 clauses.