diff --git a/packages/safe-ds-lang/src/language/typing/model.ts b/packages/safe-ds-lang/src/language/typing/model.ts index cb92542e7..2b2f9984d 100644 --- a/packages/safe-ds-lang/src/language/typing/model.ts +++ b/packages/safe-ds-lang/src/language/typing/model.ts @@ -125,13 +125,20 @@ export class CallableType extends Type { export class LiteralType extends Type { readonly constants: Constant[]; - override readonly isNullable: boolean; + private _isNullable: boolean | undefined; constructor(...constants: Constant[]) { super(); this.constants = constants; - this.isNullable = constants.some((it) => it === NullConstant); + } + + override get isNullable(): boolean { + if (this._isNullable === undefined) { + this._isNullable = this.constants.some((it) => it === NullConstant); + } + + return this._isNullable; } override equals(other: unknown): boolean { @@ -356,6 +363,11 @@ export class ClassType extends NamedType { return new ClassType(this.declaration, newSubstitutions, this.isNullable); } + override unwrap(): ClassType { + const newSubstitutions = new Map(stream(this.substitutions).map(([key, value]) => [key, value.unwrap()])); + return new ClassType(this.declaration, newSubstitutions, this.isNullable); + } + override updateNullability(isNullable: boolean): ClassType { if (this.isNullable === isNullable) { return this; @@ -511,13 +523,20 @@ export class StaticType extends Type { export class UnionType extends Type { readonly possibleTypes: Type[]; - override readonly isNullable: boolean; + private _isNullable: boolean | undefined; constructor(...possibleTypes: Type[]) { super(); this.possibleTypes = possibleTypes; - this.isNullable = possibleTypes.some((it) => it.isNullable); + } + + override get isNullable(): boolean { + if (this._isNullable === undefined) { + this._isNullable = this.possibleTypes.some((it) => it.isNullable); + } + + return this._isNullable; } override equals(other: unknown): boolean { 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 b75def89d..ffc8609ab 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 @@ -146,12 +146,12 @@ export class SafeDsTypeChecker { private classTypeIsAssignableTo(type: ClassType, other: Type, ignoreTypeParameters: boolean): boolean { if (type.isNullable && !other.isNullable) { return false; + } else if (type.declaration === this.builtinClasses.Nothing) { + return true; } if (other instanceof ClassType) { - if (type.declaration === this.builtinClasses.Nothing) { - return true; - } else if (!this.classHierarchy.isEqualToOrSubclassOf(type.declaration, other.declaration)) { + if (!this.classHierarchy.isEqualToOrSubclassOf(type.declaration, other.declaration)) { return false; } 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 872ff3f9a..0ffbdb577 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 @@ -665,19 +665,41 @@ export class SafeDsTypeComputer { // Simplify possible types const newPossibleTypes = type.possibleTypes.map((it) => this.simplifyType(it)); - // Remove types that are subtypes of others. We do this back-to-front to keep the first occurrence of duplicate - // types. It's also makes splicing easier. + // Merge literal types and remove types that are subtypes of others. We do this back-to-front to keep the first + // occurrence of duplicate types. It's also makes splicing easier. for (let i = newPossibleTypes.length - 1; i >= 0; i--) { const currentType = newPossibleTypes[i]!; - for (let j = 0; j < newPossibleTypes.length; j++) { + for (let j = newPossibleTypes.length - 1; j >= 0; j--) { if (i === j) { continue; } let otherType = newPossibleTypes[j]!; - otherType = otherType.updateNullability(currentType.isNullable || otherType.isNullable); + // Don't merge `Nothing?` into callable types, named tuple types or static types, since that would + // create another union type. + if ( + currentType.equals(this.coreTypes.NothingOrNull) && + (otherType instanceof CallableType || + otherType instanceof NamedTupleType || + otherType instanceof StaticType) + ) { + continue; + } + + // Merge literal types + if (currentType instanceof LiteralType && otherType instanceof LiteralType) { + // Other type always occurs before current type + const newConstants = [...otherType.constants, ...currentType.constants]; + const newLiteralType = this.simplifyType(new LiteralType(...newConstants)); + newPossibleTypes.splice(j, 1, newLiteralType); + newPossibleTypes.splice(i, 1); + break; + } + + // Remove subtypes of other types + otherType = otherType.updateNullability(currentType.isNullable || otherType.isNullable); if (this.typeChecker.isAssignableTo(currentType, otherType)) { newPossibleTypes.splice(j, 1, otherType); // Update nullability newPossibleTypes.splice(i, 1); diff --git a/packages/safe-ds-lang/tests/language/typing/model.test.ts b/packages/safe-ds-lang/tests/language/typing/model.test.ts index 524abe2d9..1303be06d 100644 --- a/packages/safe-ds-lang/tests/language/typing/model.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/model.test.ts @@ -376,6 +376,10 @@ describe('type model', async () => { type: new ClassType(class1, new Map(), false), expectedType: new ClassType(class1, new Map(), false), }, + { + type: new ClassType(class1, new Map([[typeParameter1, new UnionType(UnknownType)]]), false), + expectedType: new ClassType(class1, new Map([[typeParameter1, UnknownType]]), false), + }, { type: new EnumType(enum1, false), expectedType: new EnumType(enum1, false), diff --git a/packages/safe-ds-lang/tests/resources/typing/simplification/merge literal types in union types/main.sdstest b/packages/safe-ds-lang/tests/resources/typing/simplification/merge literal types in union types/main.sdstest new file mode 100644 index 000000000..ddadce8ab --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/typing/simplification/merge literal types in union types/main.sdstest @@ -0,0 +1,18 @@ +package tests.typing.simplification.mergeLiteralTypesInUnionTypes + +class C( + // $TEST$ serialization literal<1> + p1: »union, literal<1, 1>>«, + + // $TEST$ serialization literal<1, 2> + p2: »union, literal<1, 2>>«, + + // $TEST$ serialization literal<1, 2, 3> + p3: »union, literal<2, 3>>«, + + // $TEST$ serialization literal<1, 2, 3> + p4: »union, literal<2>, literal<3>>«, + + // $TEST$ serialization union, String> + p5: »union, String, literal<2, 3>>«, +) diff --git a/packages/safe-ds-lang/tests/resources/typing/simplification/remove unneeded entries from union types/main.sdstest b/packages/safe-ds-lang/tests/resources/typing/simplification/remove unneeded entries from union types/main.sdstest index 2432ae362..032b16079 100644 --- a/packages/safe-ds-lang/tests/resources/typing/simplification/remove unneeded entries from union types/main.sdstest +++ b/packages/safe-ds-lang/tests/resources/typing/simplification/remove unneeded entries from union types/main.sdstest @@ -51,3 +51,23 @@ class C( // $TEST$ serialization Any? p16: »union«, ) + +class TestsInvolvingNothing( + // $TEST$ serialization Any + p1: »union«, + + // $TEST$ serialization Any? + p2: »union«, + + // $TEST$ serialization literal<1> + p3: »union, Nothing>«, + + // $TEST$ serialization literal<1, null> + p4: »union, Nothing?>«, + + // $TEST$ serialization () -> () + p5: »union<() -> (), Nothing>«, + + // $TEST$ serialization union<() -> (), Nothing?> + p6: »union<() -> (), Nothing?>«, +) diff --git a/packages/safe-ds-lang/tests/resources/typing/simplification/replace literals types that allow only null with NothingNullable/main.sdstest b/packages/safe-ds-lang/tests/resources/typing/simplification/replace literals types that allow only null with NothingOrNull/main.sdstest similarity index 88% rename from packages/safe-ds-lang/tests/resources/typing/simplification/replace literals types that allow only null with NothingNullable/main.sdstest rename to packages/safe-ds-lang/tests/resources/typing/simplification/replace literals types that allow only null with NothingOrNull/main.sdstest index dc2a9a396..b52fa6af9 100644 --- a/packages/safe-ds-lang/tests/resources/typing/simplification/replace literals types that allow only null with NothingNullable/main.sdstest +++ b/packages/safe-ds-lang/tests/resources/typing/simplification/replace literals types that allow only null with NothingOrNull/main.sdstest @@ -1,4 +1,4 @@ -package tests.typing.simplification.replaceLiteralTypesThatAllowOnlyNullWithNothingNullable +package tests.typing.simplification.replaceLiteralTypesThatAllowOnlyNullWithNothingOrNull class C( // $TEST$ serialization Nothing?