All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cqframework.cql.cql2elm.Cql2ElmVisitor Maven / Gradle / Ivy

Go to download

The cql-to-elm library for the Clinical Quality Language Java reference implementation

There is a newer version: 3.18.0
Show newest version
package org.cqframework.cql.cql2elm;

import java.math.BigDecimal;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.cqframework.cql.cql2elm.LibraryBuilder.IdentifierScope;
import org.cqframework.cql.cql2elm.model.*;
import org.cqframework.cql.cql2elm.model.invocation.*;
import org.cqframework.cql.cql2elm.preprocessor.*;
import org.cqframework.cql.elm.tracking.TrackBack;
import org.cqframework.cql.elm.tracking.Trackable;
import org.cqframework.cql.gen.cqlLexer;
import org.cqframework.cql.gen.cqlParser;
import org.hl7.cql.model.*;
import org.hl7.elm.r1.*;
import org.hl7.elm_modelinfo.r1.ModelInfo;

public class Cql2ElmVisitor extends CqlPreprocessorElmCommonVisitor {
    private final SystemMethodResolver systemMethodResolver;

    private final Set definedExpressionDefinitions = new HashSet<>();
    private final Stack forwards = new Stack<>();
    private final Map functionHeaders = new HashMap<>();

    private final Map functionHeadersByDef = new HashMap<>();
    private final Map functionDefinitions = new HashMap<>();
    private final Stack timingOperators = new Stack<>();
    private final List retrieves = new ArrayList<>();
    private final List expressions = new ArrayList<>();
    private final Map contextDefinitions = new HashMap<>();

    public Cql2ElmVisitor(LibraryBuilder libraryBuilder, TokenStream tokenStream, LibraryInfo libraryInfo) {
        super(libraryBuilder, tokenStream);
        this.libraryInfo = Objects.requireNonNull(libraryInfo, "libraryInfo required");
        this.systemMethodResolver = new SystemMethodResolver(this, libraryBuilder);
    }

    public List getRetrieves() {
        return retrieves;
    }

    public List getExpressions() {
        return expressions;
    }

    @Override
    public Object visitLibrary(cqlParser.LibraryContext ctx) {
        Object lastResult = null;

        // Loop through and call visit on each child (to ensure they are tracked)
        for (int i = 0; i < ctx.getChildCount(); i++) {
            ParseTree tree = ctx.getChild(i);
            TerminalNode terminalNode = tree instanceof TerminalNode ? (TerminalNode) tree : null;
            if (terminalNode != null && terminalNode.getSymbol().getType() == cqlLexer.EOF) {
                continue;
            }

            Object childResult = visit(tree);
            // Only set the last result if we received something useful
            if (childResult != null) {
                lastResult = childResult;
            }
        }

        // Return last result (consistent with super implementation and helps w/
        // testing)
        return lastResult;
    }

    @Override
    @SuppressWarnings("unchecked")
    public VersionedIdentifier visitLibraryDefinition(cqlParser.LibraryDefinitionContext ctx) {
        List identifiers = (List) visit(ctx.qualifiedIdentifier());
        VersionedIdentifier vid = of.createVersionedIdentifier()
                .withId(identifiers.remove(identifiers.size() - 1))
                .withVersion(parseString(ctx.versionSpecifier()));
        if (!identifiers.isEmpty()) {
            vid.setSystem(libraryBuilder.resolveNamespaceUri(String.join(".", identifiers), true));
        } else if (libraryBuilder.getNamespaceInfo() != null) {
            vid.setSystem(libraryBuilder.getNamespaceInfo().getUri());
        }
        libraryBuilder.setLibraryIdentifier(vid);

        return vid;
    }

    @Override
    @SuppressWarnings("unchecked")
    public UsingDef visitUsingDefinition(cqlParser.UsingDefinitionContext ctx) {
        List identifiers = (List) visit(ctx.qualifiedIdentifier());
        String unqualifiedIdentifier = identifiers.remove(identifiers.size() - 1);
        String namespaceName = !identifiers.isEmpty()
                ? String.join(".", identifiers)
                : libraryBuilder.isWellKnownModelName(unqualifiedIdentifier)
                        ? null
                        : (libraryBuilder.getNamespaceInfo() != null
                                ? libraryBuilder.getNamespaceInfo().getName()
                                : null);

        String path = null;
        NamespaceInfo modelNamespace = null;
        if (namespaceName != null) {
            String namespaceUri = libraryBuilder.resolveNamespaceUri(namespaceName, true);
            path = NamespaceManager.getPath(namespaceUri, unqualifiedIdentifier);
            modelNamespace = new NamespaceInfo(namespaceName, namespaceUri);
        } else {
            path = unqualifiedIdentifier;
        }

        String localIdentifier =
                ctx.localIdentifier() == null ? unqualifiedIdentifier : parseString(ctx.localIdentifier());
        if (!localIdentifier.equals(unqualifiedIdentifier)) {
            throw new IllegalArgumentException(String.format(
                    "Local identifiers for models must be the same as the name of the model in this release of the translator (Model %s, Called %s)",
                    unqualifiedIdentifier, localIdentifier));
        }

        // The model was already calculated by CqlPreprocessorVisitor
        final UsingDef usingDef = libraryBuilder.resolveUsingRef(localIdentifier);
        libraryBuilder.pushIdentifier(localIdentifier, usingDef, IdentifierScope.GLOBAL);
        return usingDef;
    }

    public Model getModel() {
        return getModel((String) null);
    }

    public Model getModel(String modelName) {
        return getModel(null, modelName, null, null);
    }

    public Model getModel(NamespaceInfo modelNamespace, String modelName, String version, String localIdentifier) {
        if (modelName == null) {
            var defaultUsing = libraryInfo.getDefaultUsingDefinition();
            modelName = defaultUsing.getName();
            version = defaultUsing.getVersion();
        }

        var modelIdentifier = new ModelIdentifier().withId(modelName).withVersion(version);
        if (modelNamespace != null) {
            modelIdentifier.setSystem(modelNamespace.getUri());
        }
        return libraryBuilder.getModel(modelIdentifier, localIdentifier);
    }

