Skip to content

Commit

Permalink
feat: check whether lower and upper bounds of a type parameter are co…
Browse files Browse the repository at this point in the history
…mpatible (#885)

Closes #875

### Summary of Changes

Show an error if the lower bound of a type parameter is not assignable
to its upper bound. In this case, there is no valid substitution for the
type parameter.
  • Loading branch information
lars-reimann authored Feb 12, 2024
1 parent 6b6f738 commit 2fc7fe6
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 12 deletions.
30 changes: 22 additions & 8 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 @@ -782,11 +782,18 @@ export class SafeDsTypeComputer {
// -----------------------------------------------------------------------------------------------------------------

/**
* Returns the lower bound for the given type parameter type. If no lower bound is specified explicitly, the result
* is `Nothing`. If invalid lower bounds are specified (e.g. because of an unresolved reference or a cycle),
* `$unknown` is returned. The result is simplified as much as possible.
* Returns the lower bound for the given input. If no lower bound is specified explicitly, the result is `Nothing`.
* If invalid lower bounds are specified (e.g. because of an unresolved reference or a cycle), `$unknown` is
* returned. The result is simplified as much as possible.
*/
computeLowerBound(type: TypeParameterType): Type {
computeLowerBound(nodeOrType: SdsTypeParameter | TypeParameterType): Type {
let type: TypeParameterType;
if (nodeOrType instanceof TypeParameterType) {
type = nodeOrType;
} else {
type = this.computeType(nodeOrType) as TypeParameterType;
}

return this.doComputeLowerBound(type, new Set());
}

Expand Down Expand Up @@ -821,11 +828,18 @@ export class SafeDsTypeComputer {
}

/**
* Returns the upper bound for the given type parameter type. If no upper bound is specified explicitly, the result
* is `Any?`. If invalid upper bounds are specified, but are invalid (e.g. because of an unresolved reference or a
* cycle), `$unknown` is returned. The result is simplified as much as possible.
* Returns the upper bound for the given input. If no upper bound is specified explicitly, the result is `Any?`. If
* invalid upper bounds are specified, but are invalid (e.g. because of an unresolved reference or a cycle),
* `$unknown` is returned. The result is simplified as much as possible.
*/
computeUpperBound(type: TypeParameterType): Type {
computeUpperBound(nodeOrType: SdsTypeParameter | TypeParameterType): Type {
let type: TypeParameterType;
if (nodeOrType instanceof TypeParameterType) {
type = nodeOrType;
} else {
type = this.computeType(nodeOrType) as TypeParameterType;
}

const result = this.doComputeUpperBound(type, new Set());
return result.updateNullability(result.isNullable || type.isNullable);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,37 @@ import { SafeDsNodeMapper } from '../../../helpers/safe-ds-node-mapper.js';
import { NamedType, UnknownType } from '../../../typing/model.js';
import { SafeDsTypeComputer } from '../../../typing/safe-ds-type-computer.js';

export const CODE_TYPE_PARAMETER_INCOMPATIBLE_BOUNDS = 'type-parameter/incompatible-bounds';
export const CODE_TYPE_PARAMETER_INSUFFICIENT_CONTEXT = 'type-parameter/insufficient-context';
export const CODE_TYPE_PARAMETER_INVALID_BOUND = 'type-parameter/invalid-bound';
export const CODE_TYPE_PARAMETER_MULTIPLE_BOUNDS = 'type-parameter/multiple-bounds';
export const CODE_TYPE_PARAMETER_USAGE = 'type-parameter/usage';
export const CODE_TYPE_PARAMETER_VARIANCE = 'type-parameter/variance';

export const typeParameterBoundsMustBeCompatible = (services: SafeDsServices) => {
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsTypeParameter, accept: ValidationAcceptor) => {
const lowerBound = typeComputer.computeLowerBound(node);
if (lowerBound === UnknownType) {
return;
}

const upperBound = typeComputer.computeUpperBound(node);
if (upperBound === UnknownType) {
return;
}

if (!typeChecker.isAssignableTo(lowerBound, upperBound)) {
accept('error', `The lower bound '${lowerBound}' is not assignable to the upper bound '${upperBound}'.`, {
node,
code: CODE_TYPE_PARAMETER_INCOMPATIBLE_BOUNDS,
});
}
};
};

export const typeParameterMustHaveSufficientContext = (node: SdsTypeParameter, accept: ValidationAcceptor) => {
const containingCallable = getContainerOfType(node, isSdsCallable);
/* c8 ignore start */
Expand Down Expand Up @@ -63,7 +88,7 @@ export const typeParameterMustHaveSufficientContext = (node: SdsTypeParameter, a
}
};

export const typeParameterMustNotHaveMultipleBounds = (services: SafeDsServices) => {
export const typeParameterMustHaveOneValidLowerAndUpperBound = (services: SafeDsServices) => {
const typeComputer = services.types.TypeComputer;

return (node: SdsTypeParameter, accept: ValidationAcceptor) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ import {
} from './other/declarations/segments.js';
import { typeParameterBoundLeftOperandMustBeOwnTypeParameter } from './other/declarations/typeParameterBounds.js';
import {
typeParameterBoundsMustBeCompatible,
typeParameterMustBeUsedInCorrectPosition,
typeParameterMustHaveOneValidLowerAndUpperBound,
typeParameterMustHaveSufficientContext,
typeParameterMustNotHaveMultipleBounds,
typeParameterMustOnlyBeVariantOnClass,
} from './other/declarations/typeParameters.js';
import { callArgumentMustBeConstantIfParameterIsConstant, callMustNotBeRecursive } from './other/expressions/calls.js';
Expand Down Expand Up @@ -348,9 +349,10 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts],
SdsTypeCast: [typeCastExpressionMustHaveUnknownType(services)],
SdsTypeParameter: [
typeParameterMustHaveSufficientContext,
typeParameterBoundsMustBeCompatible(services),
typeParameterMustBeUsedInCorrectPosition(services),
typeParameterMustNotHaveMultipleBounds(services),
typeParameterMustHaveSufficientContext,
typeParameterMustHaveOneValidLowerAndUpperBound(services),
typeParameterMustOnlyBeVariantOnClass,
],
SdsTypeParameterBound: [typeParameterBoundLeftOperandMustBeOwnTypeParameter],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tests.validation.other.declarations.typeParameters.lowerBoundMustBeAssignableToUpperBound

class C<
// $TEST$ no error r"The lower bound .* is not assignable to the upper bound .*\."
»OnlyLowerBound«,
// $TEST$ no error r"The lower bound .* is not assignable to the upper bound .*\."
»OnlyUpperBound«,
// $TEST$ no error r"The lower bound .* is not assignable to the upper bound .*\."
»CompatibleBounds«,
// $TEST$ error "The lower bound 'Number' is not assignable to the upper bound 'String'."
»IncompatibleBounds«,
// $TEST$ no error r"The lower bound .* is not assignable to the upper bound .*\."
»UnresolvedLowerBound«,
// $TEST$ no error r"The lower bound .* is not assignable to the upper bound .*\."
»UnresolvedUpperBound«,
> where {
OnlyLowerBound super Number,
OnlyUpperBound sub Number,

CompatibleBounds super Number,
CompatibleBounds sub Number,

IncompatibleBounds super Number,
IncompatibleBounds sub String,

UnresolvedLowerBound super unknown,
UnresolvedUpperBound sub unknown,
}

0 comments on commit 2fc7fe6

Please sign in to comment.