package edu.stanford.nlp.sempre.interactive;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.common.collect.Lists;
import com.google.common.collect.ObjectArrays;
import com.google.common.collect.Sets;
import edu.stanford.nlp.sempre.*;
import fig.basic.LogInfo;
import fig.basic.Option;
/**
* Handles action lambda DCS where the world has a flat structure, i.e. a list
* of allitems all supporting the same operations supports ActionFormula here,
* and does conversions of singleton sets
*
* @author sidaw
*/
public class DALExecutor extends Executor {
public static class Options {
@Option(gloss = "Whether to convert NumberValue to int/double")
public boolean convertNumberValues = true;
@Option(gloss = "Whether to convert name values to string literal")
public boolean convertNameValues = true;
@Option(gloss = "Print stack trace on exception")
public boolean printStackTrace = false;
// the actual function will be called with the current ContextValue as its
// last argument if marked by contextPrefix
@Option(gloss = "Reduce verbosity by automatically appending, for example, edu.stanford.nlp.sempre to java calls")
public String classPathPrefix = "edu.stanford.nlp.sempre";
@Option(gloss = "The type of world used")
public String worldType = "VoxelWorld";
@Option(gloss = "the maximum number of primitive calls until we stop executing")
public int maxSteps = 1000;
@Option(gloss = "The maximum number of while calls")
public int maxWhile = 20;
}
public static Options opts = new Options();
@Override
public Response execute(Formula formula, ContextValue context) {
// We can do beta reduction here since macro substitution preserves the
// denotation (unlike for lambda DCS).
World world = World.fromContext(opts.worldType, context);
formula = Formulas.betaReduction(formula);
try {
performActions((ActionFormula) formula, world);
return new Response(new StringValue(world.toJSON()));
} catch (Exception e) {
// Comment this out if we expect lots of innocuous type checking failures
if (opts.printStackTrace) {
LogInfo.log("Failed to execute " + formula.toString());
e.printStackTrace();
}
return new Response(ErrorValue.badJava(e.toString()));
}
}
@SuppressWarnings("rawtypes")
private void performActions(ActionFormula f, World world) {
if (f.mode == ActionFormula.Mode.primitive) {
// use reflection to call primitive stuff
Value method = ((ValueFormula) f.args.get(0)).value;
String id = ((NameValue) method).id;
// all actions takes a fixed set as argument
invoke(id, world, f.args.subList(1, f.args.size()).stream().map(x -> processSetFormula(x, world)).toArray());
world.merge();
} else if (f.mode == ActionFormula.Mode.sequential) {
for (Formula child : f.args) {
performActions((ActionFormula) child, world);
}
} else if (f.mode == ActionFormula.Mode.repeat) {
Set<Object> arg = toSet(processSetFormula(f.args.get(0), world));
if (arg.size() > 1)
throw new RuntimeException("repeat has to take a single number");
int times;
if (!opts.convertNumberValues)
times = (int) ((NumberValue) arg.iterator().next()).value;
else
times = (int) arg.iterator().next();
for (int i = 0; i < times; i++)
performActions((ActionFormula) f.args.get(1), world);
} else if (f.mode == ActionFormula.Mode.conditional) {
// using the empty set to represent false
boolean cond = toSet(processSetFormula(f.args.get(0), world)).iterator().hasNext();
if (cond)
performActions((ActionFormula) f.args.get(1), world);
} else if (f.mode == ActionFormula.Mode.whileloop) {
// using the empty set to represent false
boolean cond = toSet(processSetFormula(f.args.get(0), world)).iterator().hasNext();
for (int i = 0; i < opts.maxWhile; i++) {
if (cond)
performActions((ActionFormula) f.args.get(1), world);
else
break;
cond = toSet(processSetFormula(f.args.get(0), world)).iterator().hasNext();
}
} else if (f.mode == ActionFormula.Mode.forset) {
// mostly deprecated
Set<Object> selected = toSet(processSetFormula(f.args.get(0), world));
Set<Item> prevSelected = world.selected;
world.selected = toItemSet(selected);
performActions((ActionFormula) f.args.get(1), world);
world.selected = prevSelected;
world.merge();
} else if (f.mode == ActionFormula.Mode.foreach) {
Set<Item> selected = toItemSet(toSet(processSetFormula(f.args.get(0), world)));
Set<Item> prevSelected = world.selected;
// CopyOnWriteArraySet<Object> fixedset =
// Sets.newCopyOnWriteArraySet(selected);
Iterator<Item> iterator = selected.iterator();
while (iterator.hasNext()) {
world.selected = (toItemSet(toSet(iterator.next())));
performActions((ActionFormula) f.args.get(1), world);
}
world.selected = prevSelected;
world.merge();
} else if (f.mode == ActionFormula.Mode.isolate) {
Set<Item> prevAll = world.allItems;
// Set<Item> prevSelected = world.selected;
// Set<Item> prevPrevious = world.previous;
if (f.args.size() > 1)
throw new RuntimeException("No longer supporting this isolate formula: " + f);
world.allItems = Sets.newHashSet(world.selected);
// world.selected = scope;
// world.previous = scope;
performActions((ActionFormula) f.args.get(0), world);
world.allItems.addAll(prevAll); // merge, overriding;
// world.selected = prevSelected;
// world.previous = prevPrevious;
world.merge();
} else if (f.mode == ActionFormula.Mode.block || f.mode == ActionFormula.Mode.blockr) {
// we should never mutate selected in actions
Set<Item> prevSelected = world.selected;
Set<Item> prevPrevious = world.previous;
world.previous = world.selected;
for (Formula child : f.args) {
performActions((ActionFormula) child, world);
}
// restore on default blocks
if (f.mode == ActionFormula.Mode.block) {
world.selected = prevSelected;
world.merge();
}
// LogInfo.logs("CBlocking prevselected=%s selected=%s all=%s",
// prevSelected, world.selected, world.allitems);
// LogInfo.logs("BlockingWorldIs %s", world.toJSON());
world.previous = prevPrevious;
}
// } else if (f.mode == ActionFormula.Mode.let) {
// // let declares a new local variable
// // set access and reassigns the value of some variable
// // block determines what is considered local scope
// // for now the use case is just (:blk (:let x this) (:blah) (:set this
// x))
// Set<Item> varset = toItemSet(toSet(processSetFormula(f.args.get(1),
// world)));
// Value method = ((ValueFormula)f.args.get(0)).value;
// String varname = ((NameValue)method).id;
// world.variables.put(varname, varset);
// } else if (f.mode == ActionFormula.Mode.set) {
// Set<Item> varset = toItemSet(toSet(processSetFormula(f.args.get(1),
// world)));
// Value method = ((ValueFormula)f.args.get(0)).value;
// String varname = ((NameValue)method).id;
// world.variables.get(varname).clear();
// world.variables.get(varname).addAll(varset);
// }
}
@SuppressWarnings("unchecked")
private Set<Object> toSet(Object maybeSet) {
if (maybeSet instanceof Set)
return (Set<Object>) maybeSet;
else
return Sets.newHashSet(maybeSet);
}
private Object toElement(Set<Object> set) {
if (set.size() == 1) {
return set.iterator().next();
}
return set;
}
private Set<Item> toItemSet(Set<Object> maybeItems) {
Set<Item> itemset = maybeItems.stream().map(i -> (Item) i).collect(Collectors.toSet());
return itemset;
}
static class SpecialSets {
static String All = "*";
static String EmptySet = "nothing";
static String This = "this"; // current scope if it exists, otherwise the
// globally marked object
static String Previous = "prev"; // global variable for selected
static String Selected = "selected"; // global variable for selected
};
// a subset of lambda dcs. no types, and no marks
// if this gets any more complicated, you should consider the
// LambdaDCSExecutor
@SuppressWarnings("unchecked")
private Object processSetFormula(Formula formula, final World world) {
if (formula instanceof ValueFormula<?>) {
Value v = ((ValueFormula<?>) formula).value;
// special unary
if (v instanceof NameValue) {
String id = ((NameValue) v).id;
// LogInfo.logs("%s : this %s, all: %s", id,
// world.selected().toString(), world.allitems.toString());
if (id.equals(SpecialSets.All))
return world.all();
if (id.equals(SpecialSets.This))
return world.selected();
if (id.equals(SpecialSets.Selected))
return world.selected();
if (id.equals(SpecialSets.EmptySet))
return world.empty();
if (id.equals(SpecialSets.Previous))
return world.previous();
}
return toObject(((ValueFormula<?>) formula).value);
}
if (formula instanceof JoinFormula) {
JoinFormula joinFormula = (JoinFormula) formula;
if (joinFormula.relation instanceof ValueFormula) {
String rel = ((ValueFormula<NameValue>) joinFormula.relation).value.id;
Set<Object> unary = toSet(processSetFormula(joinFormula.child, world));
return world.has(rel, unary);
} else if (joinFormula.relation instanceof ReverseFormula) {
ReverseFormula reverse = (ReverseFormula) joinFormula.relation;
String rel = ((ValueFormula<NameValue>) reverse.child).value.id;
Set<Object> unary = toSet(processSetFormula(joinFormula.child, world));
return world.get(rel, toItemSet(unary));
} else {
throw new RuntimeException("relation can either be a value, or its reverse");
}
}
if (formula instanceof MergeFormula) {
MergeFormula mergeFormula = (MergeFormula) formula;
MergeFormula.Mode mode = mergeFormula.mode;
Set<Object> set1 = toSet(processSetFormula(mergeFormula.child1, world));
Set<Object> set2 = toSet(processSetFormula(mergeFormula.child2, world));
if (mode == MergeFormula.Mode.or)
return Sets.union(set1, set2);
if (mode == MergeFormula.Mode.and)
return Sets.intersection(set1, set2);
}
if (formula instanceof NotFormula) {
NotFormula notFormula = (NotFormula) formula;
Set<Item> set1 = toItemSet(toSet(processSetFormula(notFormula.child, world)));
return Sets.difference(world.allItems, set1);
}
if (formula instanceof AggregateFormula) {
AggregateFormula aggregateFormula = (AggregateFormula) formula;
Set<Object> set = toSet(processSetFormula(aggregateFormula.child, world));
AggregateFormula.Mode mode = aggregateFormula.mode;
if (mode == AggregateFormula.Mode.count)
return Sets.newHashSet(set.size());
if (mode == AggregateFormula.Mode.max)
return Sets.newHashSet(set.stream().max((s, t) -> ((NumberValue) s).value > ((NumberValue) t).value ? 1 : -1));
if (mode == AggregateFormula.Mode.min)
return Sets.newHashSet(set.stream().max((s, t) -> ((NumberValue) s).value < ((NumberValue) t).value ? 1 : -1));
}
if (formula instanceof ArithmeticFormula) {
ArithmeticFormula arithmeticFormula = (ArithmeticFormula) formula;
Integer arg1 = (Integer) processSetFormula(arithmeticFormula.child1, world);
Integer arg2 = (Integer) processSetFormula(arithmeticFormula.child2, world);
ArithmeticFormula.Mode mode = arithmeticFormula.mode;
if (mode == ArithmeticFormula.Mode.add)
return arg1 + arg2;
if (mode == ArithmeticFormula.Mode.sub)
return arg1 - arg2;
if (mode == ArithmeticFormula.Mode.mul)
return arg1 * arg2;
if (mode == ArithmeticFormula.Mode.div)
return arg1 / arg2;
}
if (formula instanceof CallFormula) {
CallFormula callFormula = (CallFormula) formula;
@SuppressWarnings("rawtypes")
Value method = ((ValueFormula) callFormula.func).value;
String id = ((NameValue) method).id;
// all actions takes a fixed set as argument
return invoke(id, world, callFormula.args.stream().map(x -> processSetFormula(x, world)).toArray());
}
if (formula instanceof SuperlativeFormula) {
throw new RuntimeException("SuperlativeFormula is not implemented");
}
throw new RuntimeException("ActionExecutor does not handle this formula type: " + formula.getClass());
}
// Example: id = "Math.cos". similar to JavaExecutor's invoke,
// but matches arg by building singleton set as needed
private Object invoke(String id, World thisObj, Object... args) {
Method[] methods;
Class<?> cls;
String methodName;
boolean isStatic = thisObj == null;
if (isStatic) { // Static methods
int i = id.lastIndexOf('.');
if (i == -1) {
throw new RuntimeException("Expected <class>.<method>, but got: " + id);
}
String className = id.substring(0, i);
methodName = id.substring(i + 1);
try {
cls = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
methods = cls.getMethods();
} else { // Instance methods
cls = thisObj.getClass();
methodName = id;
methods = cls.getMethods();
}
// Find a suitable method
List<Method> nameMatches = Lists.newArrayList();
Method bestMethod = null;
int bestCost = INVALID_TYPE_COST;
for (Method m : methods) {
if (!m.getName().equals(methodName))
continue;
m.setAccessible(true);
nameMatches.add(m);
if (isStatic != Modifier.isStatic(m.getModifiers()))
continue;
int cost = typeCastCost(m.getParameterTypes(), args);
// append optional selected parameter when needed:
if (cost == INVALID_TYPE_COST && args.length + 1 == m.getParameterCount()) {
args = ObjectArrays.concat(args, thisObj.selected);
cost = typeCastCost(m.getParameterTypes(), args);
}
if (cost < bestCost) {
bestCost = cost;
bestMethod = m;
}
}
if (bestMethod != null) {
try {
return bestMethod.invoke(thisObj, args);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getCause());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
List<String> types = Lists.newArrayList();
for (Object arg : args)
types.add(arg.getClass().toString());
throw new RuntimeException("Method " + methodName + " not found in class " + cls + " with arguments "
+ Arrays.asList(args) + " having types " + types + "; candidates: " + nameMatches);
}
private int typeCastCost(Class[] types, Object[] args) {
if (types.length != args.length)
return INVALID_TYPE_COST;
int cost = 0;
for (int i = 0; i < types.length; i++) {
// deal with singleton sets
if (types[i] == Set.class)
args[i] = toSet(args[i]);
if (types[i] != Set.class && args[i].getClass() == Set.class) {
args[i] = toElement((Set<Object>) args[i]);
}
cost += typeCastCost(types[i], args[i]);
if (cost >= INVALID_TYPE_COST) {
LogInfo.dbgs("NOT COMPATIBLE: want %s, got %s with type %s", types[i], args[i], args[i].getClass());
break;
}
}
return cost;
}
private static Object toObject(Value value) {
if (value instanceof NumberValue && opts.convertNumberValues) {
// Unfortunately, NumberValues don't make a distinction between ints and
// doubles, so this is a hack.
double x = ((NumberValue) value).value;
if (x == (int) x)
return new Integer((int) x);
return new Double(x);
} else if (value instanceof NameValue && opts.convertNameValues) {
String id = ((NameValue) value).id;
return id;
} else if (value instanceof BooleanValue) {
return ((BooleanValue) value).value;
} else if (value instanceof StringValue) {
return ((StringValue) value).value;
} else if (value instanceof ListValue) {
List<Object> list = Lists.newArrayList();
for (Value elem : ((ListValue) value).values)
list.add(toObject(elem));
return list;
} else {
return value; // Preserve the Value (which can be an object)
}
}
// Return whether the object |arg| is compatible with |type|.
// 0: perfect match
// 1: don't match, but don't lose anything
// 2: don't match, and can lose something
// INVALID_TYPE_COST: impossible
private int typeCastCost(Class<?> type, Object arg) {
if (arg == null)
return !type.isPrimitive() ? 0 : INVALID_TYPE_COST;
if (type.isInstance(arg))
return 0;
if (type == Boolean.TYPE)
return arg instanceof Boolean ? 0 : INVALID_TYPE_COST;
else if (type == Integer.TYPE) {
if (arg instanceof Integer)
return 0;
if (arg instanceof Long)
return 1;
return INVALID_TYPE_COST;
}
if (type == Long.TYPE) {
if (arg instanceof Integer)
return 1;
if (arg instanceof Long)
return 0;
return INVALID_TYPE_COST;
}
if (type == Float.TYPE) {
if (arg instanceof Integer)
return 1;
if (arg instanceof Long)
return 1;
if (arg instanceof Float)
return 0;
if (arg instanceof Double)
return 2;
return INVALID_TYPE_COST;
}
if (type == Double.TYPE) {
if (arg instanceof Integer)
return 1;
if (arg instanceof Long)
return 1;
if (arg instanceof Float)
return 1;
if (arg instanceof Double)
return 0;
return INVALID_TYPE_COST;
}
return INVALID_TYPE_COST;
}
private static final int INVALID_TYPE_COST = 1000;
}