Skip to content

Commit

Permalink
feat: error if impure callable is passed to pure parameter (#792)
Browse files Browse the repository at this point in the history
Closes #730

### Summary of Changes

Show an error if an impure callable is passed to a pure parameter.
  • Loading branch information
lars-reimann authored Nov 22, 2023
1 parent 741113e commit 5536a4a
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 3 deletions.
63 changes: 62 additions & 1 deletion packages/safe-ds-lang/src/language/validation/purity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { stream, type ValidationAcceptor } from 'langium';
import { isSubset } from '../../helpers/collectionUtils.js';
import { isSdsCall, isSdsFunction, isSdsList, type SdsFunction } from '../generated/ast.js';
import { isSdsCall, isSdsFunction, isSdsList, SdsCall, type SdsFunction, SdsParameter } from '../generated/ast.js';
import { findFirstAnnotationCallOf, getArguments, getParameters } from '../helpers/nodeProperties.js';
import { StringConstant } from '../partialEvaluation/model.js';
import type { SafeDsServices } from '../safe-ds-module.js';
Expand All @@ -12,6 +12,7 @@ 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_SET_TO_IMPURE_CALLABLE = 'purity/pure-parameter-set-to-impure-callable';

export const functionPurityMustBeSpecified = (services: SafeDsServices) => {
const annotations = services.builtins.Annotations;
Expand Down Expand Up @@ -215,3 +216,63 @@ export const impurityReasonShouldNotBeSetMultipleTimes = (services: SafeDsServic
}
};
};

export const pureParameterDefaultValueMustBePure = (services: SafeDsServices) => {
const purityComputer = services.purity.PurityComputer;
const typeComputer = services.types.TypeComputer;

return (node: SdsParameter, accept: ValidationAcceptor) => {
if (!node.defaultValue) {
return;
}

const parameterType = typeComputer.computeType(node);
if (!(parameterType instanceof CallableType) || !purityComputer.isPureParameter(node)) {
return;
}

const defaultValueType = typeComputer.computeType(node.defaultValue);
if (!(defaultValueType instanceof CallableType)) {
return;
}

if (!purityComputer.isPureCallable(defaultValueType.callable)) {
accept('error', 'Cannot pass an impure callable to a pure parameter.', {
node: node.defaultValue,
code: CODE_PURITY_PURE_PARAMETER_SET_TO_IMPURE_CALLABLE,
});
}
};
};

