Skip to content

Commit

Permalink
feat: optional type parameters (#829)
Browse files Browse the repository at this point in the history
Closes #739

### Summary of Changes

Type parameters can now have default values, akin to parameters.
  • Loading branch information
lars-reimann authored Jan 23, 2024
1 parent 67ab766 commit 0e9f67a
Show file tree
Hide file tree
Showing 25 changed files with 209 additions and 70 deletions.
2 changes: 2 additions & 0 deletions packages/safe-ds-lang/src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -979,12 +979,14 @@ SdsTypeParameterList returns SdsTypeParameterList:

interface SdsTypeParameter extends SdsNamedTypeDeclaration {
variance?: string
defaultValue?: SdsType
}

SdsTypeParameter returns SdsTypeParameter:
annotationCalls+=SdsAnnotationCall*
variance=SdsTypeParameterVariance?
name=ID
('=' defaultValue=SdsType)?
;

SdsTypeParameterVariance returns string:
Expand Down
28 changes: 26 additions & 2 deletions packages/safe-ds-lang/src/language/helpers/nodeProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import {
isSdsLambda,
isSdsModule,
isSdsModuleMember,
isSdsNamedType,
isSdsParameter,
isSdsPlaceholder,
isSdsQualifiedImport,
isSdsSegment,
isSdsTypeArgumentList,
isSdsTypeParameter,
isSdsTypeParameterList,
SdsAbstractCall,
SdsAbstractResult,
Expand All @@ -45,6 +48,7 @@ import {
SdsLiteralType,
SdsModule,
SdsModuleMember,
SdsNamedType,
SdsNamedTypeDeclaration,
SdsParameter,
SdsPlaceholder,
Expand Down Expand Up @@ -144,6 +148,16 @@ export namespace TypeArgument {
};
}

export namespace TypeParameter {
export const isOptional = (node: SdsTypeParameter | undefined): boolean => {
return Boolean(node?.defaultValue);
};

export const isRequired = (node: SdsTypeParameter | undefined): boolean => {
return isSdsTypeParameter(node) && !node.defaultValue;
};
}

// -------------------------------------------------------------------------------------------------
// Accessors for list elements
// -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -297,8 +311,18 @@ export const getStatements = (node: SdsBlock | undefined): SdsStatement[] => {
return node?.statements ?? [];
};

export const getTypeArguments = (node: SdsTypeArgumentList | undefined): SdsTypeArgument[] => {
return node?.typeArguments ?? [];
export const getTypeArguments = (node: SdsTypeArgumentList | SdsNamedType | undefined): SdsTypeArgument[] => {
if (!node) {
return [];
}

if (isSdsTypeArgumentList(node)) {
return node.typeArguments;
} else if (isSdsNamedType(node)) {
return getTypeArguments(node.typeArgumentList);
} /* c8 ignore start */ else {
return [];
} /* c8 ignore stop */
};

export const getTypeParameters = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,7 @@ export class SafeDsFormatter extends AbstractFormatter {
}

formatter.property('variance').append(oneSpace());
formatter.keyword('=').surround(oneSpace());
}

private formatSdsTypeArgumentList(node: ast.SdsTypeArgumentList): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ValidationAcceptor } from 'langium';
import { SdsTypeParameterList } from '../../../generated/ast.js';
import { TypeParameter } from '../../../helpers/nodeProperties.js';

export const CODE_TYPE_PARAMETER_LIST_REQUIRED_AFTER_OPTIONAL = 'type-parameter-list/required-after-optional';

export const typeParameterListMustNotHaveRequiredTypeParametersAfterOptionalTypeParameters = (
node: SdsTypeParameterList,
accept: ValidationAcceptor,
) => {
let foundOptional = false;
for (const typeParameter of node.typeParameters) {
if (TypeParameter.isOptional(typeParameter)) {
foundOptional = true;
} else if (foundOptional) {
accept('error', 'After the first optional type parameter all type parameters must be optional.', {
node: typeParameter,
property: 'name',
code: CODE_TYPE_PARAMETER_LIST_REQUIRED_AFTER_OPTIONAL,
});
}
}
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SdsNamedType } from '../../../generated/ast.js';
import { ValidationAcceptor } from 'langium';
import { SafeDsServices } from '../../../safe-ds-module.js';
import { getTypeArguments, getTypeParameters } from '../../../helpers/nodeProperties.js';
import { getTypeArguments, getTypeParameters, TypeParameter } from '../../../helpers/nodeProperties.js';
import { duplicatesBy } from '../../../../helpers/collections.js';
import { pluralize } from '../../../../helpers/strings.js';

Expand Down Expand Up @@ -50,21 +50,44 @@ export const namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedAr
};

export const namedTypeMustNotHaveTooManyTypeArguments = (node: SdsNamedType, accept: ValidationAcceptor): void => {
const actualTypeArgumentCount = getTypeArguments(node).length;

// We can never have too many arguments in this case
if (actualTypeArgumentCount === 0) {
return;
}

// If the declaration is unresolved, we already show another error
const namedTypeDeclaration = node.declaration?.ref;
if (!namedTypeDeclaration) {
return;
}

const typeParameters = getTypeParameters(namedTypeDeclaration);
const typeArguments = getTypeArguments(node.typeArgumentList);
const maxTypeArgumentCount = typeParameters.length;

// All is good
if (actualTypeArgumentCount <= maxTypeArgumentCount) {
return;
}

if (typeArguments.length > typeParameters.length) {
const kind = pluralize(typeParameters.length, 'type argument');
accept('error', `Expected ${typeParameters.length} ${kind} but got ${typeArguments.length}.`, {
const minTypeArgumentCount = typeParameters.filter(TypeParameter.isRequired).length;
const kind = pluralize(Math.max(minTypeArgumentCount, maxTypeArgumentCount), 'type argument');
if (minTypeArgumentCount === maxTypeArgumentCount) {
accept('error', `Expected exactly ${minTypeArgumentCount} ${kind} but got ${actualTypeArgumentCount}.`, {
node,
property: 'typeArgumentList',
code: CODE_NAMED_TYPE_TOO_MANY_TYPE_ARGUMENTS,
});
} else {
accept(
'error',
`Expected between ${minTypeArgumentCount} and ${maxTypeArgumentCount} ${kind} but got ${actualTypeArgumentCount}.`,
{
node,
property: 'typeArgumentList',
code: CODE_NAMED_TYPE_TOO_MANY_TYPE_ARGUMENTS,
},
);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ import {
} from './types.js';
import { statementMustDoSomething } from './other/statements/statements.js';
import { indexedAccessIndexMustBeValid } from './other/expressions/indexedAccess.js';
import { typeParameterListMustNotHaveRequiredTypeParametersAfterOptionalTypeParameters } from './other/declarations/typeParameterLists.js';

/**
* Register custom validation checks.
Expand Down Expand Up @@ -355,6 +356,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
],
SdsTypeParameterConstraint: [typeParameterConstraintLeftOperandMustBeOwnTypeParameter],
SdsTypeParameterList: [
typeParameterListMustNotHaveRequiredTypeParametersAfterOptionalTypeParameters,
typeParameterListsShouldBeUsedWithCaution(services),
typeParameterListShouldNotBeEmpty(services),
],
Expand Down
6 changes: 3 additions & 3 deletions packages/safe-ds-lang/src/language/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
SdsResult,
SdsYield,
} from '../generated/ast.js';
import { getTypeArguments, getTypeParameters } from '../helpers/nodeProperties.js';
import { getTypeArguments, getTypeParameters, TypeParameter } from '../helpers/nodeProperties.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { NamedTupleType } from '../typing/model.js';

Expand Down Expand Up @@ -326,7 +326,7 @@ export const yieldTypeMustMatchResultType = (services: SafeDsServices) => {
export const namedTypeMustSetAllTypeParameters =
(services: SafeDsServices) =>
(node: SdsNamedType, accept: ValidationAcceptor): void => {
const expectedTypeParameters = getTypeParameters(node.declaration?.ref);
const expectedTypeParameters = getTypeParameters(node.declaration?.ref).filter(TypeParameter.isRequired);
if (isEmpty(expectedTypeParameters)) {
return;
}
Expand All @@ -350,7 +350,7 @@ export const namedTypeMustSetAllTypeParameters =
} else {
accept(
'error',
`The type '${node.declaration?.$refText}' is parameterized, so a type argument list must be added.`,
`The type '${node.declaration?.$refText}' has required type parameters, so a type argument list must be added.`,
{
node,
code: CODE_TYPE_MISSING_TYPE_ARGUMENTS,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fun myFunction< in T = Int >()

// -----------------------------------------------------------------------------

fun myFunction<in T = Int>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fun myFunction< out T = Int >()

// -----------------------------------------------------------------------------

fun myFunction<out T = Int>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fun myFunction< T = Int >()

// -----------------------------------------------------------------------------

fun myFunction<T = Int>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// $TEST$ no_syntax_error

fun myFunction<in T = Int>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// $TEST$ no_syntax_error

fun myFunction<out T = Int>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// $TEST$ no_syntax_error

fun myFunction<T = Int>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tests.validation.other.declarations.typeParameterLists.mustNotHaveRequiredAfterOptional

// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
// $TEST$ error "After the first optional type parameter all type parameters must be optional."
// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
class MyClass1<»A«, »B« = Int, »C«, »D« = String>

// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
class MyClass2<»A«, »B« = Int>

// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
class MyClass3<»A«>


// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
// $TEST$ error "After the first optional type parameter all type parameters must be optional."
// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
fun myFunction1<»A«, »B« = Int, »C«, »D« = String>()

// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
fun myFunction2<»A«, »B« = Int>()

// $TEST$ no error "After the first optional type parameter all type parameters must be optional."
fun myFunction3<»A«>()
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package tests.validation.other.types.typeArgumentLists.tooManyTypeArguments

class MyClass1<T>
class MyClass1<A>
class MyClass2<A, B>
class MyClass3<A, B = Int>

fun myFunction(
// $TEST$ no error r"Expected \d* type arguments? but got \d*\."
f: MyClass1»<>«,
// $TEST$ no error r"Expected \d* type arguments? but got \d*\."
g: MyClass1»<Int>«,
// $TEST$ error "Expected 1 type argument but got 2."
h: MyClass1»<Int, Int>«,
// $TEST$ error "Expected 2 type arguments but got 3."
i: MyClass2»<Int, Int, Int>«,
// $TEST$ no error r"Expected \d* type arguments? but got \d*\."
j: Unresolved»<Int, Int>«
// $TEST$ no error r"Expected .* type arguments? but got \d*\."
a: MyClass1»<>«,
// $TEST$ no error r"Expected .* type arguments? but got \d*\."
b: MyClass1»<Int>«,
// $TEST$ error "Expected exactly 1 type argument but got 2."
c: MyClass1»<Int, Int>«,
// $TEST$ error "Expected exactly 2 type arguments but got 3."
d: MyClass2»<Int, Int, Int>«,
// $TEST$ error "Expected between 1 and 2 type arguments but got 3."
f: MyClass3»<Int, Int, Int>«,
// $TEST$ no error r"Expected .* type arguments? but got \d*\."
g: Unresolved»<Int, Int>«
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tests.validation.other.namedTypes.missingRequiredTypeParameter

// $TEST$ no error r"The type parameters? .* must be set here\."

class MyClass<T>

fun myFunction2(
myCallableType: MyClass
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

package tests.validation.types.namedTypes.missingRequiredTypeParameter

class MyClassWithoutTypeParameter
class MyClassWithTypeParameters<T1, T2, T3 = Int>

fun myFunction1(
// $TEST$ no error r"The type parameters? .* must be set here\."
a1: MyClassWithoutTypeParameter»<>«,
// $TEST$ no error r"The type parameters? .* must be set here\."
a2: MyClassWithoutTypeParameter»<Int>«,
// $TEST$ no error r"The type parameters? .* must be set here\."
a3: MyClassWithoutTypeParameter»<T = Int>«,

// $TEST$ error "The type parameters 'T1', 'T2' must be set here."
b1: MyClassWithTypeParameters»<>«,
// $TEST$ error "The type parameter 'T2' must be set here."
b2: MyClassWithTypeParameters»<Int>«,
// $TEST$ error "The type parameter 'T1' must be set here."
b3: MyClassWithTypeParameters»<T2 = Int>«,
// $TEST$ no error r"The type parameters? .* must be set here\."
b4: MyClassWithTypeParameters»<Int, T2 = Int>«,
// $TEST$ no error r"The type parameters? .* must be set here\."
b5: MyClassWithTypeParameters»<Int, Int, Int>«,

// $TEST$ no error r"The type parameters? .* must be set here\."
d1: Unresolved»<>«,
// $TEST$ no error r"The type parameters? .* must be set here\."
d2: Unresolved»<Int>«,
// $TEST$ no error r"The type parameters? .* must be set here\."
d3: Unresolved»<T = Int>«,
)
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
package tests.validation.types.namedTypes.missingTypeArgumentList

class MyClassWithoutTypeParameters
class MyClassWithTypeParameters<T>
class MyClassWithRequiredTypeParameters<T>
class MyClassWithOptionalTypeParameters<T = Int>

fun myFunction(
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
// $TEST$ no error r"The type '\w*' has required type parameters, so a type argument list must be added\."
a1: »MyClassWithoutTypeParameters«,
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
b1: »MyClassWithoutTypeParameters«<>,
// $TEST$ no error r"The type '\w*' has required type parameters, so a type argument list must be added\."
a2: »MyClassWithoutTypeParameters«<>,

// $TEST$ error "The type 'MyClassWithTypeParameters' is parameterized, so a type argument list must be added."
c1: »MyClassWithTypeParameters«,
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
d1: »MyClassWithTypeParameters«<>,
// $TEST$ error "The type 'MyClassWithRequiredTypeParameters' has required type parameters, so a type argument list must be added."
b1: »MyClassWithRequiredTypeParameters«,
// $TEST$ no error r"The type '\w*' has required type parameters, so a type argument list must be added\."
b2: »MyClassWithRequiredTypeParameters«<>,

// $TEST$ no error r"The type '\w*' has required type parameters, so a type argument list must be added\."
c1: »MyClassWithOptionalTypeParameters«,
// $TEST$ no error r"The type '\w*' has required type parameters, so a type argument list must be added\."
c2: »MyClassWithOptionalTypeParameters«<>,

// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
e: »UnresolvedClass«,
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
f: »UnresolvedClass«<>,
// $TEST$ no error r"The type '\w*' has required type parameters, so a type argument list must be added\."
d1: »UnresolvedClass«,
// $TEST$ no error r"The type '\w*' has required type parameters, so a type argument list must be added\."
d2: »UnresolvedClass«<>,
)
Loading

0 comments on commit 0e9f67a

Please sign in to comment.