Skip to content

Fix tuple-to-tuple implicit conversion compatibility check#1283

Merged
ewoutkramer merged 13 commits into
developfrom
copilot/fix-equivalence-check-conversion
Jul 13, 2026
Merged

Fix tuple-to-tuple implicit conversion compatibility check#1283
ewoutkramer merged 13 commits into
developfrom
copilot/fix-equivalence-check-conversion

Conversation

Copilot AI commented May 28, 2026

Copy link
Copy Markdown
Contributor
  • Create new branch with the last two commits
  • Push branch
  • Open new PR

When generic type inference for operators like Equivalent(T, T) produces
conflicting inferences from direct argument types, the system now also
tries implicit conversion targets of each argument as potential type
bindings. This allows FHIR.CodeableConcept ~ Code to resolve by finding
the common type Concept (via FHIR.CodeableConcept→Concept model
conversion and Code→Concept implicit conversion).
Copilot AI changed the title [WIP] Fix equivalence check conversion to ELM Fix Equivalent resolution for CodeableConcept ~ Code May 28, 2026
@alexzautke
alexzautke marked this pull request as ready for review May 28, 2026 19:48
Copilot AI review requested due to automatic review settings May 28, 2026 19:48
@alexzautke
alexzautke requested a review from ewoutkramer May 28, 2026 19:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes #788, where Equivalent(FHIR.CodeableConcept, Code) (and similar generic operator resolutions) failed because InvocationBuilder.MatchSignature only tried each argument's exact type as a generic binding. When neither argument type was directly compatible with the other, but both had implicit conversions to a common type (e.g. Concept), resolution gave up. The fix adds a fallback step that also tries each argument's implicit conversion targets — both system-level (Code → Concept, numeric widenings, Date → DateTime) and model-level (FHIRHelpers conversions) — as candidate generic bindings.

Changes:

  • Add CoercionProvider.GetImplicitConversionTargets enumerating system- and model-level implicit conversion targets for a type.
  • Extend InvocationBuilder.MatchSignature with a fallback inference pass over those targets when direct inference yields no compatible binding.
  • Un-ignore the FhirCodeableConcept_EquivalentTo_Code regression test now that the scenario resolves.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
Cql/Cql.CqlToElm/CoercionProvider.cs Adds GetImplicitConversionTargets to enumerate implicit conversion targets for use in generic inference.
Cql/Cql.CqlToElm/InvocationBuilder.cs Adds a fallback in MatchSignature that uses implicit conversion targets as candidate generic bindings.
Cql/CqlToElmTests/(tests)/EquivalentTest.cs Removes [Ignore] from FhirCodeableConcept_EquivalentTo_Code to reactivate the previously failing test.

Comment thread Cql/Cql.CqlToElm/CoercionProvider.cs Outdated
Comment thread Cql/Cql.CqlToElm/InvocationBuilder.cs Outdated
@alexzautke

Copy link
Copy Markdown
Member

@copilot Apply all review comments

…version, handle List/Interval, drop ParameterTypeSpecifier guard

Copilot AI commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Apply all review comments

Applied both review comments in commit 7a3b... (see latest push):

  1. GetImplicitConversionTargets sync — Added the missing ValueSet → List<Code> conversion, and added structural propagation so List<X> yields List<Y> (and Interval<X> yields Interval<Y>) for each target Y of X. This keeps the method fully consistent with HasImplicitConversion.

  2. Removed ParameterTypeSpecifier guard — Dropped the if (operandType is not ParameterTypeSpecifier) continue check so the fallback now applies to all operand types (including List<T>, Interval<T>, etc.), delegating position recognition to InferGenericArgument as the direct-inference loop does.

All 2420 CqlToElmTests still pass.

@ewoutkramer

Copy link
Copy Markdown
Member

@copilot - This is duplication of existing functionality, i.e. we have now added a function for implicit conversions for simple types that already existed (internal bool HasImplicitConversion(TypeSpecifier from, TypeSpecifier to)) - the case that this PR tries to fix should already have been supported, so instead of adding new code, we should focus on fixing the bug and finding why the existing code does not handle this.

