package validator; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.Stack; import util.gdl.factory.exceptions.GdlFormatException; import util.gdl.grammar.Gdl; import util.gdl.grammar.GdlConstant; import util.gdl.grammar.GdlDistinct; import util.gdl.grammar.GdlFunction; import util.gdl.grammar.GdlLiteral; import util.gdl.grammar.GdlNot; import util.gdl.grammar.GdlOr; import util.gdl.grammar.GdlPool; import util.gdl.grammar.GdlProposition; import util.gdl.grammar.GdlRelation; import util.gdl.grammar.GdlRule; import util.gdl.grammar.GdlSentence; import util.gdl.grammar.GdlTerm; import util.gdl.grammar.GdlVariable; import util.kif.KifReader; import util.symbol.factory.exceptions.SymbolFormatException; import validator.exception.StaticValidatorException; public class StaticValidator { private static final GdlConstant ROLE = GdlPool.getConstant("role"); private static final GdlConstant TERMINAL = GdlPool.getConstant("terminal"); private static final GdlConstant GOAL = GdlPool.getConstant("goal"); private static final GdlConstant LEGAL = GdlPool.getConstant("legal"); private static final GdlConstant DOES = GdlPool.getConstant("does"); private static final GdlConstant INIT = GdlPool.getConstant("init"); private static final GdlConstant TRUE = GdlPool.getConstant("true"); private static final GdlConstant NEXT = GdlPool.getConstant("next"); private static final GdlConstant BASE = GdlPool.getConstant("base"); private static final GdlConstant INPUT = GdlPool.getConstant("input"); /** * Validates whether a GDL description follows the rules of GDL, * to the extent that it can be determined. If the description is * invalid, throws an exception of type StaticValidatorException * explaining the problem. * * Features like finitism and monotonicity can't be definitively determined * with a static analysis; these are left to the other validator. (See * GdlValidator and the ValidatorPanel in apps.validator.) * * @param description A parsed GDL game description. * @throws StaticValidatorException The description did not pass validation. * The error message explains the error found in the GDL description. */ public static void validateDescription(List<Gdl> description) throws StaticValidatorException { /* This assumes that the description is already well-formed enough * to be made into a list of Gdl objects. We need to check those * remaining features that can be verified here. * * A + is an implemented test; a - is not (fully) implemented. * * Features of negated datalog with functions: * + All negations apply directly to sentences * + All rules are safe: All variables in the rule must appear * in some positive relation in the body. * + Arities of relation constants and function constants are fixed * + The rules are stratified: The dependency graph generated by * the rules must contain no cycles with a negative edge. * + Added restriction on functions and recursion * Additional features of GDL: * + Role relations must be ground sentences, not in rules * - Inits only in heads of rules; not in same CC as true, does, next, legal, goal, terminal * + Trues only in bodies of rules, not heads * + Nexts only in heads of rules * - Does only in bodies of rules; no paths between does and legal/goal/terminal * * + Arities: Role 1, true 1, init 1, next 1, legal 2, does 2, goal 2, terminal 0 * - Legal's first argument must be a player; ditto does, goal * - Goal values are integers between 0 and 100 * Misc.: * + All objects are relations or rules * * Things we can't really test here: * - In all cases, valid arguments to does, goal, legal * - Game terminality * - Players have goals in all states, exactly one goal in each state, * and those goals are monotonic * - Playability: legal roles for each player in each non-terminal state * - Weak winnability * * Reference for the restrictions: http://games.stanford.edu/language/spec/gdl_spec_2008_03.pdf */ List<GdlRelation> relations = new ArrayList<GdlRelation>(); List<GdlRule> rules = new ArrayList<GdlRule>(); //1) Are all objects in the description rules or relations? for(Gdl gdl : description) { if(gdl instanceof GdlRelation) { relations.add((GdlRelation) gdl); } else if(gdl instanceof GdlRule) { rules.add((GdlRule) gdl); } else { throw new StaticValidatorException("The rules include a GDL object of type " + gdl.getClass().getSimpleName() + ". Only GdlRelations and GdlRules are expected."); } } //2) Do all negations apply directly to sentences? for(GdlRule rule : rules) { for(GdlLiteral literal : rule.getBody()) { testLiteralForImproperNegation(literal); } } //3) Are the arities of all relations and all functions fixed? Map<GdlConstant, Integer> sentenceArities = new HashMap<GdlConstant, Integer>(); Map<GdlConstant, Integer> functionArities = new HashMap<GdlConstant, Integer>(); for(GdlRelation relation : relations) { addSentenceArity(relation, sentenceArities); addFunctionArities(relation, functionArities); } for(GdlRule rule : rules) { List<GdlSentence> sentences = getSentencesInRule(rule); for(GdlSentence sentence : sentences) { addSentenceArity(sentence, sentenceArities); addFunctionArities(sentence, functionArities); } } //4) Are the arities of the GDL-defined relations correct? //5) Do any functions have the names of GDL keywords (likely an error)? testPredefinedArities(sentenceArities, functionArities); //6) Are all rules safe? for(GdlRule rule : rules) { testRuleSafety(rule); } //7) Are the rules stratified? (Checked as part of dependency graph generation) //This dependency graph is actually based on relation constants, not sentence forms (like some of the other tools here) Map<GdlConstant, Set<GdlConstant>> dependencyGraph = getDependencyGraph(sentenceArities.keySet(), rules); if(!dependencyGraph.containsKey(DOES)) dependencyGraph.put(DOES, new HashSet<GdlConstant>()); if(!dependencyGraph.containsKey(TRUE)) dependencyGraph.put(TRUE, new HashSet<GdlConstant>()); //8) We check that all the keywords are related to one another correctly, according to the dependency graph checkKeywordLocations(relations, rules, dependencyGraph); //9) We check the restriction on functions and recursion Map<GdlConstant, Set<GdlConstant>> ancestorsGraph = getAncestorsGraph(dependencyGraph); for(GdlRule rule : rules) { checkRecursionFunctionRestriction(rule, ancestorsGraph); } } /** * Tests whether the parentheses in a given file match correctly. If the * parentheses are unbalanced, gives the line number of an unmatched * parenthesis. * @param file The .kif file to test. * @throws StaticValidatorException The parentheses are unbalanced. The * line number of an unmatched parenthesis is included in the error * message. */ public static void matchParentheses(File file) throws StaticValidatorException { try { BufferedReader in = new BufferedReader(new FileReader(file)); String line = in.readLine(); int lineNumber = 1; Stack<Integer> linesStack = new Stack<Integer>(); while(line != null) { for(int i = 0; i < line.length(); i++) { char c = line.charAt(i); if(c == '(') { linesStack.add(lineNumber); } else if(c == ')') { if(linesStack.isEmpty()) { in.close(); throw new StaticValidatorException("Extra close parens encountered at line " + lineNumber + "\nLine: " + line); } linesStack.pop(); } else if(c == ';') { //the line is a comment; ignore its parens break; } } line = in.readLine(); lineNumber++; } if(!linesStack.isEmpty()) { in.close(); throw new StaticValidatorException("Extra open parens encountered, starting at line " + linesStack.peek()); } in.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } private static void checkRecursionFunctionRestriction(GdlRule rule, Map<GdlConstant, Set<GdlConstant>> ancestorsGraph) throws StaticValidatorException { //TODO: This might not work 100% correctly with descriptions with //"or" in them, especially if ors are nested. For best results, deOR //before testing. //The restriction goes something like this: //Look at all the terms in each positive relation in the rule that // is in a cycle with the head. GdlConstant head = rule.getHead().getName(); Set<GdlRelation> cyclicRelations = new HashSet<GdlRelation>(); Set<GdlRelation> acyclicRelations = new HashSet<GdlRelation>(); for(GdlLiteral literal : rule.getBody()) { //Is it a relation? if(literal instanceof GdlRelation) { GdlRelation relation = (GdlRelation) literal; //Is it in a cycle with the head? if(ancestorsGraph.get(relation.getName()).contains(head)) { cyclicRelations.add(relation); } else { acyclicRelations.add(relation); } } else if(literal instanceof GdlOr) { //We'll look one layer deep for the cyclic kind GdlOr or = (GdlOr) literal; for(int i = 0; i < or.arity(); i++) { GdlLiteral internal = or.get(i); if(internal instanceof GdlRelation) { GdlRelation relation = (GdlRelation) internal; if(ancestorsGraph.get(relation.getName()).contains(head)) { cyclicRelations.add(relation); } //Don't add acyclic relations, as we can't count on them } } } } for(GdlRelation relation : cyclicRelations) { for(GdlTerm term : relation.getBody()) { //There are three ways to be okay boolean safe = false; //One: Is it ground? if(term.isGround()) safe = true; //Two: Is it a term in the head relation? if(rule.getHead() instanceof GdlRelation) { for(GdlTerm headTerm : rule.getHead().getBody()) { if(headTerm.equals(term)) safe = true; } } //Three: Is it in some other positive conjunct not in a cycle with the head? for(GdlRelation acyclicRelation : acyclicRelations) { for(GdlTerm acyclicTerm : acyclicRelation.getBody()) { if(acyclicTerm.equals(term)) safe = true; } } if(!safe) throw new StaticValidatorException("Recursion-function restriction violated in rule " + rule + ", for term " + term); } } } private static Map<GdlConstant, Set<GdlConstant>> getAncestorsGraph( Map<GdlConstant, Set<GdlConstant>> dependencyGraph) { Map<GdlConstant, Set<GdlConstant>> ancestorsGraph = new HashMap<GdlConstant, Set<GdlConstant>>(); for(GdlConstant head : dependencyGraph.keySet()) { ancestorsGraph.put(head, getAncestors(head, dependencyGraph)); } return ancestorsGraph; } private static void checkKeywordLocations(List<GdlRelation> relations, List<GdlRule> rules, Map<GdlConstant, Set<GdlConstant>> dependencyGraph) throws StaticValidatorException { //- Role relations must be ground sentences, not in rules if(!dependencyGraph.get(ROLE).isEmpty()) throw new StaticValidatorException("The role relation should be defined by ground statements, not by rules"); //- Trues only in bodies of rules, not heads if(!dependencyGraph.get(TRUE).isEmpty()) throw new StaticValidatorException("The true relation should never be in the head of a rule"); //- Does only in bodies of rules if(!dependencyGraph.get(DOES).isEmpty()) throw new StaticValidatorException("The does relation should never be in the head of a rule"); //- Inits only in heads of rules; not in same CC as true, does, next, legal, goal, terminal //- Nexts only in heads of rules for(Set<GdlConstant> relsInBodies : dependencyGraph.values()) { if(relsInBodies.contains(INIT)) throw new StaticValidatorException("The init relation should never be in the body of a rule"); if(relsInBodies.contains(NEXT)) throw new StaticValidatorException("The next relation should never be in the body of a rule"); if(relsInBodies.contains(BASE)) throw new StaticValidatorException("The base relation should never be in the body of a rule"); if(relsInBodies.contains(INPUT)) throw new StaticValidatorException("The input relation should never be in the body of a rule"); } //no paths between does and legal/goal/terminal //connected component restrictions //Base and input: same rules as init? } private static Map<GdlConstant, Set<GdlConstant>> getDependencyGraph( Set<GdlConstant> relationNames, List<GdlRule> rules) throws StaticValidatorException { Map<GdlConstant, Set<GdlConstant>> dependencyGraph = new HashMap<GdlConstant, Set<GdlConstant>>(); Map<GdlConstant, Set<GdlConstant>> negativeEdges = new HashMap<GdlConstant, Set<GdlConstant>>(); for(GdlConstant relationName : relationNames) { dependencyGraph.put(relationName, new HashSet<GdlConstant>()); negativeEdges.put(relationName, new HashSet<GdlConstant>()); } for(GdlRule rule : rules) { GdlConstant headName = rule.getHead().getName(); for(GdlLiteral literal : rule.getBody()) { addLiteralAsDependent(literal, dependencyGraph.get(headName), negativeEdges.get(headName)); } } checkForNegativeCycles(dependencyGraph, negativeEdges); return dependencyGraph; } private static void checkForNegativeCycles( Map<GdlConstant, Set<GdlConstant>> dependencyGraph, Map<GdlConstant, Set<GdlConstant>> negativeEdges) throws StaticValidatorException { while(!negativeEdges.isEmpty()) { //Look for a cycle containing this edge GdlConstant tail = negativeEdges.keySet().iterator().next(); Set<GdlConstant> heads = negativeEdges.get(tail); negativeEdges.remove(tail); for(GdlConstant head : heads) { //Check for any head->tail path in dependencyGraph Set<GdlConstant> ancestors = getAncestors(head, dependencyGraph); if(ancestors.contains(tail)) throw new StaticValidatorException("There is a negative edge from " + tail + " to " + head + " in a cycle in the dependency graph"); } } } private static Set<GdlConstant> getAncestors(GdlConstant child, Map<GdlConstant, Set<GdlConstant>> dependencyGraph) { Set<GdlConstant> ancestors = new HashSet<GdlConstant>(); Queue<GdlConstant> unexpanded = new LinkedList<GdlConstant>(); ancestors.addAll(dependencyGraph.get(child)); unexpanded.addAll(ancestors); while(!unexpanded.isEmpty()) { GdlConstant toExpand = unexpanded.remove(); for(GdlConstant parent : dependencyGraph.get(toExpand)) { if(ancestors.add(parent)) unexpanded.add(parent); } } return ancestors; } private static void addLiteralAsDependent(GdlLiteral literal, Set<GdlConstant> dependencies, Set<GdlConstant> negativeEdges) { if(literal instanceof GdlSentence) { dependencies.add(((GdlSentence) literal).getName()); } else if(literal instanceof GdlNot) { addLiteralAsDependent(((GdlNot) literal).getBody(), dependencies, negativeEdges); addLiteralAsDependent(((GdlNot) literal).getBody(), negativeEdges, negativeEdges); } else if(literal instanceof GdlOr) { GdlOr or = (GdlOr) literal; for(int i = 0; i < or.arity(); i++) { addLiteralAsDependent(or.get(i), dependencies, negativeEdges); } } } private static void testRuleSafety(GdlRule rule) throws StaticValidatorException { List<GdlVariable> unsupportedVariables = new ArrayList<GdlVariable>(); if(rule.getHead() instanceof GdlRelation) getVariablesInBody(rule.getHead().getBody(), unsupportedVariables); for(GdlLiteral literal : rule.getBody()) { getUnsupportedVariablesInLiteral(literal, unsupportedVariables); } //Supported variables are those in a positive relation in the body Set<GdlVariable> supportedVariables = new HashSet<GdlVariable>(); for(GdlLiteral literal : rule.getBody()) { getSupportedVariablesInLiteral(literal, supportedVariables); } for(GdlVariable var : unsupportedVariables) if(!supportedVariables.contains(var)) throw new StaticValidatorException("Unsafe rule " + rule + ": Variable " + var + " is not defined in a positive relation in the rule's body"); } private static void getUnsupportedVariablesInLiteral(GdlLiteral literal, Collection<GdlVariable> unsupportedVariables) { //We're looking for all variables in distinct or negated relations if(literal instanceof GdlNot) { GdlLiteral internal = ((GdlNot) literal).getBody(); if(internal instanceof GdlRelation) { getVariablesInBody(((GdlRelation) internal).getBody(), unsupportedVariables); } } else if(literal instanceof GdlOr) { GdlOr or = (GdlOr) literal; for(int i = 0; i < or.arity(); i++) { getUnsupportedVariablesInLiteral(or.get(i), unsupportedVariables); } } else if(literal instanceof GdlDistinct) { GdlDistinct distinct = (GdlDistinct) literal; List<GdlTerm> pair = new ArrayList<GdlTerm>(2); //Easy way to parse functions pair.add(distinct.getArg1()); pair.add(distinct.getArg2()); getVariablesInBody(pair, unsupportedVariables); } } private static void getSupportedVariablesInLiteral(GdlLiteral literal, Collection<GdlVariable> variables) { if(literal instanceof GdlRelation) { getVariablesInBody(((GdlRelation) literal).getBody(), variables); } else if(literal instanceof GdlOr) { GdlOr or = (GdlOr) literal; if(or.arity() == 0) return; LinkedList<GdlVariable> vars = new LinkedList<GdlVariable>(); getSupportedVariablesInLiteral(or.get(0), vars); for(int i = 1; i < or.arity(); i++) { Set<GdlVariable> newVars = new HashSet<GdlVariable>(); getSupportedVariablesInLiteral(or.get(i), newVars); vars.retainAll(newVars); } variables.addAll(vars); } } private static void getVariablesInBody(List<GdlTerm> body, Collection<GdlVariable> variables) { for(GdlTerm term : body) { if(term instanceof GdlVariable) { variables.add((GdlVariable) term); } else if(term instanceof GdlFunction) { getVariablesInBody(((GdlFunction) term).getBody(), variables); } } } private static void testPredefinedArities( Map<GdlConstant, Integer> sentenceArities, Map<GdlConstant, Integer> functionArities) throws StaticValidatorException { if(!sentenceArities.containsKey(ROLE)) { throw new StaticValidatorException("No role relations found in the game description"); } else if(sentenceArities.get(ROLE) != 1) { throw new StaticValidatorException("The role relation should have arity 1 (argument: the player name)"); } else if(!sentenceArities.containsKey(TERMINAL)) { throw new StaticValidatorException("No terminal proposition found in the game description"); } else if(sentenceArities.get(TERMINAL) != 0) { throw new StaticValidatorException("'terminal' should be a proposition, not a relation"); } else if(!sentenceArities.containsKey(GOAL)) { throw new StaticValidatorException("No goal relations found in the game description"); } else if(sentenceArities.get(GOAL) != 2) { throw new StaticValidatorException("The goal relation should have arity 2 (first argument: the player, second argument: integer from 0 to 100)"); } else if(!sentenceArities.containsKey(LEGAL)) { throw new StaticValidatorException("No legal relations found in the game description"); } else if(sentenceArities.get(LEGAL) != 2) { throw new StaticValidatorException("The legal relation should have arity 2 (first argument: the player, second argument: the move)"); } else if(sentenceArities.containsKey(DOES) && sentenceArities.get(DOES) != 2) { throw new StaticValidatorException("The does relation should have arity 2 (first argument: the player, second argument: the move)"); } else if(sentenceArities.containsKey(INIT) && sentenceArities.get(INIT) != 1) { throw new StaticValidatorException("The init relation should have arity 1 (argument: the base truth)"); } else if(sentenceArities.containsKey(TRUE) && sentenceArities.get(TRUE) != 1) { throw new StaticValidatorException("The true relation should have arity 1 (argument: the base truth)"); } else if(sentenceArities.containsKey(NEXT) && sentenceArities.get(NEXT) != 1) { throw new StaticValidatorException("The next relation should have arity 1 (argument: the base truth)"); } else if(sentenceArities.containsKey(BASE) && sentenceArities.get(BASE) != 1) { throw new StaticValidatorException("The base relation should have arity 1 (argument: the base truth)"); } else if(sentenceArities.containsKey(INPUT) && sentenceArities.get(INPUT) != 2) { throw new StaticValidatorException("The input relation should have arity 2 (first argument: the player, second argument: the move)"); } //Look for function arities with these names if(functionArities.containsKey(ROLE) || functionArities.containsKey(TERMINAL) || functionArities.containsKey(GOAL) || functionArities.containsKey(LEGAL) || functionArities.containsKey(DOES) || functionArities.containsKey(INIT) || functionArities.containsKey(TRUE) || functionArities.containsKey(NEXT) || functionArities.containsKey(BASE) || functionArities.containsKey(INPUT)) { throw new StaticValidatorException("Probable error: Misuse of a keyword as a function"); } } private static void addSentenceArity(GdlSentence sentence, Map<GdlConstant, Integer> sentenceArities) throws StaticValidatorException { Integer curArity = sentenceArities.get(sentence.getName()); if(curArity == null) { sentenceArities.put(sentence.getName(), sentence.arity()); } else if(curArity != sentence.arity()) { throw new StaticValidatorException("The sentence with the name " + sentence.getName() + " appears with two different arities, " + sentence.arity() + " and " + curArity + "."); } } private static void addFunctionArities(GdlSentence sentence, Map<GdlConstant, Integer> functionArities) throws StaticValidatorException { for(GdlFunction function : getFunctionsInSentence(sentence)) { Integer curArity = functionArities.get(function.getName()); if(curArity == null) { } else if(curArity != function.arity()) { throw new StaticValidatorException("The function with the name " + function.getName() + " appears with two different arities, " + function.arity() + " and " + curArity); } } } private static List<GdlSentence> getSentencesInRule(GdlRule rule) { List<GdlSentence> sentences = new ArrayList<GdlSentence>(); sentences.add(rule.getHead()); for(GdlLiteral literal : rule.getBody()) { getSentencesInLiteral(literal, sentences); } return sentences; } private static void getSentencesInLiteral(GdlLiteral literal, List<GdlSentence> sentences) { if(literal instanceof GdlSentence) { sentences.add((GdlSentence) literal); } else if(literal instanceof GdlNot) { getSentencesInLiteral(((GdlNot) literal).getBody(), sentences); } else if(literal instanceof GdlOr) { GdlOr or = (GdlOr) literal; for(int i = 0; i < or.arity(); i++) { getSentencesInLiteral(or.get(i), sentences); } } } private static List<GdlFunction> getFunctionsInSentence(GdlSentence sentence) { List<GdlFunction> functions = new ArrayList<GdlFunction>(); if(sentence instanceof GdlProposition) return functions; //Propositions have no body addFunctionsInBody(sentence.getBody(), functions); return functions; } private static void addFunctionsInBody(List<GdlTerm> body, List<GdlFunction> functions) { for(GdlTerm term : body) { if(term instanceof GdlFunction) { GdlFunction function = (GdlFunction) term; functions.add(function); addFunctionsInBody(function.getBody(), functions); } } } private static void testLiteralForImproperNegation(GdlLiteral literal) throws StaticValidatorException { if(literal instanceof GdlNot) { GdlNot not = (GdlNot) literal; if(!(not.getBody() instanceof GdlSentence)) throw new StaticValidatorException("The negation " + not + " contains a literal " + not.getBody() + " that is not a sentence. Only a single sentence is allowed inside a negation."); } else if(literal instanceof GdlOr) { GdlOr or = (GdlOr) literal; for(int i = 0; i < or.arity(); i++) { testLiteralForImproperNegation(or.get(i)); } } } /** * Tries to test most of the rulesheets in the games directory. This should * be run when developing a new game to spot errors. */ public static void main(String[] args) { File kifDirectory = new File("games/rulesheets"); for(File kifFile : kifDirectory.listFiles()) { if(!kifFile.getName().endsWith(".kif")) continue; //These are test cases for smooth handling of errors that often //appear in rulesheets. They are intentionally invalid. if(kifFile.getName().equals("test_case_3b.kif")) continue; if(kifFile.getName().equals("test_case_3e.kif")) continue; if(kifFile.getName().equals("test_case_3f.kif")) continue; System.out.println("Testing " + kifFile.getName()); try { matchParentheses(kifFile); List<Gdl> description = KifReader.read(kifFile.getAbsolutePath()); StaticValidator.validateDescription(description); } catch (IOException e) { e.printStackTrace(); } catch (SymbolFormatException e) { e.printStackTrace(); } catch (GdlFormatException e) { e.printStackTrace(); } catch (StaticValidatorException e) { e.printStackTrace(); //Draw attention to the error return; } } } }