ObjectType specialize(ObjectType other) { Preconditions.checkState(areRelatedClasses(this.nominalType, other.nominalType)); if (this == TOP_OBJECT && other.objectKind.isUnrestricted()) { return other; } NominalType resultNomType = NominalType.pickSubclass(this.nominalType, other.nominalType); ObjectKind ok = ObjectKind.meet(this.objectKind, other.objectKind); if (resultNomType != null && resultNomType.isClassy()) { Preconditions.checkState(this.fn == null && other.fn == null); PersistentMap<String, Property> newProps = meetPropsHelper(true, resultNomType, this.props, other.props); if (newProps == BOTTOM_MAP) { return BOTTOM_OBJECT; } return new ObjectType(resultNomType, newProps, null, false, ok); } FunctionType thisFn = this.fn; boolean isLoose = this.isLoose; if (resultNomType != null && resultNomType.isFunction() && this.fn == null) { thisFn = other.fn; isLoose = other.fn.isLoose(); } PersistentMap<String, Property> newProps = meetPropsHelper(true, resultNomType, this.props, other.props); if (newProps == BOTTOM_MAP) { return BOTTOM_OBJECT; } FunctionType newFn = thisFn == null ? null : thisFn.specialize(other.fn); if (!FunctionType.isInhabitable(newFn)) { return BOTTOM_OBJECT; } return new ObjectType(resultNomType, newProps, newFn, isLoose, ok); }
static ObjectType meet(ObjectType obj1, ObjectType obj2) { Preconditions.checkState(areRelatedClasses(obj1.nominalType, obj2.nominalType)); if (obj1 == TOP_OBJECT) { return obj2; } else if (obj2 == TOP_OBJECT) { return obj1; } NominalType resultNomType = NominalType.pickSubclass(obj1.nominalType, obj2.nominalType); FunctionType fn = FunctionType.meet(obj1.fn, obj2.fn); if (!FunctionType.isInhabitable(fn)) { return BOTTOM_OBJECT; } boolean isLoose = obj1.isLoose && obj2.isLoose || fn != null && fn.isLoose(); if (resultNomType != null && resultNomType.isFunction() && fn == null) { fn = obj1.fn == null ? obj2.fn : obj1.fn; isLoose = fn.isLoose(); } PersistentMap<String, Property> props; if (isLoose) { props = joinPropsLoosely(obj1.props, obj2.props); } else { props = meetPropsHelper(false, resultNomType, obj1.props, obj2.props); } if (props == BOTTOM_MAP) { return BOTTOM_OBJECT; } ObjectKind ok = ObjectKind.meet(obj1.objectKind, obj2.objectKind); return new ObjectType(resultNomType, props, fn, isLoose, ok); }
private ObjectType( NominalType nominalType, PersistentMap<String, Property> props, FunctionType fn, boolean isLoose, ObjectKind objectKind) { Preconditions.checkArgument( fn == null || fn.isQmarkFunction() || fn.isLoose() == isLoose, "isLoose: %s, fn: %s", isLoose, fn); Preconditions.checkArgument(FunctionType.isInhabitable(fn)); Preconditions.checkArgument( fn == null || nominalType != null, "Cannot create function %s without nominal type", fn); if (nominalType != null) { Preconditions.checkArgument( !nominalType.isClassy() || !isLoose, "Cannot create loose objectType with nominal type %s", nominalType); Preconditions.checkArgument( fn == null || nominalType.isFunction(), "Cannot create objectType of nominal type %s with function (%s)", nominalType, fn); Preconditions.checkArgument( !nominalType.isFunction() || fn != null, "Cannot create Function instance without a FunctionType"); } this.nominalType = nominalType; this.props = props; this.fn = fn; this.isLoose = isLoose; this.objectKind = objectKind; }
static ObjectType join(ObjectType obj1, ObjectType obj2) { if (obj1 == TOP_OBJECT || obj2 == TOP_OBJECT) { return TOP_OBJECT; } NominalType nom1 = obj1.nominalType; NominalType nom2 = obj2.nominalType; Preconditions.checkState(areRelatedClasses(nom1, nom2)); if (obj1.equals(obj2)) { return obj1; } boolean isLoose = obj1.isLoose || obj2.isLoose; FunctionType fn = FunctionType.join(obj1.fn, obj2.fn); PersistentMap<String, Property> props; if (isLoose) { fn = fn == null ? null : fn.withLoose(); props = joinPropsLoosely(obj1.props, obj2.props); } else { props = joinProps(obj1.props, obj2.props, nom1, nom2); } NominalType nominal = NominalType.pickSuperclass(nom1, nom2); // TODO(blickly): Split TOP_OBJECT from empty object and remove this case if (nominal == null || !nominal.isFunction()) { fn = null; } return ObjectType.makeObjectType( nominal, props, fn, isLoose, ObjectKind.join(obj1.objectKind, obj2.objectKind)); }
private NominalType getMaybeParentClass( JSDocInfo jsdoc, String functionName, Node funNode, ImmutableList<String> typeParameters, DeclaredTypeRegistry registry) { if (!jsdoc.hasBaseType()) { return null; } if (!jsdoc.isConstructor()) { warnings.add(JSError.make(funNode, EXTENDS_NOT_ON_CTOR_OR_INTERF, functionName)); return null; } Node docNode = jsdoc.getBaseType().getRoot(); JSType extendedType = getMaybeTypeFromComment(docNode, registry, typeParameters); if (extendedType == null) { return null; } NominalType parentClass = extendedType.getNominalTypeIfSingletonObj(); if (parentClass != null && parentClass.isClass()) { return parentClass; } if (parentClass == null) { warnings.add( JSError.make(funNode, EXTENDS_NON_OBJECT, functionName, extendedType.toString())); } else { Preconditions.checkState(parentClass.isInterface()); warnings.add(JSError.make(funNode, CONFLICTING_EXTENDED_TYPE, "constructor", functionName)); } return null; }
private void handleConstructorAnnotation( String functionName, Node funNode, RawNominalType constructorType, NominalType parentClass, ImmutableSet<NominalType> implementedIntfs, DeclaredTypeRegistry registry, FunctionTypeBuilder builder) { String className = constructorType.toString(); NominalType builtinObject = registry.getCommonTypes().getObjectType(); if (parentClass == null && !functionName.equals("Object")) { parentClass = builtinObject; } if (parentClass != null) { if (!constructorType.addSuperClass(parentClass)) { warnings.add(JSError.make(funNode, INHERITANCE_CYCLE, className)); } else if (parentClass != builtinObject) { if (constructorType.isStruct() && !parentClass.isStruct()) { warnings.add(JSError.make(funNode, CONFLICTING_SHAPE_TYPE, "struct", className)); } else if (constructorType.isDict() && !parentClass.isDict()) { warnings.add(JSError.make(funNode, CONFLICTING_SHAPE_TYPE, "dict", className)); } } } if (constructorType.isDict() && !implementedIntfs.isEmpty()) { warnings.add(JSError.make(funNode, DICT_IMPLEMENTS_INTERF, className)); } boolean noCycles = constructorType.addInterfaces(implementedIntfs); Preconditions.checkState(noCycles); builder.addNominalType(constructorType.getInstanceAsJSType()); }
// We require finalization for the interfaces here because the inheritance // chain of each type may not be correct until after the type is finalized. public ImmutableSet<NominalType> getInstantiatedInterfaces() { Preconditions.checkState(this.rawType.isFinalized()); ImmutableSet.Builder<NominalType> result = ImmutableSet.builder(); for (NominalType interf : this.rawType.getInterfaces()) { result.add(interf.instantiateGenerics(typeMap)); } return result.build(); }
// The main difference from getInstantiatedInterfaces is that this method // can be used on non-finalized types. private ImmutableSet<NominalType> getInstantiatedIObjectInterfaces() { ImmutableSet.Builder<NominalType> result = ImmutableSet.builder(); for (NominalType interf : this.rawType.getInterfaces()) { if (interf.inheritsFromIObjectReflexive()) { result.add(interf.instantiateGenerics(typeMap)); } } return result.build(); }
private Property getLeftmostProp(QualifiedName qname) { String objName = qname.getLeftmostName(); Property p = props.get(objName); if (p != null) { return p; } if (nominalType != null) { return nominalType.getProp(objName); } return builtinObject == null ? null : builtinObject.getProp(objName); }
// A special-case of meet static NominalType pickSubclass(NominalType c1, NominalType c2) { if (c1 == null) { return c2; } if (c2 == null) { return c1; } if (c1.isNominalSubtypeOf(c2)) { return c1; } return c2.isNominalSubtypeOf(c1) ? c2 : null; }
/** * Required properties are acceptable where an optional is required, but not vice versa. Optional * properties create cycles in the type lattice, eg, { } \le { p: num= } and also { p: num= } \le * { }. */ boolean isSubtypeOf(boolean keepLoosenessOfThis, ObjectType other) { if (other == TOP_OBJECT) { return true; } if ((keepLoosenessOfThis && this.isLoose) || other.isLoose) { return this.isLooseSubtypeOf(other); } NominalType thisNt = this.nominalType; NominalType otherNt = other.nominalType; if (thisNt == null && otherNt != null || thisNt != null && otherNt != null && !thisNt.isSubtypeOf(otherNt)) { return false; } if (otherNt == null && !this.objectKind.isSubtypeOf(other.objectKind) // Interfaces are structs but we allow them to be used in a context that // expects a record type, even though it is unsound. // TODO(dimvar): Remove this when we switch to structural interfaces. && !(this.isInterfaceInstance() && other.objectKind.isUnrestricted())) { return false; } // If nominalType1 < nominalType2, we only need to check that the // properties of other are in (obj1 or nominalType1) for (Map.Entry<String, Property> entry : other.props.entrySet()) { String pname = entry.getKey(); Property prop2 = entry.getValue(); Property prop1 = this.getLeftmostProp(new QualifiedName(pname)); if (prop2.isOptional()) { if (prop1 != null && !prop1.getType().isSubtypeOf(prop2.getType())) { return false; } } else { if (prop1 == null || prop1.isOptional() || !prop1.getType().isSubtypeOf(prop2.getType())) { return false; } } } if (other.fn == null) { return true; } else if (this.fn == null) { // Can only be executed if we have declared types for callable objects. return false; } return this.fn.isSubtypeOf(other.fn); }
public boolean isTypeVariableInScope(String tvar) { if (typeParameters != null && typeParameters.contains(tvar)) { return true; } // We don't look at this.nominalType, b/c if this function is a generic // constructor, then typeParameters contains the relevant type variables. if (receiverType != null && receiverType.isUninstantiatedGenericType()) { RawNominalType rawType = receiverType.getRawNominalType(); if (rawType.getTypeParameters().contains(tvar)) { return true; } } return false; }
JSType getIndexType() { if (isIObject()) { return this.typeMap.get(this.rawType.getTypeParameters().get(0)); } // This type is a subtype of all indexed types it inherits from, // and we use contravariance for the key of the index operation, // so we join here. JSType result = getCommonTypes().BOTTOM; for (NominalType interf : getInstantiatedIObjectInterfaces()) { JSType tmp = interf.getIndexType(); if (tmp != null) { result = JSType.join(result, tmp); } } return result.isBottom() ? null : result; }
boolean unifyWithSubtype( NominalType other, List<String> typeParameters, Multimap<String, JSType> typeMultimap, SubtypeCache subSuperMap) { other = other.findMatchingAncestorWith(this); if (other == null) { return false; } if (!isGeneric()) { // Non-generic nominal types don't contribute to the unification. return true; } // Most of the time, both nominal types are already instantiated when // unifyWith is called. Rarely, when we call a polymorphic function from the // body of a method of a polymorphic class, then other.typeMap is // empty. For now, don't do anything fancy in that case. Preconditions.checkState(!typeMap.isEmpty()); if (other.typeMap.isEmpty()) { return true; } boolean hasUnified = true; for (String typeParam : this.rawType.getTypeParameters()) { JSType fromOtherMap = other.typeMap.get(typeParam); Preconditions.checkNotNull( fromOtherMap, "Type variable %s not found in map %s", typeParam, other.typeMap); hasUnified = hasUnified && this.typeMap .get(typeParam) .unifyWithSubtype(fromOtherMap, typeParameters, typeMultimap, subSuperMap); } return hasUnified; }
private static PersistentMap<String, Property> meetPropsHelper( boolean specializeProps1, NominalType resultNominalType, PersistentMap<String, Property> props1, PersistentMap<String, Property> props2) { PersistentMap<String, Property> newProps = props1; if (resultNominalType != null) { for (Map.Entry<String, Property> propsEntry : props1.entrySet()) { String pname = propsEntry.getKey(); Property nomProp = resultNominalType.getProp(pname); if (nomProp != null) { newProps = addOrRemoveProp(specializeProps1, newProps, pname, nomProp, propsEntry.getValue()); if (newProps == BOTTOM_MAP) { return BOTTOM_MAP; } } } } for (Map.Entry<String, Property> propsEntry : props2.entrySet()) { String pname = propsEntry.getKey(); Property prop2 = propsEntry.getValue(); Property newProp; if (!props1.containsKey(pname)) { newProp = prop2; } else { Property prop1 = props1.get(pname); if (prop1.equals(prop2)) { continue; } newProp = specializeProps1 ? prop1.specialize(prop2) : Property.meet(prop1, prop2); } if (resultNominalType != null && resultNominalType.getProp(pname) != null) { Property nomProp = resultNominalType.getProp(pname); newProps = addOrRemoveProp(specializeProps1, newProps, pname, nomProp, newProp); if (newProps == BOTTOM_MAP) { return BOTTOM_MAP; } } else { if (newProp.getType().isBottom()) { return BOTTOM_MAP; } newProps = newProps.with(pname, newProp); } } return newProps; }
private static Property getProp(Map<String, Property> props, NominalType nom, String pname) { if (props.containsKey(pname)) { return props.get(pname); } else if (nom != null) { return nom.getProp(pname); } return null; }
private boolean isStructuralSubtypeOf(NominalType other, SubtypeCache subSuperMap) { Preconditions.checkArgument(other.isStructuralInterface()); for (String pname : other.getAllPropsOfInterface()) { Property prop2 = other.getProp(pname); Property prop1 = this.getProp(pname); if (prop2.isOptional()) { if (prop1 != null && !prop1.getType().isSubtypeOf(prop2.getType(), subSuperMap)) { return false; } } else if (prop1 == null || prop1.isOptional() || !prop1.getType().isSubtypeOf(prop2.getType(), subSuperMap)) { return false; } } return true; }
private JSType getNominalTypeHelper( RawNominalType rawType, Node n, DeclaredTypeRegistry registry, ImmutableList<String> outerTypeParameters) throws UnknownTypeException { NominalType uninstantiated = rawType.getAsNominalType(); if (!rawType.isGeneric() && !n.hasChildren()) { return rawType.getInstanceWithNullability(NULLABLE_TYPES_BY_DEFAULT); } ImmutableList.Builder<JSType> typeList = ImmutableList.builder(); if (n.hasChildren()) { // Compute instantiation of polymorphic class/interface. Preconditions.checkState(n.getFirstChild().isBlock(), n); for (Node child : n.getFirstChild().children()) { typeList.add(getTypeFromCommentHelper(child, registry, outerTypeParameters)); } } ImmutableList<JSType> typeArguments = typeList.build(); ImmutableList<String> typeParameters = rawType.getTypeParameters(); int typeArgsSize = typeArguments.size(); int typeParamsSize = typeParameters.size(); if (typeArgsSize != typeParamsSize) { // We used to also warn when (typeArgsSize < typeParamsSize), but it // happens so often that we stopped. Array, Object and goog.Promise are // common culprits, but many other types as well. if (typeArgsSize > typeParamsSize) { warnings.add( JSError.make( n, INVALID_GENERICS_INSTANTIATION, uninstantiated.getName(), String.valueOf(typeParamsSize), String.valueOf(typeArgsSize))); } return maybeMakeNullable( JSType.fromObjectType( ObjectType.fromNominalType( uninstantiated.instantiateGenerics( fixLengthOfTypeList(typeParameters.size(), typeArguments))))); } return maybeMakeNullable( JSType.fromObjectType( ObjectType.fromNominalType(uninstantiated.instantiateGenerics(typeArguments)))); }
JSType getIndexedType() { if (isIObject()) { return this.typeMap.get(this.rawType.getTypeParameters().get(1)); } // This type is a subtype of all indexed types it inherits from, // and we use covariance for the value of the index operation, // so we meet here. JSType result = getCommonTypes().TOP; // We need this because the index type may explicitly be TOP. boolean foundIObject = false; for (NominalType interf : getInstantiatedIObjectInterfaces()) { JSType tmp = interf.getIndexedType(); if (tmp != null) { foundIObject = true; result = JSType.meet(result, tmp); } } return foundIObject ? result : null; }
boolean isNominalSubtypeOf(NominalType other) { RawNominalType thisRaw = this.rawType; if (thisRaw == other.rawType) { return areTypeMapsCompatible(other); } if (other.isBuiltinObject()) { return true; } if (other.isInterface()) { // If thisRaw is not finalized, thisRaw.interfaces may be null. for (NominalType i : thisRaw.getInterfaces()) { if (i.instantiateGenerics(this.typeMap).isNominalSubtypeOf(other)) { return true; } } } // Note that other can still be an interface here (implemented by a superclass) return isClass() && thisRaw.getSuperClass() != null && thisRaw.getSuperClass().instantiateGenerics(this.typeMap).isNominalSubtypeOf(other); }
StringBuilder appendTo(StringBuilder builder) { if (!hasNonPrototypeProperties()) { if (fn != null) { return fn.appendTo(builder); } else if (getNominalType() != null) { return getNominalType().appendTo(builder); } } if (nominalType != null && !nominalType.getName().equals("Function")) { nominalType.appendTo(builder); } else if (isStruct()) { builder.append("struct"); } else if (isDict()) { builder.append("dict"); } if (fn != null) { builder.append("<|"); fn.appendTo(builder); builder.append("|>"); } if (nominalType == null || !props.isEmpty()) { builder.append('{'); boolean firstIteration = true; for (String pname : new TreeSet<>(props.keySet())) { if (firstIteration) { firstIteration = false; } else { builder.append(", "); } builder.append(pname); builder.append(':'); props.get(pname).appendTo(builder); } builder.append('}'); } if (isLoose) { builder.append(" (loose)"); } return builder; }
// Returns a type with the same raw type as other, but possibly different type maps. private NominalType findMatchingAncestorWith(NominalType other) { RawNominalType thisRaw = this.rawType; if (thisRaw == other.rawType) { return this; } if (other.isInterface()) { for (NominalType i : thisRaw.getInterfaces()) { NominalType nt = i.instantiateGenerics(this.typeMap).findMatchingAncestorWith(other); if (nt != null) { return nt; } } } // Note that other can still be an interface here (implemented by a superclass) if (isClass() && thisRaw.getSuperClass() != null) { return thisRaw .getSuperClass() .instantiateGenerics(this.typeMap) .findMatchingAncestorWith(other); } return null; }
private ImmutableSet<NominalType> getInterfacesHelper( JSDocInfo jsdoc, DeclaredTypeRegistry registry, ImmutableList<String> typeParameters, boolean implementedIntfs) { ImmutableSet.Builder<NominalType> builder = ImmutableSet.builder(); for (JSTypeExpression texp : (implementedIntfs ? jsdoc.getImplementedInterfaces() : jsdoc.getExtendedInterfaces())) { Node expRoot = texp.getRoot(); JSType interfaceType = getMaybeTypeFromComment(expRoot, registry, typeParameters); if (interfaceType != null) { NominalType nt = interfaceType.getNominalTypeIfSingletonObj(); if (nt != null && nt.isInterface()) { builder.add(nt); } else if (implementedIntfs) { warnings.add(JSError.make(expRoot, IMPLEMENTS_NON_INTERFACE, interfaceType.toString())); } else { warnings.add(JSError.make(expRoot, EXTENDS_NON_INTERFACE, interfaceType.toString())); } } } return builder.build(); }
ObjectType substituteGenerics(Map<String, JSType> concreteTypes) { if (concreteTypes.isEmpty()) { return this; } PersistentMap<String, Property> newProps = PersistentMap.create(); for (Map.Entry<String, Property> propsEntry : this.props.entrySet()) { String pname = propsEntry.getKey(); Property newProp = propsEntry.getValue().substituteGenerics(concreteTypes); newProps = newProps.with(pname, newProp); } FunctionType newFn = fn == null ? null : fn.substituteGenerics(concreteTypes); return makeObjectType( nominalType == null ? null : nominalType.instantiateGenerics(concreteTypes), newProps, newFn, newFn != null && newFn.isQmarkFunction() || isLoose, objectKind); }
/** * Unify the two types symmetrically, given that we have already instantiated the type variables * of interest in {@code t1} and {@code t2}, treating JSType.UNKNOWN as a "hole" to be filled. * * @return The unified type, or null if unification fails */ static ObjectType unifyUnknowns(ObjectType t1, ObjectType t2) { NominalType nt1 = t1.nominalType; NominalType nt2 = t2.nominalType; NominalType nt; if (nt1 == null && nt2 == null) { nt = null; } else if (nt1 == null || nt2 == null) { return null; } else { nt = NominalType.unifyUnknowns(nt1, nt2); if (nt == null) { return null; } } FunctionType newFn = null; if (t1.fn != null || t2.fn != null) { newFn = FunctionType.unifyUnknowns(t1.fn, t2.fn); if (newFn == null) { return null; } } PersistentMap<String, Property> newProps = PersistentMap.create(); for (String propName : t1.props.keySet()) { Property prop1 = t1.props.get(propName); Property prop2 = t2.props.get(propName); if (prop2 == null) { return null; } Property p = Property.unifyUnknowns(prop1, prop2); if (p == null) { return null; } newProps = newProps.with(propName, p); } return makeObjectType( nt, newProps, newFn, t1.isLoose || t2.isLoose, ObjectKind.join(t1.objectKind, t2.objectKind)); }
/** * Unify {@code this}, which may contain free type variables, with {@code other}, a concrete type, * modifying the supplied {@code typeMultimap} to add any new template varaible type bindings. * * @return Whether unification succeeded */ boolean unifyWithSubtype( ObjectType other, List<String> typeParameters, Multimap<String, JSType> typeMultimap) { if (fn != null) { if (other.fn == null || !fn.unifyWithSubtype(other.fn, typeParameters, typeMultimap)) { return false; } } if (nominalType != null && other.nominalType != null) { return nominalType.unifyWithSubtype(other.nominalType, typeParameters, typeMultimap); } if (nominalType != null || other.nominalType != null) { return false; } for (String propName : this.props.keySet()) { Property thisProp = props.get(propName); Property otherProp = other.props.get(propName); if (otherProp == null || !thisProp.unifyWithSubtype(otherProp, typeParameters, typeMultimap)) { return false; } } return true; }
// A special-case of join static NominalType pickSuperclass(NominalType c1, NominalType c2) { if (c1 == null || c2 == null) { return null; } if (c1.isNominalSubtypeOf(c2)) { return c2; } if (c1.isRawSubtypeOf(c2)) { return c2.instantiateGenericsWithUnknown(); } if (c2.isNominalSubtypeOf(c1)) { return c1; } if (c2.isRawSubtypeOf(c1)) { return c1.instantiateGenericsWithUnknown(); } return null; }
private boolean areTypeMapsCompatible(NominalType other) { Preconditions.checkState(this.rawType.equals(other.rawType)); if (this.typeMap.isEmpty()) { return other.instantiationIsUnknownOrIdentity(); } if (other.typeMap.isEmpty()) { return instantiationIsUnknownOrIdentity(); } for (String typeVar : this.rawType.getTypeParameters()) { Preconditions.checkState( this.typeMap.containsKey(typeVar), "Type variable %s not in the domain: %s", typeVar, this.typeMap.keySet()); Preconditions.checkState( other.typeMap.containsKey(typeVar), "Other (%s) doesn't contain mapping (%s->%s) from this (%s)", other, typeVar, this.typeMap.get(typeVar), this); JSType thisType = this.typeMap.get(typeVar); JSType otherType = other.typeMap.get(typeVar); JSTypes commonTypes = getCommonTypes(); if (commonTypes.bivariantArrayGenerics && this.rawType.isBuiltinWithName("Array")) { thisType = thisType.removeType(commonTypes.NULL_OR_UNDEFINED); otherType = otherType.removeType(commonTypes.NULL_OR_UNDEFINED); if (!thisType.isSubtypeOf(otherType) && !otherType.isSubtypeOf(thisType)) { return false; } } else if (!thisType.isSubtypeOf(otherType)) { return false; } } return true; }
static ObjectType fromNominalType(NominalType cl) { return ObjectType.makeObjectType(cl, null, null, false, cl.getObjectKind()); }
private static boolean areRelatedClasses(NominalType c1, NominalType c2) { if (c1 == null || c2 == null) { return true; } return c1.isSubtypeOf(c2) || c2.isSubtypeOf(c1); }