Skip to content

Commit

Permalink
feat: improve handling of subclasses of lists/maps (#890)
Browse files Browse the repository at this point in the history
### Summary of Changes

* Handle subclasses of lists/maps when computing the result type of an
indexed access.
* Validate the key type for an indexed access if the receiver is a
subclass of map.
* Check bounds of list index also if the list is nullable.
  • Loading branch information
lars-reimann authored Feb 17, 2024
1 parent 1277bd1 commit bb0c94b
Show file tree
Hide file tree
Showing 17 changed files with 1,885 additions and 1,641 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class SafeDsTypeHierarchyProvider extends AbstractTypeHierarchyProvider {
}

private getSupertypesOfClass(node: SdsClass): TypeHierarchyItem[] | undefined {
const parentClass = this.classHierarchy.streamSuperclasses(node).head();
const parentClass = this.classHierarchy.streamProperSuperclasses(node).head();
if (!parentClass) {
/* c8 ignore next 2 */
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,23 @@ export class SafeDsClassHierarchy {
return true;
}

return node === other || this.streamSuperclasses(node).includes(other);
return node === other || this.streamProperSuperclasses(node).includes(other);
}

/**
* Returns a stream of all superclasses of the given class. Direct ancestors are returned first, followed by their
* ancestors and so on. The class itself is not included in the stream unless there is a cycle in the inheritance
* hierarchy.
*/
streamSuperclasses(node: SdsClass | undefined): Stream<SdsClass> {
streamProperSuperclasses(node: SdsClass | undefined): Stream<SdsClass> {
if (!node) {
return EMPTY_STREAM;
}

return stream(this.superclassesGenerator(node));
return stream(this.properSuperclassesGenerator(node));
}

private *superclassesGenerator(node: SdsClass): Generator<SdsClass, void> {
private *properSuperclassesGenerator(node: SdsClass): Generator<SdsClass, void> {
const visited = new Set<SdsClass>();
let current = this.parentClass(node);
while (current && !visited.has(current)) {
Expand All @@ -61,7 +61,7 @@ export class SafeDsClassHierarchy {
return EMPTY_STREAM;
}

return this.streamSuperclasses(node).flatMap(getClassMembers);
return this.streamProperSuperclasses(node).flatMap(getClassMembers);
}

/**
Expand Down
119 changes: 68 additions & 51 deletions packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getContainerOfType, stream } from 'langium';
import { getContainerOfType } from 'langium';
import type { SafeDsClasses } from '../builtins/safe-ds-classes.js';
import { isSdsCallable, isSdsClass, isSdsEnum, type SdsAbstractResult, SdsDeclaration } from '../generated/ast.js';
import {
Expand Down Expand Up @@ -44,60 +44,67 @@ export class SafeDsTypeChecker {
}

// -----------------------------------------------------------------------------------------------------------------
// isAssignableTo
// General cases
// -----------------------------------------------------------------------------------------------------------------

/**
* Checks whether {@link type} is assignable {@link other}.
* Checks whether {@link type} is a supertype of {@link other}.
*/
isAssignableTo = (type: Type, other: Type, options?: IsAssignableToOptions): boolean => {
const ignoreTypeParameters = options?.ignoreTypeParameters ?? false;
isSupertypeOf = (type: Type, other: Type, options: TypeCheckOptions = {}): boolean => {
return this.isSubtypeOf(other, type, options);
};

/**
* Checks whether {@link type} is a subtype of {@link other}.
*/
isSubtypeOf = (type: Type, other: Type, options: TypeCheckOptions = {}): boolean => {
if (type === UnknownType || other === UnknownType) {
return false;
} else if (other instanceof TypeParameterType) {
const otherLowerBound = this.typeComputer().computeLowerBound(other);
const otherUpperBound = this.typeComputer().computeUpperBound(other);

if (!(type instanceof TypeParameterType)) {
return this.isAssignableTo(otherLowerBound, type) && this.isAssignableTo(type, otherUpperBound);
return (
this.isSubtypeOf(otherLowerBound, type, options) && this.isSubtypeOf(type, otherUpperBound, options)
);
}

const typeLowerBound = this.typeComputer().computeLowerBound(type);
const typeUpperBound = this.typeComputer().computeUpperBound(type);

return (
this.isAssignableTo(otherLowerBound, typeLowerBound) &&
this.isAssignableTo(typeUpperBound, otherUpperBound)
this.isSubtypeOf(otherLowerBound, typeLowerBound, options) &&
this.isSubtypeOf(typeUpperBound, otherUpperBound, options)
);
} else if (other instanceof UnionType) {
return other.possibleTypes.some((it) => this.isAssignableTo(type, it));
return other.possibleTypes.some((it) => this.isSubtypeOf(type, it, options));
}

if (type instanceof CallableType) {
return this.callableTypeIsAssignableTo(type, other);
return this.callableTypeIsSubtypeOf(type, other, options);
} else if (type instanceof ClassType) {
return this.classTypeIsAssignableTo(type, other, ignoreTypeParameters);
return this.classTypeIsSubtypeOf(type, other, options);
} else if (type instanceof EnumType) {
return this.enumTypeIsAssignableTo(type, other);
return this.enumTypeIsSubtypeOf(type, other);
} else if (type instanceof EnumVariantType) {
return this.enumVariantTypeIsAssignableTo(type, other);
return this.enumVariantTypeIsSubtypeOf(type, other);
} else if (type instanceof LiteralType) {
return this.literalTypeIsAssignableTo(type, other);
return this.literalTypeIsSubtypeOf(type, other, options);
} else if (type instanceof NamedTupleType) {
return this.namedTupleTypeIsAssignableTo(type, other);
return this.namedTupleTypeIsSubtypeOf(type, other, options);
} else if (type instanceof StaticType) {
return this.staticTypeIsAssignableTo(type, other);
return this.staticTypeIsSubtypeOf(type, other, options);
} else if (type instanceof TypeParameterType) {
return this.typeParameterTypeIsAssignableTo(type, other);
return this.typeParameterTypeIsSubtypeOf(type, other, options);
} else if (type instanceof UnionType) {
return this.unionTypeIsAssignableTo(type, other);
return this.unionTypeIsSubtypeOf(type, other, options);
} /* c8 ignore start */ else {
throw new Error(`Unexpected type: ${type.constructor.name}`);
} /* c8 ignore stop */
};

private callableTypeIsAssignableTo(type: CallableType, other: Type): boolean {
private callableTypeIsSubtypeOf(type: CallableType, other: Type, options: TypeCheckOptions): boolean {
if (other instanceof ClassType) {
return other.declaration === this.builtinClasses.Any;
} else if (other instanceof CallableType) {
Expand All @@ -122,7 +129,7 @@ export class SafeDsTypeChecker {
}

// Types must be contravariant
if (!this.isAssignableTo(otherEntry.type, typeEntry.type)) {
if (!this.isSubtypeOf(otherEntry.type, typeEntry.type, options)) {
return false;
}
}
Expand All @@ -143,7 +150,7 @@ export class SafeDsTypeChecker {
// Names must not match since we always fetch results by index

// Types must be covariant
if (!this.isAssignableTo(typeEntry.type, otherEntry.type)) {
if (!this.isSubtypeOf(typeEntry.type, otherEntry.type, options)) {
return false;
}
}
Expand All @@ -156,7 +163,7 @@ export class SafeDsTypeChecker {
}
}

private classTypeIsAssignableTo(type: ClassType, other: Type, ignoreTypeParameters: boolean): boolean {
private classTypeIsSubtypeOf(type: ClassType, other: Type, options: TypeCheckOptions): boolean {
if (type.isNullable && !other.isNullable) {
return false;
} else if (type.declaration === this.builtinClasses.Nothing) {
Expand All @@ -170,14 +177,12 @@ export class SafeDsTypeChecker {

// We are done already if we ignore type parameters or if the other type has no type parameters
const typeParameters = getTypeParameters(other.declaration);
if (ignoreTypeParameters || isEmpty(typeParameters)) {
if (options.ignoreTypeParameters || isEmpty(typeParameters)) {
return true;
}

// Get the parent type that refers to the same class as `other`
const candidate = stream([type], this.typeComputer().streamSupertypes(type)).find(
(it) => it.declaration === other.declaration,
);
const candidate = this.typeComputer().computeMatchingSupertype(type, other.declaration);
if (!candidate) {
/* c8 ignore next 2 */
return false;
Expand All @@ -191,17 +196,17 @@ export class SafeDsTypeChecker {
if (TypeParameter.isInvariant(it)) {
return candidateType !== UnknownType && candidateType.equals(otherType);
} else if (TypeParameter.isCovariant(it)) {
return this.isAssignableTo(candidateType, otherType);
return this.isSubtypeOf(candidateType, otherType, options);
} else {
return this.isAssignableTo(otherType, candidateType);
return this.isSubtypeOf(otherType, candidateType, options);
}
});
} else {
return false;
}
}

private enumTypeIsAssignableTo(type: EnumType, other: Type): boolean {
private enumTypeIsSubtypeOf(type: EnumType, other: Type): boolean {
if (type.isNullable && !other.isNullable) {
return false;
}
Expand All @@ -215,7 +220,7 @@ export class SafeDsTypeChecker {
}
}

private enumVariantTypeIsAssignableTo(type: EnumVariantType, other: Type): boolean {
private enumVariantTypeIsSubtypeOf(type: EnumVariantType, other: Type): boolean {
if (type.isNullable && !other.isNullable) {
return false;
}
Expand All @@ -232,7 +237,7 @@ export class SafeDsTypeChecker {
}
}

private literalTypeIsAssignableTo(type: LiteralType, other: Type): boolean {
private literalTypeIsSubtypeOf(type: LiteralType, other: Type, options: TypeCheckOptions): boolean {
if (type.isNullable && !other.isNullable) {
return false;
} else if (type.constants.length === 0) {
Expand All @@ -248,7 +253,7 @@ export class SafeDsTypeChecker {
return true;
}

return type.constants.every((constant) => this.constantIsAssignableToClassType(constant, other));
return type.constants.every((constant) => this.constantIsSubtypeOfClassType(constant, other, options));
} else if (other instanceof LiteralType) {
return type.constants.every((constant) =>
other.constants.some((otherConstant) => constant.equals(otherConstant)),
Expand All @@ -258,29 +263,35 @@ export class SafeDsTypeChecker {
}
}

private constantIsAssignableToClassType(constant: Constant, other: ClassType): boolean {
private constantIsSubtypeOfClassType(constant: Constant, other: ClassType, options: TypeCheckOptions): boolean {
const classType = this.typeComputer().computeClassTypeForConstant(constant);
return this.isAssignableTo(classType, other);
return this.isSubtypeOf(classType, other, options);
}

private namedTupleTypeIsAssignableTo(type: NamedTupleType<SdsDeclaration>, other: Type): boolean {
private namedTupleTypeIsSubtypeOf(
type: NamedTupleType<SdsDeclaration>,
other: Type,
options: TypeCheckOptions,
): boolean {
if (other instanceof NamedTupleType) {
return (
type.length === other.length &&
type.entries.every((typeEntry, i) => {
const otherEntry = other.entries[i]!;
// We deliberately ignore the declarations here
return typeEntry.name === otherEntry.name && this.isAssignableTo(typeEntry.type, otherEntry.type);
return (
typeEntry.name === otherEntry.name && this.isSubtypeOf(typeEntry.type, otherEntry.type, options)
);
})
);
} else {
return false;
}
}

private staticTypeIsAssignableTo(type: StaticType, other: Type): boolean {
private staticTypeIsSubtypeOf(type: StaticType, other: Type, options: TypeCheckOptions): boolean {
if (other instanceof CallableType) {
return this.isAssignableTo(this.associatedCallableTypeForStaticType(type), other);
return this.isSubtypeOf(this.associatedCallableTypeForStaticType(type), other, options);
} else {
return type.equals(other);
}
Expand Down Expand Up @@ -322,26 +333,24 @@ export class SafeDsTypeChecker {
}
}

private typeParameterTypeIsAssignableTo(type: TypeParameterType, other: Type): boolean {
private typeParameterTypeIsSubtypeOf(type: TypeParameterType, other: Type, options: TypeCheckOptions): boolean {
const upperBound = this.typeComputer().computeUpperBound(type);
return this.isAssignableTo(upperBound, other);
return this.isSubtypeOf(upperBound, other, options);
}

private unionTypeIsAssignableTo(type: UnionType, other: Type): boolean {
return type.possibleTypes.every((it) => this.isAssignableTo(it, other));
private unionTypeIsSubtypeOf(type: UnionType, other: Type, options: TypeCheckOptions): boolean {
return type.possibleTypes.every((it) => this.isSubtypeOf(it, other, options));
}

// -----------------------------------------------------------------------------------------------------------------
// Other
// Special cases
// -----------------------------------------------------------------------------------------------------------------

/**
* Checks whether {@link type} is allowed as the type of the receiver of an indexed access.
*/
canBeAccessedByIndex = (type: Type): boolean => {
// We must create the non-nullable version since indexed accesses can be null-safe
const nonNullableReceiverType = this.typeComputer().computeNonNullableType(type);
return this.isList(nonNullableReceiverType) || this.isMap(nonNullableReceiverType);
return this.isList(type) || this.isMap(type);
};

/**
Expand Down Expand Up @@ -392,26 +401,34 @@ export class SafeDsTypeChecker {
/**
* Checks whether {@link type} is some kind of list (with any element type).
*/
isList(type: Type): type is ClassType {
isList(type: Type): type is ClassType | TypeParameterType {
const listOrNull = this.coreTypes.List(UnknownType).updateNullability(true);

return (
!type.equals(this.coreTypes.Nothing) &&
this.isAssignableTo(type, this.coreTypes.List(UnknownType), { ignoreTypeParameters: true })
!type.equals(this.coreTypes.NothingOrNull) &&
this.isSubtypeOf(type, listOrNull, {
ignoreTypeParameters: true,
})
);
}

/**
* Checks whether {@link type} is some kind of map (with any key/value types).
*/
isMap(type: Type): type is ClassType {
isMap(type: Type): type is ClassType | TypeParameterType {
const mapOrNull = this.coreTypes.Map(UnknownType, UnknownType).updateNullability(true);

return (
!type.equals(this.coreTypes.Nothing) &&
this.isAssignableTo(type, this.coreTypes.Map(UnknownType, UnknownType), {
!type.equals(this.coreTypes.NothingOrNull) &&
this.isSubtypeOf(type, mapOrNull, {
ignoreTypeParameters: true,
})
);
}
}

interface IsAssignableToOptions {
interface TypeCheckOptions {
ignoreTypeParameters?: boolean;
}
Loading

0 comments on commit bb0c94b

Please sign in to comment.