Skip to content

Commit

Permalink
feat: compute highest common subtype (#901)
Browse files Browse the repository at this point in the history
Closes #860

### Summary of Changes

Add a function to compute the highest common subtype of a list of types.
This is needed to compute the lowest common supertype of a list of types
with contravariant type parameters. Example:

```
class Consumer<in T>
class Logger sub Consumer<String>

segment mySegment(
    p1: Consumer<Any>,
    p2: Logger,
) {
    val a = [p1, p2];
}
```

We can now infer the type of the placeholder `a` to
`List<Consumer<String>>`, where previously we would have inferred
`List<Consumer<Nothing>>`.
  • Loading branch information
lars-reimann authored Feb 20, 2024
1 parent cf92762 commit 5630a9f
Show file tree
Hide file tree
Showing 22 changed files with 1,145 additions and 92 deletions.
16 changes: 3 additions & 13 deletions packages/safe-ds-lang/src/language/typing/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,6 @@ export class UnionType extends Type {
// occurrence of duplicate types. It's also makes splicing easier.
for (let i = newTypes.length - 1; i >= 0; i--) {
const currentType = newTypes[i]!;
const currentTypeIsNothing = currentType.equals(this.coreTypes.Nothing);
const currentTypeIsNothingOrNull = currentType.equals(this.coreTypes.NothingOrNull);

for (let j = newTypes.length - 1; j >= 0; j--) {
Expand All @@ -699,17 +698,6 @@ export class UnionType extends Type {
break;
}

// We can always attempt to replace `Nothing` or `Nothing?` with other types, since they are the bottom
// types. But otherwise, we cannot use a type that is not fully substituted as a replacement. After
// substitution, we might lose information about the original type:
//
// Consider the type `union<C, T>`, where `C` is a class and `T` is a type parameter without an upper
// bound. While `C` is a subtype of `T`, we cannot replace the union type with `T`, since we might later
// substitute `T` with a type that is not a supertype of `C`.
if (!currentTypeIsNothing && !currentTypeIsNothingOrNull && !otherType.isFullySubstituted) {
continue;
}

// Don't merge `Nothing?` into callable types, named tuple types or static types, since that would
// create another union type.
if (
Expand Down Expand Up @@ -738,7 +726,9 @@ export class UnionType extends Type {
const candidateType = otherType.withExplicitNullability(
currentType.isExplicitlyNullable || otherType.isExplicitlyNullable,
);
if (this.typeChecker.isSupertypeOf(candidateType, currentType)) {
if (
this.typeChecker.isSupertypeOf(candidateType, currentType, { strictTypeParameterTypeCheck: true })
) {
// Replace the other type with the candidate type (updated nullability)
newTypes.splice(j, 1, candidateType);
// Remove the current type
Expand Down
36 changes: 34 additions & 2 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 @@ -68,8 +68,22 @@ export class SafeDsTypeChecker {
}

if (other instanceof TypeParameterType) {
const otherUpperBound = this.typeComputer().computeUpperBound(other);
return this.isSubtypeOf(type, otherUpperBound, options);
if (options.strictTypeParameterTypeCheck) {
// `T` can always be assigned to `T` and `T?`
if (
type instanceof TypeParameterType &&
type.declaration === other.declaration &&
(!type.isExplicitlyNullable || other.isExplicitlyNullable)
) {
return true;
}

const otherLowerBound = this.coreTypes.Nothing.withExplicitNullability(other.isExplicitlyNullable);
return this.isSubtypeOf(type, otherLowerBound, options);
} else {
const otherUpperBound = this.typeComputer().computeUpperBound(other);
return this.isSubtypeOf(type, otherUpperBound, options);
}
} else if (other instanceof UnionType) {
return other.types.some((it) => this.isSubtypeOf(type, it, options));
}
Expand Down Expand Up @@ -441,6 +455,24 @@ export class SafeDsTypeChecker {
}
}

/**
* Options for {@link SafeDsTypeChecker.isSubtypeOf} and {@link SafeDsTypeChecker.isSupertypeOf}.
*/
interface TypeCheckOptions {
/**
* Whether to ignore type parameters when comparing class types.
*/
ignoreTypeParameters?: boolean;

/**
* By default, type parameter types are replaced with their upper bound when comparing types. This is usually the
* correct behavior, e.g. to check whether the type `Int` can be assigned to an unsubstituted type parameter type
* `T`.
*
* However, in some cases, we have to assume that the type parameter type that we compare to gets substituted later,
* e.g. by its lower bound `Nothing`. This options enables a strict check, replacing type parameter types in the
* first argument of {@link SafeDsTypeChecker.isSubtypeOf} with their upper bound, and in the second argument with
* their lower bound.
*/
strictTypeParameterTypeCheck?: boolean;
}
Loading

0 comments on commit 5630a9f

Please sign in to comment.