/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * RecordType.java * Created: Feb 17, 2004 * By: Bo Ilic */ package org.openquark.cal.compiler; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import org.openquark.cal.internal.serialization.ModuleSerializationTags; import org.openquark.cal.internal.serialization.RecordInputStream; import org.openquark.cal.internal.serialization.RecordOutputStream; /** * A class used to represent record types such as: * literal (non record-polymorphic) records: * <ul> * <li> {name :: String, height :: Double} * <li> Num a => {foo :: [a], bar :: Char, zap :: Maybe a} * </ul> * record-polymorphic records: * <ul> * <li> r\field => {r | field1 :: Char} * <li> (r\name, r\height) => {r | name :: String, height :: Double} * </ul> * <p> * Implementation note: * RecordType objects can be though of as defining a finite sequence of extensions from a base record variable: * the non-record polymorphic records are: {NO_FIELDS | extensionFieldsMap1 | extensionFieldsMap2 | ... | extensionFieldsMapN} * the record-polymorphic records are: r\lacksFields => {r | extensionFieldsMap1 | extensionFieldsMap2 | ... | extensionFieldsMapN} * Where the extensionFieldsMaps are restricted by construction to having distinct domains (i.e. the fields don't overlap) and there * are similar consistency constraints on the record variable r. * <p> * After type-checking however, we can reduce these representations to the simpler cannonical forms: * {NO_FIELDS | hasFieldsMap} * r\lacksFields => {r | hasFieldsMap} * <p> * This is similar to how TypeExpr values after type-checking are reduced to having no instantiated type variables (deep pruning). * * @author Bo Ilic */ public final class RecordType extends TypeExpr { private static final int serializationSchema = 0; /** * Corresponds to what is on the left hand side of the topmost extension operator (|) in a record type. * If the record type is e.g. (r\field1, r\field2, r\field3, r\foo) => {r | field1 :: Int, field2 :: Char, field3 :: Boolean} * then recordVar corresponds to the "r" part. * If the record type is e.g. {{field1 :: Int, field2 :: Char} | field3 :: Boolean}, then this corresponds to a RecordVar * which is instantiated to {field1 :: Int, field2 :: Char}. */ private RecordVar baseRecordVar; /** * (FieldName -> TypeExpr) the baseRecordVar is extended with additional fields, and this map holds onto the fields * along with their associated types. By construction, the fields in the extension cannot belong to the base. * * For example, for the record * (r\field1, r\field2, r\field3, r\foo) => {r | field1 :: Int, field2 :: Char, field3 :: Boolean} * this is the map: [(field1, Int), (field2, Char), (field3, Boolean)]. * * For the record * {{field1 :: Int, field2 :: Char} | field3 :: Boolean}, this is the map: [(field3, Boolean)]. * * For the record: * (r\field1, r\field2) -> { {r | field1 :: Int} | field2 :: Char} this is the map [(field2, Char)]. */ private Map<FieldName, TypeExpr> extensionFieldsMap; /** * Note that all the fields in the extensionFieldsMap must be * lacks constraints in the pruned recordVar if this is a polymorphic * record extension (although the recordVar can have more lacks constraints). * * @param recordVar RecordVar * @param extensionFieldsMap Map (FieldName -> TypeExpr) */ RecordType (RecordVar recordVar, Map<FieldName, TypeExpr> extensionFieldsMap) { if (recordVar == null || extensionFieldsMap == null) { throw new NullPointerException(); } //a sanity check- it is a programming error if this exception is thrown. RecordVar prunedRecordVar = recordVar.prune(); if (!prunedRecordVar.isNoFields() && !prunedRecordVar.getLacksFieldsSet().containsAll(extensionFieldsMap.keySet())) { //Can't have extensions such as {r | field1 :: Int}. r must have a field1 lacks constraint. Set<FieldName> missingLacksConstraints = new HashSet<FieldName>(extensionFieldsMap.keySet()); missingLacksConstraints.removeAll(prunedRecordVar.getLacksFieldsSet()); throw new IllegalArgumentException("The extension fields " + missingLacksConstraints + " must be lacks constraints on the record variable." ); } this.baseRecordVar = recordVar; this.extensionFieldsMap = extensionFieldsMap; } // private constructor used by serialization code. private RecordType () { } /** * @return The base record type, or null if the baseRecordVar is NO_FIELDS or an uninstantiated record var. */ RecordType getBaseRecordType() { return baseRecordVar.getInstance(); } /** * If baseRecordVar is instantiated (i.e. it is not NO_FIELDS or an uninstantiated record * variable), then this function follows the chain of instantiations to get to a RecordVar * which is NO_FIELDS or uninstantiated. * * @return follows the chain of instantiated RecordVars to reach an uninstantiated RecordVar * or NO_FIELDS. The returned RecordVar may be this.baseRecordVar if this.baseRecordVar * is already uninstantiated or NO_FIELDS. */ RecordVar getPrunedRecordVar() { return baseRecordVar.prune(); } /** * @return (FieldName -> TypeExpr) an unmodifiable version of extensionFieldsMap */ Map<FieldName, TypeExpr> getExtensionFieldsMap() { return Collections.unmodifiableMap(extensionFieldsMap); } /** * Collects the full mapping of fields to type expressions specified by this record. In other words, the domain of this * map is the fields that this record is asserted to have. The returned map is a copy, and can be freely modified * by callers. * @return SortedMap */ public SortedMap<FieldName, TypeExpr> getHasFieldsMap() { SortedMap<FieldName, TypeExpr> hasFieldsMap = new TreeMap<FieldName, TypeExpr>(extensionFieldsMap); for (RecordType baseRecordType = getBaseRecordType(); baseRecordType != null; baseRecordType = baseRecordType.getBaseRecordType()) { hasFieldsMap.putAll(baseRecordType.extensionFieldsMap); } return hasFieldsMap; } /** * The fields that this record is asserted to have. The returned Set is a copy, and can be * freely modified by callers. * @return SortedSet (of FieldName objects) */ SortedSet<FieldName> getHasFields() { SortedSet<FieldName> hasFieldsSet = new TreeSet<FieldName>(extensionFieldsMap.keySet()); for (RecordType baseRecordType = getBaseRecordType(); baseRecordType != null; baseRecordType = baseRecordType.getBaseRecordType()) { hasFieldsSet.addAll(baseRecordType.extensionFieldsMap.keySet()); } return hasFieldsSet; } /** * Returns the field names that this record is asserted to have, ordered by the ordering on FieldName * (which is ordinal field names first, in numerical order, followed by textual field names in * alphabetic order). * * For example, for the record type * r\name => {title :: String, author :: {r | name :: [Char]}, #5 :: Int}, * this returns the list ["#5", "author", "title"]. * 'name' is not part of this list because it is not part of the outermost record type. * * @return List (FieldName) of has field names, ordered in ascending FieldName order. * This is a copy, and can be freely modified by the caller. */ public List<FieldName> getHasFieldNames() { //todoBI why was this function added? The list does not have duplicates so why not just return a SortedSet? return new ArrayList<FieldName>(getHasFields()); } /** * Returns the number of field that this record type is asserted to have. * * For example, for the record type * (r\name) => {title :: String, author :: {r | name :: [Char]}}, * this returns 2, counting 'title' and 'author', but not 'name'. * * @return the number of fields that this RecordType is asserted to have. */ public int getNHasFields() { int nHasFields = extensionFieldsMap.size(); for (RecordType baseRecordType = getBaseRecordType(); baseRecordType != null; baseRecordType = baseRecordType.getBaseRecordType()) { nHasFields += baseRecordType.extensionFieldsMap.size(); } return nHasFields; } /** * Get the type expression for a has field of this record * * Ex: for the record type * (r\name) => {title :: String, author :: {r | name :: [Char]}}, * the field "title" has type String, but field "name" is not a has field of this record * and will return null. * * @param fieldName * @return TypeExpr type expression of the specified field, or null if the field is not a has field. */ public TypeExpr getHasFieldType(FieldName fieldName) { if (fieldName == null) { throw new NullPointerException(); } TypeExpr fieldType = extensionFieldsMap.get(fieldName); if (fieldType != null) { return fieldType; } for (RecordType baseRecordType = getBaseRecordType(); baseRecordType != null; baseRecordType = baseRecordType.getBaseRecordType()) { fieldType = baseRecordType.extensionFieldsMap.get(fieldName); if (fieldType != null) { return fieldType; } } return null; } /** {@inheritDoc} */ @Override public boolean containsTypeExpr(TypeExpr searchTypeExpr) { if (this == searchTypeExpr) { return true; } RecordType baseRecordType = getBaseRecordType(); if (baseRecordType != null) { if (baseRecordType.containsTypeExpr(searchTypeExpr)) { return true; } } for (final Map.Entry<FieldName, TypeExpr> entry : extensionFieldsMap.entrySet()) { TypeExpr fieldTypeExpr = entry.getValue(); if (fieldTypeExpr.containsTypeExpr(searchTypeExpr)) { return true; } } return false; } /** {@inheritDoc} */ @Override boolean containsRecordVar(RecordVar searchRecordVar) { if (this.baseRecordVar == searchRecordVar) { return true; } RecordType baseRecordType = getBaseRecordType(); if (baseRecordType != null) { if (baseRecordType.containsRecordVar(searchRecordVar)) { return true; } } for (final Map.Entry<FieldName, TypeExpr> entry : extensionFieldsMap.entrySet()) { TypeExpr fieldTypeExpr = entry.getValue(); if (fieldTypeExpr.containsRecordVar(searchRecordVar)) { return true; } } return false; } /** {@inheritDoc} */ @Override public boolean isPolymorphic() { RecordType baseRecordType = getBaseRecordType(); if (baseRecordType != null) { if (baseRecordType.isPolymorphic()) { return true; } } else { if (!baseRecordVar.isNoFields()) { //an uninstantiated polymorphic row variable return true; } } for (final Map.Entry<FieldName, TypeExpr> entry : extensionFieldsMap.entrySet()) { TypeExpr fieldTypeExpr = entry.getValue(); if (fieldTypeExpr.isPolymorphic()) { return true; } } return false; } /** {@inheritDoc} */ @Override void getGenericClassConstrainedPolymorphicVars(Set<PolymorphicVar> varSet, NonGenericVars nonGenericVars) { RecordVar recordVar = this.getPrunedRecordVar(); if (!recordVar.isNoFields()) { //a record-polymorphic type if (!recordVar.noClassConstraints() && (nonGenericVars == null || nonGenericVars.isGenericRecordVar(recordVar))) { varSet.add(baseRecordVar); } } //important!!! //need to sort by field name so that varSet has a definite order (which is required by the contract of the function). SortedMap<FieldName, TypeExpr> hasFieldsMap = getHasFieldsMap(); for (final Map.Entry<FieldName, TypeExpr> entry : hasFieldsMap.entrySet()) { TypeExpr fieldTypeExpr = entry.getValue(); fieldTypeExpr.getGenericClassConstrainedPolymorphicVars(varSet, nonGenericVars); } } /** {@inheritDoc} */ @Override void getUninstantiatedTypeVars(Set<TypeVar> varSet) { RecordType baseRecordType = getBaseRecordType(); if (baseRecordType != null) { baseRecordType.getUninstantiatedTypeVars(varSet); } for (final Map.Entry<FieldName, TypeExpr> entry : extensionFieldsMap.entrySet()) { TypeExpr fieldTypeExpr = entry.getValue(); fieldTypeExpr.getUninstantiatedTypeVars(varSet); } } /** {@inheritDoc} */ @Override void getUninstantiatedRecordVars(Set<RecordVar> varSet) { if(baseRecordVar != null) { baseRecordVar.getUninstantiatedRecordVars(varSet); } RecordType baseRecordType = getBaseRecordType(); if(baseRecordType != null) { baseRecordType.getUninstantiatedRecordVars(varSet); } for(final TypeExpr fieldTypeExpr : extensionFieldsMap.values()) { fieldTypeExpr.getUninstantiatedRecordVars(varSet); } } /** {@inheritDoc} */ @Override public TypeExpr getCorrespondingTypeExpr(TypeExpr correspondingSuperType, TypeExpr typeToFind) { if (this == typeToFind) { return correspondingSuperType; } RecordType correspondingRecordType = correspondingSuperType.rootRecordType(); if (correspondingRecordType == null) { return null; } Map<FieldName, TypeExpr> hasFieldsMap = getHasFieldsMap(); Map<FieldName, TypeExpr> correspondingHasFieldsMap = correspondingRecordType.getHasFieldsMap(); for (final Map.Entry<FieldName, TypeExpr> entry : hasFieldsMap.entrySet()) { FieldName fieldName = entry.getKey(); TypeExpr fieldType = entry.getValue(); TypeExpr correspondingFieldType = correspondingHasFieldsMap.get(fieldName); //if the field does not exist in the super type record skip it - //provided the super type record is polymorphic it need not have all the fields of the specialized type if (correspondingFieldType == null) { continue; } TypeExpr correspondingType = fieldType.getCorrespondingTypeExpr(correspondingFieldType, typeToFind); if (correspondingType != null) { return correspondingType; } } return null; } /** {@inheritDoc} */ @Override public int getArity() { return 0; } /** {@inheritDoc} */ @Override void patternMatch(TypeExpr anotherTypeExpr, PatternMatchContext patternMatchContext) throws TypeException { if (anotherTypeExpr instanceof TypeVar) { TypeVar anotherTypeVar = (TypeVar)anotherTypeExpr; //anotherTypeVar must be uninstantiated. if (anotherTypeVar.getInstance()!= null) { throw new IllegalArgumentException("RecordType.patternMatch: programming error."); } //Can't instantiate a type variable from the declared type expression. //For example, this error occurs with the following declaration. //funnyFelix :: r\felix => {r | felix :: a} -> {r | }; //public funnyFelix r = r.felix; if (patternMatchContext.isUninstantiableTypeVar(anotherTypeVar)) { throw new TypeException("Attempt to instantiate a type variable from the declared type."); } //todoBI update to an example involving record types. The reasoning is still the same... //can't pattern match a nongeneric type variable to a type constructor involving uninstantiated type variables. //For example, for the function: //g x = let y :: [a]; y = x; in y; //we attempt to instantiate a non-generic type variables (corresponding to x) to [a]. //It is OK though to instantiate to a concrete type e.g. //g x = let y :: [Int]; y = x; in y; NonGenericVars nonGenericVars = patternMatchContext.getNonGenericVars(); if (nonGenericVars != null && !nonGenericVars.isGenericTypeVar(anotherTypeVar) && !this.getUninstantiatedTypeVars().isEmpty()) { throw new TypeException("Attempt to match a non-generic type variable to a type involving type variables."); } // check for infinite types e.g. an attempt to pattern match a to (a->Int). if (containsUninstantiatedTypeVar(anotherTypeVar)) { throw new TypeException("Type clash: attempt to create an infinite type."); } if (!anotherTypeVar.noClassConstraints()) { //check that there is a record instance for each of the class constraints on anotherTypeVar for (final TypeClass typeClass : anotherTypeVar.getTypeClassConstraintSet()) { ClassInstance classInstance = patternMatchContext.getModuleTypeInfo().getVisibleClassInstance(new ClassInstanceIdentifier.UniversalRecordInstance(typeClass.getName())); if (classInstance == null) { throw new TypeException("Type clash: the record type " + this +" not a member of the type class " + typeClass.getName() + "."); } } //create an intermediate record type whose recordVar and extension fields map all have //the type class constraint set of anotherTypeVar. This is because instances of records all have the //simple form "instance C a => C {a}" . //For example, to pattern match //{field1 :: Char, field2 :: Boolean} //and //(Eq a, Outputable a) => a //we create the intermediate record //(Eq r, Outputable r, r\field1, r\field2, Eq b, Outputable b, Eq c, Outputable c) => {r | field1 :: b, field2 :: c} //and then pattern match //{field1 :: Char, field2 :: Boolean} //and the intermediate record. //(In reality, we short circuit creation of a polymorphic intermediate record if possible). Map<FieldName, TypeExpr> constrainedExtensionFieldsMap = new HashMap<FieldName, TypeExpr>(); SortedSet<TypeClass> anotherTypeClassConstraintSet = anotherTypeVar.getTypeClassConstraintSet(); Set<FieldName> hasFieldsSet = this.getHasFieldsMap().keySet(); for (final FieldName fieldName : hasFieldsSet) { constrainedExtensionFieldsMap.put(fieldName, TypeVar.makeTypeVar(null, anotherTypeClassConstraintSet, false)); } RecordVar constrainedRecordVar; RecordVar recordVar = this.getPrunedRecordVar(); if (recordVar.isNoFields()) { constrainedRecordVar = RecordVar.NO_FIELDS; } else { constrainedRecordVar = RecordVar.makeRecordVar(null, hasFieldsSet, anotherTypeClassConstraintSet, false); } RecordType constrainedRecordType = new RecordType(constrainedRecordVar, constrainedExtensionFieldsMap); anotherTypeVar.setInstance(constrainedRecordType); patternMatch(constrainedRecordType, patternMatchContext); return; } anotherTypeVar.setInstance(this); return; } else if (anotherTypeExpr instanceof TypeConsApp || anotherTypeExpr instanceof TypeApp) { throw new TypeException("Type clash: The type declaration " + toString() + " does not match the type " + anotherTypeExpr + "."); } else if (anotherTypeExpr instanceof RecordType) { RecordType anotherRecordType = (RecordType)anotherTypeExpr; RecordVar recordVar = getPrunedRecordVar(); RecordVar anotherRecordVar = anotherRecordType.getPrunedRecordVar(); if (recordVar.isNoFields()) { if (anotherRecordVar.isNoFields()) { //2 literal (non-polymorphic) records. //the fields must match exactly Set<FieldName> hasFieldsSet = this.getHasFieldsMap().keySet(); Set<FieldName> anotherHasFieldsSet = anotherRecordType.getHasFieldsMap().keySet(); if (!hasFieldsSet.equals(anotherHasFieldsSet)) { throw new TypeException("the fields of the two record types " + this + " and " + anotherRecordType + " must match exactly."); } pairwisePatternMatch(hasFieldsSet, anotherRecordType, patternMatchContext); return; } //this RecordType is not polymorphic, anotherRecordType is. If they pattern match, then anotherRecordType will //be non-polymorphic with the same fields as this RecordType. //Can't instantiate a record variable from the declared type expression. //For example, this error occurs with the following declaration. //abcFunction :: (r\abc) => {r | } -> {abc :: Prelude.String}; //abcFunction x = {x | abc = "abc"}; if (patternMatchContext.isUninstantiableRecordVar(anotherRecordVar)) { throw new TypeException("Attempt to instantiate a record variable from the declared type."); } //the hasFields of this RecordType must contain all the hasFields of anotherRecordType. containsHasFieldsOf(anotherRecordType); //the fields in the hasFields of this RecordType that are not in the hasFields of anotherRecordType //must not be in the lacks fields of anotherRecordType's recordVar. //for example, {foo :: Char, bar :: Int} and (r\foo, r\bar) => {r | bar :: Int} can't pattern match //because to do so, we must set r = foo, but this is disallowed by the lacks constraint on r. checkHasLacksCompatibility(anotherRecordType); //the hasFields in common must pairwise pattern match Set<FieldName> anotherHasFieldsSet = anotherRecordType.getHasFieldsMap().keySet(); pairwisePatternMatch(anotherHasFieldsSet, anotherRecordType, patternMatchContext); //add the missing (fieldName, type) bindings to anotherRecordType Map<FieldName, TypeExpr> neededHasFieldsForAnotherRecordType = getNeededHasFields(anotherRecordType); //the needed has fields must satisfy the type class constraints of anotherRecordVar //for example, this check will prevent the pattern match of //{field1 :: Double -> Double} //and //Eq a => a //since the function type is not in Eq. patternMatchRecordVarConstraints(anotherRecordVar, neededHasFieldsForAnotherRecordType, patternMatchContext); //bind the recordVar for anotherRecordType to NO_FIELDS to make it non-polymorphic. anotherRecordVar.setInstance(new RecordType(RecordVar.NO_FIELDS, neededHasFieldsForAnotherRecordType)); return; } else { if (anotherRecordVar.isNoFields()) { //this RecordType is polymorphic, anotherRecordType is not. anotherRecordType cannot specialize to //this RecordType. throw new TypeException("Type clash: The type " + anotherRecordType + " cannot specialize to " + this +"."); } //both this RecordType and anotherRecordType are polymorphic. The pattern match, if possible, will be polymorphic. //the hasFields of this recordType must contain all the hasFields of anotherRecordType. containsHasFieldsOf(anotherRecordType); //the fields in the hasFields of this RecordType that are not in the hasFields of anotherRecordType //must not be in the lacks fields of anotherRecordType's recordVar. //for example, {foo :: Char, bar :: Int} and (r\foo, r\bar) => {r | bar :: Int} can't pattern match //because to do so, we must set r = foo, but this is disallowed by the lacks constraint on r. checkHasLacksCompatibility(anotherRecordType); //the lacks constraints on anotherRecordType.recordVar must be contained in the lacks constraints on this RecordTypes's recordVar. Set<FieldName> lacksFieldsSet = recordVar.getLacksFieldsSet(); Set<FieldName> anotherLacksFieldsSet = anotherRecordVar.getLacksFieldsSet(); if (!lacksFieldsSet.containsAll(anotherLacksFieldsSet)) { SortedSet<FieldName> missingFieldsSet = new TreeSet<FieldName>(anotherLacksFieldsSet); missingFieldsSet.removeAll(lacksFieldsSet); throw new TypeException("the record type " + anotherRecordType + " has lacks fields " + missingFieldsSet + " not included in the record type " + this +"."); } //the hasFields in common must pairwise pattern match Set<FieldName> anotherHasFieldsSet = anotherRecordType.getHasFieldsMap().keySet(); pairwisePatternMatch(anotherHasFieldsSet, anotherRecordType, patternMatchContext); //now we know that things pattern match. Must patch up anotherRecordType. //the has fields of both records will be the same. Map<FieldName, TypeExpr> neededHasFieldsForAnotherRecordType = getNeededHasFields(anotherRecordType); //the recordVar will be the same, in fact the very same instance... if (recordVar == anotherRecordVar) { if (neededHasFieldsForAnotherRecordType.isEmpty()) { //nothing needs to be done return; } else { //we want to set: anotherRecordVar.setInstance(new RecordType(recordVar, neededHasFieldsForAnotherRecordType)); //however, this will create a circular instantiation //todoBI probably more checks needed to avoid an infinite record typoe. throw new TypeException("Attempt to instantiate a record variable to itself"); } } if (patternMatchContext.isUninstantiableRecordVar(anotherRecordVar)) { throw new TypeException("Attempt to instantiate a record variable from the declared type."); } //can't pattern match a nongeneric record variable to a record-polymorphic record type. //For example, for the function: //gb s = // let // y :: (r\#1, r\#2) => {r | #1 :: Prelude.Double, #2 :: Prelude.Char}; // y = {s | #1 = 10.0, #2 = 'y'}; // in // y; //we should get a compile-time error at this point. //Intuitively, the problem is that the type signature for y declares r to be record-polymorphic //whereas in fact r is a non-generic record variable depending on the type of s and is not //free to be specialized independently of s. NonGenericVars nonGenericVars = patternMatchContext.getNonGenericVars(); if (nonGenericVars != null && !nonGenericVars.isGenericRecordVar(anotherRecordVar)) { throw new TypeException("Attempt to specialize a non-generic record variable to a record-polymorphic type."); } //the needed has fields must satisfy the type class constraints of anotherRecordVar //for example, this check will prevent the pattern match of //Eq r => {r | field1 :: Double -> Double} //and //Eq a => a //since the function type is not in Eq. patternMatchRecordVarConstraints(anotherRecordVar, neededHasFieldsForAnotherRecordType, patternMatchContext); //the class constraints on this RecordVar must be a superset of those on anotherRecordVar. //this prevents pattern matching things like: //Eq r => {r} //and //(Eq s, Outputable s) => {s} //(although the other was around would work fine). TypeVar.makeTypeVar(null, recordVar.getTypeClassConstraintSet(), false).patternMatch(TypeVar.makeTypeVar(null, anotherRecordVar.getTypeClassConstraintSet(), false), patternMatchContext); anotherRecordVar.setInstance(new RecordType(recordVar, neededHasFieldsForAnotherRecordType)); return; } } throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override TypeExpr prune() { return this; } /** {@inheritDoc} */ @Override TypeExpr deepPrune() { RecordVar prunedRecordVar = getPrunedRecordVar(); if (this.baseRecordVar != prunedRecordVar) { //after deep pruning, baseRecordVar is either NO_FIELDS or uninstantiated. Also, the hasFields are the same as the //extension fields extensionFieldsMap.putAll(baseRecordVar.getInstance().getHasFieldsMap()); baseRecordVar = prunedRecordVar; } if (!extensionFieldsMap.isEmpty()) { Map<FieldName, TypeExpr> updatedExtensionFields = new HashMap<FieldName, TypeExpr>(); for (final Map.Entry<FieldName, TypeExpr> entry : extensionFieldsMap.entrySet()) { FieldName extensionFieldName = entry.getKey(); TypeExpr fieldTypeExpr = entry.getValue(); TypeExpr deepPrunedFieldTypeExpr = fieldTypeExpr.deepPrune(); if (fieldTypeExpr != deepPrunedFieldTypeExpr) { updatedExtensionFields.put(extensionFieldName, deepPrunedFieldTypeExpr); } } extensionFieldsMap.putAll(updatedExtensionFields); } return this; } /** {@inheritDoc} */ @Override TypeExpr normalize() { RecordVar normalizedRecordVar = this.getPrunedRecordVar(); Map<FieldName, TypeExpr> normalizedExtensionFieldsMap = new HashMap<FieldName, TypeExpr>(getHasFieldsMap()); //FieldName -> TypeExpr for (final Map.Entry<FieldName, TypeExpr> entry : normalizedExtensionFieldsMap.entrySet()) { FieldName extensionFieldName = entry.getKey(); TypeExpr fieldTypeExpr = entry.getValue(); TypeExpr normalizedFieldTypeExpr = fieldTypeExpr.normalize(); if (fieldTypeExpr != normalizedFieldTypeExpr) { normalizedExtensionFieldsMap.put(extensionFieldName, normalizedFieldTypeExpr); } } return new RecordType(normalizedRecordVar, normalizedExtensionFieldsMap); } boolean isEmptyRecord() { return getNHasFields() == 0 && !isRecordPolymorphic(); } @Override public boolean sameType(TypeExpr anotherTypeExpr) { anotherTypeExpr = anotherTypeExpr.prune(); if (anotherTypeExpr instanceof RecordType) { return this.toString().equals(anotherTypeExpr.toString()); } if (anotherTypeExpr instanceof TypeConsApp || anotherTypeExpr instanceof TypeVar || anotherTypeExpr instanceof RecordType) { return false; } throw new IllegalStateException(); } /** {@inheritDoc} */ @Override void toSourceText(StringBuilder sb, PolymorphicVarContext polymorphicVarContext, ParenthesizationInfo parenthesizationInfo, ScopedEntityNamingPolicy namingPolicy) { RecordVar recordVar = this.getPrunedRecordVar(); //need to sort by field name so that the toString has a definite order. SortedMap<FieldName, TypeExpr> hasFieldsMap = getHasFieldsMap(); final int nFields = hasFieldsMap.size(); //if the record type represents a tuple (the fields are #1, #2, ..., #n with no gaps, and n >= 2), then display the type //using tuple notation. if (recordVar.isNoFields() && nFields >= 2) { FieldName lastFieldName = hasFieldsMap.lastKey(); if (lastFieldName instanceof FieldName.Ordinal) { FieldName.Ordinal lastOrdinalFieldName = (FieldName.Ordinal)lastFieldName; if (lastOrdinalFieldName.getOrdinal() == nFields) { sb.append('('); int fieldN = 0; for (final Map.Entry<FieldName, TypeExpr> entry : hasFieldsMap.entrySet()) { TypeExpr fieldTypeExpr = entry.getValue(); fieldTypeExpr.toSourceText(sb, polymorphicVarContext, TypeExpr.ParenthesizationInfo.NONE, namingPolicy); if (fieldN < nFields - 1) { sb.append(", "); } ++fieldN; } sb.append(')'); return; } } } sb.append('{'); if (!recordVar.isNoFields()) { sb.append(recordVar.toString(polymorphicVarContext)); if (nFields > 0) { sb.append(" | "); } } int fieldN = 0; for (final Map.Entry<FieldName, TypeExpr> entry : hasFieldsMap.entrySet()) { FieldName hasFieldName = entry.getKey(); TypeExpr fieldTypeExpr = entry.getValue(); sb.append(hasFieldName.getCalSourceForm()); sb.append(" :: "); fieldTypeExpr.toSourceText(sb, polymorphicVarContext, TypeExpr.ParenthesizationInfo.NONE, namingPolicy); if (fieldN < nFields - 1) { sb.append(", "); } ++fieldN; } sb.append('}'); } /** {@inheritDoc} */ @Override SourceModel.TypeExprDefn makeDefinitionSourceModel(PolymorphicVarContext polymorphicVarContext, ScopedEntityNamingPolicy namingPolicy) { RecordVar recordVar = this.getPrunedRecordVar(); //need to sort by field name so that the toString has a definite order. SortedMap<FieldName, TypeExpr> hasFieldsMap = getHasFieldsMap(); final int nFields = hasFieldsMap.size(); //if the record type represents a tuple (the fields are #1, #2, ..., #n with no gaps, and n >= 2), then display the type //using tuple notation. if (recordVar.isNoFields() && nFields >= 2) { FieldName lastFieldName = hasFieldsMap.lastKey(); if (lastFieldName instanceof FieldName.Ordinal) { FieldName.Ordinal lastOrdinalFieldName = (FieldName.Ordinal)lastFieldName; if (lastOrdinalFieldName.getOrdinal() == nFields) { SourceModel.TypeExprDefn[] components = new SourceModel.TypeExprDefn[nFields]; int fieldN = 0; for (final Map.Entry<FieldName, TypeExpr> entry : hasFieldsMap.entrySet()) { TypeExpr fieldTypeExpr = entry.getValue(); components[fieldN] = fieldTypeExpr.makeDefinitionSourceModel(polymorphicVarContext, namingPolicy); ++fieldN; } return SourceModel.TypeExprDefn.Tuple.make(components); } } } SourceModel.TypeExprDefn.TypeVar baseRecordVar = recordVar.isNoFields() ? null : SourceModel.TypeExprDefn.TypeVar.make(SourceModel.Name.TypeVar.make(recordVar.toString(polymorphicVarContext))); ArrayList<SourceModel.TypeExprDefn.Record.FieldTypePair> extensionFields = new ArrayList<SourceModel.TypeExprDefn.Record.FieldTypePair>(); for (final Map.Entry<FieldName, TypeExpr> entry : hasFieldsMap.entrySet()) { FieldName hasFieldName = entry.getKey(); TypeExpr fieldTypeExpr = entry.getValue(); extensionFields.add(SourceModel.TypeExprDefn.Record.FieldTypePair.make( SourceModel.Name.Field.make(hasFieldName), fieldTypeExpr.makeDefinitionSourceModel(polymorphicVarContext, namingPolicy))); } return SourceModel.TypeExprDefn.Record.make( baseRecordVar, extensionFields.toArray( new SourceModel.TypeExprDefn.Record.FieldTypePair[0])); } /** {@inheritDoc} */ @Override int unifyType(TypeExpr anotherTypeExpr, ModuleTypeInfo contextModuleTypeInfo) throws TypeException { if (anotherTypeExpr instanceof TypeVar || anotherTypeExpr instanceof TypeConsApp || anotherTypeExpr instanceof TypeApp) { return TypeExpr.unifyType(anotherTypeExpr, this, contextModuleTypeInfo); } else if (anotherTypeExpr instanceof RecordType) { RecordType anotherRecordType = (RecordType)anotherTypeExpr; RecordVar recordVar = this.getPrunedRecordVar(); RecordVar anotherRecordVar = anotherRecordType.getPrunedRecordVar(); if (recordVar.isNoFields()) { if (anotherRecordVar.isNoFields()) { //2 literal (non-polymorphic) records. //the fields must match exactly Set<FieldName> hasFieldsSet = this.getHasFieldsMap().keySet(); Set<FieldName> anotherHasFieldsSet = anotherRecordType.getHasFieldsMap().keySet(); if (!hasFieldsSet.equals(anotherHasFieldsSet)) { throw new TypeException("the fields of the two record type " + this + " and " + anotherRecordType + " must match exactly."); } return pairwiseUnify(hasFieldsSet, anotherRecordType, contextModuleTypeInfo); } //this RecordType is not polymorphic, anotherRecordType is. If they can unify, they will both be //non-polymorphic with the same fields as this RecordType. //the hasFields of this RecordType must contain all the hasFields of anotherRecordType. //for example, {zap :: Boolean} fails to unify with (r\foo) => {r | foo :: Int} containsHasFieldsOf(anotherRecordType); //the fields in the hasFields of this RecordType that are not in the hasFields of anotherRecordType //must not be in the lacks fields of anotherRecordType's recordVar. //for example, {foo :: Char, bar :: Int} and (r\foo, r\bar) => {r | bar :: Int} can't unify //because to do so, we must set r = foo, but this is disallowed by the lacks constraint on r. checkHasLacksCompatibility(anotherRecordType); //the hasFields in common must pairwise unify Set<FieldName> anotherHasFieldsSet = anotherRecordType.getHasFieldsMap().keySet(); int typeCloseness = pairwiseUnify(anotherHasFieldsSet, anotherRecordType, contextModuleTypeInfo); //the keys of this map are the hasFields of this RecordType that are not hasFields of anotherRecordType Map<FieldName, TypeExpr> neededHasFieldsMap = getNeededHasFields(anotherRecordType); //String -> TypeExpr //the needed has fields must satisfy the type class constraints of anotherRecordVar typeCloseness += unifyRecordVarConstraints(anotherRecordVar, neededHasFieldsMap, contextModuleTypeInfo); //instantiate anotherRecordVar to complete the unification. //instantiate to {NO_FIELDS | the missing (fieldName, type) binding} RecordType newBaseRecordType = new RecordType(RecordVar.NO_FIELDS, neededHasFieldsMap); anotherRecordVar.setInstance(newBaseRecordType); return typeCloseness; } else { if (anotherRecordVar.isNoFields()) { return anotherTypeExpr.unifyType(this, contextModuleTypeInfo); } //both this RecordType and anotherRecordType are polymorphic. The unification (if possible) will be polymorphic. //check the compatibility of has and lacks constraints, in both directions checkHasLacksCompatibility(anotherRecordType); anotherRecordType.checkHasLacksCompatibility(this); Set<FieldName> hasFieldsSet = this.getHasFieldsMap().keySet(); Set<FieldName> anotherHasFieldsSet = anotherRecordType.getHasFieldsMap().keySet(); //check that the hasFields in common pairwise unify SortedSet<FieldName> commonFields = new TreeSet<FieldName>(hasFieldsSet); commonFields.retainAll(anotherHasFieldsSet); int typeCloseness = pairwiseUnify(commonFields, anotherRecordType, contextModuleTypeInfo); //the has fields of both records will be the same. Map<FieldName, TypeExpr> neededHasFieldsForAnotherRecordType = getNeededHasFields(anotherRecordType); Map<FieldName, TypeExpr> neededHasFieldsForThisRecordType = anotherRecordType.getNeededHasFields(this); //the needed has fields must satisfy the type class constraints of anotherRecordVar, and vice-versa typeCloseness += unifyRecordVarConstraints(anotherRecordVar, neededHasFieldsForThisRecordType, contextModuleTypeInfo); typeCloseness += unifyRecordVarConstraints(recordVar, neededHasFieldsForAnotherRecordType, contextModuleTypeInfo); //now we know that things unify. Must patch up the 2 records. //the recordVar will be the same, in fact the very same instance... Set<FieldName> newLacksFields = new HashSet<FieldName>(recordVar.getLacksFieldsSet()); newLacksFields.addAll(anotherRecordVar.getLacksFieldsSet()); newLacksFields.addAll(hasFieldsSet); //the class constraints on the 2 record vars will be the same. TypeVar typeVar1 = TypeVar.makeTypeVar(null, recordVar.getTypeClassConstraintSet(), false); TypeVar typeVar2 = TypeVar.makeTypeVar(null, anotherRecordVar.getTypeClassConstraintSet(), false); //this should never fail- we just reuse the code for type variables since the type closeness stuff //is not completely trivial. typeCloseness += TypeExpr.unifyType(typeVar1, typeVar2, contextModuleTypeInfo); SortedSet<TypeClass> newTypeClassConstraintSet = typeVar1.rootTypeVar().getTypeClassConstraintSet(); RecordVar newRowVar = RecordVar.makeRecordVar(null, newLacksFields, newTypeClassConstraintSet, false); recordVar.setInstance(new RecordType(newRowVar, neededHasFieldsForThisRecordType)); if (recordVar != anotherRecordVar) { anotherRecordVar.setInstance(new RecordType(newRowVar, neededHasFieldsForAnotherRecordType)); } return typeCloseness; } } throw new IllegalStateException(); } /** * A helper function that verifies that the type of each field in fieldToTypeMap satisfies all the * type class constraints in recordVar. * @param recordVar * @param fieldToTypeMap * @param contextModuleTypeInfo * @return int typeCloseness * @throws TypeException */ private int unifyRecordVarConstraints(RecordVar recordVar, Map<FieldName, TypeExpr> fieldToTypeMap, ModuleTypeInfo contextModuleTypeInfo) throws TypeException { int typeCloseness = 0; SortedSet<TypeClass> constraintSet = recordVar.getTypeClassConstraintSet(); for (final Map.Entry<FieldName, TypeExpr> entry : fieldToTypeMap.entrySet()) { FieldName fieldName = entry.getKey(); TypeExpr fieldType = entry.getValue(); TypeVar typeVar = TypeVar.makeTypeVar(null, constraintSet, false); try { typeCloseness += TypeExpr.unifyType(typeVar, fieldType, contextModuleTypeInfo); } catch (TypeException te) { throw new TypeException ("record type unification failed at field " + fieldName + ".", te); } } return typeCloseness; } /** * A helper function that verifies that the type of each field in fieldToTypeMap satisifies all the * type class constraints in recordVar. * @param recordVar * @param fieldToTypeMap * @param patternMatchContext * @throws TypeException */ private void patternMatchRecordVarConstraints(RecordVar recordVar, Map<FieldName, TypeExpr> fieldToTypeMap, PatternMatchContext patternMatchContext) throws TypeException { SortedSet<TypeClass> constraintSet = recordVar.getTypeClassConstraintSet(); for (final Map.Entry<FieldName, TypeExpr> entry : fieldToTypeMap.entrySet()) { FieldName fieldName = entry.getKey(); TypeExpr fieldType = entry.getValue(); TypeVar typeVar = TypeVar.makeTypeVar(null, constraintSet, false); try { fieldType.patternMatch(typeVar, patternMatchContext); } catch (TypeException te) { throw new TypeException ("record pattern matching failed at field " + fieldName + ".", te); } } } /** * A helper function that returns the has fields in this RecordType that are not has fields * in anotherRecordType. * * @param anotherRecordType * @return Map (FieldName -> TypeExpr) the extension fields needed by anotherRecordType */ private Map<FieldName, TypeExpr> getNeededHasFields(RecordType anotherRecordType) { Map<FieldName, TypeExpr> hasFieldsMap = getHasFieldsMap(); Map<FieldName, TypeExpr> anotherHasFieldsMap = anotherRecordType.getHasFieldsMap(); Map<FieldName, TypeExpr> neededHasFields = new HashMap<FieldName, TypeExpr>(); for (final Map.Entry<FieldName, TypeExpr> entry : hasFieldsMap.entrySet()) { FieldName neededFieldName = entry.getKey(); if (!anotherHasFieldsMap.containsKey(neededFieldName)) { neededHasFields.put(neededFieldName, entry.getValue()); } } return neededHasFields; } /** * A helper for record unification and pattern matching. * * The fields in the hasFields of this RecordType that are not in the hasFields of anotherRecordType * must not be in the lacks fields of anotherRecordType's recordVar. * * For example, {foo :: Char, bar :: Int} and (r\foo, r\bar) => {r | bar :: Int} can't unify * because to do so, we must set r = foo, but this is disallowed by the lacks constraint on r. * * @param anotherRecordType * @throws TypeException */ private void checkHasLacksCompatibility(RecordType anotherRecordType) throws TypeException { Set<FieldName> illegalNeededFieldsSet = getHasFieldsMap().keySet(); //will iterate in FieldName order illegalNeededFieldsSet.removeAll(anotherRecordType.getHasFieldsMap().keySet()); illegalNeededFieldsSet.retainAll(anotherRecordType.getPrunedRecordVar().getLacksFieldsSet()); if (!illegalNeededFieldsSet.isEmpty()) { throw new TypeException("the record type " + anotherRecordType + " cannot unify or pattern match with " + this + " because the needed fields " + illegalNeededFieldsSet + " are not allowed by its lacks constraint."); } } /** * A helper function that does pairwise unification of the "hasFields" in 2 record types. * * @param fieldSet fields to iterate over for the pairwise unification * @param anotherRecordType * @param contextModuleTypeInfo * @return int type closeness measure for the unification * @throws TypeException */ private int pairwiseUnify(Set<FieldName> fieldSet, RecordType anotherRecordType, ModuleTypeInfo contextModuleTypeInfo) throws TypeException { // get 1 type closeness point for the fact that the 2 records match i.e. the "Record" type constructors match. // get an additional point for each field that the 2 records have in common int typeCloseness = 1 + fieldSet.size(); if (!fieldSet.isEmpty()) { Map<FieldName, TypeExpr> hasFieldsMap = this.getHasFieldsMap(); Map<FieldName, TypeExpr> anotherHasFieldsMap = anotherRecordType.getHasFieldsMap(); for (final FieldName hasFieldName : fieldSet) { TypeExpr fieldTypeExpr = hasFieldsMap.get(hasFieldName); TypeExpr anotherFieldTypeExpr = anotherHasFieldsMap.get(hasFieldName); try { typeCloseness += TypeExpr.unifyType(fieldTypeExpr, anotherFieldTypeExpr, contextModuleTypeInfo); } catch (TypeException te) { throw new TypeException ("record type unification failed at field " + hasFieldName + ".", te); } } } return typeCloseness; } /** * A helper function that does pairwise pattern matching of the "hasFields" in 2 record types. * * @param fieldSet (FieldName Set) field names to iterate over for the pairwise unification * @param anotherRecordType * @param patternMatchContext * @throws TypeException */ private void pairwisePatternMatch(Set<FieldName> fieldSet, RecordType anotherRecordType, PatternMatchContext patternMatchContext) throws TypeException { if (!fieldSet.isEmpty()) { Map<FieldName, TypeExpr> hasFieldsMap = this.getHasFieldsMap(); Map<FieldName, TypeExpr> anotherHasFieldsMap = anotherRecordType.getHasFieldsMap(); for (final FieldName hasFieldName : fieldSet) { TypeExpr fieldTypeExpr = hasFieldsMap.get(hasFieldName).prune(); TypeExpr anotherFieldTypeExpr = anotherHasFieldsMap.get(hasFieldName).prune(); try { fieldTypeExpr.patternMatch(anotherFieldTypeExpr, patternMatchContext); } catch (TypeException te) { throw new TypeException ("record type unification failed at field " + hasFieldName + ".", te); } } } } /** * A helper functions that fails in an error if the hasFields of anotherRecordType * are not all contained in the hasFields of this RecordType. * * @param anotherRecordType * @throws TypeException */ private void containsHasFieldsOf(RecordType anotherRecordType) throws TypeException { Set<FieldName> hasFieldsSet = this.getHasFieldsMap().keySet(); Set<FieldName> anotherHasFieldsSet = anotherRecordType.getHasFieldsMap().keySet(); if (!hasFieldsSet.containsAll(anotherHasFieldsSet)) { SortedSet<FieldName> missingFieldsSet = new TreeSet<FieldName>(anotherHasFieldsSet); missingFieldsSet.removeAll(hasFieldsSet); throw new TypeException("the record type " + this + " is missing the fields " + missingFieldsSet + " from the record type " + anotherRecordType +"."); } } /** {@inheritDoc} */ @Override public boolean usesForeignType() { RecordType baseRecordType = getBaseRecordType(); if (baseRecordType != null) { if (baseRecordType.usesForeignType()) { return true; } } for (final Map.Entry<FieldName, TypeExpr> entry : extensionFieldsMap.entrySet()) { TypeExpr fieldTypeExpr = entry.getValue(); if (fieldTypeExpr.usesForeignType()) { return true; } } return false; } /** * Forms the record type: {baseType | extensionFieldToTypeMap}, if this is possible, * and fails in a TypeException otherwise. * @param baseType must be a RecordType or TypeVar to succeed (there are other conditions on these as well). * @param extensionFieldsMap (FieldName -> TypeExpr) * @return RecordType type of the extended record * @throws TypeException */ static RecordType recordExtension(TypeExpr baseType, Map<FieldName, TypeExpr> extensionFieldsMap) throws TypeException { baseType = baseType.prune(); if (baseType instanceof RecordType) { RecordType baseRecordType = (RecordType)baseType; //todoBI clean this up //handle the case {} specially to keep the empty record as a singleton. if (baseRecordType == TypeExpr.EMPTY_RECORD && extensionFieldsMap.isEmpty()) { return baseRecordType; } //the baseRecordType cannot have hasFields that are in the extensionFieldToTypeMap. //For example, {{colour :: Colour} | a :: String, colour :: Colour} results in a TypeException. SortedSet<FieldName> overlappingHasFieldsSet = new TreeSet<FieldName>(baseRecordType.getHasFieldsMap().keySet()); overlappingHasFieldsSet.retainAll(extensionFieldsMap.keySet()); if (!overlappingHasFieldsSet.isEmpty()) { throw new TypeException(baseRecordType + " has fields " + overlappingHasFieldsSet + " overlapping with the record extension fields."); } //if we are extending a record-polymorphic record, then the lacks constraint must add all the extension fields. //e.g. {{r\field1 | field1 :: Char} | field2 :: Char} ----> {s\field1, s\field2 | field1 :: Char, field2 :: Char} RecordVar recordVar = baseRecordType.getPrunedRecordVar(); if (!recordVar.isNoFields()) { Set<FieldName> extendedLacksFieldsSet = new HashSet<FieldName>(recordVar.getLacksFieldsSet()); extendedLacksFieldsSet.addAll(extensionFieldsMap.keySet()); recordVar.setInstance(new RecordType(RecordVar.makeRecordVar(null, extendedLacksFieldsSet, TypeClass.NO_CLASS_CONSTRAINTS, false), new HashMap<FieldName, TypeExpr>())); } RecordVar recordVarInstantiatedToBaseRecord = RecordVar.makePolymorphicRecordVar(null); recordVarInstantiatedToBaseRecord.setInstance(baseRecordType); return new RecordType(recordVarInstantiatedToBaseRecord, extensionFieldsMap); } else if (baseType instanceof TypeVar) { TypeVar baseTypeVar = (TypeVar)baseType; //since record types cannot be instances of type classes (currently) we cannot instantiate a constrained type variable if (!baseTypeVar.noClassConstraints()) { throw new TypeException(baseTypeVar + " is a constrained type variable and cannot be the base type of a record extension."); } //baseTypeVar will be instantiated to {r|} where the lacks constraints on r come from the extensionFieldToTypeMap. RecordVar recordVar = RecordVar.makeRecordVar(null, new HashSet<FieldName>(extensionFieldsMap.keySet()), TypeClass.NO_CLASS_CONSTRAINTS, false); RecordType baseRecordType = new RecordType(recordVar, new HashMap<FieldName, TypeExpr>()); baseTypeVar.setInstance(baseRecordType); return RecordType.recordExtension(baseRecordType, extensionFieldsMap); } TypeConsApp baseTypeConsApp = (TypeConsApp)baseType; throw new TypeException(baseTypeConsApp + " a type constructor cannot be the base of a record extension."); } /** * Returns the names of fields which this record type lacks. * Note: For non record-polymorphic records, this returns an empty set. * * Ex: for the record type (r\field1, r\field2, r2\name) => {r | field2 = {r2 | name :: [Char]}}, * this returns the set [field1, field2, field3] * * Ex: for the record type {title :: String, author :: (r\name) => {r | name :: [Char]}}, * this returns the empty set * * @return Set field names which the record lacks */ public Set<FieldName> getLacksFieldsSet() { RecordVar baseRecordVar = getPrunedRecordVar(); if (baseRecordVar.isNoFields()) { return Collections.emptySet(); } else { return baseRecordVar.getLacksFieldsSet(); } } /** * Indicates whether this type is a record-polymorphic record * * Ex: (r\name) => {r | name :: [Char]} is record polymorphic. * * Ex: {name :: [Char]} is non record polymorphic. * * Ex: (Eq a) => {age :: a, height :: Double} is non record polymorphic, * even though one of its fields is polymorphic. * * Ex: (r\name) => {age :: Int, author :: {r | name :: [Char]}} ) is non record polymorphic, * even though one of its fields is. * * @return whether the base record variable of this type is polymorphic. */ public boolean isRecordPolymorphic() { RecordVar baseRecordVar = getPrunedRecordVar(); return !baseRecordVar.isNoFields(); } /** {@inheritDoc} */ @Override void writeActual (RecordOutputStream s, Map<TypeExpr, Short> visitedTypeExpr, Map<RecordVar, Short> visitedRecordVar) throws IOException { s.startRecord(ModuleSerializationTags.RECORD_TYPE, serializationSchema); baseRecordVar.write(s, visitedTypeExpr, visitedRecordVar); s.writeShortCompressed(extensionFieldsMap.size()); for (final Map.Entry<FieldName, TypeExpr> entry : extensionFieldsMap.entrySet()) { FieldName fn = entry.getKey(); TypeExpr te = entry.getValue(); FieldNameIO.writeFieldName(fn, s); te.write(s, visitedTypeExpr, visitedRecordVar); } s.endRecord(); } /** * Load an instance of RecordType from the RecordInputStream. * The read position in the stream will be after the RecordType record header. * @param s * @param schema * @param mti * @param visitedTypeExpr * @param visitedRecordVar * @param msgLogger the logger to which to log deserialization messages. * @return an instance of RecordType. * @throws IOException */ static TypeExpr load (RecordInputStream s, int schema, ModuleTypeInfo mti, Map<Short, TypeExpr> visitedTypeExpr, Map<Short, RecordVar> visitedRecordVar, CompilerMessageLogger msgLogger) throws IOException { DeserializationHelper.checkSerializationSchema(schema, serializationSchema, mti.getModuleName(), "RecordType", msgLogger); // We want to create this RecordType instance and add it to the visitedTypeExpr // map before we load anything that might contain a TypeExpr which refers to this instance. RecordType rt = new RecordType(); visitedTypeExpr.put(new Short((short)visitedTypeExpr.size()), rt); RecordVar rv = RecordVar.load(s, mti, visitedTypeExpr, visitedRecordVar, msgLogger); Map<FieldName, TypeExpr> extensionFields = new HashMap<FieldName, TypeExpr>(); int nExtensionFields = s.readShortCompressed(); for (int i = 0; i < nExtensionFields; ++i) { FieldName fn = FieldNameIO.load(s, mti.getModuleName(), msgLogger); TypeExpr te = TypeExpr.load(s, mti, visitedTypeExpr, visitedRecordVar, msgLogger); extensionFields.put(fn, te); } s.skipRestOfRecord(); rt.baseRecordVar = rv; rt.extensionFieldsMap = extensionFields; return rt; } }