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

Handle overloaded forward function declarations. #1213

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f175c83
Handle overloaded forward function declarations with a new dedicated …
lukedegruchy Sep 20, 2023
d797360
Throw a compiler exception if a FunctionDefinitionInfo is already in …
lukedegruchy Sep 20, 2023
c63e5c9
Rough implementation and tests for forward invocation implicit parame…
lukedegruchy Sep 26, 2023
9d81ca8
Further refinements to algorithm and enhanced test case.
lukedegruchy Sep 26, 2023
034b16b
Tweak algorithm further.
lukedegruchy Sep 27, 2023
a40c3c5
Reverse changes to ConversionMap and various tweaks.
lukedegruchy Sep 27, 2023
de40c15
More small tweaks.
lukedegruchy Sep 27, 2023
463b965
Merge branch 'master' into 1191-overloaded-function-declarations-fail…
JPercival Sep 28, 2023
286562b
Fixed master merge
JPercival Sep 28, 2023
9c99d2b
Fixed a spelling error, started fixing tests
JPercival Sep 28, 2023
d7267a8
Eliminate duplicate errors by caching results from the preprocessor a…
lukedegruchy Sep 29, 2023
7082827
Refactor existing solution.
lukedegruchy Sep 29, 2023
c42d950
Small fixes.
lukedegruchy Oct 2, 2023
808abb7
Merge branch 'master' into 1191-overloaded-function-declarations-fail…
JPercival Oct 3, 2023
63beb66
Merge remote-tracking branch 'origin/master' into 1191-overloaded-fun…
lukedegruchy Oct 4, 2023
5cbc410
Merge branch 'master' into 1191-overloaded-function-declarations-fail…
lukedegruchy Oct 4, 2023
535ea63
Merge remote-tracking branch 'origin/master' into 1191-overloaded-fun…
lukedegruchy Oct 6, 2023
dc4bcd1
Hack to call validateAmbiguousOverloadedForwardDeclarationsSignatureN…
lukedegruchy Oct 6, 2023
437479e
Merge branch 'master' into 1191-overloaded-function-declarations-fail…
JPercival Oct 11, 2023
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,7 @@ public Library run(CharStream is) {
parser.addErrorListener(errorListener);
ParseTree tree = parser.library();

CqlPreprocessorVisitor preprocessor = new CqlPreprocessorVisitor();
preprocessor.setTokenStream(tokens);
CqlPreprocessorVisitor preprocessor = new CqlPreprocessorVisitor(builder, tokens);
preprocessor.visit(tree);

visitor.setTokenStream(tokens);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.cqframework.cql.cql2elm;

import org.cqframework.cql.cql2elm.preprocessor.FunctionDefinitionInfo;

/**
* Result of each candidate function for forward invocation, including the implicit conversion scores, if applicable.
*/
class ForwardInvocationResult {
private final int[] scores;
private final FunctionDefinitionInfo functionDefinitionInfo;

public static ForwardInvocationResult noMatch(FunctionDefinitionInfo functionDefinitionInfo) {
return new ForwardInvocationResult(functionDefinitionInfo, Integer.MAX_VALUE);
}

public ForwardInvocationResult(FunctionDefinitionInfo functionDefinitionInfo, int... scores) {
this.functionDefinitionInfo = functionDefinitionInfo;
this.scores = scores;
}

public int[] getScores() {
return scores;
}

public boolean isNoMatch() {
return scores.length == 1 && Integer.MAX_VALUE == scores[0];
}

public FunctionDefinitionInfo getFunctionDefinitionInfo() {
return functionDefinitionInfo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package org.cqframework.cql.cql2elm;

import org.cqframework.cql.cql2elm.model.*;
import org.cqframework.cql.cql2elm.preprocessor.FunctionDefinitionInfo;
import org.cqframework.cql.elm.tracking.Trackable;
import org.hl7.cql.model.DataType;
import org.hl7.elm.r1.FunctionDef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
* Compares the function for which we want to resolve a forward reference with one of the candidates by leveraging preCompile/function headers.
* <p/>
* <ol>
* <li>Compare the {@link CallContext} of the function from the point of view of the calling code to the candidate function definition, which includes a list of {@link FunctionDefinitionInfo}s retrieved by function name.</li>
* <li>Compare the data types of the parameters for the calling and called functions.</li>
* <li>Take into account implicit conversions (ex FHIRHelpers) when the parameter lists don't match, including each conversion's score.</li>
* </ol>
*/
public class ForwardInvocationValidator {
private static final Logger logger = LoggerFactory.getLogger(ForwardInvocationValidator.class);

public static FunctionDefinitionInfo resolveOnSignature(CallContext callContextFromCaller, Iterable<FunctionDefinitionInfo> candidateFunctionDefinitions, ConversionMap conversionMap) {
if (candidateFunctionDefinitions != null) {
final List<DataType> paramTypesFromCaller = StreamSupport.stream(callContextFromCaller.getSignature().getOperandTypes().spliterator(), false)
.collect(Collectors.toList());;

final Map<DataType, List<Conversion>> implicitConversionsPerParamType = paramTypesFromCaller.stream()
.distinct()
.collect(Collectors.toMap(Function.identity(), entry -> conversionMap.getConversions(entry)
.stream()
.filter(Conversion::isImplicit)
.collect(Collectors.toList())));

final List<ForwardInvocationResult> resolvedFunctionDefinitionInfos = new ArrayList<>();

for (FunctionDefinitionInfo candidateFunctionDefinition : candidateFunctionDefinitions) {
final ForwardInvocationResult currentResult = scoreFunctionHeaderOrNothing(callContextFromCaller, candidateFunctionDefinition, implicitConversionsPerParamType);

evaluateCandidateParamScores(currentResult, resolvedFunctionDefinitionInfos);
}
if (resolvedFunctionDefinitionInfos.size() == 0) {
throw new CqlCompilerException("forward declaration resolution found NO functions for name:" + callContextFromCaller.getOperatorName());
}
if (resolvedFunctionDefinitionInfos.size() > 1) {
throw new CqlCompilerException("forward declaration resolution found more than one functions for name:" + callContextFromCaller.getOperatorName());
}
return resolvedFunctionDefinitionInfos.get(0).getFunctionDefinitionInfo();
}

return null;
}

private static void evaluateCandidateParamScores(ForwardInvocationResult currentResult, List<ForwardInvocationResult> previousForwardInvocationResults) {
if (currentResult.isNoMatch()) {
return;
}

if (previousForwardInvocationResults.isEmpty()) {
previousForwardInvocationResults.add(currentResult);
return;
}

if (previousForwardInvocationResults.stream()
.map(ForwardInvocationResult::getScores)
.anyMatch(scores -> scores.length != currentResult.getScores().length)) {
// Sanity check: This should never happen
return;
}

Boolean allScoresLessThanOrEqual = null;
Boolean isPrevMatchScoreLessThanOrEqual = null;

for (int index = 0; index < currentResult.getScores().length; index++) {
final int currentScore = currentResult.getScores()[index];

final List<int[]> previousScoresForPreviousResults = previousForwardInvocationResults.stream()
.map(ForwardInvocationResult::getScores)
.collect(Collectors.toList());

for (int[] previousScores : previousScoresForPreviousResults) {
final boolean isScoreLessThanOrEqual = currentScore <= previousScores[index];

if (allScoresLessThanOrEqual == null) {
allScoresLessThanOrEqual = isScoreLessThanOrEqual;
} else {
allScoresLessThanOrEqual = isScoreLessThanOrEqual && allScoresLessThanOrEqual;
}

if (isPrevMatchScoreLessThanOrEqual != null) {
// Previous candidate has scores of [4,5] but the current candidate has scores [5,4] so we cannot resolve
if (isPrevMatchScoreLessThanOrEqual != isScoreLessThanOrEqual) {
throw new CqlCompilerException("Cannot resolve forward declaration for function call:" + currentResult.getFunctionDefinitionInfo().getName());
}
}

isPrevMatchScoreLessThanOrEqual = isScoreLessThanOrEqual;
}
}

if (allScoresLessThanOrEqual != null && allScoresLessThanOrEqual.booleanValue()) {
previousForwardInvocationResults.clear();
previousForwardInvocationResults.add(currentResult);
}
}

public static ForwardInvocationResult scoreFunctionHeaderOrNothing(CallContext callContextFromCaller, FunctionDefinitionInfo candidateFunctionDefinition, Map<DataType, List<Conversion>> implicitConversionsPerParamType) {
final FunctionDef functionDefFromCandidate = candidateFunctionDefinition.getPreCompileOutput().getFunctionDef();

if (! callContextFromCaller.getOperatorName().equals(functionDefFromCandidate.getName())) {
return ForwardInvocationResult.noMatch(candidateFunctionDefinition);
}

final List<DataType> paramTypesFromCaller = StreamSupport.stream(callContextFromCaller.getSignature().getOperandTypes().spliterator(), false)
.collect(Collectors.toList());;

final List<DataType> paramTypesFromCandidate = functionDefFromCandidate.getOperand()
.stream()
.map(Trackable::getResultType)
.collect(Collectors.toList());

if (paramTypesFromCaller.size() != paramTypesFromCandidate.size()) {
return ForwardInvocationResult.noMatch(candidateFunctionDefinition);
}

final int[] scores = new int[paramTypesFromCaller.size()];

for (int index = 0; index < paramTypesFromCaller.size(); index++) {
final DataType dataTypeFromCaller = paramTypesFromCaller.get(index);
final DataType dataTypeFromCandidate = paramTypesFromCandidate.get(index);

final int score = compareEachMethodParam(dataTypeFromCaller, dataTypeFromCandidate, implicitConversionsPerParamType);

scores[index] = score;
}

return new ForwardInvocationResult(candidateFunctionDefinition, scores);
}

private static int compareEachMethodParam(DataType dataTypeFromCaller, DataType dataTypeFromCandidate, Map<DataType, List<Conversion>> implicitConversionsPerParamType) {
if (dataTypeFromCaller.isCompatibleWith(dataTypeFromCandidate)) {
return Integer.MIN_VALUE;
}

return handleImplicitConversion(dataTypeFromCaller, dataTypeFromCandidate, implicitConversionsPerParamType);
}

private static int handleImplicitConversion(DataType dataTypeFromCaller, DataType dataTypeFromCandidate, Map<DataType, List<Conversion>> implicitConversionsPerParamType) {
final List<Conversion> conversions = implicitConversionsPerParamType.get(dataTypeFromCaller);

final List<Conversion> conversionsMatchingToType = conversions
.stream()
.filter(conv -> conv.getToType().equals(dataTypeFromCandidate))
.collect(Collectors.toList());

if (conversionsMatchingToType.size() != 1) {
return Integer.MAX_VALUE;
}

return conversionsMatchingToType.get(0).getScore();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public List<CqlCompilerException> getExceptions() {
}

private final Map<String, Model> models = new LinkedHashMap<>();

private final Map<String, ResultWithPossibleError<NamedTypeSpecifier>> nameTypeSpecifiers = new HashMap<>();
private final Map<String, CompiledLibrary> libraries = new LinkedHashMap<>();
private final SystemFunctionResolver systemFunctionResolver = new SystemFunctionResolver(this);
private final Stack<String> expressionContext = new Stack<>();
Expand Down Expand Up @@ -263,6 +265,16 @@ public Model getModel(ModelIdentifier modelIdentifier, String localIdentifier) {
return model;
}

public ResultWithPossibleError<NamedTypeSpecifier> getNamedTypeSpecifierResult(String namedTypeSpecifierIdentifier) {
return nameTypeSpecifiers.get(namedTypeSpecifierIdentifier);
}

public void addNamedTypeSpecifierResult(String namedTypeSpecifierIdentifier, ResultWithPossibleError<NamedTypeSpecifier> namedTypeSpecifierResult) {
if (! nameTypeSpecifiers.containsKey(namedTypeSpecifierIdentifier)) {
nameTypeSpecifiers.put(namedTypeSpecifierIdentifier, namedTypeSpecifierResult);
}
}

private void loadConversionMap(Model model) {
for (Conversion conversion : model.getConversions()) {
conversionMap.add(conversion);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.cqframework.cql.cql2elm;

import org.hl7.elm.r1.FunctionDef;
import org.hl7.elm.r1.TypeSpecifier;

import java.util.Objects;
import java.util.StringJoiner;

/**
* POJO for the result of a pre compile operation (AKA: partial compile of function headers)
*/
public class PreCompileOutput {
private final FunctionDef functionDef;
private final TypeSpecifier resultType;

public FunctionDef getFunctionDef() {
return functionDef;
}

public TypeSpecifier getResultType() {
return resultType;
}

public static PreCompileOutput noReturnType(FunctionDef functionDef) {
return new PreCompileOutput(functionDef, null);
}

public static PreCompileOutput withReturnType(FunctionDef functionDef, TypeSpecifier resultType) {
return new PreCompileOutput(functionDef, resultType);
}

private PreCompileOutput(FunctionDef functionDef, TypeSpecifier resultType) {
this.functionDef = functionDef;
this.resultType = resultType;
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
PreCompileOutput that = (PreCompileOutput) other;
return Objects.equals(functionDef, that.functionDef) && Objects.equals(resultType, that.resultType);
}

@Override
public int hashCode() {
return Objects.hash(functionDef, resultType);
}

@Override
public String toString() {
return new StringJoiner(", ", PreCompileOutput.class.getSimpleName() + "[", "]")
.add("functionDef=" + functionDef)
.add("resultType=" + resultType)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.cqframework.cql.cql2elm;

/**
* Indicate either a populated result or the presence of an error that prevented the result from being created.
*/
public class ResultWithPossibleError<T> {
private final T underlyingThingOrNull;

public static <T> ResultWithPossibleError<T> withError() {
return new ResultWithPossibleError<>(null);
}

public static <T> ResultWithPossibleError<T> withTypeSpecifier(T underlyingThingOrNull) {
return new ResultWithPossibleError<>(underlyingThingOrNull);
}

private ResultWithPossibleError(T namedTypeSpecifierOrNull) {
this.underlyingThingOrNull = namedTypeSpecifierOrNull;
}

public boolean hasError() {
return (underlyingThingOrNull == null);
}

public T getUnderlyingResultIfExists() {
if (hasError()) {
throw new IllegalArgumentException("Should have called hasError() first");
}

return underlyingThingOrNull;
}
}
Loading
Loading