Skip to content

Commit

Permalink
feat: full implementation of partial evaluator (#798)
Browse files Browse the repository at this point in the history
Closes #603

### Summary of Changes

Fully port the partial evaluator from Xtext to Langium. The Langium
implementation now has all the features of the previous version (and
many more).
  • Loading branch information
lars-reimann authored Nov 25, 2023
1 parent a5db23c commit 7643794
Show file tree
Hide file tree
Showing 57 changed files with 860 additions and 899 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.map]
insert_final_newline = false

[{*.yaml,*.yml}]
indent_size = 2
127 changes: 43 additions & 84 deletions packages/safe-ds-lang/src/language/flow/safe-ds-call-graph-computer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ import {
type AstNodeLocator,
getContainerOfType,
getDocument,
isNamed,
stream,
streamAst,
WorkspaceCache,
} from 'langium';
import {
isSdsAnnotation,
isSdsBlockLambda,
isSdsCall,
isSdsCallable,
isSdsCallableType,
isSdsClass,
isSdsEnumVariant,
isSdsExpressionLambda,
Expand All @@ -35,9 +32,8 @@ import {
import type { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import type { SafeDsServices } from '../safe-ds-module.js';
import {
BlockLambdaClosure,
EvaluatedCallable,
ExpressionLambdaClosure,
EvaluatedEnumVariant,
NamedCallable,
ParameterSubstitutions,
substitutionsAreEqual,
Expand All @@ -46,7 +42,7 @@ import {
import { CallGraph } from './model.js';
import { getArguments, getParameters } from '../helpers/nodeProperties.js';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import { CallableType, StaticType } from '../typing/model.js';
import { CallableType } from '../typing/model.js';
import { isEmpty } from '../../helpers/collectionUtils.js';
import { SafeDsPartialEvaluator } from '../partialEvaluation/safe-ds-partial-evaluator.js';

Expand Down Expand Up @@ -238,7 +234,11 @@ export class SafeDsCallGraphComputer {

private createSyntheticCallForCall(call: SdsCall, substitutions: ParameterSubstitutions): SyntheticCall {
const evaluatedCallable = this.getEvaluatedCallable(call.receiver, substitutions);
const newSubstitutions = this.getNewSubstitutions(evaluatedCallable, getArguments(call), substitutions);
const newSubstitutions = this.getParameterSubstitutionsAfterCall(
evaluatedCallable,
getArguments(call),
substitutions,
);
return new SyntheticCall(evaluatedCallable, newSubstitutions);
}

Expand All @@ -247,110 +247,69 @@ export class SafeDsCallGraphComputer {
}

private getEvaluatedCallable(
expression: SdsExpression,
expression: SdsExpression | undefined,
substitutions: ParameterSubstitutions,
): EvaluatedCallable | undefined {
// TODO use the partial evaluator here; necessary for closures
// const value = this.partialEvaluator.evaluate(expression, substitutions);
// if (value instanceof EvaluatedCallable) {
// return value;
// }
//
// return undefined;

let callableOrParameter = this.getCallableOrParameter(expression);

if (!callableOrParameter || isSdsAnnotation(callableOrParameter) || isSdsCallableType(callableOrParameter)) {
if (!expression) {
/* c8 ignore next 2 */
return undefined;
} else if (isSdsParameter(callableOrParameter)) {
// Parameter is set
const substitution = substitutions.get(callableOrParameter);
if (substitution) {
if (substitution instanceof EvaluatedCallable) {
return substitution;
} else {
/* c8 ignore next 2 */
return undefined;
}
}

// First try to get the callable via the partial evaluator
const value = this.partialEvaluator.evaluate(expression, substitutions);
if (value instanceof EvaluatedCallable) {
return value;
} else if (value instanceof EvaluatedEnumVariant) {
if (!value.hasBeenInstantiated) {
return new NamedCallable(value.variant);
}

// Parameter is not set
return new NamedCallable(callableOrParameter);
} else if (isNamed(callableOrParameter)) {
return new NamedCallable(callableOrParameter);
} else if (isSdsBlockLambda(callableOrParameter)) {
return new BlockLambdaClosure(callableOrParameter, substitutions);
} else if (isSdsExpressionLambda(callableOrParameter)) {
return new ExpressionLambdaClosure(callableOrParameter, substitutions);
} else {
/* c8 ignore next 2 */
return undefined;
}
}

private getCallableOrParameter(expression: SdsExpression): SdsCallable | SdsParameter | undefined {
// Fall back to getting the called parameter via the type computer
const type = this.typeComputer.computeType(expression);
if (!(type instanceof CallableType)) {
return undefined;
}

if (type instanceof CallableType) {
return type.parameter ?? type.callable;
} else if (type instanceof StaticType) {
const declaration = type.instanceType.declaration;
if (isSdsCallable(declaration)) {
return declaration;
}
const parameterOrCallable = type.parameter ?? type.callable;
if (isSdsParameter(parameterOrCallable)) {
return new NamedCallable(parameterOrCallable);
} else if (isSdsFunction(parameterOrCallable)) {
// Needed for instance methods
return new NamedCallable(parameterOrCallable);
}

return undefined;
}

private getNewSubstitutions(
private getParameterSubstitutionsAfterCall(
callable: EvaluatedCallable | undefined,
args: SdsArgument[],
substitutions: ParameterSubstitutions,
): ParameterSubstitutions {
// TODO: Use this in the partial evaluator too. Here (maybe) filter and keep only the substitutions that are
// callables.
if (!callable || isSdsParameter(callable.callable)) {
return NO_SUBSTITUTIONS;
}

// Substitutions on creation
const substitutionsOnCreation = callable.substitutionsOnCreation;

// Substitutions on call via arguments
// Compute which parameters are set via arguments
const parameters = getParameters(callable.callable);
const substitutionsOnCall = new Map(
args.flatMap((it) => {
// Ignore arguments that don't get assigned to a parameter
const parameterIndex = this.nodeMapper.argumentToParameter(it)?.$containerIndex ?? -1;
if (parameterIndex === -1) {
/* c8 ignore next 2 */
return [];
}
const argumentsByParameter = this.nodeMapper.parametersToArguments(parameters, args);

// argumentToParameter returns parameters of callable types. We have to remap this to parameter of the
// actual callable.
const parameter = parameters[parameterIndex];
if (!parameter) {
/* c8 ignore next 2 */
return [];
}

const value = this.getEvaluatedCallable(it.value, substitutions);
if (!value) {
// We still have to remember that a value was passed, so the default value is not used
return [[parameter, UnknownEvaluatedNode]];
}

return [[parameter, value]];
}),
);
let result = callable.substitutionsOnCreation;

// Substitutions on call via default values
let result = new Map([...substitutionsOnCreation, ...substitutionsOnCall]);
for (const parameter of parameters) {
if (!result.has(parameter) && parameter.defaultValue) {
// Default values may depend on the values of previous parameters, so we have to evaluate them in order
if (argumentsByParameter.has(parameter)) {
// Substitutions on call via arguments
const value =
this.getEvaluatedCallable(argumentsByParameter.get(parameter), substitutions) ??
UnknownEvaluatedNode;

// Remember that a value was passed, so calls/callables in default values are not considered later
result = new Map([...result, [parameter, value]]);
} else if (parameter.defaultValue) {
// Substitutions on call via default values
const value = this.getEvaluatedCallable(parameter.defaultValue, result);
if (value) {
result = new Map([...result, [parameter, value]]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,22 +471,24 @@ export class SafeDsPythonGenerator {
}
}

const partiallyEvaluatedNode = this.partialEvaluator.evaluate(expression);
if (partiallyEvaluatedNode instanceof BooleanConstant) {
return traceToNode(expression)(partiallyEvaluatedNode.value ? 'True' : 'False');
} else if (partiallyEvaluatedNode instanceof IntConstant) {
return traceToNode(expression)(String(partiallyEvaluatedNode.value));
} else if (partiallyEvaluatedNode instanceof FloatConstant) {
const floatValue = partiallyEvaluatedNode.value;
return traceToNode(expression)(Number.isInteger(floatValue) ? `${floatValue}.0` : String(floatValue));
} else if (partiallyEvaluatedNode === NullConstant) {
return traceToNode(expression)('None');
} else if (partiallyEvaluatedNode instanceof StringConstant) {
return expandTracedToNode(expression)`'${this.formatStringSingleLine(partiallyEvaluatedNode.value)}'`;
if (!this.purityComputer.expressionHasSideEffects(expression)) {
const partiallyEvaluatedNode = this.partialEvaluator.evaluate(expression);
if (partiallyEvaluatedNode instanceof BooleanConstant) {
return traceToNode(expression)(partiallyEvaluatedNode.value ? 'True' : 'False');
} else if (partiallyEvaluatedNode instanceof IntConstant) {
return traceToNode(expression)(String(partiallyEvaluatedNode.value));
} else if (partiallyEvaluatedNode instanceof FloatConstant) {
const floatValue = partiallyEvaluatedNode.value;
return traceToNode(expression)(Number.isInteger(floatValue) ? `${floatValue}.0` : String(floatValue));
} else if (partiallyEvaluatedNode === NullConstant) {
return traceToNode(expression)('None');
} else if (partiallyEvaluatedNode instanceof StringConstant) {
return expandTracedToNode(expression)`'${this.formatStringSingleLine(partiallyEvaluatedNode.value)}'`;
}
}

// Handled after constant expressions: EnumVariant, List, Map
else if (isSdsTemplateString(expression)) {
if (isSdsTemplateString(expression)) {
return expandTracedToNode(expression)`f'${joinTracedToNode(expression, 'expressions')(
expression.expressions,
(expr) => this.generateExpression(expr, frame),
Expand Down
35 changes: 35 additions & 0 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 @@ -201,6 +201,41 @@ export class SafeDsNodeMapper {
})?.defaultValue;
}

/**
* Create a mapping from parameters to arguments. Parameters that are not mapped to an argument are not included in
* the result. Neither are arguments that are not mapped to a parameter. If multiple arguments are mapped to the
* same parameter, the first one wins.
*
* @param parameters The parameters to map to arguments.
* @param args The arguments.
*/
parametersToArguments(parameters: SdsParameter[], args: SdsArgument[]): Map<SdsParameter, SdsArgument> {
const result = new Map<SdsParameter, SdsArgument>();

for (const argument of args) {
const parameterIndex = this.argumentToParameter(argument)?.$containerIndex ?? -1;
if (parameterIndex === -1) {
continue;
}

/*
* argumentToParameter returns parameters of callable types. We have to remap this to parameter of the
* actual callable.
*/
const parameter = parameters[parameterIndex];
if (!parameter) {
continue;
}

// The first occurrence wins
if (!result.has(parameter)) {
result.set(parameter, argument);
}
}

return result;
}

/**
* Returns all references that target the given parameter.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isSdsTypeArgument,
isSdsTypeParameter,
isSdsTypeParameterConstraint,
isSdsYield,
} from '../generated/ast.js';
import { SafeDsServices } from '../safe-ds-module.js';

Expand Down Expand Up @@ -113,6 +114,13 @@ export class SafeDsSemanticTokenProvider extends AbstractSemanticTokenProvider {
property: 'leftOperand',
type: SemanticTokenTypes.typeParameter,
});
} else if (isSdsYield(node)) {
// For lack of a better option, we use the token type for parameters here
acceptor({
node,
property: 'result',
type: SemanticTokenTypes.parameter,
});
}
}

Expand Down
Loading

0 comments on commit 7643794

Please sign in to comment.