folproof
Version:
A first-order logic proof verifier
538 lines (485 loc) • 18.2 kB
JavaScript
var u = require("./util");
var Rule = require("./rule.js");
var Justifier = require("./justifier.js");
var rules = {
"premise" : new Rule({
name : "Premise",
type : "simple",
verifier : new Justifier(null, function(proof, step) { return true; })
}),
"assumption" : new Rule({
name : "Assumption",
type : "simple",
verifier : new Justifier(null, function(proof, step) {
if (proof.steps[step].isFirstStmt())
return true;
return "Assumptions can only be made at the start of an assumption box.";
})
}),
"lem" : new Rule({
name : "LEM",
type : "derived",
verifier : new Justifier(null, function(proof, step) {
var s = proof.steps[step].getSentence();
if (s[0] !== "or")
return "LEM: must be phi or not phi.";
var left = s[1], right = s[2];
if (right[0] !== "not" || !semanticEq(left, right[1]))
return "LEM: right side must be negation of left.";
return true;
})
}),
"copy" : new Rule({
name : "COPY",
type : "derived",
verifier : new Justifier({stepRefs:["num"]},
function(proof, step, part, steps) {
var curStep = proof.steps[step].getSentence();
var refStep = proof.steps[steps[0]].getSentence();
if (!semanticEq(curStep, refStep))
return "Copy: Current step is not semantically equal to the referenced step.";
return true;
}
)
}),
"mt" : new Rule({
name : "MT",
type : "derived",
verifier : new Justifier({stepRefs:["num","num"]},
function(proof, step, part, steps) {
var impStep = proof.steps[steps[0]].getSentence();
if (impStep[0] !== "->")
return "MT: 1st referenced step must be implication.";
var left = impStep[1], right = impStep[2];
var negStep = proof.steps[steps[1]].getSentence();
if (negStep[0] !== "not" || !semanticEq(negStep[1], right))
return "MT: 2nd ref step must be negation of right side of 1st ref step.";
var s = proof.steps[step].getSentence();
if (s[0] !== 'not' || !semanticEq(left, s[1]))
return "MT: current step must be negation of left side of ref step.";
return true;
})
}),
"pbc" : new Rule({
name : "PBC",
type : "derived",
verifier : new Justifier(
{ hasPart : false, stepRefs : ["range"], subst : false },
function(proof, step, part, steps) {
var assumptionExpr = proof.steps[steps[0][0]].getSentence();
var contraExpr = proof.steps[steps[0][1]].getSentence();
if (! isContradiction(contraExpr)) {
return "PBC: Final step in range must be a contradiction.";
}
if (assumptionExpr[0] !== 'not')
return "PBC: Assumption is not a negation. Might you be thinking of not-introduction?";
var semEq = semanticEq(assumptionExpr[1], proof.steps[step].getSentence());
if (semEq)
return true;
return "PBC: Negation of assumption doesn't match current step.";
})
}),
"contra" : new Rule({
name : "Contradiction",
type : "normal",
elimination : new Justifier(
{ hasPart : false, stepRefs : ["num"], subst : false },
function(proof, step, part, steps) {
var refStep = proof.steps[steps[0]].getSentence();
if (refStep[0] != 'id' || (refStep[1] != 'contradiction' && refStep[1] != '_|_'))
return "Contra-elim: Referenced step is not a contradiction.";
return true;
})
}),
"notnot" : new Rule({
name : "Double-negation",
type : "normal",
elimination : new Justifier(
{ hasPart : false, stepRefs : ["num"], subst : false },
function(proof, step, part, steps) {
var curStep = proof.steps[step].getSentence();
var refStep = proof.steps[steps[0]].getSentence();
if (refStep[0] !== 'not' || refStep[1][0] !== 'not')
return "Notnot-elim: Referenced step is not a double-negation.";
if (!semanticEq(refStep[1][1], curStep))
return "Notnot-elim: Does not result in current step.";
return true;
})
}),
"->" : new Rule({
name : "Implication",
type : "normal",
introduction : new Justifier(
{ hasPart : false, stepRefs : ["range"], subst : false },
function(proof, step, part, steps) {
var truth = proof.steps[steps[0][0]].getSentence();
var result = proof.steps[steps[0][1]].getSentence();
var implies = proof.steps[step].getSentence();
if (implies[0] != '->')
return "Implies-Intro: Current step is not an implication";
var truthSemEq = semanticEq(implies[1], truth);
if (! truthSemEq)
return "Implies-Intro: The left side does not match the assumption.";
var resultSemEq = semanticEq(implies[2], result);
if (! resultSemEq)
return "Implies-Intro: The result does not match the right side.";
return true;
}
),
elimination : new Justifier(
{ hasPart : false, stepRefs : ["num", "num"], subst : false },
function(proof, step, part, steps) {
var truthStep = steps[1], impliesStep = steps[0];
if (truthStep >= step || impliesStep >= step)
return "Implies-Elim: Referenced proof steps must precede current step.";
var truth = proof.steps[truthStep].getSentence();
var implies = proof.steps[impliesStep].getSentence();
if (implies[0] != '->')
return "Implies-Elim: Step " + steps[0] + " is not an implication";
var truthSemEq = semanticEq(implies[1], truth);
var resultSemEq = semanticEq(implies[2], proof.steps[step].getSentence());
if (truthSemEq) {
if (resultSemEq) {
return true;
} else {
return "Implies-Elim: The left side does not imply this result.";
}
}
return "Implies-Elim: The implication's left side does not match the referenced step.";
}
)
}),
"and" : new Rule({
name : "And",
type : "normal",
introduction : new Justifier(
{ stepRefs : ["num", "num"] },
function(proof, step, part, steps) {
var s = proof.steps[step].getSentence();
if (s[0] !== 'and')
return "And-Intro: Current step is not an 'and'-expression." + proof.steps[step].getSentence();
if (semanticEq(s[1], proof.steps[steps[0]].getSentence())) {
if (semanticEq(s[2], proof.steps[steps[1]].getSentence())) {
return true;
} else {
return "And-Intro: Right side doesn't match referenced step.";
}
}
return "And-Intro: Left side doesn't match referenced step.";
}),
elimination : new Justifier(
{ hasPart: true, stepRefs: ["num"] },
function(proof, step, part, steps) {
var andExp = proof.steps[steps[0]].getSentence();
if (andExp[0] != 'and')
return "And-Elim: Referenced step is not an 'and' expression.";
var semEq = semanticEq(andExp[part], proof.steps[step].getSentence());
if (semEq)
return true;
return "And-Elim: In referenced line, side " + part + " does not match current step.";
})
}),
"or" : new Rule({
name : "Or",
type : "normal",
introduction : new Justifier(
{ hasPart: true, stepRefs: ["num"] },
function(proof, step, part, steps) {
var s = proof.steps[step].getSentence();
if (s[0] !== 'or')
return "Or-Intro: Current step is not an 'or'-expression.";
if (semanticEq(s[part], proof.steps[steps[0]].getSentence()))
return true;
return "Or-Intro: Side " + part + " doesn't match referenced step.";
}),
elimination : new Justifier(
{ stepRefs : ["num", "range", "range"] },
function(proof, step, part, steps) {
var currStepExpr = proof.steps[step].getSentence();
var orStepExpr = proof.steps[steps[0]].getSentence();
var a1p1Expr = proof.steps[steps[1][0]].getSentence();
var a1p2Expr = proof.steps[steps[1][1]].getSentence();
var a2p1Expr = proof.steps[steps[2][0]].getSentence();
var a2p2Expr = proof.steps[steps[2][1]].getSentence();
// and through the gauntlet...
if (orStepExpr[0] !== 'or')
return "Or-Elim: First referenced step is not an 'or'-expression.";
if (!semanticEq(orStepExpr[1], a1p1Expr))
return "Or-Elim: First range intro doesn't match left side of 'or'.";
if (!semanticEq(orStepExpr[2], a2p1Expr))
return "Or-Elim: Second range range intro doesn't match right side of 'or'.";
if (!semanticEq(a1p2Expr, a2p2Expr))
return "Or-Elim: Step range conclusions don't match.";
if (!semanticEq(a1p2Expr, currStepExpr))
return "Or-Elim: Current step doesn't match step range conclusions.";
return true;
})
}),
"not" : new Rule({
name : "Not",
type : "normal",
introduction : new Justifier(
{ stepRefs: ["range"] },
function(proof, step, part, steps) {
var assumptionExpr = proof.steps[steps[0][0]].getSentence();
var contraExpr = proof.steps[steps[0][1]].getSentence();
if (! isContradiction(contraExpr)) {
return "Not-Intro: Final step in range must be a contradiction.";
}
var curStep = proof.steps[step].getSentence();
if (curStep[0] !== 'not') {
return "Not-Intro: Current step is not a negation. Might you be thinking of PBC?";
} else {
var semEq = semanticEq(assumptionExpr, curStep[1]);
if (semEq)
return true;
return "Not-Intro: Negation of assumption doesn't match current step.";
}
}),
elimination : new Justifier(
{ stepRefs: ["num", "num"] },
function(proof, step, part, steps) {
var s = proof.steps[step].getSentence();
if (! isContradiction(s))
return "Not-Elim: Current step is not a contradiction." + proof.steps[step].getSentence();
var step1expr = proof.steps[steps[0]].getSentence();
var step2expr = proof.steps[steps[1]].getSentence();
var semEq;
if (step1expr[0] === 'not') {
semEq = semanticEq(step1expr[1], step2expr);
} else if (step2expr[0] === 'not') {
semEq = semanticEq(step2expr[1], step1expr);
} else {
return "Not-Elim: Neither referenced proof step is a 'not' expression.";
}
if (semEq) return true;
return "Not-Elim: Subexpression in not-expr does not match other expr.";
})
}),
"a." : new Rule({
name : "ForAll",
type : "normal",
introduction : new Justifier(
{ stepRefs : ["range"], subst : true },
function(proof, step, part, steps, subst) {
var currStep = proof.steps[step];
var currExpr = currStep.getSentence();
var startStep = proof.steps[steps[0][0]];
var startExpr = startStep.getSentence();
var scope = startStep.getScope(); // ex: [['x0','x'], ['y0', 'y'], ...], LIFO
var endExpr = proof.steps[steps[0][1]].getSentence();
if (currExpr[0] !== 'forall')
return "All-x-Intro: Current step is not a 'for-all' expression.";
if (scope.length == 0 || scope[0] == null)
return "All-x-Intro: Not valid without a scoping assumption (e.g., an x0 box).";
// check if any substitutions from our scope match refExpr
var scopeVar = scope[scope.length-1];
var found = scope.slice().reverse().reduce(function(a,e) { return a && (e == null || e == subst[1]); }, true);
if (! found)
return "All-x-intro: Substitution " + subst[1] + " doesn't match scope: " + scope.filter(function(e) { if (e != null) return e; }).join(", ");
var endExprSub = substitute(endExpr, subst[1], subst[0]);
if (semanticEq(endExprSub, currExpr[2]))
return true;
return "All-x-Intro: Last step in range doesn't match current step after " + subst[0] + "/" + subst[1] + ".";
}),
elimination : new Justifier(
{ stepRefs : ["num"], subst: true },
function(proof, step, part, steps, subst) {
var currStep = proof.steps[step];
var currExpr = currStep.getSentence();
var refExpr = proof.steps[steps[0]].getSentence();
if (refExpr[0] !== 'forall')
return "All-x-Elim: Referenced step is not a for-all expression.";
var refExprSub = substitute(refExpr[2], subst[0], subst[1]);
if (semanticEq(refExprSub, currExpr))
return true;
return "All-x-Elim: Referenced step did not match current step after " + subst[1] + "/" + subst[0] + ".";
})
}),
"e." : new Rule({
name : "Exists",
type : "normal",
introduction : new Justifier(
{ stepRefs: ["num"], subst: true },
function(proof, step, part, steps, subst) {
var currStep = proof.steps[step];
var currExpr = currStep.getSentence();
var refExpr = proof.steps[steps[0]].getSentence();
if (currExpr[0] !== 'exists')
return "Exists-x-Intro: Current step is not an 'exists' expression.";
var refExprSub = substitute(refExpr, subst[1], subst[0]);
if (semanticEq(refExprSub, currExpr[2]))
return true;
return "Exists-x-Intro: Referenced step did not match current step after " + subst[1] + "/" + subst[0] + " substitution.";
}),
elimination : new Justifier(
{ stepRefs: ["num", "range"], subst: true },
function(proof, step, part, steps, subst) {
var currStep = proof.steps[step];
var currExpr = currStep.getSentence();
var refExpr = proof.steps[steps[0]].getSentence();
var startStep = proof.steps[steps[1][0]];
var startExpr = startStep.getSentence();
var scope = startStep.getScope(); // ex: [['x0','x'], ['y0', 'y'], ...], LIFO
var endExpr = proof.steps[steps[1][1]].getSentence();
if (refExpr[0] !== 'exists')
return "Exists-x-Elim: Referenced step is not an 'exists' expression.";
if (scope.length == 0 || scope[scope.length - 1] == null)
return "Exists-x-Elim: Range must be within an assumption scope (e.g., an x0 box).";
// check whether substition matches ref line with current line
var scopeVars = scope[scope.length-1];
var refExprSub = substitute(refExpr[2], subst[0], subst[1]);
if (semanticEq(refExprSub, startExpr)) {
if (semanticEq(endExpr, currExpr))
return true;
return "Exists-x-Elim: assumption ending step does not match current step.";
}
return "Exists-x-Elim: assumption beginning step doesn't match ref step for " + scopeVars[0] + ".";
})
}),
"=" : new Rule({
name : "Equality",
type : "normal",
introduction : new Justifier(
{ /* no params required */ },
function(proof, step, part, steps) {
var s = proof.steps[step].getSentence();
if (s[0] !== '=')
return "Equality-Intro: Current step is not an equality." + proof.steps[step].getSentence();
if (semanticEq(s[1], s[2]))
return true;
return "Equality-Intro: Left and right sides do not match.";
}),
elimination : new Justifier(
{ stepRefs: ["num", "num"] },
function(proof, step, part, steps) {
var equalityExpr = proof.steps[steps[0]].getSentence();
var elimExpr = proof.steps[steps[1]].getSentence();
var proposedResult = proof.steps[step].getSentence();
if (equalityExpr[0] !== '=')
return "Equality-Elim: First referenced step is not an equality.";
if (!semanticEq(elimExpr, proposedResult, equalityExpr[1], equalityExpr[2]))
return "Equality-Elim: Does not result in current step.";
return true;
})
}),
};
function substitute(startExpr, a, b, bound) {
u.debug("substitute", startExpr, a, b);
bound = bound ? bound : [];
var binOps = ["->", "and", "or", "<->", "="];
var unOps = ["not", "forall", "exists"];
// remove parens, which are basically stylistic no-ops
while (startExpr[0] === 'paren') startExpr = startExpr[1];
if (arrayContains(binOps, startExpr[0])) {
var leftSide = substitute(startExpr[1], a, b);
var rightSide = substitute(startExpr[2], a, b);
return [startExpr[0], leftSide, rightSide];
} else if (arrayContains(unOps, startExpr[0])) {
if (startExpr[0] === "forall" || startExpr[0] === "exists") {
bound = bound.slice(0);
bound.push(startExpr[1]);
return [startExpr[0], startExpr[1],
substitute(startExpr[2], a, b, bound)];
}
return [startExpr[0], substitute(startExpr[1], a, b, bound)];
} else if (startExpr[0] === 'id') {
if (startExpr.length === 2) { // our loverly base case
if (! arrayContains(bound, startExpr[1])) {
if (startExpr[1] === a)
return [startExpr[0], b];
}
return startExpr;
}
if (startExpr.length === 3) {
var newTerms = [];
for (var i=0; i<startExpr[2].length; i++) {
newTerms.push(substitute(startExpr[2][i], a, b, bound));
}
return [startExpr[0], startExpr[1], newTerms];
}
throw Error("Unexpected AST format.");
}
}
/**
* Determines whether two expressions are semantically equivalent
* under the given (and optional) substitution.
* a, b - abstract syntax trees of the expressions to be compared.
* suba, subb (optional) - does comparison after substituting suba in a with subb.
*/
function semanticEq(A, B, suba, subb) {
u.debug("semanticEq", A, B);
var bound = {}, sub;
if (suba) {
sub = true;
return _rec(A, B, {});
} else {
sub = false;
return _rec(A, B);
}
function _rec(a, b, bound) {
var binOps = ["->", "and", "or", "<->", "="];
var unOps = ["not"];
// if eq w/substitution, return true, otherwise continue
if (sub && semanticEq(a, suba)) {
if ((a[0] !== 'id' || !bound[a[1]]) && _rec(subb, b, bound)) return true;
}
if (arrayContains(binOps, a[0]) && a[0] === b[0]) {
if (_rec(a[1], b[1], bound) && _rec(a[2], b[2], bound)) {
return true;
}
return false;
} else if (arrayContains(unOps, a[0]) && a[0] === b[0]) {
if (_rec(a[1], b[1], bound)) {
return true;
}
return false;
} else if (a[0] === 'exists' || a[0] === 'forall' && a[0] === b[0]) {
var newb;
if (sub) {
newb = clone(bound);
newb[a[1]] = true;
}
if (_rec(a[2], b[2], newb)) {
return true;
}
return false;
} else if (a[0] === "id") {
if (b && a[1] !== b[1]) return false;
if (a.length == 2 && b.length == 2) {
return true;
}
if (a.length == 3 && b.length == 3) {
if (a[2].length != b[2].length) {
return false;
}
for (var i=0; i<a[2].length; i++) {
if (!_rec(a[2][i], b[2][i], bound)) {
return false;
}
}
return true;
}
}
return false;
}
}
function isContradiction(s) {
return (s[0] === 'id' && (s[1] === '_|_' || s[1] === 'contradiction'));
}
function arrayContains(arr, el) {
for (var i=0; i<arr.length; i++) {
if (arr[i] === el) return true;
}
return false;
}
function clone(obj) {
var newo = {};
for(var k in Object.keys(obj)) {
newo[k] = obj[k];
}
return newo;
}
if (typeof require !== 'undefined' && typeof exports !== 'undefined') {
module.exports = rules;
}