    private String getLibraryPath(String namespaceName, String unqualifiedIdentifier) {
        if (namespaceName != null) {
            String namespaceUri = libraryBuilder.resolveNamespaceUri(namespaceName, true);
            return NamespaceManager.getPath(namespaceUri, unqualifiedIdentifier);
        }

        return unqualifiedIdentifier;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object visitIncludeDefinition(cqlParser.IncludeDefinitionContext ctx) {
        List identifiers = (List) visit(ctx.qualifiedIdentifier());
        String unqualifiedIdentifier = identifiers.remove(identifiers.size() - 1);
        String namespaceName = !identifiers.isEmpty()
                ? String.join(".", identifiers)
                : (libraryBuilder.getNamespaceInfo() != null
                        ? libraryBuilder.getNamespaceInfo().getName()
                        : null);
        String path = getLibraryPath(namespaceName, unqualifiedIdentifier);
        IncludeDef library = of.createIncludeDef()
                .withLocalIdentifier(
                        ctx.localIdentifier() == null ? unqualifiedIdentifier : parseString(ctx.localIdentifier()))
                .withPath(path)
                .withVersion(parseString(ctx.versionSpecifier()));

        // TODO: This isn't great because it complicates the loading process (and
        // results in the source being loaded
        // twice in the general case)
        // But the full fix is to introduce source resolution/caching to enable this
        // layer to determine whether the
        // library identifier resolved
        // with the namespace
        if (!libraryBuilder.canResolveLibrary(library)) {
            namespaceName = identifiers.size() > 0
                    ? String.join(".", identifiers)
                    : libraryBuilder.isWellKnownLibraryName(unqualifiedIdentifier)
                            ? null
                            : (libraryBuilder.getNamespaceInfo() != null
                                    ? libraryBuilder.getNamespaceInfo().getName()
                                    : null);
            path = getLibraryPath(namespaceName, unqualifiedIdentifier);
            library = of.createIncludeDef()
                    .withLocalIdentifier(
                            ctx.localIdentifier() == null ? unqualifiedIdentifier : parseString(ctx.localIdentifier()))
                    .withPath(path)
                    .withVersion(parseString(ctx.versionSpecifier()));
        }

        libraryBuilder.addInclude(library);
        libraryBuilder.pushIdentifier(library.getLocalIdentifier(), library, IdentifierScope.GLOBAL);

        return library;
    }

    @Override
    public ParameterDef visitParameterDefinition(cqlParser.ParameterDefinitionContext ctx) {
        ParameterDef param = of.createParameterDef()
                .withAccessLevel(parseAccessModifier(ctx.accessModifier()))
                .withName(parseString(ctx.identifier()))
                .withDefault(parseLiteralExpression(ctx.expression()))
                .withParameterTypeSpecifier(parseTypeSpecifier(ctx.typeSpecifier()));

        DataType paramType = null;
        if (param.getParameterTypeSpecifier() != null) {
            paramType = param.getParameterTypeSpecifier().getResultType();
        }

        if (param.getDefault() != null) {
            if (paramType != null) {
                libraryBuilder.verifyType(param.getDefault().getResultType(), paramType);
            } else {
                paramType = param.getDefault().getResultType();
            }
        }

        if (paramType == null) {
            throw new IllegalArgumentException(
                    String.format("Could not determine parameter type for parameter %s.", param.getName()));
        }

        param.setResultType(paramType);
        if (param.getDefault() != null) {
            param.setDefault(libraryBuilder.ensureCompatible(param.getDefault(), paramType));
        }

        libraryBuilder.addParameter(param);
        libraryBuilder.pushIdentifier(param.getName(), param, IdentifierScope.GLOBAL);

        return param;
    }

    @Override
    public TupleElementDefinition visitTupleElementDefinition(cqlParser.TupleElementDefinitionContext ctx) {
        TupleElementDefinition result = of.createTupleElementDefinition()
                .withName(parseString(ctx.referentialIdentifier()))
                .withElementType(parseTypeSpecifier(ctx.typeSpecifier()));

        if (getIncludeDeprecatedElements()) {
            result.setType(result.getElementType());
        }

        return result;
    }

    @Override
    public AccessModifier visitAccessModifier(cqlParser.AccessModifierContext ctx) {
        switch (ctx.getText().toLowerCase()) {
            case "public":
                return AccessModifier.PUBLIC;
            case "private":
                return AccessModifier.PRIVATE;
            default:
                throw new IllegalArgumentException(String.format(
                        "Unknown access modifier %s.", ctx.getText().toLowerCase()));
        }
    }

    @Override
    public CodeSystemDef visitCodesystemDefinition(cqlParser.CodesystemDefinitionContext ctx) {
        CodeSystemDef cs = (CodeSystemDef) of.createCodeSystemDef()
                .withAccessLevel(parseAccessModifier(ctx.accessModifier()))
                .withName(parseString(ctx.identifier()))
                .withId(parseString(ctx.codesystemId()))
                .withVersion(parseString(ctx.versionSpecifier()));

        if (libraryBuilder.isCompatibleWith("1.5")) {
            cs.setResultType(libraryBuilder.resolveTypeName("System", "CodeSystem"));
        } else {
            cs.setResultType(new ListType(libraryBuilder.resolveTypeName("System", "Code")));
        }

        libraryBuilder.addCodeSystem(cs);
        libraryBuilder.pushIdentifier(cs.getName(), cs, IdentifierScope.GLOBAL);
        return cs;
    }

    @Override
    public CodeSystemRef visitCodesystemIdentifier(cqlParser.CodesystemIdentifierContext ctx) {
        String libraryName = parseString(ctx.libraryIdentifier());
        String name = parseString(ctx.identifier());
        CodeSystemDef def;
        if (libraryName != null) {
            def = libraryBuilder.resolveLibrary(libraryName).resolveCodeSystemRef(name);
            libraryBuilder.checkAccessLevel(libraryName, name, def.getAccessLevel());
        } else {
            def = libraryBuilder.resolveCodeSystemRef(name);
        }

        if (def == null) {
            // ERROR:
            throw new IllegalArgumentException(String.format("Could not resolve reference to code system %s.", name));
        }

        return (CodeSystemRef) of.createCodeSystemRef()
                .withLibraryName(libraryName)
                .withName(name)
                .withResultType(def.getResultType());
    }

    @Override
    public CodeRef visitCodeIdentifier(cqlParser.CodeIdentifierContext ctx) {
        String libraryName = parseString(ctx.libraryIdentifier());
        String name = parseString(ctx.identifier());
        CodeDef def;
        if (libraryName != null) {
            def = libraryBuilder.resolveLibrary(libraryName).resolveCodeRef(name);
            libraryBuilder.checkAccessLevel(libraryName, name, def.getAccessLevel());
        } else {
            def = libraryBuilder.resolveCodeRef(name);
        }

        if (def == null) {
            // ERROR:
            throw new IllegalArgumentException(String.format("Could not resolve reference to code %s.", name));
        }

        return (CodeRef)
                of.createCodeRef().withLibraryName(libraryName).withName(name).withResultType(def.getResultType());
    }

    @Override
    public ValueSetDef visitValuesetDefinition(cqlParser.ValuesetDefinitionContext ctx) {
        ValueSetDef vs = of.createValueSetDef()
                .withAccessLevel(parseAccessModifier(ctx.accessModifier()))
                .withName(parseString(ctx.identifier()))
                .withId(parseString(ctx.valuesetId()))
                .withVersion(parseString(ctx.versionSpecifier()));

        if (ctx.codesystems() != null) {
            for (cqlParser.CodesystemIdentifierContext codesystem :
                    ctx.codesystems().codesystemIdentifier()) {
                var cs = (CodeSystemRef) visit(codesystem);
                if (cs == null) {
                    throw new IllegalArgumentException(
                            String.format("Could not resolve reference to code system %s.", codesystem.getText()));
                }

                vs.getCodeSystem().add(cs);
            }
        }
        if (libraryBuilder.isCompatibleWith("1.5")) {
            vs.setResultType(libraryBuilder.resolveTypeName("System", "ValueSet"));
        } else {
            vs.setResultType(new ListType(libraryBuilder.resolveTypeName("System", "Code")));
        }
        libraryBuilder.addValueSet(vs);
        libraryBuilder.pushIdentifier(vs.getName(), vs, IdentifierScope.GLOBAL);

        return vs;
    }

    @Override
    public CodeDef visitCodeDefinition(cqlParser.CodeDefinitionContext ctx) {
        CodeDef cd = of.createCodeDef()
                .withAccessLevel(parseAccessModifier(ctx.accessModifier()))
                .withName(parseString(ctx.identifier()))
                .withId(parseString(ctx.codeId()));

        if (ctx.codesystemIdentifier() != null) {
            cd.setCodeSystem((CodeSystemRef) visit(ctx.codesystemIdentifier()));
        }

        if (ctx.displayClause() != null) {
            cd.setDisplay(parseString(ctx.displayClause().STRING()));
        }

        cd.setResultType(libraryBuilder.resolveTypeName("Code"));
        libraryBuilder.addCode(cd);
        libraryBuilder.pushIdentifier(cd.getName(), cd, IdentifierScope.GLOBAL);

        return cd;
    }

    @Override
    public ConceptDef visitConceptDefinition(cqlParser.ConceptDefinitionContext ctx) {
        ConceptDef cd = of.createConceptDef()
                .withAccessLevel(parseAccessModifier(ctx.accessModifier()))
                .withName(parseString(ctx.identifier()));

        if (ctx.codeIdentifier() != null) {
            for (cqlParser.CodeIdentifierContext ci : ctx.codeIdentifier()) {
                cd.getCode().add((CodeRef) visit(ci));
            }
        }

        if (ctx.displayClause() != null) {
            cd.setDisplay(parseString(ctx.displayClause().STRING()));
        }

        cd.setResultType(libraryBuilder.resolveTypeName("Concept"));
        libraryBuilder.addConcept(cd);

        return cd;
    }

    @Override
    public NamedTypeSpecifier visitNamedTypeSpecifier(cqlParser.NamedTypeSpecifierContext ctx) {
        List qualifiers = parseQualifiers(ctx);
        String modelIdentifier = getModelIdentifier(qualifiers);
        String identifier = getTypeIdentifier(qualifiers, parseString(ctx.referentialOrTypeNameIdentifier()));

        final ResultWithPossibleError retrievedResult =
                libraryBuilder.getNamedTypeSpecifierResult(String.format("%s:%s", modelIdentifier, identifier));

        if (retrievedResult != null) {
            if (retrievedResult.hasError()) {
                return null;
            }
            return retrievedResult.getUnderlyingResultIfExists();
        }

        DataType resultType = libraryBuilder.resolveTypeName(modelIdentifier, identifier);
        if (null == resultType) {
            throw new CqlCompilerException(
                    String.format("Could not find type for model: %s and name: %s", modelIdentifier, identifier),
                    getTrackBack(ctx));
        }
        NamedTypeSpecifier result = of.createNamedTypeSpecifier().withName(libraryBuilder.dataTypeToQName(resultType));

        // Fluent API would be nice here, but resultType isn't part of the model so...
        result.setResultType(resultType);

        return result;
    }

    private boolean isUnfilteredContext(String contextName) {
        return contextName.equals("Unfiltered")
                || (libraryBuilder.isCompatibilityLevel3() && contextName.equals("Population"));
    }

    @Override
    public Object visitContextDefinition(cqlParser.ContextDefinitionContext ctx) {
        String modelIdentifier = parseString(ctx.modelIdentifier());
        String unqualifiedIdentifier = parseString(ctx.identifier());

        setCurrentContext(
                modelIdentifier != null ? modelIdentifier + "." + unqualifiedIdentifier : unqualifiedIdentifier);

        if (!isUnfilteredContext(unqualifiedIdentifier)) {
            ModelContext modelContext = libraryBuilder.resolveContextName(modelIdentifier, unqualifiedIdentifier);

            // If this is the first time a context definition is encountered, construct a
            // context definition:
            // define  = element of []
            Element modelContextDefinition = contextDefinitions.get(modelContext.getName());
            if (modelContextDefinition == null) {
                if (libraryBuilder.hasUsings()) {
                    ModelInfo modelInfo = modelIdentifier == null
                            ? libraryBuilder
                                    .getModel(libraryInfo.getDefaultModelName())
                                    .getModelInfo()
                            : libraryBuilder.getModel(modelIdentifier).getModelInfo();
                    // String contextTypeName = modelContext.getName();
                    // DataType contextType = libraryBuilder.resolveTypeName(modelInfo.getName(),
                    // contextTypeName);
                    DataType contextType = modelContext.getType();
                    modelContextDefinition = libraryBuilder.resolveParameterRef(modelContext.getName());
                    if (modelContextDefinition != null) {
                        contextDefinitions.put(modelContext.getName(), modelContextDefinition);
                    } else {
                        Retrieve contextRetrieve =
                                of.createRetrieve().withDataType(libraryBuilder.dataTypeToQName(contextType));
                        track(contextRetrieve, ctx);
                        contextRetrieve.setResultType(new ListType(contextType));
                        String contextClassIdentifier = ((ClassType) contextType).getIdentifier();
                        if (contextClassIdentifier != null) {
                            contextRetrieve.setTemplateId(contextClassIdentifier);
                        }

                        modelContextDefinition = of.createExpressionDef()
                                .withName(unqualifiedIdentifier)
                                .withContext(getCurrentContext())
                                .withExpression(of.createSingletonFrom().withOperand(contextRetrieve));
                        track(modelContextDefinition, ctx);
                        ((ExpressionDef) modelContextDefinition).getExpression().setResultType(contextType);
                        modelContextDefinition.setResultType(contextType);
                        libraryBuilder.addExpression((ExpressionDef) modelContextDefinition);
                        contextDefinitions.put(modelContext.getName(), modelContextDefinition);
                    }
                } else {
                    modelContextDefinition = of.createExpressionDef()
                            .withName(unqualifiedIdentifier)
                            .withContext(getCurrentContext())
                            .withExpression(of.createNull());
                    track(modelContextDefinition, ctx);
                    ((ExpressionDef) modelContextDefinition)
                            .getExpression()
                            .setResultType(libraryBuilder.resolveTypeName("System", "Any"));
                    modelContextDefinition.setResultType(((ExpressionDef) modelContextDefinition)
                            .getExpression()
                            .getResultType());
                    libraryBuilder.addExpression((ExpressionDef) modelContextDefinition);
                    contextDefinitions.put(modelContext.getName(), modelContextDefinition);
                }
            }
        }

        ContextDef contextDef = of.createContextDef().withName(getCurrentContext());
        track(contextDef, ctx);
        if (libraryBuilder.isCompatibleWith("1.5")) {
            libraryBuilder.addContext(contextDef);
        }

        return getCurrentContext();
    }

    private boolean isImplicitContextExpressionDef(ExpressionDef def) {
        for (Element e : contextDefinitions.values()) {
            if (def == e) {
                return true;
            }
        }

        return false;
    }

    private void removeImplicitContextExpressionDef(ExpressionDef def) {
        for (Map.Entry e : contextDefinitions.entrySet()) {
            if (def == e.getValue()) {
                contextDefinitions.remove(e.getKey());
                break;
            }
        }
    }

    public ExpressionDef internalVisitExpressionDefinition(cqlParser.ExpressionDefinitionContext ctx) {
        String identifier = parseString(ctx.identifier());
        ExpressionDef def = libraryBuilder.resolveExpressionRef(identifier);

        // First time visiting this expression definition, create a lightweight ExpressionDef to be used to output a
        // hiding warning message
        // If it's the second time around, we'll be able to resolve it and we can assume it's already on the
        // hiding stack.
        if (def == null) {
            final ExpressionDef hollowExpressionDef =
                    of.createExpressionDef().withName(identifier).withContext(getCurrentContext());
            libraryBuilder.pushIdentifier(identifier, hollowExpressionDef, IdentifierScope.GLOBAL);
        }

        if (def == null || isImplicitContextExpressionDef(def)) {
            if (def != null && isImplicitContextExpressionDef(def)) {
                libraryBuilder.removeExpression(def);
                removeImplicitContextExpressionDef(def);
                def = null;
            }
            libraryBuilder.pushExpressionContext(getCurrentContext());
            try {
                libraryBuilder.pushExpressionDefinition(identifier);
                try {
                    def = of.createExpressionDef()
                            .withAccessLevel(parseAccessModifier(ctx.accessModifier()))
                            .withName(identifier)
                            .withContext(getCurrentContext())
                            .withExpression((Expression) visit(ctx.expression()));
                    if (def.getExpression() != null) {
                        def.setResultType(def.getExpression().getResultType());
                    }
                    libraryBuilder.addExpression(def);
                } finally {
                    libraryBuilder.popExpressionDefinition();
                }
            } finally {
                libraryBuilder.popExpressionContext();
            }
        }

        return def;
    }

    @Override
    public ExpressionDef visitExpressionDefinition(cqlParser.ExpressionDefinitionContext ctx) {
        this.libraryBuilder.pushIdentifierScope();
        try {
            ExpressionDef expressionDef = internalVisitExpressionDefinition(ctx);
            if (forwards.isEmpty() || !forwards.peek().getName().equals(expressionDef.getName())) {
                if (definedExpressionDefinitions.contains(expressionDef.getName())) {
                    // ERROR:
                    throw new IllegalArgumentException(
                            String.format("Identifier %s is already in use in this library.", expressionDef.getName()));
                }

                // Track defined expression definitions locally, otherwise duplicate expression
                // definitions will be missed
                // because they are
                // overwritten by name when they are encountered by the preprocessor.
                definedExpressionDefinitions.add(expressionDef.getName());
            }
            return expressionDef;

        } finally {
            this.libraryBuilder.popIdentifierScope();
        }
    }

    @Override
    public Literal visitStringLiteral(cqlParser.StringLiteralContext ctx) {
        final Literal stringLiteral = libraryBuilder.createLiteral(parseString(ctx.STRING()));
        // Literals are never actually pushed to the stack. This just emits a warning if
        // the literal is hiding something
        libraryBuilder.pushIdentifier(stringLiteral.getValue(), stringLiteral);
        return stringLiteral;
    }

    @Override
    public Literal visitSimpleStringLiteral(cqlParser.SimpleStringLiteralContext ctx) {
        return libraryBuilder.createLiteral(parseString(ctx.STRING()));
    }

    @Override
    public Literal visitBooleanLiteral(cqlParser.BooleanLiteralContext ctx) {
        return libraryBuilder.createLiteral(Boolean.valueOf(ctx.getText()));
    }

    @Override
    public Object visitIntervalSelector(cqlParser.IntervalSelectorContext ctx) {
        return libraryBuilder.createInterval(
                parseExpression(ctx.expression(0)),
                ctx.getChild(1).getText().equals("["),
                parseExpression(ctx.expression(1)),
                ctx.getChild(5).getText().equals("]"));
    }

    @Override
    public Object visitTupleElementSelector(cqlParser.TupleElementSelectorContext ctx) {
        TupleElement result = of.createTupleElement()
                .withName(parseString(ctx.referentialIdentifier()))
                .withValue(parseExpression(ctx.expression()));
        result.setResultType(result.getValue().getResultType());
        return result;
    }

    @Override
    public Object visitTupleSelector(cqlParser.TupleSelectorContext ctx) {
        Tuple tuple = of.createTuple();
        TupleType tupleType = new TupleType();
        for (cqlParser.TupleElementSelectorContext elementContext : ctx.tupleElementSelector()) {
            TupleElement element = (TupleElement) visit(elementContext);
            tupleType.addElement(new TupleTypeElement(element.getName(), element.getResultType()));
            tuple.getElement().add(element);
        }
        tuple.setResultType(tupleType);
        return tuple;
    }

    @Override
    public Object visitInstanceElementSelector(cqlParser.InstanceElementSelectorContext ctx) {
        InstanceElement result = of.createInstanceElement()
                .withName(parseString(ctx.referentialIdentifier()))
                .withValue(parseExpression(ctx.expression()));
        result.setResultType(result.getValue().getResultType());
        return result;
    }

    @Override
    public Object visitInstanceSelector(cqlParser.InstanceSelectorContext ctx) {
        Instance instance = of.createInstance();
        NamedTypeSpecifier classTypeSpecifier = (NamedTypeSpecifier) visitNamedTypeSpecifier(ctx.namedTypeSpecifier());
        instance.setClassType(classTypeSpecifier.getName());
        instance.setResultType(classTypeSpecifier.getResultType());

        for (cqlParser.InstanceElementSelectorContext elementContext : ctx.instanceElementSelector()) {
            InstanceElement element = (InstanceElement) visit(elementContext);
            PropertyResolution resolution =
                    libraryBuilder.resolveProperty(classTypeSpecifier.getResultType(), element.getName());
            element.setValue(libraryBuilder.ensureCompatible(element.getValue(), resolution.getType()));
            element.setName(resolution.getName());
            if (resolution.getTargetMap() != null) {
                // TODO: Target mapping in instance selectors
                throw new IllegalArgumentException("Target Mapping in instance selectors not yet supported");
            }
            instance.getElement().add(element);
        }

        return instance;
    }

    @Override
    public Object visitCodeSelector(cqlParser.CodeSelectorContext ctx) {
        Code code = of.createCode();
        code.setCode(parseString(ctx.STRING()));
        code.setSystem((CodeSystemRef) visit(ctx.codesystemIdentifier()));
        if (ctx.displayClause() != null) {
            code.setDisplay(parseString(ctx.displayClause().STRING()));
        }

        code.setResultType(libraryBuilder.resolveTypeName("System", "Code"));
        return code;
    }

    @Override
    public Object visitConceptSelector(cqlParser.ConceptSelectorContext ctx) {
        Concept concept = of.createConcept();
        if (ctx.displayClause() != null) {
            concept.setDisplay(parseString(ctx.displayClause().STRING()));
        }

        for (cqlParser.CodeSelectorContext codeContext : ctx.codeSelector()) {
            concept.getCode().add((Code) visit(codeContext));
        }

        concept.setResultType(libraryBuilder.resolveTypeName("System", "Concept"));
        return concept;
    }

    @Override
    public Object visitListSelector(cqlParser.ListSelectorContext ctx) {
        TypeSpecifier elementTypeSpecifier = parseTypeSpecifier(ctx.typeSpecifier());
        org.hl7.elm.r1.List list = of.createList();
        ListType listType = null;
        if (elementTypeSpecifier != null) {
            ListTypeSpecifier listTypeSpecifier = of.createListTypeSpecifier().withElementType(elementTypeSpecifier);
            track(listTypeSpecifier, ctx.typeSpecifier());
            listType = new ListType(elementTypeSpecifier.getResultType());
            listTypeSpecifier.setResultType(listType);
        }

        DataType elementType = elementTypeSpecifier != null ? elementTypeSpecifier.getResultType() : null;
        DataType inferredElementType = null;

        List elements = new ArrayList<>();
        for (cqlParser.ExpressionContext elementContext : ctx.expression()) {
            Expression element = parseExpression(elementContext);

            if (elementType != null) {
                libraryBuilder.verifyType(element.getResultType(), elementType);
            } else {
                if (inferredElementType == null) {
                    inferredElementType = element.getResultType();
                } else {
                    DataType compatibleType =
                            libraryBuilder.findCompatibleType(inferredElementType, element.getResultType());
                    if (compatibleType != null) {
                        inferredElementType = compatibleType;
                    } else {
                        inferredElementType = libraryBuilder.resolveTypeName("System", "Any");
                    }
                }
            }

            elements.add(element);
        }

        if (elementType == null) {
            elementType =
                    inferredElementType == null ? libraryBuilder.resolveTypeName("System", "Any") : inferredElementType;
        }

        for (Expression element : elements) {
            if (!elementType.isSuperTypeOf(element.getResultType())) {
                Conversion conversion =
                        libraryBuilder.findConversion(element.getResultType(), elementType, true, false);
                if (conversion != null) {
                    list.getElement().add(libraryBuilder.convertExpression(element, conversion));
                } else {
                    list.getElement().add(element);
                }
            } else {
                list.getElement().add(element);
            }
        }

        if (listType == null) {
            listType = new ListType(elementType);
        }

        list.setResultType(listType);
        return list;
    }

    @Override
    public Object visitTimeLiteral(cqlParser.TimeLiteralContext ctx) {
        String input = ctx.getText();
        if (input.startsWith("@")) {
            input = input.substring(1);
        }

        Pattern timePattern = Pattern.compile("T(\\d{2})(\\:(\\d{2})(\\:(\\d{2})(\\.(\\d+))?)?)?");
        // -1-------2---3-------4---5-------6---7-----------

        Matcher matcher = timePattern.matcher(input);
        if (matcher.matches()) {
            try {
                Time result = of.createTime();
                int hour = Integer.parseInt(matcher.group(1));
                int minute = -1;
                int second = -1;
                int millisecond = -1;
                if (hour < 0 || hour > 24) {
                    throw new IllegalArgumentException(String.format("Invalid hour in time literal (%s).", input));
                }
                result.setHour(libraryBuilder.createLiteral(hour));

                if (matcher.group(3) != null) {
                    minute = Integer.parseInt(matcher.group(3));
                    if (minute < 0 || minute >= 60 || (hour == 24 && minute > 0)) {
                        throw new IllegalArgumentException(
                                String.format("Invalid minute in time literal (%s).", input));
                    }
                    result.setMinute(libraryBuilder.createLiteral(minute));
                }

                if (matcher.group(5) != null) {
                    second = Integer.parseInt(matcher.group(5));
                    if (second < 0 || second >= 60 || (hour == 24 && second > 0)) {
                        throw new IllegalArgumentException(
                                String.format("Invalid second in time literal (%s).", input));
                    }
                    result.setSecond(libraryBuilder.createLiteral(second));
                }

                if (matcher.group(7) != null) {
                    millisecond = Integer.parseInt(matcher.group(7));
                    if (millisecond < 0 || (hour == 24 && millisecond > 0)) {
                        throw new IllegalArgumentException(
                                String.format("Invalid millisecond in time literal (%s).", input));
                    }
                    result.setMillisecond(libraryBuilder.createLiteral(millisecond));
                }

                result.setResultType(libraryBuilder.resolveTypeName("System", "Time"));
                return result;
            } catch (RuntimeException e) {
                throw new IllegalArgumentException(
                        String.format(
                                "Invalid time input (%s). Use ISO 8601 time representation (hh:mm:ss.fff).", input),
                        e);
            }
        } else {
            throw new IllegalArgumentException(
                    String.format("Invalid time input (%s). Use ISO 8601 time representation (hh:mm:ss.fff).", input));
        }
    }

    private Expression parseDateTimeLiteral(String input) {
        /*
         * DATETIME
         * : '@'
         * [0-9][0-9][0-9][0-9] // year
         * (
         * (
         * '-'[0-9][0-9] // month
         * (
         * (
         * '-'[0-9][0-9] // day
         * ('T' TIMEFORMAT?)?
         * )
         * | 'T'
         * )?
         * )
         * | 'T'
         * )?
         * ('Z' | ('+' | '-') [0-9][0-9]':'[0-9][0-9])? // timezone offset
         * ;
         */

        Pattern dateTimePattern = Pattern.compile(
                "(\\d{4})(((-(\\d{2}))(((-(\\d{2}))((T)((\\d{2})(\\:(\\d{2})(\\:(\\d{2})(\\.(\\d+))?)?)?)?)?)|(T))?)|(T))?((Z)|(([+-])(\\d{2})(\\:(\\d{2}))))?");
        // 1-------234-5--------678-9--------11--11-------1---1-------1---1-------1---1-----------------2------2----22---22-----2-------2---2-----------
        // ----------------------------------01--23-------4---5-------6---7-------8---9-----------------0------1----23---45-----6-------7---8-----------

        /*
         * year - group 1
         * month - group 5
         * day - group 9
         * day dateTime indicator - group 11
         * hour - group 13
         * minute - group 15
         * second - group 17
         * millisecond - group 19
         * month dateTime indicator - group 20
         * year dateTime indicator - group 21
         * utc indicator - group 23
         * timezone offset polarity - group 25
         * timezone offset hour - group 26
         * timezone offset minute - group 28
         */

        /*
         * Pattern dateTimePattern =
         * Pattern.compile(
         * "(\\d{4})(-(\\d{2}))?(-(\\d{2}))?((Z)|(T((\\d{2})(\\:(\\d{2})(\\:(\\d{2})(\\.(\\d+))?)?)?)?((Z)|(([+-])(\\d{2})(\\:?(\\d{2}))?))?))?"
         * );
         * //1-------2-3---------4-5---------67---8-91-------1---1-------1---1-------1--
         * -1-------------11---12-----2-------2----2---------------
         * //----------------------------------------0-------1---2-------3---4-------5--
         * -6-------------78---90-----1-------2----3---------------
         */

        Matcher matcher = dateTimePattern.matcher(input);
        if (matcher.matches()) {
            try {
                GregorianCalendar calendar = (GregorianCalendar) GregorianCalendar.getInstance();
                DateTime result = of.createDateTime();
                int year = Integer.parseInt(matcher.group(1));
                int month = -1;
                int day = -1;
                int hour = -1;
                int minute = -1;
                int second = -1;
                int millisecond = -1;
                result.setYear(libraryBuilder.createLiteral(year));
                if (matcher.group(5) != null) {
                    month = Integer.parseInt(matcher.group(5));
                    if (month < 0 || month > 12) {
                        throw new IllegalArgumentException(
                                String.format("Invalid month in date/time literal (%s).", input));
                    }
                    result.setMonth(libraryBuilder.createLiteral(month));
                }

                if (matcher.group(9) != null) {
                    day = Integer.parseInt(matcher.group(9));
                    int maxDay = 31;
                    switch (month) {
                        case 2:
                            maxDay = calendar.isLeapYear(year) ? 29 : 28;
                            break;
                        case 4:
                        case 6:
                        case 9:
                        case 11:
                            maxDay = 30;
                            break;
                        default:
                            break;
                    }

                    if (day < 0 || day > maxDay) {
                        throw new IllegalArgumentException(
                                String.format("Invalid day in date/time literal (%s).", input));
                    }

                    result.setDay(libraryBuilder.createLiteral(day));
                }

                if (matcher.group(13) != null) {
                    hour = Integer.parseInt(matcher.group(13));
                    if (hour < 0 || hour > 24) {
                        throw new IllegalArgumentException(
                                String.format("Invalid hour in date/time literal (%s).", input));
                    }
                    result.setHour(libraryBuilder.createLiteral(hour));
                }

                if (matcher.group(15) != null) {
                    minute = Integer.parseInt(matcher.group(15));
                    if (minute < 0 || minute >= 60 || (hour == 24 && minute > 0)) {
                        throw new IllegalArgumentException(
                                String.format("Invalid minute in date/time literal (%s).", input));
                    }
                    result.setMinute(libraryBuilder.createLiteral(minute));
                }

                if (matcher.group(17) != null) {
                    second = Integer.parseInt(matcher.group(17));
                    if (second < 0 || second >= 60 || (hour == 24 && second > 0)) {
                        throw new IllegalArgumentException(
                                String.format("Invalid second in date/time literal (%s).", input));
                    }
                    result.setSecond(libraryBuilder.createLiteral(second));
                }

                if (matcher.group(19) != null) {
                    millisecond = Integer.parseInt(matcher.group(19));
                    if (millisecond < 0 || (hour == 24 && millisecond > 0)) {
                        throw new IllegalArgumentException(
                                String.format("Invalid millisecond in date/time literal (%s).", input));
                    }
                    result.setMillisecond(libraryBuilder.createLiteral(millisecond));
                }

                if (matcher.group(23) != null && matcher.group(23).equals("Z")) {
                    result.setTimezoneOffset(libraryBuilder.createLiteral(0.0));
                }

                if (matcher.group(25) != null) {
                    int offsetPolarity = matcher.group(25).equals("+") ? 1 : -1;

                    if (matcher.group(28) != null) {
                        int hourOffset = Integer.parseInt(matcher.group(26));
                        if (hourOffset < 0 || hourOffset > 14) {
                            throw new IllegalArgumentException(String.format(
                                    "Timezone hour offset is out of range in date/time literal (%s).", input));
                        }

                        int minuteOffset = Integer.parseInt(matcher.group(28));
                        if (minuteOffset < 0 || minuteOffset >= 60 || (hourOffset == 14 && minuteOffset > 0)) {
                            throw new IllegalArgumentException(String.format(
                                    "Timezone minute offset is out of range in date/time literal (%s).", input));
                        }

                        result.setTimezoneOffset(libraryBuilder.createLiteral(
                                (double) (hourOffset + ((double) minuteOffset / 60)) * offsetPolarity));
                    } else {
                        if (matcher.group(26) != null) {
                            int hourOffset = Integer.parseInt(matcher.group(26));
                            if (hourOffset < 0 || hourOffset > 14) {
                                throw new IllegalArgumentException(String.format(
                                        "Timezone hour offset is out of range in date/time literal (%s).", input));
                            }

                            result.setTimezoneOffset(
                                    libraryBuilder.createLiteral((double) (hourOffset * offsetPolarity)));
                        }
                    }
                }

                if (result.getHour() == null
                        && matcher.group(11) == null
                        && matcher.group(20) == null
                        && matcher.group(21) == null) {
                    org.hl7.elm.r1.Date date = of.createDate();
                    date.setYear(result.getYear());
                    date.setMonth(result.getMonth());
                    date.setDay(result.getDay());
                    date.setResultType(libraryBuilder.resolveTypeName("System", "Date"));
                    return date;
                }

                result.setResultType(libraryBuilder.resolveTypeName("System", "DateTime"));
                return result;
            } catch (RuntimeException e) {
                throw new IllegalArgumentException(
                        String.format(
                                "Invalid date-time input (%s). Use ISO 8601 date time representation (yyyy-MM-ddThh:mm:ss.fff(Z|(+/-hh:mm)).",
                                input),
                        e);
            }
        } else {
            throw new IllegalArgumentException(String.format(
                    "Invalid date-time input (%s). Use ISO 8601 date time representation (yyyy-MM-ddThh:mm:ss.fff(Z|+/-hh:mm)).",
                    input));
        }
    }

    @Override
    public Object visitDateLiteral(cqlParser.DateLiteralContext ctx) {
        String input = ctx.getText();
        if (input.startsWith("@")) {
            input = input.substring(1);
        }

        return parseDateTimeLiteral(input);
    }

    @Override
    public Object visitDateTimeLiteral(cqlParser.DateTimeLiteralContext ctx) {
        String input = ctx.getText();
        if (input.startsWith("@")) {
            input = input.substring(1);
        }

        return parseDateTimeLiteral(input);
    }

    @Override
    public Null visitNullLiteral(cqlParser.NullLiteralContext ctx) {
        Null result = of.createNull();
        result.setResultType(libraryBuilder.resolveTypeName("System", "Any"));
        return result;
    }

    @Override
    public Expression visitNumberLiteral(cqlParser.NumberLiteralContext ctx) {
        return libraryBuilder.createNumberLiteral(ctx.NUMBER().getText());
    }

    @Override
    public Expression visitSimpleNumberLiteral(cqlParser.SimpleNumberLiteralContext ctx) {
        return libraryBuilder.createNumberLiteral(ctx.NUMBER().getText());
    }

    @Override
    public Literal visitLongNumberLiteral(cqlParser.LongNumberLiteralContext ctx) {
        String input = ctx.LONGNUMBER().getText();
        if (input.endsWith("L")) {
            input = input.substring(0, input.length() - 1);
        }
        return libraryBuilder.createLongNumberLiteral(input);
    }

    private BigDecimal parseDecimal(String value) {
        try {
            return new BigDecimal(value);
        } catch (Exception e) {
            throw new IllegalArgumentException(String.format("Could not parse number literal: %s", value));
        }
    }

    @Override
    public Expression visitQuantity(cqlParser.QuantityContext ctx) {
        if (ctx.unit() != null) {
            Quantity result =
                    libraryBuilder.createQuantity(parseDecimal(ctx.NUMBER().getText()), parseString(ctx.unit()));
            return result;
        } else {
            return libraryBuilder.createNumberLiteral(ctx.NUMBER().getText());
        }
    }

    private Quantity getQuantity(Expression source) {
        if (source instanceof Literal) {
            return libraryBuilder.createQuantity(parseDecimal(((Literal) source).getValue()), "1");
        } else if (source instanceof Quantity) {
            return (Quantity) source;
        }

        throw new IllegalArgumentException("Could not create quantity from source expression.");
    }

    @Override
    public Expression visitRatio(cqlParser.RatioContext ctx) {
        Quantity numerator = getQuantity((Expression) visit(ctx.quantity(0)));
        Quantity denominator = getQuantity((Expression) visit(ctx.quantity(1)));
        return libraryBuilder.createRatio(numerator, denominator);
    }

    @Override
    public Not visitNotExpression(cqlParser.NotExpressionContext ctx) {
        Not result = of.createNot().withOperand(parseExpression(ctx.expression()));
        libraryBuilder.resolveUnaryCall("System", "Not", result);
        return result;
    }

    @Override
    public Exists visitExistenceExpression(cqlParser.ExistenceExpressionContext ctx) {
        Exists result = of.createExists().withOperand(parseExpression(ctx.expression()));
        libraryBuilder.resolveUnaryCall("System", "Exists", result);
        return result;
    }

    @Override
    public BinaryExpression visitMultiplicationExpressionTerm(cqlParser.MultiplicationExpressionTermContext ctx) {
        BinaryExpression exp = null;
        String operatorName = null;
        switch (ctx.getChild(1).getText()) {
            case "*":
                exp = of.createMultiply();
                operatorName = "Multiply";
                break;
            case "/":
                exp = of.createDivide();
                operatorName = "Divide";
                break;
            case "div":
                exp = of.createTruncatedDivide();
                operatorName = "TruncatedDivide";
                break;
            case "mod":
                exp = of.createModulo();
                operatorName = "Modulo";
                break;
            default:
                throw new IllegalArgumentException(String.format(
                        "Unsupported operator: %s.", ctx.getChild(1).getText()));
        }

        exp.withOperand(parseExpression(ctx.expressionTerm(0)), parseExpression(ctx.expressionTerm(1)));

        libraryBuilder.resolveBinaryCall("System", operatorName, exp);

        return exp;
    }

    @Override
    public Power visitPowerExpressionTerm(cqlParser.PowerExpressionTermContext ctx) {
        Power power = of.createPower()
                .withOperand(parseExpression(ctx.expressionTerm(0)), parseExpression(ctx.expressionTerm(1)));

        libraryBuilder.resolveBinaryCall("System", "Power", power);

        return power;
    }

    @Override
    public Object visitPolarityExpressionTerm(cqlParser.PolarityExpressionTermContext ctx) {
        if (ctx.getChild(0).getText().equals("+")) {
            return visit(ctx.expressionTerm());
        }

        Negate result = of.createNegate().withOperand(parseExpression(ctx.expressionTerm()));
        libraryBuilder.resolveUnaryCall("System", "Negate", result);
        return result;
    }

    @Override
    public Expression visitAdditionExpressionTerm(cqlParser.AdditionExpressionTermContext ctx) {
        Expression exp = null;
        String operatorName = null;
        switch (ctx.getChild(1).getText()) {
            case "+":
                exp = of.createAdd();
                operatorName = "Add";
                break;
            case "-":
                exp = of.createSubtract();
                operatorName = "Subtract";
                break;
            case "&":
                exp = of.createConcatenate();
                operatorName = "Concatenate";
                break;
            default:
                throw new IllegalArgumentException(String.format(
                        "Unsupported operator: %s.", ctx.getChild(1).getText()));
        }

        if (exp instanceof BinaryExpression) {
            ((BinaryExpression) exp)
                    .withOperand(parseExpression(ctx.expressionTerm(0)), parseExpression(ctx.expressionTerm(1)));

            libraryBuilder.resolveBinaryCall("System", operatorName, (BinaryExpression) exp);

            if (exp.getResultType() == libraryBuilder.resolveTypeName("System", "String")) {
                Concatenate concatenate = of.createConcatenate();
                concatenate.getOperand().addAll(((BinaryExpression) exp).getOperand());
                concatenate.setResultType(exp.getResultType());
                exp = concatenate;
            }
        } else {
            Concatenate concatenate = (Concatenate) exp;
            concatenate.withOperand(parseExpression(ctx.expressionTerm(0)), parseExpression(ctx.expressionTerm(1)));

            for (int i = 0; i < concatenate.getOperand().size(); i++) {
                Expression operand = concatenate.getOperand().get(i);
                Literal empty = libraryBuilder.createLiteral("");
                ArrayList params = new ArrayList();
                params.add(operand);
                params.add(empty);
                Expression coalesce = libraryBuilder.resolveFunction("System", "Coalesce", params);
                concatenate.getOperand().set(i, coalesce);
            }
            libraryBuilder.resolveNaryCall("System", operatorName, concatenate);
        }
        return exp;
    }

    @Override
    public Object visitPredecessorExpressionTerm(cqlParser.PredecessorExpressionTermContext ctx) {
        return libraryBuilder.buildPredecessor(parseExpression(ctx.expressionTerm()));
    }

    @Override
    public Object visitSuccessorExpressionTerm(cqlParser.SuccessorExpressionTermContext ctx) {
        return libraryBuilder.buildSuccessor(parseExpression(ctx.expressionTerm()));
    }

    @Override
    public Object visitElementExtractorExpressionTerm(cqlParser.ElementExtractorExpressionTermContext ctx) {
        SingletonFrom result = of.createSingletonFrom().withOperand(parseExpression(ctx.expressionTerm()));

        libraryBuilder.resolveUnaryCall("System", "SingletonFrom", result);
        return result;
    }

    @Override
    public Object visitPointExtractorExpressionTerm(cqlParser.PointExtractorExpressionTermContext ctx) {
        PointFrom result = of.createPointFrom().withOperand(parseExpression(ctx.expressionTerm()));

        libraryBuilder.resolveUnaryCall("System", "PointFrom", result);
        return result;
    }

    @Override
    public Object visitTypeExtentExpressionTerm(cqlParser.TypeExtentExpressionTermContext ctx) {
        String extent = parseString(ctx.getChild(0));
        TypeSpecifier targetType = parseTypeSpecifier(ctx.namedTypeSpecifier());
        switch (extent) {
            case "minimum": {
                return libraryBuilder.buildMinimum(targetType.getResultType());
            }

            case "maximum": {
                return libraryBuilder.buildMaximum(targetType.getResultType());
            }

            default:
                throw new IllegalArgumentException(String.format("Unknown extent: %s", extent));
        }
    }

    @Override
    public Object visitTimeBoundaryExpressionTerm(cqlParser.TimeBoundaryExpressionTermContext ctx) {
        UnaryExpression result = null;
        String operatorName = null;

        if (ctx.getChild(0).getText().equals("start")) {
            result = of.createStart().withOperand(parseExpression(ctx.expressionTerm()));
            operatorName = "Start";
        } else {
            result = of.createEnd().withOperand(parseExpression(ctx.expressionTerm()));
            operatorName = "End";
        }

        libraryBuilder.resolveUnaryCall("System", operatorName, result);
        return result;
    }

    private DateTimePrecision parseDateTimePrecision(String dateTimePrecision) {
        return parseDateTimePrecision(dateTimePrecision, true, true);
    }

    private DateTimePrecision parseComparableDateTimePrecision(String dateTimePrecision) {
        return parseDateTimePrecision(dateTimePrecision, true, false);
    }

    private DateTimePrecision parseComparableDateTimePrecision(String dateTimePrecision, boolean precisionRequired) {
        return parseDateTimePrecision(dateTimePrecision, precisionRequired, false);
    }

    private DateTimePrecision parseDateTimePrecision(
            String dateTimePrecision, boolean precisionRequired, boolean allowWeeks) {
        if (dateTimePrecision == null) {
            if (precisionRequired) {
                throw new IllegalArgumentException("dateTimePrecision is null");
            }

            return null;
        }

        switch (dateTimePrecision) {
            case "a":
            case "year":
            case "years":
                return DateTimePrecision.YEAR;
            case "mo":
            case "month":
            case "months":
                return DateTimePrecision.MONTH;
            case "wk":
            case "week":
            case "weeks":
                if (!allowWeeks) {
                    throw new IllegalArgumentException("Week precision cannot be used for comparisons.");
                }
                return DateTimePrecision.WEEK;
            case "d":
            case "day":
            case "days":
                return DateTimePrecision.DAY;
            case "h":
            case "hour":
            case "hours":
                return DateTimePrecision.HOUR;
            case "min":
            case "minute":
            case "minutes":
                return DateTimePrecision.MINUTE;
            case "s":
            case "second":
            case "seconds":
                return DateTimePrecision.SECOND;
            case "ms":
            case "millisecond":
            case "milliseconds":
                return DateTimePrecision.MILLISECOND;
            default:
                throw new IllegalArgumentException(String.format("Unknown precision '%s'.", dateTimePrecision));
        }
    }

    @Override
    public Object visitTimeUnitExpressionTerm(cqlParser.TimeUnitExpressionTermContext ctx) {
        String component = ctx.dateTimeComponent().getText();

        UnaryExpression result = null;
        String operatorName = null;
        switch (component) {
            case "date":
                result = of.createDateFrom().withOperand(parseExpression(ctx.expressionTerm()));
                operatorName = "DateFrom";
                break;
            case "time":
                result = of.createTimeFrom().withOperand(parseExpression(ctx.expressionTerm()));
                operatorName = "TimeFrom";
                break;
            case "timezone":
                if (!libraryBuilder.isCompatibilityLevel3()) {
                    // ERROR:
                    throw new IllegalArgumentException("Timezone keyword is only valid in 1.3 or lower");
                }
                result = of.createTimezoneFrom().withOperand(parseExpression(ctx.expressionTerm()));
                operatorName = "TimezoneFrom";
                break;
            case "timezoneoffset":
                result = of.createTimezoneOffsetFrom().withOperand(parseExpression(ctx.expressionTerm()));
                operatorName = "TimezoneOffsetFrom";
                break;
            case "year":
            case "month":
            case "day":
            case "hour":
            case "minute":
            case "second":
            case "millisecond":
                result = of.createDateTimeComponentFrom()
                        .withOperand(parseExpression(ctx.expressionTerm()))
                        .withPrecision(parseDateTimePrecision(component));
                operatorName = "DateTimeComponentFrom";
                break;
            case "week":
                throw new IllegalArgumentException("Date/time values do not have a week component.");
            default:
                throw new IllegalArgumentException(String.format("Unknown precision '%s'.", component));
        }

        libraryBuilder.resolveUnaryCall("System", operatorName, result);
        return result;
    }

    @Override
    public Object visitDurationExpressionTerm(cqlParser.DurationExpressionTermContext ctx) {
        // duration in days of X <=> days between start of X and end of X
        Expression operand = parseExpression(ctx.expressionTerm());

        Start start = of.createStart().withOperand(operand);
        libraryBuilder.resolveUnaryCall("System", "Start", start);

        End end = of.createEnd().withOperand(operand);
        libraryBuilder.resolveUnaryCall("System", "End", end);

        DurationBetween result = of.createDurationBetween()
                .withPrecision(
                        parseDateTimePrecision(ctx.pluralDateTimePrecision().getText()))
                .withOperand(start, end);

        libraryBuilder.resolveBinaryCall("System", "DurationBetween", result);
        return result;
    }

    @Override
    public Object visitDifferenceExpressionTerm(cqlParser.DifferenceExpressionTermContext ctx) {
        // difference in days of X <=> difference in days between start of X and end of
        // X
        Expression operand = parseExpression(ctx.expressionTerm());

        Start start = of.createStart().withOperand(operand);
        libraryBuilder.resolveUnaryCall("System", "Start", start);

        End end = of.createEnd().withOperand(operand);
        libraryBuilder.resolveUnaryCall("System", "End", end);

        DifferenceBetween result = of.createDifferenceBetween()
                .withPrecision(
                        parseDateTimePrecision(ctx.pluralDateTimePrecision().getText()))
                .withOperand(start, end);

        libraryBuilder.resolveBinaryCall("System", "DifferenceBetween", result);
        return result;
    }

    @Override
    public Object visitBetweenExpression(cqlParser.BetweenExpressionContext ctx) {
        // X properly? between Y and Z
        Expression first = parseExpression(ctx.expression());
        Expression second = parseExpression(ctx.expressionTerm(0));
        Expression third = parseExpression(ctx.expressionTerm(1));
        boolean isProper = ctx.getChild(0).getText().equals("properly");

        if (first.getResultType() instanceof IntervalType) {
            BinaryExpression result = isProper
                    ? of.createProperIncludedIn()
                    : of.createIncludedIn()
                            .withOperand(first, libraryBuilder.createInterval(second, true, third, true));

            libraryBuilder.resolveBinaryCall("System", isProper ? "ProperIncludedIn" : "IncludedIn", result);
            return result;
        } else {
            BinaryExpression result = of.createAnd()
                    .withOperand(
                            (isProper ? of.createGreater() : of.createGreaterOrEqual()).withOperand(first, second),
                            (isProper ? of.createLess() : of.createLessOrEqual()).withOperand(first, third));

            libraryBuilder.resolveBinaryCall("System", isProper ? "Greater" : "GreaterOrEqual", (BinaryExpression)
                    result.getOperand().get(0));
            libraryBuilder.resolveBinaryCall("System", isProper ? "Less" : "LessOrEqual", (BinaryExpression)
                    result.getOperand().get(1));
            libraryBuilder.resolveBinaryCall("System", "And", result);
            return result;
        }
    }

    @Override
    public Object visitDurationBetweenExpression(cqlParser.DurationBetweenExpressionContext ctx) {
        BinaryExpression result = of.createDurationBetween()
                .withPrecision(
                        parseDateTimePrecision(ctx.pluralDateTimePrecision().getText()))
                .withOperand(parseExpression(ctx.expressionTerm(0)), parseExpression(ctx.expressionTerm(1)));

        libraryBuilder.resolveBinaryCall("System", "DurationBetween", result);
        return result;
    }

    @Override
    public Object visitDifferenceBetweenExpression(cqlParser.DifferenceBetweenExpressionContext ctx) {
        BinaryExpression result = of.createDifferenceBetween()
                .withPrecision(
                        parseDateTimePrecision(ctx.pluralDateTimePrecision().getText()))
                .withOperand(parseExpression(ctx.expressionTerm(0)), parseExpression(ctx.expressionTerm(1)));

        libraryBuilder.resolveBinaryCall("System", "DifferenceBetween", result);
        return result;
    }

    @Override
    public Object visitWidthExpressionTerm(cqlParser.WidthExpressionTermContext ctx) {
        UnaryExpression result = of.createWidth().withOperand(parseExpression(ctx.expressionTerm()));
        libraryBuilder.resolveUnaryCall("System", "Width", result);
        return result;
    }

    @Override
    public Expression visitParenthesizedTerm(cqlParser.ParenthesizedTermContext ctx) {
        return parseExpression(ctx.expression());
    }

    @Override
    public Object visitMembershipExpression(cqlParser.MembershipExpressionContext ctx) {
        String operator = ctx.getChild(1).getText();

        switch (operator) {
            case "in":
                if (ctx.dateTimePrecisionSpecifier() != null) {
                    In in = of.createIn()
                            .withPrecision(parseComparableDateTimePrecision(ctx.dateTimePrecisionSpecifier()
                                    .dateTimePrecision()
                                    .getText()))
                            .withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));

                    libraryBuilder.resolveBinaryCall("System", "In", in);
                    return in;
                } else {
                    Expression left = parseExpression(ctx.expression(0));
                    Expression right = parseExpression(ctx.expression(1));
                    return libraryBuilder.resolveIn(left, right);
                }
            case "contains":
                if (ctx.dateTimePrecisionSpecifier() != null) {
                    Contains contains = of.createContains()
                            .withPrecision(parseComparableDateTimePrecision(ctx.dateTimePrecisionSpecifier()
                                    .dateTimePrecision()
                                    .getText()))
                            .withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));