Copilot AI commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot - This is duplication of existing functionality, i.e. we have now added a function for implicit conversions for simple types that already existed (internal bool HasImplicitConversion(TypeSpec...

The duplication is now removed. The system-level conversion list lives in a single private static GetSystemImplicitConversionTargets method. HasImplicitConversion now delegates to it (replacing the explicit if-else chain), and GetImplicitConversionTargets also iterates it, so both methods share a single source of truth.

Regarding "the case should already have been supported": the issue is structural — HasImplicitConversion(from, to) is a binary check that requires knowing both sides, but generic inference must first determine what type to check against before HasImplicitConversion can be useful. In MatchSignature, when direct inference binds T = FHIR.CodeableConcept (from arg0), coercing Code to FHIR.CodeableConcept is incompatible; and T = Code (from arg1) makes FHIR.CodeableConcept → Code incompatible. Neither HasImplicitConversion call can help because the wrong target types are being tried. The fallback enumerates conversion targets as candidate bindings for T, which is what allows T = Concept to be discovered and HasImplicitConversion to then confirm both conversions. Happy to discuss further if there's a different approach in mind.

@ewoutkramer

Copy link
Copy Markdown
Member

@copilot This is a smart solution, but I think it is uncommon in most programming languages that we need to bind generic arguments to a coercable type (so not polymorphic, but through conversion operators) to resolve an overload. Do we have any proof that CQL requires this kind of binding?

Copilot AI commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot This is a smart solution, but I think it is uncommon in most programming languages that we need to bind generic arguments to a coercable type (so not polymorphic, but through conversion opera...

The CQL specification doesn't define the generic type inference algorithm explicitly, so there's no direct "proof" text. However, the spec does support this interpretation:

From the Developer's Guide (§ Function Resolution):

"Function resolution proceeds by attempting to match the signature of the invocation... Because the CQL type system supports subtyping, generics, and implicit conversion and casting, it is possible for an invocation signature to match multiple defined signatures."

This explicitly lists generics alongside implicit conversion as mechanisms that allow invocation signatures to match. For ~<T>(left T, right T) called with (FHIR.CodeableConcept, Code), binding T = Concept (so that both arguments match via model-level and system-level implicit conversions respectively) is consistent with that description — both conversions are valid per the spec's own implicit-conversion table and FHIRHelpers.

The alternative interpretation (bind T only to a directly-supplied argument type) would make ~ effectively unusable with FHIR model types that must go through FHIRHelpers conversions to reach a CQL-native type. The spec doesn't state or imply that restriction.

That said, the spec is silent on the exact algorithm, so you're right that this is an interpretation rather than a guaranteed requirement. If you have a preference for a different approach (for example, requiring the user to write an explicit conversion like O.code.toConcept() ~ ToConcept(myCode)), happy to explore that direction instead.

Copilot AI changed the title Fix Equivalent resolution for CodeableConcept ~ Code Fix tuple-to-tuple implicit conversion compatibility check Jun 3, 2026

@alexzautke alexzautke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Add a dedicated test that covers the Concept vs CodeableConcept mismatch, preferably as a Library test based on CQL

Copilot AI commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Add a dedicated test that covers the Concept vs CodeableConcept mismatch, preferably as a Library test based on CQL

Added a dedicated CQL Library-based test FhirCodeableConcept_EquivalentTo_Concept_ResolvesToEquivalent in EquivalentTest.cs. It builds a library comparing a FHIR.CodeableConcept with a Concept via ~, and asserts the AreEquivalent definition resolves to an Equivalent expression (not an unresolved-operator error), covering the Concept vs CodeableConcept type mismatch end-to-end.

@ewoutkramer

Copy link
Copy Markdown
Member

@alexzautke - is there anything more I need to do on this PR?

@alexzautke

Copy link
Copy Markdown
Member

@ewoutkramer No, I will take this up either during DevDays with Bryn or at the next Connectathon

@ewoutkramer
ewoutkramer removed their request for review July 7, 2026 12:15
@ewoutkramer

Copy link
Copy Markdown
Member

Unassigned myself, since I don't think there is a concrete action for me to do here at thim moment.

Copy link
Copy Markdown
Member

Some evidence from the dqm-content-qicore-2025 measure corpus arguing in favor of landing this PR:

The pattern this fixes is pervasive in the CMS measure CQL. QICoreCommon.cql compares a FHIR.CodeableConcept to a declared Code with ~ twenty-six times (condition.clinicalStatus ~ "active" in isActive, prevalenceInterval, and friends), and the same pattern appears 117 times across 30 CQL files in the corpus (clinicalStatus ~, verificationStatus ~, ...). Since isActive/prevalenceInterval gate nearly every condition-based population criterion, virtually every measure depends on it.

The reference translator resolves it exactly the way this PR proposes. In the shipped ELM, clinicalStatus ~ "active" is translated as Equivalent(FHIRHelpers.ToConcept(clinicalStatus), ToConcept(activeCode)) — neither operand converts to the other; both are implicitly converted to a common third type (Concept). That is precisely the signature-matching capability GetImplicitConversionTargets adds.

The internal translator fails on it today. Running the exact QICoreCommon construct through the current CqlToElm front-end (develop):

codesystem "ConditionClinicalStatusCodes": 'http://terminology.hl7.org/CodeSystem/condition-clinical'
code "active": 'active' from "ConditionClinicalStatusCodes"

define fluent function isActive(condition Condition):
  condition.clinicalStatus ~ "active"

fails with:

Could not resolve call to operator Equivalent with signature ({http://hl7.org/fhir}CodeableConcept, Code).

— the error this PR's un-ignored test (FhirCodeableConcept_EquivalentTo_Code, #788) targets.

Bottom line: the CMS measures only work today because they arrive as pre-translated reference ELM. Translating the dqm-content-qicore-2025 CQL sources with the SDK's own front-end (e.g. PackagerCLI cql instead of elm) is impossible without this capability: QICoreCommon itself does not translate, which takes the whole corpus with it.

Related: #1355 fixes the back-end/runtime half of the same theme (performing the implicit conversions the ELM implies, inside tuple unions). The two PRs touch disjoint projects and compose: once the front-end starts producing operands unified via implicit conversion targets, the back-end paths fixed there handle them. One observation worth carrying over: compatibility checks should be directional — AreCompatibleForUnionOperation in the back-end used a bidirectional CanConvert, which let code compile whose actually-needed conversion direction was missing. The directional GetImplicitConversionTargets here avoids that pitfall.


Generated by Claude Code

@ewoutkramer
ewoutkramer enabled auto-merge July 13, 2026 12:06
@ewoutkramer
ewoutkramer merged commit 0073eab into develop Jul 13, 2026
7 checks passed
@ewoutkramer
ewoutkramer deleted the copilot/fix-equivalence-check-conversion branch July 13, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Equivalence check between FHIR.CodeableConcept and Cql.Code is not properly converted to ELM

5 participants