package util.gdl.transforms; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; 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.GdlRule; import util.gdl.grammar.GdlSentence; import util.gdl.grammar.GdlTerm; import util.gdl.grammar.GdlVariable; import util.gdl.model.SentenceModel; import util.gdl.model.SentenceModel.TermModel; /** * * @author Alex Landau * */ public class VariableConstrainer { /** * Modifies a GDL description by replacing all rules in which variables could be bound to * functions, so that the new rules will only bind constants to variables. Also automatically * removes GdlOrs from the rules using the DeORer. * * Not guaranteed to work if the GDL is written strangely, such as when they include rules * in which certain conjuncts are never or always true. Not guaranteed to work when rules * are unsafe, i.e., they contain variables only appearing in the head, a negated literal, * and/or a distinct literal. (In fact, this can be a good way to test for GDL errors, which * often result in exceptions.) * * Not guaranteed to finish in a reasonable amount of time in pathological cases, where the * number of possible functional structures is prohibitively large. * * @param description A GDL game description. * @return A modified version of the same game. * @throws SimplifierException */ public static List<Gdl> replaceFunctionValuedVariables(List<Gdl> description) { //until we have "or mode" working, do this first List<Gdl> deoredDescription = DeORer.run(description); SentenceModel model = new SentenceModel(deoredDescription); List<Gdl> result = getVarLiteralSimplification(deoredDescription, model); return result; } private static List<Gdl> getVarLiteralSimplification(List<Gdl> description, SentenceModel model) { //General strategy: //Look for rules with conjuncts on the RHS where variables //are in positions corresponding to TermModels with //functional terms available //These should be replaced with specific rules for the //different functions List<Gdl> newDescription = new ArrayList<Gdl>(); for(Gdl gdl : description) { if(!(gdl instanceof GdlRule)) { newDescription.add(gdl); } else { GdlRule rule = (GdlRule) gdl; //Look for variables in the conjuncts //Crawl though the model and the rule together List<GdlVariable> variablesInRule = SentenceModel.getVariables(rule); Set<GdlVariable> varsToReplace = new HashSet<GdlVariable>(); Map<GdlVariable, List<TermModel>> varModels = new HashMap<GdlVariable, List<TermModel>>(); for(GdlVariable var : variablesInRule) varModels.put(var, new ArrayList<TermModel>()); for(int i = 0; i < rule.arity(); i++) { GdlLiteral conjunct = rule.get(i); //Get the appropriate body model processConjunct(conjunct, model, varModels, varsToReplace); } //Now we have all the variables to replace and the term models Map<GdlVariable, TermModel> replacements = new HashMap<GdlVariable, TermModel>(); for(GdlVariable var : varsToReplace) { //In "or" mode, replace this with union... is that all we need to do? TermModel replacementModel = TermModel.getIntersection(varModels.get(var)); replacements.put(var, replacementModel); } //Now we figure out how to rewrite the rule. //In "compatibility mode", we want, for each variable, every possible //way of splitting it into components. rewriteAndRecordRule(rule, replacements, newDescription); } } return newDescription; } private static void rewriteAndRecordRule(GdlRule rule, Map<GdlVariable, TermModel> replacements, List<Gdl> newDescription) { Set<String> usedVarNames = new HashSet<String>(SentenceModel.getVariableNames(rule)); rewriteAndRecordRule(rule, replacements, newDescription, usedVarNames); } private static void rewriteAndRecordRule(GdlRule craftedRule, Map<GdlVariable, TermModel> replacements, List<Gdl> newDescription, Set<String> usedVarNames) { //If no replacements left to make, record the rule if(replacements.isEmpty()) { newDescription.add(craftedRule); return; } GdlVariable var = replacements.keySet().iterator().next(); TermModel replacement = replacements.get(var); replacements.remove(var); for(GdlConstant c : replacement.getConstants()) { GdlRule tempRule = CommonTransforms.replaceVariable(craftedRule, var, c); rewriteAndRecordRule(tempRule, replacements, newDescription, usedVarNames); } for(Entry<String, List<TermModel>> f : replacement.getFunctions().entrySet()) { //crafted functions List<GdlFunction> cfs = getCraftableFunctions(f, usedVarNames); for(GdlFunction cf : cfs) { GdlRule tempRule = CommonTransforms.replaceVariable(craftedRule, var, cf); rewriteAndRecordRule(tempRule, replacements, newDescription, usedVarNames); } } replacements.put(var, replacement); } private static List<GdlFunction> getCraftableFunctions( Entry<String, List<TermModel>> f, Set<String> usedVarNames) { //Here we get the GdlFunctions that could match the function model //(without including any function-valued variables) //String functionKey = f.getKey(); //String functionName = functionKey.substring(0, functionKey.lastIndexOf("_")); String functionName = f.getKey(); List<TermModel> bodyModel = f.getValue(); List<GdlTerm> functionBodySoFar = new ArrayList<GdlTerm>(); //We recursively fill in the terms of the function according to the model List<GdlFunction> craftableFunctions = new ArrayList<GdlFunction>(); craftFunctionTerm(functionBodySoFar, bodyModel, 0, craftableFunctions, usedVarNames, GdlPool.getConstant(functionName)); return craftableFunctions; } private static void craftFunctionTerm(List<GdlTerm> functionBodySoFar, List<TermModel> bodyModel, int i, List<GdlFunction> craftableFunctions, Set<String> usedVarNames, GdlConstant functionName) { if(i == bodyModel.size()) { craftableFunctions.add(GdlPool.getFunction(functionName, functionBodySoFar)); return; } //We need to go through all the possibilities for term number i TermModel term = bodyModel.get(i); //Easy case: No functions; just replace with a variable if(!term.hasFunctions()) { GdlVariable newVar = getUnusedVariable(usedVarNames); functionBodySoFar.add(newVar); craftFunctionTerm(functionBodySoFar, bodyModel, i+1, craftableFunctions, usedVarNames, functionName); } else { //Hard case: One for each constant, recurse for each function (in compatibility mode) functionBodySoFar.add(null); //Add the cases for individual constants instead of variables //Otherwise, state machines that do assign functions to variables //could conceivably get confused for(GdlConstant c : term.getConstants()) { functionBodySoFar.set(i, c); craftFunctionTerm(functionBodySoFar, bodyModel, i+1, craftableFunctions, usedVarNames, functionName); } for(Entry<String, List<TermModel>> f : term.getFunctions().entrySet()) { List<GdlFunction> cfs = getCraftableFunctions(f, usedVarNames); for(GdlFunction cfTerm : cfs) { functionBodySoFar.set(i, cfTerm); craftFunctionTerm(functionBodySoFar, bodyModel, i+1, craftableFunctions, usedVarNames, functionName); } } } } static int nextVarNum = 1; private static GdlVariable getUnusedVariable(Set<String> usedVarNames) { String candidateName = "?a" + nextVarNum; nextVarNum++; while(usedVarNames.contains(candidateName)) { candidateName = "?a" + nextVarNum; nextVarNum++; } usedVarNames.add(candidateName); return GdlPool.getVariable(candidateName); } private static void processConjunct(GdlLiteral conjunct, SentenceModel model, Map<GdlVariable, List<TermModel>> varModels, Set<GdlVariable> varsToReplace) { if(conjunct instanceof GdlSentence) { GdlSentence sentence = (GdlSentence) conjunct; List<GdlTerm> conjunctBody; try {conjunctBody = sentence.getBody();} catch(RuntimeException e) {conjunctBody = Collections.emptyList();} List<TermModel> modelBody = model.getBodyForSentence(sentence); //This is useful information for debugging a faulty GDL file. if(modelBody == null) System.out.println("model body null: " + sentence + "; " + model); searchForBoundFunctions(conjunctBody, modelBody, varModels, varsToReplace); } else if(conjunct instanceof GdlNot) { //Nothing is needed here, as we will find the variable's functional value //in a positive literal (if the game has no unsafe rules). } else if(conjunct instanceof GdlOr) { GdlOr or = (GdlOr) conjunct; for(int i = 0; i < or.arity(); i++) processConjunct(or.get(i), model, varModels, varsToReplace); } else if(conjunct instanceof GdlDistinct) { //Do I do anything here? //There may be cases where a variable is being compared to a function //(or two variables are checked to see if they are the same function). //If the GDL is well-formed, those variables will appear in other //conjuncts in the rule; if those conjuncts are at all useful, the //variables models will indicate the functional value in those other //conjuncts, so we will catch and expand the variable. //So, we don't need to do anything here. } else { throw new RuntimeException("The GDL specification has changed since this was built"); } } private static void searchForBoundFunctions(List<GdlTerm> body, List<TermModel> bodyModel, Map<GdlVariable, List<TermModel>> varModels, Set<GdlVariable> varsToReplace) { //Look at each term for our goal for(int t = 0; t < body.size(); t++) { GdlTerm term = body.get(t); TermModel termModel = bodyModel.get(t); if(term instanceof GdlVariable) { varModels.get(term).add(termModel); //See if there are functional candidates if(termModel.hasFunctions()) { varsToReplace.add((GdlVariable)term); } } else if(term instanceof GdlFunction) { GdlFunction function = (GdlFunction) term; List<GdlTerm> functionBody = function.getBody(); List<TermModel> functionBodyModel = termModel.getFunction(function); if(functionBodyModel == null) System.out.println("function model body null: " + body + "; " + functionBodyModel); searchForBoundFunctions(functionBody, functionBodyModel, varModels, varsToReplace); } } } }