                    libraryBuilder.resolveBinaryCall("System", "Contains", contains);
                    return contains;
                } else {
                    Expression left = parseExpression(ctx.expression(0));
                    Expression right = parseExpression(ctx.expression(1));
                    if (left instanceof ValueSetRef) {
                        InValueSet in = of.createInValueSet()
                                .withCode(right)
                                .withValueset((ValueSetRef) left)
                                .withValuesetExpression(left);
                        libraryBuilder.resolveCall("System", "InValueSet", new InValueSetInvocation(in));
                        return in;
                    }

                    if (left instanceof CodeSystemRef) {
                        InCodeSystem in = of.createInCodeSystem()
                                .withCode(right)
                                .withCodesystem((CodeSystemRef) left)
                                .withCodesystemExpression(left);
                        libraryBuilder.resolveCall("System", "InCodeSystem", new InCodeSystemInvocation(in));
                        return in;
                    }

                    Contains contains = of.createContains().withOperand(left, right);
                    libraryBuilder.resolveBinaryCall("System", "Contains", contains);
                    return contains;
                }
        }

        throw new IllegalArgumentException(String.format("Unknown operator: %s", operator));
    }

    @Override
    public And visitAndExpression(cqlParser.AndExpressionContext ctx) {
        And and = of.createAnd().withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));

        libraryBuilder.resolveBinaryCall("System", "And", and);
        return and;
    }

    @Override
    public Expression visitOrExpression(cqlParser.OrExpressionContext ctx) {
        if (ctx.getChild(1).getText().equals("xor")) {
            Xor xor =
                    of.createXor().withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));
            libraryBuilder.resolveBinaryCall("System", "Xor", xor);
            return xor;
        } else {
            Or or = of.createOr().withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));
            libraryBuilder.resolveBinaryCall("System", "Or", or);
            return or;
        }
    }

    @Override
    public Expression visitImpliesExpression(cqlParser.ImpliesExpressionContext ctx) {
        Implies implies =
                of.createImplies().withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));

        libraryBuilder.resolveBinaryCall("System", "Implies", implies);
        return implies;
    }

    @Override
    public Object visitInFixSetExpression(cqlParser.InFixSetExpressionContext ctx) {
        String operator = ctx.getChild(1).getText();

        Expression left = parseExpression(ctx.expression(0));
        Expression right = parseExpression(ctx.expression(1));

        switch (operator) {
            case "|":
            case "union":
                return libraryBuilder.resolveUnion(left, right);
            case "intersect":
                return libraryBuilder.resolveIntersect(left, right);
            case "except":
                return libraryBuilder.resolveExcept(left, right);
        }

        return of.createNull();
    }

    @Override
    public Expression visitEqualityExpression(cqlParser.EqualityExpressionContext ctx) {
        String operator = parseString(ctx.getChild(1));
        if (operator.equals("~") || operator.equals("!~")) {
            BinaryExpression equivalent = of.createEquivalent()
                    .withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));

            libraryBuilder.resolveBinaryCall("System", "Equivalent", equivalent);

            if (!"~".equals(parseString(ctx.getChild(1)))) {
                track(equivalent, ctx);
                Not not = of.createNot().withOperand(equivalent);
                libraryBuilder.resolveUnaryCall("System", "Not", not);
                return not;
            }

            return equivalent;
        } else {
            BinaryExpression equal = of.createEqual()
                    .withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));

            libraryBuilder.resolveBinaryCall("System", "Equal", equal);
            if (!"=".equals(parseString(ctx.getChild(1)))) {
                track(equal, ctx);
                Not not = of.createNot().withOperand(equal);
                libraryBuilder.resolveUnaryCall("System", "Not", not);
                return not;
            }

            return equal;
        }
    }

    @Override
    public BinaryExpression visitInequalityExpression(cqlParser.InequalityExpressionContext ctx) {
        BinaryExpression exp;
        String operatorName;
        switch (parseString(ctx.getChild(1))) {
            case "<=":
                operatorName = "LessOrEqual";
                exp = of.createLessOrEqual();
                break;
            case "<":
                operatorName = "Less";
                exp = of.createLess();
                break;
            case ">":
                operatorName = "Greater";
                exp = of.createGreater();
                break;
            case ">=":
                operatorName = "GreaterOrEqual";
                exp = of.createGreaterOrEqual();
                break;
            default:
                throw new IllegalArgumentException(
                        String.format("Unknown operator: %s", ctx.getChild(1).getText()));
        }
        exp.withOperand(parseExpression(ctx.expression(0)), parseExpression(ctx.expression(1)));

        libraryBuilder.resolveBinaryCall("System", operatorName, exp);
        return exp;
    }

    @Override
    public List visitQualifiedIdentifier(cqlParser.QualifiedIdentifierContext ctx) {
        // Return the list of qualified identifiers for resolution by the containing
        // element
        List identifiers = new ArrayList<>();
        for (cqlParser.QualifierContext qualifierContext : ctx.qualifier()) {
            String qualifier = parseString(qualifierContext);
            identifiers.add(qualifier);
        }

        String identifier = parseString(ctx.identifier());
        identifiers.add(identifier);
        return identifiers;
    }

    @Override
    public List visitQualifiedIdentifierExpression(cqlParser.QualifiedIdentifierExpressionContext ctx) {
        // Return the list of qualified identifiers for resolution by the containing
        // element
        List identifiers = new ArrayList<>();
        for (cqlParser.QualifierExpressionContext qualifierContext : ctx.qualifierExpression()) {
            String qualifier = parseString(qualifierContext);
            identifiers.add(qualifier);
        }

        String identifier = parseString(ctx.referentialIdentifier());
        identifiers.add(identifier);
        return identifiers;
    }

    @Override
    public String visitSimplePathReferentialIdentifier(cqlParser.SimplePathReferentialIdentifierContext ctx) {
        return (String) visit(ctx.referentialIdentifier());
    }

    @Override
    public String visitSimplePathQualifiedIdentifier(cqlParser.SimplePathQualifiedIdentifierContext ctx) {
        return visit(ctx.simplePath()) + "." + visit(ctx.referentialIdentifier());
    }

    @Override
    public String visitSimplePathIndexer(cqlParser.SimplePathIndexerContext ctx) {
        return visit(ctx.simplePath()) + "[" + visit(ctx.simpleLiteral()) + "]";
    }

    @Override
    public Object visitTermExpression(cqlParser.TermExpressionContext ctx) {
        Object result = super.visitTermExpression(ctx);

        if (result instanceof LibraryRef) {
            // ERROR:
            throw new IllegalArgumentException(String.format(
                    "Identifier %s is a library and cannot be used as an expression.",
                    ((LibraryRef) result).getLibraryName()));
        }

        return result;
    }

    @Override
    public Object visitTerminal(TerminalNode node) {
        String text = node.getText();
        int tokenType = node.getSymbol().getType();

        if (cqlLexer.EOF == tokenType) {
            return null;
        }

        if (cqlLexer.STRING == tokenType
                || cqlLexer.QUOTEDIDENTIFIER == tokenType
                || cqlLexer.DELIMITEDIDENTIFIER == tokenType) {
            // chop off leading and trailing ', ", or `
            text = text.substring(1, text.length() - 1);

            // This is an alternate style of escaping that was removed when we switched to
            // industry-standard escape
            // sequences
            // if (cqlLexer.STRING == tokenType) {
            // text = text.replace("''", "'");
            // }
            // else {
            // text = text.replace("\"\"", "\"");
            // }
        }

        return text;
    }

    @Override
    public Object visitConversionExpressionTerm(cqlParser.ConversionExpressionTermContext ctx) {
        if (ctx.typeSpecifier() != null) {
            TypeSpecifier targetType = parseTypeSpecifier(ctx.typeSpecifier());
            Expression operand = parseExpression(ctx.expression());
            if (!DataTypes.equal(operand.getResultType(), targetType.getResultType())) {
                Conversion conversion =
                        libraryBuilder.findConversion(operand.getResultType(), targetType.getResultType(), false, true);
                if (conversion == null) {
                    // ERROR:
                    throw new IllegalArgumentException(String.format(
                            "Could not resolve conversion from type %s to type %s.",
                            operand.getResultType(), targetType.getResultType()));
                }

                return libraryBuilder.convertExpression(operand, conversion);
            }

            return operand;
        } else {
            String targetUnit = parseString(ctx.unit());
            targetUnit = libraryBuilder.ensureUcumUnit(targetUnit);
            Expression operand = parseExpression(ctx.expression());
            Expression unitOperand = libraryBuilder.createLiteral(targetUnit);
            track(unitOperand, ctx.unit());
            ConvertQuantity convertQuantity = of.createConvertQuantity().withOperand(operand, unitOperand);
            track(convertQuantity, ctx);
            return libraryBuilder.resolveBinaryCall("System", "ConvertQuantity", convertQuantity);
        }
    }

    @Override
    public Object visitTypeExpression(cqlParser.TypeExpressionContext ctx) {
        // NOTE: These don't use the buildIs or buildAs because those start with a
        // DataType, rather than a TypeSpecifier
        if (ctx.getChild(1).getText().equals("is")) {
            Is is = of.createIs()
                    .withOperand(parseExpression(ctx.expression()))
                    .withIsTypeSpecifier(parseTypeSpecifier(ctx.typeSpecifier()));
            is.setResultType(libraryBuilder.resolveTypeName("System", "Boolean"));
            return is;
        }

        As as = of.createAs()
                .withOperand(parseExpression(ctx.expression()))
                .withAsTypeSpecifier(parseTypeSpecifier(ctx.typeSpecifier()))
                .withStrict(false);
        DataType targetType = as.getAsTypeSpecifier().getResultType();
        DataTypes.verifyCast(targetType, as.getOperand().getResultType());
        as.setResultType(targetType);
        return as;
    }

    @Override
    public Object visitCastExpression(cqlParser.CastExpressionContext ctx) {
        // NOTE: This doesn't use buildAs because it starts with a DataType, rather than
        // a TypeSpecifier
        As as = of.createAs()
                .withOperand(parseExpression(ctx.expression()))
                .withAsTypeSpecifier(parseTypeSpecifier(ctx.typeSpecifier()))
                .withStrict(true);
        DataType targetType = as.getAsTypeSpecifier().getResultType();
        DataTypes.verifyCast(targetType, as.getOperand().getResultType());
        as.setResultType(targetType);
        return as;
    }

    @Override
    public Expression visitBooleanExpression(cqlParser.BooleanExpressionContext ctx) {
        UnaryExpression exp = null;
        Expression left = (Expression) visit(ctx.expression());
        String lastChild = ctx.getChild(ctx.getChildCount() - 1).getText();
        String nextToLast = ctx.getChild(ctx.getChildCount() - 2).getText();
        switch (lastChild) {
            case "null":
                exp = of.createIsNull().withOperand(left);
                libraryBuilder.resolveUnaryCall("System", "IsNull", exp);
                break;

            case "true":
                exp = of.createIsTrue().withOperand(left);
                libraryBuilder.resolveUnaryCall("System", "IsTrue", exp);
                break;

            case "false":
                exp = of.createIsFalse().withOperand(left);
                libraryBuilder.resolveUnaryCall("System", "IsFalse", exp);
                break;

            default:
                throw new IllegalArgumentException(String.format("Unknown boolean test predicate %s.", lastChild));
        }

        if ("not".equals(nextToLast)) {
            track(exp, ctx);
            exp = of.createNot().withOperand(exp);
            libraryBuilder.resolveUnaryCall("System", "Not", exp);
        }

        return exp;
    }

    @Override
    public Object visitTimingExpression(cqlParser.TimingExpressionContext ctx) {
        Expression left = parseExpression(ctx.expression(0));
        Expression right = parseExpression(ctx.expression(1));
        TimingOperatorContext timingOperatorContext = new TimingOperatorContext(left, right);
        timingOperators.push(timingOperatorContext);
        try {
            return visit(ctx.intervalOperatorPhrase());
        } finally {
            timingOperators.pop();
        }
    }

    @Override
    public Object visitConcurrentWithIntervalOperatorPhrase(cqlParser.ConcurrentWithIntervalOperatorPhraseContext ctx) {
        // ('starts' | 'ends' | 'occurs')? 'same' dateTimePrecision? (relativeQualifier
        // | 'as') ('start' | 'end')?
        TimingOperatorContext timingOperator = timingOperators.peek();
        ParseTree firstChild = ctx.getChild(0);
        if ("starts".equals(firstChild.getText())) {
            Start start = of.createStart().withOperand(timingOperator.getLeft());
            track(start, firstChild);
            libraryBuilder.resolveUnaryCall("System", "Start", start);
            timingOperator.setLeft(start);
        }

        if ("ends".equals(firstChild.getText())) {
            End end = of.createEnd().withOperand(timingOperator.getLeft());
            track(end, firstChild);
            libraryBuilder.resolveUnaryCall("System", "End", end);
            timingOperator.setLeft(end);
        }

        ParseTree lastChild = ctx.getChild(ctx.getChildCount() - 1);
        if ("start".equals(lastChild.getText())) {
            Start start = of.createStart().withOperand(timingOperator.getRight());
            track(start, lastChild);
            libraryBuilder.resolveUnaryCall("System", "Start", start);
            timingOperator.setRight(start);
        }

        if ("end".equals(lastChild.getText())) {
            End end = of.createEnd().withOperand(timingOperator.getRight());
            track(end, lastChild);
            libraryBuilder.resolveUnaryCall("System", "End", end);
            timingOperator.setRight(end);
        }

        String operatorName = null;
        BinaryExpression operator = null;
        boolean allowPromotionAndDemotion = false;
        if (ctx.relativeQualifier() == null) {
            if (ctx.dateTimePrecision() != null) {
                operator = of.createSameAs()
                        .withPrecision(parseComparableDateTimePrecision(
                                ctx.dateTimePrecision().getText()));
            } else {
                operator = of.createSameAs();
            }
            operatorName = "SameAs";
        } else {
            switch (ctx.relativeQualifier().getText()) {
                case "or after":
                    {
                        if (ctx.dateTimePrecision() != null) {
                            operator = of.createSameOrAfter()
                                    .withPrecision(parseComparableDateTimePrecision(
                                            ctx.dateTimePrecision().getText()));
                        } else {
                            operator = of.createSameOrAfter();
                        }
                        operatorName = "SameOrAfter";
                        allowPromotionAndDemotion = true;
                    }
                    break;
                case "or before":
                    {
                        if (ctx.dateTimePrecision() != null) {
                            operator = of.createSameOrBefore()
                                    .withPrecision(parseComparableDateTimePrecision(
                                            ctx.dateTimePrecision().getText()));
                        } else {
                            operator = of.createSameOrBefore();
                        }
                        operatorName = "SameOrBefore";
                        allowPromotionAndDemotion = true;
                    }
                    break;
                default:
                    throw new IllegalArgumentException(String.format(
                            "Unknown relative qualifier: '%s'.",
                            ctx.relativeQualifier().getText()));
            }
        }

        operator = operator.withOperand(timingOperator.getLeft(), timingOperator.getRight());
        libraryBuilder.resolveBinaryCall("System", operatorName, operator, true, allowPromotionAndDemotion);

        return operator;
    }

    @Override
    public Object visitIncludesIntervalOperatorPhrase(cqlParser.IncludesIntervalOperatorPhraseContext ctx) {
        // 'properly'? 'includes' dateTimePrecisionSpecifier? ('start' | 'end')?
        boolean isProper = false;
        boolean isRightPoint = false;
        TimingOperatorContext timingOperator = timingOperators.peek();
        for (ParseTree pt : ctx.children) {
            if ("properly".equals(pt.getText())) {
                isProper = true;
                continue;
            }

            if ("start".equals(pt.getText())) {
                Start start = of.createStart().withOperand(timingOperator.getRight());
                track(start, pt);
                libraryBuilder.resolveUnaryCall("System", "Start", start);
                timingOperator.setRight(start);
                isRightPoint = true;
                continue;
            }

            if ("end".equals(pt.getText())) {
                End end = of.createEnd().withOperand(timingOperator.getRight());
                track(end, pt);
                libraryBuilder.resolveUnaryCall("System", "End", end);
                timingOperator.setRight(end);
                isRightPoint = true;
                continue;
            }
        }

        String dateTimePrecision = ctx.dateTimePrecisionSpecifier() != null
                ? ctx.dateTimePrecisionSpecifier().dateTimePrecision().getText()
                : null;

        // If the right is not convertible to an interval or list
        // if (!isRightPoint &&
        // !(timingOperator.getRight().getResultType() instanceof IntervalType
        // || timingOperator.getRight().getResultType() instanceof ListType)) {
        // isRightPoint = true;
        // }

        if (isRightPoint) {
            if (isProper) {
                return libraryBuilder.resolveProperContains(
                        timingOperator.getLeft(),
                        timingOperator.getRight(),
                        parseComparableDateTimePrecision(dateTimePrecision, false));
            }

            return libraryBuilder.resolveContains(
                    timingOperator.getLeft(),
                    timingOperator.getRight(),
                    parseComparableDateTimePrecision(dateTimePrecision, false));
        }

        if (isProper) {
            return libraryBuilder.resolveProperIncludes(
                    timingOperator.getLeft(),
                    timingOperator.getRight(),
                    parseComparableDateTimePrecision(dateTimePrecision, false));
        }

        return libraryBuilder.resolveIncludes(
                timingOperator.getLeft(),
                timingOperator.getRight(),
                parseComparableDateTimePrecision(dateTimePrecision, false));
    }

    @Override
    public Object visitIncludedInIntervalOperatorPhrase(cqlParser.IncludedInIntervalOperatorPhraseContext ctx) {
        // ('starts' | 'ends' | 'occurs')? 'properly'? ('during' | 'included in')
        // dateTimePrecisionSpecifier?
        boolean isProper = false;
        boolean isLeftPoint = false;
        TimingOperatorContext timingOperator = timingOperators.peek();
        for (ParseTree pt : ctx.children) {
            if ("starts".equals(pt.getText())) {
                Start start = of.createStart().withOperand(timingOperator.getLeft());
                track(start, pt);
                libraryBuilder.resolveUnaryCall("System", "Start", start);
                timingOperator.setLeft(start);
                isLeftPoint = true;
                continue;
            }

            if ("ends".equals(pt.getText())) {
                End end = of.createEnd().withOperand(timingOperator.getLeft());
                track(end, pt);
                libraryBuilder.resolveUnaryCall("System", "End", end);
                timingOperator.setLeft(end);
                isLeftPoint = true;
                continue;
            }

            if ("properly".equals(pt.getText())) {
                isProper = true;
                continue;
            }
        }

        String dateTimePrecision = ctx.dateTimePrecisionSpecifier() != null
                ? ctx.dateTimePrecisionSpecifier().dateTimePrecision().getText()
                : null;

        // If the left is not convertible to an interval or list
        // if (!isLeftPoint &&
        // !(timingOperator.getLeft().getResultType() instanceof IntervalType
        // || timingOperator.getLeft().getResultType() instanceof ListType)) {
        // isLeftPoint = true;
        // }

        if (isLeftPoint) {
            if (isProper) {
                return libraryBuilder.resolveProperIn(
                        timingOperator.getLeft(),
                        timingOperator.getRight(),
                        parseComparableDateTimePrecision(dateTimePrecision, false));
            }

            return libraryBuilder.resolveIn(
                    timingOperator.getLeft(),
                    timingOperator.getRight(),
                    parseComparableDateTimePrecision(dateTimePrecision, false));
        }

        if (isProper) {
            return libraryBuilder.resolveProperIncludedIn(
                    timingOperator.getLeft(),
                    timingOperator.getRight(),
                    parseComparableDateTimePrecision(dateTimePrecision, false));
        }

        return libraryBuilder.resolveIncludedIn(
                timingOperator.getLeft(),
                timingOperator.getRight(),
                parseComparableDateTimePrecision(dateTimePrecision, false));
    }

    @Override
    public Object visitBeforeOrAfterIntervalOperatorPhrase(cqlParser.BeforeOrAfterIntervalOperatorPhraseContext ctx) {
        // ('starts' | 'ends' | 'occurs')? quantityOffset? ('before' | 'after')
        // dateTimePrecisionSpecifier? ('start' |
        // 'end')?

        // duration before/after
        // A starts 3 days before start B
        // * start of A same day as start of B - 3 days
        // A starts 3 days after start B
        // * start of A same day as start of B + 3 days

        // or more/less duration before/after
        // A starts 3 days or more before start B
        // * start of A <= start of B - 3 days
        // A starts 3 days or more after start B
        // * start of A >= start of B + 3 days
        // A starts 3 days or less before start B
        // * start of A in [start of B - 3 days, start of B) and B is not null
        // A starts 3 days or less after start B
        // * start of A in (start of B, start of B + 3 days] and B is not null

        // less/more than duration before/after
        // A starts more than 3 days before start B
        // * start of A < start of B - 3 days
        // A starts more than 3 days after start B
        // * start of A > start of B + 3 days
        // A starts less than 3 days before start B
        // * start of A in (start of B - 3 days, start of B)
        // A starts less than 3 days after start B
        // * start of A in (start of B, start of B + 3 days)

        TimingOperatorContext timingOperator = timingOperators.peek();
        boolean isBefore = false;
        boolean isInclusive = false;
        for (ParseTree child : ctx.children) {
            if ("starts".equals(child.getText())) {
                Start start = of.createStart().withOperand(timingOperator.getLeft());
                track(start, child);
                libraryBuilder.resolveUnaryCall("System", "Start", start);
                timingOperator.setLeft(start);
                continue;
            }

            if ("ends".equals(child.getText())) {
                End end = of.createEnd().withOperand(timingOperator.getLeft());
                track(end, child);
                libraryBuilder.resolveUnaryCall("System", "End", end);
                timingOperator.setLeft(end);
                continue;
            }

            if ("start".equals(child.getText())) {
                Start start = of.createStart().withOperand(timingOperator.getRight());
                track(start, child);
                libraryBuilder.resolveUnaryCall("System", "Start", start);
                timingOperator.setRight(start);
                continue;
            }

            if ("end".equals(child.getText())) {
                End end = of.createEnd().withOperand(timingOperator.getRight());
                track(end, child);
                libraryBuilder.resolveUnaryCall("System", "End", end);
                timingOperator.setRight(end);
                continue;
            }
        }

        for (ParseTree child : ctx.temporalRelationship().children) {
            if ("before".equals(child.getText())) {
                isBefore = true;
                continue;
            }

            if ("on or".equals(child.getText()) || "or on".equals(child.getText())) {
                isInclusive = true;
                continue;
            }
        }

        String dateTimePrecision = ctx.dateTimePrecisionSpecifier() != null
                ? ctx.dateTimePrecisionSpecifier().dateTimePrecision().getText()
                : null;

        if (ctx.quantityOffset() == null) {
            if (isInclusive) {
                if (isBefore) {
                    SameOrBefore sameOrBefore =
                            of.createSameOrBefore().withOperand(timingOperator.getLeft(), timingOperator.getRight());
                    if (dateTimePrecision != null) {
                        sameOrBefore.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                    }
                    libraryBuilder.resolveBinaryCall("System", "SameOrBefore", sameOrBefore, true, true);
                    return sameOrBefore;

                } else {
                    SameOrAfter sameOrAfter =
                            of.createSameOrAfter().withOperand(timingOperator.getLeft(), timingOperator.getRight());
                    if (dateTimePrecision != null) {
                        sameOrAfter.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                    }
                    libraryBuilder.resolveBinaryCall("System", "SameOrAfter", sameOrAfter, true, true);
                    return sameOrAfter;
                }
            } else {
                if (isBefore) {
                    Before before = of.createBefore().withOperand(timingOperator.getLeft(), timingOperator.getRight());
                    if (dateTimePrecision != null) {
                        before.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                    }
                    libraryBuilder.resolveBinaryCall("System", "Before", before, true, true);
                    return before;

                } else {
                    After after = of.createAfter().withOperand(timingOperator.getLeft(), timingOperator.getRight());
                    if (dateTimePrecision != null) {
                        after.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                    }
                    libraryBuilder.resolveBinaryCall("System", "After", after, true, true);
                    return after;
                }
            }
        } else {
            Quantity quantity = (Quantity) visit(ctx.quantityOffset().quantity());

            if (timingOperator.getLeft().getResultType() instanceof IntervalType) {
                if (isBefore) {
                    End end = of.createEnd().withOperand(timingOperator.getLeft());
                    track(end, timingOperator.getLeft());
                    libraryBuilder.resolveUnaryCall("System", "End", end);
                    timingOperator.setLeft(end);
                } else {
                    Start start = of.createStart().withOperand(timingOperator.getLeft());
                    track(start, timingOperator.getLeft());
                    libraryBuilder.resolveUnaryCall("System", "Start", start);
                    timingOperator.setLeft(start);
                }
            }

            if (timingOperator.getRight().getResultType() instanceof IntervalType) {
                if (isBefore) {
                    Start start = of.createStart().withOperand(timingOperator.getRight());
                    track(start, timingOperator.getRight());
                    libraryBuilder.resolveUnaryCall("System", "Start", start);
                    timingOperator.setRight(start);
                } else {
                    End end = of.createEnd().withOperand(timingOperator.getRight());
                    track(end, timingOperator.getRight());
                    libraryBuilder.resolveUnaryCall("System", "End", end);
                    timingOperator.setRight(end);
                }
            }

            if (ctx.quantityOffset().offsetRelativeQualifier() == null
                    && ctx.quantityOffset().exclusiveRelativeQualifier() == null) {
                // Use a SameAs
                // For a Before, subtract the quantity from the right operand
                // For an After, add the quantity to the right operand
                if (isBefore) {
                    Subtract subtract = of.createSubtract().withOperand(timingOperator.getRight(), quantity);
                    track(subtract, timingOperator.getRight());
                    libraryBuilder.resolveBinaryCall("System", "Subtract", subtract);
                    timingOperator.setRight(subtract);
                } else {
                    Add add = of.createAdd().withOperand(timingOperator.getRight(), quantity);
                    track(add, timingOperator.getRight());
                    libraryBuilder.resolveBinaryCall("System", "Add", add);
                    timingOperator.setRight(add);
                }

                SameAs sameAs = of.createSameAs().withOperand(timingOperator.getLeft(), timingOperator.getRight());
                if (dateTimePrecision != null) {
                    sameAs.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                }
                libraryBuilder.resolveBinaryCall("System", "SameAs", sameAs);
                return sameAs;
            } else {
                boolean isOffsetInclusive = ctx.quantityOffset().offsetRelativeQualifier() != null;
                String qualifier = ctx.quantityOffset().offsetRelativeQualifier() != null
                        ? ctx.quantityOffset().offsetRelativeQualifier().getText()
                        : ctx.quantityOffset().exclusiveRelativeQualifier().getText();

                switch (qualifier) {
                    case "more than":
                    case "or more":
                        // For More Than/Or More, Use a Before/After/SameOrBefore/SameOrAfter
                        // For a Before, subtract the quantity from the right operand
                        // For an After, add the quantity to the right operand
                        if (isBefore) {
                            Subtract subtract = of.createSubtract().withOperand(timingOperator.getRight(), quantity);
                            track(subtract, timingOperator.getRight());
                            libraryBuilder.resolveBinaryCall("System", "Subtract", subtract);
                            timingOperator.setRight(subtract);

                            if (!isOffsetInclusive) {
                                Before before = of.createBefore()
                                        .withOperand(timingOperator.getLeft(), timingOperator.getRight());
                                if (dateTimePrecision != null) {
                                    before.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                                }
                                libraryBuilder.resolveBinaryCall("System", "Before", before, true, true);
                                return before;
                            } else {
                                SameOrBefore sameOrBefore = of.createSameOrBefore()
                                        .withOperand(timingOperator.getLeft(), timingOperator.getRight());
                                if (dateTimePrecision != null) {
                                    sameOrBefore.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                                }
                                libraryBuilder.resolveBinaryCall("System", "SameOrBefore", sameOrBefore, true, true);
                                return sameOrBefore;
                            }
                        } else {
                            Add add = of.createAdd().withOperand(timingOperator.getRight(), quantity);
                            track(add, timingOperator.getRight());
                            libraryBuilder.resolveBinaryCall("System", "Add", add);
                            timingOperator.setRight(add);

                            if (!isOffsetInclusive) {
                                After after = of.createAfter()
                                        .withOperand(timingOperator.getLeft(), timingOperator.getRight());
                                if (dateTimePrecision != null) {
                                    after.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                                }
                                libraryBuilder.resolveBinaryCall("System", "After", after, true, true);
                                return after;
                            } else {
                                SameOrAfter sameOrAfter = of.createSameOrAfter()
                                        .withOperand(timingOperator.getLeft(), timingOperator.getRight());
                                if (dateTimePrecision != null) {
                                    sameOrAfter.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                                }
                                libraryBuilder.resolveBinaryCall("System", "SameOrAfter", sameOrAfter, true, true);
                                return sameOrAfter;
                            }
                        }

                    case "less than":
                    case "or less":
                        // For Less Than/Or Less, Use an In
                        // For Before, construct an interval from right - quantity to right
                        // For After, construct an interval from right to right + quantity
                        Expression lowerBound = null;
                        Expression upperBound = null;
                        Expression right = timingOperator.getRight();
                        if (isBefore) {
                            lowerBound = of.createSubtract().withOperand(right, quantity);
                            track(lowerBound, right);
                            libraryBuilder.resolveBinaryCall("System", "Subtract", (BinaryExpression) lowerBound);
                            upperBound = right;
                        } else {
                            lowerBound = right;
                            upperBound = of.createAdd().withOperand(right, quantity);
                            track(upperBound, right);
                            libraryBuilder.resolveBinaryCall("System", "Add", (BinaryExpression) upperBound);
                        }

                        // 3 days or less before -> [B - 3 days, B)
                        // less than 3 days before -> (B - 3 days, B)
                        // 3 days or less after -> (B, B + 3 days]
                        // less than 3 days after -> (B, B + 3 days)
                        Interval interval = isBefore
                                ? libraryBuilder.createInterval(lowerBound, isOffsetInclusive, upperBound, isInclusive)
                                : libraryBuilder.createInterval(lowerBound, isInclusive, upperBound, isOffsetInclusive);

                        track(interval, ctx.quantityOffset());
                        In in = of.createIn().withOperand(timingOperator.getLeft(), interval);
                        if (dateTimePrecision != null) {
                            in.setPrecision(parseComparableDateTimePrecision(dateTimePrecision));
                        }
                        track(in, ctx.quantityOffset());
                        libraryBuilder.resolveBinaryCall("System", "In", in);

                        // if the offset or comparison is inclusive, add a null check for B to ensure
                        // correct
                        // interpretation
                        if (isOffsetInclusive || isInclusive) {
                            IsNull nullTest = of.createIsNull().withOperand(right);
                            track(nullTest, ctx.quantityOffset());
                            libraryBuilder.resolveUnaryCall("System", "IsNull", nullTest);
                            Not notNullTest = of.createNot().withOperand(nullTest);
                            track(notNullTest, ctx.quantityOffset());
                            libraryBuilder.resolveUnaryCall("System", "Not", notNullTest);
                            And and = of.createAnd().withOperand(in, notNullTest);
                            track(and, ctx.quantityOffset());
                            libraryBuilder.resolveBinaryCall("System", "And", and);
                            return and;
                        }

                        // Otherwise, return the constructed in
                        return in;
                }
            }
        }

        throw new IllegalArgumentException("Unable to resolve interval operator phrase.");
    }

    private BinaryExpression resolveBetweenOperator(String unit, Expression left, Expression right) {
        if (unit != null) {
            DurationBetween between = of.createDurationBetween()
                    .withPrecision(parseDateTimePrecision(unit))
                    .withOperand(left, right);
            libraryBuilder.resolveBinaryCall("System", "DurationBetween", between);
            return between;
        }

        return null;
    }

    @Override
    public Object visitWithinIntervalOperatorPhrase(cqlParser.WithinIntervalOperatorPhraseContext ctx) {
        // ('starts' | 'ends' | 'occurs')? 'properly'? 'within' quantityLiteral 'of'
        // ('start' | 'end')?
        // A starts within 3 days of start B
        // * start of A in [start of B - 3 days, start of B + 3 days] and start B is not
        // null
        // A starts within 3 days of B
        // * start of A in [start of B - 3 days, end of B + 3 days]

        TimingOperatorContext timingOperator = timingOperators.peek();
        boolean isProper = false;
        for (ParseTree child : ctx.children) {
            if ("starts".equals(child.getText())) {
                Start start = of.createStart().withOperand(timingOperator.getLeft());
                track(start, child);
                libraryBuilder.resolveUnaryCall("System", "Start", start);
                timingOperator.setLeft(start);
                continue;
            }

            if ("ends".equals(child.getText())) {
                End end = of.createEnd().withOperand(timingOperator.getLeft());
                track(end, child);
                libraryBuilder.resolveUnaryCall("System", "End", end);
                timingOperator.setLeft(end);
                continue;
            }

            if ("start".equals(child.getText())) {
                Start start = of.createStart().withOperand(timingOperator.getRight());
                track(start, child);
                libraryBuilder.resolveUnaryCall("System", "Start", start);
                timingOperator.setRight(start);
                continue;
            }

            if ("end".equals(child.getText())) {
                End end = of.createEnd().withOperand(timingOperator.getRight());
                track(end, child);
                libraryBuilder.resolveUnaryCall("System", "End", end);
                timingOperator.setRight(end);
                continue;
            }

            if ("properly".equals(child.getText())) {
                isProper = true;
                continue;
            }
        }

        Quantity quantity = (Quantity) visit(ctx.quantity());
        Expression lowerBound = null;
        Expression upperBound = null;
        Expression initialBound = null;
        if (timingOperator.getRight().getResultType() instanceof IntervalType) {
            lowerBound = of.createStart().withOperand(timingOperator.getRight());
            track(lowerBound, ctx.quantity());
            libraryBuilder.resolveUnaryCall("System", "Start", (Start) lowerBound);
            upperBound = of.createEnd().withOperand(timingOperator.getRight());
            track(upperBound, ctx.quantity());
            libraryBuilder.resolveUnaryCall("System", "End", (End) upperBound);
        } else {
            lowerBound = timingOperator.getRight();
            upperBound = timingOperator.getRight();
            initialBound = lowerBound;
        }

        lowerBound = of.createSubtract().withOperand(lowerBound, quantity);
        track(lowerBound, ctx.quantity());
        libraryBuilder.resolveBinaryCall("System", "Subtract", (BinaryExpression) lowerBound);

        upperBound = of.createAdd().withOperand(upperBound, quantity);
        track(upperBound, ctx.quantity());
        libraryBuilder.resolveBinaryCall("System", "Add", (BinaryExpression) upperBound);

        Interval interval = libraryBuilder.createInterval(lowerBound, !isProper, upperBound, !isProper);
        track(interval, ctx.quantity());

        In in = of.createIn().withOperand(timingOperator.getLeft(), interval);
        libraryBuilder.resolveBinaryCall("System", "In", in);

        // if the within is not proper and the interval is being constructed from a
        // single point, add a null check for
        // that point to ensure correct interpretation
        if (!isProper && (initialBound != null)) {
            IsNull nullTest = of.createIsNull().withOperand(initialBound);
            track(nullTest, ctx.quantity());
            libraryBuilder.resolveUnaryCall("System", "IsNull", nullTest);
            Not notNullTest = of.createNot().withOperand(nullTest);
            track(notNullTest, ctx.quantity());
            libraryBuilder.resolveUnaryCall("System", "Not", notNullTest);
            And and = of.createAnd().withOperand(in, notNullTest);
            track(and, ctx.quantity());
            libraryBuilder.resolveBinaryCall("System", "And", and);
            return and;
        }

        // Otherwise, return the constructed in
        return in;
    }

    @Override
    public Object visitMeetsIntervalOperatorPhrase(cqlParser.MeetsIntervalOperatorPhraseContext ctx) {
        String operatorName = null;
        BinaryExpression operator;
        String dateTimePrecision = ctx.dateTimePrecisionSpecifier() != null
                ? ctx.dateTimePrecisionSpecifier().dateTimePrecision().getText()
                : null;

        if (ctx.getChildCount() == (1 + (dateTimePrecision == null ? 0 : 1))) {
            operator = dateTimePrecision != null
                    ? of.createMeets().withPrecision(parseComparableDateTimePrecision(dateTimePrecision))
                    : of.createMeets();
            operatorName = "Meets";
        } else {
            if ("before".equals(ctx.getChild(1).getText())) {
                operator = dateTimePrecision != null
                        ? of.createMeetsBefore().withPrecision(parseComparableDateTimePrecision(dateTimePrecision))
                        : of.createMeetsBefore();
                operatorName = "MeetsBefore";
            } else {
                operator = dateTimePrecision != null
                        ? of.createMeetsAfter().withPrecision(parseComparableDateTimePrecision(dateTimePrecision))
                        : of.createMeetsAfter();
                operatorName = "MeetsAfter";
            }
        }

        operator.withOperand(
                timingOperators.peek().getLeft(), timingOperators.peek().getRight());
        libraryBuilder.resolveBinaryCall("System", operatorName, operator);
        return operator;
    }

    @Override
    public Object visitOverlapsIntervalOperatorPhrase(cqlParser.OverlapsIntervalOperatorPhraseContext ctx) {
        String operatorName = null;
        BinaryExpression operator;
        String dateTimePrecision = ctx.dateTimePrecisionSpecifier() != null
                ? ctx.dateTimePrecisionSpecifier().dateTimePrecision().getText()
                : null;

        if (ctx.getChildCount() == (1 + (dateTimePrecision == null ? 0 : 1))) {
            operator = dateTimePrecision != null
                    ? of.createOverlaps().withPrecision(parseComparableDateTimePrecision(dateTimePrecision))
                    : of.createOverlaps();
            operatorName = "Overlaps";
        } else {
            if ("before".equals(ctx.getChild(1).getText())) {
                operator = dateTimePrecision != null
                        ? of.createOverlapsBefore().withPrecision(parseComparableDateTimePrecision(dateTimePrecision))
                        : of.createOverlapsBefore();
                operatorName = "OverlapsBefore";
            } else {
                operator = dateTimePrecision != null
                        ? of.createOverlapsAfter().withPrecision(parseComparableDateTimePrecision(dateTimePrecision))
                        : of.createOverlapsAfter();
                operatorName = "OverlapsAfter";
            }
        }

        operator.withOperand(
                timingOperators.peek().getLeft(), timingOperators.peek().getRight());
        libraryBuilder.resolveBinaryCall("System", operatorName, operator);
        return operator;
    }

    @Override
    public Object visitStartsIntervalOperatorPhrase(cqlParser.StartsIntervalOperatorPhraseContext ctx) {
        String dateTimePrecision = ctx.dateTimePrecisionSpecifier() != null
                ? ctx.dateTimePrecisionSpecifier().dateTimePrecision().getText()
                : null;

        Starts starts = (dateTimePrecision != null
                        ? of.createStarts().withPrecision(parseComparableDateTimePrecision(dateTimePrecision))
                        : of.createStarts())
                .withOperand(
                        timingOperators.peek().getLeft(), timingOperators.peek().getRight());

        libraryBuilder.resolveBinaryCall("System", "Starts", starts);
        return starts;
    }

    @Override
    public Object visitEndsIntervalOperatorPhrase(cqlParser.EndsIntervalOperatorPhraseContext ctx) {
        String dateTimePrecision = ctx.dateTimePrecisionSpecifier() != null
                ? ctx.dateTimePrecisionSpecifier().dateTimePrecision().getText()
                : null;

        Ends ends = (dateTimePrecision != null
                        ? of.createEnds().withPrecision(parseComparableDateTimePrecision(dateTimePrecision))
                        : of.createEnds())
                .withOperand(
                        timingOperators.peek().getLeft(), timingOperators.peek().getRight());

        libraryBuilder.resolveBinaryCall("System", "Ends", ends);
        return ends;
    }

    public Expression resolveIfThenElse(If ifObject) {
        ifObject.setCondition(libraryBuilder.ensureCompatible(
                ifObject.getCondition(), libraryBuilder.resolveTypeName("System", "Boolean")));
        DataType resultType = libraryBuilder.ensureCompatibleTypes(
                ifObject.getThen().getResultType(), ifObject.getElse().getResultType());
        ifObject.setResultType(resultType);
        ifObject.setThen(libraryBuilder.ensureCompatible(ifObject.getThen(), resultType));
        ifObject.setElse(libraryBuilder.ensureCompatible(ifObject.getElse(), resultType));
        return ifObject;
    }

    @Override
    public Object visitIfThenElseExpressionTerm(cqlParser.IfThenElseExpressionTermContext ctx) {
        If ifObject = of.createIf()
                .withCondition(parseExpression(ctx.expression(0)))
                .withThen(parseExpression(ctx.expression(1)))
                .withElse(parseExpression(ctx.expression(2)));

        return resolveIfThenElse(ifObject);
    }

    @Override
    public Object visitCaseExpressionTerm(cqlParser.CaseExpressionTermContext ctx) {
        Case result = of.createCase();
        Boolean hitElse = false;
        DataType resultType = null;
        for (ParseTree pt : ctx.children) {
            if ("else".equals(pt.getText())) {
                hitElse = true;
                continue;
            }

            if (pt instanceof cqlParser.ExpressionContext) {
                if (hitElse) {
                    result.setElse(parseExpression(pt));
                    resultType = libraryBuilder.ensureCompatibleTypes(
                            resultType, result.getElse().getResultType());
                } else {
                    result.setComparand(parseExpression(pt));
                }
            }

            if (pt instanceof cqlParser.CaseExpressionItemContext) {
                CaseItem caseItem = (CaseItem) visit(pt);
                if (result.getComparand() != null) {
                    libraryBuilder.verifyType(
                            caseItem.getWhen().getResultType(),
                            result.getComparand().getResultType());
                } else {
                    DataTypes.verifyType(
                            caseItem.getWhen().getResultType(), libraryBuilder.resolveTypeName("System", "Boolean"));
                }

                if (resultType == null) {
                    resultType = caseItem.getThen().getResultType();
                } else {
                    resultType = libraryBuilder.ensureCompatibleTypes(
                            resultType, caseItem.getThen().getResultType());
                }

                result.getCaseItem().add(caseItem);
            }
        }

        for (CaseItem caseItem : result.getCaseItem()) {
            if (result.getComparand() != null) {
                caseItem.setWhen(libraryBuilder.ensureCompatible(
                        caseItem.getWhen(), result.getComparand().getResultType()));
            }

            caseItem.setThen(libraryBuilder.ensureCompatible(caseItem.getThen(), resultType));
        }

        result.setElse(libraryBuilder.ensureCompatible(result.getElse(), resultType));
        result.setResultType(resultType);
        return result;
    }

    @Override
    public Object visitCaseExpressionItem(cqlParser.CaseExpressionItemContext ctx) {
        return of.createCaseItem()
                .withWhen(parseExpression(ctx.expression(0)))
                .withThen(parseExpression(ctx.expression(1)));
    }

    @Override
    public Object visitAggregateExpressionTerm(cqlParser.AggregateExpressionTermContext ctx) {
        switch (ctx.getChild(0).getText()) {
            case "distinct":
                Distinct distinct = of.createDistinct().withOperand(parseExpression(ctx.expression()));
                libraryBuilder.resolveUnaryCall("System", "Distinct", distinct);
                return distinct;
            case "flatten":
                Flatten flatten = of.createFlatten().withOperand(parseExpression(ctx.expression()));
                libraryBuilder.resolveUnaryCall("System", "Flatten", flatten);
                return flatten;
        }

        throw new IllegalArgumentException(
                String.format("Unknown aggregate operator %s.", ctx.getChild(0).getText()));
    }

    @Override
    public Object visitSetAggregateExpressionTerm(cqlParser.SetAggregateExpressionTermContext ctx) {
        Expression source = parseExpression(ctx.expression(0));
        Expression per = null;
        if (ctx.dateTimePrecision() != null) {
            per = libraryBuilder.createQuantity(BigDecimal.valueOf(1.0), parseString(ctx.dateTimePrecision()));
        } else if (ctx.expression().size() > 1) {
            per = parseExpression(ctx.expression(1));
        } else {
            // Determine per quantity based on point type of the intervals involved
            if (source.getResultType() instanceof ListType) {
                ListType listType = (ListType) source.getResultType();
                if (listType.getElementType() instanceof IntervalType) {
                    IntervalType intervalType = (IntervalType) listType.getElementType();
                    DataType pointType = intervalType.getPointType();

                    per = libraryBuilder.buildNull(libraryBuilder.resolveTypeName("System", "Quantity"));

                    // TODO: Test this...
                    // // Successor(MinValue) - MinValue
                    // MinValue minimum = libraryBuilder.buildMinimum(pointType);
                    // track(minimum, ctx);
                    //
                    // Expression successor = libraryBuilder.buildSuccessor(minimum);
                    // track(successor, ctx);
                    //
                    // minimum = libraryBuilder.buildMinimum(pointType);
                    // track(minimum, ctx);
                    //
                    // Subtract subtract = of.createSubtract().withOperand(successor, minimum);
                    // libraryBuilder.resolveBinaryCall("System", "Subtract", subtract);
                    // per = subtract;
                }
            } else {
                per = libraryBuilder.buildNull(libraryBuilder.resolveTypeName("System", "Quantity"));
            }
        }

        switch (ctx.getChild(0).getText()) {
            case "expand":
                Expand expand = of.createExpand().withOperand(source, per);
                libraryBuilder.resolveBinaryCall("System", "Expand", expand);
                return expand;

            case "collapse":
                Collapse collapse = of.createCollapse().withOperand(source, per);
                libraryBuilder.resolveBinaryCall("System", "Collapse", collapse);
                return collapse;
        }

        throw new IllegalArgumentException(String.format(
                "Unknown aggregate set operator %s.", ctx.getChild(0).getText()));
    }

    @Override
    @SuppressWarnings("unchecked")
    public Expression visitRetrieve(cqlParser.RetrieveContext ctx) {
        libraryBuilder.checkLiteralContext();
        List qualifiers = parseQualifiers(ctx.namedTypeSpecifier());
        String model = getModelIdentifier(qualifiers);
        String label = getTypeIdentifier(
                qualifiers, parseString(ctx.namedTypeSpecifier().referentialOrTypeNameIdentifier()));
        DataType dataType = libraryBuilder.resolveTypeName(model, label);
        if (dataType == null) {
            // ERROR:
            throw new IllegalArgumentException(String.format("Could not resolve type name %s.", label));
        }

        if (!(dataType instanceof ClassType) || !((ClassType) dataType).isRetrievable()) {
            // ERROR:
            throw new IllegalArgumentException(
                    String.format("Specified data type %s does not support retrieval.", label));
        }

        ClassType classType = (ClassType) dataType;
        // BTR -> The original intent of this code was to have the retrieve return the
        // base type, and use the
        // "templateId"
        // element of the retrieve to communicate the "positive" or "negative" profile
        // to the data access layer.
        // However, because this notion of carrying the "profile" through a type is not
        // general, it causes
        // inconsistencies
        // when using retrieve results with functions defined in terms of the same type
        // (see GitHub Issue #131).
        // Based on the discussion there, the retrieve will now return the declared
        // type, whether it is a profile or
        // not.
        // ProfileType profileType = dataType instanceof ProfileType ?
        // (ProfileType)dataType : null;
        // NamedType namedType = profileType == null ? classType :
        // (NamedType)classType.getBaseType();
        NamedType namedType = classType;

        ModelInfo modelInfo = libraryBuilder.getModel(namedType.getNamespace()).getModelInfo();
        boolean useStrictRetrieveTyping =
                modelInfo.isStrictRetrieveTyping() != null && modelInfo.isStrictRetrieveTyping();

        String codePath = null;
        Property property = null;
        CqlCompilerException propertyException = null;
        Expression terminology = null;
        String codeComparator = null;
        if (ctx.terminology() != null) {
            if (ctx.codePath() != null) {
                String identifiers = (String) visit(ctx.codePath());
                codePath = identifiers;
            } else if (classType.getPrimaryCodePath() != null) {
                codePath = classType.getPrimaryCodePath();
            }

            if (codePath == null) {
                // ERROR:
                // WARNING:
                propertyException = new CqlSemanticException(
                        "Retrieve has a terminology target but does not specify a code path and the type of the retrieve does not have a primary code path defined.",
                        useStrictRetrieveTyping
                                ? CqlCompilerException.ErrorSeverity.Error
                                : CqlCompilerException.ErrorSeverity.Warning,
                        getTrackBack(ctx));
                libraryBuilder.recordParsingException(propertyException);
            } else {
                try {
                    DataType codeType = libraryBuilder.resolvePath((DataType) namedType, codePath);
                    property = of.createProperty().withPath(codePath);
                    property.setResultType(codeType);
                } catch (Exception e) {
                    // ERROR:
                    // WARNING:
                    propertyException = new CqlSemanticException(
                            String.format(
                                    "Could not resolve code path %s for the type of the retrieve %s.",
                                    codePath, namedType.getName()),
                            useStrictRetrieveTyping
                                    ? CqlCompilerException.ErrorSeverity.Error
                                    : CqlCompilerException.ErrorSeverity.Warning,
                            getTrackBack(ctx),
                            e);
                    libraryBuilder.recordParsingException(propertyException);
                }
            }

            if (ctx.terminology().qualifiedIdentifierExpression() != null) {
                List identifiers = (List) visit(ctx.terminology());
                terminology = resolveQualifiedIdentifier(identifiers);
                track(terminology, ctx.terminology().qualifiedIdentifierExpression());
            } else {
                terminology = parseExpression(ctx.terminology().expression());
            }

            codeComparator = ctx.codeComparator() != null ? (String) visit(ctx.codeComparator()) : null;
        }

        Expression result = null;

        // Only expand a choice-valued code path if no comparator is specified
        // Otherwise, a code comparator will always choose a specific representation
        boolean hasFHIRHelpers = libraryInfo.resolveLibraryName("FHIRHelpers") != null;
        if (property != null && property.getResultType() instanceof ChoiceType && codeComparator == null) {
            for (DataType propertyType : ((ChoiceType) property.getResultType()).getTypes()) {
                if (hasFHIRHelpers
                        && propertyType instanceof NamedType
                        && ((NamedType) propertyType).getSimpleName().equals("Reference")
                        && namedType.getSimpleName().equals("MedicationRequest")) {
                    // TODO: This is a model-specific special case to support QICore
                    // This functionality needs to be generalized to a retrieve mapping in the model
                    // info
                    // But that requires a model info change (to represent references, right now the
                    // model info only
                    // includes context relationships)
                    // The reference expands to [MedicationRequest] MR with [Medication] M such that
                    // M.id =
                    // Last(Split(MR.medication.reference, '/')) and M.code in 
                    Retrieve mrRetrieve = buildRetrieve(
                            ctx, useStrictRetrieveTyping, namedType, classType, null, null, null, null, null, null);
                    retrieves.add(mrRetrieve);
                    mrRetrieve.setResultType(new ListType((DataType) namedType));
                    DataType mDataType = libraryBuilder.resolveTypeName(model, "Medication");
                    ClassType mClassType = (ClassType) mDataType;
                    NamedType mNamedType = mClassType;
                    Retrieve mRetrieve = buildRetrieve(
                            ctx, useStrictRetrieveTyping, mNamedType, mClassType, null, null, null, null, null, null);
                    retrieves.add(mRetrieve);
                    mRetrieve.setResultType(new ListType((DataType) namedType));
                    Query q = of.createQuery();
                    AliasedQuerySource aqs = of.createAliasedQuerySource()
                            .withExpression(mrRetrieve)
                            .withAlias("MR");
                    track(aqs, ctx);
                    aqs.setResultType(aqs.getExpression().getResultType());
                    q.getSource().add(aqs);
                    track(q, ctx);
                    q.setResultType(aqs.getResultType());
                    With w = of.createWith().withExpression(mRetrieve).withAlias("M");
                    track(w, ctx);
                    w.setResultType(w.getExpression().getResultType());
                    q.getRelationship().add(w);
                    String idPath = "id";
                    DataType idType = libraryBuilder.resolvePath(mDataType, idPath);
                    Property idProperty = libraryBuilder.buildProperty("M", idPath, false, idType);
                    String refPath = "medication.reference";
                    DataType refType = libraryBuilder.resolvePath(dataType, refPath);
                    Property refProperty = libraryBuilder.buildProperty("MR", refPath, false, refType);
                    Split split = of.createSplit()
                            .withStringToSplit(refProperty)
                            .withSeparator(libraryBuilder.createLiteral("/"));
                    libraryBuilder.resolveCall("System", "Split", new SplitInvocation(split));
                    Last last = of.createLast().withSource(split);
                    libraryBuilder.resolveCall("System", "Last", new LastInvocation(last));
                    Equal e = of.createEqual().withOperand(idProperty, last);
                    libraryBuilder.resolveBinaryCall("System", "Equal", e);

                    DataType mCodeType = libraryBuilder.resolvePath((DataType) mNamedType, "code");
                    Property mProperty = of.createProperty().withPath("code");
                    mProperty.setResultType(mCodeType);
                    String mCodeComparator = "~";
                    if (terminology.getResultType() instanceof ListType) {
                        mCodeComparator = "in";

                    } else if (libraryBuilder.isCompatibleWith("1.5")) {
                        mCodeComparator = terminology
                                        .getResultType()
                                        .isSubTypeOf(libraryBuilder.resolveTypeName("System", "Vocabulary"))
                                ? "in"
                                : "~";
                    }

                    Expression terminologyComparison = null;
                    if (mCodeComparator.equals("in")) {
                        terminologyComparison = libraryBuilder.resolveIn(mProperty, terminology);
                    } else {
                        BinaryExpression equivalent = of.createEquivalent().withOperand(mProperty, terminology);
                        libraryBuilder.resolveBinaryCall("System", "Equivalent", equivalent);
                        terminologyComparison = equivalent;
                    }
                    And a = of.createAnd().withOperand(e, terminologyComparison);
                    libraryBuilder.resolveBinaryCall("System", "And", a);
                    w.withSuchThat(a);

                    if (result == null) {
                        result = q;
                    } else {
                        track(q, ctx);
                        result = libraryBuilder.resolveUnion(result, q);
                    }
                } else {
                    Retrieve retrieve = buildRetrieve(
                            ctx,
                            useStrictRetrieveTyping,
                            namedType,
                            classType,
                            codePath,
                            codeComparator,
                            property,
                            propertyType,
                            propertyException,
                            terminology);
                    retrieves.add(retrieve);
                    retrieve.setResultType(new ListType((DataType) namedType));

                    if (result == null) {
                        result = retrieve;
                    } else {
                        // Should only include the result if it resolved appropriately with the
                        // codeComparator
                        // Allowing it to go through for now
                        // if (retrieve.getCodeProperty() != null && retrieve.getCodeComparator() !=
                        // null &&
                        // retrieve.getCodes() != null) {
                        track(retrieve, ctx);
                        result = libraryBuilder.resolveUnion(result, retrieve);
                        // }
                    }
                }
            }
        } else {
            Retrieve retrieve = buildRetrieve(
                    ctx,
                    useStrictRetrieveTyping,
                    namedType,
                    classType,
                    codePath,
                    codeComparator,
                    property,
                    property != null ? property.getResultType() : null,
                    propertyException,
                    terminology);
            retrieves.add(retrieve);
            retrieve.setResultType(new ListType((DataType) namedType));
            result = retrieve;
        }

        return result;
    }

    private Retrieve buildRetrieve(
            cqlParser.RetrieveContext ctx,
            boolean useStrictRetrieveTyping,
            NamedType namedType,
            ClassType classType,
            String codePath,
            String codeComparator,
            Property property,
            DataType propertyType,
            Exception propertyException,
            Expression terminology) {

        Retrieve retrieve = of.createRetrieve()
                .withDataType(libraryBuilder.dataTypeToQName((DataType) namedType))
                .withTemplateId(classType.getIdentifier())
                .withCodeProperty(codePath);

        if (ctx.contextIdentifier() != null) {
            @SuppressWarnings("unchecked")
            List identifiers = (List) visit(ctx.contextIdentifier());
            Expression contextExpression = resolveQualifiedIdentifier(identifiers);
            retrieve.setContext(contextExpression);
        }

        if (terminology != null) {
            // Resolve the terminology target using an in or ~ operator
            try {
                if (codeComparator == null) {
                    codeComparator = "~";
                    if (terminology.getResultType() instanceof ListType) {
                        codeComparator = "in";
                    } else if (libraryBuilder.isCompatibleWith("1.5")) {
                        if (propertyType != null
                                && propertyType.isSubTypeOf(libraryBuilder.resolveTypeName("System", "Vocabulary"))) {
                            codeComparator = terminology
                                            .getResultType()
                                            .isSubTypeOf(libraryBuilder.resolveTypeName("System", "Vocabulary"))
                                    ? "~"
                                    : "contains";
                        } else {
                            codeComparator = terminology
                                            .getResultType()
                                            .isSubTypeOf(libraryBuilder.resolveTypeName("System", "Vocabulary"))
                                    ? "in"
                                    : "~";
                        }
                    }
                }

                if (property == null) {
                    throw propertyException;
                }

                switch (codeComparator) {
                    case "in":
                        {
                            Expression in = libraryBuilder.resolveIn(property, terminology);
                            if (in instanceof In) {
                                retrieve.setCodes(((In) in).getOperand().get(1));
                            } else if (in instanceof InValueSet) {
                                retrieve.setCodes(((InValueSet) in).getValueset());
                            } else if (in instanceof InCodeSystem) {
                                retrieve.setCodes(((InCodeSystem) in).getCodesystem());
                            } else if (in instanceof AnyInValueSet) {
                                retrieve.setCodes(((AnyInValueSet) in).getValueset());
                            } else if (in instanceof AnyInCodeSystem) {
                                retrieve.setCodes(((AnyInCodeSystem) in).getCodesystem());
                            } else {
                                // ERROR:
                                // WARNING:
                                libraryBuilder.recordParsingException(new CqlSemanticException(
                                        String.format(
                                                "Unexpected membership operator %s in retrieve",
                                                in.getClass().getSimpleName()),
                                        useStrictRetrieveTyping
                                                ? CqlCompilerException.ErrorSeverity.Error
                                                : CqlCompilerException.ErrorSeverity.Warning,
                                        getTrackBack(ctx)));
                            }
                        }
                        break;

                    case "contains":
                        {
                            Expression contains = libraryBuilder.resolveContains(property, terminology);
                            if (contains instanceof Contains) {
                                retrieve.setCodes(
                                        ((Contains) contains).getOperand().get(1));
                            }
                            // TODO: Introduce support for the contains operator to make this possible to
                            // support with a
                            // retrieve (direct-reference code negation)
                            // ERROR:
                            libraryBuilder.recordParsingException(new CqlSemanticException(
                                    "Terminology resolution using contains is not supported at this time. Use a where clause with an in operator instead.",
                                    useStrictRetrieveTyping
                                            ? CqlCompilerException.ErrorSeverity.Error
                                            : CqlCompilerException.ErrorSeverity.Warning,
                                    getTrackBack(ctx)));
                        }
                        break;

                    case "~":
                        {
                            // Resolve with equivalent to verify the type of the target
                            BinaryExpression equivalent = of.createEquivalent().withOperand(property, terminology);
                            libraryBuilder.resolveBinaryCall("System", "Equivalent", equivalent);

                            // Automatically promote to a list for use in the retrieve target
                            if (!(equivalent.getOperand().get(1).getResultType() instanceof ListType
                                    || (libraryBuilder.isCompatibleWith("1.5")
                                            && equivalent
                                                    .getOperand()
                                                    .get(1)
                                                    .getResultType()
                                                    .isSubTypeOf(
                                                            libraryBuilder.resolveTypeName("System", "Vocabulary"))))) {
                                retrieve.setCodes(libraryBuilder.resolveToList(
                                        equivalent.getOperand().get(1)));
                            } else {
                                retrieve.setCodes(equivalent.getOperand().get(1));
                            }
                        }
                        break;

                    case "=":
                        {
                            // Resolve with equality to verify the type of the source and target
                            BinaryExpression equal = of.createEqual().withOperand(property, terminology);
                            libraryBuilder.resolveBinaryCall("System", "Equal", equal);

                            // Automatically promote to a list for use in the retrieve target
                            if (!(equal.getOperand().get(1).getResultType() instanceof ListType
                                    || (libraryBuilder.isCompatibleWith("1.5")
                                            && equal.getOperand()
                                                    .get(1)
                                                    .getResultType()
                                                    .isSubTypeOf(
                                                            libraryBuilder.resolveTypeName("System", "Vocabulary"))))) {
                                retrieve.setCodes(libraryBuilder.resolveToList(
                                        equal.getOperand().get(1)));
                            } else {
                                retrieve.setCodes(equal.getOperand().get(1));
                            }
                        }
                        break;

                    default:
                        // ERROR:
                        // WARNING:
                        libraryBuilder.recordParsingException(new CqlSemanticException(
                                String.format("Unknown code comparator %s in retrieve", codeComparator),
                                useStrictRetrieveTyping
                                        ? CqlCompilerException.ErrorSeverity.Error
                                        : CqlCompilerException.ErrorSeverity.Warning,
                                getTrackBack(ctx.codeComparator())));
                }

                retrieve.setCodeComparator(codeComparator);

                // Verify that the type of the terminology target is a List
                // Due to implicit conversion defined by specific models, the resolution path
                // above may result in a
                // List
                // In that case, convert to a list of code (Union the Code elements of the
                // Concepts in the list)
                if (retrieve.getCodes() != null
                        && retrieve.getCodes().getResultType() != null
                        && retrieve.getCodes().getResultType() instanceof ListType
                        && ((ListType) retrieve.getCodes().getResultType())
                                .getElementType()
                                .equals(libraryBuilder.resolveTypeName("System", "Concept"))) {
                    if (retrieve.getCodes() instanceof ToList) {
                        // ToList will always have a single argument
                        ToList toList = (ToList) retrieve.getCodes();
                        // If that argument is a ToConcept, replace the ToList argument with the code
                        // (skip the implicit
                        // conversion, the data access layer is responsible for it)
                        if (toList.getOperand() instanceof ToConcept) {
                            toList.setOperand(((ToConcept) toList.getOperand()).getOperand());
                        } else {
                            // Otherwise, access the codes property of the resulting Concept
                            Expression codesAccessor = libraryBuilder.buildProperty(
                                    toList.getOperand(),
                                    "codes",
                                    false,
                                    toList.getOperand().getResultType());
                            retrieve.setCodes(codesAccessor);
                        }
                    } else {
                        // WARNING:
                        libraryBuilder.recordParsingException(new CqlSemanticException(
                                "Terminology target is a list of concepts, but expects a list of codes",
                                CqlCompilerException.ErrorSeverity.Warning,
                                getTrackBack(ctx)));
                    }
                }
            } catch (Exception e) {
                // If something goes wrong attempting to resolve, just set to the expression and
                // report it as a warning,
                // it shouldn't prevent translation unless the modelinfo indicates strict
                // retrieve typing
                if ((libraryBuilder.isCompatibleWith("1.5")
                                && !(terminology
                                        .getResultType()
                                        .isSubTypeOf(libraryBuilder.resolveTypeName("System", "Vocabulary"))))
                        || (!libraryBuilder.isCompatibleWith("1.5")
                                && !(terminology.getResultType() instanceof ListType))) {
                    retrieve.setCodes(libraryBuilder.resolveToList(terminology));
                } else {
                    retrieve.setCodes(terminology);
                }
                retrieve.setCodeComparator(codeComparator);
                // ERROR:
                // WARNING:
                libraryBuilder.recordParsingException(new CqlSemanticException(
                        "Could not resolve membership operator for terminology target of the retrieve.",
                        useStrictRetrieveTyping
                                ? CqlCompilerException.ErrorSeverity.Error
                                : CqlCompilerException.ErrorSeverity.Warning,
                        getTrackBack(ctx),
                        e));
            }
        }

        return retrieve;
    }

    @Override
    public Object visitSourceClause(cqlParser.SourceClauseContext ctx) {
        boolean hasFrom = "from".equals(ctx.getChild(0).getText());
        if (!hasFrom && isFromKeywordRequired()) {
            throw new IllegalArgumentException("The from keyword is required for queries.");
        }

        List sources = new ArrayList<>();
        for (cqlParser.AliasedQuerySourceContext source : ctx.aliasedQuerySource()) {
            if (sources.size() > 0 && !hasFrom) {
                throw new IllegalArgumentException("The from keyword is required for multi-source queries.");
            }
            sources.add((AliasedQuerySource) visit(source));
        }
        return sources;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object visitQuery(cqlParser.QueryContext ctx) {
        QueryContext queryContext = new QueryContext();
        libraryBuilder.pushQueryContext(queryContext);
        List sources = null;
        try {

            queryContext.enterSourceClause();
            try {
                sources = (List) visit(ctx.sourceClause());
            } finally {
                queryContext.exitSourceClause();
            }

            queryContext.addPrimaryQuerySources(sources);

            for (AliasedQuerySource source : sources) {
                libraryBuilder.pushIdentifier(source.getAlias(), source);
            }

            // If we are evaluating a population-level query whose source ranges over any
            // patient-context expressions,
            // then references to patient context expressions within the iteration clauses
            // of the query can be accessed
            // at the patient, rather than the population, context.
            boolean expressionContextPushed = false;
            /*
             * TODO: Address the issue of referencing multiple context expressions within a
             * query (or even expression in general)
             * if (libraryBuilder.inUnfilteredContext() &&
             * queryContext.referencesSpecificContext()) {
             * libraryBuilder.pushExpressionContext("Patient");
             * expressionContextPushed = true;
             * }
             */
            List dfcx = null;
            try {
                dfcx = ctx.letClause() != null ? (List) visit(ctx.letClause()) : null;

                if (dfcx != null) {
                    for (LetClause letClause : dfcx) {
                        libraryBuilder.pushIdentifier(letClause.getIdentifier(), letClause);
                    }
                }

                List qicx = new ArrayList<>();
                if (ctx.queryInclusionClause() != null) {
                    for (cqlParser.QueryInclusionClauseContext queryInclusionClauseContext :
                            ctx.queryInclusionClause()) {
                        qicx.add((RelationshipClause) visit(queryInclusionClauseContext));
                    }
                }

                Expression where = ctx.whereClause() != null ? (Expression) visit(ctx.whereClause()) : null;
                if (getDateRangeOptimization() && where != null) {
                    for (AliasedQuerySource aqs : sources) {
                        where = optimizeDateRangeInQuery(where, aqs);
                    }
                }

                ReturnClause ret = ctx.returnClause() != null ? (ReturnClause) visit(ctx.returnClause()) : null;
                AggregateClause agg =
                        ctx.aggregateClause() != null ? (AggregateClause) visit(ctx.aggregateClause()) : null;

                if ((agg == null) && (ret == null) && (sources.size() > 1)) {
                    ret = of.createReturnClause().withDistinct(true);

                    Tuple returnExpression = of.createTuple();
                    TupleType returnType = new TupleType();
                    for (AliasedQuerySource aqs : sources) {
                        TupleElement element = of.createTupleElement()
                                .withName(aqs.getAlias())
                                .withValue(of.createAliasRef().withName(aqs.getAlias()));
                        DataType sourceType = aqs.getResultType() instanceof ListType
                                ? ((ListType) aqs.getResultType()).getElementType()
                                : aqs.getResultType();
                        element.getValue().setResultType(sourceType); // Doesn't use the fluent API to avoid casting
                        element.setResultType(element.getValue().getResultType());
                        returnType.addElement(new TupleTypeElement(element.getName(), element.getResultType()));
                        returnExpression.getElement().add(element);
                    }

                    returnExpression.setResultType(queryContext.isSingular() ? returnType : new ListType(returnType));
                    ret.setExpression(returnExpression);
                    ret.setResultType(returnExpression.getResultType());
                }

                queryContext.removeQuerySources(sources);
                if (dfcx != null) {
                    queryContext.removeLetClauses(dfcx);
                }

                DataType queryResultType = null;
                if (agg != null) {
                    queryResultType = agg.getResultType();
                } else if (ret != null) {
                    queryResultType = ret.getResultType();
                } else {
                    queryResultType = sources.get(0).getResultType();
                }

                SortClause sort = null;
                if (agg == null) {
                    queryContext.setResultElementType(
                            queryContext.isSingular() ? null : ((ListType) queryResultType).getElementType());
                    if (ctx.sortClause() != null) {
                        if (queryContext.isSingular()) {
                            // ERROR:
                            throw new IllegalArgumentException("Sort clause cannot be used in a singular query.");
                        }
                        queryContext.enterSortClause();
                        try {
                            sort = (SortClause) visit(ctx.sortClause());
                            // Validate that the sort can be performed based on the existence of comparison
                            // operators
                            // for all types involved
                            for (SortByItem sortByItem : sort.getBy()) {
                                if (sortByItem instanceof ByDirection) {
                                    // validate that there is a comparison operator defined for the result element
                                    // type
                                    // of the query context
                                    libraryBuilder.verifyComparable(queryContext.getResultElementType());
                                } else {
                                    libraryBuilder.verifyComparable(sortByItem.getResultType());
                                }
                            }
                        } finally {
                            queryContext.exitSortClause();
                        }
                    }
                } else {
                    if (ctx.sortClause() != null) {
                        // ERROR:
                        throw new IllegalArgumentException("Sort clause cannot be used in an aggregate query.");
                    }
                }

                Query query = of.createQuery()
                        .withSource(sources)
                        .withLet(dfcx)
                        .withRelationship(qicx)
                        .withWhere(where)
                        .withReturn(ret)
                        .withAggregate(agg)
                        .withSort(sort);

                query.setResultType(queryResultType);
                return query;
            } finally {
                if (expressionContextPushed) {
                    libraryBuilder.popExpressionContext();
                }
                if (dfcx != null) {
                    for (LetClause letClause : dfcx) {
                        libraryBuilder.popIdentifier();
                    }
                }
            }

        } finally {
            libraryBuilder.popQueryContext();
            if (sources != null) {
                for (AliasedQuerySource source : sources) {
                    libraryBuilder.popIdentifier();
                }
            }
        }
    }

    // TODO: Expand this optimization to work the DateLow/DateHigh property
    // attributes

    /**
     * Some systems may wish to optimize performance by restricting retrieves with
     * available date ranges. Specifying
     * date ranges in a retrieve was removed from the CQL grammar, but it is still
     * possible to extract date ranges from
     * the where clause and put them in the Retrieve in ELM. The
     * optimizeDateRangeInQuery method
     * attempts to do this automatically. If optimization is possible, it will
     * remove the corresponding "during" from
     * the where clause and insert the date range into the Retrieve.
     *
     * @param aqs   the AliasedQuerySource containing the ClinicalRequest to
     *              possibly refactor a date range into.
     * @param where the Where clause to search for potential date range
     *              optimizations
     * @return the where clause with optimized "durings" removed, or
     *         null if there is no longer a Where
     *         clause after optimization.
     */
    public Expression optimizeDateRangeInQuery(Expression where, AliasedQuerySource aqs) {
        if (aqs.getExpression() instanceof Retrieve) {
            Retrieve retrieve = (Retrieve) aqs.getExpression();
            String alias = aqs.getAlias();
            if ((where instanceof IncludedIn || where instanceof In)
                    && attemptDateRangeOptimization((BinaryExpression) where, retrieve, alias)) {
                where = null;
            } else if (where instanceof And && attemptDateRangeOptimization((And) where, retrieve, alias)) {
                // Now optimize out the trues from the Ands
                where = consolidateAnd((And) where);
            }
        }
        return where;
    }

    /**
     * Test a BinaryExpression expression and determine if it is
     * suitable to be refactored into the
     * Retrieve as a date range restriction. If so, adjust the
     * Retrieve
     * accordingly and return true.
     *
     * @param during   the BinaryExpression expression to potentially
     *                 refactor into the Retrieve
     * @param retrieve the Retrieve to add qualifying date ranges to
     *                 (if applicable)
     * @param alias    the alias of the Retrieve in the query.
     * @return true if the date range was set in the
     *         Retrieve; false
     *         otherwise.
     */
    private boolean attemptDateRangeOptimization(BinaryExpression during, Retrieve retrieve, String alias) {
        if (retrieve.getDateProperty() != null || retrieve.getDateRange() != null) {
            return false;
        }

        Expression left = during.getOperand().get(0);
        Expression right = during.getOperand().get(1);

        String propertyPath = getPropertyPath(left, alias);
        if (propertyPath != null && isRHSEligibleForDateRangeOptimization(right)) {
            retrieve.setDateProperty(propertyPath);
            retrieve.setDateRange(right);
            return true;
        }

        return false;
    }

    /**
     * Collapse a property path expression back to it's qualified form for use as
     * the path attribute of the retrieve.
     *
     * @param reference the Expression to collapse
     * @param alias     the alias of the Retrieve in the query.
     * @return The collapsed path
     *         operands (or sub-operands) were modified; false
     *         otherwise.
     */
    private String getPropertyPath(Expression reference, String alias) {
        reference = getConversionReference(reference);
        reference = getChoiceSelection(reference);
        if (reference instanceof Property) {
            Property property = (Property) reference;
            if (alias.equals(property.getScope())) {
                return property.getPath();
            } else if (property.getSource() != null) {
                String subPath = getPropertyPath(property.getSource(), alias);
                if (subPath != null) {
                    return String.format("%s.%s", subPath, property.getPath());
                }
            }
        }

        return null;
    }

    /**
     * If this is a conversion operator, return the argument of the conversion, on
     * the grounds that the date range optimization
     * should apply through a conversion (i.e. it is an order-preserving conversion)
     *
     * @param reference the Expression to examine
     * @return The argument to the conversion operator if there was one, otherwise,
     *         the given reference
     */
    private Expression getConversionReference(Expression reference) {
        if (reference instanceof FunctionRef) {
            FunctionRef functionRef = (FunctionRef) reference;
            if (functionRef.getOperand().size() == 1
                    && functionRef.getResultType() != null
                    && functionRef.getOperand().get(0).getResultType() != null) {
                Operator o = this.libraryBuilder
                        .getConversionMap()
                        .getConversionOperator(
                                functionRef.getOperand().get(0).getResultType(), functionRef.getResultType());
                if (o != null
                        && o.getLibraryName() != null
                        && o.getLibraryName().equals(functionRef.getLibraryName())
                        && o.getName() != null
                        && o.getName().equals(functionRef.getName())) {
                    return functionRef.getOperand().get(0);
                }
            }
        }

        return reference;
    }

    /**
     * If this is a choice selection, return the argument of the choice selection,
     * on the grounds that the date range optimization
     * should apply through the cast (i.e. it is an order-preserving cast)
     *
     * @param reference the Expression to examine
     * @return The argument to the choice selection (i.e. As) if there was one,
     *         otherwise, the given reference
     */
    private Expression getChoiceSelection(Expression reference) {
        if (reference instanceof As) {
            As as = (As) reference;
            if (as.getOperand() != null && as.getOperand().getResultType() instanceof ChoiceType) {
                return as.getOperand();
            }
        }

        return reference;
    }

    /**
     * Test an And expression and determine if it contains any operands
     * (first-level or nested deeper)
     * than are IncludedIn expressions that can be refactored into a
     * Retrieve. If so,
     * adjust the Retrieve accordingly and reset the corresponding
     * operand to a literal
     * true. This and branch containing a
     * true can be further consolidated
     * later.
     *
     * @param and      the And expression containing operands to
     *                 potentially refactor into the
     *                 Retrieve
     * @param retrieve the Retrieve to add qualifying date ranges to
     *                 (if applicable)
     * @param alias    the alias of the Retrieve in the query.
     * @return true if the date range was set in the
     *         Retrieve and the And
     *         operands (or sub-operands) were modified; false
     *         otherwise.
     */
    private boolean attemptDateRangeOptimization(And and, Retrieve retrieve, String alias) {
        if (retrieve.getDateProperty() != null || retrieve.getDateRange() != null) {
            return false;
        }

        for (int i = 0; i < and.getOperand().size(); i++) {
            Expression operand = and.getOperand().get(i);
            if ((operand instanceof IncludedIn || operand instanceof In)
                    && attemptDateRangeOptimization((BinaryExpression) operand, retrieve, alias)) {
                // Replace optimized part in And with true -- to be optimized out later
                and.getOperand().set(i, libraryBuilder.createLiteral(true));
                return true;
            } else if (operand instanceof And && attemptDateRangeOptimization((And) operand, retrieve, alias)) {
                return true;
            }
        }

        return false;
    }

    /**
     * If any branches in the And tree contain a true,
     * refactor it out.
     *
     * @param and the And tree to attempt to consolidate
     * @return the potentially consolidated And
     */
    private Expression consolidateAnd(And and) {
        Expression result = and;
        Expression lhs = and.getOperand().get(0);
        Expression rhs = and.getOperand().get(1);
        if (isBooleanLiteral(lhs, true)) {
            result = rhs;
        } else if (isBooleanLiteral(rhs, true)) {
            result = lhs;
        } else if (lhs instanceof And) {
            and.getOperand().set(0, consolidateAnd((And) lhs));
        } else if (rhs instanceof And) {
            and.getOperand().set(1, consolidateAnd((And) rhs));
        }

        return result;
    }

    /**
     * Determine if the right-hand side of an IncludedIn expression can
     * be refactored into the date range
     * of a Retrieve. Currently, refactoring is only supported when the
     * RHS is a literal
     * DateTime interval, a literal DateTime, a parameter representing a DateTime
     * interval or a DateTime, or an
     * expression reference representing a DateTime interval or a DateTime.
     *
     * @param rhs the right-hand side of the IncludedIn to test for
     *            potential optimization
     * @return true if the RHS supports refactoring to a
     *         Retrieve, false
     *         otherwise.
     */
    private boolean isRHSEligibleForDateRangeOptimization(Expression rhs) {
        return rhs.getResultType().isSubTypeOf(libraryBuilder.resolveTypeName("System", "DateTime"))
                || rhs.getResultType()
                        .isSubTypeOf(new IntervalType(libraryBuilder.resolveTypeName("System", "DateTime")));

        // BTR: The only requirement for the optimization is that the expression be of
        // type DateTime or
        // Interval
        // Whether or not the expression can be statically evaluated (literal, in the
        // loose sense of the word) is really
        // a function of the engine in determining the "initial" data requirements,
        // versus subsequent data requirements
        // Element targetElement = rhs;
        // if (rhs instanceof ParameterRef) {
        // String paramName = ((ParameterRef) rhs).getName();
        // for (ParameterDef def : getLibrary().getParameters().getDef()) {
        // if (paramName.equals(def.getName())) {
        // targetElement = def.getParameterTypeSpecifier();
        // if (targetElement == null) {
        // targetElement = def.getDefault();
        // }
        // break;
        // }
        // }
        // } else if (rhs instanceof ExpressionRef && !(rhs instanceof FunctionRef)) {
        // // TODO: Support forward declaration, if necessary
        // String expName = ((ExpressionRef) rhs).getName();
        // for (ExpressionDef def : getLibrary().getStatements().getDef()) {
        // if (expName.equals(def.getName())) {
        // targetElement = def.getExpression();
        // }
        // }
        // }
        //
        // boolean isEligible = false;
        // if (targetElement instanceof DateTime) {
        // isEligible = true;
        // } else if (targetElement instanceof Interval) {
        // Interval ivl = (Interval) targetElement;
        // isEligible = (ivl.getLow() != null && ivl.getLow() instanceof DateTime) ||
        // (ivl.getHigh() != null
        // && ivl.getHigh() instanceof DateTime);
        // } else if (targetElement instanceof IntervalTypeSpecifier) {
        // IntervalTypeSpecifier spec = (IntervalTypeSpecifier) targetElement;
        // isEligible = isDateTimeTypeSpecifier(spec.getPointType());
        // } else if (targetElement instanceof NamedTypeSpecifier) {
        // isEligible = isDateTimeTypeSpecifier(targetElement);
        // }
        // return isEligible;
    }

    private boolean isDateTimeTypeSpecifier(Element e) {
        return e.getResultType().equals(libraryBuilder.resolveTypeName("System", "DateTime"));
    }

    @Override
    public Object visitLetClause(cqlParser.LetClauseContext ctx) {
        List letClauseItems = new ArrayList<>();
        for (cqlParser.LetClauseItemContext letClauseItem : ctx.letClauseItem()) {
            letClauseItems.add((LetClause) visit(letClauseItem));
        }
        return letClauseItems;
    }

    @Override
    public Object visitLetClauseItem(cqlParser.LetClauseItemContext ctx) {
        LetClause letClause = of.createLetClause()
                .withExpression(parseExpression(ctx.expression()))
                .withIdentifier(parseString(ctx.identifier()));
        letClause.setResultType(letClause.getExpression().getResultType());
        libraryBuilder.peekQueryContext().addLetClause(letClause);
        return letClause;
    }

    @Override
    public Object visitAliasedQuerySource(cqlParser.AliasedQuerySourceContext ctx) {
        AliasedQuerySource source = of.createAliasedQuerySource()
                .withExpression(parseExpression(ctx.querySource()))
                .withAlias(parseString(ctx.alias()));
        source.setResultType(source.getExpression().getResultType());
        return source;
    }

    @Override
    public Object visitWithClause(cqlParser.WithClauseContext ctx) {
        AliasedQuerySource aqs = (AliasedQuerySource) visit(ctx.aliasedQuerySource());
        libraryBuilder.peekQueryContext().addRelatedQuerySource(aqs);
        try {
            Expression expression = (Expression) visit(ctx.expression());
            DataTypes.verifyType(expression.getResultType(), libraryBuilder.resolveTypeName("System", "Boolean"));
            RelationshipClause result = of.createWith();
            result.withExpression(aqs.getExpression()).withAlias(aqs.getAlias()).withSuchThat(expression);
            result.setResultType(aqs.getResultType());
            return result;
        } finally {
            libraryBuilder.peekQueryContext().removeQuerySource(aqs);
        }
    }

    @Override
    public Object visitWithoutClause(cqlParser.WithoutClauseContext ctx) {
        AliasedQuerySource aqs = (AliasedQuerySource) visit(ctx.aliasedQuerySource());
        libraryBuilder.peekQueryContext().addRelatedQuerySource(aqs);
        try {
            Expression expression = (Expression) visit(ctx.expression());
            DataTypes.verifyType(expression.getResultType(), libraryBuilder.resolveTypeName("System", "Boolean"));
            RelationshipClause result = of.createWithout();
            result.withExpression(aqs.getExpression()).withAlias(aqs.getAlias()).withSuchThat(expression);
            result.setResultType(aqs.getResultType());
            return result;
        } finally {
            libraryBuilder.peekQueryContext().removeQuerySource(aqs);
        }
    }

    @Override
    public Object visitWhereClause(cqlParser.WhereClauseContext ctx) {
        Expression result = (Expression) visit(ctx.expression());
        DataTypes.verifyType(result.getResultType(), libraryBuilder.resolveTypeName("System", "Boolean"));
        return result;
    }

    @Override
    public Object visitReturnClause(cqlParser.ReturnClauseContext ctx) {
        ReturnClause returnClause = of.createReturnClause();
        if (ctx.getChild(1) instanceof TerminalNode) {
            switch (ctx.getChild(1).getText()) {
                case "all":
                    returnClause.setDistinct(false);
                    break;
                case "distinct":
                    returnClause.setDistinct(true);
                    break;
                default:
                    break;
            }
        }

        returnClause.setExpression(parseExpression(ctx.expression()));
        returnClause.setResultType(
                libraryBuilder.peekQueryContext().isSingular()
                        ? returnClause.getExpression().getResultType()
                        : new ListType(returnClause.getExpression().getResultType()));

        return returnClause;
    }

    @Override
    public Object visitStartingClause(cqlParser.StartingClauseContext ctx) {
        if (ctx.simpleLiteral() != null) {
            return visit(ctx.simpleLiteral());
        }

        if (ctx.quantity() != null) {
            return visit(ctx.quantity());
        }

        if (ctx.expression() != null) {
            return visit(ctx.expression());
        }

        return null;
    }

    @Override
    public Object visitAggregateClause(cqlParser.AggregateClauseContext ctx) {
        libraryBuilder.checkCompatibilityLevel("Aggregate query clause", "1.5");
        AggregateClause aggregateClause = of.createAggregateClause();
        if (ctx.getChild(1) instanceof TerminalNode) {
            switch (ctx.getChild(1).getText()) {
                case "all":
                    aggregateClause.setDistinct(false);
                    break;
                case "distinct":
                    aggregateClause.setDistinct(true);
                    break;
                default:
                    break;
            }
        }

        if (ctx.startingClause() != null) {
            aggregateClause.setStarting(parseExpression(ctx.startingClause()));
        }

        // If there is a starting, that's the type of the var
        // If there's not a starting, push an Any and then attempt to evaluate (might
        // need a type hint here)
        aggregateClause.setIdentifier(parseString(ctx.identifier()));

        Expression accumulator = null;
        if (aggregateClause.getStarting() != null) {
            accumulator = libraryBuilder.buildNull(aggregateClause.getStarting().getResultType());
        } else {
            accumulator = libraryBuilder.buildNull(libraryBuilder.resolveTypeName("System", "Any"));
        }

        LetClause letClause =
                of.createLetClause().withExpression(accumulator).withIdentifier(aggregateClause.getIdentifier());
        letClause.setResultType(letClause.getExpression().getResultType());
        libraryBuilder.peekQueryContext().addLetClause(letClause);

        aggregateClause.setExpression(parseExpression(ctx.expression()));
        aggregateClause.setResultType(aggregateClause.getExpression().getResultType());

        if (aggregateClause.getStarting() == null) {
            accumulator.setResultType(aggregateClause.getResultType());
            aggregateClause.setStarting(accumulator);
        }

        return aggregateClause;
    }

    @Override
    public SortDirection visitSortDirection(cqlParser.SortDirectionContext ctx) {
        return SortDirection.fromValue(ctx.getText());
    }

    private SortDirection parseSortDirection(cqlParser.SortDirectionContext ctx) {
        if (ctx != null) {
            return visitSortDirection(ctx);
        }

        return SortDirection.ASC;
    }

    @Override
    public SortByItem visitSortByItem(cqlParser.SortByItemContext ctx) {
        Expression sortExpression = parseExpression(ctx.expressionTerm());
        if (sortExpression instanceof IdentifierRef) {
            return (SortByItem) of.createByColumn()
                    .withPath(((IdentifierRef) sortExpression).getName())
                    .withDirection(parseSortDirection(ctx.sortDirection()))
                    .withResultType(sortExpression.getResultType());
        }

        return (SortByItem) of.createByExpression()
                .withExpression(sortExpression)
                .withDirection(parseSortDirection(ctx.sortDirection()))
                .withResultType(sortExpression.getResultType());
    }

    @Override
    public Object visitSortClause(cqlParser.SortClauseContext ctx) {
        if (ctx.sortDirection() != null) {
            return of.createSortClause()
                    .withBy(of.createByDirection().withDirection(parseSortDirection(ctx.sortDirection())));
        }

        List sortItems = new ArrayList<>();
        if (ctx.sortByItem() != null) {
            for (cqlParser.SortByItemContext sortByItemContext : ctx.sortByItem()) {
                sortItems.add((SortByItem) visit(sortByItemContext));
            }
        }

        return of.createSortClause().withBy(sortItems);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object visitQuerySource(cqlParser.QuerySourceContext ctx) {
        if (ctx.expression() != null) {
            return visit(ctx.expression());
        } else if (ctx.retrieve() != null) {
            return visit(ctx.retrieve());
        } else {
            List identifiers = (List) visit(ctx.qualifiedIdentifierExpression());
            return resolveQualifiedIdentifier(identifiers);
        }
    }

    @Override
    public Object visitIndexedExpressionTerm(cqlParser.IndexedExpressionTermContext ctx) {
        Indexer indexer = of.createIndexer()
                .withOperand(parseExpression(ctx.expressionTerm()))
                .withOperand(parseExpression(ctx.expression()));

        // TODO: Support zero-based indexers as defined by the isZeroBased attribute
        libraryBuilder.resolveBinaryCall("System", "Indexer", indexer);
        return indexer;
    }

    @Override
    public Expression visitInvocationExpressionTerm(cqlParser.InvocationExpressionTermContext ctx) {
        Expression left = parseExpression(ctx.expressionTerm());
        libraryBuilder.pushExpressionTarget(left);
        try {
            return (Expression) visit(ctx.qualifiedInvocation());
        } finally {
            libraryBuilder.popExpressionTarget();
        }
    }

    @Override
    public Expression visitExternalConstant(cqlParser.ExternalConstantContext ctx) {
        return libraryBuilder.resolveIdentifier(ctx.getText(), true);
    }

    @Override
    public Expression visitThisInvocation(cqlParser.ThisInvocationContext ctx) {
        return libraryBuilder.resolveIdentifier(ctx.getText(), true);
    }

    @Override
    public Expression visitMemberInvocation(cqlParser.MemberInvocationContext ctx) {
        String identifier = parseString(ctx.referentialIdentifier());
        return resolveMemberIdentifier(identifier);
    }

    @Override
    public Expression visitQualifiedMemberInvocation(cqlParser.QualifiedMemberInvocationContext ctx) {
        String identifier = parseString(ctx.referentialIdentifier());
        return resolveMemberIdentifier(identifier);
    }

    public Expression resolveQualifiedIdentifier(List identifiers) {
        Expression current = null;
        for (String identifier : identifiers) {
            if (current == null) {
                current = resolveIdentifier(identifier);
            } else {
                current = libraryBuilder.resolveAccessor(current, identifier);
            }
        }

        return current;
    }

    public Expression resolveMemberIdentifier(String identifier) {
        if (libraryBuilder.hasExpressionTarget()) {
            Expression target = libraryBuilder.popExpressionTarget();
            try {
                return libraryBuilder.resolveAccessor(target, identifier);
            } finally {
                libraryBuilder.pushExpressionTarget(target);
            }
        }

        return resolveIdentifier(identifier);
    }

    private Expression resolveIdentifier(String identifier) {
        // If the identifier cannot be resolved in the library builder, check for
        // forward declarations for expressions
        // and parameters
        Expression result = libraryBuilder.resolveIdentifier(identifier, false);
        if (result == null) {
            ExpressionDefinitionInfo expressionInfo = libraryInfo.resolveExpressionReference(identifier);
            if (expressionInfo != null) {
                String saveContext = saveCurrentContext(expressionInfo.getContext());
                try {
                    Stack saveChunks = chunks;
                    chunks = new Stack();
                    forwards.push(expressionInfo);
                    try {
                        if (expressionInfo.getDefinition() == null) {
                            // ERROR:
                            throw new IllegalArgumentException(String.format(
                                    "Could not validate reference to expression %s because its definition contains errors.",
                                    expressionInfo.getName()));
                        }

                        // Have to call the visit to get the outer processing to occur
                        visit(expressionInfo.getDefinition());
                    } finally {
                        chunks = saveChunks;
                        forwards.pop();
                    }
                } finally {
                    setCurrentContext(saveContext);
                }
            }

            ParameterDefinitionInfo parameterInfo = libraryInfo.resolveParameterReference(identifier);
            if (parameterInfo != null) {
                visitParameterDefinition(parameterInfo.getDefinition());
            }
            result = libraryBuilder.resolveIdentifier(identifier, true);
        }

        return result;
    }

    private String ensureSystemFunctionName(String libraryName, String functionName) {
        if (libraryName == null || libraryName.equals("System")) {
            // Because these functions can be both a keyword and the name of a method, they
            // can be resolved by the
            // parser as a function, instead of as the keyword-based parser rule. In this
            // case, the function
            // name needs to be translated to the System function name in order to resolve.
            switch (functionName) {
                case "contains":
                    functionName = "Contains";
                    break;
                case "distinct":
                    functionName = "Distinct";
                    break;
                case "exists":
                    functionName = "Exists";
                    break;
                case "in":
                    functionName = "In";
                    break;
                case "not":
                    functionName = "Not";
                    break;
            }
        }

        return functionName;
    }

    private Expression resolveFunction(String libraryName, String functionName, cqlParser.ParamListContext paramList) {
        List expressions = new ArrayList();
        if (paramList != null && paramList.expression() != null) {
            for (cqlParser.ExpressionContext expressionContext : paramList.expression()) {
                expressions.add((Expression) visit(expressionContext));
            }
        }
        return resolveFunction(libraryName, functionName, expressions, true, false, false);
    }

    public Expression resolveFunction(
            String libraryName,
            String functionName,
            List expressions,
            boolean mustResolve,
            boolean allowPromotionAndDemotion,
            boolean allowFluent) {
        if (allowFluent) {
            libraryBuilder.checkCompatibilityLevel("Fluent functions", "1.5");
        }

        functionName = ensureSystemFunctionName(libraryName, functionName);

        // 1. Ensure all overloads of the function are registered with the operator map
        // 2. Resolve the function, allowing for the case that operator map is a
        // skeleton
        // 3. If the resolution from the operator map is a skeleton, compile the
        // function body to determine the result
        // type

        // Find all functionDefinitionInfo instances with the given name
        // register each functionDefinitionInfo
        if (libraryName == null || libraryName.equals("") || libraryName.equals(this.libraryInfo.getLibraryName())) {
            Iterable fdis = libraryInfo.resolveFunctionReference(functionName);
            if (fdis != null) {
                for (FunctionDefinitionInfo fdi : fdis) {
                    String saveContext = saveCurrentContext(fdi.getContext());
                    try {
                        registerFunctionDefinition(fdi.getDefinition());
                    } finally {
                        this.setCurrentContext(saveContext);
                    }
                }
            }
        }

        Invocation result = libraryBuilder.resolveFunction(
                libraryName, functionName, expressions, mustResolve, allowPromotionAndDemotion, allowFluent);

        if (result instanceof FunctionRefInvocation) {
            FunctionRefInvocation invocation = (FunctionRefInvocation) result;
            if (invocation.getResolution() != null
                    && invocation.getResolution().getOperator() != null
                    && (invocation.getResolution().getOperator().getLibraryName() == null
                            || invocation
                                    .getResolution()
                                    .getOperator()
                                    .getLibraryName()
                                    .equals(libraryBuilder
                                            .getCompiledLibrary()
                                            .getIdentifier()
                                            .getId()))) {
                Operator op = invocation.getResolution().getOperator();
                FunctionHeader fh = getFunctionHeader(op);
                if (!fh.getIsCompiled()) {
                    cqlParser.FunctionDefinitionContext ctx = getFunctionDefinitionContext(fh);
                    String saveContext = saveCurrentContext(fh.getFunctionDef().getContext());
                    Stack saveChunks = chunks;
                    chunks = new Stack();
                    try {
                        FunctionDef fd = compileFunctionDefinition(ctx);
                        op.setResultType(fd.getResultType());
                        invocation.setResultType(op.getResultType());
                    } finally {
                        setCurrentContext(saveContext);
                        this.chunks = saveChunks;
                    }
                }
            }
        }

        if (mustResolve) {
            // Extra internal error handling, these should never be hit if the two-phase
            // operator compile is working as
            // expected
            if (result == null) {
                throw new IllegalArgumentException("Internal error: could not resolve function");
            }

            if (result.getExpression() == null) {
                throw new IllegalArgumentException("Internal error: could not resolve invocation expression");
            }

            if (result.getExpression().getResultType() == null) {
                throw new IllegalArgumentException("Internal error: could not determine result type");
            }
        }

        if (result == null) {
            return null;
        }
        return result.getExpression();
    }

    public Expression resolveFunctionOrQualifiedFunction(String identifier, cqlParser.ParamListContext paramListCtx) {
        if (libraryBuilder.hasExpressionTarget()) {
            Expression target = libraryBuilder.popExpressionTarget();
            try {
                // If the target is a library reference, resolve as a standard qualified call
                if (target instanceof LibraryRef) {
                    return resolveFunction(((LibraryRef) target).getLibraryName(), identifier, paramListCtx);
                }

                // NOTE: FHIRPath method invocation
                // If the target is an expression, resolve as a method invocation
                if (target instanceof Expression && isMethodInvocationEnabled()) {
                    return systemMethodResolver.resolveMethod((Expression) target, identifier, paramListCtx, true);
                }

                if (!isMethodInvocationEnabled()) {
                    throw new CqlCompilerException(
                            String.format(
                                    "The identifier %s could not be resolved as an invocation because method-style invocation is disabled.",
                                    identifier),
                            CqlCompilerException.ErrorSeverity.Error);
                }
                throw new IllegalArgumentException(String.format(
                        "Invalid invocation target: %s", target.getClass().getName()));
            } finally {
                libraryBuilder.pushExpressionTarget(target);
            }
        }

        // If we are in an implicit $this context, the function may be resolved as a
        // method invocation
        Expression thisRef = libraryBuilder.resolveIdentifier("$this", false);
        if (thisRef != null) {
            Expression result = systemMethodResolver.resolveMethod(thisRef, identifier, paramListCtx, false);
            if (result != null) {
                return result;
            }
        }

        // If we are in an implicit context (i.e. a context named the same as a
        // parameter), the function may be resolved
        // as a method invocation
        ParameterRef parameterRef = libraryBuilder.resolveImplicitContext();
        if (parameterRef != null) {
            Expression result = systemMethodResolver.resolveMethod(parameterRef, identifier, paramListCtx, false);
            if (result != null) {
                return result;
            }
        }

        // If there is no target, resolve as a system function
        return resolveFunction(null, identifier, paramListCtx);
    }

    @Override
    public Expression visitFunction(cqlParser.FunctionContext ctx) {
        return resolveFunctionOrQualifiedFunction(parseString(ctx.referentialIdentifier()), ctx.paramList());
    }

    @Override
    public Expression visitQualifiedFunction(cqlParser.QualifiedFunctionContext ctx) {
        return resolveFunctionOrQualifiedFunction(parseString(ctx.identifierOrFunctionIdentifier()), ctx.paramList());
    }

    @Override
    public Object visitFunctionBody(cqlParser.FunctionBodyContext ctx) {
        return visit(ctx.expression());
    }

    private FunctionHeader getFunctionHeader(cqlParser.FunctionDefinitionContext ctx) {
        FunctionHeader fh = functionHeaders.get(ctx);
        if (fh == null) {

            final Stack saveChunks = chunks;
            chunks = new Stack<>();
            try {
                // Have to call the visit to allow the outer processing to occur
                fh = parseFunctionHeader(ctx);
            } finally {
                chunks = saveChunks;
            }

            functionHeaders.put(ctx, fh);
            functionDefinitions.put(fh, ctx);
            functionHeadersByDef.put(fh.getFunctionDef(), fh);
        }
        return fh;
    }

    private FunctionDef getFunctionDef(Operator op) {
        FunctionDef target = null;
        List st = new ArrayList<>();
        for (DataType dt : op.getSignature().getOperandTypes()) {
            st.add(dt);
        }
        Iterable fds = libraryBuilder.getCompiledLibrary().resolveFunctionRef(op.getName(), st);
        for (FunctionDef fd : fds) {
            if (fd.getOperand().size() == op.getSignature().getSize()) {
                Iterator signatureTypes =
                        op.getSignature().getOperandTypes().iterator();
                boolean signaturesMatch = true;
                for (int i = 0; i < fd.getOperand().size(); i++) {
                    if (!DataTypes.equal(fd.getOperand().get(i).getResultType(), signatureTypes.next())) {
                        signaturesMatch = false;
                    }
                }
                if (signaturesMatch) {
                    if (target == null) {
                        target = fd;
                    } else {
                        throw new IllegalArgumentException(String.format(
                                "Internal error attempting to resolve function header for %s", op.getName()));
                    }
                }
            }
        }

        return target;
    }

    private FunctionHeader getFunctionHeaderByDef(FunctionDef fd) {
        // Shouldn't need to do this, something about the hashCode implementation of
        // FunctionDef is throwing this off,
        // Don't have time to investigate right now, this should work fine, could
        // potentially be improved
        for (Map.Entry entry : functionHeadersByDef.entrySet()) {
            if (entry.getKey() == fd) {
                return entry.getValue();
            }
        }

        return null;
    }

    private FunctionHeader getFunctionHeader(Operator op) {
        FunctionDef fd = getFunctionDef(op);
        if (fd == null) {
            throw new IllegalArgumentException(
                    String.format("Could not resolve function header for operator %s", op.getName()));
        }
        FunctionHeader result = getFunctionHeaderByDef(fd);
        // FunctionHeader result = functionHeadersByDef.get(fd);
        if (result == null) {
            throw new IllegalArgumentException(
                    String.format("Could not resolve function header for operator %s", op.getName()));
        }
        return result;
    }

    private cqlParser.FunctionDefinitionContext getFunctionDefinitionContext(FunctionHeader fh) {
        cqlParser.FunctionDefinitionContext ctx = functionDefinitions.get(fh);
        if (ctx == null) {
            throw new IllegalArgumentException(String.format(
                    "Could not resolve function definition context for function header %s",
                    fh.getFunctionDef().getName()));
        }
        return ctx;
    }

    public void registerFunctionDefinition(cqlParser.FunctionDefinitionContext ctx) {
        FunctionHeader fh = getFunctionHeader(ctx);
        if (!libraryBuilder.getCompiledLibrary().contains(fh.getFunctionDef())) {
            libraryBuilder.addExpression(fh.getFunctionDef());
        }
    }

    public FunctionDef compileFunctionDefinition(cqlParser.FunctionDefinitionContext ctx) {
        FunctionHeader fh = getFunctionHeader(ctx);

        final FunctionDef fun = fh.getFunctionDef();
        final TypeSpecifier resultType = fh.getResultType();
        final Operator op = libraryBuilder.resolveFunctionDefinition(fh.getFunctionDef());
        if (op == null) {
            throw new IllegalArgumentException(String.format(
                    "Internal error: Could not resolve operator map entry for function header %s",
                    fh.getMangledName()));
        }
        libraryBuilder.pushIdentifier(fun.getName(), fun, IdentifierScope.GLOBAL);
        final List operand = op.getFunctionDef().getOperand();
        for (OperandDef operandDef : operand) {
            libraryBuilder.pushIdentifier(operandDef.getName(), operandDef);
        }

        try {
            if (ctx.functionBody() != null) {
                libraryBuilder.beginFunctionDef(fun);
                try {
                    libraryBuilder.pushExpressionContext(getCurrentContext());
                    try {
                        libraryBuilder.pushExpressionDefinition(fh.getMangledName());
                        try {
                            fun.setExpression(parseExpression(ctx.functionBody()));
                        } finally {
                            libraryBuilder.popExpressionDefinition();
                        }
                    } finally {
                        libraryBuilder.popExpressionContext();
                    }
                } finally {
                    libraryBuilder.endFunctionDef();
                }

                if (resultType != null
                        && fun.getExpression() != null
                        && fun.getExpression().getResultType() != null) {
                    if (!DataTypes.subTypeOf(fun.getExpression().getResultType(), resultType.getResultType())) {
                        // ERROR:
                        throw new IllegalArgumentException(String.format(
                                "Function %s has declared return type %s but the function body returns incompatible type %s.",
                                fun.getName(),
                                resultType.getResultType(),
                                fun.getExpression().getResultType()));
                    }
                }

                fun.setResultType(fun.getExpression().getResultType());
                op.setResultType(fun.getResultType());
            } else {
                fun.setExternal(true);
                if (resultType == null) {
                    // ERROR:
                    throw new IllegalArgumentException(String.format(
                            "Function %s is marked external but does not declare a return type.", fun.getName()));
                }
                fun.setResultType(resultType.getResultType());
                op.setResultType(fun.getResultType());
            }

            fun.setContext(getCurrentContext());
            fh.setIsCompiled();

            return fun;
        } finally {
            for (OperandDef operandDef : operand) {
                try {
                    libraryBuilder.popIdentifier();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // Intentionally do _not_ pop the function name, it needs to remain in global scope!
        }
    }

    @Override
    public Object visitFunctionDefinition(cqlParser.FunctionDefinitionContext ctx) {
        libraryBuilder.pushIdentifierScope();
        try {
            registerFunctionDefinition(ctx);
            return compileFunctionDefinition(ctx);
        } finally {
            libraryBuilder.popIdentifierScope();
        }
    }

    private Expression parseLiteralExpression(ParseTree pt) {
        libraryBuilder.pushLiteralContext();
        try {
            return parseExpression(pt);
        } finally {
            libraryBuilder.popLiteralContext();
        }
    }

    private Expression parseExpression(ParseTree pt) {
        return pt == null ? null : (Expression) visit(pt);
    }

    private boolean isBooleanLiteral(Expression expression, Boolean bool) {
        boolean ret = false;
        if (expression instanceof Literal) {
            Literal lit = (Literal) expression;
            ret = lit.getValueType()
                    .equals(libraryBuilder.dataTypeToQName(libraryBuilder.resolveTypeName("System", "Boolean")));
            if (ret && bool != null) {
                ret = bool.equals(Boolean.valueOf(lit.getValue()));
            }
        }
        return ret;
    }

    private TrackBack getTrackBack(ParseTree tree) {
        if (tree instanceof ParserRuleContext) {
            return getTrackBack((ParserRuleContext) tree);
        }
        if (tree instanceof TerminalNode) {
            return getTrackBack((TerminalNode) tree);
        }
        return null;
    }

    private TrackBack getTrackBack(TerminalNode node) {
        TrackBack tb = new TrackBack(
                libraryBuilder.getLibraryIdentifier(),
                node.getSymbol().getLine(),
                node.getSymbol().getCharPositionInLine() + 1, // 1-based instead of 0-based
                node.getSymbol().getLine(),
                node.getSymbol().getCharPositionInLine()
                        + node.getSymbol().getText().length());
        return tb;
    }

    private TrackBack getTrackBack(ParserRuleContext ctx) {
        TrackBack tb = new TrackBack(
                libraryBuilder.getLibraryIdentifier(),
                ctx.getStart().getLine(),
                ctx.getStart().getCharPositionInLine() + 1, // 1-based instead of 0-based
                ctx.getStop().getLine(),
                ctx.getStop().getCharPositionInLine() + ctx.getStop().getText().length() // 1-based instead of 0-based
                );
        return tb;
    }

    private void decorate(Element element, TrackBack tb) {
        if (locatorsEnabled() && tb != null) {
            element.setLocator(tb.toLocator());
        }

        if (resultTypesEnabled() && element.getResultType() != null) {
            if (element.getResultType() instanceof NamedType) {
                element.setResultTypeName(libraryBuilder.dataTypeToQName(element.getResultType()));
            } else {
                element.setResultTypeSpecifier(libraryBuilder.dataTypeToTypeSpecifier(element.getResultType()));
            }
        }
    }

    private TrackBack track(Trackable trackable, ParseTree pt) {
        TrackBack tb = getTrackBack(pt);

        if (tb != null) {
            trackable.getTrackbacks().add(tb);
        }

        if (trackable instanceof Element) {
            decorate((Element) trackable, tb);
        }

        return tb;
    }

    private TrackBack track(Trackable trackable, Element from) {
        TrackBack tb = from.getTrackbacks().size() > 0 ? from.getTrackbacks().get(0) : null;

        if (tb != null) {
            trackable.getTrackbacks().add(tb);
        }

        if (trackable instanceof Element) {
            decorate((Element) trackable, tb);
        }

        return tb;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy