From bfaf3ce35981d70eeed62c5cab856e2421630a7b Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 20 Feb 2024 12:03:14 +0100 Subject: [PATCH 1/3] feat: `isSubtypeOf` always returns `true` if 1. op is `Nothing` or 2. op is `Any?` --- .../language/typing/safe-ds-type-checker.ts | 14 ++++-- .../type checker/isSubOrSupertypeOf.test.ts | 47 ++++++++++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts index dfc271efd..83d1c5633 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts @@ -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) { @@ -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) => { @@ -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); } diff --git a/packages/safe-ds-lang/tests/language/typing/type checker/isSubOrSupertypeOf.test.ts b/packages/safe-ds-lang/tests/language/typing/type checker/isSubOrSupertypeOf.test.ts index 6a7d511ab..7c71cab9d 100644 --- a/packages/safe-ds-lang/tests/language/typing/type checker/isSubOrSupertypeOf.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/type checker/isSubOrSupertypeOf.test.ts @@ -103,6 +103,7 @@ const basic = async (): Promise => { const enumVariantType2 = typeComputer.computeType(enumVariant2) as EnumVariantType; return [ + // Callable type to callable type { type1: callableType1, type2: callableType1, @@ -526,6 +527,22 @@ const basic = async (): Promise => { 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(), @@ -604,6 +621,22 @@ const basic = async (): Promise => { 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), @@ -658,6 +691,16 @@ const basic = async (): Promise => { type2: UnknownType, expected: false, }, + { + type1: coreTypes.Nothing, + type2: UnknownType, + expected: true, + }, + { + type1: UnknownType, + type2: coreTypes.AnyOrNull, + expected: true, + }, ]; }; @@ -1011,7 +1054,7 @@ const typeParameterTypes = async (): Promise => { { type1: unresolved, type2: unbounded, - expected: false, + expected: true, }, { type1: coreTypes.AnyOrNull, @@ -1107,7 +1150,7 @@ const typeParameterTypes = async (): Promise => { { type1: coreTypes.Nothing, type2: unresolved, - expected: false, + expected: true, }, // Compare to some other type From 2f645c8ab94bb9b5122b165756788d7c5ab84b76 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 20 Feb 2024 12:14:14 +0100 Subject: [PATCH 2/3] feat: LCS returns `Any` or `Any?` instead of `$unknown` --- .../language/typing/safe-ds-type-computer.ts | 30 +++++++++++++------ .../unhandled type/main.sdstest | 24 +++++++++++++++ .../unknown supertype/main.sdstest | 23 -------------- .../unknown type/main.sdstest | 9 ++++++ 4 files changed, 54 insertions(+), 32 deletions(-) create mode 100644 packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unhandled type/main.sdstest delete mode 100644 packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unknown supertype/main.sdstest create mode 100644 packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unknown type/main.sdstest diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts index c285b09ee..3c21fac47 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts @@ -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)) { @@ -786,7 +794,8 @@ export class SafeDsTypeComputer { classTypes: [], enumTypes: [], enumVariantTypes: [], - hasTypeWithUnknownSupertype: false, + containsUnhandledType: false, + containsUnknownType: false, }; for (const type of types) { @@ -803,11 +812,13 @@ export class SafeDsTypeComputer { if (classType instanceof ClassType) { result.classTypes.push(classType); } else { - result.hasTypeWithUnknownSupertype = true; + 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; } } @@ -1022,7 +1033,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; } @@ -1072,7 +1083,8 @@ interface GroupTypesResult { classTypes: ClassType[]; enumTypes: EnumType[]; enumVariantTypes: EnumVariantType[]; - hasTypeWithUnknownSupertype: boolean; + containsUnhandledType: boolean; + containsUnknownType: boolean; } const NO_SUBSTITUTIONS: TypeParameterSubstitutions = new Map(); diff --git a/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unhandled type/main.sdstest b/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unhandled type/main.sdstest new file mode 100644 index 000000000..b9e60d1bb --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unhandled type/main.sdstest @@ -0,0 +1,24 @@ +package tests.typing.lowestCommonSupertype.unhandledType + +class C +@Pure fun f() -> (r1: Int, r2: Int) + +segment mySegment() { + // $TEST$ serialization List + »[1, f]«; + + // $TEST$ serialization List + »[null, f]«; + + // $TEST$ serialization List + »[1, C]«; + + // $TEST$ serialization List + »[null, C]«; + + // $TEST$ serialization List + »[1, f()]«; + + // $TEST$ serialization List + »[null, f()]«; +} diff --git a/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unknown supertype/main.sdstest b/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unknown supertype/main.sdstest deleted file mode 100644 index d4d020f91..000000000 --- a/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unknown supertype/main.sdstest +++ /dev/null @@ -1,23 +0,0 @@ -package tests.typing.lowestCommonSupertype.unknownSupertype - -class C -@Pure fun f() -> (r1: Int, r2: Int) - -segment mySegment() { - // These types really don't have common supertype - - // $TEST$ serialization List<$unknown> - »[1, C]«; - - // $TEST$ serialization List<$unknown> - »[1, unknown]«; - - - // These types don't occur in legal code when we compute the lowest common supertype, so we don't handle them better - - // $TEST$ serialization List<$unknown> - »[1, f]«; - - // $TEST$ serialization List<$unknown> - »[1, f()]«; -} diff --git a/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unknown type/main.sdstest b/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unknown type/main.sdstest new file mode 100644 index 000000000..e73d1d988 --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/typing/lowest common supertype/unknown type/main.sdstest @@ -0,0 +1,9 @@ +package tests.typing.lowestCommonSupertype.unknownType + +segment mySegment() { + // $TEST$ serialization List + »[1, unknown]«; + + // $TEST$ serialization List + »[null, unknown]«; +} From 0d6a215b160e859e3efeb7dafc26c155de0a2db6 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 20 Feb 2024 12:26:36 +0100 Subject: [PATCH 3/3] test: ignore safeguard for coverage --- .../safe-ds-lang/src/language/typing/safe-ds-type-computer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts index 3c21fac47..90f041e33 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts @@ -812,6 +812,7 @@ export class SafeDsTypeComputer { if (classType instanceof ClassType) { result.classTypes.push(classType); } else { + /* c8 ignore next 2 */ result.containsUnknownType = true; } } else if (type === UnknownType) {