public class Environment {
    // TODO make me thread safe
    private static final Map<String, Class<?>> DEFAULT_CLASSES = new HashMap<>();
    private final Map<String, Class<?>> classes = new LinkedHashMap<>(DEFAULT_CLASSES);
    private final Map<String, Variable> variables = new LinkedHashMap<>();

    static {
        for (String name : ReflectionUtil.JAVA_LANG_CLASSES) {
            try {
                DEFAULT_CLASSES.put(name, Class.forName("java.lang." + name));
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException("class java.lang." + name + " not found");
            }
        }
    }

    public void importClass(Class<?> _class) {
        final String name = _class.getCanonicalName();
        // Validate the type of class
        if (_class.isPrimitive()) {
            throw new UnsupportedOperationException("Can't import a primitive type: " + name);
        }
        if (_class.isArray()) {
            throw new UnsupportedOperationException("Can't import an array class: " + name);
        }
        if (_class.isAnonymousClass()) {
            throw new UnsupportedOperationException("Can't import an anonymous class: " + name);
        }
        // Check for existing import under the same simple name
        final String simpleName = _class.getSimpleName();
        final Class<?> existing = classes.get(simpleName);
        // ignore cases where the classes are the same as redundant imports are allowed
        if (existing != null && _class != existing) {
            throw new UnsupportedOperationException("Class " + name + " clashes with existing import " + existing.getCanonicalName());
        }
        // Add the class to the imports
        classes.put(simpleName, _class);
    }

    public Class<?> findClass(Identifier name) {
        return classes.get(name.getSource());
    }

    public Class<?> getClass(Identifier name) {
        final Class<?> _class = findClass(name);
        if (_class == null) {
            throw new UnsupportedOperationException("Class " + name.getSource() + " does not exist");
        }
        return _class;
    }

    public Collection<Class<?>> getClasses() {
        return classes.values();
    }

    public boolean hasClass(Identifier name) {
        return classes.containsKey(name.getSource());
    }

    public boolean hasClass(Class<?> _class) {
        return classes.containsValue(_class);
    }

    public void declareVariable(Identifier name, LiteralType type, Value value) {
        if (hasVariable(name)) {
            throw new UnsupportedOperationException("Variable " + name.getSource() + " is already declared");
        }
        variables.put(name.getSource(), new Variable(name, type, value));
    }

    public LiteralType getVariableType(Identifier name) {
        return findVariable(name).getType();
    }

    public Value getVariable(Identifier name) {
        return findVariable(name).getValue();
    }

    public Collection<Variable> getVariables() {
        return variables.values();
    }

    public void setVariable(Identifier name, Value value) {
        findVariable(name).setValue(value);
    }

    public boolean hasVariable(Identifier name) {
        return variables.containsKey(name.getSource());
    }

    private Variable findVariable(Identifier name) {
        final String nameString = name.getSource();
        final Variable variable = variables.get(nameString);
        if (variable == null) {
            throw new UnsupportedOperationException("Variable " + nameString + " does not exist");
        }
        return variable;
    }

    public static class Variable {
        private final Identifier name;
        private final LiteralType type;
        private Value value;

        private Variable(Identifier name, LiteralType type, Value value) {
            this.name = name;
            this.type = type;
            this.value = value;
        }

        public Identifier getName() {
            return name;
        }

        public LiteralType getType() {
            return type;
        }

        public boolean initialized() {
            return value != null;
        }

        public Value getValue() {
            if (!initialized()) {
                throw new UnsupportedOperationException("Variable " + name.getSource() + " has not been initialized");
            }
            return value;
        }

        private void setValue(Value value) {
            this.value = value;
        }
    }
} public class EvaluatorException extends SourceException {
    private static final long serialVersionUID = 1;

    public EvaluatorException(Throwable cause, SourceIndexed indexed) {
        this("EvaluatorException", cause, indexed);
    }

    public EvaluatorException(String error, Throwable cause, SourceIndexed indexed) {
        super(getMessage(error, cause), cause, null, indexed.getStart(), indexed.getEnd());
    }

    public EvaluatorException(String error, SourceIndexed indexed) {
        this(error, indexed.getStart(), indexed.getEnd());
    }

    public EvaluatorException(String error, int start, int end) {
        super(error, null, start, end);
    }

    private static String getMessage(String error, Throwable cause) {
        if (cause == null) {
            return error;
        }
        String causeMessage = cause.getMessage();
        if (causeMessage != null) {
            causeMessage = causeMessage.trim();
            if (causeMessage.isEmpty()) {
                causeMessage = null;
            }
        }
        error += " (" + cause.getClass().getSimpleName();
        if (causeMessage != null) {
            error += ": " + causeMessage;
        }
        return getMessage(error, cause.getCause()) + ")";
    }
} public class LiteralReferenceType extends SingleReferenceType implements LiteralType, TypeArgument {
    public static final LiteralReferenceType THE_STRING = LiteralReferenceType.of(String.class);
    public static final LiteralReferenceType THE_OBJECT = LiteralReferenceType.of(Object.class);
    public static final LiteralReferenceType THE_CLONEABLE = LiteralReferenceType.of(Cloneable.class);
    public static final LiteralReferenceType THE_SERIALIZABLE = LiteralReferenceType.of(Serializable.class);
    private static final Map<Class<?>, PrimitiveType> UNBOXING_CONVERSIONS = new HashMap<>();
    private final Class<?> type;
    private PrimitiveType unbox;
    private boolean unboxCached = false;
    private java.lang.reflect.TypeVariable<?>[] parameters = null;

    static {
        UNBOXING_CONVERSIONS.put(Boolean.class, PrimitiveType.THE_BOOLEAN);
        UNBOXING_CONVERSIONS.put(Byte.class, PrimitiveType.THE_BYTE);
        UNBOXING_CONVERSIONS.put(Short.class, PrimitiveType.THE_SHORT);
        UNBOXING_CONVERSIONS.put(Character.class, PrimitiveType.THE_CHAR);
        UNBOXING_CONVERSIONS.put(Integer.class, PrimitiveType.THE_INT);
        UNBOXING_CONVERSIONS.put(Long.class, PrimitiveType.THE_LONG);
        UNBOXING_CONVERSIONS.put(Float.class, PrimitiveType.THE_FLOAT);
        UNBOXING_CONVERSIONS.put(Double.class, PrimitiveType.THE_DOUBLE);
    }

    protected LiteralReferenceType(Class<?> type) {
        this.type = type;
    }

    @Override
    public String getName() {
        return type.getCanonicalName();
    }

    @Override
    public boolean isArray() {
        return type.isArray();
    }

    @Override
    public boolean isReifiable() {
        return true;
    }

    public boolean isRaw() {
        return getTypeParameters().length > 0;
    }

    public boolean isInterface() {
        return type.isInterface();
    }

    public boolean isEnum() {
        return type.isEnum();
    }

    public boolean isAbstract() {
        return Modifier.isAbstract(type.getModifiers());
    }

    public boolean isPublic() {
        return Modifier.isPublic(type.getModifiers());
    }

    public boolean isStatic() {
        return Modifier.isStatic(type.getModifiers());
    }

    public boolean isInnerClassOf(LiteralReferenceType enclosing) {
        if (enclosing == null) {
            // Enclosing as null means that it should not be an inner class
            return isStatic() || type.getEnclosingClass() == null;
        }
        return !isStatic() && type.getEnclosingClass() == enclosing.getTypeClass();
    }

    @Override
    public Class<?> getTypeClass() {
        return type;
    }

    protected java.lang.reflect.TypeVariable<?>[] getTypeParameters() {
        if (this.parameters == null) {
            // If this is an array, we need to get to the base component type
            Class<?> base = type;
            while (base.isArray()) {
                base = base.getComponentType();
            }
            this.parameters = base.getTypeParameters();
        }
        return parameters;
    }

    public boolean isBox() {
        if (!unboxCached) {
            unbox = UNBOXING_CONVERSIONS.get(type);
            unboxCached = true;
        }
        return unbox != null;
    }

    public PrimitiveType unbox() {
        if (isBox()) {
            return unbox;
        }
        throw new UnsupportedOperationException(type.getCanonicalName() + " is not a box type");
    }

    public LiteralType tryUnbox() {
        if (isBox()) {
            return unbox;
        }
        return this;
    }

    @Override
    public LiteralReferenceType getErasure() {
        return this;
    }

    @Override
    public Set<LiteralReferenceType> getDirectSuperTypes() {
        final Set<LiteralReferenceType> superTypes = new HashSet<>();
        if (isArray()) {
            // Find the number of dimensions of the array and the base component type
            int dimensions = 0;
            ComponentType componentType = this;
            do {
                if (!(componentType instanceof ReferenceType)) {
                    break;
                }
                componentType = ((ReferenceType) componentType).getComponentType();
                dimensions++;
            } while (componentType.isArray());
            if (componentType.equals(LiteralReferenceType.THE_OBJECT)) {
                // For an object component type we use the actual array direct super types
                superTypes.add(LiteralReferenceType.THE_OBJECT.asArray(dimensions - 1));
                superTypes.add(LiteralReferenceType.THE_SERIALIZABLE.asArray(dimensions - 1));
                superTypes.add(LiteralReferenceType.THE_CLONEABLE.asArray(dimensions - 1));
            } else {
                // Add all the component direct super types as arrays of the same dimension
                if (componentType instanceof LiteralReferenceType) {
                    for (LiteralReferenceType superType : ((LiteralReferenceType) componentType).getDirectSuperTypes()) {
                        superTypes.add(superType.asArray(dimensions));
                    }
                }
            }
        } else {
            // Add the direct super class and the directly implemented interfaces
            if (isInterface()) {
                // Interfaces have object as an implicit direct super class
                superTypes.add(LiteralReferenceType.THE_OBJECT);
            } else {
                // This will always return something unless this class is object
                final LiteralReferenceType superClass = getDirectSuperClass();
                if (superClass != null) {
                    superTypes.add(superClass);
                }
            }
            Collections.addAll(superTypes, getDirectlyImplementedInterfaces());
        }
        return superTypes;
    }

    @Override
    public LinkedHashSet<LiteralReferenceType> getSuperTypes() {
        final LinkedHashSet<LiteralReferenceType> result = new LinkedHashSet<>();
        final Queue<LiteralReferenceType> queue = new ArrayDeque<>();
        queue.add(capture());
        final boolean raw = isRaw();
        while (!queue.isEmpty()) {
            LiteralReferenceType child = queue.remove();
            if (raw) {
                child = child.getErasure();
            }
            if (result.add(child)) {
                queue.addAll(child.getDirectSuperTypes());
            }
        }
        return result;
    }

    @Override
    public LiteralReferenceType substituteTypeVariables(Substitutions substitution) {
        return this;
    }

    @Override
    public Set<TypeVariable> getTypeVariables() {
        return new HashSet<>();
    }

    public LiteralReferenceType getDirectSuperClass() {
        final java.lang.reflect.Type superClass = type.getGenericSuperclass();
        if (superClass != null) {
            final LiteralReferenceType wrappedClass = (LiteralReferenceType) TypeCache.wrapType(superClass);
            return isRaw() ? wrappedClass.getErasure() : wrappedClass;
        }
        return null;
    }

    public LiteralReferenceType[] getDirectlyImplementedInterfaces() {
        final java.lang.reflect.Type[] interfaces = type.getGenericInterfaces();
        final boolean raw = isRaw();
        final LiteralReferenceType[] wrapped = new LiteralReferenceType[interfaces.length];
        for (int i = 0; i < interfaces.length; i++) {
            final LiteralReferenceType wrappedInterface = (LiteralReferenceType) TypeCache.wrapType(interfaces[i]);
            wrapped[i] = raw ? wrappedInterface.getErasure() : wrappedInterface;
        }
        return wrapped;
    }

    @Override
    public LiteralReferenceType capture() {
        return this;
    }

    @Override
    public ComponentType getComponentType() {
        final Class<?> componentType = type.getComponentType();
        if (componentType == null) {
            throw new UnsupportedOperationException("Not an array type");
        }
        return (ComponentType) TypeCache.wrapClass(componentType);
    }

    @Override
    public LiteralReferenceType asArray(int dimensions) {
        return of(ReflectionUtil.asArrayType(type, dimensions));
    }

    @Override
    public Object newArray(int length) {
        return Array.newInstance(type, length);
    }

    @Override
    public Object newArray(int[] lengths) {
        return Array.newInstance(type, lengths);
    }

    @Override
    public boolean contains(TypeArgument other) {
        return equals(other);
    }

    @Override
    public boolean convertibleTo(Type to) {
        // Literal class types might be convertible to a primitive if they can be unboxed
        if (to.isPrimitive()) {
            return isBox() && unbox().convertibleTo(to);
        }
        // If the target literal type is parametrized, this type must have a parent with
        // the same erasure and compatible type arguments
        if (to instanceof ParametrizedType) {
            final ParametrizedType parametrized = (ParametrizedType) to;
            final LiteralReferenceType erasure = parametrized.getErasure();
            for (LiteralReferenceType superType : getSuperTypes()) {
                if (superType.getErasure().equals(erasure)) {
                    return !(superType instanceof ParametrizedType)
                            || parametrized.argumentsContain(((ParametrizedType) superType).getArguments());
                }
            }
            return false;
        }
        // Else they can be converted to another literal class if they are a subtype
        if (to instanceof LiteralReferenceType) {
            final LiteralReferenceType target = (LiteralReferenceType) to;
            return target.type.isAssignableFrom(type);
        }
        // They can also be converted to a type variable lower bound
        if (to instanceof TypeVariable) {
            final TypeVariable target = (TypeVariable) to;
            return convertibleTo(target.getLowerBound());
        }
        // They can also be converted to an intersection if they can be converted to each member
        if (to instanceof IntersectionType) {
            final IntersectionType target = (IntersectionType) to;
            for (SingleReferenceType type : target.getTypes()) {
                if (!convertibleTo(type)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public boolean isUncheckedConversion(Type to) {
        if (!(to instanceof ParametrizedType)) {
            return false;
        }
        final ParametrizedType parametrized = (ParametrizedType) to;
        final LiteralReferenceType erasure = parametrized.getErasure();
        for (LiteralReferenceType superType : getSuperTypes()) {
            if (superType.getErasure().equals(erasure)) {
                return !(superType instanceof ParametrizedType);
            }
        }
        return false;
    }

    public LiteralReferenceType getDeclaredInnerClass(String name, TypeArgument[] typeArguments) {
        for (Class<?> _class : type.getDeclaredClasses()) {
            final int modifiers = _class.getModifiers();
            // Ignore non-public classes and synthetic classes generated by the compiler
            // Class must be non-static to be an inner class
            if (!Modifier.isPublic(modifiers) || Modifier.isStatic(modifiers) || _class.isSynthetic()) {
                continue;
            }
            // Check name match
            if (!_class.getSimpleName().equals(name)) {
                continue;
            }
            // If this enclosing class is parametrized, than it is an owner and the inner class is also parametrized
            return typeArguments.length > 0 || this instanceof ParametrizedType ?
                    ParametrizedType.of((ParametrizedType) this, _class, Arrays.asList(typeArguments)) :
                    LiteralReferenceType.of(_class);
        }
        return null;
    }

    public ClassVariable getDeclaredField(String name) {
        // Array length field must be handled specially because the reflection API doesn't declare it
        if (isArray() && "length".equals(name)) {
            return ArrayLengthVariable.of(this);
        }
        // Only one field can match the name, not overloads are possible
        for (Field field : type.getDeclaredFields()) {
            // Ignore non-public fields and synthetic fields generated by the compiler
            if (!Modifier.isPublic(field.getModifiers()) || field.isSynthetic()) {
                continue;
            }
            // Check name match
            if (!field.getName().equals(name)) {
                continue;
            }
            return InstanceVariable.of(this, field);
        }
        return null;
    }

    public Set<ConstructorCallable> getDeclaredConstructors(TypeArgument[] typeArguments) {
        final Set<ConstructorCallable> constructors = new HashSet<>();
        for (Constructor<?> constructor : type.getDeclaredConstructors()) {
            // Ignore non-public constructors and synthetic constructors generated by the compiler
            if (!Modifier.isPublic(constructor.getModifiers()) || constructor.isSynthetic()) {
                continue;
            }
            // Try to create a callable for the constructor, might fail if the type arguments are out of bounds
            final ConstructorCallable callable = ConstructorCallable.of(this, constructor, typeArguments);
            if (callable != null) {
                constructors.add(callable);
            }
        }
        return constructors;
    }

    public Set<? extends Callable> getDeclaredMethods(String name, TypeArgument[] typeArguments) {
        // Array clone method must be handled specially because the reflection API doesn't declare it
        if (isArray() && "clone".equals(name)) {
            return Collections.singleton(ArrayCloneCallable.of(this));
        }
        final Set<MethodCallable> methods = new HashSet<>();
        for (Method method : type.getDeclaredMethods()) {
            // Ignore non-public methods and synthetic methods generated by the compiler
            if (!Modifier.isPublic(method.getModifiers()) || method.isSynthetic()) {
                continue;
            }
            // Check name match
            if (!method.getName().equals(name)) {
                continue;
            }
            // Try to create a callable for the method, might fail if the type arguments are out of bounds
            final MethodCallable callable = MethodCallable.of(this, method, typeArguments);
            if (callable != null) {
                methods.add(callable);
            }
        }
        return methods;
    }

    @Override
    public LiteralReferenceType getInnerClass(String name, TypeArgument[] typeArguments) {
        final LinkedHashSet<LiteralReferenceType> superTypes = getSuperTypes();
        for (LiteralReferenceType type : superTypes) {
            final LiteralReferenceType innerClass = type.getDeclaredInnerClass(name, typeArguments);
            if (innerClass != null) {
                return innerClass;
            }
        }
        throw new UnsupportedOperationException("No inner class for signature " + name +
                (typeArguments.length > 0 ? '<' + StringUtil.toString(typeArguments, ", ") + '>' : "") +
                " in " + getName());
    }

    @Override
    public ClassVariable getField(String name) {
        final LinkedHashSet<LiteralReferenceType> superTypes = getSuperTypes();
        for (LiteralReferenceType type : superTypes) {
            final ClassVariable field = type.getDeclaredField(name);
            if (field != null) {
                return field;
            }
        }
        throw new UnsupportedOperationException("No field named " + name + " in " + getName());
    }

    @Override
    public ConstructorCallable getConstructor(TypeArgument[] typeArguments, Type[] arguments) {
        // Get the declared constructors and look for applicable candidates with and without using vararg
        final Set<ConstructorCallable> regularCandidates = new HashSet<>();
        final Set<ConstructorCallable> varargCandidates = new HashSet<>();
        final Set<ConstructorCallable> constructors = getDeclaredConstructors(typeArguments);
        for (ConstructorCallable constructor : constructors) {
            if (constructor.isApplicable(arguments)) {
                regularCandidates.add(constructor);
            }
            if (!constructor.supportsVararg()) {
                continue;
            }
            final ConstructorCallable varargConstructor = constructor.useVararg();
            if (varargConstructor.isApplicable(arguments)) {
                varargCandidates.add(varargConstructor);
            }
        }
        // Resolve overloads
        ConstructorCallable callable = reduceCallableCandidates(regularCandidates, arguments);
        // If the regular callables don't work, try with vararg
        if (callable == null) {
            callable = reduceCallableCandidates(varargCandidates, arguments);
        }
        if (callable != null) {
            return callable.requiresUncheckedConversion(arguments) ? callable.eraseReturnType() : callable;
        }
        throw new UnsupportedOperationException("No constructor for signature: " +
                (typeArguments.length > 0 ? '<' + StringUtil.toString(typeArguments, ", ") + '>' : "") +
                type.getSimpleName() + "(" + StringUtil.toString(Arrays.asList(arguments), ", ") + ") in " + getName());
    }

    @Override
    public Callable getMethod(String name, TypeArgument[] typeArguments, Type[] arguments) {
        // Get the declared methods and look for applicable candidates with and without using vararg
        final Set<Callable> regularCandidates = new HashSet<>();
        final Set<Callable> varargCandidates = new HashSet<>();
        final LinkedHashSet<LiteralReferenceType> superTypes = getSuperTypes();
        for (LiteralReferenceType type : superTypes) {
            // Check that the type is accessible
            if (!type.isPublic()) {
                // TODO: proper access control, for now exclude non-public types
                continue;
            }
            for (Callable method : type.getDeclaredMethods(name, typeArguments)) {
                if (method.isApplicable(arguments)) {
                    regularCandidates.add(method);
                }
                if (!method.supportsVararg()) {
                    continue;
                }
                final Callable varargMethod = method.useVararg();
                if (varargMethod.isApplicable(arguments)) {
                    varargCandidates.add(varargMethod);
                }
            }
        }
        // Resolve overloads
        Callable callable = reduceCallableCandidates(regularCandidates, arguments);
        // If the regular callables don't work, try with vararg
        if (callable == null) {
            callable = reduceCallableCandidates(varargCandidates, arguments);
        }
        if (callable != null) {
            return callable.requiresUncheckedConversion(arguments) ? callable.eraseReturnType() : callable;
        }
        throw new UnsupportedOperationException("No method for signature: " +
                (typeArguments.length > 0 ? '<' + StringUtil.toString(typeArguments, ", ") + '>' : "") +
                name + "(" + StringUtil.toString(Arrays.asList(arguments), ", ") + ") in " + getName());
    }

    private static <C extends Callable> C reduceCallableCandidates(Set<C> candidates, Type[] arguments) {
        // Reduce the regular candidates to keep only the most applicable ones
        for (Iterator<C> iterator = candidates.iterator(); iterator.hasNext(); ) {
            final C candidate1 = iterator.next();
            for (C candidate2 : candidates) {
                // Don't compare with itself
                if (candidate1 == candidate2) {
                    continue;
                }
                // Remove candidate1 is candidate2 is more applicable
                if (candidate2.isMoreApplicableThan(candidate1, arguments)) {
                    iterator.remove();
                    break;
                }
            }
        }
        return candidates.size() != 1 ? null : candidates.iterator().next();
    }

    public Substitutions getSubstitutions() {
        return Substitutions.NONE;
    }

    @Override
    public boolean equals(Object other) {
        return this == other || (other instanceof LiteralReferenceType) && this.type == ((LiteralReferenceType) other).type;
    }

    @Override
    public int hashCode() {
        return type.getName().hashCode();
    }

    public static LiteralReferenceType of(Class<?> type) {
        checkOwner(null, type);
        return new LiteralReferenceType(type);
    }

    protected static ParametrizedType checkOwner(ParametrizedType paramOwner, Class<?> inner) {
        return checkOwner(paramOwner, inner, Collections.<TypeArgument>emptyList());
    }

    protected static ParametrizedType checkOwner(ParametrizedType paramOwner, Class<?> inner, List<TypeArgument> arguments) {
        final LiteralReferenceType owner;
        final Class<?> enclosingClass = inner.getEnclosingClass();
        if (paramOwner != null) {
            // If the owner is given, check that it matches what is actually declared in the code
            if (!paramOwner.getTypeClass().equals(enclosingClass)) {
                throw new UnsupportedOperationException("Mismatch between given owner and actual one: " + paramOwner + " and " + enclosingClass);
            }
            // Also check that the inner type is selectable (that is, not static)
            if (Modifier.isStatic(inner.getModifiers())) {
                throw new UnsupportedOperationException("Inner type " + inner.getSimpleName() + " cannot be used in a static context");
            }
            owner = paramOwner;
        } else if (enclosingClass != null) {
            // If it's not given, check if enclosing class is applicable: the inner class cannot be static
            if (!Modifier.isStatic(inner.getModifiers())) {
                owner = LiteralReferenceType.of(enclosingClass);
            } else {
                // The enclosing class isn't an owner type
                owner = null;
            }
        } else {
            // No enclosing class
            owner = null;
        }
        if (owner != null) {
            // Now that we found an owner, make sure we don't have a partially raw owner/inner combination
            // Owner and inner cannot have one be raw and one not raw
            final boolean ownerParam = owner instanceof ParametrizedType;
            final boolean ownerRaw = owner.isRaw();
            final boolean innerParam = !arguments.isEmpty();
            final boolean innerRaw = inner.getTypeParameters().length > 0 && arguments.isEmpty();
            if ((ownerParam || ownerRaw) && (innerParam || innerRaw) && ownerRaw != innerRaw) {
                throw new UnsupportedOperationException("Cannot have a mix of raw and generic outer and inner classes");
            }
            // We only care of the owner if it is parametrized, as the type arguments are accessible by inner types
            // We want to access those, the rest doesn't matter
            return ownerParam ? (ParametrizedType) owner : null;
        }
        return null;
    }
} public class PrimitiveType implements LiteralType {
    public static final PrimitiveType THE_BOOLEAN;
    public static final PrimitiveType THE_BYTE;
    public static final PrimitiveType THE_SHORT;
    public static final PrimitiveType THE_CHAR;
    public static final PrimitiveType THE_INT;
    public static final PrimitiveType THE_LONG;
    public static final PrimitiveType THE_FLOAT;
    public static final PrimitiveType THE_DOUBLE;
    private static final Map<Class<?>, PrimitiveType> ALL_TYPES = new HashMap<>();
    private static final Map<Class<?>, Set<Class<?>>> PRIMITIVE_CONVERSIONS = new HashMap<>();
    private static final Map<Class<?>, LiteralReferenceType> BOXING_CONVERSIONS = new HashMap<>();
    private static final Map<Class<?>, RangeChecker> NARROW_CHECKERS = new HashMap<>();
    private static final Set<Class<?>> UNARY_WIDENS_INT = new HashSet<>();
    private static final Map<Class<?>, Widener> BINARY_WIDENERS = new HashMap<>();
    private final Class<?> type;
    private final ValueKind kind;

    static {
        THE_BOOLEAN = new PrimitiveType(boolean.class, ValueKind.BOOLEAN);
        THE_BYTE = new PrimitiveType(byte.class, ValueKind.BYTE);
        THE_SHORT = new PrimitiveType(short.class, ValueKind.SHORT);
        THE_CHAR = new PrimitiveType(char.class, ValueKind.CHAR);
        THE_INT = new PrimitiveType(int.class, ValueKind.INT);
        THE_LONG = new PrimitiveType(long.class, ValueKind.LONG);
        THE_FLOAT = new PrimitiveType(float.class, ValueKind.FLOAT);
        THE_DOUBLE = new PrimitiveType(double.class, ValueKind.DOUBLE);

        ALL_TYPES.put(boolean.class, THE_BOOLEAN);
        ALL_TYPES.put(byte.class, THE_BYTE);
        ALL_TYPES.put(short.class, THE_SHORT);
        ALL_TYPES.put(char.class, THE_CHAR);
        ALL_TYPES.put(int.class, THE_INT);
        ALL_TYPES.put(long.class, THE_LONG);
        ALL_TYPES.put(float.class, THE_FLOAT);
        ALL_TYPES.put(double.class, THE_DOUBLE);

        PRIMITIVE_CONVERSIONS.put(boolean.class, new HashSet<Class<?>>(Collections.singletonList(boolean.class)));
        PRIMITIVE_CONVERSIONS.put(byte.class, new HashSet<Class<?>>(Arrays.asList(byte.class, short.class, int.class, long.class, float.class, double.class)));
        PRIMITIVE_CONVERSIONS.put(short.class, new HashSet<Class<?>>(Arrays.asList(short.class, int.class, long.class, float.class, double.class)));
        PRIMITIVE_CONVERSIONS.put(char.class, new HashSet<Class<?>>(Arrays.asList(char.class, int.class, long.class, float.class, double.class)));
        PRIMITIVE_CONVERSIONS.put(int.class, new HashSet<Class<?>>(Arrays.asList(int.class, long.class, float.class, double.class)));
        PRIMITIVE_CONVERSIONS.put(long.class, new HashSet<Class<?>>(Arrays.asList(long.class, float.class, double.class)));
        PRIMITIVE_CONVERSIONS.put(float.class, new HashSet<Class<?>>(Arrays.asList(float.class, double.class)));
        PRIMITIVE_CONVERSIONS.put(double.class, new HashSet<Class<?>>(Collections.singletonList(double.class)));

        BOXING_CONVERSIONS.put(boolean.class, LiteralReferenceType.of(Boolean.class));
        BOXING_CONVERSIONS.put(byte.class, LiteralReferenceType.of(Byte.class));
        BOXING_CONVERSIONS.put(short.class, LiteralReferenceType.of(Short.class));
        BOXING_CONVERSIONS.put(char.class, LiteralReferenceType.of(Character.class));
        BOXING_CONVERSIONS.put(int.class, LiteralReferenceType.of(Integer.class));
        BOXING_CONVERSIONS.put(long.class, LiteralReferenceType.of(Long.class));
        BOXING_CONVERSIONS.put(float.class, LiteralReferenceType.of(Float.class));
        BOXING_CONVERSIONS.put(double.class, LiteralReferenceType.of(Double.class));

        NARROW_CHECKERS.put(byte.class, new RangeChecker(-128, 0xFF));
        NARROW_CHECKERS.put(short.class, new RangeChecker(-32768, 0xFFFF));
        NARROW_CHECKERS.put(char.class, new RangeChecker(0, 0xFFFF));

        Collections.addAll(UNARY_WIDENS_INT, byte.class, short.class, char.class);

        final Widener intWidener = new Widener(int.class, byte.class, short.class, char.class);
        BINARY_WIDENERS.put(byte.class, intWidener);
        BINARY_WIDENERS.put(short.class, intWidener);
        BINARY_WIDENERS.put(char.class, intWidener);
        BINARY_WIDENERS.put(int.class, intWidener);
        BINARY_WIDENERS.put(long.class, new Widener(long.class, byte.class, short.class, char.class, int.class));
        BINARY_WIDENERS.put(float.class, new Widener(float.class, byte.class, short.class, char.class, int.class, long.class));
        BINARY_WIDENERS.put(double.class, new Widener(double.class, byte.class, short.class, char.class, int.class, long.class, float.class));
    }

    private PrimitiveType(Class<?> type, ValueKind kind) {
        this.type = type;
        this.kind = kind;
    }

    @Override
    public Class<?> getTypeClass() {
        return type;
    }

    @Override
    public String getName() {
        return type.getCanonicalName();
    }

    @Override
    public ValueKind getKind() {
        return kind;
    }

    @Override
    public boolean isVoid() {
        return false;
    }

    @Override
    public boolean isNull() {
        return false;
    }

    @Override
    public boolean isPrimitive() {
        return true;
    }

    @Override
    public boolean isNumeric() {
        return isIntegral() || type == float.class || type == double.class;
    }

    @Override
    public boolean isIntegral() {
        return type == byte.class || type == short.class || type == char.class || type == int.class || type == long.class;
    }

    @Override
    public boolean isBoolean() {
        return type == boolean.class;
    }

    @Override
    public boolean isArray() {
        return false;
    }

    @Override
    public boolean isReference() {
        return false;
    }

    @Override
    public boolean isReifiable() {
        return true;
    }

    @Override
    public LiteralReferenceType asArray(int dimensions) {
        return LiteralReferenceType.of(ReflectionUtil.asArrayType(type, dimensions));
    }

    @Override
    public Object newArray(int length) {
        return Array.newInstance(type, length);
    }

    @Override
    public Object newArray(int[] lengths) {
        return Array.newInstance(type, lengths);
    }

    public SingleReferenceType box() {
        return BOXING_CONVERSIONS.get(type);
    }

    public boolean canNarrowFrom(int value) {
        final RangeChecker checker = NARROW_CHECKERS.get(type);
        return checker != null && checker.contains(value);
    }

    public PrimitiveType unaryWiden() {
        return of(UNARY_WIDENS_INT.contains(type) ? int.class : type);
    }

    public PrimitiveType binaryWiden(PrimitiveType with) {
        return of(BINARY_WIDENERS.get(type).widen(with.getTypeClass()));
    }

    @Override
    public boolean convertibleTo(Type to) {
        // Primitive types can be converted to certain primitive types
        // If the target type isn't primitive, box the source and try again
        if (to.isPrimitive()) {
            final PrimitiveType target = (PrimitiveType) to;
            return PRIMITIVE_CONVERSIONS.get(type).contains(target.getTypeClass());
        }
        return box().convertibleTo(to);
    }

    @Override
    public PrimitiveType capture() {
        return this;
    }

    @Override
    public String toString() {
        return getName();
    }

    @Override
    public boolean equals(Object other) {
        return this == other || other instanceof PrimitiveType && this.type == ((PrimitiveType) other).getTypeClass();
    }

    @Override
    public int hashCode() {
        return type.getName().hashCode();
    }

    public static PrimitiveType of(Class<?> type) {
        return ALL_TYPES.get(type);
    }

    private static class RangeChecker {
        private final int minValue;
        private final int invertedMask;

        private RangeChecker(int minValue, int mask) {
            this.minValue = minValue;
            invertedMask = ~mask;
        }

        private boolean contains(int value) {
            return (value - minValue & invertedMask) == 0;
        }
    }

    private static class Widener {
        private final Class<?> wider;
        private final Set<Class<?>> widens = new HashSet<>();

        private Widener(Class<?> wider, Class<?>... widens) {
            this.wider = wider;
            Collections.addAll(this.widens, widens);
        }

        private Class<?> widen(Class<?> type) {
            return widens.contains(type) ? wider : type;
        }
    }
} public interface Type {
    String getName();

    ValueKind getKind();

    boolean isVoid();

    boolean isNull();

    boolean isPrimitive();

    boolean isNumeric();

    boolean isIntegral();

    boolean isBoolean();

    boolean isArray();

    boolean isReference();

    boolean isReifiable();

    boolean convertibleTo(Type to);

    Type capture();

    @Override
    String toString();

    @Override
    boolean equals(Object other);

    @Override
    int hashCode();
} public class DoubleValue implements Value {
    private final double value;

    private DoubleValue(double value) {
        this.value = value;
    }

    @Override
    public boolean asBoolean() {
        throw new UnsupportedOperationException("Cannot convert a double to a boolean");
    }

    @Override
    public byte asByte() {
        return (byte) value;
    }

    @Override
    public short asShort() {
        return (short) value;
    }

    @Override
    public char asChar() {
        return (char) value;
    }

    @Override
    public int asInt() {
        return (int) value;
    }

    @Override
    public long asLong() {
        return (long) value;
    }

    @Override
    public float asFloat() {
        return (float) value;
    }

    @Override
    public double asDouble() {
        return value;
    }

    @Override
    public Double asObject() {
        return value;
    }

    @Override
    public String asString() {
        return Double.toString(value);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T as() {
        return (T) asObject();
    }

    @Override
    public ValueKind getKind() {
        return ValueKind.DOUBLE;
    }

    @Override
    public boolean isPrimitive() {
        return true;
    }

    @Override
    public Class<?> getTypeClass() {
        return double.class;
    }

    @Override
    public String toString() {
        return asString();
    }

    public static DoubleValue of(double value) {
        return new DoubleValue(value);
    }

    public static DoubleValue defaultValue() {
        return new DoubleValue(0);
    }
} public class FloatValue implements Value {
    private final float value;

    private FloatValue(float value) {
        this.value = value;
    }

    @Override
    public boolean asBoolean() {
        throw new UnsupportedOperationException("Cannot convert a float to a boolean");
    }

    @Override
    public byte asByte() {
        return (byte) value;
    }

    @Override
    public short asShort() {
        return (short) value;
    }

    @Override
    public char asChar() {
        return (char) value;
    }

    @Override
    public int asInt() {
        return (int) value;
    }

    @Override
    public long asLong() {
        return (long) value;
    }

    @Override
    public float asFloat() {
        return value;
    }

    @Override
    public double asDouble() {
        return value;
    }

    @Override
    public Float asObject() {
        return value;
    }

    @Override
    public String asString() {
        return Float.toString(value);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T as() {
        return (T) asObject();
    }

    @Override
    public ValueKind getKind() {
        return ValueKind.FLOAT;
    }

    @Override
    public boolean isPrimitive() {
        return true;
    }

    @Override
    public Class<?> getTypeClass() {
        return float.class;
    }

    @Override
    public String toString() {
        return asString();
    }

    public static FloatValue of(float value) {
        return new FloatValue(value);
    }

    public static FloatValue defaultValue() {
        return new FloatValue(0);
    }
} public class IntValue implements Value {
    private static final IntValue[] COMMON_VALUES = new IntValue[256];
    private final int value;

    static {
        for (int i = 0; i < 256; i++) {
            COMMON_VALUES[i] = new IntValue(i - 128);
        }
    }

    private IntValue(int value) {
        this.value = value;
    }

    @Override
    public boolean asBoolean() {
        throw new UnsupportedOperationException("Cannot convert an int to a boolean");
    }

    @Override
    public byte asByte() {
        return (byte) value;
    }

    @Override
    public short asShort() {
        return (short) value;
    }

    @Override
    public char asChar() {
        return (char) value;
    }

    @Override
    public int asInt() {
        return value;
    }

    @Override
    public long asLong() {
        return value;
    }

    @Override
    public float asFloat() {
        return value;
    }

    @Override
    public double asDouble() {
        return value;
    }

    @Override
    public Integer asObject() {
        return value;
    }

    @Override
    public String asString() {
        return Integer.toString(value);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T as() {
        return (T) asObject();
    }

    @Override
    public ValueKind getKind() {
        return ValueKind.INT;
    }

    @Override
    public boolean isPrimitive() {
        return true;
    }

    @Override
    public Class<?> getTypeClass() {
        return int.class;
    }

    @Override
    public String toString() {
        return asString();
    }

    public static IntValue of(int value) {
        final int offsetValue = value + 128;
        if ((offsetValue & ~0xFF) == 0) {
            return COMMON_VALUES[offsetValue];
        }
        return new IntValue(value);
    }

    public static IntValue defaultValue() {
        return COMMON_VALUES[128];
    }
} public class LongValue implements Value {
    private static final LongValue[] COMMON_VALUES = new LongValue[256];
    private final long value;

    static {
        for (int i = 0; i < 256; i++) {
            COMMON_VALUES[i] = new LongValue(i - 128);
        }
    }

    private LongValue(long value) {
        this.value = value;
    }

    @Override
    public boolean asBoolean() {
        throw new UnsupportedOperationException("Cannot convert a long to a boolean");
    }

    @Override
    public byte asByte() {
        return (byte) value;
    }

    @Override
    public short asShort() {
        return (short) value;
    }

    @Override
    public char asChar() {
        return (char) value;
    }

    @Override
    public int asInt() {
        return (int) value;
    }

    @Override
    public long asLong() {
        return value;
    }

    @Override
    public float asFloat() {
        return value;
    }

    @Override
    public double asDouble() {
        return value;
    }

    @Override
    public Long asObject() {
        return value;
    }

    @Override
    public String asString() {
        return Long.toString(value);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T as() {
        return (T) asObject();
    }

    @Override
    public ValueKind getKind() {
        return ValueKind.LONG;
    }

    @Override
    public boolean isPrimitive() {
        return true;
    }

    @Override
    public Class<?> getTypeClass() {
        return long.class;
    }

    @Override
    public String toString() {
        return asString();
    }

    public static LongValue of(long value) {
        final long offsetValue = value + 128;
        if ((offsetValue & ~0xFFL) == 0) {
            return COMMON_VALUES[(int) offsetValue];
        }
        return new LongValue(value);
    }

    public static LongValue defaultValue() {
        return COMMON_VALUES[128];
    }
} public class ObjectValue implements Value {
    private static final ObjectValue THE_NULL = new ObjectValue();
    private final Object value;
    private final ValueKind kind;
    private final Value unboxed;

    private ObjectValue() {
        value = null;
        kind = ValueKind.OBJECT;
        unboxed = null;
    }

    private ObjectValue(Object value) {
        this.value = value;
        unboxed = ValueKind.unbox(value);
        kind = unboxed == null ? ValueKind.OBJECT : unboxed.getKind();
    }

    @Override
    public boolean asBoolean() {
        if (kind == ValueKind.OBJECT) {
            throw new UnsupportedOperationException("Cannot convert an object to a boolean");
        }
        return unboxed.asBoolean();
    }

    @Override
    public byte asByte() {
        if (kind == ValueKind.OBJECT) {
            throw new UnsupportedOperationException("Cannot convert an object to a byte");
        }
        return unboxed.asByte();
    }

    @Override
    public short asShort() {
        if (kind == ValueKind.OBJECT) {
            throw new UnsupportedOperationException("Cannot convert an object to a short");
        }
        return unboxed.asShort();
    }

    @Override
    public char asChar() {
        if (kind == ValueKind.OBJECT) {
            throw new UnsupportedOperationException("Cannot convert an object to a char");
        }
        return unboxed.asChar();
    }

    @Override
    public int asInt() {
        if (kind == ValueKind.OBJECT) {
            throw new UnsupportedOperationException("Cannot convert an object to a int");
        }
        return unboxed.asInt();
    }

    @Override
    public long asLong() {
        if (kind == ValueKind.OBJECT) {
            throw new UnsupportedOperationException("Cannot convert an object to a long");
        }
        return unboxed.asLong();
    }

    @Override
    public float asFloat() {
        if (kind == ValueKind.OBJECT) {
            throw new UnsupportedOperationException("Cannot convert an object to a float");
        }
        return unboxed.asFloat();
    }

    @Override
    public double asDouble() {
        if (kind == ValueKind.OBJECT) {
            throw new UnsupportedOperationException("Cannot convert an object to a double");
        }
        return unboxed.asDouble();
    }

    @Override
    public Object asObject() {
        return value;
    }

    @Override
    public String asString() {
        return value == null ? "null" : value.toString();
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T as() {
        return (T) asObject();
    }

    @Override
    public ValueKind getKind() {
        return kind;
    }

    @Override
    public boolean isPrimitive() {
        return false;
    }

    @Override
    public Class<?> getTypeClass() {
        return value == null ? null : value.getClass();
    }

    @Override
    public String toString() {
        return '"' + asString() + '"';
    }

    public static ObjectValue of(Object value) {
        if (value == null) {
            return THE_NULL;
        }
        return new ObjectValue(value);
    }

    public static ObjectValue defaultValue() {
        return THE_NULL;
    }
} public interface Value {
    boolean asBoolean();

    byte asByte();

    short asShort();

    char asChar();

    int asInt();

    long asLong();

    float asFloat();

    double asDouble();

    Object asObject();

    String asString();

    <T> T as();

    ValueKind getKind();

    boolean isPrimitive();

    Class<?> getTypeClass();

    String toString();
} public enum ValueKind {
    BOOLEAN(Boolean.class) {
        @Override
        public BooleanValue defaultValue() {
            return BooleanValue.defaultValue();
        }

        @Override
        public BooleanValue wrap(Object object) {
            if (object instanceof Boolean) {
                return BooleanValue.of((Boolean) object);
            }
            throw new UnsupportedOperationException("Cannot convert an object to a boolean");
        }

        @Override
        public BooleanValue convert(Value value) {
            return BooleanValue.of(value.asBoolean());
        }
    },
    BYTE(Byte.class) {
        @Override
        public ByteValue defaultValue() {
            return ByteValue.defaultValue();
        }

        @Override
        public ByteValue wrap(Object object) {
            if (object instanceof Byte) {
                return ByteValue.of((Byte) object);
            }
            throw new UnsupportedOperationException("Cannot convert an object to a byte");
        }

        @Override
        public ByteValue convert(Value value) {
            return ByteValue.of(value.asByte());
        }
    },
    SHORT(Short.class) {
        @Override
        public ShortValue defaultValue() {
            return ShortValue.defaultValue();
        }

        @Override
        public ShortValue wrap(Object object) {
            if (object instanceof Short) {
                return ShortValue.of((Short) object);
            }
            throw new UnsupportedOperationException("Cannot convert an object to a short");
        }

        @Override
        public ShortValue convert(Value value) {
            return ShortValue.of(value.asShort());
        }
    },
    CHAR(Character.class) {
        @Override
        public CharValue defaultValue() {
            return CharValue.defaultValue();
        }

        @Override
        public CharValue wrap(Object object) {
            if (object instanceof Character) {
                return CharValue.of((Character) object);
            }
            throw new UnsupportedOperationException("Cannot convert an object to a char");
        }

        @Override
        public CharValue convert(Value value) {
            return CharValue.of(value.asChar());
        }
    },
    INT(Integer.class) {
        @Override
        public IntValue defaultValue() {
            return IntValue.defaultValue();
        }

        @Override
        public IntValue wrap(Object object) {
            if (object instanceof Integer) {
                return IntValue.of((Integer) object);
            }
            throw new UnsupportedOperationException("Cannot convert an object to an int");
        }

        @Override
        public IntValue convert(Value value) {
            return IntValue.of(value.asInt());
        }
    },
    LONG(Long.class) {
        @Override
        public LongValue defaultValue() {
            return LongValue.defaultValue();
        }

        @Override
        public LongValue wrap(Object object) {
            if (object instanceof Long) {
                return LongValue.of((Long) object);
            }
            throw new UnsupportedOperationException("Cannot convert an object to a long");
        }

        @Override
        public LongValue convert(Value value) {
            return LongValue.of(value.asLong());
        }
    },
    FLOAT(Float.class) {
        @Override
        public FloatValue defaultValue() {
            return FloatValue.defaultValue();
        }

        @Override
        public FloatValue wrap(Object object) {
            if (object instanceof Float) {
                return FloatValue.of((Float) object);
            }
            throw new UnsupportedOperationException("Cannot convert an object to a float");
        }

        @Override
        public FloatValue convert(Value value) {
            return FloatValue.of(value.asFloat());
        }
    },
    DOUBLE(Double.class) {
        @Override
        public DoubleValue defaultValue() {
            return DoubleValue.defaultValue();
        }

        @Override
        public DoubleValue wrap(Object object) {
            if (object instanceof Double) {
                return DoubleValue.of((Double) object);
            }
            throw new UnsupportedOperationException("Cannot convert an object to a double");
        }

        @Override
        public DoubleValue convert(Value value) {
            return DoubleValue.of(value.asDouble());
        }
    },
    VOID(Void.class) {
        @Override
        public Value defaultValue() {
            return VoidValue.defaultValue();
        }

        @Override
        public Value wrap(Object object) {
            if (object instanceof Void) {
                return VoidValue.THE_VOID;
            }
            throw new UnsupportedOperationException("Cannot convert an object to void");
        }

        @Override
        public Value convert(Value value) {
            throw new UnsupportedOperationException("Cannot convert anything to void");
        }
    },
    OBJECT(null) {
        @Override
        public ObjectValue defaultValue() {
            return ObjectValue.defaultValue();
        }

        @Override
        public ObjectValue wrap(Object object) {
            return ObjectValue.of(object);
        }

        @Override
        public ObjectValue convert(Value value) {
            return ObjectValue.of(value.asObject());
        }
    };
    private static final Map<Class<?>, ValueKind> BOX_TO_KIND = new HashMap<>();
    private final Class<?> boxingClass;

    static {
        for (ValueKind kind : values()) {
            BOX_TO_KIND.put(kind.getBoxingClass(), kind);
        }
    }

    ValueKind(Class<?> boxingClass) {
        this.boxingClass = boxingClass;
    }

    public abstract Value defaultValue();

    public abstract Value wrap(Object object);

    public abstract Value convert(Value value);

    public Class<?> getBoxingClass() {
        return boxingClass;
    }

    public static Value unbox(Object box) {
        final ValueKind kind = BOX_TO_KIND.get(box.getClass());
        if (kind == null) {
            return null;
        }
        if (kind == OBJECT) {
            throw new UnsupportedOperationException("Not a box type: " + box.getClass().getSimpleName());
        }
        return kind.wrap(box);
    }
} public class Symbol extends Token {
    private static final Map<String, Symbol> SYMBOLS = new HashMap<>();
    private static final Symbol[] CHAR_SYMBOLS = new Symbol[256];
    private final Symbol compoundAssignOperator;

    static {
        // punctuation
        add(TokenID.SYMBOL_PERIOD, ".");
        add(TokenID.SYMBOL_COMMA, ",");
        add(TokenID.SYMBOL_COLON, ":");
        add(TokenID.SYMBOL_SEMICOLON, ";");
        // math
        add(TokenID.SYMBOL_PLUS, "+");
        add(TokenID.SYMBOL_MINUS, "-");
        add(TokenID.SYMBOL_MULTIPLY, "*");
        add(TokenID.SYMBOL_DIVIDE, "/");
        add(TokenID.SYMBOL_MODULO, "%");
        add(TokenID.SYMBOL_INCREMENT, "++", "+");
        add(TokenID.SYMBOL_DECREMENT, "--", "-");
        // bitwise
        add(TokenID.SYMBOL_BITWISE_AND, "&");
        add(TokenID.SYMBOL_BITWISE_OR, "|");
        add(TokenID.SYMBOL_BITWISE_NOT, "~");
        add(TokenID.SYMBOL_BITWISE_XOR, "^");
        // shift
        add(TokenID.SYMBOL_LOGICAL_LEFT_SHIFT, "<<");
        add(TokenID.SYMBOL_ARITHMETIC_RIGHT_SHIFT, ">>");
        add(TokenID.SYMBOL_LOGICAL_RIGHT_SHIFT, ">>>");
        // boolean
        add(TokenID.SYMBOL_BOOLEAN_NOT, "!");
        add(TokenID.SYMBOL_BOOLEAN_AND, "&&");
        add(TokenID.SYMBOL_BOOLEAN_OR, "||");
        // comparison
        add(TokenID.SYMBOL_LESSER, "<");
        add(TokenID.SYMBOL_GREATER, ">");
        add(TokenID.SYMBOL_GREATER_OR_EQUAL, ">=");
        add(TokenID.SYMBOL_LESSER_OR_EQUAL, "<=");
        add(TokenID.SYMBOL_EQUAL, "==");
        add(TokenID.SYMBOL_NOT_EQUAL, "!=");
        // assignment
        add(TokenID.SYMBOL_ASSIGN, "=");
        add(TokenID.SYMBOL_ADD_ASSIGN, "+=", "+");
        add(TokenID.SYMBOL_SUBTRACT_ASSIGN, "-=", "-");
        add(TokenID.SYMBOL_MULTIPLY_ASSIGN, "*=", "*");
        add(TokenID.SYMBOL_DIVIDE_ASSIGN, "/=", "/");
        add(TokenID.SYMBOL_REMAINDER_ASSIGN, "%=", "%");
        add(TokenID.SYMBOL_BITWISE_AND_ASSIGN, "&=", "&");
        add(TokenID.SYMBOL_BITWISE_OR_ASSIGN, "|=", "|");
        add(TokenID.SYMBOL_BITWISE_XOR_ASSIGN, "^=", "^");
        add(TokenID.SYMBOL_LOGICAL_LEFT_SHIFT_ASSIGN, "<<=", "<<");
        add(TokenID.SYMBOL_ARITHMETIC_RIGHT_SHIFT_ASSIGN, ">>=", ">>");
        add(TokenID.SYMBOL_LOGICAL_RIGHT_SHIFT_ASSIGN, ">>>=", ">>>");
        // enclosing
        add(TokenID.SYMBOL_OPEN_PARENTHESIS, "(");
        add(TokenID.SYMBOL_CLOSE_PARENTHESIS, ")");
        add(TokenID.SYMBOL_OPEN_BRACKET, "[");
        add(TokenID.SYMBOL_CLOSE_BRACKET, "]");
        add(TokenID.SYMBOL_OPEN_BRACE, "{");
        add(TokenID.SYMBOL_CLOSE_BRACE, "}");
        // comment
        add(TokenID.SYMBOL_DOUBLE_SLASH, "//");
        add(TokenID.SYMBOL_SLASH_STAR, "/*");
        add(TokenID.SYMBOL_STAR_SLASH, "*/");
        // other
        add(TokenID.SYMBOL_QUESTION_MARK, "?");
        add(TokenID.SYMBOL_DOUBLE_PERIOD, "..");
        add(TokenID.SYMBOL_TRIPLE_PERIOD, "...");
        add(TokenID.SYMBOL_DOUBLE_COLON, "::");
        add(TokenID.SYMBOL_ARROW, "->");
        add(TokenID.SYMBOL_AT, "@");
    }

    private Symbol(Symbol token, int index) {
        this(token.getID(), token.getSource(), index, token.getCompoundAssignOperator());
    }

    private Symbol(TokenID id, String source, int index, Symbol compoundAssignOperator) {
        super(id, source, index);
        this.compoundAssignOperator = compoundAssignOperator;
    }

    public Symbol getCompoundAssignOperator() {
        return compoundAssignOperator;
    }

    public static boolean is(char source) {
        return CHAR_SYMBOLS[source] != null;
    }

    public static boolean is(String source) {
        if (source.length() == 1) {
            return is(source.charAt(0));
        }
        return SYMBOLS.containsKey(source);
    }

    public static Symbol from(char source, int index) {
        final Symbol symbol = CHAR_SYMBOLS[source];
        return symbol == null ? null : new Symbol(symbol, index);
    }

    public static Symbol from(String source, int index) {
        if (source.length() == 1) {
            return from(source.charAt(0), index);
        }
        final Symbol symbol = SYMBOLS.get(source);
        return symbol == null ? null : new Symbol(symbol, index);
    }

    public static Collection<Symbol> all() {
        return Collections.unmodifiableCollection(SYMBOLS.values());
    }

    private static void add(TokenID id, String source) {
        add(id, source, null);
    }

    private static void add(TokenID id, String source, String compoundAssignOperator) {
        final Symbol symbol = new Symbol(id, source, 0, SYMBOLS.get(compoundAssignOperator));
        SYMBOLS.put(source, symbol);
        if (source.length() == 1) {
            CHAR_SYMBOLS[source.charAt(0)] = symbol;
        }
    }
} public enum TokenID {
    IDENTIFIER(TokenGroup.IDENTIFIER),
    KEYWORD_ASSERT(TokenGroup.UNSPECIFIED),
    KEYWORD_IF(TokenGroup.UNSPECIFIED),
    KEYWORD_ELSE(TokenGroup.UNSPECIFIED),
    KEYWORD_WHILE(TokenGroup.UNSPECIFIED),
    KEYWORD_DO(TokenGroup.UNSPECIFIED),
    KEYWORD_FOR(TokenGroup.UNSPECIFIED),
    KEYWORD_BREAK(TokenGroup.UNSPECIFIED),
    KEYWORD_CONTINUE(TokenGroup.UNSPECIFIED),
    KEYWORD_SWITCH(TokenGroup.UNSPECIFIED),
    KEYWORD_CASE(TokenGroup.UNSPECIFIED),
    KEYWORD_DEFAULT(TokenGroup.UNSPECIFIED),
    KEYWORD_RETURN(TokenGroup.UNSPECIFIED),
    KEYWORD_THROW(TokenGroup.UNSPECIFIED),
    KEYWORD_TRY(TokenGroup.UNSPECIFIED),
    KEYWORD_CATCH(TokenGroup.UNSPECIFIED),
    KEYWORD_FINALLY(TokenGroup.UNSPECIFIED),
    KEYWORD_VOID(TokenGroup.UNSPECIFIED),
    KEYWORD_BOOLEAN(TokenGroup.PRIMITIVE_TYPE),
    KEYWORD_BYTE(TokenGroup.PRIMITIVE_TYPE),
    KEYWORD_SHORT(TokenGroup.PRIMITIVE_TYPE),
    KEYWORD_CHAR(TokenGroup.PRIMITIVE_TYPE),
    KEYWORD_INT(TokenGroup.PRIMITIVE_TYPE),
    KEYWORD_LONG(TokenGroup.PRIMITIVE_TYPE),
    KEYWORD_FLOAT(TokenGroup.PRIMITIVE_TYPE),
    KEYWORD_DOUBLE(TokenGroup.PRIMITIVE_TYPE),
    KEYWORD_CLASS(TokenGroup.CLASS_TYPE),
    KEYWORD_INTERFACE(TokenGroup.CLASS_TYPE),
    KEYWORD_ENUM(TokenGroup.CLASS_TYPE),
    KEYWORD_EXTENDS(TokenGroup.UNSPECIFIED),
    KEYWORD_IMPLEMENTS(TokenGroup.UNSPECIFIED),
    KEYWORD_SUPER(TokenGroup.SELF_REFERENCE),
    KEYWORD_THIS(TokenGroup.SELF_REFERENCE),
    KEYWORD_PACKAGE(TokenGroup.UNSPECIFIED),
    KEYWORD_IMPORT(TokenGroup.UNSPECIFIED),
    KEYWORD_PUBLIC(TokenGroup.ACCESS_MODIFIER),
    KEYWORD_PROTECTED(TokenGroup.ACCESS_MODIFIER),
    KEYWORD_PRIVATE(TokenGroup.ACCESS_MODIFIER),
    KEYWORD_THROWS(TokenGroup.UNSPECIFIED),
    KEYWORD_ABSTRACT(TokenGroup.OTHER_MODIFIER),
    KEYWORD_STRICTFP(TokenGroup.OTHER_MODIFIER),
    KEYWORD_TRANSIENT(TokenGroup.OTHER_MODIFIER),
    KEYWORD_VOLATILE(TokenGroup.OTHER_MODIFIER),
    KEYWORD_FINAL(TokenGroup.OTHER_MODIFIER),
    KEYWORD_STATIC(TokenGroup.OTHER_MODIFIER),
    KEYWORD_SYNCHRONIZED(TokenGroup.OTHER_MODIFIER),
    KEYWORD_NATIVE(TokenGroup.OTHER_MODIFIER),
    KEYWORD_NEW(TokenGroup.UNSPECIFIED),
    KEYWORD_INSTANCEOF(TokenGroup.BINARY_OPERATOR),
    KEYWORD_GOTO(TokenGroup.UNUSED),
    KEYWORD_CONST(TokenGroup.UNUSED),
    SYMBOL_PERIOD(TokenGroup.CALL_OPERATOR),
    SYMBOL_COMMA(TokenGroup.UNSPECIFIED),
    SYMBOL_COLON(TokenGroup.UNSPECIFIED),
    SYMBOL_SEMICOLON(TokenGroup.UNSPECIFIED),
    SYMBOL_PLUS(TokenGroup.ADD_OPERATOR),
    SYMBOL_MINUS(TokenGroup.ADD_OPERATOR),
    SYMBOL_MULTIPLY(TokenGroup.MULTIPLY_OPERATOR),
    SYMBOL_DIVIDE(TokenGroup.MULTIPLY_OPERATOR),
    SYMBOL_MODULO(TokenGroup.MULTIPLY_OPERATOR),
    SYMBOL_INCREMENT(TokenGroup.UNARY_OPERATOR),
    SYMBOL_DECREMENT(TokenGroup.UNARY_OPERATOR),
    SYMBOL_BITWISE_AND(TokenGroup.BINARY_OPERATOR),
    SYMBOL_BITWISE_OR(TokenGroup.BINARY_OPERATOR),
    SYMBOL_BITWISE_NOT(TokenGroup.UNARY_OPERATOR),
    SYMBOL_BITWISE_XOR(TokenGroup.BINARY_OPERATOR),
    SYMBOL_LOGICAL_LEFT_SHIFT(TokenGroup.SHIFT_OPERATOR),
    SYMBOL_ARITHMETIC_RIGHT_SHIFT(TokenGroup.SHIFT_OPERATOR),
    SYMBOL_LOGICAL_RIGHT_SHIFT(TokenGroup.SHIFT_OPERATOR),
    SYMBOL_BOOLEAN_NOT(TokenGroup.UNARY_OPERATOR),
    SYMBOL_BOOLEAN_AND(TokenGroup.BINARY_OPERATOR),
    SYMBOL_BOOLEAN_OR(TokenGroup.BINARY_OPERATOR),
    SYMBOL_LESSER(TokenGroup.COMPARISON_OPERATOR),
    SYMBOL_GREATER(TokenGroup.COMPARISON_OPERATOR),
    SYMBOL_GREATER_OR_EQUAL(TokenGroup.COMPARISON_OPERATOR),
    SYMBOL_LESSER_OR_EQUAL(TokenGroup.COMPARISON_OPERATOR),
    SYMBOL_EQUAL(TokenGroup.EQUAL_OPERATOR),
    SYMBOL_NOT_EQUAL(TokenGroup.EQUAL_OPERATOR),
    SYMBOL_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_ADD_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_SUBTRACT_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_MULTIPLY_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_DIVIDE_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_REMAINDER_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_BITWISE_AND_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_BITWISE_OR_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_BITWISE_XOR_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_LOGICAL_LEFT_SHIFT_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_ARITHMETIC_RIGHT_SHIFT_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_LOGICAL_RIGHT_SHIFT_ASSIGN(TokenGroup.ASSIGNMENT),
    SYMBOL_OPEN_PARENTHESIS(TokenGroup.UNSPECIFIED),
    SYMBOL_CLOSE_PARENTHESIS(TokenGroup.UNSPECIFIED),
    SYMBOL_OPEN_BRACKET(TokenGroup.UNSPECIFIED),
    SYMBOL_CLOSE_BRACKET(TokenGroup.UNSPECIFIED),
    SYMBOL_OPEN_BRACE(TokenGroup.UNSPECIFIED),
    SYMBOL_CLOSE_BRACE(TokenGroup.UNSPECIFIED),
    SYMBOL_DOUBLE_SLASH(TokenGroup.COMMENT_DELIMITER),
    SYMBOL_SLASH_STAR(TokenGroup.COMMENT_DELIMITER),
    SYMBOL_STAR_SLASH(TokenGroup.COMMENT_DELIMITER),
    SYMBOL_QUESTION_MARK(TokenGroup.UNSPECIFIED),
    SYMBOL_DOUBLE_PERIOD(TokenGroup.UNUSED),
    SYMBOL_TRIPLE_PERIOD(TokenGroup.UNSPECIFIED),
    SYMBOL_DOUBLE_COLON(TokenGroup.UNSPECIFIED),
    SYMBOL_ARROW(TokenGroup.UNSPECIFIED),
    SYMBOL_AT(TokenGroup.UNSPECIFIED),
    LITERAL_TRUE(TokenGroup.LITERAL),
    LITERAL_FALSE(TokenGroup.LITERAL),
    LITERAL_CHARACTER(TokenGroup.LITERAL),
    LITERAL_NULL(TokenGroup.LITERAL),
    LITERAL_STRING(TokenGroup.LITERAL),
    LITERAL_DOUBLE(TokenGroup.LITERAL),
    LITERAL_FLOAT(TokenGroup.LITERAL),
    LITERAL_INT(TokenGroup.LITERAL),
    LITERAL_LONG(TokenGroup.LITERAL);
    private final TokenGroup group;

    TokenID(TokenGroup group) {
        this.group = group;
    }

    public TokenGroup getGroup() {
        return group;
    }
} public interface Expression extends SourceIndexed {
    Type getType(Environment environment);

    Value getValue(Environment environment);

    void setStart(int start);

    void setEnd(int end);

    String toString();
} public final class TypeUtil {
    private TypeUtil() {
    }

    public static <T extends Type> Set<T> removeSubTypes(Collection<T> types) {
        return filterTypes(types, SubTypeFilter.INSTANCE, SuperTypeFilter.INSTANCE);
    }

    public static <T extends Type> Set<T> removeSuperTypes(Collection<T> types) {
        return filterTypes(types, SuperTypeFilter.INSTANCE, SubTypeFilter.INSTANCE);
    }

    private static <T extends Type> Set<T> filterTypes(Collection<T> types, BiPredicate<Type, Type> potentialFilter, BiPredicate<Type, Type> existingFilter) {
        if (types.isEmpty()) {
            // Fast track trivial cases
            return Collections.emptySet();
        }
        if (types.size() == 1) {
            // Fast track trivial cases
            return Collections.singleton(types.iterator().next());
        }
        // Discard any member that is a super type of another
        final Set<T> filteredTypes = new LinkedHashSet<>();
        potentialCandidates:
        for (T potentialCandidate : types) {
            for (Iterator<T> iterator = filteredTypes.iterator(); iterator.hasNext(); ) {
                final T existingCandidate = iterator.next();
                if (potentialFilter.test(potentialCandidate, existingCandidate)) {
                    // potential candidate is filtered for existing one, don't add
                    continue potentialCandidates;
                } else if (existingFilter.test(potentialCandidate, existingCandidate)) {
                    // existing candidate is filtered for potential one, remove it
                    iterator.remove();
                }
            }
            filteredTypes.add(potentialCandidate);
        }
        return filteredTypes;
    }

    public static Set<SingleReferenceType> expandIntersectionTypes(Collection<? extends ReferenceType> intersection) {
        final Set<SingleReferenceType> expandedTypes = new LinkedHashSet<>(intersection.size());
        for (ReferenceType type : intersection) {
            if (type instanceof IntersectionType) {
                expandedTypes.addAll(((IntersectionType) type).getTypes());
            } else {
                expandedTypes.add((SingleReferenceType) type);
            }
        }
        return expandedTypes;
    }

    public static IntersectionType lowestUpperBound(ReferenceType... types) {
        return lowestUpperBound(Arrays.asList(types));
    }

    public static IntersectionType lowestUpperBound(Collection<? extends ReferenceType> types) {
        return lowestUpperBound(expandIntersectionTypes(types));
    }

    public static IntersectionType lowestUpperBound(Set<SingleReferenceType> types) {
        return lowestUpperBound(types, new HashMap<Set<SingleReferenceType>, Integer>());
    }

    private static IntersectionType lowestUpperBound(Set<SingleReferenceType> types, Map<Set<SingleReferenceType>, Integer> recursions) {
        // Simplify the argument set to standardize it and make things faster
        types = TypeUtil.removeSubTypes(types);
        if (types.isEmpty()) {
            // Fast track trivial cases
            return IntersectionType.NOTHING;
        }
        // Get the number of recursive calls so far with the same arguments
        Integer count = recursions.get(types);
        if (count == null) {
            count = 0;
        }
        if (++count > 2) {
            // Recursive lowest upper bound calls can lead to infinite types, don't compute a bound after two recursions (like javac does)
            return IntersectionType.NOTHING;
        }
        recursions.put(types, count);
        if (types.size() == 1) {
            // Fast track trivial cases
            return IntersectionType.of(types);
        }
        // Get the super type sets ST(U)
        final Map<ReferenceType, Set<LiteralReferenceType>> superTypeSets = new HashMap<>();
        final Collection<Set<LiteralReferenceType>> superTypes = superTypeSets.values();
        for (SingleReferenceType type : types) {
            if (type.convertibleTo(NullType.THE_NULL)) {
                // The null type has a universal set of super types, we can skip it
                continue;
            }
            superTypeSets.put(type, type.getSuperTypes());
        }
        // Intersect the erased super type sets EST(U) to generate the erased candidate set EC
        final Set<LiteralReferenceType> erasedCandidates = new HashSet<>();
        final Iterator<Set<LiteralReferenceType>> superTypesIterator = superTypes.iterator();
        erasedCandidates.addAll(getErasures(superTypesIterator.next()));
        while (superTypesIterator.hasNext()) {
            erasedCandidates.retainAll(getErasures(superTypesIterator.next()));
        }
        // Get the minimal erased candidate set MEC
        final Set<LiteralReferenceType> minimalErasedCandidates = removeSuperTypes(erasedCandidates);
        // For generic candidates (those with relevant invocations) compute the least containing invocation
        final Set<SingleReferenceType> lowestUpperBound = new HashSet<>();
        for (LiteralReferenceType candidate : minimalErasedCandidates) {
            final Set<ParametrizedType> invocations = relevantInvocations(superTypes, candidate);
            if (invocations.isEmpty()) {
                lowestUpperBound.add(candidate);
            } else {
                lowestUpperBound.add(leastContainingInvocation(invocations, recursions));
            }
        }
        return IntersectionType.of(lowestUpperBound);
    }

    private static ParametrizedType leastContainingInvocation(Set<ParametrizedType> invocations, Map<Set<SingleReferenceType>, Integer> recursions) {
        final Iterator<ParametrizedType> iterator = invocations.iterator();
        ParametrizedType left = iterator.next();
        if (invocations.size() == 1) {
            // For a single invocation we get the least containing type argument of each invocation argument
            final List<TypeArgument> arguments = left.getArguments();
            final List<TypeArgument> leastArguments = new ArrayList<>(arguments.size());
            for (TypeArgument argument : arguments) {
                leastArguments.add(leastContainingTypeArgument(argument, recursions));
            }
            return ParametrizedType.of(left.getErasure(), leastArguments);
        }
        // For many invocation we reduce pairwise, left with right
        while (iterator.hasNext()) {
            // The least containing type argument is done pairwise on the arguments of the left and right type
            final ParametrizedType right = iterator.next();
            final List<TypeArgument> leftArguments = left.getArguments();
            final List<TypeArgument> rightArguments = right.getArguments();
            final List<TypeArgument> leastArguments = new ArrayList<>(leftArguments.size());
            for (int i = 0; i < leftArguments.size(); i++) {
                leastArguments.add(leastContainingTypeArgument(leftArguments.get(i), rightArguments.get(i), recursions));
            }
            left = ParametrizedType.of(left.getErasure(), leastArguments);
        }
        return left;
    }

    private static TypeArgument leastContainingTypeArgument(TypeArgument left, TypeArgument right, Map<Set<SingleReferenceType>, Integer> recursions) {
        // In the case where one argument isn't a wildcard type, ensure the non-wildcard is on the left
        if (!(right instanceof WildcardType)) {
            final TypeArgument swap = left;
            left = right;
            right = swap;
        }
        if (left instanceof WildcardType) {
            // Pair of wildcards
            return leastContainingTypeArgument((WildcardType) left, (WildcardType) right, recursions);
        }
        if (right instanceof WildcardType) {
            // Only one wildcard
            return leastContainingTypeArgument(left, (WildcardType) right, recursions);
        }
        // No wildcards
        final Set<SingleReferenceType> combinedUpperBound = new HashSet<>();
        combinedUpperBound.add((SingleReferenceType) left);
        combinedUpperBound.add((SingleReferenceType) right);
        return left.equals(right) ? left : WildcardType.of(IntersectionType.EVERYTHING, lowestUpperBound(combinedUpperBound, recursions));
    }

    private static TypeArgument leastContainingTypeArgument(WildcardType left, WildcardType right, Map<Set<SingleReferenceType>, Integer> recursions) {
        final Set<SingleReferenceType> combinedUpperBound = new HashSet<>(left.getUpperBound().getTypes());
        final Set<SingleReferenceType> combinedLowerBound = new HashSet<>(left.getLowerBound().getTypes());
        combinedUpperBound.addAll(right.getUpperBound().getTypes());
        combinedLowerBound.addAll(right.getLowerBound().getTypes());
        return combinedUpperBound.equals(combinedLowerBound) ? IntersectionType.of(combinedUpperBound)
                : WildcardType.of(IntersectionType.of(combinedLowerBound), lowestUpperBound(combinedUpperBound, recursions));
    }

    private static TypeArgument leastContainingTypeArgument(TypeArgument left, WildcardType right, Map<Set<SingleReferenceType>, Integer> recursions) {
        final Set<SingleReferenceType> combinedUpperBound = new HashSet<>(right.getUpperBound().getTypes());
        final Set<SingleReferenceType> combinedLowerBound = new HashSet<>(right.getLowerBound().getTypes());
        combinedUpperBound.add((SingleReferenceType) left);
        combinedLowerBound.add((SingleReferenceType) left);
        return WildcardType.of(IntersectionType.of(combinedLowerBound), lowestUpperBound(combinedUpperBound, recursions));
    }

    private static TypeArgument leastContainingTypeArgument(TypeArgument argument, Map<Set<SingleReferenceType>, Integer> recursions) {
        final Set<SingleReferenceType> combinedUpperBound = new HashSet<>();
        final Set<SingleReferenceType> combinedLowerBound = new HashSet<>();
        // Left type is argument
        if (argument instanceof WildcardType) {
            final WildcardType wildcard = (WildcardType) argument;
            combinedUpperBound.addAll(wildcard.getUpperBound().getTypes());
            combinedLowerBound.addAll(wildcard.getLowerBound().getTypes());
        } else {
            combinedUpperBound.add((SingleReferenceType) argument);
            combinedLowerBound.add((SingleReferenceType) argument);
        }
        // Right type is ?
        combinedUpperBound.add(LiteralReferenceType.THE_OBJECT);
        combinedLowerBound.add(NullType.THE_NULL);
        return WildcardType.of(IntersectionType.of(combinedLowerBound), lowestUpperBound(combinedUpperBound, recursions));
    }

    private static Set<ParametrizedType> relevantInvocations(Collection<Set<LiteralReferenceType>> superTypeSets, LiteralReferenceType type) {
        // Search for an invocation of the erased type in any of the super type sets
        final Set<ParametrizedType> invocations = new HashSet<>();
        for (Set<LiteralReferenceType> superTypes : superTypeSets) {
            for (LiteralReferenceType superType : superTypes) {
                if (superType instanceof ParametrizedType) {
                    final ParametrizedType parametrizedType = (ParametrizedType) superType;
                    if (parametrizedType.getErasure().equals(type)) {
                        invocations.add(parametrizedType);
                    }
                }
            }
        }
        return invocations;
    }

    private static Set<LiteralReferenceType> getErasures(Set<LiteralReferenceType> types) {
        final Set<LiteralReferenceType> erased = new HashSet<>();
        for (LiteralReferenceType type : types) {
            erased.add(type.getErasure());
        }
        return erased;
    }

    public static PrimitiveType coerceToPrimitive(Environment environment, Expression expression) {
        return coerceToPrimitive(expression, expression.getType(environment));
    }

    public static PrimitiveType coerceToPrimitive(Expression expression, Type type) {
        final PrimitiveType primitiveType;
        if (type instanceof PrimitiveType) {
            primitiveType = (PrimitiveType) type;
        } else if (type instanceof LiteralReferenceType && ((LiteralReferenceType) type).isBox()) {
            primitiveType = ((LiteralReferenceType) type).unbox();
        } else {
            throw new EvaluatorException("Not a primitive type: " + type.getName(), expression);
        }
        return primitiveType;
    }

    public static boolean isValidReferenceCast(ReferenceType sourceType, ReferenceType targetType) {
        if (sourceType.isNull()) {
            // source type is null, can always cast to anything
            return true;
        }
        if (sourceType instanceof LiteralReferenceType) {
            // source is a literal reference type, which has sub-cases
            final LiteralReferenceType source = (LiteralReferenceType) sourceType;
            if (source.isArray()) {
                // source is an array type, which has sub-cases
                if (targetType instanceof LiteralReferenceType) {
                    // target is a literal reference type, which has sub-cases
                    final LiteralReferenceType target = (LiteralReferenceType) targetType;
                    if (target.isArray()) {
                        // target is an array type, allow if same primitive type else apply recursively on components
                        final ComponentType sourceComponent = source.getComponentType();
                        final ComponentType targetComponent = target.getComponentType();
                        return sourceComponent.isPrimitive() && targetComponent.isPrimitive() && sourceComponent.equals(targetComponent)
                                || isValidReferenceCast((ReferenceType) sourceComponent, (ReferenceType) targetComponent);
                    }
                    // target is a class or interface type, only allow object, serializable and cloneable
                    return target.equals(LiteralReferenceType.THE_OBJECT) || target.equals(LiteralReferenceType.THE_SERIALIZABLE)
                            || target.equals(LiteralReferenceType.THE_CLONEABLE);
                }
                if (targetType instanceof TypeVariable) {
                    // target is a type variable, apply recursively to upper bound
                    final TypeVariable target = (TypeVariable) targetType;
                    return isValidReferenceCast(source, target.getUpperBound());
                }
                // any other target type is undefined
                return false;
            }
            if (source.isInterface()) {
                // source is an interface type, which has sub-cases
                if (targetType instanceof LiteralReferenceType) {
                    // target is a literal reference type, which has sub-cases
                    final LiteralReferenceType target = (LiteralReferenceType) targetType;
                    if (target.isArray()) {
                        // target is an array type, source must be serializable or cloneable
                        return source.equals(LiteralReferenceType.THE_SERIALIZABLE) || source.equals(LiteralReferenceType.THE_CLONEABLE);
                    }
                    if (Modifier.isFinal(target.getTypeClass().getModifiers())) {
                        // target is final, which has sub-cases
                        if (target instanceof ParametrizedType || target.isRaw()) {
                            // target is parametrized or raw, target must be an invocation of source generic declaration
                            return hasInvocationSuperType(target, source);
                        }
                        // target is neither parametrized nor raw, target must implement source
                        return target.convertibleTo(source);
                    }
                    // target is not final, generic parents cannot conflict
                    return !haveProvablyDistinctInvocationsInSuperTypes(source, target);
                }
                // any other target type is always valid
                return true;
            }
            // source is a class type, which has sub-cases
            if (targetType instanceof LiteralReferenceType) {
                // target is a literal reference type, which has sub-cases
                final LiteralReferenceType target = (LiteralReferenceType) targetType;
                if (target.isArray()) {
                    // target is an array type, source must be object
                    return source.equals(LiteralReferenceType.THE_OBJECT);
                }
                if (target.isInterface()) {
                    // target is an interface type, which has sub-cases
                    if (Modifier.isFinal(source.getTypeClass().getModifiers())) {
                        // source is final, source must implement target
                        return source.convertibleTo(target);
                    }
                    // source is not final, generic parents cannot conflict
                    return !haveProvablyDistinctInvocationsInSuperTypes(source, target);
                }
                // target is a class type, erasures must be super or sub types and generic parents cannot conflict
                final LiteralReferenceType sourceErasure = source.getErasure();
                final LiteralReferenceType targetErasure = target.getErasure();
                return (sourceErasure.convertibleTo(targetErasure) || targetErasure.convertibleTo(targetErasure))
                        && !haveProvablyDistinctInvocationsInSuperTypes(source, target);
            }
            if (targetType instanceof TypeVariable) {
                // target is a type variable, apply recursively to upper bound
                final TypeVariable target = (TypeVariable) targetType;
                return isValidReferenceCast(source, target.getUpperBound());
            }
        }
        if (sourceType instanceof TypeVariable) {
            // source is a type variable, apply recursively to upper bound
            final TypeVariable source = (TypeVariable) sourceType;
            return isValidReferenceCast(source.getUpperBound(), targetType);
        }
        if (sourceType instanceof IntersectionType) {
            // source is an intersection type, apply to each member individually
            final IntersectionType source = (IntersectionType) sourceType;
            for (SingleReferenceType type : source.getTypes()) {
                if (!isValidReferenceCast(type, targetType)) {
                    return false;
                }
            }
            return true;
        }
        // any other source type is undefined
        return false;
    }

    public static boolean haveProvablyDistinctInvocationsInSuperTypes(LiteralReferenceType... types) {
        return haveProvablyDistinctInvocationsInSuperTypes(Arrays.asList(types));
    }

    public static boolean haveProvablyDistinctInvocationsInSuperTypes(Collection<LiteralReferenceType> types) {
        return validateInvocationsInSuperTypes(types, ProvablyDistinctFilter.INSTANCE);
    }

    public static boolean haveDifferentInvocationsInSuperTypes(LiteralReferenceType... types) {
        return haveDifferentInvocationsInSuperTypes(Arrays.asList(types));
    }

    public static boolean haveDifferentInvocationsInSuperTypes(Collection<LiteralReferenceType> types) {
        return validateInvocationsInSuperTypes(types, NotEqualsFilter.<ParametrizedType, ParametrizedType>getInstance());
    }

    public static boolean validateInvocationsInSuperTypes(Collection<LiteralReferenceType> types,
                                                          BiPredicate<ParametrizedType, ParametrizedType> test) {
        if (types.size() <= 1) {
            // Fast track trivial cases
            return false;
        }
        // Keep a map of all super types that have the same erasure, excluding non-parametrized types
        final Map<LiteralReferenceType, ParametrizedType> commonErasedSuperTypes = new HashMap<>();
        final Iterator<LiteralReferenceType> iterator = types.iterator();
        // Add all parametrized parents and their erasures for the first type
        for (LiteralReferenceType superType : iterator.next().getSuperTypes()) {
            if (superType instanceof ParametrizedType) {
                commonErasedSuperTypes.put(superType.getErasure(), (ParametrizedType) superType);
            }
        }
        // For the other types, look for an entry in the common set and ensure they have a valid parametrization
        // If not, fail; if so, add to the common set
        do {
            for (LiteralReferenceType superType : iterator.next().getSuperTypes()) {
                if (!(superType instanceof ParametrizedType)) {
                    continue;
                }
                final ParametrizedType parametrizedSuperType = (ParametrizedType) superType;
                final LiteralReferenceType erasure = parametrizedSuperType.getErasure();
                final ParametrizedType commonSuperType = commonErasedSuperTypes.get(erasure);
                if (commonSuperType != null) {
                    if (test.test(parametrizedSuperType, commonSuperType)) {
                        return true;
                    }
                } else {
                    commonErasedSuperTypes.put(erasure, parametrizedSuperType);
                }
            }
        } while (iterator.hasNext());
        // No conflict found, we're good
        return false;
    }

    private static boolean hasInvocationSuperType(LiteralReferenceType type, LiteralReferenceType invocation) {
        // We need an invocation of the same declaration in the super types (that is, same erasures) with the same arguments
        final LiteralReferenceType declaration = invocation.getErasure();
        for (LiteralReferenceType superType : type.getSuperTypes()) {
            if (superType.getErasure().equals(declaration)) {
                return superType.equals(invocation);
            }
        }
        return false;
    }

    private interface BiPredicate<S, T> {
        boolean test(S s, T t);
    }

    private static class SubTypeFilter implements BiPredicate<Type, Type> {
        private static final SubTypeFilter INSTANCE = new SubTypeFilter();

        @Override
        public boolean test(Type left, Type right) {
            return left.convertibleTo(right);
        }
    }

    private static class SuperTypeFilter implements BiPredicate<Type, Type> {
        private static final SuperTypeFilter INSTANCE = new SuperTypeFilter();

        @Override
        public boolean test(Type left, Type right) {
            return right.convertibleTo(left);
        }
    }

    private static class NotEqualsFilter<S, T> implements BiPredicate<S, T> {
        private static final NotEqualsFilter<?, ?> INSTANCE = new NotEqualsFilter<>();

        @Override
        public boolean test(S left, T right) {
            return !left.equals(right);
        }

        @SuppressWarnings("unchecked")
        private static <S, T> NotEqualsFilter<S, T> getInstance() {
            return (NotEqualsFilter<S, T>) INSTANCE;
        }
    }

    private static class ProvablyDistinctFilter implements BiPredicate<ParametrizedType, ParametrizedType> {
        private static final ProvablyDistinctFilter INSTANCE = new ProvablyDistinctFilter();

        @Override
        public boolean test(ParametrizedType left, ParametrizedType right) {
            return left.provablyDistinct(right);
        }
    }
}
import ca.sapon.jici.evaluator.value.IntValue;
import ca.sapon.jici.evaluator.value.LongValue;
import ca.sapon.jici.evaluator.value.ObjectValue;
import ca.sapon.jici.evaluator.value.Value;
import ca.sapon.jici.evaluator.value.ValueKind;
import ca.sapon.jici.lexer.Symbol;
import ca.sapon.jici.lexer.TokenID;
import ca.sapon.jici.parser.expression.Expression;
import ca.sapon.jici.util.TypeUtil;
import ca.sapon.jici.evaluator.Environment;
import ca.sapon.jici.evaluator.EvaluatorException;
import ca.sapon.jici.evaluator.type.LiteralReferenceType;
import ca.sapon.jici.evaluator.type.PrimitiveType;
import ca.sapon.jici.evaluator.type.Type;
import ca.sapon.jici.evaluator.value.DoubleValue;
import ca.sapon.jici.evaluator.value.FloatValue;
/*
 * This file is part of JICI, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2015-2016 Aleksi Sapon <http://sapon.ca/jici/>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package ca.sapon.jici.parser.expression.arithmetic;


public class Arithmetic implements Expression {
    private final Expression left;
    private final Expression right;
    private final Symbol operator;
    private int start;
    private int end;
    private boolean stringConcatenation = false;
    private Type leftType = null;
    private Type type = null;

    public Arithmetic(Expression left, Expression right, Symbol operator) {
        this.left = left;
        this.right = right;
        this.operator = operator;
        start = left.getStart();
        end = right.getEnd();
    }

    @Override
    public Type getType(Environment environment) {
        if (type == null) {
            final Type leftType = left.getType(environment);
            final Type rightType = right.getType(environment);
            if (operator.getID() == TokenID.SYMBOL_PLUS && (leftType.equals(LiteralReferenceType.THE_STRING) || rightType.equals(LiteralReferenceType.THE_STRING))) {
                if (leftType.isVoid()) {
                    throw new EvaluatorException("Cannot convert void to java.lang.String", left);
                }
                if (rightType.isVoid()) {
                    throw new EvaluatorException("Cannot convert void to java.lang.String", right);
                }
                type = LiteralReferenceType.THE_STRING;
                stringConcatenation = true;
                this.leftType = leftType;
            } else {
                final PrimitiveType leftPrimitiveType = TypeUtil.coerceToPrimitive(left, leftType);