Skip to content

Commit

Permalink
feat: use bounds of type system where possible (#899)
Browse files Browse the repository at this point in the history
### Summary of Changes

* `isSubtypeOf(Nothing, X)` now always returns `true`.
* `isSubtypeOf(X, Any?)` now always returns `true`.
* `lowestCommonSupertype` now returns `Any` or `Any?` if given
`$unknown` or some other unexpected type, instead of propagating
`$unknown`.
  • Loading branch information
lars-reimann authored Feb 20, 2024
1 parent 9d6ce28 commit cf92762
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 37 deletions.
14 changes: 11 additions & 3 deletions packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,13 @@ export class SafeDsTypeChecker {
* Checks whether {@link type} is a subtype of {@link other}.
*/
isSubtypeOf = (type: Type, other: Type, options: TypeCheckOptions = {}): boolean => {
if (type === UnknownType || other === UnknownType) {
if (type.equals(this.coreTypes.Nothing) || other.equals(this.coreTypes.AnyOrNull)) {
return true;
} else if (type === UnknownType || other === UnknownType) {
return false;
} else if (other instanceof TypeParameterType) {
}

if (other instanceof TypeParameterType) {
const otherUpperBound = this.typeComputer().computeUpperBound(other);
return this.isSubtypeOf(type, otherUpperBound, options);
} else if (other instanceof UnionType) {
Expand Down Expand Up @@ -262,7 +266,9 @@ export class SafeDsTypeChecker {
other: Type,
options: TypeCheckOptions,
): boolean {
if (other instanceof NamedTupleType) {
if (other instanceof ClassType) {
return other.declaration === this.builtinClasses.Any;
} else if (other instanceof NamedTupleType) {
return (
type.length === other.length &&
type.entries.every((typeEntry, i) => {
Expand All @@ -281,6 +287,8 @@ export class SafeDsTypeChecker {
private staticTypeIsSubtypeOf(type: StaticType, other: Type, options: TypeCheckOptions): boolean {
if (other instanceof CallableType) {
return this.isSubtypeOf(this.associatedCallableTypeForStaticType(type), other, options);
} else if (other instanceof ClassType) {
return other.declaration === this.builtinClasses.Any;
} else {
return type.equals(other);
}
Expand Down
31 changes: 22 additions & 9 deletions packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,14 +742,22 @@ export class SafeDsTypeComputer {
}
});

// Includes type with unknown supertype
// Group types by their kind
const groupedTypes = this.groupTypes(replacedTypes);
if (groupedTypes.hasTypeWithUnknownSupertype) {
return UnknownType;

// Includes unknown type
if (groupedTypes.containsUnknownType) {
return this.coreTypes.AnyOrNull;
}

// The result must be nullable if any of the types is nullable
const isNullable = replacedTypes.some((it) => it.isExplicitlyNullable);

// Includes unhandled type
if (groupedTypes.containsUnhandledType) {
return this.Any(isNullable);
}

// Class-based types
if (!isEmpty(groupedTypes.classTypes)) {
if (!isEmpty(groupedTypes.enumTypes) || !isEmpty(groupedTypes.enumVariantTypes)) {
Expand Down Expand Up @@ -786,7 +794,8 @@ export class SafeDsTypeComputer {
classTypes: [],
enumTypes: [],
enumVariantTypes: [],
hasTypeWithUnknownSupertype: false,
containsUnhandledType: false,
containsUnknownType: false,
};

for (const type of types) {
Expand All @@ -803,11 +812,14 @@ export class SafeDsTypeComputer {
if (classType instanceof ClassType) {
result.classTypes.push(classType);
} else {
result.hasTypeWithUnknownSupertype = true;
/* c8 ignore next 2 */
result.containsUnknownType = true;
}
} else if (type === UnknownType) {
result.containsUnknownType = true;
} else {
// Other types don't have a clear lowest common supertype
result.hasTypeWithUnknownSupertype = true;
// Since these types don't occur in legal programs, we don't need to handle them better
result.containsUnhandledType = true;
return result;
}
}
Expand Down Expand Up @@ -1022,7 +1034,7 @@ export class SafeDsTypeComputer {
current = this.parentClassType(current);
}

const Any = this.coreTypes.Any.withExplicitNullability(type.isExplicitlyNullable);
const Any = this.Any(type.isExplicitlyNullable);
if (Any instanceof ClassType && !visited.has(Any.declaration)) {
yield Any;
}
Expand Down Expand Up @@ -1072,7 +1084,8 @@ interface GroupTypesResult {
classTypes: ClassType[];
enumTypes: EnumType[];
enumVariantTypes: EnumVariantType[];
hasTypeWithUnknownSupertype: boolean;
containsUnhandledType: boolean;
containsUnknownType: boolean;
}

const NO_SUBSTITUTIONS: TypeParameterSubstitutions = new Map();
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const basic = async (): Promise<IsSubOrSupertypeOfTest[]> => {
const enumVariantType2 = typeComputer.computeType(enumVariant2) as EnumVariantType;

return [
// Callable type to callable type
{
type1: callableType1,
type2: callableType1,
Expand Down Expand Up @@ -526,6 +527,22 @@ const basic = async (): Promise<IsSubOrSupertypeOfTest[]> => {
type2: enumType1,
expected: false,
},
// Named tuple type to class type
{
type1: factory.createNamedTupleType(),
type2: classType1,
expected: false,
},
{
type1: factory.createNamedTupleType(),
type2: coreTypes.Any,
expected: true,
},
{
type1: factory.createNamedTupleType(),
type2: coreTypes.AnyOrNull,
expected: true,
},
// Named tuple type to named tuple type
{
type1: factory.createNamedTupleType(),
Expand Down Expand Up @@ -604,6 +621,22 @@ const basic = async (): Promise<IsSubOrSupertypeOfTest[]> => {
type2: callableType12,
expected: true,
},
// Static type to class type
{
type1: factory.createStaticType(classType1),
type2: classType1,
expected: false,
},
{
type1: factory.createStaticType(classType1),
type2: coreTypes.Any,
expected: true,
},
{
type1: factory.createStaticType(classType1),
type2: coreTypes.AnyOrNull,
expected: true,
},
// Static type to static type
{
type1: factory.createStaticType(classType1),
Expand Down Expand Up @@ -658,6 +691,16 @@ const basic = async (): Promise<IsSubOrSupertypeOfTest[]> => {
type2: UnknownType,
expected: false,
},
{
type1: coreTypes.Nothing,
type2: UnknownType,
expected: true,
},
{
type1: UnknownType,
type2: coreTypes.AnyOrNull,
expected: true,
},
];
};

Expand Down Expand Up @@ -1011,7 +1054,7 @@ const typeParameterTypes = async (): Promise<IsSubOrSupertypeOfTest[]> => {
{
type1: unresolved,
type2: unbounded,
expected: false,
expected: true,
},
{
type1: coreTypes.AnyOrNull,
Expand Down Expand Up @@ -1107,7 +1150,7 @@ const typeParameterTypes = async (): Promise<IsSubOrSupertypeOfTest[]> => {
{
type1: coreTypes.Nothing,
type2: unresolved,
expected: false,
expected: true,
},

// Compare to some other type
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package tests.typing.lowestCommonSupertype.unhandledType

class C
@Pure fun f() -> (r1: Int, r2: Int)

segment mySegment() {
// $TEST$ serialization List<Any>
»[1, f]«;

// $TEST$ serialization List<Any?>
»[null, f]«;

// $TEST$ serialization List<Any>
»[1, C]«;

// $TEST$ serialization List<Any?>
»[null, C]«;

// $TEST$ serialization List<Any>
»[1, f()]«;

// $TEST$ serialization List<Any?>
»[null, f()]«;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tests.typing.lowestCommonSupertype.unknownType

segment mySegment() {
// $TEST$ serialization List<Any?>
»[1, unknown]«;

// $TEST$ serialization List<Any?>
»[null, unknown]«;
}

0 comments on commit cf92762

Please sign in to comment.