// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.yql; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.util.Arrays; import java.util.List; import java.util.Map; /** * Represents a use of an operator against concrete arguments. The types of arguments depend on the operator. * The extension point of this scheme is the Operator rather than new types of Nodes. * Operators SHOULD take a fixed number of arguments -- wrap variable argument counts in Lists. */ final class OperatorNode { public static OperatorNode create(T operator, Object... args) { operator.checkArguments(args == null ? EMPTY_ARGS : args); return new OperatorNode(operator, args); } public static OperatorNode create(Location loc, T operator, Object... args) { operator.checkArguments(args == null ? EMPTY_ARGS : args); return new OperatorNode(loc, operator, args); } public static OperatorNode create(Location loc, Map annotations, T operator, Object... args) { operator.checkArguments(args == null ? EMPTY_ARGS : args); return new OperatorNode(loc, annotations, operator, args); } private static final Object[] EMPTY_ARGS = new Object[0]; private final Location location; private final T operator; private Map annotations = ImmutableMap.of(); private final Object[] args; private OperatorNode(T operator, Object... args) { this.location = null; this.operator = operator; if (args == null) { this.args = EMPTY_ARGS; } else { this.args = args; } } private OperatorNode(Location loc, T operator, Object... args) { this.location = loc; this.operator = operator; if (args == null) { this.args = EMPTY_ARGS; } else { this.args = args; } } private OperatorNode(Location loc, Map annotations, T operator, Object... args) { this.location = loc; this.operator = operator; this.annotations = ImmutableMap.copyOf(annotations); if (args == null) { this.args = EMPTY_ARGS; } else { this.args = args; } } public T getOperator() { return operator; } public Object[] getArguments() { // this is only called by a test right now, but ImmutableList.copyOf won't tolerate null elements if (args.length == 0) { return args; } Object[] copy = new Object[args.length]; System.arraycopy(args, 0, copy, 0, args.length); return copy; } public T getArgument(int i) { return (T) args[i]; } public T getArgument(int i, Class clazz) { return clazz.cast(getArgument(i)); } public Location getLocation() { return location; } public Object getAnnotation(String name) { return annotations.get(name); } public OperatorNode putAnnotation(String name, Object value) { if (annotations.isEmpty()) { annotations = Maps.newLinkedHashMap(); } else if (annotations instanceof ImmutableMap) { annotations = Maps.newLinkedHashMap(annotations); } annotations.put(name, value); return this; } public Map getAnnotations() { // TODO: this should be a read-only view? return ImmutableMap.copyOf(annotations); } public OperatorNode transform(Function argumentTransform) { if (args.length == 0) { // nothing to transform, so no change is possible return this; } Object[] newArgs = new Object[args.length]; boolean changed = false; for (int i = 0; i < args.length; ++i) { Object target = args[i]; if (target instanceof List) { List newList = Lists.newArrayListWithExpectedSize(((List) target).size()); for (Object val : (List) target) { newList.add(argumentTransform.apply(val)); } newArgs[i] = newList; // this will always 'change' the tree, maybe fix later } else { newArgs[i] = argumentTransform.apply(args[i]); } changed = changed || newArgs[i] != args[i]; } if (changed) { return new OperatorNode<>(location, annotations, operator, newArgs); } return this; } public void visit(OperatorVisitor visitor) { if (visitor.enter(this)) { for (Object target : args) { if (target instanceof List) { for (Object val : (List) target) { if (val instanceof OperatorNode) { ((OperatorNode) val).visit(visitor); } } } else if (target instanceof OperatorNode) { ((OperatorNode) target).visit(visitor); } } } visitor.exit(this); } // we are aware only of types used in our logical operator trees -- OperatorNode, List, and constant values private static final Function COPY = new Function<>() { @Override public Object apply(Object input) { if (input instanceof List) { List newList = Lists.newArrayListWithExpectedSize(((List) input).size()); for (Object val : (List) input) { newList.add(COPY.apply(val)); } return newList; } else if (input instanceof OperatorNode) { return ((OperatorNode) input).copy(); } else if (input instanceof String || input instanceof Number || input instanceof Boolean) { return input; } else { // this may be annoying but COPY not understanding how to COPY and quietly reusing // when it may not be immutable could be dangerous throw new IllegalArgumentException("Unexpected value type in OperatorNode tree: " + input); } } }; public OperatorNode copy() { Object[] newArgs = new Object[args.length]; for (int i = 0; i < args.length; ++i) { newArgs[i] = COPY.apply(args[i]); } return new OperatorNode<>(location, ImmutableMap.copyOf(annotations), operator, newArgs); } public void toString(StringBuilder output) { output.append("(") .append(operator.name()); if(location != null) { output.append(" L") .append(location.getCharacterOffset()) .append(":") .append(location.getLineNumber()); } if(annotations != null && !annotations.isEmpty()) { output.append(" {"); Joiner.on(", ").withKeyValueSeparator("=") .appendTo(output, annotations); output.append("}"); } boolean first = true; for(Object arg : args) { if(!first) { output.append(","); } first = false; output.append(" "); if(arg instanceof OperatorNode) { ((OperatorNode) arg).toString(output); } else if(arg instanceof Iterable) { output.append("["); Joiner.on(", ").appendTo(output, (Iterable)arg); output.append("]"); } else { output.append(arg.toString()); } } output.append(")"); } public String toString() { StringBuilder output = new StringBuilder(); toString(output); return output.toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OperatorNode that = (OperatorNode) o; if (!annotations.equals(that.annotations)) return false; // Probably incorrect - comparing Object[] arrays with Arrays.equals if (!Arrays.equals(args, that.args)) return false; if (!operator.equals(that.operator)) return false; return true; } @Override public int hashCode() { int result = operator.hashCode(); result = 31 * result + annotations.hashCode(); result = 31 * result + Arrays.hashCode(args); return result; } }