Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optional type parameters #829

Merged
merged 5 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading