/**
 * Handles operator overloading.
 *
 * Specification: $(LINK2 https://dlang.org/spec/operatoroverloading.html, Operator Overloading)
 *
 * Copyright:   Copyright (C) 1999-2025 by The D Language Foundation, All Rights Reserved
 * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
 * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
 * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/opover.d, _opover.d)
 * Documentation:  https://dlang.org/phobos/dmd_opover.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/opover.d
 */

module dmd.opover;

import core.stdc.stdio;
import dmd.aggregate;
import dmd.aliasthis;
import dmd.arraytypes;
import dmd.astenums;
import dmd.dclass;
import dmd.declaration;
import dmd.dscope;
import dmd.dstruct;
import dmd.dsymbol;
import dmd.dsymbolsem;
import dmd.dtemplate;
import dmd.errors;
import dmd.expression;
import dmd.expressionsem;
import dmd.func;
import dmd.funcsem;
import dmd.globals;
import dmd.hdrgen;
import dmd.id;
import dmd.identifier;
import dmd.location;
import dmd.mtype;
import dmd.optimize;
import dmd.statement;
import dmd.templatesem;
import dmd.tokens;
import dmd.typesem;
import dmd.visitor;

/***********************************
 * Determine if operands of binary op can be reversed
 * to fit operator overload.
 */
bool isCommutative(EXP op) @safe
{
    switch (op)
    {
    case EXP.add:
    case EXP.mul:
    case EXP.and:
    case EXP.or:
    case EXP.xor:
    // EqualExp
    case EXP.equal:
    case EXP.notEqual:
    // CmpExp
    case EXP.lessThan:
    case EXP.lessOrEqual:
    case EXP.greaterThan:
    case EXP.greaterOrEqual:
        return true;
    default:
        break;
    }
    return false;
}

/// Returns: whether `op` can be overloaded with `opBinary`
private bool hasOpBinary(EXP op) pure @safe
{
    switch (op)
    {
        case EXP.add: return true;
        case EXP.min: return true;
        case EXP.mul: return true;
        case EXP.div: return true;
        case EXP.mod: return true;
        case EXP.and: return true;
        case EXP.or: return true;
        case EXP.xor: return true;
        case EXP.leftShift: return true;
        case EXP.rightShift: return true;
        case EXP.unsignedRightShift: return true;
        case EXP.concatenate: return true;
        case EXP.pow: return true;
        case EXP.in_: return true;
        default: return false;
    }
}

/**
 * Remove the = from op=, e.g. += becomes +
 *
 * Params:
 *     op = tag for a binary assign operator
 * Returns: the corresponding binary operator, or `op` if it wasn't an assign operator
*/
private EXP stripAssignOp(EXP op)
{
    switch (op)
    {
    case EXP.addAssign: return EXP.add;
    case EXP.minAssign: return EXP.min;
    case EXP.mulAssign: return EXP.mul;
    case EXP.divAssign: return EXP.div;
    case EXP.modAssign: return EXP.mod;
    case EXP.andAssign: return EXP.and;
    case EXP.orAssign: return EXP.or;
    case EXP.xorAssign: return EXP.xor;
    case EXP.leftShiftAssign: return EXP.leftShift;
    case EXP.rightShiftAssign: return EXP.rightShift;
    case EXP.unsignedRightShiftAssign: return EXP.unsignedRightShift;
    case EXP.concatenateAssign: return EXP.concatenate;
    case EXP.powAssign: return EXP.pow;
    default: return op;
    }
}

/*******************************************
 * Helper function to turn operator into template argument list
 */
Objects* opToArg(Scope* sc, EXP op)
{
    Expression e = new StringExp(Loc.initial, EXPtoString(stripAssignOp(op)));
    e = e.expressionSemantic(sc);
    auto tiargs = new Objects();
    tiargs.push(e);
    return tiargs;
}

// Try alias this on first operand
Expression checkAliasThisForLhs(AggregateDeclaration ad, Scope* sc, BinExp e, Type[2] aliasThisStop)
{
    if (!ad || !ad.aliasthis)
        return null;

    /* Rewrite (e1 op e2) as:
     *      (e1.aliasthis op e2)
     */
    if (isRecursiveAliasThis(aliasThisStop[0], e.e1.type))
        return null;
    //printf("att %s e1 = %s\n", Token.toChars(e.op), e.e1.type.toChars());
    BinExp be = cast(BinExp)e.copy();
    // Resolve 'alias this' but in case of assigment don't resolve properties yet
    // because 'e1 = e2' could mean 'e1(e2)' or 'e1() = e2'
    bool findOnly = e.isAssignExp() !is null;
    be.e1 = resolveAliasThis(sc, e.e1, true, findOnly);
    if (!be.e1)
        return null;

    return be.trySemanticAliasThis(sc, aliasThisStop);
}

// Try alias this on second operand
Expression checkAliasThisForRhs(AggregateDeclaration ad, Scope* sc, BinExp e, Type[2] aliasThisStop)
{
    if (!ad || !ad.aliasthis)
        return null;
    /* Rewrite (e1 op e2) as:
     *      (e1 op e2.aliasthis)
     */
    if (isRecursiveAliasThis(aliasThisStop[1], e.e2.type))
        return null;
    //printf("att %s e2 = %s\n", Token.toChars(e.op), e.e2.type.toChars());
    BinExp be = cast(BinExp)e.copy();
    be.e2 = resolveAliasThis(sc, e.e2, true);
    if (!be.e2)
        return null;

    return be.trySemanticAliasThis(sc, aliasThisStop);
}

Expression opOverloadUnary(UnaExp e, Scope* sc)
{
    if (auto ae = e.e1.isArrayExp())
    {
        ae.e1 = ae.e1.expressionSemantic(sc);
        ae.e1 = resolveProperties(sc, ae.e1);
        Expression ae1old = ae.e1;
        const(bool) maybeSlice = (ae.arguments.length == 0 || ae.arguments.length == 1 && (*ae.arguments)[0].isIntervalExp());
        IntervalExp ie = null;
        if (maybeSlice && ae.arguments.length)
        {
            ie = (*ae.arguments)[0].isIntervalExp();
        }
        Type att = null; // first cyclic `alias this` type
        while (true)
        {
            if (ae.e1.isErrorExp())
            {
                return ae.e1;
            }
            Expression ae1save = ae.e1;
            ae.lengthVar = null;

            AggregateDeclaration ad = isAggregate(ae.e1.type);
            if (!ad)
                break;

            if (search_function(ad, Id.opIndexUnary))
            {
                Expression e0;
                // Deal with $
                Expression ae2 = resolveOpDollar(sc, ae, e0);
                if (!ae2) // op(a[i..j]) might be: a.opSliceUnary!(op)(i, j)
                    goto Lfallback;
                if (ae2.isErrorExp())
                    return ae2;
                /* Rewrite op(a[arguments]) as:
                 *      a.opIndexUnary!(op)(arguments)
                 */
                Expression result = dotTemplateCall(ae.e1, Id.opIndexUnary, opToArg(sc, e.op), (*ae.arguments)[]);
                if (maybeSlice) // op(a[]) might be: a.opSliceUnary!(op)()
                    result = result.trySemantic(sc);
                else
                    result = result.expressionSemantic(sc);

                if (result)
                    return Expression.combine(e0, result);
            }
        Lfallback:
            if (maybeSlice && search_function(ad, Id.opSliceUnary))
            {
                // Deal with $
                Expression e0;
                auto ae2 = resolveOpDollar(sc, ae, ie, e0);
                if (ae2.isErrorExp())
                    return ae2;
                /* Rewrite op(a[i..j]) as:
                 *      a.opSliceUnary!(op)(i, j)
                 */
                Expression result = ie ?
                    dotTemplateCall(ae.e1, Id.opSliceUnary, opToArg(sc, e.op), ie.lwr, ie.upr) :
                    dotTemplateCall(ae.e1, Id.opSliceUnary, opToArg(sc, e.op));

                return Expression.combine(e0, result.expressionSemantic(sc));
            }
            // Didn't find it. Forward to aliasthis
            if (ad.aliasthis && !isRecursiveAliasThis(att, ae.e1.type))
            {
                /* Rewrite op(a[arguments]) as:
                 *      op(a.aliasthis[arguments])
                 */
                ae.e1 = resolveAliasThis(sc, ae1save, true);
                if (ae.e1)
                    continue;
            }
            break;
        }
        ae.e1 = ae1old; // recovery
        ae.lengthVar = null;
    }
    e.e1 = e.e1.expressionSemantic(sc);
    e.e1 = resolveProperties(sc, e.e1);
    Type att = null; // first cyclic `alias this` type
    while (1)
    {
        if (e.e1.isErrorExp())
        {
            return e.e1;
        }

        AggregateDeclaration ad = isAggregate(e.e1.type);
        if (!ad)
            break;

        /* Rewrite as:
         *      e1.opUnary!(op)()
         */
        if (Dsymbol fd = search_function(ad, Id.opUnary))
            return dotTemplateCall(e.e1, Id.opUnary, opToArg(sc, e.op)).expressionSemantic(sc);

        // Didn't find it. Forward to aliasthis
        if (ad.aliasthis && !isRecursiveAliasThis(att, e.e1.type))
        {
            /* Rewrite op(e1) as:
             *      op(e1.aliasthis)
             */
            //printf("att una %s e1 = %s\n", EXPtoString(op).ptr, this.e1.type.toChars());
            if (auto e1 = resolveAliasThis(sc, e.e1, true))
            {
                e.e1 = e1;
                continue;
            }
            break;
        }

        // For ++ and --, rewrites to += and -= are also tried, so don't error yet
        if (!e.isPreExp())
        {
            error(e.loc, "operator `%s` is not defined for `%s`", EXPtoString(e.op).ptr, ad.toChars());
            errorSupplemental(ad.loc, "perhaps overload the operator with `auto opUnary(string op : \"%s\")() {}`",
                EXPtoString(e.op).ptr);
            return ErrorExp.get();
        }

        break;
    }
    return null;
}

Expression opOverloadArray(ArrayExp ae, Scope* sc)
{
    ae.e1 = ae.e1.expressionSemantic(sc);
    ae.e1 = resolveProperties(sc, ae.e1);
    Expression ae1old = ae.e1;
    const(bool) maybeSlice = (ae.arguments.length == 0 || ae.arguments.length == 1 && (*ae.arguments)[0].isIntervalExp());
    IntervalExp ie = null;
    if (maybeSlice && ae.arguments.length)
    {
        ie = (*ae.arguments)[0].isIntervalExp();
    }
    Type att = null; // first cyclic `alias this` type
    while (true)
    {
        if (ae.e1.isErrorExp())
        {
            return ae.e1;
        }
        Expression e0 = null;
        Expression ae1save = ae.e1;
        ae.lengthVar = null;
        Type t1b = ae.e1.type.toBasetype();
        AggregateDeclaration ad = isAggregate(t1b);
        if (!ad)
        {
            // If the non-aggregate expression ae.e1 is indexable or sliceable,
            // convert it to the corresponding concrete expression.
            if (isIndexableNonAggregate(t1b) || ae.e1.isTypeExp())
            {
                // Convert to SliceExp
                if (maybeSlice)
                    return new SliceExp(ae.loc, ae.e1, ie).expressionSemantic(sc);

                // Convert to IndexExp
                if (ae.arguments.length == 1)
                    return new IndexExp(ae.loc, ae.e1, (*ae.arguments)[0]).expressionSemantic(sc);
            }
            break;
        }
        if (search_function(ad, Id.opIndex))
        {
            // Deal with $
            auto ae2 = resolveOpDollar(sc, ae, e0);
            if (!ae2) // a[i..j] might be: a.opSlice(i, j)
                goto Lfallback;
            if (ae2.isErrorExp())
                return ae2;
            /* Rewrite e1[arguments] as:
             *      e1.opIndex(arguments)
             */
            Expressions* a = ae.arguments.copy();
            Expression result = new DotIdExp(ae.loc, ae.e1, Id.opIndex);
            result = new CallExp(ae.loc, result, a);
            if (maybeSlice) // a[] might be: a.opSlice()
                result = result.trySemantic(sc);
            else
                result = result.expressionSemantic(sc);

            if (result)
                return Expression.combine(e0, result);
        }
    Lfallback:
        if (maybeSlice && ae.e1.isTypeExp())
        {
            Expression result = new SliceExp(ae.loc, ae.e1, ie);
            result = result.expressionSemantic(sc);
            return Expression.combine(e0, result);
        }
        if (maybeSlice && search_function(ad, Id.opSlice))
        {
            // Deal with $
            auto ae2 = resolveOpDollar(sc, ae, ie, e0);

            if (ae2.isErrorExp())
            {
                if (!e0 && !search_function(ad, Id.dollar))
                    ad.loc.errorSupplemental("perhaps define `opDollar` for `%s`", ad.toChars());

                return ae2;
            }
            /* Rewrite a[i..j] as:
             *      a.opSlice(i, j)
             */
            auto a = new Expressions();
            if (ie)
            {
                a.push(ie.lwr);
                a.push(ie.upr);
            }
            Expression result = new DotIdExp(ae.loc, ae.e1, Id.opSlice);
            result = new CallExp(ae.loc, result, a);
            result = result.expressionSemantic(sc);
            return Expression.combine(e0, result);
        }
        // Didn't find it. Forward to aliasthis
        if (ad.aliasthis && !isRecursiveAliasThis(att, ae.e1.type))
        {
            //printf("att arr e1 = %s\n", this.e1.type.toChars());
            /* Rewrite op(a[arguments]) as:
             *      op(a.aliasthis[arguments])
             */
            ae.e1 = resolveAliasThis(sc, ae1save, true);
            if (ae.e1)
                continue;
        }
        break;
    }
    ae.e1 = ae1old; // recovery
    ae.lengthVar = null;
    return null;
}

