Overriding Operations on Types

The last section illustrates how semantic changes can be implemented by overriding operations on ASTs. While most semantic changes are handled this ways, certain semantic changes are solely based on types. Type classes define many operations on types so that the type system needs not depend on the AST for this kind of semantic checks. These type operations should be overridden to accommodate semantic changes on types.
For example, in the CArray extension, we cannot assign a constant array to a nonconstant array. Moreover, constant arrays are covariant, i.e., for constant arrays ca1 and ca2, the assignment ca2 = ca1 is legal only if the type of ca1 is a subtype of the type of ca2. Let t1 be the type of ca1 and similarly for t2 as the type of ca2. It follows that both t1 and t2 are instances of ConstArrayType. Then, t1 is a subtype of t2 if the base type of t1 is a subtype of the base type of t2.
The semantic enforcement above requires an appropriate answer to the question: Can something of type t1 be assigned to something of type t2? This question is a query on types. First, let us take a look at how type checking for assignments is performed. The following is an excerpt from method typeCheck in class Assign_c that issues this type query, where s is the same as t1 above, and t is the same as t2 above:

There are three subformulas in the conditional expression: Failures to satisfy any of these predicates mean that an expression of type s cannot be assigned to an expression of type t, so a compile-time error is issued. For our CArray extension, we only have to modify the definition of the first predicate, isImplicitCastValid.
The type system forwards the query isImplicitCastValid(from, to) to instance method isImplicitCastValidImpl of from type. In this way, the implementations of operations on types are defined in type classes without cluttering the implementation of the type system, and also maintain modularity between type classes.
To implement the semantic enforcement on constant-array types, we will override isImplicitCastValidImpl in ConstArrayType_c. Here is the implementation in ArrayType_c to be overridden:
    @Override
    public boolean isImplicitCastValidImpl(Type toType) {
        if (toType.isArray()) {
            if (base().isPrimitive() || toType.toArray().base().isPrimitive()) {
                return ts.typeEquals(base(), toType.toArray().base());
            }
            else {
                return ts.isImplicitCastValid(base(), toType.toArray().base());
            }
        }

        // toType is not an array, but this is.  Check if the array
        // is a subtype of the toType.  This happens when toType
        // is java.lang.Object.
        return ts.isSubtype(this, toType);
    }
Given toType, the type to be assigned to, if toType is an array type, there are two cases:
  1. If the base types of the two array types are primitive, then these two base types must be equivalent.
  2. Otherwise, the base type of the source array must be assignable to the base type of the target array.
Otherwise, toType is not an array type. In this case, the array type must be a subtype of toType to make the assignment legal. This happens when toType is the primordial class Object.
First, let us begin with a skeleton of the overriding method in ConstArrayType_c:
    @Override
    public boolean isImplicitCastValidImpl(Type toType) {
        // TODO: Implement this method.
    }
Given a type to be assigned to (toType), when is it legal to assign a value of constant-array type to toType? There are three cases to consider:
  1. toType is not an array type. Then, toType must be the primordial class Object for the assignment to be legal. Since the superclass handles this case properly, we simply invoke the superclass implementation:
            if (!toType.isArray()) {
                // ?1 = ?2 const[]
                // This const array type is assignable to ?1 only if ?1 is Object.
                // Let the base language check this fact.
                return super.isImplicitCastValidImpl(toType);
            }
    		
  2. Otherwise, toType must be an array type. If toType is a constant-array type, then the source array type must be assignable to the target array type. That is, covariance must hold. Since the superclass also handles this case properly, we also invoke the superclass implementation:
            if (toType instanceof ConstArrayType) {
                // ?1 const[] = ?2 const[]
                // Let the base language check whether ?2 is assignable to ?1.
                return super.isImplicitCastValidImpl(toType);
            }
    		
  3. Otherwise, toType must be a nonconstant-array type. Since we cannot assign a constant array to a nonconstant array, the result of the type query is always false:
            // ?1[] = ?2 const[]
            // We cannot assign a const array to a non-const array.
            return false;
    		
Here is the full implementation of isImplicitCastValidImpl:
    @Override
    public boolean isImplicitCastValidImpl(Type toType) {
        if (!toType.isArray()) {
            // ?1 = ?2 const[]
            // This const array type is assignable to ?1 only if ?1 is Object.
            // Let the base language check this fact.
            return super.isImplicitCastValidImpl(toType);
        }

        // From this point, toType is an array.
        if (toType instanceof ConstArrayType) {
            // ?1 const[] = ?2 const[]
            // Let the base language check whether ?2 is assignable to ?1.
            return super.isImplicitCastValidImpl(toType);
        }

        // From this point, toType is a non-const array.
        // ?1[] = ?2 const[]
        // We cannot assign a const array to a non-const array.
        return false;
    }
In addition to isImplicitCastValidImpl, more operations on type objects for constant-array types need to be redefined:

Exercise

Implement the semantic change in CArray related to casting from constant-array types. That is, constant arrays can be cast to other constant arrays as long as casts are valid between base types. That is, the validity of casts between constant-array types are the same as that of traditional array types in Java. The method of interest is isCastValidImpl, which returns true if this type can be cast to the given type.
Solution: + Reveal...
Here is the implementation of ConstArrayType_c so far: + Reveal...

Exercise

Not all semantic changes have been implemented. Specifically, the following checks must be implemented: Therefore, ArrayType_c must be extended to handle these changes. Implement these changes in class CArrayArrayType_c.
Hint: + Reveal...
Solution: + Reveal...
Now, all test cases should pass. That means our implementation of CArray respects our designed semantics. Congratulations on building a working language extension in Polyglot!
Next, we will explore the language dispatcher, which ensures that the correct implementation of AST operations gets chosen, which in turn ensures the correctness of the language extension. We will also learn that language dispatchers can be extended to add new operations on ASTs, thereby supporting new compiler passes, which are useful if we would like to perform an analysis that does not exist in the base language, or if we would like to prepare the AST for later phases of the compiler, such as translation to other languages.