package edu.stanford.nlp.sempre; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import fig.basic.MapUtils; import fig.basic.Option; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; /** * JavaExecutor takes a Formula which is composed recursively of CallFormulas, * does reflection, and returns a Value. * * @author Percy Liang */ public class JavaExecutor extends Executor { public static class Options { @Option(gloss = "Whether to convert NumberValue to int/double") public boolean convertNumberValues = 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 = "Formula in the grammar whose name startsWith contextPrefix is context sensitive") public String contextPrefix = "context:"; @Option(gloss = "Reduce verbosity by automatically appending, for example, edu.stanford.nlp.sempre to java calls") public String classPathPrefix = ""; // e.g. "edu.stanford.nlp.sempre"; } public static Options opts = new Options(); private static JavaExecutor defaultExecutor = new JavaExecutor(); // To simplify logical forms, define some shortcuts. private Map<String, String> shortcuts = Maps.newHashMap(); public JavaExecutor() { String className = BasicFunctions.class.getName(); shortcuts.put("+", className + ".plus"); shortcuts.put("-", className + ".minus"); shortcuts.put("*", className + ".times"); shortcuts.put("/", className + ".divide"); shortcuts.put("%", className + ".mod"); shortcuts.put("!", className + ".not"); shortcuts.put("<", className + ".lessThan"); shortcuts.put("<=", className + ".lessThanEq"); shortcuts.put("==", className + ".equals"); shortcuts.put(">", className + ".greaterThan"); shortcuts.put(">=", className + ".greaterThanEq"); shortcuts.put("if", className + ".ifThenElse"); shortcuts.put("map", className + ".map"); shortcuts.put("reduce", className + ".reduce"); shortcuts.put("select", className + ".select"); shortcuts.put("range", className + ".range"); } public static class BasicFunctions { public static double plus(double x, double y) { return x + y; } public static int plus(int x, int y) { return x + y; } public static int minus(int x, int y) { return x - y; } public static double minus(double x, double y) { return x - y; } public static int times(int x, int y) { return x * y; } public static double times(double x, double y) { return x * y; } public static int divide(int x, int y) { return x / y; } public static double divide(double x, double y) { return x / y; } public static int mod(int x, int y) { return x % y; } public static boolean not(boolean x) { return !x; } public static boolean lessThan(double x, double y) { return x < y; } public static boolean lessThanEq(double x, double y) { return x <= y; } public static boolean equals(double x, double y) { return x == y; } public static boolean greaterThan(double x, double y) { return x > y; } public static boolean greaterThanEq(double x, double y) { return x >= y; } public static Object ifThenElse(boolean b, Object x, Object y) { return b ? x : y; } // For very simple string concatenation public static String plus(String a, String b) { return a + b; } public static String plus(String a, String b, String c) { return a + b + c; } public static String plus(String a, String b, String c, String d) { return a + b + c + d; } public static String plus(String a, String b, String c, String d, String e) { return a + b + c + d + e; } public static String plus(String a, String b, String c, String d, String e, String f) { return a + b + c + d + e + f; } public static String plus(String a, String b, String c, String d, String e, String f, String g) { return a + b + c + d + e + f + g; } private static String toString(Object x) { if (x instanceof String) return (String) x; else if (x instanceof Value) return (x instanceof NameValue) ? ((NameValue) x).id : ((StringValue) x).value; else return null; } // Apply func to each element of |list| and return the resulting list. public static List<Object> map(List<Object> list, LambdaFormula func) { List<Object> newList = new ArrayList<Object>(); for (Object elem : list) { Object newElem = apply(func, elem); newList.add(newElem); } return newList; } // list = [3, 5, 2], func = (lambda x (lambda y (call + (var x) (var y)))) // Returns (3 + 5) + 2 = 10 public static Object reduce(List<Object> list, LambdaFormula func) { if (list.size() == 0) return null; Object x = list.get(0); for (int i = 1; i < list.size(); i++) x = apply(func, x, list.get(i)); return x; } // Return elements x of |list| such that func(x) is true. public static List<Object> select(List<Object> list, LambdaFormula func) { List<Object> newList = new ArrayList<Object>(); for (Object elem : list) { Object test = apply(func, elem); if ((Boolean) test) newList.add(elem); } return newList; } private static Object apply(LambdaFormula func, Object x) { // Apply the function func to x. In order to do that, need to convert x into a value. Formula formula = Formulas.lambdaApply(func, new ValueFormula<Value>(toValue(x))); return defaultExecutor.processFormula(formula, null); } private static Object apply(LambdaFormula func, Object x, Object y) { // Apply the function func to x and y. In order to do that, need to convert x into a value. Formula formula = Formulas.lambdaApply(func, new ValueFormula<Value>(toValue(x))); formula = Formulas.lambdaApply((LambdaFormula) formula, new ValueFormula<Value>(toValue(y))); return defaultExecutor.processFormula(formula, null); } public static List<Integer> range(int start, int end) { List<Integer> result = new ArrayList<Integer>(); for (int i = start; i < end; i++) result.add(i); return result; } } public Response execute(Formula formula, ContextValue context) { // We can do beta reduction here since macro substitution preserves the // denotation (unlike for lambda DCS). formula = Formulas.betaReduction(formula); try { return new Response(toValue(processFormula(formula, context))); } catch (Exception e) { // Comment this out if we expect lots of innocuous type checking failures if (opts.printStackTrace) e.printStackTrace(); return new Response(ErrorValue.badJava(e.toString())); } } private Object processFormula(Formula formula, ContextValue context) { if (formula instanceof ValueFormula) // Unpack value and convert to object (e.g., for ints) return toObject(((ValueFormula) formula).value); if (formula instanceof CallFormula) { // Invoke the function. // Recurse CallFormula call = (CallFormula) formula; Object func = processFormula(call.func, context); List<Object> args = Lists.newArrayList(); for (Formula arg : call.args) { args.add(processFormula(arg, context)); } if (!(func instanceof NameValue)) throw new RuntimeException("Invalid func: " + call.func + " => " + func); String id = ((NameValue) func).id; if (id.indexOf(opts.contextPrefix) != -1) { args.add(context); id = id.replace(opts.contextPrefix, ""); } id = MapUtils.get(shortcuts, id, id); // classPathPrefix, like edu.stanford.nlp.sempre.interactive if (!Strings.isNullOrEmpty(opts.classPathPrefix) && !id.startsWith(".") && !id.startsWith(opts.classPathPrefix)) { id = opts.classPathPrefix + "." + id; } if (id.startsWith(".")) // Instance method return invoke(id.substring(1), args.get(0), args.subList(1, args.size()).toArray(new Object[0])); else // Static method return invoke(id, null, args.toArray(new Object[0])); } // Just pass it through... return formula; } // Convert the Object back to a Value private static Value toValue(Object obj) { if (obj instanceof Value) return (Value) obj; if (obj instanceof Boolean) return new BooleanValue((Boolean) obj); if (obj instanceof Integer) return new NumberValue(((Integer) obj).intValue()); if (obj instanceof Double) return new NumberValue(((Double) obj).doubleValue()); if (obj instanceof String) return new StringValue((String) obj); if (obj instanceof List) { List<Value> list = Lists.newArrayList(); for (Object elem : (List) obj) list.add(toValue(elem)); return new ListValue(list); } throw new RuntimeException("Unhandled object: " + obj + " with class " + obj.getClass()); } // Convert a Value (which are specified in the formulas) to an Object (which // many Java functions take). 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 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) } } // Example: id = "Math.cos" private Object invoke(String id, Object 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); 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++) { 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; } // 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; }