export const callArgumentAssignedToPureParameterMustBePure = (services: SafeDsServices) => {
const nodeMapper = services.helpers.NodeMapper;
const purityComputer = services.purity.PurityComputer;
const typeComputer = services.types.TypeComputer;

return (node: SdsCall, accept: ValidationAcceptor) => {
for (const argument of getArguments(node)) {
const parameter = nodeMapper.argumentToParameter(argument);
if (!parameter) {
continue;
}

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

const argumentType = typeComputer.computeType(argument);
if (!(argumentType instanceof CallableType)) {
continue;
}

if (!purityComputer.isPureCallable(argumentType.callable)) {
accept('error', 'Cannot pass an impure callable to a pure parameter.', {
node: argument,
code: CODE_PURITY_PURE_PARAMETER_SET_TO_IMPURE_CALLABLE,
});
}
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,12 @@ import {
unionTypeShouldNotHaveDuplicateTypes,
} from './other/types/unionTypes.js';
import {
callArgumentAssignedToPureParameterMustBePure,
functionPurityMustBeSpecified,
impurityReasonParameterNameMustBelongToParameterOfCorrectType,
impurityReasonShouldNotBeSetMultipleTimes,
impurityReasonsOfOverridingMethodMustBeSubsetOfOverriddenMethod,
pureParameterDefaultValueMustBePure,
} from './purity.js';
import {
annotationCallArgumentListShouldBeNeeded,
Expand Down Expand Up @@ -223,6 +225,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsBlockLambda: [blockLambdaMustContainUniqueNames],
SdsCall: [
callArgumentListShouldBeNeeded(services),
callArgumentAssignedToPureParameterMustBePure(services),
callArgumentMustBeConstantIfParameterIsConstant(services),
callMustNotBeRecursive(services),
callReceiverMustBeCallable(services),
Expand Down Expand Up @@ -317,6 +320,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
constantParameterMustHaveTypeThatCanBeEvaluatedToConstant(services),
parameterMustHaveTypeHint,
parameterDefaultValueTypeMustMatchParameterType(services),
pureParameterDefaultValueMustBePure(services),
requiredParameterMustNotBeDeprecated(services),
requiredParameterMustNotBeExpert(services),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package tests.generator.assignment

@Impure([ImpurityReason.Other]) fun f1(param: Any?)
@Impure([ImpurityReason.Other]) fun f2(param: () -> r: Int?)
@Impure([ImpurityReason.PotentiallyImpureParameterCall("param")]) fun f2(param: () -> r: Int?)

@Impure([ImpurityReason.Other]) fun g() -> (a: Int, b: Int, c: Int)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package tests.generator.expressionStatement

@Impure([ImpurityReason.Other]) fun f(param: () -> ())
@Impure([ImpurityReason.PotentiallyImpureParameterCall("param")]) fun f(param: () -> ())

@Impure([ImpurityReason.Other]) fun g() -> result: Int

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package tests.validation.purity.impureCallablePassedToPureParameter.arguments

@Pure
fun pureFunction()

@Impure([ImpurityReason.Other])
fun impureFunction()

annotation MyAnnotation(
f: () -> (),
other: Int = 1,
)

class MyClass(
f: () -> (),
other: Int = 1,
)

enum MyEnum {
MyEnumVariant(
f: () -> (),
other: Int = 1,
)
}

@Impure([
ImpurityReason.PotentiallyImpureParameterCall("g"),
])
fun myFunction(
f: () -> (),
g: () -> (),
other: Int = 1,
)

segment mySegment1(
f: () -> (),
other: Int = 1,
) {}

segment mySegment2(
myCallableType: (
f: () -> (),
other: Int,
) -> ()
) {
val myBlockLambda = (
f: () -> (),
other: Int = 1,
) {};

val myExpressionLambda = (
f: () -> (),
other: Int = 1,
) -> 1;

// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
MyAnnotation(»pureFunction«, »pureFunction«);
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
MyAnnotation(»impureFunction«, »impureFunction«);

// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
MyClass(»pureFunction«, »pureFunction«);
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
MyClass(»impureFunction«, »impureFunction«);

// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
MyEnum.MyEnumVariant(»pureFunction«, »pureFunction«);
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
MyEnum.MyEnumVariant(»impureFunction«, »impureFunction«);

// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
myFunction(»pureFunction«, »pureFunction«, »pureFunction«);
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
myFunction(»impureFunction«, »impureFunction«, »impureFunction«);

// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
mySegment1(»pureFunction«, »pureFunction«);
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
mySegment1(»impureFunction«, »impureFunction«);

// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
myCallableType(»pureFunction«, »pureFunction«);
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
myCallableType(»impureFunction«, »impureFunction«);

// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
myBlockLambda(»pureFunction«, »pureFunction«);
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
myBlockLambda(»impureFunction«, »impureFunction«);

// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
myExpressionLambda(»pureFunction«, »pureFunction«);
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
myExpressionLambda(»impureFunction«, »impureFunction«);
}

// Argument does not have callable type
pipeline myPipeline {
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
MyClass(»1«, »1«);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package tests.validation.purity.impureCallablePassedToPureParameter.defaultValues

@Pure
fun pureFunction()

@Impure([ImpurityReason.Other])
fun impureFunction()

annotation MyAnnotation(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »pureFunction«,
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
g: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
)

class MyClass1(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »pureFunction«,
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
g: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
)

enum MyEnum {
MyEnumVariant(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »pureFunction«,
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
g: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
)
}

@Pure
fun myFunction1(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »pureFunction«,
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
g: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
)

@Impure([
ImpurityReason.PotentiallyImpureParameterCall("f1"),
ImpurityReason.PotentiallyImpureParameterCall("g1"),
])
fun myFunction2(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f1: () -> () = »pureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
g1: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f2: () -> () = »pureFunction«,
// $TEST$ error "Cannot pass an impure callable to a pure parameter."
g2: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
)

segment mySegment1(
myCallableType: (
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »pureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
g: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
) -> ()
) {}

segment mySegment2(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »pureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
g: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
) {}

pipeline myPipeline {
(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »pureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
g: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
) {};

(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »pureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
g: () -> () = »impureFunction«,
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
other: Int = »impureFunction«,
) -> 1;
}

// Default value does not have callable type
class MyClass2(
// $TEST$ no error "Cannot pass an impure callable to a pure parameter."
f: () -> () = »1«,
)

0 comments on commit 5536a4a

Please sign in to comment.