/***********************************************
 * This is mostly the same as opOverloadUnary but has
 * a different rewrite.
 */
Expression opOverloadCast(CastExp e, Scope* sc, Type att = null)
{
    AggregateDeclaration ad = isAggregate(e.e1.type);
    if (!ad)
        return null;

    // Rewrite as: e1.opCast!(T)()
    if (Dsymbol fd = search_function(ad, Id.opCast))
    {
        version (all)
        {
            // Backwards compatibility with D1 if opCast is a function, not a template
            if (fd.isFuncDeclaration())
            {
                // Rewrite as:  e1.opCast()
                return build_overload(e.loc, sc, e.e1, null, fd);
            }
        }
        auto tiargs = new Objects();
        tiargs.push(e.to);
        return dotTemplateCall(e.e1, Id.opCast, tiargs).expressionSemantic(sc);
    }
    // Didn't find it. Forward to aliasthis
    if (ad.aliasthis && !isRecursiveAliasThis(att, e.e1.type))
    {
        // Rewrite `e1.opCast()` as `e1.aliasthis.opCast()`
        if (auto e1 = resolveAliasThis(sc, e.e1, true))
        {
            CastExp result = e.copy().isCastExp();
            result.e1 = e1;
            return result.opOverloadCast(sc, att);
        }
    }
    return null;
}

// When no operator overload functions are found for `e`, recursively try with `alias this`
// Returns: `null` when still no overload found, otherwise resolved lowering
Expression binAliasThis(BinExp e, Scope* sc, Type[2] aliasThisStop)
{
    AggregateDeclaration ad1 = isAggregate(e.e1.type);
    AggregateDeclaration ad2 = isAggregate(e.e2.type);
    Expression rewrittenLhs;
    if (!(e.isAssignExp && ad2 && ad1 == ad2)) // https://issues.dlang.org/show_bug.cgi?id=2943
    {
        if (Expression result = checkAliasThisForLhs(ad1, sc, e, aliasThisStop))
        {
            /* https://issues.dlang.org/show_bug.cgi?id=19441
             *
             * alias this may not be used for partial assignment.
             * If a struct has a single member which is aliased this
             * directly or aliased to a ref getter function that returns
             * the mentioned member, then alias this may be
             * used since the object will be fully initialised.
             * If the struct is nested, the context pointer is considered
             * one of the members, hence the `ad1.fields.length == 2 && ad1.vthis`
             * condition.
             */
            auto ae = result.isAssignExp();
            if (!ae)
                return result;     // i.e: Rewrote `e1 = e2` -> `e1(e2)`

            auto dve = ae.e1.isDotVarExp();
            if (!dve)
                return result;     // i.e: Rewrote `e1 = e2` -> `e1() = e2`

            if (auto ad = dve.var.isMember2())
            {
                // i.e: Rewrote `e1 = e2` -> `e1.some.var = e2`
                // Ensure that `var` is the only field member in `ad`
                if (ad.fields.length == 1 || (ad.fields.length == 2 && ad.vthis))
                {
                    if (dve.var == ad.aliasthis.sym)
                        return result;
                }
            }
            rewrittenLhs = ae.e1;
        }
    }
    if (!(e.isAssignExp && ad1 && ad1 == ad2)) // https://issues.dlang.org/show_bug.cgi?id=2943
    {
        if (Expression result = checkAliasThisForRhs(ad2, sc, e, aliasThisStop))
            return result;
    }
    if (rewrittenLhs)
    {
        error(e.loc, "cannot use `alias this` to partially initialize variable `%s` of type `%s`. Use `%s`",
                e.e1.toChars(), ad1.toChars(), rewrittenLhs.toChars());
        return ErrorExp.get();
    }
    return null;
}

Expression opOverloadAssign(AssignExp e, Scope* sc, Type[2] aliasThisStop)
{
    AggregateDeclaration ad1 = isAggregate(e.e1.type);
    AggregateDeclaration ad2 = isAggregate(e.e2.type);
    if (ad1 == ad2)
    {
        StructDeclaration sd = ad1.isStructDeclaration();
        if (sd &&
            (!sd.hasIdentityAssign ||
                /* Do a blit if we can and the rvalue is something like .init,
                 * where a postblit is not necessary.
                 */
                (sd.hasBlitAssign && !e.e2.isLvalue())))
        {
            /* This is bitwise struct assignment. */
            return null;
        }
    }
    Dsymbol s = search_function(ad1, Id.opAssign);

    bool choseReverse;
    if (auto result = pickBestBinaryOverload(sc, null, s, null, e, choseReverse))
        return result;

    return binAliasThis(e, sc, aliasThisStop);
}

Expression opOverloadBinary(BinExp e, Scope* sc, Type[2] aliasThisStop)
{
    if (Expression err = binSemanticProp(e, sc))
        return err;

    AggregateDeclaration ad1 = isAggregate(e.e1.type);
    AggregateDeclaration ad2 = isAggregate(e.e2.type);

    // Try opBinary and opBinaryRight
    Dsymbol s = search_function(ad1, Id.opBinary);
    if (s && !s.isTemplateDeclaration())
    {
        error(e.e1.loc, "`%s.opBinary` isn't a template", e.e1.toChars());
        return ErrorExp.get();
    }

    Dsymbol s_r = search_function(ad2, Id.opBinaryRight);
    if (s_r && !s_r.isTemplateDeclaration())
    {
        error(e.e2.loc, "`%s.opBinaryRight` isn't a template", e.e2.toChars());
        return ErrorExp.get();
    }
    if (s_r && s_r == s) // https://issues.dlang.org/show_bug.cgi?id=12778
        s_r = null;

    bool choseReverse;
    if (auto result = pickBestBinaryOverload(sc, opToArg(sc, e.op), s, s_r, e, choseReverse))
        return result;

    return binAliasThis(e, sc, aliasThisStop);
}

/**
 * If applicable, print an error relating to implementing / fixing `opBinary` functions.
 * Params:
 *   e = binary operation
 *   sc = scope to try `opBinary!""` semantic in for error messages
 * Returns: `true` when an error related to `opBinary` was printed
 */
bool suggestBinaryOverloads(BinExp e, Scope* sc)
{
    if (!e.op.hasOpBinary)
        return false;

    AggregateDeclaration ad1 = isAggregate(e.e1.type);
    AggregateDeclaration ad2 = isAggregate(e.e2.type);

    if (ad1)
    {
        if (Dsymbol s = search_function(ad1, Id.opBinary))
        {
            // This expressionSemantic will fail, otherwise operator overloading would have succeeded before
            dotTemplateCall(e.e1, Id.opBinary, opToArg(sc, e.op), e.e2).expressionSemantic(sc);
            errorSupplemental(s.loc, "`opBinary` defined here");
            return true;
        }
        error(e.loc, "operator `%s` is not defined for type `%s`", EXPtoString(e.op).ptr, e.e1.type.toChars);
        errorSupplemental(ad1.loc, "perhaps overload the operator with `auto opBinary(string op : \"%s\")(%s rhs) {}`", EXPtoString(e.op).ptr, e.e2.type.toChars);
        return true;
    }
    else if (ad2)
    {
        if (Dsymbol s_r = search_function(ad1, Id.opBinaryRight))
        {
            dotTemplateCall(e.e2, Id.opBinaryRight, opToArg(sc, e.op), e.e1).expressionSemantic(sc);
            errorSupplemental(s_r.loc, "`opBinaryRight` defined here");
            return true;
        }
        error(e.loc, "operator `%s` is not defined for type `%s`", EXPtoString(e.op).ptr, e.e2.type.toChars);
        errorSupplemental(ad2.loc, "perhaps overload the operator with `auto opBinaryRight(string op : \"%s\")(%s rhs) {}`", EXPtoString(e.op).ptr, e.e1.type.toChars);
        return true;
    }
    return false;
}

/**
 * If applicable, print an error relating to implementing / fixing `opOpAssign` or `opUnary` functions.
 * Params:
 *   exp = binary operation
 *   sc = scope to try `opOpAssign!""` semantic in for error messages
 *   parent = if `exp` was lowered from this `PreExp` or `PostExp`, mention `opUnary` as well
 * Returns: `true` when an error related to `opOpAssign` was printed
 */
bool suggestOpOpAssign(BinAssignExp exp, Scope* sc, Expression parent)
{
    auto ad = isAggregate(exp.e1.type);
    if (!ad)
        return false;

    if (parent && (parent.isPreExp() || parent.isPostExp()))
    {
        error(exp.loc, "operator `%s` not supported for `%s` of type `%s`", EXPtoString(parent.op).ptr, exp.e1.toChars(), ad.toChars());
        errorSupplemental(ad.loc,
            "perhaps implement `auto opUnary(string op : \"%s\")() {}`"~
            " or `auto opOpAssign(string op : \"%s\")(int) {}`",
            EXPtoString(stripAssignOp(parent.op)).ptr,
            EXPtoString(stripAssignOp(exp.op)).ptr
            );
        return true;
    }

    if (const s = search_function(ad, Id.opOpAssign))
    {
        // This expressionSemantic will fail, otherwise operator overloading would have succeeded before
        dotTemplateCall(exp.e1, Id.opOpAssign, opToArg(sc, exp.op), exp.e2).expressionSemantic(sc);
    }
    else
    {
        error(exp.loc, "operator `%s` not supported for `%s` of type `%s`", EXPtoString(exp.op).ptr, exp.e1.toChars(), ad.toChars());
        errorSupplemental(ad.loc, "perhaps implement `auto opOpAssign(string op : \"%s\")(%s) {}`",
            EXPtoString(stripAssignOp(exp.op)).ptr, exp.e2.type.toChars());
    }
    return true;
}

// Helper to construct e.id!tiargs(args), e.g. `lhs.opBinary!"+"(rhs)`
private Expression dotTemplateCall(Expression e, Identifier id, Objects* tiargs, Expression[] args...)
{
    auto ti = new DotTemplateInstanceExp(e.loc, e, id, tiargs);
    auto expressions = new Expressions();
    expressions.pushSlice(args);
    return new CallExp(e.loc, ti, expressions);
}

Expression opOverloadEqual(EqualExp e, Scope* sc, Type[2] aliasThisStop)
{
    Type t1 = e.e1.type.toBasetype();
    Type t2 = e.e2.type.toBasetype();

    /* Array equality is handled by expressionSemantic() potentially
     * lowering to object.__equals(), which takes care of overloaded
     * operators for the element types.
     */
    if (t1.isStaticOrDynamicArray() && t2.isStaticOrDynamicArray())
    {
        return null;
    }

    /* Check for class equality with null literal or typeof(null).
     */
    if (t1.isTypeClass() && e.e2.isNullExp() ||
        t2.isTypeClass() && e.e1.isNullExp())
    {
        error(e.loc, "use `%s` instead of `%s` when comparing with `null`",
            EXPtoString(e.op == EXP.equal ? EXP.identity : EXP.notIdentity).ptr,
            EXPtoString(e.op).ptr);
        return ErrorExp.get();
    }
    if (t1.isTypeClass() && t2.isTypeNull() ||
        t1.isTypeNull() && t2.isTypeClass())
    {
        // Comparing a class with typeof(null) should not call opEquals
        return null;
    }

    /* Check for class equality.
     */
    if (t1.isTypeClass() && t2.isTypeClass())
    {
        ClassDeclaration cd1 = t1.isClassHandle();
        ClassDeclaration cd2 = t2.isClassHandle();
        if (!(cd1.classKind == ClassKind.cpp || cd2.classKind == ClassKind.cpp))
        {
            /* Rewrite as:
             *      .object.opEquals(e1, e2)
             */
            if (!ClassDeclaration.object)
            {
                error(e.loc, "cannot compare classes for equality because `object.Object` was not declared");
                return null;
            }

            Expression e1x = e.e1;
            Expression e2x = e.e2;

            /* The explicit cast is necessary for interfaces
             * https://issues.dlang.org/show_bug.cgi?id=4088
             */
            Type to = ClassDeclaration.object.getType();
            if (cd1.isInterfaceDeclaration())
                e1x = new CastExp(e.loc, e.e1, t1.isMutable() ? to : to.constOf());
            if (cd2.isInterfaceDeclaration())
                e2x = new CastExp(e.loc, e.e2, t2.isMutable() ? to : to.constOf());

            Expression result = new IdentifierExp(e.loc, Id.empty);
            result = new DotIdExp(e.loc, result, Id.object);
            result = new DotIdExp(e.loc, result, Id.opEquals);
            result = new CallExp(e.loc, result, e1x, e2x);
            if (e.op == EXP.notEqual)
                result = new NotExp(e.loc, result);
            result = result.expressionSemantic(sc);
            return result;
        }
    }

    EXP cmpOp;
    if (Expression result = compare_overload(e, sc, Id.opEquals, cmpOp, aliasThisStop))
    {
        if (lastComma(result).isCallExp() && e.op == EXP.notEqual)
        {
            result = new NotExp(result.loc, result);
            result = result.expressionSemantic(sc);
        }
        return result;
    }

    /* Check for pointer equality.
     */
    if (t1.isTypePointer() || t2.isTypePointer())
    {
        /* Rewrite:
         *      ptr1 == ptr2
         * as:
         *      ptr1 is ptr2
         *
         * This is just a rewriting for deterministic AST representation
         * as the backend input.
         */
        auto op2 = e.op == EXP.equal ? EXP.identity : EXP.notIdentity;
        Expression r = new IdentityExp(op2, e.loc, e.e1, e.e2);
        return r.expressionSemantic(sc);
    }

    /* Check for struct equality without opEquals.
     */
    if (t1.isTypeStruct() && t2.isTypeStruct())
    {
        auto sd = t1.isTypeStruct().sym;
        if (sd != t2.isTypeStruct().sym)
            return null;

        import dmd.clone : needOpEquals;
        if (!sc.previews.fieldwise && !needOpEquals(sd))
        {
            // Use bitwise equality.
            auto op2 = e.op == EXP.equal ? EXP.identity : EXP.notIdentity;
            Expression r = new IdentityExp(op2, e.loc, e.e1, e.e2);
            return r.expressionSemantic(sc);
        }

        /* Do memberwise equality.
         * https://dlang.org/spec/expression.html#equality_expressions
         * Rewrite:
         *      e1 == e2
         * as:
         *      e1.tupleof == e2.tupleof
         *
         * If sd is a nested struct, and if it's nested in a class, it will
         * also compare the parent class's equality. Otherwise, compares
         * the identity of parent context through void*.
         */
        e = e.copy().isEqualExp();
        e.e1 = new DotIdExp(e.loc, e.e1, Id._tupleof);
        e.e2 = new DotIdExp(e.loc, e.e2, Id._tupleof);

        auto sc2 = sc.push();
        sc2.noAccessCheck = true;
        Expression r = e.expressionSemantic(sc2);
        sc2.pop();
        return r;
    }

    /* Check for tuple equality.
     */
    auto tup1 = e.e1.isTupleExp();
    auto tup2 = e.e2.isTupleExp();
    if (tup1 && tup2)
    {
        size_t dim = tup1.exps.length;
        if (dim != tup2.exps.length)
        {
            error(e.loc, "mismatched sequence lengths, `%d` and `%d`",
                cast(int)dim, cast(int)tup2.exps.length);
            return ErrorExp.get();
        }

        Expression result;
        if (dim == 0)
        {
            // zero-length tuple comparison should always return true or false.
            result = IntegerExp.createBool(e.op == EXP.equal);
        }
        else
        {
            for (size_t i = 0; i < dim; i++)
            {
                auto ex1 = (*tup1.exps)[i];
                auto ex2 = (*tup2.exps)[i];
                auto eeq = new EqualExp(e.op, e.loc, ex1, ex2);

                if (!result)
                    result = eeq;
                else if (e.op == EXP.equal)
                    result = new LogicalExp(e.loc, EXP.andAnd, result, eeq);
                else
                    result = new LogicalExp(e.loc, EXP.orOr, result, eeq);
            }
            assert(result);
        }
        result = Expression.combine(tup1.e0, tup2.e0, result);
        result = result.expressionSemantic(sc);

        return result;
    }
    return null;
}

Expression opOverloadCmp(CmpExp exp, Scope* sc, Type[2] aliasThisStop)
{
    //printf("CmpExp:: () (%s)\n", e.toChars());
    EXP cmpOp = exp.op;
    auto e = compare_overload(exp, sc, Id.opCmp, cmpOp, aliasThisStop);
    if (!e)
        return null;

    if (!e.type.isScalar() && e.type.equals(exp.e1.type))
    {
        error(e.loc, "recursive `opCmp` expansion");
        return ErrorExp.get();
    }
    if (!e.isCallExp())
        return e;

    Type t1 = exp.e1.type.toBasetype();
    Type t2 = exp.e2.type.toBasetype();
    if (!t1.isTypeClass() || !t2.isTypeClass())
    {
        return new CmpExp(cmpOp, exp.loc, e, IntegerExp.literal!0).expressionSemantic(sc);
    }

    // Lower to object.__cmp(e1, e2)
    Expression cl = new IdentifierExp(exp.loc, Id.empty);
    cl = new DotIdExp(exp.loc, cl, Id.object);
    cl = new DotIdExp(exp.loc, cl, Id.__cmp);
    cl = cl.expressionSemantic(sc);

    auto arguments = new Expressions();
    // Check if op_overload found a better match by calling e2.opCmp(e1)
    // If the operands were swapped, then the result must be reversed
    // e1.opCmp(e2) == -e2.opCmp(e1)
    // cmpop takes care of this
    if (exp.op == cmpOp)
    {
        arguments.push(exp.e1);
        arguments.push(exp.e2);
    }
    else
    {
        // Use better match found by op_overload
        arguments.push(exp.e2);
        arguments.push(exp.e1);
    }

    cl = new CallExp(e.loc, cl, arguments);
    cl = new CmpExp(cmpOp, exp.loc, cl, new IntegerExp(0));
    return cl.expressionSemantic(sc);
}

/*********************************
 * Operator overloading for op=
 */
Expression opOverloadBinaryAssign(BinAssignExp e, Scope* sc, Type[2] aliasThisStop)
{
    if (auto ae = e.e1.isArrayExp())
    {
        ae.e1 = ae.e1.expressionSemantic(sc);
        ae.e1 = resolveProperties(sc, ae.e1);
        Expression ae1old = ae.e1;
        const(bool) maybeSlice = (ae.arguments.length == 0 || ae.arguments.length == 1 && (*ae.arguments)[0].isIntervalExp());
        IntervalExp ie = null;
        if (maybeSlice && ae.arguments.length)
        {
            ie = (*ae.arguments)[0].isIntervalExp();
        }
        Type att = null; // first cyclic `alias this` type
        while (true)
        {
            if (ae.e1.isErrorExp())
                return ae.e1;

            Expression e0 = null;
            Expression ae1save = ae.e1;
            ae.lengthVar = null;
            AggregateDeclaration ad = isAggregate(ae.e1.type);
            if (!ad)
                break;
            if (search_function(ad, Id.opIndexOpAssign))
            {
                // Deal with $
                Expression ae2 = resolveOpDollar(sc, ae, e0);
                if (!ae2) // (a[i..j] op= e2) might be: a.opSliceOpAssign!(op)(e2, i, j)
                    goto Lfallback;
                if (ae2.isErrorExp())
                    return ae2;
                e.e2 = e.e2.expressionSemantic(sc);
                if (e.e2.isErrorExp())
                    return e.e2;

                /* Rewrite a[arguments] op= e2 as:
                 *      a.opIndexOpAssign!(op)(e2, arguments)
                 */
                Expressions* a = ae.arguments.copy();
                a.insert(0, e.e2);
                Expression result = dotTemplateCall(ae.e1, Id.opIndexOpAssign, opToArg(sc, e.op), (*a)[]);
                if (maybeSlice) // (a[] op= e2) might be: a.opSliceOpAssign!(op)(e2)
                    result = result.trySemantic(sc);
                else
                    result = result.expressionSemantic(sc);

                if (result)
                    return Expression.combine(e0, result);
            }
        Lfallback:
            if (maybeSlice && search_function(ad, Id.opSliceOpAssign))
            {
                // Deal with $
                Expression ae2 = resolveOpDollar(sc, ae, ie, e0);
                if (ae2.isErrorExp())
                    return ae2;

                e.e2 = e.e2.expressionSemantic(sc);
                if (e.e2.isErrorExp())
                    return e.e2;

                /* Rewrite (a[i..j] op= e2) as:
                 *      a.opSliceOpAssign!(op)(e2, i, j)
                 */
                auto result = ie ?
                    dotTemplateCall(ae.e1, Id.opSliceOpAssign, opToArg(sc, e.op), e.e2, ie.lwr, ie.upr) :
                    dotTemplateCall(ae.e1, Id.opSliceOpAssign, opToArg(sc, e.op), e.e2);

                return Expression.combine(e0, result.expressionSemantic(sc));
            }
            // Didn't find it. Forward to aliasthis
            if (ad.aliasthis && !isRecursiveAliasThis(att, ae.e1.type))
            {
                /* Rewrite (a[arguments] op= e2) as:
                 *      a.aliasthis[arguments] op= e2
                 */
                ae.e1 = resolveAliasThis(sc, ae1save, true);
                if (ae.e1)
                    continue;
            }
            break;
        }
        ae.e1 = ae1old; // recovery
        ae.lengthVar = null;
    }

    if (Expression result = e.binSemanticProp(sc))
        return result;

    // Don't attempt 'alias this' if an error occurred
    if (e.e1.type.isTypeError() || e.e2.type.isTypeError())
        return ErrorExp.get();

    AggregateDeclaration ad1 = isAggregate(e.e1.type);
    Dsymbol s = search_function(ad1, Id.opOpAssign);
    if (s && !s.isTemplateDeclaration())
    {
        error(e.loc, "`%s.opOpAssign` isn't a template", e.e1.toChars());
        return ErrorExp.get();
    }

    bool choseReverse;
    if (auto res = pickBestBinaryOverload(sc, opToArg(sc, e.op), s, null, e, choseReverse))
        return res;

    Expression result = checkAliasThisForLhs(ad1, sc, e, aliasThisStop);
    if (result || !s) // no point in trying Rhs alias-this if there's no overload of any kind in lhs
        return result;

    return checkAliasThisForRhs(isAggregate(e.e2.type), sc, e, aliasThisStop);
}

/**
Given symbols `s` and `s_r`, try to instantiate `e.e1.s!tiargs(e.e2)` and `e.e2.s_r!tiargs(e.e1)`,
and return the one with the best match level.

Params:
    sc = scope
    tiargs = (optional) template arguments to instantiate symbols with
    s = (optional) symbol of straightforward template (e.g. opBinary)
    s_r = (optional) symbol of reversed template (e.g. opBinaryRight)
    e = binary expression being overloaded, supplying arguments to the function calls
    choseReverse = set to true when `s_r` was chosen instead of `s`
Returns:
    Resulting operator overload function call, or `null` if neither symbol worked
*/
private Expression pickBestBinaryOverload(Scope* sc, Objects* tiargs, Dsymbol s, Dsymbol s_r, BinExp e, out bool choseReverse)
{
    if (!s && !s_r)
        return null;

    Expressions* args1 = new Expressions(1);
    (*args1)[0] = e.e1;
    expandTuples(args1);
    Expressions* args2 = new Expressions(1);
    (*args2)[0] = e.e2;
    expandTuples(args2);
    MatchAccumulator m;

    if (s)
    {
        functionResolve(m, s, e.loc, sc, tiargs, e.e1.type, ArgumentList(args2), null);
        if (m.lastf && (m.lastf.errors || m.lastf.hasSemantic3Errors()))
            return ErrorExp.get();
    }
    FuncDeclaration lastf = m.lastf;
    int count = m.count;
    if (s_r)
    {
        functionResolve(m, s_r, e.loc, sc, tiargs, e.e2.type, ArgumentList(args1), null);
        if (m.lastf && (m.lastf.errors || m.lastf.hasSemantic3Errors()))
            return ErrorExp.get();
    }
    if (m.count > 1)
    {
        /* The following if says "not ambiguous" if there's one match
         * from s and one from s_r, in which case we pick s.
         * This doesn't follow the spec, but is a workaround for the case
         * where opEquals was generated from templates and we cannot figure
         * out if both s and s_r came from the same declaration or not.
         * The test case is:
         *   import std.typecons;
         *   void main() {
         *    assert(tuple("has a", 2u) == tuple("has a", 1));
         *   }
         */
        if (!(m.lastf == lastf && m.count == 2 && count == 1))
        {
            // Error, ambiguous
            error(e.loc, "overloads `%s` and `%s` both match argument list for `%s`", m.lastf.type.toChars(), m.nextf.type.toChars(), m.lastf.toChars());
        }
    }
    else if (m.last == MATCH.nomatch)
    {
        if (tiargs)
            return null;
        m.lastf = null;
    }

    if (lastf && m.lastf == lastf || !s_r && m.last == MATCH.nomatch)
    {
        choseReverse = false;
        // Rewrite (e1 op e2) as e1.opfunc(e2)
        return build_overload(e.loc, sc, e.e1, e.e2, m.lastf ? m.lastf : s);
    }
    else
    {
        choseReverse = true;
        // Rewrite (e1 op e2) as e2.opfunc_r(e1)
        return build_overload(e.loc, sc, e.e2, e.e1, m.lastf ? m.lastf : s_r);
    }
}

/******************************************
 * Common code for overloading of EqualExp and CmpExp
 */
private Expression compare_overload(BinExp e, Scope* sc, Identifier id, ref EXP cmpOp, Type[2] aliasThisStop)
{
    //printf("BinExp::compare_overload(id = %s) %s\n", id.toChars(), e.toChars());
    AggregateDeclaration ad1 = isAggregate(e.e1.type);
    AggregateDeclaration ad2 = isAggregate(e.e2.type);
    Dsymbol s = search_function(ad1, id);
    Dsymbol s_r = search_function(ad2, id);

    if (s == s_r)
        s_r = null;

    bool choseReverse;
    if (auto res = pickBestBinaryOverload(sc, null, s, s_r, e, choseReverse))
    {
        if (choseReverse)
            cmpOp = reverseRelation(e.op);
        return res;
    }

    /*
     * https://issues.dlang.org/show_bug.cgi?id=16657
     * at this point, no matching opEquals was found for structs,
     * so we should not follow the alias this comparison code.
     */
    if (e.isEqualExp() && ad1 == ad2)
        return null;
    Expression result = checkAliasThisForLhs(ad1, sc, e, aliasThisStop);
    if (result)
        return result;

    result = checkAliasThisForRhs(isAggregate(e.e2.type), sc, e, aliasThisStop);
    if (result)
        return result;

    if (s || s_r)
        return null;

    Expression suggestOverloading(Expression other, AggregateDeclaration ad)
    {
        error(e.loc, "no operator `%s` for type `%s`", EXPtoString(e.op).ptr, ad.toChars);
        string op = e.isEqualExp() ? "bool" : "int";
        errorSupplemental(ad.loc, "perhaps overload it with `%.*s %s(%s other) const {}`", op.fTuple.expand, id.toChars, other.type.toChars);
        return ErrorExp.get();
    }

    // Classes have opCmp and opEquals defined in `Object` to fall back on already
    if (ad1 && ad1.isStructDeclaration)
        return suggestOverloading(e.e2, ad1);
    if (ad2 && ad2.isStructDeclaration)
        return suggestOverloading(e.e1, ad2);

    return null;
}

