Skip to content

Commit

Permalink
refactor: move callToParameterValue into NodeMapper (#770)
Browse files Browse the repository at this point in the history
### Summary of Changes

Move the method `callToParameterValue` to `SafeDsNodeMapper`.
Previously, it was private in `SafeDsAnnotations`. Given a call and a
parameter (or its name), it returns the value assigned to the parameter
in the call. It also takes default values into account.
  • Loading branch information
lars-reimann authored Nov 13, 2023
1 parent 061d3b1 commit 275366a
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 27 deletions.
33 changes: 9 additions & 24 deletions packages/safe-ds-lang/src/language/builtins/safe-ds-annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,7 @@ import {
SdsModule,
SdsParameter,
} from '../generated/ast.js';
import {
findFirstAnnotationCallOf,
getArguments,
getEnumVariants,
getParameters,
hasAnnotationCallOf,
} from '../helpers/nodeProperties.js';
import { findFirstAnnotationCallOf, getEnumVariants, hasAnnotationCallOf } from '../helpers/nodeProperties.js';
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import {
EvaluatedEnumVariant,
Expand Down Expand Up @@ -79,7 +73,7 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {

streamImpurityReasons(node: SdsFunction | undefined): Stream<SdsEnumVariant> {
// If allReasons are specified, but we could not evaluate them to a list, no reasons apply
const value = this.getArgumentValue(node, this.Impure, 'allReasons');
const value = this.getParameterValue(node, this.Impure, 'allReasons');
if (!(value instanceof EvaluatedList)) {
return EMPTY_STREAM;
}
Expand Down Expand Up @@ -107,7 +101,7 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
}

getPythonCall(node: SdsFunction | undefined): string | undefined {
const value = this.getArgumentValue(node, this.PythonCall, 'callSpecification');
const value = this.getParameterValue(node, this.PythonCall, 'callSpecification');
if (value instanceof StringConstant) {
return value.value;
} else {
Expand All @@ -120,7 +114,7 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
}

getPythonModule(node: SdsModule | undefined): string | undefined {
const value = this.getArgumentValue(node, this.PythonModule, 'qualifiedName');
const value = this.getParameterValue(node, this.PythonModule, 'qualifiedName');
if (value instanceof StringConstant) {
return value.value;
} else {
Expand All @@ -133,7 +127,7 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
}

getPythonName(node: SdsAnnotatedObject | undefined): string | undefined {
const value = this.getArgumentValue(node, this.PythonName, 'name');
const value = this.getParameterValue(node, this.PythonName, 'name');
if (value instanceof StringConstant) {
return value.value;
} else {
Expand All @@ -160,7 +154,7 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
}

// If targets are specified, but we could not evaluate them to a list, no target is valid
const value = this.getArgumentValue(node, this.Target, 'targets');
const value = this.getParameterValue(node, this.Target, 'targets');
if (!(value instanceof EvaluatedList)) {
return EMPTY_STREAM;
}
Expand All @@ -187,7 +181,7 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
* Finds the first call of the given annotation on the given node and returns the value that is assigned to the
* parameter with the given name.
*/
private getArgumentValue(
private getParameterValue(
node: SdsAnnotatedObject | undefined,
annotation: SdsAnnotation | undefined,
parameterName: string,
Expand All @@ -197,16 +191,7 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
return UnknownEvaluatedNode;
}

// Parameter is set explicitly
const argument = getArguments(annotationCall).find(
(it) => this.nodeMapper.argumentToParameter(it)?.name === parameterName,
);
if (argument) {
return this.partialEvaluator.evaluate(argument.value);
}

// Parameter is not set explicitly, so we use the default value
const parameter = getParameters(annotation).find((it) => it.name === parameterName);
return this.partialEvaluator.evaluate(parameter?.defaultValue);
const parameterValue = this.nodeMapper.callToParameterValue(annotationCall, parameterName);
return this.partialEvaluator.evaluate(parameterValue);
}
}
47 changes: 44 additions & 3 deletions packages/safe-ds-lang/src/language/helpers/safe-ds-node-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isSdsClass,
isSdsEnumVariant,
isSdsNamedType,
isSdsParameter,
isSdsReference,
isSdsSegment,
isSdsType,
Expand Down Expand Up @@ -48,7 +49,7 @@ export class SafeDsNodeMapper {
}

/**
* Returns the parameter that the argument is assigned to. If there is no matching parameter, returns undefined.
* Returns the parameter that the argument is assigned to. If there is no matching parameter, returns `undefined`.
*/
argumentToParameter(node: SdsArgument | undefined): SdsParameter | undefined {
if (!node) {
Expand Down Expand Up @@ -126,7 +127,7 @@ export class SafeDsNodeMapper {
}

/**
* Returns the callable that is called by the given call. If no callable can be found, returns undefined.
* Returns the callable that is called by the given call. If no callable can be found, returns `undefined`.
*/
callToCallable(node: SdsAbstractCall | undefined): SdsCallable | undefined {
if (!node) {
Expand All @@ -150,6 +151,46 @@ export class SafeDsNodeMapper {
return undefined;
}

/**
* Returns the value that is assigned to the given parameter in the given call. This can be either the argument
* value, or the parameter's default value if no argument is provided. If no value can be found, returns
* `undefined`.
*
* @param call The call whose parameter value to return.
* @param parameter The parameter whose value to return. Can be either a parameter itself or its name.
*/
callToParameterValue(
call: SdsAbstractCall | undefined,
parameter: SdsParameter | string | undefined,
): SdsExpression | undefined {
if (!call || !parameter) {
return undefined;
}

// Parameter is set explicitly
const argument = getArguments(call).find((it) => {
if (isSdsParameter(parameter)) {
return this.argumentToParameter(it) === parameter;
} else {
return this.argumentToParameter(it)?.name === parameter;
}
});
if (argument) {
return argument.value;
}

// Parameter is not set but might have a default value
// We must ensure the parameter belongs to the called callable, so we cannot directly get the defaultValue
const callable = this.callToCallable(call);
return getParameters(callable).find((it) => {
if (isSdsParameter(parameter)) {
return it === parameter;
} else {
return it.name === parameter;
}
})?.defaultValue;
}

/**
* Returns all references that target the given parameter.
*/
Expand Down Expand Up @@ -210,7 +251,7 @@ export class SafeDsNodeMapper {

/**
* Returns the type parameter that the type argument is assigned to. If there is no matching type parameter, returns
* undefined.
* `undefined`.
*/
typeArgumentToTypeParameter(node: SdsTypeArgument | undefined): SdsTypeParameter | undefined {
if (!node) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { EmptyFileSystem } from 'langium';
import { describe, expect, it } from 'vitest';
import {
isSdsModule,
SdsAbstractCall,
SdsFunction,
SdsParameter,
SdsPipeline,
} from '../../../../src/language/generated/ast.js';
import { createSafeDsServices } from '../../../../src/language/index.js';
import { Constant, IntConstant } from '../../../../src/language/partialEvaluation/model.js';
import { getNodeOfType } from '../../../helpers/nodeFinder.js';
import { getModuleMembers, getParameters } from '../../../../src/language/helpers/nodeProperties.js';

const services = createSafeDsServices(EmptyFileSystem).SafeDs;
const callGraphComputer = services.flow.CallGraphComputer;
const nodeMapper = services.helpers.NodeMapper;
const partialEvaluator = services.evaluation.PartialEvaluator;

const code = `
fun myFunction(p1: String, p2: String = 0)
pipeline myPipeline {
myFunction(1, 2);
myFunction();
unresolved();
}
`;
const module = await getNodeOfType(services, code, isSdsModule);
const myFunction = getModuleMembers(module)[0] as SdsFunction;
const p1 = getParameters(myFunction)[0]!;
const p2 = getParameters(myFunction)[1]!;
const myPipeline = module?.members[1] as SdsPipeline;
const call1 = callGraphComputer.getCalls(myPipeline)[0]!;
const call2 = callGraphComputer.getCalls(myPipeline)[1]!;
const call3 = callGraphComputer.getCalls(myPipeline)[2]!;

describe('SafeDsNodeMapper', () => {
const testCases: CallToParameterValueTest[] = [
{
testName: 'undefined call, undefined parameter',
call: undefined,
parameter: undefined,
expectedResult: undefined,
},
{
testName: 'undefined call, defined parameter',
call: undefined,
parameter: p1,
expectedResult: undefined,
},
{
testName: 'defined call, undefined parameter',
call: call1,
parameter: undefined,
expectedResult: undefined,
},
{
testName: 'parameter is object, required parameter, value provided',
call: call1,
parameter: p1,
expectedResult: new IntConstant(1n),
},
{
testName: 'parameter is object, optional parameter, value provided',
call: call1,
parameter: p2,
expectedResult: new IntConstant(2n),
},
{
testName: 'parameter is object, required parameter, no value provided',
call: call2,
parameter: p1,
expectedResult: undefined,
},
{
testName: 'parameter is object, optional parameter, no value provided',
call: call2,
parameter: p2,
expectedResult: new IntConstant(0n),
},
{
testName: 'parameter is string, required parameter, value provided',
call: call1,
parameter: 'p1',
expectedResult: new IntConstant(1n),
},
{
testName: 'parameter is string, optional parameter, value provided',
call: call1,
parameter: 'p2',
expectedResult: new IntConstant(2n),
},
{
testName: 'parameter is string, required parameter, no value provided',
call: call2,
parameter: 'p1',
expectedResult: undefined,
},
{
testName: 'parameter is string, optional parameter, no value provided',
call: call2,
parameter: 'p2',
expectedResult: new IntConstant(0n),
},
{
testName: 'parameter is object, required parameter, unresolved callable',
call: call3,
parameter: p1,
expectedResult: undefined,
},
{
testName: 'parameter is object, optional parameter, unresolved callable',
call: call3,
parameter: p2,
expectedResult: undefined,
},
{
testName: 'parameter is string, required parameter, unresolved callable',
call: call3,
parameter: 'p1',
expectedResult: undefined,
},
{
testName: 'parameter is string, optional parameter, unresolved callable',
call: call3,
parameter: 'p2',
expectedResult: undefined,
},
];

describe.each(testCases)('callToParameterValue', ({ testName, call, parameter, expectedResult }) => {
it(testName, () => {
const parameterValue = nodeMapper.callToParameterValue(call, parameter);
if (expectedResult === undefined) {
expect(parameterValue).toBeUndefined();
return;
}

const evaluatedParameterValue = partialEvaluator.evaluate(parameterValue);
expect(evaluatedParameterValue).toStrictEqual(expectedResult);
});
});
});

/**
* A test case for {@link SafeDsNodeMapper.callToParameterValue}.
*/
interface CallToParameterValueTest {
/**
* A short description of the test case.
*/
testName: string;

/**
* The abstract call to test.
*/
call: SdsAbstractCall | undefined;

/**
* The parameter to test.
*/
parameter: SdsParameter | string | undefined;

/**
* The expected result.
*/
expectedResult: Constant | undefined;
}

0 comments on commit 275366a

Please sign in to comment.