Skip to content

Scopes and Resolution

Ewout Kramer edited this page Nov 10, 2023 · 3 revisions

Usings, includes and Definitions

Let us first look at introducing new identifiers in the scope, this can be done by a using, an include, or any other definition/function. These identifiers, as one would expect, need to be unique:

using FHIR version '4.0.1'

// Error: Identifier FHIR is already in use in this library
include cq_core.FHIRHelpers version '4.0.1' called FHIR

So, the name of the model library and a function library may not clash. This keeps the alias unique when using it to qualify datatypes or a function call from another library.

By the way, trying to assign an alias to the model in the using statement does not work in our version of the Java tools:

// complains about extraneous input, does not seem to be recognized by ANTLR.
using FHIR version '4.0.1' called FHIR

// Error: Local identifiers for models must be the same as the name of the model in
// this release of the translator (Model FHIR, Called FHIRx)
using FHIR version '4.0.1' called FHIRx

The namespace for the model and the libraries is shared with the definitions, so they must be unique all over the library:

using FHIR version '4.0.1'
include cq_core.FHIRHelpers version '4.0.1' called FHIRHelpers

// Error: Identifier FHIRHelpers is already in use in this library.
define FHIRHelpers: 'hi!'

// Error: Identifier FHIRHelpers is already in use in this library.
define FHIR: 5

// Error: Identifier System is already in use in this library. The identifier "System" is always already defined, so using that will give an error:

include cq_core.FHIRHelpers version '4.0.1' called System

define "System": 4

For functions, the name of the function does not need to be unique, this makes it possible to define overloads. In fact, it seems functions have their own namespace, so function names can overlap with anything, like definitions, valuesets etc. This means the following code compiles just fine:

using FHIR version '4.0.1'
include cq_core.FHIRHelpers version '4.0.1' called FHIRHelpers

define A: 'hello'

define function A(a Integer, b Integer): a+b
define function A(a String, b String): a+b
define function A(): 5
define function FHIRHelpers(): 4

Oddly, trying to use the identifier for a model does give an error:

// Error: Function FHIR is marked external but does not declare a return type.
define function FHIR: 'no way'

This is probably a bug.

Types

Types are not introduced in a library, but are defined in an external, referenced model. They are given a separate namespace, so the next fragment is totally valid:

// Fine, even though this exists as a type in model FHIR
define Patient: 4
define X: Patient + 5

// Also still fine, since this is the Patient from FHIR, not the
// defined expression above.
define Y: null as Patient

Ultimately, this (correctly) lets us mix identifiers from types and expressions:

// Expression of type 'System.Integer' cannot be cast as a value of
// type 'Patient'.
define Patient: 4
define x: Patient as Patient

Also, when an expression is expected, one cannot use a type:

// Error: Could not resolve identifier Organization in the current library.
define Z: Organization

Assuming types have their own resolution space, let's turn this around and see what happens when we try to use definitions where types are expected:

define A: 4

// Error: class org.hl7.elm.r1.Null cannot be cast to class 
// org.hl7.elm.r1.TypeSpecifier (org.hl7.elm.r1.Null and
// org.hl7.elm.r1.TypeSpecifier are in unnamed module of loader 
// com.google.cloud.functions.invoker.runner.Invoker$FunctionClassLoader
// @3b22cdd0)
define B: null as A
define C: null as FHIRHelpers
define D: null as FHIRHelpers.ToString
define E: null as FHIR

Referring to definitions where a type is expected raises an exception, which is neatly caught and presented as an error. We may safely assume that this signals that we have used a non-type identifier where a type was epxected.

Expressions

We can introduce definitions in the library, and also refer to definitions in other libraries.

What happens if we refer to non-definition things where an expression would be expected?

using FHIR version '4.0.1'
include cq_core.FHIRHelpers version '4.0.1' called FHIRHelpers

// Error: FHIRHelpers is a library and cannot be used as an expression.
define X: FHIRHelpers

// Error: Could not resolve identifier FHIR in the current library.
define Y: FHIR

// Error: Member Patient not found for type null.
define Z: FHIR.Patient

// Error: Could not resolve identifier ToString in library FHIRHelpers.
define Q: FHIRHelpers.ToString

As we can see, the errors sometimes are more specific than just saying an identifier cannot be found (like noticing an identifier is a library type), and even trigger a bug. In the last expression, we use the identifier of a function in the place of an expression. Here, the error does not mention we are dealing with a function, but just uses the generic "identifier" moniker.

As could be expected, introducing a query introduces another syntactical scope:

define X: 4
define Y: 5

// No problem, query introduces a new scope
define S: 
 ({}) X
 let Y: X
 return Y

The scope starts after the query alias, which is (of course) still part of the parent scope.

The compiler correctly catches a recursive call:

// Cannot resolve reference to expression or function S because it results 
// in a circular reference.
define S: S

It is also allowed to reference a function or expression that has not been defined yet:

define function A(): X()
define function C(): Y
define E: X()
define F: Y

define function X(): 6
define Y: 8

Contexts

It gets more subtle with context, as context refers to a type, introduce a name of a context, but also "effectively" introduce a function with the same name as the context into the namespace:

context Patient

// Overrides the default "Patient" function that would otherwise be generated
// for the context
define Patient: 4

So, even though the context Patient would normally introduce both a ContextDef and a ExpressionDef (called Patient), the latter would be replaced by the definition for Patient given later in the file.

Contexts should be types (even non-resource types work), so trying to resolve a context with anything else fails:

// Error:  Could not resolve context name FHIR in model FHIR.
context FHIR

// Error: Could not resolve context name FHIRHelpers in model FHIR.
context FHIRHelpers

context Patient

define Y: Patient

// Error: Could not resolve context name Y in model FHIR.
context Y

It might be a detail, but the error calls out "context name", not "identifier", when resolution fails, which it will do for all other resolution errors:

/// Error: Could not resolve identifier Y in the current library.
define X: Y

Though these different phrasing of errors suggest contexts have "names" (which are types), the "name" still needs to be unique in the scope:

using FHIR version '4.0.1'

// Let us give FHIRHelpers a silly name
include cq_core.FHIRHelpers version '4.0.1' called Patient

// Error: Identifier Patient is already in use in this library
context Patient

As we see, the "name" "Patient" is now an identifier again, as the error does not use the term "context name" anymore here, but uses "identifier" instead.