package edu.stanford.nlp.sempre;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import fig.basic.*;
import fig.exec.Execution;
import java.io.File;
import java.io.PrintWriter;
import java.util.*;
/**
* The grammar is a set of rules of the form:
* (rule lhsCat (rhs ... rhs) semanticFn (key value) ... (key value))
*
* Some programming-language-esque features are supported to make life easier.
* All grammar-file variables should start with '@' (both loop variables and global definitions).
*
* Includes (reads in commands from the given file):
* (include base.grammar)
* Control flow:
* (when (and tag1 (not tag2) (or tag3 tag4)) ...)
* Macro definitions:
* (def @type fb:type.object.type)
* Iteration:
* (for @x (a an the) (rule $DT (@x) (IdentityFn)))
*
* @author Percy Liang
*/
public class Grammar {
public static class Options {
@Option public List<String> inPaths = new ArrayList<>();
@Option(gloss = "Variables which are used to interpret the grammar file")
public List<String> tags = new ArrayList<>();
@Option public boolean binarizeRules = true;
@Option(gloss = "Specifiy which ApplyFn to use: defaults to JoinFn when null")
public String useApplyFn = null;
}
public static Options opts = new Options();
// All the rules in the grammar. Each parser can read these and transform
// them however the parser wishes.
// This contains binarized rules
ArrayList<Rule> rules = new ArrayList<>();
public List<Rule> getRules() { return rules; }
Map<String, LispTree> macros = new HashMap<>(); // Map from macro name to its replacement value
Set<String> validTags = new TreeSet<>(); // All tags in the grammar file
// Verbatim copy of all the lines read, so we can preserve the grammar file.
List<String> statements = new ArrayList<>();
public void read() {
LogInfo.begin_track("Grammar.read");
read(opts.inPaths);
LogInfo.logs("%s rules", rules.size());
LogInfo.end_track();
}
public void read(String path) { read(Collections.singletonList(path)); }
public void read(List<String> paths) {
for (String path : paths)
readOnePath(path, Sets.newHashSet(opts.tags));
verifyValid();
}
private void verifyValid() {
// Make sure that all the categories which are used are actually defined.
Set<String> defined = new HashSet<>();
defined.add(Rule.tokenCat);
defined.add(Rule.phraseCat);
defined.add(Rule.lemmaTokenCat);
defined.add(Rule.lemmaPhraseCat);
for (Rule rule : rules)
defined.add(rule.lhs);
// Make sure every non-terminal is defined
for (Rule rule : rules) {
for (String item : rule.rhs) {
if (Rule.isCat(item) && !defined.contains(item)) {
LogInfo.warnings("Category not defined in the grammar: %s; used in rule: %s", item, rule);
}
}
}
// Check if all tags are defined in a grammar file
LogInfo.logs("Valid tags: %s", validTags);
LogInfo.logs("Used tags: %s", new TreeSet<>(opts.tags));
for (String tag : opts.tags) {
if (!validTags.contains(tag)) {
LogInfo.warnings("Tag %s not defined in grammar", tag);
}
}
}
/**
* @param contextPath Path relative to which grammar includes and
* such are expanded.
*/
public void addStatement(String stmt, String contextPath, Set<String> tags) {
statements.add(stmt);
interpret(contextPath, LispTree.proto.parseFromString(stmt), Sets.newHashSet(Iterables.concat(tags, opts.tags)));
}
public void addStatement(String stmt, String contextPath) {
Set<String> s = Collections.emptySet();
addStatement(stmt, null, s);
}
public void addStatement(String stmt) {
addStatement(stmt, null);
}
private static boolean isValidVar(String var) {
return var.startsWith("@");
}
private static void checkIsValidVar(String var) {
if (!isValidVar("@"))
throw new RuntimeException("Invalid variable: '" + var + "' doesn't start with '@'");
}
// Replace all leaves of LispTree with value in macros if exists
private static LispTree applyMacros(Map<String, LispTree> macros, LispTree tree) {
if (tree.isLeaf()) {
LispTree replacement = macros.get(tree.value);
if (replacement != null) return replacement;
if (isValidVar(tree.value))
throw new RuntimeException("Undefined macro: " + tree.value);
else
return tree;
}
LispTree newTree = LispTree.proto.newList();
for (LispTree child : tree.children)
newTree.addChild(applyMacros(macros, child));
return newTree;
}
// Apply the macro substitutions to |tree|.
public LispTree applyMacros(LispTree tree) {
return applyMacros(this.macros, tree);
}
private void readOnePath(String path, Set<String> tags) {
// Save raw lines
if (statements.size() > 0) statements.add("");
statements.add("####### " + path);
for (String line : IOUtils.readLinesHard(path))
statements.add(line);
Iterator<LispTree> trees = LispTree.proto.parseFromFile(path);
while (trees.hasNext()) {
LispTree tree = trees.next();
interpret(path, tree, tags);
collectValidTags(tree);
}
}
public void write() {
String path = Execution.getFile("grammar");
if (path == null) return;
PrintWriter out = IOUtils.openOutHard(path);
for (String statement : statements)
out.println(statement);
out.close();
out = IOUtils.openOutHard(Execution.getFile("processed-grammar"));
for (Rule rule : rules)
out.println(rule.toLispTree().toString());
out.close();
}
private void interpret(String path, LispTree tree, Set<String> tags) {
if (tree.isLeaf())
throw new RuntimeException("Expected list, got " + tree);
try {
String command = tree.child(0).value;
if ("rule".equals(command)) {
interpretRule(tree);
} else if ("include".equals(command)) {
if (path == null) {
throw new RuntimeException(
"Grammar include statement given without context path");
}
for (int i = 1; i < tree.children.size(); i++)
readOnePath(new File(path).getParent() + "/" + tree.child(i).value, tags);
} else if ("when".equals(command)) {
if (interpretBoolean(tree.child(1), tags)) {
for (int i = 2; i < tree.children.size(); i++)
interpret(path, tree.child(i), tags);
}
} else if ("def".equals(command)) {
interpretMacroDef(tree);
} else if ("for".equals(command)) {
interpretFor(path, tree, tags);
} else {
throw new RuntimeException("Invalid command: " + command);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error on " + tree + ": " + e);
}
}
private boolean interpretBoolean(LispTree tree, Set<String> tags) {
if (tree.isLeaf())
return tags.contains(tree.value);
if ("not".equals(tree.child(0).value)) {
if (tree.children.size() != 2)
throw new RuntimeException("Too many arguments for not: " + tree);
return !interpretBoolean(tree.child(1), tags);
}
if ("and".equals(tree.child(0).value)) {
for (int i = 1; i < tree.children.size(); i++)
if (!interpretBoolean(tree.child(i), tags)) return false;
return true;
}
if ("or".equals(tree.child(0).value)) {
for (int i = 1; i < tree.children.size(); i++)
if (interpretBoolean(tree.child(i), tags)) return true;
return false;
}
throw new RuntimeException("Expected a single tag, but got: " + tree);
}
public void interpretMacroDef(LispTree tree) {
if (tree.children.size() != 3 || !tree.child(1).isLeaf()) {
throw new RuntimeException("Invalid usage (def |name| |value|): " + tree);
}
String var = tree.child(1).value;
checkIsValidVar(var);
macros.put(var, applyMacros(tree.child(2)));
}
public void interpretFor(String path, LispTree tree, Set<String> tags) {
if (tree.children.size() <= 3 || !tree.child(1).isLeaf()) {
throw new RuntimeException("Invalid usage (for |var| (|value| ... |value|) |statement| ...) " + tree);
}
String var = tree.child(1).value;
checkIsValidVar(var);
List<LispTree> values = applyMacros(tree.child(2)).children;
LispTree old = macros.get(var);
for (LispTree value : values) {
macros.put(var, value);
for (int j = 3; j < tree.children.size(); j++)
interpret(path, tree.child(j), tags);
}
if (old == null) macros.remove(var);
else macros.put(var, old);
}
private static String checkCatName(String cat) {
if (isIntermediate(cat))
LogInfo.warnings("Category '%s' starts with '$Intermediate'; please avoid this unless you know what you are doing.");
return cat;
}
private void interpretRule(LispTree tree) {
if (tree.children.size() < 4)
throw new RuntimeException("Invalid rule: " + tree);
// (rule lhs rhs semantics (key value) ... (key value))
tree = applyMacros(tree);
// Parse LHS
if (!tree.child(1).isLeaf())
throw new RuntimeException("Invalid LHS: " + tree.child(1));
String lhs = checkCatName(tree.child(1).value);
// Parse RHS
List<String> rhs = Lists.newArrayList();
List<Boolean> isOptionals = new ArrayList<>();
LispTree rhsTree = tree.child(2);
if (rhsTree.isLeaf())
throw new RuntimeException("RHS needs to be list, but got: " + rhsTree);
for (int i = 0; i < rhsTree.children.size(); i++) {
LispTree child = rhsTree.child(i);
boolean isOptional = false;
if (child.isLeaf()) { // $PHRASE
rhs.add(checkCatName(child.value));
} else {
// e.g., ($PHRASE optional)
// First item is the token/category; the rest of the items
// specify parameters (currently, only parameter is optional).
rhs.add(checkCatName(child.child(0).value));
for (int j = 1; j < child.children.size(); j++)
if (child.child(j).value.equals("optional"))
isOptional = true;
}
isOptionals.add(isOptional);
}
// Parse semantic function
SemanticFn sem = parseSemanticFn(tree.child(3));
Rule rule = new Rule(lhs, rhs, sem);
// Parse extra info
for (int i = 4; i < tree.children.size(); i++) {
LispTree item = tree.child(i);
if (!item.isLeaf() && item.children.size() != 2)
throw new RuntimeException("Invalid key-value pair: " + item);
try {
rule.addInfo(item.child(0).value, Double.parseDouble(item.child(1).value));
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid key-value pair: " + item);
}
}
addRule(rule, isOptionals);
}
public synchronized void addRule(Rule rule) {
List<Boolean> isOptionals = new ArrayList<>();
for (int i = 0; i < rule.rhs.size(); i++) isOptionals.add(false);
addRule(rule, isOptionals);
}
// Add a rule to the grammar.
public synchronized void addRule(Rule rule, List<Boolean> isOptionals) {
rules.addAll(binarizeRule(rule, isOptionals));
}
private void collectValidTags(LispTree tree) {
if (tree.isLeaf())
throw new RuntimeException("Expected list, got " + tree);
try {
String command = tree.child(0).value;
if ("when".equals(command)) {
collectValidTagsBoolean(tree.child(1));
for (int i = 2; i < tree.children.size(); i++)
collectValidTags(tree.child(i));
} else if ("for".equals(command)) {
for (int i = 3; i < tree.children.size(); i++)
collectValidTags(tree.child(i));
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error on " + tree + ": " + e);
}
}
private void collectValidTagsBoolean(LispTree tree) {
if (tree.isLeaf()) {
validTags.add(tree.value);
return;
}
String command = tree.child(0).value;
if ("not".equals(command))
collectValidTagsBoolean(tree.child(1));
else if ("and".equals(command) || "or".equals(command))
for (int i = 1; i < tree.children.size(); i++)
collectValidTagsBoolean(tree.child(i));
}
// Generate intermediate categories for binarization.
public static final String INTERMEDIATE_PREFIX = "$Intermediate";
private int freshCatIndex = 0;
private String generateFreshCat() {
freshCatIndex++;
return INTERMEDIATE_PREFIX + freshCatIndex;
}
public static boolean isIntermediate(String cat) {
return cat.startsWith(INTERMEDIATE_PREFIX);
}
// Create multiple versions of this rule if there are optional RHS.
// Restriction: must be able to split the RHS into two halves, each of
// which contains at most one non-optional category.
// Recall that only the non-optional categories on the RHS are arguments into the SemanticFn.
// Example: stop? $A stop $Stop? $B stop $Stop?
private List<Rule> binarizeRule(Rule rule, List<Boolean> isOptionals) {
List<Rule> newRules = new ArrayList<>();
// Don't binarize: do same as before
if (!opts.binarizeRules) {
if (isOptionals.contains(true))
throw new RuntimeException("Can't have optionals if don't binarize: " + rule + " " + isOptionals);
newRules.add(rule);
return newRules;
}
// Special case: JoinFn with an arg0Fn but with multiple non-optional categories.
// In this case, we need to use arg0Fn on just the first such category, and
// then use function application on the rest.
// Old: (rule $A (a ($Z optional) $B $C $D) (JoinFn (arg0 (lambda b (lambda c (lambda d ...))))))
// New rules to binarize ($B is the first non-optional category):
// 1. (rule $I1 (a ($Z optional) $B) (JoinFn (arg0 (lambda b (lambda c (lambda d ...))))))
// 2. (rule $A ($I1 $C $D) (JoinFn forward betaReduce))
if (rule.sem instanceof JoinFn && ((JoinFn) rule.sem).getArg0Fn() != null) {
// Find the first non-optional category
int i = 0;
while (i < rule.rhs.size() && !(Rule.isCat(rule.rhs.get(i)) && !isOptionals.get(i)))
i++;
// Find the next non-optional category
int j = i + 1;
while (j < rule.rhs.size() && !(Rule.isCat(rule.rhs.get(j)) && !isOptionals.get(j)))
j++;
// If one exists, then we have to invoke special binarization
if (j < rule.rhs.size()) {
// Create an intermediate category
String intCat = generateFreshCat();
// Add rule 1
List<String> rhs1 = new ArrayList<>(rule.rhs.subList(0, i + 1));
newRules.addAll(binarizeRule(new Rule(intCat, rhs1, rule.sem).setInfo(rule), isOptionals.subList(0, i + 1)));
// Add rule 2
List<String> rhs2 = new ArrayList<>();
rhs2.add(intCat);
rhs2.addAll(rule.rhs.subList(i + 1, rule.rhs.size()));
SemanticFn forwardBetaReduce = new JoinFn();
forwardBetaReduce.init(LispTree.proto.parseFromString("(JoinFn forward betaReduce)"));
newRules.addAll(binarizeRule(new Rule(rule.lhs, rhs2, forwardBetaReduce).setInfo(rule), isOptionals.subList(i, isOptionals.size())));
return newRules;
}
}
if (!isOptionals.contains(false))
throw new RuntimeException("Can't have all RHS items be optional: " + rule + " " + isOptionals);
// Zero-aries (for floating rules) and Unaries: don't need to binarize
if (rule.rhs.size() <= 1) {
newRules.add(rule);
return newRules;
}
// Stores the current RHS that we're building up.
List<String> newRhs = new ArrayList<>();
List<Boolean> newIsOptional = new ArrayList<>();
List<Boolean> isRequiredCat = new ArrayList<>(); // These are the arguments to the semantic function
// Left-binarize.
assert rule.rhs.size() >= 2;
boolean appliedRuleSem = false;
for (int i = 0; i < rule.rhs.size(); i++) {
newRhs.add(rule.rhs.get(i));
newIsOptional.add(isOptionals.get(i));
isRequiredCat.add(!isOptionals.get(i) && Rule.isCat(rule.rhs.get(i)));
// Aim is to create rules with two RHS required categories
// (binarized rules + tokens which don't cost anything).
// Sometimes semantic functions take more than one argument.
// Note: we assume they just left-binarize.
if (newRhs.size() < 2) // This should only happen in the beginning
continue;
if (isRequiredCat.get(0) && isRequiredCat.get(1))
appliedRuleSem = true;
boolean atEnd = (i == rule.rhs.size() - 1);
// TODO(pliang): currently, too many intermediate categories are created. Remove
// to make flatter grammars (will generate fewer derivations).
String lhs = atEnd && appliedRuleSem ? rule.lhs : generateFreshCat();
// Create rule with newRhs possibly excluding the optionals (there should be at most 2)
assert (newRhs.size() == 2);
boolean allCanBeOptional = false;
for (int b0 = 0; b0 < 2; b0++) { // Whether to include newRhs.get(0)
if (b0 == 0 && !newIsOptional.get(0)) continue;
for (int b1 = 0; b1 < 2; b1++) { // Whether to include newRhs.get(1)
if (b1 == 0 && !newIsOptional.get(1)) continue;
List<String> rhs = Lists.newArrayList();
SemanticFn sem;
if (b0 == 1) rhs.add(newRhs.get(0));
if (b1 == 1) rhs.add(newRhs.get(1));
if (isRequiredCat.get(0) && isRequiredCat.get(1))
sem = rule.sem;
else if (b0 == 1 && Rule.isCat(rhs.get(0)) && isRequiredCat.get(1))
sem = new SelectFn(1);
else if (isRequiredCat.get(0) || isRequiredCat.get(1))
sem = new SelectFn(0);
else
sem = new ConstantFn(Formula.nullFormula);
// We can't allow empty RHS, but if we need it, just mark it as all
// can be optional.
if (rhs.size() > 0)
newRules.add(new Rule(lhs, rhs, sem).setInfo(rule));
else
allCanBeOptional = true;
}
}
boolean req = isRequiredCat.get(0) || isRequiredCat.get(1);
// Replace with new category.
newRhs.clear(); newRhs.add(lhs);
newIsOptional.clear(); newIsOptional.add(allCanBeOptional);
isRequiredCat.clear(); isRequiredCat.add(req);
}
assert newRhs.size() == 1;
assert !newIsOptional.get(0);
// Final unary rule if needed
if (!appliedRuleSem)
newRules.add(new Rule(rule.lhs, Lists.newArrayList(newRhs), rule.sem).setInfo(rule));
// LogInfo.begin_track("binarize %s", rule);
// for (Rule r : newRules) LogInfo.logs("%s", r);
// LogInfo.end_track();
return newRules;
}
// Examples of |tree|
// (ConstantFn null)
// (NumberFn)
private SemanticFn parseSemanticFn(LispTree tree) {
// Syntactic sugar: foo => (ConstantFn foo)
if (tree.isLeaf()) {
LispTree newTree = LispTree.proto.newList();
newTree.addChild("ConstantFn");
newTree.addChild(tree.value);
tree = newTree;
}
String name = tree.child(0).value;
// Syntactic sugar: (lambda x (f (var x))) => (useApplyFn (lambda x (f (var x))))
// defaults to (lambda x (var x)) => (JoinFn betaReduce forward (arg0 (lambda x (var x))))
if (name.equals("lambda")) {
LispTree newTree = LispTree.proto.newList();
if (Grammar.opts.useApplyFn == null) {
newTree.addChild("JoinFn");
newTree.addChild("betaReduce");
newTree.addChild("forward");
newTree.addChild(LispTree.proto.newList("arg0", tree));
} else {
newTree.addChild(Grammar.opts.useApplyFn);
newTree.addChild(tree);
}
tree = newTree;
name = tree.child(0).value;
}
// For backward compatibility: SemanticFn which have moved.
if (name.equals("LexiconFn") || name.equals("BridgeFn"))
name = "freebase." + name;
SemanticFn fn;
fn = (SemanticFn) Utils.newInstanceHard(SempreUtils.resolveClassName(name));
if (fn == null)
throw new RuntimeException("Invalid SemanticFn name: " + name);
fn.init(tree);
return fn;
}
}