/***********************************
 * Utility to build a function call out of this reference and argument.
 */
Expression build_overload(Loc loc, Scope* sc, Expression ethis, Expression earg, Dsymbol d)
{
    assert(d);
    Expression e;
    if (Declaration decl = d.isDeclaration())
        e = new DotVarExp(loc, ethis, decl, false);
    else
        e = new DotIdExp(loc, ethis, d.ident);
    e = new CallExp(loc, e, earg);
    e = e.expressionSemantic(sc);
    return e;
}

/***************************************
 * Search for function funcid in aggregate ad.
 */
Dsymbol search_function(ScopeDsymbol ad, Identifier funcid)
{
    if (!ad)
        return null;
    if (Dsymbol s = ad.search(Loc.initial, funcid))
    {
        //printf("search_function: s = '%s'\n", s.kind());
        Dsymbol s2 = s.toAlias();
        //printf("search_function: s2 = '%s'\n", s2.kind());
        FuncDeclaration fd = s2.isFuncDeclaration();
        if (fd && fd.type.isTypeFunction())
            return fd;
        if (TemplateDeclaration td = s2.isTemplateDeclaration())
            return td;
    }
    return null;
}

/**************************************
 * Figure out what is being foreach'd over by looking at the ForeachAggregate.
 * Params:
 *      sc = context
 *      isForeach = true for foreach, false for foreach_reverse
 *      feaggr = ForeachAggregate
 *      sapply = set to function opApply/opApplyReverse, or delegate, or null.
 *               Overload resolution is not done.
 * Returns:
 *      true if successfully figured it out; feaggr updated with semantic analysis.
 *      false for failed, which is an error.
 */
bool inferForeachAggregate(Scope* sc, bool isForeach, ref Expression feaggr, out Dsymbol sapply)
{
    //printf("inferForeachAggregate(%s)\n", feaggr.toChars());
    bool sliced;
    Type att = null;
    auto aggr = feaggr;
    while (1)
    {
        aggr = aggr.expressionSemantic(sc);
        aggr = resolveProperties(sc, aggr);
        aggr = aggr.optimize(WANTvalue);
        if (!aggr.type || aggr.isErrorExp())
            return false;
        Type tab = aggr.type.toBasetype();
        switch (tab.ty)
        {
        case Tarray:            // https://dlang.org/spec/statement.html#foreach_over_arrays
        case Tsarray:           // https://dlang.org/spec/statement.html#foreach_over_arrays
        case Ttuple:            // https://dlang.org/spec/statement.html#foreach_over_tuples
        case Taarray:           // https://dlang.org/spec/statement.html#foreach_over_associative_arrays
            break;

        case Tclass:
        case Tstruct:
        {
            AggregateDeclaration ad = isAggregate(tab);
            if (!sliced)
            {
                sapply = search_function(ad, isForeach ? Id.apply : Id.applyReverse);
                if (sapply)
                {
                    // https://dlang.org/spec/statement.html#foreach_over_struct_and_classes
                    // opApply aggregate
                    break;
                }
                if (feaggr.op != EXP.type)
                {
                    /* See if rewriting `aggr` to `aggr[]` will work
                     */
                    Expression rinit = new ArrayExp(aggr.loc, feaggr);
                    rinit = rinit.trySemantic(sc);
                    if (rinit) // if it worked
                    {
                        aggr = rinit;
                        sliced = true;  // only try it once
                        continue;
                    }
                }
            }
            if (ad.search(Loc.initial, isForeach ? Id.Ffront : Id.Fback))
            {
                // https://dlang.org/spec/statement.html#foreach-with-ranges
                // range aggregate
                break;
            }
            if (ad.aliasthis)
            {
                if (isRecursiveAliasThis(att, tab))     // error, circular alias this
                    return false;
                aggr = resolveAliasThis(sc, aggr);
                continue;
            }
            return false;
        }

        case Tdelegate:        // https://dlang.org/spec/statement.html#foreach_over_delegates
            if (auto de = aggr.isDelegateExp())
            {
                sapply = de.func;
            }
            break;

        case Terror:
            break;

        default:
            return false;
        }
        feaggr = aggr;
        return true;
    }
    assert(0);
}

/*****************************************
 * Given array of foreach parameters and an aggregate type,
 * find best opApply overload,
 * if any of the parameter types are missing, attempt to infer
 * them from the aggregate type.
 * Params:
 *      fes = the foreach statement
 *      sc = context
 *      sapply = null or opApply or delegate, overload resolution has not been done.
 *               Do overload resolution on sapply.
 * Returns:
 *      false for errors
 */
bool inferApplyArgTypes(ForeachStatement fes, Scope* sc, ref Dsymbol sapply)
{
    if (!fes.parameters || !fes.parameters.length)
        return false;
    if (sapply) // prefer opApply
    {
        foreach (Parameter p; *fes.parameters)
        {
            if (p.type)
            {
                p.type = p.type.typeSemantic(fes.loc, sc);
                p.type = p.type.addStorageClass(p.storageClass);
            }
        }

        // Determine ethis for sapply
        Expression ethis;
        Type tab = fes.aggr.type.toBasetype();
        if (tab.isTypeClass() || tab.isTypeStruct())
            ethis = fes.aggr;
        else
        {
            assert(tab.isTypeDelegate() && fes.aggr.isDelegateExp());
            ethis = fes.aggr.isDelegateExp().e1;
        }

        /* Look for like an
         *  int opApply(int delegate(ref Type [, ...]) dg);
         * overload
         */
        if (FuncDeclaration fd = sapply.isFuncDeclaration())
        {
            if (auto fdapply = findBestOpApplyMatch(ethis, fd, fes.parameters))
            {
                // Fill in any missing types on foreach parameters[]
                matchParamsToOpApply(fdapply.type.isTypeFunction(), fes.parameters, true);
                sapply = fdapply;
                return true;
            }
            return false;
        }
        return true;   // shouldn't this be false?
    }

    Parameter p = (*fes.parameters)[0];
    Type taggr = fes.aggr.type;
    assert(taggr);
    Type tab = taggr.toBasetype();
    switch (tab.ty)
    {
    case Tarray:
    case Tsarray:
    case Ttuple:
        if (fes.parameters.length == 2)
        {
            if (!p.type)
            {
                p.type = Type.tsize_t; // key type
                p.type = p.type.addStorageClass(p.storageClass);
            }
            p = (*fes.parameters)[1];
        }
        if (!p.type && !tab.isTypeTuple())
        {
            p.type = tab.nextOf(); // value type
            p.type = p.type.addStorageClass(p.storageClass);
        }
        break;

    case Taarray:
        {
            TypeAArray taa = tab.isTypeAArray();
            if (fes.parameters.length == 2)
            {
                if (!p.type)
                {
                    p.type = taa.index; // key type
                    p.type = p.type.addStorageClass(p.storageClass);
                    if (p.storageClass & STC.ref_) // key must not be mutated via ref
                        p.type = p.type.addMod(MODFlags.const_);
                }
                p = (*fes.parameters)[1];
            }
            if (!p.type)
            {
                p.type = taa.next; // value type
                p.type = p.type.addStorageClass(p.storageClass);
            }
            break;
        }

    case Tclass:
    case Tstruct:
    {
        AggregateDeclaration ad = isAggregate(tab);
        if (fes.parameters.length == 1)
        {
            if (!p.type)
            {
                /* Look for a front() or back() overload
                 */
                Identifier id = (fes.op == TOK.foreach_) ? Id.Ffront : Id.Fback;
                Dsymbol s = ad.search(Loc.initial, id);
                FuncDeclaration fd = s ? s.isFuncDeclaration() : null;
                if (fd)
                {
                    // Resolve inout qualifier of front type
                    p.type = fd.type.nextOf();
                    if (p.type)
                    {
                        p.type = p.type.substWildTo(tab.mod);
                        p.type = p.type.addStorageClass(p.storageClass);
                    }
                }
                else if (s && s.isTemplateDeclaration())
                {
                }
                else if (s && s.isDeclaration())
                    p.type = s.isDeclaration().type;
                else
                    break;
            }
            break;
        }
        break;
    }

    case Tdelegate:
    {
        auto td = tab.isTypeDelegate();
        if (!matchParamsToOpApply(td.next.isTypeFunction(), fes.parameters, true))
            return false;
        break;
    }

    default:
        break; // ignore error, caught later
    }
    return true;
}

