Skip to content

Commit

Permalink
TypedUnion used only for >= 2 types. EmptyUnion (Typed.empty) removed
Browse files Browse the repository at this point in the history
  • Loading branch information
arkadius committed Jan 28, 2024
1 parent bdaf6c9 commit 6dc66a1
Show file tree
Hide file tree
Showing 30 changed files with 442 additions and 355 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ sealed trait DefinedBranchParameter extends BaseDefinedParameter {

def expressionByBranchId: Map[String, TypedExpression]

final override def returnType: TypingResult = Typed(expressionByBranchId.values.map(_.returnType).toSet)
final override def returnType: TypingResult =
Typed.fromIterableOrUnknownIfEmpty(expressionByBranchId.values.map(_.returnType))

}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package pl.touk.nussknacker.engine.api.typed

import cats.data.Validated._
import cats.data.ValidatedNel
import cats.data.{NonEmptyList, NonEmptySet, ValidatedNel}
import cats.implicits.{catsSyntaxValidatedId, _}
import org.apache.commons.lang3.ClassUtils
import pl.touk.nussknacker.engine.api.typed.typing._
Expand All @@ -26,8 +26,10 @@ trait CanBeSubclassDeterminer {
case (Unknown, _) => ().validNel
case (TypedNull, other) => canNullBeSubclassOf(other)
case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel
case (given: SingleTypingResult, superclass: TypedUnion) => canBeSubclassOf(Set(given), superclass.possibleTypes)
case (given: TypedUnion, superclass: SingleTypingResult) => canBeSubclassOf(given.possibleTypes, Set(superclass))
case (given: SingleTypingResult, superclass: TypedUnion) =>
canBeSubclassOf(NonEmptyList(given, Nil), superclass.possibleTypes)
case (given: TypedUnion, superclass: SingleTypingResult) =>
canBeSubclassOf(given.possibleTypes, NonEmptyList(superclass, Nil))
case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeSubclassOf(given, superclass)
case (given: TypedUnion, superclass: TypedUnion) => canBeSubclassOf(given.possibleTypes, superclass.possibleTypes)
}
Expand Down Expand Up @@ -151,8 +153,8 @@ trait CanBeSubclassDeterminer {
}

