Skip to content

Commit

Permalink
feat: check type parameter bounds for default values and named types (#…
Browse files Browse the repository at this point in the history
…919)

Closes partially #614

### Summary of Changes

* Ensure default values of type parameters respect their upper bound.
* Ensure type arguments of named types respect the upper bound of the
corresponding type parameter.
  • Loading branch information
lars-reimann authored Feb 25, 2024
1 parent 4302ca6 commit 7003ea6
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,13 @@ import {
listMustNotContainNamedTuples,
mapMustNotContainNamedTuples,
namedTypeMustSetAllTypeParameters,
namedTypeTypeArgumentsMustMatchBounds,
parameterDefaultValueTypeMustMatchParameterType,
parameterMustHaveTypeHint,
prefixOperationOperandMustHaveCorrectType,
resultMustHaveTypeHint,
typeCastExpressionMustHaveUnknownType,
typeParameterDefaultValueMustMatchUpperBound,
yieldTypeMustMatchResultType,
} from './types.js';
import { statementMustDoSomething } from './other/statements/statements.js';
Expand Down Expand Up @@ -313,6 +315,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
namedTypeMustSetAllTypeParameters(services),
namedTypeTypeArgumentListShouldBeNeeded(services),
namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments,
namedTypeTypeArgumentsMustMatchBounds(services),
],
SdsParameter: [
constantParameterMustHaveConstantDefaultValue(services),
Expand Down Expand Up @@ -347,6 +350,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts],
SdsTypeCast: [typeCastExpressionMustHaveUnknownType(services)],
SdsTypeParameter: [
typeParameterDefaultValueMustMatchUpperBound(services),
typeParameterMustBeUsedInCorrectPosition(services),
typeParameterMustHaveSufficientContext,
typeParameterMustOnlyBeVariantOnClass,
Expand Down
61 changes: 61 additions & 0 deletions packages/safe-ds-lang/src/language/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
SdsPrefixOperation,
SdsResult,
SdsTypeCast,
SdsTypeParameter,
SdsYield,
} from '../generated/ast.js';
import { getTypeArguments, getTypeParameters, TypeParameter } from '../helpers/nodeProperties.js';
Expand Down Expand Up @@ -251,6 +252,44 @@ export const mapMustNotContainNamedTuples = (services: SafeDsServices) => {
};
};

export const namedTypeTypeArgumentsMustMatchBounds = (services: SafeDsServices) => {
const nodeMapper = services.helpers.NodeMapper;
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsNamedType, accept: ValidationAcceptor): void => {
const type = typeComputer.computeType(node);
if (!(type instanceof ClassType) || isEmpty(type.substitutions)) {
return;
}

for (const typeArgument of getTypeArguments(node)) {
const typeParameter = nodeMapper.typeArgumentToTypeParameter(typeArgument);
if (!typeParameter) {
continue;
}

const typeArgumentType = type.substitutions.get(typeParameter);
if (!typeArgumentType) {
/* c8 ignore next 2 */
continue;
}

const upperBound = typeComputer
.computeUpperBound(typeParameter, { stopAtTypeParameterType: true })
.substituteTypeParameters(type.substitutions);

if (!typeChecker.isSubtypeOf(typeArgumentType, upperBound, { strictTypeParameterTypeCheck: true })) {
accept('error', `Expected type '${upperBound}' but got '${typeArgumentType}'.`, {
node: typeArgument,
property: 'value',
code: CODE_TYPE_MISMATCH,
});
}
}
};
};

export const parameterDefaultValueTypeMustMatchParameterType = (services: SafeDsServices) => {
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;
Expand Down Expand Up @@ -326,6 +365,28 @@ export const typeCastExpressionMustHaveUnknownType = (services: SafeDsServices)
};
};

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

return (node: SdsTypeParameter, accept: ValidationAcceptor): void => {
if (!node.defaultValue || !node.upperBound) {
return;
}

const defaultValueType = typeComputer.computeType(node.defaultValue);
const upperBoundType = typeComputer.computeUpperBound(node, { stopAtTypeParameterType: true });

if (!typeChecker.isSubtypeOf(defaultValueType, upperBoundType, { strictTypeParameterTypeCheck: true })) {
accept('error', `Expected type '${upperBoundType}' but got '${defaultValueType}'.`, {
node,
property: 'defaultValue',
code: CODE_TYPE_MISMATCH,
});
}
};
};

export const yieldTypeMustMatchResultType = (services: SafeDsServices) => {
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tests.validation.types.checking.defaultValues

class SomeClass<T1, T2 sub Number>(
// $TEST$ no error r"Expected type .* but got .*\."
p1: T1 = »true«,
// $TEST$ no error r"Expected type .* but got .*\."
p2: T1 = »1«,
// $TEST$ error "Expected type 'T2' but got 'literal<"">'."
p3: T2 = »""«,
)

fun someFunction<T1, T2 sub Number>(
// $TEST$ no error r"Expected type .* but got .*\."
p1: T1 = »true«,
// $TEST$ no error r"Expected type .* but got .*\."
p2: T1 = »1«,
// $TEST$ error "Expected type 'T2' but got 'literal<"">'."
p3: T2 = »""«,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package tests.validation.types.checking.typeParameterBoundsForDefaultValues

class C1<
T1 sub Any,
// $TEST$ error "Expected type 'Number' but got 'T1'."
T2 sub Number = »T1«,
// $TEST$ error "Expected type 'Number' but got 'Any'."
T3 sub Number = »Any«,
// $TEST$ error "Expected type 'T1' but got 'Any'."
T4 sub T1 = »Any«,
>
class C2<
T1 sub Number,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T2 sub Number = »T1«,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T3 sub Number = »Number«,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T4 sub T1 = »T1«,
>
class C3<
T1 sub Int,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T2 sub Number = »T1«,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T3 sub Number = »Int«,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T4 sub T1 = »Nothing«,
>

@Pure fun f1<
T1 sub Any,
// $TEST$ error "Expected type 'Number' but got 'T1'."
T2 sub Number = »T1«,
// $TEST$ error "Expected type 'Number' but got 'Any'."
T3 sub Number = »Any«,
// $TEST$ error "Expected type 'T1' but got 'Any'."
T4 sub T1 = »Any«,
>()
@Pure fun f2<
T1 sub Number,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T2 sub Number = »T1«,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T3 sub Number = »Number«,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T4 sub T1 = »T1«,
>()
@Pure fun f3<
T1 sub Int,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T2 sub Number = »T1«,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T3 sub Number = »Int«,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
T4 sub T1 = »Nothing«,
>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package tests.validation.types.checking.typeParameterBoundsForNamedTypes

class C1<T1>
class C2<T1 sub Number>
class C3<T1, T2 sub T1>

@Pure fun f(
// $TEST$ no error r"Expected type '.*' but got '.*'\."
a1: C1<»Any?«>,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
a2: C1<T1 = »Any?«>,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
a3: C1<Unknown = »Any?«>,

// $TEST$ no error r"Expected type '.*' but got '.*'\."
b1: C2<»Number«>,
// $TEST$ error "Expected type 'Number' but got 'String'."
b2: C2<»String«>,

// $TEST$ no error r"Expected type '.*' but got '.*'\."
// $TEST$ no error r"Expected type '.*' but got '.*'\."
c1: C3<»Number«, »Number«>,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
// $TEST$ no error r"Expected type '.*' but got '.*'\."
c2: C3<»Number«, »Int«>,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
// $TEST$ error "Expected type 'Int' but got 'Number'."
c3: C3<»Int«, »Number«>,
)

0 comments on commit 7003ea6

Please sign in to comment.