Skip to content

Commit

Permalink
feat: streamline purity information (#779)
Browse files Browse the repository at this point in the history
### Summary of Changes

Remove the option to mark callable parameters as `@Pure`, since it could
collide with the impurity reasons on the containing function in various
ways. Now only the context decides, whether a callable parameter only
accepts pure callables or may be potentially impure.
  • Loading branch information
lars-reimann committed Nov 15, 2023
1 parent c15c70e commit 75a9e5b
Show file tree
Hide file tree
Showing 11 changed files with 8 additions and 299 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
return this.getAnnotation(PURITY_URI, 'Impure');
}

callsPure(node: SdsFunction | SdsParameter | undefined): boolean {
callsPure(node: SdsFunction | undefined): boolean {
return hasAnnotationCallOf(node, this.Pure);
}

Expand Down
85 changes: 2 additions & 83 deletions packages/safe-ds-lang/src/language/validation/purity.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 { isSdsCall, isSdsFunction, isSdsList, type SdsFunction } from '../generated/ast.js';
import { findFirstAnnotationCallOf, getArguments, getParameters } from '../helpers/nodeProperties.js';
import { EvaluatedEnumVariant, StringConstant } from '../partialEvaluation/model.js';
import { StringConstant } from '../partialEvaluation/model.js';
import type { SafeDsServices } from '../safe-ds-module.js';
import { CallableType } from '../typing/model.js';

Expand All @@ -12,66 +12,6 @@ export const CODE_PURITY_IMPURITY_REASONS_OF_OVERRIDING_METHOD = 'purity/impurit
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.callsPure(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.callsPure(node) &&
!builtinAnnotations.callsPure(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 @@ -275,24 +215,3 @@ export const impurityReasonShouldNotBeSetMultipleTimes = (services: SafeDsServic
}
};
};

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

return (node: SdsParameter, accept: ValidationAcceptor) => {
// Don't show an error if no type is specified (yet) or if the parameter is not marked as pure
if (!node.type || !builtinAnnotations.callsPure(node)) {
return;
}

const type = typeComputer.computeType(node);
if (!(type instanceof CallableType)) {
accept('error', 'A pure parameter must have a callable type.', {
node,
property: 'name',
code: CODE_PURITY_PURE_PARAMETER_MUST_HAVE_CALLABLE_TYPE,
});
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,10 @@ import {
unionTypeShouldNotHaveDuplicateTypes,
} from './other/types/unionTypes.js';
import {
callableParameterPurityMustBeSpecified,
functionPurityMustBeSpecified,
impurityReasonParameterNameMustBelongToParameterOfCorrectType,
impurityReasonShouldNotBeSetMultipleTimes,
impurityReasonsOfOverridingMethodMustBeSubsetOfOverriddenMethod,
pureParameterMustHaveCallableType,
} from './purity.js';
import {
annotationCallArgumentListShouldBeNeeded,
Expand All @@ -157,7 +155,6 @@ import {
importedDeclarationAliasShouldDifferFromDeclarationName,
memberAccessNullSafetyShouldBeNeeded,
namedTypeTypeArgumentListShouldBeNeeded,
pureAnnotationCallOnParameterShouldBeNeeded,
segmentResultListShouldNotBeEmpty,
typeParameterListShouldNotBeEmpty,
unionTypeShouldNotHaveASingularTypeArgument,
Expand Down Expand Up @@ -256,14 +253,12 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsEnumVariant: [enumVariantMustContainUniqueNames, enumVariantParameterListShouldNotBeEmpty],
SdsExpressionLambda: [expressionLambdaMustContainUniqueNames],
SdsFunction: [
callableParameterPurityMustBeSpecified(services),
functionMustContainUniqueNames,
functionResultListShouldNotBeEmpty,
functionPurityMustBeSpecified(services),
impurityReasonsOfOverridingMethodMustBeSubsetOfOverriddenMethod(services),
impurityReasonParameterNameMustBelongToParameterOfCorrectType(services),
impurityReasonShouldNotBeSetMultipleTimes(services),
pureAnnotationCallOnParameterShouldBeNeeded(services),
pythonCallMustOnlyContainValidTemplateExpressions(services),
pythonNameMustNotBeSetIfPythonCallIsSet(services),
],
Expand Down Expand Up @@ -320,7 +315,6 @@ export const registerValidationChecks = function (services: SafeDsServices) {
constantParameterMustHaveTypeThatCanBeEvaluatedToConstant(services),
parameterMustHaveTypeHint,
parameterDefaultValueTypeMustMatchParameterType(services),
pureParameterMustHaveCallableType(services),
requiredParameterMustNotBeDeprecated(services),
requiredParameterMustNotBeExpert(services),
],
Expand Down
39 changes: 2 additions & 37 deletions packages/safe-ds-lang/src/language/validation/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import {
SdsTypeParameterList,
SdsUnionType,
} from '../generated/ast.js';
import { findFirstAnnotationCallOf, getParameters, getTypeParameters, Parameter } from '../helpers/nodeProperties.js';
import { getParameters, getTypeParameters, Parameter } from '../helpers/nodeProperties.js';
import { NullConstant } from '../partialEvaluation/model.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { CallableType, UnknownType } from '../typing/model.js';
import { UnknownType } from '../typing/model.js';

export const CODE_STYLE_UNNECESSARY_ASSIGNMENT = 'style/unnecessary-assignment';
export const CODE_STYLE_UNNECESSARY_ARGUMENT_LIST = 'style/unnecessary-argument-list';
Expand All @@ -33,7 +33,6 @@ export const CODE_STYLE_UNNECESSARY_CONSTRAINT_LIST = 'style/unnecessary-constra
export const CODE_STYLE_UNNECESSARY_ELVIS_OPERATOR = 'style/unnecessary-elvis-operator';
export const CODE_STYLE_UNNECESSARY_IMPORT_ALIAS = 'style/unnecessary-import-alias';
export const CODE_STYLE_UNNECESSARY_PARAMETER_LIST = 'style/unnecessary-parameter-list';
export const CODE_STYLE_UNNECESSARY_PURE_ANNOTATION_CALL = 'style/unnecessary-pure-annotation-call';
export const CODE_STYLE_UNNECESSARY_RESULT_LIST = 'style/unnecessary-result-list';
export const CODE_STYLE_UNNECESSARY_SAFE_ACCESS = 'style/unnecessary-safe-access';
export const CODE_STYLE_UNNECESSARY_TYPE_ARGUMENT_LIST = 'style/unnecessary-type-argument-list';
Expand Down Expand Up @@ -252,40 +251,6 @@ export const enumVariantParameterListShouldNotBeEmpty = (node: SdsEnumVariant, a
}
};

// -----------------------------------------------------------------------------
// Unnecessary pure annotation calls
// -----------------------------------------------------------------------------

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

return (node: SdsFunction, accept: ValidationAcceptor) => {
if (!builtinAnnotations.callsPure(node) || builtinAnnotations.callsImpure(node)) {
return;
}

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

const pureAnnotationCall = findFirstAnnotationCallOf(parameter, builtinAnnotations.Pure);
if (pureAnnotationCall) {
accept(
'info',
'Callable parameters of a pure function are always pure, so this annotation call can be removed.',
{
node: pureAnnotationCall,
code: CODE_STYLE_UNNECESSARY_PURE_ANNOTATION_CALL,
},
);
}
}
};
};

// -----------------------------------------------------------------------------
// Unnecessary result lists
// -----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
package safeds.lang

/**
* **If called on a function:** Indicates that the function has no side effects and always returns the same results
* given the same arguments.
* Indicates that the function has no side effects and always returns the same results given the same arguments.
*
* Calls to such a function may be eliminated, if its results are not used. Moreover, the function can be memoized, i.e.
* we can remember its results for a set of arguments. Finally, a pure function can be called at any time, allowing
* reordering of calls or parallelization.
*
* **If called on a parameter:** Indicates that the parameter only accepts pure callables.
*/
@Experimental
@Target([AnnotationTarget.Function, AnnotationTarget.Parameter])
@Target([AnnotationTarget.Function])
annotation Pure

/**
* Indicates that the function has side effects and/or does not always return the same results given the same arguments.
*
* @param allReasons
* A list of **all** reasons why the function is impure. If no specific {@link ImpurityReason} applies, include
* `ImpurityReason.Other`.
* A list of **all** reasons why the function is impure. If no specific {@link ImpurityReason} applies, include `ImpurityReason.Other`.
*/
@Experimental
@Target([AnnotationTarget.Function])
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit 75a9e5b

Please sign in to comment.