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: check whether purity of callable parameters of functions is set properly #777

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
20 changes: 19 additions & 1 deletion packages/safe-ds-lang/src/language/builtins/safe-ds-enums.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { getContainerOfType, URI } from 'langium';
import { resourceNameToUri } from '../../helpers/resources.js';
import { isSdsEnum, SdsEnum } from '../generated/ast.js';
import { isSdsEnum, SdsEnum, type SdsEnumVariant } from '../generated/ast.js';
import { getEnumVariants } from '../helpers/nodeProperties.js';
import { EvaluatedEnumVariant, EvaluatedNode } from '../partialEvaluation/model.js';
import type { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsModuleMembers } from './safe-ds-module-members.js';

const ANNOTATION_USAGE_URI = resourceNameToUri('builtins/safeds/lang/annotationUsage.sdsstub');
Expand All @@ -26,3 +28,19 @@ export class SafeDsEnums extends SafeDsModuleMembers<SdsEnum> {
return this.getModuleMember(uri, name, isSdsEnum);
}
}

export class SafeDsImpurityReasons {
private readonly builtinEnums: SafeDsEnums;

constructor(services: SafeDsServices) {
this.builtinEnums = services.builtins.Enums;
}

get PotentiallyImpureParameterCall(): SdsEnumVariant | undefined {
return this.getEnumVariant('PotentiallyImpureParameterCall');
}

private getEnumVariant(name: string): SdsEnumVariant | undefined {
return getEnumVariants(this.builtinEnums.ImpurityReason).find((variant) => variant.name === name);
}
}
4 changes: 3 additions & 1 deletion packages/safe-ds-lang/src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'langium';
import { SafeDsAnnotations } from './builtins/safe-ds-annotations.js';
import { SafeDsClasses } from './builtins/safe-ds-classes.js';
import { SafeDsEnums } from './builtins/safe-ds-enums.js';
import { SafeDsEnums, SafeDsImpurityReasons } from './builtins/safe-ds-enums.js';
import { SafeDsCommentProvider } from './documentation/safe-ds-comment-provider.js';
import { SafeDsDocumentationProvider } from './documentation/safe-ds-documentation-provider.js';
import { SafeDsCallGraphComputer } from './flow/safe-ds-call-graph-computer.js';
Expand Down Expand Up @@ -49,6 +49,7 @@ export type SafeDsAddedServices = {
Annotations: SafeDsAnnotations;
Classes: SafeDsClasses;
Enums: SafeDsEnums;
ImpurityReasons: SafeDsImpurityReasons;
};
evaluation: {
PartialEvaluator: SafeDsPartialEvaluator;
Expand Down Expand Up @@ -93,6 +94,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
Annotations: (services) => new SafeDsAnnotations(services),
Classes: (services) => new SafeDsClasses(services),
Enums: (services) => new SafeDsEnums(services),
ImpurityReasons: (services) => new SafeDsImpurityReasons(services),
},
documentation: {
CommentProvider: (services) => new SafeDsCommentProvider(services),
Expand Down
91 changes: 89 additions & 2 deletions packages/safe-ds-lang/src/language/validation/purity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { stream, type ValidationAcceptor } from 'langium';
import { isSubset } from '../../helpers/collectionUtils.js';
import { isSdsCall, isSdsFunction, isSdsList, type SdsFunction, type SdsParameter } from '../generated/ast.js';
import { findFirstAnnotationCallOf, getArguments, getParameters } from '../helpers/nodeProperties.js';
import { StringConstant } from '../partialEvaluation/model.js';
import { EvaluatedEnumVariant, StringConstant } from '../partialEvaluation/model.js';
import type { SafeDsServices } from '../safe-ds-module.js';
import { CallableType } from '../typing/model.js';

Expand All @@ -11,8 +11,68 @@ export const CODE_PURITY_IMPURE_AND_PURE = 'purity/impure-and-pure';
export const CODE_PURITY_IMPURITY_REASONS_OF_OVERRIDING_METHOD = 'purity/impurity-reasons-of-overriding-method';
export const CODE_PURITY_INVALID_PARAMETER_NAME = 'purity/invalid-parameter-name';
export const CODE_PURITY_MUST_BE_SPECIFIED = 'purity/must-be-specified';
export const CODE_PURITY_POTENTIALLY_IMPURE_PARAMETER_NOT_CALLABLE = 'purity/potentially-impure-parameter-not-callable';
export const CODE_PURITY_PURE_PARAMETER_MUST_HAVE_CALLABLE_TYPE = 'purity/pure-parameter-must-have-callable-type';

export const callableParameterPurityMustBeSpecified = (services: SafeDsServices) => {
const builtinAnnotations = services.builtins.Annotations;
const possibleImpurityReasons = services.builtins.ImpurityReasons;
const typeComputer = services.types.TypeComputer;

return (node: SdsFunction, accept: ValidationAcceptor) => {
const potentiallyImpureParameterCall = possibleImpurityReasons.PotentiallyImpureParameterCall;
if (!potentiallyImpureParameterCall) {
return;
}

const parameterNameParameter = getParameters(potentiallyImpureParameterCall).find(
(it) => it.name === 'parameterName',
)!;
const impurityReasons = builtinAnnotations.streamImpurityReasons(node).toArray();

for (const parameter of getParameters(node)) {
const parameterType = typeComputer.computeType(parameter);
if (!(parameterType instanceof CallableType)) {
continue;
}

const expectedImpurityReason = new EvaluatedEnumVariant(
possibleImpurityReasons.PotentiallyImpureParameterCall,
new Map([[parameterNameParameter, new StringConstant(parameter.name)]]),
);

if (
builtinAnnotations.isPure(parameter) &&
impurityReasons.some((it) => it.equals(expectedImpurityReason))
) {
accept(
'error',
"'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive.",
{
node: parameter,
property: 'name',
code: CODE_PURITY_IMPURE_AND_PURE,
},
);
} else if (
!builtinAnnotations.isPure(node) &&
!builtinAnnotations.isPure(parameter) &&
!impurityReasons.some((it) => it.equals(expectedImpurityReason))
) {
accept(
'error',
"The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function.",
{
node: parameter,
property: 'name',
code: CODE_PURITY_MUST_BE_SPECIFIED,
},
);
}
}
};
};

export const functionPurityMustBeSpecified = (services: SafeDsServices) => {
const annotations = services.builtins.Annotations;

Expand Down Expand Up @@ -86,11 +146,13 @@ export const impurityReasonsOfOverridingMethodMustBeSubsetOfOverriddenMethod = (
};
};

export const impurityReasonParameterNameMustBelongToParameter = (services: SafeDsServices) => {
export const impurityReasonParameterNameMustBelongToParameterOfCorrectType = (services: SafeDsServices) => {
const builtinAnnotations = services.builtins.Annotations;
const builtinEnums = services.builtins.Enums;
const impurityReasons = services.builtins.ImpurityReasons;
const nodeMapper = services.helpers.NodeMapper;
const partialEvaluator = services.evaluation.PartialEvaluator;
const typeComputer = services.types.TypeComputer;

return (node: SdsFunction, accept: ValidationAcceptor) => {
const annotationCall = findFirstAnnotationCallOf(node, builtinAnnotations.Impure);
Expand Down Expand Up @@ -134,6 +196,7 @@ export const impurityReasonParameterNameMustBelongToParameter = (services: SafeD
continue;
}

// A parameter with the given name must exist
if (!parameterNames.has(evaluatedParameterName.value)) {
const parameterNameArgument = getArguments(reason).find(
(it) => nodeMapper.argumentToParameter(it)?.name === 'parameterName',
Expand All @@ -143,6 +206,30 @@ export const impurityReasonParameterNameMustBelongToParameter = (services: SafeD
node: parameterNameArgument,
code: CODE_PURITY_INVALID_PARAMETER_NAME,
});

continue;
}

// The parameter must have the correct type
if (evaluatedReason.variant === impurityReasons.PotentiallyImpureParameterCall) {
const parameter = getParameters(node).find((it) => it.name === evaluatedParameterName.value)!;
if (!parameter.type) {
continue;
}

const parameterType = typeComputer.computeType(parameter);
if (parameterType instanceof CallableType) {
continue;
}

const parameterNameArgument = getArguments(reason).find(
(it) => nodeMapper.argumentToParameter(it)?.name === 'parameterName',
)!;

accept('error', `The parameter '${evaluatedParameterName.value}' must have a callable type.`, {
node: parameterNameArgument,
code: CODE_PURITY_POTENTIALLY_IMPURE_PARAMETER_NOT_CALLABLE,
});
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ import {
unionTypeShouldNotHaveDuplicateTypes,
} from './other/types/unionTypes.js';
import {
callableParameterPurityMustBeSpecified,
functionPurityMustBeSpecified,
impurityReasonParameterNameMustBelongToParameter,
impurityReasonParameterNameMustBelongToParameterOfCorrectType,
impurityReasonShouldNotBeSetMultipleTimes,
impurityReasonsOfOverridingMethodMustBeSubsetOfOverriddenMethod,
pureParameterMustHaveCallableType,
Expand Down Expand Up @@ -254,11 +255,12 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsEnumVariant: [enumVariantMustContainUniqueNames, enumVariantParameterListShouldNotBeEmpty],
SdsExpressionLambda: [expressionLambdaMustContainUniqueNames],
SdsFunction: [
callableParameterPurityMustBeSpecified(services),
functionMustContainUniqueNames,
functionResultListShouldNotBeEmpty,
functionPurityMustBeSpecified(services),
impurityReasonsOfOverridingMethodMustBeSubsetOfOverriddenMethod(services),
impurityReasonParameterNameMustBelongToParameter(services),
impurityReasonParameterNameMustBelongToParameterOfCorrectType(services),
impurityReasonShouldNotBeSetMultipleTimes(services),
pythonCallMustOnlyContainValidTemplateExpressions(services),
pythonNameMustNotBeSetIfPythonCallIsSet(services),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tests.validation.purity.callableParameterWithUnspecifiedPurity

// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."

class MyClass(
p1: Int,
@Pure p2: () -> (),
»p3«: () -> ()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package tests.validation.purity.callableParameterWithUnspecifiedPurity

fun functionWithUnknownPurity(
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
»p1«: Int,
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
@Pure »p2«: () -> (),
// $TEST$ error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
»p3«: () -> ()
)

@Pure
fun pureFunction(
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
»p1«: Int,
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
@Pure »p2«: () -> (),
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
»p3«: () -> ()
)

@Impure([])
fun impureFunctionWithoutReasons(
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
»p1«: Int,
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
@Pure »p2«: () -> (),
// $TEST$ error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
»p3«: () -> ()
)

@Impure([
ImpurityReason.PotentiallyImpureParameterCall("p1"),
ImpurityReason.PotentiallyImpureParameterCall("p2"),
ImpurityReason.PotentiallyImpureParameterCall("p3")
])
fun impureFunctionWithReasons(
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
»p1«: Int,
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
@Pure »p2«: () -> (),
// $TEST$ no error "The purity of a callable parameter must be specified. Call the annotation '@Pure' or add the impurity reason 'PotentiallyImpureParameterCall' to the containing function."
»p3«: () -> ()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tests.validation.purity.potentiallyImpureParameterMustHaveCallableType

@Impure([
// $TEST$ error "The parameter 'a' must have a callable type."
ImpurityReason.PotentiallyImpureParameterCall(»"a"«),
// $TEST$ error "The parameter 'b' must have a callable type."
ImpurityReason.PotentiallyImpureParameterCall(»"b"«),
// $TEST$ no error r"The parameter '.*' must have a callable type\."
ImpurityReason.PotentiallyImpureParameterCall(»"c"«),
// $TEST$ no error r"The parameter '.*' must have a callable type\."
ImpurityReason.PotentiallyImpureParameterCall(»"d"«),
// $TEST$ no error r"The parameter '.*' must have a callable type\."
ImpurityReason.PotentiallyImpureParameterCall(»"e"«),
])
fun myFunction(
a: Int,
b: Unresolved,
c: () -> (),
d,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package tests.validation.purity.pureAndPotentiallyImpureCallableParameter

fun functionWithUnknownPurity(
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
@Pure »p1«: Int,
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
@Pure »p2«: () -> (),
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
»p3«: () -> ()
)


@Impure([])
fun impureFunctionWithoutReasons(
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
@Pure »p1«: Int,
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
@Pure »p2«: () -> (),
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
»p3«: () -> ()
)

@Impure([
ImpurityReason.PotentiallyImpureParameterCall("p1"),
ImpurityReason.PotentiallyImpureParameterCall("p2"),
ImpurityReason.PotentiallyImpureParameterCall("p3")
])
fun impureFunctionWithReasons(
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
@Pure »p1«: Int,
// $TEST$ error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
@Pure »p2«: () -> (),
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
»p3«: () -> ()
)

@Pure
fun pureFunction(
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
@Pure »p1«: Int,
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
@Pure »p2«: () -> (),
// $TEST$ no error "'@Pure' and the impurity reason 'PotentiallyImpureParameterCall' on the containing function are mutually exclusive."
»p3«: () -> ()
)