/*********************************************
 * Find best overload match on fstart given ethis and parameters[].
 * Params:
 *      ethis = expression to use for `this`
 *      fstart = opApply or foreach delegate
 *      parameters = ForeachTypeList (i.e. foreach parameters)
 * Returns:
 *      best match if there is one, null if error
 */
private FuncDeclaration findBestOpApplyMatch(Expression ethis, FuncDeclaration fstart, Parameters* parameters)
{
    MOD mod = ethis.type.mod;
    MATCH match = MATCH.nomatch;
    FuncDeclaration fd_best;
    FuncDeclaration fd_ambig;

    overloadApply(fstart, (Dsymbol s)
    {
        auto f = s.isFuncDeclaration();
        if (!f)
            return 0;           // continue
        auto tf = f.type.isTypeFunction();
        MATCH m = MATCH.exact;
        if (f.isThis())
        {
            if (!MODimplicitConv(mod, tf.mod))
                m = MATCH.nomatch;
            else if (mod != tf.mod)
                m = MATCH.constant;
        }
        if (!matchParamsToOpApply(tf, parameters, false))
            m = MATCH.nomatch;
        if (m > match)
        {
            fd_best = f;
            fd_ambig = null;
            match = m;
        }
        else if (m == match && m > MATCH.nomatch)
        {
            assert(fd_best);
            auto bestTf = fd_best.type.isTypeFunction();
            assert(bestTf);

            // Found another overload with different attributes?
            // e.g. @system vs. @safe opApply
            // @@@DEPRECATED_2.112@@@
            // See semantic2.d Semantic2Visitor.visit(FuncDeclaration):
            // Remove `false` after deprecation period is over.
            bool ambig = tf.attributesEqual(bestTf, false);

            // opApplies with identical attributes could still accept
            // different function bodies as delegate
            // => different parameters or attributes
            if (ambig)
            {
                // Fetch the delegates that receive the function body
                auto tfBody = tf.parameterList[0].type.isTypeDelegate().next;
                assert(tfBody);

                auto bestBody = bestTf.parameterList[0].type.isTypeDelegate().next;
                assert(bestBody);

                // Ignore covariant matches, as later on it can be redone
                // after the opApply delegate has its attributes inferred.
                ambig = !(tfBody.covariant(bestBody) == Covariant.yes || bestBody.covariant(tfBody) == Covariant.yes);
            }

            if (ambig)
                fd_ambig = f;                           // not covariant, so ambiguous
        }
        return 0;               // continue
    });

    if (fd_ambig)
    {
        .error(ethis.loc, "`%s.%s` matches more than one declaration:",
            ethis.toChars(), fstart.ident.toChars());
        .errorSupplemental(fd_best.loc, "`%s`\nand:", fd_best.type.toChars());
        .errorSupplemental(fd_ambig.loc, "`%s`", fd_ambig.type.toChars());
        return null;
    }

    return fd_best;
}

/******************************
 * Determine if foreach parameters match opApply parameters.
 * Infer missing foreach parameter types from type of opApply delegate.
 * Params:
 *      tf = type of opApply or delegate
 *      parameters = foreach parameters
 *      infer = infer missing parameter types
 * Returns:
 *      true for match for this function
 *      false for no match for this function
 */
private bool matchParamsToOpApply(TypeFunction tf, Parameters* parameters, bool infer)
{
    enum nomatch = false;

    /* opApply/delegate has exactly one parameter, and that parameter
     * is a delegate that looks like:
     *     int opApply(int delegate(ref Type [, ...]) dg);
     */
    if (tf.parameterList.length != 1)
        return nomatch;

    /* Get the type of opApply's dg parameter
     */
    Parameter p0 = tf.parameterList[0];
    auto de = p0.type.isTypeDelegate();
    if (!de)
        return nomatch;
    TypeFunction tdg = de.next.isTypeFunction();

    /* We now have tdg, the type of the delegate.
     * tdg's parameters must match that of the foreach arglist (i.e. parameters).
     * Fill in missing types in parameters.
     */
    const nparams = tdg.parameterList.length;
    if (nparams == 0 || nparams != parameters.length || tdg.parameterList.varargs != VarArg.none)
        return nomatch; // parameter mismatch

    foreach (u, p; *parameters)
    {
        Parameter param = tdg.parameterList[u];
        if (p.type)
        {
            if (!p.type.equals(param.type))
                return nomatch;
        }
        else if (infer)
        {
            p.type = param.type;
            p.type = p.type.addStorageClass(p.storageClass);
        }
    }
    return true;
}

/**
 * Reverse relational operator, eg >= becomes <=
 * Note this is not negation.
 * Params:
 *      op = comparison operator to reverse
 * Returns:
 *      reverse of op
 */
private EXP reverseRelation(EXP op) pure @safe
{
    switch (op)
    {
        case EXP.greaterOrEqual:  op = EXP.lessOrEqual;    break;
        case EXP.greaterThan:     op = EXP.lessThan;       break;
        case EXP.lessOrEqual:     op = EXP.greaterOrEqual; break;
        case EXP.lessThan:        op = EXP.greaterThan;    break;
        default:                  break;
    }
    return op;
}
