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 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 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 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 class IntLiteral extends NumberLiteral {
    private int value = 0;
    private boolean evaluated = false;

    public IntLiteral(String source, int index) {
        super(TokenID.LITERAL_INT, source, index);
    }

    private IntLiteral(String source, int start, int end) {
        super(TokenID.LITERAL_INT, source, start, end);
    }

    private void evaluate() {
        if (evaluated) {
            return;
        }
        String source = StringUtil.reduceSign(getSource());
        final int radix = StringUtil.findRadix(source);
        try {
            source = StringUtil.removeRadixIdentifier(source, radix);
            value = Integer.parseInt(source, radix);
        } catch (Exception exception) {
            throw new EvaluatorException(exception.getMessage(), this);
        }
        evaluated = true;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

    @Override
    public Type getType(Environment environment) {
        evaluate();
        return PrimitiveType.THE_INT;
    }

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

    @Override
    public Value getValue(Environment environment) {
        return this;
    }

    public IntLiteral withSign(Symbol sign) {
        final TokenID id = sign.getID();
        if (id != TokenID.SYMBOL_MINUS && id != TokenID.SYMBOL_PLUS) {
            throw new UnsupportedOperationException("Expected a sign symbol");
        }
        return new IntLiteral(sign.getSource() + (sign.getSource().charAt(0) == getSource().charAt(0) ? ' ' : "") + getSource(), sign.getIndex(), getEnd());
    }
} public interface Expression extends SourceIndexed {
    Type getType(Environment environment);

    Value getValue(Environment environment);

    void setStart(int start);

    void setEnd(int end);

    String toString();
} public class Shift implements Expression {
    private final Expression left;
    private final Expression right;
    private final Symbol operator;
    private int start;
    private int end;
    private Type type = null;
    private Type shiftType = null;

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

    @Override
    public Type getType(Environment environment) {
        if (type == null) {
            final PrimitiveType leftType = TypeUtil.coerceToPrimitive(environment, left);
            final PrimitiveType rightType = TypeUtil.coerceToPrimitive(environment, right);
            if (!leftType.isIntegral()) {
                throw new EvaluatorException("Not an integral type: " + leftType.getName(), left);
            }
            if (!rightType.isIntegral()) {
                throw new EvaluatorException("Not an integral type: " + rightType.getName(), right);
            }
            type = leftType.unaryWiden();
            shiftType = rightType.unaryWiden();
        }
        return type;
    }

    @Override
    public Value getValue(Environment environment) {
        final Value leftValue = left.getValue(environment);
        final Value rightValue = right.getValue(environment);
        final ValueKind leftWidenKind = type.getKind();
        final ValueKind rightWidenKind = shiftType.getKind();
        switch (operator.getID()) {
            case SYMBOL_LOGICAL_LEFT_SHIFT:
                return doLogicalLeftShift(leftValue, leftWidenKind, rightValue, rightWidenKind);
            case SYMBOL_LOGICAL_RIGHT_SHIFT:
                return doLogicalRightShift(leftValue, leftWidenKind, rightValue, rightWidenKind);
            case SYMBOL_ARITHMETIC_RIGHT_SHIFT:
                return doArithmeticRightShift(leftValue, leftWidenKind, rightValue, rightWidenKind);
            default:
                throw new EvaluatorException("Invalid operator for shift: " + operator, operator);
        }
    }

    @Override
    public int getStart() {
        return start;
    }

    @Override
    public int getEnd() {
        return end;
    }

    @Override
    public void setStart(int start) {
        this.start = start;
    }

    @Override
    public void setEnd(int end) {
        this.end = end;
    }

    private Value doLogicalLeftShift(Value leftValue, ValueKind leftWidenKind, Value rightValue, ValueKind rightWidenKind) {
        switch (leftWidenKind) {
            case INT:
                switch (rightWidenKind) {
                    case INT:
                        return IntValue.of(leftValue.asInt() << rightValue.asInt());
                    case LONG:
                        return IntValue.of(leftValue.asInt() << rightValue.asLong());
                    default:
                        throw new EvaluatorException("Invalid type for logical-left-shift right operand: " + rightWidenKind, this);
                }
            case LONG:
                switch (rightWidenKind) {
                    case INT:
                        return LongValue.of(leftValue.asLong() << rightValue.asInt());
                    case LONG:
                        return LongValue.of(leftValue.asLong() << rightValue.asLong());
                    default:
                        throw new EvaluatorException("Invalid type for logical-left-shift right operand: " + rightWidenKind, this);
                }
            default:
                throw new EvaluatorException("Invalid type for logical-left-shift left operand: " + leftWidenKind, this);
        }
    }

    private Value doLogicalRightShift(Value leftValue, ValueKind leftWidenKind, Value rightValue, ValueKind rightWidenKind) {
        switch (leftWidenKind) {
            case INT:
                switch (rightWidenKind) {
                    case INT:
                        return IntValue.of(leftValue.asInt() >>> rightValue.asInt());
                    case LONG:
                        return IntValue.of(leftValue.asInt() >>> rightValue.asLong());
                    default:
                        throw new EvaluatorException("Invalid type for logical-right-shift right operand: " + rightWidenKind, this);
                }
            case LONG:
                switch (rightWidenKind) {
                    case INT:
                        return LongValue.of(leftValue.asLong() >>> rightValue.asInt());
                    case LONG:
                        return LongValue.of(leftValue.asLong() >>> rightValue.asLong());
                    default:
                        throw new EvaluatorException("Invalid type for logical-right-shift right operand: " + rightWidenKind, this);
                }
            default:
                throw new EvaluatorException("Invalid type for logical-right-shift left operand: " + leftWidenKind, this);
        }
    }

    private Value doArithmeticRightShift(Value leftValue, ValueKind leftWidenKind, Value rightValue, ValueKind rightWidenKind) {
        switch (leftWidenKind) {
            case INT:
                switch (rightWidenKind) {
                    case INT:
                        return IntValue.of(leftValue.asInt() >> rightValue.asInt());
                    case LONG:
                        return IntValue.of(leftValue.asInt() >> rightValue.asLong());
                    default:
                        throw new EvaluatorException("Invalid type for arithmetic-right-shift right operand: " + rightWidenKind, this);
                }
            case LONG:
                switch (rightWidenKind) {
                    case INT:
                        return LongValue.of(leftValue.asLong() >> rightValue.asInt());
                    case LONG:
                        return LongValue.of(leftValue.asLong() >> rightValue.asLong());
                    default:
                        throw new EvaluatorException("Invalid type for arithmetic-right-shift right operand: " + rightWidenKind, this);
                }
            default:
                throw new EvaluatorException("Invalid type for arithmetic-right-shift left operand: " + leftWidenKind, this);
        }
    }

    @Override
    public String toString() {
        return "Shift(" + left + " " + operator + " " + right + ")";
    }
} 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);
                final PrimitiveType rightPrimitiveType = TypeUtil.coerceToPrimitive(right, rightType);
                if (!leftPrimitiveType.isNumeric()) {
                    throw new EvaluatorException("Not a numeric type: " + leftPrimitiveType.getName(), left);
                }
                if (!rightPrimitiveType.isNumeric()) {
                    throw new EvaluatorException("Not a numeric type: " + rightPrimitiveType.getName(), right);
                }
                type = leftPrimitiveType.binaryWiden(rightPrimitiveType);
                this.leftType = leftPrimitiveType;
            }
        }
        return type;
    }

    public Type getLeftType() {
        return leftType;
    }

    @Override
    public Value getValue(Environment environment) {
        final Value leftValue = left.getValue(environment);
        final Value rightValue = right.getValue(environment);
        if (stringConcatenation) {
            return doStringConcatenation(leftValue, rightValue);
        }
        final ValueKind widenKind = type.getKind();
        switch (operator.getID()) {
            case SYMBOL_PLUS:
                return doAdd(leftValue, rightValue, widenKind);
            case SYMBOL_MINUS:
                return doSubtract(leftValue, rightValue, widenKind);
            case SYMBOL_MULTIPLY:
                return doMultiply(leftValue, rightValue, widenKind);
            case SYMBOL_DIVIDE:
                return doDivide(leftValue, rightValue, widenKind);
            case SYMBOL_MODULO:
                return doModulo(leftValue, rightValue, widenKind);
            default:
                throw new EvaluatorException("Invalid operator for arithmetic: " + operator, operator);
        }
    }

    @Override
    public int getStart() {
        return start;
    }

    @Override
    public int getEnd() {
        return end;
    }

    @Override
    public void setStart(int start) {
        this.start = start;
    }

    @Override
    public void setEnd(int end) {
        this.end = end;
    }

    private Value doStringConcatenation(Value leftValue, Value rightValue) {
        return ObjectValue.of(leftValue.asString() + rightValue.asString());
    }

    private Value doAdd(Value leftValue, Value rightValue, ValueKind widenKind) {
        switch (widenKind) {
            case INT:
                return IntValue.of(leftValue.asInt() + rightValue.asInt());
            case LONG:
                return LongValue.of(leftValue.asLong() + rightValue.asLong());
            case FLOAT:
                return FloatValue.of(leftValue.asFloat() + rightValue.asFloat());
            case DOUBLE:
                return DoubleValue.of(leftValue.asDouble() + rightValue.asDouble());
            default:
                throw new EvaluatorException("Invalid type for add: " + widenKind, this);
        }
    }

    private Value doSubtract(Value leftValue, Value rightValue, ValueKind widenKind) {
        switch (widenKind) {
            case INT:
                return IntValue.of(leftValue.asInt() - rightValue.asInt());
            case LONG:
                return LongValue.of(leftValue.asLong() - rightValue.asLong());
            case FLOAT:
                return FloatValue.of(leftValue.asFloat() - rightValue.asFloat());
            case DOUBLE:
                return DoubleValue.of(leftValue.asDouble() - rightValue.asDouble());
            default:
                throw new EvaluatorException("Invalid type for subtract: " + widenKind, this);
        }
    }

    private Value doMultiply(Value leftValue, Value rightValue, ValueKind widenKind) {
        switch (widenKind) {
            case INT:
                return IntValue.of(leftValue.asInt() * rightValue.asInt());
            case LONG:
                return LongValue.of(leftValue.asLong() * rightValue.asLong());
            case FLOAT:
                return FloatValue.of(leftValue.asFloat() * rightValue.asFloat());
            case DOUBLE:
                return DoubleValue.of(leftValue.asDouble() * rightValue.asDouble());
            default:
                throw new EvaluatorException("Invalid type for multiply: " + widenKind, this);
        }
    }

    private Value doDivide(Value leftValue, Value rightValue, ValueKind widenKind) {
        switch (widenKind) {
            case INT:
                return IntValue.of(leftValue.asInt() / rightValue.asInt());
            case LONG:
                return LongValue.of(leftValue.asLong() / rightValue.asLong());
            case FLOAT:
                return FloatValue.of(leftValue.asFloat() / rightValue.asFloat());
            case DOUBLE:
                return DoubleValue.of(leftValue.asDouble() / rightValue.asDouble());
            default:
                throw new EvaluatorException("Invalid type for divide: " + widenKind, this);
        }
    }

    private Value doModulo(Value leftValue, Value rightValue, ValueKind widenKind) {
        switch (widenKind) {
            case INT:
                return IntValue.of(leftValue.asInt() % rightValue.asInt());
            case LONG:
                return LongValue.of(leftValue.asLong() % rightValue.asLong());
            case FLOAT:
                return FloatValue.of(leftValue.asFloat() % rightValue.asFloat());
            case DOUBLE:
                return DoubleValue.of(leftValue.asDouble() % rightValue.asDouble());
            default:
                throw new EvaluatorException("Invalid type for modulo: " + widenKind, this);
        }
    }

    @Override
    public String toString() {
        return "Arithmetic(" + left + " " + operator + " " + right + ")";
    }
} public class BitwiseLogic implements Expression {
    private final Expression left;
    private final Expression right;
    private final Symbol operator;
    private int start;
    private int end;
    private Type type = null;

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

    @Override
    public Type getType(Environment environment) {
        if (type == null) {
            final PrimitiveType leftType = TypeUtil.coerceToPrimitive(environment, left);
            final PrimitiveType rightType = TypeUtil.coerceToPrimitive(environment, right);
            if (leftType.isBoolean()) {
                if (rightType.isBoolean()) {
                    type = PrimitiveType.THE_BOOLEAN;
                } else {
                    throw new EvaluatorException("Not a boolean type: " + rightType.getName(), right);
                }
            } else if (leftType.isIntegral()) {
                if (rightType.isIntegral()) {
                    type = leftType.binaryWiden(rightType);
                } else {
                    throw new EvaluatorException("Not an integral type: " + rightType.getName(), right);
                }
            } else {
                throw new EvaluatorException("Not a boolean or integral type: " + leftType.getName(), left);
            }
        }
        return type;
    }

    @Override
    public Value getValue(Environment environment) {
        final Value leftValue = left.getValue(environment);
        final Value rightValue = right.getValue(environment);
        final ValueKind widenKind = type.getKind();
        switch (operator.getID()) {
            case SYMBOL_BITWISE_AND:
                return doBitwiseAND(leftValue, rightValue, widenKind);
            case SYMBOL_BITWISE_XOR:
                return doBitwiseXOR(leftValue, rightValue, widenKind);
            case SYMBOL_BITWISE_OR:
                return doBitwiseOR(leftValue, rightValue, widenKind);
            default:
                throw new EvaluatorException("Invalid operator for bitwise logic: " + operator, operator);
        }
    }

    @Override
    public int getStart() {
        return start;
    }

    @Override
    public int getEnd() {
        return end;
    }

    @Override
    public void setStart(int start) {
        this.start = start;
    }

    @Override
    public void setEnd(int end) {
        this.end = end;
    }

    private Value doBitwiseAND(Value leftValue, Value rightValue, ValueKind widenKind) {
        switch (widenKind) {
            case BOOLEAN:
                return BooleanValue.of(leftValue.asBoolean() & rightValue.asBoolean());
            case INT:
                return IntValue.of(leftValue.asInt() & rightValue.asInt());
            case LONG:
                return LongValue.of(leftValue.asLong() & rightValue.asLong());
            default:
                throw new EvaluatorException("Invalid type for bitwise AND: " + widenKind, this);
        }
    }

    private Value doBitwiseXOR(Value leftValue, Value rightValue, ValueKind widenKind) {
        switch (widenKind) {
            case BOOLEAN:
                return BooleanValue.of(leftValue.asBoolean() ^ rightValue.asBoolean());
            case INT:
                return IntValue.of(leftValue.asInt() ^ rightValue.asInt());
            case LONG:
                return LongValue.of(leftValue.asLong() ^ rightValue.asLong());
            default:
                throw new EvaluatorException("Invalid type for bitwise XOR: " + widenKind, this);
        }
    }

    private Value doBitwiseOR(Value leftValue, Value rightValue, ValueKind widenKind) {
        switch (widenKind) {
            case BOOLEAN:
                return BooleanValue.of(leftValue.asBoolean() | rightValue.asBoolean());
            case INT:
                return IntValue.of(leftValue.asInt() | rightValue.asInt());
            case LONG:
                return LongValue.of(leftValue.asLong() | rightValue.asLong());
            default:
                throw new EvaluatorException("Invalid type for bitwise OR: " + widenKind, this);
        }
    }

    @Override
    public String toString() {
        return "BitwiseLogic(" + left + " " + operator + " " + right + ")";
    }
} public interface Reference extends Expression {
    Type getTargetType(Environment environment);

    void setValue(Environment environment, Value value);

    String toString();
} public class VariableAccess implements Reference {
    private final Identifier name;
    private int start;
    private int end;
    private Type type = null;

    public VariableAccess(Identifier name) {
        this.name = name;
        start = name.getStart();
        end = name.getEnd();
    }

    @Override
    public Type getType(Environment environment) {
        return getTargetType(environment);
    }

    @Override
    public Type getTargetType(Environment environment) {
        if (type != null) {
            return type;
        }
        try {
            type = environment.getVariableType(name);
        } catch (UnsupportedOperationException exception) {
            throw new EvaluatorException(exception.getMessage(), name);
        }
        return type;
    }

    @Override
    public Value getValue(Environment environment) {
        try {
            return environment.getVariable(name);
        } catch (UnsupportedOperationException exception) {
            throw new EvaluatorException(exception.getMessage(), name);
        }
    }

    @Override
    public void setValue(Environment environment, Value value) {
        try {
            environment.setVariable(name, value);
        } catch (UnsupportedOperationException exception) {
            throw new EvaluatorException(exception.getMessage(), name);
        }
    }

    @Override
    public int getStart() {
        return start;
    }

    @Override
    public int getEnd() {
        return end;
    }

    @Override
    public void setStart(int start) {
        this.start = start;
    }

    @Override
    public void setEnd(int end) {
        this.end = end;
    }

    @Override
    public String toString() {
        return name.toString();
    }
} public interface Statement extends SourceIndexed {
    void execute(Environment environment);

    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.parser.expression.Shift;
import ca.sapon.jici.parser.expression.arithmetic.Arithmetic;
import ca.sapon.jici.parser.expression.logic.BitwiseLogic;
import ca.sapon.jici.parser.expression.reference.Reference;
import ca.sapon.jici.parser.expression.reference.VariableAccess;
import ca.sapon.jici.parser.statement.Statement;
import ca.sapon.jici.util.TypeUtil;
import ca.sapon.jici.evaluator.Environment;
import ca.sapon.jici.evaluator.EvaluatorException;
import ca.sapon.jici.evaluator.type.Type;
import ca.sapon.jici.evaluator.value.Value;
import ca.sapon.jici.lexer.Symbol;
import ca.sapon.jici.lexer.literal.number.IntLiteral;
import ca.sapon.jici.parser.expression.Expression;
/*
 * 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.assignment;


public class Assignment implements Expression, Statement {
    private final Reference assignee;
    private final Expression value;
    private int start;
    private int end;
    private boolean simpleAssign = false;
    private Type type = null;

    public Assignment(Reference assignee, Expression value, Symbol operator) {
        this.assignee = assignee;
        switch (operator.getID()) {
            case SYMBOL_ASSIGN:
                simpleAssign = true;
                this.value = value;
                break;
            case SYMBOL_ADD_ASSIGN:
            case SYMBOL_SUBTRACT_ASSIGN:
            case SYMBOL_MULTIPLY_ASSIGN:
            case SYMBOL_DIVIDE_ASSIGN:
            case SYMBOL_REMAINDER_ASSIGN:
                this.value = new Arithmetic(assignee, value, operator.getCompoundAssignOperator());
                break;
            case SYMBOL_BITWISE_AND_ASSIGN:
            case SYMBOL_BITWISE_OR_ASSIGN:
            case SYMBOL_BITWISE_XOR_ASSIGN:
                this.value = new BitwiseLogic(assignee, value, operator.getCompoundAssignOperator());
                break;
            case SYMBOL_LOGICAL_LEFT_SHIFT_ASSIGN:
            case SYMBOL_ARITHMETIC_RIGHT_SHIFT_ASSIGN:
            case SYMBOL_LOGICAL_RIGHT_SHIFT_ASSIGN:
                this.value = new Shift(assignee, value, operator.getCompoundAssignOperator());
                break;
            default:
                throw new EvaluatorException("Not a valid operator for assign: " + operator, operator);
        }
        start = assignee.getStart();
        end = value.getEnd();
    }

    @Override
    public void execute(Environment environment) {
        try {
            getType(environment);
            getValue(environment);
        } catch (EvaluatorException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new EvaluatorException(exception, this);
        }
    }

    @Override
    public Type getType(Environment environment) {
        if (type != null) {
            return type;
        }
        Type valueType = value.getType(environment);
        final Type assigneeType = assignee.getTargetType(environment);
        // Must capture variables subject to assignment conversion
        if (value instanceof VariableAccess) {