C: resolution refutation with clauses as bit masks
Represent each clause by two masks, one for positive and one for negative literals, and resolve until a contradiction appears.
On this page
C bitwise operators, structs, and the idea of propositional clauses.
- Represent each clause by two masks, one for positive and one for negative literals, and resolve until a contradiction appears.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A clause is a disjunction of literals, such as A or not C. It can be stored as two bit masks: one with a bit for every positive literal and one with a bit for every negated literal. Two clauses resolve on a variable when one contains it positively and the other negatively. The resolvent combines the remaining literals of both.
To prove that a knowledge base entails C, add not-C and resolve. If the empty clause appears, with both masks zero, the knowledge base together with not-C is contradictory, so C is entailed. The knowledge base is: A or B, not A or C, and not B or C. A resolvent that contains a variable both positively and negatively is a tautology and is discarded.
Code example
#include <stdio.h>
typedef struct {
unsigned pos;
unsigned neg;
} Clause;
#define A 1u
#define B 2u
#define C 4u
static void show(Clause c) {
const char *names[] = {"A", "B", "C"};
int first = 1;
if (c.pos == 0 && c.neg == 0) {
printf("empty");
}
for (int v = 0; v < 3; v++) {
if (c.pos & (1u << v)) {
printf("%s%s", first ? "" : " v ", names[v]);
first = 0;
}
if (c.neg & (1u << v)) {
printf("%s~%s", first ? "" : " v ", names[v]);
first = 0;
}
}
}
static int same(Clause x, Clause y) {
return x.pos == y.pos && x.neg == y.neg;
}
int main(void) {
Clause clauses[64] = {
{A | B, 0},
{C, A},
{C, B},
{0, C},
};
int count = 4;
for (int i = 1; i < count; i++) {
for (int j = 0; j < i; j++) {
for (int v = 0; v < 3; v++) {
unsigned bit = 1u << v;
int opposite = ((clauses[i].pos & bit) && (clauses[j].neg & bit)) ||
((clauses[i].neg & bit) && (clauses[j].pos & bit));
if (!opposite) {
continue;
}
Clause r;
r.pos = (clauses[i].pos | clauses[j].pos) & ~bit;
r.neg = (clauses[i].neg | clauses[j].neg) & ~bit;
if (r.pos & r.neg) {
continue;
}
int seen = 0;
for (int k = 0; k < count; k++) {
if (same(clauses[k], r)) {
seen = 1;
}
}
if (seen) {
continue;
}
clauses[count++] = r;
printf("derived ");
show(r);
printf("\n");
if (r.pos == 0 && r.neg == 0) {
printf("KB entails C: 1\n");
return 0;
}
}
}
}
printf("KB entails C: 0\n");
return 0;
}derived B v C derived A v C derived ~A derived ~B derived C derived B derived A derived empty KB entails C: 1
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
#include <stdio.h> typedef struct { unsigned pos; unsigned neg;} Clause; #define A 1u#define B 2u#define C 4u static void show(Clause c) { const char *names[] = {"A", "B", "C"}; int first = 1; if (c.pos == 0 && c.neg == 0) { printf("empty"); } for (int v = 0; v < 3; v++) { if (c.pos & (1u << v)) { printf("%s%s", first ? "" : " v ", names[v]); first = 0; } if (c.neg & (1u << v)) { printf("%s~%s", first ? "" : " v ", names[v]); first = 0; } }} static int same(Clause x, Clause y) { return x.pos == y.pos && x.neg == y.neg;} int main(void) { Clause clauses[64] = { {A | B, 0}, {C, A}, {C, B}, {0, C}, }; int count = 4; for (int i = 1; i < count; i++) { for (int j = 0; j < i; j++) { for (int v = 0; v < 3; v++) { unsigned bit = 1u << v; int opposite = ((clauses[i].pos & bit) && (clauses[j].neg & bit)) || ((clauses[i].neg & bit) && (clauses[j].pos & bit)); if (!opposite) { continue; } Clause r; r.pos = (clauses[i].pos | clauses[j].pos) & ~bit; r.neg = (clauses[i].neg | clauses[j].neg) & ~bit; if (r.pos & r.neg) { continue; } int seen = 0; for (int k = 0; k < count; k++) { if (same(clauses[k], r)) { seen = 1; } } if (seen) { continue; } clauses[count++] = r; printf("derived "); show(r); printf("\n"); if (r.pos == 0 && r.neg == 0) { printf("KB entails C: 1\n"); return 0; } } } } printf("KB entails C: 0\n"); return 0;}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| The clause A or B: two positive bits, no negative ones. | — | — | |
| The negated query not-C is added as a clause. | — | — | |
| The resolvent collects the other literals of both parents and drops the resolved variable. | — | — | |
| A variable that appears both ways makes the clause a tautology, which is useless and skipped. | — | — | |
| Each new resolvent is printed: B v C and A v C come from the knowledge base alone, ~A and ~B from resolving with the negated query, and then C, B, A, and finally the empty clause. | — | derived B v C derived A v C derived ~A derived ~B derived C derived B derived A derived empty | |
| The empty clause has been derived, so the knowledge base entails C. | — | derived B v C derived A v C derived ~A derived ~B derived C derived B derived A derived empty KB entails C: 1 |
Behind the scenes
How C processes this code
The C compiler produces a native program. Variables occupy memory, and execution enters main.
Cost and common mistakes
Exponential in the number of variables in the worst case, since there are 3ⁿ possible clauses.
- The clause array holds 64 entries. A real prover must stop or grow storage before it overflows.
- Resolution is complete for propositional logic, but the number of clauses can grow exponentially. Strategies such as set of support and unit preference reduce the search.
- Both masks being zero means the empty clause, a contradiction, not a clause that is trivially true.
Try it yourself
What changes if the query is A?
Reveal answer and reasoning
The empty clause is never derived, because the knowledge base has a model with A false, so the loop ends with KB entails C: 0 for that query.