private def canBeSubclassOf(
givenTypes: Set[SingleTypingResult],
superclassCandidates: Set[SingleTypingResult]
givenTypes: NonEmptyList[SingleTypingResult],
superclassCandidates: NonEmptyList[SingleTypingResult]
): ValidatedNel[String, Unit] = {
// Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against
// e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action.
Expand All @@ -162,9 +164,9 @@ trait CanBeSubclassDeterminer {
givenTypes.exists(given => superclassCandidates.exists(singleCanBeSubclassOf(given, _).isValid)),
(),
s"""None of the following types:
|${givenTypes.map(" - " + _.display).mkString(",\n")}
|${givenTypes.map(" - " + _.display).toList.mkString(",\n")}
|can be a subclass of any of:
|${superclassCandidates.map(" - " + _.display).mkString(",\n")}""".stripMargin
|${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pl.touk.nussknacker.engine.api.typed

import cats.data.NonEmptyList
import io.circe.Json._
import io.circe._
import pl.touk.nussknacker.engine.api.typed.TypeEncoders.typeField
Expand Down Expand Up @@ -31,8 +32,10 @@ object TypeEncoders {
case single: SingleTypingResult => encodeSingleTypingResult(single)
case typing.Unknown => encodeUnknown
case typing.TypedNull => encodeNull
case TypedUnion(classes) =>
JsonObject("union" -> fromValues(classes.map(typ => fromJsonObject(encodeTypingResult(typ))).toList))
case union: TypedUnion =>
JsonObject(
"union" -> fromValues(union.possibleTypes.map(typ => fromJsonObject(encodeTypingResult(typ))).toList)
)
})
.+:(typeField -> fromString(TypingType.forType(result).toString))
.+:("display" -> fromString(result.display))
Expand Down Expand Up @@ -145,7 +148,14 @@ class TypingResultDecoder(loadClass: String => Class[_]) {
}

private def typedUnion(obj: HCursor): Decoder.Result[TypingResult] = {
obj.downField("union").as[Set[SingleTypingResult]].map(TypedUnion)
obj.downField("union").as[List[SingleTypingResult]].flatMap { list =>
NonEmptyList
.fromList(list)
.map(nel => Right(Typed(nel)))
.getOrElse(
Left(DecodingFailure(s"Union should has at least 2 elements but it has ${list.size} elements", obj.history))
)
}
}

private def typedClass(obj: HCursor): Decoder.Result[TypedClass] = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package pl.touk.nussknacker.engine.api.typed.supertype

import cats.data.NonEmptyList
import org.apache.commons.lang3.ClassUtils
import pl.touk.nussknacker.engine.api.typed.supertype.CommonSupertypeFinder.{
Intersection,
SupertypeClassResolutionStrategy
}
import pl.touk.nussknacker.engine.api.typed.supertype.CommonSupertypeFinder.SupertypeClassResolutionStrategy
import pl.touk.nussknacker.engine.api.typed.typing._

/**
Expand All @@ -17,18 +15,22 @@ import pl.touk.nussknacker.engine.api.typed.typing._
*/
class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassResolutionStrategy) {

def commonSupertype(left: TypingResult, right: TypingResult)(
// It returns None if none supertype found (it is possible only for SupertypeClassResolutionStrategy.Intersection)
// see commonSuperTypeForClassesNotInSameInheritanceLine and fallback in singleCommonSupertype
def commonSupertypeOpt(left: TypingResult, right: TypingResult)(
implicit numberPromotionStrategy: NumberTypesPromotionStrategy
): TypingResult = {
): Option[TypingResult] = {
(left, right) match {
case (Unknown, _) => Unknown // can't be sure intention of user - union is more secure than intersection
case (_, Unknown) => Unknown
case (TypedNull, r) => commonSupertypeWithNull(r)
case (r, TypedNull) => commonSupertypeWithNull(r)
case (l: SingleTypingResult, r: TypedUnion) => Typed(commonSupertype(Set(l), r.possibleTypes))
case (l: TypedUnion, r: SingleTypingResult) => Typed(commonSupertype(l.possibleTypes, Set(r)))
case (Unknown, _) => Some(Unknown) // can't be sure intention of user - union is more secure than intersection
case (_, Unknown) => Some(Unknown)
case (TypedNull, r) => Some(commonSupertypeWithNull(r))
case (r, TypedNull) => Some(commonSupertypeWithNull(r))
case (l: SingleTypingResult, r: TypedUnion) =>
commonSupertype(NonEmptyList(l, Nil), r.possibleTypes).map(Typed(_))
case (l: TypedUnion, r: SingleTypingResult) =>
commonSupertype(l.possibleTypes, NonEmptyList(r, Nil)).map(Typed(_))
case (l: SingleTypingResult, r: SingleTypingResult) => singleCommonSupertype(l, r)
case (l: TypedUnion, r: TypedUnion) => Typed(commonSupertype(l.possibleTypes, r.possibleTypes))
case (l: TypedUnion, r: TypedUnion) => commonSupertype(l.possibleTypes, r.possibleTypes).map(Typed(_))
}
}

Expand All @@ -39,17 +41,19 @@ class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassReso
case r => r
}

private def commonSupertype(leftSet: Set[SingleTypingResult], rightSet: Set[SingleTypingResult])(
private def commonSupertype(leftList: NonEmptyList[SingleTypingResult], rightList: NonEmptyList[SingleTypingResult])(
implicit numberPromotionStrategy: NumberTypesPromotionStrategy
): Set[TypingResult] =
leftSet.flatMap(l => rightSet.map(singleCommonSupertype(l, _)))
): Option[NonEmptyList[TypingResult]] = {
// .sequence won't do the work because it returns None if any element of list returned None
NonEmptyList.fromList(leftList.flatMap(l => rightList.map(singleCommonSupertype(l, _))).toList.flatten)
}

private def singleCommonSupertype(left: SingleTypingResult, right: SingleTypingResult)(
implicit numberPromotionStrategy: NumberTypesPromotionStrategy
): TypingResult = {
): Option[TypingResult] = {
def fallback = classResolutionStrategy match {
case SupertypeClassResolutionStrategy.FallbackToObjectType => klassCommonSupertype(left.objType, right.objType)
case SupertypeClassResolutionStrategy.Intersection => Typed.empty
case SupertypeClassResolutionStrategy.Intersection => None
}
// We can't do if (left == right) left because spel promote byte and short classes to integer always so returned type for math operation will be different
(left, right) match {
Expand All @@ -65,26 +69,27 @@ class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassReso
// and want to compare it with literal record that doesn't have this field
TypedObjectTypingResult(fields, commonSupertype)
}
.getOrElse(fallback)
.orElse(fallback)
case (l: TypedObjectTypingResult, r) => singleCommonSupertype(l.objType, r)
case (l, r: TypedObjectTypingResult) => singleCommonSupertype(l, r.objType)
case (TypedTaggedValue(leftType, leftTag), TypedTaggedValue(rightType, rightTag)) if leftTag == rightTag =>
singleCommonSupertype(leftType, rightType) match {
singleCommonSupertype(leftType, rightType).map {
case single: SingleTypingResult => TypedTaggedValue(single, leftTag)
case other => other
}
case (_: TypedTaggedValue, _) => fallback
case (_, _: TypedTaggedValue) => fallback
case (TypedObjectWithValue(leftType, leftValue), TypedObjectWithValue(rightType, rightValue))
if leftValue == rightValue =>
klassCommonSupertype(leftType, rightType) match {
klassCommonSupertype(leftType, rightType).map {
case typedClass: TypedClass => TypedObjectWithValue(typedClass, leftValue)
case other => other
}
case (l: TypedObjectWithValue, r) => singleCommonSupertype(l.underlying, r)
case (l, r: TypedObjectWithValue) => singleCommonSupertype(l, r.underlying)
case (_: TypedDict, _) => fallback
case (_, _: TypedDict) => fallback
case (l: TypedObjectWithValue, r) => singleCommonSupertype(l.underlying, r)
case (l, r: TypedObjectWithValue) => singleCommonSupertype(l, r.underlying)
case (l: TypedDict, r: TypedDict) if l == r => Some(l)
case (_: TypedDict, _) => fallback
case (_, _: TypedDict) => fallback
}
}

Expand All @@ -100,11 +105,9 @@ class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassReso
case (fieldName, singleType :: Nil) =>
Some(fieldName -> singleType)
case (fieldName, leftType :: rightType :: Nil) =>
val common = commonSupertype(leftType, rightType)
if (common == Typed.empty)
None // fields type collision - skipping this field
else
Some(fieldName -> common)
commonSupertypeOpt(leftType, rightType).map { common =>
fieldName -> common
}
case (_, longerList) =>
throw new IllegalArgumentException(
"Computing union of more than two fields: " + longerList
Expand All @@ -123,8 +126,7 @@ class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassReso
case _ => Some(Typed.typedClass[Number])
}
} else {
val forComplexTypes = commonSuperTypeForComplexTypes(boxedLeftClass, left.params, boxedRightClass, right.params)
forComplexTypes match {
commonSuperTypeForComplexTypes(boxedLeftClass, left.params, boxedRightClass, right.params).flatMap {
case tc: TypedClass => Some(tc)
case _ => None // empty, union and so on
}
Expand All @@ -133,11 +135,11 @@ class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassReso

private def klassCommonSupertype(left: TypedClass, right: TypedClass)(
implicit numberPromotionStrategy: NumberTypesPromotionStrategy
): TypingResult = {
): Option[TypingResult] = {
val boxedLeftClass = ClassUtils.primitiveToWrapper(left.klass)
val boxedRightClass = ClassUtils.primitiveToWrapper(right.klass)
if (List(boxedLeftClass, boxedRightClass).forall(classOf[Number].isAssignableFrom)) {
numberPromotionStrategy.promoteClasses(boxedLeftClass, boxedRightClass)
Some(numberPromotionStrategy.promoteClasses(boxedLeftClass, boxedRightClass))
} else {
commonSuperTypeForComplexTypes(boxedLeftClass, left.params, boxedRightClass, right.params)
}
Expand All @@ -148,11 +150,11 @@ class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassReso
leftParams: List[TypingResult],
right: Class[_],
rightParams: List[TypingResult]
) = {
): Option[TypingResult] = {
if (left.isAssignableFrom(right)) {
genericClassWithSuperTypeParams(left, leftParams, rightParams)
Some(genericClassWithSuperTypeParams(left, leftParams, rightParams))
} else if (right.isAssignableFrom(left)) {
genericClassWithSuperTypeParams(right, rightParams, leftParams)
Some(genericClassWithSuperTypeParams(right, rightParams, leftParams))
} else {
// until here things are rather simple
commonSuperTypeForClassesNotInSameInheritanceLine(left, right)
Expand All @@ -163,7 +165,7 @@ class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassReso
superType: Class[_],
superTypeParams: List[TypingResult],
subTypeParams: List[TypingResult]
) = {
): TypedClass = {
// Here is a little bit heuristics. We are not sure what generic types we are comparing, for List[T] with Collection[U]
// it is ok to look for common super type of T and U but for Comparable[T] and Integer it won't be ok.
// Maybe we should do this common super type checking only for well known cases?
Expand All @@ -179,13 +181,16 @@ class CommonSupertypeFinder private (classResolutionStrategy: SupertypeClassReso
Typed.genericTypeClass(superType, commonSuperTypesForGenericParams)
}

private def commonSuperTypeForClassesNotInSameInheritanceLine(left: Class[_], right: Class[_]): TypingResult = {
val result = Typed(ClassHierarchyCommonSupertypeFinder.findCommonSupertypes(left, right).map(Typed(_)))
private def commonSuperTypeForClassesNotInSameInheritanceLine(
left: Class[_],
right: Class[_]
): Option[TypingResult] = {
// None is possible here
val foundTypes = ClassHierarchyCommonSupertypeFinder.findCommonSupertypes(left, right).map(Typed(_))
val result = NonEmptyList.fromList(foundTypes.toList).map(Typed(_))
classResolutionStrategy match {
case SupertypeClassResolutionStrategy.Intersection =>
result
case SupertypeClassResolutionStrategy.FallbackToObjectType =>
if (result == Typed.empty) Unknown else result
case SupertypeClassResolutionStrategy.Intersection => result
case SupertypeClassResolutionStrategy.FallbackToObjectType => result.orElse(Some(Unknown))
}
}

Expand All @@ -200,7 +205,12 @@ object CommonSupertypeFinder {
private val delegate = new CommonSupertypeFinder(SupertypeClassResolutionStrategy.FallbackToObjectType)

def commonSupertype(left: TypingResult, right: TypingResult): TypingResult = {
delegate.commonSupertype(left, right)(NumberTypesPromotionStrategy.ToSupertype)
delegate
.commonSupertypeOpt(left, right)(NumberTypesPromotionStrategy.ToSupertype)
.getOrElse(
// We don't return Unknown as a sanity check, that our fallback strategy works correctly and supertypes for object types are used
throw new IllegalStateException(s"Common super type not found: for $left and $right")
)
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package pl.touk.nussknacker.engine.api.typed.supertype

import cats.data.NonEmptyList

import java.lang
import org.apache.commons.lang3.ClassUtils
import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers
Expand Down Expand Up @@ -33,9 +35,9 @@ trait NumberTypesPromotionStrategy extends Serializable {
}
}

private def toSingleTypesSet(typ: TypingResult): Either[Unknown.type, Set[SingleTypingResult]] =
private def toSingleTypesSet(typ: TypingResult): Either[Unknown.type, NonEmptyList[SingleTypingResult]] =
typ match {
case s: SingleTypingResult => Right(Set(s))
case s: SingleTypingResult => Right(NonEmptyList(s, Nil))
case u: TypedUnion => Right(u.possibleTypes)
case TypedNull => Left(Unknown)
case Unknown => Left(Unknown)
Expand Down
Loading

0 comments on commit 6dc66a1

Please sign in to comment.